ディレクトリ内の一部のファイルだけ、.gitignoreによる除外対象から除外(=ステージ対象に戻す)

.gitignoreで、頭に!を付けて記述したファイルはステージ対象になる…はずが上手くいかなかったのでメモ。

参考:gitignore(5)

~/tmp % git --version
git version 1.7.9

~/tmp % ls -R
.:
hoge

./hoge:
hage.txt  hige.txt

まず思いついた通りに.gitignoreを書くと…

~/tmp % cat .gitignore
/hoge
!/hoge/hage.txt

~/tmp % git status
# On branch master
#
# Initial commit
#
# Untracked files:
#   (use "git add ..." to include in what will be committed)
#
#       .gitignore
nothing added to commit but untracked files present (use "git add" to track)

ちょっと!うちのhage.txtちゃんも仲間に入れてよ!

ほんとに!の除外機能動いているのかよ、と結構長い間思っていましたが、ディレクトリ自体は除外しないようにすればいけることがわかりました。

~/tmp % cat .gitignore
/hoge/*
!/hoge/hage.txt

~/tmp % git status
# On branch master
#
# Initial commit
#
# Untracked files:
#   (use "git add ..." to include in what will be committed)
#
#       .gitignore
#       hoge/
nothing added to commit but untracked files present (use "git add" to track)

~/tmp % git add .
~/tmp % git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD ..." to unstage)
#
#       new file:   hoge/hage.txt
#

階層が深くなったら

解決したので満足しかかりましたが、階層が深くなったときにはさらに面倒そうです。ワイルドカード表記で、その階層のディレクトリもひっかけてしまうので。

~/tmp2 % mkdir -p a/b/c/d
~/tmp2 % touch a/a.txt
~/tmp2 % touch a/b/b.txt
~/tmp2 % touch a/b/c/c.txt
~/tmp2 % touch a/b/c/d/d.txt
~/tmp2 % cat .gitignore
/a/b/*
!/a/b/c/c.txt

~/tmp2 % git add .
~/tmp2 % git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD ..." to unstage)
#
#       modified:   .gitignore
#       new file:   a/a.txt
#

上記の例では、a/b/cが除外されているので、もはやc以下を見に行ってくれません。cに.gitignoreを置いて !c.txt を書いて復活させようというのも無駄。

~/tmp2 % cat .gitignore
/a/*
!/a/b
/a/b/*
!/a/b/c
/a/b/c/*
!/a/b/c/c.txt

~/tmp2 % git add .
~/tmp2 % git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD ..." to unstage)
#
#       modified:   .gitignore
#       new file:   a/b/c/c.txt
#

欲しい挙動は実現できましたが、本当にこんなめんどい書き方しないといけないんでしょうか?