ja ja ja ja ja ja ja ja ja en ja ja ja ja ja ja ja ja ja ja ja nb ja ja ja ja ja ja ja ja ja ja ja ja ja en ja en ja ja yue ja en ja en ja ja dv ja sq ja sq ja ja ja ja en ja ja en ja ja en ja ja fr ja ja ja ja en ja en ja fr ja ja ja ja ja ja ja en ja en ja en ja en ja en ja en ja en ja en ja ja ja ja fr ja en ja ja ja fr ja ja ja ja ja ja ja ja en ja ja ja ja ja ja en ja ja en ja ja en ja ja ja ja ja en ja en ja ja ja ja en ja en ja fr ja ja ja ja ja en ja ja en ja ja ja ja ja ja ja ja ja ja sv ja ja ja ja ja ja ja ja en ja ja en ja ja vi ja sv eu ja en en hu ja ja en en pl ja ja jbo hbs_lat en ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja
複数の作業環境から変更が同一ブランチにpushされるGitリポジトリについて、リモートにある内容を確実に手元に反映する - 2016-09-02 - ククログ
A-+ [remote master] ==|================================ | +-B---C---D [作業者1 local master] | +-E---F---G [作業者2 local master]
rebaseの何が難しいのかを分かりやすく示すために、まず普通のpullの場合を説明します。
ここで作業者1が作業の成果をpushすると、ブランチの状態は以下のようになります。
A-+-B---C---D("NG-NG-NG") [remote master] ==|============================================= | +-E---F---G('OK-OK-OK') [作業者2 local master]
作業者2はこの時、そのままgit pushはできません(しようとしてもエラーになります)。
そこで一旦git pullします。 すると、
A-+-B---C---D("NG-NG-NG")-+ [remote master] ==|=======================|======================= | | +-E---F---G('OK-OK-OK')-+-[作業者2 local master]  >>>>>> xxxxxxxxxxxx
このように、DとGの内容が矛盾しているので矛盾箇所を修正するように求められます。 これが「衝突(conflict)」という状態です。
衝突をどう解消するかは場合によりますが、ここでは2人の作業者の意図を尊重して「OKはNGにする」「ダブルクオートはシングルクオートにする」というそれぞれの折衷案を採る事にしました。 ファイルの内容を'NG-NG-NG'に書き換えてgit commit -aし、コミットします。
A-+-B---C---D("NG-NG-NG")-+ [remote master] ==|=======================|===================================== | | +-E---F---G('OK-OK-OK')-+-H('NG-NG-NG') [作業者2 local master]
こうして、DとGの矛盾を解消するコミットHができました。 このように、remote masterとlocal masterのような複数のブランチを統合(マージ)するために必要な、矛盾を解消するコミットを「マージコミット」と言います。
衝突が解消されてマージコミットHができたら、作業者2はようやくgit pushで変更点を共有リポジトリのブランチに反映させられる状態になります。 以下はgit pushして共有リポジトリに作業者2の成果が反映された状態です。
A-+-B---C---D-+--H[remote master ('NG-NG-NG')] | | +-E---F---G-+ ============================================== (作業者1、2の手元に固有の変更は無い)
不要なマージコミットの問題とrebase
先程、「複数のブランチを統合するために必要な、矛盾を解消するコミットをマージコミットと言う」と述べました。 しかし場合によっては各ブランチは矛盾無く統合できることもあります。 それぞれの作業者で違うファイルを編集していたり、単にファイルを追加しただけだったりする場合、矛盾を解消するための手作業での修正は必要なく、Gitは単に「2つのブランチを統合した」という事を示すだけのマージコミットを自動的に作成してくれます。
これは言い換えると、自動的に統合できた場面でのマージコミットには、「2つのブランチを無事に統合できた」という事以上のさしたる情報は含まれないという事です。 この事から、プロジェクト関係者の判断によって、「こういった無益なマージコミットが履歴の上に何度も現れるのは目障りなので、マージコミットが作成されないrebaseを使うように」という通達がなされる場合があります。
rebaseは、作業の起点になったコミットを別のコミット、特にリモートブランチの最新のコミットに切り替えるという操作です。 先の説明の途中の「作業者1と作業者2の作業の成果がそれぞれあって、作業者2の成果をpushできない状態」に戻ってrebaseの様子を示しましょう。
A-+-B-C-D [remote master] ==|============================= | +-E-F-G [作業者2 local master]
ここで*git pull --rebase*とすると、作業者2の作業の起点がリモートブランチのAからDに切り替わります。 つまり、「Aから派生して作業していたのではなく、Dから派生して作業していた事になる」という小規模な歴史改変が行われます。
A-B-C-D-+ [remote master] ========|============================= | +-E-F-G [作業者2 local master]
この状態からであれば、作業者2は問題なく成果をpushできます。
A-B-C-D-E-F-G [remote master] ==================================== (作業者1、2の手元に固有の変更は無い)
今度は先程の通常のpullの場合と異なり、マージコミットが作成されていません。 大した意味のないコミットがコミット履歴の中に現れる事がないため、履歴を見るのが楽になっています。
この例では減ったマージコミットは1つだけなので、あまり差が分からないかもしれません。 しかし、1日の間にマージコミットが10個も20個も発生するような状況だと、履歴はマージコミットだらけになってしまい、本当に重要なコミットが埋もれて分からなくなってしまいます。 そのような状況下では、「普通のpullではなくrebaseするようにする」という方針が大きな意味を持ってきます。
rebaseで変更が衝突した状況からの回復の、間違ったケース
上記の図ではつつがなくrebaseできた事にしていますが、実際には、リモートブランチの先頭であるDの状態と、作業者の手元のE、F、Gの状態は矛盾しています。 そのため、「Aから派生して作業していたのではなく、Dから派生して作業していた事になる」という歴史改変は行えません。 これも「衝突(conflict)」で、通常のpullでマージコミットを自分で作成するのと同様に、rebaseの場合も衝突を解消する必要があります。
ただ、rebaseでの衝突の解消は、通常のpullでの衝突の解消とは勝手が違います。 通常のpullではたった1つのマージコミットHで衝突を解消する事になるので、解消の手間は1回で済みますが、rebaseでは場合によっては何度も解消を繰り返さなくてはなりません。 通常のpullしか知らない人にとっては、ここがrebaseの分かりにくい点です。
作業者1が変更をpushした直後の状態から順番に見ていきましょう。 筆者は当初、以下のような誤解をしていました。
A-+-B-C-D("NG-NG-NG") [remote master] ==|=================================== | +-E-F-G('OK-OK-OK') [作業者2 local master]
ここでgit pull --rebaseを実行すると、以下のように衝突した事を示すメッセージが表示されます。
$ git pull --rebase ... From XXXXXXX * branch HEAD -> FETCH_HEAD First, rewinding head to replay your work on top of it... Applying: E Using index info to reconstruct a base tree... M sample.js Falling back to patching base and 3-way merge... Auto-merging sample.js CONFLICT (content): Merge conflict in sample.js error: Failed to merge in the changes. Patch failed at 0001 E The copy of the patch that failed is found in: .git/rebase-apply/patch When you have resolved this problem, run "git rebase --continue". If you prefer to skip this patch, run "git rebase --skip" instead. To check out the original branch and stop rebasing, run "git rebase --abort".
通常のpullの時と同じ要領で衝突を手作業で修正しようとするのですが、どうも先程のマージコミットの作成時とは様子が違います。 手元では'OK-OK-OK'が最新の状態だったはずなのに、ずいぶん前の状態との差分が出ています。
まあいいでしょう、先程のマージの時のように2人の作業者の意図を汲んで「OKはNGにする」「ダブルクオートはシングルクオートにする」という変更を統合することにします。
そして、先程のようにマージコミットにするべくgit commit -aします。 rebaseでは作業をリモートのブランチの最新の状態から行ったように歴史改変してくれるということなので、こういう状態になっているはずです。
A-B-C-D("NG-NG-NG")-+ [remote master] ====================|=================================== | +-E?-F?-G?('OK-OK-OK') [作業者2 local master?]
ところが、衝突を解消したと思ってgit pushしてみても何も送信される気配がありません。 もしかしてリモート側にまた作業者1がpushしたのか?と思ってgit pull --rebaseしたら、こんな事を言われます。
$ git pull --rebase You are not currently on a branch. Please specify which branch you want to rebase against. See git-pull(1) for details. git pull
どういうことか?と思ってgit branchしてみると、
$ git branch * (no branch, rebasing master) master
えっ、masterブランチにいたはずなのにどうして別のブランチになってるの!? という事で慌ててgit checkout masterでmasterに切り替え直してリポジトリの内容を見ると、git pull --rebaseする前の状態に戻っています。
A-+-B-C-D("NG-NG-NG") [remote master] ==|=================================== | +-E-F-G('OK-OK-OK') [作業者2 local master]
とりあえず元の状態に戻せたので一安心ですが、しかし衝突を直すために行った作業は電子の藻屑と消えてしまいました。 一体どうして……?
rebaseで変更が衝突した状況からの回復の、正しいケース
この時何が起こっていたのか、どうして衝突を解決したはずなのにその情報が失われてしまったのか。 これはrebaseの時に実際には何が起こっているのかを順を追って見ていかないと理解できません。
もう一度、作業者1が変更をpushした直後の状態から見ていきましょう。
A("OK")-+-B-C-D("NG-NG-NG") [remote master] ========|=================================== | +-E('OK')-F-G [作業者2 local master]
ここでgit pull --rebaseを実行すると、何が起こるのか。 もう一度、git pull --rebaseした時のメッセージを見てみましょう。
$ git pull --rebase ... From XXXXXXX * branch HEAD -> FETCH_HEAD First, rewinding head to replay your work on top of it... Applying: E Using index info to reconstruct a base tree... M sample.js Falling back to patching base and 3-way merge... Auto-merging sample.js CONFLICT (content): Merge conflict in sample.js error: Failed to merge in the changes. Patch failed at 0001 E ...
First, rewinding head to replay your work on top of it...というメッセージは、「リモートブランチの最新の状態に移動して、そこにあなたの作業の成果を反映します」という事を言っています。 そして続けてApplying: E(「Eを反映中」)と出た後、自動でのマージに失敗した事と、衝突したファイルの情報が出力されています。
つまり、この時Gitは「手元のmasterブランチの内容とリモートのmasterブランチの内容を完全に統合」しようとはしておらず、あくまで*「リモートのmasterから改めて作業を行った場合のコミットとしてEを反映」しようとしている*という状態なのです。
この状態を図に示しましょう。
A("OK")-+-B-C-D("NG-NG-NG")-+ [remote master] ========|===================|================================== | | | +-+-[作業者2 local temporary] | |  >>>>>> E | | +-E('OK')-------------+-F---G [作業者2 local master]
AではなくDから作業を開始したかのように歴史を改変しようとして、DとEが衝突します。 そこで、Gitは「Aから作業を行った時のEではなく、Dから作業を行った時に妥当な内容となるE」の作成を作業者2に求めているというわけです。
また、この時手元のmasterではなく、衝突解消作業用の新しい一時的なブランチ(のようなもの)にフォーカスが移っている事にも注意が必要です。 実際に、この段階でgit branchを実行すると、今いるブランチは(no branch, rebasing master)と表示され、masterではないことが分かります。
全体のマージではなく、自分がcloneの後に行ったコミットの中で、最初にリモートのmasterと衝突してしまったEのやり直しが求められている。
この時参照しているのは、リモートのmasterブランチでもローカルのmasterブランチでもなく、rebaseの衝突修正用に一時的に生まれたブランチ(のようなもの)である。
rebaseではこの2点を忘れないようにくれぐれも注意して下さい。
さて、作業者2は手作業で衝突箇所を修正しますが、ここでは作業者1の成果に作業者2のEでの作業「ダブルクオートはシングルクオートにする」を反映することにしましょう。
しかし、衝突を解消してもgit commit -aなどとして普通にコミットしてはいけません。 git commitは、実はrebaseの衝突の解消の作業の中で実行される事が想定されていない操作です。 この時点で実行してしまうと、リポジトリの状態がGitの想定する状態からずれてしまい、以後の衝突の解消の操作がすべて破綻してしまいます。
では、一体どうすればよいのか。 答えはgit pull --rebaseで衝突が発生した時のメッセージの最後に書かれています。
... When you have resolved this problem, run "git rebase --continue". If you prefer to skip this patch, run "git rebase --skip" instead. To check out the original branch and stop rebasing, run "git rebase --abort".
問題を解消したら、git rebase --continueを実行する。
このコミットを反映せず飛ばしたい時は、git rebase --skipを実行する。
rebaseを取りやめて元の状態に戻したい時は、git rebase --abortを実行する。
つまり、先程衝突を解消したので、ここではgit rebase --continueを実行すればよいということになります。
ただし、ここでgit rebase --continueを実行してもまたエラーメッセージが表示されてしまいます。
$ git rebase --continue sample.js: needs merge You must edit all merge conflicts and then mark them as resolved using git add
メッセージ内に説明があるのですが、衝突を解消した後は「この衝突は解消済み」という事をGitに伝えなくてはなりません。 それには、git add (ファイルのパス)で変更をステージングに移すという操作を行います。 ステージングに移された変更は解消済みの衝突と見なされるため、これでやっとgit rebase --continueできるようになります。
git add sample.jsで変更をステージングに移してgit rebase --continueを実行すると、Gitは自動的に、「Dから作業した場合のE」すなわち「E'」として現在ステージング済みの状態をコミットします。
A-+-B-C-D-+ [remote master ("NG-NG-NG")] ==|=======|========================================= | | | +-E'('NG-NG-NG') [作業者2 local temporary] | +-E-F('OK-OK')-G [作業者2 local master]
これで手元のブランチのコミットEはリベースが完了しました。
するとGitは続けて、E'の後にFを行ったかのように歴史を改変しようとします。 しかしE'とFも矛盾するため、また衝突が起こります。
A-+-B-C-D("NG-NG-NG")-+ [remote master] ==|===================|=========================================== | | | +-E'('NG-NG-NG')-+-[作業者2 local temporary] | |  >>>>>> F | | +-E-F('OK-OK')-----------------------+-G [作業者2 local master]
今度の矛盾はどう解消しましょう。ここでは2通りのやり方があります。
先程のE'と同じように、Fの意図を汲んでE'に反映する新しいコミット「F'」を作る。
最終的に持っていきたい状態を考えるとFの作業内容は必要ないため、Fは破棄する。
F'を作る場合は、先程同様にファイルを編集してgit addしてgit rebase --continueします。
Fを破棄する場合、衝突を解消するための手作業での修正は行わずに、git rebase --skipを実行します。 ここではFを破棄したとしましょう。
A-+-B-C-D("NG-NG-NG")-+ [remote master] ==|===================|========================================= | | | +-E'('NG-NG-NG') [作業者2 local temporary] | +-E-F-G[作業者2 local master ('OK-OK-OK')]
すると、「F'を作成するための変更」がキャンセルされます。 そして、今度はGitはE'の後にFを行ったのではなく、E'の後にGを行ったかのように歴史を改変しようとします。
A-+-B-C-D("NG-NG-NG")-+ [remote master] ==|===================|=========================================== | | | +-E'('NG-NG-NG')-+-[作業者2 local temporary] | |  >>>>>> G | | +-E-F-G('OK-OK-OK')------------------+ [作業者2 local master]
はい、また衝突しました。ここでも先程と同様に2通りの選択肢があります。
先程のE'と同じように、Gの意図を汲んでE'に反映する新しいコミット「G'」を作る。
最終的に持っていきたい状態を考えるとGの作業内容は必要ないため、Gは破棄する。
どちらにしてもいいのですが、今度もまたGを破棄してgit rebase --skipすることにしましょう。 すると、「G'を作成するための変更」がキャンセルされます。
A-+-B-C-D("NG-NG-NG")-+ [remote master] ==|===================|========================================= | | | +-E'('NG-NG-NG') [作業者2 local temporary] | +-E-F-G('OK-OK-OK') [作業者2 local master]
そして、すべての手元の変更をrebaseし終えたということで、Gitは先程までrebase作業用の一時的なブランチ(のようなもの)だった物を新しいlocalのmasterブランチに昇格させ、今までmasterだったブランチは破棄します(リポジトリ内にコミットごとのデータはまだ残っていますが、どこからも参照されなくなるので見えなくなります)。
A-B-C-D("NG-NG-NG")-+ [remote master] ====================|========================================= | +-E'('NG-NG-NG') [作業者2 local master]
この状態からであれば、作業者2は変更をpushできます。 ということでgit pushしてしまいましょう。
A-B-C-D-E'('NG-NG-NG') [remote master] ====================================== (作業者1、2の手元に固有の変更は無い)
以上が、rebaseで実際に起こっている事の詳細と、rebaseでの衝突の正しい解消手順です。
「今自分がどのブランチで作業しているのか」を一目で分かるようにする
rebaseが分かりにくい理由の1つには、自分が作業していたはずのブランチから何の警告もなく勝手に別の一時的なブランチ(のようなもの)に移ってしまい、自分で明示的にrebase終了の操作をするまではそのブランチにいるままになってしまうから、という理由があるように筆者には思えます。
この対策としてお薦めなのが、シェルのプロンプト上に現在のブランチ名を自動的に表示するように設定しておくという方法です。 過去に紹介したおすすめzsh設定にもそのような設定が含まれていますが、ここではmacOS(OS X)や多くのLinuxディストリビューションで端末エミュレータを起動した時の既定のシェルになっているBash用の設定をご紹介します。
BashでGitリポジトリの状態をプロンプトに表示するツールとしては、bash-git-promptが有名なようです。 以下のようにするとインストールできます。
cd ~/ git clone https://github.com/magicmonty/bash-git-prompt.git .bash-git-prompt --depth=1 echo 'GIT_PROMPT_ONLY_IN_REPO=1' >> ~/.bashrc echo 'source ~/.bash-git-prompt/gitprompt.sh' >> ~/.bashrc
後半2つのコマンド列は、Bashを起動した時点で最初からこのツールが読み込まれた状態にしておくための設定です。
以上の設定を施した状態で新しい端末を起動し、任意のGitリポジトリの中にcdすると、プロンプトがGitリポジトリの状態を示す内容に自動的に切り替わります。 プロンプトの表示が大きく変わりますので、慣れない場合はテーマを切り替えたりカスタマイズしたりしてみるとよいでしょう。 筆者の場合はUbuntuの通常のプロンプトからの変化を最小限に抑えたかったため、
override_git_prompt_colors() { GIT_PROMPT_THEME_NAME="Single_line_Ubuntu_default" GIT_PROMPT_START_USER="\u@\h:${PathShort}" GIT_PROMPT_END_USER="${ResetColor}$ " GIT_PROMPT_END_ROOT="${BoldRed}# " } reload_git_prompt_colors "Single_line_Ubuntu_default"
以上の内容のテーマ定義ファイルを~/.bash-git-prompt/themes/Single_line_Ubuntu_default.bgpthemeの位置に置き、~/.bashrcの末尾にGIT_PROMPT_THEME=Single_line_Ubuntu_defaultという行を追加して、この自作テーマを使うように設定しています。
通常のpullしか知らない状態でgit pull --rebaseしようとして、衝突が発生した時にお手上げになってしまう状況について、筆者の事例を元に誤解のポイントとrebase作業の正しい流れを解説しました。 また、rebaseで失敗の元になりやすいと思われる「今いるブランチがぱっと見で分からない」問題について、Bashのプロンプトにブランチ名を表示させるための方法もご紹介しました。
rebaseは、衝突が発生しない状況では変更履歴を簡単に綺麗に保つ事ができてとても便利です。 しかし一度衝突が発生すると、途端に衝突と解消の連続の地獄に陥ります。 現在自分がどの状態にあってどのコミットまで衝突を解消したのか、この衝突の解消ではどこまでやって良くてどこから先はやらない方がよいのか、常に正確に把握しながら作業しないと、どこを目指して衝突を解消すればよいのかをすぐに見失ってしまいます。
この問題について、通常のpullでの単一のマージコミットのような、シンプルで分かりやすい解決策は残念ながらありません。 いわゆる「運用でカバー」な方法としては以下のような対策が考えられます。
複数人で並行して作業をしていて衝突が頻発するような状況では、rebaseを使わない。 (マージコミットの発生については、避けられない事として諦める。)
例えば、Git FlowやGitHub Flowのように、masterには直接コミットせず、必ずトピックブランチを作成してそれをmasterにマージする形でのみ変更を反映する、という運用を徹底する。このような状況であればrebaseはそもそも行わなくて済む。
自分が作業を開始する前に必ずgit pull --rebaseを実行し、また、1つ何かコミットする度にもgit pull --rebaseして、rebaseでの衝突の解消を「リモートブランチの最新の状態と、手元の先程の作業1つとの衝突の解消」に留めるようにする。 (手元のブランチの「その次の作業」との整合性まで考慮した衝突の解消は困難極まりないので、しないで済むようにする。)
以上のような対策を取りつつ、皆さんも是非rebaseを正しく活用していって下さい。
現在クリアコードではソフトウェア開発者を募集しています! フリーソフトウェア開発での開発の仕方をベースに、成果や知識を公開し共有しながら業務に携われます。
Speee Cafe Meetup #02 :OSS開発者を増やしたい! #speee_lounge
書き捨てのRubyスクリプトをgemにするときの育て方の一例
Apache Arrow
milter manager
ククログの記事はCC BY-SA 4.0とGFDLのデュアルライセンスで自由に利用できます。
クリアコードはプログラミングが楽しいソフトウェア開発者を1名募集しています。
クリアコードは「クリアコードをいい感じにする人」を1名募集しています。
クリアコードはフリーソフトウェア開発で培った技術力を提供しています。特にMozilla製品(Mozilla FirefoxとMozilla Thunderbird)とRubyとGroonga(全文検索)に関連した開発を得意としています。