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
61 changes: 53 additions & 8 deletions gitgalaxy/metrics/chronometer.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,43 @@ def _initialize_history_scan(self):
"""Dispatches the survey engines to establish boundaries and churn cache."""
t_start = time.time()

# Step A: Git Binary Verification
if (self.root / ".git").exists():
# Step A: Git Worktree Verification
# #2976: `.git` only exists at the repository ROOT, so the old check
# `(self.root / ".git").exists()` silently disabled the entire temporal
# pipeline for any scan rooted at a SUBDIRECTORY of a repo (galaxyscope
# path/to/repo/src, or a control-corpus language folder) -- the mtime
# fallback then fired, a fresh checkout's uniform mtimes tripped the
# TEMPORAL COLLAPSE guard below, and every file read the neutral
# (stability 50, churn 0). Ask git itself: rev-parse resolves upward
# from any subdirectory, and a linked worktree's `.git` FILE pointer
# passes the same way. A non-git directory still lands in the OS-walk
# fallback exactly as before.
#
# Determinism-sensitive harnesses (the golden-crucible pins scan a
# subdirectory of a pinned checkout whose git history is NOT part of
# the measured structure) opt out EXPLICITLY here -- before the fix
# they got temporal neutrality by accident of the broken root check.
if (
self.chrono_config.get("DISABLE_GIT_HISTORY", False)
or os.environ.get("GITGALAXY_DISABLE_GIT_HISTORY", "") == "1"
):
# is_git_enabled stays False: Steps B/C below run the same OS-walk
# fallback (including the collapse guard) a non-git directory gets.
self.logger.info("Git history disabled by configuration. Using OS-walk fallback.")
else:
try:
subprocess.run([_GIT_BIN, "--version"], capture_output=True, check=True) # noqa: S603
self.is_git_enabled = True
self.logger.debug("Git binary verified. Commencing Deep Boundary Survey.")
except (subprocess.CalledProcessError, FileNotFoundError):
self.logger.warning("Git binary not found. Falling back to OS Walk.")
res = subprocess.run( # noqa: S603 -- _GIT_BIN resolved absolute, fixed args
[_GIT_BIN, "rev-parse", "--is-inside-work-tree"],
cwd=self.root,
capture_output=True,
text=True,
check=True,
)
if res.stdout.strip() == "true":
self.is_git_enabled = True
self.logger.debug("Git worktree verified. Commencing Deep Boundary Survey.")
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
self.logger.warning("Not inside a git worktree (or git unavailable). Falling back to OS Walk.")

# Step B: Establish Absolute Project Boundaries (Min/Max Time)
self._determine_commit_bounds()
Expand Down Expand Up @@ -268,7 +297,12 @@ def _scan_git_history(self):
# Axis 1 (Volume): Stop scanning once 50% of active files are mapped (max 5000).
# Axis 2 (Time): Hard abort after 'timeout_limit' seconds.
# ======================================================================
required_files = min(int(total_files * 0.50), 5000)
# #2976: a tiny subtree's year-log parses in milliseconds -- the 50% early
# exit only ever protected monorepo scans, and on a 6-file control-corpus
# folder it stopped after mapping 3 files, leaving the rest with no history
# at all. Full coverage at or below 200 files; the monorepo math is
# unchanged above that, and the Axis-2 time budget still applies to both.
required_files = total_files if total_files <= 200 else min(int(total_files * 0.50), 5000)
timeout_limit = self.chrono_config.get("STREAM_TIMEOUT_SECONDS", 15.0)

self.logger.info(f"Chronometer: Engaging 1-Year Historical Sweep. Budget: {timeout_limit}s")
Expand All @@ -277,13 +311,24 @@ def _scan_git_history(self):

# 3. The Command: Limit Git to the last year of commits.
# This generates massive churn spikes without getting bogged down in decade-old bedrock.
# #2976: `--relative -- .` scopes the stream to the scanned subtree and names
# files relative to cwd. Without it, a subdirectory scan streams the WHOLE
# repo's log (burning the time budget on unrelated files) and `--name-only`
# emits repo-relative paths while `git ls-files` above emits cwd-relative
# ones -- so churn_map/mtime_map would be keyed differently from both the
# tracked-files denominator and the scanner's scan-root-relative rel_path,
# and every lookup would miss. At the repository root both flags are no-ops:
# the output is byte-identical to the previous command.
cmd = [
_GIT_BIN,
"log",
"--since=1.year",
"--name-only",
"--relative",
"--pretty=format:@@GIT_COMMIT@@|%H|%at|%an",
"--no-merges",
"--",
".",
]

# Execute the stream
Expand Down
6 changes: 6 additions & 0 deletions gitgalaxy/standards/gitgalaxy_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,12 @@
# Consumed by: chronometer.py
# ------------------------------------------------------------------------------
CHRONOMETER_CONFIG = {
# #2976: force the OS-walk fallback even inside a git worktree. For
# determinism-sensitive harnesses (the golden-crucible pins) whose
# corpus git history is not part of the measured structure. The
# GITGALAXY_DISABLE_GIT_HISTORY=1 environment variable is the same
# switch for subprocess invocations.
"DISABLE_GIT_HISTORY": False,
# The absolute ceiling for OS-level fallback scanning
"FALLBACK_SCAN_LIMIT": 25000,
# Process management
Expand Down
9 changes: 4 additions & 5 deletions tests/core_engine/test_chronometer.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,13 @@ def test_chronometer_no_git_fallback(mock_getmtime, mock_walk, mock_run, tmp_pat
def test_chronometer_git_boundaries(mock_run, tmp_path):
"""Proves the boundary scanner correctly extracts min/max times from git logs."""

# Simulate the .git directory existing so the hardware check fires
(tmp_path / ".git").mkdir()

def git_side_effect(cmd, **kwargs):
m = MagicMock()
m.stdout = ""
if "--version" in cmd:
m.stdout = "git version 2.40.0\n"
if "rev-parse" in cmd:
# #2976: the gate asks git itself rather than testing (root/.git).exists(),
# so a subdirectory of a repo (and a linked worktree) passes too.
m.stdout = "true\n"
elif "log" in cmd and "-1" in cmd and "HEAD" not in cmd and len(cmd) == 4:
m.stdout = "5000\n" # Max Time
elif "rev-list" in cmd:
Expand Down
1 change: 1 addition & 0 deletions tests/dead_key_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
# --- External YAML/env config ---
"galaxyscope": "top-level section name in a user's .galaxyscope.yml project config file",
"GITGALAXY_LICENSE_KEY": "environment variable (os.environ.get), not a repo-produced dict",
"GITGALAXY_DISABLE_GIT_HISTORY": "environment variable (os.environ.get), not a repo-produced dict (#2976)",
"vulnerability_density_min": "optional risk_tuning YAML key (signal_processor.py risk-equation-style tuning)",
"asymptotic_dampener": "optional risk_tuning YAML key (signal_processor.py)",
"quarantine": "STATIC_ARCHETYPES app-config constant, read with a graceful string fallback",
Expand Down
10 changes: 9 additions & 1 deletion tests/test_golden_crucible.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,15 @@ def test_golden_crucible_matches_baseline(tmp_path):
],
check=True,
timeout=180,
env={**os.environ, "GITGALAXY_LICENSE_KEY": "COMMUNITY_FREE_TIER"},
env={
**os.environ,
"GITGALAXY_LICENSE_KEY": "COMMUNITY_FREE_TIER",
# #2976: the golden masters pin a corpus whose git history is not part
# of the measured structure. Before the chronometer's subdir fix they
# got temporal neutrality by accident (the scan root data/ has no .git);
# this makes the same neutrality explicit and deterministic.
"GITGALAXY_DISABLE_GIT_HISTORY": "1",
},
)

actual_path = output_dir / "data_galaxy_audit.json"
Expand Down
Loading