IMSoP's first comment contains a key question:
I'm not even quite sure what the desired output would be in this case - how would you distinguish between multiple "future" branches that all changed the line in different ways?
For instance, consider the following simple sequence of commits, in which the uppercase letters stand in for the commit hash IDs, and earlier commits are to the left with later commits to the right:
D--E--F <-- branch1
/
A--B--C
\
G--H--I <-- branch2
If you check out branch1
, you get commit F
. Running git log
on file.py
examines the copy of file.py
in commit F
, then the one in commit E
, and so on, backwards. (I'm skipping over tons of detail here on purpose: we're focusing on mechanism first, though we'll come back to goal.) If you check out branch2
, you get commit I
, so a git log
here will visit commits I
, then H
, then G
, then C
, B
, and A
in that order.
Obviously, if you check out any commit after C
, you'll pick one "time line", as it were: either the time line of commits for branch1
, or those for branch2
. But if you check out C
, there are two ways to go forward: via D
, or via G
.
In a maths-and-graph sense, what we have here is a partial order induced by the existence of a directed graph. Commit D
precedes E
which precedes F
, which we denote as D ≺ E ≺ F.1 But partial orders leave some relationships unstated: in D
vs G
, there's no clear order, so D ⊀ G and yet also G ⊀ D. This is a fancy, math-y way to say: From C
, we don't know which commit to move forwards to.
In Git, git log
doesn't even try to solve this problem. Git makes you solve it, by checking out a later commit, or telling git log
to start working backwards from some later commit. Problem solved—or at least swept under the rug, n'est-ce pas? Well, um, actually, non. Consider this graph fragment:
I--J
/ \
...--G--H M--N <-- main (HEAD)
\ /
K--L
Here, we've checked out commit N
via branch main
(that's what the HEAD
in parentheses indicates), so git log
with a file name will start at commit N
and work backwards. But commit M
is a merge commit, at which two branches—with no names any more; the branch names, if any existed,2 are long gone—were merged. When we make a merge commit, we are moving forwards—but to a time-traveler like Git, passing through a merge represents a divergence. So will git log
go from commit M
to commit J
, or to commit L
?
The git log
command has multiple different answers for this. When running git log
with no arguments, it will in fact visit both parent commits—in some unspecified order, and using a technique we won't cover here. But when running git log
with a file name, with or without -L
arguments, it usually defaults to visiting only one "branch" or "leg" of the merge (what to call it is up to you; I tend to call them legs myself). What git log
does in this case is look to see which of the two parent commits J
and L
has a version of the file that matches the one in commit M
. It then uses that parent to pick the leg to follow. If both match, it picks one leg more or less at random.
It would be possible for git log
to do this in the other direction, when using --reverse
from commit C
in our first graph, for instance. But it doesn't, and in fact --reverse
does not mean go forwards in git log
: git log
still goes backwards. It just prints out the commits in reverse, i.e., forwards, later.
Ultimately it comes down to this: each of Git's tools has certain limitations. Be aware of them, and when they pinch, consider another tool. In this case, you might want to look at git blame
, which does have the ability to "go forwards" (though you must specify a particular revision range for this operation).
1Git internally implements instead the precedes-or-equals relationship, in which D ≼ D as well, but adding equality as an option doesn't affect what we're concerned with here.
2Branch names, in Git, exist so that we can find commits. But there are other ways to find commits, so we don't always require a branch name here. The other obvious way to find a commit is to work backwards (from some commit, probably found by a branch name), but any method that produces the right commit hash ID suffices. Since commit M
, being a merge commit, contains the hash IDs of two different previous commits J
and L
, it works to find both previous commits, and we do not need branch names for either one.