Skip to content

audit: squash+fast-forward repos report zero merges and zero deployments #175

Description

@AlexanderMakarov

Summary

On repositories that merge with GitLab's squash + fast-forward strategy, the audit reports merge frequency (DF-01) and merges-per-contributor as a confident 0 even though every merge request is visible to the engine through the already-fetched code-host connector. Reported from an org-mode audit over 13 GitLab repos: 12 of 13 showed zero merges and zero deployments.

Two separate defects combine to produce this.

Defect A — GitLab squash commits are not recognized as merge events

collectors/git.ts counts a merge event as either a two-parent merge commit on the first-parent trunk, or a squash-merged commit identified by a forge reference in the subject or body:

const SQUASH_SUBJECT_RXS = [
  /\(#\d+\)\s*$/,            // GitHub:       "Title (#123)"
  /^Merged PR \d+:/,         // Azure DevOps: "Merged PR 123: Title"
  /\(pull request #\d+\)/i,  // Bitbucket:    "Title (pull request #12)"
];
const SQUASH_BODY_RX = /^See merge request [^\s!]*!\d+/m;

See merge request group/project!45 comes from GitLab's merge commit template. The squash commit template defaults to the merge-request title alone and carries no !NN reference. So on a project configured for squash + fast-forward there is no merge commit and no reference anywhere in the message: the trunk is a flat run of ordinary commits and the engine detects zero merge events.

Defect B — the "unknown" merge strategy publishes a number instead of skipping

classifyMergeStrategy() returns 'unknown' when it finds neither merge commits nor squash events:

if (mergeCommits === 0 && squashMerges === 0) return 'unknown';

Four metrics already refuse to report a value on a squash workflow, precisely so a squash repo is not mis-measured — metrics/lead_time_for_change.ts:116, metrics/pr_cycle_time.ts:204, metrics/mttr.ts:145, metrics/review_rework.ts:103:

if (raw.window_stats?.merge_strategy === 'squash') { /* report unavailable */ }

That guard keys on 'squash' and therefore never fires in exactly the case it was written for: when detection fails completely the strategy is 'unknown', not 'squash'.

Meanwhile metrics/merge_frequency.ts (DF-01) and the display value window_stats.merges_per_active have no guard at all and publish 0. metrics/change_failure_rate.ts and metrics/rework_rate.ts divide by window_stats.merges and SKIP outright at zero, so they are silently unmeasured on every squash + fast-forward repo too.

Evidence

collected/git.json from one affected repo (90-day window):

field value
raw.trunk.ref origin/master (source: origin-head, matches default_branch)
period.history_available_days 3066
window_startwindow_anchor 2026-05-09 → 2026-08-07
window_stats.trunk_commits 14
window_stats.merge_commits 0
window_stats.squash_merges 0
window_stats.merges 0
window_stats.merge_strategy "unknown"
window_stats.merges_per_active 0
raw.merge_records 421 records, newest 2020-05-22

Fourteen merge requests landed on the trunk inside the window and none were counted. The repo stopped producing merge commits in 2020, when it moved to squash. Trunk resolution, clone depth, and the window are all correct, so neither a wrong default branch nor the 90-day lookback explains the zeros.

The same audit reported GitLab MRs via glab (13/13) under Connections & Sources — collected/code_host.json was present for every repo, holding the merged-MR records with timestamps.

Proposed fix

  1. Source merge counts from the code host. code_host.json already carries every merged PR/MR with merged_at, and is fetched whenever gh/glab is reachable. Wire it into metrics/merge_frequency.ts, window_stats.merges_per_active, metrics/change_failure_rate.ts and metrics/rework_rate.ts as the source when window_stats.merges === 0 and code_host.available is true. This removes the dependency on commit-message shape entirely and is the durable fix.
  2. Extend the 'squash' guard to 'unknown' in the four metrics that already have it, so an undetectable workflow reports unavailable rather than a value derived from residue.
  3. Never render an unmeasured merge count as 0. With an available code host and zero detected merge events, the honest output is "not measurable from this repo's history", not a confident zero — a zero reads as a delivery finding about the team.
  4. Optionally broaden GitLab squash detection to an !NN merge-request reference anywhere in the subject or body, for projects whose squash template does include it.

Interim workaround for users

Setting the project's squash commit message template (GitLab → Settings → Merge requests) to include See merge request %{reference} makes new squashes detectable by the current engine. It does not fix history already merged.

Also worth fixing nearby

reason_if_absent is never rendered in org-mode reports — render.ts:1932 prints it only in single-repo mode, so an org report that references the field gives the reader no way to reach it. The per-repo reasons live in per-repo/<repo>/audit.json.

Extra AC: notify into source Slack thread about the fix once merged.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions