こんにちは。テクマトリックスの長久保です。

昨今の開発現場でのデファクトスタンダードになっている「Git」。Subversionからの移行を考えている方も多いかと思います。
「SubversionからGitに移行すべきか?」という問いに対しては、以下のブログにて詳しく説明しておりますので、ぜひ併せてご参照ください。

本ブログではSubversionからGitへの移行に対する注意点や懸念点について整理しています。SubversionからGitへ移行する際の1つの視点として参考にしていただけると幸いです。

前提条件

今回の検証に用いた環境は以下の通りです。

  • OS:Windows11
  • Git:version 2.39.2.windows.1
  • GitLab:GitLab Community Edition 15.0.2
  • Subversion:svn, version 1.14.3
    VisualSVN Server Manager:バージョン: 5.4.0
  • 実行PC
    CPU:Core i7-1165G7
    メモリ:32GB
  • 移行の方法:SubversionからGitへの移行については、Gitの拡張機能であるgit svnコマンドを用いて実行します。

移行案の検討

移行する方法にはいくつか手段が考えられるかと思います。

  1. Subversionの履歴を含めてすべて移行する
  2. 履歴の一部分のみ移行する
  3. 最新のコードのみを移行し、Subversionについては移行しない

1を選択したくなるかと思いますが、現実的に難しい場合もあります。私個人としては3を推奨します。

このことはMicrosoftの記事「一元化されたバージョン コントロールから Git に移行する」の記事の「履歴を移行するかどうかを決定する」の章の中でも触れられています。

Subversion (SVN) などの別のバージョン コントロール システムから Git に移行する場合、通常は、履歴を含めずに、リポジトリ コンテンツの最新バージョンのみを移行する “tip migration” を実行することをお勧めします。

https://learn.microsoft.com/ja-jp/azure/devops/repos/git/perform-migration-from-svn-to-git?view=azure-devops

以下では、Subversionの履歴を含めてすべて移行する方法と、移行に際しての課題を紹介します。

SubversionからGitへの移行例

git svnコマンドを利用して移行します。git svnコマンドについての詳細な使用方法については、以下のドキュメントを参照ください。
https://git-scm.com/docs/git-svn

事前準備

今回は以下の標準的なレイアウトのリポジトリを作成し、移行します。

サンプルデータとして、txt、png、jpg、mkvをランダムに追加や変更を行います。

リビジョン数は326、リビジョングラフは上記のようになります。リポジトリのサイズは一番サイズの大きな200102で300MB程度となりました。

移行方法

git svnコマンドは、SubversionリポジトリをGitリポジトリに変換するためのGitの拡張機能です。まず、git svnコマンドの標準的な使い方について、基本的な手順とよく使うオプションを交えて説明します。

1.ブランチのマッピング

まず初めに、SubversionのブランチをどのようにGitのブランチとして作成するか、マッピングを定義します。
Subversionリポジトリが標準的なレイアウト(trunk、branches、tagsがリポジトリのルートにある)を使用している場合には–stdlayoutオプションでの移行が可能です。
これ以外の場合は-T、-b、-tオプションをつけ、リポジトリについて個別に指定する必要があります。

2.ユーザー名のマッピング

Subversionのユーザー名は、多くの場合Gitのユーザー名とメールアドレスの形式(名前 <メールアドレス>)とは異なります。git svnコマンドでは、–authors-fileオプションを使って、Subversionのユーザー名をGitのユーザー名とメールアドレスにマッピングできます。

以下の形式のテキストファイルをauthors.txtとして作成します。

3.実際に移行

まず移行対象の作業ディレクトリを作成します。git initはgit svnコマンド内で実行してくれるため不要です。
今回はSubversionリポジトリが標準的なレイアウトなので、–stdlayoutを指定し、以下のコマンドで実行します。

git svn clone {svnリポジトリパス} --prefix=svn/ --no-metadata --authors-file "authors.txt" --stdlayout {gitの作業ディレクトリ}

コマンドを実行すると、以下のように処理が始まります。

しばらく待機していると、ローカルリポジトリへの処理が完了します。

問題なくローカルリポジトリにブランチが作成されています。リモートリポジトリにローカルリポジトリの内容をプッシュすれば移行は完了となります。

履歴も移行できていますし、ブランチも問題なく移行が完了しました。
ただし、このgit svnコマンドを用いての移行に関して、懸念点があるため以下に記載します。

移行時に問題となるケース

速度の問題

検証を行ったPCのスペックは特段低いものではなく、SubversionもGitLabもローカル環境で完結しているため、ネットワークアクセスも発生しません。それでも今回の検証に用いたリビジョン326のリポジトリでは、11分程度時間がかかりました。

git svnコマンド実行前後で時間を取得し出力。

これはあくまでも小規模なリポジトリでの例です。実際の運用環境では、数千、数万のコミット履歴を持つリポジトリも珍しくありません。そのような大規模なリポジトリでは、git svnコマンドによる移行に時間がかかりすぎる場合があります。また、長時間実行するとSubversion側の接続やネットワークがタイムアウトする可能性もあります。

移行時にオプションを使用することで巨大なリポジトリの移行速度を改善できる場合がありますが、オプションの説明にある通り、タイムアウトが発生するリスクもあるため、検証が必要です。

–log-window-size=
Subversion 履歴をスキャンするときに、リクエストごとに<n> 個のログ エントリを取得します。デフォルトは 100 です。非常に大きな Subversion リポジトリの場合、クローン/フェッチを妥当な時間内に完了するには、より大きな値が必要になることがあります。ただし、値が大きすぎると、メモリ使用量が増加し、リクエストがタイムアウトする可能性があります。

https://git-scm.com/docs/git-svn#Documentation/git-svn.txt—log-window-sizeltngt(Google Translateにて翻訳)

非標準レイアウトへの対応の難しさ

前述の通り、git svnコマンドは、標準的なレイアウト(trunk,branches,tagsがリポジトリのルートにある)を前提としています。しかし、実際のSubversionリポジトリは、必ずしも標準的なレイアウトに従っているとは限りません。例えば、trunkの代わりにmainという名前のディレクトリを使用していたり、branchesやtagsがリポジトリのルートではなく、別のディレクトリ配下に存在していたりする場合もあります。これに限らず、企業の運用に応じた様々な構成が考えられます。
このような場合、-T,-b,-tオプションを使って、trunk,branches,tagsのパスを明示的に指定する必要があります。しかし、構造が複雑すぎる場合や一貫性がない場合は、これらのオプションを使っても正しく移行できない可能性があります。

ユーザーマッピングの複雑さ

Subversionでは、ユーザー名のみでコミットが可能ですが、Gitではユーザー名とメールアドレスの両方が必要です。そのため、前述の通り、SubversionからGitへ移行する際には、Subversionのユーザー名とGitのユーザー名・メールアドレスを関連付ける必要があります。

この関連付けを行うために、authors.txtファイルを作成し、git svnコマンドに–authors-fileオプションで指定します。しかし、authors.txtファイルの作成と管理は、意外と手間がかかりますし、多くのユーザーが関わっている大規模なリポジトリでは、正確なマッピング情報を作成するのが困難な場合もあります。

特定のブランチやタグのみの移行が難しい

git svnコマンドは、デフォルトではリポジトリ全体の履歴を移行します。しかし、場合によっては、特定のブランチやタグのみを移行したい場合もあるかと思います。

git svnコマンドでも、-rオプションを使用して特定のリビジョン範囲を指定することで、部分的な移行を行うことは可能です。しかし、このオプションを使用する場合、事前に移行対象のブランチやタグがどのように作成され、マージされたのかを正確に把握しておく必要があります。

例えば、先ほどの例でリビジョン番号300以降のみ移行するとします。

git svn -r 300:HEAD clone {svnリポジトリパス} --prefix=svn/ --no-metadata --authors-file "authors.txt" --stdlayout {gitの作業ディレクトリ}

当然移行時間は早くなりますが、リビジョン番号300以降で処理されたブランチ、つまり上の例だと200102のブランチのみが移行されます。

リビジョンの指定方法によっては、上記のように特定のブランチが移行されないことも考えられます。また、複雑なブランチ戦略を採用しているリポジトリでは、この作業は非常に困難になる可能性があります。

コマンドだけでは移行できないケース

svn:externalsを利用している場合

Subversionのsvn:externalsプロパティは、別のリポジトリや、同じリポジトリの別の場所を参照する仕組みです。複数のプロジェクトで共通のライブラリを使用する場合などに利用されている環境もあるかと思います。
しかし、git svnコマンドでは自動的な Externals の変換機能は備わっていないようです。–show-externalsオプションはありましたが、移行に関するオプションは調べた範囲では見つかりませんでした。
svn:externalsを使用しているリポジトリを移行する場合、移行後に手動で修正する必要があり、移行プロセスが複雑になります。外部参照を Git のサブモジュールに置き換えるなどの対策を検討することになりますが、これも機械的に置き換えるのは難しいでしょう。リポジトリの構造や運用方法を大きく変更する必要がある場合もあります。

Subversionのフックスクリプト

Subversionではフックスクリプトなどを使って、コミットメッセージに特定のフォーマット(例えば、チケット番号を必ず含めるなど)を強制することができます。git svnコマンドはこの設定を引き継がないため、Git移行後に同様の仕組みを再構築する必要があります。

注意が必要なケース

svn:ignoreとGitの.gitignoreの差異やsvn:global-ignores

Subversionでは、svn:ignoreプロパティやsvn:global-ignoresプロパティを使用して、バージョン管理から除外するファイルやディレクトリを指定できます。一方、Gitでは.gitignoreファイルを使用して同様の設定を行います。
移行自体はshow-ignoreコマンドcreate-ignoreコマンドを用いて、移行することができそうです。

git svn show-ignore --id=origin/trunk > .gitignore
git add .gitignore
git commit -m 'svn:ignoreから.gitignore.への移行'

基本的には上記のコマンドで移行できるとは思いますが、.gitignoreは既に追跡されている場合はキャッシュ削除が必要といった細かな違いがあります。そのため、svn:ignoreを使用しているリポジトリを移行する場合、移行後に.gitignoreを手動で調整、検証する必要がある場合があります。
また、ローカル環境の除外リストを管理するsvn:global-ignoresを利用している開発者がいる場合、それらの設定は移行されません。移行後の.gitignore設定時に考慮する必要があります。

途中でブランチの名称が変更になった場合

Subversionでブランチ名が途中で変更された場合、名称変更前のブランチが作成されます。これはSubversionとGitのブランチの扱い方の違いから発生するものです。

  • Subversion:ブランチは単なるディレクトリのコピー。ブランチ名の変更は、ディレクトリ名の変更と同じ扱いになります。
  • Git:ブランチは特定のコミットを指すポインタ。ブランチ名の変更は、ポインタの名前を変更するだけで、コミットの履歴自体には影響しません。

git svnコマンドは、Subversionのディレクトリ構造をGitのブランチとして解釈します。そのため、Subversionでブランチ名を変更すると、git svnコマンドはそれを「元のブランチから新しい名前のブランチが作成された」とみなします。結果として、名称変更前・後それぞれのブランチが作成されます。

具体例

例えば、Subversionリポジトリで以下のような操作が行われたとします。

  • trunkで2回コミットを行う
  • trunkからfeature-aという名前のブランチを作成
  • feature-aで2回コミットを行う
  • feature-aをfeature-bに名前変更
  • feature-bで2回コミットを行う

リビジョン2までtrunkでコミット、その後feature-aでも開発を進めます。

その後、feature-aの名称をfeature-bに変更します。

feature-aは名称変更によって存在しなくなり、feature-bの履歴は上記になります。
この状態でgit svnコマンドを用いて移行すると、Gitリポジトリ上では以下のような状態になります。Subversionでは名称変更したfeature-aブランチ(名称変更前のディレクトリ)も移行されてしまいます。

  • feature-aブランチは、名前変更前のコミットまでの履歴(リビジョン1-6)を持つ
  • feature-bブランチは、名前変更前のコミットを含めた履歴((リビジョン1-9)を持つ

幸いなことにgit svnコマンド内で履歴の接続が行われているため、履歴が途切れることはありませんでした。

–follow-parentオプション
このオプションは、ブランチを追跡している場合にのみ関係します (リポジトリ レイアウト オプション –trunk、–tags、–branches、–stdlayout のいずれかを使用)。追跡されている各ブランチについて、そのリビジョンがコピーされた場所を見つけ、ブランチの最初の Git コミットで適切な親を設定します。これは、リポジトリ内で移動されたディレクトリを追跡している場合に特に役立ちます。この機能を無効にすると、git svnによって作成されたブランチはすべて線形になり、履歴を共有しなくなります。つまり、ブランチが分岐またはマージされた場所に関する情報がなくなります。ただし、長い/複雑な履歴をたどるには長い時間がかかる可能性があるため、この機能を無効にするとクローン作成プロセスが高速化される可能性があります。この機能はデフォルトで有効になっています。無効にするには、–no-follow-parent を使用します。

https://git-scm.com/docs/git-svn#Documentation/git-svn.txt—follow-parent(Google Translateにて翻訳)

ただし、この場合でも不要なブランチを削除する作業などが発生するため注意が必要です。
また、今回は検証していませんが、より複雑な履歴の変更やディレクトリ構造の変更を行っている場合、–follow-parentオプションが正常に動作しないことも考えられます。

それ以外の懸念事項

半角スペースの扱い

ファイル名やディレクトリ名に半角スペースが含まれている場合も移行時に問題が発生する可能性があります。

例えば、半角が混じったファイルをgit addしようとすると、上記のように分割され解釈されるため、エラーとなります。ファイル名を引用符で囲んだり、バックスラッシュでエスケープする等の対応は可能ですが、都度実施するのは運用上、手間になることが多いです。

また、極端な例ですが、Subversionではブランチ名(ディレクトリ名)に半角スペースを混ぜることも可能です。

今回はbranches以下に「a b c」、「a b c え」というブランチを作成しました。
git svnコマンドは成功し、ローカルリポジトリに移行は完了します。

ただ、この状態で移行した場合、プッシュ時に半角スペースによってエラーになります。

対策として、移行前にsvn renameコマンド等でブランチ名、ファイル名からは半角スペースなどの特殊文字を削除することを推奨します。

空のディレクトリ

空のディレクトリの扱いもSubversionとGitでは異なります。これが原因で、git svnコマンドでの移行時に問題が発生する可能性があります。

  • Subversion:空のディレクトリをバージョン管理できます。svn addコマンドで空のディレクトリをリポジトリに追加できます。
  • Git: 空のディレクトリを直接バージョン管理することはできません。

次の様にSubversionのディレクトリを作成します。folder1は空、folder2の中はtxtファイルが入っている状態です。

git svnコマンドを実行すると、folder1、folder2は見かけ上作成されますが、リモートリポジトリにpushしたタイミングでfolder1は消えます。

GitLabの画面です。
folder2に関しては想定通り移行されますが、folder1は消えています。

アプリケーションやビルドシステムが特定のディレクトリが存在することを前提としている場合、そのディレクトリが移行時に無視されると、アプリケーションが正しく動作しなくなる可能性があります。

不要なファイルの移行

長期開発をされているプロジェクトだとよくあることかと思うのですが、現在は不要になって削除済みであっても、過去にコミットされてしまった巨大なファイルも当然履歴上には存在するため、ローカルリポジトリに移行されてしまいます。これは–ignore-pathsオプションで移行対象から除外することも可能です。

過去の特殊対応

可能性の話であり、これを考慮し始めると切りがなくなってしまいますが、例えば手動で履歴を削除したり、特定のコミットを修正したり、サーバー上の直接ファイルを編集したりしていた場合、移行時に予期せぬ問題が発生する可能性はあると思います。

まとめ

SubversionからGitへの移行は推奨するところではありますが、移行する場合は上記の内容を考慮に入れ、移行する必要があります。
また、何か発生した際のSubverisonとGitの平行稼働時のマージ処理もそれなりに運用ルールを徹底する必要がありますし、移行後も移行漏れや追加対応が発生する可能性も大いに考えられます。

これらのことから、先述の通り、私個人としても以下の方法を推奨したいと思っています。

Subversion (SVN) などの別のバージョン コントロール システムから Git に移行する場合、通常は、履歴を含めずに、リポジトリ コンテンツの最新バージョンのみを移行する “tip migration” を実行することをお勧めします。

https://learn.microsoft.com/ja-jp/azure/devops/repos/git/perform-migration-from-svn-to-git?view=azure-devops

ただ、やはり過去の履歴を含めて移行したい場合もあるかと思います。その際の検討事項として、本ブログの内容が参考になれば幸いです。

開発基盤ソリューションではGitの移行の相談を受け付けています。

Gitに関する開発基盤ソリューションのブログ記事はこちらから。

Gitや構成管理(バージョン管理)ツール、CI/CDのご相談はこちらから。

By nagakubo

主にCI環境構築をメインで担当しています。 Certified CloudBees Jenkins Engineer (CCJE)