Git の内部構造を知っていたら、リモートリポジトリへアクセスできないホストへ差分を適用できた話(実話)

  • 11
    いいね
  • 6
    コメント

Git の内部構造を知っていたら、意外な状況で実務に役立った話です。
※ 実話です。

私について

git challenge という、Git を使った学生向け競技イベントの作問を担当しています。この git challenge は 2017/01/28(土)に第4回目が開催されますので、ご興味ある方は是非ご応募ください。

前提

あるサーバーAがあり、このサーバー上であるアプリケーションが稼働しています。このアプリケーションのコードは、Git で管理されています。

ただし、remote が謎の Git サーバーを指しています:

アプリケーションサーバー上
$ git remote --verbose
origin  git@192.0.2.1:example/repo (fetch)
origin  git@192.0.2.1:example/repo (push)

この Git サーバーの詳細は不明で、なるべく利用したくありません。また、アプリケーションが稼働しているサーバーのネットワーク設定変更はめんどくさいという状況です(他部署への依頼が必要)。

また、あなたの手元のマシンからこのアプリケーションサーバーへの接続には SSH が利用できます。

整理すると、登場するマシンは以下の3つです:

マシン 説明
アプリケーションの稼働しているサーバー 謎の remote を参照している。セキュリティ設定をいじれない。
謎の Git サーバー 詳細不明なので触りたくない。
手元のマシン 送信したい差分が含まれたローカルリポジトリがこのマシンにある。また、アプリケーションサーバーへ SSH 接続ができる。

さて、あなたは手元のマシンのローカルリポジトリから、このサーバーへ差分を送信しなければなりません。

計画

手段は2つ考えられます:

  • 手段1: 適当な Git サーバーを利用して、アプリケーションサーバーから pull する
  • 手段2: SSH 経由で Git オブジェクトを送り込む
  • 手段3(追記): (もっと簡単な方法があります。コメントをご参照ください)

手段1では、アプリケーションサーバーからのアウトバウンド接続が許可されていなければなりません。面倒なので、今回は SSH が繋がるならどんな状況でも対応可能な手段2をとることにします。

なお、手段2にある「Git オブジェクト」について、軽く説明しておきましょう。Git が管理するリソースは、主に次の4種類のオブジェクトとして保存されています1:

  • Commit: 1つのコミットを表すオブジェクト。コミットメッセージや日時、直前のコミットへの参照などが記録されている。
  • Tree: 1つのディレクトリを表すオブジェクト。ディレクトリが含むファイルやディレクトリの一覧(名前、SHA1、パーミッション)などが記録されている。
  • Blob: ファイルの内容だけを表すオブジェクト。
  • Tag2: Git オブジェクトにひもづく署名などを記録するオブジェクト。

これらの Git オブジェクトは、.git/objects/ 以下に Git オブジェクトのハッシュ値(SHA1)の名前で保存されます(例: .git/objects/00/0042d79352b4a7bb789d60e5324af92f29871a)。意外に思うかもしれませんが、Commit や Tree などのオブジェクトは、すべて同じディレクトリに格納されているのです。なお、容量削減のために .git/objects/pack/ 以下にまとめて圧縮されていることもあります(なお、git clone 直後は圧縮された状態です)。

さて、普段私たちが git pushgit pull で送受信しているのは、この Git オブジェクトたちです。したがって、SSH を介してこの Git オブジェクトをアプリケーションサーバーへ届けることができれば、アプリケーションサーバー上に差分を適用できるのです!

手順

まず、手元の Git オブジェクトをバラバラに送るのは面倒なので、ひとまとめにしてしまいましょう。ひとまとめにするには、git gcgit pack-objects コマンドが便利です。今回は、お手軽な git gc を使います。

この git gc は、Git リポジトリのお掃除と整理をおこなうコマンドです。
もともとは参照されていない Git オブジェクトのお掃除(Garbage Collection)を意味していますが、実際は Git オブジェクトの圧縮などもおこないます。この過程で、すべての Git オブジェクトが .git/objects/pack/pack-????.pack というファイルにひとまとめにして圧縮されます3。これは利用できますね!

手元のローカルリポジトリで実行
$ # バラバラだった Git オブジェクトを…
$ find .git/objects -type f
.git/objects/00/0042d79352b4a7bb789d60e5324af92f29871a
.git/objects/00/0079a2eaef17b7eae70e1f0f635557ea67b644
.git/objects/00/01140536704ecf3f0f86686fb6586cbc185ece
.git/objects/00/0147bbe4a00525d68efb1358c013812e10dcca
...

$ # 一つのファイルへまとめる(*.pack が圧縮されたファイル本体)
$ git gc
$ find .git/objects -type f | head
.git/objects/info/packs
.git/objects/pack/pack-65b96fd56b3f0c01ddd61cdb0c2b85890d7daa47.idx
.git/objects/pack/pack-65b96fd56b3f0c01ddd61cdb0c2b85890d7daa47.pack

$ # SSH 経由でアプリケーションサーバーへ送り込む
$ scp .git/objects/pack/pack-4ed9eb82b68c28bf2726c818a183b5adc096ba8d.pack app-server:/home/me/

Git オブジェクトをまとめたファイルをアプリケーションサーバーへ送り込めたので、サーバー上で展開します:

アプリケーションサーバー上で実行
$ # アプリケーションサーバー上のローカルリポジトリへ移動
$ cd /path/to/local-repo

$ # まとめた Git オブジェクトを展開
$ git unpack-objects < /home/me/4ed9eb82b68c28bf2726c818a183b5adc096ba8d.pack

現在の状態で、手元の Git オブジェクトは全てアプリケーションサーバー上でも存在するようになりました。ただし、まだ HEAD が指しているブランチが古いままなので、HEAD を最新のブランチに向きなおす必要があります:

手元のローカルリポジトリで実行
$ # 現在の HEAD が指しているブランチを確認しておく
$ git branch
* master
...

$ # ブランチの SHA1 を控えておく
$ git rev-parse master
1d1bdafd64266e5ee3bd46c6965228f32e4022ea
アプリケーションサーバー上で実行
$ # 現在の HEAD が指しているブランチを確認しておく
$ git branch
* master
...

$ # master を転送してきた最新の HEAD へ移動する
$ git reset --hard 1d1bdafd64266e5ee3bd46c6965228f32e4022ea

以上で、作業は完了です!

まとめ

実は、この話にはオチがあります。この不明な Git サーバーは、なんとアプリケーションサーバー自身を向いていたのでした。つまり、アプリケーションサーバーに Git サーバーが同居していたということになりますね(経緯不明かつ意味不明)!

こんな状況に陥らないように、なるべく謎の野良 Git サーバーを立てるのは避けたほうがよいでしょう。

この投稿は Git Advent Calendar 201622日目の記事です。