2

I'd like to cherry-pick single commits from on branch to another. I expect file renames to be quite common but still want to be able to apply changes without human intervention.

Since the built-in cherry-pick command doesn't seam to detect renames (at least in my test cases), especially when combined with modification to the renamed file.

I tried a bit and finally came up with a solution involving two rebase operations.

Let's assume I have a branch named target pointing at the commit I want to apply the cherry-pick to. The commit I want to cherry-pick is pointed to by a branch named source.

I then execute the following commands:

  1. create branch sourceTemp pointing at the same commit as source (since I want to retain the branch source )
  2. git rebase --strategy="recursive" --strategy-option="rename-threshold=30" target sourceTemp (maybe use another threshold; the test file was quite small and the changes therefore relatively large)
  3. git rebase --onto target sourceTemp~ sourceTemp

This applies only the changes introduced by the last commit in branch source to target.

I also put my test on github:

https://github.com/fraschfn/cherry-pick

What I like to know is, if this approach is feasible or if it only worked in my simple test setting!

Update: Alternative method

I rebase the patch to the merge base of source and target:

start situation

    A - B     <--- target
   /
  M 
   \
    C - D     <--- source

I want to cherry-pick D onto B.

  1. rebase D onto M after creating a new branch patch

      A - B     <--- target
     /
    M - D'      <--- patch
     \
      C - D     <--- source
    
  2. merge C and D' to obtain a replacement for source

    merge B and D' to obtain the patched version of target

       A - B    <--- target
      /     \
     /      E   <--- patched target
    /      / 
    M -  D'     <--- patch
     \    \
      \   F     <--- new source (same snapshot as source different history)
       \ /
        C - D   <--- source (will be discarded)
    

The advantage is that E and F can now be merged without a problem. Alternative way: Include the patch as early as possible in the hierarchy thus not creating D but directly D' and saving yourself the rebase.

The advantage above the previous version is that you can merge the two branches "new source" and "patched target" and it will work (if the merge of source and target would work of course) and not introduce the same changeset twice since git knows due to the merge operation which introduced the changeset into both branches.

Onur
  • 5,017
  • 5
  • 38
  • 54
  • I like your `--strategy-option="rename-threshold=30"` option, but when I try it on my own repo, I get `Unknown exit code (128) from command: git-merge-recursive 45e8a8d56a476c153abd338129d4a9e901619bc4^ -- HEAD 45e8a8d56a476c153abd338129d4a9e901619bc4`. – Leo Apr 13 '13 at 05:46

4 Answers4

7

Your rename-threshold approach is a viable one for what you're trying to do. That written, regular cherry-picks between branches is never a sustainable workflow, unless your branches are forked projects that will never merge. If that's the case, go forth and good luck. If you ever anticipate merging your branches back into a cohesive whole, I'd recommend changing the way you flow your code. Here are some great resources on that:

  1. The git project docs.

  2. The gitflow model, quite popular around these parts.

  3. ProGit's chapter on distributed workflows.

Regular cherry-picking between branches generates identical change sets with divergent SHA1 hashes. Scaled enough over a long enough time period, tracking code becomes difficult, understanding your history gets nearly impossible, and merging branches makes you feel like you blacked out and woke up in an M.C. Escher painting. It's just not a good time.

Based on your comments on this answer, your usecase sounds like a viable one for cherry-picks. In which case, I'd suggest a slightly less labor-intensive way to apply patch sets to renamed files:

git checkout branchB
git diff <commit>~1 <commit> |
    sed 's:<path_on_branchA>:<path_on_branchB>:g' |
    git apply

where <commit> is the commit you want to move from branchA to branchB. With this method you won't get the commit metadata, i.e. it'll just apply the change, it won't commit it. But you can just as easily manipulate the output of git format-patch with sed and git am. Just depends on what you want to do.

This will save you the hassle of generating a temp branch, picking the correct rename threshold, and rebasing. It's never going to be as clean as a straight up git merge, but I've used it before and it's pretty easy once you get the hang of it.

Christopher
  • 42,720
  • 11
  • 81
  • 99
  • I understand that cherry-picking, rebasing and this kind of stuff should be used with care and not the standard operation. What would you suggest if I want to maintain several "master" branches simultaneously (since there are indeed several software products) and I want to include a patch from one project to the others? – Onur Aug 02 '12 at 12:59
  • Are they distinct forks you won't want to merge? If so, your approach isn't a bad one. I'd personally maintain different repositories for each project, rather than different branches, and use something like github's or gitorious's merge requests or just `git diff | sed -e 's/one file/one other/' | git apply` if I didn't have a fancy git server. Really it's what makes the most sense to you. I just want to wave you off `cherry-pick` if you ever plan on merging the projects. – Christopher Aug 02 '12 at 13:20
  • Yes, I don't plan to ever merge them completely. They represent the software of variants of a family of machines. Since they machines' hardware cannot be "merged" ;-), the software for them won't so, too. But I want to somehow manage the common parts of the software. Since the software has to be laid out in a specific manner I don't have complete choice of methods. The software is kind of an early days Pascal and so sometimes different versions need to copy-paste-adjust'ed. For these files I'd like to be able to cherry-pick. – Onur Aug 02 '12 at 13:39
  • Seems reasonable then. I edited in one alternative method that might make your life a little easier, but for you use case cherry-pick seems viable. – Christopher Aug 02 '12 at 14:01
  • I discovered a new method and updated my question. This one involves merging and can cope with merging of the two branches. – Onur Aug 02 '12 at 14:30
2

git rebase uses the same rename detection logic as git merge does, and even the same option --strategy-option="rename-threshold=30" to control it.

What is happening here is that you first rebase the entire source branch onto your target branch (as sourceTemp), and the essentially cherry-pick the last commit on it onto target. Incidentally, git rebase -onto target src~ src is the same as doing git cherry-pick src on target: Take the one commit at src and apply it to target.

What changes in your workflow versus a direct cherry pick is that you gradually process the commits on the source branch which may catch renames more easily than doing it all in one step. If you directly cherry-pick then renames have to be recognized by looking at the difference between target and src~ which is potentially a long way; when first rebasing the entire source branch the renames are handled in small steps.

Andreas Krey
  • 6,426
  • 2
  • 18
  • 13
0
git checkout target

# Make an artificial merge to link target and source's parent
git merge source^ --strategy=ours

# Equivalent to cherry-pick but includes rename detection
git merge source --no-ff

# ... fix all merge conflicts and finish the merge

# Get rid of those artificial merges preserving file changes
git reset --soft HEAD~2

# Make a commit to store cherry-picked files
git commit -m "Cherry-pick source branch"
mnaoumov
  • 2,146
  • 2
  • 22
  • 31
0

Sometimes, files diverged too much so none of the rename algorithms can determine renames accurately, so I use a "hammer approach".

git checkout target

# get list of files modified in source branch
git diff source^ source --name-only

# ... (A) rename corresponding target files to match source's ones (possibly write script to automate this)

git add -A
git commit -m "Temporarily rename files for cherry-pick purposes"

# There is no need for rename detection anymore
git cherry-pick source

# ... resolve all conflicts and finish the cherry-pick

# ... rename files back, reverting (A) step. Note that you cannot use `git revert` here

git add -A
git commit -m "Rename files back"

# Get rid of the artificial commits made but preserve file changes
git reset --soft HEAD~3

# Make a commit to store cherry-picked files
git commit -m "Cherry-pick source branch"
mnaoumov
  • 2,146
  • 2
  • 22
  • 31