thunderbird-patch-review

Simple email patch review tool for Thunderbird

git clone git://mccd.space/thunderbird-patch-review
commit 9f8e985c2dbe55902916148a494d8b3a9c2d21db
parent 9f4c4376617f9e5f5a8ec419b0717995ce1eff7a
Author: Pi Agent <agent@pi.local>
Date:   Mon, 20 Jul 2026 10:47:36 +0200

apply(): refuse rather than abort an in-progress git am

A prior unfinished `git am` leaves a rebase-apply/ directory in the
git dir; a fresh `git am` exits non-zero ("a previous rebase
directory .git/rebase-apply still exists but mbox given"),
whereupon apply()'s abort path ran `git am --abort` in the user's
own repository — resetting the branch and discarding their ongoing
conflict resolution. "the repository is back to its previous state"
was actively wrong in that case: it was someone else's previous state.

Guard before running: resolve the git dir
(rev-parse --absolute-git-dir, falling back to --git-dir for pre-2.31
git), check for <gitdir>/rebase-apply, and if present return a refusal
that names the location and leaves the session untouched. The user
resolves it themselves (git am --continue / --skip / --abort) and
applies again.

Stage 3 of check() runs in a fresh --detach worktree whose own
per-worktree git dir has no rebase-apply, so it is unaffected and need
not be guarded.

Diffstat:
Mextension/api/patchHost/implementation.js | 46++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 46 insertions(+), 0 deletions(-)
diff --git a/extension/api/patchHost/implementation.js b/extension/api/patchHost/implementation.js
@@ -64,6 +64,31 @@ async function emptyHooksDir() {
   return dir;
 }
 
+// Detect a `git am` already in progress in the user's repository: an
+// unfinished am leaves a rebase-apply/ directory inside the git dir, and
+// a fresh `git am` exits non-zero on it ("a previous am session is in
+// progress"). apply()'s abort path would then tear that session down,
+// discarding the user's in-progress conflict resolution; the guard before
+// apply() refuses instead. Resolve the git dir absolutely so a linked
+// worktree's own per-worktree git dir is checked (the path --git-path
+// rebase-apply would return for the worktree, not the common .git).
+// --absolute-git-dir lands in git 2.31 (2021); fall back to relative
+// --git-dir (absolute for linked worktrees, ".git" for the main repo) on
+// older builds, joined against repo. Returns the rebase-apply path if a
+// session is in progress, else null.
+async function amInProgress(git, repo) {
+  let gd = await run(git, ["-C", repo, "rev-parse", "--absolute-git-dir"]);
+  if (gd.exitCode !== 0 || !gd.output.trim()) {
+    gd = await run(git, ["-C", repo, "rev-parse", "--git-dir"]);
+    if (gd.exitCode !== 0) return null;
+  }
+  let dir = gd.output.trim();
+  if (!dir) return null;
+  if (!PathUtils.isAbsolute(dir)) dir = PathUtils.join(repo, dir);
+  const ra = PathUtils.join(dir, "rebase-apply");
+  return (await IOUtils.exists(ra)) ? ra : null;
+}
+
 async function findGit() {
   try {
     return await Subprocess.pathSearch("git", Subprocess.getEnvironment());
@@ -119,6 +144,27 @@ async function apply(repo, strategy, patches) {
     throw new Error("no patches in request");
   }
 
+  // Refuse before touching the repo if a previous `git am` is mid-series
+  // here: a fresh `git am` would fail immediately ("a previous am session
+  // is in progress") and the abort below would then reset the user's
+  // branch and discard their ongoing conflict resolution — exactly the
+  // session this add-on did not start. Surface git's own diagnostic
+  // location and leave the working tree untouched, so the user resolves it
+  // themselves (git am --continue / --skip / --abort) and applies again.
+  const inProgress = await amInProgress(git, repo);
+  if (inProgress) {
+    return {
+      ok: false,
+      output:
+        "A previous `git am` session is already in progress in this repository\n" +
+        `        (${inProgress}).\n` +
+        "Resolve it yourself — git am --continue, --skip, or --abort — then\n" +
+        "apply again. This add-on will not run git am --abort against a\n" +
+        "session it did not start, so your in-progress resolution is left\n" +
+        "untouched.\n",
+    };
+  }
+
   const work = PathUtils.join(
     PathUtils.tempDir,
     `patch-review.${Date.now()}.${Math.floor(Math.random() * 1e9)}`