令和のSVN->Git変換

## はじめに
Gitが主流になってからもう10年以上は経っているでしょうか。
しかし、かつて覇権を握っていたSubversion（以下、SVN）をいまだにソース管理に利用している開発現場もあることでしょう。私の現場がそうでした。
そんな時降ってきたSVNサーバーの閉鎖案内とGit移行の大号令、さらに移行方法は現場任せというデスゲームに参加したときのことを綴ろうと思います。
誰かの助けになれば幸いです。

## 作業環境
OS：Windows10
利用ソフトウェア：Git for Windows, Subversion for Windows, cmd, PowerShell
SVNサーバーへのアクセス：fileプロトコルのみ

CUIで移行していきます。GUIのツール等は使用しません。
個人的にはその方が内部でなにやってるかについて解像度が上がると思ってます。

## ソース管理とは履歴管理、移行とは履歴の移行
まず初めに考えることは、「履歴を移行するか？」です。
私を含め、チームメンバーがコミットしてきた差分が”歴史”としてSVNに蓄積されていっています。
それこそがソース管理の本質であり魂ですのでほとんどの場合は移行することになるかと思います。まぁ、履歴を引き継がないということであれば、このデスゲームから離脱できるのですが。

### author（作成者）情報を取得する
SVNとGitでは履歴の管理体系が違うため履歴情報を変換させる必要がありますが、
その際、「SVN履歴のこの作成者をGitではこのユーザ名、このメールアドレスと紐付けます」
という情報を渡してあげる必要があります。
これがないと履歴変換の途中で怒られるので注意しましょう。

次のコマンドでSVNログから履歴作成者の情報だけを一意になるように抜き出します。
SVNサーバーへのアクセスをhttpで行なっている場合はfile://~~のところをhttpのアドレスにして貰えば大丈夫です。

```powershell:powershell
svn log --xml file://SVNServerRepoPath | Select-String "<author" | Get-Unique
```
おそらく結果として、次のような情報が取得できているでしょう。
```xml:txt
<author>yamada-tarou</author>
<author>tanaka-hanako</author>
<author>hogehoge-fugafuga</author>
...
```
しかしこのままでは使用できないため、次の形式に変換してください。
（作成者情報取得と同時に変換することもできなくはないですが、作業環境によって変換手順が変わってくることも考えられるためここでは紹介しません）
```terminal:txt
yamada-tarou = yamada-tarou <yamadaTarou@subversion.com>
tanaka-hanako = tanaka-hanako <tanakaHanako@git.com>
hogehoge-fugafuga = hogehoge-fugafuga <hogefuga@mail.com>
...
```
＝の前後は一致していなくても問題ありません。Gitに移行する際、名前を変えたければ変えられます。

注意するべき点として、SVNでのコミットの際に作成者情報を付与せずにコミットしている人がいた場合です。
SVNログ上は「(no author)」として記録されていますのでこれも作成者のひとりとして変換情報に加える必要があります。こんな感じで。
```terminal:txt
(no author) = no_author <no_author@no_author>
```

### authorファイルの参照設定

そうしてできたauthor情報をテキストファイルにして任意の場所に配置します。
そして次のコマンドでauthorの情報はここですよーとGitに教えてあげましょう。
```shell:cmd
git config --global svn.authorsfile authorsFilePath.txt
```
もし移行するリポジトリが複数あり、authorが変わってくるのであれば、別々でファイルを作成しておいて変換時に指定する形でも良いでしょう。
ただ、別のリポジトリの情報が混ざっていても害はないので１ファイルにまとめてしまった方が楽かもしれません。お好きな方をどうぞ。

## SVNリポジトリをGitリポジトリとしてクローン
さて、ここから実際に履歴を変換し移行していくわけですが、使用するのはGitインストール時から一緒に入っているgit-svnというツールです。Git公式としてSVNからの移行をサポートしているわけですね。ありがてぇ。
### ちなみに
クローンすると言っておきながら、実際はクローンはしません。すいません。
というのもこれはGitについて勉強すればわかることですが、cloneは「initしてからfetchする」という動作を１コマンドで行うときに使用します。便利な気がしますが難点があり、initとfetchの両方が正常終了することで初めて完了となるので、途中でエラーになるとそれまで行ったことが無かったことになるんですね。
この変換作業においてそんなことをしていると大変面倒臭いのでcloneは使用せず、initとfetchは別々で行うこととしました。

### ※番外編：私の環境で発生した特殊手順※
私の現場ではSVNサーバーへはFileプロトコルでしかアクセスができない特殊環境でしたので、リポジトリのURLがそのままでは使用できない状況でした。
解決するために必要なツールとしてはSVNのインストール時一緒に入っているsvnserveです。
開発したソースを動作確認するときにローカルでサーバーたてますよね？ね？
ものとしてはあんなイメージです。
svnserveがSVNサーバーとの橋渡しをしてくれることで、fileプロトコルでもサーバーへアクセス出来るようになるのです。
やることはsvnserve.exeを実行するだけです。
（RepoParentsPathはfileプロトコル部分を除いたリポジトリURL）
```shell:cmd
cd "C:/Program Files/Subversion/bin/"
svnserve.exe --daemon --root=RepoParentsPath 
```

### クローンする前にオプションについて知ろう 
さて、ではgit-svnのinitで移行結果が格納されるGitリポジトリをローカルに生成するわけですが、私の環境の場合、単純に移行元のSVNリポジトリと移行先のGitリポジトリを指定してホイッ！ってわけにはいきませんでした。
簡単にいうとブランチの管理が杜撰だったんです。
branchesディレクトリ配下が整理されていないことや、不必要に階層が深いことで変換の仕様上ブランチと認識されないものがあったわけですね。
加えて、他アプリとリポジトリ内で同居をするという禁忌まで犯していたため、別居させる必要がありました。けしからんですね。
```
/repoRoot
 |-/trunk
 |-/tags
 |-/branches
   |-anotherApp-branch1
   |-anotherApp-branch2
   |-/hoge
   | |-feture-hoga
   | |...
   |-/fuga
   | |...
```

別居させる方法としては、どのディレクトリがbranch、trunk、tagなのかを指定するオプションがあります。trunkとタグは１つだけですが、branchは複数指定できます。上の図でいうrepoRootは含めずにその下から指定しましょう。
その指定部分だけをお見せするとこうなります。
```shell:cmd
--trunk=/trunkDirPath --branch=/branchDirPath1 --branch=/branchDirPath2 --tags=/tagsDirPath
```
あとはおすすめのオプションをつらつらと...
接頭辞の設定：変換後、Git側にsvnの取得元が記録されるときのパスに接頭辞がつけられます。svn/で始まるようにすると後から判別つきやすいのでおすすめです。
```
--prefix=svn/
```
メタデータ無し：変換後のログ情報にsvn-idという視覚的に邪魔な情報がつきますのでそれを排除できます。
```
--no-metadata
```
対象パス指定：特定の拡張子のファイルだけ抜き出して変換したいときなどに使用します。正規表現も使えますよ。(例はjavaファイルとxmlファイルだけを指定)
```
--include-paths=".*/.java|.*/.xml"
```
対象外パス指定：今度は逆に除外したいものがあれはこちらで指定できます。正規表現使用可能。
```
--ignore-paths=".*/.java|.*/.xml"
```

### initコマンドで「箱」作成
オプションを理解してどのような設定で変換を行うのかが決まったらinitコマンドを実行してSVNリポジトリをGitリポジトリに変換した後の結果を格納するための「箱」を作成します。
変換元のSVNリポジトリのURLを第一引数に指定します。svn://localhostの部分は前述のsvnserveを使用している場合に使用します。httpプロトコルで接続できる場合はそちらでかまいません。実行した際、エラーがなければ何も返ってこないコマンドです。
```shell:cmd
git svn init svn://localhost/SVNRepoPath --prefix=svn/ ...
```

### いよいよ変換
initコマンドで移行結果を格納する「箱」となるGitリポジトリを作成したので、続いてfetchコマンドを実行していきます。ローカルに作成する先のパスを先に作成しておいて、移動するのをお忘れなく。
author情報のテキストファイルをGitに覚えさせた場合、--authors-fileオプションは不要です。

```shell:cmd
mkdir "C:¥MakeGitRepoDirPath"
cd "C:¥MakeGitRepoDirPath"
git svn fetch --authors-file=../authors.txt
```

変換が実行されている間は今変換しているリビジョンの情報が表示されます。
こんな感じ。
```
r123 = 546983vm5y908m459y83b589b7n3958mb9mb3598 (refs/remotes/svn/trunk)
         A     directory/filename1.txt
         A     directory/filename2.txt
         A     directory/filename3.txt
         A     directory/filename4.txt
r124 = fsm4lkj7n54jb345366jnk57nk3n5lk3fg4fv4vf (refs/remotes/svn/trunk)
         M     directory/filename1.txt
         M     directory/filename3.txt
         D     directory/filename2.txt
         D     directory/filename4.txt
```
しばらく待ちましょう。
体感ですが、500リビジョンで１時間くらいです。もっと早くやる方法がもしかしたらあるのかもしれませんでしたが、私たちには時間がなかったのです。（？）

### 変換時のトラブルシューティング
- author情報が不足しているとそのユーザが作成したリビジョンのところでエラーになり止まります。author情報のテキストに加筆して同じfetchコマンドを実行すれば止まったところから再開できますので安心してください。
```
...
         D     directory/filename2.txt
         D     directory/filename4.txt
Author: hogehoge not defined in authors.txt file
```
- SVNの時にマージやらリネームやらをしていると、その影響で変換が失敗する時もあります。なんでかは知りません。お母さんに聞いてください。失敗する時そのリビジョンは諦めて変換をスキップしましょう。リビジョン344でエラーになった場合、以下のように345から再開できます。
```shell:cmd
git svn fetch -r 345:HEAD
```

## 移行先リポジトリにpushする
変換が正常に終わったら移行先のリモートGitリポジトリにpushします。
変換さえできてしまえばここからはGitの通常の操作になります。

### リモートリポジトリのアドレスを追加する
pushするにもpush先が分かってなければいけませんのでremote addで追加します。
まだ作成していない場合はここで作成。
```shell:cmd
git remote add origin https://RemoteGitRepoURL
```

### タグはどうなった？
そういえば、SVNから変換する時にタグはどうなったんでしょうか？
tagの一覧を見てみましょう。
```shell:cmd
git tag -l
```
何も出てこないはずです。
branchの一覧をのぞいてみましょう。
```shell:cmd
git branch -a
 * main
   /remotes/svn/tags/tag_20250101
   /remotes/svn/tags/tag_20250204
   /remotes/svn/tags/tag_20250305
   /remotes/svn/tags/tag_20250406
   ...
```
なんかいますね。これはまだ完全にGit側に取り込まれていないタグ達。
ちゃんと”Gitのタグ”として認識させてあげる必要があります。
上の例の2025/4/6に作成したっぽいタグをGitのものにする場合はこうすればOKです。
```shell:cmd
git tag tag_20250406 refs/remotes/svn/tags/tag_20250406
```
SVNの時の名前がダサかったら、Gitに持ってくる時に名前を変えることができます。
第一引数：Gitに持ってくるときのタグ名
第二引数：SVN側のタグのリファレンス名

ただ、ちょっとこれ大変なんですが１つずつ手作業でtagコマンドを実行する必要があります。
先にテキストエディタとかでコマンド用意しておいてバッと流しちゃうのがいいかも。

### いざ、push
ここまできたらやっとpush出来ます。
```shell:cmd
git push origin main:main
```
第一引数：push先リモート名
第二引数：[ローカルブランチ名]:[リモートブランチ名] ※ここでリネームできます
upstreamを設定するなどはお好きなようにしていただければと。

これだけではtagがpushできていないのでもうひとつ。
```shell:cmd
git push --tags
```
こちらのコマンドでtagがすべてpushされます。

### push時のトラブルシューティング
大半の場合、問題なくpushできると思いますが私の現場ではここにさらなる山が存在していました...

長くなるので一例として[別記事](https://qiita.com/sasami9999/items/2e2c6ee14706eb23177c)でご紹介しています。

## さいごに
こういった手順でデスゲームを乗り越えたわけですが、今回ご紹介したのはあくまで「リポジトリの移行」になりますので、この後には一番やっかいな「運用の移行」が待っています。すんなり適用できるメンバーで構成されていればいいですが、そうでない場合は...お察しします。
それでは良きGitライフを。