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