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: () => {} } }); -}