简体   繁体   中英

How to resolve git conflicts on branch without merging or rebasing

My usual feature/bug branch workflow is like this:

  • Branch
  • Make changes
  • Rebase to master
  • Push and make github pull request (PR)
  • Make more changes
  • Somebody else reviews code and clicks the github merge button

Let's assume the merge button on the PR can't be clicked because my feature branch now has conflicts with master. At this point I typically want to resolve the conflicts with master, and I want to do that on my feature branch so that I can have the person who is reviewing my code see:

  1. A nice diff that doesn't have merge commits and/or random changes from master
  2. My conflict resolutions
  3. A merge button they can click

However, I may not want to rebase because code review is in progress already (sometimes I rebase anyway, but other times I want to avoid it).

How can I reliably and efficiently use git to achieve that?

What I currently do is a mixture of these things:

  • "Just knowing" what needs cherry picking or changing to resolve the conflict (sometimes...)
  • Trial merges ( git merge master ; (see which file has conflicts, then git annotate to find relevant commit); git reset --hard origin/my-feature-branch ; git cherry-pick <some commit> (or maybe make some change by hand); (repeat))

Sometimes that doesn't work because the conflicts are empty, so I don't know what to annotate to find the right commit. Edit: in fact in the empty-conflict case I imagine it's not possible to resolve without merge or rebase (but it is possible in other cases -- see example below).

When it does work, it seems like it's a lot of work that git might be able to help me do in a more automated way.

I've tried git-imerge also -- that doesn't seem designed exactly for this purpose, and it also exited with an unhandled exception.

Here is a concrete working example, since there is skepticism in answers here that it is sometimes possible to resolve conflicts on a branch as I describe here without merging or rebasing (note this doesn't show every step of the workflow above, and demonstrates only the 'resolve conflicts on branch without merge or rebase' part):

$ mkdir -p conflict-example/upstream
$ cd conflict-example/upstream
$ git init .
Initialised empty Git repository in /tmp/conflict-example/upstream/.git/
$ echo 'changed_only_upstream before' > changed_only_upstream
$ echo 'changed_only_downstream before' > changed_only_downstream
$ echo 'changed_in_both before' > changed_in_both
$ git add .
$ git commit -m 'initial'
[master (root-commit) 23040ea] initial
 3 files changed, 3 insertions(+)
 create mode 100644 changed_in_both
 create mode 100644 changed_only_downstream
 create mode 100644 changed_only_upstream
$ cd ..
$ git clone upstream downstream
Cloning into 'downstream'...
done.
$ cd downstream
$ git checkout -b downstream
Switched to a new branch 'downstream'
$ vim changed_in_both
$ vim changed_only_downstream
$ cat changed_in_both
changed_in_both before
downstream
$ cat changed_only_downstream
changed_only_downstream before
downstream
$ git commit -am 'downstream'
[downstream 6ead47f] downstream
 2 files changed, 2 insertions(+)
$ cd ../upstream
$ vim changed_in_both
$ vim changed_only_upstream
$ cat changed_in_both
changed_in_both before
upstream
$ cat changed_only_upstream
changed_only_upstream before
upstream
$ git commit -m 'upstream conflict' changed_in_both
[master e9ec7c5] upstream conflict
 1 file changed, 1 insertion(+)
$ git commit -m 'upstream non-conflict' changed_only_upstream
[master d4057e0] upstream non-conflict
 1 file changed, 1 insertion(+)
$ cd ../downstream/
$ git checkout master
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.
$ git pull
remote: Counting objects: 6, done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 6 (delta 2), reused 0 (delta 0)
Unpacking objects: 100% (6/6), done.
From /tmp/conflict-example/upstream
   23040ea..d4057e0  master     -> origin/master
Updating 23040ea..d4057e0
Fast-forward
 changed_in_both       | 1 +
 changed_only_upstream | 1 +
 2 files changed, 2 insertions(+)
$ git checkout downstream
Switched to branch 'downstream'
$ git merge master
Auto-merging changed_in_both
CONFLICT (content): Merge conflict in changed_in_both
Recorded preimage for 'changed_in_both'
Automatic merge failed; fix conflicts and then commit the result.
$ git merge --abort
$ git log --all --graph --pretty=oneline --abbrev-commit --decorate
* d4057e0 (origin/master, origin/HEAD, master) upstream non-conflict
* e9ec7c5 upstream conflict
| * 6ead47f (HEAD -> downstream) downstream
|/
* 23040ea initial
$ git cherry-pick e9ec7c5
error: could not apply e9ec7c5... upstream conflict
hint: after resolving the conflicts, mark the corrected paths
hint: with 'git add <paths>' or 'git rm <paths>'
hint: and commit the result with 'git commit'
$ vim changed_in_both
$ cat changed_in_both
changed_in_both before
upstream
$ git add changed_in_both
$ git commit
Recorded resolution for 'changed_in_both'.
[downstream 7a4f7a7] upstream conflict
 Date: Sat Aug 27 14:41:13 2016 +0100
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --all --graph --pretty=oneline --abbrev-commit --decorate
* 7a4f7a7 (HEAD -> downstream) upstream conflict
* 6ead47f downstream
| * d4057e0 (origin/master, origin/HEAD, master) upstream non-conflict
| * e9ec7c5 upstream conflict
|/
* 23040ea initial
$ git checkout master
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.
$ git merge downstream
Merge made by the 'recursive' strategy.
 changed_only_downstream | 1 +
 1 file changed, 1 insertion(+)
$ git log --all --graph --pretty=oneline --abbrev-commit --decorate
*   c036d60 (HEAD -> master) Merge branch 'downstream'
|\
| * 7a4f7a7 (downstream) upstream conflict
| * 6ead47f downstream
* | d4057e0 (origin/master, origin/HEAD) upstream non-conflict
* | e9ec7c5 upstream conflict
|/
* 23040ea initial

I believe if I had chosen a different resolution in the cherry-pick, I wouldn't have been able to merge using that merge command (which is at least similar to what github merge button does). In those situations, typically I either do the merge myself or rebase -- but that is not what this question is about (though if there's some way to achieve merge button-clickability without merging master or rebasing in those situations too it'd be interesting to hear about.).

Here is a script that does the job:

#!/bin/bash

declare -a conflicts

echo "Detecting conflicts..."
for rev in `git rev-list HEAD..master`
do
  git cherry-pick --no-commit $rev > /dev/null 2>&1
  if [ $? -eq 1 ]
  then
    conflicts+=($rev)
  fi
  git reset --hard HEAD > /dev/null
done

for rev in ${conflicts[*]}
do
  git cherry-pick --no-commit $rev > /dev/null 2>&1
  echo "Commit $rev cherry-picked."
  read -p "Resolve conflicts, then press any key to continue: "
done

echo "Done cherry-picking! Commit your changes now!"

Run this script, and each time you are prompted, resolve any conflicts in your text editor, and do git add from another window. When you are finished, you can git commit (as prompted).

During my testing so far, I have found two problems with this script:

  1. When I merge the feature branch back to master , I get a couple small conflicts. These conflicts are much smaller than what you get if merging from master to the feature branch. In fact, they are such that you can do:

     git checkout master git merge --no-ff feature/my-feature -x theirs 

    and it should work. However, this probably means that the GitHub Merge button would not work, and I don't think there's a way to tell GitHub to use -x theirs .

    I'm not sure if this just depends on the relative changes made, so this may just be an issue caused by my particular test repo.

  2. If you have eg a commit bbb that depends on aaa , both on master , then the cherry-pick of bbb will be detected as a conflict. My testing indicates that it doesn't matter whether you keep such a change in your cherry-pick or discard it. (It doesn't seem to affect issue #1 either.)

I'm looking for solutions to both of these issues, but that should be enough to get you started.

The conflicts appear only because of the changes (on the master branch) that are not contained in your feature branch. So you cannot resolve the conflicts on your feature branch, without introducing those changes from the master branch. Because they simply don't exist then.

So even if there was a nice way to just push a simple patch, so you get a nice history later, it simply doesn't even work conceptually because you can't fix issues that are not there yet.

So you really only have the two choices of merging or rebasing to actually introduce those changes that will cause the conflicts into your feature branch. Depending on the complexity of your pull request, one way might be favorable over the other, eg the history is easier to look at when you rebase it, while a merge keeps the history (including the information that there was a conflict) intact. So choose what makes the most sense to you, or ask the project owner what you should do. In most open source projects, if the owners don't merge the changes themselves, they often expect you to rebase your changes against the current master.

Here is a possible workaround. It does include a merge commit, but only one, and that is the commit containing your conflict resolutions. The main problem is that you will pull in unrelated changes from master .


Do:

git checkout my-feature
git merge --no-ff master

Rather than looking for things to cherry-pick at that point, just resolve any conflicts and commit.

This has the downside of complicating your history somewhat, but you will have 1 commit that resolves conflicts, and that commit will be on your feature branch.

The reviewers should then be able to use GitHub's one-click Merge button without issues.

Your history will look something like this:

*---*---*---B [master]
 \       \ /
  *---*---A [feature]

Where A is the commit that you resolve your conflicts in, and B is the merge commit created by GitHub.

In my case, I had two branches in my fork. One feature and the main branch. I synced the fork main branch with the original main branch on github repo page, then did a git pull on my main branch.

This made all the changes come into the main branch and it came in sync with the base repo. Then in my feature branch, I did a merge from my local main branch. After resolving all the conflicts there, I did a commit and push to the feature branch remote on which I had the PR.

So here are the commands to do that: (currently in the forked repo)

git checkout main
git pull

git checkout feature-branch-with-PR
git merge main

[Resolve all the conflicts in VSCode]

git commit -am "Synced changes with remote"
git push

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM