Note that if there's only a simple linear path from feature
back to master
, you're good with the accepted answer to Git - how to find first commit of specific branch:
A--B--C--D <-- master
\
E--F--G <-- feature
Here git log --oneline master..feature
will list commit G
, then F
, then E
, so the last output line will be that for commit E
. (If you just want the hash ID, use git rev-list master..feature | tail -1
.)
But consider:
A--B--C--D <-- master
\
E <-- develop
\
F--G <-- feature
Here master..feature
still lists commit E
, even if you wanted commit F
: you need develop..feature
to have git log
list G
then F
and then stop.
Or:
A--B--C--D <-- master
\
\ F--G <-- feature1
\ / \
E J--K <-- feature3
\ /
H--I <-- feature2
Here, commit B
is shared by all branches; commit E
is on all branches except master
; F
and G
are shared by feature1
and feature3
; H
and I
are shared by feature2
and feature3
; and J
and K
are exclusive to feature3
.
Using master..feature3
, you'll enumerate commits E
through K
, but the order of, specifically, F
through I
is changeable. (E
will come out after all four, F
will not be mentioned before G
is listed, and H
will not be mentioned before I
is listed, but that still leaves a good number of possible orderings.) You can choose the order, within limits, using the various sorting controls; the default is to go by committer date, when there are multiple possible commits for git log
to emit at this point.
In general, Git works by starting at a tip commit, as pointed-to by one of these branch names, and working backwards. When reaching a place where there's a join-up (like merge commit J
), Git starts walking both paths at the same time. With git log
, it prints those commits in some order based on the sorting order you chose. (Eventually the paths usually re-converge, as in this case they do at commit E
. At that point, git log
can resume doing one commit at a time.)
The commits themselves are all fixed in place for as long as they exist—you can draw the graph a bit differently, but the parent of E
is always B
, for instance—but the names, the branch names that point to specific commits, are all moveable: any name can be moved at any time to point to any one specific commit. Names can be added or deleted at any time, with one caveat: if you delete all the names that let you find a commit, it can be very hard to get that commit back.1 In general, Git commands and Git range listings like A..B
and so on, resolve the names to the commits, then use the commit graph to do their job.2
1If git gc
runs at the wrong time, and you have reflogs disabled or the reflog entries are too old and stale, the Grim Garbage Collector can throw out any unreachable commit. At that point, the commit is really gone, and even knowing its hash ID won't save you.
2The major exception here is git diff
: if you give git diff
a range, it just treats it as if you gave it two different commit hash IDs. That is, git diff A..B
does not walk the graph at all, it just finds A
and B
and then acts exactly like git diff A B
. The git diff
command also has special treatment for the three-dot syntax, A...B
.
git rebase
has also acquired a special case for A...B
(with three dots) in its --onto
syntax.