first commit with the GitButler client. :)

This commit is contained in:
Scott Chacon 2023-03-11 08:53:07 -08:00
parent fd6bdb24b3
commit 1707508b7e
5 changed files with 320 additions and 81 deletions

View File

@ -426,6 +426,29 @@ fn git_match_paths(
Ok(files) Ok(files)
} }
#[tauri::command]
fn git_commit(
handle: tauri::AppHandle,
project_id: &str,
message: &str,
files: Vec<&str>,
push: bool,
) -> Result<bool, Error> {
let app_state = handle.state::<App>();
let repo = repositories::Repository::open(
&app_state.projects_storage,
&app_state.users_storage,
project_id,
)?;
let success = repo
.commit(message, files, push)
.with_context(|| "Failed to commit")?;
Ok(success)
}
fn main() { fn main() {
let quit = tauri::CustomMenuItem::new("quit".to_string(), "Quit"); let quit = tauri::CustomMenuItem::new("quit".to_string(), "Quit");
let hide = tauri::CustomMenuItem::new("toggle".to_string(), format!("Hide {}", app_title())); let hide = tauri::CustomMenuItem::new("toggle".to_string(), format!("Hide {}", app_title()));
@ -539,7 +562,8 @@ fn main() {
search, search,
git_status, git_status,
git_file_paths, git_file_paths,
git_match_paths git_match_paths,
git_commit
]); ]);
let tauri_context = generate_context!(); let tauri_context = generate_context!();

View File

@ -1,5 +1,6 @@
use crate::{deltas, fs, projects, sessions, users}; use crate::{deltas, fs, projects, sessions, users};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use git2::Signature;
use std::{collections::HashMap, path::Path}; use std::{collections::HashMap, path::Path};
use tauri::regex::Regex; use tauri::regex::Regex;
use walkdir::WalkDir; use walkdir::WalkDir;
@ -157,8 +158,6 @@ impl Repository {
// get file status from git // get file status from git
pub fn status(&self) -> Result<HashMap<String, String>> { pub fn status(&self) -> Result<HashMap<String, String>> {
println!("Git Status");
let mut options = git2::StatusOptions::new(); let mut options = git2::StatusOptions::new();
options.include_untracked(true); options.include_untracked(true);
options.include_ignored(false); options.include_ignored(false);
@ -197,6 +196,51 @@ impl Repository {
return Ok(files); return Ok(files);
} }
// commit method
pub fn commit(&self, message: &str, files: Vec<&str>, push: bool) -> Result<bool> {
println!("Git Commit");
let repo = &self.git_repository;
let config = repo.config()?;
let name = config.get_string("user.name")?;
let email = config.get_string("user.email")?;
// Get the repository's index
let mut index = repo.index()?;
// Add the specified files to the index
for path_str in files {
let path = Path::new(path_str);
index.add_path(path)?;
}
// Write the updated index to disk
index.write()?;
// Get the default signature for the repository
let signature = Signature::now(&name, &email)?;
// Create the commit with the updated index
let tree_id = index.write_tree()?;
let tree = repo.find_tree(tree_id)?;
let parent_commit = repo.head()?.peel_to_commit()?;
let commit = repo.commit(
Some("HEAD"),
&signature,
&signature,
message,
&tree,
&[&parent_commit],
)?;
println!("Created commit {}", commit);
if push {
println!("Pushing to remote");
}
return Ok(true);
}
} }
fn init( fn init(

View File

@ -5,20 +5,38 @@
import BookmarkIcon from './icons/BookmarkIcon.svelte'; import BookmarkIcon from './icons/BookmarkIcon.svelte';
import BranchIcon from './icons/BranchIcon.svelte'; import BranchIcon from './icons/BranchIcon.svelte';
import { invoke } from '@tauri-apps/api'; import { invoke } from '@tauri-apps/api';
import { redirect } from '@sveltejs/kit';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { shortPath } from '$lib/paths';
let showCommand = false; let showCommand = false;
let showCommit = false;
let is_command_down = false; let is_command_down = false;
let is_k_down = false; let is_k_down = false;
let is_c_down = false;
export let projectId: string; export let projectId: string;
let palette: HTMLElement; let palette: HTMLElement;
let commitPalette: HTMLElement;
let changedFiles = {};
let commitMessage = '';
let commitMessageInput: HTMLElement;
const listFiles = (params: { projectId: string }) =>
invoke<Record<string, string>>('git_status', params);
const matchFiles = (params: { projectId: string; matchPattern: string }) => const matchFiles = (params: { projectId: string; matchPattern: string }) =>
invoke<Array<string>>('git_match_paths', params); invoke<Array<string>>('git_match_paths', params);
const commit = (params: {
projectId: string;
message: string;
files: Array<string>;
push: boolean;
}) => invoke<boolean>('git_commit', params);
function onKeyDown(event: KeyboardEvent) { function onKeyDown(event: KeyboardEvent) {
if (event.repeat) return; if (event.repeat) return;
switch (event.key) { switch (event.key) {
@ -29,8 +47,12 @@
case 'k': case 'k':
is_k_down = true; is_k_down = true;
break; break;
case 'c':
is_c_down = true;
break;
case 'Escape': case 'Escape':
showCommand = false; showCommand = false;
showCommit = false;
break; break;
case 'ArrowDown': case 'ArrowDown':
if (showCommand) { if (showCommand) {
@ -57,6 +79,10 @@
document.getElementById('command')?.focus(); document.getElementById('command')?.focus();
}, 100); }, 100);
} }
if (is_command_down && is_c_down) {
showCommit = true;
executeCommand('commit');
}
} }
function onKeyUp(event: KeyboardEvent) { function onKeyUp(event: KeyboardEvent) {
@ -69,6 +95,10 @@
is_k_down = false; is_k_down = false;
event.preventDefault(); event.preventDefault();
break; break;
case 'c':
is_c_down = false;
event.preventDefault();
break;
} }
} }
@ -77,6 +107,9 @@
if (showCommand && !palette.contains(target)) { if (showCommand && !palette.contains(target)) {
showCommand = false; showCommand = false;
} }
if (showCommit && !commitPalette.contains(target)) {
showCommit = false;
}
} }
let activeClass = ['active', 'bg-zinc-700/50', 'text-white']; let activeClass = ['active', 'bg-zinc-700/50', 'text-white'];
@ -121,10 +154,15 @@
function selectItem() { function selectItem() {
showCommand = false; showCommand = false;
showCommit = false;
const menu = document.getElementById('commandMenu'); const menu = document.getElementById('commandMenu');
if (menu) { if (menu) {
const active = menu.querySelector('li.active'); const active = menu.querySelector('li.active');
if (active) { if (active) {
const command = active.getAttribute('data-command');
if (command) {
executeCommand(command);
}
console.log('active', active); console.log('active', active);
} else { } else {
goto('/projects/' + projectId + '/search?search=' + search); goto('/projects/' + projectId + '/search?search=' + search);
@ -132,6 +170,25 @@
} }
} }
function executeCommand(command: string) {
switch (command) {
case 'commit':
listFiles({ projectId: projectId }).then((files) => {
console.log('files', files);
changedFiles = files;
});
showCommit = true;
setTimeout(function () {
commitMessageInput.focus();
}, 100);
break;
case 'bookmark':
break;
case 'branch':
break;
}
}
let search = ''; let search = '';
$: { $: {
@ -139,9 +196,9 @@
} }
let baseCommands = [ let baseCommands = [
{ text: 'Commit', key: 'C', icon: CommitIcon }, { text: 'Commit', key: 'C', icon: CommitIcon, command: 'commit' },
{ text: 'Bookmark', key: 'B', icon: BookmarkIcon }, { text: 'Bookmark', key: 'B', icon: BookmarkIcon, command: 'bookmark' },
{ text: 'Branch', key: 'H', icon: BranchIcon } { text: 'Branch', key: 'H', icon: BranchIcon, command: 'branch' }
]; ];
$: menuItems = baseCommands; $: menuItems = baseCommands;
@ -171,12 +228,35 @@
menuItems = searchResults; menuItems = searchResults;
} }
} }
function doCommit() {
console.log('do commit', commitMessage);
// get checked files
let changedFiles: Array<string> = [];
let doc = document.getElementsByClassName('file-checkbox');
Array.from(doc).forEach((c) => {
if (c.checked) {
changedFiles.push(c.dataset['file']);
}
});
console.log('files', changedFiles, commitMessage);
commit({
projectId: projectId,
message: commitMessage,
files: changedFiles,
push: false
}).then((result) => {
console.log('commit result', result);
commitMessage = '';
showCommit = false;
});
}
</script> </script>
<svelte:window on:keydown={onKeyDown} on:keyup={onKeyUp} on:click={checkCommandModal} /> <svelte:window on:keydown={onKeyDown} on:keyup={onKeyUp} on:click={checkCommandModal} />
<div> <div>
{#if showCommand} {#if showCommand || showCommit}
<div class="relative z-10" role="dialog" aria-modal="true"> <div class="relative z-10" role="dialog" aria-modal="true">
<div <div
class="fixed inset-0 bg-zinc-900 bg-opacity-80 transition-opacity" class="fixed inset-0 bg-zinc-900 bg-opacity-80 transition-opacity"
@ -184,69 +264,148 @@
out:fade={{ duration: 50 }} out:fade={{ duration: 50 }}
/> />
<div class="command-palette-modal fixed inset-0 z-10 overflow-y-auto p-4 sm:p-6 md:p-20"> {#if showCommand}
<div <div class="command-palette-modal fixed inset-0 z-10 overflow-y-auto p-4 sm:p-6 md:p-20">
bind:this={palette} <div
in:fade={{ duration: 100 }} bind:this={palette}
out:fade={{ duration: 100 }} in:fade={{ duration: 100 }}
class="mx-auto max-w-2xl transform divide-y divide-zinc-500 divide-opacity-20 overflow-hidden rounded-xl bg-zinc-900 shadow-2xl transition-all border border-zinc-700" out:fade={{ duration: 100 }}
style=" class="mx-auto max-w-2xl transform divide-y divide-zinc-500 divide-opacity-20 overflow-hidden rounded-xl bg-zinc-900 shadow-2xl transition-all border border-zinc-700"
style="
border-width: 0.5px; border-width: 0.5px;
-webkit-backdrop-filter: blur(20px) saturate(190%) contrast(70%) brightness(80%); -webkit-backdrop-filter: blur(20px) saturate(190%) contrast(70%) brightness(80%);
backdrop-filter: blur(20px) saturate(190%) contrast(70%) brightness(80%); backdrop-filter: blur(20px) saturate(190%) contrast(70%) brightness(80%);
background-color: rgba(24, 24, 27, 0.60); background-color: rgba(24, 24, 27, 0.60);
border: 0.5px solid rgba(63, 63, 70, 0.50);" border: 0.5px solid rgba(63, 63, 70, 0.50);"
>
<div class="relative">
<svg
class="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-zinc-500"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
/>
</svg>
<input
id="command"
type="text"
bind:value={search}
class="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-white focus:ring-0 sm:text-sm"
placeholder="Search..."
/>
</div>
<!-- Default state, show/hide based on command palette state. -->
<ul
class="max-h-80 scroll-py-2 divide-y divide-zinc-500 divide-opacity-20 overflow-y-auto"
> >
<li class="p-1"> <div class="relative">
<ul id="commandMenu" class="text-sm text-zinc-400"> <svg
{#each menuItems as item} class="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-zinc-500"
<!-- Active: "bg-zinc-800 text-white" --> fill="none"
<li viewBox="0 0 24 24"
class="group flex cursor-default select-none items-center rounded-md px-3 py-2" stroke-width="1.5"
> stroke="currentColor"
<!-- Active: "text-white", Not Active: "text-zinc-500" --> aria-hidden="true"
<svelte:component this={item.icon} /> >
<span class="ml-3 flex-auto truncate">{item.text}</span> <path
{#if item.key} stroke-linecap="round"
<span class="ml-3 flex-none text-xs font-semibold text-zinc-400 px-1 py-1 bg-zinc-800 border-b border-black rounded"> stroke-linejoin="round"
<kbd class="font-sans"></kbd><kbd class="font-sans">{item.key}</kbd> d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
</span> />
{/if} </svg>
</li> <input
{/each} id="command"
</ul> type="text"
</li> bind:value={search}
</ul> class="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-white focus:ring-0 sm:text-sm"
placeholder="Search..."
/>
</div>
<!-- Default state, show/hide based on command palette state. -->
<ul
class="max-h-80 scroll-py-2 divide-y divide-zinc-500 divide-opacity-20 overflow-y-auto"
>
<li class="p-1">
<ul id="commandMenu" class="text-sm text-zinc-400">
{#each menuItems as item}
<!-- Active: "bg-zinc-800 text-white" -->
<li
class="group flex cursor-default select-none items-center rounded-md px-3 py-2"
on:click={() => {
executeCommand(item.command);
}}
data-command={item.command}
>
<!-- Active: "text-white", Not Active: "text-zinc-500" -->
<svelte:component this={item.icon} />
<span class="ml-3 flex-auto truncate">{item.text}</span>
{#if item.key}
<span
class="ml-3 flex-none text-xs font-semibold text-zinc-400 px-1 py-1 bg-zinc-800 border-b border-black rounded"
>
<kbd class="font-sans"></kbd><kbd class="font-sans">{item.key}</kbd>
</span>
{/if}
</li>
{/each}
</ul>
</li>
</ul>
</div>
</div> </div>
</div> {/if}
{#if showCommit}
<div class="commit-palette-modal fixed inset-0 z-10 overflow-y-auto p-4 sm:p-6 md:p-20">
<div
in:fade={{ duration: 100 }}
out:fade={{ duration: 100 }}
bind:this={commitPalette}
class="mx-auto max-w-2xl transform overflow-hidden rounded-xl bg-zinc-900 shadow-2xl transition-all border border-zinc-700"
style="
border-width: 0.5px;
border: 0.5px solid rgba(63, 63, 70, 0.50);"
>
<div class="w-full border-b border-zinc-700 text-lg text-white mb-4 p-4">
Commit Your Changes
</div>
<div
class="relative transform overflow-hidden text-left transition-all sm:w-full sm:max-w-sm p-2 m-auto"
>
{#if Object.entries(changedFiles).length > 0}
<div>
<div class="">
<h3 class="text-base font-semibold text-zinc-200" id="modal-title">
Commit Message
</h3>
<div class="mt-2">
<div class="mt-2">
<textarea
rows="4"
name="message"
id="commit-message"
bind:this={commitMessageInput}
bind:value={commitMessage}
class="block w-full rounded-md p-4 border-0 text-zinc-200 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:py-1.5 sm:text-sm sm:leading-6"
/>
</div>
</div>
</div>
</div>
<div class="mt-5 sm:mt-6">
<button
type="button"
on:click={doCommit}
class="inline-flex w-full justify-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
>Commit Your Changes</button
>
</div>
<div class="text-zinc-200 mt-4 py-4">
<h3 class="text-base font-semibold text-zinc-200" id="modal-title">
Changed Files
</h3>
{#each Object.entries(changedFiles) as file}
<div class="flex flex-row space-x-2">
<div>
<input type="checkbox" class="file-checkbox" data-file={file[0]} checked />
</div>
<div>
{file[1]}
</div>
<div class="font-mono">
{shortPath(file[0])}
</div>
</div>
{/each}
</div>
{:else}
<div class="text-white mx-auto text-center">No changes to commit</div>
{/if}
</div>
</div>
</div>
{/if}
</div> </div>
{/if} {/if}
</div> </div>

12
src/lib/paths.ts Normal file
View File

@ -0,0 +1,12 @@
export function shortPath(path: string, max = 3) {
if (path.length < 30) {
return path;
}
const pathParts = path.split('/');
const file = pathParts.pop();
if (pathParts.length > 0) {
const pp = pathParts.map((p) => p.slice(0, max)).join('/');
return `${pp}/${file}`;
}
return file;
}

View File

@ -4,25 +4,13 @@
import type { UISession } from '$lib/uisessions'; import type { UISession } from '$lib/uisessions';
import type { Activity } from '$lib/sessions'; import type { Activity } from '$lib/sessions';
import type { Delta } from '$lib/deltas'; import type { Delta } from '$lib/deltas';
import { shortPath } from '$lib/paths';
export let data: LayoutData; export let data: LayoutData;
$: project = data.project; $: project = data.project;
$: dateSessions = data.dateSessions as Readable<Record<number, UISession[]>>; $: dateSessions = data.dateSessions as Readable<Record<number, UISession[]>>;
$: filesStatus = data.filesStatus; $: filesStatus = data.filesStatus;
function shortPath(path: string, max = 3) {
if (path.length < 30) {
return path;
}
const pathParts = path.split('/');
const file = pathParts.pop();
if (pathParts.length > 0) {
const pp = pathParts.map((p) => p.slice(0, max)).join('/');
return `${pp}/${file}`;
}
return file;
}
// convert a list of timestamps to a sparkline // convert a list of timestamps to a sparkline
function timestampsToSpark(tsArray: number[]) { function timestampsToSpark(tsArray: number[]) {
let range = tsArray[0] - tsArray[tsArray.length - 1]; let range = tsArray[0] - tsArray[tsArray.length - 1];
@ -147,7 +135,9 @@
day: 'numeric' day: 'numeric'
})} })}
</div> </div>
<div class="results-card rounded bg-[#2F2F33] border border-zinc-700 p-4 drop-shadow-lg"> <div
class="results-card rounded bg-[#2F2F33] border border-zinc-700 p-4 drop-shadow-lg"
>
{#each Object.entries(fileSessions) as filetime} {#each Object.entries(fileSessions) as filetime}
<div class="flex flex-row justify-between"> <div class="flex flex-row justify-between">
<div class="font-mono text-zinc-100">{filetime[0]}</div> <div class="font-mono text-zinc-100">{filetime[0]}</div>
@ -171,9 +161,17 @@
<div class="work-in-progress-container border-b border-zinc-700 py-4 px-4"> <div class="work-in-progress-container border-b border-zinc-700 py-4 px-4">
<h2 class="mb-2 text-lg font-bold text-zinc-300">Work in Progress</h2> <h2 class="mb-2 text-lg font-bold text-zinc-300">Work in Progress</h2>
{#if $filesStatus.length == 0} {#if $filesStatus.length == 0}
<div class="flex align-middle rounded border border-green-700 bg-green-900 p-4 text-green-400"> <div
class="flex align-middle rounded border border-green-700 bg-green-900 p-4 text-green-400"
>
<div class="icon h-5 w-5 mr-2"> <div class="icon h-5 w-5 mr-2">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill="#4ADE80" fill-rule="evenodd" d="M2 10a8 8 0 1 0 16 0 8 8 0 0 0-16 0Zm12.16-1.44a.8.8 0 0 0-1.12-1.12L9.2 11.28 7.36 9.44a.8.8 0 0 0-1.12 1.12l2.4 2.4c.32.32.8.32 1.12 0l4.4-4.4Z" /></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
><path
fill="#4ADE80"
fill-rule="evenodd"
d="M2 10a8 8 0 1 0 16 0 8 8 0 0 0-16 0Zm12.16-1.44a.8.8 0 0 0-1.12-1.12L9.2 11.28 7.36 9.44a.8.8 0 0 0-1.12 1.12l2.4 2.4c.32.32.8.32 1.12 0l4.4-4.4Z"
/></svg
>
</div> </div>
Everything is committed Everything is committed
</div> </div>
@ -196,7 +194,9 @@
> >
<h2 class="text-lg font-bold text-zinc-300">Recent Activity</h2> <h2 class="text-lg font-bold text-zinc-300">Recent Activity</h2>
{#each recentActivity($dateSessions) as activity} {#each recentActivity($dateSessions) as activity}
<div class="recent-activity-card mt-4 mb-1 rounded border border-zinc-700 text-zinc-400 drop-shadow-lg"> <div
class="recent-activity-card mt-4 mb-1 rounded border border-zinc-700 text-zinc-400 drop-shadow-lg"
>
<div class="flex flex-col p-3 rounded bg-[#2F2F33]"> <div class="flex flex-col p-3 rounded bg-[#2F2F33]">
<div class="flex flex-row justify-between text-zinc-500 pb-2"> <div class="flex flex-row justify-between text-zinc-500 pb-2">
<div class=""> <div class="">