git: cumulative diff with commit-limiting

怎甘沉沦 提交于 2019-11-30 21:53:03

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.

# 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
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!