3

BACKGROUND

We have an SVN repository with multiple projects:

ROOT
 - Project1
   - trunk
 - Project2
   - trunk

..etc

Then we started a migration to git. We setup subgit to keep SVN and GIT repositories in sync (both-ways). For a few months already, we had some developers using svn and some using git, apparently without any problem.

Each of the projects above became a git repository, with subgit configured as:

trunk = trunk:refs/heads/master
branches = branches/*:refs/heads/*
tags = tags/*:refs/tags/*
shelves = shelves/*:refs/shelves/*

So far so good. Let's focus on Project1 from now on.

PROBLEM

I noticed that some of the recent commits done in svn were not ported to git. Looking at the logs on the svn side, I found out those commits are in an "inconsistent" state in svn as well (log partially anonymized):

$ svn log -v Project1

------------------------------------------------------------------------
r6109 | user1 | 2015-01-13 12:47:43 +0100 (di, 13 jan 2015) | 1 line
Changed paths:
   R /Project1/trunk (from /Project1/trunk:5477)

Branch '/trunk' replaced from /trunk:5477
------------------------------------------------------------------------
r6089 | user2 | 2015-01-08 13:46:27 +0100 (do, 08 jan 2015) | 1 line
Changed paths:
   M /Project1/trunk/src/com/....

-- fix xyz
------------------------------------------------------------------------
r5978 | user2 | 2014-12-26 21:30:28 +0100 (vr, 26 dec 2014) | 4 lines
Changed paths:
   M /Project1/trunk/src/com/...
-- fix abc
------------------------------------------------------------------------
[ ... more commits ... ]
------------------------------------------------------------------------
r5477 | user2 | 2014-10-16 17:07:25 +0200 (do, 16 okt 2014) | 1 line
Changed paths:
   M /Project1/trunk/src/com/...
-- fix bug23

Now, the last commit r6109 looks scary. It seems to say that the current trunk has been overwritten by r5477.

Indeed, the log of the trunk folder completely misses the revisions in between:

$ svn log -v Project1/trunk

------------------------------------------------------------------------
r6109 | user1 | 2015-01-13 12:47:43 +0100 (di, 13 jan 2015) | 1 line
Changed paths:
   R /Project1/trunk (from /Project1/trunk:5477)

Branch '/trunk' replaced from /trunk:5477
------------------------------------------------------------------------
r5477 | user2 | 2014-10-16 17:07:25 +0200 (do, 16 okt 2014) | 1 line
Changed paths:
   M /Project1/trunk/src/com/...
-- fix bug23

QUESTION

I'd like some help with identifying the current status and possible solutions.

What I think has happened (please help me here!):

  • at some point, a developer using git on the same source has used a git reset --hard to reset the working copy to the local master branch which was at the commit corresponding to the svn r5477, and pushed this to the remote master.
  • this, translated by subgit, resulted in the svn revision 6109, which says "Branch '/trunk' replaced from /trunk:5477"
  • Because the git repository is mapped to Project1/trunk, this has been rewritten with r5477
  • However, the folder Project1 is not even seen by subgit, which explains why the history of Project1 still contains the log of the now missing commits (the code in trunk is actually back to the r5477 revision).

Can anyone confirm that this is likely to be the current state? Am I still overlooking something? I didn't know that svn could allow you (subgit in this case) to end up with inconsistent histories.

If this is true, could anyone suggest what the best solution could be? Mind that the git repository does not have that part of the history, because it only syncs Project1/trunk and not Project1. I have the feeling this should be fixed in the svn repository. But I don't know how.

Is the following a possible solution?

$ cd Project1
$ svn merge -rHEAD:6089 .
$ svn ci

Warnings and/or better suggestions anyone?

cornuz
  • 2,678
  • 18
  • 35

2 Answers2

3

I'll explain what happened.Non-fast-forward update translation

There was an update of 'master' in Git repository, which is bound to trunk in SVN. Moreover the update was non-fast-forward, by default Git doesn't allow such pushes without -f option of git push command. Non-fast-forward updates are translated to branch replacements, because this is the closest analog in SVN repository. For example you can compare git log master after such and update with correponding svn log -v Project1/trunk output and see that both don't contain r6089, and the first commit listed will be r5477 (if we don't consider r6109 with no changes that has no analog in Git). To see revision numbers in git log output you can configure you Git client in a way described in "4.6. Recommended client-side Git configuration" section of SubGit book and run git fetch. So I don't think that SVN and Git repositories are inconsistent.

If you want to prevent branches replacements in the future you shouldn't use git push -f command. If you want to forbid such updates strictly, you can set receive.denyNonFastForwards option to true of your Git config on the server.

What about repairing: there're 2 approaches. If it's ok for you to have such a commit in your Subversion history, you can replace your trunk again but from trunk@6089. You can do that either from SVN side, e.g. in 2 steps:

$ svn delete Project1/trunk
$ svn commit -m "Trunk deleted"
$ svn update
$ svn cp <URL of trunk>@6089 Project1/trunk
$ svn commit -m "Trunk recreated from r6089"

Or you can update it from Git side (make sure your working tree is clean before doing that):

$ git update-ref refs/heads/master <SHA-1 of r6089 commit>
$ git push origin master -f

If your current branch is master, probably you'll need to run git reset --hard HEAD now.

Note that Git commit corresponding to r6089 is not lost, for such commits with no references SubGit creates an "attic" reference (to prevent collecting them by "git gc"), for your case it is refs/svn/attic/trunk/6089, I guess, so you can fetch is with

$ git fetch origin refs/svn/attic/trunk/6089:refs/heads/trunk6089

to have this commit on the client. Note, that after that you'll hardly ever notice any evidence of r6109 in the future, svn log (as well as git log) will jump over it directly to the last replace source, i.e. to r6089.

Another (worse) approach is to remove the r6109 from SVN history (I assume you have no other commits with the trunk). This can be done with "svnadmin dump/load" procedure. On the server run the following

$ svnadmin dump path/to/your/current/svn/repository -r1:6108 > repo.dump
$ svnadmin create path/for/repaiered/svn/repository
$ svnadmin load path/for/repaiered/svn/repository < repo.dump

Then configure access to this new repository instead of your SVN repository and reinstall SubGit for it. Some SHA-1 hashes may become different. There's a way to preserve the most of the hashes, but it depends on your SubGit mode: local or remote.

What about your approach with merge, as for me it would make the history complicated. Especially if you merge the whole project instead of some branches, so I wouldn't perform that merge.

If you have any questions feel free to contact support@subgit.com

Dmitry Pavlenko
  • 8,530
  • 3
  • 30
  • 38
0

The answer provided by Dmitry Pavlenko is correct. An important information is that there is a 1:1 relation between Git commits and SVN revisions as explained in a SubGit bugreport.

To prevent these branch replacements from happening, we do not sync branches containing a slash (default SubGit config) and use a server-side git hook (.git/hooks/user-pre-receive):

#!/bin/bash

check_ref() {

    OLD=$(git rev-parse $1)
    NEW=$(git rev-parse $2)
    REFNAME=$3

    ZERO="0000000000000000000000000000000000000000"

    if ! [[ $REFNAME == refs/heads/* ]]; then
            # Not a branch action, probably a tag
            return
    fi

    BRANCH_NAME=$(expr "$REFNAME" : "refs/heads/\(.*\)")
    FEATURE_BRANCH=$(expr "$BRANCH_NAME" : "\(.*/.*\)")

    if [ "$OLD" = "$ZERO" ]; then
            if [ ${FEATURE_BRANCH} ]; then
                    # Permit feature branch creation
                    return
            else
                    echo "*** Rejected creation of non-feature branch"
                    exit 1
            fi
    fi

    if [ "$NEW" = "$ZERO" ]; then
            if [ ${FEATURE_BRANCH} ]; then
                    # Permit feature branch deletion
                    return
            else
                    echo "*** Rejected deletion of non-feature branch"
                    exit 1
            fi
    fi

    if [ ${FEATURE_BRANCH} ]; then
            # Pushes to feature branches are always allowed
            return
    fi

    for COMMIT in `git rev-list $OLD ^$NEW`; do
            # $COMMIT is reachable from $OLD, but not $NEW -> Force push
            echo "*** Force push is not allowed on branch $REFNAME"
            exit 1
    done

    # Check for non-fast-forward merge
    ALL_REVS=`git rev-list $OLD..$NEW | wc -l`
    REVS_WITH_SINGLE_PARENT=`git rev-list $OLD..$NEW --max-parents=1 | wc -l`

    if [ ${ALL_REVS} -ne ${REVS_WITH_SINGLE_PARENT} ]; then
            echo "*** Non-fast-forward merge detected on branch $REFNAME"
            echo "*** Please rebase your work before pushing!"
            exit 1
    fi

    for COMMIT in `git rev-list $OLD..$NEW`; do
            # Checking commit $COMMIT
            git branch --contains $COMMIT | grep -q -v / && echo "*** Commit $COMMIT rejected because it is contained in other branches" && exit 1
    done

}

while read old new refname;
do
    check_ref $old $new $refname
done

exit 0