skip to content

In Git, how do you resolve an expression like HEAD~3 to the exact commit ID?

level: middleimportance: should knowfreq 45%

answer

  1. Plumbing command, machine-readable output
  2. Turns any revision expression into an ID
  3. Use it before anything destructive
  4. --abbrev-ref goes the other direction
  5. Its set-valued sibling walks whole ranges

basics

~20 s

Run git rev-parse HEAD~3. It is Git's revision parser: it turns any gitrevisions expression — branch name, tag, abbreviation, caret/tilde suffix — into the full object ID, printing nothing else, which makes it the safe check before a destructive command.

solid answer

~40 s

`git rev-parse` is the plumbing command that converts a revision expression into an object ID. `git rev-parse HEAD~3` prints the 40-character hash, `--short` (or `--short=8`) abbreviates it, and `--abbrev-ref HEAD` prints the current branch name instead. `--verify` insists the argument resolves to exactly one object and, with `--quiet`, lets a script test a revision without noisy errors. Suffixes like `^{commit}` peel an annotated tag down to the commit it points at, and `^{tree}` gives the snapshot. The habit worth showing in an interview is using it *before* anything destructive: `git rev-parse --short HEAD~3` costs nothing and tells you exactly which commit `git reset --hard HEAD~3` would land on. Its bulk counterpart is `git rev-list`, which prints the whole set of commits a range selects rather than a single resolved ID.

code

console · 10 lines
console
$ git rev-parse HEAD~3
1c7d3f0a9b8c4e5d6f708192a3b4c5d6e7f80912
$ git rev-parse --short=8 HEAD~3
1c7d3f0a
$ git rev-parse --abbrev-ref HEAD
main
$ git rev-parse --verify --quiet v9.9 || echo "no such revision"
no such revision
$ git rev-list --count main..feature
4

go deeper

for a junior

Know that git rev-parse <revision> prints the commit ID an expression points to, and that checking beforehand is cheaper than recovering afterwards.

for a middle

Explain the flags that matter — --short, --abbrev-ref, --verify --quiet — and the peeling suffixes that turn an annotated tag into its commit.

for a senior

Show scripting judgment: plumbing output is stable and safe to parse, porcelain output is not, and resolving a revision is the guard you put in front of destructive automation.

for a principal

Own the standard for repository automation — which commands are permitted in hooks and CI glue, and why parsing human-facing output is a reliability bug waiting to happen.

## One job: expression in, object ID out Every Git command that accepts a revision runs it through the same parser, and `git rev-parse` exposes that parser directly. Feed it anything the `gitrevisions` syntax allows and it prints the resulting object ID: - a branch or tag name (`main`, `v2.0`) - an abbreviated hash (`9f1c2ab`) - suffix navigation (`HEAD~3`, `main^2`, `v2.0^{commit}`) - `@` on its own, which is a synonym for `HEAD` - message search (`:/fix parser` finds the most recent commit whose message contains that text) Because it is plumbing, output is machine-friendly: one ID per line, no decoration, stable across versions. That makes it the right tool inside scripts and hooks. ## The flags that matter `--short` or `--short=<n>` prints an abbreviation long enough to be unambiguous in this repository. `--abbrev-ref` goes the other way and prints a symbolic name — `git rev-parse --abbrev-ref HEAD` is the standard way for a script to learn the current branch, and it prints `HEAD` when the checkout is detached, which is itself a useful signal. `--verify` requires the argument to name exactly one object and errors otherwise; combined with `--quiet` it becomes a clean existence test, so `git rev-parse --verify --quiet "$ref" >/dev/null || echo missing` is a common guard. `^{commit}` and `^{tree}` are *peeling* suffixes rather than parent selectors. An annotated tag is its own object pointing at a commit, so `v2.0` resolves to the tag object's ID while `v2.0^{commit}` resolves to the commit — the difference matters when you compare IDs. ## Resolving a set: git rev-list Where `rev-parse` answers "which object is this name", `git rev-list` answers "which commits does this range contain". It walks the DAG and prints IDs, newest first, and it accepts the same range syntax: - `git rev-list --count HEAD` — how many commits are reachable from HEAD. - `git rev-list --count main..feature` — how many commits a branch adds. - `git rev-list --left-right --count main...feature` — divergence in both directions. - `git rev-list --max-parents=1 HEAD` — skip merges, since merges have two or more parents. `git log` is the porcelain built on the same walker; `rev-list` is what you script against because its output will not change to suit human readers. ## Why this is an interview question It separates people who *guess* at revisions from people who *check* them. `git reset --hard HEAD~3` and `git reset --hard HEAD^3` differ by one character and can discard very different amounts of work; resolving first turns a gamble into a decision. It also demonstrates that you know the porcelain/plumbing split exists at all: the same expression grammar underlies `log`, `diff`, `reset`, `checkout` and `push`, because all of them call the same parser. ## Practical notes Ambiguity is real: if a branch and a tag share a name, Git applies a documented precedence order and warns about the ambiguity — the fix is to spell the ref in full, such as `refs/heads/release` versus `refs/tags/release`. And `rev-parse` only reads; it never moves refs or touches the working tree, so it is always safe to run first.

  • How would a script find the current branch name, and what does it get in a detached HEAD?
    `git rev-parse --abbrev-ref HEAD` prints the branch name. When HEAD is detached it prints the literal string `HEAD`, which scripts can test for; `git symbolic-ref --quiet HEAD` is the stricter alternative that simply fails when there is no branch.
  • What is the difference between resolving v2.0 and v2.0^{commit}?
    An annotated tag is its own object, so `v2.0` resolves to the tag object's ID. The `^{commit}` peeling suffix follows it to the commit it points at. A lightweight tag has no tag object, so both forms give the same ID.
  • When would you reach for git rev-list instead of git rev-parse?
    When you want a set rather than a single object: counting the commits a range contains, feeding a list of IDs to another command, or filtering by parent count. `rev-parse` resolves one expression; `rev-list` walks the graph and prints every commit the range selects.

saying these in an interview costs you the question

  • Guesses which commit a revision names before resetting
  • Thinks rev-parse can modify refs
  • Confuses peeling ^{commit} with parent selection ^1
  • Parses git log output in scripts instead of rev-list
  • Assumes an annotated tag's ID is the commit ID

context