此文章是整理常用需要的場景與對應的 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 保持同步,則流程不同,以下內容會再說明 ![image](https://hackmd.io/_uploads/SkjvXTb9lg.png) 第二章圖此圖說明操作的常見流程 Git 四個主要區域 Workspace:工作區,實際編輯檔案的地方。 Staging (Index):暫存區,用來記錄即將建立 commit 的檔案。 Local repo:本機端的版本庫,儲存 commit 歷史。 Remote repo:遠端版本庫(例如 GitHub)。 ![Pasted image 20240307172804](https://hackmd.io/_uploads/rylry6W9ee.png) # 常用 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 ``` ![Pasted image 20240706193818](https://hackmd.io/_uploads/rkRHn2-cxx.png) ![Pasted image 20240706193915](https://hackmd.io/_uploads/ry5v33Z9eg.png) ## 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 ``` -->