Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to select git commit ranges with inclusive end points

Tags:

git

I need to specify all commits downstream of the 1st commit in a git repo.

I am trying to run this command:

$ git filter-branch --index-filter 'git rm --ignore-unmatch --cached some/path/file' -- 33e0331^..

And I am getting:

fatal: ambiguous argument '33e0331^..': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'

Because 33e0331 is the first commit in the repo. How do I do this ?

like image 355
Ankur Agarwal Avatar asked Dec 06 '22 13:12

Ankur Agarwal


2 Answers

The question was specifically about using git filter-branch, but the way it was expressed applies to many Git commands. I like to use the term "select with history" here; it's how git log and git rev-list and anything else that uses git rev-list—including git filter-branch—work. For other Git commands, writing down a name for a single commit just selects the single commit itself, and we must use the exclude..include syntax or similar to get a whole range of commits:

git cherry-pick A..B

for instance will cherry-pick the commit after A, and then the commit after that, and so on until it finally reaches commit B. The very similar:

git revert A..B

will revert commit B, then the commit before B, then the commit before that, and so on, but stop and not revert commit A. (Note that this assumes something about the graph "between" A and B: the notion of "between" is rather slippery.)

Mark Adelsberger's answer is the right one for git filter-branch, and indeed for any Git command that uses the "select commit with history" mode. [Edit: Oops, I see he has augmented his answer since then! Well, this is now even more supplemental.] But what can you do with, e.g., git cherry-pick, when you want to cherry-pick commit A too?

There are several obvious answers—well, "obvious" once you grasp the commit-graph-following in the first place, and the funky syntactic operations Git provides with things like A^:

git cherry-pick A^..B

means "select commit B and all its history, excluding commit A^ and all of its history" and since A^ means "the parent commit of A", this usually does the trick. You must, of course, be aware of the way that the exclusion itself works: if there are merges in the commit graph, or commit A is not an ancestor of commit B, the exclusion may not produce what you wanted.

Nonetheless, there are some especially tricky cases where one funky Git syntax comes in handy. If commit A is itself a merge commit, and you want to exclude all of A's parents without excluding A itself, there is a syntax for that, documented in the gitrevisions(7) page: A^@ means "all the parents of commit A, but not commit A itself".

[Edit, Jun 2021]: This, unfortunately, cannot be combined with the two-dot syntax, but there sometimes is a way to handle it. We need to use git rev-parse with A^@ first to get the list of commit hash IDs to exclude. Then we need to prefix each hash ID in this list with ^. Finally, we need to provide this list, and a "positive reference" to B:

git rev-parse A^@ | sed s/^/^/

achieves the first goal (at least on Unix-like systems using sh or bash), and:

git <command> $(git rev-parse A^@ | sed s/^/^/) B

then runs the given <command> with the hat-prefixed list of commits. This list is empty when A is a root commit.

Unfortunately, this doesn't work with git cherry-pick at all, as it now just sees B. So for this particular case we just have to run some sort of "is A a root commit" test first (e.g., use A^..B). If A^ fails because A is a root commit, then we can use:

git cherry-pick A A..B

which will first copy commit A, then all commits after A up through B inclusive.

For git revert, the equivalent would be:

git revert A..B A

when A is a root commit (but note that if this revert works at all, it just leaves you with a no-files-in-it-at-all commit).

like image 64
torek Avatar answered Jan 29 '23 16:01

torek


Just don't specify the starting point. If you say master, for example, that implies "everything reachable from master". The ability to include a starting point is only to designate some "early history" to exclude.

While that should address the scenario you've put forward, a more general answer to the title question: You can't. You don't really specify "ranges of commits" (as most people would intuitively define it) at all.

The range notation is a convenience, but it can be misleading. In non-trivial cases, it can be useful to remember that you can specify (a) that everything reachable from a commit should be excluded, or (b) that everything reachable from a commit should be included. There are multiple notations for each. A..B means "exclude everything reachable from A, but other than that include everything reachable from B"; it's a shorthand for something like ^A B (and, interestingly, it's not actually shorter...)

Now in a simple case, that looks like a range of commits

x1 -- x2 -- A -- O1 -- O2 -- B -- x3 -- x4

In such a simple case, you could kinda-sotta make it an "inclusive range" by using "parent-of" notation as you tried (^A^ B or A^..B).

If A is a root, as in your example:

A -- O1 -- O2 -- B -- x1 -- x2

still A..B means to include the Os and the B. For an "inclusive" range, you would just omit the starting point as in the original form of my answer. B would include A, the Os, and B.

You can stretch your definition of "range" a little bit:

x1 -- x2 -- A
        \
         O1 -- O2 -- B

Here, A..B (which, again, is just ^A B) would again include just the Os and the B. It may seem that the "starting point" is not upstream of B, but you exclude everything reachable from A, which includes the xs

So now, what would an "inclusive range" even mean?

This is a common usage of the .. notation, because it allows you to peal the commits from a branch away from the commits of its "parent branch". (I say "parent branch" in quotes, because this is not a thing to git. It's just a common interpretation of the branch topology.)

But where you can really lose your mind trying to think of A..B as a "range" is something like

x1 -- A -- O1 -- O2 -- B <--(master)
                      /
              O3 -- O4

Now why are O3 and O4 not x2 and x3? They aren't in the "range" from A to B...? Well, they aren't reachable from A, and they are reachable from B, so they are included. To get the range (as you might intuitively understand it) you would have to say something like ^A ^o4 B

like image 44
Mark Adelsberger Avatar answered Jan 29 '23 15:01

Mark Adelsberger