5

Here's my situation: git merge master results in 50 files with merge conflicts. I want 45 of them to just be accepted from master and be done, and I want to manually resolve conflicts in the remaining 5. All 45 of those files are in directory some/dir. The other 5 are scattered elsewhere. IF I wanted just to accept master's changes for all 50 conflicts, I'd run this:

git merge -X theirs master
# OR (same thing):
git merge --strategy-option theirs master

But I don't want that, like I said. I'd like something more like this:

git merge -X theirs master some/dir

so that it automatically chooses "theirs" (master's) side for all conflicts in some/dir, but otherwise let's me manually fix the conflicts. This, however, doesn't exist.

So, my work-around is this:

SHORT DESCRIPTION:

Start a merge, manually fix just the few files I need to, among the many many conflicting files. Back up any files I just fixed. Abort the merge. Redo the merge with git merge -X theirs master to automatically accept master's changes for ALL conflicts in ALL files. Manually copy my few manually-fixed files back into the repo, and commit them.

This avoids having to manually fix 50 files, which can be very tedious and time-consuming when only 5 really require my attention.

FULL, DETAILED STEPS:

  1. Start a normal merge:

     git merge master
    
  2. Manually resolve conflicts in just the 5 files I want to do that in, and save each file. Then, MAKE COPIES OF THEM to a location outside of the repo (or at least in a folder that is ignored by the repo). I now have copies of those 5 files I manually resolved. Here is one way to accomplish this:

     mkdir -p temp  # create `temp` dir inside the repo.
    
     # Note: if you're not aware, the ".git/info/exclude" file in your repo is
     # an **untracked** form of a ".gitignore" file for the repo. We will use 
     # this file below.
    
     # Add a `/temp/` entry to the ".git/info/exclude" file to keep
     # the repo from tracking the /temp/ dir, but withOUT using the
     # shared ".gitignore" file since this is my own personal setting
     # not everyone else on the team necessarily wants.
     echo -e "\n# don't track this temporary folder for my arbitrary use\n/temp/" \
     >> .git/info/exclude
    
     # Now manually make copies of your 5 files you just resolved conflicts in:
     cp some/dir/file1.cpp temp
     cp some/dir/file2.cpp temp
     cp some/dir/file3.cpp temp
     cp some/dir/file4.cpp temp
     cp some/dir/file5.cpp temp
    
  3. Do git merge --abort to abort the merge, then do git merge -X theirs master to redo the merge, except this time automatically accepting all of master's changes for all 50 files (ie: for all conflicts).

  4. Now, manually copy my manually-resolved backup copies from above back into the repo on top of their respective files:

     cp temp/file1.cpp some/dir
     cp temp/file2.cpp some/dir
     cp temp/file3.cpp some/dir
     cp temp/file4.cpp some/dir
     cp temp/file5.cpp some/dir
    
  5. Finally, do git add -A and git commit --amend to amend them to the merge commit, and voila! I have the equivalent of an automatic resolution on the 45 files, and the manual resolution on the 5 files.

Is there a better way?

I think this way works pretty well and is pretty effective, but I'm open to learning alternatives, especially if they are faster or easier.

Update: here is the answer. The currently-most-upvoted answer is in fact not correct, and produces the wrong behavior under certain really important circumstances, despite being the most-upvoted.

Related, but not duplicates:

  1. Merging two branches, how do I accept one branch for all conflicts
  2. Simple tool to 'accept theirs' or 'accept mine' on a whole file using git
Gabriel Staples
  • 36,492
  • 15
  • 194
  • 265
  • I think you might be looking for [this answer](https://stackoverflow.com/a/55963225/1290731), get the remaining unmerged files from e.g. `git ls-files -u`, run the scripted resolver inplace on each of them, add the result. – jthill Sep 08 '20 at 22:24

3 Answers3

7

You can checkout the files from the other branch directly when resolving the conflict.

So after doing git merge and resolve the conflicts for the files that you need to fix.

Do git checkout <branch you're merging> -- some/dir this will move the files over from the other branch with those changes only. I believe it will also make them ready to commit.

If there are changes from the ours branch, you can just list the files after the -- rather than just checking out the whole dir.

Schleis
  • 41,516
  • 7
  • 68
  • 87
  • See also the **Merge conflict resolution examples** section of my answer here: [Who is “us” and who is “them” according to Git?](https://stackoverflow.com/a/63911630/4561887). – Gabriel Staples Feb 11 '21 at 19:51
  • 1
    This works great by the way. I just tested it! I never knew you could pass directories like `some/dir` to git. I always thought you had to specify files individually. – Gabriel Staples Feb 11 '21 at 20:08
  • 1
    @GabrielStaples the one caveat with directories is that they cannot be empty. Git only tracks files so empty directories will cause problems. – Schleis Feb 12 '21 at 15:20
  • Schleis, it turns out your answer isn't right after-all, unfortunately. I have detailed the problem and the correct solution now [in my new answer here](https://stackoverflow.com/a/66466786/4561887). – Gabriel Staples Mar 04 '21 at 00:17
3

After upvoting and marking correct this answer 6 months ago, I realized today after a very large and very botched merge where this was needed that that answer is wrong. Let's say you did this, to update your feature_branch with the latest changes from master:

git checkout feature_branch
git merge master
# conflicts result...

...and there are a ton of conflicts. You see like 25 of them are inside some/dir, and you want to keep all conflicting changes from feature_branch, so you do any of these 3 commands (all the same thing in this case):

git checkout -- some/dir
# OR
git checkout HEAD -- some/dir
# OR
git checkout feature_branch -- some/dir

Well, you just messed up! master (the --theirs side for git merge) had also added some changes and new files inside some/dir, many of which had ZERO CONFLICTS with your feature_branch changes (the --ours side for git merge), and all of which you want (this is the whole point of merging latest master into your feature_branch!) but now they are all gone because you just OVERWROTE THEM with your some/dir from feature_branch. That's not what we want at all!

So, here's the correct answer:

# RIGHT ANSWER
# NB: you must be **in the middle of resolving conflicts in a `git merge`**
# in order for this to have the behavior I describe here. 

# **Keep conflicting changes** for all conflicts within files inside
# some/dir, from the "--ours" (`feature_branch` in this case) side.
# This does NOT delete files added by the `master` (`--theirs`) side which 
# have **no conflicts** with your `feature_branch` (`--ours`) side. 
git checkout --ours -- some/dir
git add some/dir
git merge --continue

Again, that is NOT the same thing as this:

# WRONG ANSWER

# **Overwrite this entire `some/dir` directory** with everything from
# the `feature_branch` (`--ours`) side. 
# This DOES delete files added by the `master` (`--theirs`) side which 
# have **no conflicts** with your `feature_branch` (`--ours`) side,
# which is NOT what we want.
git checkout feature_branch -- some/dir
git add some/dir
git merge --continue

So, here's the right answer in full context:

# merge latest master into feature_branch to get those upstream changes
# from other people into your feature_branch
git fetch origin master:master
git checkout feature_branch
git merge master 

# conflicts result here...

# Keep all changes from `feature_branch` for conflicts in some/dir
git checkout --ours -- some/dir
# OR, keep all changes from `master` for conflicts in some/dir
git checkout --theirs -- some/dir

git add some/dir
git merge --continue

Done!

For details and clarity, see my answer here: Who is "us" and who is "them" according to Git?. This is covered in the "WARNING WARNING WARNING" section of that answer, although this answer is a more-thorough example of it, and better shows the problem.

Gabriel Staples
  • 36,492
  • 15
  • 194
  • 265
  • This doesn't actually MERGE the differences. It simply resolves the merge by choosing theirs or ours. That's a big distinction! – onlinespending Mar 18 '22 at 22:15
  • 2
    @onlinespending, looking at your comment & downvote on my answer, I think you misunderstand my question _and_ my answer. My answer is in fact the right answer, and the currently-most-upvoted answer is in fact incorrect. My question states: _How do I accept git merge conflicts from "their" branch for only a certain directory?_--meaning: merge everything _except_ a certain directory, for which I will accept only one side or the other. My answer shows exactly that: merge everything except the content of `some/dir`, for which dir we will only accept one side or the other (`--ours` or `--theirs`). – Gabriel Staples Mar 18 '22 at 23:52
  • it's not a 3-way merge which considers the common ancestor. That's the problem. – onlinespending Mar 19 '22 at 00:08
  • 1
    @onlinespending, what do you mean exactly? I don't want a 3-way merge. I want to merge latest `master` into my `feature_branch`, merging both sides for all files except those in `some/dir`, which I want to just keep from one side only _in the case of conflicts_. Are you trying to solve a different problem? – Gabriel Staples Mar 19 '22 at 00:11
  • what do you think happens when you do a 'git merge'? it uses the common ancestor to understand non-conflicting changes made to each version of the files in the divergent branches. unfortunately 'git checkout' will simply choose one over the other, destroying non-conflicting changes. – onlinespending Mar 19 '22 at 00:14
  • @onlinespending, that's exactly what I am saying in my answer. `git checkout feature_branch -- some/dir` is _wrong_, because it destroys non-conflicting changes. `git checkout --ours -- some/dir` is _right_, because it does _not_ destroy non-conflicting changes. That's the whole point of my answer. – Gabriel Staples Mar 19 '22 at 00:17
  • try it. 'git checkout theirbranch --theirs -- somefile' will destroy any changes you've made to the common ancestor version of somefile with whatever theirbranch's version of somefile is. What version of git are you using? I'm on 2.28.0. Maybe I need to update? – onlinespending Mar 19 '22 at 00:20
  • NOPE. that wasn't it. Installed 2.35.1 and it results in the same behavior. – onlinespending Mar 19 '22 at 00:29
  • @onlinespending, my `git --version` is `2.35.1`. I think you're running this outside of a `git merge`? You must be in the middle of resolving conflicts in a `git merge` for `git checkout --ours -- some/dir` to have different behavior from `git checkout feature_branch -- some/dir`, I'm pretty sure. – Gabriel Staples Mar 19 '22 at 00:31
  • This is a hard problem to sandbox. Let me see if I can create a sandbox version of it since I don't have the code open that I was working on when I wrote this answer. – Gabriel Staples Mar 19 '22 at 00:37
  • 1
    no. I just think you don't have files in this directory that have non-conflicting changes (again, to A FILE in some/dir) between the two divergent branches. If you did you'd see what I mean. I'm suspecting you simply have some files in some/dir that have a conflict and some that don't. But not different changes between the two branches to a single file within some/dir – onlinespending Mar 19 '22 at 00:37
  • I have tried both within the middle of a merge and outside of it. After doing the merge, the conflict exists. And I do see the non-conflicting added line that was made in 'our' version of the file. But once the 'git checkout --theirs -- somefile' is issued, it destroys that – onlinespending Mar 19 '22 at 00:43
  • try this out. add and commit a file with a few lines in it in branch1. checkout branch2 and add a new line to this file (non-conflicting change) and edit an existing line. go back to branch1 and edit that same existing line (conflicting change). go back to branch2 and 'git merge branch1'. you'll naturally see a conflict. the merged file will show the non-conflicting added line as well as the conflict between the one line that was edited in both branches. now issue 'git checkout --theirs -- thisfile'. it will choose theirs and destroy that non-conflicting added line. not a true 3-way merge – onlinespending Mar 19 '22 at 00:55
  • https://stackoverflow.com/questions/71533905/git-merge-a-single-file-and-a-true-3-way-merge-not-just-a-checkout-of-ours-v – onlinespending Mar 19 '22 at 15:54
  • @onlinespending, thanks for the info. I'll dig more into this when able. – Gabriel Staples Mar 19 '22 at 18:18
0

I think @Schleis's answer is a good one. If your repo squash-merges anyways, another alternative is... before you even start a merge, just do:

git checkout gabriel.staples/MyBranch
git checkout develop path/You/Want/To/Accept/Everything
git add path/You/Want/To/Accept/Everything
git commit -m "Partially merge develop at SHA XXXXX (squash-style)"
git merge develop
<resolve conflicts>
git add path/To/Your/Hand/Resolved/Files
git merge --continue

If you really want, you can also make the above closer to a real merge (give it a parent) using:

git commit-tree aboveMergeSHA^{tree} -p `git merge-base develop pseudoMergeSHA` -p develop -m "Merge develop at SHA XXXXX"

But at that point, I think you might as well use @Schleis' answer. I don't like messing with parents manually when I don't have to...

Sean
  • 393
  • 2
  • 11