MarkdownとBullet Journal

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

【Git】show,diff,status,logの活用

Gitで色々確認する際に使うコマンド

git status, git log ,git diff, git showはいずれもお世話になっている便利な確認用コマンドであり、見やすい表示など活用例をまとめた。

オススメ.bashrcの設定例

alias gitshow='git show --oneline --color-words'
alias gitstatus='git status --short --branch'
alias gitlog='git log --graph --decorate --oneline'
alias gitdiff='git diff --color-words'

git status色々

$ git status --short        # staging状態等を簡潔表示
$ git status --branch       # 1行目にブランチ名を表示
  • 簡潔表示の表記説明:
  • 1文字目が対象fileのstaging(index)の状態を表示
  • 2文字目が対象fileの作業ディレクトリ内の状態を表示
凡例 説明
M_ file変更をstaging済み (staged)
_M file変更をstagingしていない (modified)
MM staging後に再びfile変更 (modified)
A_ 新規fileをstaging済み
_A 新規fileを追跡file化したが未staging:git add -N
_D fileをshellでrmしたがgit addやgit rmでは未処理
?? gitが追跡していない(untracked)
UU mergeでconflictした (unmerged)

git show色々

$ git show --oneline        # commitを1行表示+単語単位のカラー差分表示
$ git show --color-words    # commitを1行表示+単語単位のカラー差分表示
$ git show SHA1:files       # あるcommit時点の特定fileの内容を確認
$ git show -1               # 最新commit情報と変更点の表示(git show と同じ)
$ git show -2               # 直前commit情報と変更点の表示
$ git show -3               # 2つ前のcommit情報と変更点の表示

git diff色々

$ git diff origin/main           # ローカルとリモートとの変更箇所表示
$ git diff HEAD..origin/main     # git pull前にリモートとの変更箇所表示
$ git diff origin/main..HEAD     # git push前にリモートとの変更箇所表示
$ git diff old_SHA1..new_SHA1    # commit間の変更箇所表示
$ git diff any_SHA1^..any_SHA1   # あるcommitの変更箇所表示
$ git diff branch..anotherbranch # ブランチ間の変更箇所表示
$ git diff HEAD^                 # 最新commitの変更箇所表示
$ git diff                       # 作業ディレクトリとstagingの変更箇所表示
$ git diff --staged              # stagingと最新commitの変更箇所表示
$ git diff -- files              # staging前のfileの変更箇所表示
$ git diff branch..anotherbranch -- files  # ブランチ間の特定file変更箇所表示
$ git diff --stat                # 変更した行数だけを見る
$ git diff --name-only           # 変更したfile名だけを見る
$ git diff --color-words         # 差分が行単位から単語単位でカラー表示

..の右側が時系列的に新しいものとみなされる

git log色々

$ git log --oneline              # commitメッセージを1行のみ表示
$ git log --graph                # commit/merge履歴をGUI的に表示
$ git log --decorate             # 各branchのHEADの位置を表示
$ git log --stat                 # 変更file名と行数の簡易表示
$ git log --since=2021-12-1      # 指定した日付以降のログを表示
$ git log --until=2022-4-1       # 指定した日付までのログを表示
$ git log --since=2022-1-1 --until=2022-4-1 # 指定日から指定日まで表示
$ git log master                 # ブランチを指定して表示
$ git log files                  # 特定fileのログのみを表示

【Git】同時に複数の作業ディレクトリを扱う

複数ブランチを同時編集

Gitで複数のブランチを同時編集したい場合、git checkout ブランチ名で頻繁に移動しながら編集することになるが、都度別ブランチのfileを確認しようとすると不便なため、全く別のフォルダに暫定的にコピーしたり、stashしたり、別クローンしたりなど結構手間がかかる作業を強いられがちになる。

その様な課題に対してGitは、1つのリポジトリに対して複数ブランチの各作業ディレクトリ(worktree)を同時展開して、編集作業やcommitをそれぞれの作業ディレクトリで並行して行える機能を提供している。コマンド名もgit worktree

f:id:ProgrammingForEver:20210922204302p:plain

同時展開は、現在のブランチを展開する作業ディレクトリの下に、他のブランチがそれぞれサブディレクトリとなって存在する形で展開される(全く別のフォルダでも良い)。なのでディレクトリを移動するだけで各ブランチに入り、そのブランチに対するGit操作が行える。このサブディレクトリとして存在する各作業ディレクトリは仮想的な存在なので各ブランチ本体への操作とイコールであり、作業完了後このサブディレクトリは不要になるので抹消する。

VSCodeなどのエディターを用いれば各ブランチに該当するサブディレクトリを一覧して同時編集や差分表示が出来る。Git操作もVSCodeのターミナル画面のシェルでディレクトリを変更するだけで各ブランチに入ってそのブランチのcommitが行える。とても便利なので実例を示しながら紹介したい。

git worktreeコマンドを用いた同時編集の例

説明のために、以前の記事でまとめた内容と同じく、ブランチ数が1つ、commit数が4つ、fileもa.sh,b.sh,c.sh,d.shの4つだけのシンプルな状態を出発点とする。

programmingforever.hatenablog.com

実証のために2つブランチを作成

まずtest1ブランチを作成して、e.sh(空file)を追加してからstaging&commitを行う。

$ git checkout -b test1
Switched to a new branch 'test1'
$ touch e.sh
$ git add .
$ git commit -m "test1のブランチ作成"
[test1 6a63d27] test1のブランチ作成
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 ee.sh
$ 

次にtest2ブランチを作成して、f.sh(空file)を追加してからstaging&commitを行う。

$ git checkout -b test2
Switched to a new branch 'test2'
$ touch f.sh
$ git add .
$ git commit -m "test2のブランチ作成"
[test2 0e5d6c1] test2のブランチ作成
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 ee.sh
$ 

git worktreeコマンドを使う

ここでmainブランチに戻り、git worktreeコマンドを用いてtest1ブランチとtest2ブランチの2つを現在の作業ディレクトリの下に配置する。なお配置先は現在の作業ディレクトリの下でなくても構わないが、VSCodeなどで編集する際に各ブランチがそれぞれサブフォルダに収まっていると一覧出来て扱い易いと思う(もちろん別フォルダに置いてVSCodeのworkspace機能を使って編集しても良い)。

コマンドとしてgit worktree add サブディレクトリ名(新規) ブランチ名と打つと、指定したブランチの作業ディレクトリ一式が新規に作られるサブディレクトリ内に入る。もちろんcommit履歴などのobjectもそのまま扱える。つまり並行宇宙の様に別のブランチ一式が同時に扱える状態で存在している。初めて使った時はまるで魔法の様に感じた。

十分に発達した科学は魔法と見分けがつかない by.アーサー・クラーク

$ git checkout main
Switched to branch 'main'

$ git worktree add test1 test1
Preparing worktree (checking out 'test1')
HEAD is now at 6a63d27 test1ブランチ作成

$ git worktree add test2 test2
Preparing worktree (checking out 'test2')
HEAD is now at 0e5d6c1 test2ブランチ作成

ここでmainブランチの状態を確認すると、追加されたtest1/, test2のサブディレクトリを検出して未追跡folderだとメッセージが出るが、これらのサブディレクトリは作業完了後にGitが削除するので無視して問題ない

$ ls
a.sh    b.sh    c.sh    d.sh    test1    test2   # - 追加したサブディレクトリが見える

$ git status
On branch main
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        test1/
        test2/

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

ではtest1ブランチを入れたサブディレクトリのtest1/に移動してみよう。するとまるでこのサブディレクトリが全く別の作業ディレクトリの様に振る舞い、Gitの各コマンドも機能する。まずはtest1ブランチがクリーンな状態のままであることを確認してから、e.shを編集してcommitする。記載した様にこのcommitは現在のmainブランチとは全く関係なく、worktreeとして取り込んだtest1ブランチのcommitになる

$ cd test1
$ ls
a.sh    b.sh    c.sh    d.sh    e.sh
$ git status
On branch test1
nothing to commit, working tree clean
$ vi e.sh
$ git status
On branch test1
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:   e.sh

no changes added to commit (use "git add" and/or "git commit -a")
$ git add .
$ git status
On branch test1
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   e.sh

$ git commit -m "worktreeでcommit"
[test1 dcadb5c] worktreeでcommit
 1 file changed, 1 insertion(+)

同様にtest2ブランチを入れたサブディレクトリのtest2/に移動してから、f.shを編集してcommitする。こちらも同様にmainとは全く関係ない、worktreeとして取り込んだtest2ブランチのcommitだ。

$ cd ../test2
$ git status
On branch test2
nothing to commit, working tree clean
$ vi f.sh
$ git add .
$ git commit -m "test2のworktreeでcommit"
[test2 ba6f4fe] test2のworktreeでcommit
 1 file changed, 1 insertion(+)

この状態でそれぞれの作業ディレクトリに移動してログを見てみる。まずは元のmainの作業ディレクトリに移動してログを確認すると最初の4commitが確認出来る。

$ cd ..
$ git log --oneline
cac41d4 (HEAD -> main) 4番目のcommit
8a20ac3 3番目のcommit
1999418 2番目のcommit
ef11425 最初のcommit

次にtest1サブディレクトリに移動するとtest1ブランチのログが全て確認出来る。test2ブランチのcommitは当然含まれない

$ cd ../test1
$ git log --oneline
dcadb5c (HEAD -> test1) worktreeでcommit
6a63d27 test1ブランチ作成
cac41d4 (main) 4番目のcommit
8a20ac3 3番目のcommit
1999418 2番目のcommit
ef11425 最初のcommit

さらにtest2サブディレクトリに移動するとtest2ブランチのログが全て確認出来る。別ブランチのtest1の"worktreeでcommit"のcommitも存在しない。

$ cd ../test2
$ git log --oneline
ba6f4fe (HEAD -> test2) test2のworktreeでcommit
0e5d6c1 test2ブランチ作成
6a63d27 test1ブランチ作成
cac41d4 (main) 4番目のcommit
8a20ac3 3番目のcommit
1999418 2番目のcommit
ef11425 最初のcommit

以上の様に各ブランチの編集とcommitを終えたら、不要となったworktreeを削除するこの削除は各ブランチの本体に全く影響しない。一時的に構成された仮想作業場所での用事が済んだので、その仮想作業ディレクトリを削除する行為に過ぎない。

$ cd ..
$ git checkout main
Switched to branch 'main'

$ git worktree remove test1
$ git worktree remove test2

$ ls
a.sh    b.sh    c.sh    d.sh       # - test1, test2サブディレクトリが消えた

【Git】複数PCを用いたresetやamend利用法

commit改変時の複数PCのリポジトリの合わせ方

個人開発では会社用と自宅用にそれぞれPCを置いて同じリポジトリを用いながら編集を継続する運用方法がある(私もそう)。その場合、どちらかのPCでリモートからpull&更新&commit&pushを行い、もう1台のPCを使う際には再びリモートからpull&更新&commit&pushを行う形で、違うPCでもよどみなく編集を継続出来る。

但しcommitを整理する目的でgit rebase,git resetgit commit --amendなどを使用するとcommit IDが変わるため、リモートリポジトリやもう1台でのローカルリポジトリgit pullを行う際にconflictする。この記事は個人で複数台のPC使用時にreset使用等でconflictした場合のcommit履歴の整理方法を記載する。

f:id:ProgrammingForEver:20210922205020p:plain

git reset使用例

個人開発で複数台のPCを利用するケースで、git resetなどcommit IDの改変が行われる作業を行うと、複数人での開発同様に各ローカルリポジトリやリモートリポジトリとのconflictが生じる。

以下の例では、ある1台のPCで一気に最初のcommitまでresetする極端なgit resetを行ったと仮定する。その段階での作業ディレクトリにはa.sh, b.sh, c.sh, d.shの4fileだけがあると仮定し、reset直後にstaging&commitを行うとする(2回目のcommitになる)。

$ git reset HEAD~10
Unstaged changes after reset:
M       a.sh
M       b.sh
M       c.sh
M       d.sh

$ git log
commit f0f8ac18d38bf64ff3cab023fb7330280fdadcf4 (HEAD -> main)
Author: ************
Date:   Mon Sep 20 15:39:36 2021 +0900

    最初のcommit

$ git add .
$ git commit -m "2回目のcommit"
[main 736ed4a] 2回目のcommit
 2 files changed, 6 insertions(+)

$ git log --oneline
736ed4a (HEAD -> main, origin/main) 2回目のcommit   # - 1台目PCのcommit履歴
f0f8ac1 最初のcommit

リモートリポジトリへ強制push

次にresetした後のcommit(新2回目)をGitHubなどのリモートリポジトリにpushする。但し、reset前の2回目のcommitからIDが変化しているので普通にpushするとマッチしないためエラーで停止する。そこでpushコマンドに強制オプションの-fを付けて実行することで、resetを行ったローカルリポジトリの内容をリモートリポジトリに強制的に更新する。

この記事では自分のPC間のリポジトリ共有を対象としているので納得した上で行えるが、複数人で開発している場合は事前許諾を得てから行わなければならない。

$ git push -f
Enumerating objects: 9, done.
Counting objects: 100% (9/9), done.
Delta compression using up to 8 threads
Compressing objects: 100% (5/5), done.
Writing objects: 100% (5/5), 788 bytes | 788.00 KiB/s, done.
Total 5 (delta 4), reused 0 (delta 0)
remote: Resolving deltas: 100% (4/4), completed with 4 local objects.

conflict発生

さてリモートリポジトリにreset後のcommitが無事に登録出来たので、もう1台のPCでこのリモートリポジトリからgit pullすると、ここでもconflictが発生して本当なら2つだけのcommitのはずが4つcommitが見えており、マージ作業が求められる。これはもう1台の2番目のcommit(reset前のcommit:旧IDとする)とreset&push&pullした新しいcommitのID(新ID)が一致しないためで、このconflictも当然の結果だ。

$ git pull
remote: Enumerating objects: 9, done.
remote: Counting objects: 100% (9/9), done.
remote: Compressing objects: 100% (1/1), done.
remote: Total 5 (delta 4), reused 5 (delta 4), pack-reused 0
Unpacking objects: 100% (5/5), done.
From https://github.com/*********
 + e0a9a2d...0fccb57 main       -> origin/main  (forced update)
Merge made by the 'recursive' strategy.

$ git log --oneline
9739d8f (HEAD -> main) Merge branch 'main' of https://github.com/********** into main
0fccb57 (origin/main) 2回目のcommit   # - git pull後のcommit(新ID)
e0a9a2d 2回目のcommit   # - git pull前のcommit(旧ID)
f0f8ac1 最初のcommit

しかし本来commitの整理のためにgit resetgit commit --amendを使ったのに、その都度余分なマージcommitが生成される様では本末転倒である。ここでやりたいのはgit push -fの様に強制的なgit pullなのでその方法を以下説明する。

git-pullの強制conflictの解決方法

作業対象をmainブランチとする。なお下記作業を行うとstaging、作業ディレクトリにある変更が全て消失する点に注意。

$ git checkout main  # - mainブランチにcheckoutする
$ git fetch origin main   # -  リモートの最新状態を取り込む
$ git reset --hard origin/main   # - hardモードでリセット

$ git log --oneline
736ed4a (HEAD -> main, origin/main) 2回目のcommit   # - 1台目PCと同じcommit履歴になる
f0f8ac1 最初のcommit

既にgit pullでconflictしてしまった場合も以下で対応出来る。

$ git merge --abort     # - mergeを取り消す
$ git fetch origin main   # - リモートの最新状態を取り込む
$ git reset --hard origin/main   # - hardモードでリセット

$ git log --oneline
736ed4a (HEAD -> main, origin/main) 2回目のcommit   # - 1台目PCと同じcommit履歴になる
f0f8ac1 最初のcommit

上記の様に2台目のPCも1台目のPCと同じく整理されたcommit状態になる

【Git】リポジトリをコンパクトにする

リポジトリのディスク容量を削減する

開発を何年も継続し続けるとリポジトリもそれなりの規模になる。エンジニアの心理としてはリポジトリを整理したくなるもので、5種類の方法を紹介する。

方法 過去へのアクセス 削減度 作業の手軽さ
git-gc 50%~
partial-clone 90%~
shallow-clone 90%~
logic-compress 90%~
reborn 100%

その1:git gcを用いる

これは最も簡単でそれなりの効果が得られる手段だ。通常Gitはfileをsnapshotして全て保存しているが、git gcを発動するとバイナリfileに差分保存することでディスク容量を圧縮する。オプションを付ける事でさらに圧縮率を上げる事が可能。

programmingforever.hatenablog.com

その2:クローンサイズを下げる:パーシャルクローン

Git のリポジトリが大きくなると、非常に多くのcommit,tree,そしてblob(file)objectを保持する様になる。これは開発者が何年も前の特定commitにいつでも戻れる機能を維持するためであり、すべての到達可能なデータがローカルリポジトリにある事を意味する。しかし、Git の全履歴にある全file(blob)を保持しなくてもよい用途には、保持するデータを限定する「パーシャルクローン」機能がある。名前の通り、必要な一部のobjectのみ保持して、過去のfile参照などの必要が生じたら該当するfileをリモートから自動ダウンロードする機能である。

過去の履歴はほとんど使用せず、追加作業が主であればローカルリポジトリが過去のfile(blob)を保持していなくてもよい実態に合わせたクローンであり、ディスク容量の軽減が可能となる。保持するレベルでプロブレスクローンとツリークローンが選択出来る。

すでにフルクローンがローカルリポジトリにある場合、一旦それを削除してからパーシャルクローンで再度ローカルリポジトリを構成する方法が取れる。

以下比較のために各クローンのローカルリポジトリのobjectの保持状態一覧を示す。

各クローン選択 commit履歴 tree blob
フルクローン 全て 全て 全て
プロブレスクローン 全て 全て 最新のみ
ツリーレスクローン 全て 最新のみ 最新のみ
シャロークローン 最新のみ 最新のみ 最新のみ

フルクローンコマンド:git clone

  • 保持するcommit:全commit
  • 保持するtree:全tree
  • 保持するblob:全blob

パーシャルクローン:ブロブレスクローンコマンド:git clone --filter=blob:none

バーシャルクローン:ブロブレスクローン は、到達可能な全commitとtreeを保持するが、blob(file)は必要に応じて取得する。このクローンは、開発環境に普通に利用出来る。

  • 保持するcommit:全commit
  • 保持するtree:全tree
  • 保持するblob:最新blobのみ

パーシャルクローン:ツリーレスクローンコマンド: git clone --filter=tree:0

バーシャルクローン:ツリーレスクローンは、到達可能な全commitを保持するが、treeとblob(file)は必要に応じて取得する。このクローンは、commit履歴にアクセスしたいビルド環境に向く。

  • 保持するcommit:全commit
  • 保持するtree:最新treeのみ
  • 保持するblob:最新blobのみ

その3:クローンサイズを下げる:シャロークローン

さらに過去のcommitにはアクセスせず、現時点の最新fileだけのビルドだけ行うなどの用途には、シャロークローンが適している。

シャロークローンコマンド: git clone --depth=1

シャロークローンはHEAD以外の全てのcommit履歴も捨てる形でクローンのサイズを小さくする。当然ながら利用可能な Git コマンドが制限される。このクローンは後からのフェッチで過度の作業量を発生するため開発者の使用は推奨できない。

  • 保持するcommit:最新commitのみ(HEAD)
  • 保持するtree:最新treeのみ
  • 保持するblob:最新blobのみ

その4:不要と判断するブランチやblobを整理してgcで高圧縮

ここから下はリポジトリの論理的に不要となった部分を削除する方法を記載する。まずは元のリポジトリに色々と手を加える方法を紹介し、次のその5では過去のリポジトリと絶縁して新生する方法を紹介する。

その4に関しては詳細記事の紹介に留める。

zenn.dev

その5:ゼロから新しいリポジトリを生成

シャロークローンはHEAD以外の過去のcommit履歴を取り込まない事を説明したが、作業ディレクトリに入った最新fileを元に、ゼロから新しいリポジトリを立ち上げる運用も可能だ。何年も経過している様なプロジェクトでも一気にリポジトリがゼロになる。もちろんフルクローンでも同じことが出来るが、作業時間の無駄を避けたいと考えるのがエンジニアであり、本用途にはシャロークローンが適していると考える。

注意:この方法で作る新生リポジトリは元のリポジトリと完全に縁が切れた別物になる点に注意。また元のリポジトリ削除のためにrm -rfコマンドを使うので作業の際はディレクトリの位置などに十分注意すること

$ git clone --depth=1 <url> # - シャロークローンで最新リポジトリから最新file一式を取り込む
$ cd NewDirectory           # - 新しく作成された作業ディレクトリに移動 
$ rm -rf .git/    # - file以外の不要なリポジトリ一式を抹消する
$ git init        # -  最新fileだけの新しいローカルリポジトリを作る
$ git add .       # - 最新fileを全てstaging
$ git commit      # - 新生ローカルリポジトリの最初のcommit

$ git remote add origin <url>  # - 新生のリモートリポジトリのurlを入力
$ git push -u origin main      # - ローカルをリモートにpush

補足:新生に合わせてブランチ名をmainに変えたい場合

昨今の流れからブランチ名をmasterからmainに変更したい場合は下記手順を実行する。

  • ①ローカルのブランチ名をmainに変更(最初にローカルを変えること)
$ git branch -m master main
  • ②リモートのブランチ名をmainに変更
$ git push -u origin main  # - ローカルで変更したmainブランチをリモートへpush
  • GitHubの目的のリモートリポジトリにアクセス
  • ④Settings>Branches>Default branchでmainに変更してUPDATEを押す
  • リポジトリのTOP画面に戻ってブランチ名の欄の中にあるView all branchesボタンを押す
  • ⑤masterを消去(ゴミ箱をクリック)

補足:間違ってrmを実行した場合の復元方法

以下参考までにrmで消去したfileの復元方法を記載するが、プロセスが有効な状態に限られるし、削除したのが.gitフォルダの場合はこの方法では無理だ(代わりに下記外部ツールの利用参照)。いずれにせよそんな事態にならないのが一番

rmコマンドはinodeへのリンクを削除するがinodeは削除しないので、inodeへのリンクがあればデータは存在する。よってfileを扱ったプロセスがまだ存在すれば/proc/【プロセスID】/fd/を辿って復元出来る。

$ rm a.sh   # - fileを削除
$ lsof | grep "a.sh"   # - 削除したfileのプロセスIDを取得
less      8324        hoge    4r      REG  202,1        20 12592058 /home/hoge/a.sh (deleted)

1列目はプロセスに関連付けられたコマンドの名前、2列目がプロセスID、4列目の数字はファイル記述子である(”4r”の”r”は”regular file”、通常fileの意味)。プロセス8324がまだファイルを開いており(望みがある)、file記述子が4で通常アクセス出来るので/procからコピーする。

$ ls -l /proc/8324/fd/4
lr-x------  1 hoge hoge 64 Sep 21 22:31 /proc/8324/fd/4 -> /home/hoge/a.sh (deleted)

$ cp /proc/8324/fd/4 a.sh  # - cpコマンドで復元

上記で無理な場合は外部ツールに頼る方法もある。参考まで。

unskilled.site

【Git】実はファイルを差分でも保存する

Gitのファイル管理の続き

前回の記事でGitは作業ディレクトリ内のGit追跡fileを、staging及びcommit履歴に"blob"と呼ばれる圧縮独自フォーマットのfileで全て保存していることを説明した。

programmingforever.hatenablog.com

またその各file(blob)はsnapshot、つまり以前のfileとの変更点の差分保存ではなく、各fileの内容のまま(blobに形を変えた上で)全て保存していると説明した。

しかしそうなると、例えば1万行あるfileのたった1行だけを修正しても、非常によく似た大きなfile(blob)が2つ保存されてしまう。Git が最初のfile(blob)を保存するのは当然として、2つ目以降のfile(blob)は最初との差分(delta)のみを格納すれば効率良く保存出来るのにと、Gitが全てのfile(blob)をsnapshotとして保存するやり方に無駄を感じるのはごく自然だと思う。

Gitは条件成立時に差分保存を行う

実はその心配は無用だ。Gitはfileの差分保存を実に上手いやり方で実装している。編集作業中に行うgit addgit commitなどのコマンドを受けたGit がfile(blob)を保存する時は、確かに一つ一つのblobをsnapshotとして個々に保存している。これを緩いオブジェクトフォーマット(loose object format)と呼ぶ。

それに対して、規定または指定の条件が成立するとGitのgcガベージコレクション)が発動し、 Git はblobやtree,commitオブジェクトをpackfileと呼ばれるバイナリfileに差分単位で詰め込む作業を自動実行する(*設定による)。あるいは手動でgit gc コマンドを打つと実行する。

つまり、直近作業のfile(blob)は差分ではなくsnapshotの形で全て保存されるが、規定または指定の条件成立時にfile(blob)は差分管理で保存される、ハイブリッド制御が行われることを示す。

発動条件は色々あり、指定やパラメータ調整も出来る(設定fileの gc.auto と gc.autopacklimitなど)。なおgcの別機能である絶縁commitや不要なblobを自動削除する発動期間の設定とは別である。

では実際にgit gcコマンドを使って実証してみる。例題として前記事の通りの作業ディレクトリを新たに用意する。

ここで差分保存の効果を確認出来る様にa.shを100kバイト超の大きなテキストfileとし、No.3のcommitで1行だけ変更する。またb.sh, c.sh, d.shは全て空fileとしたため3fileのblob IDが同じになる点に注意(file内容が同一であれば誰がどこで作成しようとも必ず同じIDになる)

f:id:ProgrammingForEver:20210915192442j:plain

最初のcommit

最初にa.sh,b,sh,c,sh,d,shの4fileを用意して、a.shのみstaging&commitを行う。

$ ls -a
.       ..      .git    a.sh    b.sh    c.sh    d.sh
$ find .git/objects -type f
$ git add a.sh
$ git commit -m "最初のcommit"
[master (root-commit) ef11425] 最初のcommit
 1 file changed, 1775 insertions(+)
 create mode 100644 a.sh

ここでGitレポジトリのobject folderを覗くコマンド、及びツリーを見るコマンドでblob objectの SHA-1ハッシュ値が確認出来る。下の例ではa.shが.git/objects/57/9da06-----である事が分かる。

$ find .git/objects -type f
.git/objects/57/9da06934bd26a1a2f791021a989b95dfcd8a39
.git/objects/fc/0010737a9457dd224c2f778d430b3c293c2e89
.git/objects/ef/11425c7169155b5c167dcfc3837c250613d12f

$ git cat-file -p master^{tree}
100644 blob 579da06934bd26a1a2f791021a989b95dfcd8a39    a.sh

そして、a.shつまり.git/objects/57の容量を確認すると88kバイトになっている事が分かる。

$ du .git/objects
88      .git/objects/57      # - これがa.sh
8       .git/objects/fc
0       .git/objects/pack
0       .git/objects/info
8       .git/objects/ef
104     .git/objects

$ 

以上でa.shをblobに格納されており、容量は88kバイトであることが確認出来た。]

2回目のcommit

次にb.shを加える2回目のcommitを行う。

$ git add b.sh
$ git commit -m "2番目のcommit"
[master 1999418] 2番目のcommit
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 b.sh
$ find .git/objects -type f
.git/objects/57/9da06934bd26a1a2f791021a989b95dfcd8a39
.git/objects/fc/0010737a9457dd224c2f778d430b3c293c2e89
.git/objects/19/99418d81ddda2f848a3e91cdd65d7c211963a0
.git/objects/ef/11425c7169155b5c167dcfc3837c250613d12f
.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
.git/objects/4a/e759d0ac93c9f59dc682cf3c606bef2f5ee25f

$ git cat-file -p master^{tree}
100644 blob 579da06934bd26a1a2f791021a989b95dfcd8a39    a.sh
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    b.sh

$ du .git/objects
88      .git/objects/57
8       .git/objects/fc
0       .git/objects/pack
8       .git/objects/19
0       .git/objects/info
8       .git/objects/ef
8       .git/objects/e6
8       .git/objects/4a
128     .git/objects

同様にb.shが追加されている事がわかる。なおb.shは空fileだがblobフォーマットの関係などで8kバイトの容量になる。

3回目のcommit

3回目のcommitでは、a.shの一行目に一文字だけの僅かな変更を加えた上で、a.shとc.shをcommitする

$ vi a.sh                      # - 一文字だけ加える
$ git add a.sh c.sh
$ git commit -m "3番目のcommit"
[master 8a20ac3] 3番目のcommit
 2 files changed, 1 insertion(+), 1 deletion(-)
 create mode 100644 c.sh

$ find .git/objects -type f
.git/objects/57/9da06934bd26a1a2f791021a989b95dfcd8a39
.git/objects/d7/754295c833a6a8147c6bd22122d145d2f09b47
.git/objects/fc/0010737a9457dd224c2f778d430b3c293c2e89
.git/objects/8a/20ac30c95d90fc1c92aaa7b433e91ffe7f8d8f
.git/objects/19/99418d81ddda2f848a3e91cdd65d7c211963a0
.git/objects/6e/9ab2c475344fa2243fc0b3a7a683143fe1eeb4
.git/objects/ef/11425c7169155b5c167dcfc3837c250613d12f
.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
.git/objects/4a/e759d0ac93c9f59dc682cf3c606bef2f5ee25f

$ git cat-file -p master^{tree}
100644 blob d7754295c833a6a8147c6bd22122d145d2f09b47    a.sh
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    b.sh
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    c.sh

$ du .git/objects
88      .git/objects/57     # - 最初のa.sh
88      .git/objects/d7     # - 一文字だけ修正したa.sh
8       .git/objects/fc
0       .git/objects/pack
8       .git/objects/8a
8       .git/objects/19
8       .git/objects/6e
0       .git/objects/info
8       .git/objects/ef
8       .git/objects/e6
8       .git/objects/4a
232     .git/objects

上のgit/objects/57が最初のa.sh、.git/objects/d7が改訂したa.shで、共に88kバイトの容量で保存されている事がわかる。

4回目のcommit

続いて最後のcommitを行う。

$ git add d.sh
$ git commit -m "4番目のcommit"
[master cac41d4] 4番目のcommit
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 d.sh
$ find .git/objects -type f
.git/objects/57/9da06934bd26a1a2f791021a989b95dfcd8a39
.git/objects/d7/754295c833a6a8147c6bd22122d145d2f09b47
.git/objects/fc/0010737a9457dd224c2f778d430b3c293c2e89
.git/objects/ca/c41d483660425387f0488f0551df45f9c9e186
.git/objects/8a/20ac30c95d90fc1c92aaa7b433e91ffe7f8d8f
.git/objects/19/99418d81ddda2f848a3e91cdd65d7c211963a0
.git/objects/6e/9ab2c475344fa2243fc0b3a7a683143fe1eeb4
.git/objects/55/250e90d114ed645ba6a8ef4d3d5b8b8c99fd10
.git/objects/ef/11425c7169155b5c167dcfc3837c250613d12f
.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
.git/objects/4a/e759d0ac93c9f59dc682cf3c606bef2f5ee25f

$ git cat-file -p master^{tree}
100644 blob d7754295c833a6a8147c6bd22122d145d2f09b47    a.sh
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    b.sh
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    c.sh
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    d.sh

$ du .git/objects
88      .git/objects/57
88      .git/objects/d7
8       .git/objects/fc
8       .git/objects/ca
0       .git/objects/pack
8       .git/objects/8a
8       .git/objects/19
8       .git/objects/6e
0       .git/objects/info
8       .git/objects/55
8       .git/objects/ef
8       .git/objects/e6
8       .git/objects/4a
248     .git/objects

これまでの4つのcommit の結果、合計11オブジェクト、容量はa.shの新旧2つ分の176kバイトにb.sh〜d.shの24kバイトを足した200kバイトになった。

git gcコマンド実行

以上を確認したら、手動でgit gcを実行する。

$ git gc
Enumerating objects: 11, done.
Counting objects: 100% (11/11), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (11/11), done.
Total 11 (delta 2), reused 0 (delta 0)
Computing commit graph generation numbers: 100% (4/4), done.

$ find .git/objects -type f
.git/objects/pack/pack-a4e0422cb92e03407ff3d178f35cd04ce7c568d3.idx
.git/objects/pack/pack-a4e0422cb92e03407ff3d178f35cd04ce7c568d3.pack
.git/objects/info/commit-graph
.git/objects/info/packs

上の通りpackfileにまとめられ、11個あったオブジェクトが4個に減少している。もちろん下記の様にfileとしていつでも取り出せる状態だ。

$ git cat-file -p master^{tree}
100644 blob d7754295c833a6a8147c6bd22122d145d2f09b47    a.sh
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    b.sh  # - 以下3fileは
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    c.sh  # - 同一内容なので
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    d.sh # - 同じIDになる

ここで容量を確認してみよう。すると.git/objects/packにまとめられたfileの容量はa.shの新旧2つ分の176kバイトにb.sh〜d.shの24kバイトを足した200kバイトから80kバイトに減少したのが分かる。

$ du .git/objects
80      .git/objects/pack
16      .git/objects/info
96      .git/objects
  • a.shの新旧file(blob)の重複内容が差分保存になり大幅に削減された
  • b.sh, c.sh, d.shの各fileは空fileなので単純にフォーマット用の8kバイトが削除された

まとめ

以上実証した通り、Gitは追跡対象のfile(blob)をsnapshotで保存するが、ある程度期間が経過したfile(blob)を自動で差分保存することでディスクの容量削減を実現する、優れたハイブリッド制御を行っている。

【Git】内部のファイル管理

Gitのfile管理方法

Gitは内部のfile管理のイメージを掴むと理解が一気に進む。そこで図とログを同時に扱ってGitが3大ツリーでfileをどう扱っているかなどを説明しgit resetの動きも理解出来る図を掲載した。

Gitの3大ツリーでのfileの扱い

3大ツリーでのfileの扱いはそれぞれ下記の様に異なっている。

  • 作業ディレクトリ(Working directory):編集作業を行うフォルダを指し、各fileはシェルやエディターで自由に扱える。fileはGitが追跡するfileとGitが追跡しない対象外のfileが混在する
  • staging(ステージング、index、インデックス):次のcommit候補一式(Gitが追跡する全file)を、blob(object)と称する圧縮独自フォーマットに変換して保管している
  • commit(コミット履歴):staging同様に各commit毎に、GItが追跡する全fileをblobで保管している。但し以前のcommitと変化が無いfileは参照Linkを用いることで同一fileの重複保存を回避している

blobはfileにヘッダーを追加してからSHA1でハッシュ化を行い、それらの情報を付与したfileをzlibで圧縮した上で保存する。SHA1は objectのIDとして利用出来るので、何らかのミスでfileを失ってもそのSHA1を手がかりに復活させる方法が存在する。

サンプル

理解のためにシンプルな構成を想定する(下図)。

  • 全部で4commit
  • 各commit毎にa.sh、b.sh、c.sh、d.shとfileを1つずつ追加する
  • 3番目のcommit前にa.shの内容を修正してからcommitする

f:id:ProgrammingForEver:20210915192442j:plain

これを用いて1つずつcommitを進めてみる。

1回目のcommitまで

a.shを追加し、staging&commitを行う。fileは作業ディレクトリに1個、stagingにblobに変化して1個、commitにもblobが1個、以上3個のfileが存在する。

注意:fileの扱いに特化して説明する関係上、treeおよびcommit objectは除外する

$ touch a.sh
$ git add .
$ git commit -m "最初のcommit"

f:id:ProgrammingForEver:20210915192914j:plain

stagingの確認:git ls-filesコマンドでstagingされているfile(blob)一覧が出る(--stageオプションを加えると各fileのID(SHA1)も確認出来る)。以下ではa.shがstagingされているのが見える。

$ git ls-files --stage
100644 579da06934bd26a1a2f791021a989b95dfcd8a39 0       a.sh

commitの確認:git logでcommitのID(SHA1)を確認後にgit show ID(SHA1) --name-onlyと打つと、そのcommitで保存されたfile(blob)一覧が出る。以下ではcommitにa.shが入っている事が分かる。

$ git log --oneline
ef11425 (HEAD -> master) 最初のcommit

$ git show ef11 --name-only
commit ef11425c7169155b5c167dcfc3837c250613d12f
Author: 
Date:   Sun Sep 19 15:33:23 2021 +0900

    最初のcommit

a.sh

2回目のcommitまで

b.shを追加し、staging&commitを行う。fileは作業ディレクトリに2個、stagingにblobに変化して2個、commitにもblobが2個、以上6個のfileが存在する。なお各commitにはGitの追跡fileが全て登録されるが、下図のa.shの様に前回のcommitから変更されていない同一内容のfileはそのfileを指し示すLinkを作成することで同一fileの重複保存を避けている。

$ touch b.sh
$ git add .
$ git commit -m "2回目のcommit"

f:id:ProgrammingForEver:20210915193354j:plain

同様にstagingにはa.shとb.sh、commitには新規追加したb.shだけが入っているのが分かる。

$ git ls-files --stage
100644 579da06934bd26a1a2f791021a989b95dfcd8a39 0       a.sh
100644 5d308e1d060b0c387d452cf4747f89ecb9935851 0       b.sh

$ git log --oneline
1999418 (HEAD -> master) 2番目のcommit
ef11425 最初のcommit

$ git show 1999 --name-only
commit 1999418d81ddda2f848a3e91cdd65d7c211963a0
Author: 
Date:   Sun Sep 19 15:46:49 2021 +0900

    2番目のcommit

b.sh

3回目のcommitまで

c.shを追加し、さらにa.shの内容を修正した上でstaging&commitを行う。fileは作業ディレクトリに3個、stagingにblobに変化して3個、そしてcommitにはblobが4個登録される(合計10個のfile)。これはGitがfileの差分管理ではなく、変更がある毎に別のfileとして保存するためで、図の様にa.shの変更前のblobがNo.1commit、a.shの変更後のblobがNo.3commitにそれぞれ保管されている。差分管理ではなく別fileとして管理する方式を取ったことで、branchやreset、rebaseなどの処理が容易に行える様になっている。

$ touch c.sh         # - c.shを追加
$ vi a.sh            # - a.shを編集
$ git add .
$ git commit -m "3回目のcommit"

f:id:ProgrammingForEver:20210915194422j:plain

a.shを変更、c.shを追加したのでstagingには3file(blob)が入る。なおa.shは内容が変更されたためSHA1ハッシュが違うIDに変更されている

commitは変更されたa.shと新規のc.shの2つのfile(blob)が保存されている。

$ git ls-files --stage
100644 d7754295c833a6a8147c6bd22122d145d2f09b47 0       a.sh
100644 5d308e1d060b0c387d452cf4747f89ecb9935851 0       b.sh
100644 61780798228d17af2d34fce4cfbdf35556832472 0       c.sh

$ git log --oneline
8a20ac3 (HEAD -> master) 3番目のcommit
1999418 2番目のcommit
ef11425 最初のcommit

$ git show 8a20 --name-only
commit 8a20ac30c95d90fc1c92aaa7b433e91ffe7f8d8f
Author: 
Date:   Sun Sep 19 15:50:03 2021 +0900

    3番目のcommit

a.sh
c.sh

4回目のcommitまで

d.shを追加してstaging&commitを行う。fileは作業ディレクトリに4個、stagingにblobが4個、commitにはblobが5個登録される(合計13個のfile)。

$ touch d.sh         # - d.shを追加
$ git add .
$ git commit -m "4回目のcommit"

f:id:ProgrammingForEver:20210915194612j:plain

以上4commitの結果、stagingには4file(blob)が入っている。またcommitは最終追加のd.shが保存されている。

$ git ls-files --stage
100644 d7754295c833a6a8147c6bd22122d145d2f09b47 0       a.sh
100644 5d308e1d060b0c387d452cf4747f89ecb9935851 0       b.sh
100644 61780798228d17af2d34fce4cfbdf35556832472 0       c.sh
100644 f2ad6c76f0115a6ba5b00456a849810e7ec0af20 0       d.sh

$ git log --oneline
cac41d4 (HEAD -> master) 4番目のcommit
8a20ac3 3番目のcommit
1999418 2番目のcommit
ef11425 最初のcommit

$ git show cac4 --name-only
commit cac41d483660425387f0488f0551df45f9c9e186 (HEAD -> master)
Author: 
Date:   Sun Sep 19 15:51:13 2021 +0900

    4番目のcommit

d.sh

fileの総数

ここで改めてfileの総数を数えると、作業ディレクトリに4個、stagingに4個(blob)、commit履歴に5個(blob)の合計13個になった。前記した通りa.shはcommit履歴のNo.1 commitに初期版、No.3 commitに修正版と内容が異なるfileが2つ存在している。

f:id:ProgrammingForEver:20210915195724j:plain

高効率なfile保存

開発では各commit毎に1つのfileを修正&commitするパターンが多く、仮に10個のfileで30commitある場合は、作業ディレクトリに10個、stagingに10個(blob)、commitに30個(blob)の合計50fileが存在する事になる。

仮に10fileの各大きさを10kバイトとした場合、10k*10=100kバイトの実容量に対して、blobの圧縮率を1/4とするとstagingとcommit履歴を足しても2.5k*(10+30)=100kバイト程度に収まり、スマートにcommit履歴を保存出来ている計算となる。

実はGitは一定期間が過ぎるとgit gcコマンドが自動で走って、file(blob)の各差分をバイナリfileにまとめる方法でより高圧縮に保存する。詳しくは下記の記事を参照して欲しい。

programmingforever.hatenablog.com

git resetを実行する

ここで2つ前のcommitまでresetすべくgit reset HEAD~2を実行してみる

$ git reset HEAD~2

HEADが移動して、No.2のcommit履歴からblobをstagingに戻す(copy)。a.shはLink先を辿ってNo.1のcommit履歴からblobを取り出す。resetは--mixedの動作なので作業ディレクトリは最新commitの状態のままだ。

f:id:ProgrammingForEver:20210915200005j:plain

git reset --hardを実行すると

上記を元に戻してから、今度は2つ前のcommitまでをreset --hardで実行してみる

$ git reset ORIG_HEAD    # - 元に戻す
$ git reset --hard HEAD~2

HEADが移動して、No.2のcommit履歴からblobをstagingに戻す(copy)。a.shはLink先を辿ってNo.1のcommit履歴からblobを取り出す。次にresetは--hardの動作なのでstagingに戻したblobを通常のfileに解凍した上で作業ディレクトリに展開する(作業ディレクトリは更新されるので、元の状態は破壊される点に注意)。

f:id:ProgrammingForEver:20210915200726j:plain

いずれもGitが差分管理ではなく、変更file管理方式を採用していることでresetなどのcommit編集が容易に行えることが分かる。

【Git】stashで退避する範囲

git stashの有効範囲を調べる

以前の記事でも記載した様にGitは、fileを6つの状態で扱う。

programmingforever.hatenablog.com

そこで、その内ローカル側の5つの状態のfileをgit stashコマンドでどの様に退避するのかをまとめた。但しcommitはstashの対象にならないため実際は以下の2から5までの状態に置いた4つのfileの退避を確認する。

各shell scriptの状態

説明のために5つのfileをそれぞれ下記の状態にして動作を追う。

  1. a.sh:fileをcommitした状態
  2. b.sh:fileを変更してstagingした状態
  3. c.sh:fileを変更してstagingしていない状態
  4. d.sh:fileをGitがまだ追跡していない状態(新規file等)
  5. e.sh:fileをGitが無視する状態(.gitignoreで指定したfile)

コマンドで退避前の各fileの状態を確認

$ ls    
a.sh    b.sh    c.sh    d.sh    e.sh   # - 各fileが見える 
$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    modified:   b.sh     # - b.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)
    modified:   c.sh     # - c.shは変更あり 

Untracked files:
  (use "git add <file>..." to include in what will be committed)
    d.sh                 # - d.shはGit追跡対象外、e.shは無視なので見えない

$           

図で退避される範囲を確認

f:id:ProgrammingForEver:20210913111049j:plain

git stash

Gitの追跡対象かつ変更したfile及びfolderを作業ディレクトリとstagingから退避し、作業ディレクトリとstagingをcommt直後に戻す。

git stash popで退避を戻すとstaging状態が解除されている。またstagingしたfileを追加修正してstagingしていない場合、stagingしたfileは消失して追加修正したfileが残る。

  1. 退避する(commit直後のstaging状態に戻る)
  2. 退避する(commit直後の作業ディレクトリ状態に戻る)
  3. 退避しない
  4. 退避しない
$ git stash         # - 退避を実行 
Saved working directory and index state WIP on main: 80afbdd test1
$ ls  
a.sh    b.sh    c.sh    d.sh    e.sh     # - b.sh, c.shの内容は変更前に戻っている 
$ git status
On branch main
Untracked files:
  (use "git add <file>..." to include in what will be committed)
    d.sh            # - staging中のb.sh、変更&staging前のc.shが消えた

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

$ git stash show    # - b.shとc.shが退避されているのが見える
 b.sh | 2 +-
 c.sh | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

$ git stash pop     # - 退避を元に戻すと・・ 
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:   b.sh   # - stagingが解除されている点に注意 
    modified:   c.sh

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

no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (ec403a9850501332a6bbc52a694c5473c97ba435)
$ git add b.sh      # - 必要に応じてstagingし直す 
$

git stash -k

-kオプションを付けると、Gitの追跡対象かつ変更したfile及びfolderを作業ディレクトリとstagingから退避し、作業ディレクトリはcommit直後に戻すが、staging状態は現状を維持する。

stagingしたfileを追加修正してstagingしていない場合は、stagingした内容と追加修正した内容が、git stash popで退避を戻す際にマージされるので対象fileを編集してconflictを解決する。

  1. 退避する(staging状態を維持する)
  2. 退避する(commit直後の作業ディレクトリ状態に戻る)
  3. 退避しない
  4. 退避しない
$ git stash -k         # - -kオプションを付けて実行
Saved working directory and index state WIP on main: 80afbdd test1
$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    modified:   b.sh   # - stagingのb.shが残っている 

Untracked files:
  (use "git add <file>..." to include in what will be committed)
    d.sh  # - 変更&staging前のc.shが隠され、追跡外のd.shが見える 

$ git stash show    # - b.shとc.shが退避されているのが見える
 b.sh | 2 +-
 c.sh | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

$ git stash pop        # - 退避を元に戻すとstash前と同じ状態
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    modified:   b.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:   c.sh

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

Dropped refs/stash@{0} (c976e74a49d3960940bd367e1da312d030cf75f9)
$

git stash -u

-uオプションを付けると、Gitの追跡対象かつ変更したfile及びfolderを作業ディレクトリとstagingから退避し、作業ディレクトリとstagingをcommt直後に戻す動作に加えて、

作業ディレクトリ内のGit追跡対象外のfile及びfolderも、作業ディレクトリとstaging内から退避して、popするまで作業ディレクトリから消失させる。但し.gitignoreで指定したfileは退避しない。空のfolderは削除されて戻らない。

  1. 退避する(commit直後のstaging状態に戻る)
  2. 退避する(commit直後の作業ディレクトリ状態に戻る)
  3. 退避する
  4. 退避しない
$ git stash -u            # - -uオプション付きで退避を実行 
Saved working directory and index state WIP on main: 80afbdd test1
$ ls  
a.sh    b.sh    c.sh    e.sh     # - Git追跡対象外fileのd.shが消えた 
$ git status
On branch main
nothing to commit, working tree clean  # - 何も変更がないと表示される
$ git stash pop           # - 退避を戻すと・・・
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:   b.sh      # - stagingは外される 
    modified:   c.sh

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

no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (11ee1ab69090b3360d2f19bf97b9a646f575fc15)

$ ls
a.sh    b.sh    c.sh    d.sh    e.sh   # - 消えたd.shが復活   
$ git add b.sh            # - 必要に応じてstagingし直す 
$

git stash -a

-aオプションを付けると、Gitの追跡対象かつ変更したfile及びfolderを作業ディレクトリとstagingから退避し、作業ディレクトリとstagingをcommt直後に戻す動作に加えて、

作業ディレクトリ内のGit追跡対象外やGitに無視する様に指定したfile及びfolderも、作業ディレクトリとstaging内から退避して、popするまで作業ディレクトリから消失させる。空のfolderは削除されて戻らない。

  1. 退避する(commit直後のstaging状態に戻る)
  2. 退避する(commit直後の作業ディレクトリ状態に戻る)
  3. 退避する
  4. 退避する
$ git stash -a           # - -aオプション付きで退避を実行 
Saved working directory and index state WIP on main: 80afbdd test1
$ ls  
a.sh    b.sh    c.sh     # - Git追跡対象外のd.shと無視指定のe.shが消えた 
$ git status
On branch main
nothing to commit, working tree clean  # - 何も変更がないと表示される
$ git stash pop          # - 退避を戻すと・・・
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:   b.sh     # - stagingは外される 
    modified:   c.sh

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

no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (11ee1ab69090b3360d2f19bf97b9a646f575fc15)

$ ls
a.sh    b.sh    c.sh    d.sh    e.sh   # - 消えたd.sh, e.shが復活   
$ git add b.sh          # - 必要に応じてstagingし直す 
$

git stash -- files

git stash -- filesで指定fileだけを退避出来る。

git stash -p

git stash -pでハンク単位で退避出来る。