36

I've seen different posts on StackOverflow that explain cherry picking a bit, but the comments in their code aren't very specific as to what's a branch and what's a directory. Example git checkout A -- X Y doesn't tell me much.

Basically I want this:

  • Create new branch featureA off of master
  • Merge directory /tools/my-tool from branch dev into featureA
daleyjem
  • 2,335
  • 1
  • 23
  • 34
  • 4
    General rule, when there's a `--` in a git command like that, on the left is a branch name or commit specifier, on the right are paths. – torek Nov 06 '13 at 20:39
  • Why do you need cherrypicking for that? You can simply create manually merge the directory. – Max Yankov Nov 06 '13 at 20:48
  • 4
    thanks @torek. what i ended up doing was a `git checkout dev -- tools/my-tool` – daleyjem Nov 06 '13 at 22:56

6 Answers6

37

Here is the right way to cherry-pick commits from another branch for one folder:

git format-patch -k --stdout master...featureA -- tools/mytool | git am -3 -k

This will apply the patches to the "tools/mytool" files only, in order.

If you have a merge conflict on any commit, it will pause for you to fix it. git am --continue will resume where it left off.

See also Git documentation for:

akaihola
  • 26,309
  • 7
  • 59
  • 69
Sean Dunlap
  • 471
  • 4
  • 3
  • 2
    @daleyjem This truly deserves to be accepted ✅ as the right answer. It does exactly what you want. `Checkout checkout A -- X` works if only new files are created, and none deleted, between your version of X and A's. – Inigo May 31 '21 at 00:38
  • @sean-dunap, this deserves to be written up as an answer to a well written question. If one doesn't exist, let me know and I will write one! – Inigo May 31 '21 at 00:40
  • This is definitely a lot closer to what I was looking for than the most upvoted answer. +1 – BHarms Apr 18 '22 at 23:21
  • Similar, working around the limitations of `git format-patch` that merge commits are ripped apart: https://stackoverflow.com/a/8840381/946850. That answer shows how to create one commit per top-level-commit. – krlmlr Aug 21 '23 at 16:32
35

To answer the original question about how to cherry-pick some directories (as commits instead of a brute-force checkout), this is possible. Imagine that featureA has diverged from master and you want to bring over the tools/my-tool commits.

Assuming that you never made any commits that contain both stuff from /tools/my-tool and stuff from other directories

This will get you the list of commits to master in tools/my-tool (that are not already in featureA), in reverse-chronological order:

git log --no-merges featureA...master tools/my-tool

To say it another way:

git log --no-merges source_branch...dest_branch my/firstpath my/secondpath [...]

To get just the commits you need in chronological order, you need to first reverse the order of the input lines (such as with tail -r or tac), then isolate the column for the commit hash (such as with cut):

git log --format=oneline --no-merges featureA...master tools/my-tool \
    | tail -r \
    | cut -d " " -f 1

And to do the whole operation at once, do this:

git cherry-pick $(git log --format=oneline --no-merges featureA...master tools/my-tool | tail -r | cut -d " " -f 1)
Ian
  • 11,280
  • 3
  • 36
  • 58
  • 8
    tail -r is not available on linux, "tac" is an alternative to tail -r – kokorins Jan 31 '17 at 14:00
  • A slight tweaked change to the chronological ordering - `git log --pretty=tformat:"%h" --no-merges featureA...master tools/my-too | tail -r` – rh0dium Jul 23 '17 at 16:04
  • This seems to include all the commits that are alreay applied in the feature branch. How can you get just the commits that aren't alreay merged? – Chris Dodd Aug 01 '18 at 23:02
  • @ChrisDodd I don't think I understand your question. If you're asking how to list only commits from one branch that don't exist in another, that can be done as a separate step. – Ian Aug 03 '18 at 12:23
  • I'm trying to answer the question in the title -- how do I find all the changes in one branch that are not in a second branch (need to be merged to bring it up to date) AND are in a particular subdirectory. Like what git cherry does, but restricted to a particular subdirectory, – Chris Dodd Aug 13 '18 at 19:56
  • As [this other answer](https://stackoverflow.com/a/19830479/2063546) pointed out, commits don't have a notion of "path". This is the first workaround I could come up with. Brute-force checkout of the final branch (losing all history in between) is the easiest option. There is probably another option that would effectively work like an interactive rebase (where you just delete the changes outside of the subdir you care about). Look at `git filter-branch` and `git for-each-ref`. – Ian Aug 15 '18 at 01:09
  • 5
    Instead of using `tac` or `tail -r` and `cut`, one can also use `git rev-list --no-merges --reverse` to just get a list of revision IDs/hashes in reverse order directly. – Achilleas May 22 '19 at 20:14
  • That's true, there are more efficient ways to run this command. For myself, the most important thing is to be able to build up the command little by little and make sure that I both understand and approve of what is going to happen when I run the entire thing. – Ian May 23 '19 at 12:35
17

Note:

  • git cherry-pick is about applying a full commit (or commits) to another branch. There is no notion of "path".
  • git checkout is about updating the working tree (and HEAD if no path is specified, effectively switching branches)

    git checkout [-p|--patch] [<tree-ish>] [--] <pathspec>...
    

When <paths> or --patch are given, git checkout does not switch branches.
It updates the named paths in the working tree from the index file or from a named <tree-ish> (most often a commit). The <tree-ish> argument can be used to specify a specific tree-ish (i.e. commit, tag or tree) to update the index for the given paths before updating the working tree.

Your git checkout dev -- tools/my-tool updates a specific path, but it isn't a "merge" or a "git cherry-pick".

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
13

You can use git checkout <from_branch> -- <files_to_bring>.
I'd do this: git checkout dev -- tools/my-tool

Explanation: this tells git to replace/copy files from the branch dev and the path tools/my-tool to your current branch.

funerr
  • 7,212
  • 14
  • 81
  • 129
1

Jason Rudolph does a great job of summarising this scenario, and the accepted solution in this post:

https://jasonrudolph.com/blog/2009/02/25/git-tip-how-to-merge-specific-files-from-another-branch/

This is a good old question, and the above answers did a great job of answering it, but since I recently came across the issue, and his article stated it so concisely I thought I'd share it here.

Fehr
  • 476
  • 4
  • 5
  • That solution, while interesting, obliterates commit history. If I just wanted the latest versions of those files, I could do it with file history in my IDE. – Jason Feb 12 '22 at 00:57
1

For anyone needing to also exclude some files or directories when moving commits from a repo to another, be aware that you can use Sean Dunlap's recipe and add exclusion rules. So the reverse of his example, to include everything but the tools/mytool directory, would be:

git format-patch -k --stdout \
  master...featureA \
  -- . ':!tools/mytool' \
  | git am -3 -k

The . after -- first includes everything in the repository. ':!tools/mytool' then excludes that one directory (you can also use the long form ':(exclude)tools/mytool').

(Thanks to Nikita Kouevda for the tip in this Reddit comment.)

See also: Git <pathspec> documentation for :(exclude) and :!

akaihola
  • 26,309
  • 7
  • 59
  • 69