Git 自分が変更していないファイルのコミット履歴が作られるのはなぜか

目次
-
- 6.1. リベース(Rebase)
- 6.2. スカッシュ(squash)
- 6.3. チェリーピック(cherry-pick)
はじめに
Git を使っていると、自分が変更していないファイルのコミットが自動生成されて困惑したことがないでしょうか?自動生成されたコミットには Merge branch 'hoge' of ・・・
というコミットメッセージが付与されています。どうやらマージが発生しているようですが、コミットの内容を見てみると自分が修正したファイルではありません。ただしコミット者は自分になっています。自分が修正したファイルは別にありますが、自動でコミットされたファイルとは無関係なので競合は発生しないはずです。このような挙動は svn などの他のソース管理ツールでは発生しないので、不自然な動きに思えます。
競合していないはずなのになぜマージが必要なのか、今回はこの疑問について解説していきます。1
用語の整理:マージコミット
ここで自動生成されているコミットはマージコミットです。マージコミットとは、分岐した履歴を統合したときに作られる特別なコミットです。マージコミットが作成されるタイミングは以下の2つです。
- 2つのブランチを明示的にマージする場合
gitGraph commit id:"C1" commit id:"C2" branch develop checkout develop commit id:"C3" checkout main commit id:"C4" checkout develop checkout main merge develop id:"明示的にマージ"
- プルした時にリモートとローカルの履歴が分岐している場合
--- config: gitGraph: mainBranchName: "origin/main" --- gitGraph commit id:"C1" commit id:"C2" branch main checkout main commit id:"C3" checkout "origin/main" commit id:"C4" checkout main commit id:"ここでプル" type:HIGHLIGHT commit id:"自動生成コミット"
自動生成されるコミットは後者に該当します。以降では、この自動生成されるコミットをマージコミットと呼びます。
Git における分岐とは
「分岐した履歴を統合」とありますが、Git での分岐とはどのような状態を指すでしょうか。 Git における分岐は「ブランチを切る」に限定されません。
分岐している状態
--- config: gitGraph: mainBranchName: "origin/main" --- gitGraph commit id:"C1" commit id:"C2" branch main checkout main commit id:"C3" checkout "origin/main" commit id:"C4"
上図のような場合でも、Git では分岐しているとみなされます。
- リモート(origin/main)には自分以外の誰かがプッシュした
C4
コミットが存在 - ローカル(main)には自分がコミットした
C3
コミットが存在
これは分岐している状態です。 つまり「分岐」というのは、Git においては単に履歴が二方向に伸びた状態を指すので、「ブランチを作成した」かどうかとは関係なく現れます。このような状態でプルを実行した場合、「リモートとローカルの履歴が分岐している」のでマージコミットが作成されることになります。
分岐していない状態
では、以下の状態はどうでしょうか。
--- config: gitGraph: mainBranchName: "origin/main" --- gitGraph commit id:"C1" commit id:"C2" branch main checkout main commit id:"C3"
- リモートには未プルのコミットが存在しない
- ローカルには未プッシュのコミットが存在する
これは分岐していません。
ローカルがリモートより「進んでいる」だけで、枝分かれ(分岐)はしていません。
実際にプルを行っても、リモートにはプルされるコミットがないので何も起きません。
次に以下の場合について見てみましょう。
--- config: gitGraph: mainBranchName: "origin/main" --- gitGraph commit id:"C1" commit id:"C2" branch main commit id:"何もコミットしていない" type:HIGHLIGHT checkout "origin/main" commit id:"C4"
- リモートには未プルのコミットが存在する
- ローカルには未プッシュのコミットが存在しない
これも分岐していません。
リモートがローカルより「進んでいる」だけで、枝分かれ(分岐)はしていません。
ここでプルを行うと、リモートの C4
コミットがローカルの履歴に単に追加されます(マージコミットは発生しません)。
分岐のまとめ
表にまとめると以下のようになります。
リモートに未プルのコミット | ローカルに未プッシュのコミット | 分岐 |
---|---|---|
なし | なし | なし |
なし | あり | なし |
あり | なし | なし |
あり | あり | 分岐あり |
この「分岐あり」の状態のときにプルを行うとマージコミットが発生します。
マージコミットの必要性
マージコミットが発生する条件は分かりましたが、なぜマージコミットが必要なのでしょうか。
--- config: gitGraph: mainBranchName: "origin/main" --- gitGraph commit id:"C1" commit id:"C2" branch main checkout main commit id:"C3" checkout "origin/main" commit id:"C4" checkout main commit id:"ここでプル" type:HIGHLIGHT commit id:"C5 マージコミット"
ここでプッシュを行うと以下の状態になります。
--- config: gitGraph: mainBranchName: "main origin/main" --- gitGraph commit id:"C1" commit id:"C2" branch "枝分かれした履歴" checkout "枝分かれした履歴" commit id:"C3" checkout "main origin/main" commit id:"C4" checkout "枝分かれした履歴" commit id:"ここでプル" type:HIGHLIGHT commit id:"プッシュ" type:HIGHLIGHT checkout "main origin/main" merge "枝分かれした履歴" id:"C5 マージコミット"
枝分かれしていた main
と origin/main
がマージコミットの箇所で統合(マージ)されています。 つまり、マージコミットには分岐した履歴をどの時点で統合したかという情報があります。マージコミットが無い場合、このような分岐の履歴を保持することができません。そのため、マージコミットが無い場合は以下のような一本線の履歴になります。
--- config: gitGraph: mainBranchName: "main origin/main" --- gitGraph commit id:"C1" commit id:"C2" commit id:"C4" commit id:"C3"
この分岐の履歴を残すことがマージコミットの存在理由 2 となります。
自動生成されたマージコミットにはファイルの変更履歴は含まれていない
ここでもう一つの疑問があります。
--- config: gitGraph: mainBranchName: "main origin/main" --- gitGraph commit id:"C1" commit id:"C2" branch "枝分かれした履歴" checkout "枝分かれした履歴" commit id:"C3" checkout "main origin/main" commit id:"C4" checkout "枝分かれした履歴" checkout "main origin/main" merge "枝分かれした履歴" id:"C5 マージコミット"
すでに C4
コミットの履歴があるのだから、マージコミットを作成したら C4
分の変更履歴が2重に発生するのでは?という疑問です。実は、競合がなく自動生成されたマージコミットにはファイルの変更履歴は含まれていません。変更履歴が2重に発生しているように見えるのは GUI ツールの表示結果を誤解している ことに理由があります。要点は以下の通りです。
- GUI ツールは親コミットとの差分を表示している
- 通常のコミットとは異なり、マージコミット
C5
には親が2つあります3(C3
とC4
) - 親
C3
を基準とした場合、C4
分が差分として見える - 親
C4
を基準とした場合、差分はない
以下は SourceTree の例です(図1、図2)。
SoureTree では歯車アイコンから表示の基準となる親コミットを選択できます。
Git コマンドでもマージコミットにはファイルの変更履歴が含まれていないことを確認できます。
C4
コミット(ファイルの変更内容が表示されます)
> git --no-pager show 264ab16
commit 264ab16a01f17f256ec902668742ae174be7de75
Author: UserA <korochin0911@gmail.com>
Date: Sun Sep 7 09:41:19 2025 +0900
Commit 4
diff --git a/FileX.txt b/FileX.txt
index 7a13578..9ebd7de 100644
--- a/FileX.txt
+++ b/FileX.txt
@@ -1 +1 @@
-FileX
\ No newline at end of file
+FileX Commit 4
\ No newline at end of file
C5
マージコミット(コミットメッセージのみです)
> git --no-pager show fcf152f
commit fcf152f06c075c2aee6d613bad64059cf31f386d (HEAD -> main, origin/main, origin/HEAD)
Merge: 6a241b3 264ab16
Author: UserB <korochin0911@gmail.com>
Date: Sun Sep 7 09:43:10 2025 +0900
Merge branch 'main' of https://github.com/korochin0911/hoge
つまり、このメモ書きのタイトルは「自分が変更していないファイルのコミット履歴が作られるのはなぜか」でしたが、「自分が変更していないファイルのコミット履歴が作られているように見えますが、実は作られていない」が正しかったのです。4
マージコミットの生成を回避する手段
ここまでで、マージコミットが生成される理由について解説してきました。分岐の履歴を残すためにはマージコミットが必要ですが、そのような履歴を残したくない場合、Git にはいくつかの手段があります。
リベース(Rebase)
プル(git pull
)した時の動作はデフォルトでマージ(merge
)になっています。
>git config pull.rebase
false
この設定を true
に変更するとプルした時の動作が rebase
になります。
>git config pull.rebase true
rebase
の場合、マージコミットは作成されません。
--- config: gitGraph: mainBranchName: "origin/main" --- gitGraph commit id:"C1" commit id:"C2" branch main checkout main commit id:"C3" checkout "origin/main" commit id:"C4"
この状態から rebase
を行うと、以下のようになります。
--- config: gitGraph: mainBranchName: "origin/main" --- gitGraph commit id:"C1" commit id:"C2" commit id:"C4" branch main checkout main commit id:"C3"
プッシュすると、以下のような1直線の履歴になります。
--- config: gitGraph: mainBranchName: "main origin/main" --- gitGraph commit id:"C1" commit id:"C2" commit id:"C4" commit id:"C3"
C3
コミットは C4
コミットの後に(Git 用語的には先頭に)付け替える形で履歴が統合されます。
C3
コミットの親は元々 C2
でしたが、rebase
によって C4
に親が書き換えられる点に注意してください。rebase
には履歴をシンプルな直線にするメリットがありますが、正確な分岐の履歴は残すことができないデメリットがあります。
スカッシュ(squash)
Squash
とは複数のコミットを1つにまとめてからマージする方法です。
--- config: gitGraph: mainBranchName: "origin/main" --- gitGraph commit id:"C1" commit id:"C2" branch main checkout main commit id:"C3" checkout "origin/main" commit id:"C4"
スカッシュ(squash)後は以下のようになります。
git merge --squash origin/main
--- config: gitGraph: mainBranchName: "origin/main" --- gitGraph commit id:"C1" commit id:"C2" branch main checkout main commit id:"C3" checkout "origin/main" commit id:"C4" checkout main commit id:"C5 スカッシュ"
プッシュすると、以下のようになります。
--- config: gitGraph: mainBranchName: "main origin/main" --- gitGraph commit id:"C1" commit id:"C2" commit id:"C3" commit id:"C5"
C4
はどこに消えたのか?という疑問もありますが、実際には通常のプッシュは行えず、強制プッシュ(git push --force
)が必要になります。C4
に相当する内容は C5
になりますがコミットとしては別物になります。リモートとローカルで異なる履歴になるため、強制プッシュを行わなければなりません。マージコミットは作成されませんが、リモートの履歴を破壊するので、現実的には使えない手段となります。(squash の実用的な用途は feature ブランチの複数のコミットをまとめて main にコミットするようなケースです)
チェリーピック(cherry-pick)
cherry-pick
は特定のコミットを別のブランチに取り込む手段です。
--- config: gitGraph: mainBranchName: "origin/main" --- gitGraph commit id:"C1" commit id:"C2" branch main checkout main commit id:"C3" checkout "origin/main" commit id:"C4"
C4
をローカルの main にチェリーピックします。
--- config: gitGraph: mainBranchName: "origin/main" --- gitGraph commit id:"C1" commit id:"C2" branch main checkout main commit id:"C3" checkout "origin/main" commit id:"C4" checkout main commit id:"C4' チェリーピック"
プルすると、以下のようになります。
--- config: gitGraph: mainBranchName: "origin/main" --- gitGraph commit id:"C1" commit id:"C2" branch main checkout main commit id:"C3" checkout "origin/main" commit id:"C4" checkout main commit id:"C4' チェリーピック" commit id:"C5 マージコミット"
C4
と C4'
は変更内容は同じですが、コミットは別物です。リモートとローカルは分岐しているのでマージコミットが生成されます。マージコミットが作成されるなら意味がないのでは?と思うかもしれません。実際、あまり意味が無いと思いますが、強いていえばここで生成される C5
のコミットは SourceTree で見てもファイルの変更差分が表示されないので、「自分が変更していないのに変更されているように見える」という誤解は生じません。ただし、そのような目的のために cherry-pick
を使用するのは現実的ではありません。
まとめ
リモートとローカルで履歴が分岐している状態でプルを行うと、(Git のデフォルト設定では)マージコミットが生成されます。このコミットは一見冗長な情報のように見えますが、分岐の履歴を残すために必要なものでした。また、GUI ツールの表示ではマージコミットでファイルの変更が行われているように誤解しがちですが、実際にはファイルの変更履歴は含まれていません(競合が生じていない場合)ことも示しました。
プル時の挙動としてはマージ(merge:デフォルト)とリベース(rebase)の2種類があります。
- マージ(merge)
- メリット:分岐の正確な履歴が残る
- デメリット: 自分の変更分が把握しにくい
- リベース(rebase)
- メリット:履歴が直線的で見やすくなる
- デメリット: 分岐の正確な履歴は残らない
それぞれの特徴を踏まえて使い分けを行う必要があります。
脚注
-
「変更していないはずのファイルのコミットが生成される」原因についてよく挙げられるものに以下がありますが、このメモ書きでは取り上げません。
- 改行コードの問題(
git config core.autocrlf
、.gitattributes
のtext=auto
) - パーミッションの変更(
git config core.filemode
) - 自動整形ツールによるファイルの変更
- 改行コードの問題(
-
より正確には、コミットは親コミット(一つ前のコミット)の情報を持っているので、
C3
がC2
から分岐したことは分かります。ただし、どの時点で統合したかはマージコミットが無いと分からないのです(枝分かれのタイミングは分かりますが、合流のタイミングがわからないのです)。↩ -
親が3つ以上のマージもありますが、ここでは取り上げません。↩
-
マージコミットにファイルの変更履歴が含まれていないことを確認する別の方法として
git log ファイル名
を使用する方法があります。SourceTree では「選択のログを表示」がこれに対応します。以下のようにファイルの変更履歴にマージコミットは含まれていません。↩