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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,10 @@ Canonical output supports local unstaged (`index` → working tree), staged
(`HEAD` → index), explicit two-dot (`BASE..HEAD`), and explicit three-dot
(`BASE...HEAD`) comparisons. Two-dot resolves both refs to immutable commits;
three-dot resolves their merge base and compares it with the immutable head,
matching Git diff semantics. The artifact records those exact comparison OIDs
and every file's pre/post blob identities. Invalid refs and unavailable merge
matching Git diff semantics. For commit ranges, `diff_ref` records both the
user-requested base/head refs (`requested_base_ref`, `requested_head_ref`) and
the resolved immutable comparison endpoints (`base_ref`, `head_ref`), plus
`comparison_mode`; every file records its pre/post blob identities. Invalid refs and unavailable merge
bases produce structured warnings instead of false changes.

Put pathspecs after `--`. Pathspecs are interpreted relative to the directory
Expand Down
17 changes: 15 additions & 2 deletions diffgraph/schema/diffgraph-v2.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,24 @@
},
"base_ref": {
"type": ["string", "null"],
"description": "Commit SHA or ref for the base side. null for working-tree diffs."
"description": "Resolved comparison-base commit object ID; for three_dot ranges this is the resolved merge base. null for working-tree diffs."
},
"head_ref": {
"type": ["string", "null"],
"description": "Commit SHA or ref for the head side. null for working-tree diffs."
"description": "Immutable commit object ID for the head side. null for working-tree diffs."
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
"requested_base_ref": {
"type": ["string", "null"],
"description": "User-supplied base ref before immutable resolution. null for working-tree diffs."
},
"requested_head_ref": {
"type": ["string", "null"],
"description": "User-supplied head ref before immutable resolution. null for working-tree diffs."
},
"comparison_mode": {
"type": ["string", "null"],
"enum": ["two_dot", "three_dot", null],
"description": "Commit-range semantics. three_dot uses the resolved merge-base in base_ref. null for working-tree diffs."
},
"pathspecs": {
"type": "array",
Expand Down
5 changes: 5 additions & 0 deletions diffgraph/structural.py
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,11 @@ def analyze_local_diff(
resolution.comparison_base_oid if is_commit_range else "HEAD" if staged else None
),
"head_ref": resolution.head_oid if is_commit_range else None,
"requested_base_ref": resolution.base_ref if is_commit_range else None,
"requested_head_ref": resolution.head_ref if is_commit_range else None,
"comparison_mode": (
("three_dot" if three_dot else "two_dot") if is_commit_range else None
),
"pathspecs": list(pathspecs or []), "repo_root": root,
},
"files": files, "symbols": symbols, "relationships": relationships,
Expand Down
22 changes: 22 additions & 0 deletions tests/test_structural.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,22 @@ def test_unstaged_uses_index_to_worktree_exact_identity_and_is_stable(tmp_path):
assert first["symbols"][0]["change_kind"] == "modified"


@pytest.mark.parametrize("staged", [False, True])
def test_worktree_diffs_do_not_record_commit_range_mode(tmp_path, staged):
root = repo(tmp_path)
write(root, "app.py", "def value():\n return 1\n")
commit(root)
write(root, "app.py", "def value():\n return 2\n")
if staged:
git(root, "add", "app.py")

artifact = analyze_local_diff(str(root), staged=staged, three_dot=True)

assert_valid(artifact)
assert artifact["diff_ref"]["kind"] == ("staged" if staged else "unstaged")
assert artifact["diff_ref"]["comparison_mode"] is None


def test_untracked_python_is_an_exact_added_snapshot(tmp_path):
root = repo(tmp_path)
write(root, "tracked.txt", "baseline\n")
Expand Down Expand Up @@ -696,6 +712,9 @@ def test_cli_structural_json_resolves_immutable_two_dot_range(tmp_path, monkeypa
assert artifact["diff_ref"]["kind"] == "commit_range"
assert artifact["diff_ref"]["base_ref"] == base_oid
assert artifact["diff_ref"]["head_ref"] == head_oid
assert artifact["diff_ref"]["requested_base_ref"] == "HEAD~1"
assert artifact["diff_ref"]["requested_head_ref"] == "HEAD"
assert artifact["diff_ref"]["comparison_mode"] == "two_dot"
provenance = json.loads(artifact["files"][0]["evidence"][0]["detail"])
assert provenance["old_oid"] == old_blob
assert provenance["new_oid"] == new_blob
Expand Down Expand Up @@ -734,6 +753,9 @@ def test_cli_structural_json_three_dot_uses_merge_base_and_pathspec(tmp_path, mo
"kind": "commit_range",
"base_ref": merge_base_oid,
"head_ref": head_oid,
"requested_base_ref": "left",
"requested_head_ref": "right",
"comparison_mode": "three_dot",
"pathspecs": ["included.py"],
"repo_root": str(root),
}
Expand Down
Loading