问题
git log
has some very useful commit-limiting options, such as --no-merges
and --first-parent
. I'd like to be able to use these options when generating a cumulative diff patch/stat/numstat for a range of commits.
With these commands:
git log --oneline --first-parent --no-merges --patch 29665b0..0b76a27
git log --oneline --first-parent --no-merges --stat 29665b0..0b76a27
git log --oneline --first-parent --no-merges --numstat 29665b0..0b76a27
the diff is not cumulative (the changes are listed individually for each commit).
With these commands:
git diff --patch 29665b0..0b76a27
git diff --stat 29665b0..0b76a27
git diff --numstat 29665b0..0b76a27
the diff is cumulative, but unfortunately git diff
doesn't support the commit-limiting options.
So what I'd like is the cumulative diff functionality of git diff
combined with the commit-limiting functionality of git log
.
One idea I had is to use git log
to generate a list of commit hashes, and then somehow pipe that list to git diff
to generate a cumulative diff of the specified commits. Something like this (obviously this method of piping hashes to git diff
doesn't actually work):
git log --pretty=format:%h --first-parent --no-merges 29665b0..0b76a27 | git diff
where --pretty=format:%h
outputs the hashes of the matching commits.
Update
Thanks to @torek and @twalberg, I now understand git diff
's operation more clearly. The range syntax 29665b0..0b76a27
is indeed misleading, and I now understand that it's not actually performing a cumulative diff over a range of commits. Looking through the docs, I found this:
"diff" is about comparing two endpoints, not ranges, and the range notations (
<commit>..<commit>
and<commit>...<commit>
) do not mean a range as defined in the "SPECIFYING RANGES" section in gitrevisions(7).
Taking this into account, I'll rephrase my question. With these commands:
git log --oneline --first-parent --no-merges --patch 29665b0..0b76a27
git log --oneline --first-parent --no-merges --stat 29665b0..0b76a27
git log --oneline --first-parent --no-merges --numstat 29665b0..0b76a27
the changes are listed individually for each matching commit. How can I combine those individual changes, to produce a cumulative patch/stat/numstat?
The answers to the linked possible duplicate question are helpful, suggesting a solution: create a temporary branch, cherry-pick the relevant commits, and then generate the diff.
I've just posted an answer which uses this technique, but I'm still interested to know if there's a solution which doesn't require a temporary branch?
回答1:
There is at least one basic misapprehension here. Specifically, git diff
is not really cumulative at all: instead, it's simply pairwise.
Specifically, these two commands do the same thing:
git diff rev1 rev2
git diff rev1..rev2
That is, in git diff
, there really is no such thing as a range in the first place.
With that out of the way, let's take a look behind the scenes at git log
. What git log
does with a range is really1 to hand the range to git rev-list
, which produces a list of every rev in the range, applying the modifiers along the way:
git rev-list 29665b0..0b76a27
spits out every rev reachable from 0b76a27
that is not also reachable from 29665b0
. Adding --first-parent
, --max-parents=1
(aka --no-merges
), and so on filters away some of the revs that would be listed here.
The final result is given back to git log
, which then looks at each revision in the order git rev-list
spits them out—this is also controllable via --date-order
and --topo-order
and so on; see the documentation for git rev-list—and shows you each log entry, perhaps along with a diff as produced by git diff-tree (which for single-parent commits, compares the commit to its parent).
What you can do, then, is invoke git rev-list
yourself, directly, and then peel off the top and bottom revisions from its output. (In this particular case you probably want --topo-order
too, to make sure that the last rev really is the earliest, graph-wise, regardless of dates.) For instance, in a script:
#! /bin/sh
tempfile=$(mktemp -t mydiff)
trap "rm -f $tempfile" 1 2 3 15
git rev-list 29665b0..0b76a27 --first-parent --no-merges --topo-order > $tempfile
# remember that the first rev listed is the last rev in the range
last=$(head -1 $tempfile)
first=$(tail -1 $tempfile)
rm -f $tempfile # done with it, don't leave it around while showing diff
git diff $first $last
You can get considerably fancier by using git rev-parse
to parse options and split them into diff options vs rev-list options, but that's way beyond what you need here. The main thing to improve above is to get rid of the hard-coded revision-range.
1Some git commands really really do hand arguments off to git rev-list
, as they're just shell scripts that use git rev-list
and other git commands to handle this. Others are built together, so that git log
and git rev-list
are actually a single binary, and one part hands a job off to another part, but without invoking a new program.
In any case, note that git log master
simply hands master
off to git rev-list
, which produces a list of all revs reachable from the branch-label master
. If you add --no-walk
, git rev-list
produces just one rev, so that git log
shows only that one revision.
回答2:
# Create a temporary branch to mark the start of the cherry-picked commits
git branch tmpstart
# Create and checkout a temporary branch for the cherry-picked commits
git checkout -b tmpend
# Use git log to filter the range of commits with the desired
# commit-limiting options, and then cherry-pick each matching commit
git log \
--first-parent \ # Commit-limiting
--no-merges \ # Commit-limiting
--reverse \ # Reverse the order (ascending chronological order)
--pretty=format:%h \ # Output the abbreviated hash of each matching commit
29665b0..0b76a27 \ # Range of commits
| xargs -n 1 git cherry-pick
# Generate the patch/stat/numstat of the cherry-picked commits
git diff --patch tmpstart tmpend
git diff --stat tmpstart tmpend
git diff --numstat tmpstart tmpend
来源:https://stackoverflow.com/questions/25403705/git-cumulative-diff-with-commit-limiting