git rev-list --count and --left-right in CI
git rev-list --count <range> prints how many commits are in a range, and git rev-list --left-right --count A...B prints the ahead/behind counts between two branches.
When a pipeline needs to know "how many commits since the tag" or "is this branch behind main", rev-list is the plumbing that gives an exact number. The symmetric-difference form is how you compute ahead/behind.
What it does
git rev-list lists commit SHAs in a range. With --count it prints just the total. The triple-dot symmetric difference A...B with --left-right --count yields two numbers: commits in A not in B, and in B not in A, which is the ahead/behind relationship.
Common usage
# commits since the last tag
git rev-list --count v1.4.0..HEAD
# total commits on the branch
git rev-list --count HEAD
# ahead/behind vs origin/main (prints: <ahead> <behind>)
git rev-list --left-right --count origin/main...HEAD
# count excluding merges
git rev-list --count --no-merges main..featureOptions
| Flag | What it does |
|---|---|
| --count | Print the number of commits instead of listing them |
| --left-right | Mark/count each side of a symmetric difference (A...B) |
| --no-merges | Exclude merge commits |
| --first-parent | Follow only the first parent of merges |
| --since=<date> / --until=<date> | Bound by commit date |
| --all | Consider all refs as starting points |
In CI
Ahead/behind gates need both refs present; a shallow clone may not have the merge base, giving wrong counts, so --unshallow or fetch enough depth first. Remember A...B (three dots) is symmetric difference for --left-right, while A..B (two dots) is the one-sided range used with plain --count.
Common errors in CI
"fatal: bad revision 'v1.4.0..HEAD'" means v1.4.0 is unknown (shallow clone or missing tag). On a shallow clone, counts can be capped at the shallow boundary and silently wrong, no error, just a smaller number than reality. "fatal: ambiguous argument" means a ref name does not resolve; check it with git rev-parse --verify.
Using this in CI
CI checkouts are shallow and detached by default, which changes the answer this command gives you. Commands that read history, branch names, or tags need the checkout configured for it.
- uses: actions/checkout@v4
with:
fetch-depth: 0 # history, tags, and git describe all need this
- run: |
git rev-parse --is-shallow-repository # expect false
git rev-parse --abbrev-ref HEAD # prints HEAD when detached