svn:externals代替計画

個人的なソースコードsubversionに全部突っ込んでまして、それなりに便利に使ってたのですが、時代の流れと共にいろいろ不都合が生じてきました。具体的には1.7系列にしたらsvkが動かなくなったのでオフラインコミットができなくなったり(将来的に本家で実装する予定だそうですがとりあえずsvkが動かないのは「今」なのです)、githubを使うようになって二重管理めんどかったり(公開用gitリポジトリの方はgit svn fetchしてmerge --squashしてcommitしてpushしてるだけなので手間はたいしたことないのですが、HDD上に同じソースコードが2つあるってのは何かと面倒を生じるのです)、あとまあいつの間にかbitbucketがプライベートリポジトリ作り放題になってたり最近私はWindowsを全く使ってない等の諸々の事情が合わさってsubversionを捨ててgitに完全移行する障壁が無くなってきた感じです。

という日記はともかく、今回はsvn:externalsの真似の話です。
各種分散型vcsも外部リポジトリを取り込む機能は提供してますが、subversionが便利だったのは、svn:externalsプロパティひとつで各種ゴタゴタを良きに計らってくれる点でした。

  1. svn:externalsでもpartial checkoutできる。
  2. 同じリポジトリ内での相対パスを取り込める。
  3. svkを併用すると、ローカルでの変更をローカルに伝播できる。

具体的には↓な運用。

lib1/
  source/
    lib.ads
app1/
  extlib/ svn:externalsに../../lib/source lib1を設定

この状態で全体をチェックアウトすると↓になります。

lib1/
  source/
    lib.ads
app1/
  extlib/
    lib1/
      lib.ads

lib1/source/lib.adsを更新してupdateするとapp1/extlib/lib1/lib.adsに反映されますし、逆も可。svkを使ってますと全部オフラインでできます(できました)。push先の指定なんかも不要で操作ミスを誘発するような要素もなくて、単純明快です。で、lib1以下だけ、app1以下だけをチェックアウトしても上手く動きます。

これをgitで再現したいわけです。

操作が煩雑になるのはまあ仕方ない。リポジトリを細かく分けないといけないのも仕方ない。app1にlib1を取り込むのはsubmoduleでできます。後はpartial checkout。ここまで前置き。

やりたいこととしては、lib1のmasterはlib1に必要なもの全部入りで、それとは別に外部からlib1を利用するため用に、lib1のsourceのみブランチを用意しよう、と。
http://progit.org/book/ja/ch6-7.html で説明されている例では、ブランチをmasterのサブディレクトリとして取り込み、ブランチ側の更新をmasterでマージしていますが、この逆ができたらいいなと。

とりあえずmaster作ります。

$ mkdir subtree && cd subtree && git init # 実験用リポジトリ
Initialized empty Git repository in ~/subtree/.git/
$ mkdir source
$ edit source/lib.ads # ソースを追加(editはTextWranglerを起動するコマンド)
$ edit manual.txt # ソースコード以外のものも追加しておく
$ git add source/lib.ads 
$ git add manual.txt 
$ git status
# On branch master
#
# Initial commit
#
# Changes to be committed:
#   (use "git rm --cached <file>..." to unstage)
#
#	new file:   manual.txt
#	new file:   source/lib.ads
#
$ git commit -m "master initial commit"
[master (root-commit) 52b8a83] master initial commit
 2 files changed, 2 insertions(+), 0 deletions(-)
 create mode 100644 manual.txt
 create mode 100644 source/lib.ads

空ブランチを作ります。作り方はgithub:pagesから。

$ git symbolic-ref HEAD refs/heads/sourceonly # 新しいブランチの名前
$ rm .git/index
$ git clean -fdx # (.gitディレクトリ以外の)ファイルを全部手動で消してもいい
Removing manual.txt
Removing source/
$ git status
# On branch sourceonly
#
# Initial commit
#
nothing to commit (create/copy files and use "git add" to track)
$ git branch
  master # sourceonlyブランチは出てこない。initial commitがまだ無いから?

この新しいブランチに、masterのsourceディレクトリをそのままコミットします。

$ git cat-file -p master # masterブランチが指しているコミットオブジェクトを調べる
tree b9231259976ca289eba4c430902a912d9bb12af7 # このtreeが本体だな
author yt <...> 1327636245 +0900
committer yt <...> 1327636245 +0900

master initial commit
$ git cat-file -p b9231259976ca289eba4c430902a912d9bb12af7 # treeの中を調べる
100644 blob ba0c3d41594a6bf8c9c45b8ea750426abc4789c2	manual.txt
040000 tree 493e1a42635421242b367f4793bc1cbd92cbcbef	source # 目的のディレクトリ発見
$ git read-tree 493e1a42635421242b367f4793bc1cbd92cbcbef # sourceディレクトリをそのままindexにする
$ ls # ワーキングコピーには反映されてない
$ git status
# On branch sourceonly
#
# Initial commit
#
# Changes to be committed:
#   (use "git rm --cached <file>..." to unstage)
#
#	new file:   lib.ads
#
# Changes not staged for commit:
#   (use "git add/rm <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#	deleted:    lib.ads # ワーキングコピーがないためなんか言われてますが無視します
#
$ git commit -m "initial source only"
[sourceonly (root-commit) 84cb94a] initial source only
 1 files changed, 1 insertions(+), 0 deletions(-)
 create mode 100644 lib.ads
$ git branch
  master
* sourceonly

めでたくsourceonlyブランチができました。
続けてマージのテスト。

$ git checkout master # masterに戻って
Switched to branch 'master'
$ edit source/lib.ads # 何がしかの変更を加えます
$ git status
# On branch master
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#	modified:   source/lib.ads
#
no changes added to commit (use "git add" and/or "git commit -a")
$ git add source/lib.ads
$ git commit -m "change in master"
[master fbdd687] change in master
 1 files changed, 3 insertions(+), 1 deletions(-)
$ git checkout sourceonly # sourceonlyブランチに切り替え
Switched to branch 'sourceonly'
$ ls
lib.ads
$ git merge -s subtree master # サブツリーマージ
Auto-merging lib.ads
CONFLICT (add/add): Merge conflict in lib.ads
Automatic merge failed; fix conflicts and then commit the result.
$ git status
# On branch sourceonly
# Unmerged paths:
#   (use "git add/rm <file>..." as appropriate to mark resolution)
#
#	both added:         lib.ads
#
no changes added to commit (use "git add" and/or "git commit -a")

あれ、なんかコンフリクトしました……?なんででしょう?

$ git reset --hard # というわけでやり直し
HEAD is now at 84cb94a initial source only
$ git merge -s subtree -Xtheirs master # とにかくmasterを使え
Auto-merging lib.ads
Merge made by the 'subtree' strategy.
 lib.ads |    4 +++-
 1 files changed, 3 insertions(+), 1 deletions(-)
$ git diff master # git diffには-sオプションが無いぞ……
# 大量のログ省略
$ edit lib.ads # しょうがないので目視確認、ちゃんとmasterの内容になってそう
$ git log
commit 5f079753cadb727a813556de9b29f2a2851b0552
Merge: 84cb94a fbdd687
Author: yt <...>
Date:   Fri Jan 27 13:30:36 2012 +0900

    Merge branch 'master' into sourceonly

commit fbdd68745883a47099ad386a1a40d2514ad3dca2
Author: yt <...>
Date:   Fri Jan 27 13:20:02 2012 +0900

    change in master

commit 84cb94a7ea53233bc89374413980181694fd0b6c
Author: yt <...>
Date:   Fri Jan 27 12:53:00 2012 +0900

    initial source only

commit 52b8a833a00339de319d8dbd2037fd41e521e34f
Author: yt <...>
Date:   Fri Jan 27 12:50:45 2012 +0900

    master initial commit

後はこのsouceonlyブランチを使う側でサブモジュールとして取り込めばおしまいです。
めでたしめでたし……煩雑ですよねー。
subversionのローカルコミット実装を熱烈希望します!!