-
1. Gitを始めるにあたって
- 1.1 バージョン管理について
- 1.2 Gitの歴史
- 1.3 Gitとは何か?
- 1.4 コマンドライン
- 1.5 Gitのインストール
- 1.6 Gitの初回セットアップ
- 1.7 ヘルプの利用
- 1.8 まとめ
-
2. Gitの基本
- 2.1 Gitリポジトリの取得
- 2.2 リポジトリへの変更の記録
- 2.3 コミット履歴の表示
- 2.4 元に戻す操作
- 2.5 リモートでの作業
- 2.6 タグ付け
- 2.7 Gitエイリアス
- 2.8 まとめ
-
3. Gitのブランチ機能
- 3.1 ブランチの基本
- 3.2 基本的なブランチとマージ
- 3.3 ブランチ管理
- 3.4 ブランチワークフロー
- 3.5 リモートブランチ
- 3.6 リベース
- 3.7 まとめ
-
4. サーバー上のGit
- 4.1 プロトコル
- 4.2 サーバーにGitをセットアップする
- 4.3 SSH公開鍵の生成
- 4.4 サーバーのセットアップ
- 4.5 Gitデーモン
- 4.6 スマートHTTP
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 サードパーティのホスティングオプション
- 4.10 まとめ
-
5. 分散Git
- 5.1 分散ワークフロー
- 5.2 プロジェクトへの貢献
- 5.3 プロジェクトの管理
- 5.4 まとめ
-
6. GitHub
- 6.1 アカウントのセットアップと設定
- 6.2 プロジェクトへの貢献
- 6.3 プロジェクトの管理
- 6.4 組織の管理
- 6.5 GitHubのスクリプト
- 6.6 まとめ
-
7. Gitツール
- 7.1 リビジョンの選択
- 7.2 インタラクティブステージング
- 7.3 スタッシュとクリーン
- 7.4 作業に署名する
- 7.5 検索
- 7.6 履歴の書き換え
- 7.7 Resetの解明
- 7.8 高度なマージ
- 7.9 Rerere
- 7.10 Gitを使ったデバッグ
- 7.11 サブモジュール
- 7.12 バンドル
- 7.13 置換
- 7.14 認証情報の保存
- 7.15 まとめ
-
8. Gitのカスタマイズ
- 8.1 Gitの設定
- 8.2 Git属性
- 8.3 Gitフック
- 8.4 Gitによるポリシー適用例
- 8.5 まとめ
-
9. Gitと他のシステム
- 9.1 クライアントとしてのGit
- 9.2 Gitへの移行
- 9.3 まとめ
-
10. Gitの内側
- 10.1 PlumbingとPorcelain
- 10.2 Gitオブジェクト
- 10.3 Gitリファレンス
- 10.4 Packfile
- 10.5 Refspec
- 10.6 転送プロトコル
- 10.7 メンテナンスとデータ復旧
- 10.8 環境変数
- 10.9 まとめ
-
A1. 付録A: 他の環境でのGit
- A1.1 グラフィカルインターフェース
- A1.2 Visual StudioでのGit
- A1.3 Visual Studio CodeでのGit
- A1.4 IntelliJ / PyCharm / WebStorm / PhpStorm / RubyMineでのGit
- A1.5 Sublime TextでのGit
- A1.6 BashでのGit
- A1.7 ZshでのGit
- A1.8 PowerShellでのGit
- A1.9 まとめ
-
A2. 付録B: アプリケーションへのGitの組み込み
- A2.1 コマンドラインGit
- A2.2 Libgit2
- A2.3 JGit
- A2.4 go-git
- A2.5 Dulwich
-
A3. 付録C: Gitコマンド
- A3.1 セットアップと設定
- A3.2 プロジェクトの取得と作成
- A3.3 基本的なスナップショット
- A3.4 ブランチとマージ
- A3.5 プロジェクトの共有と更新
- A3.6 検査と比較
- A3.7 デバッグ
- A3.8 パッチ適用
- A3.9 メール
- A3.10 外部システム
- A3.11 管理
- A3.12 Plumbingコマンド
7.6 Gitツール - 履歴の書き換え
履歴の書き換え
Gitで作業していると、ローカルのコミット履歴を修正したいと何度も思うかもしれません。Gitの素晴らしい点の一つは、最後の最後に意思決定ができることです。ステージングエリアを使えば、コミット直前にどのファイルをどのコミットに入れるかを決めることができます。git stash
を使えば、まだ作業すべきではなかったものについて作業してしまったと決めることができます。そして、すでに行われたコミットを、まるで別の方法で行われたかのように見えるように書き換えることができます。これには、コミットの順序の変更、メッセージの変更、コミット内のファイルの変更、コミットの結合や分割、またはコミット全体の削除が含まれることがあります。これらすべてを、他の人と作業を共有する前に行うことができます。
このセクションでは、これらのタスクを実行する方法を説明します。これにより、他の人と共有する前にコミット履歴を思い通りにすることができます。
注
|
満足するまで作業をプッシュしないでください
Gitの基本的なルールの1つは、多くの作業がクローン内でローカルに行われるため、ローカルで履歴を書き換える自由が非常に大きいということです。しかし、一度作業をプッシュしてしまうと、話はまったく別になり、よほどの理由がない限り、プッシュされた作業は最終的なものと見なすべきです。つまり、満足して他の世界と共有する準備ができるまで、作業をプッシュすることは避けるべきです。 |
最後のコミットを変更する
最も最近のコミットを変更することは、おそらく最もよく行う履歴の書き換えでしょう。最後のコミットに対して、通常、次の2つの基本的なことを行いたいと思うでしょう。コミットメッセージを単純に変更するか、ファイルを追加、削除、変更することでコミットの実際の内容を変更するかです。
最後のコミットメッセージを単に修正したい場合、それは簡単です。
$ git commit --amend
上記のコマンドは、以前のコミットメッセージをエディタセッションに読み込みます。そこでメッセージに変更を加え、その変更を保存して終了できます。エディタを保存して閉じると、エディタは更新されたコミットメッセージを含む新しいコミットを書き込み、それを新しい最後のコミットにします。
一方、最後のコミットの実際の内容を変更したい場合も、基本的には同じ手順です。まず、忘れていたと思われる変更を加え、それらの変更をステージングし、その後のgit commit --amend
が最後のコミットを新しい、改善されたコミットで置き換えます。
このテクニックを使用する際には注意が必要です。なぜなら、修正するとコミットのSHA-1が変更されるからです。これは非常に小さなリベースのようなものです。すでにプッシュした最後のコミットは修正しないでください。
ヒント
|
修正されたコミットは、修正されたコミットメッセージを必要とする場合もあれば、しない場合もあります。
コミットを修正する際、コミットメッセージとコミットの内容の両方を変更する機会があります。コミットの内容を大幅に修正する場合、その修正された内容を反映するようにコミットメッセージも更新すべきです。 一方、修正が十分に些細な場合(愚かなタイプミスを修正したり、ステージングし忘れたファイルを追加したりなど)で、以前のコミットメッセージで問題ない場合は、単に変更を加え、ステージングし、不要なエディタセッションを完全に回避することができます。
|
複数のコミットメッセージを変更する
履歴の中にある、より過去のコミットを修正するには、より複雑なツールに移行する必要があります。Gitには履歴修正ツールはありませんが、リベースツールを使用して、一連のコミットを元のベースとなっていたHEADにリベースすることができます。対話型リベースツールを使用すると、変更したい各コミットの後で停止し、メッセージを変更したり、ファイルを追加したり、好きなことをしたりできます。git rebase
に-i
オプションを追加することで、対話的にリベースを実行できます。どのコミットまでさかのぼってコミットを書き換えたいかをコマンドに伝えることで、その範囲を指定する必要があります。
例えば、最後の3つのコミットメッセージ、またはそのグループ内の任意のコミットメッセージを変更したい場合、git rebase -i
の引数として、編集したい最後のコミットの親であるHEAD~2^
またはHEAD~3
を指定します。最後の3つのコミットを編集しようとしているので、~3
の方が覚えやすいかもしれませんが、実際には4つ前のコミット、つまり編集したい最後のコミットの親を指定していることに注意してください。
$ git rebase -i HEAD~3
これはリベースコマンドであるということをもう一度覚えておいてください。HEAD~3..HEAD
の範囲内のメッセージが変更されたすべてのコミット、およびそのすべての子孫が書き換えられます。すでに中央サーバーにプッシュしたコミットは含めないでください。そうすると、同じ変更の代替バージョンを提供することになり、他の開発者を混乱させます。
このコマンドを実行すると、テキストエディタに次のようなコミットのリストが表示されます。
pick f7f3f6d Change my name a bit
pick 310154e Update README formatting and add blame
pick a5f4a0d Add cat-file
# Rebase 710f0f8..a5f4a0d onto 710f0f8
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out
これらのコミットが、通常log
コマンドで見る順序とは逆の順序でリストされていることに注意することが重要です。log
を実行すると、次のような表示になります。
$ git log --pretty=format:"%h %s" HEAD~3..HEAD
a5f4a0d Add cat-file
310154e Update README formatting and add blame
f7f3f6d Change my name a bit
逆順になっていることに注意してください。対話型リベースは、実行するスクリプトをあなたに提供します。コマンドラインで指定したコミット(HEAD~3
)から開始し、これらのコミットのそれぞれで導入された変更を上から下へ順にリプレイします。最も古いものが一番上にリストされています。これは、それが最初にリプレイされるためです。
スクリプトを編集して、編集したいコミットで停止するようにする必要があります。そのためには、スクリプトを停止させたい各コミットについて、「pick」という単語を「edit」という単語に変更します。例えば、3番目のコミットメッセージのみを変更するには、ファイルを次のように変更します。
edit f7f3f6d Change my name a bit
pick 310154e Update README formatting and add blame
pick a5f4a0d Add cat-file
エディタを保存して終了すると、Gitはリスト内の最後のコミットに戻り、次のメッセージとともにコマンドラインに表示されます。
$ git rebase -i HEAD~3
Stopped at f7f3f6d... Change my name a bit
You can amend the commit now, with
git commit --amend
Once you're satisfied with your changes, run
git rebase --continue
これらの指示は、何をすべきかを正確に教えてくれます。入力してください。
$ git commit --amend
コミットメッセージを変更して、エディタを終了します。次に、実行します。
$ git rebase --continue
このコマンドは他の2つのコミットを自動的に適用し、これで完了です。pick
をedit
に複数の行で変更した場合、edit
に変更した各コミットに対してこれらの手順を繰り返すことができます。その都度、Gitは停止し、コミットを修正させ、完了したら続行します。
コミットの並べ替え
対話型リベースを使用して、コミットを並べ替えたり、完全に削除したりすることもできます。「Add cat-file」コミットを削除し、他の2つのコミットが導入される順序を変更したい場合は、リベーススクリプトを次のように変更できます。
pick f7f3f6d Change my name a bit
pick 310154e Update README formatting and add blame
pick a5f4a0d Add cat-file
これを次のようにします。
pick 310154e Update README formatting and add blame
pick f7f3f6d Change my name a bit
エディターを保存して終了すると、Gitはブランチをこれらのコミットの親に戻し、310154e
を適用してからf7f3f6d
を適用し、停止します。これにより、これらのコミットの順序が効果的に変更され、「Add cat-file」コミットは完全に削除されます。
コミットの統合
対話型リベースツールを使用して、一連のコミットを1つのコミットに統合することも可能です。スクリプトは、リベースメッセージに役立つ指示を入れます。
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out
「pick」または「edit」の代わりに「squash」を指定すると、Gitはその変更と直前の変更の両方を適用し、コミットメッセージを結合させます。したがって、これら3つのコミットから1つのコミットを作成したい場合、スクリプトを次のようにします。
pick f7f3f6d Change my name a bit
squash 310154e Update README formatting and add blame
squash a5f4a0d Add cat-file
エディターを保存して終了すると、Gitは3つの変更すべてを適用し、その後、3つのコミットメッセージをマージするためにエディターに戻されます。
# This is a combination of 3 commits.
# The first commit's message is:
Change my name a bit
# This is the 2nd commit message:
Update README formatting and add blame
# This is the 3rd commit message:
Add cat-file
これを保存すると、以前の3つのコミットのすべての変更を導入する単一のコミットが完成します。
コミットの分割
コミットを分割すると、コミットが取り消され、その後、最終的に作成したいコミットの数だけ部分的にステージングおよびコミットが行われます。例えば、3つのコミットの真ん中のコミットを分割したいとします。「READMEのフォーマットを更新し、blameを追加」の代わりに、「READMEのフォーマットを更新」という最初のコミットと、「blameを追加」という2番目のコミットに分割したいとします。これは、rebase -i
スクリプトで、分割したいコミットの指示を「edit」に変更することで行うことができます。
pick f7f3f6d Change my name a bit
edit 310154e Update README formatting and add blame
pick a5f4a0d Add cat-file
その後、スクリプトがコマンドラインに戻ったとき、そのコミットをリセットし、リセットされた変更を取り込み、それらから複数のコミットを作成します。エディタを保存して終了すると、Gitはリスト内の最初のコミット(f7f3f6d
)の親に戻り、最初のコミット(f7f3f6d
)を適用し、2番目のコミット(310154e
)を適用し、コンソールに表示されます。そこで、git reset HEAD^
でそのコミットをミックスリセットすると、そのコミットが実質的に取り消され、変更されたファイルはアンステージングされたままになります。これで、複数のコミットになるまでファイルをステージングしてコミットし、完了したらgit rebase --continue
を実行します。
$ git reset HEAD^
$ git add README
$ git commit -m 'Update README formatting'
$ git add lib/simplegit.rb
$ git commit -m 'Add blame'
$ git rebase --continue
Gitはスクリプトの最後のコミット (a5f4a0d
) を適用し、履歴は次のようになります。
$ git log -4 --pretty=format:"%h %s"
1c002dd Add cat-file
9b29157 Add blame
35cfb2b Update README formatting
f7f3f6d Change my name a bit
これは、リストにある最新の3つのコミットのSHA-1を変更するため、すでに共有リポジトリにプッシュした変更済みコミットがこのリストに表示されないように注意してください。リストの最後のコミット (f7f3f6d
) は変更されていないことに注意してください。このコミットはスクリプトに表示されていますが、「pick」とマークされ、リベースの変更が適用される前に適用されたため、Gitはコミットを修正しません。
コミットの削除
コミットを削除したい場合は、rebase -i
スクリプトを使用して削除できます。コミットのリストで、削除したいコミットの前に「drop」という単語を入れるか(またはリベーススクリプトからその行を削除するだけです)。
pick 461cb2a This commit is OK
drop 5aecc10 This commit is broken
Git がコミットオブジェクトを構築する方法のため、コミットを削除したり変更したりすると、それに続くすべてのコミットが書き換えられます。リポジトリの履歴を遡れば遡るほど、再作成が必要なコミットが増えます。これにより、削除したコミットに依存する多くのコミットがシーケンスの後半にある場合、多くのマージ競合が発生する可能性があります。
このようなリベースの途中で、良いアイデアではないと判断した場合は、いつでも停止できます。git rebase --abort
と入力すると、リポジトリはリベースを開始する前の状態に戻ります。
リベースを完了して、それが望むものではないと判断した場合、git reflog
を使用してブランチの以前のバージョンを回復できます。reflog
コマンドの詳細については、データ回復を参照してください。
注
|
Drew DeVaultは、 |
最終手段:filter-branch
もしスクリプト可能な方法で多数のコミットを書き換える必要がある場合(たとえば、メールアドレスを一括変更したり、すべてのコミットからファイルを削除したりする場合)に使えるもう1つの履歴書き換えオプションがあります。そのコマンドはfilter-branch
で、履歴の広範囲を書き換えることができるため、プロジェクトがまだ公開されておらず、他の人が書き換えようとしているコミットに基づいて作業を行っていない限り、おそらく使うべきではありません。しかし、非常に役立つ場合もあります。その機能の一部を理解できるように、いくつかの一般的な使用法を学びます。
注意
|
|
すべてのコミットからファイルを削除する
これはかなりよく起こります。誰かがうっかりgit add .
という軽率なコマンドで巨大なバイナリファイルをコミットしてしまい、それをすべて削除したい場合です。あるいは、誤ってパスワードを含むファイルをコミットしてしまい、プロジェクトをオープンソース化したい場合もあるでしょう。filter-branch
は、履歴全体をクリーンアップするために使用したいツールでしょう。履歴全体からpasswords.txt
という名前のファイルを削除するには、filter-branch
の--tree-filter
オプションを使用できます。
$ git filter-branch --tree-filter 'rm -f passwords.txt' HEAD
Rewrite 6b9b3cf04e7c5686a9cb838c3f36a8cb6a0fc2bd (21/21)
Ref 'refs/heads/master' was rewritten
--tree-filter
オプションは、プロジェクトの各チェックアウト後に指定されたコマンドを実行し、結果を再コミットします。この場合、passwords.txt
というファイルは、存在するかどうかにかかわらず、すべてのスナップショットから削除されます。誤ってコミットされたエディタのバックアップファイルをすべて削除したい場合は、git filter-branch --tree-filter 'rm -f *~' HEAD
のようなコマンドを実行できます。
Gitがツリーとコミットを書き換え、最後にブランチポインタを移動するのを見ることができます。通常、これをテストブランチで行い、結果が本当に望むものであると判断した後、master
ブランチをハードリセットするのが良い考えです。すべてのブランチに対してfilter-branch
を実行するには、コマンドに--all
を渡します。
サブディレクトリを新しいルートにする
他のソース管理システムからインポートを行い、意味のないサブディレクトリ(trunk
、tags
など)がある場合を想像してみてください。すべてのコミットに対してtrunk
サブディレクトリを新しいプロジェクトルートにしたい場合、filter-branch
もそれを行うのに役立ちます。
$ git filter-branch --subdirectory-filter trunk HEAD
Rewrite 856f0bf61e41a27326cdae8f09fe708d679f596f (12/12)
Ref 'refs/heads/master' was rewritten
これで、新しいプロジェクトルートは、毎回trunk
サブディレクトリにあったものになります。Gitは、サブディレクトリに影響を与えなかったコミットも自動的に削除します。
メールアドレスを一括で変更する
もう一つのよくあるケースは、作業を開始する前にgit config
を実行して名前とメールアドレスを設定し忘れた場合、あるいは職場のプロジェクトをオープンソース化して、すべての職場のメールアドレスを個人のアドレスに変更したい場合です。いずれにせよ、filter-branch
を使えば、複数のコミットのメールアドレスを一括で変更できます。自分のメールアドレスだけを変更するように注意する必要があるため、--commit-filter
を使います。
$ git filter-branch --commit-filter '
if [ "$GIT_AUTHOR_EMAIL" = "schacon@localhost" ];
then
GIT_AUTHOR_NAME="Scott Chacon";
GIT_AUTHOR_EMAIL="schacon@example.com";
git commit-tree "$@";
else
git commit-tree "$@";
fi' HEAD
これにより、すべてのコミットが新しいアドレスを持つように書き換えられます。コミットは親のSHA-1値を含むため、このコマンドは、一致するメールアドレスを持つコミットだけでなく、履歴内のすべてのコミットのSHA-1を変更します。