Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
333 views
in Technique[技术] by (71.8m points)

git revert - Why does Git know it can cherry-pick a reverted commit?

In a branch, say, there are 3 commits: A <- B <- C. If I cherry-pick B directly (Test A), Git says:

The previous cherry-pick is now empty, possibly due to conflict resolution.
If you wish to commit it anyway, use:

    git commit --allow-empty

I can understand that because B is already in this branch, it's no-op to cherry-pick it again.

Then I reverted B and C in a batch commit by:

git revert -n B^..C
git commit -a -m "xxx"

This would be a new big commit D which reverts B and C, the branch should be like A <- B <- C <- D.

Then I need to redo B and C due to some reason. I tried:

git cherry-pick B^..C

I see two new commits B' and C' are appended to the branch: A <- B <- C <- D <- B' <- C'.

My first question is, How can Git intelligently knows it should create B' and C'? I thought Git would find B and C are already in branch history, so it may just skip them like when I cherry-pick 'B' directly in Test A.

Then, after that, since the branch is already A <- B <- C <- D <- B' <- C', I run this command again:

git cherry-pick B^..C

I expected Git can recognize this is a no-op operation. But this time Git complains confliction. My second question is, why does Git fail to recognize and skip this operation this time?

question from:https://stackoverflow.com/questions/65867521/why-does-git-know-it-can-cherry-pick-a-reverted-commit

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

cherry-pick is a merge, of the diffs from your cherry-pick's parent to the cherry-pick, with the diffs from your cherry-pick's parent to your checked-out tip. That's it. Git doesn't have to know any more than that. It doesn't care "where" any of the commits are, it cares about merging those two sets of diffs.

revert is a merge of the diffs from your revert to its parent with the diffs from your revert to your checked-out tip. That's it. Git doesn't have to know any more.

Here: try this:

git init test; cd $_
printf %s\n 1 2 3 4 5 >file; git add .; git commit -m1
sed -si 2s,$,x, file; git commit -am2
sed -si 4s,$,x, file; git commit -am3

Run git diff :/1 :/2 and git diff :/1 :/3. Those are the diffs git runs when you say git cherry-pick :/2 here. The first diff changes line 2, and the second commit changes lines 2 and 4; the line 4 change does not abut any changes in the first diff and the line 2 change is identical in both. There's nothing left to do, all the :/1-:/2 changes are also in :/1-:/3.

Now before you start on what follows, let me say this: this is harder to explain in prose than it is to just see. Do the example sequence above and look at the output. It is much, much easier to see what's going on by looking at it than by reading any description of it. Everybody goes through a stretch where this is too new and maybe a little orientation will help, and that's what the paragraphs below are for, but again: the prose, alone, is harder to understand than the diffs. Run the diffs, try to understand what you're looking at, if you need a little help over what I promise is a very small hump follow along in the text below. When it snaps into focus see if you don't at least mentally slap your forehead and think "wow why was that so hard to see?", just like, well, just about everybody.

Git's merge rules are pretty straightforward: identical changes to overlapping or abutting lines are accepted as-is. Changes to lines with no changes in one diff for changed lines, or lines abutting changed lines, in the other, are accepted as is. Different changes to any overlapping or abutting lines, well, there's an awful lot of history to look at and nobody's ever found a rule that will predict what the results of that should be every time, so git declares the changes conflict, dumps both sets of results into the file and lets you decide what the result should be.

So what happens if you now change line 3?

sed -si 3s,$,x, file; git commit -amx

run git diff :/1 :/2 and git diff :/1 :/x, and you'll see that where, relative to the cherry-pick's parent, :/2 changed line 2 and your tip changed lines 2,3 and 4. 2 and 3 abut, that's historically too close for automated genies to handle properly, so yay, you get to do it: git cherry-pick :/2 now will declare a conflict, showing you the change to line 2 and the two different versions of lines 3 and 4 (:/2 changed neither, your tip changed both, in context here it's clear the line 3 and 4 changes are fine as-is but again: nobody's ever figured out an automatic rule for reliably identifying such contexts).

You can ring changes on this setup to test out how reverts work. Also stash pops, and merges, and git checkout -m which runs a quick ad-hoc merge with your index.

Your git cherry-pick B^..C is a cherry-pick of two commits, B and C. It does them one after another, exactly as described above. Since you've reverted B and C, and then cherry-picked them again, this has the exact same effect as applying B and C and then cherry-picking B (with the intent of then cherry-picking C). I conclude that B and C touch overlapping or abutting lines, so git diff B^ B will show changes that overlap or abut changes in git diff B^ C', and that's what Git's not going to just pick for you, because whatever looks right here, in other circumstances nobody can write a rule for identifying, an identical-looking choice will be wrong. So git says the two sets of changes conflict and you get to sort it out.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...