thunderbird-patch-review
Simple email patch review tool for Thunderbird
git clone git://mccd.space/thunderbird-patch-reviewcommit 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:
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)}`