git config --add receive.denyCurrentBranch ignoreはどう危険なのか

git config --add receive.denyCurrentBranch ignoreをやるとどう危険なのか。一言で言うと「ある人が行った実装を、別の人が無意識に削除してコミットする」という事態を引き起こす。これが危険じゃなくて何なんだ。

まずローカルで実験用のリポジトリを作ってみよう。fという名前のリポジトリを作って、READMEをおく。今は中身は空っぽだ。

$ git init f
Initialized empty Git repository in /Users/nishio/tmp//f/.git/
$ cd f
f$ touch README
f$ git add README
f$ git commit -m "initial"
[master (root-commit) ce6d7d5] initial
 0 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 README

次はそのリポジトリをgって名前でcloneしてみる。

f$ cd ..
$ git clone f g
Cloning into g...
done.

できた。次はgのワークツリーでREADMEを編集してfにpushしてみよう。READMEにhogeって書き足してコミットし、pushする。push時にエラーが出る。

$ cd g
g$ cat >> README
hoge
g$ git add README
g$ git commit -m "modify on g"
[master 93295fc] modify on g
 1 files changed, 1 insertions(+), 0 deletions(-)
g$ git push
Counting objects: 5, done.
Writing objects: 100% (3/3), 256 bytes, done.
Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
remote: error: refusing to update checked out branch: refs/heads/master
remote: error: By default, updating the current branch in a non-bare repository
remote: error: is denied, because it will make the index and work tree inconsistent
remote: error: with what you pushed, and will require 'git reset --hard' to match
remote: error: the work tree to HEAD.
remote: error:
remote: error: You can set 'receive.denyCurrentBranch' configuration variable to
remote: error: 'ignore' or 'warn' in the remote repository to allow pushing into
remote: error: its current branch; however, this is not recommended unless you
remote: error: arranged to update its work tree to match what you pushed in some
remote: error: other way.
remote: error:
remote: error: To squelch this message and still keep the default behaviour, set
remote: error: 'receive.denyCurrentBranch' configuration variable to 'refuse'.
To /Users/nishio/tmp/gittest/f
 ! [remote rejected] master -> master (branch is currently checked out)
error: failed to push some refs to '/Users/nishio/tmp/gittest/f'

push時に「bareでない(ワークツリーを持っている)リポジトリの、今チェックアウトされているブランチをpushすることはできない。なぜならインデックスとワークツリーが不整合になるからだ」と言われてエラーになっている。

こういう状況になった時に「ちょっと危険だけど git config --add receive.denyCurrentBranch ignore すればいいよ」なんてことを説明しているサイトがあるらしい。じゃあそれをやったらどんなことが起こるのか経験してみよう。リポジトリfでgit config --add receive.denyCurrentBranch ignoreする。

g$ cd ../f
f$ git config --add receive.denyCurrentBranch ignore

リポジトリgに戻ってpushすると、今度はすんなり成功する。

f$ cd ../g
g$ git push
Counting objects: 5, done.
Writing objects: 100% (3/3), 256 bytes, done.
Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
To /Users/nishio/tmp/gittest/f
   ce6d7d5..93295fc  master -> master

この状態、fさんにとっては「知らない間にgさんがmasterブランチを書き換えている」って状態になっている。ログを見ると確かにgさんが書き換えたって情報は出ている。でもワークツリーのREADMEはgさんが書き換える前の「空っぽ」のままだ。これにfugaと「追記」してコミットしてみよう。

g$ cd ../f
f$ git log --oneline
93295fc modify on g
ce6d7d5 initial
f$ cat >> README
fuga
f$ git add README
f$ git commit -m "modify on f"
[master 14a82ca] modify on f
 1 files changed, 1 insertions(+), 1 deletions(-)

目ざとい人は気づいたかもしれない。「1 deletions(-)」と表示されている。

では今のコミットで何が行われたか確認してみよう。

f$ git show
commit 14a82ca87ba5dd536492b9758582179104de09bd
Author: NISHIO Hirokazu <nishio.hirokazu@gmail.com>
Date:   Mon Apr 16 12:35:11 2012 +0900

    modify on f

diff --git a/README b/README
index 2262de0..9128c3e 100644
--- a/README
+++ b/README
@@ -1 +1 @@
-hoge
+fuga

fさんのコミットは「gさんの行った修正を削除して自分の修正を追加」になっている。多分しばらくたってから

f「おい、g、この前頼んだ修正はまだか」
g「え、だいぶ前にコミットしたぞ」
f「直ってねーよ!ほら!」
g「そんなはずはない…なんだこれは、お前が俺の修正を削除したんじゃないか」
f「えっ、そんなはずは…ほんとだ…いや、消したつもりはないんだが」
g「勝手に消えたのか?git怖い」

などという不毛な会話がかわされることになる。ほんとうに怖いのは「ちょっと危険だけど」とか言いながらバッドノウハウを撒き散らすサイトの存在だ。

じゃあどうする?

一番素直なのは「githubなりbitbucketなり自分で作るなりしてbareなブランチを用意する」なんだけども、あえて今回のような「bareでないリポジトリからcloneして、しかも二人ともmasterで作業している」というひどい状態からどうするかを考えてみる。

まずgではまた何か変更を行なったとする。piyoって書き足している。

g$ cat >> README 
piyo
g$ git add README 
g$ git commit
Waiting for Emacs...
[master 29cfa4a] modify on g (piyo)
 1 files changed, 1 insertions(+), 0 deletions(-)

本当を言えば「変更の前にブランチを作ってその中で作業する」が望ましいんだが、いまからでも遅くないので「piyoを追加する」という意味のブランチを作る。

g$ git checkout -b add_feature_piyo
Switched to a new branch 'add_feature_piyo'

そして、そのブランチだけをorigin(clone元、つまりこの場合はf)にpushする。

g$ git push origin add_feature_piyo
Counting objects: 9, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (9/9), 678 bytes, done.
Total 9 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (9/9), done.
To /Users/nishio/tmp/gittest/f
 * [new branch]      add_feature_piyo -> add_feature_piyo

そうすると、fの側ではgの変更が新しいブランチとして現れるので、fの作業が一段落した、fの都合の良いタイミングでそこからmergeする。

g$ cd ../f
f$ git branch
  add_feature_piyo
* master
f$ git merge add_feature_piyo
Auto-merging README
CONFLICT (content): Merge conflict in README
Automatic merge failed; fix conflicts and then commit the result.
f$ cat README 
<<<<<<< HEAD
fuga
=======
hoge
piyo
>>>>>>> add_feature_piyo

さっきうっかり「gの変更を削除してコミット」しちゃったせいでコンフリクトが起きているね。READMEの中身はこんな風になっているので、今したいことは両方の変更を採用することだからそうなるように編集しよう。

f$ emacsclient README # 編集している 
Waiting for Emacs...
f$ cat README 
fuga
hoge
piyo

あとは普通にこのファイルをaddしてcommitするだけ。ちなみにcommit -vをすると、今から何をコミットしようとしているかのdiffなどが見れるからより安心。今回のケースでgit commit -vすると、下のようにhogeとpiyoを追加した旨が表示される。

Merge branch 'add_feature_piyo'

Conflicts:
	README
#
# It looks like you may be committing a merge.
# If this is not correct, please remove the file
#	.git/MERGE_HEAD
# and try again.


# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# Changes to be committed:
#
#	modified:   README
#
diff --git a/README b/README
index 9128c3e..05a9867 100644
--- a/README
+++ b/README
@@ -1 +1,3 @@
 fuga
+hoge
+piyo