Can you use the git commit count as the build number, and what breaks it: shallow clones, squashes, rebases and multiple branches?

25.8.2 · L3 · Interview

Predict first

Commits are counted in a full repository, a depth-1 clone, after unshallowing, and after squashing three commits. What are the counts?

// === run.sh ===
export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@x GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@x GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null
git init -q -b main origin && cd origin
for i in 1 2 3 4 5; do echo $i > f$i; git add .; git commit -qm "c$i"; done
echo "full history count: $(git rev-list --count HEAD)"
cd .. && git clone -q --depth 1 "file://$PWD/origin" shallow && cd shallow
echo "shallow clone: shallow=$(git rev-parse --is-shallow-repository) count=$(git rev-list --count HEAD)"
git fetch -q --unshallow && echo "after unshallow: count=$(git rev-list --count HEAD)"
cd ../origin && git checkout -q -b squashed && git reset -q --soft HEAD~3 && git commit -qm squash
echo "after squashing three commits: count=$(git rev-list --count HEAD)"
  • 5, then 1 (shallow), then 5, then 3 after the squash
  • 5 everywhere
  • 1 after unshallowing

Mental model. Numbering invoices by counting the lines in the filing cabinet: shred some pages or only look at the top drawer and the next invoice gets an old number.

The commit count (git rev-list --count HEAD) is a tempting build number: it needs no server state and increases with every commit. It is only as trustworthy as the history it counts, and the check shows two ways it fails. A CI checkout with --depth 1 (the default for many providers, to save time) counts 1 where the full history counts 5, and after git fetch --unshallow it is 5 again, so an unconfigured runner would submit build 1 and be rejected, or worse, accepted for a new version. And the count is not monotonic: squashing three commits made it fall from 5 to 3, and rebasing, force pushes or counting from a different branch do the same. App Store Connect needs uploads to be unique and increasing per version (Q481), so a number that can fall is a latent rejection.

Safer options, in rising independence from git: the CI run number (monotonic per workflow, but a different workflow or provider resets it), a timestamp such as YYYYMMDDHHMM (monotonic if clocks are right, fits as dotted integers), or a counter you own (a small service or the highest build App Store Connect reports plus one, fetched through the API, Q480). Whatever you choose: compute it in one place, once per pipeline run, write it to the build settings (CURRENT_PROJECT_VERSION) instead of editing the project file, tag the commit with version and build, and fetch full history (fetch-depth: 0 in GitHub Actions, --unshallow otherwise) wherever you count commits or run git describe. Only local repositories were used in the check; provider-specific defaults should be read from your CI’s documentation.

The count depends on the history you have and can fall when history is rewritten.

Common traps

  • Counting commits on a shallow clone. The number is tiny and unrelated to the real history.
  • Assuming a squash-merge keeps the count rising. Collapsing commits lowers the count of the merged branch.

They might follow up with

  • Does git describe have the same problem? Yes: without tags and history it fails or reports a short distance.
  • Can two branches share a build number? Not safely; the number must come from a single authority, not from each branch’s history.

Related questions

  • 25.2.2 How do marketing versions and build numbers work, how should CI generate them, and which rules does App Store…
  • 25.2.3 How do you design a release pipeline: hermetic builds, secrets, caching, artefact promotion and the order of…
  • 25.2.1 What are the main ways to run iOS CI/CD (Xcode Cloud, GitHub Actions, Fastlane, others), and what do they all…
  • 25.8.3 How do tags, release branches and cherry-picks work together for a hotfix, and how do you check that a fix…

← Why can a CI job pass when the build failed, and how do pipes…   How do tags, release branches and cherry-picks work together for a… →