In Git, how does HEAD@{2} differ from HEAD~2?
answer
- Two coordinate systems, one notation each
- Parents versus operations
- Only one of them is identical in every clone
- Checkouts consume a slot but add no ancestor
- Braces also accept a date
basics
~10 sHEAD~2 is graph navigation: two commits back along parent links from the current commit. HEAD@{2} is reflog navigation: the commit HEAD pointed at two ref movements ago, regardless of ancestry.
solid answer
~40 sThe tilde walks the commit graph and the at-brace walks the reflog. `HEAD~2` means "follow first-parent links twice from the commit HEAD names" — pure ancestry, identical in every clone of the repository. `HEAD@{2}` means "the value HEAD had two entries ago in *this* repository's reflog", which is chronological and local. They coincide only by accident: if your last two actions were checking out an unrelated branch and then resetting, `HEAD@{2}` may be nowhere near `HEAD~2`. The same brace syntax takes a time instead of a count — `HEAD@{2.days.ago}`, `main@{yesterday}` — resolving to where the ref pointed then, and it works per branch: `main@{1}` reads `main`'s own journal, not HEAD's. If the journal does not reach back far enough, Git warns and uses its oldest entry.
code
bash · 4 linesgit log --oneline -1 'HEAD~2' # grandparent in the graph
git log --oneline -1 'HEAD@{2}' # where HEAD was two moves ago
git log --oneline -1 'main@{1}' # main's value before its last update
git show 'main@{yesterday}' # main's position yesterday, per its refloggo deeper
Remember the split: the tilde counts commits backwards in history, the braces count entries in the reflog. When undoing an operation you want the brace form.
Explain why they diverge — checkouts, resets and rebases add reflog entries without adding ancestors — and know that any ref with a journal supports branch@{n} and the date form.
Verify before acting: inspect the reflog entry's action label rather than resetting blind, and know that brace expressions are local and expire, so a runbook cannot depend on them.
Own the point that reflog coordinates are not reproducible across machines or time, so recovery procedures and automation must record commit ids or refs, never @{n} expressions.
## Two coordinate systems Git revisions can be selected in two completely different ways, and the notation is the giveaway. **Ancestry (`~` and `^`)** navigates the commit graph. `HEAD~1` is the first parent of the commit HEAD names, `HEAD~2` its grandparent along first-parent links, `HEAD^2` the *second* parent of a merge commit. These are properties of the objects themselves: anyone with the same commit resolves `abc123~2` to the same object, forever. **Reflog (`@{…}`)** navigates a ref's journal of past values. `HEAD@{0}` is the current value, `HEAD@{1}` the value before the last update, `HEAD@{2}` the one before that. This is a property of *your* repository's local history of operations. On another machine, or after the reflog expires, the same expression resolves differently or fails. ## Why they diverge Ancestry counts commits; the reflog counts *actions*. Actions that are not commits still consume a reflog slot: - `git switch other-branch` writes an entry, and the new position is not an ancestor of the old one at all. - `git reset --hard <somewhere>` writes an entry that can jump anywhere in the graph. - `git rebase` writes several entries for one logical operation, as it detaches, replays each commit, and finishes. - `git pull` writes entries for the fetch-driven update and the merge or rebase. So after `git commit` twice in a row on one branch, `HEAD@{2}` and `HEAD~2` happen to agree. After a checkout and a merge, they almost certainly do not. Confusing them is the classic mistake: typing `git reset --hard HEAD@{1}` when you meant "one commit back" can move the branch somewhere entirely unexpected. ## Per-ref journals The brace form applies to any ref that has a reflog, not just HEAD. `main@{1}` is where `main` pointed before its most recent update, read from `main`'s own journal. This is often the precise tool you want: HEAD's journal is polluted by every checkout you made, while a branch's journal contains only that branch's movements, so `main@{1}` after a botched rebase names the pre-rebase tip directly. ## The time form Instead of a count you can write a date specification: `HEAD@{2.days.ago}`, `main@{yesterday}`, `HEAD@{one.week.ago}`, or an absolute timestamp. Git finds the value the ref held at that moment according to the journal. Two behaviours are worth knowing: - If the journal does not reach that far back — because it was created later or entries expired — Git prints a warning that the log for the ref only goes back to a given date, and falls back to the oldest available entry rather than failing silently in your face. - Because it is journal-based, the time refers to *when your repository's ref moved*, not to commit author or committer dates. `main@{yesterday}` is not "the commit authored yesterday". ## A related but different notation `@{-1}` means the previously checked-out branch, which is what `git switch -` expands to, and it stacks: `@{-2}` is the one before that. Do not confuse either with `@{u}` / `@{upstream}`, which resolves to a branch's configured upstream and has nothing to do with the reflog — it is tracking configuration, not history of movement. Recognising that three different things share brace syntax is a good discriminator in an interview. ## Practical guidance When you mean "undo the last operation", reach for the reflog form and **verify before acting**: `git log --oneline -1 'HEAD@{1}'` or a plain `git reflog` costs nothing and shows the action label that produced the entry. When you mean "the commit before this one in history", use `~`. And quote the expression in shells where braces are special.
- When would you prefer main@{1} over HEAD@{1}?When you care about one branch's movements. HEAD's journal records every checkout and switch, so the entry you want may be several positions back and hard to identify. `main@{1}` reads `main`'s own journal and names the value it held before its last update — precisely the pre-rebase or pre-reset tip. Note that deleting a branch removes its journal.
- Why should a recovery runbook avoid hard-coding HEAD@{5}?Because reflog indices are local and shift with every ref update, so the same expression means something different an hour later or on another machine, and entries eventually expire. A runbook should say "inspect `git reflog`, identify the entry by its action message, and use the commit id" — ids are stable, positions are not.
saying these in an interview costs you the question
- Treating HEAD@{2} as a synonym for two commits back
- Assuming @{n} expressions resolve the same in another clone
- Confusing @{u} for upstream with @{n} for reflog position
- Believing main@{yesterday} means the commit authored yesterday
- Expecting @{n} to keep working long after the operation