gitgit-merge-conflictgit-addgit-stagegit-index

Can I mark a file for resolution without adding it to the index with one command?


When there's a conflict, git status shows this:

Unmerged paths:
  (use "git restore --staged <file>..." to unstage)
  (use "git add <file>..." to mark resolution)
    both modified:   some_file

Now I can git add some_file to mark resolution, as the command suggests, but this also stages some_file, which I might not want to.

How can I mark the file for resolution without adding it to the index? In two steps it's easy:

git add some_file
git restore --staged some_file

but how do I do it in one step, if it's possible?

If doing so is not recommended by best practices, please, tell me.


Solution

  • How can I mark the file for resolution without adding it to the index?

    Mu. (In other words, this question presupposes something that cannot exist.)

    A merge conflict is represented by having multiple copies of a file in the index / staging-area.1 That is, instead of just one file being staged—this one file either matches the current commit, and Git just shuts up about it, or doesn't, and Git prints staged for commit about it—there are three files staged for commit. Git can't actually commit three copies of one file and won't let you commit, so you must resolve this situation ... but the act of resolving consists of erasing those three files and putting one file in place.

    That one file that you put in place is added to the index. It either matches the current commit's file, and Git says nothing, or it doesn't, and Git says staged for commit. The file exists either way.

    If what you want is to make Git's index contain, as the one copy of the file, the copy that is in the current commit, you can do that. But that means that you have added that version of the file as the one staged for commit.


    1This isn't quite right technically: a merge conflict is represented by having any nonzero stage entry in the index. However, when you do get merge conflicts, most often you wind up with one or more files, each of which has three entries. The other cases occur with add/add, modify/delete, rename/rename, and rename/delete conflicts. These also leave more than one entry representing the "same" file.


    Example

    Let's illustrate this. First, let's create a tiny repository and set up a conflict:

    $ mkdir test-resolve
    $ cd test-resolve/
    $ git init
    Initialized empty Git repository in .../.git/
    $ echo test conflict resolution > README
    $ echo fee file fo fum > file
    $ git add . && git commit -q -m initial
    $ git checkout -b branch
    Switched to a new branch 'branch'
    $ echo foo >> file && git add file && git commit -q -m foo
    $ git checkout -q master
    $ echo bar >> file && git add file && git commit -q -m bar
    $ git log --all --decorate --oneline --graph
    * 8921373 (HEAD -> master) bar
    | * 679121a (branch) foo
    |/  
    * a7a3f27 initial
    

    Now let's see what's in Git's staging area / index:

    $ git ls-files --stage
    100644 7eafc9636afdf576278e921d7430598dd8754bdd 0       README
    100644 1f4f7a3f149c9b0e7740a5f2f801b1840f2d68f8 0       file
    

    These are the files in Git's staging area, complete with their staging numbers. Stage number zero indicates that there is no merge conflict; there cannot be any other entries for such a file. Note that both files are staged for commit! They just match the HEAD commit version, so that git status doesn't say staged for commit.

    Now we'll run the merge:

    $ git merge branch
    Auto-merging file
    CONFLICT (content): Merge conflict in file
    Automatic merge failed; fix conflicts and then commit the result.
    $ git ls-files --stage
    100644 7eafc9636afdf576278e921d7430598dd8754bdd 0       README
    100644 8fee5c26846ed992dc2dd912e224a2001a2b6820 1       file
    100644 1f4f7a3f149c9b0e7740a5f2f801b1840f2d68f8 2       file
    100644 af8330063fa2da41a81f8a789929fd87692adb2f 3       file
    

    Voila! Here are our three copies of the file named file. They have nonzero staging numbers; these represent the merge base copy of the file (stage 1), the ours copy (stage 2), and the theirs copy (stage 3). If we like, we can view the files: git show :1:file, git show :2:file, and so on:

    $ git show :3:file
    fee file fo fum
    foo
    

    "Their" file, from the tip of branch branch, is the one where line 2 reads foo (ours reads bar).

    Now, the working tree copy of file file contains Git's attempt to resolve the conflict. If we view that, we can see it did not go well. Note that I have merge.conflictStyle configured to diff3 so that I get the merge base version of the text as well:

    $ cat file
    fee file fo fum
    <<<<<<< HEAD
    bar
    ||||||| a7a3f27
    =======
    foo
    >>>>>>> branch
    

    We now return to your question, which I'll quote again

    How can I mark the file for resolution

    Pick one of the three copies that exist in the three stages, or the copy that exists in the HEAD commit or any other commit, and choose that as the copy to go into slot zero in Git's index. Erase the other two slots. The file is now staged for commit.

    without adding it to the index?

    It's already in the index. You're subtracting two or three copies, probably leaving the third and moving it to slot zero; or you subtract away all three copies and add one new one to the index.

    You can use git add to copy the working tree file into the index, or you can choose some existing Git-ified copy and move that in. Let's say you'd like to stick the HEAD copy of the file into the index, without touching the work-tree copy. Here, git restore or git reset (both will do this job) are the way to go:

    $ git restore --source HEAD -S file
    $ git ls-files --stage
    100644 7eafc9636afdf576278e921d7430598dd8754bdd 0       README
    100644 1f4f7a3f149c9b0e7740a5f2f801b1840f2d68f8 0       file
    

    (Note that the hash ID here is the same one we saw in slot 2 earlier.)

    $ git show :0:file
    fee file fo fum
    bar
    $ cat file
    fee file fo fum
    <<<<<<< HEAD
    bar
    ||||||| a7a3f27
    =======
    foo
    >>>>>>> branch
    

    Since the index copy is the file Git will commit, a git commit now would make another commit that matches the current commit (file README still matches the current commit). Since I'm in the middle of a merge, with all conflicts resolved, this will make the final merge commit, with the snapshot matching what I would have gotten had I used git merge -s ours or git merge -s recursive -X ours, for instance:

    $ git commit -m 'resolved by keeping ours'
    [master b896bc3] resolved by keeping ours
    $ git log --all --decorate --oneline --graph
    *   b896bc3 (HEAD -> master) resolved by keeping ours
    |\  
    | * 679121a (branch) foo
    * | 8921373 bar
    |/  
    * a7a3f27 initial
    

    (My working tree is still a bit of a mess, of course, and git status will show that my working tree copy of file differs from my index copy of file, as the working tree version still has the unresolved conflict in it. But Git didn't use that copy, to make the commit: it used the index copy.)