1

Let's assume a feature branch was deleted after it was merged to master.

A few days/weeks later, I have a need to generate patch files for all the commits that were in that feature branch.

If I had the feature branch around, I could use the following command to generate patches:

git format-patch $(git merge-base master my_feature_branch)..my_feature_branch

So, how could I re-create the feature branch so I can use the above command?

akirekadu
  • 2,244
  • 3
  • 24
  • 31
  • kind of an unusual problem; so you *know* that you need to hand someone a patch that describes everything that happened in that branch, but you can't remember when that started? – Marcus Müller Aug 01 '17 at 21:26
  • Do you want to know how to *find* the commit that was the latest commit in the feature branch, or how to *create a branch* on that commit once you have found it? – mkrieger1 Aug 01 '17 at 21:37

3 Answers3

8

Well, first, branches don't really get deleted. Only branch names get deleted. But this gets us into a thorny question: What exactly do we mean by "branch"?

Let's take a fast look at the process of merging. We start with a series of commits in the commit graph (or "DAG"; see the linked question) that look like this:

...--o--*-----o   <-- master
         \
          o--o--o--o--o   <-- feature

We then run:

git checkout master
git merge feature

which somehow figures out what we've changed in both master and feature since the last time we merged them (which was actually never, but they were together at one time, at the point marked * here). Git then makes a new merge commit that points back to both of these branch tip commits:

...--o--*-----o---------o   <-- master
         \             /
          o--o--o--o--o   <-- feature

and we have "a merge": a commit, of type merge commit.

We can now erase the word feature and the arrow, i.e., remove the name. The graph remains intact, retained by the name master:

...--o--*-----o---------M   <-- master
         \             /
          o--o--o--o--F

Should we wish to see what went into master via feature, all we have to do is find the commit I have labeled F (for Feature) here. Note that I have also labeled the merge commit M (for merge).

The way to find it is to start from master and work backwards until we find M. (It's right there at the tip of master right now, although later, it will be some number of steps back from the tip.) Then, we simply look at the second parent commit of M.

To find the second parent of a commit whose hash ID we know, we just tell Git: tell me the hash ID of the second parent of this other hash ID. The easy way to do this is with git rev-parse. Let's say the hash ID of M is badf00d:

git rev-parse badf00d^2

Git spits out the full hash ID of F. The hat-two suffix means "second parent" (hat-one, or just hat by itself, means "first parent").

Now we may also want to find commit *. That's the merge base of the commit that is the first parent of M, and this particular commit F that we just found. To find the merge base of two commits, we ask Git:

git merge-base badf00d^1 badf00d^2

We can then look at every commit in the range starting just after the merge base * and going up through and including commit F, using git log or git format-patch or whatever.

We can do this with the raw hashes, or we can point names (temporary or permanent, they will live exactly as long as you like) to commits M, F, and/or *, using git branch or git tag. Each name remembers the hash ID for you. The chief difference between a tag name and a branch name is that if you git checkout a tag name, you get a "detached HEAD" and are not on a branch, but if you git checkout a branch name, you get on that branch, and if you make new commits, they will cause that branch to advance:

$ git branch newname <hash-ID-of-commit-F>

...--o--*-----o---------M--o--o--o   <-- master
         \             /
          o--o--o--o--F   <-- newname

$ git checkout newname
... hack away ...
$ git commit ...

...--o--*-----o---------M--o--o--o   <-- master
         \             /
          o--o--o--o--F--o   <-- newname (HEAD)

This is all that branches are, in Git: the names just point to commits, while the branch structure, the history or DAGlet or whatever name you want, is formed by the permanent parts of the commit DAG. Branch names have the special feature that you can git checkout them and make them advance by running git commit.

torek
  • 448,244
  • 59
  • 642
  • 775
  • Very useful info. I understood various commands involved in finding commits in a merged branch. So far I have used either the IDE or 'tig' to find them. The tools that are widely used in the organization require a branch name as the input. Seems like one option is to write a script that will find commits F and *(taking M as the input), create a branch from * and cherry pick the range between * and F (inclusive). I would have been nice if the branch command supported an option to create a branch this way :) thanks – akirekadu Aug 02 '17 at 23:54
  • 1
    Incidentally, it's not at all clear to me why you would want to re-copy the commits leading to F. You can just make a new branch name pointing to commit `F`, check out the new branch by name, and make new commits on that branch. – torek Aug 03 '17 at 00:15
  • legacy tools. They use git merge-base between given branch and master to generate patch for all commits in the branch. Creating a branch off F would not return * as the merge base. – akirekadu Aug 11 '17 at 20:12
2

Let's assume the following simplified history: The feature branch was based on commit N and contained the commits P and Q. In the meantime, commit O was added to the master branch. The feature branch was merged in commit R. After that, commits S and T were created, which is where master is now.

M -- N -- O -- R -- S -- T [master]
      \       /
       P --- Q

You want to find commit Q. I don't think there is a programmatic way to do this, so you need to use any repository browser, for example gitk.

Once you have found Q, you can easily recreate the feature branch:

$ git branch my_feature_branch <hash of commit Q>

I'm not sure if you are aware of this, but a key to understanding this is knowing that a branch is not much more than a "pointer" or "bookmark" to a commit.

mkrieger1
  • 19,194
  • 5
  • 54
  • 65
  • Won't work. Refer to this image: http://imgur.com/a/0DL9l - merge base for 'my-feature-branch' there is 4fa54c2f300a6c7b0056568270c153b215d5b8d8 vs 46436103594f6bc35ce1106fc09fd702011b08ad for 'new-my-feature-branch' which is created from the commit ID as you suggested. – akirekadu Aug 01 '17 at 22:35
  • @akirekadu That's because you've recreated a different situation there. Note the `Fast-forward` when you merged. In the end you have a plain linear history `A - B - C - D` with `master` on `D` and `new_my_feature_branch` on `C`. – mkrieger1 Aug 02 '17 at 10:13
  • To create a real merge commit when fast-forwarding would be possible, use `git merge --no-ff`. – mkrieger1 Aug 02 '17 at 10:14
  • Sure, there will be a merge commit if you use '--no-ff'. Our CI system does not do that. Let's say we can change it. Still merge base will be different for 'my_feature_branch' and 'new_my_feature_branch'. Try it out.. – akirekadu Aug 02 '17 at 22:22
  • After reading @torek's answer I understand that what you proposed is a pointer in the right direction. His post offers all the info required to resurrect the feature branch in question, so decided accept that one. Than you. – akirekadu Aug 02 '17 at 23:57
0

If you have a base commit, the one from which the now-deleted feature branch had diverged, you can proceed like follows -

  1. Check out a branch based off the base commit.
  2. Using git cherry-pick, cherry-pick the merge commit that had merged the feature branch into master. If this doesn't work, you can cherry-pick individual commits that were part of the feature branch.
KunalG
  • 61
  • 8