學習git分支
基礎篇
git commit
Git 倉庫中的提交記錄保存的是你的目錄下所有文件的快照,就像是把整個目錄復制,然后再粘貼一樣,但比復制粘貼優雅許多!Git 希望提交記錄盡可能地輕量,因此在你每次進行提交時,它并不會盲目地復制整個目錄。條件允許的情況下,它會將當前版本與倉庫中的上一個版本進行對比,并把所有的差異打包到一起作為一個提交記錄。Git 還保存了提交的歷史記錄。
git branch和git checkout
git branch <分支名>
創建分支
git checkout <分支名>
切換分支
git checkout -b <your-branch-name>
創建一個新的分支同時切換到新創建的分支的話注意:在 Git 2.23 版本中,引入了一個名為 git switch 的新命令,最終會取代 git checkout,因為 checkout 作為單個命令有點超載(它承載了很多獨立的功能)。
git merge
git merge <分支名>
合并<分支名>到當前分支在 Git 中合并兩個分支時會產生一個特殊的提交記錄,它有兩個 parent 節點。翻譯成自然語言相當于:“我要把這兩個 parent 節點本身及它們所有的祖先都包含進來。
git rebase
git rebase <分支名>
合并<分支名>到當前分支Rebase 實際上就是取出一系列的提交記錄,“復制”它們,然后在另外一個地方逐個的放下去。
高級篇
分離HEAD
我們首先看一下 “HEAD”。 HEAD 是一個對當前所在分支的符號引用 —— 也就是指向你正在其基礎上進行工作的提交記錄。HEAD 總是指向當前分支上最近一次提交記錄。大多數修改提交樹的 Git 命令都是從改變 HEAD 的指向開始的。HEAD 通常情況下是指向分支名的(如 bugFix)。在你提交時,改變了 bugFix 的狀態,這一變化通過 HEAD 變得可見。(譯者注:實際這些命令并不是真的在查看 HEAD 指向,看下一屏就了解了。如果想看 HEAD 指向,可以通過 cat .git/HEAD 查看, 如果 HEAD 指向的是一個引用,還可以用 git symbolic-ref HEAD 查看它的指向。但是該程序不支持這兩個命令)
相對引用(^)
git checkout HEAD^
main^ 相當于“main 的 parent 節點”
main^^ 是 main 的第二個 parent 節點git log
來查看提交記錄的哈希值
git checkout <哈希值>首先看看操作符 (^)。把這個符號加在引用名稱的后面,表示讓 Git 尋找指定提交記錄的 parent 提交。通過指定提交記錄哈希值的方式在 Git 中移動不太方便。在實際應用時,并沒有像本程序中這么漂亮的可視化提交樹供你參考,所以你就不得不用 git log 來查查看提交記錄的哈希值。并且哈希值在真實的 Git 世界中也會更長(譯者注:基于 SHA-1,共 40 位)。例如前一關的介紹中的提交記錄的哈希值可能是 fed2da64c0efc5293610bdd892f82a58e8cbc5d8。舌頭都快打結了吧...比較令人欣慰的是,Git 對哈希的處理很智能。你只需要提供能夠唯一標識提交記錄的前幾個字符即可。因此我可以僅輸入fed2 而不是上面的一長串字符。
相對引用2(~)
git checkout HEAD~4如果你想在提交樹中向上移動很多步的話,敲那么多 ^ 貌似也挺煩人的,Git 當然也考慮到了這一點,于是又引入了操作符 ~。該操作符后面可以跟一個數字(可選,不跟數字時與 ^ 相同,向上移動一次),指定向上移動多少次。
撤銷變更
git reset HEAD^
回退到上一個節點(遠程分支是無效)
git revert HEAD
回退到上一個節點(遠程分支是有效)git reset 通過把分支記錄回退幾個提交記錄來實現撤銷改動。你可以將這想象成“改寫歷史”。git reset 向上移動分支,原來指向的提交記錄就跟從來沒有提交過一樣。:
雖然在你的本地分支中使用 git reset 很方便,但是這種“改寫歷史”的方法對大家一起使用的遠程分支是無效的哦!為了撤銷更改并分享給別人,我們需要使用 git revert
移動提交記錄
Git Cherry-pick
git cherry-pick <提交號>...
Git將被提交記錄按順序抓過來放到當前分支下了如果你想將一些提交復制到當前所在的位置(HEAD)下面的話, Cherry-pick 是最直接的方式了。我個人非常喜歡 cherry-pick,因為它特別簡單。
交互式 rebase
git rebase -i HEAD~4當你知道你所需要的提交記錄(并且還知道這些提交記錄的哈希值)時, 用 cherry-pick 再好不過了 —— 沒有比這更簡單的方式了。但是如果你不清楚你想要的提交記錄的哈希值呢? 幸好 Git 幫你想到了這一點, 我們可以利用交互式的 rebase —— 如果你想從一系列的提交記錄中找到想要的記錄, 這就是最好的方法了咱們具體來看一下……
交互式 rebase 指的是使用帶參數 --interactive 的 rebase 命令, 簡寫為 -i如果你在命令后增加了這個選項, Git 會打開一個 UI 界面并列出將要被復制到目標分支的備選提交記錄,它還會顯示每個提交記錄的哈希值和提交說明,提交說明有助于你理解這個提交進行了哪些更改。在實際使用時,所謂的 UI 窗口一般會在文本編輯器 —— 如 Vim —— 中打開一個文件。
雜項
只取一個提交記錄
來看一個在開發中經常會遇到的情況:我正在解決某個特別棘手的 Bug,為了便于調試而在代碼中添加了一些調試命令并向控制臺打印了一些信息。這些調試和打印語句都在它們各自的提交記錄里。最后我終于找到了造成這個 Bug 的根本原因,解決掉以后覺得沾沾自喜!最后就差把 bugFix 分支里的工作合并回 main 分支了。你可以選擇通過 fast-forward 快速合并到 main 分支上,但這樣的話 main 分支就會包含我這些調試語句了。你肯定不想這樣,應該還有更好的方式……實際我們只要讓 Git 復制解決問題的那一個提交記錄就可以了。跟之前我們在“整理提交記錄”中學到的一樣,我們可以使用git rebase -i
git cherry-pick來達到目的。
提交的技巧 #1
接下來這種情況也是很常見的:你之前在 newImage 分支上進行了一次提交,然后又基于它創建了 caption 分支,然后又提交了一次。此時你想對某個以前的提交記錄進行一些小小的調整。比如設計師想修改一下 newImage 中圖片的分辨率,盡管那個提交記錄并不是最新的了。接下來這種情況也是很常見的:你之前在 newImage 分支上進行了一次提交,然后又基于它創建了 caption 分支,然后又提交了一次。此時你想對某個以前的提交記錄進行一些小小的調整。比如設計師想修改一下 newImage 中圖片的分辨率,盡管那個提交記錄并不是最新的了。我們可以通過下面的方法來克服困難:先用 git rebase -i 將提交重新排序,然后把我們想要修改的提交記錄挪到最前
然后用 git commit --amend 來進行一些小修改
接著再用 git rebase -i 來將他們調回原來的順序
最后我們把 main 移到修改的最前端(用你自己喜歡的方法),就大功告成啦!
提交的技巧 #2
我們可以使用 rebase -i 對提交記錄進行重新排序。只要把我們想要的提交記錄挪到最前端,我們就可以很輕松的用 --amend 修改它,然后把它們重新排成我們想要的順序。但這樣做就唯一的問題就是要進行兩次排序,而這有可能造成由 rebase 而導致的沖突。git cherry-pick是另一種方法
Git Tag
git tag v1 C1
我們將這個標簽命名為 v1,并且明確地讓它指向提交記錄 C1
git tag v1
我們將這個標簽命名為 v1,并且明確地讓它HEAD相信通過前面課程的學習你已經發現了:分支很容易被人為移動,并且當有新的提交時,它也會移動。分支很容易被改變,大部分分支還只是臨時的,并且還一直在變。你可能會問了:有沒有什么可以永遠指向某個提交記錄的標識呢,比如軟件發布新的大版本,或者是修正一些重要的 Bug 或是增加了某些新特性,有沒有比分支更好的可以永遠指向這些提交的方法呢?當然有了!Git 的 tag 就是干這個用的啊,它們可以(在某種程度上 —— 因為標簽可以被刪除后重新在另外一個位置創建同名的標簽)永久地將某個特定的提交命名為里程碑,然后就可以像分支一樣引用了。更難得的是,它們并不會隨著新的提交而移動。你也不能切換到某個標簽上面進行修改提交,它就像是提交樹上的一個錨點,標識了某個特定的位置。咱們來看看標簽到底是什么樣。
咱們先建立一個標簽,指向提交記錄 C1,表示這是我們 1.0 版本。
git tag v1 C1
很容易吧!我們將這個標簽命名為 v1,并且明確地讓它指向提交記錄 C1,如果你不指定提交記錄,Git 會用 HEAD 所指向的位置。
Git Describe
Git Describe
由于標簽在代碼庫中起著“錨點”的作用,Git 還為此專門設計了一個命令用來描述離你最近的錨點(也就是標簽),它就是 git describe!Git Describe 能幫你在提交歷史中移動了多次以后找到方向;當你用 git bisect(一個查找產生 Bug 的提交記錄的指令)找到某個提交記錄時,或者是當你坐在你那剛剛度假回來的同事的電腦前時, 可能會用到這個命令。
git describe 的語法是:git describe <ref><ref> 可以是任何能被 Git 識別成提交記錄的引用,如果你沒有指定的話,Git 會使用你目前所在的位置(HEAD)。它輸出的結果是這樣的:<tag>_<numCommits>_g<hash>tag 表示的是離 ref 最近的標簽, numCommits 是表示這個 ref 與 tag 相差有多少個提交記錄, hash 表示的是你所給定的 ref 所表示的提交記錄哈希值的前幾位。當 ref 提交記錄上有某個標簽時,則只輸出標簽名稱
高級話題
兩個 parent 節點
git checkout main^2
選擇另一個 parent 提交……選擇 parent 提交記錄
操作符 ^ 與 ~ 符一樣,后面也可以跟一個數字。但是該操作符后面的數字與 ~ 后面的不同,并不是用來指定向上返回幾代,而是指定合并提交記錄的某個 parent 提交。還記得前面提到過的一個合并提交有兩個 parent 提交吧,所以遇到這樣的節點時該選擇哪條路徑就不是很清晰了。Git 默認選擇合并提交的“第一個” parent 提交,在操作符 ^ 后跟一個數字可以改變這一默認行為。廢話不多說,舉個例子。Git示范
這里有一個合并提交記錄。如果不加數字修改符直接切換到 main^,會回到第一個 parent 提交記錄。(在我們的圖示中,第一個 parent 提交記錄是指合并提交記錄正上方的那個提交記錄。)
Push & Pull —— Git 遠程倉庫!
Git Clone
在本地創建一個遠程倉庫的拷貝(比如從 github.com)
遠程分支
既然你已經看過 git clone 命令了,咱們深入地看一下發生了什么。你可能注意到的第一個事就是在我們的本地倉庫多了一個名為 o/main 的分支, 這種類型的分支就叫遠程分支。由于遠程分支的特性導致其擁有一些特殊屬性。遠程分支反映了遠程倉庫(在你上次和它通信時)的狀態。這會有助于你理解本地的工作與公共工作的差別 —— 這是你與別人分享工作成果前至關重要的一步.遠程分支有一個特別的屬性,在你切換到遠程分支時,自動進入分離 HEAD 狀態。Git 這么做是出于不能直接在這些分支上進行操作的原因, 你必須在別的地方完成你的工作, (更新了遠程分支之后)再用遠程分享你的工作成果。為什么有 o/?
你可能想問這些遠程分支的前面的 o/ 是什么意思呢?好吧, 遠程分支有一個命名規范 —— 它們的格式是:<remote name>/<branch name>
因此,如果你看到一個名為 o/main 的分支,那么這個分支就叫 main,遠程倉庫的名稱就是 o。大多數的開發人員會將它們主要的遠程倉庫命名為 origin,并不是 o。這是因為當你用 git clone 某個倉庫時,Git 已經幫你把遠程倉庫的名稱設置為 origin 了不過 origin 對于我們的 UI 來說太長了,因此不得不使用簡寫 o :) 但是要記住, 當你使用真正的 Git 時, 你的遠程倉庫默認為 origin!說了這么多,讓我們看看實例。
Git Fetch
Git Fetch
Git 遠程倉庫相當的操作實際可以歸納為兩點:向遠程倉庫傳輸數據以及從遠程倉庫獲取數據。既然我們能與遠程倉庫同步,那么就可以分享任何能被 Git 管理的更新(因此可以分享代碼、文件、想法、情書等等)。本節課我們將學習如何從遠程倉庫獲取數據 —— 命令如其名,它就是 git fetch。你會看到當我們從遠程倉庫獲取數據時, 遠程分支也會更新以反映最新的遠程倉庫。在上一節課程中我們已經提及過這一點了。git fetch 做了些什么
git fetch 完成了僅有的但是很重要的兩步:從遠程倉庫下載本地倉庫中缺失的提交記錄
更新遠程分支指針(如 o/main)
git fetch 實際上將本地倉庫中的遠程分支更新成了遠程倉庫相應分支最新的狀態。如果你還記得上一節課程中我們說過的,遠程分支反映了遠程倉庫在你最后一次與它通信時的狀態,git fetch 就是你與遠程倉庫通信的方式了!希望我說的夠明白了,你已經了解 git fetch 與遠程分支之間的關系了吧。git fetch 通常通過互聯網(使用 http:// 或 git:// 協議) 與遠程倉庫通信。git fetch 不會做的事
git fetch 并不會改變你本地倉庫的狀態。它不會更新你的 main 分支,也不會修改你磁盤上的文件。理解這一點很重要,因為許多開發人員誤以為執行了 git fetch 以后,他們本地倉庫就與遠程倉庫同步了。它可能已經將進行這一操作所需的所有數據都下載了下來,但是并沒有修改你本地的文件。我們在后面的課程中將會講解能完成該操作的命令 :D所以, 你可以將 git fetch 的理解為單純的下載操作。
Git Pull
git pull --rebase
git pull
既然我們已經知道了如何用 git fetch 獲取遠程的數據, 現在我們學習如何將這些變化更新到我們的工作當中。其實有很多方法的 —— 當遠程分支中有新的提交時,你可以像合并本地分支那樣來合并遠程分支。也就是說就是你可以執行以下命令:git cherry-pick o/main
git rebase o/main
git merge o/main
等等
實際上,由于先抓取更新再合并到本地分支這個流程很常用,因此 Git 提供了一個專門的命令來完成這兩個操作。它就是我們要講的 git pull。
同樣的結果!這清楚地說明了 git pull 就是 git fetch 和 git merge 的縮寫!
模擬團隊合作
Git Push
OK,我們已經學過了如何從遠程倉庫獲取更新并合并到本地的分支當中。這非常棒……但是我如何與大家分享我的成果呢?嗯,上傳自己分享內容與下載他人的分享剛好相反,那與 git pull 相反的命令是什么呢?git push!git push 負責將你的變更上傳到指定的遠程倉庫,并在遠程倉庫上合并你的新提交記錄。一旦 git push 完成, 你的朋友們就可以從這個遠程倉庫下載你分享的成果了!你可以將 git push 想象成發布你成果的命令。它有許多應用技巧,稍后我們會了解到,但是咱們還是先從基礎的開始吧……注意 —— git push 不帶任何參數時的行為與 Git 的一個名為 push.default 的配置有關。它的默認值取決于你正使用的 Git 的版本,但是在教程中我們使用的是 upstream。 這沒什么太大的影響,但是在你的項目中進行推送之前,最好檢查一下這個配置。
關于 origin 和它的周邊 —— Git 遠程倉庫高級操作
合并遠程倉庫
在開發社區里,有許多關于 merge 與 rebase 的討論。以下是關于 rebase 的優缺點:優點:Rebase 使你的提交樹變得很干凈, 所有的提交都在一條線上
缺點:Rebase 修改了提交樹的歷史
比如, 提交 C1 可以被 rebase 到 C3 之后。這看起來 C1 中的工作是在 C3 之后進行的,但實際上是在 C3 之前。一些開發人員喜歡保留提交歷史,因此更偏愛 merge。而其他人(比如我自己)可能更喜歡干凈的提交樹,于是偏愛 rebase。仁者見仁,智者見智。 :D
遠程追蹤
前幾節課程中有件事兒挺神奇的,Git 好像知道 main 與 o/main 是相關的。當然這些分支的名字是相似的,可能會讓你覺得是依此將遠程分支 main 和本地的 main 分支進行了關聯。這種關聯在以下兩種情況下可以清楚地得到展示:pull 操作時, 提交記錄會被先下載到 o/main 上,之后再合并到本地的 main 分支。隱含的合并目標由這個關聯確定的。
push 操作時, 我們把工作從 main 推到遠程倉庫中的 main 分支(同時會更新遠程分支 o/main) 。這個推送的目的地也是由這種關聯確定的!
直接了當地講,main 和 o/main 的關聯關系就是由分支的“remote tracking”屬性決定的。main 被設定為跟蹤 o/main —— 這意味著為 main 分支指定了推送的目的地以及拉取后合并的目標。你可能想知道 main 分支上這個屬性是怎么被設定的,你并沒有用任何命令指定過這個屬性呀!好吧, 當你克隆倉庫的時候, Git 就自動幫你把這個屬性設置好了。當你克隆時, Git 會為遠程倉庫中的每個分支在本地倉庫中創建一個遠程分支(比如 o/main)。然后再創建一個跟蹤遠程倉庫中活動分支的本地分支,默認情況下這個本地分支會被命名為 main。克隆完成后,你會得到一個本地分支(如果沒有這個本地分支的話,你的目錄就是“空白”的),但是可以查看遠程倉庫中所有的分支(如果你好奇心很強的話)。這樣做對于本地倉庫和遠程倉庫來說,都是最佳選擇。這也解釋了為什么會在克隆的時候會看到下面的輸出:local branch "main" set to track remote branch "o/main"
我能自己指定這個屬性嗎?
當然可以啦!你可以讓任意分支跟蹤 o/main, 然后該分支會像 main 分支一樣得到隱含的 push 目的地以及 merge 的目標。 這意味著你可以在分支 totallyNotMain 上執行 git push,將工作推送到遠程倉庫的 main 分支上。有兩種方法設置這個屬性,第一種就是通過遠程分支切換到一個新的分支,執行:git checkout -b totallyNotMain o/main就可以創建一個名為 totallyNotMain 的分支,它跟蹤遠程分支 o/main。
Git push 的參數
很好! 既然你知道了遠程跟蹤分支,我們可以開始揭開 git push、fetch 和 pull 的神秘面紗了。我們會逐個介紹這幾個命令,它們在理念上是非常相似的。首先來看 git push。在遠程跟蹤課程中,你已經學到了 Git 是通過當前所在分支的屬性來確定遠程倉庫以及要 push 的目的地的。這是未指定參數時的行為,我們可以為 push 指定參數,語法是:git push <remote> <place><place> 參數是什么意思呢?我們稍后會深入其中的細節, 先看看例子, 這個命令是:git push origin main把這個命令翻譯過來就是:切到本地倉庫中的“main”分支,獲取所有的提交,再到遠程倉庫“origin”中找到“main”分支,將遠程倉庫中沒有的提交記錄都添加上去,搞定之后告訴我。我們通過“place”參數來告訴 Git 提交記錄來自于 main, 要推送到遠程倉庫中的 main。它實際就是要同步的兩個倉庫的位置。需要注意的是,因為我們通過指定參數告訴了 Git 所有它需要的信息, 所以它就忽略了我們所切換分支的屬性!
Git push 參數 2
還記得之前課程說的吧,當為 git push 指定 place 參數為 main 時,我們同時指定了提交記錄的來源和去向。你可能想問 —— 如果來源和去向分支的名稱不同呢?比如你想把本地的 foo 分支推送到遠程倉庫中的 bar 分支。哎,很遺憾 Git 做不到…… 開個玩笑,別當真!當然是可以的啦 :) Git 擁有超強的靈活性(有點過于靈活了)接下來咱們看看是怎么做的……
要同時為源和目的地指定 <place> 的話,只需要用冒號 : 將二者連起來就可以了:git push origin <source>:<destination>這個參數實際的值是個 refspec,“refspec” 是一個自造的詞,意思是 Git 能識別的位置(比如分支 foo 或者 HEAD~1)
Git fetch 的參數
Git fetch 的參數
我們剛學習了 git push 的參數,很酷的 <place> 參數,還有用冒號分隔的 refspecs(<source>:<destination>)。 這些參數可以用于 git fetch 嗎?你猜中了!git fetch 的參數和 git push 極其相似。他們的概念是相同的,只是方向相反罷了(因為現在你是下載,而非上傳)讓我們逐個討論下這些概念……<place> 參數
如果你像如下命令這樣為 git fetch 設置 的話:git fetch origin fooGit 會到遠程倉庫的 foo 分支上,然后獲取所有本地不存在的提交,放到本地的 o/foo 上。來看個例子(還是前面的例子,只是命令不同了)
沒有 source 的 source
古怪的 <source>
Git 有兩種關于 <source> 的用法是比較詭異的,即你可以在 git push 或 git fetch 時不指定任何 source,方法就是僅保留冒號和 destination 部分,source 部分留空。git push origin :side
git fetch origin :bugFix
我們分別來看一下這兩條命令的作用……
如果 push 空 到遠程倉庫它會刪除遠程倉庫中的分支!
如果 fetch 空 到本地,會在本地HEAD位置創建一個新分支
Git pull 的參數
既然你已經掌握關于 git fetch 和 git push 參數的方方面面了,關于 git pull 幾乎沒有什么可以講的了 :)因為 git pull 到頭來就是 fetch 后跟 merge 的縮寫。你可以理解為用同樣的參數執行 git fetch,然后再 merge 你所抓取到的提交記錄。還可以和其它更復雜的參數一起使用, 來看一些例子:以下命令在 Git 中是等效的:git pull origin foo 相當于:git fetch origin foo; git merge o/foo還有...git pull origin bar~1:bugFix 相當于:git fetch origin bar~1:bugFix; git merge bugFix看到了? git pull 實際上就是 fetch + merge 的縮寫, git pull 唯一關注的是提交最終合并到哪里(也就是為 git fetch 所提供的 destination 參數)
常用基本命令
git init
把這個目錄變成git可以管理的倉庫git clone
clone一個git倉庫git add <文件名>
git add .
git add *.html
git add *.txt
添加某個文件類型到暫存區,比如所有的 .html 文件
git add index/
添加整個文件夾到暫存區,比如index文件夾
全部添加到暫存區查看分支:git branch創建分支:git branch name切換分支:git checkout name創建+切換分支:git checkout –b name合并某分支到當前分支:git merge name刪除分支:git branch –d namegit config:配置信息
我們可以通過git config來配置用戶名和郵箱地址,便于我們將代碼提交到遠程倉庫,具體格式如下:
git config --global user.name '你的用戶名'
git config --global user.email '你的郵箱'git status:查看文件的狀態命令git diff:查看更新的詳細信息命令git commit:提交命令git reset HEAD:取消緩存命令git rm:刪除命令git mv:移動或重命名命令git log
–oneline :查看歷史記錄的簡潔版本
–graph :查看歷史中什么時候出現了分支、合并
–reverse :逆向顯示所有日志
–author :查找指定用戶的提交日志
–since、–before、 --until、–after: 指定帥選日期
–no-merges :選項以隱藏合并提交git tag -a v1.0來創建一個標簽。a 選項意為"創建一個帶注解的標簽"。 不用 -a 選項也可以執行的,但它不會記錄這標簽是啥時候打的,誰打的,也不會讓你添加個標簽的注解。git remote add可以添加一個遠程倉庫,其命令格式如下:
git remote add [alias] [url]
關于github
在了解之前,先注冊github賬號,由于你的本地Git倉庫和github倉庫之間的傳輸是通過SSH加密的,所以需要一點設置:
第一步:創建SSH Key。在用戶主目錄下,看看有沒有.ssh目錄,如果有,再看看這個目錄下有沒有id_rsa和id_rsa.pub這兩個文件,如果有的話,直接跳過此如下命令,如果沒有的話,打開命令行,輸入如下命令:ssh-keygen -t rsa –C “youremail@example.com”登錄github,打開” settings”中的SSH Keys頁面,然后點擊“Add SSH Key”,填上任意title,在Key文本框里黏貼id_rsa.pub文件的內容。git remote add origin <遠程倉庫鏈接>
將鏈接縮減為origin
git push origin master