git のメモ
merge と merge --no-ff と merge --squash と rebase
以下ではコミットのハッシュ値をコミットIDと呼び a のように表記し、今ブランチAに居ることを A * と表記する
merge, merge --no-ff
code:git log
a-b A *
\-c B
この状態で git merge B をすると
code:git log
a-b-d A*
\-c-/ B
のように、どのコミットIDも変化せず、マージコミット d が作られる
ところで
code:git log
a A *
\-c B
のような、 a から続く別のコミットが A に存在せず分岐になっていない場合は、 git merge B をするとfast-forwardが起きる
fast-forwardは、現在のブランチが先頭として参照するコミットだけを変える操作で、新しいコミットは作られず、既存のコミットIDも変化しない
今回であれば A が先頭として参照するコミットが a から c に変化し、具体的には以下のようになる
code:git log
a-c A *, B
ブランチ B の先頭は変わらず c だが、この状態で B にコミットを行い d を作ると
code:git log
a-c A*
\-d B
のように B は最初から c で分岐したかのように表示される
merge はデフォルトで --ff というオプションが有効であり、fast-forwardが可能な場合はマージコミットを作らないが、 --no-ff をつけることで必ずマージコミットを作るマージを行うようになる
merge --squash
code:git log
a-b A *
\-c-d B
まず git merge --squash は、ワーキングディレクトリとインデックスだけを実際にマージが起きた後の状態に更新するコマンドで、ツリーには一切の変化を起こさない
しかし例えば git merge --squash B をすると、ワーキングディレクトリとインデックスは b に c と d での変更を適用した状態になるため、ここでコミットを行い e を作ると、この e は c と d での変更を含む A 上のコミットであるため、マージコミットが作れることになる
code:git log
a-b-e A *
\-c-d B
※ e は c と d での変更を含むコミット
これにより、 git commit のひと手間はかかるが、複数のコミットを1つのコミットに圧縮してマージすることが可能になる
rebase
code:git log
a-b A *
\-c-d B
この状態でgit rebase B をすると
code:git log
a-c-d-e A *
\-c-d B
※ e は b と同じ内容の自動で作られるコミット
のように b が消えて、 B の内容をとり込んだのち b と内容は同じコミット e が作られる
ところでこの図は、 c と d は B にも含まれているため
code:git log
e A *
a-c-d-/ B
のように書きなおせる
つまり git rebase B は、現在作業しているブランチ A の全てのコミットを B の後ろに新しいコミットとして付け加える
ちなみに git rebase B は git rebase B A の略であるため、今 A にいなくても A を B の先につけることができる
rebase は例えば、PRをmasterの変更に追従させるのに使われる
code:git log
a-c-d master
\-b A *
ここで git rebase master A をすると
code:git log
a-c-d master
\-e A *
のように、 d の先に b と同じ内容の e がつくため、このままマージすればfast-forwardによって a-c-d-e のように直線的な歴史を作れる
他にもCIを使っているような場合、 d にも b にもそれぞれをビルドするための内容が含まれ、 b をマージするとき b をビルドするための内容をコミットしたいという場合、普通にマージするとマージコミットに b の内容を打ち消すような本質的でない差分が含まれてしまうが、 rebase を使えばこれを回避できるというのもある
ただしこのようなケースは、変更の行数が少なければ merge --squash によるマージでも見栄えは損なわれない
結局 rebase の利点は見栄えだけであり、変更が複雑な場合は正しい歴史を作るのも難しいし、 push -f が必要になる場合も多い
コミットログは成果物ではなくあくまで作業履歴であり、多少汚なくとも正しい履歴が残っていることが重要で、綺麗で正しい歴史に整形する工数や教育コストも考えると常用するものではなさそう
以下のような、 rebase を使ったことでビルドが通らなくなるといった話もある
rebase で履歴を整えるよりは、大きすぎるPRを出さないようにしたり、適度に Squash and merge を使ったりするほうが現実的に思える
cherry-pick と rebase
別のブランチから特定のコミットを取り出して現在のブランチに適用するのは git cherry-pick でできる
.. で範囲指定する場合は、取り込みたいコミットの1つ前のコミットから指定する必要がある
code:sh
# fromBranch ブランチのコミットを新しいもの順に表示
$ git log --pretty=format:%h 'fromBranch'
f77d749
7c83ae6
0794374
# 7c83ae6 から f77d749 までの変更を toBranch ブランチに取り込む
$ git checkout 'toBranch'
$ git cherry-pick 0794374..f77d749
実は rebase は cherry-pick を使ってもできる
差分の少ないPRを出すために新しいブランチを作り、古いコミットから生えているブランチでのコミット全てを cherry-pick で持ってきたい場合の図を考えてみる
code:git-log
/ newBranchForPR *
a-b-c master
\-d-e oldBranch
を
code:git-log
/-f-g newBranchForPR *
a-b-c master
※ f, g は d, e と同じ内容のコミット
としたいが、これは git cherry-pick a..e でも git checkout oldBranch && git rebase master でも実現できる
rebase の場合は newBranchForPR ブランチが不要なかわりに oldBranch が破壊的に変更されるという違いはある
pull と fetch
fetch は指定したローカルのリモートブランチを更新(ないなら取得)する
pull は fetch した後、そのローカルのリモートブランチを今作業しているブランチに merge する
pull -r とすると、 merge のかわりに rebase を行う
pull -r は開発中のブランチにmasterの変更をマージコミットを作らずに取り込みたい場合に便利
PRのコンフリクトをrebaseで解決する
コンフリクトしている作業ブランチで
git pull -r upstream master
コンフリクトがなくなるまで
コンフリクトを修正 → git add → git rebase --continue
を繰り返し自分のリポジトリにpushする
git push -f origin feature-branch
git pull -r origin master は master に push し忘れて別のコミットが積まれてしまった場合にも使える
code:git log
a-b origin/master
\-c master *
a-b-d(内容はc) となり -f なしで push origin master できる
ちなみに merge --squash 前提なら、普通に git merge master してコンフリクトを解決して git commit -am 'resolve conflict' とかして git push origin するほうが push -f しなくていいので安全
ファイルパスと内容の両方が大きく変わっている古いPRを扱う場合などもこの方針をとったほうが楽なはず
ちなみに merge master のかわりに merge --squash master を使うとコンフリクトがちゃんと解消できないので注意
既に削除されてしまったリポジトリからのPRをチェックアウトしたり編集したりする
GitHubのPRは、PRを出した時点で出されたリポジトリに(元のブランチに追従する)コピーが作られるため、PRにしているブランチやforkしたリポジトリ自体が消えてもPRは残る
origin に出された PR#42 を、 pr42 という名前でチェックアウトするには
code:bash
$ git fetch origin pull/42/head:pr42
$ git checkout pr42
のようにする
これは通常のリポジトリでも可能なため、新しく git remote add などしなくても、PRの動作確認をしたりできる
ただし pull/42/head などは読み取り専用であり、ここに git push origin:pull/42/head pr42 つまり追加でコミットしていくことはできない
既に削除されたリポジトリからのPRを編集するには、 git push origin pr42 のように origin:pull/42/head の内容を含むブランチを自分で作りそこにコミットしていけばよい
ローカルで pr42 にコミットした後 git push origin pr42 するのでも、リモートリポジトリの作成が前後するだけで作業としては同じ
この場合はブランチを push しなくても、ローカルでマージコミットを作ってこれを push するのでも結果は同じだが、ブランチの段階で push してPRを作って(このとき PR#42 などに言及する)これをマージするほうが何が起きたか分かりやすい
他人からのPRに追加でコミットする
上とは少し違う話で、こちらはPRにされているブランチ(上でいう「元のブランチ」)にfork元のコミッターがコミットしていく話
つまりPRを出されたリポジトリのコントリビューターが、PRを出したリポジトリのPRにされているブランチに、PRの製作者がコミットを追加してPRを更新するのと同じようにコミットを追加してPRを更新する方法の説明
例えばPR元のブランチが someuser:xxx であった場合は、
code:sh
$ git remote add someuser git@github.com:...
$ git fetch someuser xxx
$ git checkout -t someuser/xxx
...
$ git push someuser xxx
のようにすればよい
親コミットのない独立したブランチを作る
git checkout --orphan gh-pages
git rm -r --cached . も忘れずに
ローカルのリモートブランチを更新する
自分がforkしたリポジトリを origin 、fork元のリポジトリを upstream としたとき、ローカルの upstream/master を更新する方法
code:bash
$ git fetch upstream master
$ git checkout -b feature upstream/master
...
$ git push origin feature
あとはfork元にfeatureブランチでPRを出せば良い
その他一行コマンド
あるファイルだけをステージングから取り下げる
git reset <paths>
ファイル内容も戻す場合は git checkout <paths>
それぞれ git restore -S <paths>, git restore -W <paths> でもできる
とりあえず最新のコミットに戻す
git reset --hard
位置を省略しているので HEAD を指していることになる
ファイルはそのままで直前のコミットを消す
$ git reset @^
ブランチを切り忘れたままコミットしてしまった
$ git branch -M new-branch-name
$ git branch old-branch-name HEAD^
-M で現在のブランチを new-branch-name にリネームし、old-branch-name HEAD^ で元のブランチを再構成している
ファイルの実行権限もコミットする
git add --chmod=+x install.sh
文字単位で差分を確認する
$ git diff --color-words='.'
--color-words='.' は --word-diff=color --word-diff-regex='.' の略記
管理中のファイルの一覧
git ls-files
最低限見やすい git log
git log --graph --decorate --oneline
ブランチaを clone する
git clone -b a URL
リモートにあるブランチを checkout する
git checkout -t repository/branch
pull -f する
$ git fetch origin branch_name
$ git reset --hard origin/branch_name
git fetch origin branch_name は git fetch だけでもいい
リモートリポジトリaのブランチbを消す
git push -d a b
これは git push a :b と同じ
リモートで削除されたブランチをローカルでも消す
git fetch -p origin
addしたあとに変更点を確認する
git diff --cached
ステージング環境とHEADとの比較
git diff はステージング環境とワークスペースの比較
全てのタグを一度にpushする
git push origin --tags
個別にpushするには git push origin タグ名
比較的安全に push -f する
--force-with-lease をつけて push するとローカルが最新でない場合に push を失敗させられる
よって push -f --force-with-lease とすると他の人のコミットを吹き飛ばす恐れが軽減される
そもそも共同開発で push -f は避けるべきなので、やむを得ない場合につけると事故がおきにくくなるという感じか
ドキュメントでは push した内容を rebase する必要があるケースを例としている
特定のファイルの過去の状態を見る
git show ab1c783:app.js
マージした後で内容を確認する
git diff origin/master master
便利なエイリアス
git clone --recurse-submodules
git clone && git submodule update --init --recursive
.gitignore
プロジェクトのルートディレクトリにあるものを除き、ある拡張子のファイル以外の全てのファイルとフォルダを無視する
例えばLICENSEは無視したくないが、サブディレクトリには不要なファイルがたくさんあるという場合に使える
code:.gitignore
*/*
!*.md
最初の行を * にすると、ある拡張子のファイル以外の全てのファイルとフォルダを無視する
*/* だけにすれば、全てのサブディレクトリを無視する
.gitconfig
code:.gitconfig
quotePath = false
autocrlf = input
excludesfile = ~/.gitignore_global
default = current
# View log like tree
l = log --graph --pretty=format:'%x09%C(yellow)%h%Creset %C(magenta)<%an>%Creset%C(cyan)%d%Creset%n%x09%x09%Cgreen%ci, %cr%Creset %s' # View one-line log like tree
lo = log --graph --pretty=format:'%x09%C(yellow)%h%Creset%C(cyan)%d%Creset %C(magenta)<%an>%Creset %Cgreen%ci%Creset %s' git lo --all で全てのブランチの分岐が見れる
merge.ff, pull.ff
マージの際に必ずマージコミットを作るようにする --no-ff は .gitconfig でも設定できる
git config --global --add merge.ff false
ただし pull した時はマージコミットを作ってほしくないので git config --global --add pull.ff only もやっておく
push.default current
git push -u origin master で次回以降 git push とだけ打ったときから暗黙に origin master が指定されたことになる
これは git config --global push.default current で十分な気がする
GitHubPagesのデプロイスクリプト
./deploy.sh 'コミットメッセージ' myapp1 myapp2 ...
code:deploy.sh
echo 'コミットメッセージとプロジェクトの指定が必要です'
exit 1
fi
for dir in ${@:2}; do
cd $dir
npm run build
cd ..
done
if [ git branch --list gh-pages ]; then
git checkout gh-pages
git rm -rq --ignore-unmatch ${@:2}
else
git checkout --orphan gh-pages
git rm -rfq .
echo 'node_modules' > .gitignore
fi
for dir in ${@:2}; do
mv $dir/build/* $dir
done
git add -A
git commit -m "$1"
elmなら npm run build を elm-app build に、 echo 'node_modules' を echo -e 'elm-stuff\nnode_modules' にする
(printf 'elm-stuff\nnode_modules\n' でもいいけど ${@:2} を使ってるので bash でしか動かないのは変わらない)