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

Terminalbash
git rev-list --reverse HEAD
a04c6e0a7a1c1ecffa04bccbe6deaaad1e56ef15
e0b4a4e6f18a54f609f4cee1c5414dcfb3b4fec0
d087ec455114bbdcf17b601bbc8be2fbeb01299e
…

Why rev-list is the clean interface

  • git rev-list performs the same revision walk underlying many porcelain commands but emits commit object IDs without subjects, dates, decoration, color, or graph lanes.

  • HEAD is the starting revision; only commits reachable through its parent links are selected.

  • --reverse reverses 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

Terminalbash
git rev-parse --show-object-format
sha1

Do 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-format reports 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 sha1 under 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.

Terminalbash
git rev-list --reverse --topo-order v2.3.0..v2.4.0
4ad930ff0c1e…
729cb08c12aa…
f48e31d92a70…

The left endpoint is subtraction

  • A..B is shorthand for the commits reachable from B minus every commit reachable from A.

  • The commit at A is normally excluded. If a migration attached to that exact commit must run, model that requirement explicitly.

  • --topo-order respects ancestry constraints while avoiding distracting intermixing of parallel lines; --reverse then 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-list emits.

Choose a range that matches the job

Commits introduced on a topic branch

Terminalbash
git rev-list --reverse --topo-order origin/main..HEAD

What this branch comparison assumes

  • origin/main is 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-merges only 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

Terminalbash
git rev-list --reverse --first-parent last-deploy..main

This is a release narrative, not every contributing commit

  • --first-parent follows 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-deploy an immutable tag or recorded commit ID if a moving branch name would make reruns ambiguous.

Commits on the ancestry path between two points

Terminalbash
git rev-list --reverse --ancestry-path bad-release..fixed-release

Useful for tracing causality, not general change inventory

  • A plain D..M can include commits that contributed to M without being descendants of D.

  • --ancestry-path narrows 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

Terminalbash
revision=v2.4.0
git rev-parse --verify --end-of-options "${revision}^{commit}"
f48e31d92a70f06b38f26a73cf81dd427893a091

Each guard closes a different ambiguity

  • --verify requires exactly one valid object name and returns its full object ID.

  • --end-of-options prevents 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-parse manual.

A small processor with an explicit contract

commit-list.shbash
#!/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}"
done

Validate 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 pipefail makes 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 -r consumes each hexadecimal ID without trimming or treating backslashes specially.

  • git show --no-patch reads metadata without displaying a diff; %H%x09%s emits 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

Terminalbash
git rev-list --count v2.3.0..v2.4.0
287

A count is a useful circuit breaker

  • --count reports 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 HEAD returned 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 -r loop.

  • 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 -P or 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 early git rev-list failure 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-repository when early history is absent.

  • Remember that --all means 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 --count with 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.

Authoritative Git documentation