Interactively merge files tracked with git and untracked local files

那年仲夏 提交于 2019-12-05 00:20:45

git checkout --patch selects diff hunks, simplest here might be to put your content at the upstream path, do that, and clean up after:

cp config  config.example
git checkout -p upstream   config.example
mv config.example  config
git checkout @  config.example

that will get you the two-diff hunk selection from git add --patch.

  • You can use vim as a merge tool with vimdiff.
  • Emacs can do this as well with ediff-mode.

You could probably approach this in many ways. I could think of two: git-merge-file and good old patch. The git-merge-file method offers some interactivity, whereas the patch method offers none.

Solution with git-merge-file

Lets say you have an original file config.example which you use to create a local unversioned file, config.local. Now when upstream updates config.example, you can follow something like the following steps to merge any new changes.

$ git fetch
$ git show master:config.example > config.example.base
$ git show origin/master:config.example > config.example.latest
$ git merge-file config.local config.example.base config.example.latest

This will update config.local with the usual conflict markers, which you will then have to resolve with your favourite merge tool (ediff in Emacs is nice, I'm sure there are similar modes for Vim). For example, the following three files,

config.example.base:

Original: 
          some config

config.example.latest:

Original: 
          some config

Upstream:
          new upstream config

config.local:

Original: 
          some config

My changes:
          some other config

will be merged like this:

Original: 
          some config

<<<<<<< config.local
My changes:
          some other config
=======
Upstream:
          new upstream config
>>>>>>> config.example.latest

You could probably script this without much effort. Btw, git-merge-file can operate on any 3 files, they need not be version controlled under git. That means one could use it to merge any three files!

Solution with patch:

Assuming the same file names, config.local and config.example, the following should work.

$ git fetch
$ git diff master..origin/master -- config.example | sed -e 's%\(^[-+]\{3\}\) .\+%\1 config.local%g' > /tmp/mypatch
$ patch < /tmp/mypatch

If you want the full git merge, you can get git to do it with arbitrary content by setting up an index entry as git read-tree does and then invoking git's normal merge driver on just that entry. Git refers to the different content versions as "stages"; they are 1: the original, 2: yours, 3: theirs. The merge compares the changes from 1 to 2 and from 1 to 3 and does its thing. To set it up, use git update-index:

orig_example=        # fill in the commit with the config.example you based yours on
new_upstream=        # fill in the name of the upstream branch

( while read; do printf "%s %s %s\t%s\n" $REPLY; done \
| git update-index --index-info ) <<EOD
100644 $(git rev-parse $orig_example:config.example)  1 config
100644 $(git hash-object -w config)                   2 config
100644 $(git rev-parse $new_upstream:config.example)  3 config
EOD

and you've staged a merge of custom content for that path. Now do it:

git merge-index git-merge-one-file -- config

and it'll either automerge or leave the usual conflict droppings, fix it up as you like and git rm --cached --ignore-unmatch (or keep) the index entry if you want.

The pathname you put in the index (the "config" in all three entries here), by the way, doesn't have to already exist or have anything to do with anything. You could name it "wip" or "deleteme" or anything you want. The merge is of the content id'd in the index entry.

I think that's likely to do what you want here. If you really do want to pick and choose from the upstream changes you can put your own content at config.example and do git checkout -p upstream -- config.example, that does the inverse of git add -p, then put things back the way they were.

To diff just config.example in your local repo with the corresponding file in upstream/HEAD you can run:

git diff upstream/HEAD config.example

Unfortunately I don't know of a way to make git directly apply the changes to a file that git doesn't track.

There is a tool called sdiff that might do what you want.

Invoke it (in your case) with sdiff -o config config.example config

The following should work:

git diff <some-args> | perl -pe 's/path\/to\/changes\/file/path\/other/g' > t
patch -p1 < t 
rm t
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!