MarkdownとBullet Journal

いわゆるプログラマーのつぶやき

【Git】ファイルの6つの状態

Gitではfileが6つの状態に遷移する

Gitではいわゆる3大ツリーである、commit(commit履歴)、staging(ステージング、index、インデックス)、作業ディレクトリ(Working directory)を用いてfile管理を行なっている。

これをもう少し細かく見るとfileには以下の6つの状態がある事がわかる。

  1. fileをpushした状態
  2. fileをcommitした状態
  3. fileをstagingした状態
  4. fileをgitが追跡する状態(git addした事がある作業ディレクトリ内のfile)
  5. fileをgitが追跡しないが認識する状態(新規file等の作業ディレクトリ内のfile)
  6. fileをgitが無視する状態(追跡も認識もしない作業ディレクトリ内のfile)

この6つの状態と遷移を表にまとめ、以下丸数字に従って説明する。

f:id:ProgrammingForEver:20210913090355j:plain

①$ touch files →fileを作業ディレクトリに追加

touch,mvなどで作業ディレクトリに新規fileが追加された段階からGitはfileを認識する。

$ touch a.sh b.sh c.sh       # - a.sh, b.sh, c.shの3fileを新規作成
$ git status                 # - 状態を確認
On branch main
Untracked files:
  (use "git add <file>..." to include in what will be committed)
    a.sh                     # - 各fileを追跡対象外fileとして認識している
    b.sh
    c.sh

nothing added to commit but untracked files present (use "git add" to track)
$

②$ git add -N files →fileをgit追跡対象にするがstagingはしない

一般的にはgit add filesでfileをGitの追跡対象にすると同時にstagingするが、 git add -N filesを使うとGitの追跡対象にはするがstagingはしない。何のためにその様な状態にするのか?というと、stagingはまだしたくないが、新規作成したfileをGitの配下に置いて差分をgit diffでいつでも見たいといった運用が出来る。例えばgit add -N .と打つと、作業ディレクトリ内のfileをすべてGitの追跡対象に出来るので、差分を確認しながら編集を行い、整ったものからstagingが出来る。

逆動作になる後述の「⑦fileをGit追跡対象から外す」と合わせた図表を示す。

f:id:ProgrammingForEver:20210913091026j:plain

$ git add -N b.sh.          # - b.shをgit追跡対象にする(stagingはしない)
$ git status     
On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
    new file:   b.sh        # - 追跡対象になったがstagingされていない状態 

Untracked files:
  (use "git add <file>..." to include in what will be committed)
    a.sh                    # - 追跡対象外fileとして認識中
    c.sh

no changes added to commit (use "git add" and/or "git commit -a")
$

③$ Vim files →Git追跡fileを編集する

上記①ではfileを新規作成したが、理解を深めるべく別のパターンとしてGit追跡fileを修正した場合を加える。後述の⑤の2つの図で違いが分かる

④$ git add files →fileをstagingに上げる

対象fileが上記③の様にGitの追跡対象ならgit add filesでstagingする。また対象fileがGitの追跡対象でない場合は追跡対象fileにした上でstagingする。次の⑤のresetコマンド動作と対になるので、fileが追跡対象外の時と追跡対象時のgit add filesコマンドの動作の違いをresetの項にある図表で確認して欲しい。

$ git add a.sh             # - a.shをstagingする
$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    new file:   a.sh       # - 追跡対象になり、かつstagingされた

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
    new file:   b.sh        # - 追跡対象になったがstagingされていない状態

Untracked files:
  (use "git add <file>..." to include in what will be committed)
    c.sh                    # - 追跡対象外fileとして認識中
$

⑤$ git reset files →fileをstagingから下ろす

stagingしたfileを破棄する(unstaging)。作業ディレクトリ内のfileは変化しないが、対象fileの過去のcommitの有無で扱いが変わる。一度でもcommitした事があるfileは引き続きGitの追跡対象だが、commitした事が無ければGitの追跡対象外になる(addの逆動作)。

以下、対動作となる「③fileをstagingに上げる」も記載した図表を、対象fileのcommit実績の有無別で示す。

fileがcommit実績ある場合:unstagingだけを実施

f:id:ProgrammingForEver:20210913092725j:plain

fileがcommit実績ない場合:unstagingして、Gitの追跡対象からも外す(⑤+⑦の動作)

f:id:ProgrammingForEver:20210913092544j:plain

$git reset a.sh              # - a.shのstagingを取り消す
Unstaged changes after reset:
A   b.sh
$ git status
On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
    modified:   a.sh         # - 1回はcommitしたfileは引き続きgitの追跡対象
    new file:   b.sh

Untracked files:
  (use "git add <file>..." to include in what will be committed)
    a.sh                     # - 未commitのfileはgitの追跡対象外になる 
    c.sh

no changes added to commit (use "git add" and/or "git commit -a")
$

⑥$ git checkout files →fileをstagingから作業ディレクトリに戻す

上記⑤のstagingのfile破棄とは異なり、stagingのfileを作業ディレクトリにCOPYする動作。以下の説明は、a.shをstaging後に追加編集した状態から、その追加編集を取り消してstagingの内容に戻す所までを示す。

% git status              # - staging後にa.shを追加編集した直後
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    new file:   a.sh

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
    modified:   a.sh      # - 変更された表示になる
    new file:   b.sh

Untracked files:
  (use "git add <file>..." to include in what will be committed)
    c.sh

$ git checkout a.sh       # - stagingの内容に戻すコマンド
Updated 1 path from the index
$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    new file:   a.sh

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
    new file:   b.sh      # - 変更したa.shが取り下げられて消えた

Untracked files:
  (use "git add <file>..." to include in what will be committed)
    c.sh

$

⑦$ git rm --cached files →fileをgit追跡対象から外す

git rmはfileを削除するコマンドだが、 git rm --cashed filesコマンドを使うとfileを残したままGitの追跡対象から外せる。外されたfileは引き続きgitに認識されるので、⑨で説明するfileをGitの認識対象から外す操作が必要になる。

これも対動作になる「②fileをGit追跡対象にする(stagingはしない)」と「⑨fileをGitが認識する/無視する」と合わせた図表で示す。

f:id:ProgrammingForEver:20210913094105j:plain

$ git rm --cached b.sh        # - 上の状態からb.shをgitの追跡対象から外す
rm 'b.sh'
$ git status          
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    new file:   a.sh

Untracked files:
  (use "git add <file>..." to include in what will be committed)
    b.sh                      # - b.shがgitの追跡対象から外れた(認識は継続)
    c.sh

$

⑧$ git rm files →fileを削除

git rm filesでGit追跡対象fileを削除する。commitされていないfileは失うためエラーになるが強制的な削除も可能(git rm -f files)。 なおGitの追跡対象外のfileをこのコマンドで削除しようとしても(Gitの働きが及ばないfileのため)エラーになるのでその場合はシェルのrm filesで削除する。

f:id:ProgrammingForEver:20210913095112j:plain

$ git rm a.sh            # - staging中のa.shを削除しようとすると・・
error: the following file has changes staged in the index:
    a.sh                 # - 変更された内容を失わない様にエラーで中断
(use --cached to keep the file, or -f to force removal)
$
$ git rm -f a.sh         # - 今度は強制オプションを付けて削除する 
rm 'a.sh'                # - 削除された 
$
$ git rm b.sh            # - gitの追跡対象外のfileを削除しようとすると・・ 
fatal: pathspec 'b.sh' did not match any files    # - エラーで中断 
$ rm b.sh                # - 代わりにシェルのrmコマンドなら削除出来る
$ 

⑨$ Vim .gitignore →fileをGitが認識する/無視する

.gitignoreにfile名を登録すると認識対象から外され無視される。その登録を抹消すると再び認識対象になる。なお一旦Gitの追跡対象になったfileはここに記載しただけではGitの追跡対象から外れないので、⑦で記載した方法でGitの追跡対象から外す必要がある。改めて⑦で示した図表を再掲する

f:id:ProgrammingForEver:20210913094105j:plain

⑩$ git commit →fileをcommit

git commitでfileをcommitする

$ git commit

【Git】rm:ファイルを削除 or 管理から外す

git rm コマンド

git追跡fileの削除はシェルのrmコマンドではなく、git rmコマンドを使う。

最初に現在の編集状況を解説し、次に丸数字の順に説明する。

f:id:ProgrammingForEver:20210908132056p:plain

想定する現在の編集状況(ある開発中の状態)

  • 社内業務用のシェルプログラムのプロジェクトとする
  • 10回前のcommitでfileはa.shとb.shがあり、b.sh内の変数名はhoge
  • 1回前のcommitでb.shの変数名をfugaに修正
  • 最新commitでc.sh fileを追加
  • 編集を再開しb.shの変数名をpiyoに変えてstaging(index)した。d.sh fileも追加&staging
  • さらにb.shの変数名をfugafugaに変えて、e.sh fileを追加した(e.shはまだgitで追跡されていない)
  • この状態を基本として、git rmコマンドを実行する
  • 変化が発生した部分を朱書きで表示

① git rm file名

gitが追跡しているfileを削除する。staging、作業ディレクトリーからファイルを削除する。削除した行為をgitが認識しているので次回のgit commit実行時にfile削除をリポジトリに記録出来る。シェルのrmコマンドを使うとstagingには削除したfileが残ったままとなるためgit rmを使用すること

② git rm file名(内容変更時)

gitの追跡対象fileで、内容が変更されたfileをgit rmで削除しようとするとエラーになる。これは変更内容が保存されていないfileの損失を防ぐためのもの

③ git rm -f file名(内容変更時)

しかし上記の様な場合でも「-f」オプションを付けると、file内容が変更されていても削除する(変更内容を失う)

④ git rm --cached file名

対象fileをgitの追跡対象から外すコマンド。stagingからは削除されるが、作業ディレクトリ内にはgitの追跡対象外fileとして残る。残したfileは .gitignoreに追記しないと常にstaging候補のfileとして表示される点に注意

(このd.shはcommitで保存されていないため①の様にgit rm d.shで削除しようとするとエラーになる)

git addとgit resetとgit rmの関係

git rmコマンドの話の延長として、git addコマンド、git resetコマンドの機能と比較しながら着目してみる。

  • git addは、fileをgit追跡対象fileにして、そのfileをstagingに上げる
  • git resetは、git追跡対象fileをstagingから下ろす
  • git rmは、git追跡対象fileを追跡対象から外す(or 削除)

つまり新規fileに対してgit addを行うと、①fileをgit追跡対象にして②stagingの2つの動作を行う。これは③git resetのunstagingと④git rmのgit追跡対象から外す( or 削除する)という二つのコマンドの逆動作を一気に行っている事になる。となると、git rmの機能だけの逆動作、つまり新規fileをgit追跡対象fileにするだけのコマンドが有っても良い理屈になり、実際に存在する。

git add -N コマンド

それがgit add -N file名コマンドで、対象fileをgit追跡対象にするがstagingはしない動作になる。コマンド例を示す。

  • a.shファイルは、git add a.shでgit追跡対象fileにしてstagingに上げる
  • b.shファイルは、git add -N b.shでgit追跡対象fileにするがstagingしない
  • c.shファイルは、何もしない(git追跡対象外のまま)
$ touch a.sh b.sh c.sh         # - a.sh , b.sh , c.sh を作成
$ git add a.sh                 # - a.shをstaging
$ git add -N b.sh              # -b.shを add -Nオプションで実行

$ git status                   # - 現在の状態を見る
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    new file:   a.sh           # - a.shはstagingされている

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
    new file:   b.sh           # - b.shは非stagingだがgit追跡対象になった

Untracked files:
  (use "git add <file>..." to include in what will be committed)
    c.sh                       # - c.shはgit追跡対象外のまま

$ git rm -f a.sh               # - git追跡対象fileなのでgit rm -f で強制削除出来る 
rm 'a.sh'
$ git rm -f b.sh               # - git追跡対象fileなのでgit rm -f で強制削除出来る
rm 'b.sh'
$ git rm -f c.sh               # - git追跡対象外fileなのでgit rm -f では削除出来ない
fatal: pathspec 'c.sh' did not match any files
$ 

上記の様にgit statusを実行すると、git add -Nを用いたb.shファイルには Changes not staged for commit: のメッセージが現れる。そしてgit追跡対象fileになっているのでgit rm -fの強制コマンドで削除出来る(修正保存されていないのでgit rmコマンドはエラーになる)。

fileの動き

作業ディレクトリに新規file追加してからの各コマンドによる動きを表にした。

f:id:ProgrammingForEver:20210913090355j:plain

説明は下記の別記事を参照して欲しい。

programmingforever.hatenablog.com

どう使うのか

このgit add -Nコマンドを使えば、stagingはしたくないが、新規作成したfileをgitの配下に置いて差分をgit diffでいつでも見られる運用が実現する。例えばgit add -N .と打つと、作業ディレクトリ内の全fileをすべてgitの追跡対象に出来るので、差分を確認しながら編集を行い、整ったものからstaging出来る。

【Git】ハンク操作

Gitは行単位でstagingやresetが出来る

  • 作業ディレクトリの変更内容をステージングする git add files コマンド
  • ステージングをクリアする git reset files コマンド
  • 作業ディレクトリをステージングの内容に戻す git checkout files コマンド。
  • 指定commitの内容をステージングに戻す git reset "commit指定" files コマンド。
  • 指定commitの内容を作業ディレクトリとステージングに戻す git checkout "commit指定" files コマンド。

以上の各コマンドはファイル単位だけではなく、変更箇所の行の塊(ハンク)単位でも作業を行える。以下ハンク単位や、さらにハンクの中の一部の行だけをstagingやresetする方法を記載する。reset, checkoutの行編集はaddと論理が反転する点に注意

①git add -pで特定ハンクだけstagingする場合

特定ハンクだけをstagingしたい場合は、git add -p filesと-pオプション(--patch)を使う。 git add -p filesコマンドを打つと、対象のファイルにハンクが8個ある場合、最初のハンクが表示され、選択を求められる。

$ git add -p sample.sh
diff --git a/sample.sh b/sample.sh
index 5252aee..44eeb14 100644
--- a/sample.sh
+++ b/sample.sh
@@ -6,15 +6,16 @@

(変更の無い直前行が表示)  # - 変更箇所(ハンク)の表示
-1. 変更前の行①          # - 変更箇所(ハンク)の表示
-2. 変更前の行②          # - 変更箇所(ハンク)の表示
+1. 変更後の行①          # - 変更箇所(ハンク)の表示
+2. 変更後の行②          # - 変更箇所(ハンク)の表示
(変更の無い直後行が表示)  # - 変更箇所(ハンク)の表示

(1/8) Stage this hunk [y,n,q,a,d,j,J,g,/,e,?] 

各選択肢の説明:

  • y : このハンクをstagingする
  • n : このハンクをstagingしない
  • q : staging作業をここまでで中断する
  • a : このハンクをstagingし、残りのハンクも全てstagingする
  • d : このハンクをstagingせず、残りのハンクも全てstagingしない
  • s : ハンクを分割する
  • e : エディターを用いてさらに細かく行単位でstagingする(次の②で説明)

各ハンクを見て、stagingしたい場合はy、したく無い場合はnを押していくと選択したハンクだけstagingされる。

②git add -pでハンクの特定行だけstagingする方法

これは特定ハンクの中からさらに希望する行だけをstagingする方法だ。エディターを使うことで1行単位の選択が可能になる。

$ git add -p sample.sh
diff --git a/sample.sh b/sample.sh
index 5252aee..44eeb14 100644
--- a/sample.sh
+++ b/sample.sh
@@ -6,15 +6,16 @@

(変更の無い直前行が表示)  # - 変更箇所(ハンク)の表示
-1. 変更前の行①          # - 変更箇所(ハンク)の表示
-2. 変更前の行②          # - 変更箇所(ハンク)の表示
+1. 変更後の行①          # - 変更箇所(ハンク)の表示
+2. 変更後の行②          # - 変更箇所(ハンク)の表示
(変更の無い直後行が表示)  # - 変更箇所(ハンク)の表示

(1/8) Stage this hunk [y,n,q,a,d,j,J,g,/,e,?] e↩️と打つとVimが起動する

eを選択するとVimが起動して次の編集画面が現れる

# Manual hunk edit mode -- see bottom for a quick guide
@@ -6,15 +6,16 @@
(変更の無い直前行が表示)  # - エディターに変更箇所として表示される
-1. 変更前の行①          # - エディターに変更箇所として表示される
-2. 変更前の行②          # - エディターに変更箇所として表示される
+1. 変更後の行①          # - エディターに変更箇所として表示される
+2. 変更後の行②          # - エディターに変更箇所として表示される
(変更の無い直後行が表示)  # - エディターに変更箇所として表示される
----
  • stagingしたい行は、そのまま何もしない(自動的にstagingになる)
  • stagingしたくない行は編集が必要
  • 以下、「変更後の行②」をstagingしない時の編集作業
# Manual hunk edit mode -- see bottom for a quick guide
@@ -6,15 +6,16 @@
(変更の無い直前行が表示)
-1. 変更前の行① 
 2. 変更前の行②          # まずstagingしたくない行の-行の - をスペースに置き換える
+1. 変更後の行① 
                        # 次にstagingしたくない行の+行を削除する
(変更の無い直後行が表示) 
  • これで保存すると指定した行だけstagingとなる

③git reset -pで特定ハンクだけunstagingする場合

stagingされた状態から、特定ハンクだけをunstagingしたい場合は、git reset -p filesと-pオプション(--patch)を使う。 git reset -pコマンドを打つと、対象のファイルにハンクが8個ある場合、最初のハンクが表示され、選択を求められる。

$ git reset -p sample.sh
diff --git a/sample.sh b/sample.sh
index 5252aee..44eeb14 100644
--- a/sample.sh
+++ b/sample.sh
@@ -6,15 +6,16 @@

(変更の無い直前行が表示)  # - 変更箇所(ハンク)の表示
-1. 変更前の行①          # - 変更箇所(ハンク)の表示
-2. 変更前の行②          # - 変更箇所(ハンク)の表示
+1. 変更後の行①          # - 変更箇所(ハンク)の表示
+2. 変更後の行②          # - 変更箇所(ハンク)の表示
(変更の無い直後行が表示)  # - 変更箇所(ハンク)の表示

(1/8) Stage this hunk [y,n,q,a,d,j,J,g,/,e,?] 

各ハンクを見て、unstagingしたい場合はy、したく無い場合はnを押していくと選択したハンクだけunstagingされる。

④git reset -pでさらに特定行だけunstagingする方法

特定ハンクの中からさらに希望する行だけをunstagingする方法だ。エディターを使うことで1行単位の選択が可能になる。

$ git reset -p sample.sh
diff --git a/sample.sh b/sample.sh
index 5252aee..44eeb14 100644
--- a/sample.sh
+++ b/sample.sh
@@ -6,15 +6,16 @@

(変更の無い直前行が表示)  # - 変更箇所(ハンク)の表示
-1. 変更前の行①          # - 変更箇所(ハンク)の表示
-2. 変更前の行②          # - 変更箇所(ハンク)の表示
+1. 変更後の行①          # - 変更箇所(ハンク)の表示
+2. 変更後の行②          # - 変更箇所(ハンク)の表示
(変更の無い直後行が表示)  # - 変更箇所(ハンク)の表示

(1/8) Unstage this hunk [y,n,q,a,d,j,J,g,/,e,?] e↩️と打つとVimが起動する

eを選択するとVimが起動して次の編集画面が現れる

# Manual hunk edit mode -- see bottom for a quick guide
@@ -6,15 +6,16 @@
(変更の無い直前行が表示)  # - エディターに変更箇所として表示される
-1. 変更前の行①          # - エディターに変更箇所として表示される
-2. 変更前の行②          # - エディターに変更箇所として表示される
+1. 変更後の行①          # - エディターに変更箇所として表示される
+2. 変更後の行②          # - エディターに変更箇所として表示される
(変更の無い直後行が表示)  # - エディターに変更箇所として表示される
----
  • unstagingしたい行は、そのまま何もしない(自動的にunstagingになる)
  • unstagingしたくない行は編集が必要
  • 以下、「変更後の行②」をunstagingしない時の編集作業
# Manual hunk edit mode -- see bottom for a quick guide
@@ -6,15 +6,16 @@
(変更の無い直前行が表示)  #
-1. 変更前の行①          #
   # -- まずunstagingしたくない行の -が付いている行を削除し、
+1. 変更後の行①          # 
 2. 変更後の行②          # 次にその行の +をスペースに置き換える 
(変更の無い直後行が表示)  # 
  • これで保存すると指定した通りunstagingとなる
  • git add -pとは逆の編集になる(add -pでは -をスペースに置き換えて +行を削除する)
  • 何回か繰り返すと理解できると思う

git reset -p "commit指定" filesは、上記同様なので説明を省略する

⑤git checkout -pで特定ハンクだけ作業ディレクトリからDiscardする場合

作業ディレクトリの変更ファイルの特定ハンクだけをstagingから戻す場合は、git checkout -p filesと-pオプション(--patch)を使う(変更箇所が破棄される点に注意)。 git checkout -pコマンドを打つと、対象のファイルにハンクが8個ある場合、最初のハンクが表示され、選択を求められる。

$ git checkout -p sample.sh
diff --git a/sample.sh b/sample.sh
index 5252aee..44eeb14 100644
--- a/sample.sh
+++ b/sample.sh
@@ -6,15 +6,16 @@

(変更の無い直前行が表示)  # - 変更箇所(ハンク)の表示
-1. 変更前の行①          # - 変更箇所(ハンク)の表示
-2. 変更前の行②          # - 変更箇所(ハンク)の表示
+1. 変更後の行①          # - 変更箇所(ハンク)の表示
+2. 変更後の行②          # - 変更箇所(ハンク)の表示
(変更の無い直後行が表示)  # - 変更箇所(ハンク)の表示

(1/8) Discard This hunk from worktree [y,n,q,a,d,j,J,g,/,e,?] 

各ハンクを見て、Discard(作業ディレクトリの編集箇所を破棄してstagingに戻す)場合はy、したく無い場合はnを押していくと選択したハンクだけDiscardされる。

⑥git checkout -pで指定ハンクからさらに特定行だけDiscardする方法

特定ハンクの中からさらに希望する行だけをDiscardする方法だ。エディターを使うことで1行単位の選択が可能になる。

$ git checkout -p sample.sh
diff --git a/sample.sh b/sample.sh
index 5252aee..44eeb14 100644
--- a/sample.sh
+++ b/sample.sh
@@ -6,15 +6,16 @@

(変更の無い直前行が表示)  # - 変更箇所(ハンク)の表示
-1. 変更前の行①          # - 変更箇所(ハンク)の表示
-2. 変更前の行②          # - 変更箇所(ハンク)の表示
+1. 変更後の行①          # - 変更箇所(ハンク)の表示
+2. 変更後の行②          # - 変更箇所(ハンク)の表示
(変更の無い直後行が表示)  # - 変更箇所(ハンク)の表示

(1/8) Discard This hunk from worktree[y,n,q,a,d,j,J,g,/,e,?] e↩️と打つとVimが起動する

eを選択するとVimが起動して次の編集画面が現れる

# Manual hunk edit mode -- see bottom for a quick guide
@@ -6,15 +6,16 @@
(変更の無い直前行が表示)  # - エディターに変更箇所として表示される
-1. 変更前の行①          # - エディターに変更箇所として表示される
-2. 変更前の行②          # - エディターに変更箇所として表示される
+1. 変更後の行①          # - エディターに変更箇所として表示される
+2. 変更後の行②          # - エディターに変更箇所として表示される
(変更の無い直後行が表示)  # - エディターに変更箇所として表示される
----
  • Discardしたい行は、そのまま何もしない(自動的にunstagingになる)
  • Discardしたくない行は編集が必要
  • 以下、「変更後の行②」をDiscardしない時の編集作業
# Manual hunk edit mode -- see bottom for a quick guide
@@ -6,15 +6,16 @@
(変更の無い直前行が表示)  #
-1. 変更前の行①          #
   # -- まずDiscardしたくない行の -が付いている行を削除し、
+1. 変更後の行①          # 
 2. 変更後の行②          # 次にその行の +をスペースに置き換える 
(変更の無い直後行が表示)  # 
  • これで保存すると指定した通りDiscardする
  • git add -pとは逆の編集になる(add -pでは -をスペースに置き換えて +行を削除する)

git checkout -p "commit指定" filesは、上記同様なので説明を省略する

【Git】開発環境を新しいMacに移す

内容

Windows10で開発中の開発環境、Git 、開発プログラムを遠隔のMacGitHubを介して移す作業一式を述べる。なおGItHubに仕様変更があり、2021.8.14以降に同様の作業を考えている方に役立つ内容だと思う。

Macの用意

別記事で述べた内容で準備する。この内容でgit, VSCodeが使える様になっている。

programmingforever.hatenablog.com

WindowsPCに移したい場合は、下記手順でGit BashをインストールすればOK。

  1. https://gitforwindows.org/ にアクセス
  2. DownloadをクリックしてDownloadしたファイルを開く
  3. 各種設定の中で下記は重要
    • インストール先フォルダ:C:\Program Files\Gitなどが望ましい
    • Select componentsで必ず「Git Bash here」にクリック。これは神ツールのため
    • エディター選択画面でVisual Studio Codeを選ぶ

GitHubからMacでgit pull

現在、WIndows10とESP32開発ボードを用いてESP32のプログラムを開発している状況とする。このWindowsからGitHubのリモートリポジトリにpushしたものを、新規Macでpullから始める場合は以下の手順となる。

  1. GitHubにアクセスしてクローン情報(HTTPS版)を入手(リモートリポジトリが無ければ、最初にGitHubでPrivateのリモートリポジトリを作成しておく)
  2. 作業フォルダを作りたい「親フォルダ」に移動要
  3. そこで「git clone 上記1でcopyしたURL」を実行するとリポジトリ名の作業フォルダが作られる

しかしうまく行かない。開発ボードでの実行までに以下4つの課題解決があった。

  1. GitHubの仕様変更①トークン変更
  2. GitHubの仕様変更②defaultブランチをmaster→main変更
  3. Mac常駐プログラムの影響
  4. ESP32ボードの手動操作

以下、それぞれの解決方法を記載する。

トークンの解決

1. GitHub接続がトークン認証に変更

https://github.blog/2020-12-15-token-authentication-requirements-for-git-operations/

2021.8.14以降のGitHub接続がパスワードからトークンに変わっている。従来のgit pullでは繋がらない。

2.個人アクセストークンを使用する

https://docs.github.com/ja/github/authenticating-to-github/keeping-your-account-and-data-secure/creating-a-personal-access-token

  • トークンの作成
  • GitHubページの右上プロフィール画像をクリックし、続いてSettingsをクリック
  • 左サイドバーで [Developer settings] をクリック
  • 左のサイドバーでPersonal access tokens(個人アクセストークン)をクリック
  • [Generate new token] をクリック
  • トークンにわかりやすい名前を付ける
  • このトークンに付与するスコープ、すなわち権限を選択。 トークンを使用してコマンドラインからリポジトリにアクセスするには、[repo] を選択。とりあえず全部ONでOK
  • [Generate token] をクリック
  • 表示されたトークンを保存
  • しかしまだ設定がいるので次の作業へ
3. トークンを受け入れるためのMac設定

https://docs.github.com/ja/github/getting-started-with-github/getting-started-with-git/updating-credentials-from-the-macos-keychain

このキーチェーンアクセスでGItHubの項目そのものを抹消しないとトークンを受け付けない

GitHub標準がmainに変更になったことへの対応

gitのdefaultはmasterのままなのに、GitHubのdefaultはmainが標準になった。WindowsからGitHubにpushしても一致しないため、Githubからリポジトリをクローンすると、何もないGitHu標準のmainブランチしかcloneされない。

参照記事: https://off.tokyo/blog/can-not-clone-except-the-master-branch/

解決手段①:そのまま何とかする方法:

解決手順としてはまずリモートからpull対象のリモートのクローンしたいブランチ名を探し(通常はorigin/master)、次に対象のリモートブランチをcheckoutで指定する。checkoutの第一引数にローカルリポジトリでのブランチ名を、第二引数にリモートから落としていたいブランチ名を指定する

$ # -- ①リモートのブランチを調べる
$ git branch -r
origin/HEAD -> origin/main # -- GitHubのデフォルトブランチ main が見える
origin/develop    # -- その他ブランチ
origin/master     # -- gitのデフォルトブランチ master が見える
$
$ git checkout -b develop origin/develop -- ②対象にブランチを切り替え

あるいは単に下記で良かったかもしれない(未実証、確認したら追記予定)

$ # --(初回のみ)pull前にブランチを指定
$ git branch --set-upstream-to=origin/master
Branch 'master' set up to track remote branch 'master' from 'origin'.
$
$ git pull # -- pull実行
解決手段②:GitHubのデフォルトブランチ名をmasterに戻す
解決手段③:gitのデフォルトブランチ名をmainにする
$             # - gitのデフォルトブランチをmainにするcomfig設定 
$ git config --global init.defaultBranch main
$             # -ローカルで変えたブランチをリモートへpush
$ git push -u origin main

③UARTがつながらないトラブル

これは常駐プログラムのTOURBOXが悪さをしていたので、開発時はMacメニュー右上のTOURBOXアイコン操作でOFFにすると解決する。

④ロード出来ないトラブル(Macのみ)

  • ESP32開発ボードに接続出来て、何やらLEDも色々光るがタイムアウトする
  • その状態で1回FLASHスイッチを押すと書き込みを開始する
  • 書き込み終了後にRESETスイッチを押すとプログラムが走る

無事に開発ボードでプログラムが動いたので若干の修正を加えてcommit, pushしておく。

ESP32開発:上記のMacでpushした分を遠隔地のWindowsでpull

$ # --(初回のみ)pull前にブランチを指定(*pullがエラーとなった場合)
$ git branch --set-upstream-to=origin/master
Branch 'master' set up to track remote branch 'master' from 'origin'.
$
$ git pull # -- pull実行
Updating a5a9cbf..c91a277
Fast-forward
 README.md    | 2 ++
 src/main.cpp | 4 ++--
 2 files changed, 4 insertions(+), 2 deletions(-)
 create mode 100644 README.md 

とりあえずこれでVSCode+PlatformIO+gitによるESP32開発が、Windows - GitHub - Macの連携で行えるようになった。

【Git】リカバリーメモ

目的

色々とリカバリーするコマンドをまとめた

stashを誤って削除した際の復元方法

stashはpopコマンドやdropコマンドで削除出来るが、誤って必要なstashを削除しても復元できる(有効期限あり)

$ # stash復元方法
$ # ①シェル画面をバックスクロールしてstashをdrop/popした行に移動
$ ----
$ git stash pop
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   a.sh

no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (1a4ccf68abd1ed3512085ac18bb85c4ce8c8fd46) →②copy
$ ----
$ # ③cherry-pickコマンドを実行
$ git cherry-pick -n -m1 1a4ccf68abd1ed3512085ac18bb85c4ce8c8fd46 ←paste
$

staging後にcommitを忘れて reset --hard して消え去ったfileの復元方法

一般的には諦める様なケースだが、Gitはstagingしたfileで破棄されたものを特殊なfolderに全て保管しており復元出来る(有効期限あり)。下記の手段で複数表示されるblobを順に調べて見つけて復活させる。

$ # ロストしたstagingファイルの復元方法
$ git fsck --lost-found # - ①サーチコマンド
Checking object directories: 100% (256/256), done.
dangling blob c073eade7db14753c55098e310a9d309eeb95064
dangling blob 4a1d9a6867f9713af79a63b2b02365d4aceae39f
dangling commit 722e8fd66cde5691f77af8340523c0d71f6b7cdb
dangling commit 73a25b0e21edaeae91eb2900fac6f42971aa8e85
dangling commit 74fccf1afbb327ed6c760d41ffd5978cde594013
dangling blob 753bd569bc1d5b08fe969b8e1c167b664a319bdd
dangling blob 79350d80be3503537651aa8b58d7f0109d8b4881 # - ②どれかcopy

$ git cat-file -p 79350d80be3503537651aa8b58d7f0109d8b4881  # - ③中身を確認
----②③を繰り返す
$ git unpack-file 79350d80be3503537651aa8b58d7f0109d8b4881 # - ④見つかったらunpack
.merge_file_a03280                       # - ⑤候補ファイルがリネームされて復活
$ mv .merge_file_a03280 piyo.sh  # - ⑥元の名前でリネーム
$

参考: http://git-scm.com/docs/git-fsck

stagingを重ねて失った数回前にstagingしたfileの復元方法

Gitはstaging毎にfileをblob objectで全て保管しているので、これも復元しようと思えば出来る。commit前ならば、git resetしてから上の手順の様にgit fsck --lost-foundコマンドでblob(file)を順に確認する方法が使える。

commit後は下記手順で可能だが、対象のblob(file)を手動で探すのは大変なのでblobを更新時間順にソートするシェルスクリプトの活用が望ましい

$ # 重ねたstagingファイルの復元方法
$ du .git/objects # ←①object一覧を出す
----
12      .git/objects/7c   # - ②候補を見つける(手作業では大変)
----
42524   .git/objects/pack
42625   .git/objects

$ cd .git/objects/7c   # - ③そのフォルダに移動して対象blobのSHA1をcopy
$ ls
3197b4421d92bd6fad81eddd241a6c9dcd421d   # - ④これにfolder名の7cを先頭に加える

$ git cat-file -p 7c3197b4421d92bd6fad81eddd241a6c9dcd421d  # - ⑤中身を確認
$ git unpack-file 7c3197b4421d92bd6fad81eddd241a6c9dcd421d # - ⑥OKならunpack
.merge_file_a11580  
$ mv .merge_file_a11580 hoge.sh  # ⑦元の名前にリネーム
$

最新commitの軽微な修正

git commit --amendコマンドを用いればcommitメッセージの修正やファイル追加など、新たなcommitを作らずに修正出来る(但しcommit IDは更新されてcommit履歴が変わるので公開したcommitには使用しないこと)。内容的にはgit reset --soft HEAD^を使う感覚と同じ(commit IDが更新される点も)

$ # -- a.shファイルを追加してcommit
$
$ touch a.sh      # -- 追加ファイル作成
$ git add a.sh    # -- staging
$ git commit -m "a.shファイルを追加"   # -- message作成してcommit
$
$ # -- 上のcommitにb.shを追加修正する
$
$ touch b.sh      # -- 追加ファイル作成とstaging
$ git add b.sh    # -- commit messageのみ修正の場合はこの2行は不要
$ git commit --amend m "a.shとb.shファイルを追加"  # messageも修正して再度commit
$ # -- commitが修正された(commit IDは更新)

過去commitの修正

  • git resetコマンド: -不要commitの削除

  • git rebaseコマンド:

    • ブランチ繋ぎ変え
    • 過去の複数commitを一つに集約
    • commit並び替え

以下git rebaseの解説記事

https://git-scm.com/book/ja/v2/Git-%E3%81%AE%E3%81%95%E3%81%BE%E3%81%96%E3%81%BE%E3%81%AA%E3%83%84%E3%83%BC%E3%83%AB-%E6%AD%B4%E5%8F%B2%E3%81%AE%E6%9B%B8%E3%81%8D%E6%8F%9B%E3%81%88:tittle

  • git cherry-pickコマンド:

    • 欲しいcommitを単体または連続で現在のブランチのHEADにコピー
  • git rebasegit cherry-pickの組み合わせ:

    • 過去のcommitに別のcommitを挿入

失ったcommitの復活

get resetを用いた作業などでcommit消失時の復活方法

  • git reflogで失った消失commitを見つけてSHA1をメモ
  • git cherrry-pick SHA1で消失commitを最新HEADにコピー(消失commit数が1個程度の時)
  • ③あるいはgit checkout SHA1でcommit消失群の先頭へ移動してから$ git branch New-Branchで新しいブランチを作るとそこにcommit消失群が復活する(消失commit数が数個以上ある場合に最適)

【Git】reset , checkout , 「 . 」有無の比較表

間違えやすい操作を表で整理

以前にgit checkoutとgit resetをまとめたが、各コマンド後のステージング(staging, index)と作業ディレクトリのファイルの変化を比較表で整理し直した。特に全file選択を意味する末尾の「.」の有無で、

  • git checkout "指定commit"  指定commitに移動するコマンド
  • git checkout "指定commit" .  指定commitから指定fileを作業ディレクトリとstagingに戻すコマンド
  • git reset "指定commit"    指定commitをHEADに更新するコマンド
  • git reset "指定commit" .    指定commitからfileをstagingに戻すコマンド

と全く異なる動作になる点を明確にする。詳細な説明は前の記事に譲り、視覚的に差を分かりやすくまとめた。

以前の記事:

programmingforever.hatenablog.com

機能整理表とファイルの動き比較表

以上各コマンドの機能整理と各ファイルの動きがわかる比較表をまとめる。最初にcommit指定時の表を記載し、次に最新commitまたはcommit未指定時の表を記載する。最後に各コマンド毎にfile移動の詳細説明を行う。

⒈ commit指定時の各コマンドの機能整理表

f:id:ProgrammingForEver:20210905090833j:plain

例:10個前のcommitを指定した時のHEADとfileの動き

f:id:ProgrammingForEver:20210908090453p:plain f:id:ProgrammingForEver:20210908093550p:plain

前の記事でまとめた各コマンドの利用目的と照らし合わせると理解しやすいと思う。

file指定がないコマンドの利用目的:
  • git reset --soft "指定commit" は、commitを無かったことにしたい時
  • git reset "指定commit" は、整理されたcommitに改変したい時
  • git reset --hard "指定commit" は、一からやり直したい時
  • git checkout "指定commit" は、以前のVer.確認やブランチを切りたい時
file指定があるコマンドの利用目的:
  • git checkout "指定commit" files:作業ディレクトリ内の指定fileは不要で、指定commitから指定fileを戻したい時
  • git reset "指定commit" files:作業ディレクトリ内の指定fileは必要で、指定commitから指定fileを戻して利用したい時

共にHEADは動かず、git chechoutはstagingと作業ディレクトリに指定fileを戻す動作、git resetはstagingだけに戻す指定fileを戻す動作なので上記目的に適する。

2. 最新commitを指定または省略時の各コマンドの機能整理表

状態:commit後にfile編集+staging、さらにfile編集した状態からのコマンド入力を想定

f:id:ProgrammingForEver:20210905105850j:plain

例:最新commit (HEAD)を指定または省略時のfileの動き

f:id:ProgrammingForEver:20210908092750p:plain f:id:ProgrammingForEver:20210908093343p:plain

こちらは少し解説する。

  • git resetは、最新commitをstagingに戻す
  • git reset --hardは、最新commitをstagingと作業ディレクトリに戻す
  • git checkout filesは、staging内の指定fileを作業ディレクトリに戻す
  • git checkout HEAD filesは、最新commit内の指定fileをstagingと作業ディレクトリに戻す
  • git reset files は、最新commit内の指定fileをstagingに戻す

git reset --hard と git checkout HEAD . の結果は一見同じ様に見えるが、最新commit後に追加したファイル(図ではd.sh)がresetコマンドでは削除され、checkoutコマンドでは残る点が違う

またgit checkout HEAD. とgit reset . もよく似ているが**reset . はstagingにだけコピー、checkout HEAD . はstagingと作業ディレクトリにコピー(編集内容を失う)となる点が全く違う。

ファイルではなく行単位での操作も可能

ここで挙げた中で . または file名が付くコマンドに-pオプション(--patch)を加えると、file単位ではなく変更のあった行の塊(ハンク)単位、あるいは1行単位でreset(ステージングの修正)やDiscard(作業ディレクトリの修正)が出来る。2つのfileの差分を見ながらハンク(行の塊)単位で残すか否かを対話式で選択出来る。詳しくは「gitのハンク」記事を参照願いたい。

programmingforever.hatenablog.com

以下丸数字の番号順にfileの移動を說明

①想定する初期状態(ある開発中の状態)

  • 社内業務用のシェルプログラムのプロジェクトとする
  • 10回前のcommitでファイルはa.shとb.shがあり、b.sh内の変数名はhoge
  • 1回前のcommitでb.shの変数名をfugaに修正
  • 最新commitでc.shファイルを追加
  • 編集を再開しb.shの変数名をpiyoに変えてstaging(index)した。d.shファイルも追加&staging
  • さらにb.shの変数名をfugafugaに変えた所で、stagingと作業ディレクトリの内容が違っている
  • この状態を基本として、色々とresetやcheckoutを使って変化させてみる
  • 変化が発生した部分を朱書きで表示

f:id:ProgrammingForEver:20210902080348p:plain

②git reset --soft

  • HEAD: 移動しない(元の位置を指すため *commi未指定時はHEAD)
  • staging: 変化なし
  • 作業ディレクトリ: 変化なし

③git reset (--mixed)

  • HEAD: 移動しない(元の位置を指すため *commi未指定時はHEAD)
  • staging: 最新commitの内容に戻す
  • 作業ディレクトリ: 変化なし

④git reset --hard

  • HEAD: 移動しない(元の位置を指すため *commi未指定時はHEAD)
  • staging: 最新commitの内容に戻す
  • 作業ディレクトリ: stagingと同一(管理対象外ファイルを除く)

git reset .

  • HEAD: 移動しない(ファイルコピー動作)
  • staging: HEAD/masterのファイルが一式コピーされる。同一名のファイルは上書きされる。staging内のみ存在するファイルは削除されず残る
  • 作業ディレクトリ: 変化なし

git reset file名

  • HEAD: 移動しない(ファイルコピー動作)
  • staging: HEAD/masterから指名したファイルだけがコピーされる。同一名のファイルは上書きされる。他は影響されない
  • 作業ディレクトリ: 変化なし

f:id:ProgrammingForEver:20210902080529p:plain

⑤git checkout または git checkout HEAD

  • HEAD: 移動しない(commit指定がない、あるいは元の位置を指しているため)
  • staging: 変化なし
  • 作業ディレクトリ: 変化なし

⑥git checkout .

  • HEAD: 移動しない(ファイルコピー動作)
  • staging: 変化なし(commit指定がないためコピー出来ない)
  • 作業ディレクトリ: stagingと同一(管理対象外ファイルを除く)

⑦git checkout HEAD .

  • HEAD: 移動しない(ファイルコピー動作)
  • staging: HEAD/masterのファイルが一式コピーされる。同一名のファイルは上書きされる。staging内のみ存在するファイルは削除されず残る
  • 作業ディレクトリ: stagingと同一(管理対象外ファイルを除く)

⑧git checkout HEAD file名

  • HEAD: 移動しない(ファイルコピー動作)
  • staging: HEAD/masterから指名したファイルだけがコピーされる。同一名のファイルは上書きされる。他は影響されない
  • 作業ディレクトリ: stagingと同一(管理対象外ファイルを除く)

f:id:ProgrammingForEver:20210902080616p:plain

⑨git reset --soft HEAD~10

  • HEAD: 指定された10個前のcommitまで戻り、それまでのcommit履歴を削除する
  • staging: 変化なし
  • 作業ディレクトリ: 変化なし

⑩git reset (--mixed) HEAD~10

  • HEAD: 指定された10個前のcommitまで戻り、それまでのcommit履歴を削除する
  • staging: 指定commitの内容に戻す
  • 作業ディレクトリ: 変化なし

⑪git reset --hard HEAD~10

  • HEAD: 指定された10個前のcommitまで戻り、それまでのcommit履歴を削除する
  • staging: 指定commitの内容に戻す
  • 作業ディレクトリ: stagingと同一(管理対象外ファイルを除く)

git reset HEAD~10 .

  • HEAD: 移動しない(ファイルコピー動作)
  • staging: 指定commitからファイルが一式コピーされる。同一名のファイルは上書きされる。staging内のみ存在するファイルは削除されず残る
  • 作業ディレクトリ: 変化なし

git reset HEAD~10 file名

  • HEAD: 移動しない(ファイルコピー動作)
  • staging: 指定commitから指名したファイルだけがコピーされる。同一名のファイルは上書きされる。他は影響されない
  • 作業ディレクトリ: 変化なし

f:id:ProgrammingForEver:20210902080704p:plain

⑫git checkout HEAD~10

  • HEAD: 指定されたcommitに移動する(Detouch状態)
  • staging: 指定commitの内容に戻す
  • 作業ディレクトリ: stagingと同一(管理対象外ファイルを除く)

⑬git checkout HEAD~10 .

  • HEAD: 移動しない(ファイルコピー動作)
  • staging: 指定commitからファイルが一式コピーされる。同一名のファイルは上書きされる。staging内のみ存在するファイルは削除されず残る
  • 作業ディレクトリ: stagingと同一(管理対象外ファイルを除く)

⑭git checkout HEAD~10 file名

  • HEAD: 移動しない(ファイルコピー動作)
  • staging: 指定commitから指名したファイルだけがコピーされる。同一名のファイルは上書きされる。他は影響されない
  • 作業ディレクトリ: stagingと同一(管理対象外ファイルを除く)

【Git】checkoutとresetの振る舞い

述べたいこと

Gitのcommit履歴を移動するコマンドとしてgit checkoutとgit resetがあるが、どの記事を見てもgit 3大ツリーとの関わり(挙動)を細かく記載したものがなかったため作成した(下記の別記事)。この記事では初心者向けにgit checkoutgit resetの基本的な違いに関して述べる。

programmingforever.hatenablog.com

コマンド機能表

git checkoutgit resetコマンド実行後に変更される、HEAD(commit履歴内の位置)、staging(ステージング or index)、作業ディレクトリ(Working directory)の一覧表を記載する。詳細はこの後に各図で説明するが、この表で理解出来る方は上で記載した別記事を推奨する(本記事は初心者向け)。

f:id:ProgrammingForEver:20210905090833j:plain f:id:ProgrammingForEver:20210905105850j:plain

誰を対象にした記事か

  • Git初心者
  • 失敗した時にcheckoutやresetを使うが今ひとつ違いが分からない
  • コマンド実行後の3大ツリーの状態が調べても中々掴めない

この記事が上記の様に感じている方々への解になれば幸いである。

git checkoutとgit resetの違い

git checkoutgit reset、さらにそれぞれfile名を付けた時の動作は一見似ているが、基本的に全く違う。

まず異なるのが作業ディレクトリへの影響で、checkoutは作業ディレクトリを破壊するが、resetは破壊しない(--hardオプションを除く)。これはfile指定した場合も同じだ。

次にcommitのHEAD移動だが、git checkoutは指定先にHEADを移動、git resetは指定先までHEADを抹消して戻る。またgit checkout files, git reset filesの様にfile指定するとHEADは動かない。

各コマンドの概要

以下、①checkout、②reset、③checkout file名、④reset file名、の各コマンドの概略を説明する

①git checkout:指定commitにHEADを移動、作業ディレクトリは破壊

自由に移動先のcommitを自由に指定出来るので、これまでの作業でcommitさえ切っていれば知りたい過去や分岐先に移動出来る。そこで新たに編集したい場合はブランチを切ればOKだ。そして用事が済めばいつでもmaster(最新commit)に戻れる。まさにプロジェクトのタイムマシンと言って過言ではない。作業ディレクトリにcommitしていないfileがあると、編集内容を失わない様にエラーとなるためcommitかstashによる保存が必要だ。

②git reset:指定commitまでcommitを抹消、作業ディレクトリは非破壊(--hardモードは破壊)

指定commitから最新commitまでを抹消して指定commitを新たな最新commitにする。checkout同様に過去に戻れる機能だがresetで消去したcommitにはもう戻れない*。過去を無理やり消し去る乱暴なタイムマシンとでも言うべきか。なおresetのdefault動作は作業ディレクトリ内のfileを変更しないが、各モードで異なるため詳細は⑦で述べる。

③git checkout files:指定commitからfileを戻す、作業ディレクトリは破壊

指定したcommitから指定fileをstagingと作業ディレクトリに戻す。作業ディレクトリに過去のfileを戻すので、編集中のfileが不要で過去のfileに戻したい時に使う

④git reset files:指定commitからfileを戻す、作業ディレクトリは非破壊

指定したcommitから指定fileをstagingだけに戻す。stagingだけに過去のfileを戻すので、編集に過去のfileを利用したい時に使う

実はgit resetして消失したcommitも「git reflogコマンド」を使えば取り戻せる。但しresetして消えたcommitはcommit履歴から浮いた状態のため、そのまま放置すると不要なcommitを自動消去するgitのクローラーで消されるため、戻す場合は早めに処理した方が良い

以上述べた各動作を順番に説明する。

説明

全体フロー図

git checkout, git resetの概略を理解するために、各コマンドによるfileの動きをまとめた表を掲載する。なお要素が多いため、正確な詳細を知りたい場合は上記の別記事を参照して欲しい。

f:id:ProgrammingForEver:20210901064626j:plain

commit直後の状態

まず最初に、git checkout, git resetコマンドを実行する前に、編集作業を終えてcommitを行った直後の図を示す。

f:id:ProgrammingForEver:20210830144115j:plain

file編集、staging(ステージング or index)、commit(コミット)の各作業を行い、3大ツリー(作業ディレクトリ、staging、commit:HEAD)の内容が全て一致している状態(gitの管理対象外fileを除く)。前回のcommitである変数名をhogeに変えた編集を維持していると仮定する。

一般的な編集&staging&commit

次に順調に修正を行ってからstaging(git add . コマンド)、commitをするパターンの図を見る。 f:id:ProgrammingForEver:20210830144640j:plain

$ # コマンド
$ vi hoge.sh                  # ①file編集
$ git add hoge.sh             # ②hoge.shをstaging
$ vi hoge.sh                  #  再びfile編集
$ -----
$ git commit                  # ③commit作業

図の様にfile編集後にstagingはするがcommitはせず、再度file編集している(変数名をpiyoに変えた)。これで最新commitで保存されたfile内の変数名はhoge、stagingされたfile内の変数名はfuga、作業ディレクトリのfileの変数名はpiyoとそれぞれ異なるコードになっている。この作業の後、やっぱりfugaのままで良ければそのまま③のcommitを行うし、piyoにするなら②再度stagingした上で③commitする。

④stagingから作業ディレクトリに戻す

$ # コマンド
$ git checkout              # 何も起きない(省略時はHEADで同じなので移動しない)
$ git checkout files        # stagingにある指定fileを作業ディレクトリに戻す

以下の図では、stagingの後に再度file編集したが(fuga→piyo)、失敗と判断して一旦stagingした内容を戻すべく、④stagingのfileを作業ディレクトリに戻すコマンドとして使っている。

f:id:ProgrammingForEver:20210901064755j:plain

通常はcheckoutコマンドにcommit指定が入ると、指定commitの指定fileがstagingと作業ディレクトリに戻るが、(commit指定が無いのでcommitからは戻せず)stagingから戻すだけの動作になる。

checkoutを実行すると、指定commitの指定fileをまずstagingへ戻し(1)、次に作業ディレクトリにも戻す(2)と考えても良いだろう。この④はcommit指定がないので(1)が無効となり(2)のstagingから作業ディレクトリに戻す動作だけになると判断できる

⑤HEAD commitからstagingと作業ディレクトリに戻す

最新commit後に何らかの要因でうまくいかずfileを元に戻す。commit内容に戻す場合はgit resetコマンド、指定fileを戻す場合はgit checkout files コマンドを使う。一見似ているがどれも違う内容なので注意。

$ # コマンド
$ git checkout HEAD           # 何も起きない(現在のcommitと同じなので移動しないため)
$ git checkout HEAD files     # HEADの指定fileをstagingと作業ディレクトリに戻す
$ git reset --hard            # HEADの状態にstagingと作業ディレクトリを戻す(gitの管理対象外fileを除く)

図で示す。

f:id:ProgrammingForEver:20210901064902j:plain

図の様にHEAD(最新commit)から指定fileをstagingと作業ディレクトリに戻す。

⑥HEAD commitをstagingだけに戻す。

$ # コマンド
$ git reset                 # HEAD commitの内容にstagingに戻す
$ git reset files           # HEAD commitから指定fileをstagingに戻す

この⑥は、stagingに格納したものを取り消したい時に用いる。fileを指定すると指定fileだけが取り消される。いずれも作業ディレクトリの中身は維持されるので安全だ。図を見てみる。

f:id:ProgrammingForEver:20210901064822j:plain

比較のためにすでに説明した④⑤も残している。これで④⑤⑥の振る舞いの違いが分かると思う。

reset 3モード

⑦指定commitにHEADを戻す(git reset SHA1 --soft)
⑧指定commitにHEADを戻し、file一式をstagingに戻す(git reset SHA1 (--mixed))
⑨指定commitにHEADを戻し、file一式をstagingと作業ディレクトリに戻す(git reset SHA1 --hard)

最新commitから指定commitまでの過去を抹消するgit resetコマンドは、オプションで違うモードになる。それぞれ利用目的が異なり、オプション無しの時は⑧のmixedが選ばれる。

$ # ---- resetの各モード
$ # ---- 全てHEADを指定commitまで戻すのは同じ
$ git reset SHA1 --soft           # stagingと作業ディレクトリは変化しない
$ git reset SHA1 (--mixed)        # 指定commitの内容にstagingに戻す。作業ディレクトリは変化しない
$ git reset SHA1 --hard           # 指定commitの内容にstagingと作業ディレクトリに戻す(gitの管理対象外fileを除く)

f:id:ProgrammingForEver:20210901065005j:plain

  • 各モードの利用パターン
    • ⑦はcommitを消去して、commitを無かったことにしたい時
    • ⑧はcommitを消去して、整理されたcommitに改変したい時
    • ⑨はcommitも変更fileも消去して、一からやり直したい時

⑦はcommitの記録を失うものの、stagingと作業ディレクトリはそのままなので、消去したい過去commitを好きなだけ消去した後にcommitすると最新の作業ディレクトリ内のfileに基づく最新commitが記録されて完了する。例えば作業着手から10程度のcommitを立てながら仕様変更対応が完了して動作するプログラムが作業ディレクトリにあり、バグfixで色々散らかしてしまいレビューに役立たないcommit群になっているとしよう。完成したコードを最新commitするが、着手からの過程は悪戦苦闘で無意味なcommitが連なっており、着手から現在までのcommitを無かったことにしたい場合に⑦は適している

⑧は最も価値のある使い方だ。使用例として、上記同様に作業着手から10程度のcommitを立てながら仕様変更対応が完了して動作するプログラムが作業ディレクトリにあり、同様に役立たないcommit群になっているとしよう。その散らかったcommitを今回の仕様変更の作業開始点まで戻し(reset)、実装した変更要素毎に分割staging & commitをするのに⑧は適している。stagingの中身は戻した過去commitの内容に戻っているので、git add -pコマンドで、最新の作業ディレクトリ内にある各fileの変更された全てのハンク(行の塊)が表示される。順に出てくる各ハンク毎に対話方式で、ステージングする/しないを選択する作業が行えるので容易に分割staging & commitが行える。

⑨は現在から指定commitまでの過去の間に構築したものを全て破棄する。つまり着手から現在までのcommitや変更fileを全て抹消したい場合に⑨は適している。これまで積み重ねた作業を全て失うアクションであり要注意(しばらくの間はgit reflogで取り戻せる)。

いずれも存在するcommitが消失するため共同開発で公開されたcommitに対しての利用は避けるべき。

⑩指定commitに移動/指定commit内の指定fileを戻す

git checkoutコマンドは安全に過去の指定commitに移動してファイルを閲覧出来るコマンド。また-bオプションで指定commitに新規ブランチを立てることも出来る。なお、git checkout filesとfile指定があると指定commitから指定fileをstagingと作業ディレクトリに戻す、全く異なる動作になる点に注意。またgit reset filesとresetにfile指定があると指定commitから指定fileをstagingだけに戻す動作になる。

$ # コマンド
$ git checkout SHA1            # HEADを指定commitに移動し、指定commitのfile一式をstagingと作業ディレクトリに戻す
$ git checkout SHA1 files      # 指定commitから指定fileをstagingと作業ディレクトリにコピー、HEADは変わらない
$ git reset SHA1 files         # 指定commitから指定fileをstagingだけにコピー、HEADは変わらない
  • 過去のcommitに移動して中身を精査したい時やブランチを切る用途にはfile名を付けない。なお作業ディレクトリのfileが変更されていたら移動するとfileを失うためエラーで停止する。エラーを解除するにはcommitかstashで未変更fileを保存するか、 -f オプションで強制して(fileを失う)再度checkoutする。精査後はgit checkout masterコマンドで元の最新commitに戻れる。
  • stagingと作業ディレクトリに、過去のcommitのfileを戻したい(*同名fileは過去のfileに置き換わる)場合は、git checkout filesの様にcheckoutコマンドの末尾に「.」やfile名を付ける。HEADの位置は変わらない。作業ディレクトリのfileが変更されていても構わず実行されるため、同一名のfileがあればその変更内容を失う。
  • stagingだけに、過去のcommitのfileを戻したい(*同名fileは過去のfileに置き換わる)場合は、git reset filesの様にresetコマンドの末尾に「.」やfile名を付ける。HEADの位置は変わらない。作業ディレクトリは変更されないので変更内容を失わない。

f:id:ProgrammingForEver:20210901065032j:plain

checkoutコマンドは、自由に選択出来る指定commit内に格納された特定fileまたはfile一式をstagingと作業ディレクトリに戻す。よって、

  • SHA1(ハッシュ)やHEAD, HEAD^ ,HEAD~4などの指定commitから指定fileをstagingと作業ディレクトリに戻す
  • ⑤HEAD commitから指定fileをstagingと作業ディレクトリに戻す(上記の指定がHEADになっただけ)
  • ④commit指定が無く、staging内容から指定fileを作業ディレクトリに戻す

といずれの場合も「指定commit」から希望するfileをstagingと作業ディレクトリに戻す点は同じだ。

どう使い分けるか

git resetの3モード、checkout、checkout files、reset filesをどう使い分けるか。人によって異なるのは当然だが、だいたい下記の様な使い分けになるだろう

  • git reset --softは、commitを無かったことにしたい時
  • git reset --mixedは、整理されたcommitに改変したい時
  • git reset --hardは、一からやり直したい時
  • git checkoutは、以前のVer.確認やブランチを切りたい時
  • git checkout files は、以前のVer.のfileに置き換えたい時
  • git reset files は、以前のVer.のfileを利用したい時