Skip to content

Commit 21d6912

Browse files
committed
Detect changed files across the whole base..HEAD range
Changed-file detection reads a full comparison range only when it recognizes the CI environment: a GitHub pull request, a GitLab merge request, a Bitbucket pull request, or a Buildkite pull request. Every other run falls through to `git show HEAD`, which sees the tip commit alone. That makes dependency gating depend on commit ordering. A pull request whose manifest changed in an earlier commit, followed by a source-only commit, looks like a source-only change: the supported-manifest check fails, the comparison is abandoned for a full scan, and blocking is suppressed, so the run reports no new issues and exits 0. A caller that supplies a base commit has stated the range outright, so honor it ahead of any inference from the environment, reusing the same range detection the recognized providers already use. An unresolvable base commit warns rather than degrading quietly, because the fallback silently narrows the comparison to one commit.
1 parent 7c95310 commit 21d6912

3 files changed

Lines changed: 160 additions & 33 deletions

File tree

‎socketsecurity/core/git_interface.py‎

Lines changed: 64 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,21 @@
1111
class Git:
1212
repo: Repo
1313
path: str
14+
base_commit_sha: str | None
1415

15-
def __init__(self, path: str):
16+
def __init__(self, path: str, base_commit_sha: str | None = None):
17+
"""
18+
Reads the repository state a scan is built from.
19+
20+
Args:
21+
path: Path to the repository working tree
22+
base_commit_sha: Commit the comparison should start from. Supplied when
23+
the caller knows the range and no CI environment describes it, which
24+
is the only way changed-file detection can see commits behind HEAD.
25+
"""
1626
initialization_start = time.perf_counter()
1727
self.path = path
28+
self.base_commit_sha = base_commit_sha
1829
self._fetched_ref_commits = {}
1930
self.ensure_safe_directory(path)
2031
self.repo = Repo(path)
@@ -164,40 +175,61 @@ def __init__(self, path: str):
164175
buildkite_pr = os.getenv('BUILDKITE_PULL_REQUEST')
165176
buildkite_base_ref = os.getenv('BUILDKITE_PULL_REQUEST_BASE_BRANCH')
166177
buildkite_head_ref = os.getenv('BUILDKITE_BRANCH')
167-
if self._is_buildkite_pull_request(buildkite_pr) and buildkite_base_ref:
168-
detected = self._detect_pull_request_changes(
169-
provider="Buildkite",
170-
base_ref=buildkite_base_ref,
171-
head_ref=buildkite_head_ref,
172-
)
173-
if detected:
174-
detection_source = "buildkite-pr"
175-
elif github_event_name == 'pull_request' and github_base_ref:
178+
179+
# An explicitly supplied base commit states the comparison range outright,
180+
# so it is honored before any inference from CI environment variables.
181+
if self.base_commit_sha:
176182
detected = self._detect_pull_request_changes(
177-
provider="GitHub",
178-
base_ref=github_base_ref,
179-
head_ref=github_head_ref,
183+
provider="explicit base commit",
184+
base_ref=self.base_commit_sha,
185+
head_ref=None,
180186
)
181187
if detected:
182-
detection_source = "github-pr"
183-
# Commits to default branch (push events)
184-
elif github_event_name == 'push' and github_before_sha and github_sha:
185-
try:
186-
diff_files = self.repo.git.diff('--name-only', f'{github_before_sha}..{github_sha}')
187-
self.show_files = diff_files.splitlines()
188-
log.debug(f"Changed files detected via git diff (GitHub push): {self.show_files}")
189-
detected = True
190-
detection_source = "github-push"
191-
except Exception as error:
192-
log.debug(f"Failed to get changed files via git diff (GitHub push): {error}")
193-
elif github_event_name == 'push':
194-
try:
195-
self.show_files = self.repo.git.show(self.commit, name_only=True, format="%n").splitlines()
196-
log.debug(f"Changed files detected via git show (GitHub push fallback): {self.show_files}")
197-
detected = True
198-
detection_source = "github-push-fallback"
199-
except Exception as error:
200-
log.debug(f"Failed to get changed files via git show (GitHub push fallback): {error}")
188+
detection_source = "explicit-base-commit"
189+
else:
190+
log.warning(
191+
f"Could not resolve base commit {self.base_commit_sha} in this "
192+
"checkout, so changed-file detection falls back to the current "
193+
"commit alone. A manifest changed earlier in the range will not "
194+
"be seen, which can skip the comparison entirely. Deepen the "
195+
"clone or fetch the base commit to compare the full range."
196+
)
197+
198+
if not detected:
199+
if self._is_buildkite_pull_request(buildkite_pr) and buildkite_base_ref:
200+
detected = self._detect_pull_request_changes(
201+
provider="Buildkite",
202+
base_ref=buildkite_base_ref,
203+
head_ref=buildkite_head_ref,
204+
)
205+
if detected:
206+
detection_source = "buildkite-pr"
207+
elif github_event_name == 'pull_request' and github_base_ref:
208+
detected = self._detect_pull_request_changes(
209+
provider="GitHub",
210+
base_ref=github_base_ref,
211+
head_ref=github_head_ref,
212+
)
213+
if detected:
214+
detection_source = "github-pr"
215+
# Commits to default branch (push events)
216+
elif github_event_name == 'push' and github_before_sha and github_sha:
217+
try:
218+
diff_files = self.repo.git.diff('--name-only', f'{github_before_sha}..{github_sha}')
219+
self.show_files = diff_files.splitlines()
220+
log.debug(f"Changed files detected via git diff (GitHub push): {self.show_files}")
221+
detected = True
222+
detection_source = "github-push"
223+
except Exception as error:
224+
log.debug(f"Failed to get changed files via git diff (GitHub push): {error}")
225+
elif github_event_name == 'push':
226+
try:
227+
self.show_files = self.repo.git.show(self.commit, name_only=True, format="%n").splitlines()
228+
log.debug(f"Changed files detected via git show (GitHub push fallback): {self.show_files}")
229+
detected = True
230+
detection_source = "github-push-fallback"
231+
except Exception as error:
232+
log.debug(f"Failed to get changed files via git show (GitHub push fallback): {error}")
201233
# GitLab CI Merge Request context
202234
if not detected:
203235
gitlab_target = os.getenv('CI_MERGE_REQUEST_TARGET_BRANCH_NAME')

‎socketsecurity/socketcli.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ def main_code():
405405
is_repo = False
406406
git_repo: Git
407407
try:
408-
git_repo = Git(config.target_path)
408+
git_repo = Git(config.target_path, base_commit_sha=config.base_commit_sha)
409409
is_repo = True
410410
if not config.repo:
411411
config.repo = git_repo.repo_name

‎tests/unit/test_git_interface.py‎

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,3 +249,98 @@ def test_targeted_fetch_never_uses_all():
249249
)
250250
def test_buildkite_pull_request_detection(value, expected):
251251
assert Git._is_buildkite_pull_request(value) is expected
252+
253+
254+
@pytest.fixture
255+
def commit_range_repo(tmp_path):
256+
"""A manifest changes mid-range, then a source-only commit lands on top of it."""
257+
path = tmp_path / "range-repo"
258+
path.mkdir()
259+
_git(path, "init", "-b", "main")
260+
_git(path, "config", "user.name", "Socket Test")
261+
_git(path, "config", "user.email", "socket@example.com")
262+
(path / "README.md").write_text("base\n", encoding="utf-8")
263+
_git(path, "add", "README.md")
264+
_git(path, "commit", "-m", "base")
265+
base_sha = _git(path, "rev-parse", "HEAD")
266+
267+
_git(path, "checkout", "-b", "feature")
268+
(path / "pom.xml").write_text("<project/>\n", encoding="utf-8")
269+
_git(path, "add", "pom.xml")
270+
_git(path, "commit", "-m", "add dependency")
271+
manifest_sha = _git(path, "rev-parse", "HEAD")
272+
273+
(path / "App.java").write_text("class App {}\n", encoding="utf-8")
274+
_git(path, "add", "App.java")
275+
_git(path, "commit", "-m", "source only")
276+
return SimpleNamespace(path=path, base_sha=base_sha, manifest_sha=manifest_sha)
277+
278+
279+
def test_head_commit_alone_misses_a_manifest_changed_earlier_in_the_range(
280+
commit_range_repo, mocker,
281+
):
282+
mocker.patch.object(Git, "ensure_safe_directory")
283+
284+
repository = Git(str(commit_range_repo.path))
285+
286+
# Without a stated base the range is unknown, so only the tip commit is read.
287+
assert repository.changed_files == ["App.java"]
288+
289+
290+
def test_explicit_base_commit_covers_the_whole_range(
291+
commit_range_repo, mocker, caplog,
292+
):
293+
mocker.patch.object(Git, "ensure_safe_directory")
294+
295+
with caplog.at_level(logging.INFO, logger="socketdev"):
296+
repository = Git(
297+
str(commit_range_repo.path),
298+
base_commit_sha=commit_range_repo.base_sha,
299+
)
300+
301+
assert sorted(repository.changed_files) == ["App.java", "pom.xml"]
302+
assert any(
303+
"source=explicit-base-commit" in record.message
304+
for record in caplog.records
305+
)
306+
307+
308+
def test_explicit_base_commit_takes_precedence_over_ci_environment(
309+
commit_range_repo, monkeypatch, mocker, caplog,
310+
):
311+
# The CI variables describe the whole branch; the explicit base describes only
312+
# the last commit. They disagree, so the winner is unambiguous in the result.
313+
monkeypatch.setenv("GITHUB_EVENT_NAME", "pull_request")
314+
monkeypatch.setenv("GITHUB_BASE_REF", "main")
315+
monkeypatch.setenv("GITHUB_HEAD_REF", "feature")
316+
mocker.patch.object(Git, "ensure_safe_directory")
317+
318+
with caplog.at_level(logging.INFO, logger="socketdev"):
319+
repository = Git(
320+
str(commit_range_repo.path),
321+
base_commit_sha=commit_range_repo.manifest_sha,
322+
)
323+
324+
assert repository.changed_files == ["App.java"]
325+
assert any(
326+
"source=explicit-base-commit" in record.message
327+
for record in caplog.records
328+
)
329+
330+
331+
def test_unresolvable_base_commit_warns_and_falls_back(
332+
commit_range_repo, mocker, caplog,
333+
):
334+
mocker.patch.object(Git, "ensure_safe_directory")
335+
fetch = mocker.patch.object(Git, "_fetch_ref", return_value=None)
336+
337+
with caplog.at_level(logging.WARNING, logger="socketdev"):
338+
repository = Git(str(commit_range_repo.path), base_commit_sha="0" * 40)
339+
340+
# Falling back silently would hide that the comparison lost most of its range.
341+
assert repository.changed_files == ["App.java"]
342+
assert any(
343+
"Could not resolve base commit" in record.message
344+
for record in caplog.records
345+
)
346+
fetch.assert_called_once()

0 commit comments

Comments
 (0)