チャチャチャおもちゃの抹茶っちゃ

ゲームのこととかプログラミングのこととか。気が向いたら書く。ブログタイトルは友人が考えました。

【Git入門】Gitを使い始めたい人へ「その他細かいこと」 ~A Beginner to Beginners~ 【その7】

【Git入門】Gitを使い始めたい人へ「リモートリポジトリを操作する」 ~A Beginner to Beginners~ 【その6】 : 前

目次

ここまででリポジトリの様々な操作をできるようになった。ここではその操作の細かなところを見ていく。

コミットへのタグ付け

作業していくとどんどんコミットが増えていく。その中で、重要なコミット(転換点とか)に目印として「タグ」をつけていくことができる。

$ git show <tag>

とすれば、タグの示すコミットの詳細を見ることができるし、

$ git reset [option] <tagname>

とすれば任意のタグの示すコミットにHEADを移動することができる(ブランチも移動する)。

$ git tag

とすれば現在存在するタグを見ることが可能。自分よりも子にあたるコミットのタグなどもすべて表示される。さらに

$ git tag -l v1.1.*

などとしてあげれば「v1.1」で始まるタグのみをすべてリストアップできる。

現在の最新のコミットにタグをつける

現在いるブランチの最新のコミットにタグをつける。

$ git tag <tagname>

とすれば最新のコミットにタグ付けできる。例えば、git tag v1.0とした場合、
f:id:mattyan1053:20180727175110j:plain
というように一番新しいところに「v1.0」というタグがつけられる。

注釈付きタグ

タグに注釈をつけることができる。タグの説明を付与できるということだ。

$ git tag -a <tagname> -m "message of tag"

とすれば良い。こうすると

$ git show tag

としたときに、タグの注釈まで表示してくれる。-mオプションを省略した場合はエディタが起動するはずなので、そこで注釈を入力する。

過去のコミットにタグ付けする

現在のコミットのみでなく、過去のコミットにタグをつけることも可能だ。

$ git tag <tagname> <commit>

<commit>のところはコミットのハッシュ値やブランチ、HEAD~などコミットを示すものを入れれば良い。また、-a-mと合わせて注釈付きタグを過去のコミットにつけることも可能。このときは

$ git tag -a <tagname> -m "tag message" <commit>

とコミット名を最後につければOK。

タグを削除する

作ったタグを削除したいときは

$ git tag -d <tagname>

でOK。

タグをリモートリポジトリでも共有する

タグは通常ローカルのみのものであり、プッシュしてもリモートリポジトリには反映されない。リモートリポジトリにもタグを共有したいときは

$ git push origin <tagname>

とタグをリモートリポジトリ(ここでは「origin」)にプッシュしてあげる必要がある。

マージに関するあれこれ

Gitの機能の中でも特徴的な機能がマージだ。通常バージョンを分岐させてしまうと合流させるのは容易ではないがGitはそれを簡単にしてくれる。
マージは実は結構奥深いので、ここで少し掘り下げておく。

fast-forward

プルを行ったときや、分岐したブランチから元のブランチにマージしようとしたとき、「fast-forward」と出たことがあるのを見覚えあるだろうか。まずはこの「fast-forward」がどのような状態なのか説明する。次の状態を考えてほしい。
f:id:mattyan1053:20180722224300j:plain
この状態でmasterにbranch-1をマージをしたとき、つまり

$ git checkout master
$ git merge branch-1

としたとき、何が起こるかと言うとmasterとbranch-1をマージした結果できあがるのはbranch-1と同じ状態なので、新しいコミットは作成されず、masterがbranch-1と同じ場所に移動する。つまり枝分かれしない。
f:id:mattyan1053:20180722224706j:plain
このような状態になる。
どういうことか整理すると「branch-1のコミットの履歴」に「masterのコミットの履歴」が完全に含まれている時、branch-1をmasterにマージするとmasterはbranch-1の指し示すコミットまでfast-forwardするということだ。
ちなみにfast-forwardできないとき、つまり
f:id:mattyan1053:20180722225314j:plain
のように、branch-1のコミットの履歴にmasterのコミットの履歴が含まれない時(masterの指しているものが含まれていないため)は、新しくコミットが作成される。
f:id:mattyan1053:20180722225425j:plain
逆に、fast-forwardできるときでも、

$ git merge --no-ff branch-1

とすると、fast-forwardせずに新しくコミットをつくってくれるので、分岐があったことが履歴でわかりやすくなる。
f:id:mattyan1053:20180722225647j:plain

[--squash]オプション

マージには[--squash]オプションというものがある。これは、ワークツリーとインデックスを別ブランチの根本からたどっていき書き換えるが、コミットまではしない状態にもっていく操作である。
f:id:mattyan1053:20180722230745j:plain
この状態ではまだコミットは作られていないので変更飲みされている状態である。

$ git merge --squash branch-1

とするとワークツリーとインデックスが書き換えられ

$ git status
On branch master

Changes to be committed:
    modified : sample.c

のような状態になる。したがって、

$ git merge --squash branch-1
$ git commit -m "squash merged"

とすれば
f:id:mattyan1053:20180722231035j:plain
のような状態にすることができる(履歴がスッキリ)。

プルの仕様

プルでリモートリポジトリからローカルリポジトリに変更点を写すことができるが、実はこれは二段階の工程を踏んでいる。どういうことかというと

$ git pull origin master

$ git fetch origin master
$ git merge origin/master

は同義である。
git fetch ではローカルリポジトリにまだないコミットデータをダウンロードしてくる。今回masterをリモートリポジトリからとってきたので「origin/master」というところに記憶される。
f:id:mattyan1053:20180722233030j:plain
次にgit mergeすると、origin/masterをmasterにマージする(この場合fast-forwardになる)。
f:id:mattyan1053:20180722233420j:plain
これで実質pullと同じ動きをすることになった。
前回、プルリクエストの競合を直すときにpullで競合を起こした。これはorigin/masterブランチをfetchしたあとmergeする先のブランチをmasterではなくclone-branchにしていたため競合が発生したということである。
f:id:mattyan1053:20180722234017j:plain

リベース

これまでやってきたGit操作の履歴を見てみると、分岐が多くて見るのが少し大変である。
f:id:mattyan1053:20180722234544p:plain
これを少しきれいにするのに便利な操作がリベースである。リベースをすると、複数に分岐していたコミット履歴を直線的な履歴に書き換えることができる(これが良いか悪いかという話はおいておいて)。

git rebase

リベースの流れを図を用いて確認する。
f:id:mattyan1053:20180722234950j:plain
上記の状態を普通にマージすると
親コミットを2つ持つ新しいコミットが生成される。これを履歴で見ると分岐して合流するというあまり見やすくはない履歴になるだろう。
f:id:mattyan1053:20180722235253j:plain
ところが

$ git checkout branch-1
$ git rebase master

とすると
f:id:mattyan1053:20180722235626j:plain
というふうに、branch-1で行った変更を追いかけるように順番に、masterの位置から変更していってくれる(branch-1も移動する)。競合するたびにとまるので、止まったときは競合を解決したあと、

$ git add <filename>
$ git rebase --continue

とすればrebaseの続きから再開してくれる。 こうしてbranch-1の先端の変更と同じ変更まで追いつくと、masterブランチの先のほうにもbranch-1の変更と同じ変更が適用されたコミットが完成する。当然branch-1のコミット履歴を見れば一直線の見やすい履歴になっていることだろう。あとは「master」に「branch-1」をmergeしてあげれば良い(fast-forwardになる)。
mergeとrebaseでそれぞれ分岐したブランチを統合したときの図は次のようになる。
f:id:mattyan1053:20180723001440p:plain
分岐していた部分がmasterの上にそのままくっついた形になったいるのがおわかりいただけるだろうか。

git rebase -i

このコマンドを使うと、複数のコミットを一つにまとめることができる。
たとえば
f:id:mattyan1053:20180723002211j:plain
branch-1において2回コミットし、それぞれ別の言葉を追加していたとする。しかしこの2つのコミットは似たような作業をしただけなので、一つにまとめてしまいたい。こういうときに

$ git rebase -i HEAD~~

とすると、

$ git rebase -i HEAD~~
pick eaa773d <commit message1>
pick fba24fd <commit message2>
(略)

テキストエディタが起動して表示されたはずだ。ここで、二つ目の「pick」を「squash」または「s」に書き換えてエディタを終了してみる。

pick eaa773d <commit message1>
s fba24fd <commit message2>

すると、再度エディタが起動し、今度はコミットメッセージの編集ができる。

# This is a combination of 2 commits.
# The first commit's message is;

add hello

#This is the 2nd commit message:

add good evening

(略)

これを書き換えて

add hello and good evening

とすれば、前2つのコミットをまとめることができる。
まとめるコミットは2つでなくてもいいので、例えば

$ git rebase -i <branchname> HEAD~4

とすれば4つのコミットをまとめることもできたりする。当然ログも短くなっている。
f:id:mattyan1053:20180723003510p:plain
このように、ログが短くなりコミットメッセージは更新されている。
また、git rebase -iはコミットをまとめるだけでなく編集も可能である。

$ git rebase -i HEAD~~

としたあとに表示されるエディタで、「pick」のところを先ほどは「squash」に書き換えたが今度は「edit」に書き換えてみると

Stopped at fba24fd...  add good evening
You can amend the commit now, with

  git commit --amend

Once you are satisfied with your changes, run

  git rebase --continue

と表示されるはずだ。これであとは好きなように編集して、満足したらgit commit --amendする(git commitはしなくて良い、すでにあるコミットを書き換えるため)。まだrebaseは終わっていないので「git rebase --continue」で続きを促すと

$ git add sample.c
$ git commit --amend
$git rebase --continue

これで指定の内容のコミットを修正することができた。ログで変更されていることを確認できる。ちなみに、rebaseを途中でやっぱりやめたくなった場合は

$ git rebase --abort

とすればそれまでの作業を取り消して中断できる。
f:id:mattyan1053:20180723004541p:plain
上図の場合はコミットメッセージが修正できていることが確認できる。

リベースの怖いところとお約束

リベースは履歴をきれいにしたり、間違ってコミットしたものを修正したり余計にコミットしたものをまとめたりできる便利な道具である。しかし同時に、行っていることはコミット履歴の改ざんにほかならない。バグの生まれた箇所を見つけにくくなるし、誰がどこでどう分岐して変更したのかが正確にはわからなくなる(コミットメッセージなどでだいたいはわかるが・・・・・・)。使い所をよく考える必要があるだろう。

一度コミットしたものをリベースする

一度コミットしたものをリベースすると、すでにリモートリポジトリに保存されているコミットの履歴と内容があわなくなってしまう。したがって一度コミットしたものをリベースして書き換えた後再度pushすると、コミット履歴が途中まですら一致しなくなり失敗してしまう。ここで、強制的にpush(ローカルリポジトリのコミット履歴をリモートリポジトリに反映する)には

$ git push -f <リモートリポジトリ> <ブランチ名>

とすること可能だ。
とはいえ、一度コミットしたものを書き換えるのはあまり推奨されない。なぜなら、コミットを編集することでコミット名(ハッシュ値)が変わり、リモートリポジトリとローカルリポジトリのコミット履歴の間で大きな不一致が発生してしまうからだ。一度コミットしたものはrebaseせず、自分の作業ブランチを管理者が見やすくするために予めrebaseでコミットをまとめたり分岐をなくしたりしてからコミットする程度にrebaseの利用は留めるのがベターかもしれない(あるいはrebaseの利用基準をチームで明確に決めておく)。

まとめ

今回はマージやリベースなど、コミット操作について少し詳しく説明した。
これらの作業は更新履歴をいじる危険な作業であるから慎重に行うべきである。仕様をしっかり理解しておかしなバグを産まないようにしたい。

最後に

長かったGit入門もここまで。何分自分もGit初心者なのでいろいろ整理しながら書くことができてよかった。アウトプットの大切さがよくわかる。ここまでわかればGitで結構な操作ができるんじゃないだろうか。一応自分なりにジャンル分けしたコマンド一覧を作ったのでそちらのURLも貼っておく。

qiita.com

それでは皆さん良い開発ライフを。

【Git入門】Gitを使い始めたい人へ「リモートリポジトリを操作する」 ~A Beginner to Beginners~ 【その6】 : 前