Context
When an integration merge fails, git fi resets the working tree and then lists any files the failed merge left behind, offering a command line per file to clean them up:
Some extra untracked files have been left as a result of the failed merge(s):
* conflict-file.txt
You can delete these by running:
rm "conflict-file.txt"
That line is built by interpolating the filename into a double-quoted string (src/merge.ts, in mergeProcess's failure branch). Double quotes stop word-splitting and globbing, but they do not stop command substitution: $(...) and backticks are still expanded by the shell inside them.
The filenames are not the user's own. They are content carried in by a teammate's branch that the octopus merge started to apply before it failed, so their names are chosen by whoever wrote that branch. git fi then prints them, on a report whose whole purpose is to be read and acted on, to every person on the team whose next merge fails.
The output is also wrong in a second, quieter way. git ls-files --other --exclude-standard honors core.quotePath, which is on by default, so a filename with a non-ASCII byte comes back already C-quoted — and is then wrapped in a second pair of double quotes, producing a command that names no file that exists.
C-quoting? git's way of making an unusual path safe to print on one line: it wraps the path in double quotes and escapes the awkward bytes (caf\303\251.txt). Printable ASCII like $ and ` is not awkward by that definition, so it passes through untouched — which is why the quoting that fires here is the one case that doesn't help.
Three filenames, and what each currently produces:
| Filename in the repo |
Printed command |
Result when pasted |
a`id`b.txt |
rm "a`id`b.txt" |
runs id, then deletes the wrong path |
c$(id)d.txt |
rm "c$(id)d.txt" |
same, via $( ) |
weird ünï.txt |
rm ""weird \303\274n\303\257.txt"" |
deletes nothing — the path doesn't exist |
Proposed approach
Two changes, both mirroring what the conflict-attribution report already does for branch names:
-
Read the paths as NUL-terminated records. Add -z to both git ls-files --other --exclude-standard calls in mergeProcess — the before-snapshot and the after-snapshot — and split on NUL rather than newline. -z turns off C-quoting, so the path arrives as its real bytes, and a filename containing a newline stops being able to split one record into two. Both call sites need it together, since the two sets are compared to find what the merge added.
-
Single-quote the filename in the printed command. Single quotes are the only form that stops command substitution. git fi already has this in shq(), which quotes a branch name for exactly the same reason before printing it in a rebase command; the same function applies unchanged to a path.
shq() currently lives as a module-private helper in src/readiness.ts. Wherever it ends up, one implementation should serve both call sites — the two are the same problem, and a report that quotes the branch name on one line and not the filename three lines below is the state this issue is about.
Acceptance criteria
- A file whose name contains a backtick or
$( ), left behind by a failed merge, is printed in a rm line that deletes exactly that file and executes nothing else when pasted into bash or zsh.
- A file whose name contains a space or a non-ASCII character is printed in a
rm line that names the real path, with no leftover C-quote escapes and no doubled quotes.
- A file whose name contains a newline does not split into two entries in either the bullet list or the
rm lines.
- A file with an ordinary name still prints as
rm conflict-file.txt or rm "conflict-file.txt" — something a reader recognizes as what they would have typed.
- Tests cover the backtick, the non-ASCII, and the ordinary case.
MERGE-11 in SPEC.md states the quoting rule for the paths it specifies printing.
Considerations
Context
When an integration merge fails,
git firesets the working tree and then lists any files the failed merge left behind, offering a command line per file to clean them up:That line is built by interpolating the filename into a double-quoted string (
src/merge.ts, inmergeProcess's failure branch). Double quotes stop word-splitting and globbing, but they do not stop command substitution:$(...)and backticks are still expanded by the shell inside them.The filenames are not the user's own. They are content carried in by a teammate's branch that the octopus merge started to apply before it failed, so their names are chosen by whoever wrote that branch.
git fithen prints them, on a report whose whole purpose is to be read and acted on, to every person on the team whose next merge fails.The output is also wrong in a second, quieter way.
git ls-files --other --exclude-standardhonorscore.quotePath, which is on by default, so a filename with a non-ASCII byte comes back already C-quoted — and is then wrapped in a second pair of double quotes, producing a command that names no file that exists.Three filenames, and what each currently produces:
a`id`b.txtrm "a`id`b.txt"id, then deletes the wrong pathc$(id)d.txtrm "c$(id)d.txt"$( )weird ünï.txtrm ""weird \303\274n\303\257.txt""Proposed approach
Two changes, both mirroring what the conflict-attribution report already does for branch names:
Read the paths as NUL-terminated records. Add
-zto bothgit ls-files --other --exclude-standardcalls inmergeProcess— the before-snapshot and the after-snapshot — and split on NUL rather than newline.-zturns off C-quoting, so the path arrives as its real bytes, and a filename containing a newline stops being able to split one record into two. Both call sites need it together, since the two sets are compared to find what the merge added.Single-quote the filename in the printed command. Single quotes are the only form that stops command substitution.
git fialready has this inshq(), which quotes a branch name for exactly the same reason before printing it in a rebase command; the same function applies unchanged to a path.shq()currently lives as a module-private helper insrc/readiness.ts. Wherever it ends up, one implementation should serve both call sites — the two are the same problem, and a report that quotes the branch name on one line and not the filename three lines below is the state this issue is about.Acceptance criteria
$( ), left behind by a failed merge, is printed in armline that deletes exactly that file and executes nothing else when pasted intobashorzsh.rmline that names the real path, with no leftover C-quote escapes and no doubled quotes.rmlines.rm conflict-file.txtorrm "conflict-file.txt"— something a reader recognizes as what they would have typed.MERGE-11inSPEC.mdstates the quoting rule for the paths it specifies printing.Considerations
shq()is introduced by Say where each branch stands, and whose rebase fixes a failure #11. Landing that first makes this a small change that reuses it; landing this first means writing the helper here and having Say where each branch stands, and whose rebase fixes a failure #11 adopt it. Either works — they should not both define one.bulletListis a separate question. The same filenames are also printed as a plain bullet list just above thermlines. That list is prose rather than a command, so nothing executes from it, but it is where the C-quoting shows up as unreadable output. Fixing the read with-zfixes the display too; no quoting is wanted there.