0

I have a git look like this, for both remote and local.

A---B---C---D master

Now I want my master to be reverted at B commit, but I also don't want to lose my C and D commit.

Is there a way to make my git look like this for both remote and local?

      C---D new branch
     /
A---B master

I know it's not possible to backtrack to a previous point, maybe something like this also does the trick:

                          C---D new branch
                         /
A---B---C---D---revertToB master

Please give me some advice.

Hao Wu
  • 17,573
  • 6
  • 28
  • 60
  • Note down the commit SHAs of `C` and `D`. While on `master`, `git reset --hard HEAD~2`. Then `git checkout -b new-branch`, and `git cherry-pick` the aforementioned commits. Your local `master` will need to be force pushed to the remote, but the end state will be as you need it. – miqh Mar 13 '20 at 07:00

1 Answers1

1

You can get this:

     C--D   <-- new-branch
    /
A--B   <-- master

but to do that, you must force all Git repositories that have a branch named master pointing to existing commit D to retract their master to point to existing commit B. That can be difficult, since Git "likes" to add commits, but not to "remove" them.

To get this locally, assuming you have:

A--B--C--D   <-- master (HEAD)

run:

git branch new-branch master
git reset --hard HEAD~2

You now have:

     C--D   <-- new-branch
    /
A--B   <-- master (HEAD)

locally. Unfortunately, the other Git repository over at origin still has its master pointing to commit D. Any clones made from that other Git repository also have their master pointing to commit D. You must get all of those repositories to do the same kind of retraction. If there is only the one repository, and you have permission to use git push --force to retract it, you can now:

git push origin new-branch   # to create new-branch on origin
git push -f origin master    # to force them to retract their master

(You may want to add -u to the first git push.)


To get the safer thing you're looking for—adding a new commit to master that resets it back to the way it was at commit B—you can use:

git branch temp master

which sets things up to read:

A--B--C--D   <-- master (HEAD), temp

Then you can run git revert -n HEAD~2..HEAD; git commit, or git read-tree -u HEAD~2; git commit to make a new commit E whose snapshot matches that of B:

A--B--C--D   <-- temp
          \
           E   <-- master (HEAD)

Existing commits C-D are stuck where they are. If you want new-branch to be a descendant of master, you must now create it. Note that, if you are OK with new-branch pointing to existing commit D, you can just use the name new-branch instead of temp and you will have what you wanted. But if you need a new commit—let's call it F—that comes after E that matches the contents of commit D, you can create new-branch now:

git checkout -b new-branch master

giving:

A--B--C--D   <-- temp
          \
           E   <-- master, new-branch (HEAD)

Now you can:

  • revert the reversion commit E, or
  • git read-tree commit D and commit, or
  • git cherry-pick commits C and D.

To do the first, run git revert master or git revert HEAD (either suffices) and you have:

A--B--C--D   <-- temp
          \
           E   <-- master
            \
             F   <-- new-branch (HEAD)

To do the second, run git read-tree -u temp; git commit to make new commit F (you will need to enter a new commit message).

To do the third, run git cherry-pick temp~2..temp; this will produce:

A--B--C--D   <-- temp
          \
           E   <-- master
            \
             C'-D'  <-- new-branch (HEAD)

where the names C' and D' indicate that these are cherry-picked copies of C and D respectively. (When we reverted to make E we combined both undoing steps, using git revert -n, so E is not exactly an undo of either D or C, but rather an undo of both at once.)

It's now safe to remove the name temp as we do not need it to find C and D conveniently any more. (We did not need it all along, as we could have found them anyway, but it was convenient to have a name for commit D so that commits C and B were temp~2 and temp~1 respectively.)

It's now safe (and does not require a git push --force) to git push origin master new-branch.


Alternatively, you could, instead of creating temp, make the name new-branch point to commit D all along. That is, starting with:

A--B--C--D   <-- master (HEAD)

you could do:

git branch new-branch master

giving:

A--B--C--D   <-- master (HEAD), new-branch

Then you can "undo" the C-D commits on master as before, e.g., producing:

A--B--C--D   <-- new-branch
          \
           E   <-- master (HEAD)

As the next step, you could run:

git checkout new-branch; git merge -s ours master

to make Git create a new merge commit F whose snapshot matches that of existing commit D:

A--B--C--D---F   <-- new-branch
          \ /
           E   <-- master (HEAD)

Commit F is now ahead of master as before, but when reading backwards in a straight line, across first-parent links, it never visits commit E: Git walks back from F to D to C and so on. Again, this is provided you use --first-parent when having git log or git rev-list traverse back from F. By default, a traversal back from F visits both commits E and D (and since the default is to go by committer-date order, you'll see E, which we made later than D, before seeing D).

torek
  • 448,244
  • 59
  • 642
  • 775
  • Wow, that's a very thorough explanation, thank you very much! Also, is there a way to `cherry-pick` a series of commits? Like it's not just `C` or `D`, it's from `C` to `Z`, do I have to manually pick them one by one? – Hao Wu Mar 13 '20 at 07:43
  • 1
    Yes: `git cherry-pick` takes range arguments, as shown above with `temp~2..temp` for instance. Note that a range includes the final commit (`temp`) but excludes the first one (`temp~2`) so if you want to include commit `C` make sure your "stop" commit on the left is `B`, not `C`. – torek Mar 13 '20 at 08:51