株式会社クリアコード > ククログ

ククログ

タグ:

複数の作業環境から変更が同一ブランチにpushされるGitリポジトリについて、リモートにある内容を確実に手元に反映する

この記事では、Gitの「普通のpull」と「rebase」について、「普通のpullではなくrebaseした方が良いと言われるけれども、実際にrebaseを使ってみたら、衝突が発生した時に色々よくわからない事が起こって辛い思いをした」という人を対象として、rebaseの正しい使い方を解説します。 GitそのものやGitHubそのものの説明については省略していますのでご注意下さい。 また、GitはLinuxやmacOS(OS X)の端末エミュレータ上のシェルでコマンド列を実行して利用する形態を想定しています。

Gitでの「手元のコードをremoteの最新の状態と同期する」場面

  • GitHub上にあるリポジトリを複数人で手元にcloneして、各人が作業の成果をcommitしてpushしている
  • 自宅と会社のPCそれぞれにcloneして、各環境で変更をcommitしてpushしている

このような状況では、pushしようとしてできないという事が度々起こります。

  • 自分が作業をしている間に他の誰かが変更をpushしたせいでremoteのmasterの内容が変わってしまっているにも関わらず、そのまま自分の変更をpushしようとした
  • 自宅PCで作業した内容をpushし忘れたまま出勤し、会社のPCで改めて同じような変更をpushした後、帰宅後に自宅PCで作業を再開し、変更をpushしようとした

言うまでもなくこれはpullのし忘れによって起こることで、こういう時は一旦remoteの変更を手元にpullして、それから改めてpushするという操作が必要になります。

このときpullの仕方には、ざっくり言って「普通のpull」「rebase」の2通りがあります。

ブランチの運用スタイルによっては、「普通のpullではなくrebaseするほうが良い」とアドバイスされたり、あるいはルールやマナーとして「rebaseするように」と求められる場合があります。 筆者の印象では、Git FlowやGitHub Flowのようなトピックブランチを使う開発スタイルではなく、masterブランチに関係者それぞれが直接変更をpushするスタイルの時に、rebaseが推奨される印象があります。

普通のpullもrebaseも、どちらも衝突(conflict)がなければ問題ないのですが、衝突が発生してしまった場合、rebaseはその後でとる対応が普通のpullに比べると分かりにくくて面倒です。 このときの対応を誤ると、せっかくの作業の成果を失うことにもなりかねません。

作業の成果が衝突する状況の例

もう少し具体的に、衝突が発生する状況を示しましょう。 共有リポジトリにmasterというブランチがあり、変更は直接このブランチにpushしていく運用だと仮定します。 また、2人の作業者がいて、それぞれの手元で変更を行った結果以下の図のような状態になっているとします。 (図の左が過去、右が未来を指しています。========より上は共有リポジトリのブランチ、下は作業者の手元のリポジトリのブランチです。)

A-+ [remote master]
==|================================
  |
  +-B---C---D [作業者1 local master]
  |
  +-E---F---G [作業者2 local master]

ブランチの内容は、Aの段階では以下のような内容のファイル「sample.js」が1つだけあります。

"OK"

作業者1は、「OKをNGに書き換える」という意図の元で、Aから以下のように作業を行いました。

  1. 「NG」に書き換える。(コミットB)

    "NG"
    
  2. 「NG」を増やす。(コミットC)

    "NG-NG"
    
  3. 「NG」をさらに増やす。(コミットD)

    "NG-NG-NG"
    

一方、作業者2は「ダブルクオートからシングルクオートにする」という意図の元で、Aから以下のように作業を行いました。

  1. ダブルクオートからシングルクオートに書き換える。(コミットE)

    'OK'
    
  2. 「OK」を増やす。(コミットF)

    'OK-OK'
    
  3. 「OK」をさらに増やす。(コミットG)

    'OK-OK-OK'
    

はい、いかにも衝突しそうですね。

pullで変更が衝突した状況からの回復

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]
                            <<<<<<< HEAD
                            'OK-OK-OK'
                            =======
                            "NG-NG-NG"
                            >>>>>>> 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'が最新の状態だったはずなのに、ずいぶん前の状態との差分が出ています。

<<<<<<< xxxxxxxxxxxx
"NG-NG-NG"
=======
'OK'
>>>>>>> E

まあいいでしょう、先程のマージの時のように2人の作業者の意図を汲んで「OKはNGにする」「ダブルクオートはシングルクオートにする」という変更を統合することにします。

'NG-NG-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 <remote> <branch>

どういうことか?と思って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]
        |                     | <<<<<<< xxxxxxxxxxxx
        |                     | "NG-NG-NG"
        |                     | =======
        |                     | 'OK'
        |                     | >>>>>>> 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]
  |                                    | <<<<<<< xxxxxxxxxxxx
  |                                    | 'NG-NG-NG'
  |                                    | =======
  |                                    | 'OK-OK'
  |                                    | >>>>>>> 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]
  |                                    | <<<<<<< xxxxxxxxxxxx
  |                                    | 'NG-NG-NG'
  |                                    | =======
  |                                    | 'OK-OK-OK'
  |                                    | >>>>>>> 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を正しく活用していって下さい。

タグ: Git
2016-09-02

Gitで不適切なコミットメッセージを削除した公開リポジトリを作る

分散バージョン管理システムのGitには様々なサブコマンドがありますが、その中の1つである git filter-branch を使用すると、過去のコミットを完全に無かった事にしてしまうなどの強力なコミット履歴の編集が可能となります。大きなリポジトリの特定のディレクトリ以下の内容をコミット履歴付きで別の小さなリポジトリとして取り出したりファイルの中に書かれていた生のパスワードを履歴の中から消去したり、というのはよく紹介される例です。

このエントリでは別の例として、コミットメッセージだけを後からまとめて修正する手順をご紹介しましょう。

元々非公開なプロジェクトとして開発を進めていたものを、公開リポジトリに移動したいとなると、やはり機密情報は完全に取り除いておく必要があります。リポジトリに格納されているファイルそのものの内容の編集方法については上記の例で解説されていますが、それ以外の場合として、コミットメッセージに書き込まれている情報を消去したいという事がたまにあります。

実際あったケースで問題になったのは、マージコミットのメッセージでした。

複数人で開発しているプロジェクトでは、自分のコミットをpushしようとするとエラーになってしまったので、一旦pullしてマージしてから再度pushする、という事がよく起こります。この時、gitが生成する既定のコミットメッセージには以下のように、pullしたリポジトリの位置が含まれてしまっています。

commit 213aa813611179b5cc4139e82f672922921e340a
Merge: a57bd32 511348d
Author: SHIMODA Hiroshi <shimoda@clear-code.com>
Date:   Wed Jun 15 15:44:15 2011 +0900

    Merge branch 'master' of github.com:piroor/treestyletab

この例ではgithubのリポジトリの位置が書き込まれていますが、プロジェクトの参加者全員で使用している中央リポジトリが秘密のサーバの上に置かれていた場合や、あるいは参加者の各PCの間で直接IPアドレスを指定するなどしてpullしあっていた場合には、pullしたリポジトリの位置として「192.168.1.2」のような公開される情報には相応しくない内容が出現してしまいます。

git pull --rebase としてpullすればこのような事は起こらないのですが、やってしまった物はもう仕方がありません。このようなコミット履歴がたくさんある時でも、git filter-branch を使用すると、コミットメッセージを任意の内容で書き換えた新しいリポジトリを作成する事ができます。具体的な手順は、以下の通りです。

% git clone git@internal.example.com:private-project.git
% cd private-project
% git filter-branch --msg-filter 'sed -e "s/Merge.*internal.*\$/Merge/"' -f

filter-branch サブコマンドの --msg-filter オプションには、任意のシェルコマンドを文字列として渡す事ができます。このコマンドに対しては、元のコミットメッセージが標準入力として流し込まれ、コマンドの実行結果の標準出力が新しいコミットメッセージとなります。上記の例のように sed などを使って文字列を置換すれば、あまり人目にさらしたくないコミットメッセージを削除するという事もできます(ここでは例として、「Merge」という文字列で始まり行中に「internal」という内容を含む行があれば、それをすべて単に「Merge」という文字列に置き換えています)*1

git log で編集後のコミットログを確認して、期待通りの編集結果が得られていれば、後は公開リポジトリにpushするだけです。

% git push git@public.example.com:public-project.git

以上、コミットメッセージの中に含まれた不適切な内容を書き換えた新しいリポジトリを作る手順をご紹介しました。皆さんもこの手順を使って、社内でしか共有されていなかった便利ツールなどを広く一般に公開してみてはいかがでしょうか。

*1 ただ、この時実際には、古いコミットメッセージを伴う元々のコミットと、新しいコミットメッセージを伴う全く別のコミットの両方が、リポジトリには含まれた状態になっています。元々のコミットはmasterなどのブランチに紐付けられていない迷子のコミットという扱いになるため、git log では通常は表示されませんが、git log --all とオプションを指定したり、リビジョンを直接指定した場合には、古いコミットメッセージが残っている事を確認できます。この時の作業用のリポジトリの内容を新しい空の公開リポジトリにpushする場合は、このような迷子のコミットはpushされないので気にする必要はありませんが、何らかの理由でその作業用のリポジトリを(ファイルコピーなどで)そのまま外部に公開するとなると、迷子のコミットであってもリポジトリの中に残っているのは危険です。このような場合には、git gc --prune=now でゴミを掃除して迷子のコミットを完全に消去し、それらを閲覧できないようにしてしまうとよいでしょう。

つづき: 2013-01-08
タグ: Git
2012-01-05

2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|
タグ:
RubyKaigi 2015 sponsor RubyKaigi 2015 speaker RubyKaigi 2015 committer RubyKaigi 2014 official-sponsor RubyKaigi 2014 speaker RubyKaigi 2014 committer RubyKaigi 2013 OfficialSponsor RubyKaigi 2013 Speaker RubyKaigi 2013 Committer SapporoRubyKaigi 2012 OfficialSponsor SapporoRubyKaigi 2012 Speaker RubyKaigi2010 Sponsor RubyKaigi2010 Speaker RubyKaigi2010 Committer badge_speaker.gif RubyKaigi2010 Sponsor RubyKaigi2010 Speaker RubyKaigi2010 Committer
SapporoRubyKaigi02Sponsor
SapporoRubyKaigi02Speaker
RubyKaigi2009Sponsor
RubyKaigi2009Speaker
RubyKaigi2008Speaker