Posted at

git-svnでSVN→Gitへの移行をやってみたログ

More than 5 years have passed since last update.

※本稿は諸事情により過去の投稿を再投稿したものです。


はじめに

git-svn を使うと、素直な SVN リポジトリーなら簡単に移行できますが、実運用してきた SVN リポジトリーを移行する際はつまづくことも多々あります。

また Git リポジトリー化ができてもブランチやタグは手作業で作ることになります。

今回、たまたま移行を検討する機会があったので、その予行作業のなかで得た知識をメモしておきます。

作業を行ったクライアント環境は Ubuntu 14.04 / Git 1.9.3 です。

SVN リポジトリーは HTTPS でアクセスできる状態です。


移行の第一歩

まずは SVN リポジトリーからローカル環境に Git リポジトリーを作成します。

git svn clone または git svn init + git svn fetch を使います。

ヘルプや Book にも記載されていますが、 cloneinit => fetch と同じです。

次の2例はいずれも同じ Git リポジトリーが得られます。

clone

git svn clone -s --prefix=svn/ https://your.svn/repos/project

init => fetch

git svn init -s --prefix=svn/ https://your.svn/repos/project

git svn fetch

clone は常に 1~HEAD を取得しようとするので、エラーで止まった際に途中からスタートできません。

再開したい場合は fetch を使います。

たとえば、もし何らかの原因でエラーになってしまった場合


r3594 = 98235bbf9c9c5e62c23344203c09538d2930113b (refs/remotes/svn/tags/3.2.0)

Connection timed out: Error running context: Connection timed out at /usr/share/perl5/Git/SVN.pm line 184.


このようにコンソールに SVN リビジョン番号が残っているので、ここから fetch を再開できます。

# エラーのリビジョンから再開

git svn fetch -r 3594:HEAD

clone で作業をスタートしていても中では init => fetch が行われていますので、エラーになってから fetch を開始するような手順でも構いません。

また、エラーのリビジョンを諦めてスキップすることもできます。

(そのリビジョンの歴史を捨てることになります)

# エラーの次のリビジョンから再開

git svn fetch -r 3595:HEAD


オプションなしで fetch すると 1~HEAD まで取得しようとしますが(clone の動作)、私の経験上これは失敗することが多いように感じました。


fatal: refs/remotes/trunk: not a valid SHA1

update-ref refs/heads/master refs/remotes/trunk: command returned error: 128


このような参照更新に関するエラーがよく出ます。

HEAD を名前で指定するのをやめてリビジョン番号にした ところ、すんなり流れました。

# HEADリビジョンを調べる。

# 日本語環境の場合は ... grep '^リビジョン' ...
SVN_HEAD_REV=$(svn info $SVNURL/MyProject | grep '^Revision' | awk -F': ' '{print $2}')

git svn fetch -r 1:$SVN_HEAD_REV

ちなみに、上のコマンド例でリビジョンを調べている行は svn info からリビジョンの数字を取り出しているだけですので、結果を見て直接リビジョンを書いても構いません。


SVN ブランチを区別する

一般的には clone (init) に --prefix=svn/ をつけるのがベターとされています。

何も付けなくとも実行できますが --prefix=origin/ を付けるように促す警告が表示されます。

(Git 2.0 からはこれがデフォルトになるよ、という表示もあります)

例えば SVN 側に develop-hoge ブランチがある場合、何もつけないと Git 側に取り込んだリモートブランチ名も develop-hoge となり、純粋な Git ブランチなのか SVN と繋がっているブランチなのか判別しにくくなります。

--prefix=svn/ を与えると svn/develop-hoge という名前になるので、判別しやすくなります。


ブランチとタグの移行

無事 clone (init => fetch) できたら、 SVN のブランチとタグを Git 側に反映します。

fetch で細かくリビジョンをわけて取得した場合でも、 fetch に成功している範囲内のブランチやタグは取得できています。

git branch -r を見てみると、次のようなブランチが確認できると思います。

(これは --prefix=svn/ の例)


  • svn/trunk

  • svn/some-branch1

  • svn/some-branch2

  • ...

  • svn/tags/1.0

  • svn/tags/1.1

  • ...

すべての SVN ディレクトリが Git のリモートブランチとして登録されています。

SVN の trunk とブランチはそのままの名前で取得され、タグには "tags/" がついていることがわかります。

なお、この時点で git tag を実行しても Git のタグは1つもありません。

ここから、新しい Git リモートリポジトリーに push するためのブランチとタグを作っていきます。


ブランチの移行

今回の例では SVN ブランチ "develop-hoge" が Git リモートブランチ "svn/develop-hoge" となっているので、接頭辞を削除して Git ブランチとします。

ブランチがごく少ない場合は、 git branch -r | grep svn で確認して手作業でチェックアウトするのが一番確実です。

数が多い場合は、次のようなコマンドでループ処理しましょう。

# svn/trunk, svn/tags/*, svn/*@xxx を除いたリモートブランチを checkout

for BRANCH_NAME in $(git branch -r | grep -ve 'svn/tags\|svn/trunk\|.*@\d*' | sed -e 's:svn/::'); do
git checkout -b "$BRANCH_NAME" "svn/$BRANCH_NAME"
done;

# この時点でループの最後のブランチになっているので一応 master に戻しておく
git checkout master

ポイントとしては、稀に SVN リビジョンつきの hoge@105 のような名前になったブランチができているので、これも除外してやります。

これでローカルに SVN と同名のブランチができていますので、 Git リモートリポジトリーに push してやります。

git remote add origin YOUR_GIT_REPOS_URL

git push -u origin --all


タグの移行

ブランチと似たようなことをします。

SVN ブランチから "svn/tags" を探し、 Git のタグを作成します。

for TAG_NAME in $(git branch -r | grep -e 'svn/tags' | grep -ve '.*@\d*' | sed -e 's:svn/tags/::'); do

git tag "$TAG_NAME" "svn/tags/$TAG_NAME"
done

push します。

git push origin --tags


ブランチ名、タグ名に関する注意

超簡単にまとめると メタっぽい名前は避けろ ということです。

以下 git check-ref-format --help より簡潔に


  • 階層化やグループ化のためにスラッシュ / を使える

  • ドット . で始まってはいけない


  • .lock で終わってはいけない

  • 連続したドット .. は使えない

  • 制御文字, スペース, チルダ ~, キャレット ^, コロン : は使えない

  • クエスチョンマーク ?, アスタリスク *, 開始ブラケット [ は使えない



    • .git/config のリモートリポジトリー refspec 記述は除く



  • スラッシュ / で開始・終了したり、2つの連続したスラッシュ // を使ってはいけない

  • ドット . で終わってはいけない

  • アットマークとカッコを連続 @{ させてはいけない(git reflog で使っているから?)

  • アットマーク @ 1つだけの名前はダメ

  • バックスラッシュ \ は使えない

スラッシュによるグループ化は、大規模なリポジトリーできちんと規則を作れば便利そうですね。

ユーザー名ごとのグループブランチを作って、作業の成果を頻繁にプッシュさせるという運用を聞いたことがあります。これをやると、チーム内の活動がより見通せるようになるそうです。


Check : 移行に踏み切る前に


レイアウトは大丈夫か?

いわゆる標準レイアウトとは、次のような構造です。

REPOS_ROOT

└── project
├── trunk
├── branches
└── tags

この場合は、多少のエラーは喰らえども、少しずつ fetch して移行できるでしょう。

しかし、特殊なディレクトリ構造になっている場合もあるかもしれません。

そんな時は、 git svn init したあとに .git/config を直接編集してブランチの refspec を細かく指定してやります。

(参考リンクを参照)


不要なファイルがたくさん残っていないか?

ブランチやタグごとにディレクトリ全体のスナップショットを残す SVN よりはマシですが、それでも歴史の長いリポジトリーを移行すると、サイズが大きくなりがちです。

移行する前に、 SVN リポジトリーから不要な物を削除してダイエットさせましょう。

(参考リンクを参照)


参考リンク

これらの点について、こちらの記事で解説されています。


おわりに

以上で、新しい Git リモートリポジトリーに 可能な限りの SVN 履歴を残したまま移行することができました。

よき Git ライフを!