Git – rebase -i~コミットの統合

概要

git rebaseは今いるブランチのベースを変更するが、-i (intaractive)オプションを付けることで、複数コミットを統合することができる。

interactiveという通り、その過程で何度かエディターが起動して操作内容が表示され、コミット統合に必要な内容に書き換えていく。

準備

以下のように、sample.txtに1行ずつ追加するコミットを含むリポジトリーを準備する。

2つのコミットの統合

rebaseコマンド

main-2main-3のコミットを統合するため、その直前=main-1のコミット直後を指定してrebaseする。このとき-iオプションを付けることで、リベースの過程での操作を指定できる。

各コミットへの対応記述

rebaseを実行するとエディターが起動して以下のように表示される。

  • この段階では、ベースとなるmain-1より後のmain-2main-3の2つのコミットについて、そのまま適用されることになる
  • つまり、main-1コミットの後にmain-2main-3がそのまま連なることになり、何も変わらない
  • これを、main-2main-3をまとめた上でmain-1の後に来るようにしたい

Commands:のところを見ると、以下のように読める。

  • p, pickはそのコミットをそのまま使う
  • s, squashはそのコミットを使うが、1つ前のコミットに統合(融合?)させる

そこで、後の方のコミットmain-3pickからsquashに変更する。こうすれば、main-2コミットはそのまま使われ、そこにmain-3コミットも統合されるはず。

コミットメッセージの編集

上記の内容で保存・終了すると、さらにエディターが起動して以下のように表示される。

2つのコミットを1つにするので、コミットメッセージを1つだけにして、終了の空行を入れる。

処理結果

上記の内容を保存・終了すると、rebaseの処理内容が以下のように表示される。

ログで確認すると、main-2main-3が1つにまとめられ、指定したコミットメッセージになっている。

コミットをまとめただけなので、sample.txtの内容は変化しない。

3つ以上のコミットの統合

main-13の3つの連続するコミットをまとめるため、その直前のコミットを指定してrebase。

エディターが起動。

以下のようにpicksquashに変更して保存。これにより、main-2main-1に統合され、そこにmain-3も統合される。

再度エディターが起動。

統合後のコミットメッセージを編集して保存・終了。

実行結果。

3つのコミットが1つにまとめられた。

イニシャルコミットとの統合

一番最初のコミットと統合するには、以下のコマンドを使う。

git rabase -i --root

以下の例では、README.mdを修正した2つ目のコミットを1つ目のInitial commitに統合している。

リモートへの反映

リモートからpullしたリポジトリーの場合、そのままではpushできない。

ローカルでのrebaseの結果をリモートにそのまま反映するには、push -fで強制的に上書きする。

ただし他のローカルでその内容を反映させるには、改めてcloneする必要がある。

 

Git – マージ – squash~ブランチをまとめて取り込む

概要

git merge--squashオプションを付けると、指定したブランチの全コミットを1つにまとめて今のブランチに追加する。

準備

以下のようなリポジトリーを使う。

  • mainブランチのコミットでREADME.txtに新規書き込み
  • topicブランチの2つのコミットでREADME.txtに新規書き込みと1行追加

マージ

mainブランチで、--suquashオプションを指定してgit mergetopicブランチをマージ。今回は同じREADME.txtを編集しているので競合が発生している。

README.txtの競合状態を確認。

README.txtを編集して競合を解消。

結果をadd、コミット。

no-fast-forwardマージとしてエディターが起動。内容を確認して保存・終了。

コミット結果が表示される。

ログで確認。topicブランチでの2つのコミットが1つのsquashed commitとしてつなげられた。

マージの取り消し

git reset --hard ORIG_HEADでマージ前に戻ることができる。

ログも元に戻る。

 

Git – cherry-pic – コミットを取り込む

概要

  • git cherry-pickは他のブランチのコミットの一部を今のブランチに取り込む
  • 1つないし複数のコミットを指定可能
  • 結果は自動的にコミットされるが、-nオプションでステージングのレベルに留めることができる

英語の”cherry pick”はサクランボ狩りのことで、いいサクランボを選んで摘むことから、次のような使われ方をするようだ。

  • 自分の気に入ったものだけを選ぶ
  • 選り好みする
  • 品物や人材を厳選する
  • 目玉商品を探し漁る
  • 都合のいいデータ・証拠だけを選んで偽装する

準備

次のような内容のローカルリポジトリーを使う。それぞれのコミットで、対応するファイルが作成される。

cherry-pick実行

mainブランチに、topicブランチのtopic1コミットだけを取り込みたいとする。

mainブランチで、topic1コミットのチェックサムを指定して取り込む。

git cherry-pic コミット指定

結果は以下の通りで、topicブランチのtopic1コミットがmainブランチにマージされている。

暫定的なcherry-pick

-nオプション

コミットせずに確認だけしたい場合は-nオプションを付ける。そうすると、コミットはされずにステージングエリアに上げられた状態で留まる。

ワーキングツリーではファイルは作成されている。

しかしコミットされていないので、ログには反映されていない。

ファイルはステージングエリアにあり、コミット前の状態。

暫定cherry-pickの破棄

暫定的なcherry-pickの内容を破棄したい場合は、addを取り消してファイルを削除する。

まず、git reset HEADaddを取り消す。

そして作成されたファイルを削除。

暫定cherry-pickのコミット

-nオプションによる暫定的なcherry-pickの結果を正式にコミットしたい場合は、git commitを実行する。

ログで確認。

 

Git – reset – コミットの巻き戻し

概要

  • git resetは指定したコミットの実行直後まで処理を巻き戻す
  • それより後のコミットの実行結果は取り消され、履歴も消される
  • コミットの指定には、HEADからの相対指定による方法と対象コミットの絶対指定による方法がある
  • 直前のresetのコミット位置はORIG_HEADに保持されているので、resetの取り消しは可能

準備

Git – タグで使った以下のリポジトリーを流用。

最新のsample.txtの内容を確認。

HEADからの相対位置でreset

HEADから2つ遡ったcommit-1終了時までresetするため、HEAD~~を指定。

get reset --hard HEAD~~

あるいは2つ前を数値指定することもできる。

get reset --hard HEAD~2

ログを確認すると、commit-1以降のコミットがなくなっている。

sample.txtの内容も、commit-1実行直後の内容になっている。

resetの取り消し~ORIG_HEAD

reset前のコミット位置はORIG_HEADに保持されている。以下のコマンドで、直前のresetを取り消すことができる。

git reset --hard ORIG_HEAD

ログも元に戻っている。

コミットを絶対指定したreset

遡りたいコミットのチェックサム(の一部)やタグを指定してリセットできる。

先の例でcommit-1まで遡りたい場合、以下は等価。

  • git reset --hard f69f176435cf6436c3b3329af644999ba96da862
  • git reset --hard f69f176
  • git reset --hard beta

チェックサムの一部は元のチェックサムの一部になっていて、git log --onelineでも表示される。

 

Git – revert – 取り消しコミット

概要

“revert”は「元に戻す」意味。

  • git revertはコミットの内容を元に戻すためのrevertコミットを実行する
  • それまでのコミットの履歴を残したまま、その内容を打ち消す

準備

Git – タグで使った以下のリポジトリーを流用。

HEAD位置のコミットの内容を確認。commit-3でファイルに1行追加している。

sample.txtの内容も確認しておく。

revert実行

git revertでHEAD位置のコミットを指定。

git revert HEAD

実行するとエディターが起動し、revertコミットのデフォルトメッセージが表示される。

このままの内容で保存・終了すると、git revertの実行結果が表示されている。

ログを確認するとcommit-3は残ったままで、その後にRevert "commit-3"コミットが実行されている。

ファイルの内容はcommit-3の実行前に戻っている。

revertのrevert

revertコミットをさらにrevertすると元に戻る。

ログにも2回のrivertコミットの記録が残る。

sample.txtの内容も元に戻っている。

飛び越しはできない

冒頭のログの状態で、直前のコミットの1つ前のコミットを直接revertしようとするとエラー。

1つずつ遡るのは可能

まず直前のコミットをrevert。

ログ確認。

その上でcommit-2(ver_1.1タグ)のコミットをrevertは可能。

ログには2回のrevertコミットが残っていて、コミットを遡りながら1行ずつ追加された行を削除している。

ファイルはcommit-2実行前の状態に戻っている。

 

Git – タグ

準備

GitHubに空のリポジトリーを作ってクローン。

軽量タグ(lightweight)

コミット

ファイルを1つ作成してコミット。

コミット後のログ。

タグ付け~git tag ...

HEADがあるコミットにタグをつけるには以下のコマンド。

git tag タグ名

ログにタグが反映された。

注釈付きタグ(annotated)

コミット

ファイルに変更を加えてコミット。

タグ付け~git tag -a ... -m ...

このコミットに注釈付きタグをつける。

git tag -a タグ名 -m "注釈"

ログでの表示は軽量版と同じ。

タグの一覧

タグ名のみ~git tag

タグ名のみの一覧はgit tagで表示できる。

注釈付き~git tag -n

タグの注釈も含めて表示するには以下のコマンド。軽量版はコミットのコメントが表示される。

git tag -n

タグの削除~git tag -d

タグ名を指定して削除するコマンド。

git tag -d タグ名

タグ一覧で削除の確認。

ログで削除の確認。

過去のコミットへのタグ付け

特定のコミットにタグをつける場合は、コミットのチェックサム(一部でもよい)を指定する。

git tag タグ名 チェックサム(の一部)

タグの追加の確認。

タグの詳細表示~git show ...

タグの突いたコミットの詳細を見るのにgit showでコミットのチェックサムを指定する代わりにタグ名を指定できる。注釈付きタグの場合は、タグに関する情報も表示される。

リモートへの反映~git push --tags

単にgit pushしただけではタグは反映されない。

上部のtagsが0のまま。

タグをリモートに反映させるには、以下のように--tagsオプションをつける

git push --tags

ただしgit push --tagsはタグのみをプッシュする。

tagsが2となっている。

tagsをクリックするとタグ一覧が表示され、そのコミットに飛んだりzipをダウンロードできる。

Git – amend – 直前のコミットの修正

概要

git commit --amendは、直前のコミットの内容を修正する。

  • 直前のコミットが対象→2つ以上前のコミットは修正できない
  • コミットメッセージの修正
  • ファイルなどのコミット追加→ファイルなどの削除はできない

参考サイト:コミットの修正には git commit –amend が便利

準備

以下のローカルリポジトリーexampleを使う。

ファイル作成とファイル修正の2つのコミット。

コミット後のファイルの内容。

直前のコミットのコメントを修正する

エディターでコメント修正

以下のコマンドを実行するとエディター画面が開く。

git commit --amend

エディターに現在のコミットメッセージが表示されているので、必要な内容に変更する。

以下のように修正して保存・エディター終了。

エディターを終了すると、コマンドラインに実行結果が表示されている。

ログを確認すると、変更が反映されている。

コマンドラインでコメント設定

-mオプションでコミットメッセージを指定すると、エディターは起動せず、指定した内容でメッセージが書き換えられる。

git gommit --amend -m "メッセージ"

ログで確認。

直前のコミットに追加修正する

既存ファイルを修正~メッセージ変更なし

以下のコマンドは、コミットメッセージの編集をしない。ステージングされている操作があればコミットに追加される。

git commit --amend --no-edit

たとえばsample.txtにもう1行追加する作業を忘れていて、これを最後のコミットに加えたいとする。

git addした後、amendを実行。

git log -pで確認すると、最後のコミットで2行追加されている。コミットメッセージは変わっていない。

新たなファイルを追加~追加とメッセージ変更

たとえば別のファイルsample2.txtを作成し、これも最後のコミットに追加したいとする。

git addでステージングし、amendを実行する。今度は--no-editではなく、-mオプションで新たにファイルを追加したメッセージに変更。

git log -pで確認すると、最後のコミットにsample.txtの2つの追加とsample2.txtの追加が全て盛り込まれている。

 

Git – リモート – push時エラー~no-fast-forward

概要

  • ローカルでの作業結果をリモートにpushしようとしたとき、その間に他のユーザーがリモートに変更を加えていると、エラーになりpushできない
  • そのようなときは、リモートからpullまたはfetch & mergeで変更内容をローカルにマージし、その結果をpushする

準備

リモートリポジトリーの準備

あらかじめ空のリモートリポジトリーexampleを準備しておく。以降、これに対してローカルで2人のユーザーを想定して変更を加える。

user1の作成とクローン

ローカルでuser1ディレクトリーを作成し、そこにリモートリポジトリーをクローンして移動。

ローカルのログ。

 

同様にuser2ディレクトリーを作成し、そこにもリモートリポジトリーをクローンして移動。

ログはuser1と同じ内容。

user1によるコミット・プッシュ

user1で新規ファイルを作成してコミット。

ローカルのログには新たなコミットが加わっている。

この内容でリモートにプッシュする。リモートには新たなコミットが追加されるが、user1側のローカルには反映されない。

user2によるコミット・プッシュ・エラー

現時点でuser2のローカルリポジトリーの内容がリモートと異なっている状態で、user2でファイルを作成・コミット。

user2のローカルのログでは、user1のコミットは当然反映されていない。

この状態でuser2からコミット結果をプッシュしようとすると、リモートリポジトリーがその後の作業結果を含んでいるので更新できない、とエラーになる。

対応処理

このようなエラーの場合、リモートの内容をgit pullするか、フェッチ後にマージする。ここではfetch & mergeで進める。

フェッチ

まずリモートの内容をフェッチ。

フェッチされたリポジトリーFETCH_HEADに移動。

フェッチされた内容のログ。user2クローン後のuser1による変更が記録されている。

マージ

mainブランチに移動

作業中のmainブランチにフェッチされた内容をマージ。このマージはno-faseet-forwardになるので、エディターが立ち上がる。

マージ後のログ。user1user2のコミットが含まれ、マージコミットが加わっている。

git log --graphでも確認。

プッシュ

マージされた内容で、改めてプッシュ。

リモートへの繁栄の確認

プッシュ後のリモートの内容をuser1でプル。

user2のコミットも反映されている。

ログも最終状態になっている。

 

Git – リモート – clone/push/pull/fetch

clone~リモートリポジトリーをローカルに複製

コマンド形式

git cloneはリモートリポジトリーの複製をローカルリポジトリーとして作成する。パスワード認証、SSH認証に対するコマンド形式は以下の通り。

パスワード認証

git clone https://github.com/account/remote_rep [local_rep]
SSH認証
git clone git@github.com:account/remote_rep.git [local_rep]
  • local_repを省略するとremote_repと同じ名前でディレクトリーが作成され、ローカルリポジトリーに設定される
  • パスワード認証はログインパスワードが非推奨になり、2段階認証化が必要
  • SSH認証の場合は別途キーセットの生成・登録などが必要

実行例

  • あらかじめリモートリポジトリーexampleを作成しておく
  • user1ディレクトリーを作成し、そこにexampleリポジトリーをクローン

push~ローカルのコミットをリモートに反映

コマンド形式

ローカルリポジトリー内でgit pushを実行すると、ローカルでのコミットをリモートに反映

git push

実行例

user1でcloneしたローカルリポジトリーのでファイルを作成・コミット。

コミット結果のログ。

コミット結果をリモートにプッシュ。

GitHub上で作成されたファイルが確認できる。

cloneはその時点でのリモート内容を複製

user2ディレクトリーを作成してexampleをクローン。

user1が作成したファイルが反映される。

リモートへのpushはfast-forward

user2ファイル作成・コミット。

コミット結果のログ。

user2のコミット結果をプッシュ。リモート上でfast-forwardでマージされる。この間にリモートで他のコミットが行われているとpushできない。

リモートリポジトリーからのpull

user1でリモートのexampleをプル。リモートの内容がfast-forwardでローカルリポジトリーにマージされる。

この時点までの変更(user2でのファイル作成)が反映されている。

ログも最新状態になる。

pullの後のコミットとpush

user1でファイルを変更、コミット。

コミット後のログ。

user1のローカルリポジトリーからリモートにpush

fetch~一時的なリモートの内容確認

フェッチの確認

user2git fetchを実行。

フェッチを実行した結果。

  • フェッチした場合はローカルリポジトリーにマージされず、FETCH_HEADブランチに複製される
  • FETCH_HEADは不可視でgit branchでは確認できないが、git checkoutで移動することができる
  • FETCH_HEADで変更・コミットした内容は最終的に破棄される
  • 破棄したくない場合はgit switch -cで新たなブランチを作成できる
  • この操作はgit switch -cで取り消せる

FERCH_HEADブランチのログ。

フェッチ内容の反映 = pull

FETCH_HEADがある状態でmainブランチに移動。

FETCH_HEADブランチをマージ。

FETCH_HEADで変更していなければ、結果は直接pullを実行したときと同じ。

 

Git – rebase – 競合あり

準備

Git – merge – no-fast-forward – 競合ありで準備したのと同じ内容でブランチとコミットを準備する。

mainブランチのファイル、ログの内容。

topicブランチのファイル、ログの内容。

準備後の状態は以下の通り。

rebase

リベースは、現在いるブランチの出発点を指定したブランチの末尾に変更する。ここではtopicブランチをmainブランチに繋げるため、topicブランチに移動する(あるいはtopicブランチにいることを確認する)。

topicブランチでmainブランチを指定してgit rebaseを実行。common.txtで自動マージを実行しようとしたときに競合が発生している。

common.txtの内容を確認してみると、マージの時と同じく競合部分の差分がマークされている。

ただし、マージで競合した場合(以下の内容)と違うところがある。

topicでログを見ると、ベース変更のためにmainブランチのコミットを適用したところで止まっている。

abort

競合が発生したリベースを中止したい場合は、git rebase --abortを実行。

ログを確認すると、rebase実行前の状態に戻っている。

競合の解消

再度リベースを試みる。

競合しているcommon.txtのマークされた部分を修正。

競合解消後のリベースの手順は以下の通り

  • 競合を解消したファイルをgit add
  • ここでコミットせずに、git rebase --continue

ところがまた競合が生じた。

common.txtの内容。今度はtopicブランチでの追加結果が反映されていて、上で新たに修正した内容と食い違っていると言われている。

common.txtを修正。

git addしてgit rebase --continue。今度は最後まで通った。

リベース後のtopicブランチのログ。コミットが繋がっている。

mainブランチの方は特に変更されていない。

リベース後の状態は以下のようになっている。

rebaseの撤回

リベースした後に取り消したい場合。git reflogで詳細なログを表示し、リベース開始直前の項目のログ番号を確認する。

rebaseを実行したブランチ(この場合はtopicブランチ)で以下を実行。間違ってmainブランチで実行すると、mainのコミット結果がtopic1topic2で書き変わってしまう。

topicログの結果。リベース前に戻った。

mainブランチは変更されていない。

リベース結果のmainブランチへの反映

topicブランチでリベースを実行すると、その結果はtopicブランチでのものとなる。最終的にmainブランチにその結果を反映させたいときは、mainブランチに移動してそこでtopicブランチをマージする。

mainブランチでコミットが行われていなければ、マージはfast-forwardで行われ、mainの内容がリベース後の結果まで送られる。

作業結果も反映されている。

マージ・リベースの撤回

マージの撤回

まずマージを撤回する。git reflogでマージ直前のログ番号を確認。

確認したログ番号でgit reset

mainブランチの内容が元に戻る。

topicブランチの内容はリベース直後の状態。

リベースの撤回

先ほどのリベースの撤回と同じ手順。

ログ番号を指定してgit reset

topicブランチの内容が元に戻っている(mainブランチも)。