.gitignoreを後から設定するときのベストプラクティス

すでにGitの管理対象になっているファイルを後から除外したい場合には、`.gitignore`を追加、追記するだけでは除外されず、`git rm --cached <file-name>`などで、管理対象から除外してあげないといけません。
しかし、除外ファイル数が多くなったり、うっかり`git rm --cached`を忘れて管理対象から外れていないファイルがあったりした場合には少々手間です。

よく紹介されている`git rm --cached <file-name>`だと、除外したいファイルを一つ一つ指定していないといけませんし、`git rm --cached .`だと、管理対象のファイルが一旦全て除外されてしまうため、その後に`git add`してまたステージングエリアに上げないといけないので二度手間です。

後から`.gitignore`する方法に関してはすでに色々な情報がありますが、なかなか万能な方法が見つからなかったので僕なりのベストプラクティスを考えてみました。

## 動作環境

この記事は、以下のバージョンのGitで動作確認しています。

```shell
git version 2.32.0
```

## まずは結論から

`.gitignore`を追加、または追記した後に以下のコマンドを実行することで、管理対象から新しく除外したいファイルのみを除外することができます。

```shell
git rm --cached `git ls-files -ci --exclude-standard`
```
なお、ローカルのファイル自体が削除されるわけではありません。

上記の方法だと`.gitignore`以外にも、`.git/info/exclude`やグローバルな設定などの一般的なGitの除外設定も含まれるので、リポジトリの`.gitignore`だけを対象に除外したい場合は以下のようにします。

```shell
git rm --cached `git ls-files -ci --exclude-from=.gitignore`
```

これで、ワンコマンドで管理対象になっているファイルの中から、`.gitignore`で指定されているファイルだけを除外することができます。

特に理由がなければ`--exclude-standard`の方でいいかなと思います。

`.gitignore`に差分がある場合は、そちらも忘れずにステージングエリアに上げてからコミットしましょう。

とりあえず目的が達成できればいいよって方は、[エイリアスにしておくと便利](#エイリアスにしておくと便利)まで飛ばしてもらって、紹介しているエイリアスを設定して使ってもらうと便利です。

理解してないコマンドは叩きたくない！という方のために、以降から詳しく解説していきます。

## 背景

対象ファイルが把握できる数なら`git rm --cached <file-name>`でも問題ないですが、例えば後から新規に`.gitignore`のテンプレを追加したりして、除外対象のファイルが多くなってくると面倒です。

また、過去の遺物や引き継ぎなどで`.gitignore`に記載はあるものの、管理対象から除外されていないファイルがあるリポジトリを整備する際などには一気に除外したいものですが、前述の通り`git rm --cached .`では管理対象のファイルが一旦全て除外されてしまうので、`git add`してステージングエリアに上げなおす手間がかかります。

今回の方法だと、ワンコマンドで目的のファイルだけピンポイントに除外できるのでオススメです。

引き継ぎリポジトリとかだとメンテが行き届いていなくて、除外対象のファイルが大量に出てくる場合があるので、[エイリアスにしておくと便利](#エイリアスにしておくと便利)セクションではそんなときに便利な、除外する前に除外されるファイルを確認する方法も紹介しています。

できるだけGitの公式リファレンスのリンクをつけているので、より詳細な説明はそちらも参考にしてみてください。（リンクは全てGitバージョン2.32.0のリファレンスへのリンクになっています。）

## コマンドの説明

使用している各コマンドの詳細を説明します。
ここからは便宜上、`--exclude-standard`や`--exclude-from=.gitignore`のexclude系のオプション群を、まとめて`--exclude*`と表記します。


### [git rm --cached **<**file1 file2 ...>](https://git-scm.com/docs/git-rm/2.32.0#Documentation/git-rm.txt---cached)

指定されたファイルを管理対象から除外します。スペース区切りで複数ファイルの指定が可能です。
ローカルのファイルは削除されずに残ります。
`--cached`オプションを付けないと**ファイルごと削除されてしまう**ので注意です。

今回の方法では`git ls-files -ci --exclude*`で出力されたファイルを、管理対象から除外しているというわけです。


### [git ls-files](https://git-scm.com/docs/git-ls-files/2.32.0)

オプションなしで実行することで、Gitの管理対象になっているファイルを表示します。
暗黙的に`-c`オプションがデフォルトで指定されています。
`-o`オプションを指定することで、逆にGitの管理対象になっていないファイルを表示することができます。

https://git-scm.com/docs/git-ls-files


#### オプション説明

`git ls-files -ci --exclude*`で、除外対象ファイルの特定に使用している`git ls-files`コマンドのオプションについて説明します。

##### [-c](https://git-scm.com/docs/git-ls-files/2.32.0#Documentation/git-ls-files.txt--c), [--cached](https://git-scm.com/docs/git-ls-files/2.32.0#Documentation/git-ls-files.txt---cached)

出力の中でキャッシュされているファイルのみを表示します。
キャッシュされているとは、Gitのインデックスに登録されている、つまり管理対象であるということで、要は管理対象のファイルが表示されるということです。
これは`ls-files`のデフォルトの挙動となっていますが、`-i`オプションを使用する際には明示的に指定してあげないといけません。

##### [-i](https://git-scm.com/docs/git-ls-files/2.32.0#Documentation/git-ls-files.txt--i), [--ignored](https://git-scm.com/docs/git-ls-files/2.32.0#Documentation/git-ls-files.txt---ignored)

無視されたファイルのみを出力に表示します。
少なくとも1つの`--exclude*`オプションを使って無視ルールを指定する必要があります。

このオプションは`--exclude*`オプションとセットで使う必要があるため、否定の否定みたいで少しわかりにくいですが、`--exclude*`で無視ルールとして指定されたパターンに一致するファイル（無視されたファイル）が（逆に）出力されるというイメージです。

また、`-o`または`-c`オプションも一緒に使用する必要があります。
指定しないと以下のエラーが出ます。

```shell
fatal: ls-files -i must be used with either -o or -c
```

`--exclude*`系のオプションは`-o`または`-i`オプションが指定されている場合にのみ利用できるようです。

https://git-scm.com/docs/git-ls-files/2.32.0#_exclude_patterns

>git ls-files can use a list of "exclude patterns" when traversing the directory tree and finding files to show when the flags --others or --ignored are specified. gitignore[5] specifies the format of exclude patterns.

`git ls-files -c --exclude-standard`でも実行自体はできましたが、出力からexcludeパターンに指定されているファイルが除外されませんでした。
`git ls-files -o --exclude-standard`だと除外ルールが適用され、excludeパターンに指定されているファイルが出力から除外されました。

##### [--exclude-from=**<**file>](https://git-scm.com/docs/git-ls-files/2.32.0#Documentation/git-ls-files.txt---exclude-fromltfilegt)

`file`から除外パターンを読み込んで、そのルールにしたがって出力から除外します。
今回はこのオプションに`.gitignore`を渡しています。
出力から`.gitignore`に指定のあるファイルのみを除外するイメージです。
前述の通り、`--exclude*`系のオプションは`-o`または`-i`オプションが指定されている場合にのみ利用できるようです。

##### [--exclude-standard](https://git-scm.com/docs/git-ls-files/2.32.0#Documentation/git-ls-files.txt---exclude-standard)

標準的なGitの除外設定で指定されているファイルを出力から除外します。
`.git/info/exclude`、各ディレクトリの`gitignore`、ユーザーのグローバルな除外ファイルが対象のようです。

>Add the standard Git exclusions: .git/info/exclude, .gitignore in each directory, and the user’s global exclusion file.

***

少しわかりにくいのが、`-i`オプションと`--exclude*`オプションを組み合わせたときに、**「excludeパターンにマッチするものだけが表示される」**というところです。冒頭のコマンドの一部を以下のように、オプション指定の順番を入れ替えてみるとわかりやすいかもしれません。

```shell
git ls-files -c --exclude-standard -i
```

つまり、`git ls-files -c`の出力の中で、`--exclude*`オプションによって、無視（除外）されたファイルを、`-i`オプションで表示させている、と考えると少しわかりやすいかもしれません。

## エイリアスにしておくと便利

Gitのグローバル設定でaliasにしておくと便利です。

```config:~/.gitconfig
# 以下を追記
[alias]
	# 除外設定されているファイルのうち、まだ管理対象から除外されていないファイルを表示します。
	unignored = ls-files -ci --exclude-standard
	# または
	# unignored = ls-files -ci --exclude-from=.gitignore

	# 除外設定されているファイルのうち、まだ管理対象から除外されていないファイルを除外します。
	cache-clean = !git rm --cached `git ls-files -ci --exclude-standard`
	# または
	# cache-clean = !git rm --cached `git ls-files -ci --exclude-from=.gitignore`
```

以下のようにして使います。

1. `.gitignore`を追加するなり追記するなりして、Git管理から除外したいファイルを設定する。
2. `.gitignore`に記載されているものの、まだ管理対象から除外されていないファイルを表示して確認する。

    ```shell
    git unignored
    ```

3. 前の手順で確認したファイルを管理対象から除外する。

    ```shell
    git cache-clean
    ```

4. コミットする時は、忘れずに`.gitignore`の変更をステージングエリアに上げてからコミットする。


### 補足

- `unignored`は英単語として正しくないかもしれませんが、利便性を考慮してこのように表現してみました。
- `cache-clean`は文法的には`clean-cache`の方が正しいかもしれませんが、万が一`git clean`が実行されてしまうと怖いので、このように表現しています。
- `cache-clean`にて、Gitのエイリアスではバッククォートがコマンド置換されないので、エイリアスの先頭に`!`をつけてシェルコマンドとして扱うようにしています。ただし、`!`のコマンドの実行ディレクトリは**カレントディレクトリではなく、リポジトリの最上位ディレクトリになる**というところには注意が必要です。

https://git-scm.com/docs/git-ls-files#_exclude_patterns
>If the alias expansion is prefixed with an exclamation point, it will be treated as a shell command. 
> ~~~
>Note that shell commands will be executed from the top-level directory of a repository, which may not necessarily be the current directory.

何か気づいた点があればコメントいただけると嬉しいです🙌


## 参考

- [Git - Reference](https://git-scm.com/docs)