How to git rebase while overriding specific local conflicting commits with upstream code

心不动则不痛 提交于 2019-12-13 03:07:21

问题


We forked and opensource project and developed features on top of that. These features are in particular branch

Now I am trying to rebase our code with new upstream branch. The rebase fails many conflicts in error messages. I wish to override all the error messages with incoming commits but I could not find how to do that.

From what I found, the following command would override all conflicts with upstream code

git rebase -Xours upstream/branch

But I wish to very specifically override those commits which are not around code but documentation only

Any way to achieve this ?


回答1:


Note: this is an answer to a somewhat modified question (see the long comment thread). Here's the modified question:

We have some branch B we made in a local repository, based on an upstream repository and its branch upstream/U, which used to identify commit 1234567.

The upstream repository has been updated by its owner, and now upstream/U identifies commit 89abcde. There are many commits and many changes between 1234567 and 89abcde.

We have chosen to rebase our branch B, which has a long list of commits, so that all of our commits will be applied after 89abcde. That is, we now have:

  ...--o--1234567--o--o--...--o--89abcde   <-- upstream/U
               \
                B1--B2--B3--...--B1000   <-- B

but will, in the end, have:

                B1--B2--B3--...--B1000   [abandoned]
               /
  ...--o--1234567--o--o--...--o--89abcde   <-- upstream/U
                                    \
                                     B1'-B2'-B3'-...--B1000'  <-- B

Therefore, we started this operation by doing:

git checkout B
git rebase upstream/U

Along the way, though, we hit a lot of cases where, while copying commit B1 to B1, B2 to B2, and so on, we get a conflict.

Resolving these conflicts sometimes means "take the upstream version of the file". This is true for all the documentation files, for instance. It's also true for some cases where the only file conflicting is named .gitreview.

If we run git checkout --ours .gitreview to take their version of the file, and attempt to continue like this, Git says:

# git checkout --ours `.gitreview`
# git add .gitreview
# git rebase --continue
Applying: Update .gitreview for stable/mitaka
No changes - did you forget to use 'git add'?
#

What does this mean and how should we proceed?

Now, as shown in the illustration within the question, what git rebase is doing is copying each commit, B1, B2, ..., B1000, for every commit that is on branch B that is not on branch upstream/U. To copy a commit, Git essentially runs either git cherry-pick (some variants of git rebase use this) or git format-patch followed by git am.

Either one turns each commit into a diff (a changeset, in Version Control System terms) that can then be applied to some other commit. When using git cherry-pick this changeset will be applied through Git's three-way merge machinery, with the merge base being the parent of the cherry-picked commit. The two branch tip commits are the current commit, which is the left side or "local" or --ours commit that I call L, and the commit being cherry-picked as the right side or "remote" or --theirs commit that I call R.

Note that these changesets are applied one at a time, after which Git makes a new commit. Git starts by checking out commit 89abcde, using what Git calls its "detached HEAD" mode, so that commit 89abcde is what is in the index and work-tree and there is no current branch at all. Git will then cherry-pick commit B1. Since the current commit is 89abcde, we will refer to the upstream code as --ours or the local or L version. Since the commit being applied now is B1, we will refer to our own code from branch B as --theirs or the remote or R version.

Assuming the changeset from B1 is successfully applied to 89abcde, Git makes a new commit, using the log message from commit B1. This new commit is now B1', and our detached HEAD now points to this new B1':

                B1--B2--B3--...--B1000   <-- B
               /
  ...--o--1234567--o--o--...--o--89abcde   <-- upstream/U
                                    \
                                     B1'  <-- HEAD (detached)

The process now repeats with B2, and repeats over and over again for as many commits as there are to copy (1000, in this example), until we are exhausted and thoroughly sick of Git. :-)

When using git am, the change-set is applied slightly differently, but usually the result is the same. The key difference is that Git doesn't immediately resort to the three-way merge; instead, it first attempts to apply the diff directly, to the file named in the git format-patch output. If the diff fails to apply, then Git extracts a merge-base version of the file to which the diff failed to apply. The diff will apply correctly to the merge-base version, producing the R version of the file. (The L version is simply the one in the existing work-tree.)

As with git cherry-pick, the merge base version is whatever version of that file is stored in the parent commit of the commit we're copying right now. So when falling back, Git diffs the merge base version against the HEAD version.

Let's look now at what happens with .gitreview. Suppose we're copying commit B11. The file .gitreview contains the name of a branch foo in commit B11, but it contains a different branch name bar in the commit we made as B10'. Git has, in diffing B11 against B10, identified .gitreview as changed, so it's in the changeset. In fact, it's the only file in the changeset: B11 consists entirely of a change to .gitreview.

We now choose not to take our change, so we run:1

# git checkout --ours .gitreview
# git add .gitreview
# git rebase --continue

and get the complaint.

The reason we get this complaint is that this modified .gitreview file was the only change. By using git checkout --ours .gitreview to extract the version of .gitreview that goes with commit B10', rather than the version that goes with B11, we've made the current index-and-work-tree match commit B10' exactly.

Git notices that there's nothing changed in the index and work-tree as compared with HEAD (which is B10'), and complains.

The solution at this point is to run:

# git rebase --skip

which tells Git, in effect, we no longer want commit B11 after all: just drop it from the list of commits to copy.

Git will go on to commit B12. This may have a change to .gitreview, or it may not. If it doesn't, we won't see any problem there.

If B12 does have a change to .gitreview, we may get a conflict, or we may not, depending on:

  1. Are we doing git am or git cherry-pick?
  2. If we're doing git am, did the patch apply cleanly, or not?
  3. If we're doing a three-way merge because of git cherry-pick, or because the patch didn't apply cleanly in (2), is there a merge conflict, or not?

Assuming B12 copies without conflict, we'll now have:

                B1--B2--B3--...--B1000   <-- B
               /
  ...--o--1234567--o--o--...--o--89abcde   <-- upstream/U
                                    \
                                     B1'-...--B10'-B12'  <-- HEAD (detached)

Note how B11 is simply skipped. Git will proceed with copying B13, and so on.

Remember, in all cases, all Git is doing is a line-by-line application of diffs. It has no idea what these diffs mean. It just knows that they can be applied, or cannot; or that they conflict with each other (when combining two diffs via three-way merge), or do not.

You can get one merge conflict per file, per commit. So if there are 1000 commits to copy, and 1000 files in each commit, you could have up to 1 million merge conflicts. Chances are that most commits change at most a few files, though, and most changes apply cleanly or have no merge conflicts; so it's pretty likely that there will be at most a few conflicts per merge, for at most a few thousand of these to resolve.

Nonetheless, you may want to use some strategy other than copying all 1000 (or however many) commits.


1These # prompts concern me slightly: they suggest you're doing all this as user root. The usual sh/bash prompt is or ends with $ for non-root users. It's generally wisest to "live" as much as possible as someone other than root.



来源:https://stackoverflow.com/questions/47587688/how-to-git-rebase-while-overriding-specific-local-conflicting-commits-with-upstr

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!