0

The repository I'm working in uses Git's tagging feature to handle "releases" from our master branch.

I've just been asked to remove the commits associated with a previous release from master, but also preserve those commits on a different branch should they need to be brought back into master in the future.

This is an example of what the release history looks like:

  1. release-v3 (most recent)
  2. release-v2 (second most recent)
  3. release-v1 (third most recent)

where release-v2 contains the commits that need to be removed from master.

Ultimately, I think what I want to have happen is:

master contains: 1. release-v3 2. release-v1

preserved-branch contains the preserved commits: 1. release-v2

I'm not sure how best to proceed with this request. Will I eventually need to --force push over the current state of master?

ctrlaltdel
  • 685
  • 2
  • 10
  • 21
  • There's a lot going on here, but not a lot of detail. Yes, you will have to do a force push, and before that, probably a rebase. – Mad Physicist Jul 24 '18 at 16:12
  • 1
    Are all the commits squashed? Are the releases tags? Can you show a small sample representation of master? Look at https://stackoverflow.com/q/1628563/2988730 for a sample of how to format your sample. – Mad Physicist Jul 24 '18 at 16:14

2 Answers2

3

There are basically two ways to go about this. Each has pros and cons.

So let's draw up a simple approximate current state picture. I'm inferring, based on your question title, that the work is done on branches and master consists of merge commits from those branches. If so it might look a bit like this:

  A -- B    C    D -- E
 /      \  / \  /      \
O ------ M1 - M2 ------ M3 <--(master)
         ^    ^         ^
         v1   v2        v3

Now you want to update master so that it doesn't contain the changes from C, but you want to keep those changes around in case you need them in the future. Note that if changes in C overlap with changes in later commits, then every approach will involve some amount of conflict resolution.

With rebase

One option is to rewrite the history. That is the option that will involve a --force (or, preferably, --force-with-lease) push over master, and that in turn will require clean-up of everybody's local clones to avoid undoing the rewrite. Also, doing it is complicated by the presence of merge commits.

The first step is to create the new branch; easiest to do that before anything else is changed. So check out commit C. You'll need an expression that resolves to commit C - the commit ID is the most universal thing you could use, but in our example you could also use something like master^^2 (the 2nd parent of the 1st parent of the master tip commit).

git checkout master^^2
git branch v2-archive

Now you have

              C <--(v2-archive)
             / \
  A -- B    /   \    D -- E
 /      \  /     \  /      \
O ------ M1 ----- M2 ------ M3 <--(master)
         ^        ^         ^
         v1       v2        v3

and what's left is to rewrite master. The idea here is to rebase, but rebase doesn't mix well with merge commits.

You can try to use the --prseerve-merges option. If M3 (or, more generally, every merge being rewritten) was completed successfully using default merge settings, then this will probably be ok. The command would look like

git rebase --preserve-merges --onto master~2 master^ master

where master~2, master^, and master stand in as expressions that resolve to M1, M2, and M3 in our example. If it works, this will leave you with

  A -- B    /- D' -- E'
 /      \  /           \
O ------ M1  ---------- M3' <--(master)
         ^ \
         v1 \-- C  <--(v2-archive)
             \   \
              \-- M2 -- D -- E
                  ^  \        \
                  v2  \------- M2 
                               ^
                               v3

where D' and E' are the result of replaying changes from D and E onto the v1 commit, and M3' merges that back to master (essentially a rewrite of M3). Note that all the original commits are still present, and in particular the tags still point where the always pointed.

Now you have to decide what to do about tag v3. Normal convention is that tags don't move. My advice is to create a new version number and tag M3' accordingly. You might then choose to delete the v2 and v3 tags, or just leave them for archival purposes.

If that doesn't go smoothly, the other option (starting back at

              C <--(v2-archive)
             / \
  A -- B    /   \    D -- E
 /      \  /     \  /      \
O ------ M1 ----- M2 ------ M3 <--(master)
         ^        ^         ^
         v1       v2        v3

again), would be to create a temporary branch at the E commit

git checkout master^2
git branch temp

then rebase the temporary branch

git rebase --onto master~2 master^ temp

so you have

              C <--(v2-archive)
             / \
  A -- B    /   \    D -- E
 /      \  /     \  /      \
O ------ M1 ----- M2 ------ M3 <--(master)
         ^ \      ^         ^
         v1 \     v2        v3
             \
              D' -- E' <--(temp)

Next you have to push master back to M1, and then merge in the temp branch

git checkout master
git reset --hard master~2
git merge temp
git branch -D temp

taking care to account for any non-standard merge behaviors that were originally in M3. The end result is basically the same; it's just a more manual way of ensuring the merge is handled correctly. You still need to decide what to do about the tags, just as before.

Either way, at this point you have completed the rewrite. D', E', and M3' are all untested states of the code, and ideally they should all be validated to be sure they pass automated tests. Of course M3' must be re-tested, but if you want debugging tools like bisect to work well in the future, then you should also make sure the rewritten intermediate states are "clean".

And then you can do the force push and begin the clean-up.

With revert

The other option is to use git revert. This keeps the history as it is; so if the real goal is to have the next version of the product exclude v2 features, this is perhaps fine; but if the actual removal of the changes from history is a literal requirement, this isn't the way.

The big advantage is, no history rewrite means no force push and no recovery from upstream rebase for everyone on the team. Generally this should be the simpler procedure. The down-side is, you have to take an extra step to allow for future re-integration of the changes from v2.

So we're back at our initial state

  A -- B    C    D -- E
 /      \  / \  /      \
O ------ M1 - M2 ------ M3 <--(master)
         ^    ^         ^
         v1   v2        v3

Again we put a branch at C.

git checkout master^^2
git branch v2-archive

But then we proceed with

git revert -m1 master^

Note the -m1 option; this tells git to undo merge M2 from the perspective of its "first parent" - i.e. undo the changes from C. (In the example, you could just revert C instead of M2; but if the reverted branch really has many commits, then reverting the merge is more straightforward.)

So now you have

              C <--(v2-archive)
             / \
  A -- B    /   \    D -- E
 /      \  /     \  /      \
O ------ M1 ----- M2 ------ M3 -- !M2 <--(master)
         ^        ^         ^
         v1       v2        v3

where !M2 reverses all the changes that were introduced into master by M2. As with the history rewrite, you need to decide how you're going to handle tags. In this case maybe it's a little more clear why v2 and v3 might be best left alone (or just deleted), so you might want to create a new version number for !M2.

But now, if you try to merge v2-archive into master, git will find that everything is already up to date because C - the commit you're trying to re-integrate - is already "reachable" from master (by parent pointers). For this reason, the git docs say that reverting a merge means you're permanently discarding the changes from the branch; but there is a work-around.

git rebase -f v2-archive^ v2-archive

This creates a new copy of C that's basically identical to C, but it's not reachable from master

              C' <--(v2-archive)
             / 
  A -- B    /- C    D -- E
 /      \  /    \  /      \
O ------ M1 ---- M2 ------ M3 -- !M2 <--(master)
         ^       ^         ^
         v1      v2        v3
Mark Adelsberger
  • 42,148
  • 4
  • 35
  • 52
1

I would create reverse commits from the commits from release 2 and check them in to master.

This would give you a chance to explain what is happening (what changes you are reversing).

This also avoids rewriting history and --forcing anything.

tymtam
  • 31,798
  • 8
  • 86
  • 126