Search code examples
git3-way-merge

git checkout --ours/--theirs after git merge-files with external files


Follow-up of Forcefully set files status as "unmerged" in a Git repository.

The Copier tool lets you generate projects from templates, and update the generated projects when the template changes.

When updating, it does 3-way merges like this:

git merge-file \
  -L "before updating" \
  -L "last update" \
  -L "after updating" \
  README.md \
  /tmp/project_generated_from_previous_template_version/README.md \
  /tmp/project_generated_from_latest_template_version/README.md

After doing so, the output of git ls-files -s | grep README would look like this:

% git ls-files -s | grep README
100644 8f7a2b8e27bddabb05a3eee508d2b77c33c424f1 0       README.md

README.md's contents might look like this:

<<<<<<< before updating
Hello girls.
=======
Hello guys.
>>>>>>> after updating

However, whether I pass --ours or --theirs to git checkout, the result is always the "before updating" part (current/ours).

git checkout --ours README.md  # Hello girls. OK.
git checkout --theirs README.md  # Hello girls. I expected Hello guys.

My question is therefore: why? and how to make the checkout command work as I'd expect?

Is this because the files are external to the repository (/tmp/*), and therefore Git doesn't have the corresponding blobs, and will always use the only blob it knows, which is the current version of the README? I'm not very Git-knowledgeable, so what I'm saying might not make any sense.


Solution

  • To get a "theirs" or "ours" version of a file using git checkout, it is necessary that a merge conflict is recorded in the index.

    Since you invoke git merge-file manually, I assume that you are doing so outside of a regular merge operation. No merge conflict is recorded in the index, but only the usual "stage 0", which happens to be identical to the "before updating" version. In this case, git checkout simply ignores --ours and --theirs and just checks out the file recorded in the index.

    To make git checkout work as you expect, you have to record the missing stages in the index:

    theirs=$(git hash-object -w /tmp/project_generated_from_latest_template_version/README.md)
    ours=$(git hash-object -w /tmp/project_generated_from_previous_template_version/README.md)
    
    # must remove stage 0
    git rm --cached README.md
    
    # record the missing stages
    cat <<EOF | git update-index --index-info
    100644 $ours 2 README.md
    100644 $theirs 3 README.md
    EOF
    

    (Important: the whitespace before the path must be a tab character, not the space that it is here.)