はじめに
私用なり仕事なり、Gitを利用して開発を行う際、その行為に専念したいものです。Gitの扱いに手間取って本質から逸脱するのは本末転倒であり、それを避けるためにこの記事を作成しました。
序盤は、Gitの概要や基本コマンドの使い方といった基礎内容になっています。中盤は、データ(ファイル)がどう扱われているのか、また基本コマンドの挙動といったGit内部の仕組みを説明しています。そして終盤は、実践を想定したコマンド操作方法をできる限り網羅しました。
かなり長い記事で、1つ1つの説明が冗長になっています。その理由は、どこを見返しても見た部分だけで理解できるようにするためです。
Git概要
この節は、Gitとはどういう機能を提供するソフトウェアなのか、またその内部構造がどのようになっているのかを、Gitを構成する主要な用語を説明することで大まかに理解することが目的です。
Gitとは?
ある一時点のファイル内容やディレクトリ構造の状態を記録し、それを利用して現在との比較や過去の状態を確認、または巻き戻したりすることができるソフトウェアです。
一時点の状態のことをバージョンといい、それを管理することからバージョン管理システムと呼ばれています。その中でも、Gitのアーキテクチャは分散型管理システムを採っています。
分散型管理システムとは、1つのリポジトリでバージョンを管理するのではなく、各自のパソコン上に独自(ローカル)リポジトリを持ち、そこで各々が管理を行うシステムです。バージョンを共有する場合は、GitHubといったホスティングサービスを利用して共有の(リモート)リポジトリを置き、個人の(ローカル)リポジトリとデータを同期させながら管理します。
リポジトリ
ファイルやディレクトリの変更履歴を保存する場所です。リポジトリは2種類あり、それぞれリモートリポジトリとローカルリポジトリです。
リモートリポジトリ
リモートリポジトリは、GitHubなどのホスティングサービスや社内サーバーといったネットワーク上にあるもので、複数人で変更履歴の管理をする際に使用します。
ローカルリポジトリの作業内容をリモートリポジトリにアップロード(Push)したり、リモートリポジトリの内容をローカルリポジトリに取り込む(Pull)ことで同期します。
ローカルリポジトリ
ローカルリポジトリは、手元マシンに置き個人で使用するものです。実体はファイルシステム上の1つのディレクトリで、その中には.git
ディレクトリがあり、objects
やconfig
、index
といったファイルやディレクトリなど履歴管理に必要な情報が格納されています。
3つの領域
ローカルリポジトリ内にはリポジトリだけでなく、さらにワークツリーとインデックス(もしくはステージングエリア)と呼ばれている領域があります。
ワークツリー
ファイルやディレクトリの編集作業を行う場所です。ワークツリーの実体は、.gitの親ディレクトリーです。
ワークツリーで行った作業内容をリポジトリに保存する場合、後述するインデックスに一度登録する必要があります。また、インデックスに登録することをaddといいます。
インデックス
ローカルリポジトリに変更内容を保存することをcommitといいます。インデックスは次回のcommitに含める予定の、内容変更されたファイルを置いておく場所です。ステージングエリアとも呼ばれています。実体は、.git
ディレクトリ内にあるindex
ファイルです。
つまり、「ワークツリーで作業を終えたら内容をaddし、その後commitを実行する。」という手順を必ず踏みます。
なぜ必要なのか
インデックスには、最終確認の役割があると思います。直接commitすることによる、間違ったファイルを保存してしまったり、逆に漏れが生じてしまうことを軽減できます。
また、工程が1つ増えるということは、柔軟性が増すということでもあります。例えば、「複数の変更内容の内半分を今、残りを次回のcommitで保存する。」というようにです。
リビジョン
リポジトリ内にあるコミット履歴の1つ1つのことで、単にコミットとも呼ばれます。Git公式リファレンスではコミットと呼ぶほうが多かったので、この記事でもコミットと呼んでます。
具体的には、commitを実行した時点での全てのファイルやディレクトリの状態のことです。これを管理することがGitの本質です。詳しくは「こちら」で説明しています。
ブランチ
コミットの連なりのことです。リポジトリには1つ以上のブランチが存在します。最初に作成されるブランチをmainブランチといいます。mainは、バージョン管理の中心となる主要なブランチです。
他にも任意で複数のブランチを作成することができ、それらを総称してトピックブランチといいます。一般的にこのブランチの役割は、バグの修正や新しい機能の開発などといった特定の目的のためです。そのため一時的に作成され、役目を果たすと最終的にmainブランチに統合されます。
また、複数人が並行して作業を行うときにもブランチは活躍します。各々が自分専用のブランチで作業することで、他人の作業に影響を与えたり、または受けたりすることを防ぐことができます。つまり、ブランチは開発を効率的に進めるのに役立ちます。
mainブランチは、2021以前までmasterという名称で扱われていました。そのため、古いバージョンのGitではデフォルト名がmasterになっています。
merge
ローカルリポジトリ内の異なる2つのブランチ間のコミットや、リモートとローカルリポジトリを跨いだブランチ間のコミットを統合することです。
コンフリクト
mergeを実行したとき、コンフリクトが発生するとmerge処理が一時中断されます。コンフリクトとは、コミット間で同じファイルの同じ部分を変更していたり、リポジトリ間でコミット履歴の整合性が損なわれている場合、Gitはどちらの状態にすることが正しいのか判断できません。そのとき、処理を止めるために起こすエラーのことです。
その他の用語
Tracked file
Git管理対象にあるファイルのことです。具体的には、一度でもインデックスに登録(add)されたことがあるファイルです。
Untracked file
Git管理対象外のファイルのことです。具体的には、一度もインデックスに登録(add)されたことがないファイルです。Gitはこれに対して何の処理もせず放置します。
チェックアウト
操作対象をカレントブランチから別のブランチに切り替えること、もしくは特定のコミット内容をワークツリーに展開することを指します。
HEAD
ある1つのコミットを示す参照(ポインタ)です。一般的には、カレントブランチ上の最新コミットを示すポインタで、チェックアウトするとブランチが切り替わるとともにHEADも移動先の最新コミットを示すものに置き換わります。
また、コマンド操作によって任意のコミットを示すようにもできます。
.gitignore
commit処理時に、任意のファイルやディレクトリを無視(対象外)して実行するようにしてくれるファイルです。詳しくは、「こちら」で説明しています。
cloneとfork
cloneは、ローカルリポジトリに、他人もしくは自分のリモートリポジトリの内容を複製することです。
forkは、自分が管理しているリモートリポジトリに、他人のリモートリポジトリの内容を複製することです。
ブランチの詳細
この節では、ブランチの種類について説明します。各ブランチの関係を示した概要図は次節にあります。
ローカルブランチ
ローカルリポジトリ上に存在する、1つまたは複数のブランチのことです。チェックアウト中のものをカレントブランチといいます。
リモートブランチ
リモートリポジトリ上に存在する、1つまたは複数のブランチのことです。
上流ブランチ
ある1つのローカルブランチが、履歴を追跡する1つのリモートブランチのことです。追跡するとは、ブランチの内容(コミット履歴)を同じ状態にすることです。
正体
上流ブランチはローカルブランチやリモートブランチ、後述するリモート追跡ブランチの様に各リポジトリに存在するブランチではありません。
また、ローカルブランチはリモートブランチと直接ではなくリモート追跡ブランチを介して紐付けられています。
つまり、厳密には上流ブランチの正体はリモート追跡ブランチであり、それの別称ということです。
リモート追跡ブランチ
ある1つのリモートブランチを追跡するブランチのことです。ローカルリポジトリ上に存在します。
例えば、リモート追跡ブランチ名がorigin/main
だとすると、「origin(リモートリポジトリ)のmainブランチを追跡している」ということです。
役割
直近にpullもしくは、push、fetchしたリモートブランチの状態が保持されているため、ネットワークを介さずにローカル環境で前回の内容が確認できます。
統合コマンド
ブランチを統合するためのコマンドはいくつかあり、それぞれ役割が異なります。この節では、各統合コマンドを説明します。
commit
インデックスの内容をカレントブランチ(ローカルブランチ)に追加します。commit時にはコミットメッセージが入力必須です。
コミットメッセージ
自分もしくは他者が後からコミット履歴を見直した際に、変更内容や変更理由が容易に理解できるように記述します。
エディタもしくはオプション(-m)で直接入力します。基本の形式は1行目に変更内容の要約、空白を挟んで3行目に変更理由を記述します。オプションを利用して複数行入力したい場合は、下記のようにするとオプション毎に改行されます。
$ git commit -m "内容要約(1行目)" -m "空白(2行目)" -m "変更理由(3行目)"
基本操作
commitを実行するにはまず、内容変更したファイルをadd(インデックスに登録)する必要があります。
$ git add <ファイル名> # 引数に指定したファイルをインデックスに登録します。
$ git add -A # Untracked fileも含めて全てインデックスに登録します。
git status
コマンドで、ワークツリーやインデックスの状況を確認できます。つまり、ファイルがaddされているか確認できます。
$ git status
On branch main
Your branch is up to date with 'origin/main'.
Changes to be committed: # addされたファイルです。
(use "git restore --staged <file>..." to unstage)
modified: test.txt
Changes not staged for commit: # 内容変更したがまだaddしていないファイルです。
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: newTest.txt
Untracked files: # Git管理下にないファイルです。
(use "git add <file>..." to include in what will be committed)
sample.txt
最後にcommitを実行します。-m
オプションを使用しない場合、エディタが起動するのでコミットメッセージを記述します。エディタの使い方は「こちら」です。
$ git commit # インデックスの内容をローカルリポジトリにcommitします。
git commit -a # Untracked fileを除く、全ての変更したファイルをaddを省略してcommitします。
git commit -m "" # コミットメッセージを引数に記述できます。
-a
オプションは、addとcommitを一括で行うことができます。但し、Tracked fileのみ有効で、Untracked fileは対象外なのでこのオプションを使用することは実行できません。
fetch
リモートブランチの内容をリモート追跡ブランチに反映させます。pullではなく一旦fetchすることで、リモートブランチの作業内容を把握したり、ローカルブランチと比較しコンフリクト発生の有無を確認できます。
基本操作
$ git fetch <リモートリポジトリ名> <ブランチ名>
# 指定したリモートリポジトリのブランチの履歴をfetchします。
$ git fetch
# 引数を指定しない場合、カレントブランチの上流ブランチに、リモートブランチをfetchします。
# 概要図の場合、リモート追跡ブランチorigin/mainにリモートブランチmainをfetchします。
# ※上流ブランチが設定されていない場合、リモートリポジトリ全てのブランチをfetchします。
$ git fetch <リモートリポジトリ名>
# ブランチ名のみ省略すると、リモートリポジトリ全てのブランチをfetchします。
# 概要図の場合、リモート追跡ブランチorigin/mainとorigin/developに、
# リモートブランチmainとdevelopをfetchします。
merge
任意のブランチをカレントブランチに統合します。
基本操作
$ git merge <ブランチ名>
# カレントブランチをベースに、引数に指定したブランチをmergeします。
$ git merge
# 引数を指定しない場合、カレントブランチに上流ブランチをmergeします。
# ※上流ブランチが設定されていない場合、mergeが実行されません。
仕組み
mergeが実行されると、対象コミットの状態によって2通りの処理が行われます。それぞれfast-forward mergeと3-way mergeです。
fast-forward merge
merge元(topic)のブランチを作成してから、merge先(main)ブランチで一度もコミットしていない場合、fast-forward mergeが行われます。
fast-forward mergeは、新しいコミットを作成するのではなく、merge先(main)のHEADをmerge元(topic)のコミットに移動させるだけです。つまり、merge先(main)ブランチがmerge元(topic)のコミット履歴を共有するということです。
3-way merge
一方、merge元(topic)のブランチを作成してから、merge先(main)ブランチでコミットが追加されている場合3-way mergeが行われます。
3-way mergeは、merge元(D)、merge先(C')、それらの共通の祖先(B)の3つのコミットを用います。まず、共通の祖先(B)とmerge元(D)の差分を抽出します。次に共通の祖先(B)とmerge先(C')の差分を抽出します。そして共通の祖先(B)にそれらを結合したものを適用して、新しいコミット(M)を作成します。最後に、merge先(main)のHEADがそのコミット(M)を指します。もし、差分を結合する際にコンフリクトが発生するとmergeは中断され、解決してmergeし直すか中止しなければいけません。
確認方法
git reflog
コマンドで、どちらのmergeが行われたか確認できます。fast-forwardによってmergeされた場合は、下記のように表示されます。
$ git reflog
4aedf9e (HEAD -> main, other) HEAD@{0}: merge other: Fast-forward
rebaseコマンド
ブランチ間を統合するコマンドは、他にもrebaseがあります。
rebase
ブランチの統合だけでなく、コミットの修正、削除、1つのコミットを複数コミットに分割、複数コミットを1つにまとめる、コミット履歴の並べ替え、を行うことができるコマンドです。
ブランチの統合におけるmergeとの違いは、mergeのようにマージコミットを作成して各ブランチを統合するのではなく、統合先ブランチの先頭に統合元ブランチを直接付け加えるところです。
下記は公式リファレンスから引用です。
まずふたつのブランチ (現在いるブランチとリベース先のブランチ) の共通の先祖に移動し、現在のブランチ上の各コミットの diff を取得して一時ファイルに保存し、現在のブランチの指す先をリベース先のブランチと同じコミットに移動させ、そして先ほどの変更を順に適用していきます。
上画像の場合、カレントブランチが透明のX,Yコミットがある方で、リベース先ブランチとの共通先祖はAコミットです。まず、X, Yの変更内容を取得し一時ファイルに保存します。次にカレントブランチのHEADをリベース先ブランチのCにします。そこに一時ファイルの内容を適用していくことでブランチを統合させます。
つまり、rebaseによって先端に付け加えられたコミット履歴は新しいものです。元々のX, Yコミットと、Cコミットの先につ付け加えられたX, Yは内容は同じですが、別の新しいコミットとして生成されました。また、rebaseも一時ファイルを適用する際に、内部的にmerge処理されています。
基本操作
下記のコマンドは、カレントブランチのコミット履歴が、引数で指定した<ブランチ名>の先端に移動します。
$ git rebase <ブランチ名>
その他の活用法
ブランチの統合以外の、コミット履歴の移動、修正、削除などはrebaseコマンドにオプション-i
を指定しての操作になります。詳細は「こちら」です。
pull
fetchとmergeの処理をまとめて行います。最後にpullを実行してからローカルブランチに変更がない場合、fast-forward mergeが行われます。つまり、リモートブランチ内の前回から新しくできたコミットをコピーして、ローカルの先頭に追加しHEADを移動させるだけです。
一方、最後にpullを実行してからローカルブランチに変更があった場合、3-way mergeが行われます。そして、コンフリクトが発生しなければマージコミットが作成されます。
注意点
merge処理よりもfetchを先に実行するため、もしmergeでコンフリクトが発生しそれを中止したとしても、mergeは取り消され実行前の状態に戻りますが、リモート追跡ブランチにはfetchが適用された状態のままです。
基本操作
$ git pull <リモートリポジトリ名> <ブランチ名>
# まず引数で指定したリモートブランチをリモート追跡ブランチにfetchします。
# 次にfetchした内容をカレントブランチにmergeします。
$ git pull
# 引数を指定しない場合、カレントブランチの上流ブランチに、リモートブランチをfetchします。
# 次にカレントブランチに上流ブランチをmergeします。
# ※上流ブランチが未設定だと、全てのリモートブランチをfetchするが、mergeは実行しません。
$ git pull <リモートリポジトリ名>
# 引数にリモートリポジトリのみ指定した場合、リモートブランチ全てfetchします。
# 次にカレントブランチに上流ブランチをmergeします。
# ※上流ブランチが未設定だと、全てのリモートブランチをfetchするが、mergeは実行しません。
push
ローカルブランチのコミット履歴を、リモートブランチとリモート追跡ブランチ両方に反映させます。
基本操作
$ git push <リモートリポジトリ名> <ブランチ名>
# ローカルブランチと同名のリモートブランチとリモート追跡ブランチにコミット履歴を反映させます。
$ git push
# 引数を指定しない場合、カレントブランチを紐付けたリモートブランチと上流ブランチにpushします。
# 上流ブランチを設定していない場合、pushは実行されません。
pushに引数を指定しない場合の挙動は、Gitのバージョンによって異なるようです。Git2.0以降は、デフォルトでsimpleモードに設定されています。公式リファレンスに次のように説明されています。
simple - push the current branch with the same name on the remote.
If you are working on a centralized workflow (pushing to the same repository you pull from, which is typically origin), then you need to configure an upstream branch with the same name.
This mode is the default since Git 2.0, and is the safest option suited for beginners.
つまり、引数にブランチ名を指定せずに実行すると、カレントブランチを上流ブランチにpushします。但し、それら2つのブランチは同名でないといけないそうです。
データ管理の仕組み
この節では、コミットがローカルリポジトリでどのように管理されているのか知るために、コミットの概要と、addとcommitコマンド実行時の内部挙動を説明します。
コミットはスナップショット
Gitは、ファイルやディレクトリの変更履歴をスナップショットで保存しています。スナップショットとは、ある時点のファイルやディレクトリの状態を丸ごとコピーしたものです。つまり、Tracked fileやディレクトリ全てを1つのコミットとして管理しています。
Gitが扱うデータはオブジェクト
Gitは、ファイル内容、ディレクトリ構造、コミット、注釈付きタグ、をオブジェクトと呼ばれているデータ構造で扱っています。
作成手順
各オブジェクトは以下の手順で作成されます。最初に、オブジェクトの種類と内容サイズを元にヘッダーが作成されます。ヘッダーの形式はblob #{content.length}\0
で、左からオブジェクトの種類、内容のサイズ、ヌルバイト(終端を示す)を表しています。
次に、オブジェクト内容とヘッダーを結合させ、Zlibライブラリを用いて圧縮ファイルにします。その圧縮ファイルからSHA-1を用いてハッシュ値を算出します。ハッシュ値は40文字で、先頭2文字分がサブディレクトリ名で残りがオブジェクトIDに割り当てられます。
最後に、IDを付与されたオブジェクトがリポジトリ(.git/objects
ディレクトリ)に格納されます。
サンプルコード
下記のコマンド出力結果がオブジェクトの例です。
# このコマンドは指定したディレクトリ内にある、ファイル形式のみを出力します。
$ find .git/objects -type f
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30
# /83/がサブディレクトリで、残りがオブジェクトIDです。
IDはランダム値ではない
SHA-1ハッシュ関数は、入力する値が同じものなら同じ値を返します。そのため、以降のコマンド結果にあるオブジェクトIDは、ファイルやディレクトリを全く同じように作成すると同じIDになります。つまりランダムな値ではありません。但し、後述するcommitオブジェクトは、コミット日時や作成者名が環境によって異なるため、同じ値にはなりません。
blobオブジェクト
ファイルの内容だけを保持します。例えば、画像、テキストファイルなど、あらゆる種類のファイルがblobオブジェクトとして扱われます。あくまでも中身だけなのでファイル名や属性は含まれません。
サンプルコード
例えば、内容がversion 1
のtest.txt
を作成しcommitすると、83baae61804e65cc73a7201a7252750c76066a30
というオブジェクトが作成されます。それをいくつかのコマンドで確認してみると以下の結果になります。
# このコマンドは、指定したオブジェクトのタイプを出力します。
$ git cat-file -t 83baae61804e65cc73a7201a7252750c76066a30
blob
# このコマンドは、指定したオブジェクトの中身を出力します。
$ git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30
version 1
つまり、83baae
からはじまるIDのオブジェクトはblobであり、version 1
という内容だけを保持していることがわかります。
ファイル名は持たない
ファイル名が異なっているが内容が同じファイルは、1つのblobオブジェクトを共有します。各ファイル毎にblobオブジェクトが作成されることはありません。
先ほどの例の状態に、さらに内容がversion 1
のexam.txt
を作成しcommitしてみます。すると以下の結果になります。
$ git cat-file -p cb8f87a17f4b1b4102d4ac774bbc35544012fbf # これは後述するtreeオブジェクトです。
100644 blob 83baae61804e65cc73a7201a7252750c76066a30 exam.txt
100644 blob 83baae61804e65cc73a7201a7252750c76066a30 test.txt
コマンドの引数に指定したIDは、後述するtreeオブジェクトです。この出力結果はルートディレクトリを表示しています。2,3行目の3列目がオブジェクトIDで4行目がそれに対応するファイル名です。結果からわかる通り、exam.txt
のオブジェクトIDはtest.txt
と同じです。
つまり、blobオブジェクトはファイル内容しか扱っていないため、ファイル名を考慮していないということがわかります。Gitは重複した内容のオブジェクトを保持しない効率的な管理をします。
treeオブジェクト
1つのディレクトリ構造を保持しています。具体的には、ディレクトリに格納されているファイルやサブディレクトリのパーミッション、オブジェクトの種類、オブジェクトID、ファイル名です。
つまり、treeオブジェクトは1つのディレクトリを表していて、そのディレクトリに格納されているblob(ファイル)やtree(ディレクトリ)の情報を保持しています。
サンプルコード
例えば、ルートディレクトリにtest.txt
とlib
ディレクトリがあり、lib
ディレクトリには空のsimplegit.rb
があります。commitを実行するといくつかのオブジェクトが生成されますが、そのうちの34d9a7c0251e9ae22ad610b56265731bc6cdba32
と8ede28eed8c7a5113ba5a8aa9704fa6017a996c7
を確認すると下の結果になります。
$ git cat-file -t 34d9a7c0251e9ae22ad610b56265731bc6cdba32
tree
$ git cat-file -t 8ede28eed8c7a5113ba5a8aa9704fa6017a996c7
tree
# 出力結果は左から、[パーミッション]オブジェクトタイプ][オブジェクトID][ファイル/ディレクトリ名]
$ git cat-file -p 34d9a7c0251e9ae22ad610b56265731bc6cdba32
040000 tree 8ede28eed8c7a5113ba5a8aa9704fa6017a996c7 lib
100644 blob 83baae61804e65cc73a7201a7252750c76066a30 test.txt
$ git cat-file -p 8ede28eed8c7a5113ba5a8aa9704fa6017a996c7
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 simplegit.rb
つまり、34d9a7c
と8ede28
からはじまるオブジェクトはtreeであり、各々が保持している内容から34d9a7c
はルートディレクトリで8ede28
はlibディレクトリということがわかります。
commitオブジェクト
commitを実行したときの情報を保持しています。具体的には、treeオブジェクトID、親commitオブジェクトID、作者とcommitしたユーザの名前とアドレス、commit日時、commitメッセージです。
このオブジェクトが持つtreeオブジェクトは、ルートディレクトリ構造を保持するtreeオブジェクトIDです。そこからtree内にあるtree、またその中にあるtreeと辿っていくことで全体のファイルシステム構造を把握することができます。つまり、スナップショットの正体です。
サンプルコード
# 引数のオブジェクトIDは、commitオブジェクトIDです。
$ git cat-file -p b362048e2db95f77c9c04982a4e5b927d98ec72f
tree 34d9a7c0251e9ae22ad610b56265731bc6cdba32 # treeオブジェクトID
parent 6b6a85977ce2800129ac3f7be2f910bd6d56157a # 親commitオブジェクトID
author 名前 <メールアドレス> 1737120694 +0900 # コードを書いた人
committer 名前 <メールアドレス> 1737120694 +0900 # commitを実行した人
add lib dir and simplegit.rb # コミットメッセージ
先述しましたが、commitオブジェクトIDはコミット日時や作成者名が環境によって異なるため、同じ値にはなりません。
tagオブジェクト
タグの役割はコミットに印をつけることです。軽量版と注釈付き版の2種類あり、軽量版はcommitオブジェクトID値を示すポインタです。一方、注釈付き版はcommitオブジェクトIDだけでなく、作者名とメールアドレス、作成日時、タグメッセージが格納されたオブジェクトです。
サンプルコード
実際に、obj_tag
という名前の注釈付き版を作成してみました。すると.git/refs/tags
内にタグが作成され、内容は以下でした。
$ cat .git/refs/tags/obj_tag
32441c1813adf3bba0820db88853acfac23706ce
タグ内に記載されたオブジェクトIDの内容を調べると以下でした。
# 2行目から、[オブジェクトID][オブジェクトの種類][タグ名][作成者][タグメッセージ]
$ git cat-file -p 32441c1813adf3bba0820db88853acfac23706ce
object b362048e2db95f77c9c04982a4e5b927d98ec72f
type commit
tag obj_tag
tagger 作者名 <メールアドレス> 1737200301 +0900
test
つまり注釈付き版は、指定したコミットの情報を持つtagオブジェクトが作成され、そこにタグが付くということです。現実で例えると、品物をダンボールで梱包しそこにラベルを貼るということです。軽量版の場合は、指定したコミットに直接付きます。
addコマンドの挙動
コマンドを実行すると以下の順序で処理されます。まず、add対象ファイルのblobオブジェクトがリポジトリ(.git/objects
)に作成されます。
次に、インデックス(index
ファイル)にblobオブジェクトIDとそれに対応するファイル名などの情報が紐付けられます。

サンプルコード
実際に内容がhtmlファイル
のsample.html
を作成し、git add sample.html
を実行してみます。その後、インデックスの内容を確認してみると以下のようになっています。
# このコマンドはインデックスの内容を表示します。
# 出力結果は左から、[パーミッション][オブジェクトID][ステージ番号][ファイルorディレクトリ名]
$ git ls-files --stage
100644 83baae61804e65cc73a7201a7252750c76066a30 0 exam.txt
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 lib/simplegit.rb
100644 7b273b328bb9e7e9500e52b2e6ce2dac6cedb56d 0 sample.html # これ
100644 83baae61804e65cc73a7201a7252750c76066a30 0 test.txt
addしたことによって、7b273b
から始まるblobオブジェクトが作成され、それとsample.html
のファイル情報が紐づけられます。
ちなみに、結果の3列目の0
は、ステージ番号と呼ばれるものです。詳しくは「こちら」で説明しています。
addを取り消すとどうなるのか?
addを実行した後、その行為をgit restore --staged
などのコマンドで取り消した場合、インデックスの内容は取り消されますが、blobオブジェクトはそのままリポジトリに存在したままです。gitには自動でガベージコレクションを実行してくれる機能があり、どこからも参照されないオブジェクトを削除してくれます。但し、ある程度の数に達するまでは実行されないようです。
commitコマンドの挙動
コマンドを実行すると以下の順序で処理されます。インデックスの内容の内、変更があったファイルを格納しているディレクトリに対応するtreeオブジェクトが作成されます。もしそれがルートオブジェクトではない場合、そのオブジェクトの情報を保有している1階層上のtreeオブジェクトも内容が変わるため、新しいtreeオブジェクトが作成されます。これをルートオブジェクトまで続けることで、新しいtreeオブジェクト全体を構築します。
最後に、新規のtreeオブジェクト(ルートディレクトリに対応する)や親commitオブジェクトIDなどの情報を持つcommitオブジェクトが作成されます。

効率的なスナップショット管理
この節の冒頭で、コミットはスナップショットと説明しましが、Gitはcommitの度に毎回ファイルの完全なコピーを保存しているわけではありません。上記の画像を見ればわかりますが、commitが実行されると内容変更があった(addされた)ファイルのみオブジェクトが作成されます(初めてaddされたファイルも作成されます)。それ以外の変更がなかったファイルは何もされません。
この理由は、commitオブジェクトはtreeオブジェクトの参照(ポインタ)を、treeオブジェクトはblobオブジェクトのポインタを保持しているにすぎないからです。あくまでもblobオブジェクトがファイル内容を保持しています。commitやtreeオブジェクトは、自身が持つポインタ情報の内、変更があったファイルに対するblobオブジェクトへのポインタを新しいものに書き換え、変更がなかったファイルのポインタは使いまわします。
つまり、ある時点での物理的な完全なファイル群ではなく、ポインタを組み合わせることで完全なスナップショットとしています。ある1つのコミットでは、初めてaddされたファイルや変更があったファイルに対してのみオブジェクトが新規作成されるだけなので、リポジトリ内に重複するオブジェクトが存在することはありません。これによって、ストレージの容量を無駄なく効率的に利用することができます。
例えば、リポジトリを新規作成しsample.txt
をcommitしたとします。そのファイルを一切変更せずcommitを4回行い、計5つのコミットがあるとします。この状況でもリポジトリ内にsample.txt
のblobオブジェクトが5つ存在することなく1つしかないのは、commitやtreeオブジェクトがポインタを保持しているからです。
初期設定
この節では、手元のマシンにローカルリポジトリを、GitHub上にリモートリポジトリを作成しそれらを紐付けする方法と、cloneを利用する方法を説明します。
ローカルリポジトリの作成
リポジトリとして扱いたいディレクトリ上でgit init
コマンドを実行します。すると初期化処理が行われ、ディレクトリ内に.git
ディレクトリが作成さます。
コミット作業
ローカルリポジトリ内にコミットが1つもない状態だと、リモートリポジトリにpushすることがでません。下記のエラーが発生します。
$ git push -u origin main
error: src refspec main does not match any
mainブランチがローカルリポジトリに存在しないことによるエラーです。そのため、任意の作業を行いcommitするか、下記のコマンドを使って空コミットを作成しておきましょう。
$ git commit --allow-empty -m "コミットメッセージ" # 空のコミットを作成します。
personal access token発行
コマンドラインからHTTPS方式でGitHubにアクセスするには、パスワードの代わりにこのtokenが必要なので作成します。下記公式サイトに詳細な手順が掲載されています。
リモートリポジトリの作成
公式サイトに詳細な手順が掲載されています。
作成後に表示される一連のコマンドをローカルリポジトリ上で実行すると、リモートとローカルリポジトリが紐付けられ、さらにカレントブランチ(main)に上流ブランチ(origin/main)が紐付けられます。具体的なコマンドは以下です。
$ git remote add origin https://github.com/<ユーザー名>/<リモートリポジトリ名>.git
# ローカルリポジトリにリモートリポジトリを紐づけます。
$ git branch -M main
# ローカルカレントブランチ名をmainに変更します。
$ git push -u origin main
# ローカルブランチmainとリモートブランチmainを紐付けてpushします。
リモートリポジトリを複製する
cloneを利用してローカルリポジトリを作成する方法は「こちら」で説明しています。
場面毎の操作法
この節では、Gitをコマンドライン上で操作する方法を説明します。具体的には、使用頻度の高い基本操作方法とその内部でどういう処理が行われているのかや、誤操作による間違いや予期しない結果に対する解決方法です。
ワークツリー
この項は、ワークツリー内のファイル削除や作業内容の取り消し方法と、commitせずにブランチ切り替えを行った時の影響、についての説明です。
ファイルを削除する
$ git rm <ファイル名> # ワークツリーとインデックスから削除します。
$ git rm -f <ファイル名> # ワークツリーとインデックスから強制的に削除します。
$ git rm -r <ディレクトリ名> # ディレクトリを、ワークツリーとインデックスから削除します。
$ git rm --cached <ファイル名> # インデックスから削除します。
このコマンドは、ワークツリーとインデックスから、もしくはオプション使用でインデックスからのみファイルを削除します。但し、Untracked fileは削除することができません。
--cached
オプションの場合、インデックスから無くなりますがワークツリーにはUntracked fileとして残ります。つまり、Git管理対象外ファイルになるということです。
rmとの違い
LinuxのrmコマンドやVSCodeのGUI操作でファイルを削除した場合、ワークツリー内の処理なので手作業でaddしなければいけません。一方git rm
は、インデックス内で削除処理をするため、addされた状態となります。
$ rm sample.txt
$ git status
Changes not staged for commit:
deleted: sample.txt # rmを使用するとaddされていません。
$ git rm sample.txt
$ git status
Changes to be committed:
deleted: first.txt # git rmだとaddされています。
作業内容の取消し1 (git restoreを使用)
$ git restore <ファイル名> # 指定したファイルの変更を取り消します。
$ git restore . # 全ファイルの変更を取り消します。
このコマンドは、インデックスの内容をワークツリーに展開します。インデックス内に指定したファイルがない(変更内容をまだaddしていない)場合は、直近のコミット内容で上書きされます。その処理を利用してワークツリーの内容を取り消すということです。
注意点
しかし、変更内容をaddしてインデックスに登録している場合は、同じ内容をワークツリーに上書きすることになるため取り消されません。addを実行済みの場合は、一旦git restore --staged <ファイル名>
などで、インデックスから取り消すことでできるようになります。
※最新コミットを利用した処理のため、一度もコミットしていないファイル(Untracked file)はこの操作ができません。
作業内容の取消し2 (git checkoutを使用)
$ git checkout -- <ファイル名> # 指定したファイルの変更を取り消します。
$ git checkout -- . # 全ての変更を取り消します。
# 引数の「--」は、ブランチ名とファイル名の区別のためなので、同名のものがない場合省略しても
# 問題はありません。
Git公式リファレンスの説明は以下です。
Overwrite the contents of the files that match the pathspec. When the <tree-ish> (most often a commit) is not given, overwrite working tree with the contents in the index. When the <tree-ish> is given, overwrite both the index and the working tree with the contents at the <tree-ish>.
つまり、checkoutコマンドにファイル名のみを指定すると、インデックスの内容をワークツリーに展開します。(以下の内容は、「作業内容の取消し1」と同じです。)
注意点
インデックス内に指定したファイルがない(変更内容をまだaddしていない)場合は、直近のコミット内容で上書きされます。その処理を利用してワークツリーの内容を取り消すということです。
しかし、変更内容をaddしてインデックスに登録している場合は、同じ内容をワークツリーに上書きすることになるため取り消されません。addを実行済みの場合は、一旦git restore --staged <ファイル名>
などで、インデックスから取り消すことでできるようになります。
※最新コミットを利用した処理のため、一度もコミットしていないファイル(Untracked file)はこの操作ができません。
作業内容の取消し3 (git resetを使用)
$ git reset --hard # 全ての変更を取り消します。
このコマンドは、ワークツリーとインデックスの全てのファイルを最新のコミット内容に書き換えます。
実際にはHEADにも影響を及ぼしますが、今回は引数に特定のコミットを指定していないため、現在のHEADを指定したことになり結果変化していません。詳しくは以下の公式リファレンスで詳しく説明されています。
ブランチ切り替えによる影響
ブランチを切り替えると、ワークツリー上に切り替え先ブランチの最新コミットの内容が展開されます。もし、ワークツリー上でファイルに変更を加えてcommitせず別ブランチにチェックアウトすると、以下のような2通りの結果になります。
1つ目は、コンフリクトが発生するとエラーになりチェックアウトが実行されません。
2つ目は、コンフリクトが発生せずチェックアウトが実行され、ワークツリー上に切り替え先ブランチの最新コミットが展開されます。その内容に、元々あったワークツリー上の未commitの変更内容が反映されます。
検証
実際に、commitせずにチェックアウトするとワークツリーの内容がどう変化するのか確かめました。長くなるので折りたたみました。
内容
検証の準備
はじめに、ローカルリポジトリを作成しmainブランチでcommon.txt
を作成しcommitを実行。common.txt
の内容は、「全ブランチ共有ファイル」です。
次に、新たにAブランチを作成しそれにチェックアウトしprivate_A.txt
を作成しcommitを実行。private_A.txt
の内容は、「Aブランチ専用ファイル」です。
最後に、新たにBブランチを作成しそれにチェックアウトしprivate_B.txt
を作成しcommitを実行。private_B.txt
の内容は`「Bブランチ専用ファイル」です。
実験1 : ブランチ間で共通するファイルの内容を変更してチェックアウトするとどうなるか?
mainブランチでcommon.txt
の内容を「訂正」に変更して、commitせずAブランチに切り替えるとAブランチのcommon.txt
の内容も「訂正」でした。Bブランチに切り替えても同じでした。
この理由は、Git公式リファレンスに下記のように書かれていました。
git checkout [<branch>]
To prepare for working on , switch to it by updating the index and the files in the working tree, and by pointing HEAD at the branch. Local modifications to the files in the working tree are kept, so that they can be committed to the .
つまり、checkoutを実行するときワークツリー内に未commitの変更内容がある場合、それを維持したままチェックアウトします。
ちなみに、addを実行してインデックスにファイルを登録した状態で上記の実験1を行っても、同じ結果になりました。
実験2 : チェックアウト先に無いファイルを変更するとどうなるのか?
Aブランチでprivate_A.txt
の内容を「訂正」に変更して、commitせずBブランチに切り替えると、下記のエラーが発生しチェックアウトが中断されました。
error: Your local changes to the following files would be overwritten by merge:
private_A.txt
Please commit your changes or stash them before you merge.
変更内容を適用するファイルがブランチBにないためエラーが発生しました。
ちなみに、addを実行してインデックスにファイルを登録した状態で上記の実験2を行っても、同じ結果になりました。
実験3 : ブランチ間で共通するファイルを削除しチェックアウトするとどうなる?
mainブランチでcommon.txt
を削除してcommitせずAブランチに切り替えると、Aのcommon.txt
も削除されていました。Bブランチに切り替えても同じでした。
この理由は、実験1と同じくワークツリー内の変更内容が維持されたためです。
ちなみに、addを実行してインデックスにファイルを登録した状態で上記の実験3を行っても、同じ結果になりました。
実験4 : ブランチの固有ファイルを削除してチェックアウトするとどうなるか?
Aブランチでprivate_A.txt
を削除してcommitせずBブランチに切り替えると、エラーもなくチェックアウトは実行されます。そして、再度Aブランチに切り替えるとファイルが復活しています。
この理由は、Bブランチに存在しないファイルに対する変更処理なのでエラーが発生するはずが、変更内容が削除でかつBブランチに元々そのファイルがないという状態が一致したため、コンフリクトが発生せずチェックアウトが実行されたのだと思います。そして、変更内容が、存在しないファイルへの処理なのでワークツリーから破棄されたため、Aブランチに切り替えるとprivate_A.txt
が削除されず復活していたと思います。※あくまで憶測であり、間違っているかもしれません。
ちなみに、addを実行してインデックスにファイルを登録した状態で上記の実験4を行っても、同じ結果になりました。
実験5 : untracked fileがある状態でチェックアウトするとどうなるか?
Aブランチで新たにファイルを作成しcommitせずBブランチに切り替えると、ファイルは存在したままでした。どのブランチに切り替えても同じでした。
この理由は、untracked fileはGitの管理対象ではないため、何の処理もされず放置されるからです。
ちなみに、addを実行してインデックスにファイルを登録した状態で上記の実験5を行っても、同じ結果になりました。
つまり、意図しない変更をチェックアウト先のブランチに加えてしまう可能性があります。チェックアウトする際は必ずcommitを実行するか、stash領域を利用してワークツリー上を整理してから新たに別ブランチで作業することが推奨されます。
インデックス(ステージングエリア)
この項は、インデックス内のファイル削除や内容の表示と、addコマンドの取り消し方法についての説明です。
ファイルを削除する
「こちら」で説明しています。
インデックスの内容を確認する
$ git ls-files # Tracked fileの一覧を表示します。
test1.txt
test2.txt
$ git ls-files -m # 内容変更したが、addしていないファイル一覧を表示します。
test2.txt
$ git ls-files -s # blobオブジェクトとファイル名の対応表を表示します。
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 test1.txt
100644 ee4c5e68d6a2170b8ed4ff0fd28ebe2f9a41afb3 0 test2.txt
# 出力結果は左から、[パーミッション][オブジェクトID][ステージ番号][ファイルorディレクトリ名]
ステージ番号
0から3まであり、0はTacked fileに割り当てられます。残りの1から3は、merge処理でコンフリクトが発生した時に使用する番号です。merge先と元の共通の祖先の内容に1、merge先(カレントブランチ)の内容に2、merge元の内容に3がそれぞれ割り当てられます。
これらによってコンフリクト対象のファイル状態を明確に見分けられ、コンフリクトをどう解決するか判断材料にできます。
git addを取消す1 (git restoreを使用)
$ git restore --staged <ファイル名> # 指定したファイルのaddコマンドを取り消します。
$ git restore --staged <.> # 全てのファイルのaddコマンドを取り消します。
このコマンドは、インデックスを最新コミットで上書きすることで取り消しを行います。
git addを取消す2 (git resetを使用)
$ git reset <ファイル名> # 指定したファイルのaddコマンドを取り消します。
$ git reset --mixed HEAD <ファイル名> # 上のコマンドを省略せずに書いたものです。
$ git reset . # 全てのaddコマンドを取り消します。
このコマンドは、インデックスを最新のコミット内容に上書きします。
※実際にはHEADにも影響を及ぼしますが、1つ目のコマンドは引数に特定のコミットを指定していないため現在のHEADを指定したことになり、2つ目はHEADを指定しているため変化していません。
詳しくは以下の公式リファレンスで説明されています。
ブランチ
この項は、ブランチとコミット履歴の操作についての説明です。
上流ブランチを設定する
$ git push -u <リモートリポジトリ名> <ブランチ名>
引数で指定したブランチ名と同名のものをリモートリポジトリに作成し、それを上流ブランチとして紐付けます。そしてpushを実行します。
上流ブランチの確認方法
$ git branch -vv
* main a7a8c7a [origin/main] first commit
iss53 7e424c3 [origin/iss53: ahead 3] first commit of iss53 branch
serverfix f8674d9 [origin/serverfix: behind 1] first commit of serverfix branch
testing 5ea463a trying something new
# 出力結果は左から、[ローカルブランチ][最新commitオブジェクトID][リモートブランチ][コミットメッセージ]
このコマンドは、ローカルブランチの一覧と、各々に紐付いた上流ブランチが表示されます(*はカレントを示す)。また、コミット履歴のズレも確認することができます。
例えば、[origin/iss53: ahead 3]
だとリモートよりローカルが3つ分先行している(pushしていない)ことを、[origin/serverfix: behind 1]
だとローカルが1つ遅れを取っている(pullしていない)ということを示しています。trying something new
は追跡するブランチが設定されていないことを示しています。
※但し、この出力結果はローカルリポジトリ内にある、直近のfetchもしくはpush、pullした内容です。そのため必ずしも最新の情報ではありません。
ローカルブランチを作成する
$ git branch <ブランチ名>
指定した名前のブランチが既に存在する場合、エラーになります。
ローカルブランチを削除する
$ git branch -d <ブランチ名>
ローカルブランチ一覧を表示する
$ git branch
* main
other_branch
ブランチ名の先頭に「*」がついているのがカレントブランチです。
チェックアウトする
$ git checkout <ブランチ名> # 指定したブランチにチェックアウトします。
$ git switch <ブランチ名> # 指定したブランチにチェックアウトします。
$ git checkout -b <ブランチ名> # 指定したブランチを作成してチェックアウトします。
$ git switch -c <ブランチ名> # 指定したブランチを作成してチェックアウトします。
checkoutとswitchどちらを使用しても同じ結果になります。元々多機能なcheckoutコマンドがあり、その後チェックアウト関連の機能だけを備えたswitchが実装されたようです。
ブランチ名を変更する
$ git branch -m <旧名> <新しい名>
コミット履歴を表示する
$ git log
commit b362048e2db95f77c9c04982a4e5b927d98ec72f (HEAD -> main) # 直近のcommitオブジェクトID
Author: 作者名 <メールアドレス> # 作者とメールアドレス
Date: Sun Jan 19 11:06:43 2025 +0900 # コミット日時
add sample.txt # コミットメッセージ
commit 5ab1f484fb516e6bbf2b54d61bbcc0488a1f3295 # 1つ遡ったcommitオブジェクトID
# 以下省略
このコマンドは、カレントブランチの最新コミットから順番に出力します。出力結果はページャを使って出力されるため、終了する場合q
キーを入力する必要があります。
-p
オプションで、各コミットの変更点をdiff形式で表示します。
$ git log -p
--pretty=oneline
ロングオプションで、各コミットを1行で表示します。
$ git log --pretty=oneline
085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 removed unnecessary test
a11bef06a3f659402fe7563abf99ad00de2209e6 first commit
# 出力結果は、左から[commitオブジェクトID][コミットメッセージ]
--graph
ロングオプションで、コミット履歴をアスキーグラフで表示します。ブランチの流れが視覚的でわかりやすくなります。
$ git log --graph
* commit 9535725df00adb47d71a23a675c487bbcaf18390 (HEAD -> main, origin/main)
| Author: John Doe <XXX@XXX.XX>
| Date: Tue Jan 28 21:11:47 2025 +0900
|
| add sample.txt
|
* commit bee0e4875b7cc283c97b8fd6f0994842abbb994a
Author: John Doe <XXX@XXX.XX>
Date: Mon Jan 27 21:38:02 2025 +0900
first commit
--graph
を他のオプションと組み合わせると、より見やすく表示することができます。
--abbrev-commit
ロングオプションは、commitオブジェクトIDを省略形で出力します。
$ git log --graph --pretty=oneline --abbrev-commit
* a6d281 (HEAD -> main, origin/main) merge commit
|\
| * d3af835 other modify newTest.txt
* | 5ab1f48 main add newTest.txt
|/
* 4aedf9e second commit of other. modify test.txt
* 743f169 first commit of other. modify test.txt
* efa7d5c first commit
複数のブランチを指定すると、各ブランチのコミット履歴を組み合わせて表示します。
$ git log --graph --pretty=oneline --abbrev-commit main other
* de5df40 (other) other modify test.txt
| * af6d281 (HEAD -> main, origin/main) merge commit
| |\
| |/
|/|
* | d3af835 other modify newTest.txt
| * 5ab1f48 main add newTest.txt
|/
* 4aedf9e second commit of other. modify test.txt
* 743f169 first commit of other. modify test.txt
* efa7d5c first commit
直近のコミットを修正する
$ git commit --amend # 直近のコミットとコミットメッセージを修正します。
$ git commit --amend --no-edit # このオプションでコミットメッセージ修正を省略します。
$ git commit --amend --author="名前 <メールアドレス>" --no-edit # 作者情報も修正します。
$ git commit --amend -m "" # エディタを開かずコマンドラインでコミットメッセージを修正します。
git commit --amend
は、インデックスの変更内容を直近のコミットに反映させます。コミットメッセージは、エディタかオプション-m
で直接コマンドラインで記述します。エディタの使い方は「こちら」です。
注意
このコマンドは、直近のコミットに修正を加えるだけでなく、新しいコミットとして作り直します。
つまり、push済みのコミットにこのコマンドを実行してしまうと、新しいコミットに置き換わってしまうためリモートとローカルの整合性が失われてしまいます。その結果pushすることができなくなってしまいます。
もし、git push -f
などのコマンドで強制的にpushした場合、リモートリポジトリを共有する他者の作業に影響を及ぼし、回復に手間取らせてしまう可能性があります。
push済みのコミットにgit commit --amend
を実行してしまったら、下記コマンドを実行することで、直近のコミットをリモート追跡ブランチの状態に戻せます。それによってgit commit --amend
を無効にすることになります。
$ git reset origin/main # HEADをリモート追跡ブランチの状態に戻します。
# 引数の書式は、[リモートリポジトリ名/ブランチ名]
2つ以上前のコミットを修正する
$ git rebase -i HEAD~n # nに3を指定すると以下の結果になります。
pick d998000 first commit
pick 7150488 add test.txt
pick cfc4e96 add exam.txt
# Rebase a7a8c7a..d998000 onto a7a8c7a (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# 以下省略
$ git rebase --abort # rebase実行中に処理を中止したい場合は、このコマンドを実行します。
このコマンドを実行するとエディタが起動し、指定したコミットより新しいコミットが古い順に表示されます。例えば、HEAD~3と指定すると、HEAD~2、HEAD~、HEADの順番で表示されます。修正したいコミットが書かれた行の先頭の「pick」を「edit」に書き換えた後、保存しエディタを閉じます。エディタの使い方は「こちら」です。
するとHEADが「edit」に書き換えた古いコミットに移動しているので、任意の変更処理をします。それが終わったらaddしgit commit --amend
を実行します。
最後にgit rebase --continue
を実行してrebase処理を終了します。複数のコミットを「edit」にした場合、git rebase --continue
を実行すると、次の「edit」に書き換えたコミットにHEADが移動します。古いコミットから順番に移動していきます。
注意
このコマンドは、特定の古いコミットに修正を加えるだけでなく、新しいコミットとして作り直します。つまり、push済みのコミットにこのコマンドを実行してしまうと、新しいコミットに置き換わってしまうため、リモートとローカルの整合性が失われてしまいます。その結果pushすることができなくなってしまいます。
もしgit push -f
などのコマンドで強制的にpushした場合、リモートリポジトリを共有する他者の作業に影響を及ぼし、回復に手間取らせてしまう可能性があります。
push済みのコミットにgit rebase -i HEAD~n
を実行してしまったら、下記コマンドのようにすることで、rebaseする前の状態に戻せます。
$ git reflog
$ git reset --hard HEAD@{n} # reflog出力結果にある、戻りたい番号をnに入れます。
reflogコマンドの詳細は下記で説明しています。
git reflog
このコマンドは、Gitで実行した操作一覧を表示します。
仕組みは、commitやチェックアウト、rebaseなどHEADを移動させる度にHEADの位置を内部で記録しています。reflogのデータは .git/logs/
に保存されています。
$ git reflog
9928ffb HEAD@{0}: rebase (finish): returning to refs/heads/main
9928ffb HEAD@{1}: commit (amend): add third.txt
d998000 HEAD@{2}: rebase: fast-forward
7150488 HEAD@{3}: rebase (start): checkout HEAD~3
d998000 HEAD@{4}: commit: add two.txt
上記のコマンド結果の場合、git reset --hard HEAD@{4}
を実行することでgit rebase
操作の前の状態に戻すことができます。
コミットからファイルを削除する
$ git filter-branch --tree-filter 'rm <ファイル名>' HEAD
# カレントブランチにある全コミットから<ファイル名>を削除します。
$ git filter-branch --index-filter 'git rm --cached --ignore-unmatch <ファイル名>' HEAD
# 上記と同じ。(--ignore-unmatchはgit rmでのエラー回避のためです。)
git filter-branch
は、指定したブランチ内のコミット全てに、指定したコマンドを実行します。上記のようにHEADを指定すると、カレントブランチが対象になります。
注意
このコマンドは、コミット履歴を改変してしまうためとても危険です。push済みのコミットにこのコマンドを実行してしまうと、リモートとローカルの整合性が失われてしまいます。その結果、pushすることができなくなってしまいます。
もし、git push -f
などのコマンドで強制的にpushした場合、リモートリポジトリを共有する他者の作業に影響を及ぼし、回復に手間取らせてしまう可能性があります。
用途
このコマンドは例えば、「個人で利用していたリポジトリを、ネットワーク上に公開したり共有して利用するといった用途に変更するので、機密ファイルをコミット履歴から削除したい。」といった場合などに使用します。
内部挙動
--tree-filter
は、まず対象ブランチにある1つのコミット内容をワークツリーに展開し、指定されたコマンドを実行します。それをインデックスに追加して新しいコミットを作成する、ということを全ての対象コミットに繰り返し行います。
--index-filter
は、まず対象ブランチにある1つのコミット内容をインデックスに展開します。そして指定されたコマンドをインデックス上で実行し、新しいコミットを作成するということを全ての対象コミットに繰り返します。
--index-filter
は、ワークツリーにコミット内容を展開しないため、処理が高速です。
--tree-filter
は、ワークツリーの内容に対して任意のコマンドを実行するため、インデックスに登録されていないファイル(Untracked file)にも、「各コミットに追加する」といった処理をすることが可能です。
注意点
どちらかのコマンドを実行するとカレントブランチ内にある全コミットから、指定したファイルが削除されます。しかし、Git内部ではファイルが消えずに残ってしまっています。
その理由は、Gitの各コミットはファイル(Gitではオブジェクトとして扱っているため、以後オブジェクトと表現します)自体を保持しているのではなく、その参照(ポインタ)情報を格納しているからです。
そのため、コミット自体もしくは今回のようにコミット内ファイルの削除処理を行ってもそれはポインタを削除したことにすぎず、オブジェクト自体は.git/objects
に残っているのです。
そして、オブジェクト自体を削除するには、それをどこからも参照されていない状態にした上でgit gc
を実行する必要があります。gcはガーベジコレクションを任意に実行するコマンドで、どこからも参照されていないオブジェクトを破棄してくれます。
filter-branch
によって、対象オブジェクトを参照しなくなった新しいコミットが作成されますが、オブジェクトのポインタを保持する元々あったコミットは内部に残っています。その箇所が.git/refs/original
と.git/logs
の2つです。
まず.git/refs/original
は、結論から言うとバックアップが保存されています。公式リファレンスのfilter-branch
のオプションに以下の文が記載されています。
--original
Use this option to set the namespace where the original commits will be stored.
The default value is refs/original.
つまり、このコマンドは既存のコミット履歴を破壊してしまう危険な処理をするため、実行前の状態を自動的に保存してくれるということです。そのデフォルトの場所が.git/refs/original
です。
ちなみに、既にこのディレクトリにバックアップが存在する場合、filter-branch
を実行するとバックアップを上書きすることになるので、Gitはエラーでfilter-branch
の処理を止めてくれます。
A previous backup already exists in refs/original/
Force overwriting the backup with -f
エラー文で指示された通り-f
オプションを加えるとfilter-branch
を実行できるようになります。
filter-branch
を取消す
バックアップを利用してfilter-branch
実行前に戻す場合は、まず以下のコマンドを実行し、ファイルに記載されているオブジェクトIDをコピーします。
$ cat .git/refs/original/refs/heads/<カレントブランチ名>
次に、先ほど得たオブジェクトIDを以下のコマンドの引数に指定し実行すると、元の状態に戻ることができます。
$ git reset --hard <オブジェクトID>
次の.git/logs
は、様々なログデータが格納されているディレクトリで、reflogコマンド用のポインタが格納されています。HEADの遷移履歴を保持しているため、このディレクトリも削除する必要があります。
完全削除方法
つまり、ファイルを完全に削除するための大まかな流れを説明すると、対象オブジェクトへのポインタを持つものを全て破棄し、どこからも参照されていない状態にします。そしてオブジェクト自体を削除します。
具体的な手順は以下です。例えば、現在のコミット履歴が下記で、最新コミットに追加したpassword.txt
をローカルリポジトリから完全に削除したいとします。
$ git log
commit 427a2071aab0cea15aad487c5e6c6b5bb6266105 (HEAD -> main, origin/main)
Author: xxxx <xxx@xxxx>
Date: Fri Jan 24 20:50:54 2025 +0900
second commit. add password.txt # このコミットが話題の対象です。
commit 62181ed97a6a2eb1b2061a0f93775752524b8514
Author: xxx <xxx@xxx>
Date: Fri Jan 24 20:49:25 2025 +0900
first commit. add sample.txt
まず、最終的にファイルが完全に削除できたか確認するために必要なオブジェクトIDを探します。(ちなみにコミットもオブジェクトです。詳細は「こちら」です。)
# git logで得たcommitオブジェクトIDを使って目的ファイルのオブジェクトIDを調べます。
$ git cat-file -p 427a2071aab0cea15aad487c5e6c6b5bb6266105
tree e7fdb0e6421d8ed3c16f367113dad3c914610d04 # これを次に使います。
parent 62181ed97a6a2eb1b2061a0f93775752524b8514
author kohei <kamachi_80s@yahoo.co.jp> 1737719454 +0900
committer kohei <kamachi_80s@yahoo.co.jp> 1737719454 +0900
# 上記のtreeのIDをさらに調べます。
$ git cat-file -p e7fdb0e6421d8ed3c16f367113dad3c914610d04
100644 blob f3097ab13082b70f67202aab7dd9d1b35b7ceac2 password.txt # 目的のファイルです。
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 sample.txt
password.txt
のオブジェクトのポインタを持つコミット(commitオブジェクト)ID427a2071aab0cea15aad487c5e6c6b5bb6266105
と、password.txt
のオブジェクトIDf3097ab13082b70f67202aab7dd9d1b35b7ceac2
を覚えておきます。
次に、filter-branch
コマンドを実行します。
$ git filter-branch --index-filter 'git rm --cached --ignore-unmatch password.txt' HEAD
git log
で確認してみると、新しいコミットb6b05f
に変わっています。内容も確認してみると、password.txt
が見当たりません。
$ git log
commit b6b05f28ac835f21387b028d6b4a5116c669b7e2 (HEAD -> main) # 新しいコミット。
Author: kohei <kamachi_80s@yahoo.co.jp>
Date: Fri Jan 24 20:50:54 2025 +0900
second commit. add password.txt
commit 62181ed97a6a2eb1b2061a0f93775752524b8514
Author: xxx <xxx@xxx>
Date: Fri Jan 24 20:49:25 2025 +0900
first commit. add sample.txt
$ git cat-file -p b6b05f28ac835f21387b028d6b4a5116c669b7e2
tree ea6e17c94d59d819952b2c2cf0f1168864cee048 # これを次に使います。
parent 62181ed97a6a2eb1b2061a0f93775752524b8514
$ git cat-file -p ea6e17c94d59d819952b2c2cf0f1168864cee048
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 sample.txt
# password.txtが消えています。
一見削除できたように見えますが、先ほど覚えておいたIDを確認してみると、まだリポジトリ内に残っていることがわかります。
$ git cat-file -p 427a2071aab0cea15aad487c5e6c6b5bb6266105 # commitオブジェクトが存在しています。
tree e7fdb0e6421d8ed3c16f367113dad3c914610d04
parent 62181ed97a6a2eb1b2061a0f93775752524b8514
author xxx <xxx@xxx> 1737719454 +0900
committer xxx <xxx@xxx> 1737719454 +0900
$ git cat-file -p f3097ab13082b70f67202aab7dd9d1b35b7ceac2 # password.txtが存在しています。
100644 blob f3097ab13082b70f67202aab7dd9d1b35b7ceac2 password.txt
ローカルリポジトリ内部にあるものも全て削除したいので、まず下記のコマンドを実行してバックアップを削除します。
$ rm -rf .git/refs/original
次に、 下記のコマンドを実行してreflogのポインタを削除します。
$ rm -rf .git/logs/
ここで一旦、リポジトリ内のオブジェクトの状況を確認します。git fsck
は、リポジトリの整合性をチェックするコマンドで、どこからも参照されていないオブジェクトがあれば出力してくれます。出力結果のdangling commit
は、どこからも参照されていないコミットを示します。
先ほど覚えておいたcommitオブジェクトIDが表示されています。このような結果になっていれば、今のところ上手くいっています。
$ git fsck
Checking object directories: 100% (256/256), done.
dangling commit 427a2071aab0cea15aad487c5e6c6b5bb6266105 # password.txtのポインタを持つコミットです。
次に、下記コマンドを実行してオブジェクトを削除します。git gc
は、デフォルトで2週間以上経過した参照されていないオブジェクトを削除対象とします。すぐに反映させたいため、--prune=now
オプションを指定します。
$ git gc --prune=now
最後に確認を行います。削除したいcommitオブジェクトID、password.txt
のオブジェクトIDがリポジトリ内に存在しないので、コマンドが失敗しています。これで完全にリポジトリから削除することができました。
$ git cat-file -p 427a2071aab0cea15aad487c5e6c6b5bb6266105
fatal: Not a valid object name 427a2071aab0cea15aad487c5e6c6b5bb6266105
$ git cat-file -p f3097ab13082b70f67202aab7dd9d1b35b7ceac2
fatal: Not a valid object name f3097ab13082b70f67202aab7dd9d1b35b7ceac2
注意
もしも、対象ファイル(オブジェクト)のポインタを持つコミットをリモートリポジトリにpush済みの場合は、追加の手順が必要です。.git/refs/remotes
にリモートブランチの状態が、.git/logs/refs/remotes
にpushしたコミット履歴が残っています。そこにも削除したいファイルを参照しているcommitオブジェクトが残っている可能性が高いので、rm -rf
してからgit gc
を行いましょう。
注意
公式リファレンスでは、git filter-branch
は非推奨となっています。
git filter-branch has a plethora of pitfalls that can produce non-obvious manglings of the intended history rewrite (and can leave you with little time to investigate such problems since it has such abysmal performance). These safety and performance issues cannot be backward compatibly fixed and as such, its use is not recommended. Please use an alternative history filtering tool such as git filter-repo.
代替コマンドとして、git filter-repo
が推奨されています。このコマンドはインストールが必要です。詳しくは下記公式GitHubで説明されています。
コミットを取消す
$ git reset --soft HEAD~{n} # コミットのみ取り消します。
$ git reset --mixed HEAD~{n} # コミットを取り消し、インデックスの内容を変更します。
$ git reset --hard HEAD~{n} # コミットを取り消し、インデックスとワークツリーの内容を変更します。
このコマンドは、HEADが指すコミットを、指定したコミットの位置まで移動させます。この動作によってコミットを取り消す結果になります。
例えば、現在ブランチが、(最古)〇--->〇--->〇--->〇(HEAD)このような状態として、git reset --soft HEAD~{2}
を実行すると、(最古)〇--->〇(HEAD)--->〇--->〇(最新)このようにHEADが移動します。その結果、移動先のHEADより最新のコミットを取り消すことになります。
影響範囲が変わる
オプションの--mixed
を付けて実行するとHEADの位置を移動するだけでなく、インデックスの内容が移動先のコミットの内容に書き換わります。--hard
の場合は、インデックスだけでなく、ワークツリーの内容も移動先のコミットの内容に書き換わります。
もし、現在の作業が未commitで、内容を保持したい場合はsoftを使いましょう。
注意
push済みのコミットにこのコマンドを実行してしまうと、リモートとローカルの整合性が失われてしまい、pushすることができなくなってしまいます。もし、git push -f
などのコマンドで強制的にpushした場合、リモートリポジトリを共有する他者の作業に影響を及ぼし、回復に手間取らせてしまう可能性があります。
push済みのコミットにgit reset
を実行してしまったら、下記コマンドのようにすることで、git reset
する前の状態に戻せます。
$ git reflog
$ git reset --hard HEAD@{n} # reflog出力結果にある、戻りたい番号をnに入れます。
# --softでも--mixedでもよい
reflogは「こちら」の最後で説明しています。
mergeを取消す
$ git reset --hard <コミット名> # マージコミットの1つ過去のコミットを指定します。
$ git reset --hard origin/<ブランチ名> # マージコミットができたブランチ名を引数に指定します。
$ git revert -m 1 <マージコミット名> # 詳しくは下記で説明します。
git reset --hard <コミット名>
は、<コミット名>にマージコミットの1つ過去のコミットを指定しHEADの位置を移動させ、merge前のカレントブランチのコミット状態に戻します。移動先より新しいコミットは参照されなくなるので、結果的にmergeを取り消したことになります。
git reset --hard origin/<ブランチ名>
は、HEADをリモート追跡ブランチの状態に戻すことで、mergeを取り消します。但し、マージコミットをpush済み場合、リモート追跡ブランチにも反映されているのでこのコマンドに意味はありません。
git rebert -m 1 <マージコミット名>
は、mergeで取り込まれる側の変更内容を打ち消します。その結果、merge前のカレントブランチのコミット状態に戻ります。コマンドの詳細は下記で説明します。
revert
指定したコミットの変更内容を打ち消す内容のコミットを作成するコマンドです。既存のコミットを取り消す際に使用します。
resetとの違い
resetはHEADの位置を移動させることでコミットを取り消すため、その行為が行われたことをコミット履歴から知ることができません。一方revertの場合は、新しいコミットが追加されるため、コミットを取り消した履歴を残すことになります。
注意
マージコミットを対象にrevertした場合、mergeを取り消したわけではないため実行済みとみなされます。その結果次にmergeすると、以前取り消したmergeの内容が反映されません。
上記の画像だと2回目に行ったmergeで作成したコミット(M2)には、testブランチのAコミットの内容は含まれていません。
基本操作
以下は、revertのコマンド例です。
$ git revert <commit> # 指定したコミットを打ち消すコミットが新しく追加されます。
$ git revert <commit> --no-edit # コミットメッセージを編集しません。
$ git revert <commit> -n # コミットせず、インデックスと作業ディレクトリに内容が反映されます。
エディタの使い方は「こちら」です。コミットメッセージを省略すると、Revert "<引数のコミットのコミットメッセージ>"
になります。
マージコミット
マージコミットは2つの親コミットを持ちます、そのためrevertする際にどちらの変更内容を打ち消すのか明確にしなければいけません。-m <番号>
オプションは指定した番号の変更内容を残し、それ以外を打ち消します。1がmerge先で、2がmerge元(取り込まれる側)です。例えば、mainにtopicをmergeする場合、1がmain、2がtopicになります。
番号はgit logで確認することができます。
$ git log
commit e6c442c4ec21dafbdcc43d70ed9c651fc27bb6fc
Merge: afdbc24 ded34ab # 1がmerge先、2がmerge元のcommitオブジェクトIDです。
Author: John Doe
Date: Fri Jan 3 19:09:41 2025 +0900
pushを取消す
$ git reset --hard HEAD~ # HEADの位置を変更することで取り消します。
$ git revert HEAD # コミットの内容を打ち消すコミットを作成することで取り消します。
resetの場合
push済みのコミットをresetコマンドで取り消した後pushすると、リモートとローカルの整合性が失われた状態になっているためコンフリクトが発生します。そのため、git push -f origin HEAD
などのコマンドで強制的にpushすることになります。
注意
この操作を行うとリモートリポジトリを共有する他者の作業に影響を及ぼし、回復に手間取らせてしまう可能性があります。
revertの場合
このコマンドは「こちら」の最後で説明しています。
安全策
revertの場合は、指定したコミットを打ち消す内容の新しいコミットが作成されます。つまり、push済みのコミットに手を加えません。その結果、整合性を保ったままなので、再度pushしてもコンフリクトが発生しません。resetと比べて安全なpushの取消し方ですが、余分なコミットが増えてしまいます。
pull取消し
$ git reflog
$ git reset --hard HEAD@{n} # reflogの出力結果の戻りたい番号をnにいれます。
reflogでpull操作前のコミットを調べて、resetで指定して実行します。但し、リモート追跡ブランチの状態は戻すことができません。reflogは「こちら」の最後で説明しています。
コンフリクト処理1 (mergeの場合)
$ git status
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths: # コンフリクト発生原因のファイルです。
(use "git add <file>..." to mark resolution)
both modified: index.txt
コンフリクトが発生したときにgit status
を実行するとUnmerged paths
に原因のファイルが表示されます。
中止する
merge実行中にgit merge --abort
コマンドを実行すると、merge処理が中止され実行前の状態に戻ります。
$ git merge --abort # mergeを中止します。
解決する
コンフリクト発生の原因になったファイルの中身には、次のようなテキストが挿入されます。
<<<<<<< HEAD:index.html
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
please contact us at support@github.com
</div>
>>>>>>> iss53:index.html
=======
から上が、mergeを実行したときのカレントブランチ(merge先)を、下がmerge元のブランチを表しています。どちらか任意の内容を選んだり組み合わせたりした後、それ以外の必要ない部分を削除します。
例えば、2行目の<div id="footer">contact : email.support@github.com</div>
を選んだのなら、それ以外の行をすべて削除します。修正を終えたらadd、commitを実行します。
コンフリクト処理2 (pullの場合)
中止する
pullよってコンフリクトが発生したとき、git merge --abort
コマンドを実行するとpullが中止され、実行前の状態に戻ります。しかし、fetchは実行されてしまうため、リモート追跡ブランチには反映されてしまいます。
git merge --abort # pullを中止します。
解決する
コンフリクトを解決する場合、merge時のコンフリクト処理と同じように原因ファイルを修正します。そしてadd、commitを実行します。
コンフリクト処理3 (rebaseの場合)
中止する
rebase実行中にgit rebase --abort
コマンドを実行すると、rebase処理が中止され実行前の状態に戻ります。
git rebase --abort # rebaseを中止します。
解決する
コンフリクトを解決する場合、merge時のコンフリクト処理と同じように、原因ファイルを修正しaddを実行します。その後は、git rebase --continue
コマンドを実行します。
git rebase --continue # rebaseを完了させます。
タグを作成する
$ git tag <タグ名> [<コミットID>] # 軽量タグを作成します。コミット指定がない場合HEADに付けます。
$ git tag <タグ名> -m "" [<コミットID>] # タグメッセージをコマンドラインで記述。
$ git tag -a <タグ名> [<コミットID>] # 注釈タグを作成します。コミット指定がない場合HEADに付けます。
-a
オプションを付けることで注釈付き版タグを作成することができます。
軽量版と注釈付き版どちらも、1つのコミットに対して複数付けることができます。
タグメッセージは、エディタもしくはオプション-m
でコマンドラインに直接記述します。エディタの使い方は「こちら」です。
タグ情報を表示する
$ git tag # タグ一覧を表示します。
$ git show <タグ名> # 指定したタグの情報を表示します。
git show
コマンドは、ページャを使って出力されるため、終了する場合q
キーを入力する必要があります。
ローカルタグを削除する
$ git tag -d <タグ名> # 指定したタグを削除します。
リモートリポジトリ
この項は、リモートリポジトリの設定やデータのやり取りについて説明します。
リモートリポジトリをローカルに紐付ける
$ git remote add <リモートリポジトリ名> <URL>
URLは、Githubリモートリポジトリ内の<>Codeボタン
を押すと表示されるプルダウンメニューに記載されています。
紐付いたリモートリポジトリを確認する
$ git remote -v
origin https://github.com/userName/repositoryName.git (fetch) # リモートリポジトリ名とfetchのURL
origin https://github.com/userName/repositoryName.git (push) # リモートリポジトリ名とpushのURL
リモートリポジトリを複製する
$ git clone <URL>
コマンドラインでローカルリポジトリとして扱いたいディレクトリに移動し、このコマンドを実行します。すると、ローカルリポジトリにリモートリポジトリの完全なコピーを作成します。
URLは、Githubリモートリポジトリページ内の<>Codeボタン
を押すと表示されるプルダウンメニューに記載されています。
リモートリポジトリ名を変更する
Githubリモートリポジトリページ内のSettings(設定)タブ内に変更フォームがあります。
名前を変更すると、ローカルリポジトリに保存しているリモートリポジトリURLを変更する必要があります。下記コマンドで変更を登録します。
$ git remote set-url <リモートリポジトリ名> <新しいURL>
URLは、Githubリモートリポジトリページ内の<>Codeボタン
を押すと表示されるプルダウンメニューに記載されています。
リモートブランチ一覧を表示する
$ git branch -r
origin/main
origin/other
リモートブランチを削除する
$ git push <リモートリポジトリ> --delete <ブランチ名>
タグをリモートとやり取りする
$ git push origin <タグ名> # 指定したローカルタグをpushします。
$ git push origin --tags # すべてのローカルタグをpushします。
$ git fetch origin --tags # リモートにあるタグを全てfetchします。
$ git ls-remote --tags # リモートのタグ一覧を表示します。
リモートタグを削除する
$ git push origin --delete <タグ名> # 指定したリモートのタグを削除します。
stash領域
この項は、stash領域とその操作方法を説明します。まずstash領域とは、ワークツリーやインデックス内のcommitしていない作業内容を、一時的に記憶しておく領域です。
役割
作業中に別の作業に着手する必要ができたのでブランチを切り替えたいが、今のままでは中途半端な状態でコミットしなければいけない。そんなときstash領域に現在の作業内容を一時保存しておけば、別ブランチで新たな作業をしても影響は及びません。そしてそれが終わった後、stash内容を元に戻せば元の作業を続行することができます。
変更を退避させる
$ git stash # Tracking fileを対象にtrashを実行します。
# 具体的には、git statusで表示されるChanges to be committedと
# Changes not staged for commit項目のファイルです。
$ git stash -u # Untracked fileも含めてtrashを実行します。
退避させた一覧を表示する
$ git stash list
stash@{0}: WIP on master: 049d078 added the index file
stash@{1}: WIP on master: c264051 Revert "added file_size"
# 出力結果の左から、[{0}が直近のもの: ] [WIP on ブランチ名: ][コミットID] [コミットメッセージ]
退避させた内容が、どこで行われていたものかも表示してくれます。
作業を元に戻す
$ git stash apply # 直近のものを元に戻します。
$ git stash apply stash@{1} # 名前を指定して任意のものを元に戻します。
$ git stash apply --index # 退避させた時と同じ状態で元に戻します。
このコマンドによって戻したものは、全てaddされていない状態で戻されます。--index
オプションを付けると、退避させた時と同じ状態で元に戻します。
作業を削除する
$ git stash drop # 直近のものを削除します。
$ git stash drop stash@{1} # 名前を指定して任意のものを削除します。
$ git stash clear # 全て削除します。
$ git stash pop # 直近のものを元に戻してから削除します。
$ git stash pop stash@{1} # 名前を指定して任意のものを元に戻してから削除します。
git stash pop
は、退避させた時と同じ状態で元に戻します。
作業内容を確認する
$ git stash show # 直近のものの内容をdiff形式で表示します。
$ git stash show stash@{1} # 名前を指定して任意のものの内容をdiff形式で表示します。
$ git stash show -p # 内容をdiff形式で詳細に表示します。
git stash show
は、stashに退避させた内容と、その作業を行っていたブランチのコミットとの差分を表示します。
Appendix
この節では、本編の補足情報を説明します。
エディタ操作
Gitはデフォルトでは、viエディタを採用しています。
viエディタにはモードが2つあり、それぞれCOMMANDモードとINSERTモードです。
COMMANDモード
コマンド操作を行うモードです。
INSERTモードにいる状態でesc
を押すとCOMMANDモードに切り替わります。
カーソル移動は、方向キー↑
↓
→
←
もしくは 下:j
、上:k
、左:h
、右:l
です。
保存は:w
、終了は:q
です。
エディタを起動すると、COMMANDモードから始まります。
INSERTモード
ファイルに書き込みを行うモードです。
COMMANDモードにいる状態でi
を押すとINSERTモードに切り替わります。(左下にINSERTと表記されます。)
操作手順
tagやcommitなどのコマンドを実行すると、viエディタが起動します。
まず、i
でINSERTモードに切り替え、ファイルに書き込みを行います。
次にesc
でCOMMANDモードに切り替え、:wq
で保存しエディタを閉じます。
rebase -iでできること
rebaseに-i
オプションを付けると、コミットの修正、削除、1つのコミットを複数コミットに分割、複数コミットを1つにまとめる、コミット履歴の並べ替え、を行うことができます。
コミットを修正する
「こちら」で説明しています。
コミットを削除する
rebase -i
実行結果に表示されるコミット一覧の、削除したいコミットの行頭「pick」を「drop」に書き換え、エディタを保存、終了します。エディタの使い方は「こちら」です。
上のやり方以外に、消したい行を削除することでも動作します。
$ git rebase -i HEAD~2
pick 36fg2sk first commit
pick d038af0 second commit # ここを削除したい場合、pickをdropにするか、この行を削除します。
注意
rebaseによって削除したコミットより先にあるコミットは、全て新しいコミットとして作り直されます。例えば、〇--->●--->〇--->〇--->〇(HEAD)この黒丸の部分を削除すると、次の〇からHEADの3つ全て新しいコミットに作り替えられます。
この理由は、「データ管理の仕組み」節で説明しましたが、commitオブジェクトはコミット情報を保持しており、その中に親オブジェクトへの参照も含まれています。そして、コミット情報の内容によってID値が算出されcommitオブジェクト自身に割り当てられます。
つまり、内容が変更される(今回では親オブジェクトへの参照)とID値が変化するためです。
コミットを分割する
1つのコミットを複数に分割するには、rebase -i
実行結果に表示されるコミット一覧の、分割したいコミットの行頭「pick」を「edit」に書き換え、エディタを保存、終了します。
次に、git reset HEAD~
で1つ前のコミットに移動します。そこで、わけたい分だけadd、commitすることで分割します。最後に、git rebase --continue
でrebaseを終了します。
サンプルコード
例えば、下記の真ん中のコミットを分割する場合、
$ git rebase -i HEAD~3
pick f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame # ここ
pick a5f4a0d added cat-file
まず、行頭の「pick」を「edit」に変更しエディタを保存、終了します。すると、HEADが〇--->〇--->〇(HEAD)ここから、〇--->〇(HEAD)--->〇に移動します。具体的には、310154e
をコミットした直後です。次に、git reset HEAD~
を実行してHEADを1つ前に遡ります。今の状況は〇(HEAD)--->〇--->〇です。resetを実行したことでインデックスの内容も取り消されることで変更内容がaddされてない状態になります。
そこで、コミットメッセージのupdated README formatting
に該当する変更内容だけadd、commitし、次に残りのadded blame
に該当する変更内容をadd、commitをします。
すると、コミットが分割され〇--->●--->●--->〇(HEAD)このように4つのコミットを形成するようになります。(黒い丸が分割されたコミットです)
注意
rebase -i
で分割してできたコミットと、それより先にあるコミットは新しいものに作り替えられます。
コミットをまとめる
複数のコミットを1つのコミットにまとめるには、rebase -i
実行結果に表示されるコミット一覧の、まとめたいコミットの内一番古いもの以外の行頭「pick」を「squash」に書き換え、エディタを保存、終了します。次に、エディタでコミットメッセージを記述します。最後に、git rebase --continue
でrebaseを終了します。
要は、「squash」に書き換えたコミットは、1つ前に遡ったコミットと結合されるということです。
サンプルコード
例えば、下記の3つをまとめたい場合、一番古いコミットのf7f3f6d
以外の「pick」を「squash」に書き換えます。
$ git rebase -i HEAD~3
pick f7f3f6d changed my name a bit
squash 310154e updated README formatting and added blame # ここ
squash a5f4a0d added cat-file # ここ
するとf7f3f6d
に他2つがまとまり、1つのコミットになります。
最後にコミットメッセージをエディタで記述します。今回の例では下記の画面が出力されます。行頭が#
以外の部分を削除して、任意の箇所に新しいメッセージを記述します。
# This is a combination of 3 commits.
# The first commit's message is:
changed my name a bit
# This is the 2nd commit message:
updated README formatting and added blame
# This is the 3rd commit message:
added cat-file
注意
rebase -i
でまとめてできたコミットと、それより先にあるコミットは新しいものに作り替えられます。
コミットを並び替え
rebase -i
実行結果に表示されるコミット一覧の順番を、手作業で入れ替えるだけです。
# git rebase -i HEAD~3
pick f7f3f6d changed my name a bit
pick a5f4a0d added cat-file
pick 310154e updated README formatting and added blame
# 2行目と3行目を入れ替えました。
注意
rebase -i
で入れ替えたコミットと、それより先にあるコミットは新しいものに作り替えられます。
git diffの使い方
ファイル間の差分を出力します。
引数にコミットやブランチを指定することもできます。
$ git diff # HEADとワークツリーの差分を表示します。
$ git diff --staged # HEADとインデックスの差分を表示します。
以下は、出力結果の見方です。
$ git diff
diff --git a/sample.txt b/sample.txt # 対象リソースで、この場合aがHEAD、bがワークツリーです。
index 6b0c6cf..b37e70a 100644 # GitオブジェクトIDとパーミッションです。
--- a/sample.txt # -記号をaに割り当ます。
+++ b/sample.txt # +記号をbに割り当ます。
@@ -1,2 +1,3 @@ # @@ [aの変更開始行],[行数] [bの変更開始行],[行数] @@
-row 1 # (-)は、aにはあるがbでは削除されてることを示します。
line 2 # スペースは、変更なしを示します。
+new line # (+)は、aにはないがbでは追加されていることを示します。
プルリクエスト
自分の作業内容をコミットしたトピックブランチを、リモートリポジトリ(のmainブランチ)に取り込んでもらえるように依頼する機能です。
プルリクエストによって、レビューやマージ担当者に通知されます。
具体的な操作方法や、関係者間のやり取りは下記サイトで説明されています。修正依頼やコンフリクト発生の対処手順も掲載されています。
.gitignore
使い方は、.gitignore内に任意のファイルやディレクトリ名を記述します。
下記は書き方のサンプルです。ファイルやディレクトリ名を直接指定したり、ワイルドカードを利用して記述します。
$ cat .gitignore
# Write an explanation here.
.DS_Store # ファイル名を示します。
*.com # *は0文字以上の任意の文字列示します。
???test.txt # ?は任意の1文字を示します。
/spec/reports/ # 先頭の/は、ルートからのパスを示します。
build/ # 末尾の/はディレクトリを示します。
!build/diary.txt # !は除外を示します。この場合、build内のdiary.txtのみ無視されません。
GitHub公式リポジトリに、用途毎のテンプレートがコレクションされています。参考になると思います。
リモートリポジトリで共有する
.gitignore
自体をcommitする必要はありませんが、もしリモートリポジトリを共有する人達と共通するファイルを無視するようにしたい場合、.gitignore
をcommitすれば他のローカルリポジトリにも反映させることができます。
.git/info/exclude
また、リポジトリには.git/info/exclude
というファイルがあり、こちらも.gitignore
と同じ働きをします。違いは、ローカルリポジトリでのみ有効というところです。
使い分け
複数人が共通したファイルを持っておりそれを無視したい場合、.gitignore
を使用します。一方、ローカルリポジトリにしか存在しないファイルを無視したい場合、.git/info/exclude
を利用するといった使い分けができます。
注意点
一度もインデックスに登録(add)されたことがないファイルのみ有効で、既にコミット履歴に含まれてしまっているファイルを.gitignore
に追加しても、無視されずcommitされてしまいます。
もし、コミット済みのものを無視させたい場合は、まず.gitignore
に記述します。次に以下のコマンドを実行します。
$ git rm --cached <ファイル名> # インデックスからファイルを削除する。
$ git rm -r --cached <ディレクトリ名> # インデックスからディレクトリを削除する。
このコマンドは、指定したファイルやディレクトリを、インデックスから削除するだけでワークツリーには残ります。
但し、この一連の動作を行った後からcommit対象外になるだけで、過去のコミットには存在しています。
コミットの記号
HEAD
と~
や^
の記号を組み合わせて、特定のコミットを表現すことができます。
親コミットを表す
HEAD~
もしくはHEAD^
で、HEADの親コミット(1世代前)を表します。
祖先コミットを表す
~n
もしくは^の数
で任意の祖先コミットを表します。
例えば、~3
や^^^
なら3世代前のコミットを表します。
マージコミットの親
^2
とすると、マージコミットの片方の親を表します。
参考文献
Gitリファレンス. 「Documentation」. https://git-scm.com/, (参照 2025-01-25).
サル先生のGit入門. 「Git-tutorial」. https://backlog.com/ja/git-tutorial/, (参照 2025-01-25).
GitHub Docs. 「Docs」. https://docs.github.com/ja, (参照 2025-01-25).