TIM Labs

タグ「git」が付けられているもの

問題

git rebase は便利ですが、時々コンフリクトが起きて停止します:

$ git rebase master
Applying: Add GeneratedColumn
Applying: Add EnumerateGeneratedColumns
Applying: Use EnumerateGeneratedColumns to CreateQuery
Using index info to reconstruct a base tree...
M       AAA/BBB/Summary.cs
Falling back to patching base and 3-way merge...
Auto-merging AAA/BBB/Summary.cs
CONFLICT (content): Merge conflict in AAA/BBB/Summary.cs
Failed to merge in the changes.
Patch failed at 0037 Use EnumerateGeneratedColumns to CreateQuery
The copy of the patch that failed is found in:
   c:/Users/VimWizard/projects/SecretProject/.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".

まず何が影響してるか、 git statusgit diff で確認しますよね。 あれこれ端末内で情報を表示して状況を確認した結果、コンフリクトの解決に取り掛かる訳ですが、 解決するには元々 何のコミットをapplyしようとしてコンフリクトしたのか という情報も重要です。 この情報自体は上記の git rebase からのメッセージに含まれています。 が、しかし、

  • git statusgit diff を何回も実行しているので git rebase のメッセージは流れています。 あのメッセージがどこにあったのかスクロールして探すのは面倒です。
  • 仮に端末内の出力を検索したとしても、 git rebase はコンフリクトしたコミットのコミットメッセージは表示してくれているものの、 コミットIDは表示してくれないので、結局問題のコミットにおける変更点だけを見るには不向きです。

どうにかして一発でapplyし損ねたコミットを表示できないものでしょうか。

他人の開発画面、覗いてみた事ありますか。

覗いてみた事が無い方は、是非見せてもらってください。意外と面白いですよ。 gitのコミットの仕方ひとつとっても、人によって全く違うんだなと思うはずです。

git使いが10人いれば、使い方も10通りなんだなと、他人のコンソール覗きこむたびに感じます。

私の場合、プロジェクトが数ヶ月から半年に一度変わる(または並行して複数のプロジェクトに入る)という事が多く、いろんなgit使いの方に触れる 機会が多いです。

コンソール上でgitを操作する人、SourceTreeやEclipseプラグイン等のGUIツールで操作する人、git rebaseを多様する人、全くしない人、コミットグラフが綺麗な人、汚い人(そもそもあんまりこだわりを持っていない人?)、コミットメッセージを日本語で書く人、英語で書く人。。。

私は、「git pull」というコマンドを使う事はほぼ無いのですが、たぶんそういう情報は私の開発画面じーっと覗き込んでないと知りえない事だと思います。 gitを長い間使っている方は、自分流のgitの使い方を持っているかと思います。

今回は私のローカルでのgitの使い方を晒してみます。

ちょっと記事が長くなってしまいました。以下、今回の要点を最初に書いておきます。

  • コマンドは短く
  • 必要な情報は常に表示する
  • とにかくcommit
  • コミットコメントは一行で、かつ自分が分かりやすいコメントを
  • hookを活用する
  • 余計なものは無視する、または消す

疑問

Gitを使っていると時々コマンドを入力し損ねたまま実行してしまうことがあります。 この時、Gitは親切にも近い名前のコマンドを列挙してくれます:

$ git committ
git: 'committ' is not a git command. See 'git --help'.

Did you mean this?
        commit

$ git pick
git: 'pick' is not a git command. See 'git --help'.

Did you mean one of these?
        fsck
        repack

これは一体どのように実装されているのでしょうか。

問題

git checkout another_branch にすら時間がかかる......というのは大規模なプロジェクトだとありがちです。 例えば

  1. 自分が長期休暇を取ったら
  2. 自分の居ない間に盛大なリリースが行われ
  3. master にコミットがとんでもなく積み重なっている

という状況を考えてみましょう。 普通なら

git checkout master
git pull

だけで自分が居ない間に行われた修正が取り込めますが、

  • checkout 前のブランチと master が結構背離してて作業ツリーの更新に時間がかかる
  • しかもその時間をかけて行われた作業ツリーの更新が pull ですぐに上書きされる

という精神衛生上大変よろしくない事態が発生します。 どうにかしてブランチを checkout せずに共有リポジトリの取り込むことができないものでしょうか。

Git を使用していると特定のコミットを指定したいことが多々あります。 例えば:

等々です。 この時、一番確実なのは git log でコミットのIDを調べて、そのIDで指定することです。 ただ、これは確実ではあるものの、40文字もの英数字の羅列をコピーするのは面倒です。 どうにかして簡単に指定できないものでしょうか。

以前 gitで一度行った変更をなかったことにする方法4つ を紹介しましたが、 日常的に git を使用していると他にも様々な 「なかったことにしたい」「元に戻したい」 という状況に遭遇します。 そのひとつひとつについて対処方法を紹介していきます。

問題

バグフィックスなりリファクタリングなり何かをするため、 コード中の複数のブロックのインデントを変更するということは少なくありません。

例えば以下のようなコードがあるとしましょう:

private void UpdateData()
{
    var db = GetDatabaseConnection();

    data1.UpdateSomething();
    data2.UpdateSomething();
    data3.UpdateSomething();

    data1.Save(db);
    data2.Save(db);
    data3.Save(db);
}

このコードは一連のデータを更新してデータベースに保存しています。 しかしこの手の更新はデータの一貫性を保証するためにトランザクション内で実行されなければなりません。 という訳でこのコードは以下のように修正されるべきです:

private void UpdateData()
{
    var db = GetDatabaseConnection();

    db.BeginTransaction();
    try
    {
        data1.UpdateSomething();
        data2.UpdateSomething();
        data3.UpdateSomething();

        data1.Save(db);
        data2.Save(db);
        data3.Save(db);

        db.Transaction.Commit();
    }
    catch
    {
        db.Transaction.Rollback();
    }
    finally
    {
        db.EndTransaction();
    }
}

さて、コミットする前に git diff を実行して変更点をレビューすることにしましょう。 ところが残念なことに以下のような結果が得られます:

diff --git a/t.cs b/t.cs
index e4e18dd..53a3b56 100644
--- a/t.cs
+++ b/t.cs
@@ -2,11 +2,26 @@ private void UpdateData()
 {
     var db = GetDatabaseConnection();

-    data1.UpdateSomething();
-    data2.UpdateSomething();
-    data3.UpdateSomething();
+    db.BeginTransaction();
+    try
+    {
+        data1.UpdateSomething();
+        data2.UpdateSomething();
+        data3.UpdateSomething();

-    data1.Save(db);
-    data2.Save(db);
-    data3.Save(db);
+        data1.Save(db);
+        data2.Save(db);
+        data3.Save(db);
+
+        db.Transaction.Commit();
+    }
+    catch
+    {
+        db.Transaction.Rollback();
+        throw;
+    }
+    finally
+    {
+        db.EndTransaction();
+    }
 }

なんということでしょう。 git diff は正しく動作しているものの、 これでは本質的な変更点が何なのか全く分かりません! ここでは一連の更新処理をトランザクションで括り、 さらに元々あった更新処理のインデントを調整しました。 本質的な変更的は前者であって後者ではありません。 しかしこの出力からは本質的な変更点を識別することは困難です。

問題

八百万あるVimのコマンドで特に有用なもののひとつとして gf があります。 このコマンドはカーソル下にあるファイル名らしき文字列を探し、 該当するファイルがあればそれを開くというものです。

gf はカーソル下にあるファイル名らしき文字列をそのまま使うだけでなく、 特定のディレクトリ下にあるかどうか検索(例えばC言語でなら /usr/include./include を検索)したり、 特定の拡張子を付加して検索(例えばJavaなら SomeClass のファイル名は SomeClass.java なので、 .java を付加して検索)することができ、 そこそこ賢く動いてくれます。

さて、日常的に git を使っている身としては 日常的に git diff の出力を眺める機会も多いです。 「git diff の出力を眺めて変更のあったファイルを開く」ということも頻繁に行います。 これには gf を使えばよいと思うことでしょう。

ところが深遠なる事情により git diff の出力フォーマットでは変更のあったパスにプレフィックスとして a/b/ が付加されます。 例えば master/Foo.cs の変更を含む git diff の出力には a/master/Foo.csb/master/Foo.cs といった名前が含まれます。 a/master/Foo.csb/master/Foo.cs の上で gf を実行しても該当するファイルは存在しないためエラーとなってしまいます。 これは不便です。

一応、ビジュアルモードで a/b/ を含まないよう範囲選択をして gf を実行すれば、 選択範囲のファイル名に該当するファイルを開くことができます。 しかしいちいち範囲選択をするのも面倒な話です。

どうにかして git diff 固有の出力フォーマットでも gf で適切なファイルを開きたいところです。 しかしどうすればよいでしょうか。

問題

「トピックブランチをばんばん作ってある程度のところで統合ブランチへマージする」 という開発スタイルを行っていたとしましょう。 例えばバージョン1をベースにガリゴリとトピックブランチを作って統合ブランチへマージし、 一区切りついたところでバージョン2としてリリースするといった具合です。

マージするトピックの数が数個であれば特に問題はないのですが、 10や20になってくると git log を眺めてもどのトピックブランチが取り込まれているのか一目では分からなくなってきます。 例えば git log master..$topic の出力が空であれば $topic が取り込まれていないことは分かりますが、 10や20もトピックブランチがある状態ならばこんな方法で1個づつ調べる気は起きません。 どうにかして統合ブランチへ取り込まれたトピックブランチの一覧を抽出する方法はないでしょうか。

Mercurialは、Merucurial拡張という拡張モジュールを使って、Merucrialの挙動をいろいろ拡張できるようになっています。 デフォルトのままだと使いにくいので、Mercurialを使う上で便利にしてくれる拡張を設定しておきましょう。 デフォルトでバンドルされているMercurial拡張は、Using Mercurial Extensionsにまとめられています。

今回はGit使いがMercurial使いに転職するときに、Gitで実現できたことをMercurialで実現するための、組み込み拡張、および、サードパーティ製の拡張について紹介します。

問題

「トピックブランチをばんばん作ってある程度のところで統合ブランチへマージする」 という開発スタイルを行っていたとしましょう。 トピックブランチでの作業中や統合ブランチへマージする前など、 状況確認のためにトピックブランチに対してどれだけ変更が行われているか調べることはよくあります。

コミットした数が大したことないのであれば単に git log を実行して 最初の数コミット分だけ眺めれば済む話なのですが、 余分なものが表示されると五月蠅いですし、 コミット数が増えてくるとさすがに面倒になってきます。 どうすれば必要な分だけ列挙することができるでしょうか。

テストしたい機能がデプロイされているかどうか確認するためにアプリのリビジョン番号を埋め込というのはよくある話です。

mavenでこの機能を実現するプラグインが存在しないか確認したのですが、とくに存在しなかったようなので、弊社のプロジェクトでは次の様な設定をpom.xmlに設定してリビジョン番号を取得しています。

問題

git submodule で他のリポジトリ(以下「サブモジュール」)の内容を埋め込むことが簡単にできるのですが、 ごく稀に追加したサブモジュールを削除したくなる場合があります。 サブモジュールの追加や更新は git submodule addgit submodule update で簡単にできるものの、 何故か git submodule rm サブコマンドはありません。 どうすれば削除できるのでしょうか。

gitでは様々な方法でコミットログを書き換えることができます。 その一例として一度行った変更をなかったことにする方法を4つ紹介します。

gitでは様々な方法でコミットログを書き換えることができます。 その一例として最後に行ったコミットを修正する方法を紹介します。

問題

先日紹介した gitで複数のコミットを1つにまとめる 例ですが、 実際には最後に行ったコミットだけを修正するケースの方が多いです。

例えば意気揚々と新機能を実装してコミットを行ったとしましょう:

$ git commit -am 'Addd new feature X'

ところが興奮のあまり「d」をタイプしすぎて「Add」が「Addd」になってしまいました。

しかもコミット後の状態を確認してみると:

$ git status
# On branch master
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#       lib/x.c
#       lib/x.h
nothing added to commit but untracked files present (use "git add" to track)

上記の通り新たにバージョン管理下に置くべきファイルを追加し忘れているというありさまです。

この場合、ミスを修正するコミットを行って git rebase -i で1つにまとめてしまってもよいのですが、 さすがにこの程度のことで git rebase -i を使うのは面倒臭いです。 どうすればよいでしょうか。

問題

git では空気を吸うようにブランチを作り空気を吐くようにマージを行います。 gitでマージ作業を中止して元の状態に戻す ではマージに際してよくある問題の対処方法について紹介しました。

運用をきちんとしていればトピックブランチのマージでコンフリクトが発生することは稀です。 しかし稀とはいえコンフリクトが発生するときは発生します。 例えば新機能Xの実装を始めるとしましょう:

$ git checkout -b topic-x master
$ $EDITOR
$ git commit -am 'Fix outdated comments'
$ $EDITOR
$ git commit -am 'Revise existing API'
$ $EDITOR
$ git commit -am 'Implement X'

          o---o---o  <- topic-x
         /
o---o---o  <- master

新機能Xを実装している間に他の人がバグ修正Yを行い、 それが master に取り込まれていたとしましょう:

$ git checkout -b topic-y master
$ $EDITOR
$ git commit -am 'Fix apple'
$ $EDITOR
$ git commit -am 'Fix banana'

$ git checkout master
$ git fetch
$ git merge origin/master
$ git merge topic-y
$ git push origin master

          o---*---o  <- topic-x
         /
o---o---o---*---o  <- master, topic-y

さて新機能Xを master にマージしたところ、 偶然にも topic-x と topic-y で変更範囲が重なっていたためコンフリクトが発生しました (グラフの * で示したコミットです)。 コンフリクトの解決は問題なく終わり、マージ作業を終えたとします:

$ git checkout master
$ git fetch
$ git merge origin/master
$ git merge topic-x  # (c1)
$ $EDITOR
$ git commit -a  # (r1)

          o---*---o  <- topic-x
         /         \
o---o---o---*---o---o  <- master
                ^
             topic-y

しかしマージ作業を終えたところで実は topic-x に致命的なミスがあったことに気付きました。 まだ git push する前だったので、 一度マージ作業を巻き戻してから topic-x を修正して再度マージすることにします:

$ git checkout topic-x
$ $EDITOR
$ git commit -am 'Fix $&*!)($&'

$ git checkout master
$ git fetch
$ git merge origin/master
$ git merge topic-x  # (c2)

          o---*---o---o  <- topic-x
         /             \
o---o---o---*---o-------o  <- master
                ^
             topic-y

これで手順としては何も間違っていないのですが、 当然ながら(c2)でも(c1)と同様のコンフリクトが発生します。 一度(r1)で解決したにもかかわらず、 同様の作業を繰り返すことになります。 いくらなんでもこれは面倒です。 どうにかして自動化できないでしょうか。

問題

git では空気を吸うようにブランチを作り空気を吐くようにマージを行います。

例えば新機能Xを実装する場合、

  1. X用のトピックブランチを作成し、
  2. 実装を進めて、
  3. 完成したら統合ブランチへマージする

というのが普通です。 具体的にコマンド例を挙げると以下のようなものになります:

$ git checkout -b topic-x master
Switched to a new branch 'topic-x'

$ $EDITOR

$ git add foo.x bar.x baz.x

$ git commit -m 'Implement X'
[topic-x 0000001] Implement X
 3 files changed, 8 insertions(+), 5 deletions(-)

$ $EDITOR

$ git add qux.x

$ git commit -m 'Fix a typo'
[topic-x 0000002] Fix a typo
 1 files changed, 7 insertions(+), 5 deletions(-)

$ git checkout master
Switched to branch 'master'

$ git merge topic-x
Updating 0000000..0000002
Fast-forward
 foo.x |    5 ++++-
 bar.x |    4 ++--
 baz.x |    4 ++--
 qux.x |   12 +++++++-----
 3 files changed, 15 insertions(+), 10 deletions(-)

さてマージが完了したは良いものの、 よくよくログを見返してみると topic-x の内容に誤りがあり、 実はまだ完成とは言えない状態だということに気付いたとしましょう。 topic-x で修正を積み重ねてから再度masterへマージしてもよいのですが、 この作業はまだ外部へ公開(push)していないので、 できれば外面を綺麗に保つために一旦マージをなかったことにしてから 修正を行いたいものです。 しかしどうすればよいでしょうか。

問題

gitで誤ったブランチに対して行った変更を正しいブランチへ移す(cherry-pick編) では一度コミットしてしまった変更を別のブランチへ付け変える方法について紹介しました。

この例では誤ったブランチに対して一度コミットしてしまいましたが、 コミットする前に誤ったブランチで作業していたことに気付くこともよくあります。 例えば以下のような状態です:

$ git branch
  master
* topic-y

$ git branch topic-x master

$ $EDITOR  # git checkout topic-x を忘れて作業してしまった。

$ git status
# On branch topic-y
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#       modified:   application.source
#       modified:   library.source
#
no changes added to commit (use "git add" and/or "git commit -a")

一度コミットしていたのであれば git cherry-pick で対処できますが、 この場合はまだコミットしていません。 どうすればよいでしょうか。

gitでは様々な方法でコミットログを書き換えることができます。 その一例として誤ったブランチに対して行った変更を正しいブランチへ移す方法を紹介します。

問題

これまで「新機能Xを追加する」という設定で以下のトピックについて解説していました:

これにはまず

$ git branch topic-x master
$ git checkout topic-x

としてこの作業用のトピックブランチを作成してそちらで作業を行うのが普通です。

しかし git branch を実行したところで安心してしまい、 git checkout を忘れて全く違うブランチで作業を行ってしまう というミスは時々やってしまいます (git checkout -b という方法もありますがここではそれも忘れていたとしましょう)。 例えば以下のような状況だったとしましょう:

$ git branch
  master
  topic-x
* topic-y

$ git log --oneline --decorate --graph
* c000022 (HEAD, topic-y) Update to use X
* c000012 Refactor - Sort using statements
* b000001 Add a neat feature X into the library
* 0100002 Update to use Y
* 0100001 Add a great feature Y in to the library
* 0000000 (master, topic-x) Initial import

日本語に訳すと以下の通りです:

  1. 新機能Yを追加するためのトピックブランチtopic-yをmasterから作成し、そこで作業を行った。 この時点でmasterは0000000を、topic-yは0100002を指していた。
  2. 新機能Xを追加するためのトピックブランチtopic-xをmasterから作成したが、 ブランチの切り替えを忘れていたため、 topic-xに対して行う変更をtopic-y上で行ってしまった。 結果としてtopic-yは0100002からc000022を指すようになった。

本来ならば以下のような形なっているところです:

$ git branch
  master
* topic-x
  topic-y

$ git log --oneline --decorate --graph master topic-x topic-y
* d000022 (HEAD, topic-x) Update to use X
* d000012 Refactor - Sort using statements
* d000001 Add a neat feature X into the library
| * 0100002 (topic-y) Update to use Y
| * 0100001 Add a great feature Y in to the library
|/
* 0000000 (master) Initial import

まだまだtopic-xの作業内容はどこにも公開されていないので、 いまのうちにコミットログを綺麗な形に書き換えるとしましょう。 しかし具体的にはどうすればよいでしょうか。

gitでは様々な方法でコミットログを書き換えることができます。 その一例として1つのコミットを複数のコミットに分割する方法を紹介します。

問題

先日紹介した gitで複数のコミットを1つにまとめる 例ですが、 そこでは以下のコミットログを:

$ git log -n 4 --oneline --reverse
0000001 Add a neat feature X into the library
0000002 Update to use X
0000003 Fix a typo in X
0000004 Fix a bug in X

以下のような形になるよう書き換えました:

$ git rebase -i HEAD~4
[detached HEAD b000001] Add a neat feature X into the library
 8 files changed, 94 insertions(+), 9 deletions(-)
Successfully rebased and updated refs/heads/topic-x.

$ git log -n 2 --oneline --reverse
b000001 Add a neat feature X into the library
b000002 Update to use X

ライブラリ側の変更点(0000001と0000003と0000004)を1つにまとめて 順序を入れ替えた形になります。

当初のコミットログから比較するとかなり美しくなりました。 しかし改めて git log -p などとしてコミットログを確認していると新たな問題が発覚しました。 b000002は新機能Xを使うようアプリケーション側を変更するコミットだったのですが、 よくよく見返してみると「新機能Xを使うよう更新する」変更とは関係のない 「using等の宣言の羅列をソートする」 といったリファクタリング的な変更がいっしょくたになっていました。 これは美しくありません。

この場合、b000002を「新機能Xを使うよう更新する」と「using等の宣言の羅列をソートする」の2つに分割すれば よりコミットログが美しくなります。 しかし具体的にはどうすればよいでしょうか。

2  

タグ

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。