git で一度 push した履歴は簡単には消えないが、ダウンロードも難しい

# 動機

[リモートリポジトリに含めたくないコミット履歴をgit rebaseコマンドで無に帰す方法](https://qiita.com/bakira/items/8ba745c18c4a245c6ae5) 等(TODO: 要アップデート)、git において過去の履歴を消す方法が挙げられている。これは本当に安全なのか。

疑問は <blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">これ本当に大丈夫なんだろうか（参照されないコミットが残ってそうな気がする）： リモートリポジトリに含めたくないコミット履歴をgit rebaseコマンドで無に帰す方法 on <a href="https://twitter.com/Qiita?ref_src=twsrc%5Etfw">@Qiita</a> <a href="https://t.co/rBL6GgUmQt">https://t.co/rBL6GgUmQt</a></p>&mdash; Kazuhiko Kikuchi (@kazuk) <a href="https://twitter.com/kazuk/status/950967907575480320?ref_src=twsrc%5Etfw">2018年1月10日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> という意見による。

# 結論

結論を先に書くと、一度 push してしまうと、しばらくは(見つけにくいけれども)データが残る可能性がある。ということになった。~~ただし、cygwin 上のファイルシステムと、ssh 越しにしか実験をしていないため、GitHub, GitLab, bitbucket 等での扱いは別途調べる必要がある。~~

# 検証手法

とても長い…

## 概要

1. サーバへのアップロード
 1. 本来隠したい情報を持つコミットを行う
 2. `git push` する # 時点 B
 3. 本来隠したい情報を消すコミットを加える
 4. `git rebase` で、本来隠したい情報のコミット(コミット自体の存在を)を消す
 5. `git push -f` する # 時点 C
 6. 別のリポジトリに `git push` する
2. サーバからのダウンロード

## サーバへのアップロード

### SSH 越しサーバに対する実行例

#### サーバ側準備

``` sh
mkdir -p git_test
cd git_test
git init --bare repo_bare.git
git init --bare repo_bare_2.git
```

#### 履歴追加の流れ

``` sh
git clone ssh://USER@example.com/~/git_test/repo_bare.git repo_a
## warning: You appear to have cloned an empty repository.

cd repo_a
touch test.txt
echo "init" >> test.txt
git add test.txt
git commit -m "Initial commit"
## [master (root-commit) c8c1fa6] Initial commit
##  1 file changed, 1 insertion(+)
##  create mode 100644 test.txt

# ここで、最初のコミットを push
git push
# この時点を A とする

echo "IMPORTANT_KEY" >> test.txt
git add test.txt
git commit -m "IMPORTANT_KEY"
## [master 12458d0] IMPORTANT_KEY
##  1 file changed, 1 insertion(+)

# ここで、問題のあるファイルを含むコミットを push
git push
# この時点を B とする

# KEY を消すような編集をコミット
echo "init" > test.txt
echo "KEY_REMOVED" >> test.txt
git add test.txt
git commit -m "KEY_REMOVED"
## [master 988ae1e] KEY_REMOVED
##  1 file changed, 1 insertion(+)

# ここで git push をすると、サーバ上で object が増えるが
# push していなくても大勢の結果は変わらないはず

# 最後の commit を squash する
git rebase -i "HEAD~2"
# 編集画面で
## pick 12458d0 IMPORTANT_KEY
## s 988ae1e KEY_REMOVED
# とし、保存。コミットメッセージは適当に。

## [detached HEAD b7864a0] KEY_REMOVED
##  Date: Thu Jan 11 20:36:46 2018 +0900
##  1 file changed, 1 insertion(+)
## Successfully rebased and updated refs/heads/master.

# 上書き
git push -f
# この時点を C とする

## Counting objects: 3, done.
## Writing objects: 100% (3/3), 273 bytes | 39.00 KiB/s, done.
## Total 3 (delta 0), reused 0 (delta 0)
## To ssh://example.com/~/git_test/repo_bare.git
##  + 12458d0...b7864a0 master -> master (forced update)
```

#### 新しいリポジトリには push されないのを確認

``` sh
git remote add repo_2 ssh://USER@example.com/~/git_test/repo_bare_2.git
git push repo_2
# この時点を D とする
## Counting objects: 6, done.
## Delta compression using up to 4 threads.
## Compressing objects: 100% (2/2), done.
## Writing objects: 100% (6/6), 461 bytes | 115.00 KiB/s, done.
## Total 6 (delta 0), reused 0 (delta 0)
## To ssh://example.com/~/git_test/repo_bare_2.git
##  * [new branch]      master -> master
```

### 実行結果を確認

#### 時点 A でのサーバサイド

```sh
cd ~/git_test/repo_bare.git
find . -type f
```

すると

```text
./objects/c8/c1fa69ff834dcb24f7007b38f6d01e25f48e3e
./objects/e1/29ad3ad5453f614c853331670e9f87156e9d40
./objects/b1/b716105590454bfc4c0247f193a04088f39c7f
./config
./hooks/pre-applypatch.sample
./hooks/pre-push.sample
./hooks/pre-commit.sample
./hooks/pre-rebase.sample
./hooks/post-update.sample
./hooks/applypatch-msg.sample
./hooks/update.sample
./hooks/prepare-commit-msg.sample
./hooks/commit-msg.sample
./HEAD
./info/exclude
./refs/heads/master
./description
```

となる。以下、hooks は邪魔なので載せない。

#### 時点 B でのサーバサイド
時点 B では

```text
./objects/c8/c1fa69ff834dcb24f7007b38f6d01e25f48e3e
./objects/13/eaf4447e61e19b92fa7d707fd2ec909ed027f2
./objects/12/458d00bfda6ad83156238f0001f6a27d40c01d
./objects/e1/29ad3ad5453f614c853331670e9f87156e9d40
./objects/8a/8368bda34f336c403d2cce0bda106796d45454
./objects/b1/b716105590454bfc4c0247f193a04088f39c7f
./config
./HEAD
./info/exclude
./refs/heads/master
./description
```

である。いくつかオブジェクトが増えているが、減るものはない。抽出すると

```text
13/eaf4447e61e19b92fa7d707fd2ec909ed027f2
12/458d00bfda6ad83156238f0001f6a27d40c01d
8a/8368bda34f336c403d2cce0bda106796d45454
```

が増えている。これはそれぞれ(順不同)、時点 B での commit object, tree object, blob object である。内容は

```sh
git cat-file -p 8a83
```
のように確認でき、内容が

```
init
IMPORTANT_KEY
```

等と分かる。ここに、消したい情報が含まれている。

#### 時点 C でのサーバサイド
時点 C では

```text
./objects/c8/c1fa69ff834dcb24f7007b38f6d01e25f48e3e
./objects/13/eaf4447e61e19b92fa7d707fd2ec909ed027f2
./objects/49/02ccbada222f23c21482ceae2b68c0922c80bd
./objects/b9/4c8f66fbc9ae8e205b2cae341f34ce75bc98ad
./objects/12/458d00bfda6ad83156238f0001f6a27d40c01d
./objects/e1/29ad3ad5453f614c853331670e9f87156e9d40
./objects/8a/8368bda34f336c403d2cce0bda106796d45454
./objects/b7/864a0792da218c60d042078b9d7f90d76d3759
./objects/b1/b716105590454bfc4c0247f193a04088f39c7f
./config
./HEAD
./info/exclude
./refs/heads/master
./description
```

である。

```
49/02ccbada222f23c21482ceae2b68c0922c80bd
b9/4c8f66fbc9ae8e205b2cae341f34ce75bc98ad
b7/864a0792da218c60d042078b9d7f90d76d3759
```
が増えている。これらは、rebase された後の commit object, tree object, blob object である。

`8a/8368bda34f336c403d2cce0bda106796d45454` が消えていないことに注意しよう。つまり、サーバ管理者には、一度アップロードしたコミットの内容は丸見えとなる。

#### 時点 D でのサーバサイド

```sh
cd ~/git_test/repo_bare_2.git
find . -type f
```

```text
./objects/c8/c1fa69ff834dcb24f7007b38f6d01e25f48e3e
./objects/49/02ccbada222f23c21482ceae2b68c0922c80bd
./objects/b9/4c8f66fbc9ae8e205b2cae341f34ce75bc98ad
./objects/e1/29ad3ad5453f614c853331670e9f87156e9d40
./objects/b7/864a0792da218c60d042078b9d7f90d76d3759
./objects/b1/b716105590454bfc4c0247f193a04088f39c7f
./config
./HEAD
./info/exclude
./refs/heads/master
./description
```

である。(hook は除いている) コミットオブジェクトが少ない。`8a/8368bda34f336c403d2cce0bda106796d45454` がないので、情報漏洩はない。

## サーバからのダウンロード

ここからが難しい。時点 C で、サーバを直接覗けばファイルがあることは分かる。そのデータを手に入れることはできるのだろうか。

```sh
git clone ssh://USER@example.com/~/git_test/repo_bare.git repo_b

cd repo_b
git cat-file -p 8a83
```

すると、`clone` で

```text
Cloning into 'repo_b'...
Enter passphrase for key '/home/tako/.ssh/id_ed25519':
remote: Counting objects: 6, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 6 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (6/6), done.
```

の後、

```text
fatal: Not a valid object name 8a83
```

となる。つまり、`8a83...` という object は入手できていない。つまり、第三者は簡単に入手できない。

しかし、「何らかの方法で入手できる可能性がある」場合、セキュリティ的に問題がないとは言えない。入手する方法はあるだろうか？

ここでは、ファイルそのものの中身は分からないが、「commit object や、tree object の id が分かってしまっている」という仮定を置こう。

```text
$ git cat-file -p 1245
tree 13eaf4447e61e19b92fa7d707fd2ec909ed027f2
parent c8c1fa69ff834dcb24f7007b38f6d01e25f48e3e
author XXX <test@example.com> 1515670606 +0900
committer XXX <test@example.com> 1515670606 +0900

IMPORTANT_KEY

$ git cat-file -p 13ea
100644 blob 8a8368bda34f336c403d2cce0bda106796d45454    test.txt

$ git cat-file -p 8a83
init
IMPORTANT_KEY
```

である。最初の commit id が 1245 のところは、環境によって異なる。

```
12458d00bfda6ad83156238f0001f6a27d40c01d
13eaf4447e61e19b92fa7d707fd2ec909ed027f2
8a8368bda34f336c403d2cce0bda106796d45454
```

さて、これを取得するために、fetch をしようとすると

```sh
$ git fetch origin 1245
fatal: Couldn't find remote ref 1245
fatal: The remote end hung up unexpectedly
```

となる。取得できない。

`git` には、`fetch-pack` という、object を取得する仕組みがある。これを利用して、オブジェクトの取得を試みる。

```sh
git fetch-pack -v ssh://USER@example.com/~/git_test/repo_bare.git 12458d00bfda6ad83156238f0001f6a27d40c01d
Server supports multi_ack_detailed
Server supports side-band-64k
Server supports ofs-delta
Server version is git/2.7.4
Marking b7864a0792da218c60d042078b9d7f90d76d3759 as complete
error: Server does not allow request for unadvertised object 12458d00bfda6ad83156238f0001f6a27d40c01d
```

取得できない。`Server does not allow request for unadvertised object` ということで、おそらく、ref から辿れない object は取得できない。

サーバ側で、`git config uploadpack.allowtipsha1inwant true` を実行する、または、config に

```text
[uploadpack]
    allowtipsha1inwant = true
```

を書いておいてみる。

```sh
$ git fetch-pack -v ssh://USER@example.com/~/git_test/repo_bare.git 8a8368bda34f336c403d2cce0bda106796d45454
Server supports multi_ack_detailed
Server supports side-band-64k
Server supports allow-tip-sha1-in-want
Server supports ofs-delta
Server version is git/2.7.4
Marking b7864a0792da218c60d042078b9d7f90d76d3759 as complete
want 8a8368bda34f336c403d2cce0bda106796d45454 (8a8368bda34f336c403d2cce0bda106796d45454)
have b7864a0792da218c60d042078b9d7f90d76d3759
done
fatal: git upload-pack: not our ref 8a8368bda34f336c403d2cce0bda106796d45454
fatal: The remote end hung up unexpectedly
```

で、様相が変わる。なんと

```sh
git fetch-pack -v ssh://USER@example.com/~/git_test/repo_bare.git 8a8368bda34f336c403d2cce0bda106796d45454
```

ができてしまう。

https://git-scm.com/docs/git-config を見ると、いろいろなオプションがあることが分かる。

```sh
git fetch-pack -v ssh://USER@example.com/~/git_test/repo_bare.git 12458d00bfda6ad83156238f0001f6a27d40c01d
```

はいろいろ試してみて私の環境ではだめだったが、サーバ側で

```sh
git config uploadarchive.allowUnreachable true
```

すると

```sh
git archive --remote ssh://USER@example.com/~/git_test/repo_bare.git 12458d00bfda6ad83156238f0001f6a27d40c01d
```

が応答を返すようになる。

```text
git archive --remote ssh://USER@example.com/~/git_test/repo_bare.git 12458d00bfda6ad83156238f0001f6a27d40c01d
pax_global_header00006660000000000000000000000064132256461160014520gustar00rootroot0000000000000052 comment=12458d00bfda6ad83156238f0001f6a27d40c01d
test.txt000066400000000000000000000000231322564611600125770ustar00rootroot00000000000000init
IMPORTANT_KEY
```

という感じである。

# ここまでの結論

object の sha1 つまり、commit id 等が分かっただけでは、ssh 接続で、git コマンドだけを使っている限りは、隠されたデータを取得することはできない。

ただし、サーバ側でオプション(config) を設定している場合はその限りではない。

## github について

### git プロトコルに `git fetch-pack` を利用

```sh
# PRIVATE_KEY
git fetch-pack -v git://github.com/USER/github_test.git 12458d00bfda6ad83156238f0001f6a27d40c01d

# Initial commit
git fetch-pack -v git://github.com/USER/github_test.git c8c1fa69ff834dcb24f7007b38f6d01e25f48e3e
```

は、受信に失敗する。`error: Server does not allow request for unadvertised object c8c1fa69ff834dcb24f7007b38f6d01e25f48e3e` 等と表示される。一方で、

```sh
# KEY_REMOVED
git fetch-pack -v git://github.com/USER/github_test.git b7864a0792da218c60d042078b9d7f90d76d3759
```

は成功する。何もオブジェクトを持っていない場合は

```text
Looking up github.com ... done.
Connecting to github.com (port 9418) ... 192.30.255.113 done.
Server supports multi_ack_detailed
Server supports side-band-64k
Server supports ofs-delta
Server version is git/github-g7f1c57c193
want b7864a0792da218c60d042078b9d7f90d76d3759 (b7864a0792da218c60d042078b9d7f90d76d3759)
done
remote: Counting objects: 6, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 6 (delta 0), reused 6 (delta 0), pack-reused 0
Unpacking objects: 100% (6/6), done.
b7864a0792da218c60d042078b9d7f90d76d3759 b7864a0792da218c60d042078b9d7f90d76d3759
```

等となる。

### https プロトコル

アウトプットは長いので省略する。

```sh
# KEY_REMOVED
# OK
printf "0032want b7864a0792da218c60d042078b9d7f90d76d3759\n00000032have 0000000000000000000000000000000000000000\n0009done\n"| curl -H "User-Agent: git/2.14.1" --data-binary @- "https://github.com/USER/github_test.git/git-upload-pack" -H "Content-Type: application/x-git-upload-pack-request" --trace-ascii /dev/stdout

# PRIVATE_KEY
# NG
printf "0032want 12458d00bfda6ad83156238f0001f6a27d40c01d\n00000032have 0000000000000000000000000000000000000000\n0009done\n"| curl -H "User-Agent: git/2.14.1" --data-binary @- "https://github.com/USER/github_test.git/git-upload-pack" -H "Content-Type: application/x-git-upload-pack-request" --trace-ascii /dev/stdout

# Initial commit
# OK
printf "0032want c8c1fa69ff834dcb24f7007b38f6d01e25f48e3e\n00000032have 0000000000000000000000000000000000000000\n0009done\n"| curl -H "User-Agent: git/2.14.1" --data-binary @- "https://github.com/USER/github_test.git/git-upload-pack" -H "Content-Type: application/x-git-upload-pack-request" --trace-ascii /dev/stdout
```

ということで、参照するのが難しくなったコミットの PACK は取得できない。

### サイトにオブジェクト id を指定してアクセス

ところが、これができてしまった。

https://github.com/USER/github_test/commit/12458d00bfda6ad83156238f0001f6a27d40c01d

(ダミーです)にアクセスできてしまった。よって、github はダメ。

https://qiita.com/dtan4/items/34e41e3bd40a43fd8cbf
にあるように、

> 実はコミット単体のページは GitHub 上にキャッシュとして存在しており、https://github.com/dtan4/terraforming/commit/64f752dd8d93c5b7326175a69cece9e742fd010b のようにコミットハッシュを直接指定すると普通に閲覧できます。

>そこで、https://github.com/contact から GitHub のサポートにキャッシュを消すよう連絡する必要があります。

## https, webdav について追記予定