Search code examples
gitbranchgit-resetgit-revert

git - revert branch to look like master?


I have two branches develop and master. There are lots of commits in develop that are not in master yet. Though I need to make develop branch look exactly the same as master. To preserve all changes that happened into develop, I will create new branch from develop so all those changes won't be lost.

But after doing "copy" of develop, how can I safely reset or revert to look like master?

I saw this: Git: reset/revert a whole branch to another branches state?

So to reset, I can do:

git checkout develop
git reset --hard master

But the problem is develop branch is already pushed to remote and there others that had pulled develop already.

Maybe there is safer way to do this using revert or some other means? But I want to revert (if possible) in a way that would revert to master state, not manually selecting every commit, because some latest commits need to be kept in develop because they came from master (hotfixes).

So history of commits on develop looks something like this (most top means latest commit by date):

commit hotfix2 - in both develop and master
some other commits that are only in develop
commit hotfix1 - in both develop and master
some commits that are only in develop
all commits that came when develop was created from master

Solution

  • The standard process to undo commits in a non-destructive way is to use git revert. This command basically takes the inversed diff of a target commit and tries to apply it. So you get a new commit that undoes all the changes.

    In order to undo multiple commits at once, you can also specify a commit range. Given that you only have two ranges you would want to undo (those between those hotfixes), this would be actually manageable.

    You can also use the flag --no-commit, or -n, to not automatically create a commit. This allows you to chain multiple git revert -n <commit> commands after another without creating a reverting commit for each. Then, when you’re done selecting all commits or commit ranges you want to undo, you can make a single commit that combines them all.

    In your case, since you have another branch which has the exact (working directory) state you want to put your develop branch into, it is a lot easier to do that. All you have to do is to check out the working tree for master into your develop branch and commit that state onto the develop branch. You would do this using git checkout master -- .. Unfortunately, this will not work for paths that are unknown to the master branch. So if you added new files in the develop branch, those are kept and you would have to delete them separately.

    Instead, we start a new branch off master (which then has the exact same content), and reset that branch so it is based on develop instead. That way, we keep the working directory state from master but a commit would be follow develop instead. Afterwards, we can fast-forward develop that one commit:

    # checkout a new branch off master
    git checkout -b new-develop master
    
    # make a soft reset to develop
    git reset --soft develop
    
    # commit the changes
    git commit
    
    # get back to develop and fast forward
    git checkout develop
    git merge --ff-only new-develop
    git branch -d new-develop
    

    This will result in the same thing you would get by chaining git revert -n with all the commits that are exclusive to develop. There are a few other ways to reach that state, but that’s really the easiest.

    Regardless of which way you use to get to this state, you should consider making a merge afterwards. The merge will not actually do anything (since both branches have the same content) but it will combine the branches in the history, so you see that they actually converge.

    So assuming the history looks like this right originally:

                           master
                             ↓
    * ------------ h1 ------ h2
     \              \         \
      * -- * -- * -- * -- * -- *
                               ↑
                             develop
    

    You would want to turn it into this:

                                    master
                                      ↓
    * ------------ h1 ------ h2 ----- M
     \              \         \      /  ↖
      * -- * -- * -- * -- * -- * -- F   develop
    

    F being the fix commit we created above. This assumes that you would want to merge develop into master (git merge develop while on master) and then fast-forward develop (git merge master while on develop) to start the development work fresh from that point. Of course, you could also do it in the other direction if you prefer that.

    Alternatively, we can also do this merge M and the fix F in a single step. You would effectively merge develop into master and merge everything in the way that you end up with the contents of master. This would look like this:

                                    master
                                      ↓
    * ------------ h1 ------ h2 ---- FM
     \              \         \      /  ↖
      * -- * -- * -- * -- * -- * ---/   develop
    

    You can get there by hand like this:

    # since we merge into master, we start there
    git checkout master
    
    # start the merge, but don’t attempt to fast-forward and do not
    # commit the merge automatically (since we want to change it)
    git merge --no-ff --no-commit develop
    
    # reset the index that was prepared during the merge
    git reset
    
    # now checkout the files from master and commit the merge
    git checkout master -- .
    git add .
    git commit
    

    And actually, this is such a common scenario that git merge comes with a merge strategy that does exactly this. So instead of the above, we can just use the ours merge strategy and discard anything from the branch we merge into the current:

    git checkout master
    git merge -s ours develop