mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-24 13:37:34 +03:00
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:
parent
4df03fbcdc
commit
1210d35288
@ -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!();
|
||||||
|
@ -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")?;
|
||||||
|
@ -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'), {
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
>
|
>
|
||||||
|
20
src/routes/projects/[projectId]/+page.ts
Normal file
20
src/routes/projects/[projectId]/+page.ts
Normal 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
|
||||||
|
};
|
||||||
|
};
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user