git で不必要に複雑なコミット家系図にしてしまっていた原因が少しわかった

git 初心者。メンテナンス用の maint と新規開発用の master、二つの統合ブランチがあるだけなのに、なぜかコミット家系図が込み入ったものになってしまう、しかも望まないマージが行われたりしてしまう、という現象に悩んでいた。ひとつずつ手順を踏んで、毎回 gitk でコミット家系図を確認していて、原因が少しわかった。

# 最初は 1 から 9 までの数字が一行ごとに並んだファイル prime を作る。           
# git 管理。最初のコミットを A とする。                                         
mkdir first; cd first; seq 9 > prime
git init; git add .; git commit -m 'first commit'; cd ..

# 上流リポジトリ repos を作って、そこに作業用ディレクトリから push する。       
# push できれば作業用ディレクトリ first は要らないから消しちゃう。              
mkdir repos; cd repos; git --bare init --shared; cd ..
cd first; git push ../repos master; cd ..
rm -fr first

# プロジェクトを進行する人 a さんと b さん、それぞれのリポジトリを              
# sidea と sideb に作る。                                                       
git clone repos sidea; git clone repos sideb

# a さんの作業。10 までの素数表を作っていく統合ブランチ p10 を作る。            
cd sidea
git branch p10 origin/master
# master ブランチはとりあえず 20 までの素数表を作っていくことにする。           
git checkout master
seq 19 > prime; git commit -a -m 'seq 19'
# ここで作られたコミットを B とする。                                           
# ブランチ master, p10 を push しておく。                                       
git push origin master
git push origin p10
# 奇数を抜き出す変更を行う。変更はリモート追跡ブランチ origin/p10 から          
# 起こしたトピックブランチ topic 上で行う。                                     
git checkout -b topic origin/p10
mv prime tmp; awk 'NR%2' tmp > prime; rm tmp
git commit -a -m 'odd'
# ここでできたコミットを C とする。                                             
# 各統合ブランチ master, p10 で topic を併合して push する。                    
git checkout master
git pull origin master # ... (1)                                                
git merge topic
git checkout p10
git pull origin p10    # ... (2)                                                
git merge topic
git push
# topic を master に併合したときに作られたコミットを D とする。

(2) の直前、

p10,origin/p10
|   topic
|    |
v    v
A----C
 \    \
  \    \
   B----D
   ^    ^
   |    |
   |    master 
origin/master

となっている。ここで (2) を実行しても変化はない。けれど、(2) のところで引数なしの git pull を実行してしまうと次のようになる。

origin/p10
|   topic
|    |
v    v
A----C
 \    \
  \    \
   B----D
   ^    ^
   |    |
   |    master 
p10,origin/master

ブランチ p10 の先頭が B になってしまう。A->B の変更はブランチ p10 に含めたくないにも関らず。この状態で topic を併合すると B と C を親とした新しいマージコミット E が作られてしまう。

origin/p10
|   topic p10
|    |    |
v    v    v
A----C----E
 \   _\__/
  \ /  \
   B----D
   ^    ^
   |    |
   |    master 
origin/master

git pull は git fetch + git merge だから、無闇に pull すると現在のチェックアウトしてあるブランチに思いもよらぬブランチ (origin/master かな) が併合されてしまう、ってことだろうか。とりあえずはブランチを明示して git pull する、ということでこのような現象は防げそうな気がしている。けど、何かまだ根本的に間違って理解しているかもしれない。

追記

引数なしで git pull したときに、どのリポジトリのどのブランチをマージするかは、構成変数に記述されていることがわかった。

[takeyuki@sunya sidea]$ cat .git/config
[core]
        repositoryformatversion = 0
        filemode = true
        bare = false
        logallrefupdates = true
[remote "origin"]
        fetch = +refs/heads/*:refs/remotes/origin/*
        url = /home/takeyuki/gitest/2500/repos
[branch "master"]
        remote = origin
        merge = refs/heads/master
[branch "p10"]
        remote = origin
        merge = refs/heads/master
[branch "topic"]
        remote = origin
        merge = refs/heads/p10

ブランチ p10 を作ったときには origin/master の先頭コミットを起点にしたから、branch.p10.merge は refs/heads/master、すなわち origin/master になっているわけだ。だから p10 をチェックアウトした状態で git pull すると origin/master がマージされる、と。
ブランチを明示して git pull するようにする、というのはひとつの解決策だけれど、引数なしの git pull で大丈夫なように構成変数を書き換えておく、というのも手だろう。

[takeyuki@sunya sidea]$ git config --unset branch.p10.merge master
[takeyuki@sunya sidea]$ git config --add branch.p10.merge refs/heads/p10
[takeyuki@sunya sidea]$ git config --local -l
core.repositoryformatversion=0
core.filemode=true
core.bare=false
core.logallrefupdates=true
remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
remote.origin.url=/home/takeyuki/gitest/2500/repos
branch.master.remote=origin
branch.master.merge=refs/heads/master
branch.p10.remote=origin
branch.p10.merge=refs/heads/p10
branch.topic.remote=origin
branch.topic.merge=refs/heads/p10