Home > Software engineering >  Git revert to remove only code from the specific commit
Git revert to remove only code from the specific commit

Time:11-30

I'm having a little bit of a issue getting the hang of git revert (I might be using git revert incorrectly).

GOAL: I am trying to revert a commit, while leaving the future commits intact. (in other words, I'm trying to cherry pick the code to be extracted).

ISSUE: reverting a commit, 2 or 3 commits back, also reverts any future commits (this might be expected behavior)

Example:

I have added text to the, header, main & footer. Each text added was committed (git commit -am 'X text') separately. In my mind, this should allow me to remove only the code from one of those commits, without affecting the other commits.

enter image description here

So if I wanted to revert 'header text added' i.e. git revert 0713432 - this would remove only the text from the header. While leaving the text in the main & footer intact.

But instead I am given a merge conflict, wanting to remove all changes made ahead of this commit:

enter image description here

Questions:

Have I made a mistake, is this expected behavior? (I was under the impression that by comiting code in sections, you would be able to remove specific sections of code).

Is there a way to only remove specific sections of code? i.e. a commit, while leaving any future commits intact?

Thank you for any help on this subject, I appreciate that this is a fairly entry level question, but after spending some time trying to source an answer alone, it is time to ask for some input.

ATB - W

CodePudding user response:

The reason of your issue is that the modified lines of 3 commits are next to each other.

git-revert works as reversely apply a patch. When git applying a patch, it looks for the 3 lines above and below of the changes to locate where to apply them, but in your case, the 3 lines above the added header texts were altered by the 2 commits after it, and thus incurred a conflict.

You can set the conflict style to diff3 using the below command to see the conflicts more clearly.

git config --global merge.conflictStyle diff3

To set it back, use git config --global --unset merge.conflictStyle

CodePudding user response:

TL:DR All your expectations and reactions are wrong:

  • A revert is a merge.

  • Merge conflicts are normal, not bad.

  • The repeated phrase about "affecting future commits" is nonsense.

Simple solution: Resolve the conflict and move on.


Okay, if you've read this far, you're ready for the longer version of the story.

A revert is a merge.

In order to understand why a revert is a merge, you have to know what a merge is. (Surprisingly few Git users do.) A basic merge involves three commits: the base, version one, and version two. Git works out two diffs — the diff between base and version one, and the diff between base and version two — and applies both diffs to base to form a new (merge) commit.

So how does a revert fit into that? Let's say you have

A <- B <- C <- D = HEAD

Now you say git revert C. Then:

  • C is base
  • B is version one
  • D is version two

In other words, Git is now starting with C, and it is asking itself, what would I have to do to C in order to get both B and D? It creates a commit that expresses that, and the result is the revert commit.

If you think about it, you'll see that this is exactly what you expect a revert to do. If C added "hello" at the start of a (long) file, and D added "goodbye" at the end of that same file, then the goal is to make a commit which, from C's point of view, adds "goodbye" at the end but removes "hello" at the start.

What a merge conflict is.

The phrase "merge conflict" has got to be the worst piece of terminology in all of Git. It makes this situation sound aggressive and unnatural. It is neither. It is normal and simple. Yesterday I had twenty merge conflicts; I was neither surprised nor upset (though I did have to drink quite a bit of coffee).

A merge conflict is simply a situation where Git cannot work out in any simple way how to obey the rules I just enunciated. It is being asked to apply two diffs that are not independent of one another. It could try to guess how you want this done, but its normal response is to let you tell it how you want this done.

For example, if C added "hello" at the start of a file, and D changed "hello" to "howdy", then if you ask to revert C, it is not at all obvious how to perform the merge I described in the previous section. If you have enabled diff3 conflict notation (as suggested in another answer), you'll see something like this:

<<<<<<< HEAD
howdy
||||||| d511c88 (C)
hello
=======
>>>>>>> parent of d511c88 (C)

That means:

Hello, Wally. I'm in a bit of a fix here. Here's the situation. C (which you are asking me to revert) has "hello". You currently have "howdy" in that spot ("currently" is HEAD, which here means D). But the thing we are trying to revert to (B, the "parent of d511c88") has nothing in that spot. How am I supposed to change "hello" to both "howdy" and nothing? Sorry if I'm being a bit dense, but I really have no idea how to do that, so please do it for me. Thank you! Share and enjoy.

What you probably meant by this revert is that you wish you'd never introduced "hello" in the first place, so you would just delete all of that text (leaving the nothingness that you actually desire), and then say

git add .
git revert --continue

"Affecting the other commits" is a red herring.

The existence of "other commits" simply means that the commit you are reverting is not very recent. Let's change the diagram so that more commits have been made:

A <- B <- C <- D <- E <- F = HEAD

Now you say git revert C. Then:

  • C is base
  • B is version one
  • F is version two

Note that Git is totally uninterested in D and E. But of course D and E may have introduced changes that affect what F contains!

For example, using the example we're already using, if C added "hello" at the start and D changed "hello" to "howdy", and E and F worked on totally different areas of the project, then F still has "howdy" at the start. So the conflict is between B and F, even though the change that got us into this situation was introduced in D. In doing the revert, Git knows nothing of D (I'm simplifying but not lying): it just looks at C, B, and F, and tries to form the new (revert) commit based on those, in the way I've already described.

Resolving your conflict

Your conflicted file looks like this:

<body>
<<<<<< HEAD
<header>
  <h1>header Text</h1>
</header>
<main>
  <h2>Main text</h2>
</main>
<footer>
  <p>Footer text</p>
</footer>
======
<header></header>
<main></main>
<footer></footer>
>>>>> parent of 0713431
</body>
</html>

I presume (even I have to guess! so no wonder Git is confused) that what you mean is that you are sorry you added <h1>header Text</h1>. So the conflict resolution would be what HEAD has, minus that line:

<body>
<header>
</header>
<main>
  <h2>Main text</h2>
</main>
<footer>
  <p>Footer text</p>
</footer>
</body>
</html>

So make that edit, resolve the conflict, finish the revert, and move on.

CodePudding user response:

The problem here is expectations :)

How patches work

Inspecting the commit that is going to be reverted should reveal something like this:

$ git show 0713432
...
diff --git a/index.html b/index.html
index de53e21..a48ef5a 100644
--- a/index.html
    b/index.html
@@ -3,7  3,9 @@
     whatever
   </head>
   <body>
-    <header></header>
     <header>
       <h1>header Text</h1>
     </header>
     <main></main>
     <footer></footer>
   </body>

That commit replaced the line <header></header> with 3 new lines. Here's a randomly googled reference on patch syntax:

The line beginning with @@ indicates by line number and length the positions of this hunk in the A and B versions

What that means here is that git is looking for these 7 lines:

     whatever
   </head>
   <body>
     <header></header>
     <main></main>
     <footer></footer>
   </body>

to replace it with these 9 lines:

     whatever
   </head>
   <body>
     <header>
       <h1>header Text</h1>
     </header>
     <main></main>
     <footer></footer>
   </body>

It's a little more intelligent than described - hopefully this level of explanation is sufficient :)

Reverting is just another commit

Reverting a commit just creates an inverse-patch and tries to apply it, so that would mean git is looking for this:

     whatever
   </head>
   <body>
     <header>
       <h1>header Text</h1>
     </header>
     <main></main>          # <---
     <footer></footer>
   </body>

However the file in the HEAD has this content:

    something else
  </head>
  <body>
    <header>
      <h1>header Text</h1>
    </header>
    <main>                   # <--- The line `<main></main>` does not exist
      <h1>Main Text</h1>
    </main>
    <footer>
      <h1>Footer Text</h1>
    </footer>
  </body>

The lines its looking to find and replace do not exist. Git cannot determine, without human intervention, how to resolve this discrepancy - hence the patch does not apply cleanly and the resultant merge conflict.

Questions

reverting a commit, 2 or 3 commits back, also reverts any future commits (this might be expected behavior)

That's not what's happening - there is a merge conflict which needs manual intervention to resolve. It is coincidental that the entirety of the future commits are in the conflicted lines.

Have I made a mistake, is this expected behavior?

Getting a merge conflict in the circumstances described in the question is absolutely expected.

I was under the impression that by committing code in sections, you would be able to remove specific sections of code.

That'll certainly work if modified lines aren't right next to each other. E.g. if the initial commit had the file formatted like so:

  <body>
    <header>
    </header>
    <main>
    </main>
    <footer>
    </footer>
  </body>

There would not have been any conflicts with the changes described in the question.

Is there a way to only remove specific sections of code? i.e. a commit, while leaving any future commits intact?

Yes, the same steps in the question - but this will not avoid merge conflicts as they arise.

CodePudding user response:

You're looking for git-rebase.

You can move all of the commits after a given one to just before it, effectively removing it from the history.

git rebase --onto HASH_TO_REMOVE^ HASH_TO_REMOVE BRANCH_NAME

Here HASH_TO_REMOVE BRANCH_NAME refers to all of the commits from head, up to but not including HASH_TO_REMOVE. These are then placed onto the commit before HASH_TO_REMOVE called HASH_TO_REMOVE^.

If there are any conflicts in this, you'll still have to resolve them and then git rebase --continue to keep moving up the commits.

More explicitly, in your case you want:

git rebase --onto 0713432^ 0713432 dev
  •  Tags:  
  • git
  • Related