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

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


# 私について

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


# 前提

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

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

```console:アプリケーションサーバー上
$ 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**: ファイルの内容だけを表すオブジェクト。
- **Tag**[^2]: Git オブジェクトにひもづく署名などを記録するオブジェクト。

[^1]: [オブジェクトDB - Git ユーザーマニュアル](http://img.atwikiimg.com/www8.atwiki.jp/git_jp/pub/Documentation.ja/user-manual.html#the-object-database)
[^2]: `.git/refs/tags` 以下の_軽量タグ_とは別です。

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

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


# 手順

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

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

[^3]: [Runs a number of housekeeping tasks within the current repository, such as compressing file revisions (to reduce disk space and increase performance) and removing unreachable objects which may have been created from prior invocations of git add. - `man git-gc`](https://git-scm.com/docs/git-gc)

```console:手元のローカルリポジトリで実行
$ # バラバラだった 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 オブジェクトをまとめたファイルをアプリケーションサーバーへ送り込めたので、サーバー上で展開します:

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

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

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

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

$ # ブランチの SHA1 を控えておく
$ git rev-parse master
1d1bdafd64266e5ee3bd46c6965228f32e4022ea
```

```console:アプリケーションサーバー上で実行
$ # 現在の HEAD が指しているブランチを確認しておく
$ git branch
* master
...

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

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


# まとめ

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

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