9

Is it possible to modify the default git-merge-one-file program to do everything in the index without touching the working tree, leaving it completely unmodified?

UPDATE AND DETAILS

So I understand now that a file-level merge (where the merge is acting on lines in the file rather than whole files) can't occur without using a worktree. (Unlike a merge acting on whole files.) So I'm gonna have to use a worktree.

Another detail: I'm okay with the solution working only in the cases where the merge can be done automatically without manual resolution. It's okay if it just shows an error message if the merge is not automatic. (And of course, leave everything clean.)

Another detail: I'm not using git-merge-one-file directly, I'm using it inside this script: https://gist.github.com/cool-RR/6575042

I tried to follow @torek 's advice and use a temporary work tree (as you can see in the script), because that seems like the best direction so far. Problem is, I get these errors:

git checkout-index: my_file is not in the cache
error: my_file: cannot add to the index - missing --add option?

I googled these error messages but couldn't find anything helpful.

Any idea what to do?

Ram Rachum
  • 84,019
  • 84
  • 236
  • 374
  • What is the goal? Performance? Previewing a merge for another script to tie into? Preserving the contents? – Will Palmer Sep 15 '13 at 16:38
  • @WillPalmer The goal is for it to be used in a script of mine which merges branch `a` to branch `b` without requiring having either of them checked out, and while using a temporary index file so neither the index file nor working directory are touched. – Ram Rachum Sep 15 '13 at 17:11
  • @RamRachum as I mention [in my answer below](http://stackoverflow.com/a/18822572/6309), only `git notes` does that, to my knowledge. It does its merge in an internal, dedicated and temporary work-tree – VonC Sep 17 '13 at 12:19
  • @VonC I want to try using an internal, dedicated and temporary work-tree, and I tried, but I got the bug shown above. (The C code doesn't help me, I don't read C.) – Ram Rachum Sep 17 '13 at 18:21
  • @RamRachum maybe you need to specify `git-dir` too (to point to your `repo/.git` folder). If not, then the implementation I reference is the only way I know... – VonC Sep 17 '13 at 18:24
  • I'm already setting `GIT_WORK_TREE` to point at my temporary folder, and I run the command while in the git repo. – Ram Rachum Sep 17 '13 at 19:31
  • Can anyone help me with the error shown in my update? – Ram Rachum Sep 20 '13 at 18:20

5 Answers5

5

While git needs a place to do its work, you could point it off to a different work-tree location for the duration of the "merge one file" op.

I have no idea if/how this works "out of the box" for merge-one-file, but the env variable to set is GIT_WORK_TREE:

env GIT_WORK_TREE=/some/where/else git ...

(you can leave out env with most, but not all, shells).

A more or less equivalent method that might "feel safer" :-) or be more convenient for some purposes is to work in the other directory, and use GIT_DIR to the location of the repo:

cd /some/where/else
env GIT_DIR=/place/with/repo/.git git ...

You can even combine them, setting both GIT_DIR and GIT_WORK_TREE.

torek
  • 448,244
  • 59
  • 642
  • 775
  • Okay, that's a nice workaround. Unless there's a cleaner way (probably involving modifying `git-merge-one-file` to not even try writing to disk), that'll be the best answer. – Ram Rachum Sep 13 '13 at 16:31
  • I tried this now, but I get errors. I get `git checkout-index: my_file is not in the cache` and then `error: my_file: cannot add to the index - missing --add option?` – Ram Rachum Sep 15 '13 at 22:58
  • If I get time I'll experiment with this later. `git-merge-one-file` is a shell script and when I looked at it very briefly I could not tell whether this would work "out of the box", as I noted. – torek Sep 15 '13 at 23:00
  • I'm not using `git-merge-one-file` directly, I'm using this script: https://gist.github.com/cool-RR/6575042 – Ram Rachum Sep 15 '13 at 23:02
2

No, but the simplest way to do it is to stash, merge, stage, and unstash:

git stash save
git merge-file foo.txt base-foo.txt their-foo.txt
git add foo.txt
git stash pop

If you don't want to stash, then you're left with the diff and patch option: save working tree changes to a patch, remove working-tree changes, make necessary changes, and re-apply patch

git diff -p --raw foo.txt > foo.txt.diff
git checkout -- foo.txt
git merge-file foo.txt base-foo.txt their-foo.txt
patch -p1 < foo.txt.diff    
CharlesB
  • 86,532
  • 28
  • 194
  • 218
  • Not a valid solution for me because stashing has its own complications. I'm looking for a solution that just changes `git-merge-one-file` to not use the working tree at all, and just do everything on the index. – Ram Rachum Sep 13 '13 at 13:18
  • The index is not a real file, it's already object stuff internally. So you can't work with index without touching the work tree. Then the only possibility is to save working tree changes to a patch, `git checkout foo.txt` to reset file to the index, make necessary changes, and re-apply patch – CharlesB Sep 13 '13 at 15:35
  • I know I can do simple merges, even non-fast-forward ones, completely on the index without touching the working tree using `read-tree`. So why is the working tree necessary for merges using `git-merge-one-file`? – Ram Rachum Sep 13 '13 at 15:46
  • mmh don't know what happens when using `read-tree`, don't using it myself. I see `merge-file` as something only working on files, while `read-tree` works with the index. – CharlesB Sep 13 '13 at 15:50
  • in http://git-scm.com/docs/git-merge-file "git merge-file is designed to be a minimal clone of RCS merge;" So it really doesn't care about your index. Basically Git is better designed to work on trees and directories than with single files – CharlesB Sep 13 '13 at 15:52
  • I didn't ask about `merge-file`, I asked about the similarly-named but different `git-merge-one-file`, used with `merge-index`. – Ram Rachum Sep 13 '13 at 16:02
  • Sorry I thought you mistyped, I didn't know this command. Way too git-hacky for me – CharlesB Sep 13 '13 at 16:09
2

To merge the changes in two different files you nave to examine their content: merging changes is working on file contents. Work on contents is done in work trees. Doing the work somewhere else and pretending it isn't a work tree is just wordplay.

If you want to leave your current worktree untouched while doing a merge, then use another worktree. git clone is cheap, it's built for stuff like this:

# merge into branch m2 from branch m1 but leave your (non-m2) worktree untouched:
git clone --no-checkout -b m2 . ../m2-work
cd ../m2-work
git reset    # this will warn about the empty worktree, you could instead do
#              git read-tree HEAD to get the same effect without the chatter
git merge origin/m1
git push origin m2

notice the --no-checkout on the clone. Merge does have to have a worktree to do its work, but it doesn't care about any actual file contents other than the ones that need comparison.

jthill
  • 55,082
  • 5
  • 77
  • 137
  • Could you please explain why `git-merge-one-file` can't simply be fixed to do all the work in the index instead of the work tree? I'd prefer to avoid creating temporary files if possible. – Ram Rachum Sep 15 '13 at 18:34
  • 1
    The index doesn't carry content, it's pure metadata showing the relationship between the current worktree and your next commit. Also, the merge might produce conflicts, in which case you're going to have to fix up the conflicts and add the corrected data to the resulting commit -- which you do in the worktree, where the merged file's contents are checked out. – jthill Sep 15 '13 at 21:59
2

A merge in git is a three-way merge between:

  • the source ('remote' or 'theirs', what you want to merge)
  • the destination ('local' or 'ours', which is always the working tree, where HEAD is checked out)
  • the common ancestor (or 'base')

See the 'local', 'base', 'remote', 'merged' illustrated in "git rebase, keeping track of 'local' and 'remote'".
You can see an example in "git revert does not work as expected".

git read-tree mentioned in "Subtree Merging" and "Git Objects" (and that you are using in your gist) is about merging trees (for subtree merging), not file content (blob).
git write-tree can be use to create tree object, but its documentation does mention "The index must be in a fully merged state." (a bit hard when you want to use the index for merging files).

The git index (documented here) is there to record what you have staged (the 'merged' result), as part of the merge resolution, from your working tree.
It doesn't have all the information about the file content, only pointers ("index entry") to said content. It is simply not the right structure to do a merge.


Even the git-merge-one-file.sh script itself does mention:

require_work_tree

The function comes from the git-sh-setup.sh script (see its documentation):

test "$(git rev-parse --is-inside-work-tree 2>/dev/null)" = true ||
die "fatal: $0 cannot be used without a working tree."

That requirement comes from commit 6aaeca90 (peff Jeff King):

The merge-one-file tool predates the invention of GIT_WORK_TREE.

For the most part, merge-one-file just works with GIT_WORK_TREE; most of its heavy lifting is done by plumbing commands which do respect GIT_WORK_TREE properly.


If you really need to not use the working tree, you can try and go the route chosen for Merging notes:
notes-merge.c does creates its own working tree to merge git notes.

Community
  • 1
  • 1
VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
1

Jeff King helped me solve the problem and I updated the script to work:

https://gist.github.com/cool-RR/6575042

#!bash
if [ -n "$2" ]; then
  export SOURCE=$1 ;
  export DESTINATION=$2 ;
else
  export SOURCE=HEAD ;
  export DESTINATION=$1 ;
fi

export GIT_INDEX_FILE=`git rev-parse --show-toplevel`/.git/aux-merge-index ;
export GIT_WORK_TREE=`create_temporary_folder gm_`;
echo $GIT_INDEX_FILE
trap 'rm -f '"'$GIT_INDEX_FILE'"'; rm -rf '"'$GIT_WORK_TREE'" 0 1 2 3 15 ;
mkdir $GIT_WORK_TREE/.git
set -e ;
git read-tree -im `git merge-base $DESTINATION $SOURCE` $DESTINATION $SOURCE ;
#echo Finished read-tree
#sleep 1000
git merge-index git-merge-one-file -a
#echo Finished merge-index
git write-tree \
| xargs -i@ git commit-tree @ -p $DESTINATION -p $SOURCE -m "Merge $SOURCE into $DESTINATION" \
| xargs git update-ref -m"Merge $SOURCE into $DESTINATION" refs/heads/$DESTINATION ;
exit 0
VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
Ram Rachum
  • 84,019
  • 84
  • 236
  • 374