Git 只關心檔案的內容,所以只要是檔案都可以使用 git 來管理。
[git](https://gitbook.tw/#git--1)
[git book](https://git-scm.com/book/zh-tw/v2)
[為你自己學 Git](https://gitbook.tw/)
:::spoiler Git GUI
- [Sourcetree](https://www.sourcetreeapp.com/) (Win / MacOS)
- gitk / gitg (Linux)
- [gitkraken](https://www.gitkraken.com/) (Win / MaxOS / Linux)
:::
# 知識點
* Git 並不是做差異備份,而是建構式備份,讀取檔案的時候不需要從一個個歷史紀錄拼湊,而是直接讀取當前的 snapshot,相對於差異備份會多耗費一點空間。
* HEAD 有個縮寫 @,所以 `HEAD^` 等同於 `@^`
* ORIG_HEAD 是最近一次 `git merge/rebase/reset` 的備份
```bash
git ls-files [-s] ls-files # 列出 tracking 的檔案 (包含 SHA-1)
```
:::spoiler *Branch 只是一個 commit 的指標而已*
Branch 就只是存放在 *.git/refs/heads/* 目錄底下紀錄 SHA-1 的檔案而已,刪掉其中一個檔案相當於將這個 branch 刪掉。
:::
:::spoiler *HEAD 是紀錄當前 commit 狀態的指標*
其實就是一個檔案
```bash
cat .git/HEAD
ref: refs/heads/master
cat .git/refs/heads/[master/another branch]`
dd5c8314... # 這是 160-bit (40 個十六進位數) 的 SHA-1
echo "content" | git hash-object --stdin
```
:::
:::spoiler Git 有四種物件
* Blob 負責檔案,
* 以 SHA-1 命名和辨識,壓縮後儲存在 *.git/objects/*,目錄是 SHA-1 hashed 前 2 個字元,檔名是後 38 字元。要解壓縮內容可以用
```bash
git cat-file -t ... # 察看他是哪一種 object
git cat-file -p ... # 察看內容
```
* Tree 負責目錄結構;
* 以相同命名方式放在 *.git/objects/* 當中,
* 會指向 blob 檔案和 subtree 子目錄;
* Commit
* 以相同命名方式放在 *.git/objects/* 當中,
* 會指向一個 tree 目錄和 parent commit(s),
* 只要目錄內容有更動,就會有新的 tree object;
* Tag
* 以相同命名方式放在 *.git/objects/* 當中,
* 指向一個 commit。
```graphviz
digraph {
HEAD [style="filled,rounded" shape=rect penwidth=0]
branch [style="filled,rounded" shape=rect penwidth=0]
remote [style="filled,rounded" shape=rect penwidth=0]
tag [style="filled,rounded" shape=rect color=yellow]
commit [style="filled" shape=rect color=red]
tree [style="filled,rounded" shape=rect color=brown]
blob1 [label="blob" style="filled,rounded,dotted" shape=circle color=green]
blob2 [label="blob" style="filled,rounded,dotted" shape=circle color=green]
blob3 [label="blob" style="filled,rounded,dotted" shape=circle color=green]
HEAD -> branch -> commit
tag -> commit
remote -> commit -> tree -> blob1
commit -> commit
tree -> blob2
tree -> blob3
tree -> tree
}
```
:::
# 指令操作
## git 設定 / 初始化
```bash
git config --global
user.name babysuse
user.email babysuse2018@gmail.com
core.editor vim
alias.co/state/br checkout/status/branch
alias.ls "log --oneline --graph"
alias.ll 'log --graph --pretty=format:"%h <%an> %ar %s"'
# 參見 git help log
git init
...
git branch -m main # 更改 branch 名稱
```
* 要有了第一次的 commit 才會有 branch,預設是 *master*
## git 基本操作
*untracked => tracked => staging => committed*
```bash
git add -p|--patch ... # 可以只加入部份修改內容
git diff [--staged] [...] # 查看更改 (staged) 內容
git mv ...
git rm ...
git rm --cached ... # untracked
git commit -a|--all [...] # 相當於先 git add 再 git commit
# 跟 git checkout -b ... 一樣合併了兩個指令
git commit --amend --no-edit # 將更改合併到前一次 commit
# 另外一種方法是先 git reset HEAD~
git commit --allow-empty -m "..." # 可以再 staging area 空的情況下 commit
git remote add REMOTE REMOTE_URL
git push -u REMOTE LOCAL_BRANCH[:REMOTE_BRANCH]
--delete REMOTE_BRANCH # 移除 REMOTE_BRANCH
:REMOTE_BRANCH # 移除 REMOTE_BRANCH
```
* `origin` 只是 `REMOTE` 的慣用名而已
* `REMOTE_BRANCH` 預設和 `LOCAL_BRANCH` 同名
* 空的 commit 不能幹嘛,就是教學上會很方便
* 空的目錄不能提交,習慣上會再空的目錄下新增一個 *.keep* 或是 *.gitkeep*
*.gitignore* 只對建立之後的檔案有效,不溯及既往,對於早就存在的檔案需要用
```bash
git rm --cached ...
git clean -X
```
來追加。另外 `git add -f|--force ...` 可以無視 *.gitignore* 規則添加進 staging area。
```bash
# .gitignore
*.tmp
somedir/
```
### git reset/checkout/clean
```bash
git reset dd5c8314...|HEAD[^^|~5]
# 回到指定 commit (前前筆 / 前五筆)
# commit id 只要前綴就行
git checkout BRANCH # 切換 branch
git checkout [HEAD~2] ... # 回復檔案/目錄到指定狀態 (預設是上一次的 commit)
git clean -n # dry-run (for checking)
-f # 只刪除 untracked 的檔案,不會所有移除 unstaged 的更動
```
| | 工作目錄 | 暫存區 |
|:------------------- |:--------:|:-------:|
| `--soft` | - | - |
| `--mixed` (default) | - | discard |
| `--hard` | discard | discard |
用 `git log` 察看會看起來好像 commit 刪掉,但其實只是從 commit tree 的一處移到令外移處而已,用 `git reflog` 可以查到那些看起來不見了的 ID。
- 每次更動到 HEAD 時,reflog 都會紀錄,包括 `git rebase`
### git branch / tag / reflog
```bash
git branch # 列出所有 branch
git branch [-d/D] ... # ((強制)刪除/) 創建 branch
git checkout [-b] BRANCH # (創建並) 切換 branch
# 跟 git commit -a|--all [...] 一樣合併了兩個指令
git merge ... [--no-ff]
git rebase ...
git tag -a TAG -m "..." # 新增 tag
git tag -d/l # 移除/列出所有 tag
```
* 還沒 merge 的 branch 需要強制才能刪除,而當前的 branch 無法刪除
* **刪除 branch 只是移除了那個 commit 的指標,紀錄還在,可以用 `git reflog` 查詢;**
* **同理,rebase 會創建新的 commit,舊的也還在,可以用 `git reflog` 查到。**
* `git merge` 是以當前的 branch 為主,所以以當前的 branch 繼續下去
* 只會多一個 commit
* `git rebase` 是以當前的 branch 為底,所以以另外一個 branch 繼續下去
* 另外一個 branch 有幾個 commit 就會多幾個 commits
* `git tag` 會在 `git log` 當中秀出來
要復元 `git rebase` 最快有兩個方法
* 用 **`git reflog`** 查到先前的 commit,`git reset --hard` 回去
* 用 **`ORIG_HEAD`**
```bash
git reset ORIG_HEAD --hard
```
#### Conflict
`git merge` 遇到了 conflict 要修正完了才會完成 commit;
```bash
git merge ...
# git add ...
# git checkout --ours cute_animal.jpg
git commit ...
```
`git rebase` 遇到了 conflict 會停在造成 conflict 的那個 commit
```bash
git rebase ...
# git add ...
# git checkout --theirs cute_animal.jpg
git rebase --continue
```
如果是文字檔用 `git add` 修正;反之,用 `git checkout`。
### git log
```bash
git log
--oneline --graph
--author="...|..."
--grep="Title"
-S"content"
--since="9am" --until="12am" --after="2017-01"
git log [-p] ... # 察看檔案的 commit 歷史 (和修改內容)
git blame [-L 5,10] ... # 察看修改者
```
### git stash
用來暫存不想 commit 的更改
```bash
git stash [push]
--patch # 暫存部分更改
--all # 暫存並移除,相當於 git stash && git clean
git stash list
git stash apply [stash@{2}] # 不指定就採用最新的一筆
--index # 保留 staging 狀態 (預設復原後都是 unstaged)
git stash drop [stash@{2}]
git stash pop [stash@{2}] # 相當於 git stash apply && git stash pop
```
還可以將暫存記錄在 index 當中,也就是可以用 `git status -s` 察看
```bash
git stash --keep-index
git status -s
```
# 情境
## 復原
| | Untracked | Modified |
| ------------------ |:---------:|:--------:|
| `git checkout .` | - | discard |
| `git clean -f` | discard | - |
| `git reset --hard` | discard | discard |
* 只復原單一檔案的話就
```bash
git checkout [HEAD^] -- file
```
## 修改 commit 紀錄
* `git commit --amend`:只能修改最後一次
* `git rebase`
* `git reset`
## 查看兩次 commits 間更動的檔案
```bash
git diff --name-only HEAD^ HEAD~3
```
# [git hooks](https://git-scm.com/book/zh-tw/v2/Customizing-Git-Git-Hooks)
就是會針對特定行為執行特定 script 的機制。這些 scripts 就是存放在 .git/hooks 中副檔名為 .sample 且可執行的 shell scripts (當以 `git init` 初始化 git repo 的時候就會有)
## server-side
- 由 receiving push commits 觸發,push 階段所執行的 hooks,回傳非零即拒絕 push
- #### pre-receive
- 當接收到 push 的時候所執行
- #### update
- 每個 branch 要更新時都會執行,當 client 同時 push 到 n 個 branch,pre-receive 只會執行一次,update 會執行 3 次
- #### post-receive
- push 完了之後執行的
## client-side
- 透過 git 一般操作觸發,像是 commit
### commit 階段所執行的 hooks
- #### pre-commit hooks
- 在輸入 commit message 之前做檢查,回傳非零即終止 commit
- code style
- trailing whitespace
- 用 `git commit --no-verify` 來略過檢查
- #### prepare-commit-msg hooks
- 用來自動生成 templated commit message (就是輸入 commit message 前就已經出現在編輯器中的那些內容)
- 需要以下參數
- 儲存當前 commit message 的檔案路徑
- commit type (normal, amended, merged, squashed)
- commit SHA-1 如果是 amended
- #### commit-msg
- commit message 提交前對 project state 和 commit message 做檢查,回傳非零即終止 commit
- 需要以下參數
- 儲存當前 commit message 的檔案路徑
- #### post-commit
- 完成 commit 之後所執行的 hooks
{"metaMigratedAt":"2023-06-15T05:09:06.935Z","metaMigratedFrom":"YAML","title":"Git & 測試工具","breaks":true,"description":"Git 只關心檔案的內容,所以只要是檔案都可以使用 git 來管理。","contributors":"[{\"id\":\"bd34bb29-6393-4de1-a879-0655a63bb8b4\",\"add\":16150,\"del\":8184}]"}