知に至る病

お勉強したことを忘れないように書き留めています。

Dropbox で Git のワークツリーのみを同期する

Dropbox に置いたディレクトリを Git でバージョン管理しつつ,リポジトリを同期対象から外し,ワークツリーのみを同期する方法を考えてみました。 Git と Dropbox の連携というと,Dropbox をプライベートなリモートリポジトリとして利用する方法が知られていますが,この記事ではそれとは別の連携方法を試しています。

Git と Dropbox の連携

やっていること

Dropbox 内のディレクトリをリポジトリとして初期化する際に,リポジトリの管理データが格納される Git ディレクトリの場所を Dropbox の外に指定します。 git 1.7.5 で追加された --separate-git-dir オプションを使うと Git ディレクトリの場所を指定することができます。 作業を中断して別の端末で続きをするときには,Dropbox で同期した作業ディレクトリの内容を維持するために git pull ではなく git fetch + git reset を使用します。

できるようになること

現在の作業を中断して別の端末で続きをしなくてはならない状況で,一時的なコミットを作らずにワークツリーの状態を別のリポジトリにコピーすることができます。 また,移動中や出先など Git が使えない環境にいる場合やコミットしていない更新がある場合でも最新の作業内容にアクセスすることができるようになります。 GitHub はブラウザから編集する機能がありますが,当然コミット&プッシュしていないと編集できません。

Dropbox 内のディレクトリを単純に Git で管理した場合に生じるいくつかの問題が解消されます。 たとえば,同期中にコミットしてリポジトリを破壊してしまう危険性はありませんし,Git で何か操作をするたびに余計な更新が発生することもありません。 Dropbox の無料ユーザーであるわたしにとっては,リポジトリのデータでストレージ容量が圧迫されるのを防ぐことができるのがもっとも重要かもしれません。

Git ディレクトリの場所を指定する

ローカルリポジトリの初期化

Dropbox 内のディレクトリで --separate-git-dir オプションに Dropbox 外のディレクトリを指定して git init を実行します。 すでに Git 管理下にあるディレクトリで同じことをするとリポジトリの再初期化となり,Git ディレクトリの位置を変更することができます。

$ cd ~/Dropbox/project

$ git init --separate-git-dir /path/to/project.git
Initialized empty Git repository in /path/to/project.git/

指定した場所に Git ディレクトリが作成され,プロジェクトが Git の管理下に置かれました。

$ ls /path/to/project.git
config  description  HEAD  hooks/  info/  objects/  refs/

リポジトリとワークツリー

何もオプションをつけずに git init を実行すると,カレントディレクトリに .git という名前で Git ディレクトリが作成されます。 --separate-git-dir オプションで Git ディレクトリの場所を指定すると,カレントディレクトリには Git ディレクトリの絶対パスを記述した .git ファイルが代わりに作成されます。

$ ls -lA
total 1
-rw-r--r-- 1 amano41 amano41 35 Nov  6 00:00 .git

$ cat .git
gitdir: /path/to/project.git

また,リポジトリの config に core.worktree という項目が追加され,このリポジトリに対応するワークツリー(作業ディレクトリ)の絶対パスが保存されます。

$ git config --local core.worktree
/home/amano41/Dropbox/project

普通に操作することが可能

リポジトリの作成が終われば,あとは普通の Git プロジェクトとして作業することができます。

$ echo "hello world" > hello.txt

$ git add hello.txt

$ git commit -m "First commit"

リモートリポジトリとのやり取りもまったく問題ありません。

$ git remote add origin git@github.com:amano41/project.git

$ git push origin master

$ git log --oneline --decorate --all
582972b (HEAD -> master, origin/master) First commit

別の端末でもコミットできるようにする

Dropboxディレクトリを同期しながら,Git でバージョン管理ができるようになりました。 別の端末や移動中であっても,ネット環境があって Dropbox さえ使えれば最新の作業内容にアクセスできることになります。

しかし,別の端末から作業の続きをすることはできますが,このままではコミットの履歴を残すことができません。 Git で管理しているプロジェクトですからリモートリポジトリからクローンしてもよいのですが,そうすると Dropbox を使っている意味がなくなってしまいます。 別の端末でもディレクトリを Git の管理下に置き,Dropbox で同期した内容はそのままにして,リポジトリだけを最新の状態にする方法を考える必要があります。

状況の確認

別の端末でも Dropbox によってプロジェクトのディレクトリが同期されています。 この中にはリポジトリの実体(Git ディレクトリ)への参照である .git ファイルも含まれています。

$ cd ~/Dropbox/project

$ ls -A
.git  hello.txt

しかし,この端末には .git ファイルに書かれている場所に Git ディレクトリがないため,git コマンドを実行することはできません。

$ git status
fatal: Not a git repository: /path/to/project.git

ローカルリポジトリの(再)初期化

まずはローカルリポジトリを作成して,このマシンでもプロジェクトを Git の管理下に置きます。

そのまま git init を実行してもエラーとなるため,.git ファイルを削除 or リネームした上で git init を実行します。 --separate-git-dir オプションに先ほど指定したものと同じパスを指定することに注意してください。 .git ファイルの内容が変わってしまうと,先ほどの端末で Git がリポジトリを見つけられなくなります。

.git ファイルにはリポジトリの場所が絶対パスで書き込まれますが,作業ディレクトリからの相対パスに書き換えても動作するようです。 リポジトリの場所を端末間で一致させられない場合でも,作業ディレクトリからの相対的な位置関係を同じにしておけば対応できます。 --separate-git-dir オプションで相対パスを指定しても .git ファイルには絶対パスで書き込まれるので,いちいち手動で編集しなくてはならないのが面倒ですが……。

$ rm .git

$ git init --separate-git-dir /path/to/project.git
Initialized empty Git repository in /path/to/project.git/

リポジトリが作成され,この端末でも Git によるバージョン管理ができるようになりました。 Dropbox のおかげでワークツリーは最新の状態になっていますが,リポジトリを作ったばかりなのでコミットの履歴はまだありませんし,どのファイルも追跡されていない状態です。

$ git log
fatal: your current branch 'master' does not have any commits yet

$ git status
On branch master

Initial commit

Untracked files:
  (use "git add <file>..." to include in what will be committed)

        hello.txt

nothing added to commit but untracked files present (use "git add" to track)

リモートリポジトリの情報にあわせる

これまでのコミット履歴を参照できるようにするため,git fetch でリモートリポジトリから情報を取り込みます。 最初の端末で行った作業の記録が追跡ブランチ origin/master にコピーされます。 ワークツリーの内容は Dropbox によって同期されていますので,これで手元に前回の作業終了時の情報がすべて揃うことになります。

コミットがない状態 and/or ステージングされていない変更がある状態ではマージできませんので,git pull ではエラーが出ます。 仮にマージできたとしても,Dropbox で同期したワークツリーの内容が上書きされてしまうので望ましくありません。

$ git remote add origin git@github.com:amano41/project.git

$ git fetch origin master

$ git log --oneline --decorate --all
582972b (origin/master) First commit

Dropbox で同期したワークツリーの内容を保持したまま HEAD とインデックスの状態を追跡ブランチ origin/master に一致させるために git reset を実行します。 先ほど git pull を使わなかったのと同じ理由で git checkout は使い(え)ません。 リポジトリの情報が更新され,先ほどは未追跡の状態だったファイルもきちんと Git の管理するファイルとして認識されるようになります。

$ git reset origin/master

$ git log --oneline --decorate --all
582972b (HEAD -> master, origin/master) First commit

$ git status
On branch master
nothing to commit, working directory clean

最初の端末で作業を終えたときの状態が完全に再現されました。

リモートリポジトリからクローンしてくるのと違う点は,コミットしていなかった変更点も Dropbox のおかげで手元に反映されているということです。 コミットしていない変更点は git stash で一時保存できますが,それをリポジトリ間でコピーすることはできません。 中断した作業を引き継ぐには,(一時的なブランチを切って)リモートリポジトリ経由でマージするか,Git を使わずに直接コピーするしかなかったのではないかと思います。 今回は後者のやり方を Dropbox に代わりにやってもらったことになります。

中断した作業を別の端末で再開する

Dropbox で作業ディレクトリを同期しながら,それぞれの端末で Git によるバージョン管理を行う環境が構築できました。 コミットの履歴はリモートリポジトリで共有していますので,いわば作業ログも同期していることになります。

以下では,ある端末で中断した作業の続きを,別の端末で再開する流れについて例示したいと思います。 とはいっても,やり方は基本的にローカルリポジトリの再初期化をしたときと同じです。 作業終了時にそれまでのコミットをプッシュしておき,作業する端末が変わったらフェッチして最新のコミットにリセットするだけです。

端末 A で作業する

いつも通りに作業を進めます。 今日は新しいファイルを作りました。

$ echo "good bye" > bye.txt

$ git add bye.txt

$ git commit -m "Add bye.txt"

その日の作業が終わったので,別の端末からもコミット履歴がたどれるようにリモートリポジトリにプッシュしておきます。 ある程度まとまった作業が終わってからプッシュするのが一般的だと思いますが,たとえば自宅に帰ってから続きをするつもりであれば今日コミットした分は必ずプッシュしておきます。

$ git push origin master

$ git log --oneline --decorate
927bcd8 (HEAD -> master, origin/master) Add bye.txt
582972b First commit

端末 B で作業の続きをする

先ほど端末 A で作ったファイルが Dropbox によって同期されています。 この端末の Git にとっては作業ディレクトリに新しく追加されたファイルですので,これを追跡されていないファイルとして検知します。 もし,追跡対象となっている既存のファイルを変更していた場合には,ステージングされていない変更として検知されることになります。

$ ls
bye.txt  hello.txt

$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)

        bye.txt

nothing added to commit but untracked files present (use "git add" to track)

コミット履歴はリモートリポジトリがローカルリポジトリよりも先行している状態です。 リモートリポジトリが先行している分は端末 A でコミットしたものであり,現在のワークツリーはこのコミット(+α)の状態であることがわかっています。 コミットせずに中断していたり,出先や移動中にファイルを変更していたりすることもあるため,必ずしもワークツリー=最新のコミットではなく,+αの可能性があることに注意してください。

$ git fetch

$ git log --oneline --decorate --all
927bcd8 (origin/master) Add bye.txt
582972b (HEAD -> master) First commit

ローカルリポジトリを再初期化したときと同じように,git reset で HEAD とインデックスの状態を追跡ブランチ origin/master に強制的にあわせてやります。

$ git reset origin/master

$ git log --oneline --decorate
927bcd8 (HEAD -> master, origin/master) Add bye.txt
582972b First commit

$ git status
On branch master
nothing to commit, working directory clean

端末 A で作業を終えた状態(+α)が再現できましたので,あとはいつも通り作業の続きをします。 作業を終えるときには,それまでコミットした分をプッシュしておくのを忘れないでください。

端末での作業を終えるときの留意点

その日の作業を終えるときには,それまでにコミットした分をリモートリポジトリに反映させるのを忘れないようにします。 リモートリポジトリ経由でコミット履歴を取り込めない状態で Dropbox がワークツリーを同期すると,一度もコミットをしないまま多くの作業をしてしまったのと同じ状態になります。

プッシュするのを忘れていたとしても作業の続きをすることはできますが,別の端末での作業内容を部分的に修正することが難しくなりますし,あとでマージなりリベースなりする必要があって面倒です。

参考資料