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
23 changes: 23 additions & 0 deletions diffgraph/git_snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,8 +265,31 @@ def _resolve(
return SnapshotResolution((), tuple(warnings))

raw_entries = _parse_raw(output, warnings)
# A conflicted index has no single pre/post pair. ``git diff --raw`` can
# emit an unmerged ``U`` record and a second record for the same path;
# hashing that second record against an arbitrary conflict stage would
# fabricate a snapshot. Omit every record touching an unmerged path until
# the user resolves it, while retaining an actionable warning.
unmerged_paths = {
path
for raw in raw_entries
if raw.status == "U"
for path in (raw.old_path, raw.new_path)
if path is not None
}
for path in sorted(unmerged_paths, key=os.fsencode):
warnings.append(ResolutionWarning(
"unmerged_index_entry",
"Cannot resolve an exact pre/post snapshot while the index is unmerged",
path,
))
Comment thread
coderabbitai[bot] marked this conversation as resolved.

entries: List[SnapshotEntry] = []
for raw in raw_entries:
if raw.status == "U" or (
raw.old_path in unmerged_paths or raw.new_path in unmerged_paths
):
continue
if staged:
entry = _exact_staged_entry(raw, warnings)
else:
Expand Down
3 changes: 2 additions & 1 deletion diffgraph/schema/diffgraph-v2.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,8 @@
"unsupported_worktree_entry",
"worktree_read_failed",
"hash_object_failed",
"malformed_hash_object_output"
"malformed_hash_object_output",
"unmerged_index_entry"
],
"description": "Machine-readable warning code. Consumers can surface these to the user."
},
Expand Down
1 change: 1 addition & 0 deletions diffgraph/structural.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ def _resolution_warning(item: ResolutionWarning) -> Dict[str, str]:
"worktree_read_failed",
"hash_object_failed",
"malformed_hash_object_output",
"unmerged_index_entry",
}
code = item.code if item.code in known_codes else "UNKNOWN"
return _warning(code, item.path, "{}: {}".format(item.code, item.message))
Expand Down
39 changes: 39 additions & 0 deletions tests/test_git_snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,28 @@ def make_repo(tmp_path):
return repo


def make_conflicted_repo(tmp_path):
"""Create one unresolved merge conflict in an otherwise valid repository."""

repo = make_repo(tmp_path)
write(repo, "conflict.py", b"base = True\n")
commit_all(repo, "base")

git(repo, "switch", "-c", "side")
write(repo, "conflict.py", b"side = True\n")
commit_all(repo, "side")

git(repo, "switch", "master")
write(repo, "conflict.py", b"main = True\n")
commit_all(repo, "main")
completed = subprocess.run(
["git", "merge", "side"], cwd=str(repo), stdout=subprocess.PIPE,
stderr=subprocess.PIPE, check=False,
)
assert completed.returncode != 0
return repo


def test_staged_add_modify_delete_and_rename_have_exact_identities(tmp_path):
repo = make_repo(tmp_path)
write(repo, "modify.txt", b"old modify\n")
Expand Down Expand Up @@ -216,6 +238,23 @@ def test_git_failures_are_warnings_not_changes(tmp_path, monkeypatch):
assert result.warnings[0].code == "not_a_git_repository"


def test_unmerged_index_entry_is_a_warning_not_a_fabricated_snapshot(tmp_path):
repo = make_conflicted_repo(tmp_path)

staged = resolve_staged(str(repo))
unstaged = resolve_unstaged(str(repo))

assert staged.entries == ()
assert unstaged.entries == ()
expected_warning = ("unmerged_index_entry", "conflict.py")
assert [(warning.code, warning.path) for warning in staged.warnings] == [
expected_warning
]
assert [(warning.code, warning.path) for warning in unstaged.warnings] == [
expected_warning
]


def test_unstaged_intent_to_add_and_rename_have_exact_identities(tmp_path):
repo = make_repo(tmp_path)
write(repo, "rename-old.py", b"def moved():\n return 1\n" * 20)
Expand Down
20 changes: 20 additions & 0 deletions tests/test_structural.py
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,26 @@ def test_resolution_warning_preserves_machine_readable_code(monkeypatch, tmp_pat
]


def test_unmerged_index_warning_preserves_machine_readable_code(monkeypatch, tmp_path):
root = repo(tmp_path)
warning = ResolutionWarning("unmerged_index_entry", "conflict remains", "app.py")
monkeypatch.setattr(
"diffgraph.structural.resolve_unstaged",
lambda repository, pathspecs: SnapshotResolution((), (warning,)),
)

artifact = analyze_local_diff(str(root))

assert_valid(artifact)
assert artifact["metadata"]["warnings"] == [
{
"code": "unmerged_index_entry",
"file": "app.py",
"detail": "unmerged_index_entry: conflict remains",
}
]


def test_cli_structural_json_rejects_non_diff_command():
from click.testing import CliRunner
from diffgraph.cli import main
Expand Down
Loading