← back to latest

See which commits are on a branch but not on main

git log --oneline main..feature

anatomy

git log
Walk the commit graph and print what it finds. Given a range instead of a single branch, it stops listing everything reachable and starts answering a set question: which commits are on this side and not that one.
--oneline
Shorten each commit to an abbreviated hash and its subject line. A branch comparison is a checklist, and one line per commit is what makes it readable at a glance.
main..feature
Two dots mean commits reachable from feature that are not reachable from main. It is shorthand for ^main feature, where the caret excludes everything main can reach. This is not a range in the date sense: it ignores timestamps and looks only at ancestry, so it lists the branch's own work no matter how old main's newer commits are. The order matters. Swap the names and you get what main has gained that the branch has not seen yet.

Sample output

5985036 Log final webhook failure
6384621 Back off exponentially on 5xx
4f6f775 Add retry queue for webhook delivery

These three commits are the whole pull request, and nothing else will land when it merges. The two commits main gained after the branch was cut (Bump stripe SDK, Fix rounding in tax totals) do not appear, because main can already reach them.

When you would reach for it

A reviewer asks what is actually in your branch. You have come back to a branch after two weeks and cannot remember how far you got. You are about to delete a local branch and want proof that everything on it has already been merged: if git log main..old-branch prints nothing, there is nothing to lose. Flip the names to feature..main and the same command tells you how far behind main you have fallen before you rebase.

Gotchas

  • Empty output exits 0, and it is a real answer: every commit on the branch is already reachable from main. That is the check to run before git branch -D. It is not proof for squash-merged or rebased branches, though, since those land on main as new commits with new hashes and the originals still show up.
  • The range uses your local branch names. If main has moved on the server and you have not fetched, main..feature compares against a stale main. Run git fetch and compare against origin/main..feature to see what a pull request would actually contain.
  • A cherry-picked commit gets a new hash, so the range still lists the original even though its change is already on main. git log --oneline --cherry-pick --right-only main...feature hides commits whose patch already exists on the other side.
  • Dots mean something different in git diff. git diff main..feature is the same as git diff main feature, which compares the two tips and shows main’s newer work as deletions. Three dots, git diff main...feature, diffs from the point where the branch split off, which is what a pull request page shows. In git log the meanings run the other way round: two dots are the pull request view.

Variants

$ git log --oneline --left-right main...feature

Three dots: commits on either side but not both, marked < for main and > for the branch

$ git rev-list --left-right --count main...feature

Ahead and behind as two numbers (2 3 means main has 2 the branch lacks, the branch has 3 main lacks), handy for prompts and scripts

$ git log --oneline main..

Leave the right side empty and git fills in HEAD, so this shows what your current branch has added

lineage

Git's range syntax predates git log as most people know it. In the first weeks of git in 2005, Linus Torvalds wrote git rev-list as a low-level plumbing command that walks the commit graph, and it took a list of commits to include and, prefixed with a caret, commits to exclude. The two-dot form arrived soon after as shorthand for the most common case of that pattern, include one tip and exclude another. The three-dot symmetric difference followed for the question of how two branches have diverged. git log is still built on the same revision walker, which is why every command that accepts revisions, from log to shortlog to rev-list, reads the dots the same way, and why git diff, which compares two snapshots rather than walking history, is the odd one out.