1

We are missing some commits after merging a feature branch into a branch that has the same name as the now deleted branch that the feature branch came from.

To be a little more clear, here's the order of events:

  1. We had a repo with a Root and a Production Support branched from Root
  2. I branched from Production Support to create Feature Branch
  3. Time passed and work was done in the feature branch
  4. Someone deleted the Production Support branch and recreated a new branch with the same name off of the root
  5. I pulled the new Production Support branch to local
  6. I then merged Feature Branch into Production Support

After completing the merge I am missing commits in Production Support that are present in Feature Branch.

Edit: post-merge, I am missing some but not all commits from Feature Branch in Prod Support.

Am I right to think that the deletion of the Production Support branch could have caused this? Or should I look elsewhere?

jvhang
  • 747
  • 6
  • 19
  • Did you pushed 'feature branch' to a remote branch before remove the 'feature branch'? In this case, I suppose there is no way to recover. – Lazaro Fernandes Lima Suleiman Feb 04 '14 at 19:57
  • 1
    One question: how did you detect the missing commits? I've got no concrete suspicion at the moment but something's nagging at me here. Mistakes detected within the month can always be corrected (and often for much longer, gc isn't usually needed often) unless somebody explicitly asked for the git equivalent of a lobotomy, so @torek's answer will probably do it, ... ah: I've got it. separate comment coming. – jthill Feb 04 '14 at 20:54
  • You say the commits you're looking for are present on the feature branch ... did you detect this by getting the log for a particular file? If so, [try rerunning the command with `--full-history`](http://stackoverflow.com/a/19237066/1290731). – jthill Feb 04 '14 at 21:02
  • I could tell certain commits from Feature Branch weren't merged into Production Support because the features were missing or the bug(s) were present again. Looking at the code in Prod Support showed that really obvious changes were not merged in. – jvhang Feb 04 '14 at 21:24

3 Answers3

2

Merge does not delete commits. Merge either adds commits, or, in the case of a "fast forward" merge, simply "slides the label forward".

I find drawings of the commit graph, even (or maybe "especially") simplified ones, help a lot.

According to your description you had something like this:

..- o - o - o - o - o - o       <-- root
              \
                o - C - P       <-- production_support
                      \
                        o - o   <-- feature

where each o is an "uninteresting" commit (or maybe even quite a few commits) and the uppercase letters are commits that I intend to talk about below, so that I have given them letter-names.

Here there are three branches, whose branch-tips are labeled root, production_support, and feature. Commit C is the one common to both production_support and feature, and commit P is reachable only by the branch-tip-label production_support.

Again, according to your description, someone deleted the branch label production_support. The graph now looks like this:

..- o - o - o - o - o - o       <-- root
              \
                o - C - P       [no label - abandoned]
                      \
                        o - o   <-- feature

Commit P is now unreachable, except through git's "reflogs", which keep a record of where labels used to point.

The reflog for branch label production_support is deleted when the label is deleted, so that one is no help; but in at least one repository, at the time commit P was made, it was made on HEAD as well as on production_support. That particular HEAD-reflog-entry can protect P for a little while (30 days by default).

Once the last reference to a commit is gone, the commit is eligible for garbage collection. So if nothing else is hanging on to commit P, it will be collected with other garbage and discarded.

Going back to your description, after the above occurred (and possibly after P has been garbage collected as well), someone made a new branch label spelled the same way as the old branch label, and then made commits there, changing the picture to something like this:

                            o   <-- production_support
                          /
..- o - o - o - o - o - o       <-- root
              \
                o - C
                      \
                        o - o   <-- feature

You then did:

$ git checkout production_support && git merge feature

which made a new merge commit M (the graph now gets very messy :-) ... we could redraw it but that's probably just as bad, at this point):

                            o - M <-- production_support
                          /    /
..- o - o - o - o - o - o   <-------- root
              \               |
                o - C         |
                      \      /
                        o - o   <---- feature

It's true that commit P is (still) nowhere to be seen, but this is not because of the merge. The merge just added a new commit, M, with two parents. M's first parent is the old branch-tip (the o to the left of M; production_support now points to M instead) and its second parent is the merged-in commit (the commit to which feature points).

To get commit P back, if it exists at all (has not been garbage-collected), you must find it by some means other than a branch-label-name, because that went away when branch label production_support was first deleted. Look in the HEAD reflog on the repository on which it was created, or use git fsck to see if there are "dangling commits" that can be restored by adding a label (such as branch or tag name) to them.

In this particular case, where commit P has C as its immediate ancestor, if you find a commit by SHA-1 ID and think it might be P, you can check by looking at it and its parent(s). For instance:

$ git fsck
...
dangling commit de7c26f2b124f0038ab0ed03da9cb47647fc9867
...
$ git log de7c26f2b124f0038ab0ed03da9cb47647fc9867

This will show the log of "might-be-P" and its parents, and if commit C shows up that's probably it. Or, you can automate it by collecting all the candidates, finding the raw SHA-1 for commit C, and comparing the parents of each candidate:

$ id_for_c=...   # find SHA-1 for commit C, put that in here
$ for id in $(cat /tmp/candidates); do
> [ $(git rev-parse ${id}^) = $id_for_c ] && echo "likely match: ${id}"
> done

(not actually tested, could contain typos or similar).

If P really represents a whole series of commits, checking for C as immediate parent would be pointless (you'd need to know how far back to look for C) but you can still do an ancestry test (see the --is-ancestor option to git merge-base).

torek
  • 448,244
  • 59
  • 642
  • 775
  • +1 Great description! Indeed the simple graphs do tell much more than I managed to describe. I'll leave my answer, because as a git-novice, I found it "more safe" to mark the lost commits as a branch before I played with scripts or more advanced commands, where a small typo could break something ;) – quetzalcoatl Feb 04 '14 at 20:40
  • That's a really nice job. +1 from me too, I don't usually say it even when I upvote but this is just about the perfect explanation. – jthill Feb 04 '14 at 20:45
  • @quetzalcoatl: I like adding branch names too, on the (rare so far) occasion when I need to "resurrect" some commit(s). They're much more convenient than reflog entries. – torek Feb 04 '14 at 20:49
  • Thanks for the reply but there's a misunderstanding: you're trying to recover P from prod_support. I'm trying to figure out why interesting commits on Feature Branch are not present in Production Support after the merge. – jvhang Feb 04 '14 at 21:30
  • @Jelling: ok, how do you know they're not present? In particular what are you using to see what's "on" the branch? Note that you must follow the *second* parent of the merge to see commits that came from the merged-in (`feature`) branch. ...edit: Ah, per comment on question itself, you're talking about the file contents. Those depend on just how you did the merge. – torek Feb 04 '14 at 21:44
  • Can you elaborate on "how you did the merge"? I get that I could be missing stuff if there was a conflict and I didn't take the changes / manually merge them properly, but the issues I've seen have been entire commits. – jvhang Feb 04 '14 at 21:51
  • Yes, but not yet, have other thing right now. It would help if you have the merge command line still available (like, any `-s` or `--strategy` arguments or `-X` flags used). – torek Feb 04 '14 at 21:57
2

OK, everyone (all two of us :-) ) interpreted the question as "looking at the commit graph, some commits that I wanted to be ancestors of the merge-commit, are not ancestors of the merge-commit". But that's not what you're asking at all. You mean "I did a merge, which seemed to have gone well, but now the contents of the merge are not what I expected."

Let's do a quick once-over on the mechanics of a merge, in particular with how it's done in git. Let's start with this commit DAG, basically the same one as in my other answer, but I'll draw it a bit differently. I also changed the little o nodes to * so that the capital-O node is more obvious.

..- * - * - B - * - * - *   <-------- root
              \           \
                \           O   <---- production_support
                  \
                    * - * - T   <---- feature

To merge these you ran:

$ git checkout production_support; git merge feature

Important: I'm assuming no --strategy=ours or -X ours arguments here.

To do the merge git needs to identify three particular commits: "Ours", "Theirs", and the "Base". Commit O is just the tip of the current branch, i.e., the commit to which production_support points (because that was what you had checked-out when you start the merge). Commit T comes from the argument to git merge and again is just the commit itself.

Commit B is the tricky one. Git needs to find the "merge-base" for the two branches. To do this it walks backwards from the two identified commits, to find the "best common ancestor". In a simple graph like this, the "best common ancestor" is the very first common ancestor: it's obvious that the two branches diverge at B, so they're the same at B-and-before, and always different ever after. (In complex graphs with multiple cross-over points, sometimes there is no obvious best common ancestor. See the git-merge-base documentation for details.)

Having found the three commits, git does a standard three-way merge (see Stack Overflow and Wikipedia descriptions of three-way merging, for instance) of the three commits. But hey, wait, hold on a second. :-)

A three-way merge works on files, not on commits. What git does here is look at all the files in the three commits. In effect, it "checks out" all three commits B, O, and T. (These are numbered—1, 2, and 3 respectively—internally, and actually show up in the index/staging-area that way in the case of conflicts. See gitrevisions and its :n:path syntax.) Git also applies its rename-detection logic—the same thing that you see when you run git diff on two commits for instance—to guess whether file data-format.txt in the base version is now named doc/data-format.txt instead, for instance, in either O or T. Having run the rename detection and decided that the base data-format.txt is the same as the "theirs" data-format.txt but is now known as doc/data-format.txt in the "ours" version, git does the three-way merge on those three, using the name in the "ours" version as the "final" name.

Git will auto-detect some conflicts, and force you, the person doing the merge, to resolve those. But there's no guarantee that it will auto-detect every conflict ... and if the code bases have diverged "too much", it may mis-detect, or not detect, various file renamings as well. It's always up to you, the person doing the merge, to check whether the merge results are sensible, whether or not git was able to automatically resolve conflicts. For instance, if someone adds a a block of text to file A, and someone else deletes a block of text from file B, git can easily merge that—but if the new text in file A is a description of the stuff in file B, you now have a description of something that no longer exists.

There's an alternative (git-imerge, an "incremental" merger) that should in theory (I have not tested it) help out a lot in the case of gradually-diverging history. That still may not help as much as you'd like, depending on how various files were modified. There's no substitute for actually looking at the commits themselves.

In any case, if you don't like the results of the merge, i.e., the work-tree associated with the new commit M:

..- * - * - B - * - * - *   <-------- root
              \           \
                \           O - M <-- production_support
                  \           /
                    * - * - T   <---- feature

you can (as long as you haven't pushed this) (logically—commit M remains in the repository, referenced by the reflog, as described in the other answers) remove it, with git reset --hard production_support^, to make production_support point to commit O again instead. Then you can re-do the git merge (perhaps with --no-commit if needed) and fix up the work-tree this time, and git add the resulting files and then git commit that.

Community
  • 1
  • 1
torek
  • 448,244
  • 59
  • 642
  • 775
  • Thanks, this at least explains where things could have gone screwy. Do you recommend a particular merge tool for windows? We're using SourceTree so I didn't see any of the merge details other than conflicts. – jvhang Feb 05 '14 at 15:15
  • I do my best to avoid Windows entirely. For most of my work I just look at diffs directly in terminal-windows (primitive, but effective). – torek Feb 05 '14 at 19:47
1

Just let me warn you first: I'm no expert in Git. I just happened to "lose" a few things due my own stupidity (erm, that is: trying to use Git without reading the manual..) and had to recover them somehow.. Any hints/descriptions are very informal. You'll have to read more about what I say below

--

From the order of events you provided, I guess that:

  • old Prod branch has hash = AAAA
  • new Prod branch has hash = BBBB != AAAA
  • you first merged the Feature into Prod, and then you Pulled changes that replaced the Prod branch with newProdBranch

If I'm right, and you really first did the merge, then Pulled, then your local Git didn't know the new branch yet. It merged the Feature into AAA. Then you pulled and AAA was deleted and BBB was created.

If so, your goal here is now to find the lost AAA branch and to rename/mark it with some name other than "Prod" which now points to BBB. Then, you'll be able to move/copy/etc the changes from ProdOld into NewProd.

--

If you merged the branches locally, then you may still be able to recover it. Often, Git does not delete things immediatelly. You may still have everything on your local 'working copy', it may just be 'detached/orphaned'. It lost its name and is unconnected, so invisible, but if you knew its HASH, than you could simply check it out like a normal 'living' branch.

..well, of course unless you deleted and re-cloned for scratch it in the meantime. Or cleaned/pruned/GCed it up in any other way, which would cause the orphaned entries to be removed permanently..

First, try to remember as much as you can about the missing commits. Log messages, dates, ids/hashes maybe?

If you know hashes, that's great, skip to next point. If you don't know hashes, but you remember log messages, dates, or anything helpful at all, then try reflog command. It will show you all "recent" updates to any tips of any branches in the repository. Git often remembers old entries for a while. Even if they are abandoned/deleted/lost, they still may sit there. Simply run

git reflog

on your local working directory where you made the merge.
It will show you some commits and - most importantly - their hashes. If you didn't know the hashes of the lost commits - now you may learn them.

If the reflog doesn't have them, I don't know what to do. I remember I read that there was another, lower-level mechanism that can get you lost hashes, but I don't remember what was that, sorry.

Assuming that you now have the hash of the lost commit:

Check it out explicitely:

git checkout a76sa7..THE_HASH_HERE

Even if the commit was 'lost', if only the git repo was not 'pruned/GC'ed' yet, the commit will be fetched and current tip will point to it. Now create a branch from it, just to mark it and prevent from being pruned/GCed, for example:

git branch temp_restoring_commit

The new branch temp_restoring_commit will mark the commit for easier reference. From now on you can consider the commit to be 'recovered and safe' and it will not evaporate unless you remove the branch. (of course, still reflog may be able to help you again)

Now, you can try:

  • merging the commit somewhere else (to copy/port the changes to another branch)
  • rebasing the commit (to literally connect the lost commit to correct branch)
  • push it to remote repo so someone else can clean it up for you :)
  • etc.

As I said, I've did it a couple of times when I locally deleted a branch that was not yet pushed to production. I was lucky because the "damage" was fresh and the local repo was not "cleaned up" yet. If your local repo does not remember those commits anymore, you may be able to connect live to the Prod repo and try reflog/branching, but again, my knowledge ends here. Just don't try cloning the remote repo - it will not fetch "trash" like old unconnected lost commits. You need to put your hands on a "dirty" repository.

quetzalcoatl
  • 32,194
  • 8
  • 68
  • 107
  • There's a very useful side point here: it's possible that on some cloned repo where the branch was not explicitly deleted, the reflog for `production_support` still exists and holds on to the desired commit(s). – torek Feb 04 '14 at 20:37
  • Thanks for the reply. Re: "you first merged the Feature into Prod, and then you Pulled changes that replaced the Prod branch with newProdBranch", I don't think that's the case because if so I would be missing *all* of the new features developed in Feature Branch. Also, I'm using SourceTree and I'm pretty certain it prompts you to update the target before it will let you merge. – jvhang Feb 04 '14 at 21:37