Skip to content

Commit 492957a

Browse files
ptr727claude
andcommitted
Refuse on Two Repositories, Not on Two Filesystem Paths
The fallback added in this PR made every path its own root, so a run naming two files outside any checkout was refused as spanning two repositories. Reproduced: `prose_lint.py README.md docs/guide.md` from a directory under no repository exits 2 here and 0 on develop. That is a regression this PR introduced, not a pre-existing defect. Only a repository declares a workflow model, so only distinct repositories are ambiguous. The refusal now keys on non-empty git roots, which also removes the need to mark non-repository roots in the message, since none appear in it. With no repository in play, the anchor follows discovery's own rule rather than a second one invented here, so the rule set is decided over the tree that will actually be read. Two cases replace the one that asserted the marked-up message: several loose paths are not a conflict, and one repository beside a loose path is not either. Two real repositories still refuse, and the original defect stays fixed, both checked live. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 56d0cf9 commit 492957a

2 files changed

Lines changed: 41 additions & 23 deletions

File tree

‎scripts/prose_lint.py‎

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,20 +1110,24 @@ def main(argv: list[str] | None = None) -> int:
11101110
# Reading it from `.` discarded home-path on a release repository whenever the caller happened to stand in an operational one.
11111111
# That silenced the rule that exists because real paths reached a public comment, announcing it on stderr and exiting 0.
11121112
# A run spanning two repositories refuses instead of picking one, since the two can declare different models and one rule set cannot be right for both.
1113-
# A path git cannot place resolves to itself, so the roots are not all repositories.
1114-
# The refusal below names which is which rather than calling every one of them a repository.
1115-
scan_roots= {}
1116-
forrawin (a.pathsor ['.']):
1117-
found=repo_root(Path(raw))
1118-
scan_roots[foundorstr(Path(raw).resolve())] =bool(found)
1119-
iflen(scan_roots) >1:
1120-
named=', '.join(root+ (''ifis_repoelse' (no git repository)')
1121-
forroot, is_repoinsorted(scan_roots.items()))
1122-
print(f'error: the requested paths resolve to more than one root ({named}). A repository '
1123-
'declares its own workflow model, so no single rule set is correct for all of them. '
1124-
'Run the gate once per root.', file=sys.stderr)
1113+
# Only a repository declares a workflow model, so only distinct repositories are ambiguous.
1114+
# Two loose paths under no repository are not a conflict.
1115+
# Refusing on them would break the ordinary multi-file invocation outside a checkout, where every argument resolves somewhere different.
1116+
scan_paths=a.pathsor ['.']
1117+
git_roots= {foundforfoundin (repo_root(Path(p)) forpinscan_paths) iffound}
1118+
iflen(git_roots) >1:
1119+
print('error: the requested paths span more than one repository ('+
1120+
', '.join(sorted(git_roots)) +'). Each declares its own workflow model, so no '
1121+
'single rule set is correct for all of them. Run the gate once per repository.',
1122+
file=sys.stderr)
11251123
return2
1126-
scan_root=Path(next(iter(scan_roots)))
1124+
# With one repository in play, the model is that repository's own.
1125+
# With none, anchor the way discovery does, so the rule set is decided over the tree that will actually be read.
1126+
ifgit_roots:
1127+
scan_root=Path(next(iter(git_roots)))
1128+
else:
1129+
first=Path(scan_paths[0])
1130+
scan_root=firstiffirst.is_dir() elsePath('.')
11271131

11281132
# An operational repository's runbook carries the literal path an operator types.
11291133
# That is the repository's own content, not an agent quoting an environment it observed.

‎scripts/test_prose_lint.py‎

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1849,23 +1849,37 @@ def test_paths_spanning_two_repositories_refuse_rather_than_pick_one(self) -> No
18491849
withmock.patch.object(prose_lint, 'repo_root', side_effect=lambdap: str(Path(p))):
18501850
self.assertEqual(2, prose_lint.main(
18511851
[str(self.release), str(self.operational), '--check', 'home-path']))
1852-
self.assertIn('more than one root', self.err.getvalue())
1852+
self.assertIn('more than one repository', self.err.getvalue())
18531853

1854-
deftest_the_refusal_does_not_call_a_non_repository_path_a_repository(self) ->None:
1855-
"""`repo_root` returns '' for a path git cannot place, and that root is not a repository.
1854+
deftest_several_paths_under_no_repository_are_not_a_conflict(self) ->None:
1855+
"""Only a repository declares a model, so two loose paths are not ambiguous.
18561856
1857-
Naming it as one sends the reader looking for a workflow model in a directory that
1858-
declares none.
1857+
Refusing on distinct filesystem paths broke the ordinary multi-file invocation outside a
1858+
checkout, where every argument resolves somewhere different.
18591859
"""
18601860
loose=self.tmp/'loose'
1861+
(loose/'docs').mkdir(parents=True)
1862+
one, two=loose/'a.md', loose/'docs'/'b.md'
1863+
fortargetin (one, two):
1864+
target.write_text('Nothing to find here.\n', encoding='utf-8')
1865+
withmock.patch.object(prose_lint, 'repo_root', return_value=''), \
1866+
mock.patch.object(prose_lint, 'discover', return_value=[one, two]):
1867+
self.assertEqual(0, prose_lint.main([str(one), str(two), '--check', 'home-path']))
1868+
self.assertNotIn('more than one repository', self.err.getvalue())
1869+
1870+
deftest_one_repository_plus_a_loose_path_is_not_a_conflict(self) ->None:
1871+
"""One model is in play, so there is nothing to disambiguate."""
1872+
loose=self.tmp/'loose'
18611873
loose.mkdir()
1874+
bait=loose/'notes.md'
1875+
bait.write_text(f'Deploy into {NIX_HOME}/stack here.\n', encoding='utf-8')
18621876
withmock.patch.object(prose_lint, 'repo_root',
1863-
side_effect=lambdap: str(self.release) ifPath(p) ==self.releaseelse''):
1864-
self.assertEqual(2, prose_lint.main(
1877+
side_effect=lambdap: str(self.release) ifPath(p) ==self.releaseelse''), \
1878+
mock.patch.object(prose_lint, 'discover', return_value=[bait]):
1879+
# The one repository in play is a release repo, so home-path stays on and finds it.
1880+
self.assertEqual(1, prose_lint.main(
18651881
[str(self.release), str(loose), '--check', 'home-path']))
1866-
message=self.err.getvalue()
1867-
self.assertIn('no git repository', message)
1868-
self.assertIn(str(loose), message)
1882+
self.assertNotIn('more than one repository', self.err.getvalue())
18691883

18701884
deftest_a_path_under_no_repository_falls_back_to_itself_rather_than_the_caller(self) ->None:
18711885
"""The fallback must not reintroduce the dependency the fix removes.

0 commit comments

Comments
 (0)