116

Is it possible to merge only the changes for a sub-directory from a local Git branch to a remote Git branch or is it "all or nothing"?

For example, I have:

branch-a
 - content-1
 - dir-1
   - content-2

and

branch-b
 - content-1
 - dir-1
   - `content-2

I only want to merge the contents of branch-a dir-1 with the contents of branch-b dir-1.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Brad Gessler
  • 2,744
  • 4
  • 23
  • 22
  • 1
    I think this is a duplicate of: http://stackoverflow.com/questions/449541/how-do-you-merge-selective-files-with-git-merge – Karl Voigtland Jul 31 '09 at 21:20

9 Answers9

87

Just as an alternative to the SO question "How do you merge selective files with git-merge?", I just found this GitHub thread which could be more adapted for merging a whole subdirectory, based on git read-tree:

  • My repository => cookbooks
    My repository target directory => cookbooks/cassandra

  • Remote repository => infochimps
    Remote repository source I want merged into cookbooks/cassandra => infochimps/cookbooks/cassandra

Here are the commands I used to merge them

  • Add the repository and fetch it

    git remote add -f infochimps git://github.com/infochimps cluster_chef.git
    
  • Perform the merge

    git merge --allow-unrelated-histories -s ours --no-commit infochimps/master
    

(this performs a merge by using the 'ours' strategy (-s ours), which discards changes from the source branch.
This records the fact that infochimps/master has been merged, without actually modifying any file in the target branch)

  • Merge only infochimps/cookbooks/cassandra into cassandra

    git read-tree --prefix=cassandra/ -u infochimps/master:cookbooks/cassandra
    

This reads the tree for only the required source subdirectory i.e. cookbooks/cassandra, on the upstream branch of the source repository.

Note that the target subdirectory name should also be cookbooks/cassandra, or you would see:

fatal: Not a valid object name
  • Commit the change

    git commit -m 'merging in infochimps cassandra'
    

Addendum

It's bizarre,[edit me] — but the read-tree step can possibly fail like this:

error: Entry 'infochimps/cookbooks/cassandra/README' overlaps with 'cookbooks/cassandra/README'. Cannot bind.

... even when both files are identical. This might help:

git rm -r cassandra
git read-tree --prefix=cassandra/ -u infochimps/master:cookbooks/cassandra

But off course, verify manually that this does what you want.

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • 3
    @Martin http://git-scm.com/docs/git-rev-parse#_specifying_revisions look for `:`, e.g. `HEAD:README`, `:README`, `master:./README` – VonC Sep 07 '12 at 13:43
  • 6
    The `git read-tree` step fails for me: `error: Entry 'foo/bar/baz.php' overlaps with 'bar/baz.php'. Cannot bind.` – Weston Ruter Apr 19 '13 at 19:07
  • 2
    @VonC but no, I need the history. That answer does not account for the case where there are local modifications to the files within the tree. So it needs to merge. – Weston Ruter Apr 19 '13 at 22:36
  • For me, the `overlaps with` error mentioned above pops up too, on **identical files**. How weird is that? – ulidtko Mar 15 '16 at 09:58
  • `-u` "After a successful merge, **u**pdate the files in the work tree with the result" – sam Jul 30 '16 at 19:13
  • @sam Sure, feel free to edit the answer to add more information – VonC Jul 30 '16 at 19:14
  • I'm trying to use this but for some reason I'm unable to update (this is: get changes made in upstream to my local repo). Every time I `git rm -rf`` my local copy and then `git read-tree` again from upstream I get a old version of the remote. Any ideas what I'm missing here? – omni Jun 30 '18 at 16:01
  • @masi Not sure. Maybe a git fetch from the upstream repo you are merging from. In the answer: `git fetch infochimps` *before* the `git merge` + `git read-tree`. – VonC Jun 30 '18 at 16:50
  • If you later go to merge the entire source branch into the target branch, it won't pick up any of the changes you "merged" by excluding. Using the OP's example, if you merge dir-1 from branch-a to branch-b and then later want to merge all changes from branch-a to branch-b, changes to content-1 that were made before your original merge will be ignored. So this answer may cause you grief down the road. – Scott May 17 '19 at 00:59
  • 1
    The initial merge appears to perform a merge by using the `ours` strategy (`-s ours`). This discards changes from the source branch. Why do we do this? – Chris Halcrow Jul 15 '20 at 06:48
  • 1
    @ChrisHalcrow To record the fact that `infochimps/master` has been merged, but without actually modifying any file in the target branch. Because the next step `git read-tree --prefix=cassandra` will do the modification. The final commit will record the actual "merge" content. – VonC Jul 15 '20 at 08:45
  • If you get the error `Not a valid object name` in the `git read-tree` step, check my question and answer here - https://stackoverflow.com/questions/62927619/git-error-not-a-valid-object-name-when-merging-subfolder-of-repository-into-sub/62927894?noredirect=1#comment111282402_62927894 – Chris Halcrow Jul 16 '20 at 23:55
45

For my example, assume you have a branch 'source' and a branch 'destination' which both reflect upstream versions of themselves (or not, if local only) and are pulled to the latest code. Let's say I want the subdirectory in the repository called newFeature which only exists in the 'source' branch.

git checkout destination
git checkout source newFeature/
git commit -am "Merged the new feature from source to destination branch."
git pull --rebase
git push

It is significantly less convoluted than everything else I've seen and this worked perfectly for me, found here.

Note that this isn't a 'real merge', so you won't have the commit information about newFeature in the destination branch, just the modifications to the files in that subdirectory. But since you're presumably going to merge the entire branch back over later, or discard it, that might not be an issue.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Chris Arena
  • 1,602
  • 15
  • 17
18

Given the OP's scenario where they have two branches, but want to merge only the history of dir-1 from branch-a into branch-b:

# Make sure you are in the branch with the changes you want
git checkout branch-a

# Split the desired folder into its own temporary branch
# This replays all commits, so it could take a while
git subtree split -P dir-1 -b temp-branch

# Enter the branch where you want to merge the desired changes into
git checkout branch-b

# Merge the changes from the temporary branch
git subtree merge -P dir-1 temp-branch

# Handle any conflicts
git mergetool

# Commit
git commit -am "Merged dir-1 changes from branch-a"

# Delete temp-branch
git branch -d temp-branch
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
xno
  • 1,225
  • 1
  • 13
  • 20
  • Can I do it with multiple directories? – nr5 Jan 27 '21 at 11:41
  • 2
    Does it leave me with subtree in my repo? – Emsi Mar 02 '21 at 16:00
  • @nr5 - you can likely just repeat this process for each directory you want to merge the history of – xno Apr 07 '21 at 17:39
  • `git-subtree` is not part of core git. For CentOS, you can get it via the `git-subtree` package. – rjh Mar 01 '23 at 01:35
  • I got an error because of unrelated histories, unfortunately `git subtree merge` does not seem to have an `--allow-unrelated-histories` switch, but I manage to use the command from [here](https://stackoverflow.com/a/66545626/1116364): `git merge -s subtree -Xsubtree=dir-1 temp-branch --allow-unrelated-histories` – Daniel Jour May 16 '23 at 11:16
5

I got this from a forum thread at Eclipse and it worked like a charm:

git checkout source-branch
git checkout target-branch <directories-or-files-you-do-**NOT**-want> 
git commit
git checkout target-branch
git merge source-branch
tobiasbayer
  • 10,269
  • 4
  • 46
  • 64
  • 1
    I tried this and ended up with a directory from the source-branch in the target-branch that I did not want. Despite having it specified with the set of dirs I did not want on that 2nd command. – marathon Feb 21 '17 at 09:26
  • 4
    This doesn't merge, it replaces the folder with the one from the source branch. – Gp2mv3 Apr 19 '19 at 14:33
  • @Gp2mv3 I think it looks solid. `checkout` makes the folders the same => difference is only un-`checkout` folders => `merge` difference. It's a valid strategy. – Caveman Feb 14 '20 at 14:48
4

Use git cherry-pick to select the commits you want and merge only these commits. The key trick here is to get these commits in an easy way (so that you don't have to figure them out by manually checking the Git log and entering them by hand). Here's how: use git log to print the commit's SHA-1 id, like this:

git log ^<commit-a> <commit-b> --pretty=format:"%h" --reverse -- <subdir>

'commit-a' is the commit immediately before the start point of the branch to merge, and 'commit-b' is the last commit on the branch to merge. '--reverse' prints these commits in reverse order for cherry-picking later.

Then do it like:

git cherry-pick $(git log ^<commit-a> <commit-b> --pretty=format:"%h" --reverse -- <subdir>)

It is two steps, simple and stable!

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Robert
  • 1,964
  • 1
  • 22
  • 22
  • excellent and only way to merge a single dir from another branch and preserve history. but... ...if you want just one commit, create a branch from all the commits from the first command, then merge that new branch in one squash merge.... – tom Jan 16 '19 at 20:08
  • 3
    This assumes the commits found only affect the directory in question. If a commit affects both the directory you want to merge and ones you don't, this will cherry-pick too much. – Scott May 17 '19 at 00:23
  • `Two steps, simple and stable!` not _simple_ at all :) – Rafa May 21 '19 at 13:08
  • So which way do you think it's simple? @Rafa – Robert May 23 '19 at 11:33
  • @Robert I didn't mean to imply your answer isn't simple; the fault is all on `git`, which has a terrible interface and terrible mental model full of mysteries and random names and meanings. – Rafa May 29 '19 at 23:00
2

Case 0: I didn't touch the files yet

git checkout origin/branch-with-the-code-you-want ./path/to/dir1

This gets you the folder as it is on the other branch as unstaged changes.

Case 1: Team has clean commits

If your team does clean commits, then digging around in the history for the commits could be fruitful. Use git cherry-pick COMMIT_HASH for those cases. You might want to git rebase -i HEAD~N where N is some number of commits to add them as pick COMMIT_HASH lines in the history if you need to.

Case 2: Clean commits are not important, but knowing the conflicts is

If you have all the commits all over the place then you likely won't need to keep the history, here's one approach that works for those cases. Note that this approach will not delete files or give you the history, but it will give you the conflicts between each file.

# Create an orphan branch with no history but your files
git checkout --orphan temp-branch

# Get the version of the files from the other branch
git checkout origin/branch-with-the-changes ./path/to/the/folder/you/want/to/merge
git commit -m "Commit that has all files like they are on your branch, except that one folder you want to merge"

# Merge the other file tree
git checkout your-branch
git merge temp-branch --allow-unrelated

# Clean up
git branch -D temp-branch
Caveman
  • 2,527
  • 1
  • 17
  • 18
1

Create a Git repository to contain both branch-a and branch-b:

git checkout branch-a
git diff branch-b dir-1 > a.diff
patch -R -p1 < a.diff
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Sun Yin
  • 35
  • 2
  • 14
    This answer needs more information. What of this is actual code vs comments? – qodeninja Oct 04 '13 at 21:35
  • 3
    The requestor wants to merge. Using a patch to carry over the changes automatically squashes all the commits into one patch and the history is lost. – Eric Oct 18 '18 at 11:35
  • The answer is short and clear. It clearly shows how to merge single directory and it is useful to me. The requester didn't tell that history is important. Moreover, if single directory is extracted from all commits, I will say that it means new history is started from scratch. – michaldo Mar 19 '21 at 14:01
  • This answer is really confusing. It tells to create a new repository but I don't see `git init`. It's unclear what the result will be. Will this generate a commit or leave changed files locally? – SandRock Sep 09 '22 at 15:24
0

The easy workaround, merge and reset the undesired changes, keep what you want

  1. You have a repository with directory structre git/src/a, git/src/b, you only want to merge the directory b
  2. git merge origin/a-feature-branch
  3. backup the the b directory changes, for example mv b b-merged
  4. reset the branch by command git reset HEAD --hard
  5. override the b directory with b-merged, mv b b-origin; mv b-merged b;
Sunding Wei
  • 1,803
  • 17
  • 12
0

This is how i would resolve it:

First find the last commit that both branches have in common, this is where the two branches start to diverge:

$ git merge-base branchA branchB
050dc022f3a65bdc78d97e2b1ac9b595a924c3f2

Then checkout a new branch from that commit:

git branch branchC 050dc022f3a65bdc78d97e2b1ac9b595a924c3f2

Then checkout the folder you want to merge from source branch into the new branch:

git checkout branchC
git checkout branchB ./path/to/the/folder/you/want/to/merge
git commit -m "update folder to merge from branchB"

Now branchC contains only changes from the folder you want to merge.

Finally merge those changes into target branch:

git checkout branchA
git merge branchC

Now you can handle any merge conflicts as you see fit.

herostwist
  • 3,778
  • 1
  • 26
  • 34