此文章是整理常用需要的場景與對應的 git 操作,成為簡易的 git 教學與我個人的 git cheat sheet
## git 基本概念
(此為 Github 使用概念)
通常我們會先將 upstream repo fork 成自己的 remote repo (此部分通常就是按按鈕),再透過 git clone 將自己的 repo 複製到本機端
若要對 upstream repo 發送 Pull Request (PR),流程是:
先將修改推送到自己的 remote repo,然後再由 remote repo 建立 PR,請 upstream repo 的維護者審核與合併
但若沒有要發 PR,而只是希望 local repo 與 upstream repo 保持同步,則流程不同,以下內容會再說明

第二章圖此圖說明操作的常見流程
Git 四個主要區域
Workspace:工作區,實際編輯檔案的地方。
Staging (Index):暫存區,用來記錄即將建立 commit 的檔案。
Local repo:本機端的版本庫,儲存 commit 歷史。
Remote repo:遠端版本庫(例如 GitHub)。

# 常用 git 指令
## 初始設定
```
$ git config --global user.name "yourName"
$ git config --global user.email "yourEmail"
```
## 常用操作流程
`git clone`+ url -> 從遠端clone
常見工作流程
1. `git pull` -> 跟遠端做更新
1-1 接著對程式碼修修改改~
2. `git add *` -> 將所有更新的檔案加入索引
3. `git status`->看狀態
4. `git commit` -> 建立新commit 並寫 commit message 去描述該次改動
5. `git status`->看狀態
7. `git log --oneline` -> 看 log 時每個 commit 只出現單行資訊
6. `git push` -> 推上repository //沒有參數就是推上 main
## 一些我常用 command
`git remote -v `看有多少 remote repository
`git stash` 暫存尚未 commit 的變更,之後還可以再還原
`git rebase -i <起始 Commit>` 進入互動模式,可以修改 commit ..,以下還有更詳細討論
`git diff --staged` 在git add 後看修改差異
`git diff abcdef~1 abcdef` 看某 commit 的差異 `[舊commit 新 commit]`
`git reset --hard HEAD~1` 暴力刪除最新的一個 commit :P
`git commit --amend` ,尚未推到遠端,這指令能改最新的commit 的 msg (如果改了 code diff, 需先 `git add`)
- `git commit -s -v`
- 在 commit message 的最後自動加上一行 Signed-off-by (此部分是填上 git config 寫的 name 與 email)
- 在 commit message 編輯畫面中,除了顯示空白訊息區,還會把這次 commit 的 diff 一併附在下面。
`git reset HEAD^` 拆掉最新的 commit
`git log --name-only` 來列出每個 commit 修改的檔案列表
`git log --grep="keyword"` 尋找包含 keyword 的 commit message
> 只要存於 remote repo 的 commit, 我在 local repo 修改了,就都要 `git push --force`
```shell
$ git rebase dog # On branch cat
```
結果是: 將 branchcat中不屬於 branch dog 的 commits 逐一接到 branch dog 上
## 其他 command
- `git format-patch -1 HEAD -o patches` 生成最新一筆 commit 的 patch,並存於一個名為 patches 的資料夾
- `git apply` 將 patch 加進 repo 中
- `git blame fileName` 這會逐行顯示文件的修改歷史紀錄,每行是由誰修改的。
- `git rebase` = 做好幾次的 `cherry pick` (比較兩branch,拉出來做合併)
- `git describe --tage commitID` 看該 commit 是屬於 linux kernel 中第幾版
舉例:
```
~/linux$ git describe --tags aaf162d4a5dfd
v5.10-rc1-20-gaaf162d4a5df
```
## 發 patch 注意
**PR 是 認 branch!**
所以我在 github 上按下 PR 的按鈕後,想將新修改(無論是測試或是確認版) 推上我自己的 repo,upstream repo 也會一併更動!
## 發 patch 小技巧
`git shortlog --author="Yu-Chun Lin" v6.5..linux-next/master` 看我有幾個 patch 被收進 linux 了
## 與 upstream 同步
(如果還沒加) `git remote add upstream <original-repository-URL>`
1. `git fetch upstream`
2. `git merge upstream/master`(會fast-forward)
3. 再修改
4. 推上我的 remote repo: `git push origin <your-branch-name>`
## branch 與 `git checkout`
branch 概念是來自於在不同功能或修正上有獨立的開發線
[src](https://ithelp.ithome.com.tw/articles/10221768)
`git checkout` 有兩個功能,
1. 是 **branch, HASH 間**的切換
2. 工作區檔案的**還原**
- 還原全部檔案,使用 `git checkout .` (會刪除所有尚未 add 的變更)
### 建立並切換 branch
```
git checkout -b newBranch
```
-b 代表「建立新分支並切換過去」,新分支內容會與當前 branch 一致。
之後要切換分支,只需:
```
git checkout branchName
```
### delete {remote, local} branch
```
$ git push -d <remote_name> <branchname> # Delete remote
$ git branch -d <branchname> # Delete local
```
### rename {remote, local} branch
#### 在本地端改分支名字的步驟
**第一種方法:先切到要改的分支**
`git checkout <old branch name>` 切到要改的分支
`git branch -m <new branch name>` 改成想要的新分支名字
**第二種方法:不切到要改的分支直接做**
`git branch -m <old branch name> <new branch name>`
#### 改遠端分支名字的步驟
**先把新分支名字推到遠端再把舊分支名字刪掉**
`git push origin -u <new branch name>` 把本地端新改好的分支名字推到遠端
有人習慣寫 `git push — -set-upstream origin <new branch name>` 不過基本等同上一行
`git push origin --delete <old branch name>`再把舊的遠端名字刪掉
## git cherry-pick
### 情況: on branch A, replace latest commit of branch A with latest commit of branch
```shell
# remove the lastest commit on branch A
git reset --hard HEAD
# copy the commit (on branch B)
git cherry-pick <commit hash>
```
如以下這樣做,會發生 conflict
```shell
$ git checkout A
# 因為 branch B 會指向最新 commit 所以我可以寫成以下
$ git cherry-pick B
```
```
Auto-merging lkmpg.tex
CONFLICT (content): Merge conflict in lkmpg.tex
error: could not apply 1f0c2a3... Update documentation on bottom-half handling
hint: After resolving the conflicts, mark them with
hint: "git add/rm <pathspec>", then run
hint: "git cherry-pick --continue".
hint: You can instead skip this commit with "git cherry-pick --skip".
hint: To abort and get back to the state before "git cherry-pick",
hint: run "git cherry-pick --abort".
```
### 情況: 不想要撿來的 commit 直接合併
[src](https://gitbook.tw/chapters/faq/cherry-pick)
上 `--no-commit` 參數,撿過來的 Commit 不會直接合併,而是會先放在暫存區
```bash
$ git cherry-pick 6a498ec --no-commit
$ git status
On branch cat
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: dolphin.html
````
## rebase 使用
### 狀況1: 我 patch 發一半還沒真正被合進去,但 upstream 有新的變化
> 我的 branch 需要做 reabse
在master branch: `git fetch upstream` + `git merge upstream/master`
再切到我的 branch `git rebase master`
### 狀況2: 想要修改歷史中的 commit 紀錄 (此先假設是 某commit 的 code diff)
[src](https://www.cnblogs.com/oloroso/p/9723783.html)
```
A --- B ---- C ---- D ---- E
| \
有問題 \-----發布
```
**將 HEAD 通过rebase回退到有问题的位置前一個**
```
$ git rebase commit_C^ --interactive
# --interactive 可以縮寫成 -i
```
會有以下提示
You can amend the commit now
`git commit --amend`
Once you are satisfied with your changes, run
`git rebase --continue`
** 將有問題的 commit 前的`pick` 改为`edit`,然后保存退出**
在此階段也可以
- 只修改 commit msg (reword)
- 或是將多個 commit 變成一個 (squash ,以下有多解釋)
- 或是更動 commit 位置 (直接把那行 commit 剪下,再插到你預期位置) (此畫面的 commit 順序: 最上方是最舊 commit,順序越下面越新)
```
p, pick = 使用該 commit(預設)
r, reword = 使用該 commit,但修改 commit message
e, edit = 使用該 commit,並暫停讓你修改
s, squash = 與前一個 commit 合併,保留 log 訊息
f, fixup = 與前一個 commit 合併,丟棄 log 訊息
x, exec = 執行後面接的 shell 指令
d, drop = 移除該 commit
```
** 接著修改有問題的文件,解决後重新 commit 。注意 commit 使用的参数是`--amend`** 因為 rebase 停止以前會先停到你前一步表示耀庭的部分
```shell
$ vim A.cpp
$ git add A.cpp
$ git commit --amend
```
**6、使用`git rebase --continue`逐步前進到最新的 commit 位置。**
`$ git rebase --continue`
**˙7. 上面执行后因为有兩處都有修改,需要解决衝突**
重複步驟 step 5 與 6~
### 當我要把多個 commit 合併成一個
假設我要 squash 的 commit 數為 3
```shell
$ git rebase -i HEAD~3
```
這命令會打開編輯器,並顯示最近三個的 commit
```
pick abc1234 Commit message 1
pick def5678 Commit message 2
pick ghi9012 Commit message 3
```
除第一個 commit 以外的pick改為squash
```
pick abc1234 Commit message 1
squash def5678 Commit message 2
squash ghi9012 Commit message 3
```
### 當我後悔某個 git 行為
`git reflog`: 查看 rebase 動作的SHA
接著做 `git revert`
## 情況: 想把某 feature branch 的所有/部分 commit 都合進 master branch
要将一个分支的 commit 推送到 `master` 分支,有常见的方法
### 1. 使用 `git merge`
这是最常见的方法之一。首先切换到 `master` 分支,然后合并你需要的分支。
bash
```shell
# 切换到 master 分支
$ git checkout master
# 更新 master 分支
$ git pull origin master
# 合并你的分支(假设你的分支名为 feature-branch)
$ git merge feature-branch
# 推送到远程仓库
$ git push origin master
```
### 2. 使用 `git rebase`
如果你想要保持 commit 歷史的線性,可以使用 `rebase`。
首先切换到你的分支,然后将其重新基于最新的 `master`,
(其實就是連續做 cherry-pick 的動作,如果要避免 conflict ,那就一個個cherry pick)
```shell
# 切换到 feature-branch
$ git checkout feature-branch
# 基于最新的 master 进行 rebase
$ git fetch origin
$ git rebase origin/master
# 最后切换回 `master` 做合併。
$ git checkout master
# 合併你的分支
$ git merge feature-branch
# 推送到 remote repo
$ git push origin master
```
Q: 為甚麼是先切到 feature branch?
A:
想要把 `bugFix` 上與 master 不同的 commit 接到 `master` 的後面。
git 會**根據 `bugFix` 一路往上去找與 `master` 的交會點**,接著就將交會點之後的 patch 一個一個接到 `master` 的後面
```
git rebase <new base> <branch name>
```
`bugFix` 分支接到 `master` 的後面,也可以簡寫成以下
### 3. `git cherry-pick <commit 1> <commit 2>`
[教學來源](https://zlargon.gitbooks.io/git-tutorial/content/branch/rebase.html)
挑 我需要的 commit 插進來~
```
$ git checkout master
$ git cherry-pick 60cd8ab 1988bca d83549a
```
```
# b1 = 60cd8ab
# b2 = 1988bca
# b3 = d83549a
```


## git bisect: 用二分搜找第一個發生錯誤的 commit
搭配 `git log --oneline`
就開始以下步驟
1. `git bisect start`
2. 開始標示
- `git bisect good <hash id>`
- `git bisect bad` 代表當前的程式碼是 bad
- `git bisect good`
3. `git bisect reset` 停止
舉例:
```
git bisect start
git bisect bad
git bisect good <id>
// 自動跳去...,接著自己標註此程式 good or bad
gid bisecet good
// ... 最後
git bisect reset
```
<!-- ## 小邱發 github PR 作法
```shell
git remote add upstream https://github.com/sysprog21/lab0-c.git # 要與upstream同步
git remote -v
git fetch upstream
git checkout upstream/master
git log --oneline
git checkout -b enforce-newline # `-b`自動新增切換分支 git log --oneline
bash newline.sh # 他把腳本貼到newline.sh中
git add ./*.[ch] # regualar expression 表示.c .h結尾的都加入
git status
git commit
git branch
git push --set-upstream origin enforce-newline
```
-->