16

So imagine the following the happens (and that we're all using SourceTree):

  1. We're all working off origin/develop.
  2. I go on holiday for a week.
  3. My coworker has been working locally for the past several days without merging origin/develop back in to his local develop branch.
  4. He tries to do a push, gets told he has to merge first, and then does a pull.
  5. He gets a conflict, stopping the automatic commit-after-a-merge from proceeding.
  6. Assuming that Git is like SVN, my coworker discards the "new" files in his working copy and then commits the merge - wiping those "new" files from the head of origin/develop.
  7. A weeks worth of dev work goes on on top of that revision.
  8. I come back from holidays and find out that several days of my work is missing.

We're all very new to Git (this is our first project using it), but what I did to fix it was:

  1. Rename "develop" to "develop_old".
  2. Merge develop_old into a new branch "develop_new".
  3. Reset the develop_new branch to the last commit before the bad merge.
  4. Cherry pick each commit since then, one by one, resolving conflicts by hand.
  5. Push develop_old and develop_new up to the origin.

At this point, develop_new is, I'm hoping, a "good" copy of all of our changes with the subsequent weeks worth of work reapplied. I'm also assuming that "reverse commit" will do odd things on a merge, especially since the next few weeks worth of work is based on it - and since that merge contains a lot of things we do want along with stuff we don't.

I'm hoping that this never happens again, but if it does happen again, I'd like to know of an easier / better way of fixing things. Is there a better way of undoing a "bad" merge, when a lot of work has gone on in the repo based on that merge?

4
  • 1
    Could you post a screen shot of the git tree (redacted appropriately) or the output of your favorite git log format with appropriate annotations about what happened at the various commits? (I'd redact/annotate git log --graph --pretty=oneline --abbrev-commit and go from there)
    – user40980
    Commented Dec 30, 2013 at 4:11
  • 1
    It's too late for you, but unit tests would have caught that.
    – nalply
    Commented Jan 2, 2014 at 19:57
  • 2
    @nalply: Not if the bad merge also deleted the unit tests.
    – Aaronaught
    Commented Jan 9, 2014 at 2:34
  • Wow, that's really insidious bad luck.
    – nalply
    Commented Jan 9, 2014 at 9:04

3 Answers 3

8

If I understood correctly, this is your situation:

    ,-c--c--c--c--M--a--a--X ← develop
o--o--y--y--y--y-´

After some common history (o), you committed and pushed your work (y). Your coworker (c) did work on his local repository and did a bad merge (M). Afterwards there might be some additional commits (a) on top of M.

git reset --hard develop M^2
git branch coworker M^1

Now your graph looks exactly like before the bad merge:

    ,-c--c--c--c ← coworker
o--o--y--y--y--y ← develop

Do a good merge (G):

git checkout develop
git merge coworker

Resulting in:

    ,-c--c--c--c-、
o--o--y--y--y--y--G ← develop

Now transplant the additional commits:

git reset --hard X
git rebase --onto G M develop

This gives the final result:

    ,-c--c--c--c-、
o--o--y--y--y--y--G--a--a--X ← develop

Be aware that this might result in more merge conflicts. Also you just changed history, i.e. all your coworkers should clone/reset/rebase to the new history.

PS: of course you should replace G, M and X in your commands by the corresponding commit id.

2
  • 1
    It seems fairly evident in the question that all of these commits have already been pushed to a shared repository, so I don't think rebasing is a good choice. Reverting all the recent commits and then cherry-picking the reverted non-conflicting ones on top of the good merge is safer.
    – Aaronaught
    Commented Jan 9, 2014 at 2:34
  • 2
    That was the part with the clone/reset/rebase. - If there are only a few developers, telling them to update their repositories might be a valid option.
    – michas
    Commented Jan 9, 2014 at 2:38
2

It's good that you're thinking about how to fix the repository, but if your coworker only deleted new files and didn't overwrite a lot of updates, then a much simpler approach would be to simply restore the files that were deleted.

I'd probably start by trying to just cherry-pick (onto the current head) the original commits in which you added the code that has now gone missing. If for whatever reason that doesn't work, just check out the old revision with the files you added, and re-add them in a new commit.

What you describe is actually a subset of a well-known Git anti-pattern, which for the life of me I cannot remember the name of - but the gist of it is, making any "manual edits" during a Git merge (i.e. edits that aren't actually resolving any merge conflicts but are making some totally unrelated change) is considered an extremely bad practice because they are essentially masked by the merge itself, and are easily overlooked in code reviews or debugging sessions.

So definitely explain to your coworker that this was an epic fail, but consider slapping on a band-aid before prepping for major surgery.

2
  • I agree, if it is whole entire files that are gone one thing you can do is this if you don't care about history in the old files: 1.make a new branch from the currently messed up branch. 2.switch back to the bad branch and make another new branch that is your rescue branch 3. From your rescue branch go back to the commit before the fail commit. 4. Copy the deleted files somewhere outside of git 5. Switch to the other new branch. 6. Copy the new versions of the deleted files to the new branch and commit. There's a more complicated way that saves history that is a famous post by Linus T.
    – Elin
    Commented Jan 9, 2014 at 3:03
  • 1
    Actually this shows how you can use checkout jasonrudolph.com/blog/2009/02/25/… That's much nicer.
    – Elin
    Commented Jan 9, 2014 at 4:09
2

We had the same thing happen. To make matters worse, we work with documentation files and not software, so compiles don't fail and the problem goes undetected for weeks until someone lays eyeballs on something and recalls that something should be there but isn't.

Here was our starting graph when the problem was noticed:

t1--t2--c1--c2--c3
      \           \
       t3--t4--t5..cM--t6--t7--t8  ("master")

c1..c3 is two weeks of the coworker's local work. cM is the bad merge that effectively undid our team's work of t3..t5 during that same two weeks. t6..t8 represents another two weeks of work on top of the bad merge.

I'm still learning Git, but I approached this a bit differently. I created and checked out a new temporary "reapply" branch at the head of "master" at t8:

git checkout -b reapply

and the graph looks like this:

t1--t2--c1--c2--c3
      \           \
       t3--t4--t5..cM--t6--t7--t8  ("master")
                                 \
                               ("reapply")

I cherry-picked the undone commits t3..t5 into the "reapply" branch:

git cherry-pick t3^..t5 --allow-empty-message

(I'm not worried about commit messages because I squash everything from "reapply" into "master" at the end.)

If the cherry-pick runs into a conflict, I use git status to report the conflicting files, resolve the conflicts manually, then do

git add <conflicted_files>
git commit -m '' --allow-empty-message
git cherry-pick --continue --allow-empty-message

to continue. When cherry-picking completes, I check that the missing commits have been reapplied past the end of "master" in "reapply" with

git log --decorate --oneline --graph master^..reapply

and the graph should look like this:

t1--t2--x1--x2--x3
      \           \
       t3--t4--t5..xM--t6--t7--t8  ("master")
         \   \   \               \
          \   \   \               r1--r2--r3  ("reapply")
           \   \   \             /   /   /
            ----\---\-----------/   /   /
                 ----\--------------   /
                      -----------------

Now, I move back to "master", squash-merge the reapplied changes r1..r3 from "reapply" into a single commit with a suitable comment, and delete the "reapply" branch:

git checkout master
git merge --squash reapply
git commit -m 'reapply all changes lost by commit "xM"'
git branch -D reapply
git push

Now the graph looks like this:

t1--t2--x1--x2--x3
      \           \
       t3--t4--t5..xM--t6--t7--t8--t9  ("master")
                                   ^
                                   |
                              ("reapply" squashed
                             into a single commit)

and everyone in the team gets the work back next time they pull.

Would this have worked if our coworker discarded some but not all changes from t3..t5? In other words, is cherry-picking smart enough not to reapply a change that's already there? I'm not sure.

I don't know if this is the best solution. Other replies mention merging and rebasing in ways that I don't understand. But this was step-by-step, and I could understand what was happening, and it got us through this situation and I'm thankful for it.

Not the answer you're looking for? Browse other questions tagged or ask your own question.