#!/bin/bash NEWSVNURL="file:///home/colin/svn-git/soft" SOFTWARE= if [ -d ".git" -a -f ".git/config" -a -f ".git/svn/.metadata" ]; then SOFTWARE=$(git config svn-remote.svn.fetch | cut -d':' -f1 | sed 's,/trunk,,' | trim) fi NEWSOFTWARE=$SOFTWARE if [ -n "$1" ]; then NEWSOFTWARE=$1 fi echo echo "Identified software name: $SOFTWARE (New name: $NEWSOFTWARE)" if [ -z "$SOFTWARE" ]; then echo "You must run this in a git-svn clone folder." >&2 exit 1 fi if [ -d ../$(basename $SOFTWARE).git ]; then echo "It seems the converted git repository already exists (../$(basename $SOFTWARE).git)" >&2 exit 1 fi AUTHORMAP=$(realpath $(dirname $(realpath $0))/../metadata/mageia-user-map.txt) if [ ! -f "$AUTHORMAP" ]; then echo "Cannot find author map." >&2 exit 1 fi REPLAY= if [ -f ../svn-import.log ]; then REPLAYDATA=$(grep ^$SOFTWARE, ../svn-import.log | tail -n 1) if [ -n "$REPLAYDATA" ]; then resetsha1=$(echo $REPLAYDATA | cut -d',' -f 2) revision=$(echo $REPLAYDATA | cut -d',' -f 3) morerevisionsx=$(echo $REPLAYDATA | cut -d',' -f 4) skiprevisionsx=$(echo $REPLAYDATA | cut -d',' -f 5) synthesized=$(echo $REPLAYDATA | cut -d',' -f 6) echo echo "I have found data from a previous run:" echo echo "Reset SHA1: $resetsha1" echo "Mageia Import SVN Revision: $revision" echo "Extra SVN Revisions to squash:$morerevisionsx" echo "SVN Revisions to skip: $skiprevisionsx" echo echo "Do you want to replay it? [Y/n]" read REPLAY if [ -z "$REPLAY" -o "Y" = "$REPLAY" ]; then REPLAY=y else REPLAY= fi fi fi remotes=$(git remote -v | cut -f1 | sort -u | xargs) if [ -n "$remotes" ]; then echo -n "Removing old remotes '$remotes'..." for remote in $remotes; do git remote rm $remote done echo "done" fi echo -n "Finding UUID for SVN Repository $NEWSVNURL... " NEWSVNUUID=$(svn info $NEWSVNURL | grep "^Repository UUID:" | sed 's/Repository UUID: //') echo $NEWSVNUUID echo -n "Updating SVN Remote config (old value: $(git config svn-remote.svn.url))... " git config svn-remote.svn.url $NEWSVNURL git config svn-remote.svn.fetch "$NEWSOFTWARE/trunk:refs/remotes/trunk" git config svn-remote.svn.branches "$NEWSOFTWARE/branches/*:refs/remotes/*" git config svn-remote.svn.tags "$NEWSOFTWARE/tags/*:refs/remotes/tags/*" echo "done" echo -n "Updating Internal SVN metadata... " OLDSVNUUID=$(grep "uuid = " .git/svn/.metadata | cut -d= -f2 | trim) sed -i "s,reposRoot = .*$,reposRoot = $NEWSVNURL,;s,uuid = .*$,uuid = $NEWSVNUUID," .git/svn/.metadata echo "done" echo echo "OK now the fun stuff beings. You need to find the git commit at which you want" echo "to start the Mageia import. This will typically require some investigation but" echo "the date will be around February 2011." echo "I will drop you to a shell. When you are done, simply copy the SHA1 sum and" echo "exit." echo "The commands you like want are 'git log trunk' and 'git show SHA1'" echo if [ -z "$REPLAY" ]; then echo "Press any key to contiue." read -n 1 bash echo echo "OK you're back :)" echo "What was the SHA1?" read resetsha1 fi if [ -z "$resetsha1" ]; then echo "You did not give any SHA1 sum. I will not reset the git repository and carry" echo "on importing from it's current state. You can quit this script and start" echo "again if this is incorrect." else echo "I got SHA1 '$resetsha1'. I hope that's correct". echo echo "I will now reset your git trunk and master branches." if [ -z "$REPLAY" ]; then echo "Is this OK? (press ctrl+c if not)" read -n 1 fi git update-ref -m "Rewind SVN Git to pre-Mageia import" refs/remotes/trunk $resetsha1 git checkout master git reset --hard trunk # Remove any heads that are newer than this (except trunk) resetreldate=$(git show -s --pretty='format:%ar' $resetsha1) resettimestamp=$(git show -s --pretty='format:%at' $resetsha1) echo "Removing any branches/tags newer than '$resetsha1' ($resetreldate)..." git for-each-ref --format='%(refname)' refs/remotes/tags/* refs/remotes/branches/* | \ while read ref; do # As svn tags show as new commits, we mustn't parse $ref directly, but # traverse back up the tag tree to find the first commit that *do not* # result in any changes (i.e. not an svn cp) and use that as our reference # comparison point. reftreesha1=$(git rev-parse "$ref":) parentref=$ref while [ "$(git rev-parse --quiet --verify "$parentref"^: )" = "$reftreesha1" ]; do parentref="$parentref"^ done reftimestamp=$(git show -s --pretty='format:%at' $parentref) if [ $reftimestamp -gt $resettimestamp ]; then echo " Removing '$ref'" git update-ref -d $ref # Also trash the old revmap rm -f .git/svn/$ref/.rev_map.* fi done echo "... done" fi echo echo "I will now run 'svn log' on the software. You should be able to guess the right" echo "revision to base the import on. When done, simply copy (or remember) the" echo "revision number and enter it into this script. You will also be able to" echo "(optional) select several other commits which you wish to squash into this" echo "initial commit." echo if [ -z "$REPLAY" ]; then echo "Press any key to contiue." read -n 1 svn log $NEWSVNURL/$NEWSOFTWARE | less echo echo "OK you're back :)" echo "What was the initial revision number?" read revision fi revision=$(echo $revision | sed 's/r//g') revision=$(( $revision + 0 )) echo "I got revision '$revision'. I hope that's correct". echo if [ -z "$REPLAY" ]; then echo "Now enter any further commits you wish to squash into our initial commit:" read morerevisionsx fi morerevisionsx=$(echo $morerevisionsx | trim) if [ -n "$morerevisionsx" ]; then for rev in $morerevisionsx; do rev=$(echo $rev | sed 's/r//g') rev=$(( $rev + 0 )) if [ $rev -gt 0 ]; then morerevisions="$morerevisions $rev" fi done echo echo "OK, you said you also wanted to squash the following commits:" echo " $morerevisions" echo fi if [ -z "$REPLAY" ]; then echo "Now enter any commits you wish to skip completely (e.g. for commits+reverts)" read skiprevisionsx fi skiprevisionsx=$(echo $skiprevisionsx | trim) if [ -n "$skiprevisionsx" ]; then for rev in $skiprevisionsx; do rev=$(echo $rev | sed 's/r//g') rev=$(( $rev + 0 )) if [ $rev -gt 0 ]; then skiprevisions="$skiprevisions $rev" fi done echo echo "OK, you said you wanted to skip the following commits:" echo " $skiprevisions" echo fi echo echo "The next step is the dangerous bit. We will clear out all files in the current" echo "working copy, run an svn export and then drop you to a shell to investigate the" echo "differences." echo "In that shell you must git add any new files that appear and ensure you are" echo "happy with the general state of things." echo echo "Continue? (press ctrl+c if not)" read -n 1 git reset --hard trunk resetsha1=$(git rev-list --max-count=1 HEAD) # The below is a "safer" verion of rm -rf * that also deletes hidden files find . -depth -not -iwholename '.' -not -iwholename "./.git" -not -iwholename "./.git/*" -not -iwholename "./.git/.*" -delete # We have to create a subversion repository here for the import as we need a repository # for the svn merging below... svn sucks. SVNPATH=$(realpath ../$(basename $SOFTWARE).svn) rm -rf $SVNPATH mkdir -p $SVNPATH/export cd $SVNPATH/export svn export --force --ignore-keywords --ignore-externals -r $revision $NEWSVNURL/$NEWSOFTWARE/trunk . cd - svnadmin create $SVNPATH/repo svnrepo=file://$SVNPATH/repo svn import -m "Import" $SVNPATH/export $svnrepo svn checkout $svnrepo $SVNPATH/checkout cd $SVNPATH/checkout if [ -n "$morerevisions" ]; then for rev in $morerevisions; do svn merge -c $rev --ignore-ancestry --accept theirs-full --allow-mixed-revisions $NEWSVNURL/$NEWSOFTWARE/trunk . svn commit -m "Merged r$rev" if [ $? -ne 0 ]; then echo echo "Merging in svn revision $rev failed. You need to rethink your import strategy." echo exit 1 fi done fi cd - svn export --force $svnrepo . rm -rf $SVNPATH echo echo "Dropping you to a shell. Use 'git status', 'git diff' and 'git add'." echo "DO NOT run 'git commit', I'll do that later for you." bash echo echo "OK you're back :)" echo "If everything is OK, and you're ready to continue, then hit return." read -n 1 COMMITTXT=$(mktemp /tmp/commitmsg.XXXXXX) cat >$COMMITTXT <>$COMMITTXT squashedrevs=$(svn log -r 1:$revision $NEWSVNURL/$NEWSOFTWARE/trunk | grep -E "^r[0-9]+ \| " | cut -d' ' -f1 | xargs | sed 's/r//g') rev=0 if [ -n "$morerevisions" ]; then for rev in $morerevisions; do svn log -r $rev $NEWSVNURL/$NEWSOFTWARE | tail -n +2 >>$COMMITTXT done fi # Import any documentation from the CLEANUP_PROGRESS.txt file [ "$rev" = "0" ] && rev=$revision CLEANUP_PROGRESS=$(mktemp /tmp/cleanupprogress.XXXXXX) svn cat $NEWSVNURL/$NEWSOFTWARE/CLEANUP_PROGRESS.txt@$rev 2>/dev/null >$CLEANUP_PROGRESS if [ -s $CLEANUP_PROGRESS ]; then echo >>$COMMITTXT echo "In addition to the above commits, the following cleaning work has been recorded:" >>$COMMITTXT echo >>$COMMITTXT cat $CLEANUP_PROGRESS >>$COMMITTXT fi rm -f $CLEANUP_PROGRESS COMMITDATE=$(svn log -r $revision $NEWSVNURL/$NEWSOFTWARE | head -n2 | tail -n1 | cut -d'|' -f3 | cut -d'(' -f1 | trim) env GIT_COMMITTER_NAME="Mageia SVN-Git Migration" \ GIT_COMMITTER_EMAIL="svn-git-migration@mageia.org" \ GIT_COMMITTER_DATE="$COMMITDATE" \ GIT_AUTHOR_NAME="Mageia SVN-Git Migration" \ GIT_AUTHOR_EMAIL="svn-git-migration@mageia.org" \ GIT_AUTHOR_DATE="$COMMITDATE" \ git commit -a --file=$COMMITTXT rm -f $COMMITTXT echo echo "OK, looks like we're good to go :)" echo "Press any key to continue with the subversion import process". read -n 1 git svn gc fakesha1=$(git rev-list --max-count=1 HEAD) echo echo -n "Updating trunk branch to include synthesized commit $fakesha1... " git update-ref -m "Reset to synthesized commit representing Mageia cleaned import (SVN Git)" refs/remotes/trunk $fakesha1 echo "done" echo -n "Faking git-svn metadata to allow continuation... " printf "0000000: %08x%s\t................" $revision $fakesha1 | xxd -r -c24 >.git/svn/refs/remotes/trunk/.rev_map.$NEWSVNUUID echo "done" # Keep a small log of the sha1's etc. if [ -z "$REPLAY" ]; then if [ ! -f ../svn-import.log ]; then echo "software,startingcommit,startingrevision,otherrevisions,skiprevisions,fakecommit" >../svn-import.log fi echo "$SOFTWARE,$resetsha1,$revision,$morerevisions,$skiprevisions,$fakesha1" >>../svn-import.log else sed -i "s/,$synthesized\$/,$fakesha1/" ../svn-import.log fi echo "Continuing svn fetch" if [ -n "$morerevisions" ]; then for rev in $morerevisions; do git svn fetch -A $AUTHORMAP -r $(( $revision + 1 )):$(( $rev - 1 )) revision=$rev done fi # Note the revisions to skip have to be after any squashed revisions which we # should really check for.... if [ -n "$skiprevisions" ]; then for rev in $skiprevisions; do git svn fetch -A $AUTHORMAP -r $(( $revision + 1 )):$(( $rev - 1 )) revision=$rev done fi git svn fetch -A $AUTHORMAP -r $(( $revision + 1 )):HEAD git reset --hard trunk echo echo "done" echo echo "Now creating bare git repository" git init --bare ../$(basename $SOFTWARE).git git remote rm origin git remote add origin ../$(basename $SOFTWARE).git #git push --set-upstream master git push origin master 'refs/remotes/*:refs/heads/*' pushd ../$(basename $SOFTWARE).git git branch -D trunk git branch -D origin/master 2>/dev/null # Tagging logic inspired by https://github.com/nothingmuch/git-svn-abandon/blob/master/git-svn-abandon-fix-refs # Keep a map of tags and their original sha1 for the SQL database cerated below declare -A tagmap tags=$(git for-each-ref --format='%(refname)' refs/heads/tags | cut -d / -f 4) for tag in $tags; do ref="refs/heads/tags/$tag" refsha1=$(git rev-parse "$ref") reftreesha1=$(git rev-parse "$ref":) # Find the oldest ancestor for which the tree is the same parentref="$ref" while [ "$(git rev-parse --quiet --verify "$parentref"^:)" = "$reftreesha1" ]; do parentref="$parentref"^ done parent=$(git rev-parse "$parentref") # If this ancestor is in master then we can just tag it # otherwise the tag has diverged from master and it's actually more like a # branch than a tag merge=$(git merge-base master $parent) if [ "$merge" = "$parent" ]; then targetref=$parent else targetref=$refsha1 fi tagmap[$refsha1]=$tag # create an annotated tag based on the last commit in the tag, and delete the "branchy" ref for the tag git show -s --pretty='format:%s%n%n%b' "$ref" | \ env GIT_COMMITTER_NAME="$(git show -s --pretty='format:%an' "$ref")" \ GIT_COMMITTER_EMAIL="$(git show -s --pretty='format:%ae' "$ref")" \ GIT_COMMITTER_DATE="$(git show -s --pretty='format:%ad' "$ref")" \ GIT_AUTHOR_NAME="$(git show -s --pretty='format:%an' "$ref")" \ GIT_AUTHOR_EMAIL="$(git show -s --pretty='format:%ae' "$ref")" \ GIT_AUTHOR_DATE="$(git show -s --pretty='format:%ad' "$ref")" \ git tag -a -F - "$tag" "$targetref" git update-ref -d "$ref" done # Implement a branch name policy # 1. Branches matching ([1-3]) are renamed to distro/mga\1 # 2. Branches matching (20[0-9\.]+) are renamed to distro/mdv\1 # 3. Branches matching (mes[0-9].*) are renamed to distro/\1 # 4. All other branches are renamed to topic/\1 (excl. master) branches=$(git for-each-ref --format='%(refname)' refs/heads | cut -d / -f 3-) for branch in $branches; do ref="refs/heads/$branch" refsha1=$(git rev-parse "$ref") reftreesha1=$(git rev-parse "$ref":) # Find the oldest ancestor for which the tree is the same parentref="$ref" while [ "$(git rev-parse --quiet --verify "$parentref"^:)" = "$reftreesha1" ]; do parentref="$parentref"^ done parent=$(git rev-parse "$parentref") # If this ancestor is in master then we can just squash it # otherwise the branch has diverged from master and it's actually a proper # branch merge=$(git merge-base master $parent) if [ "$merge" = "$parent" ]; then git update-ref "$ref" $parent $refsha1 fi if (echo $branch | grep -qE '^[0-3]$'); then git branch -m $branch distro/mga$branch elif (echo $branch | grep -qE '^20[0-9\.]+$'); then git branch -m $branch distro/mdv$branch elif (echo $branch | grep -qE '^mes[0-9].*$'); then git branch -m $branch distro/$branch elif [ "$branch" != "master" ]; then git branch -m $branch topic/$branch fi done git gc --aggressive popd # And finally we create some SQL for creating a nice revision map database # CREATE TABLE refs (distro char(3) NOT NULL, soft varchar(255) NOT NULL, revision int(10) unsigned NOT NULL, sha1 char(40) NOT NULL, head varchar(255) NOT NULL); # Parse a git-svn revmap file into SQL # $1 = Name of revmap file # $2 = Distro ('mdv' or 'mga') # $3 = head name i.e. branch or tag name (only used when not processing trunk/master) # $4 = Special SHA1 (last sha1 to look for if 'mdv', fake sha1 commit if 'mga') parsesvnrevmap() { if [ ! -f "$1" ]; then echo "No such file '$1'" >&2 exit 1 fi for map in $(cat "$1" | xxd -c24 -g24 | cut -b 10-57); do sha1=$(echo $map | cut -b 9-) if [ "$sha1" = "0000000000000000000000000000000000000000" ]; then continue fi if [ -n "$4" -a "$2" = "mga" -a "$sha1" = "$4" ]; then continue fi rev=$(printf "%d" 0x$(echo $map | cut -b 1-8)) if [ -n "${tagmap[$sha1]}" ]; then echo "INSERT INTO refs VALUES('$2', '$SOFTWARE', $rev, '', '${tagmap[$sha1]}');" else branch=$3 if [ -n "$branch" ]; then if (echo $branch | grep -qE '^[0-3]$'); then branch=distro/mga$branch elif (echo $branch | grep -qE '^20[0-9\.]+$'); then branch=distro/mdv$branch elif (echo $branch | grep -qE '^mes[0-9].*$'); then branch=distro/$branch elif [ "$branch" != "master" ]; then branch=topic/$branch fi fi echo "INSERT INTO refs VALUES('$2', '$SOFTWARE', $rev, '$sha1', '$branch');" fi # Exit if we've reached our reset sha1 as any future revisions are not used # by Mageia if [ -n "$4" -a "$2" = "mdv" -a "$sha1" = "$4" ]; then break fi done } echo echo -n "Creating revision -> sha1 map SQL... " sql="../$(basename $SOFTWARE)-revmap.sql" rm -f "$sql" "$sql".xz # First to the old (i.e. mdv) ones revmap=".git/svn/refs/remotes/trunk/.rev_map.$OLDSVNUUID" if [ -f "$revmap" ]; then parsesvnrevmap "$revmap" "mdv" "" $resetsha1 >>"$sql" fi for revmap in $(find .git/svn/refs/remotes/tags -name .rev_map.$OLDSVNUUID 2>/dev/null); do # Note, still pass in the tag name in case the tag is really more of a branch... parsesvnrevmap "$revmap" "mdv" "$(echo $revmap | cut -d'/' -f6)" >>"$sql" done for revmap in $(find .git/svn/refs/remotes -maxdepth 2 -not -iwholename ".git/svn/refs/remotes/trunk/.rev_map.*" -name .rev_map.$OLDSVNUUID 2>/dev/null); do parsesvnrevmap "$revmap" "mdv" "$(echo $revmap | cut -d'/' -f5)" >>"$sql" done # Inject the squashed commits into the map for rev in $squashedrevs $morerevisions; do echo "INSERT INTO refs VALUES('mga', '$SOFTWARE', $rev, '$fakesha1', '');" >>"$sql" done if [ -n "$skiprevisions" ]; then for rev in $skiprevisions; do echo "INSERT INTO refs VALUES('mga', '$SOFTWARE', $rev, '', '');" >>"$sql" done fi # And the rest of the Mageia commits revmap=".git/svn/refs/remotes/trunk/.rev_map.$NEWSVNUUID" if [ -f "$revmap" ]; then parsesvnrevmap "$revmap" "mga" "" $fakesha1 >>"$sql" fi for revmap in $(find .git/svn/refs/remotes/tags -name .rev_map.$NEWSVNUUID 2>/dev/null); do # Note, still pass in the tag name in case the tag is really more of a branch... parsesvnrevmap "$revmap" "mga" "$(echo $revmap | cut -d'/' -f6)" >>"$sql" done for revmap in $(find .git/svn/refs/remotes -maxdepth 2 -not -iwholename ".git/svn/refs/remotes/trunk/.rev_map.*" -name .rev_map.$NEWSVNUUID 2>/dev/null); do parsesvnrevmap "$revmap" "mga" "$(echo $revmap | cut -d'/' -f5)" >>"$sql" done xz "$sql" echo "done"