0

Looking at all the ways to revert or reset in Git when dealing with branches, tags, etc...I thought I had a good handle on it but it's getting confusing.

So what is the simplest sequence of commands I need to do the following?

  1. Revert master branch to an earlier state via a pull request. Presumably, I'd create a feature branch, revert it, and merge it via a pull request. (This revert includes reverting earlier merges.)
  2. Make further changes in a feature branch. Not sure if it matters for later steps whether I merge to master before and after, or only after.
  3. Push and then merge to master (via a Pull Request to get it into master, outside of Git)
  4. Then reinstate the later changes that I rolled back in step 1, resolving any conflicts, and also keeping the changes from steps 2-3.

I started down the road of git --no-commit revert commit-after-the-tag..HEAD but now I am worried that there will be no easy way to reinstate the changes, since my revert commit will be newer than the changes I want to reinstate. (Also, I'm having trouble reverting as the code to revert includes merges.)

So I think I want to revert to a tag, make further changes, then reinstate all the subsequent reverted changes. How do I do that?

Patrick Szalapski
  • 8,738
  • 11
  • 67
  • 129
  • Revert the revert. Your step 1 revert will add another set of commit(s), to get those changes back, revert them again later. – Lasse V. Karlsen Dec 05 '19 at 14:00
  • But it will be okay to revert the revert and keep all the "newest" changes too? – Patrick Szalapski Dec 05 '19 at 14:01
  • I think the cleanest is to create a new feature-branch-2 from your feature-branch (starting at your desired commit). Do all the work, merge feature-branch-2 to master. Then merge feature-branch to feature-branch-2 (or rebase if this is not published, then outside world will not see your tricks), and bring it to master again. – sbat Dec 05 '19 at 14:48
  • Reverting-the-reverts should also work fine, you should see revert conflicts if changes are conflicting with later changes (as asked here) https://stackoverflow.com/questions/6084483/what-should-i-do-when-git-revert-aborts-with-an-error-message . But do you really need to see the history of this "mixup" in the main repo? – sbat Dec 05 '19 at 14:55
  • Sbat, that won't work as we will still have the newer changes in master. I need to get master back to a earlier state, fix things, deploy, then reinstate the changes. Any other idea? – Patrick Szalapski Dec 05 '19 at 15:04
  • How about creating a new branch where master is currently at, then resetting master back to some earlier commit and force-pushing master? Then later, once you've fixed whatever is wrong with the changes in your new branch you can merge it back into master. This will temporarily make it so that master doesn't even have those commits. Would this be doable? – Lasse V. Karlsen Dec 05 '19 at 15:12
  • In the general case, I can see that working. But we have a tight policy that all changes to master must go through a pull request, so I can't force push anything to master. – Patrick Szalapski Dec 05 '19 at 15:20
  • " we will still have the newer changes in master" @PatrickSzalapski oh, so unwanted changes are in master? Then you are right, if master history is immutable, you have to make "reverting" commits. You should be able to "revert the reverts" as suggested. I would probably just create a test repo with a couple of TXT files, and double check it works as you expect and properly respects later changes/flags conflicts. – sbat Dec 05 '19 at 20:00
  • I tried, but there were too many conflicts to make the revert practical. I had to violate the policy and instead do a git reset. I would think there should be a easier way to revert without regard to conflicts...just revert to a certain commit's state, whatever it takes. Basically, I want the "revert" equivalent of a `git reset --hard`. – Patrick Szalapski Dec 05 '19 at 20:08
  • Reverting the revert works, but the hard part was getting a revert in the first place. I've added a solution that avoids using `git revert`. – Patrick Szalapski Dec 09 '19 at 20:01

2 Answers2

2

Here's a way--assuming your origin Git repo is in a system that supports pull requests, such as GitHub or Azure DevOps, and thus you want to review changes and avoid modifying history. If you don't have or don't want pull requests, you can instead use a simpler git reset --HARD approach.

Now I didn't like the behavior of trying git-revert, as it made me resolve conflicts one-by-one, even if earlier changes made the conflicts redundant. So the following is a better approach.

  1. Use git patch and git apply to rollback the changes in a new branch, noting the resulting commit hash
  2. Merge the rollback plus fixes (into master) using one or more pull requests (ordinary development workflow). Do the desired build, deploy, and iteration.
  3. When ready, revert the hash noted earlier to reinstate the changes and merge into master

This approach retains all history and does not require changes to the master pull request policies. git reset --hard is a bit simpler but requires a force-push, which we don't want.

Below are exactly the commands needed for this. If any command fails or gives warnings, DO NOT continue on; pay attention to them and resolve them.

Rollback

; ensure you are in a good state without other changes to confuse you
git checkout master && git pull

; create the patch that can roll back the code
git diff master [known-good-tag] > /some/path/tempfile.patch

; create the hotfix branch and push it to origin
git checkout -b [HOTFIX-BRANCH-NAME] && git push --set-upstream origin [HOTFIX-BRANCH-NAME]

; apply the patch in the new branch
git apply  /some/path/tempfile.patch

; commit the changes locally
git add -A   && git commit -m "rolling back newest changes"
; Note the resulting commit hash: later you'll have to revert this rollback commit ONLY.  If you forget it, you can find it in the git log history by the commit comment. 

; push the commit to origin
git push

; Make further changes.  Iterate as desired with ordinary workflow.
git add -A   && git commit -m "Fix to xyz because of abc"
git push

Merge rollback+fixes with a pull request. Multiple merges are okay too.

Revert the rollback, retaining newest fixes and reinstating recent development:

; ensure you are in a good state without other changes to confuse you   
git checkout master && git pull

; After deployment Create a new branch for reverting the rollback
git checkout -b [REINSTATE-BRANCH-NAME]  && git push --set-upstream origin [REINSTATE-BRANCH-NAME]

; revert the rollback commit noted earlier.  Note that `git revert` creates a commit.
git revert --no-edit [hash]


; Resolve conflicts if necessary. I like to use VS Code because it has easy conflict resolution tools.
code <FILE_WITH_CONFLICT>
git add <FILE_WITH_CONFLICT>

; finish the revert with the resolved conflicts. Iterate if needed.
git revert --continue

; push the completed revert and request another pull request to merge to master.  Then you are back to ordinary workflow.
git push

By the way, consider if you really need to do the above. There are many easier solutions in many cases, such as:

  • Do you just need to build and/or deploy an earlier version without changes? Most build systems let you build an earlier commit, or deploy an artifact that is already built.
  • Can you just fix the current state of master and deploy, including all recent changes? Use feature toggles or other disabling features as desired.
Patrick Szalapski
  • 8,738
  • 11
  • 67
  • 129
1

I assume you have the following history (time flows left-to-right):

--o--o--T--a--b--c--d    <- feature-branch
             /
 ...--x--x--x            <- commits merged into feature-branch

Notice that the fact that this is a "feature" branch is irrelevant.

T is the tagged commit that you want to go back to and build additional changes on, so that you arrive at the following history:

--o--o--T--E--F--G--a'--b'--c'--d'
                       /
           ...--x--x--x

You can do this with the following sequence of commands. Begin with a new temporary branch at the tagged commit:

git checkout -b tmp T

This rewinds your work (but not your branch) back to the tagged commit. Now you make the commits:

# edit
git commit   # E
# edit
git commit   # F
# edit
git commit   # G

You have now this history:

          E--F--G        <- tmp
         /
--o--o--T--a--b--c--d    <- feature-branch
             /
 ...--x--x--x

Finally, you use the "rebase-merges" mode of git rebase to arrive at the final history:

git checkout feature-branch
git rebase -i -r tmp

You no longer need the temporary branch and you can remove it:

git branch -d tmp

EDIT: Following the discussion in the comments, I suggest you do this:

Rename the branch tmp:

git branch -m tmp fixups

Do whatever is needed to get hold of the current master that you have to revert to an older state. Call that branch master-reverted. Then:

git rm -r .
git checkout T -- .
git commit -m "reverted back to tag T"

(The git rm . just ensures that no files are left over that have been added since T; you can skip the step if no files were added.)

Now merge the fixups into the branch:

git merge fixups

Now make a pull request to pull your branch master-reverted and deploy it.

Then revert the revert, but do that on a new branch:

git checkout -b master-new
git revert HEAD~

Now make a pull request to get master-new out. This is the final history:

 ...--x--x--x          
             \
--o--o--T--a--b--c--d          <- feature-branch
         \           \   
          E   ...--m--m       R' <- master-new
           \           \     /
            \            R--M  <- master-reverted
             \             /
              F-----------G   <- fixups (was tmp)

The --m--m is the upstream master branch. R is the revert of master, the R' is the reverted revert of master.

j6t
  • 9,150
  • 1
  • 15
  • 35
  • This looks promising; may I ask for some tweaks? After E, F, G, I want to push and do a pull request so that I can deploy from master. How would I do that? I believe I cannot, since master still has changes a' through d'. – Patrick Szalapski Dec 05 '19 at 15:53
  • If you haven't pushed the rebased branched, then master won't have a' to d'. But you sound like it has a through d. Then you shouldn't rebase them. My advice is to just merge E-F-G into master, for example via a pull request. (Just call the branch leading to G by a different, meaningful, name, not `tmp`.) – j6t Dec 05 '19 at 21:52
  • But If I merge E-F-G into master, it will also have a-b-c-d. I need to deploy from master without changes a-b-c-d. Any further ideas? (I've tweaked the question to be a little more precise.) This strikes me as a very common git scenario. I'm suprised there isn't a common way to do this. – Patrick Szalapski Dec 06 '19 at 17:49
  • So you want to throw away part of your current `master`'s history and replace it by a new one? Then you have to reset `master` to before the merge of a through d and rebuild it with the new history that has a' through d'. It is a fairly simple task to do with plain Git commands, but I do not know how you would do that with a "pull request" (which is a concept that lives outside of plain Git). – j6t Dec 06 '19 at 18:18
  • "It is a fairly simple task to do with plain Git commands" Yes, I'd like to do so with revert, push, and merge...you can ignore the pull request part if you wish. Assume only merges are allowed in master. – Patrick Szalapski Dec 09 '19 at 13:23
  • Most importantly, you would need `git reset`, i.e., you would have to rewrite the history of `master`. If you cannot (or are not allowed to) do that, you are asking for the impossible. It is simply not possible to insert commits before existing commits without rewriting history. – j6t Dec 09 '19 at 15:37
  • I don't want to rewrite history, I just want to revert the changes in a new commit, then revert the revert. Isn't there a way to revert to a tag, including reverting merges, bypassing conflicts (i.e. just get everything in the state it was in a earlier commit. This seems like a basic thing that almost any team that requires pull requests on master would need. – Patrick Szalapski Dec 09 '19 at 16:24
  • `git checkout master && git rm -r . && git checkout T -- . && git commit -m revert` should do the revert. Why you want to do that is beyond my comprehension. If I were in your position, I would just merge the side branch E-F-G into every branch that needs these fixups. – j6t Dec 09 '19 at 21:48
  • I have controls in place that require changes to master to be merged from a pull request only, so pushing committed changes to master directly is not allowed. Also, I thought I explained the need. I need to get master back to an earlier state, add a fix, deploy from master (with the fix but without later changes), and then reinstate the later changes, and I have controls in place that require changes to master to be merged from a pull request only. You seem to have a philosophical objection to such a thing, can you help me understand it? – Patrick Szalapski Dec 11 '19 at 16:15
  • Due to the controls, I can deploy only from master, not from master-reverted. (This is to ensure every change that could possibly get deployed is reviewed via a Pull Request. If I could deploy from another branch, I could deploy unscruitized changes) – Patrick Szalapski Dec 11 '19 at 22:51
  • 1
    My extended suggestion is almost exactly what you did in your own answer. Which I didn't see until now for some reason. Oh well... I would have saved my breath if I had. A thumbs-up would be appreciated. – j6t Dec 12 '19 at 06:24
  • Yes, thanks very much. Your answer helped me think it through. – Patrick Szalapski Dec 12 '19 at 14:38