前置き
Gitはバージョン管理に欠かせないツールですが,しばらく使っていないとすぐに忘れてしまうので,よく使うコマンドをまとめておきます.この手の記事は余るほどにあるのですが,自分用のメモという意味合いでまとめておきます.
コマンド
Initializing Local Repositories
$ git init
gitリポジトリの作成.正確には,そのディレクトリに '.git' というサブディレクトリを作ることによって,git管理を可能にする.
ローカルで作成するときはこれを用いて,GitHubにあるリポジトリを引っ張ってくるときは '$ git clone'($ git clone [remote-url] [directory-name]
) を用いる.
$ git status
現在のgit状態をみる.非常によく使う.
--untracked-files=no
オプションをつけると,tracked-fileのみを表示できる.
$ git diff ([file-name])
ワーキングツリーとステージの差分を見る.ステージと最新のコミットの差分を見たいときは$ git diff --cached
のようにオプションをつける.コミット間でdiffを見たい場合は$ git diff [commit-hash(A)] [commit-hash(B)] --name-status
のようにする(AからBへの差分が表示される).ちなみに--name-status
をつけるとファイル名とその変化の種類(ModifiedならM)だけを表示できる.
$ git show [commit-hash]
特定のcommit-hashのcommit内容を表示する.
$ git blame [file-name]
[file-name]の各行が変更された最終commit(commit-hash, author, datetimeなど)を確認することができる(参照).commit-hashが分かったら,上記の$ git show
で詳しい内容を確認すればいい.
-L
オプションをつけると,行を指定できる.
$ git blame -L 125,+5 [file-name] # 125行目から5行分
$ git blame -L 125,130 [file-name] # 125行目から130行目まで
Staging & Committing
$ git add [file-name]
変更を加えたファイルをstageに載せる.変更を行った全てのファイルをstagingしたい場合は$ git add .
,untrackedでないファイルのみをstagingしたい場合はgit add -u
を使う.さらに,ファイル一つを一気にではなく部分ごとにstagingしたい場合は,git add -p
が便利.提示される部分を細かくしたければ s を押す.
下のようにすると,エディタでtailing whitespaceを削除する設定にしていたときに余分な空白をaddしなくて済む.
$ git diff -U0 -w --no-color | git apply --cached --ignore-whitespace --unidiff-zero
長いのでaliasを登録しておくと便利.
下のように,引数付きのaliasにしておけば,git addw [file-name]
のようにして使える.
$ git config --global alias.addw '!f(){ git diff -U0 -w --no-color $1 | git apply --cached --ignore-whitespace --unidiff-zero;};f'
$ git commit -m "message"
stageにある全てのファイルをcommit
$ git log
今までのcommit履歴を表示する.2番目のようにすると,ツリー状に表示される.
$ git log --graph
とすればツリー状に,さらに$ git log --oneline --decorate --graph
というオプションも付ければ簡潔なツリー状に表示できる.
以下のようにすれば,日付やauthorなども一行で出せる(カスタマイズ可)
$ git log --graph --date=short --decorate=short --pretty=format:"%C(yellow)%h%C(reset) %C(magenta)[%ad]%C(reset)%C(auto)%d%C(reset) %s %C(cyan)@%an%C(reset)"`
また,どのファイルが編集されたか詳細を確認したい場合は,--name-status
オプションをつけると良い.
$ git rm [file-name]
gitはファイルの削除も差分として認識されるので,ワーキングツリー上で削除した場合は,インデックス(ステージ)に空のファイルとして残ってしまうことがある.それを防ぐために,ファイルを完全に削除したいなら$ git rm
を使う1.このサイトを参照.
$ git mv [file-from] [file-to]
gitにはファイルの移動やファイル名の変更をを明示的に追跡することがないので,普通のmvを行う場合は以下のような操作をしなくてはいけない.代わりに$ git mv README.md README
とすれば,インデックス(ステージ)に即座に反映される.
$ mv README.md README
$ git rm README.md
$ git add README
$ git stash
or $ git stash save "message"
- まだcommitしていない変更を一旦退避する.commitしていない変更がある状態で,別のブランチにcheckoutしたり,merge/cherry-pickしようとするとエラーが出てできないが,一旦stashして再度applyすることでこの問題を解決する.
- stashはスタック構造になっており,その中身は
$ git stash list
にて確認できる.LIFOになっているので,直前のstash内容を反映させるには,$ git stash apply
,特定のstash内容を反映させる場合は,$ git stash apply@{3}
のようにする.そのstash内容をスタックから削除するには$ git stash drop
とする.$ git stash clear
はスタックを空にする. -
$ git stash pop
はスタックからpopするので,applyとdropを同時にする.
特定のファイルのみをstashする場合は他のファイルをstagingしたあとに,-k (--keep-index)
オプションをつけてstashする.saveメッセージもつけたい場合は,$ git stash save -k "message"
の順.
$ git clean
untracked fileを削除する.gitignoreされているファイルを削除する場合は-x
,ディレクトリごと削除する場合は-d
,forceする場合は-f
オプションをつける.まとめて$ git clean -xdf
などともできる. --dry-run
オプションをつけることで,削除の対象となるファイルを確認できる.
Undoing Changes
resetコマンドに関しては,この記事に非常に分かりやすく書いてあるので,以下のコマンドを盲目的に使う前に,実際に何を行っているのかを理解しておくといいと思います.
また,下の補足も参考に.
$ git reset HEAD [file-name]
stagingしたファイル(まだcommitしていない)をunstageする.(ワーキングツリーはそのまま)
$ git reset HEAD
とすると,全てのファイルがunstagingされる.
$ git reset --soft HEAD^
直前のコミットのみを取り消して,ファイル変更やインデックス(ステージ)の状態はそのままにする.
$ git reset --hard HEAD^
直前のcommitを丸々全て取り消す.そのcommitに付随したファイル変更は全て取り消される.(一つ前のcommit直後の状態に丸々戻るイメージ)
$ git reset --hard ORIG_HEAD
直前に間違えてしてしまったリセットを取り消す.commitされていなかった場合は,ORIG_HEADとして残っていないので取り戻せない.この意味で,$ git reset --hard HEAD^
は注意が必要.
$ git commit --amend
直前のcommitにファイルを加えたいorコミットメッセージを変更したいときに使う.直前のcommitが上書きされる.
$ git commit -m "message"
$ git add [forgotten-file]
$ git commit --amend
$ git rest --soft HEAD^
を使って,HEADの位置だけを元に戻してからaddし直しても機能的には同じことになるが,直前のコミットを上書きするだけならこの方が楽.たくさんのcommitを一つにまとめたいなら,以下の方法がいい(もしくは$ git rebase -i
を使う).
$ git commit -m "message"
$ git reset --soft HEAD^ #HEAD^は,HEADの一つ前のcommitを表す(HEAD~1も同じ)
$ git add [forgotten-file]
$ git commit -m "message"
$ git checkout ([commit-hash]) -- [file-name]
特定のファイルを,[commit-hash]の状態まで戻す.そのcommit以降の変更は完全に消されてしまうので,注意が必要.[commit-hash]は省略可能で,省略した場合はHEADを指す.
意味としては,$ git checkout [commit-hash]
と同じ要領で,そのファイルの最後のcommit位置までcheckoutするということ.'--'を付けなくてはいけない理由は,このサイトにあるように$ git checkout branch名
と明確に区別するためだが,branch名とバッティングがなければ機能する.
$ git checkout -p -- [file-name]
のようにpatchオプションをつけると,区分単位で選択できる.
HEADに戻す場合は,ステージングされているファイルは変更されない.
Git Branching
$ git branch
branch一覧の確認. $ git branch -v
とすると,それぞれのbranchのcommit内容も表示することができる.また,$ git branch -a
とすると,リモートブランチも表示することができる.
$ git branch [new-branch-name]
新たなbranchの作成.
$ git checkout [destination-branch]
指定のbranchへHEADを移動.
$ git checkout -b [new-branch-name]
$ git branch [new-branch-name]
+ $ git checkout [new-branch-name]
$ git branch -d [branch-to-delete]
branchの削除.branchがコミットツリーの先にいる状態などでは,branchを削除してしまうと再び戻れなくなる(厳密にはcommit-hashを指定すれば戻ってこれる)のでエラーが出るが,それでも削除したい場合は-D
にする.
$ git push --delete [remote-name] [branch-to-delete]
GitHubなどのリモートブランチを削除する.それに対応していたローカルブランチは残る.この方法でリモートブランチを削除すると,追跡ブランチも削除される(GitHub上でGUIで削除した場合は追跡ブランチが残るので$ git remote prune [remote-name]
をしないといけない).
$ git merge [branch-name]
branchのmerge.現在HEADが指しているbranchに対して,指定したbranchの内容が取り込まれる.(masterに,developブランチで行ってきた変更を取り込みたいのなら,$ git checkout master
を行ってから,$ git merge develop
を行う.)
ちなみに$ git merge [branch-name] --no-ff
とすると,mergeする二つのbranchが直接つながっているときにも,Fast Forward Mergeを行わずにcommitツリーが分かりやすくなるので,こちらをおすすめ.
conflictが起こった場合は,当該のファイル(git statusで確認すると"both modified"という表記になっている)を編集してconflictを解消した後,addしてcommitし直す(もしくはgit merge --continue
)ことでmergeが完了する.
conflictしているファイルを一括で片方の対応に合わせたい場合は,この記事にあるように,今いるブランチの変更を維持したい(マージを拒否したい)場合は--ours
,マージを完全に取り込みたい場合は--theirs
オプションをつけてcheckoutすればおk.
$ git checkout --ours [file-name]
$ git add [file-name]
$ git rebase [branch-name]
マージコミットを残さずに特定のブランチの変更を取り込む際に使う.コミットツリーを綺麗にできるが過去のコミットツリーを改ざんしてしまうので注意が必要.リモートにpushしているブランチでこの操作をしてしまうと,リモートブランチとの整合性が取れなくなってしまうので,$ git push --force origin dev
のようにforce-pushしなくてはいけなくなる.
あるリモートリポジトリに自分以外に複数のpull requestがあって,別のPRが先にmasterにマージされてしまったときに,そのままでは自分のPRはマージコンフリクトが起きてしまうので,masterの変更を取り込んで再度PRを出す必要がある(厳密に言うと,PRはリモートブランチを指定しているだけなので,このリモートブランチにpushし直せばOK).このときにpullしてしまうと一番上にマージコミットができてしまうが,rebaseを使うことであたかも過去にそのmasterの変更を知っていて,自分自身が加えた変更が最新にあるようなコミットツリーになる.
リモートブランチを一気に取り込むpullのようなコマンドはないので,fetch->rebaseの順を辿る.conflictが起きた場合は,解消後に--continueオプションを打つとrebaseが完了する.
$ git fetch origin master # 追跡ブランチorigin/masterにfetch
$ git rebase origin/master # 追跡ブランチorigin/masterをrebaseで取り込む
# conflictが起きていたら解消
$ git rebase --continue
$ git rebase -i [commit-hash]
HEADから[commit-hash]で指定した(一連の)commitログを改変することができる.[commit-hash]で指定した一つ後のcommitまで含まれる.
複数の一連のcommitを一つにまとめたり(参照),分けたり(参照),commitの順番を入れ替えたり(参照),特定のcommitの内容を書き換えたり(参照)できる.
-i
オプションだけではマージコミットは候補に含まれないので,-p
オプションもつける必要がある.
難易度が高い操作なので慎重に進める必要があり,途中で分からなくなったらひとまず$ git rebase --abort
とすれば一連の操作を取り消せる.
$ git cherry-pick [commit-hash]
branch全体をmergeするのではなく,特定のcommit内容のみを取り込む.ある範囲のcommit一連を取り込みたい場合は $ git cherry-pick [commit-hash(A)]..[commit-hash(C)]
(AがCよりも過去)のようにする(commit-hash(A)は含まれないことに注意).conflictした場合は,解消後にgit cherry-pick --continue
とすることでcherry-pickが解消する(mergeの場合と異なるので注意).また $ git cherry-pick [commit-hash] --no-commit
とすると,commitを作らずに単に今の状態に内容を取り込むことができる.
Remote Repositories
$ git remote
リモートリポジトリ一覧の確認.$ git remote -v
とすると,urlつきで表示される.
$ git remote add [remote-name] [remote-url]
リモートリポジトリを[remote-name]として登録.
$ git remote rename [old-name] [new-name]
リモートリポジトリのrename.
$ git remote rm [remote-name]
リモートリポジトリの削除.
$ git remote prune [remote-name]
GitHub上でリモートブランチを削除したときにローカルに残ってしまった追跡ブランチを削除するときなどに使う.この記事を参考に.
$ git clone [remote-url] [directory-name]
リモートリポジトリを,現在のディレクトリ直下にコピー.[directory-name]を付けないと,リモートリポジトリの名前がそのまま使われる.このとき,リモートリポジトリの名前は,自動的に**'origin'**になる.
$ git push -u [remote-name] [local-branch]
branchの内容をリモートリポジトリのbranch反映させる.$ git push -u origin master
のように使うが,共同開発をしているときにはこの記事にあるように,リモートリポジトリのmasterブランチにプルリク無しでmergeしてしまうので絶対にやってはいけない! 必ず新たなブランチを切ってから,$ git push -u origin develop
などと行う2.
正式には$ git push -u [remote-name] [local-branch]:[remote-branch]
にするが,同じ名前のブランチにpushするのが普通なので,上のようにコロン以下を省略することが多い(参照).
ちなみに,オプションの'-u'は次回から$ git push
とだけ打っても同様の操作が行われるように記憶するもの(参照).これは,"上流ブランチ"が変更されるから(参照)
また,ローカルでrebaseなどをしてリモートブランチとの整合性が取れなくなった場合は,$ git push -f [remote-name] [branch-name]
のようにforce pushしなくてはいけない.
$ git fetch [remote-name]
リモートリポジトリの変更内容を,リモート追跡ブランチに取り込む(参考).
下のようにすると,リモートリポジトリの特定のbranchのcommit履歴を追従できる.
$ git fetch origin
$ git branch review-ai-fix origin/fixing-ai-heuristics #新たにreview-ai-fixというbranchをローカルリポジトリに作ってそこにorigin/fixing-ai-heuristicsの内容を反映させる
$ git checkout review-ai-fix
$ git log --graph
$ git pull [remote-name] [remote-branch-name]
リモートリポジトリの内容を取り込んで,さらにHEADの位置にmergeする.次の操作と同じ.(pull=fetch+merge)
$ git fetch origin
$ git merge origin/master #origin/masterというリモート追跡ブランチが,HEADが指しているbranchにmergeされる
force pushされたブランチをpullしようとするとエラーが出るが,remoteの変更を信じて取り込みたい場合は,この記事にあるように,fetchしてreset --hardすればいい.
$ git fetch origin
$ git reset --hard origin/master
Submodules
$ git submodule add [remote-url]
サブモジュールをカレントディレクトリに追加する.この情報は,gitのトップディレクトリの.gitmodule
ファイルに記載される.
$ git diff --submodule
サブモジュールの内側にいる場合はgit diff
で差分を表示することができるが,外側にいる場合にはわざわざサブモジュール内の差分を出してくれない.--submodule
オプションをつけると,その中身を明示的に表示してくれる.
$ git clone [remote-url] --recursive
サブモジュールを含んだリポジトリをcloneする.こうしておくと,サブモジュールの中身のファイルまでcloneしてくれる.サブモジュールがさらにサブモジュールを含んでいる場合などにも,このコマンドは有効.
$ git submodule update
サブモジュールのどのrevision(ブランチorコミットハッシュ=コミット)を指定するかは大元のgitリポジトリによって管理される.しかし,大元のgitリポジトリでcheckoutをしてサブモジュールのrevisionを変更しても直ちにサブモジュールの中身は変更されないことに注意されたい.(git diff --submodule
でdiffが表れてしまう.)
変更内容をサブモジュールの中身に反映させるコマンドがこれ.
$ git submodule init
.gitmodule
をもとにして,.git/config
に[submodule ...]
のように記載ができる.初期化としてこれが必要で,これがないとgit submodule update
してもファイルの中身が更新されない.
初めに$ git clone [remote-url] --recursive
を忘れた場合には$ git submodule init && git submodule update
をしなくてはいけない.これは$ git submodule update --init
と短縮される.
$ git submodule update --init
サブモジュールのファイルをそもそもフェッチしていないときには--init
オプションが必要.
$ git submodule update --remote ([submodule-directory])
サブモジュールのfetch/pullをするときには,普通はサブモジュール内に入って普通に$ git fetch
をする必要がある.そうではなくて,サブモジュールの外側にいる状態でpullしたい場合はこのコマンドを使用する.[submodule-directory]を指定しないと,.gitmodule
に記載されているサブモジュール全てをpullする.その際のデフォルトのブランチはmasterを追跡する.
デフォルトで追跡したいブランチを変更する場合は,以下のコマンドを打つことで変更することができる.この中身はもちろん.gitmodule
に記載される.
$ git config -f .gitmodules submodule.[submodule-directory].branch [branch-name]
また,このコマンドだけを実行すると,サブモジュール内は通常「切り離された HEAD」状態になる.そうではなくて,中でブランチを切っていてそこに変更をマージしたい場合は--rebase
,--merge
というオプションをつける必要がある.
$ git push --recurse-submodules=check
大元のリポジトリでpushする際に,サブモジュールの変更がpushされているかをチェックするコマンド.ちゃんとpushされていないと失敗する.
$ git push --recurse-submodules=on-demand
とすると,サブモジュールを全てpushしてから大元のリポジトリをpushする動作を自動で行ってくれる.
参考記事
Tags
$ git tag ([commit-hash]) [tag-name]
特定のcommitにtagを付ける.[commit-hash]を省略すると,HEADの位置に付けられる.
$ git tag -a [tag-name] ([commit-hash]) -m "message"
注釈付きtagをつける.
$ git tag
tag一覧の表示.
$ git ls-remote --tags
リモートのtag一覧の表示.
$ git push [remote-name] [tag-name]
単にcommitをpushしただけではリモートにtagは反映されないので,個別にこのコマンドを打つ必要がある.(tagのpushは全く独立だという認識を持っておいた方がいい.tagを付ける前後で差分もない.) 全てのtagをまとめてリモートに反映させるときは$ git push [remote-name] --tags
$ git tag -d [tag-name]
ローカルでtagの削除.
$ git push origin --delete [tag-name]
リモートでtagの削除.(ローカルでtagを削除した後に$ git push [remote-name] [tag-name]
としても反映されない.反映させるという意味では,$ git push [remote-name] :[tag-name]
とすれば,反映される.)
補足
branchについて
branchは,commitによって形成された以下のようなツリーの,各commitに紐付けられた 名前 ポインタだと考える.下の画像のC0,C1,$\dots$にはそれぞれ,'b8afr45'のようなハッシュ値が紐付けられている(それらは$ git log --oneline --decorate --graph
を行えば確認できる).それらに特定の名前がついたものが,branchであるという認識をしておいた方がいい.
つまり例えば,$ git checkout [branch-name]
とすることと,$ git checkout そのbranchが指しているノードのハッシュ値
とすることは同様の操作をしている.
また,HEADとは「今いる場所」を示し,HEADがC7にいる場合,C3はHEAD^(またはHEAD~, HEAD~1),C1はHEAD^^(またはHEAD~2)と指定することができる.(HEADがC4にいる場合,HEAD^はC3,HEAD^^はC1を指す.)
ちなみに,commitにtagをつけるものには$ git tag [tag-name]
というコマンドもある.(詳しくはこの記事を参照.)こうすると,上の操作は$ git checkout [tag-name]
とも書ける.
resetとcheckoutの違いについて
まず,commitによるツリーの動きはこの記事が非常に分かりやすく,ファイルを変更することでワーキングツリーが動き,stagingすることでインデックス(ステージ)がそれに追従し,commitをすることで,HEADが追従して,ここで初めてハッシュ値が生成されて,commitツリーに新たなノードが加わるというイメージを持つといい.
resetコマンドは,--soft, --mixed(デフォルトはこれ), --hard というオプションに基づいてHEADの位置を特定のcommitノードまで移動させる操作であるが,そのときHEADが指し示していたbranchも一緒に移動する.(例えば,HEADがmasterブランチを指している状態(上図のC7にHEADがある状態)で$ git reset --soft C0のハッシュ値
とすると,HEADもmasterもC3に移動する) このときC7の情報はgitのデータベースには残っているため$ git reflog
3としてハッシュ値を取得すれば$ git reset --hard ハッシュ値
として戻ることができるが(もしくは,$ git reset --hard ORIG_HEAD
とする),
これに対してcheckoutコマンドは,HEADの位置のみを変更する.(先の例だと,HEADのみがC3に移動するが,masterブランチはC7に残ったままになる.)
このような違いがあるので,単に過去の履歴を参照したかったりbranchの切り替えを行うにはcheckoutを用いるのが得策であり,resetは直前のcommitやaddの操作を取り消したいときなど最小限に留めておくのが原則である.(そもそもgit管理の原則はこまめにcommitを行うことで履歴を残すことにあり,ツリーを綺麗にしておくことではない.)
参考記事
その他
Gitの設定
gitの設定(ユーザネームやメールアドレス)についてはgit config
コマンドで確認・変更する.
その際,--global
オプションをつけることでデフォルトでの全リポジトリ,--local
オプションをつけることで当該リポジトリを指定することができる.詳しくはこの記事を参考にする.
ignore
以下のサイトを参考にして,
- プロジェクト単位でignoreしたい
- ローカルでignoreしたい
- これ以上の変更は追跡したくない
を使い分ける.
git の監視から逃れる方法
Gitがよく解る記事
- The Git Version-Control System (UC Berkeley CS61B 2020 Spring)
- サルでもわかるGit入門 〜バージョン管理を使いこなそう〜
- 学生のための卑近な git・GitHub 入門
- こわくない Git
- GitとGitHubを利用した共同開発の練習(コンフリクト解消まで)の備忘録①
-
ワーキングツリーにはファイルを残したいがgit管理下にはおきたくない場合は,.gitigonoreにファイル名を記述する.(変更した.gitignoreをcommitすることを忘れない.) ↩
-
$ git push -u [remote-name] [remote-branch]
は,[remote-name]にある[remote-branch]というbranchに,ローカルレポジトリのHEADの内容を反映させるコマンドである.だから,$ git push origin master
としてしまうと,originにあるmasterに,プルリク無しでmergeされてしまうのである.$ git push -u origin develop
とするのは,originのdevelopブランチにmergeするだけであって(無ければ自動的に作られる),それをoriginにあるmasterにmergeするようにお願いするのが,プルリクである.そのため,$ git push -u origin develop
の'develop'は自分のdevelopブランチを指しているわけではないので,pushをする前にはcheckoutをしてpushしたいcommitの位置にHEADを持ってきておく必要がある. ↩ -
このサイトにあるように,
$ git log
はHEADが指すcommitとその祖先を出力するが,$ git reflog
はHEADが辿ってきた順序をリストとして出力する.そのため,resetコマンドで一つ前のcommitノードに戻ってしまった場合は$ git log
ではその前のcommitが確認できないが,$ git reflog
では確認することができる.($ git reflog
に--graphオプションがないのも,$ git reflog
は単にHEAD履歴の順序リストであるからである.) ↩