gitgit-svn

Why is the meaning of “ours” and “theirs” reversed with git-svn


I use git-svn and I noticed that when I have to fix a merge conflict after performing a git svn rebase, the meaning of the --ours and --theirs options to e.g. git checkout is reversed. That is, if there's a conflict and I want to keep the version that came from the SVN server and throw away the changes I made locally, I have to use ours, when I would expect it to be theirs.

Why is that?

Example:

mkdir test
cd test
svnadmin create svnrepo
svn co file://$PWD/svnrepo svnwc
cd svnwc
echo foo > test.txt
svn add test.txt
svn ci -m 'svn commit 1'
cd ..
git svn clone file://$PWD/svnrepo gitwc
cd svnwc
echo bar > test.txt 
svn ci -m 'svn commit 2'
cd ..
cd gitwc
echo baz > test.txt 
git commit -a -m 'git commit 1'
git svn rebase

git checkout --ours test.txt
cat test.txt 
# shows "bar" but I expect "baz"

git checkout --theirs test.txt
cat test.txt 
# shows "baz" but I expect "bar"

Solution

  • That seems consistent with what a rebase does.

    git rebase replays each commit from the working branch on top of the <upstream> branch.

    If you reconcile both definitions:

    In other words, SVN or not:


    Good mnemonic tip by CommaToast:

    whatever HEAD's pointing to is "ours"

    (and the first thing a git rebase upstream does is to check out the upstream branch on top of which you want to rebase: HEAD refers to upstream -- ours now.)

    This does not contradict:


    The confusion is likely coming from the role of the working branch in a classic git merge.
    When you are merging:

    As the git rebase man page mentions, a merge during a rebase means the side are swapped.


    Another way to say the same thing is to consider that:


    On a merge:

    x--x--x--x--x(*) <- current branch B ('*'=HEAD)
        \
         \
          \--y--y--y <- other branch to merge
    

    , we don't change the current branch 'B', so what we have is still what we were working on (and we merge from another branch)

    x--x--x--x--x---------o(*)  MERGE, still on branch B
        \       ^        /
         \     ours     /
          \            /
           --y--y--y--/  
                   ^
                  their
    

    But on a rebase, we switch side (as in git switch, mentioned in git rebase man page) because the first thing a rebase does is to check out the upstream branch! (to replay the current commits on top of it)

    x--x--x--x--x(*) <- current branch B
        \
         \
          \--y--y--y <- upstream branch
    

    A git rebase upstream will first change HEAD of B to the upstream branch HEAD (hence the switch of 'ours' and 'theirs' compared to the previous "current" working branch.)

    x--x--x--x--x <- former "current" branch, new "theirs"
        \
         \
          \--y--y--y(*) <- upstream branch with B reset on it,  
                           new "ours", to replay x's on it
    

    , and then the rebase will replay 'their' commits on the new 'our' B branch:

    x--x..x..x..x <- old "theirs" commits, now "ghosts", available through reflogs
        \
         \
          \--y--y--y--x'--x'--x'(*) <-  branch B with HEAD updated ("ours")
                   ^
                   |
            upstream branch
    

    The only extra step with git svn rebase is that a svn "fetch" is performed first on the Git remote branch representing SVN commits.
    You have initially:

    x--x--x--x--x(*) <- current branch B, "ours" for now.
        \                                   
         \
          \--y--y--y <- SVN tracking branch, "theirs for now"
    

    , you first update the SVN tracking branch with new commits coming from SVN

    x--x--x--x--x(*) <- current branch B, still "ours", not for long
        \                                   
         \
          \--y--y--y--y'--y' <- SVN tracking branch updated
    

    , then you switch the current branch to the SVN side (which becomes "ours")

    x--x--x--x--x <- for "B", now "their" during the rebase
        \                                   
         \
          \--y--y--y--y'--y'(*) <- SVN tracking branch updated, and branch B: 
                                   now "ours" (this is "what we now have")
    

    , before replaying the commits you were working on (but which are now "theirs" during that rebase)

    x--x..x..x..x <- old "theirs" commits, now "ghosts", available through reflogs
        \
         \
          \--y--y--y--y'--y'--x'--x'--x'(*) <-  branch B with HEAD updated ("ours")
                          ^
                          |
            upstream SVN tracking branch