A script should not have to “understand” a pretty Git log. The moment it starts trimming graph lines, guessing where a subject ends, or assuming every identifier has 40 characters, the automation has inherited a small future incident.
For machines, ask the plumbing command git rev-list for one commit object ID per line. Then define the revision set and order with the same care you would give an API contract. The output should be wonderfully boring.
One object ID per line, earliest selected commit first
git rev-list --reverse HEADa04c6e0a7a1c1ecffa04bccbe6deaaad1e56ef15
e0b4a4e6f18a54f609f4cee1c5414dcfb3b4fec0
d087ec455114bbdcf17b601bbc8be2fbeb01299e
…Why rev-list is the clean interface
git rev-listperforms the same revision walk underlying many porcelain commands but emits commit object IDs without subjects, dates, decoration, color, or graph lanes.HEADis the starting revision; only commits reachable through its parent links are selected.--reversereverses the selected output so ancestors normally appear before descendants.Newline separation is safe for object IDs because their hexadecimal representation cannot contain a newline.
The first three identifiers above were produced in this repository with Git 2.43.0; the ellipsis marks omitted output and is not an ID.
Commit hash is familiar; object ID is future-proof
git rev-parse --show-object-formatsha1Do not teach a parser that every ID is 40 characters
Traditional SHA-1 repositories display 40 hexadecimal characters for a full ID.
SHA-256 repositories use 64 hexadecimal characters. Git’s transition design also supports compatibility forms in appropriate repositories and versions.
--show-object-formatreports the repository’s storage format; newer Git variants can also distinguish input and output formats.Treat the ID as an opaque line returned by Git. Validate it with Git rather than a length-only regular expression.
This repository reported
sha1under Git 2.43.0; that result describes this checkout, not every reader’s repository.
Name the set before choosing the order
Most automation bugs blamed on ordering are actually selection bugs. --reverse cannot add a missing branch, deepen a shallow clone, or make the left endpoint of a range inclusive.
git rev-list --reverse --topo-order v2.3.0..v2.4.04ad930ff0c1e…
729cb08c12aa…
f48e31d92a70…The left endpoint is subtraction
A..Bis shorthand for the commits reachable fromBminus every commit reachable fromA.The commit at
Ais normally excluded. If a migration attached to that exact commit must run, model that requirement explicitly.--topo-orderrespects ancestry constraints while avoiding distracting intermixing of parallel lines;--reversethen produces a parent-before-child style traversal of the selected presentation.Replace tag names with refs that actually exist locally. A clone does not know remote refs or tags it never fetched.
The shortened output is illustrative; scripts should consume the full lines that
rev-listemits.
Choose a range that matches the job
Commits introduced on a topic branch
git rev-list --reverse --topo-order origin/main..HEADWhat this branch comparison assumes
origin/mainis a local remote-tracking ref, not a live query to the server.Fetch policy determines how current that baseline is. Automation should fetch explicitly when authorized, or record that it intentionally used the existing checkout.
Merged commits can be present depending on the graph. Add
--no-mergesonly if the consumer genuinely wants to omit merge commits—not as cosmetic cleanup.A force-push can change which objects the names select between runs; resolve and record immutable endpoints when reproducibility matters.
Only integration commits on a release branch
git rev-list --reverse --first-parent last-deploy..mainThis is a release narrative, not every contributing commit
--first-parentfollows the first parent at merges, which usually represents the integration branch’s line.Merge commits remain in the list; commits inside merged topics are not walked individually.
Use this for release notes or deployment checkpoints only when the repository’s merge convention makes first-parent history meaningful.
Make
last-deployan immutable tag or recorded commit ID if a moving branch name would make reruns ambiguous.
Commits on the ancestry path between two points
git rev-list --reverse --ancestry-path bad-release..fixed-releaseUseful for tracing causality, not general change inventory
A plain
D..Mcan include commits that contributed toMwithout being descendants ofD.--ancestry-pathnarrows the selected set to commits that lie on an ancestry relationship relevant to the endpoints.This can help investigate which commits carried a state from a bad release toward a fixed one.
Do not use it when the goal is to enumerate every change newly reachable from the right endpoint.
Validate revisions before a script trusts them
revision=v2.4.0
git rev-parse --verify --end-of-options "${revision}^{commit}"f48e31d92a70f06b38f26a73cf81dd427893a091Each guard closes a different ambiguity
--verifyrequires exactly one valid object name and returns its full object ID.--end-of-optionsprevents an untrusted name beginning with a dash from being parsed as another option.^{commit}peels an annotated tag when needed and rejects objects that cannot resolve to a commit.Quote the complete expression so shell whitespace and wildcard expansion cannot alter it.
The output is illustrative; the command form was validated against the current
git rev-parsemanual.
A small processor with an explicit contract
#!/usr/bin/env bash
set -euo pipefail
base=${1:?usage: commit-list.sh BASE [TIP]}
tip=${2:-HEAD}
base_commit=$(git rev-parse --verify --end-of-options "${base}^{commit}")
tip_commit=$(git rev-parse --verify --end-of-options "${tip}^{commit}")
git rev-list --reverse --topo-order "${base_commit}..${tip_commit}" |
while IFS= read -r commit_id; do
git show --no-patch --format='%H%x09%s' "${commit_id}"
doneValidate two endpoints, select a topologically ordered range, and emit full ID plus subject for each commit.
Why this loop resists common shell mistakes
set -euo pipefailmakes unhandled command failures, unset variables, and pipeline failures visible; callers must still interpret its exit status.Both user-supplied refs are resolved and restricted to commit-ish objects before the range is constructed.
while IFS= read -rconsumes each hexadecimal ID without trimming or treating backslashes specially.git show --no-patchreads metadata without displaying a diff;%H%x09%semits full ID, a tab, and subject.A subject may itself contain a tab, so this display is convenient for humans but not a lossless two-column interchange format. Keep the original one-ID-per-line stream for machine identity.
The script does no network fetch and was documentation-reviewed; the core
rev-list, object-format, count, and revision-verification commands were executed locally with Git 2.43.0.
Count first when volume changes risk
git rev-list --count v2.3.0..v2.4.0287A count is a useful circuit breaker
--countreports how many commits survive the revision selection.Apply the identical revisions and traversal filters to the count and processing commands.
A surprising zero may mean an inverted range, stale ref, shallow clone, or legitimately empty deployment.
A surprising 200,000 may justify stopping before an API call, migration, or per-commit checkout begins.
The output is illustrative; this repository’s actual
git rev-list --count HEADreturned 97 during validation.
Do not hide failures in a clever pipeline
Avoid command substitution for huge histories:
commits=$(git rev-list …)stores the entire list and later word-splits it.Avoid `for id in $(...)`: it depends on shell splitting and scales poorly; use a streaming
while IFS= read -rloop.Treat empty output deliberately: an empty range can be valid. Decide whether the calling workflow should succeed, skip, or fail.
Keep order when parallelizing:
xargs -Por background jobs can complete out of order even when input IDs are ordered.Do not parse `--oneline`: abbreviations and subjects are presentation, not an identity-only protocol.
Propagate pipeline errors: without
pipefail, an earlygit rev-listfailure can be masked by a later command that exits successfully.Pin endpoints for retries: moving refs can select a different set halfway through a deployment.
Never run untrusted code merely because its ID was listed: inspect provenance, signatures, policy, and the action the consumer will take.
If commits are missing, inspect the repository boundary
Run
git rev-parse --is-shallow-repositorywhen early history is absent.Remember that
--allmeans all refs already present locally; it does not fetch.Check replacement objects and graft-like history mechanisms if object traversal differs from another clone.
Confirm both endpoints resolve to the expected full IDs immediately before processing.
Compare
git rev-list --countwith the number of successfully processed records.Use the human-readable oldest-first Git history walkthrough when the selected set itself needs investigation.
Give the next tool less room to misunderstand you
A reliable commit list carries a quiet promise: every line is one validated identity, every identity belongs to a defined revision set, and the order means something the consumer actually needs. When that contract is explicit, the script downstream can stay simple—and simple is exactly what you want near release history.
Comments and corrections