Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions revup/amend.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,9 @@ async def rebuild_stack_last_touched(
Rebuild the stack, amending each staged file into the most recent commit that touched it.
Returns the new HEAD commit.
"""
# Map each staged file to its most recent commit in the stack (ordered oldest-first)
# Map each staged file to its most recent commit in the stack (ordered oldest-first).
# -z gives NUL-terminated, unquoted paths so names with spaces, quotes, or unicode
# match staged_files (also -z) exactly rather than git's default C-quoted form.
file_to_commit: Dict[str, int] = {}
for i, commit_obj in enumerate(stack):
touched = await git_ctx.git_stdout(
Expand All @@ -167,9 +169,10 @@ async def rebuild_stack_last_touched(
"--name-only",
"-r",
"--no-renames",
"-z",
commit_obj.commit_id,
)
for f in touched.split("\n"):
for f in touched.split("\0"):
if f and f in staged_files:
file_to_commit[f] = i

Expand All @@ -185,8 +188,8 @@ async def rebuild_stack_last_touched(
# A staged deletion has no entry here; its path stays out of staged_entries so
# the overlay omits it, which the pre-image merge base below turns into a removal.
staged_entries: Dict[str, str] = {}
ls_output = await git_ctx.git_stdout("ls-files", "--stage", "--", *files_to_amend)
for line in ls_output.split("\n"):
ls_output = await git_ctx.git_stdout("ls-files", "--stage", "-z", "--", *files_to_amend)
for line in ls_output.split("\0"):
if not line:
continue
_, path = line.split("\t", 1)
Expand Down Expand Up @@ -286,7 +289,7 @@ async def get_has_unstaged() -> bool:

if args.last_touched:
staged_files_output = await git_ctx.git_stdout(
"diff-index", "--cached", "-r", "--name-only", "--no-renames", "HEAD"
"diff-index", "--cached", "-r", "--name-only", "--no-renames", "-z", "HEAD"
)
if not staged_files_output:
return 0
Expand All @@ -304,7 +307,7 @@ async def get_has_unstaged() -> bool:
if not stack:
return 0

staged_files = set(staged_files_output.split("\n"))
staged_files = set(f for f in staged_files_output.split("\0") if f)
new_commit = await rebuild_stack_last_touched(git_ctx, stack, staged_files)
if not new_commit:
return 0
Expand Down
6 changes: 4 additions & 2 deletions revup/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -630,9 +630,11 @@ async def make_tree_from_paths(self, tree: GitTreeHash, paths: List[str]) -> Git
"""
if not paths:
return await self.empty_tree()
ls_output = await self.git_stdout("ls-tree", "-r", tree, "--", *paths)
# -z gives NUL-terminated, unquoted paths so names with special chars or
# unicode aren't C-quoted, matching make_tree_from_index_entries's format.
ls_output = await self.git_stdout("ls-tree", "-r", "-z", tree, "--", *paths)
entries = []
for line in ls_output.split("\n"):
for line in ls_output.split("\0"):
if not line:
continue
meta, path = line.split("\t", 1)
Expand Down
24 changes: 24 additions & 0 deletions tests/test_amend.py
Original file line number Diff line number Diff line change
Expand Up @@ -996,3 +996,27 @@ async def test_staged_deletion_amended_into_last_commit(self):
assert await env.get_file_at_commit("keep.txt", "HEAD~1") == "k"
assert await env.get_file_at_commit("b.txt", "HEAD") == "b1"
assert not await env.has_staged_changes()

@async_test
async def test_paths_with_special_chars_and_unicode(self):
# git C-quotes paths with spaces/unicode unless -z is used; exercise both
# a modification and a deletion of such paths in one run.
async with GitTestEnvironment() as env:
await env.commit("root", {"root.txt": "r"})
await env.git_ctx.git("branch", "origin/main", "HEAD")
await env.commit(
"first\n\nTopic: alpha",
{"süb dir/fïle a.txt": "v1", "dél été.txt": "d1"},
)
await env.commit("second\n\nTopic: beta", {"b.txt": "b1"})

# Modify the unicode/space path and delete another such path.
await env.stage_file("süb dir/fïle a.txt", "v2")
await env.git_ctx.git("rm", "dél été.txt")
args = make_amend_args(last_touched=True, parse_topics=True)
await amend.main(args, env.git_ctx)

assert await env.get_file_at_commit("süb dir/fïle a.txt", "HEAD~1") == "v2"
assert await env.git_ctx.git_return_code("cat-file", "-e", "HEAD:dél été.txt") != 0
assert await env.get_file_at_commit("b.txt", "HEAD") == "b1"
assert not await env.has_staged_changes()
Loading