Home > Enterprise >  Rollback git main branch prior to feature branch merge, do some work and merge the feature branch ba
Rollback git main branch prior to feature branch merge, do some work and merge the feature branch ba

Time:10-21

The main branch includes commits from a feature branch feature-A. I want to revert main branch to the commit before the feature branch code change was merged and deploy this code for testing. Let's say that during testing multiple bugs are reported and I push commits to main to fix the bug. When the main branch does not have any more bugs, I then want to re-merge the code changes from feature branch feature-A back into main.

Note: main is protected, so a pull request is needed to apply new changes.

Example:

(main) 1 - 2 - 3 - 4 - 5

Commits 3, 4, 5 includes feature-A

Now rollback main, so it becomes:

(main) 1 - 2

Apply bug fixes as part of commits 6 and 7

(main) 1 - 2 - 6 - 7

Now merge feature-A back to latest state of main

final state - (main) 1 - 2 - 6 - 7 - 3 - 4 - 5

What I have tried: I first created a backup of my main branch and called it main-backup

From this post: Want to change my master to an older commit, how can I do this? I tried the below which gives me a new branch prior to feature-A which can be merged to main.

If you want to avoid force pushing, here's how to revert your repo to an older commit and preserve all intervening work:

git checkout 307a5cd        # check out the commit that you want to reset to 
git checkout -b fixy        # create a branch named fixy to do the work
git merge -s ours master    # merge master's history without changing any files
git checkout master         # switch back to master
git merge fixy              # and merge in the fixed branch
git push                    # done, no need to force push!
Done! Replace 307a5cd with whatever commit you want in your repo.

(I know the first two lines can be combined, but I think that makes it less clear what's going on)

Here it is graphically:

c1 -- c2 -- c3 -- c4 -- c2' -- c5 ...
        \              /
         '------------'
You effectively remove c3 and c4 and set your project back to c2. However, c3 and c4 are still available in your project's history if you ever want to see them again.

So I create the PR and merge. Now main is at the commit before feature-A.

Now if I create a PR to merge main-backup into main it should show me all the code commits/changes related to feature-A but GitHub says there are no code changes.

So my understanding is not right here. What is the best (and possibly the safest) way to do this?

CodePudding user response:

Before I dive into the answer, I need to address this part, because I have to draw some diagrams and I like to draw correct and unambiguous ones:

Example:

(main) 1 - 2 - 3 - 4 - 5

Here, you have drawn a repository containing five commits total, all on a branch named main. There are a couple of issues I have with this particular drawing. One is quite minor, but might cause confusion later. The other is also minor but important.

The first is that while Git commits are numbered, they don't have simple sequential counting numbers. Their actual numbers are enormous (up to 2160-1 at the moment, and in the future, even bigger) and look random. As a result I think it's better to use letter codes for the commits: here I'd use A through E, or F through J, or some such.

The second issue—important, though still minor—is that a branch name like main isn't a label you should stick in front of commits. It's a label you should paste on one single commit, in this case, commit #5 or—as I will call it—commit E:

               main
                 |
                 v

 A <-B <-C <-D <-E

Note how each commit then points backwards to the previous commit.

For a somewhat more compact representation, I tend to use this:

A--B--C--D--E   <-- main

on StackOverflow. This drops the internal arrows (which isn't great, but is barely tolerable as we'll see).

The reason to draw them like this is that a branch name really does point to exactly one commit. That's the first crucial item to understanding what we can do with your existing repository and its commits. And, each commit in a chain of commits like this really does point backwards to the previous commit.

More precisely, the commits are, as we noted earlier, numbered. The actual numbers—expressed in hexadecimal—look bizarre and random, like f6b272b0c674c2d5022e90c3dec868af4ea26522 for instance. They're too difficult for humans to bother with. That's why we have the computer remember the last one in a chain using a branch name like main.

Each commit contains two things:

  • A commit has a full snapshot of every file. The files stored inside each commit are stored in a special, Git-only, compressed and de-duplicated format. This is something your computer cannot read in general; only Git can read these files. So you don't actually use these files: they are stored only for history purposes. They act like a permanent archive, like a zip or tar archive of every file.

  • And, each commit contains some metadata, or information about the commit itself. This includes the name of the person who made the commit, their email address, and a date-and-time stamp, for instance. But it also includes the raw hash ID of the earlier commit. Hence, commit E, whatever its hash ID is, literally contains the hash ID of earlier commit D (whatever that hash ID is).

The fact that a commit points backwards to its parent is what allows Git to get away with storing just one commit hash ID in a name like main. When main points to E, that's sufficient, because E points to D. Git can find D from main by going through E. Since D points backwards to C, Git can find C from main, by going back two hops ... and since C points backwards to B, Git can find B here as well, and that finds A (and then we run out of commits and git log stops here).

Note: main is protected, so a pull request is needed to apply new changes.

We now need one other fact about Git. I mentioned above that Git doesn't store changes, but rather snapshots called commits, and that Git finds commits by starting from a branch name, then working backwards as needed. The last thing you need to know is this: No commit can ever be changed.

But: if Git stores only snapshots, and no commit can ever be changed—and both of these are true—then how do we ever get any changes into a Git repository? The full and correct answer is long, so I'll take some extra time writing this up and cut it short:

  • We extract the last commit on some branch, using the branch name.
  • We then work with the extracted commit for a while, and eventually make a new commit: a new snapshot.

That is, given:

A--B--C--D--E   <-- main

we run git checkout main or git switch main to extract commit E, then we do our work, and then we run git commit to make a new commit F:

A--B--C--D--E   <-- main
             \
              F

In order to find commit F, we need a name—typically a branch name—so when we're using GitHub and protected branches, we make a new branch name along the way so that commit F is found by the new name:

A--B--C--D--E   <-- main
             \
              F   <-- feature

Then we push our own Git repository's new branch to GitHub, make a "pull request", and use the GitHub-specific pull request machinery to eventually incorporate either F or some copy of F (F') on GitHub. Again, there are a lot of details and normally I would go into all of them here, but I'm taking time to hold myself back.

  •  Tags:  
  • git
  • Related