Add working directory diff support and auto generate commit messages

This update enables the generation of a working directory diff and returns a Git commit message based on the diff changes. It includes changes to the API, Tauri, and frontend code. Notable modifications include:

- Added `git_wd_diff` function to Tauri `src/main.rs`
- Modified `Repository` struct, added `wd_diff` method to `src/repositories/repository.rs`
- New `commit` function added to `src/lib/api.ts`
- Added `fetchCommitMessage` function in `src/routes/projects/[projectId]/+page.svelte`
- Minor UI adjustments in `src/routes/projects/[projectId]/player/+page.svelte`

Overall, this commit improves user experience by automatically generating commit messages based on the changes made in the working directory.
This commit is contained in:
Scott Chacon 2023-03-18 07:52:21 +01:00
parent 4df03fbcdc
commit 1210d35288
7 changed files with 117 additions and 21 deletions

View File

@ -369,6 +369,13 @@ async fn git_status(
Ok(files) Ok(files)
} }
#[tauri::command]
async fn git_wd_diff(handle: tauri::AppHandle, project_id: &str) -> Result<String, Error> {
let repo = repo_for_project(handle, project_id)?;
let diff = repo.wd_diff().with_context(|| "Failed to get git diff")?;
Ok(diff)
}
#[tauri::command] #[tauri::command]
async fn git_file_paths(handle: tauri::AppHandle, project_id: &str) -> Result<Vec<String>, Error> { async fn git_file_paths(handle: tauri::AppHandle, project_id: &str) -> Result<Vec<String>, Error> {
let repo = repo_for_project(handle, project_id)?; let repo = repo_for_project(handle, project_id)?;
@ -579,7 +586,8 @@ fn main() {
git_branches, git_branches,
git_branch, git_branch,
git_switch_branch, git_switch_branch,
git_commit git_commit,
git_wd_diff
]); ]);
let tauri_context = generate_context!(); let tauri_context = generate_context!();

View File

@ -1,7 +1,7 @@
use crate::{deltas, fs, projects, sessions, users}; use crate::{deltas, fs, projects, sessions, users};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use git2::{BranchType, Cred, Signature}; use git2::{BranchType, Cred, Signature};
use std::{collections::HashMap, env, path::Path}; use std::{collections::HashMap, env, path::Path, str::from_utf8};
use tauri::regex::Regex; use tauri::regex::Regex;
use walkdir::WalkDir; use walkdir::WalkDir;
@ -172,6 +172,26 @@ impl Repository {
Ok(branch.to_string()) Ok(branch.to_string())
} }
pub fn wd_diff(&self) -> Result<String> {
println!("diffing");
let repo = &self.git_repository;
let head = repo.head()?;
let tree = head.peel_to_tree()?;
let diff = repo.diff_tree_to_workdir_with_index(Some(&tree), None)?;
let mut buf = String::new();
diff.print(git2::DiffFormat::Patch, |delta, hunk, line| {
buf.push_str(&format!(
"{:?} {}",
delta.status(),
delta.new_file().path().unwrap().to_str().unwrap()
));
buf.push_str(from_utf8(line.content()).unwrap());
buf.push_str("\n");
true
})?;
Ok(buf)
}
pub fn switch_branch(&self, branch_name: &str) -> Result<bool> { pub fn switch_branch(&self, branch_name: &str) -> Result<bool> {
self.flush_session(&None) self.flush_session(&None)
.with_context(|| "failed to flush session before switching branch")?; .with_context(|| "failed to flush session before switching branch")?;

View File

@ -128,6 +128,17 @@ export default (
}).then(parseResponseJSON); }).then(parseResponseJSON);
} }
}, },
summarize: {
commit: (token: string, params: { diff: string; uid?: string }): Promise<string> =>
fetch(getUrl('summarize/commit.json'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Auth-Token': token
},
body: JSON.stringify(params)
}).then(parseResponseJSON)
},
projects: { projects: {
create: (token: string, params: { name: string; uid?: string }): Promise<Project> => create: (token: string, params: { name: string; uid?: string }): Promise<Project> =>
fetch(getUrl('projects.json'), { fetch(getUrl('projects.json'), {

View File

@ -37,8 +37,20 @@ export const load: LayoutLoad = async ({ parent, params }) => {
}); });
return activitySorted.slice(0, 20); return activitySorted.slice(0, 20);
}); });
const user = building
? {
...readable<undefined>(undefined),
set: () => {
throw new Error('not implemented');
},
delete: () => {
throw new Error('not implemented');
}
}
: await (await import('$lib/users')).default();
return { return {
user: user,
project: projects.get(params.projectId), project: projects.get(params.projectId),
projectId: params.projectId, projectId: params.projectId,
orderedSessionsFromLastFourDays: orderedSessionsFromLastFourDays, orderedSessionsFromLastFourDays: orderedSessionsFromLastFourDays,

View File

@ -13,14 +13,18 @@
import { navigating } from '$app/stores'; import { navigating } from '$app/stores';
import toast from 'svelte-french-toast'; import toast from 'svelte-french-toast';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import Api from '$lib/api';
const api = Api({ fetch });
const getBranch = (params: { projectId: string }) => invoke<string>('git_branch', params); const getBranch = (params: { projectId: string }) => invoke<string>('git_branch', params);
const getDiff = (params: { projectId: string }) => invoke<string>('git_wd_diff', params);
export let data: LayoutData; export let data: LayoutData;
$: project = data.project; $: project = data.project;
$: filesStatus = data.filesStatus; $: filesStatus = data.filesStatus;
$: recentActivity = data.recentActivity as Readable<Activity[]>; $: recentActivity = data.recentActivity;
$: orderedSessionsFromLastFourDays = data.orderedSessionsFromLastFourDays; $: orderedSessionsFromLastFourDays = data.orderedSessionsFromLastFourDays;
$: user = data.user;
const commit = (params: { const commit = (params: {
projectId: string; projectId: string;
@ -31,6 +35,7 @@
let latestDeltasByDateByFile: Record<number, Record<string, Delta[][]>[]> = {}; let latestDeltasByDateByFile: Record<number, Record<string, Delta[][]>[]> = {};
let commitMessage: string; let commitMessage: string;
let placeholderMessage = 'Description of changes';
let initiatedCommit = false; let initiatedCommit = false;
let filesSelectedForCommit: string[] = []; let filesSelectedForCommit: string[] = [];
@ -183,6 +188,27 @@
return sessionsByFile; return sessionsByFile;
} }
function fetchCommitMessage() {
if ($project && $user) {
placeholderMessage = 'Summarizing changes...';
console.log('FETCHING DIFF');
getDiff({
projectId: $project.id
}).then((result) => {
console.log('DIFF', result);
api.summarize
.commit($user?.access_token, {
diff: result,
uid: $project.id
})
.then((result) => {
console.log(result);
commitMessage = result.message;
});
});
}
}
// order the sessions and summarize the changes by file // order the sessions and summarize the changes by file
function orderedSessions(dateSessions: Record<number, Record<string, Delta[][]>[]>) { function orderedSessions(dateSessions: Record<number, Record<string, Delta[][]>[]>) {
return Object.entries(dateSessions) return Object.entries(dateSessions)
@ -288,7 +314,7 @@
<div class="truncate pl-2 font-mono text-zinc-300"> <div class="truncate pl-2 font-mono text-zinc-300">
{toHumanBranchName(gitBranch)} {toHumanBranchName(gitBranch)}
</div> </div>
<div class="carrot flex items-center pl-3"> <div class="carrot flex hidden items-center pl-3">
<svg width="7" height="5" viewBox="0 0 7 5" fill="none" class="fill-zinc-400"> <svg width="7" height="5" viewBox="0 0 7 5" fill="none" class="fill-zinc-400">
<path <path
d="M3.87796 4.56356C3.67858 4.79379 3.32142 4.79379 3.12204 4.56356L0.319371 1.32733C0.0389327 1.00351 0.268959 0.5 0.697336 0.5L6.30267 0.500001C6.73104 0.500001 6.96107 1.00351 6.68063 1.32733L3.87796 4.56356Z" d="M3.87796 4.56356C3.67858 4.79379 3.32142 4.79379 3.12204 4.56356L0.319371 1.32733C0.0389327 1.00351 0.268959 0.5 0.697336 0.5L6.30267 0.500001C6.73104 0.500001 6.96107 1.00351 6.68063 1.32733L3.87796 4.56356Z"
@ -297,7 +323,7 @@
</svg> </svg>
</div> </div>
</div> </div>
<div class="branch-count-container text-md hover:text-blue-500 ">6 branches</div> <div class="branch-count-container text-md hidden hover:text-blue-500 ">6 branches</div>
</div> </div>
{/if} {/if}
{#if $filesStatus.length == 0} {#if $filesStatus.length == 0}
@ -341,7 +367,7 @@
<textarea <textarea
rows="4" rows="4"
name="message" name="message"
placeholder="Description of changes" placeholder={placeholderMessage}
bind:value={commitMessage} bind:value={commitMessage}
class="mb-2 block w-full rounded-md border-0 p-4 text-zinc-200 ring-1 ring-inset ring-gray-600 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:py-1.5 sm:text-sm sm:leading-6" class="mb-2 block w-full rounded-md border-0 p-4 text-zinc-200 ring-1 ring-inset ring-gray-600 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:py-1.5 sm:text-sm sm:leading-6"
/> />
@ -384,7 +410,7 @@
filesSelectedForCommit = $filesStatus.map((file) => { filesSelectedForCommit = $filesStatus.map((file) => {
return file.path; return file.path;
}); });
fetchCommitMessage();
initiatedCommit = true; initiatedCommit = true;
}}>Commit changes</button }}>Commit changes</button
> >

View File

@ -0,0 +1,20 @@
import { building } from '$app/environment';
import { readable } from 'svelte/store';
import type { PageLoad } from './$types';
export const load: PageLoad = async () => {
const user = building
? {
...readable<undefined>(undefined),
set: () => {
throw new Error('not implemented');
},
delete: () => {
throw new Error('not implemented');
}
}
: await (await import('$lib/users')).default();
return {
user
};
};

View File

@ -544,15 +544,17 @@
/> />
</div> </div>
<div class="playback-controller-ui mx-auto flex w-full items-center gap-2 justify-between"> <div
class="playback-controller-ui mx-auto flex w-full items-center justify-between gap-2"
>
<div class="left-side flex space-x-8"> <div class="left-side flex space-x-8">
<div class="play-button-button-container"> <div class="play-button-button-container">
{#if interval} {#if interval}
<button on:click={stop} <button on:click={stop}
><IconPlayerPauseFilled ><IconPlayerPauseFilled
class="playback-button-play icon-pointer h-6 w-6" class="playback-button-play icon-pointer h-6 w-6"
/></button /></button
> >
{:else} {:else}
<button on:click={play} <button on:click={play}
><IconPlayerPlayFilled class="icon-pointer h-6 w-6" /></button ><IconPlayerPlayFilled class="icon-pointer h-6 w-6" /></button
@ -560,8 +562,6 @@
{/if} {/if}
</div> </div>
<div class="back-forward-button-container "> <div class="back-forward-button-container ">
<button on:click={decrementPlayerValue} class="playback-button-back group"> <button on:click={decrementPlayerValue} class="playback-button-back group">
<svg <svg
@ -602,11 +602,10 @@
</button> </button>
</div> </div>
<button on:click={speedUp}>{speed}x</button> <button on:click={speedUp}>{speed}x</button>
</div> </div>
<div class="align-center flex gap-2 flex-row-reverse"> <div class="align-center flex flex-row-reverse gap-2">
<button class="checkbox-button "> <button class="checkbox-button ">
<label <label
for="full-context-checkbox" for="full-context-checkbox"
@ -645,10 +644,10 @@
</label> </label>
</button> </button>
{#if !fullContext} {#if !fullContext}
<input <input
type="number" type="number"
bind:value={context} bind:value={context}
class="pl-2 pr-1 py-1 rounded w-14" class="w-14 rounded py-1 pl-2 pr-1"
/> />
{/if} {/if}
</div> </div>