# git includeIf と filter-repo --mailmap で直す — 27個の個人リポジトリに紛れ込んでいた会社のメールアドレス

> Gitのidentityはリポジトリではなくディレクトリに属する。includeIf gitdirで以後を直し、git filter-repo --mailmapで既にpushしたコミットを直す。

- Source: https://oharu121.com/ja/blog/git-wrong-author-email-includeif-gitdir-filter-repo-mailmap/
- Published: 2026-08-04T11:58:39+09:00
- Tags: Git, 開発ツール

---
## はじめに

個人ブログを立ち上げて最初のコミットをpushし、authorの行を見た。

```text
Author: my-work-name <me@company.example>
```

勤務先のアドレスが、個人プロジェクトの、しかも公開するつもりのリポジトリに入っていた。何かが壊れたわけではない。Gitは何年も前に設定した通りに、そのまま忠実に動いていただけだ。そして、どこまで広がっているか確認しに行くと、1つのリポジトリどころではなかった。27個だった。

見つかった修正方法は、使ったことのないgitの機能だった。`includeIf "gitdir:"`は、リポジトリが置かれている場所に応じてidentityを選ぶので、リポジトリごとに設定しなくても、答えがディレクトリに従うようになる。すでにpushしたコミットには、また別の対処が必要だった。`git filter-repo --mailmap`だ。

この記事では、問題の捉え方を変えた「数える」作業、最初に試したワンコマンドの修正とそれが間違った層だった理由、条件付きincludeの設定と静かに壊す2つの落とし穴、そしてすでにGitHubにあった履歴を書き換えて起きた問題を扱う。

## ワンコマンドで直る — しかしそれは違う層だった

わかりやすい修正はワンコマンドで済む。

```bash
git config user.email personal@example.com
```

これでこのリポジトリは直る。しかし問題は解決しない。問題は最初からこのリポジトリではなかったからだ。それに気づいたのは、数えてからだった。

## まず数える

設定を書く前に、まず数えた。

```bash
for d in ~/Developer/personal/*/; do
  [ -d "$d/.git" ] || continue
  printf '%-30s %s\n' "$(basename $d)" "$(git -C "$d" config user.email)"
done
```

27個のリポジトリ。そのすべてが勤務先のアドレスだった。27回間違えたからではない。**0回**しか決めていなかったからだ。すべてのリポジトリが同じグローバルなデフォルトを継承していて、そのデフォルトは仕事用マシンで、仕事のために、一度だけ設定されたものだった。

これで修正の捉え方が変わる。1つのリポジトリに対する`git config user.email`は、間違った層に当てたパッチにすぎない。残り26個は間違ったままだし、28個目のリポジトリを作るときも、思い出させてくれるものは何もないので、また間違えることが確定している。

## 条件付き include

Gitはリポジトリが置かれている場所からidentityを選べる。この機能はgit 2.13から存在していて、使ったことがなかった。

```ini title="~/.gitconfig"
; Default: work
[user]
	name = my-work-name
	email = me@company.example

; Anything under ~/Developer/personal/ overrides the above
[includeIf "gitdir:~/Developer/personal/"]
	path = ~/.gitconfig-personal
```

```ini title="~/.gitconfig-personal"
[user]
	name = my-personal-handle
	email = personal@example.com
```

これでidentityは*コードがどこにあるか*のプロパティになった。これこそが、どのidentityが正しいかを実際に決めているものだ。新しい個人リポジトリは、作成された瞬間からカバーされる。リポジトリごとの設定はいらないし、覚えておくことも何もない。

*Figure — IdentityByDirectory: どちらのディレクトリも同じ`~/.gitconfig`を読む。個人用の方だけが`includeIf`の条件にも一致するので、そちらだけが2つ目のファイルを追加で読み込む。*

これを静かに壊す落とし穴が2つあり、どちらもエラーにはならない。

**末尾のスラッシュが重要だ。** `gitdir:~/Developer/personal/`は、そのディレクトリとその配下すべてに一致する。末尾のスラッシュがないと、単一のパスにしか一致せず、その中のリポジトリはすべて対象から外れる。

**順序も重要だ。** Gitのconfigは後勝ちなので、`includeIf`は、それが上書きする対象の`[user]`ブロックより*後*に書かなければならない。ファイルの先頭に置くと、下にあるデフォルトの`[user]`が、includeが設定したばかりの内容を単純に上書きしてしまう。ファイルを見た目上は正しく見えるのに、動作は変わらない。

思い込まずに、両方のサイドを確認する。

```bash
git -C ~/Developer/personal/some-repo config user.email   # personal
git -C ~/Developer/work/some-repo config user.email       # work
```

2つ目の出力がいつの間にか個人アドレスになっていたら、パターンが広すぎる。

## どのアドレスを使うか

本能的には自分の実際の個人メールアドレスを使いたくなる。代わりにGitHubのnoreplyアドレスを検討するとよい。

```text
26102772+oharu121@users.noreply.github.com
```

これは`<user-id>+<username>@users.noreply.github.com`という形式で、idは`gh api user --jq .id`で取得できる。このアドレスでauthorしたコミットも自分のアカウントにリンクされ、contributionグラフにもきちんとカウントされる。それでいて、実際の受信箱が公開リポジトリに残ることはない。

**実アドレス版には知っておく価値のある失敗モードがある。** そのアドレスがGitHubアカウントで未検証だと、コミットはリンクされない状態で表示され、アバターもプロフィールリンクもcontributionのカウントもつかない。しかも*Block command line pushes that expose my email*を有効にしていると、そのアドレスでのpushはそもそも失敗する。

## すでにpushした分を直す

これで今後は解決した。しかし、すでに存在するコミットは解決していない。

これは**今すぐやるか、一生やらないかのどちらか**にする価値がある。コミットが2つなら5分の作業だ。2年分の履歴と他人のcloneがあれば、それはもう移行作業になる。コストは上がる一方だ。

`git commit --amend --reset-author`は誘惑的なワンライナーだが、先頭のコミット以外に使うと間違っている。触れたコミットすべてのauthorshipを*現在の*identityに書き換えてしまい、それには自分のものではなかったコミットも含まれる。代わりにmailmapを使う。一致したエントリだけを書き換えられる。

```text title="mailmap.txt"
New Name <new@example.com> Old Name <old@company.example>
```

```bash
git filter-repo --mailmap mailmap.txt --force
```

古いidentityにマッチする方式なので、botや共同作業者のコミットはそのまま素通りする。私の場合、自分のコミットの直後にDependabotのコミットが乗っていた。一律の書き換えだったら、それが自分に付け替えられてしまうところだった。

### 3つのハマりどころ

**タグはブランチに追従しない。** `v0.1.0`タグは書き換え対象のコミットを指していたので、書き換え後もそのタグは*古い*オブジェクトを指したままだった。ブランチとタグは別々のrefであり、どちらもforce-pushが必要だ。

*Figure — TagBranchDivergence: `filter-repo`は`main`を書き換え後のコミットへ移動させる。`v0.1.0`タグは別のrefであり、`filter-repo`が一切触れないので、孤立した元のコミットを指したままになる。*

```bash
git push --force-with-lease=main:<old-sha> origin main
git push --force origin refs/tags/v0.1.0
```

素の`--force`ではなく、期待するSHAを明示した`--force-with-lease`を使う。作業中にリモートに何か新しいものが入っていたら（私の場合、数分前にCIが依存関係のPRを自動マージしていた）、破壊する代わりにpushが中止される。

**`git filter-repo`は`origin`リモートを削除する。** 意図的な挙動だ。使い捨てのcloneで作業していると想定していて、書き換えた履歴を反射的にpushし返すことを止めたがっている。削除したことは教えてくれるので、その後で自分で追加し直す。

```bash
git remote add origin <url>
```

**リリースが無事かを確認する。** GitHubのリリースはタグの*名前*に紐づいているので、タグをforceで更新してもリリースは付いたままになる。ただし、思い込まずに確認する。

```bash
gh release view v0.1.0 --json tagName,body --jq '.tagName, (.body | length)'
gh api repos/<owner>/<repo>/git/ref/tags/v0.1.0 --jq '.object.sha'
```

正直に言っておくべき注意点が1つある。古いオブジェクトはGitHub上に残り続ける。どこからも参照されないがSHAで到達可能な状態のまま、ガベージコレクションされるまで残る。単に見た目が汚いという話ではなく、露出そのものが本当に問題になる場合は、サポートへの依頼が必要になる。force pushだけでは消えない。

## まとめ

バグは、どのconfigファイルよりも一段上の層に住んでいた。identityはグローバルに設定されていたが、実際には*今どのプロジェクトにいるか*のプロパティだった。

1. 修正をどこに当てるか決める前に、影響を受けるリポジトリの数を数える。
2. `includeIf "gitdir:…/"`を使い、答えがディレクトリに従うようにする。末尾のスラッシュに注意し、`[user]`より後に置く。
3. 実際の受信箱より、GitHubのnoreplyアドレスを優先する。
4. 既存の履歴は、まだコミットが2つのうちに書き換える。mailmapを使って自分だけを書き換え、タグは別のrefであることを忘れない。

何年も前にこれを見つけられていたはずのチェックは、たった一行だ。

```bash
git log -1 --format='%an <%ae>'
```

新しいリポジトリで最初のコミットをした後に、一度実行する。
