2

I have a branch that got a faulty commit history due to a force-push in our master branch. Basicly I have the following commits in this branch history: A, B, C, D, E where E should be preserved but ABCD should be removed. They were added because of a master merge into the branch before those commits were force deleted from master origin. How can I accomplish this?

If A was the one to preserve I could just git reset --hard A but it's the other direction....

Romain Valeri
  • 19,645
  • 3
  • 36
  • 61
Joelgullander
  • 1,624
  • 2
  • 20
  • 46
  • Can you tell us more about the commits you want to remove? Are the `A` through `D` commits non-merge commits, or are they merge commits? – Tim Biegeleisen Jan 22 '19 at 14:52
  • ABCD is merged from master ( and not in the remote master anymore ) therefor should be removed. E is my latest commit and where HEAD is right now. I just want E to be in the branch history – Joelgullander Jan 22 '19 at 14:53
  • Just to be clear, do you want to revert the *changes* introduced in those commits (which would leave the commits in your repository), or would you rather get completely rid of the commits like if they never happened? – Lasse V. Karlsen Jan 22 '19 at 14:54
  • Get rid of them if it's safe.. :) @LasseVågsætherKarlsen – Joelgullander Jan 22 '19 at 14:55
  • In other words, do you want the end result of `@-A-B-C-D-E` to be `@-E` or `@-A-B-C-D-E-F-G` where `G` is "the opposite change of `A-B-C-D`" ? – Lasse V. Karlsen Jan 22 '19 at 14:55
  • Getting rid of them will rewrite history, which might require you to do another force push most likely, again, you're sure this is what you want? If so then you already got two answers using `git reset` which will help you with that. – Lasse V. Karlsen Jan 22 '19 at 14:55
  • Thanks for guiding me towards an answer @LasseVågsætherKarlsen – Joelgullander Jan 22 '19 at 15:14

3 Answers3

3
# make a backup of the current state of your branch
git branch backup your_branch

# reset to the commit prior to A
git reset --hard A^

# then re-apply E
git cherry-pick E

would be a way to do this. Rebase is another (see msanford's very detailed answer on the subject)

Romain Valeri
  • 19,645
  • 3
  • 36
  • 61
  • @TimBiegeleisen Yes, he can't do that because he would then lose E. Cherry-picking it back seems a good way to achieve this. No ? – Romain Valeri Jan 22 '19 at 14:52
  • Your answer would be rewriting the remote history, and in general, that is not the write thing to do. – Tim Biegeleisen Jan 22 '19 at 14:53
  • 1
    ABCD is merged from master ( and not in the remote master anymore ) therefor should be removed. E is my latest commit and where HEAD is right now. I just want E to be in the branch history – Joelgullander Jan 22 '19 at 14:53
  • 1
    @joelgullander That's what I understand, that's the need I address in my answer. – Romain Valeri Jan 22 '19 at 14:54
  • Thanks this worked. I'm always scared of doing --force stuff so i'm glad I got some help here. – Joelgullander Jan 22 '19 at 15:14
  • @joelgullander You can happily recover almost anything in git. However, see the note in my answer about force pushing. Above is by far the most straightforward solution though! – msanford Jan 22 '19 at 15:30
3

Option with rebase + drop

As Romain suggested "rebase is another", here is one way to do that, assuming you do want the end result of @-A-B-C-D-E to be @-E, as Lasse asked.

I offer this just as another tool in your toolbelt: it is not the simpler solution to this problem. It does however allow you to delete commits that are not in sequence (delete A, C, E and preserve B, D, for example):

git rebase -i HEAD~6

which will open your editor (probably vi) with a buffer that looks like this:

pick 4231648cb4 Some previous commit
pick 4bccf2ce81 A some message
pick 7b4cd5ff17 B some message
pick faa44efb7c C some message
pick 0ce0525a79 D some message
pick f104648cc3 E some message

# Rebase 76eb9131b5..ed71142fcb onto 4231648cb4 (6 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

Yes, the order of the commits from top to bottom is in reverse temporal order (the reverse of git log) with the most recent at the bottom. That's why "lines are executed from top to bottom" - from the oldest to the most recent.

Following the instructions, change the word pick to d (or drop) on the lines you want to remove.

pick 4231648cb4 Some previous commit
d 4bccf2ce81 A some message
d 7b4cd5ff17 B some message
d faa44efb7c C some message
d 0ce0525a79 D some message
pick f104648cc3 E some message

If you've made an unrecoverable mistake, like deleting a line, abort by quitting without saving (:q!) and try again.

If it all looks good, save and quit the buffer (:wq) and continue rebasing until your branch has been fixed up.

If something wonky happens after that (like you changed a commit hash to one that doesn't exist, or rebase stops to do something and you don't know why) you can abort the rebase completely with git rebase --abort which will bring you back to your initial state.

If your branch looks correct, force push.

git push -f

An important note on force pushing

Likely little-known, but the default push strategy before git 2 is matching, meaning when you git push, it will push all your local branches with matching remote branch names, not just your current branch.

So when you git push -f it will force push all of your branches (this happened to a colleague just yesterday). Check with git config --global push.default. That means that if you toyed with some other branch, it could force push it too.

I would suggest changing the default push strategy to simple if it's not already that. This is the default as of git 2.

Branch protection

If you're using a centrally-hosted git solution like Stash/BitBucket, Gitlab or Github, they all offer so-called "branch protection" rules to prevent, among other things, developers from force-pushing to branches.

Add a rule to prevent force pushing to master and probably release branches.

msanford
  • 11,803
  • 11
  • 66
  • 93
  • 2
    Thanks a lot for the part I had not the courage (and for parts, knowledge) to write myself! – Romain Valeri Jan 22 '19 at 15:24
  • 1
    @RomainValeri I'm happy to help! I'm actually preparing (to give) a git training session and this sort of thing helps me work through those issues. My main goal in giving my upcoming talk is "don't worry - there are 5 different ways to do almost anything in git, and you can do most of them with confidence that you can undo most screw-ups" – msanford Jan 22 '19 at 15:26
  • 1
    @RomainValeri Similarly, confronted with this problem myself I would have done what I wrote above, not even considering your much more straight-foward solution. Thanks for that! – msanford Jan 22 '19 at 15:28
2

You could use git reset --hard A^ to go to the last "good" commit and then git cherry-pick E to apply the commit you want to keep.

Then you have to force push it to the branch to reset things. Be sure to let everyone else on the team know what is going on.

Ben
  • 1,287
  • 15
  • 24