Subversion ユーザーのための Git: 第 2 回 細かく制御する

ブランチのマージの複雑さを解明する

ソフトウェアのバージョン管理を行う Linux® 開発者にとって、Git は Subversion よりもいくつかの点で優れています。そのため、共同で作業を行う開発者にとって、Git の背景にある基本的な概念は理解する価値があります。今回の記事では、Ted が Git と Subversion の両方におけるブランチとマージについて分析した後、これまでの変更内容を 2 つに分けるための「git bisect」を紹介し、最後にマージの競合を解決する方法を説明します。

Teodor Zlatanov, Programmer, Gold Software Systems

photo- teodor zlatanovTeodor Zlatanov は 1999年にボストン大学 (Boston University) でコンピューター工学の修士号を取得しています。彼は 1992年からプログラマーとして働いており、Perl、Java、C、C++ を使ってきています。彼が関心を持っている領域は、オープンソースによるテキスト構文解析、データベース・アーキテクチャー、ユーザー・インターフェース、UNIX システム管理などです。



2009年 11月 25日

この記事は 2 回連載の第 2 回目です。まだ「第 1 回」を読んでいない人は、第 1 回を読む必要があります。この記事で使用する Git と SVN (Subversion) の設定は第 1 回と同じあり、また第 1 回を読むことで私のユーモアのセンスにも慣れることができるはずです。

SVN でのブランチとマージ

VCS (バージョン管理システム) の管理者にとって最大の頭痛の種は、何と言ってもブランチとマージです。圧倒的大多数の開発者は、すべての変更をトランクにコミットすることを好みます。ブランチとマージが登場するやいなや、開発者は不平を言い始め、VCS 管理者はそれに対応しなければなりません。

開発者の立場で言うなら、ブランチとマージは恐ろしい操作です。結果は必ずしも明白ではなく、またマージを行うと他の人達の作業を取り消してしまい、問題を引き起こす可能性があります。

SVN はトランクを適切に管理してくれるため、多くの開発者はブランチを気にとめません。バージョン 1.5 よりも前の SVN クライアントはマージの追跡に関して少しばかり原始的でした。そのため、古い SVN クライアントに慣れている読者は、SVN の svn:mergeinfo プロパティーについて知らないかもしれません。

また、svnmerge.py というツールもあります (「参考文献」にリンクがあります)。svnmerge.py は svn:mergeinfo のサポートなしにマージを追跡することができ、従って古い SVN クライアントでも動作します。

SVN でのマージのサポートは複雑でバリエーションが多いため、ここでは特定の例を挙げる代わりに、Git でのブランチのマージのみを説明します。関心のある読者は「参考文献」セクションに挙げた SVN のマニュアルを読んでください。


Git でのブランチとマージ

もし、CVS (Concurrent Versions System) がブランチとマージに関して村の馬鹿者であるなら、SVN は町の牧師であり、Git は市長です。Git は、ブランチとマージを容易に行えるよう、実用的に設計されています。Git のこの機能のデモは印象的なものですが、もちろんこの機能は日々の作業でも重宝します。

一例を挙げると、Git には複数のマージ戦略があり、その中にはタコ足 (octopus) 戦略と呼ばれるものもあります。タコ足戦略を使用すると、複数のブランチを一度にマージすることができます。まさにタコ足戦略です。この種のマージを CVS や SVN で行おうとすると、どれほど大変なことになるかを想像してみてください。Git はまた、リベース (rebase) という異なる種類のマージもサポートしています。ここではリベースについて詳しく説明しませんが、リベースはリポジトリーの履歴を単純化する上で極めて有効です。そのため、リベースは調べてみる価値があります。

これから説明するマージの例に進む前に、第 1 回でのブランチの設定を理解しておく必要があります。第 1 回では、HEAD (現在作業中のブランチ、この場合は master) ブランチと empty-gdbinit ブランチがありました。ここではまず empty-gdbinitHEAD にマージし、マージが完了したら HEAD に変更を加え、今度は逆に HEADempty-gdbinit にマージしてみましょう。

リスト 1. Git を使ってブランチから HEAD に変更をマージする
# start clean
% git clone git@github.com:tzz/datatest.git
# ...clone output...
# what branches are available?
% git branch -a
#* master
#  origin/HEAD
#  origin/empty-gdbinit
#  origin/master
# do the merge
% git merge origin/empty-gdbinit
#Updating 6750342..5512d0a
#Fast forward
# gdbinit | 1005 ---------------------------------------------------------------
# 1 files changed, 0 insertions(+), 1005 deletions(-)
# now push the merge to the server
% git push
#Total 0 (delta 0), reused 0 (delta 0)
#To git@github.com:tzz/datatest.git
#   6750342..5512d0a  master -> master

masterHEAD があり、empty-gdbinit ブランチとのマージの後、master ブランチがリモート・サーバーにプッシュされて origin/master と同期される、ということを理解していれば、リスト 1 は難しくありません。つまりリモート・ブランチからローカルにマージし、その結果を別のリモート・ブランチにプッシュしています。

ここで重要なことは、どのブランチが正式なものなのかを Git が気にしないことです。ローカル・ブランチから別のローカル・ブランチにマージすることも、リモート・ブランチにマージすることもできます。Git サーバーはリモート操作のみに関係します。対照的に、SVN は必ず SVN サーバーを必要とします。SVN ではサーバー上のリポジトリーのみが正式なバージョンだからです。

もちろん、Git は分散型の VCS なので、これは驚くには当たりません。Git は中央の正式なバージョンがなくても動作するように設計されています。とは言え、CVS や SVN に慣れた開発者には自由さが少し気になるかもしれません。

Git と CVS や SVN との大きな違いを理解したところで、今度は別のローカル・ブランチを作りましょう。

リスト 2. マシン A にリリース・ブランチを作成し、そのブランチに切り換える
# create and switch to the stable branch
% git checkout -b release-stable
#Switched to a new branch "release-stable"
% git branch
#  master
#* release-stable
# push the new branch to the origin
% git push --all
#Total 0 (delta 0), reused 0 (delta 0)
#To git@github.com:tzz/datatest.git
# * [new branch]      release-stable -> release-stable

今度は、別のマシンで、master ブランチから gdbinit ファイルを削除します。もちろん、別のマシンである必要はなく、単純に別のディレクトリーでも構いませんが、ここでは第 1 回の Ubuntu での「The Other Ted (もう 1 人の Ted)」の ID をマシン B に再利用します。

リスト 3. マシン B の master ブランチから gdbinit を削除する
# start clean
% git clone git@github.com:tzz/datatest.git
# ...clone output...
% git rm gdbinit
# rm 'gdbinit'
# hey, what branch am I in?
% git branch
#* master
# all right, commit my changes
% git commit -m "removed gdbinit"
#Created commit 259e0fd: removed gdbinit
# 1 files changed, 0 insertions(+), 1 deletions(-)
# delete mode 100644 gdbinit
# and now push the change to the remote branch
% git push
#updating 'refs/heads/master'
#  from 5512d0a4327416c499dcb5f72c3f4f6a257d209f
#  to   259e0fda9a8e9f3b0a4b3019781b99a914891150
#Generating pack...
#Done counting 3 objects.
#Result has 2 objects.
#Deltifying 2 objects...
# 100% (2/2) done
#Writing 2 objects...
# 100% (2/2) done
#Total 2 (delta 1), reused 0 (delta 0)

ここには何もおかしなことはありません (ただし「deltifying (差分化)」は別です。deltifying と聞くと、スポーツ・ジムで体を鍛えるために行うエクササイズのようにも聞こえ、また川が河口近くに来ると三角州を形成することのようにも聞こえるかもしれません)。しかしマシン A の release-stable ブランチでは何が起きるのでしょう。

リスト 4. master ブランチからの gdbinit の削除をマシン A の release-stable ブランチにマージする
# remember, we're in the release-stable branch
% git branch
#  master
#* release-stable
# what's different vs. the master?
% git diff origin/master
#diff --git a/gdbinit b/gdbinit
#new file mode 100644
#index 0000000..8b13789
#--- /dev/null
#+++ b/gdbinit
#@@ -0,0 +1 @@
#+
# pull in the changes (removal of gdbinit)
% git pull origin master
#From git@github.com:tzz/datatest
# * branch            master     -> FETCH_HEAD
#Updating 5512d0a..259e0fd
#Fast forward
# gdbinit |    1 -
# 1 files changed, 0 insertions(+), 1 deletions(-)
# delete mode 100644 gdbinit
# push the changes to the remote server (updating the remote release-stable branch)
% git push
#Total 0 (delta 0), reused 0 (delta 0)
#To git@github.com:tzz/datatest.git
#   5512d0a..259e0fd  release-stable -> release-stable

第 1 回で触れた mentat インターフェースが再び diff の中に登場しています。/dev/null が何も含まない特別なファイルであることはご存知のはずですが、それゆえにリモートの master ブランチには何もありません。その一方でローカルの release-stable ブランチには gdbinit ファイルがあります。これは大部分のユーザーにとって、必ずしも明白なことではありません。

こうした楽しいことの後、pull によってローカル・ブランチを origin/master とマージし、push によってその変更を origin/release-stable に反映しています。そしていつものように、Git 開発者のお気に入りの言葉である delta が使われています。delta を使わずに作業が終わることはありません。


これまでの変更内容を 2 つに分ける

git bisect コマンドは非常に複雑なため、この記事では詳細については説明しませんが、素晴らしいツールなので、ここで簡単に触れておくことにします。これまでの変更内容を 2 つに分ける (bisect) というのは、実際にはコミット・ログ全体に対して二分探索 (binary search) を行うことです。「二分 (binary)」というのは、検索において検索区間を中央で 2 つに分け、その中央の値に対して求めるセグメントが上にあるか下にあるかをその都度判断することを意味します。

この仕組みは単純です。まず、バージョン A は適切に動作し、バージョン Z は適切に動作しないことを Git に伝えます。すると Git はユーザーに対して (または自動スクリプトに対して)、A と Z の間にあるバージョン、例えば Q は適切に動作するのかどうかを尋ねます。Q が適切に動作しない場合には、問題のあるコミットは A と Q の間にあります。そうでない場合には、問題のあるコミットは Q と Z の間にあります。問題のあるコミットが見つかるまで、このプロセスが繰り返されます。

この二分動作をテスト・スクリプトによって自動化できると、特に便利です。そうすれば、バージョン Z に対するテストを作成し、それを逆方向に使用することで、ある機能がいつ破綻したかを見つけることができます。これは多くの開発者が自動リグレッション・テストと呼ぶものです。こうしたテストによって確実に時間を節約することができます。


競合を解決する

どのような VCS でもマージの競合は避けられませんが、Git のように分散型の VCS では特に顕著にマージの競合が起こります。2 人のユーザーが、競合する形で同じブランチの中で 1 つのファイルを変更したらどうなるのでしょう。下記の 2 つの例はどちらも、この記事でこれまでに使用してきた datatest リポジトリーの master ブランチの中で行われています。

まず、マシン B の encode.pl を変更します。

リスト 5. マシン B での (実現されない) 変更
# we're at time T1
# change the contents
% echo "# this script doesn't work" > encode.pl
% git commit -a -m 'does not work'
#Created commit e61713b: does not work
# 1 files changed, 1 insertions(+), 1 deletions(-)
# we're at time T2 now, what's our status?
% git status
# On branch master
#nothing to commit (working directory clean)

今度は、マシン B での変更を意識せずにマシン A の encode.pl を変更し、push を実行します。

リスト 6. マシン A での (実現される) 変更
# we're at time T2
# change the contents
% echo "this script does work" > encode.pl
% git commit -a -m 'does not work'
#Created commit e61713b: does not work
# 1 files changed, 1 insertions(+), 1 deletions(-)
# we're at time T3 now, what's our status?
% git status
# On branch master
# Your branch is ahead of 'origin/master' by 1 commit.
#
#nothing to commit (working directory clean)
% git push
#Counting objects: 5, done.
#Delta compression using 2 threads.
#Compressing objects: 100% (2/2), done.
#Writing objects: 100% (3/3), 298 bytes, done.
#Total 3 (delta 0), reused 0 (delta 0)
#To git@github.com:tzz/datatest.git
#   259e0fd..f949703  master -> master

今度はマシン B で git pull を実行すると、結果があまりよくないことがわかります。

リスト 7. マシン B でのよくない結果
% git pull
#remote: Counting objects: 5, done.
#Compressing objects: 100% (2/2), done.)   
#remote: Total 3 (delta 0), reused 0 (delta 0)
#Unpacking 3 objects...
# 100% (3/3) done
#* refs/remotes/origin/master: fast forward to branch 'master' 
#   of git@github.com:tzz/datatest
#  old..new: 259e0fd..f949703
#Auto-merged encode.pl
#CONFLICT (content): Merge conflict in encode.pl
#Automatic merge failed; fix conflicts and then commit the result.
# the next command is optional
% echo uh-oh
#uh-oh
# you can also use "git diff" to see the conflicts
% cat encode.pl
#<<<<<<< HEAD:encode.pl
## this script doesn't work
#=======
#this script works
#>>>>>>> f9497037ce14f87ff984c1391b6811507a4dd86c:encode.pl

この状況は SVN でもとてもよくある状況です。誰か他の人が行った変更が、皆さんのバージョンのファイルと一致しません。そこで、単純にファイルを変更してコミットします。

リスト 8. マシン B で修正し、コミットする
# fix encode.pl before this to contain only "# this script doesn't work"...
% echo "# this script doesn't work" > encode.pl
# commit, conflict resolved 
% git commit -a -m ''
#Created commit 05ecdf1: Merge branch 'master' of git@github.com:tzz/datatest
% git push
#updating 'refs/heads/master'
#  from f9497037ce14f87ff984c1391b6811507a4dd86c
#  to   05ecdf164f17cd416f356385ce8f5c491b40bf01
#updating 'refs/remotes/origin/HEAD'
#  from 5512d0a4327416c499dcb5f72c3f4f6a257d209f
#  to   f9497037ce14f87ff984c1391b6811507a4dd86c
#updating 'refs/remotes/origin/master'
#  from 5512d0a4327416c499dcb5f72c3f4f6a257d209f
#  to   f9497037ce14f87ff984c1391b6811507a4dd86c
#Generating pack...
#Done counting 8 objects.
#Result has 4 objects.
#Deltifying 4 objects...
# 100% (4/4) done
#Writing 4 objects...
# 100% (4/4) done
#Total 4 (delta 0), reused 0 (delta 0)

これは簡単だったと思いませんか。では、マシン A が次回更新されたときマシン A に何が起こるのかを見てみましょう。

リスト 9. マシン B で修正し、コミットする
% git pull
#remote: Counting objects: 8, done.
#remote: Compressing objects: 100% (3/3), done.
#remote: Total 4 (delta 0), reused 0 (delta 0)
#Unpacking objects: 100% (4/4), done.
#From git@github.com:tzz/datatest
#   f949703..05ecdf1  master     -> origin/master
#Updating f949703..05ecdf1
#Fast forward
# encode.pl |    2 +-
# 1 files changed, 1 insertions(+), 1 deletions(-)
% cat encode.pl
## this script doesn't work

Fast forward は、ローカル・ブランチが自動的にリモート・ブランチに追いついたということです。これはローカル・ブランチがリモート・ブランチにとって新しいものを何も含んでいないからです。言い換えると、Fast forward はマージが必要なかったことを意味しており、どのローカル・ファイルもリモート・ブランチによる最新のプッシュよりも新しくはないということです。

最後に、git revertgit reset に触れておく必要があります。この 2 つは Git ツリーに対するコミットやその他の変更を取り消す場合に非常に便利です。ここではこの 2 つについては説明しませんが、これらの使い方を必ず理解しておいてください。


まとめ

この記事では、マージの概念を取りあげ、2 つのマシンでローカル・ブランチとリモート・ブランチを保持する方法と、それらの間での競合を解決する方法について説明しました。また、複雑で、さらには不可解ですらある Git のメッセージにも注目しました。というのも、Git のメッセージは SVN に比べ、はるかに冗長で理解しにくいからです。この事実が Git コマンドの複雑な構文と組み合わさると、大多数の初心者は Git に尻込みしてしまうかもしれません。しかし、いくつかの基本的な概念についての説明を受けると、Git は遥かに容易なものになり、楽しいものにさえなります。

参考文献

学ぶために

製品や技術を入手するために

  • svnmerge.py はマージの追跡を自動化するツールです。このツールを使うと、ブランチの管理者が自分達のブランチからの変更や自分達のブランチへの変更を容易にマージすることができます。また svnmerge.py は、どの変更が既にマージされているかを自動的に記録します。
  • Git の Web サイトには、Git のダウンロードやドキュメント、そしてさまざまなツールがあります。

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux, Open source
ArticleID=458787
ArticleTitle=Subversion ユーザーのための Git: 第 2 回 細かく制御する
publish-date=11252009