#!/bin/sh # # -- # # Wrapper around git-svn, allowing one to # * push the content of a git repo to an _empty_ SVN repo # * commit to SVN while keeping the git history intact # # Only the "master" branch is committed to SVN. # # Dependencies: # * find, to clean things up at the beginning # * git, duh # * svn, for the initial commit # # Written by Alexandre Niveau, GREYC-UniCaen # Please report bugs! # # -- # # ISC License # # Copyright © 2014, Alexandre Niveau # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. set -e # name of the script (without the full path) scname="${0##*/}" mysvnprefix="mysvn" # git directories gitdir="$(git rev-parse --git-dir)" gitroot="$(git rev-parse --show-toplevel)" mysvndir="$gitdir/$mysvnprefix" modefile="$mysvndir/mode" brmaster="master" brremotesvn="remotes/git-svn" brcurgit="refs/$mysvnprefix/current_git" # current meta-commit in git brcursvn="refs/$mysvnprefix/current_svn" # current meta-commit in SVN ### # usage function, called when the user # makes a mistake usage () { cat <" $scname check $scname {commit|ci} $scname setup [-g ] -s $scname {continue|clean|help|usage} $scname {fetch|up|pull|rebase} $scname setup {-r|-t} EOF } ### # A minimal online help. help () { cat < Sets up a new git-svn remote. If the SVN repo is empty, a dummy commit is pushed. to avoid having merge commits in master.) $scname [commit|ci] Fetches changes from the SVN repo, rebases new git commits on top of them, and commits. Could be useful: $scname clean Cleans all refs to SVN remotes from the git repo $scname [fetch|up|pull|rebase] Fetches changes from the SVN repo and rebases new git commits in master on top of them, without committing anything N.B.: it is advised to avoid merge commits in master, since conflicts have to resolved again during the rebasing of git commits onto SVN commits. In particular, if there are merge commits with conflicts in master before the initial rebase, the script will be interrupted. You have to finish the rebase and then execute $scname continue without changing branches. N.B.: fetching new SVN commits triggers a rebase on top of the latest git commit that was also in SVN. In case of conflicts, you just have to finish the rebase. This should leave things in a clean state; just try to commit again afterwards. EOF } ### # error in user input die () { echo "Error: $1" >&2 usage >&2 exit 1 } ### # check check_in_use () { [ -d "$mysvndir" ] || stateerror "git-mysvn does not seem to be used in this repo." } ### # error in state of the repo stateerror () { echo "Error: $1" >&2 exit 2 } ### # explains the need for branch setup # and sets root mode needsetuperror () { cat >&2 <] -s | | If -g is leaved out, the current meta-commit in git will be taken | to be the tip (that is, SVN is ahead of git). EOF echo "root" >"$modefile" exit 3 } ### # Removes all traces of previous SVN remotes. cleangitsvn () { echo "--- Cleaning git-svn ---" echo "Removing the svn-remote.svn section in config file…" git config --remove-section svn-remote.svn 2>/dev/null || echo " … no such section" echo "Removing the svn directory…" rm -rf "$gitdir/svn" echo "Removing all remotes…" # AFAIK, should be only # .git/logs/refs/remotes/git-svn # .git/refs/remotes/git-svn find "$gitdir" -name 'git-svn' -exec rm -f {} \; } ### # Clones an SVN repo # $1: the URL of the repo clonesvnrepo () { svnrepo="$1" [ -n "$svnrepo" ] || die "URL to SVN repo is invalid: '$svnrepo'" cleangitsvn echo "--- Cloning SVN repo ---" git svn clone "$svnrepo" "$gitroot" } ### # Initializes the mysvn directory cleanmysvn () { git rev-parse --verify -q "$brmaster" >/dev/null || die "there must be a branch named '$brmaster'" echo "--- Cleaning $scname ---" rm -rf "$mysvndir" rm -rf "$gitdir/refs/$mysvnprefix" mkdir "$mysvndir" } ### # Set current meta-commit branches according to options. # Automatic: # -r: the current meta-commit is the "root" (no common commit in git and SVN) # -t: the current meta-commit is the tip in both git and SVN # Manual: # -g : sets the current meta-commit in the git history # -s : sets the current meta-commit in the SVN history # If only one of the two are given, the other is set to the tip of the # respective branch. setupbranches () { refcurgit="unset" refcursvn="unset" mode="" while getopts g:rs:t arg do case "$arg" in g) [ "${mode:=-g or -s}" = "-g or -s" ] || die "-g has not meaning with ${mode}" refcurgit=$(git rev-parse --verify "$OPTARG") git update-ref "$brcurgit" "$refcurgit" ;; s) [ "${mode:=-g or -s}" = "-g or -s" ] || die "-s has not meaning with ${mode}" refcursvn=$(git rev-parse --verify "$OPTARG") git update-ref "$brcursvn" "$refcursvn" ;; r) [ "${mode:=-r}" = "-r" ] || die "-r has not meaning with ${mode}" git update-ref -d "$brcurgit" git update-ref -d "$brcursvn" echo "root" >"$modefile" ;; t) [ "${mode:=-t}" = "-t" ] || die "-t has not meaning with ${mode}" git update-ref "$brcurgit" "$brmaster" git update-ref "$brcursvn" "$brremotesvn" ;; \?) die "wrong parameters";; esac done [ -n "$mode" ] || die "wrong parameters" if [ "$mode" = "-g or -s" ] then [ "$refcurgit" != "unset" ] || git update-ref "$brcurgit" "$brmaster" [ "$refcursvn" != "unset" ] || git update-ref "$brcursvn" "$brremotesvn" fi } ### # Initializes an empty SVN repo with a dummy commit. # $1: the URL of the empty SVN repo initemptysvn () { svnrepo="$1" [ -n "$svnrepo" ] || die "URL to SVN repo is invalid: '$svnrepo'" svnwc="$mysvndir/svnwc" echo "--- Creating initial commit in SVN ---" [ ! -e "$svnwc" ] || stateerror "cannot create temp dir '$svnwc': already exists" svn co "$svnrepo" "$svnwc" cd "$svnwc" # dummy initial commit without creating any file or dir # idea taken here svn propset dummyproperty 1 . svn commit -m "Initial setup for git import" cd "$gitroot" rm -rf "$svnwc/.svn" rmdir "$svnwc" || stateerror "SVN repo was not empty!" } ### # Exports all commits in the master branch # on top of the SVN repo commitsvnfromroot () { echo "--- Rebasing git history on top of SVN ---" refmaster=$(git rev-parse --verify "$brmaster") git rebase --onto "$brremotesvn" --root "$refmaster" || { echo "continue" >"$modefile" cat >&2 </dev/null || ! git rev-parse --verify -q "$brcursvn" >/dev/null then needsetuperror fi if ! git rev-parse --verify -q "$brmaster" >/dev/null then git update-ref -d "$brcurgit" git update-ref -d "$brcursvn" stateerror "the '$brmaster' branch disappeared!" \ "Removing meta-commit branches." fi if ! git rev-parse --verify -q "$brremotesvn" >/dev/null then git update-ref -d "$brcurgit" git update-ref -d "$brcursvn" stateerror "the '$brremotesvn' branch disappeared!" \ "Removing meta-commit branches." fi } ### # fetches changes from SVN repo, rebases and merge with master. rebasesvn () { echo "--- Pulling new SVN commits ---" # Retrieving SVN commits, if any git checkout -q "$brcursvn" git svn rebase # If there have been commits, we need to rebase/merge them # with the git master refcursvn=$(git rev-parse --verify "$brcursvn") # tip of "local" SVN refremotesvn=$(git rev-parse --verify "$brremotesvn") # tip of remote SVN repo if [ "$refcursvn" != "$refremotesvn" ] then echo "--- Rebasing new SVN commits in git history ---" # curgit is the latest commit in the git history that # was committed to the SVN repo. refcurgit=$(git rev-parse --verify "$brcurgit") # we replay the new SVN commits (between "cursvn" and "remotesvn") # on top of our git history. git rebase --onto "$refcurgit" "$refcursvn" "$brremotesvn" # updating branch curgit git update-ref "$brcurgit" HEAD refcurgit=$(git rev-parse --verify "$brcurgit") # updating branch cursvn git update-ref "$brcursvn" "$refremotesvn" echo "--- Merging into master ---" # we merge changes into our master. # we use rebase, because merges can cause conflicts # and the conflicts will happen again when we dcommit. # put this at the end, because if there are conflicts # we are left in an illegal state. git checkout "$brmaster" git rebase "$refcurgit" fi } ### # Pushes to the SVN repo all new commits in the master branch. commitsvn () { # cursvn = remotes/git-svn: current SVN commit refremotesvn=$(git rev-parse --verify "$brremotesvn") # tip of remote SVN repo refcursvn=$(git rev-parse --verify "$brcursvn") # tip of "local" SVN [ "$refcursvn" = "$refremotesvn" ] || stateerror "$brcursvn ≠ $brremotesvn" # curgit is the latest commit in the git history that # was committed to the SVN repo. refcurgit=$(git rev-parse --verify "$brcurgit") refmaster=$(git rev-parse --verify "$brmaster") echo "--- Pushing git history to SVN repo ---" # rebase commits between curgit and master onto the svn branch git rebase --onto "$refcursvn" "$refcurgit" "$refmaster" git svn dcommit --rmdir --localtime # everything should work well… # updating branch cursvn git update-ref "$brcursvn" HEAD # updating branch curgit git update-ref "$brcurgit" "$brmaster" } ### # main if [ $# -eq 0 ] then command="help" else command="$1" shift fi currentmode="" [ -f "$modefile" ] && [ -r "$modefile" ] && currentmode="$(cat "$modefile")" if [ "$currentmode" = "continue" ] then [ "$command" = "continue" ] || stateerror "something was interrupted. Run '$scname continue'." fi case "$command" in clean) cleangitsvn;; clone) clonesvnrepo "$1";; setup) check_in_use setupbranches "$@" ;; init) # Clones SVN repo, and initializes it if empty. cleanmysvn clonesvnrepo "$1" if git rev-parse --verify -q "$brremotesvn" >/dev/null then # the SVN repo is not empty, ask for setup needsetuperror else # the SVN repo is empty initemptysvn "$1" clonesvnrepo "$1" echo "root" >"$modefile" fi ;; fetch|pull|up|rebase) check_in_use if [ "$currentmode" = "root" ] then # we only update the remote git svn fetch else checkbranches initialbranch=$(git symbolic-ref -q --short HEAD || git rev-parse --verify -q HEAD) rebasesvn git checkout "$initialbranch" fi ;; commit|ci) check_in_use initialbranch=$(git symbolic-ref -q --short HEAD || git rev-parse --verify -q HEAD) if [ "$currentmode" = "root" ] then # first we update the remote, in case there have been commits refcursvn=$(git rev-parse --verify "$brremotesvn") # tip of "local" SVN git svn fetch refremotesvn=$(git rev-parse --verify "$brremotesvn") # tip of remote SVN repo [ "$refcursvn" = "$refremotesvn" ] || needsetuperror commitsvnfromroot else checkbranches rebasesvn commitsvn fi git checkout "$initialbranch" ;; continue) check_in_use [ "$currentmode" = "continue" ] || die "nothing to continue." continuecommitsvn git checkout "$brmaster" ;; check) check_in_use echo "git-mysvn seems to be used in this repo." ;; usage) usage;; help) help;; *) die "unknown command";; esac