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
26 changes: 26 additions & 0 deletions dag/extdeps/git/git.dag
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,17 @@ fn git_observe_meta_shell_fragment() -> String {
// diff-files: each incorporates the index and can therefore report a false difference when the
// index is the stale side of the three-way state being diagnosed.

// WHY `DiffNameOnlyNoRenames` EXISTS BESIDE `DiffNameOnly`, WHICH IS THE ONLY DIFFERENCE A READER
// WILL WANT EXPLAINED. `diff.renames` defaults to true, so a pure rename prints only the
// DESTINATION path: over a `git mv a.txt b.txt`, `git diff --name-only HEAD~1 HEAD` prints `b.txt`
// alone while the same diff with `--no-renames` prints `a.txt` and `b.txt` (measured in a scratch
// repository, 2026-09-03). A caller asking which branches touch a path would therefore be told
// nobody touches it by the branch that is DELETING it -- the answer least safe to be wrong about,
// and the reason `gunbc.path_writer_set` requires the rename-blind reading.
//
// They are two operations rather than one with a flag because they model two different upstream
// behaviours -- git's rename-detecting diff and its path-literal diff -- and a caller must choose
// which question it is asking. `DiffNameOnly` remains correct for the destination-only view.
service git.Core {
operation CurrentBranch {
input {}
Expand Down Expand Up @@ -989,6 +1000,21 @@ service git.Core {
}
}

operation DiffNameOnlyNoRenames {
input { base: GitRef, head: GitRef = "HEAD" }
output {
paths: List<FilePath> from "stdout_lines"
success: Bool from "exit_success"
stderr: String from "stderr"
}
readonly
transport shell { argv: ["git", "diff", "--name-only", "--no-renames", "{base}", "{head}"] }
exit {
0 => Unit
128 => String "Invalid ref or not a git repository"
}
}

operation DiffNameOnlyMerge {
input { base: GitRef, head: GitRef = "HEAD" }
output {
Expand Down
2 changes: 1 addition & 1 deletion dag/extdeps/github/pulls.dag
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ service github.CliPulls {
"--repo", "{repo}",
"--state", "open",
"--limit", "1000",
"--json", "number,title,url,headRefName,headRefOid,isDraft,state,changedFiles",
"--json", "author,number,title,url,headRefName,headRefOid,isDraft,state,changedFiles",
]
}
}
Expand Down
18 changes: 18 additions & 0 deletions dag/gunbc/cross_pr_contradiction.dag
Original file line number Diff line number Diff line change
Expand Up @@ -388,11 +388,24 @@ type PrDiffObservation
| PrDiffEmptyCorroborated { pull_number: Int, head_oid: String }
| PrDiffUnobserved { pull_number: Int, cause: PrDiffUnobservedCause }

// `DiffRefused` IS PRODUCED BY NO CALLER IN THIS MODULE, AND THAT IS DELIBERATE. `git.Core.Diff`
// declares only a `diff` output, so a refused diff and an empty one are the same value on this
// instrument's path and the refusal is folded into `DiffEmptyUnderived`. The arm is produced by
// `gunbc.path_writer_set`, whose observation runs `git.Core.DiffNameOnlyNoRenames` and therefore CAN
// tell those apart.
//
// The arm lives HERE rather than in a second cause coproduct because "why a pull request's diff
// could not be observed" is one vocabulary with one set of consequences -- the observation is
// unread, the population is incomplete, the report may not be mistaken for a clean one -- and
// forking it per instrument would give one meaning two spellings (DESIGN section 3). This module
// gains a total arm it does not construct; that is the cost of one authority, and it is smaller
// than the cost of two.
type PrDiffUnobservedCause
= HeadRefUnresolved { ref_name: String, detail: String }
| HeadRefStale { ref_name: String, expected_oid: String, local_oid: String }
| MergeBaseUnavailable { head_oid: String, detail: String }
| DiffEmptyUnderived { head_oid: String, forge_changed_files: Int }
| DiffRefused { head_oid: String, detail: String }

fn pr_diff_unobserved_cause_text(cause: PrDiffUnobservedCause) -> String {
match cause {
Expand All @@ -408,6 +421,11 @@ fn pr_diff_unobserved_cause_text(cause: PrDiffUnobservedCause) -> String {
)
MergeBaseUnavailable { head_oid: h, detail: d } =>
concat(concat("no merge base against ", h), concat(": ", d))
DiffRefused { head_oid: h, detail: d } =>
concat(
concat("the diff against ", h),
concat(" did not run: ", d)
)
DiffEmptyUnderived { head_oid: h, forge_changed_files: n } =>
concat(
concat("the diff against ", h),
Expand Down
Loading
Loading