はじめに
アルゴリズム問題に取り組み、取り組んでいた問題をプッシュしようとしたところ、以下のエラーメッセージが発生しました。
この意味は何かを調べていくうちに、Gitの基本的な処理の流れについての理解が不足していることが発覚しました。
原因は、プッシュしようとした際に、リモートからプルする前にプッシュしようとしていたことでした。
本記事では、リモートからのプル前にプッシュしようとした時の対処法としてrebaseを使った解決法について、基本を整理しながらまとめていきます。
前提① ディレクトリ構成
Algorithm-Solutions/
├── LinkedList/
│ ├── Common/
│ │ ├── Node.js
│ │ └── SinglyLinkedList.js
│ ├── insertAtHead/
│ ├── middleNode/
│ │ ├── MiddleNodeLinkedList.js // クラス・メソッド定義
│ │ └── middleNode.test.js // テストケース
│
├── Object/
データ構造ごとにディレクトリを分け、さらに解いた問題ごとにサブディレクトリを作成しています。それぞれの問題には、クラスベースの実装、関数ベースの実装、テストケースを含めています。再利用できるクラス(例:Node.js や SinglyLinkedList.js)は Common/ にまとめてあります。
前提② ブランチ運用の方針
ブランチはデータ構造単位と問題単位で分けています。問題ごとに作業ブランチを作成し、まずデータ構造ブランチにマージしたのち、最終的に main に統合することで整理します。
ディレクトリ構成とも連動しており、ファイルの管理・履歴追跡がしやすい体制を意識しています。
以下の流れでメインブランチに統合しています。
main
└── LinkedList // データ構造ブランチ
├── insertAtHead // 問題ブランチ
└── middleNode
└── Object
└── Car
・各データ構造に対してブランチ(LinkedList, Object)を作成
・その中で解いた 問題ごとに派生ブランチ(insertAtHead, middleNode)を作成
・問題ブランチをまず データ構造ブランチにマージ
・すべての問題を解決したら、データ構造ブランチを main にマージ
【エラーメッセージ】
To https://github.com/~/Algorithm-Solutions.git
! [rejected] linkedList -> linkedList (non-fast-forward)
error: failed to push some refs to 'https://github.com/~/Algorithm-Solutions.git'
hint: Updates were rejected because a pushed branch tip is behind its remote
hint: counterpart. If you want to integrate the remote changes, use 'git pull'
hint: before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
【エラーメッセージの意味】
! [rejected] linkedList -> linkedList (non-fast-forward)
ローカルの linkedList ブランチ を、リモートの linkedList ブランチ に push しようとした結果、リモートにあるブランチが、現在のローカルにはないコミットが存在し、リモートブランチの方が「進んでいる」ため、Git は安全のために push を拒否した。
「non-fast-forward」とは、「そのままでは履歴が直線的に進まない(=整合性が崩れる)ため push できない」ということです。
◎ローカルのブランチの履歴とリモートのブランチの履歴にズレがあることを意味します
【ヒントメッセージの意味】
①hint: Updates were rejected because a pushed branch tip is behind its remote
②hint: counterpart. If you want to integrate the remote changes, use 'git pull'
③hint: before pushing again.
④hint: See the 'Note about fast-forwards' in 'git push --help' for details.
① push しようとしたブランチの先端が、リモートにある同じブランチよりも古いため、更新は拒否された
②③ リモートの変更を取り込んで統合したい場合は、再度 push する前に 'git pull' を実行
④ 詳しくは 'git push --help' の「fast-forward に関する注意事項」を参照してね
fast-forwardとは
Git において分岐のない履歴をそのまま「早送り」してブランチを進めるマージ方法です。
[main] ブランチ(リモート): A ─ B
[feature] ブランチ(ローカル): A - B - C - D
main は feature の履歴に完全に含まれています。
この状態で feature を main にマージすると以下のように履歴は一直線になります。
[main] ブランチ(リモート): A - B - C - D
これは、main のポインタを D まで早送りするだけであるため、fast-forward と呼ばれます。
たとえば、feature ブランチで新しい機能を追加し、main にマージする際、main がそのまま feature の履歴の一部であるなら、Git は新たなマージコミットを作らずに main を feature の最新コミットまで早送りしているということです。
【エラーの原因】
リモートの最新の状態をローカルのメインブランチ・データ構造ブランチそれぞれに pull していなかったこと
【実際に処理した流れ】
① データ構造ブランチから問題ブランチを作成 ← ここがエラーの原点
② 問題ブランチでディレクトリ、ファイルを作成
③ コーディング
④ ステージング(git add .)
⑤ コミット(git commit -m "~")
⑥ リモートにプッシュ ← ここで問題発生
【解決策】rebaseの使用
rebase を使用する前に押さえておくべきポイント
(1) Gitの領域とコマンドの関係
全体像とイメージ図
https://medium.com/@zjpjack/reverting-modified-in-4-stages-in-git-f3997f526902 より引用
Gitの各領域
①【ワーキングスペース・ワーキングツリー】(作業ディレクトリ)
実際に作業している場所
ファイルの編集や削除をするはここで行われます。
②【ステージングエリア】(インデックス)
次のコミットで記録したい変更だけを一時的に保管する場所
Gitは変更されたファイルをすぐにコミットしません。
まず一度ステージングエリアに入れて、「この変更を記録するよ」とGitに伝える必要があります。
なお、ステージングエリアがあることによって、変更の一部だけを選んでコミットすることができます。
③【ローカルリポジトリ】
実際にGitで管理されている履歴(コミット)が保存されている場所
使用しているパソコンの中にある、.gitというディレクトリ(隠しフォルダ)によって履歴が管理されています。
コミットを行なうことによって、ステージングエリアにある変更内容がローカルリポジトリに履歴として保存されます。
(2) rebase の基本
rebase とは、ブランチの土台(ベース)を付け替える操作(re・base)することを意味します。
あるブランチの変更履歴を、別のブランチの最新の履歴の上に移し替えることで、履歴が直線になることが特徴です。
リベースとマージコミットの違いを表したイメージ図
※https://gihyo.jp/dev/serial/01/hackgirlsgit/0006 より引用
※1つ1つの点(●)はコミットを表しています。つまり、その時点での状態全体を表します。
ある履歴から分岐して feat(ure) ブランチで機能を開発した場合、rebase を使うと、feat のコミットを元のブランチ(master)の最新の履歴の後ろに「付け替える」ことができます。
この結果、履歴はまるで main ブランチのあとに feature の開発を始めたかのような、1本の直線的な履歴になります。
一方、merge を使うと、分岐した2つの履歴を統合するための 「マージコミット」 が作成され、履歴上に「分岐して合流した形」が残ります。
つまり、rebase は 履歴をシンプルに整えたいときに便利であり、merge は 履歴をそのまま保ちながら統合したいときに使われます。
(3) rebase使用する際の注意点
ただし、rebaseは最新の履歴の後ろに「付け替える」ということから注意すべき点があります。
① 共有後のブランチには使用しない
rebase は既存のコミットを「別のベースの上に再適用」する操作です。その際、Git は新しいコミットを作り直すため、コミットのハッシュ(ID)が変わります。
その結果、他の開発者と共有しているブランチに rebase を使うと、他人が見ている履歴と自分の履歴が食い違い、マージの衝突や push 時のエラーの原因になります。
そのため、rebase は原則として、自分のローカル作業ブランチでのみ使い、push して他人と共有した後は使わないようにすることが望ましいと言えます。
② コンフリクトが起きることがある
rebase中に他のブランチとの変更がぶつかると、コンフリクトが発生します。
解決方法を知らないと途中で作業が止まってしまうため、使用には注意が必要です。
③ コミットメッセージや順序も注意
rebase -i(インタラクティブリベース)でコミットを編集できますが、うっかりミスで履歴を壊すこともありえます。
実行前に git log で履歴を確認することが推奨されます。
④ チームルールを確認すること
プロジェクトによっては「rebase禁止」「merge推奨」のルールがあります。
特にmainブランチや本番用のreleaseブランチなどでは、履歴の改ざんが厳禁とされていることがあります。
(4) rebase を使用する妥当性の検討
今回のケースで rebase を使用することが妥当かどうかを検討します。
【結論】
使用できると判断
【理由】
① 他開発者とリポジトリを共有していないこと
② ローカルブランチにしか存在しないコミットだったため
③ push 前の整理目的
以上の理由から、rebase によって履歴を書き換えても他開発者が存在せず、個人での利用が目的であるため、影響がないと判断しています。
【状況の整理】
https://medium.com/@zjpjack/reverting-modified-in-4-stages-in-git-f3997f526902 より引用
問題発覚時、上の図に照らして考えると、
リモートリポジトリをローカルリポジトリ(リモートオリジン)にフェッチしていない、および、ローカルのワーキングディレクトリにプルしていない状態でした。
つまり、リモートの状態がローカルに反映されていない状態でした。
その状態で、「ステージングエリアへの追加」・「ローカルリポジトリへコミット」を行なった状態であったため、ローカルがリモートと同じではない状態でコミットまでしている状態でした。
【解決したコマンド実行手順】
① git stash --keep-index
目的:作業ブランチのうち、ステージ済みの変更だけを残し、作業中の(未ステージ)ファイルを一時的に退避させる
このような場面で git stash --keep-index を使うことによって、
ステージ・コミット済みの内容は残しつつ、作業中の変更(未ステージや未追跡ファイル)は一時退避するため、rebase を安全に実行できるようになります。
後に、stash で退避した変更を rebase 後に戻します。
② git pull --rebase
目的:リモートの変更をローカル履歴に「挿し込む」ように統合する
この操作によって、リモートの状態が現在の作業スペースであるワーキングディレクトリに反映されます。
--rebase を使うことで、ローカルのコミットをリモートの履歴の後ろに「付け直す」ため、結果として、履歴が一直線になり、マージコミットが増えずにきれいなログになります。
③ git stash pop
目的:退避していた未ステージの変更を元に戻す
ステージ済みの変更で rebase を完了させたあと、退避していたファイルを戻します。
これによって、リモートの状態を取り込んだ後にコミットを追加した状態にすることができます。
上記の①〜③の手順により、リモートに push ができる環境が整い、git push ができました。
まとめ
今回は、ローカルでコミットした後にプッシュできないエラーに対する実際に対応した解決策を順番に解説しました。リベースはその名の通り、ベースを再構築する、つまり、履歴を改変することを意味するため、使用にあたっては十分な注意が必要です。実際の開発においては、履歴の改変やチーム開発のトラブルを避けるため、使用される場合はかなり限られると思います。
リベースの基本と実際の使い方の一部の理解を通じて、開発時のエラー対処にお役立ていただければ幸いです。
最後までお読みいただき、ありがとうございました。
参考・画像引用元URL・参考文献
https://git-scm.com/book/ja/v2/Git-%E3%81%AE%E3%83%96%E3%83%A9%E3%83%B3%E3%83%81%E6%A9%9F%E8%83%BD-%E3%83%AA%E3%83%99%E3%83%BC%E3%82%B9
『改訂2版 わかばちゃんと学ぶ Git使い方入門〈GitHub、SourceTree、コマンド操作対応〉』
https://medium.com/@zjpjack/reverting-modified-in-4-stages-in-git-f3997f526902
https://neurathsboat.blog/post/git-intro/
https://gihyo.jp/dev/serial/01/hackgirlsgit/0006

