Skip to content

Commit e6f42cc

Browse files
milesjclaude
andcommitted
fix: Exclude local index from changed files when an explicit head is provided.
`moon query changed-files` (and affected detection) always merged `git status` into the results, even when both `--base` and `--head` were passed as explicit revisions. Local uncommitted changes and untracked files showed up as false positives when walking history commit-by-commit (issue #2511, remaining half after the v2.4 rework fixed the dropped `--head`). The local index is now only consulted when no explicit head is provided, i.e. when the comparison targets the working tree. Head-only queries previously ignored the head in local mode (returning local status only); they now diff merge-base(defaultBranch, head)..head. Additionally, `MOON_BASE`/`MOON_HEAD` values that are set but empty are treated as not provided, as CI templates commonly pass these through unconditionally. Closes #2511 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 509caa8 commit e6f42cc

4 files changed

Lines changed: 125 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@
3535
- Fixed tasks that emit read-only outputs (e.g. `0444` files) failing on every cache hit with a
3636
"Permission denied" error when the `casOutputsCache` experiment is enabled. Caches that already
3737
contain read-only objects are healed automatically.
38+
- Fixed an issue where `moon query changed-files` would include uncommitted changes from the local
39+
index (`git status`) even when an explicit `--head` revision was provided, causing false positives
40+
when comparing 2 revisions. This also applies to affected detection with an explicit head, e.g the
41+
`MOON_HEAD` environment variable or `--affected base:head`. Additionally, `MOON_BASE` and
42+
`MOON_HEAD` environment variables that are set but empty are now ignored.
3843

3944
## 2.4.2
4045

crates/app/src/queries/changed_files.rs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,17 @@ async fn query_changed_files_without_stdin(
8787
let bag = GlobalEnvBag::instance();
8888
let default_branch = vcs.get_default_branch().await?;
8989
let current_branch = vcs.get_local_branch().await?;
90-
let base_value = bag.get("MOON_BASE").or(options.base.clone());
90+
// Treat empty values as not provided, as CI templates typically
91+
// pass these environment variables through unconditionally
92+
let base_value = bag
93+
.get("MOON_BASE")
94+
.or(options.base.clone())
95+
.filter(|value| !value.is_empty());
9196
let base = base_value.as_deref().unwrap_or(&default_branch);
92-
let head_value = bag.get("MOON_HEAD").or(options.head.clone());
97+
let head_value = bag
98+
.get("MOON_HEAD")
99+
.or(options.head.clone())
100+
.filter(|value| !value.is_empty());
93101
let head = head_value.as_deref().unwrap_or("HEAD");
94102

95103
// Determine whether we should check against the previous
@@ -110,7 +118,7 @@ async fn query_changed_files_without_stdin(
110118
);
111119
}
112120

113-
let only_local = options.local && base_value.is_none();
121+
let only_local = options.local && base_value.is_none() && head_value.is_none();
114122
let mut changed_files_map = ChangedFiles::default();
115123
let mut changed_files = FxHashSet::default();
116124

@@ -138,10 +146,14 @@ async fn query_changed_files_without_stdin(
138146
}
139147
}
140148

141-
// Always include local changes
142-
debug!("Against local index");
149+
// Only include local changes when the head is the working tree;
150+
// an explicit head requests a comparison between 2 revisions,
151+
// of which the local index is not a part of
152+
if head_value.is_none() {
153+
debug!("Against local index");
143154

144-
changed_files_map.merge(vcs.get_changed_files().await?);
155+
changed_files_map.merge(vcs.get_changed_files().await?);
156+
}
145157

146158
if options.status.is_empty() {
147159
debug!(

crates/cli/tests/query_changed_files_test.rs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,106 @@ mod query_changed_files {
4848
assert_eq!(json.options.status, vec![ChangedStatus::Deleted]);
4949
}
5050

51+
#[test]
52+
fn excludes_local_index_with_explicit_head() {
53+
let sandbox = create_query_sandbox();
54+
55+
sandbox.create_file("basic/committed.txt", "contents");
56+
57+
sandbox.run_git(|cmd| {
58+
cmd.args(["add", "--all", "."]);
59+
});
60+
61+
sandbox.run_git(|cmd| {
62+
cmd.args(["commit", "-m", "Commit"]);
63+
});
64+
65+
// Should not appear in the revision to revision diff
66+
sandbox.create_file("basic/dirty.txt", "contents");
67+
68+
let assert = sandbox.run_bin(|cmd| {
69+
cmd.arg("query")
70+
.arg("changed-files")
71+
.args(["--base", "HEAD~1", "--head", "HEAD"]);
72+
});
73+
74+
let json: QueryChangedFilesResult = serde_json::from_str(assert.stdout().trim()).unwrap();
75+
76+
assert_eq!(
77+
json.files.into_iter().collect::<Vec<_>>(),
78+
["basic/committed.txt"]
79+
);
80+
}
81+
82+
#[test]
83+
fn includes_local_index_without_explicit_head() {
84+
let sandbox = create_query_sandbox();
85+
86+
sandbox.create_file("basic/committed.txt", "contents");
87+
88+
sandbox.run_git(|cmd| {
89+
cmd.args(["add", "--all", "."]);
90+
});
91+
92+
sandbox.run_git(|cmd| {
93+
cmd.args(["commit", "-m", "Commit"]);
94+
});
95+
96+
sandbox.create_file("basic/dirty.txt", "contents");
97+
98+
let assert = sandbox.run_bin(|cmd| {
99+
cmd.arg("query")
100+
.arg("changed-files")
101+
.args(["--base", "HEAD~1"]);
102+
});
103+
104+
let json: QueryChangedFilesResult = serde_json::from_str(assert.stdout().trim()).unwrap();
105+
106+
let mut files = json
107+
.files
108+
.into_iter()
109+
.filter(|file| !file.as_str().starts_with(".moon"))
110+
.collect::<Vec<_>>();
111+
files.sort();
112+
113+
assert_eq!(files, ["basic/committed.txt", "basic/dirty.txt"]);
114+
}
115+
116+
#[test]
117+
fn treats_empty_head_as_working_tree() {
118+
let sandbox = create_query_sandbox();
119+
120+
sandbox.create_file("basic/committed.txt", "contents");
121+
122+
sandbox.run_git(|cmd| {
123+
cmd.args(["add", "--all", "."]);
124+
});
125+
126+
sandbox.run_git(|cmd| {
127+
cmd.args(["commit", "-m", "Commit"]);
128+
});
129+
130+
sandbox.create_file("basic/dirty.txt", "contents");
131+
132+
let assert = sandbox.run_bin(|cmd| {
133+
cmd.arg("query")
134+
.arg("changed-files")
135+
.args(["--base", "HEAD~1"])
136+
.env("MOON_HEAD", "");
137+
});
138+
139+
let json: QueryChangedFilesResult = serde_json::from_str(assert.stdout().trim()).unwrap();
140+
141+
let mut files = json
142+
.files
143+
.into_iter()
144+
.filter(|file| !file.as_str().starts_with(".moon"))
145+
.collect::<Vec<_>>();
146+
files.sort();
147+
148+
assert_eq!(files, ["basic/committed.txt", "basic/dirty.txt"]);
149+
}
150+
51151
#[test]
52152
fn can_supply_multi_status() {
53153
let sandbox = create_query_sandbox();

website/docs/commands/query/changed-files.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ Changed files are determined using the following logic:
1515
uses.
1616
- If `--local` is provided, changed files are based on your local index only (`git status`).
1717
- Otherwise, then compare the defined base (`--base`) against head (`--head`).
18+
- Additionally, unless an explicit `--head` revision is provided, changed files in your local index
19+
(`git status`) are also included.
1820

1921
```shell
2022
# Return all files

0 commit comments

Comments
 (0)