From 791275f34178e1c4b2bd700fe4d64b2cacb6ffc2 Mon Sep 17 00:00:00 2001 From: Christoph Brandau Date: Fri, 10 Jul 2026 22:53:24 +0200 Subject: [PATCH] refactor(git): improve interactive rebase workflow robustness This commit removes the dedicated feature QA page and significantly refactors the internal Git library logic for handling complex workflows, particularly interactive rebase. The changes introduce helper functions to manage temporary files and ensure proper cleanup of rebase artifacts regardless of success or failure. This improves the reliability of advanced git operations within the application. - Added comprehensive cleanup routines for rebase helpers - Centralized constants for rebase file names - Removed obsolete QA feature code --- feature-qa.html | 1 - src-tauri/src/git.rs | 94 ++++++++++++++++++++++++++++++++------------ src/feature-qa.ts | 27 ------------- 3 files changed, 69 insertions(+), 53 deletions(-) delete mode 100644 feature-qa.html delete mode 100644 src/feature-qa.ts diff --git a/feature-qa.html b/feature-qa.html deleted file mode 100644 index 11cb091..0000000 --- a/feature-qa.html +++ /dev/null @@ -1 +0,0 @@ -Git features QA
diff --git a/src-tauri/src/git.rs b/src-tauri/src/git.rs index 59c03a7..d67b573 100644 --- a/src-tauri/src/git.rs +++ b/src-tauri/src/git.rs @@ -210,6 +210,10 @@ pub struct ReflogEntry { const SEQUENCE_EDITOR_PLAN_ENV: &str = "GITTY_SEQUENCE_EDITOR_PLAN"; const COMMIT_EDITOR_QUEUE_ENV: &str = "GITTY_COMMIT_EDITOR_QUEUE"; +const REBASE_TODO_FILE: &str = "gitty-interactive-rebase-todo"; +const REWORD_QUEUE_FILE: &str = "gitty-interactive-rebase-messages"; +const SEQUENCE_HELPER_STEM: &str = ".gitty-sequence-editor"; +const COMMIT_HELPER_STEM: &str = ".gitty-commit-editor"; pub fn run_sequence_editor_if_requested() -> Option> { let executable = env::current_exe().ok()?; @@ -1632,33 +1636,24 @@ pub async fn start_interactive_rebase( let todo = build_rebase_todo(&available, &plan)?; let reword_queue = build_reword_queue(&available, &plan)?; let git_dir = git_dir_for_repo(&repo)?; - let todo_path = git_dir.join("gitty-interactive-rebase-todo"); - let reword_queue_path = git_dir.join("gitty-interactive-rebase-messages"); + cleanup_interactive_rebase_helpers(&repo); + let todo_path = git_dir.join(REBASE_TODO_FILE); + let reword_queue_path = git_dir.join(REWORD_QUEUE_FILE); fs::write(&todo_path, todo) .map_err(|err| format!("Could not prepare interactive rebase plan: {err}"))?; fs::write(&reword_queue_path, reword_queue) .map_err(|err| format!("Could not prepare reword messages: {err}"))?; - let sequence_helper_name = if cfg!(windows) { - format!(".gitty-sequence-editor-{}.exe", std::process::id()) - } else { - format!(".gitty-sequence-editor-{}", std::process::id()) - }; - let commit_helper_name = if cfg!(windows) { - format!(".gitty-commit-editor-{}.exe", std::process::id()) - } else { - format!(".gitty-commit-editor-{}", std::process::id()) - }; - let sequence_helper_path = repo.join(&sequence_helper_name); - let commit_helper_path = repo.join(&commit_helper_name); + let sequence_helper_path = repo.join(sequence_helper_name()); + let commit_helper_path = repo.join(commit_helper_name()); let current_exe = env::current_exe() .map_err(|err| format!("Could not locate the Gitty executable: {err}"))?; fs::copy(¤t_exe, &sequence_helper_path) .and_then(|_| fs::copy(¤t_exe, &commit_helper_path)) .map_err(|err| format!("Could not prepare interactive rebase helpers: {err}"))?; - let sequence_editor_command = format!("./{sequence_helper_name}"); - let commit_editor_command = format!("./{commit_helper_name}"); + let sequence_editor_command = format!("./{}", sequence_helper_name()); + let commit_editor_command = format!("./{}", commit_helper_name()); let output = git_command() .arg("-C") .arg(&repo) @@ -1670,11 +1665,14 @@ pub async fn start_interactive_rebase( .output() .map_err(|err| format!("Could not start Git. Is Git installed? {err}")); + let result = rebase_status_or_error(&repo, output?, "Interactive rebase failed", true); let _ = fs::remove_file(&todo_path); - let _ = fs::remove_file(&reword_queue_path); let _ = fs::remove_file(&sequence_helper_path); - let _ = fs::remove_file(&commit_helper_path); - rebase_status_or_error(&repo, output?, "Interactive rebase failed", true) + if !matches!(&result, Ok(status) if status.rebase_in_progress) { + let _ = fs::remove_file(&reword_queue_path); + let _ = fs::remove_file(&commit_helper_path); + } + result }) .await .map_err(|err| format!("Could not run interactive rebase: {err}"))? @@ -1687,15 +1685,27 @@ pub fn rebase_continue(path: String) -> Result { return Err("No rebase is currently in progress.".to_string()); } - let output = git_command() - .arg("-C") - .arg(&repo) - .args(["rebase", "--continue"]) - .env("GIT_EDITOR", "true") + let git_dir = git_dir_for_repo(&repo)?; + let queue_path = git_dir.join(REWORD_QUEUE_FILE); + let helper_path = repo.join(commit_helper_name()); + let mut command = git_command(); + command.arg("-C").arg(&repo).args(["rebase", "--continue"]); + if queue_path.exists() && helper_path.exists() { + command + .env("GIT_EDITOR", format!("./{}", commit_helper_name())) + .env(COMMIT_EDITOR_QUEUE_ENV, &queue_path); + } else { + command.env("GIT_EDITOR", "true"); + } + let output = command .output() .map_err(|err| format!("Could not start Git. Is Git installed? {err}"))?; - rebase_status_or_error(&repo, output, "Rebase continue failed", false) + let result = rebase_status_or_error(&repo, output, "Rebase continue failed", false); + if !matches!(&result, Ok(status) if status.rebase_in_progress) { + cleanup_interactive_rebase_helpers(&repo); + } + result } #[tauri::command] @@ -1706,6 +1716,7 @@ pub fn rebase_abort(path: String) -> Result { } run_git(&repo, ["rebase", "--abort"])?; + cleanup_interactive_rebase_helpers(&repo); status_for_repo(&repo) } @@ -1911,6 +1922,38 @@ fn git_dir_for_repo(repo: &Path) -> Result { } } +fn sequence_helper_name() -> &'static str { + if cfg!(windows) { + ".gitty-sequence-editor.exe" + } else { + SEQUENCE_HELPER_STEM + } +} + +fn commit_helper_name() -> &'static str { + if cfg!(windows) { + ".gitty-commit-editor.exe" + } else { + COMMIT_HELPER_STEM + } +} + +fn cleanup_interactive_rebase_helpers(repo: &Path) { + if let Ok(git_dir) = git_dir_for_repo(repo) { + let _ = fs::remove_file(git_dir.join(REBASE_TODO_FILE)); + let _ = fs::remove_file(git_dir.join(REWORD_QUEUE_FILE)); + } + let _ = fs::remove_file(repo.join(sequence_helper_name())); + let _ = fs::remove_file(repo.join(commit_helper_name())); +} + +fn is_interactive_rebase_helper_path(path: &str) -> bool { + matches!( + path.replace('\\', "/").rsplit('/').next(), + Some(name) if name == sequence_helper_name() || name == commit_helper_name() + ) +} + fn parse_reflog(output: &[u8]) -> Result, String> { let mut entries = Vec::new(); for raw in output.split(|byte| *byte == 0x1e) { @@ -2946,6 +2989,7 @@ fn status_for_repo(repo: &Path) -> Result { )?; let (branch, mut files) = parse_status_output(&output)?; detect_worktree_renames(repo, &mut files); + files.retain(|file| !is_interactive_rebase_helper_path(&file.path)); Ok(GitStatus { repo_path: repo.to_string_lossy().to_string(), diff --git a/src/feature-qa.ts b/src/feature-qa.ts deleted file mode 100644 index 37701a2..0000000 --- a/src/feature-qa.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { mount } from "svelte"; -import "./app.css"; -import InteractiveRebaseDialog from "./lib/components/InteractiveRebaseDialog.svelte"; -import ReflogDialog from "./lib/components/ReflogDialog.svelte"; - -const target = document.getElementById("qa")!; -const branches = [ - { name: "features/rewrite-history", current: true, remote: false }, - { name: "main", current: false, remote: false }, - { name: "origin/main", current: false, remote: true }, -]; -const commits = [ - { hash: "1111111111111111111111111111111111111111", short_hash: "1111111", summary: "Add reflog backend", author_name: "Ada", date: "2026-07-10T09:10:00+02:00" }, - { hash: "2222222222222222222222222222222222222222", short_hash: "2222222", summary: "Build interactive rebase dialog", author_name: "Linus", date: "2026-07-10T10:20:00+02:00" }, - { hash: "3333333333333333333333333333333333333333", short_hash: "3333333", summary: "Polish recovery workflow", author_name: "Grace", date: "2026-07-10T11:30:00+02:00" }, -]; -const entries = [ - { hash: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", short_hash: "aaaaaaa", selector: "HEAD@{0}", action: "commit: Add recovery workflow", author_name: "Ada", date: "2026-07-10T12:00:00+02:00" }, - { hash: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", short_hash: "bbbbbbb", selector: "HEAD@{1}", action: "rebase (finish): returning to refs/heads/feature", author_name: "Ada", date: "2026-07-10T11:00:00+02:00" }, - { hash: "cccccccccccccccccccccccccccccccccccccccc", short_hash: "ccccccc", selector: "HEAD@{2}", action: "checkout: moving from main to feature", author_name: "Ada", date: "2026-07-10T10:00:00+02:00" }, -]; - -if (location.hash === "#reflog") { - mount(ReflogDialog, { target, props: { entries, currentHash: entries[0].hash, isLoading: false, isBusy: false, operation: "", error: "", onPreview: () => {}, onRestore: (_entry, branch) => { document.title = `Recovered ${branch}`; }, onClose: () => {} } }); -} else { - mount(InteractiveRebaseDialog, { target, props: { branches, currentBranch: branches[0].name, base: "main", commits, isLoading: false, isBusy: false, operation: "", error: "", onBaseChange: () => {}, onStart: (plan) => { document.title = `Rebase ${plan.length} commits`; }, onClose: () => {} } }); -}