BFGでGitHub上のコミット履歴からパスワードを削除する


## はじめに

この記事ではBFGというツールを使ってGitHub上のコミット履歴からパスワードを削除する手順を説明します。

## 想定するユースケース

- パスワードを平文でファイルに保存し、それをGitHubにpushした。
- しばらくしてから平文はまずいということに気づき、パスワードは環境変数から取得するようにコードを書き換えた。
- しかし、過去のコミット履歴をさかのぼればパスワードが見えてしまう。なのでコミット履歴からもパスワードを削除したい。

## 制限事項

この方法ではコミット履歴に残っているパスワードは削除できますが、pull requestの履歴（diff）にはパスワードが残ったままになっています。
pull requestからもパスワードを削除したい場合はGitHub上のリポジトリを一度削除するか、[GitHubのサポート](https://github.com/contact)に依頼してpull requestを削除してもらう必要があります。

参考： [Git push has some rejections · Issue \#36 · rtyley/bfg\-repo\-cleaner](https://github.com/rtyley/bfg-repo-cleaner/issues/36)

## サンプルプロジェクトの構成

本記事では以下のような単純なリポジトリをサンプルとして想定します。

```
.
├── sample.txt
└── secrets.yml
```

secrets.ymlにはもともと次のようにパスワードが平文で書かれていました。

```secrets.yml
password: foobar
secret: barbuz
```

これを次のように修正したのが現在の状況です。

```secrets.yml
password: "<%= ENV['PASSWORD'] %>"
secret: "<%= ENV['SECRET'] %>"
```

また、GitHub上に実際のリポジトリ（パスワードはコミット履歴から削除済み）も用意しています。

https://github.com/JunichiIto/bfg-sandbox

## 手順

### BFGのインストール

BFGをインストールしていない場合はBFGをインストールします。
以下はHomebrewを使ってインストールする手順です。

```
$ brew update
$ brew install bfg
$ bfg --version
bfg 1.12.15
```

なお、gitのバージョンは2.12.2で動作確認しています。

```
$ git --version
git version 2.12.2
```

### ローカルマシン上の開発中のコードを全部GitHubにpushする

ローカルマシン上にまだpushしてない開発中のコードがあればGitHubにpushしてください。
ローカルマシンに置いたままでも構いませんが、コミット履歴が変わってしまうため、あとからローカルマシンにpull＆mergeしようとすると少し手間がかかります。

開発者が複数いる場合は、他の開発者にもその旨声をかけましょう。

### GitHub上のpull requestをmergeまたはcloseする

無用なトラブルを避けるため、GitHub上にOpenなpull requestが残っている場合はマージするかcloseしてください。

### 全ブランチのHEADで平文のパスワードが残っていないことを確認する

GitHub上に複数のブランチがある場合は、全ブランチのHEAD（つまり最新のコード）に平文のパスワードが残っていないこと（今回であれば環境変数から取得するようになっていること）を確認します。
パスワードが残ったままになっていると、BFGの実行時に以下のようなメッセージが表示され、目的のファイルのコミット履歴が変更されないためです。

```
Protected commits
-----------------

These are your protected commits, and so their contents will NOT be altered:

 * commit 922243a1 (protected by 'HEAD') - contains 1 dirty file : 
    - secrets.yml (22 B)

WARNING: The dirty content above may be removed from other commits, but as
the *protected* commits still use it, it will STILL exist in your repository.
```

パスワードが残っている場合はブランチを削除したり、最新版のコード（平文パスワードを使用しないコード）をマージするなどして、HEADにパスワードが残らないようにしてください。

### `--mirror`オプションを付けて新たにリポジトリをcloneする

ローカルマシンで現在開発しているディレクトリとは別の場所に、新たにリポジトリをcloneします。
このとき、`--mirror`オプションを付けます。

以下はworkという作業用のディレクトリを用意して、そこへcloneする例です。

```
$ mkdir work
$ cd work
$ git clone --mirror git@github.com:JunichiIto/bfg-sandbox.git
```

### passwords.txtに削除したいパスワードを保存する

passwords.txtというファイルを作成し、そこへ削除したいパスワードを記述します。

```passwords.txt
foobar
barbuz
```

workディレクトリの構成はこうなります。

```
work/
├── bfg-sandbox.git/
└── passwords.txt
```

なお、passwords.txtは他にもいくつかの指定方法があります。

参考: [BFG Repo\-Cleaner \-\-replace\-text example](https://gist.github.com/w0rd-driven/60779ad557d9fd86331734f01c0f69f0)

```
PASSWORD1                       # Replace literal string 'PASSWORD1' with '***REMOVED***' (default)
PASSWORD2==>examplePass         # replace with 'examplePass' instead
PASSWORD3==>                    # replace with the empty string
regex:password=\w+==>password=  # Replace, using a regex
regex:\r(\n)==>$1               # Replace Windows newlines with Unix newlines
```

### BFGを実行する

次のコマンドでBFGを実行します。

```
$ bfg --replace-text passwords.txt bfg-sandbox.git 
```

このコマンドを実行すると、ターミナルに以下のような表示が出ます。

```
Using repo : /Users/jit/dev/sandbox/work/bfg-sandbox.git

Found 3 objects to protect
Found 6 commit-pointing refs : HEAD, refs/heads/bob-feature, refs/heads/develop, ...

Protected commits
-----------------

These are your protected commits, and so their contents will NOT be altered:

 * commit fd0f3ec7 (protected by 'HEAD')

Cleaning
--------

Found 9 commits
Cleaning commits:       100% (9/9)
Cleaning commits completed in 55 ms.

Updating 5 Refs
---------------

	Ref                      Before     After   
	--------------------------------------------
	refs/heads/bob-feature | ef2cd1af | 9664245c
	refs/heads/develop     | 1eb3d80b | ba3c5ab8
	refs/heads/master      | fd0f3ec7 | 147f4942
	refs/pull/1/head       | 6b269cae | 7a38c8e2
	refs/pull/2/head       | 1eb3d80b | ba3c5ab8

Updating references:    100% (5/5)
...Ref update completed in 19 ms.

Commit Tree-Dirt History
------------------------

	Earliest      Latest
	|                  |
	 D D D D D  m m m m 

	D = dirty commits (file tree fixed)
	m = modified commits (commit message or parents changed)
	. = clean commits (no changes to file tree)

	                        Before     After   
	-------------------------------------------
	First modified commit | e9df6ca3 | 2cebd96f
	Last dirty commit     | a47e91e4 | 15ab3de5

Changed files
-------------

	Filename      Before & After                          
	------------------------------------------------------
	secrets.yml | 0d217b89 ⇒ 9460a9cb, 6cb378ce ⇒ 2a248e70


In total, 13 object ids were changed. Full details are logged here:

	/Users/jit/dev/sandbox/work/bfg-sandbox.git.bfg-report/2017-04-27/05-36-48

BFG run is complete! When ready, run: git reflog expire --expire=now --all && git gc --prune=now --aggressive


--
You can rewrite history in Git - don't let Trump do it for real!
Trump's administration has lied consistently, to make people give up on ever
being told the truth. Don't give up: https://www.theguardian.com/us-news/trump-administration
--
```

ターミナルに出力された"Protected commits"の欄に注目してください。

```
Protected commits
-----------------

These are your protected commits, and so their contents will NOT be altered:

 * commit fd0f3ec7 (protected by 'HEAD')
```

ここに"secrets.yml"が載っていなければ、コミット履歴からパスワードを削除できたことになります。

### GitHubにpushする

次の手順で変更した履歴をGitHubにpushします。

```
$ cd bfg-sandbox.git

$ git reflog expire --expire=now --all && git gc --prune=now --aggressive
Counting objects: 21, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (16/16), done.
Writing objects: 100% (21/21), done.
Total 21 (delta 2), reused 6 (delta 0)

$ git push
Counting objects: 21, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (14/14), done.
Writing objects: 100% (21/21), 2.19 KiB | 0 bytes/s, done.
Total 21 (delta 2), reused 21 (delta 2)
remote: Resolving deltas: 100% (2/2), done.
To github.com:JunichiIto/bfg-sandbox.git
 + ef2cd1a...9664245 bob-feature -> bob-feature (forced update)
 + 1eb3d80...ba3c5ab develop -> develop (forced update)
 + fd0f3ec...147f494 master -> master (forced update)
 ! [remote rejected] refs/pull/1/head -> refs/pull/1/head (deny updating a hidden ref)
 ! [remote rejected] refs/pull/2/head -> refs/pull/2/head (deny updating a hidden ref)
error: failed to push some refs to 'git@github.com:JunichiIto/bfg-sandbox.git'
```

`git push`したあとに`! [remote rejected]`や`error: failed to push some refs`という文言が見えますが、これは想定通りです。
制限事項の項で説明したとおり、pull request上の履歴は変更できないため、このようなメッセージが表示されます。

### GitHub上のコミット履歴からパスワードが消えたことを確認する

GitHubにアクセスしてコミット履歴を確認してください。
パスワードが`***REMOVED***`のようになっていれば成功です。

https://github.com/JunichiIto/bfg-sandbox/commit/7a38c8e217e30b718804b1a8bfa92cfa72f26200
![Screen Shot 2017-04-27 at 10.14.03.png](https://qiita-image-store.s3.amazonaws.com/0/7465/82353b7b-81f3-af3e-af0f-30a6aab318ba.png "Screen Shot 2017-04-27 at 10.14.03.png")


ただし、制限事項の項で述べたように、pull requestの履歴ではパスワードが残ったままになります。

https://github.com/JunichiIto/bfg-sandbox/pull/2/files
![Screen Shot 2017-04-27 at 8.06.01.png](https://qiita-image-store.s3.amazonaws.com/0/7465/9adc5321-2b33-fd9f-4136-1c9efa439e6d.png "Screen Shot 2017-04-27 at 8.06.01.png")

### 開発用リポジトリを改めてgit cloneする

コミット履歴が書き換えられているため、もともと開発用で使っていたリポジトリに戻って`git pull`すると、`fatal: refusing to merge unrelated histories`というメッセージが出て正常にpullできないはずです。

```
$ git pull
remote: Counting objects: 20, done.
remote: Compressing objects: 100% (13/13), done.
remote: Total 20 (delta 2), reused 19 (delta 2), pack-reused 0
Unpacking objects: 100% (20/20), done.
From github.com:JunichiIto/bfg-sandbox
 * branch            develop    -> FETCH_HEAD
 + 1eb3d80...6da4d6f develop    -> origin/develop  (forced update)
fatal: refusing to merge unrelated histories
```

`git merge --allow-unrelated-histories origin/develop`のようにすれば、強制的にマージすることもできますが、新しくマージコミットができたりするので、改めてcloneし直した方が良いと思います。

```
# 旧開発用リポジトリをリネーム
$ mv bfg-sandbox bfg-sandbox.old

# 改めてclone
$ git clone git@github.com:JunichiIto/bfg-sandbox.git
```

開発者が複数いる場合は、他の開発者にも改めてgit cloneするように伝えてください。

手順は以上です。

## 参考資料

公開したくないデータをリポジトリから削除する方法を説明しているGitHubの公式ページです。
BFGを使わずに手作業でデータを削除する方法も載っています。

[Removing sensitive data from a repository \- User Documentation](https://help.github.com/articles/removing-sensitive-data-from-a-repository/)

BFGの公式ページです。
作業前にはこちらもひととおり目を通しておくと良いでしょう。

[BFG Repo\-Cleaner by rtyley](https://rtyley.github.io/bfg-repo-cleaner/)