/--B--D--G--H--J--L--M--> dev (HEAD)
/ / /
git diff A
It's not clear to me precisely what you're looking for. But note that:
Commits store snapshots. This means that commit
K has a complete source tree that is independent of whatever is in commits
C, and so on. Likewise, commit
J has a complete snapshot, independent of whatever is in
K, or any other commit.
(The word "independent" here means that if you ask Git to retrieve commit
J, it would not matter if you had somehow managed to alter commit
K after making
J. It's not actually possible to alter any commit, ever;
git commit --amend seems to change a commit, but really does not.)
git diff on two specific commits, Git extracts each of the two snapshots, and compares those.
git diff K M will show you what is different between the current tip of
K), and the current tip of
You can also just spell this
git diff master dev.
That may be what you want to see (again, it's not really clear to me). So answer 1:
git diff master dev.
On the other hand, maybe what you want is to show one diff for commit
B, one diff for commit
D, one diff for commit
H, one diff for commit
L, and one diff for commit
M. That is, you want to see each non-merge commit, one at a time, as compared to its (single) parent.
git diff A B, and
git diff B D, and so on. But you can also just
git show B, and
git show D, and so on. This shows you the commit as a change, instead of as a snapshot.
You may be wondering how this is possible if commits store snapshots, rather than changes. (This trips a lot of people up since most other Version Control Systems actually do store changes.) The answer to this apparent contradiction is that
git show looks up the same graph you drew.
Look again at commit
B. What commit comes before it? That is, which commit is to its left, following lines leftwards? There's only one possible ancestor for commit
B, and that's its single parent, commit
git show B:
Similarly, there's only one immediate ancestor (parent) for commit
M, and that's commit
git show M:
If this is what you want, the interesting question becomes: how do you find the IDs of each commit in the
M sequence? The answer to this is a bit complicated, but the key command is
git rev-list, which is essentially the same command as
git log. What these commands (
git log and
git rev-list both) do is to walk the commit graph. That is, you pick some starting point—in this case, commit
M, the tip of
dev—and tell Git to walk backwards through the commits, looking at each commit's parents.
The problem is that when you hit a merge commit, such as commit
J, Git walks back to all of its parents. You want to restrict Git to finding only the parent that was the tip of branch
dev when you made the merge commit. Git has a flag for this, spelled
--first-parent. This tells
git rev-list to follow only the first parent of each merge commit.
We also want to skip the merges, so we can add
--no-merges (this does not affect the walking-back process, it just limits the printed revision IDs to exclude the merges).
This leads to answer 2a:
git rev-list --first-parent --no-merges ^A dev (we'll get to the "not A" part later).
Now, actually using this with
git rev-list is kind of a pain, because now we have to take each commit ID and then run
git show on it. There's a much easier way, though, because
git rev-list and
git log are essentially the same command, and
git log -p shows each commit as a patch, doing much the same thing as
This leads to answer 2b:
git log -p --first-parent --no-merges ^A dev. We don't necessarily need the
--no-merges here either; see below.
The one special thing that
git show does, that
git diff doesn't, is to handle the case of showing a merge commit, such as commit
J has two parents, namely commits
K (incidentally, this implies that you made commit
K before making commit
J :-) ). If you run
git diff H J, Git will extract the snapshots for
J and compare them. If you run
git diff K J, Git will extract the snapshots for
J and compare them. But if you run
git show J, Git will:
J(the merge), and finally
The combined diff from step 4 attempts to show, in a compact fashion, changes from both
J. In compacting the changes, Git throws out any file where the version in either
K matches the version in
That is, suppose file
README.txt has some change from
J. But suppose that
README.txt is the same in
J. In other words, when you did the
git merge, you were picking up changes from
K in order to make
J, and there were no changes to
README.txt from the other side of the merge. This means
README.txt exactly matches one "incoming side", and hence the combined diff completely ignores the file.
This means combined diffs often show nothing at all, even though the merge picked up some change(s) in the new snapshot. To see those changes, you must make two diffs, not just one. You must make one diff from
J, and another diff from
J, rather than relying on the combined diff.
git log -p, you can also see combined diffs for merges, by adding
--cc to the options. But if you don't ask for this, what Git does is actually laughably simple: it just doesn't bother to show a diff at all.
So this leads to answer 2c:
git log -p --first-parent ^A dev. All we did is drop the
--no-merges: we will now see each merge's log message, but no diff.
This also ties into your other question:
BTW, I will appreciate if anyone knows a quick way to find A without keep scrolling down the log.
The answer to this is to make a symbolic name for commit
A. Find its ID once and then choose a name, like
master (but don't use either those since those are in use!).
master are: they're just symbolic names for commits, plus the extra property that, as branch names, you can
git checkout the symbolic name and wind up "on" the branch. You can give
A a branch-name too. You will need to make sure you do not
git checkout this branch and make commits on it, because if you do that, you'll grow a new branch, rather than just leaving the branch-name pointing to commit
Alternatively, you can make a tag-name pointing to commit
A. That's almost exactly the same as a branch name. The two differences are:
git push --tagsyou'll send it upstream as a tag, and then everyone else will have it too.
In this case point 1 is in your favor and point 2 is probably not, so it's up to you whether the advantage (can't accidentally change it) is worth the risk (might accidentally publish it).
If you have the ID of
A, then, you can:
$ git tag A <id-of-commit-A>
and now you have the name
A. (You can later
git tag -d A to delete it, although if you accidentally published it, you'll probably keep getting it back from your upstream.)
Getting back to the question of the
^A string in the
git log commands, all
^A does is tell
git rev-list (and therefore
git log) to stop walking through the commit graph upon reaching commit
A. The commit is also not shown (for
git rev-list, not printed; for
git log, not shown in the log output). The prefix
^ symbol is short for "not", i.e., "get me all commits reachable from
dev, but not reachable from
--first-parent makes Git traverse only the first parent of each merge, so that we do not walk into commits merged from
^A dev syntax can also be spelled
A..dev. Note that this works with both
git log and
git rev-list, but means something very different for