はじめに
初めまして。半年ほど前に未経験からRails中心の開発にアサインされた新卒社員のyagaodekawasuです。
さて、Gitでコード管理をしながらチーム開発していたある日、作業途中のブランチについて先輩社員から**「現状把握したいから、取り敢えず今できてるとこまでpushして」**と言われました。
その時のブランチはHEADがスナップショットのための中途半端なコミットだったので、最新の一つ前までのコミットをpushしたい(下図参照)と思ったのですが、**「git push 途中まで」**とかのワードでググっても求めているソリューションにたどり着くことができませんでした。
先輩達の助言を得てなんとか目的を達成することができたものの、また同じようなことがあった時のことを考えて、忘れないうちにインターネッツに書き留めておこうと思います。
A--B--C--D
↑ ↑このコミットは作業途中でゴチャゴチャしてるから
└ A~Cをpushしたい
ゴール
ここでは便宜上"sample"という名前の適当なリポジトリを作成し、空のファイルを1つ作成しただけのコミットを4つ用意して説明することとします。初期状態では、リモートブランチには最初のコミットだけがpushされています。
ここから"Make piyo"までのコミットをpushすることが今回のゴールです。
$ git tree
* yagaodekawasu 64a1dce (HEAD -> master) Make hogehoge
* yagaodekawasu 06b4153 Make piyo
* yagaodekawasu 63c971c Make fuga
* yagaodekawasu b49190e (origin/master) Make hoge
なお、ここで叩いているgit treeは以下の記事で紹介されている拡張されたgit logコマンドのエイリアスです。非常に便利ですのでまだ導入されていない方はこの機会にどうぞ。
解法1. stashを使う△
最初に試した方法です。
コミットしていない変更点は、ステージング(add)しているかどうかに関わらずgit stash saveによってスタックに退避させることができます。
pushが完了したら、**git stash pop stash@{n}**で元に戻します。
// 最新のコミットを取り消してステージング状態に戻す
$ git reset --soft HEAD~
// 戻ってることを確認
$ git status
ブランチ master
コミット予定の変更点:
(use "git reset HEAD <file>..." to unstage)
new file: hogehoge
// 未コミットの変更を退避
$ git stash save
Saved working directory and index state WIP on master: 06b4153 Make piyo
// stashの中身を確認
$ git stash list
stash@{0}: WIP on master: 06b4153 Make piyo
// pushする
$ git push origin HEAD
Counting objects: 4, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (4/4), 407 bytes | 407.00 KiB/s, done.
Total 4 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), done.
To github.com:yagaodekawasu/sample.git
b49190e..06b4153 HEAD -> master
// 確認
$ git log
commit 06b415305173622c08ea88823dadcf5c378595b6 (HEAD -> master, origin/master)
Author: yagaodekawasu
Date: Mon Dec 2 10:15:46 2019 +0900
Make piyo
commit 63c971c71b7400262108a6548ee5148d9569828d
Author: yagaodekawasu
Date: Mon Dec 2 10:15:24 2019 +0900
Make fuga
commit b49190ec49ce28ea8762a1b234224cd9705d3c36
Author: yagaodekawasu
Date: Mon Dec 2 10:13:34 2019 +0900
Make hoge
// 退避させた変更を元に戻す
$ git stash pop stash@{0}
ブランチ master
コミット予定の変更点:
(use "git reset HEAD <file>..." to unstage)
new file: hogehoge
Dropped stash@{0} (ecf2c08c7c1df809407255b775f39d700feafe39)
上手くいきましたね。
ただ工数が多いのと、2つ以上前までのコミットをpushしたい場合に対応できない(多分不可能ではないがとても面倒)のが難点です。stashした変更を再度popし直さないといけないのもイケてません。
まぁstashの練習にはなるかな、という感じですね。
解法2. 1行で済ませる◎
お待たせしました。こっちが本題です。
解法1を社内Slackに載せて**「できたぜ!!ドヤァ...」**していたら、先輩が(っ'-')╮=͟͟͞͞ スッとこんなコマンドを投げて寄越しました。
git push origin HEAD~:<remote-branch>
思わず**「そマ!?」**と返信してしまいました。不敬罪で減給されなくてよかったです。
取り敢えず、先ほどの状態から半信半疑でコマンドを叩いてみます。
$ git tree
* yagaodekawasu 3965f49 (HEAD -> master) Make hogehoge
* yagaodekawasu 06b4153 Make piyo
* yagaodekawasu 63c971c Make fuga
* yagaodekawasu b49190e (origin/master) Make hoge
$ git push origin HEAD~:master
Counting objects: 4, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (4/4), 407 bytes | 407.00 KiB/s, done.
Total 4 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), done.
To github.com:yagaodekawasu/sample.git
b49190e..06b4153 HEAD~ -> master
$ git log
commit 3965f49e9d1fe43ce0ecaa56c7a287a1dc4c6466 (HEAD -> master)
Author: yagaodekawasu
Date: Mon Dec 2 11:31:20 2019 +0900
Make hogehoge
commit 06b415305173622c08ea88823dadcf5c378595b6 (origin/master)
Author: yagaodekawasu
Date: Mon Dec 2 10:15:46 2019 +0900
Make piyo
commit 63c971c71b7400262108a6548ee5148d9569828d
Author: yagaodekawasu
Date: Mon Dec 2 10:15:24 2019 +0900
Make fuga
commit b49190ec49ce28ea8762a1b234224cd9705d3c36
Author: yagaodekawasu
Date: Mon Dec 2 10:13:34 2019 +0900
Make hoge
ははぁ〜、上手くいってますねぇ。。。あの血の滲むような努力は何だったのか。。。
この方法なら、HEAD~をHEAD~2にすれば最新の2つ前のコミットまでをpushできるような拡張性が担保されます。
どういう仕組み?
上手く行ったのは良かったんですが、気になるのは**「どうしてこういう書き方ができるのか?」**。
私がGitを習得するために参考にしたサイトでは、git pushにこんな文法があるなんて書いてなかったような...
で、公式ドキュメントを参照しました(git push --helpでも見れます)。
これを見ると、git pushの汎用的な文法規則は
git push [<options>] [<repository> [<refspec>…]]
であることがわかります。オプションの他にはリポジトリと参照点を引数として渡せるということですね。
で、ここで問題となるのは<refspec>の部分ですが、この<refspec>は細分化すると
<refspec>… = +<src>:<dst>
と書けるようです("+"はここではあまり重要ではなさそうなので割愛)。
<src>にはpushしたいブランチ・特定のコミットのハッシュ値またはそのエイリアス("master~4"や"HEAD"など)が入ります。
<dst>はこのpushによってリモートのどのブランチが更新されるかを表します(コミットのハッシュ値・エイリアス不可)。
というわけで、先ほどの
git push origin HEAD~:master
というコマンドは、**「"origin"という名前が付けられたリモートリポジトリの"master"というブランチが、ローカルの"HEAD~"までを参照する」という意味だったんですね。
ちなみにHEAD~は@~**とも書けるし、pushするブランチがmasterの場合master~でも同じことです。
結論
やっぱマニュアルをちゃんと読もう。
余談1. git stashとは何か?
解法1でgit stash saveとかgit stash popとか叩いて、結果確認して「上手くできた。良かった。」とかしてましたが、やっぱりちゃんと理解して使わないといつか足下を掬われる気がするので調べてみました。
git stash saveした状態でログを確認すると、このような表示になります。
$ git tree
* yagaodekawasu 0d576db (refs/stash) WIP on master: 06b4153 Make piyo
|\
| * yagaodekawasu dad1769 index on master: 06b4153 Make piyo
|/
* yagaodekawasu 06b4153 (HEAD -> master, origin/master) Make piyo
* yagaodekawasu 63c971c Make fuga
* yagaodekawasu b49190e Make hoge
・・・どゆこと? と思ってググった結果、この記事にたどり着きました。
サクッと使いこなすためのgit stash Tips & stashの仕組み#おまけ---stashの仕組み
雑にまとめると、git stash saveは
①HEADからブランチを切ってステージングされた差分(index)をまとめたコミットを作成
②同じくHEADから別のブランチを切ってステージングされていない(WIP:Work In Progress「作業中」)変更をまとめたコミットを作成
③両者をrefs/stashにマージ
ということをしているらしいです。賢い。
(①、②で切り出されたブランチ名が何であるかは、今回は調べきれませんでした。)
複数stashしている場合は、最初のstashを0番目として、**stash@{n}**でアクセスできるって感じですかね。
余談2. git resetについて
解法1でgit reset --soft HEAD^というコマンドを叩きましたが、ここは--softを付けなくても問題ありません。
オプションの有無でどのような違いが生じるかは、以下のエントリで詳しく説明されているので、気になる方は読んでみてください。とても参考になりました。
[git reset (--hard/--soft)]ワーキングツリー、インデックス、HEADを使いこなす方法
余談3. git pushについてもう少し
手元で解法1を検証してから解法2の検証に移る時、
git push -f origin b49190e:master
で初期状態に戻したりしました。
b49190eは最初のコミットのハッシュ値です。"~"や"^"の使い方に自信がない時はハッシュ値で直接指定した方が安全かもしれません。
ちなみに"~"と"^"については以下の記事がわかりやすかったです。
別の使い方をすればブランチの削除もできてしまう(むしろそっちの使い方の方が有名?)ようなので、この記法一つ覚えておくとGitでできることがかなり広がるかもしれませんね。
余談4. "@"
"HEAD"のエイリアスとして"@"が導入されたのはGit 1.8.5(2013年11月29日リリース)からだそうです。
意外と検索してすぐにヒットしなかったので、覚書程度に。
Git 1.8.5 最新情報 | Atlassian Blogs
#参考文献
Git ユーザマニュアル (バージョン 1.5.3 以降用)
【git stash】コミットはせずに変更を退避したいとき
git push の取り消し方法 | WWWクリエイターズ
Git の仕組み (1) - こせきの技術日記
ご清覧ありがとうございました!