git-svn使ってる時に他のGitリポジトリにもpush


はじめに

GitのSubversionインタフェースであるgit-svnは「職場のVCSがSubversionだが、自分はGitで開発したい」って時に便利だと思う。で、「後ろにSubversion、ローカルはGit、pushの代わりにgit svn dcommit」というのが普通の運用だと思う。

で、個人的な理由で「git svn dcommitもgit pushもしたい」と思ってやってみたらダメだった、という話。

背景

まず背景として、僕は一本のSubversionリポジトリで全ての自分のドキュメントを管理していた。Subversionの管理はディレクトリ単位なので、プロジェクトもディレクトリ単位で管理されている。プロジェクトごとにリポジトリをわけなかったのは、バックアップが楽(dump一発)だというのと、WebDAVでアクセスさせる際にいちいちhttpd.confを書き直さなきゃいけなくなるのが面倒だったため。

しかし、Gitを使い始めてから、開発はGitでやりたくなった。で、こんなことを考えた。

  • Subversionで管理しているプロジェクトをディレクトリ単位でgit-svnで切り出して、ローカルではgitで開発。
  • 複数のマシンでそのプロジェクトを参照したいが、それは(git-svnではなく)gitでやる
  • そのため、Subversionリポジトリとは別にGitリポジトリも用意し、ローカルからSubversionとGit両方にcommit、pushする

イメージとしてはこんな感じ。

基本的にはGitで開発して、適宜Subversionに修正をdcommitしたかった。イメージはこんな感じ。

git_localがローカルのGitリポジトリ(master)、git_remoteがorigin、svn_repがSubversionリポジトリ。Otherはgit_remoteを通して開発、みたいな。

で、やってみるとわかるけど、git svn dcommitするとハッシュが変わってしまうため、いろいろ面倒になる。

実例(ドツボにハマってみる)

ちょっとやってみよう。

$ mkdir temp; cd temp
$ svnadmin create svn_rep
$ svn co file://`pwd`/svn_rep myrep
$ cd myrep
$ echo "Hello" > hello.txt
$ svn add hello.txt
$ svn ci -m "initial commit"
$ cd ..
$ git svn clone file://`pwd`/svn_rep git_local
$ git init --bare git_remote
$ cd git_local
$ git remote add origin ../git_remote
$ git push -u origin master

順番に

  1. svn_repというSubversionリポジトリを作る
  2. myrepという名前でsvnリポジトリをチェックアウトする
  3. 適当にファイルを作って追加し、commitする
  4. git-svnでsvnリポジトリをgit_localという名前でcloneする
  5. git_remoteというbareリポジトリを作って、そこにpushする

ということをやった。これにより、

  • Subversionリポジトリのリビジョンは1
  • masterもorigin/masterもRev. 1を指している

という状況になった。現時点でgit_remoteとgit_localの歴史は一致している1

次に、git_localで何か修正を加えよう。

$ echo "Hello2" >> hello.txt
$ git ci -m "revises hello.txt"
[master 59c7692] revises hello.txt
 1 file changed, 1 insertion(+)

ローカルの歴史が一歩進んだ。

この状態でpushすると、当然git_remoteとgit_localの歴史は一致する。

$ git push
Counting objects: 3, done.
Writing objects: 100% (3/3), 265 bytes | 265.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To ../git_remote
   4fd098d..59c7692  master -> master

次に、svnリポジトリに修正を取り込むためにgit svn dcommitしよう。

$ git svn dcommit
Committing to path/to/svn_rep ...
    M   hello.txt
Committed r2
    M   hello.txt
r2 = 3243102befcc3baf182cd3d1394066ab036aee14 (refs/remotes/git-svn)
No changes between 59c76928afb91491dcc29099f91d4538254b8972 and refs/remotes/git-svn
Resetting to the latest refs/remotes/git-svn

コミットハッシュが変わってしまい(59c7692→3243102)、ローカルにあった「origin/master」の指す先が消えてしまった。

この状態でpushしようとすると当然怒られる。

$ git push 
To ../git_remote
 ! [rejected]        master -> master (non-fast-forward)
error: failed to push some refs to '../git_remote'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

仕方ないのでgit pullしてみる。

$ git pull
Merge made by the 'recursive' strategy.

origin/masterが指していたコミットが挿入され、git_remoteと歴史を共有する形になった。

さて、こんどはpushの前にgit svn dcommitしてみる。

$ git svn dcommit 
Committing to path/to/svn_rep ...
No changes
8cc198b4c4d2c7e65c5f822b501f0a4917fe6735~1 == 8cc198b4c4d2c7e65c5f822b501f0a4917fe6735
No changes between 8cc198b4c4d2c7e65c5f822b501f0a4917fe6735 and refs/remotes/git-svn
Resetting to the latest refs/remotes/git-svn

あり?以下の状態に戻ってしまった。

このままではpushできないので、まずpullしてからpushしてみよう。

$ git pull
Merge made by the 'recursive' strategy.

$ git push
Counting objects: 2, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (2/2), 454 bytes | 454.00 KiB/s, done.
Total 2 (delta 0), reused 0 (delta 0)
To ../git_remote
   59c7692..49914fe  master -> master

状況はこうなった。

さて、gitのリモートとローカルの整合性はとれたので、あとはこれをSubversion側に反映させるだけだ。一番右のコミットがRev. 3になることを期待してgit svn dcommitすると・・・

$ git svn dcommit
Committing to path/to/svn_rep ...
No changes
49914feeb9772f9459b0b24a8298b837ffd0580c~1 == 49914feeb9772f9459b0b24a8298b837ffd0580c
No changes between 49914feeb9772f9459b0b24a8298b837ffd0580c and refs/remotes/git-svn
Resetting to the latest refs/remotes/git-svn

ぎゃー! また戻された!

回避方法

こういう状態になってしまったら、もうgit push -fするしかないと思う。

$ git push -f
Total 0 (delta 0), reused 0 (delta 0)
To ../git_remote
 + 49914fe...3243102 master -> master (forced update)

これで全てのリポジトリが整合性を保った状態になった。

git push -fを防ぐためには、普段から「必ずgit svn dcommitしてからgit pushする」する必要がある。しかしこうすると、他のサイトでGitリポジトリ経由でpush/pullできなくなる。

まとめ

GitとSubversion、両方のリモートリポジトリを持つのは(git push -fなしには)無理でした。っていうかGitのドキュメントにちゃんとその旨書いてある。

歴史を書き換えてもう一度プッシュしようなどとしてはいけません。また、他の開発者との共同作業のために複数の Git リポジトリに並行してプッシュするのもいけません。

結局のところ

  1. 全部Subversionでやる
  2. 全部Gitでやる
  3. 全部git-svnでやってリモートリポジトリはSubversionのみ

の三択ですね。3.にはメリットがなさそうに見えるけど、

  • 全プロジェクトのバックアップが一気に取れる。
  • 各ローカルリポジトリではブランチ切ったりstashしたりできる

というメリットはある。ただ、git svn dcommitする際はrebaseとかちゃんとして歴史がややこしくならないようにしないと問題起きそうですね。

後は気にせずgit push -fを使ってしまうという手もあるにはあるんだけど・・・開発者が一人しかいなければいいのかなぁ・・・。


  1. 絵を数枚描いてから気がついたけど、SubversionのRev. 0に対応するGitのコミットオブジェクトは存在しないですね。まぁ気にしないでください。