­

手把手教你使用Git管理你的軟件代碼

什麼是分佈式版本控制系統?Git有哪些常用命令?什麼是倉庫?Git的操作區域包括哪些?Git有哪些常用對象(object)?git rebase和git merge的區別是什麼?git reset,git revert和git checkout的區別是什麼?git submodule和git subtree的區別又是什麼?git push和git push -u的區別又是什麼?.gitignore如何使用?Git跟GitHub有沒有關係?如何推送自己代碼到GitHub?怎麼在Gitee建立GitHub的鏡像?pull request跟git pull有沒有關係?本文將對以上問題進行闡述。

 

目錄

手把手教你使用Git管理你的軟件代碼

1. Git概述

1.1 大神Linus Torvalds簡介

2. 創建第一個Git倉庫(Repository)

3. Git工作原理簡介

3.1 Git操作區域

3.2 Git對象(數據結構)

3.3 Git命令

3.4 Git flow

4. Git進階示例

4.1 git merge – fast forward

4.2 git reset和git revert – 刪除某個commit

4.3 git checkout,git revert和git reset

4.4 git merge – 手動解決衝突

4.5 .gitignore – 忽略某些文件的追蹤

4.6 git push – 把本地代碼推送到遠程服務器

4.7 git rebase – 團隊協作

4.8 git merge和git rebase區別

4.9 git submodule – 引用第三方模塊

4.10 git subtree – 包含第三方模塊

4.11 git tag – 發佈軟件版本

4.12 將GitHub倉庫導入到Gitee – 解決GitHub訪問速度慢問題

4.13 pull request – 貢獻自己的代碼

5. 常用Git命令說明

5.1 git config

5.2 git init

5.3 git clone

5.4 git add

5.5 git commit

5.6 git diff

5.7 git reset

5.8 git status

5.9 git rm

5.10 git log

5.11 git show

5.12 git tag

5.13 git branch

5.14 git checkout

5.15 git merge

5.16 git rebase

5.17 git remote

5.18 git push

5.19 git fetch

5.20 git pull

5.21 git revert

5.22 git restore

5.23 git reflog

5.24 git stash

5.25 git submodule

5.26 git subtree

5.27 git cherry-pick

5.28 git grep

5.29 git apply

5.30 git cat-file

5.31 git ls-files

5.32 git merge-file

6. Git重要術語列表

 

1. Git概述

當我們開發軟件的時候,會創建很多源代碼文件,這些源代碼文件一般會放在一個目錄裏面,這個就是我們的code base,對這些源代碼文件,我們每天都在迭代開發,因此需要對其進行管理,這樣我們就能知道這些源代碼文件的歷史,比如前天改了什麼,今天又改了什麼。一個人自己寫代碼對軟件開發管理感覺還不深,如果是多個人一起開發一個項目,這個時候怎麼保證每個人的修改別人可以獲取到而又不影響到其他人的代碼,這就需要軟件版本管理工具或者軟件,Git就是這樣一款軟件。

Git是一款分佈式版本控制系統(Distributed Version Control System),它可以用來追蹤任何文件的修改,協調多個開發者之間的工作,Git具有速度快,數據完整性校驗,分佈式開發,以及支持非線性工作流等眾多特點。Git最初由Linus Torvalds創作(Linus Torvalds還創作了大名鼎鼎的Linux操作系統,我們後面會對Linus進行一個簡單介紹,以示對大神的敬意),它是一款開源的免費的GPL2.0版權的軟件。Git的官網地址是://git-scm.com/,scm表示source code  management,跟前面提及的版本控制系統一個意思。

1.1 大神Linus Torvalds簡介

前面說過Git是由Linus Torvalds創作的,我覺得有必要對Linus Torvalds進行一個簡單介紹,以讓更多的人了解這個人(不感興趣的人可以直接跳過這一節!)。Linus於1969年12月28號出生於芬蘭,他11歲開始接觸編程,沒過多久,就會直接寫機器代碼去操作8-bit CPU(iini評論:從這件事就可以看出,Linus非同尋常,我們現在很多程序員不要說寫彙編代碼,就是讀彙編代碼都是很大問題,更何況直接寫機器碼;這件事也側面告訴我們,作為一個頂級的OS開發程序員,彙編語言功底是少不了的)。Linus上中學的時候就接觸了操作系統,並嘗試對其進行改進。1988年,Linus被赫爾辛基大學錄取,然後在1996年碩士畢業,他的碩士畢業論文就是:Linux,一個可移植的操作系統(iini評論:慚愧啊,感覺我碩士畢業的時候,腦子基本上還是一團漿糊)。Linux原型機於1991年第一次發佈,這個原型機只花費了Linus幾個月的時間,並沒有大家想像的那麼複雜(iini評論:這個也側面反應出Linux kernel有可能沒有大家想像得那麼難)。Linux 1.0版本則是在1994年發佈的,中間隔了2年多時間。Linux原型機發佈之後,一開始採用的人也並不多,直到它開始支持GUI(X Window System)以及SMP,Linux才開始變得慢慢流行起來。大學畢業之後,Linus就去了美國,然後就一直在OSDL和Linux Foundation工作直到現在。一開始Linux是沒有版本控制系統的,所有的版本管理工作都是由Linus手工完成的,由於Linux變得越來越大,貢獻者也越來越多,如何自動和高效地去管理Linux的版本,是擺在Linus面前的一項課題。一開始Linus使用第三方的私有版本控制系統去管理Linux,後來出於種種原因無法繼續,於是Linus在2005年創作出了Git以管理Linux版本。創作出Git之後,Linus自己都感嘆:沒想到在自己36歲之際還能做出一點成績出來(其實有人統計過,大部分大師的高光時刻都是在40歲之前的幾年,比如愛因斯坦26歲提出狹義相對論,36歲提出廣義相對論,目前看起來Linus也是符合這個規律的)。

我覺得這裡有必要說說Linux和Git兩個名字的由來,大家由此也可以看出Linus本人以及他周圍環境的一些特質,Linux是從Unix發展過來的,在開發Linux過程中,其實Linus沒有想好用什麼名字,他一開始給這個項目取的名字是:Freax,意思是Free/Freak uniX,即自由怪異的Unix,當他把這個項目上傳到一個ftp服務器以供其他人下載的時候,FTP管理員覺得Freax這個名字不好,在沒有諮詢Linus前提下,直接把這個項目的名字改為”linux”,意思是Linus Unix,這個名字後來得到了Linus的同意,並詮釋為:Linux Is Not UniX,這個應該是向GNU致敬(GNU Not Unix)(iini評論:iini這個id其實也是在向Linux和GNU致敬哦,大家猜猜看它是什麼意思)。如果Linux這個名字取得有點烏龍的感覺,那麼Git這個名字那就是Linus蓄謀已久。Git英文本義是”飯桶,蠢貨”,Linus本意是找一個三個字母組合的單詞,而且不屬於Unix的命令關鍵字,在這個前提下,可選的單詞就不多,Git被選上也是情理之中的(iini評論:在英語中,越是重要而常見的意義,它對應的單詞越簡單,比如go,do,三個字母組成的單詞他們在英語中的地位是很重要的,建議大家去理一理這些單詞,說不定對你英語學習會有一個很大的幫助),再加上Linus認為自己是一個非常自我的人,正好可以用Git的英文原意來自嘲一下自己的產品:愚蠢的信息追蹤系統。從Linux和Git兩者的名字,可以看出Linus骨子裡的那種不走尋常路的信念,以及周圍環境對他的包容和理解。(iini評論:古今中外,能不能取一個好名字是一個好作品或者好作者的重要標準,大家看看這些耳熟能詳的名字:如意金箍棒,降龍十八掌,阿Q,Java,Python,一聽就感覺是大師之作啊)

Linus是一個自由軟件的愛好者和擁護者,一生都致力於自由軟件的創作和推廣工作。自從Android採用Linux內核後,Linux就成為世界上最流行的操作系統,也就是說Linux的裝機量已經超過了Windows和蘋果的MacOS,Windows創始人Bill Gates是世界首富,蘋果公司是全球第一市值公司,而Linus自始至終都是一個工程師,因為他的操作系統是免費的。Linus創作的第二個產品Git,在此基礎上創立的公司有GitHub/Gitlab/Bitbucket/Gitee等,拿GitHub為例,微軟收購他的時候價格為75億美元,但是GitHub跟Linus沒有任何關係,Linus還是在認認真真地做他的工程師。在中國,當我們談自主知識產權操作系統的時候,大部分都是在講Linux二次開發,像倪光南院士目前很大一部分工作就是在推廣Linux操作系統。

11歲開始編程,然後一輩子專註在編程,一生只做一件事:Linux,順帶搞出一個副產品:Git,這就是Linus。(iini評論:想想我們一生,想做的事情太多了,不過到最後,我們大部分人回顧自己一生:生於哪年,卒於哪年,妻或者夫是誰,小孩是誰,僅此而已)

以上純粹是節外生枝,下面回歸正題。

2. 創建第一個Git倉庫(Repository)

現在我們開始創建自己的第一個Git倉庫。

首先前往Git官網下載Git工具://git-scm.com/downloads,安裝成功後,可以在cmd或者其他shell(比如Bash)中輸入如下命令來驗證是否安裝成功:

git --version

註:如果Git官網下載很慢的話,可以自行去其他地方下載Git,或者使用choco安裝(choco install git)。

本文將基於Windows講解Git,所有命令交互通過CMD來輸入,但相關命令可以直接運行在Linux或者MacOS的Bash中。

 

為了讓後面的講解更直觀一些,這裡我們以一個基於Zephyr的C語言工程(hello_world)為例來說明。大家對Zephyr不熟也沒關係的,你就把它當成一個目錄下包含了幾個文件,僅此而已。這個工程代碼獲取鏈接為://github.com/zephyrproject-rtos/zephyr/tree/main/samples/hello_world,將其下載下來(大家可以一個文件一個文件複製粘貼下來),放在自己的目錄下:(註:如果GitHub訪問不了,請多試幾次。如果還是不行,請訪問//gitee.com/minhua_ai/sdk-zephyr/tree/main/samples/hello_world這個鏡像網站獲取相關代碼)

上面這個工程目前還沒有任何版本控制功能,它目前就是一個純粹的源代碼文件集,即code base,大家如果去修改src目錄下面的main.c,是沒法記錄下來歷史的,為此我們需要Git來完成這件事。

打開cmd,進入上面目錄,然後依次執行下面三條命令:

git init
git add .
git commit -m "my first Git repo"

如果你是第一次跑Git,那麼最後一條commit指令會報如下錯:

請按照上面的英文提示,添加自己的用戶名和郵箱,比如我是這麼做的(註:下面是我的郵箱和用戶名,請換成你自己的郵箱和用戶名,郵箱選擇一個你在用的即可,用戶名選擇你喜歡的即可):

git config --global user.email "aimh@qq.com"
git config --global user.name "Kevin Ai"

然後再次執行git commit -m “my first Git repo”,就可以了。

上面三條命令成功執行後,我們的Git倉庫就創建成功了。Git倉庫在物理上就是下面的.git目錄:

點進去,我們可以看到:

大家可以把這個目錄以及目錄裏面的文件都點進去看看(文件通過Notepad++就可以查看),以加深對Git倉庫的印象和了解。從這個目錄我們可以看到如下名字:HEAD,index,refs,head,tag,object,commit,pack等,這些都是Git非常重要的術語,我們後面會對其進行一一闡述。

倉庫(Repository,簡寫Repo),可以看成是一個本地數據庫,Git的所有命令都是對這個數據庫進行各種操作。比如前面的”git init”可以看作是創建這個數據庫,而”git add”可以看作是把你當前工作目錄下所有的文件添加到這個數據庫,而”git commit”就是提交本次操作,形成一個版本或者歷史記錄。有人會問,我們工作目錄下的所有文件都在Git倉庫哪裡?怎麼找不到?其實Git倉庫的實際數據都放在objects目錄下,我們工作目錄下的所有文件都轉成了BLOB對象存在objects目錄下,這個後面再細講。

在我們成功執行”git commit”後,cmd會有回顯信息,裏面會提到”master”,我們可以先在CMD中輸入如下命令:

git branch

可以看出,CMD也輸出master。”git branch”表示查看當前倉庫中包含哪些分支(branch),branch英文原意就是樹枝,”git branch”返回”master”,告訴你目前倉庫只有一個master樹枝,即主幹(trunk),這個就是Git倉庫初始化後的狀態:只有一個主分支,這個主分支默認名字是master。這裡強調一下,主分支的默認名字是可以改的,大家可以使用

git config --global init.defaultBranch main

就可以把默認分支的名字改成main了,實際上GitHub這個第三方託管平台已經這麼做了,在GitHub創建Git倉庫,默認分支名就是main;還有的程序員把這個默認分支名改為trunk,這都是可以的。

我們現在跟國際接軌,也把我們的默認分支名改為main,大家執行”git config –global init.defaultBranch main”即可。我們現在把剛才創建的倉庫刪掉,怎麼刪掉倉庫?直接把.git目錄刪掉即可。刪完之後,我們再執行

git init
git add .
git commit -m "my first Git repo"
git branch

可以看出,主分支名字已改成main了。

我們打開src/main.c,然後修改這個文件,main函數變成:

void main(void)
{
     while(1)
     {
         printk("Hello World! %s\n", CONFIG_BOARD);
         k_sleep(K_SECONDS(1));
     }
}

我們在CMD輸入如下命令以查看Git的最新狀態

git status

日誌提示你:main.c文件已修改(注意:日誌中還出現了not staged for commit字樣,以及git add字樣,後面解釋他們的意思),我們可以輸入如下命令以查看具體的修改:

git diff

刪除了什麼(-表示刪除),插入了什麼(+表示插入),一目了然。

然後我們把相應的修改提交到倉庫。提交到倉庫之前,我們需要一個緩衝區,即先把修改提交到緩衝區,然後再提交到倉庫(數據庫),這個緩衝區在Git中稱為stage或者index,如何把修改添加到緩衝區?根據上面git status的日誌提示,大家應該也猜到了:git add,我們使用如下指令將所有修改都添加到stage(.表示所有修改,所有修改不僅指所有的修改文件,又指所有新添加的文件和刪除的文件)

git add .

這樣所有修改都添加到stage了,然後我們提交修改,輸入如下指令:

git commit –m "Changed main() to a print loop"

加上第一次創建倉庫時的提交,到目前為止我們就有2次提交了,我們查看一下git歷史

git log

這個時候,你再輸入git status,我們將得到如下日誌(請大家仔細看下面的日誌,體會裏面每句話的意義):

現在hello_world已經可以跑起來了,代碼也完成了,我們再添加一個新功能:把一個板子上的LED燈循環點亮,即Blinky程序,為此我們再新建一個分支(branch):blinky,大家使用如下命令新建一個分支:

git branch blinky

這樣一個新的分支:blinky就創建成功了,我們可以通過git branch查看目前倉庫包含哪幾個分支:

大家可以看到當前倉庫包含兩個分支:main和blinky,main左邊的*表示當前分支是main,如前所述,我們現在想在新分支blinky上開發,以驗證blinky程序能否正常運行,為此,我們需要先切換到blinky分支,然後再在blinky分支上進行開發,切換到blinky分支的命令為:

git checkout blinky

然後我們通過git branch來驗證切換是否成功,日誌如下所示:

現在我們已經在blinky分支上,然後我們把blinky代碼加上去,Blinky程序代碼獲取鏈接為://github.com/zephyrproject-rtos/zephyr/tree/main/samples/basic/blinky(註:如果GitHub訪問不了,請多試幾次。如果還是不行,請訪問//gitee.com/minhua_ai/sdk-zephyr/tree/main/samples/basic/blinky這個鏡像網站獲取相關代碼),大家只要把src/main.c和prj.conf兩個文件下載下來,然後直接替換我們本地的同名文件,整個blinky程序就算開發結束了。我們可以用git status和git diff查看一下我們的修改:

提示:按空格鍵繼續查看內容,按字母q鍵退出內容查看。空格鍵和字母q鍵是兩個常用的快捷鍵,適用範圍非常廣,大家一定要記住他們的用法。

然後我們提交此次修改,即輸入

git add .
git commit –m "added blinky function"

至此我們兩個分支的內容都開發結束了,一個是hello_world的輸出,一個是點亮LED燈,我們可以通過git checkout來回切換main和blinky兩個分支,看一下src/main.c的內容是不是隨着git checkout切換而跟着變化,比如:

git checkout main

這個時候src/main.c的內容又會切到之前的hello_world內容。

我們可以通過git log查看一下每個分支的歷史,以加深大家對分支和歷史的了解:

如果這個項目需要多個人一起協同開發,那麼就必須建一個服務器,這樣大家可以通過這個服務器進行代碼同步。支持Git的第三方免費的服務器有很多,比如GitHub/Gitee,這裡我們以GitHub為例,把我們的代碼上傳到GitHub服務器(Gitee服務器的做法與此類似,這裡就不再演示了)。另外你也可以使用自己的服務器,Git本身就支持服務器搭建功能。目前在國內訪問GitHub服務器速度比較慢,下面有關GitHub服務器的操作,如果碰到失敗情況,建議大家多刷幾次,有可能就成功了;或者找一個人少的時候去訪問GitHub服務器,比如凌晨,這個時候肯定速度很快。如果大家還是無法訪問GitHub服務器,那麼建議大家使用Gitee服務器,大家可以自己上網去搜一下如何註冊Gitee賬號以及如何創建Gitee倉庫,大家也可以參考本文的4.12節,裏面有關於Gitee倉庫的一些操作說明。

首先你需要註冊自己的GitHub賬號,然後進入your repositories標籤頁,選擇”new”

我們把這個新創建的倉庫命名為:git_demo

創建成功後,我們將得到這個git倉庫的HTTP或者SSH地址,我們可以通過HTTP或者SSH地址訪問這個倉庫,這裡我們以HTTP為例。

回到我們電腦本地,回到剛才的cmd操作界面,我們可以把我們剛剛創建的hello_world倉庫跟GitHub上的git_demo倉庫關聯起來,並使二者保持同步。關聯之前,我們可以查看一下.git/config文件,它的內容如下所示:

並查看一下.git/refs目錄,裏面只包含兩個目錄:

並查看.git\objects\pack目錄,此時會發現此目錄為空:

然後我們把GitHub服務器HTTP地址添加到本地倉庫,如下:

git remote add origin https://github.com/aiminhua/git_demo.git

這樣本地倉庫就跟遠程的GitHub倉庫關聯起來了,然後我們把本地倉庫的main分支推送到GitHub倉庫main分支上:

git checkout main
git push -u origin main

上述命令成功執行後,GitHub倉庫內容就跟本地倉庫內容同步起來了,如下:

本地倉庫和遠程倉庫關聯起來後,我們再次查看.git/config文件內容,如下:

再次查看.git/refs目錄,裏面多了一個remotes目錄,remotes目錄下面包含origin目錄,而origin目錄下面又包含一個main文件,這個main文件其實就代表遠程GitHub倉庫的main分支,而.git/refs/heads目錄下包含main和blinky兩個文件,分別代表本地的main和blinky兩個分支。

.git\objects\pack這個目錄大家如果去查看,此時還是空的。

我們現在模擬兩個開發者協同開發的情況,現在開發者1已經把代碼上傳到GitHub倉庫上,開發者2則直接可以把GitHub上的倉庫克隆(clone)到自己的本地機器上,為此我們另建一個目錄hello_world2,以模擬開發者2的本地倉庫,然後我們執行如下命令把遠程倉庫內容克隆下來:

git clone https://github.com/aiminhua/git_demo.git

大家可以看到,這個clone下來的倉庫跟GitHub倉庫基本上一模一樣,而且這個倉庫的.git\objects\pack目錄下面是有內容的:

下面我們將對Git工作原理進行說明。

3. Git工作原理簡介

如果你對Git不是很熟,建議你按照第2章”2. 創建第一個Git倉庫(Repository)”先實際操作一遍,然後再看本章,效果會更好。

Git是一個分佈式版本控制系統(distributed version control system),這意味着每一個開發者或者電腦都擁有一個本地倉庫,而且每個倉庫(包含服務器上的那個倉庫)關係同等,即每個倉庫都可以獨立工作,同步之後,每個倉庫都擁有所有倉庫的修改歷史記錄。跟分佈式版本控制系統相對的是集中式版本控制系統(central version control system),比如SVN(Subversion)就是集中式版本控制系統,集中式版本控制系統對服務器依賴比較高,服務器一旦出問題,版本歷史就會丟失,當多個人同時修改一個文件的時候,還需要文件鎖機制。根據最新的統計結果,Git已經成為全球使用最廣泛的版本控制系統,而且遠遠超過第二名。

3.1 Git操作區域

下圖顯示了Git管理代碼倉庫的一個示意圖:

Git的管理對象主要包括5個區:remote,clone,branches,working files和stage。remote表示遠程服務器,clone/branches/working files/stage都存在本地機器上,working files(working files在英文中也可以稱為working directory或者working space,他們都是一個意思)就是大家真正面對的code base文件,即除.git目錄外其他所有文件都屬於working files,比如下圖紅框所有文件都是working files:

branches和clone都在.git目錄中,屬於本地倉庫的一部分,branches就是本地分支,比如前述的main和blinky,branches的引用在如下目錄:

branches引用的內容都在objects目錄中。

clone就是把遠程服務器的內容直接克隆到本地機器上,算是本地機器上的一個備份,clone區也叫upstream或者remote-tracking branches。我們一般使用origin來引用遠程服務器倉庫或者clone區,當我們pull或者push的時候,origin指的是遠程服務器倉庫,當我們checkout origin/main的時候,此時就是指clone區,clone的本地引用在如下目錄:

clone指向的內容也是在objects目錄。

stage區,又稱index區,以前也稱為cache(緩衝區),它就是指.git目錄下的index文件:

當你把新的修改提交到objects數據庫之前,需要先把修改放在stage區,起到一個緩衝的作用。

3.2 Git對象(數據結構)

如大家熟悉的,對一般用戶來說,Git呈現給大家的是一系列命令,而命令就要有操作數據對象,命令我們在下一節介紹,本節我們介紹Git的數據結構。

Linus說,Git不是傳統的SCM,它更像一個文件管理系統。這句話道出了Git的核心設計理念,對於文件管理系統來說,最重要的就是它的數據結構以及對數據的引用。前面說過,Git像是一個小型數據庫,這個是站在接口層面上說的,但Git的底層實現說到底還是文件系統,但Git又不是大家常見的那種文件系統,我們常見的文件系統描述一個文件的時候一般包括兩部分:文件的meta data以及文件內容,比如一個txt文件,它的meta data就包括文件類型,文件名字,文件大小等,但是在Git中,由於它要處理的文件類型多種多樣,為了使用同一套算法去處理這些不同的文件類型,Git捨棄了meta data部分,直接把文件內容本身以二進制形成存儲下來,這些文件內容在Git中叫blob。blob沒有文件名,沒有時間戳等任何meta數據,它儲存的是文件的純二進制內容,blob文件本身在內部是以它內容的hash值來命名的,其中前2個字符為目錄名,剩下的字符為文件名。blob文件都放在objects目錄:

如上兩個blob文件的hash值分別為:ba307c13799b7136cbd26677faf687831757b2c4和bacc394eb450bad97159a1392d5d5b7808a15a96。hash值就是這兩個文件的ID,我們以後都是直接通過hash值來訪問他們。這兩個對象代表什麼意思呢?我們可以使用git cat-file來獲取hash值對應的對象的信息,由下圖可知,這兩個blob,一個是commit對象,一個是sample.yaml文件的內容。關於git cat-file和commit對象後面會有詳細論述。

由於hash值就是對象的ID,一旦對象有任何修改,它的hash值就改變了,換言之,它的ID也就變了,它就變成了另一個對象了。在Git中,每提交一次版本,如果文件有修改,那麼這個文件就會重新生成一個新的blob,所以說,blob只是某個文件的一個版本,如果這個文件有多次提交記錄,那麼在數據庫中,它會對應多個blob文件。另外,blob本身只是位於工作目錄中的原始文件內容的一個快照(snapshot),snapshot有兩層含義:一blob和原始文件內容一一對應,可以相互恢復,二blob對原始文件內容做了一定的處理,簡要地說,blob對原始文件內容做了無損壓縮。

除了blob這個對象外,Git還包含tree,commit,tag和pack等對象,每個對象都有一個對象名或者ID,像blob一樣,每個對象的ID都是其內容的hash值,Git包含的主要對象如下所示:

  • blob:文件內容本身,不帶任何meta數據。
  • tree(樹):blob不包含meta數據,如何找到它?我們的工程是包含很多目錄的,這些目錄信息存在哪裡?這些就是tree對象要做的,tree對象包含一系列文件名以及子樹,所以我們通過tree對象找到blob對應文件的文件名,子樹本身也是tree對象,它又可以遞歸下去。Git中的tree是Merkle tree
  • commit(提交):每commit一次,形成一個commit對象。commit對象把tree對象,日誌信息,父commit對象,時間戳等連接在一起,形成一個完整的歷史記錄。
  • tag:tag其實就是對某一個特殊commit的引用,如果我們要發佈某一個版本的時候,它對應的commit是一個hash值,大家根本記不住,為此引入了tag對象,tag對象指向一個commit,而且對這個commit加上一些額外meta數據,比如取個名字:v1.0.0。
  • pack:如前所述,每個文件每提交一個版本,都會生成一個blob,這樣做,雖然提高了文件系統訪問效率,但是當我們把他們上傳到遠程服務器時,這種做法就會浪費我們很多帶寬和時間,為此Git引入pack對象,對blob數據進行壓縮(zlib壓縮),並打包成pack文件。

除了上述數據對象(他們都是保存在.git/objects中),Git還會保存引用(references,簡寫refs),refs就是對commit的引用,用來找到需要的commit,refs保存在.git/refs目錄,主要有如下引用類型:

  • heads (branches):每個分支的最頂端,也是下一個新的提交的引用,每執行一次提交,自動向前推進。
  • HEAD:當前分支的head,跟工作目錄相對。
  • Tags:用來引用某一個特殊的commit。

如前所述,Git使用hash標識所有的對象,確切說,Git使用SHA-1來計算每個對象的hash值,通過hash我們可以得到對象所有的信息,我們可以使用命令:

git cat-file -p hash值

來得到該hash值對應的對象的所有信息,比如前面我們有一個blinky的提交對象: ba307c13799b7136cbd26677faf687831757b2c4,輸入如下命令:

git cat-file -p ba307c13799b7136cbd26677faf687831757b2c4

我們得到如下日誌:

可以看出,ba307c13799b7136cbd26677faf687831757b2c4這個對象包含一個tree對象(50e1fa7b00e83fb831146251317d3057b9d30c09),一個父對象(fb863cc95ca4a80fe64e51addd1db6a3910ee69e),以及相關的提交日誌。我們可以進一步通過git cat-file查看tree對象和父對象信息:

可以看出tree對象就是工作目錄的頂級目錄的快照,而父對象就是前一次的commit對象。

SHA-1是40個字符,我們可以截取SHA-1的前幾個字符來代表整個SHA-1,比如取前6個字符或者取前8個字符,前提是這些截取的字符不會在當前Git倉庫中出現重複情況,截取的做法會讓我們的命令或者文字看起來更簡潔。

我們可以使用git cat-file一一查詢我們hello_world倉庫目前所有的對象,最終我們將得到如下的對象關係圖(左上角的是對象hash的前6位):

從上面可以看出,工作目錄中的每個文件都在Git倉庫中有一個blob對象,每次提交都會生成一棵樹,這棵樹會保留工作目錄每個文件的快照(snapshot)。如果這個文件本次提交沒有修改,snapshot還是之前的blob;如果這個文件本次提交有修改,則會生成一個全新的blob,並指向它。提交是有繼承關係的,不管branch最後如何演變,所有branch不斷遞歸,都將找到git init創建倉庫時的那個commit為總根節點。上面這個圖,希望大家仔細研讀,最好自己去畫一畫,這對大家理解Git的內部對象非常有幫助。 

3.3 Git命令

最初Git不是用來直接管理源代碼版本的,而是為第三方SCM工具提供底層命令支撐,也就是說,大家操作第三方SCM工具,第三方SCM工具再調用Git底層命令。後來Git逐漸發展,大家也可以直接使用Git命令來直接管理源代碼版本了。為此,Git命令分兩種:plumbing和porcelain。plumbing命令,亦稱Git core,屬於底層命令,它是供第三方SCM工具或者腳本使用,對我們普通用戶來說,一般是不需要了解的,像git cat-file, git hash-object,git ls-files就是這種命令,這些命令比較穩定,利於腳本進行解讀。porcelain命令,就是大家面對的那些命令,比如git add,git diff,git commit,這些命令更友好,但也意味着容易演化,所以他們是人類可讀的,而不是腳本解析的。

如第2章”2. 創建第一個Git倉庫(Repository)”描述的那樣,Git是通過命令行進行交互的,Git命令一般通過shell輸入,最常見的就是bash,bash目前同時兼容Linux,Windows和MacOS。前面大家安裝好Git之後,其實Windows版的Git bash也自動裝好了,第2章裏面的所有命令都可以通過bash去輸入,如下所示:

我們之所以採用CMD來演示,主要是因為Windows用戶眾多,而且大家比較熟悉CMD,大家如果去網上搜索Git常用命令的用法的話,大部分人都是以bash為例來闡述的,因為有些操作CMD是不支持的。這也提醒大家,如果大家按照網上的提示去操作總是不成功的話,建議換成bash試一下。 

上面這個圖把一些常用Git命令的操作對象通過圖形化的方式展示出來了,比如git pull這個命令,表示把服務器的代碼拉到本地機器上,如圖所示,我們知道原來git pull是把遠程代碼直接放在工作目錄,而不是其他區域。再比如git add這個命令,由圖可知,它是把修改放在了stage區域;而git commit命令則是把stage的內容提交到branches區域(確切說branch以及branch引用的objects,這個大家讀過3.2節就能理解了),這也是為什麼當你修改了文件而沒有先執行git add,而直接執行git commit,系統會提示你”nothing added to commit”。其他命令我們這裡就不一一解讀了,我們會在第5章對一些常用的Git命令進行統一說明。

3.4 Git flow

從前面描述可知,我們可以隨時隨地創建一個新的branch(分支),然後新分支又可以引出其他分支,最後我們又可以把新分支merge(合併,後面會講述它的含義)到main分支。隨着開發的不斷發展,branch也將不斷發展,這裏面就有一個git的工作流問題,即該建哪些分支?這些分支該如何演變?有人總結了如下的branch模型(原文鏈接為://geekiam.io/how-to-use-git-flow/),可供大家參考。

上圖每一個圓點就是一次commit,每一條橫線表示一個分支,總共有6個分支:main,hotfixes,release,develop,feature和feature’,main分支用來發佈,開發主要在develop分支,每次增添新功能的時候,另起一個feature分支,在把develop分支推送到main之前,先合併到release分支。從上面的分支演化圖,我們可以看出,分支可以相互引用,錯綜複雜,但從根本上來說,我們的開發從main分支開始,也從main分支結束。 

4. Git進階示例

我們現在在第2章Git工程的基礎上,繼續演示Git的一些高級應用。

4.1 git merge – fast forward

我們首先查看一下分支情況,並切換到主分支:

這2個分支的功能是不一樣的,main分支循環打印”hello world”,blinky分支循環點亮板子上的LED燈,我們現在把這兩個分支合併(merge),即讓程序同時具備打印”hello world”功能和點亮LED功能,我們使用如下命令進行merge:

git merge blinky

從日誌可以看出,此次merge是fast-forward(快速)合併,因為blinky分支的head就在main分支head之上進行的修改提交,期間main分支沒有做任何其他修改,所以merge的時候,直接把main的head指向blinky的head,這就是fast forward。我們後面輸入git diff和git status查看狀態,可以看出此時index和working tree都是乾淨的。此時我們打開src/main.c文件,可以發現該文件已經完全被blinky分支的src/main.c覆蓋,而老的main分支裏面的src/main.c內容全都不見了:

這個時候,大家可以手動把hello_world裏面的代碼加到merge後的main.c,問題就解決了。

git merge實際上就是一次新的commit,這個可以通過git log日誌看出端倪:

從日誌可以看出,git merge後main分支多了一次commit。

4.2 git reset和git revert – 刪除某個commit

為了下面演示另一種merge情況,我們需要把剛才的merge動作撤銷,剛剛說過了,git merge就相當於一次commit,撤銷git merge的操作跟撤銷git commit的操作兩者是一樣的,如何撤銷一次commit呢?我們有兩種選擇:一是git revert,二是git reset,我們先看看git revert,它的語法是:

git revert (commit ID)

其中HEAD是一個特殊的commit ID引用,指向當前分支的最頂端commit

比如我們輸入

git revert HEAD

(註:當前環境下,該命令等價於:git revert fb863cc95ca4a80fe64e51addd1db6a3910ee69e)

我們將得到如下結果:

此時使用git log查看日誌:

可以看到,git revert其實是一個新的commit,它把fb863cc95ca4a80fe64e51addd1db6a3910ee69e對應的commit重新提交在最新的head之上。git revert之後,main分支總共有4個commit了,而且第2個commit和第4個commit兩者追蹤的文件是一模一樣的。

現在我們再來看git reset。有人覺得git revert這種做法有點怪,希望直接把第3個commit刪掉,然後把head指向第2個commit,這就是git reset要實現的功能。git reset語法跟git revert有點像:

git reset (commit ID)

與git revert類似,你也可以使用HEAD這個特殊的commit ID引用。

執行git reset後,分支的head指向指定的commit,而這個commit之後提交的所有commit都將丟失,所以建議大家慎用git reset。git reset經常跟着三個輸入參數–mixed,–soft和–hard,默認選擇–mixed選項,這3個參數有非常強大的副作用,大家一定要記住他們的區別,其中–mixed選項表示同時要複位index區,但保持working directory不變,–soft選項表示保持index區和working directory當前狀態不變,而–hard選項表示同時複位index區和working directory。據此,我們輸入如下命令:

git reset --hard fb863c

此時我們再使用git log查看一下歷史:

可以看出main分支只剩下2條commit了,成功刪除了後面的commit。

4.3 git checkout,git revert和git reset

git checkout,git revert和git reset這三者的區別示意圖如下所示:

4.4 git merge – 手動解決衝突

如果你一直按照我們的步驟來操作,那麼前面的fast forward的merge應該已經刪掉了,此時main分支只有兩條commit,而blinky分支有3條commit,我們還是切換到main分支,然後修改src/main.c文件:

做完上述修改後,我們再執行git add . 和 git commit -m “updated main.c to prepare for a merge”

此時,main分支已有3個commit了,而blinky分支也有3個commit,此時我們再merge這兩個分支:git merge blinky,有如下conflict報錯:

然後我們打開src/main.c文件,發現它已經變成:

此時git status會有如下輸出:

怎麼解決這個衝突?手動修改src/main.c,簡言之,改到符合你期望為止,main.c最後變成:

此時的main.c同時具備hello_world和blinky功能。然後我們重新提交,本次merge就成功結束,即輸入:

git add .
git merge --continue

注意:git merge –continue後,會跳出一個文本編輯器讓你寫提交信息,寫好保存,然後關閉即可。

merge是分佈式版本控制系統的一個核心概念,因為分佈式版本控制系統支持merge,所以它可以允許多個開發者同時修改一個文件,而不必像集中式版本控制系統那樣每次更新文件都需要對文件進行鎖定,每時每刻只允許一個開發者修改同一個文件。一般來說,merge採用three-way合併算法進行merge,假設我們有兩個文件A和B,他們有共同祖先C,A和B合併並生成D,它的算法是這樣的:如果A和B的內容是一樣的段落,直接輸出到D;如果A和C的內容是一樣的段落,將B對應的段落內容輸出到D;如果B和C的內容是一樣的段落,將A對應的段落內容輸出到D;如果A,B和C三者內容都不一樣的段落,即是存在衝突的段落,這個就需要用戶手工去調整和修改。

4.5 .gitignore – 忽略某些文件的追蹤

我們現在把這個工程編譯一下(註:沒有安裝Zephyr編譯系統的,大家可以只觀看,不用實際操作),編譯成功後,我們會有如下build目錄:

此時git status輸出也會顯示我們新增了這個build目錄:

這個build目錄裏面的內容都是編譯系統自動生成,我們去追蹤他是毫無意義的,更重要的是,一旦追蹤了這個build目錄,那麼每次執行git diff或者git status,你都將得到一個長長的diff表,無用信息將掩蓋有用信息,閱讀特別費勁。為此需要引入.gitignore文件,忽略某些特定文件或者目錄,我們可以直接把zephyr工程裏面的.gitignore文件拷到我們工程://github.com/zephyrproject-rtos/zephyr/blob/main/.gitignore,如下:

這樣我們的git就不再追蹤build目錄了。

關於.gitignore的用法,大家可以自己上網搜一下,這裡不再贅述。我給大家的建議是:直接使用類似的第三方開源項目裏面的.gitignore文件,省得自己去研究。

既然我們增加了新文件:.gitignore,我們就再次提交一下項目修改:

特別提醒,如果你已經追蹤了build目錄,此時再添加.gitignore文件,雖然之後build目錄內容不再提交到倉庫,但是,你會發現,git diff還是會去比較build目錄裏面的內容,這是因為git diff比較的是index和working tree兩者的不同,而index已經包含build目錄了,加上.gitignore並不會自動清理index區。此時為了得到忽略(ignore)目的,我們需要把index裏面的build目錄刪掉,可使用如下命令:

git rm --cached build

有時為了簡單化,你可以直接把整個index區清空,然後重新添加所有working files並提交,這樣肯定可以保證.gitignore文件立即生效,即使用下面命令:

git rm -r --cached .
git add .
git commit -m "added .gitignore"

4.6 git push – 把本地代碼推送到遠程服務器

我們在第2章已經把倉庫push到服務器了,現在我們在本地又做了多次提交,本地倉庫跟遠程倉庫已經不同步了,這個也可以通過git status的輸出看出:

此時我們有4個本地提交沒有推送到服務器,為此輸入如下命令:

git push -u origin main

其中參數-u表示upstream的意思,即同時讓本地分支追蹤遠程分支。強烈建議大家加上這個-u,尤其在推送分支的時候,比如git push -u origin blinky,這個-u會讓你省去很多事情!

這樣本地倉庫就與遠程倉庫同步了,這可以通過GitHub服務器上的src/main.c來驗證同步是否成功:

目前國內訪問GitHub服務器經常出錯,上述的git push命令有可能會報錯”Failed to connect to github.com port 443 after 21063 ms: Timed out”,碰到這種情況,你需要人少的時候去訪問GitHub,比如凌晨;或者使用VPN,此時需要對git進行配置,以支持vpn通道訪問服務器,配置指令如下所示:

git config https.proxy 127.0.0.1:7890
git config http.proxy 127.0.0.1:7890

相關shell輸出日誌如下所示:

4.7 git rebase – 團隊協作

我們現在進入開發者2的倉庫目錄:C:/Nordic/Blog/code/hello_world2/git_demo,並且使用bash來演示接下來的git命令交互(CMD其實也一樣),首先輸入:

cd C:/Nordic/Blog/code/hello_world2/git_demo

然後:

git branch

可以看出此時只有一個分支:main,我們現在創建一個新分支:rtt_log,這個分支將實現一個功能:把日誌從串口打印切換到RTT viewer打印,創建分支命令如下:

git branch rtt_log

然後切換到rtt_log分支:

git checkout rtt_log

然後我們對prj.conf做如下修改:

然後提交本次修改

git commit -a -m "Changed log backend to RTT viewer"

此時一次新的提交已經成功了,相關日誌輸出如下所示:

現在開發者2已經有了自己的commit了,而GitHub服務器也已經同步了開發者1的commit,此時我們能不能直接把開發者2的commit push到服務器上呢?我們先試一下:

git push -u origin main

我們得到如下輸出:

push被拒絕了,因為push只能接受fast forward類型的merge(本文4.1節有fast forward類型merge示例),即快速轉髮式的合併開發者2和開發者1的工作。為此我們需要先把服務器的更新拉下來,然後跟本地的修改merge,然後再次push。

先拉下服務器更新:

git fetch origin

大家可以使用:

git checkout origin/main

來查看遠程倉庫是否更新到本地(local),注意:origin/main表示的是遠程倉庫的本地clone,而不是本地main分支哦。

上述日誌表明,遠程更新已拉取到本地。

我們再次回到main分支:

git checkout main

上面提示我們的main分支已經落後origin/main了,我們可以把origin/main直接merge到main分支上:

git merge origin/main

這樣開發者2的本地main分支也跟遠程main分支同步了。

這裡提一下,上面git fetch origin和git merge origin/main兩條命令其實可以用一條命令取代:

git pull origin master

這個大家從git pull –help的幫助頁面說明也能看出。

現在我們再把rtt_log分支merge到main分支,我們先使用前面說的merge方式來做一下:

git merge rtt_log

提示有衝突,我們手動解決衝突後:

git add prj.conf
git merge --continue

前面提過,merge本質就是一個新commit,我們看看這個新commit的父commit是誰:

它有兩個父節點,跟我們期望的有點不一樣,我們希望main分支歷史記錄清晰,最好一一對應。

現在我們把剛才的操作複位:

git reset --hard origin/main

現在我們切換到rtt_log分支:

git checkout rtt_log

然後將其rebase到main分支,rebase的意思就是把rtt_log最新的commit基於main分支的頂端重新commit一次,rebase跟merge功能有點相似,具體區別我們後面講。我們輸入如下rebase命令:

git rebase main

此時會報conflict(衝突)錯誤,這個跟merge一樣,手動解決conflict後,再執行如下命令:

git add .
git rebase --continue

大家可以看到,rebase後的commit合併了最新的main分支和rtt_log分支,我們再看看rebase後的commit的對象關係:

可以看出,新commit直接把rtt_log分支的修改重新在main分支頂端上重新提交了一遍,而且其只有一個parent,我們再看看它的parent到底是誰:

上面日誌再次證明,rebase後的commit是基於main的head做的,而不是基於rtt_log之前的head做的。

此時rtt_log分支成功得rebase到最新的main分支,然後我們再把rtt_log分支merge到main分支:

git checkout main
git merge rtt_log
git log

此時的merge,就是一次fast-forward的merge。我們再看看這個時候的commit的對象關係圖:

從上面輸出可以看出,最新的commit只有一個parent,而且parent就是前一次的commit,這樣歷史記錄就非常清晰了。

這個時候我們就可以再次推送本地更新到遠程服務器了:

git push -u origin main

這樣開發者2也成功把自己的代碼推送到服務器了,從而開發者1和開發者2完成了協作開發。

4.8 git merge和git rebase區別

假設我們有如下分支,即main分支和feature分支,而且他們兩個分支都是從同一個commit分叉出來的。

假設我們現在在feature分支上,然後把main分支merge到feature分支上,merge成功後,我們將得到如下分支結構圖:

可以看出,merge只創建了一個新commit,而且它的父節點包含2個。

如果我們不使用git merge,而採用git rebase,那麼rebase後的分支結構圖如下所示:

可以看出rebase後,feature分支以前老的3個commit全部刪除,然後他們重新在main分支頂端一一再次提交,形成3個新的commit。Rebase後歷史記錄呈線性關係,非常乾淨清晰。

使用git rebase有一條基本原則:不要rebase公共的branch,比如上面的例子,如果我們把main rebase到feature分支上:

執行這個操作後,main分支歷史被改寫,這樣遠程的main或者其他開發者的main分支,在Git看來,就跟你的main分支分叉了,後面你就沒法直接push了。所以執行git rebase操作的時候,我們總是在開發者分支上進行,然後fast-forward merge到main分支

4.9 git submodule – 引用第三方模塊

現在我們回到開發者1的倉庫,前面開發者2已經提交了一次commit到服務器,所以我們先更新一下開發者1的倉庫,這次我們使用git pull來做:

git pull --rebase

說明一下,在這裡你也可以使用

git pull

這個命令實際上就是:

git pull --merge

即git pull默認採用merge方式,即把遠程分支merge到本地分支,前面說過merge和rebase的區別,在這裡也是適用的。

另外再提一個事,我們經常在網上看到有人說pull request,這個pull request跟我們的git pull是完全不沾邊的兩碼事,pull request是當你要貢獻自己的修改給一個第三方的repo時,你需要發起pull request,repo主人同意後,你的代碼才能提交上去,具體請見本文4.13節。

對了,現在我們使用git log查看日誌的時候,日誌已經比較多了,一屏無法顯示全部,這個時候你可以使用”空格鍵”繼續瀏覽後續日誌,或者按字母”q”退出目前操作界面。”空格鍵”和字母”q”是兩個經常用到的快捷鍵,他們適用的地方非常廣,大家一定要記住

下面我們來看看軟件開發中經常碰到的一個場景:引用第三方開源的模塊,這些模塊放在GitHub/Gitee等代碼託管平台上,這些模塊由第三方維護,而且第三方也一直在迭代開發或者維護中。

我們現在在hello_world工程中引用//github.com/aiminhua/ncs_samples這個倉庫裏面的代碼,怎麼做呢?有兩種做法:一是使用git submodule,二是使用git subtree,為了保持第三方倉庫的獨立性和完整性,我們一般使用git submodule的做法。

我們可以使用如下命令把上面這個第三方倉庫添加到本地ncs目錄下:

git submodule add https://github.com/aiminhua/ncs_samples.git ncs

執行成功後,我們會有如下日誌:

仔細再查看我們的hello_world目錄,可以看到它多了一個ncs目錄和.gitmodules文件,如下:

進入ncs目錄,我們可以看到:

此時我們在ncs目錄下使用git命令,我們可以發現,命令將直接操作ncs目錄裏面的git倉庫,而不是我們的hello_world倉庫,我們可以像操作普通git倉庫一樣去操作ncs這個第三方倉庫,比如修改文件然後提交,這個時候的提交是針對第三方倉庫的,相當於第三方倉庫又多了一條commit,跟hello_world主倉庫沒關係,感興趣的讀者可以自己去操作一下(注意:由於這個第三方倉庫不是你的,如果你想把修改push上去,是會被拒絕的,這個時候你需要走pull request流程了)。其實,我們可以通過git log的輸出日誌就能直觀理解上面的描述:

可見,git操作完全跟hello_world主倉庫不搭架,全都是第三方倉庫本身的東西。但是我們回到hello_world主目錄,此時所有的git操作又是針對我們的hello_world主倉庫了,還是看看git log的輸出就有體會了:

大家可以進到.git目錄,看看git submodule後它發生了哪些變化?.git目錄裏面會增加一個modules目錄,而且config文件會被修改:

感興趣的讀者可以把前面提到的有關文件一一點擊進去,看看他們的內容到底是什麼,這裡就不再演示了。

我們現在把剛才的修改提交,並push到服務器,輸入如下命令:

git commit -a -m "added submodule: ncs"
git push

成功後,我們再看一下服務器的情況:

可以看出,ncs這個第三方倉庫對應的代碼並沒有clone到服務器上,服務器只有第三方倉庫的引用鏈接,點擊這個鏈接,自動跳轉到第三方倉庫原始地址。

開發者1已經把第三方倉庫引用上傳到服務器了,開發者2怎麼把這個第三方倉庫clone下來呢?這裡分兩種情況,如果你是第一次clone這個工程,你只需在git clone中加入–recursive參數,即:

git clone https://github.com/aiminhua/git_demo.git --recursive

另一種情況是你已經clone了這個工程,這個時候就是update操作了,目前開發者2碰到的情況就是這種類型。為此我們先git pull主倉庫更新:

cd C:\Nordic\Blog\code\hello_world2\git_demo
git pull --rebase

此時雖然把ncs目錄拉下來了,但裏面是空的。為此,我們還需要執行如下命令:

git submodule update --init

至此我們的git submodule演示就結束了。

4.10 git subtree – 包含第三方模塊

前面提過,我們還可以使用git subtree來包含第三方模塊。git submodule是引用第三方模塊,第三方模塊是一個獨立的git 倉庫,跟主倉庫完全剝離。git subtree則是包含第三方模塊,第三方模塊代碼是主倉庫的一部分,對第三方模塊的修改會全部體現在主倉庫的歷史記錄中,使用git subtree推送第三方模塊,第三方模塊代碼會完整clone到服務器上。

我們現在把//gitee.com/minhua_ai/ncs_samples這個倉庫裏面的代碼包含到我們的hello_world工程中,輸入如下命令:

cd C:\Nordic\Blog\code\hello_world
git subtree add --prefix ncs_subtree https://gitee.com/minhua_ai/ncs_samples.git master --squash

其中,ncs_subtree是本地目錄以存儲第三方模塊代碼,master表示的是遠程倉庫的master分支,squash表示只將第三方模塊最新的一條歷史記錄合併到主倉庫中。

此時再去查看hello_world工程目錄,你會發現它下面多了一個目錄:ncs_subtree,而且ncs_subtree目錄已經包含了模塊所有代碼:

這個時候,我們使用git log看一下日誌,有:

可以看出,ncs_subtree這個模塊已經成為hello_world倉庫的一部分了。

我們現在把剛才的修改同步到服務器:

git push

可以看出,git push操作可以把第三方模塊代碼完整clone到服務器上。

如果第三方模塊更新了,我們可以通過git subtree pull拉取最新代碼到本地,比如下面命令就可以將本地的ncs_subtree模塊跟服務器同步:

git subtree pull --prefix ncs_subtree https://gitee.com/minhua_ai/ncs_samples.git master --squash

4.11 git tag – 發佈軟件版本

到目前為止,我們的Git倉庫演示也就基本結束,我們現在可以着手發佈它,也就是給我們當前的main分支head打一個tag,然後push到服務器。Tag就是一個特殊的commit,比如你要發佈的版本,比如你要臨時測試的版本,這個時候還是用commit ID去引用它,非常不利於閱讀和大家交流,而tag相當於給這個commit取了一個好記的名字,利於大家閱讀和交流。

給當前HEAD打tag命令為:

git tag -a v1.0.0 -m "git demo release v1.0.0"

然後我們通過git tag去列出目前倉庫支持的所有tag,如果上述命令成功了,就會有輸出:

然後我們把這個tag推送到服務器:

git push origin v1.0.0

Tag一旦打成功了,你可以查看tag對應的內容:

git checkout v1.0.0

如上面日誌提示的一樣,你可以查看tag內容,但是你不能修改它。如需修改tag對應的內容,你必須新起一個branch,然後去修改,而不能直接在tag上進行修改。 

4.12 將GitHub倉庫導入到Gitee – 解決GitHub訪問速度慢問題

GitHub有很多非常好的開源代碼,但是目前國內訪問GitHub有點慢,當這個GitHub倉庫比較大的時候,直接使用git clone去下載它,經常會報超時等各種莫名其妙的錯誤。此時我們可以把這個GitHub倉庫導入到Gitee,由Gitee通過云云對接的方式去clone(這個過程其實就是建立倉庫的鏡像),然後我們再從Gitee服務器把鏡像倉庫clone下來。下面我們將演示如何把我們剛剛創立的倉庫://github.com/aiminhua/git_demo導入到gitee。

首先確保你有gitee賬號,然後登錄,選擇如下菜單選項:

然後輸入GitHub倉庫地址://github.com/aiminhua/git_demo,最後點擊”導入”按鈕,Gitee就會通過云云通信的方式去抓取GitHub上的倉庫,並在Gitee服務器上建立鏡像。

成功後我們將看到:

點擊”管理”標籤頁,將倉庫設為公開:

然後我們就可以從Gitee克隆這個倉庫了。首先新建目錄hello_world3,然後把倉庫克隆到這個目錄:

git clone https://gitee.com/minhua_ai/git_demo.git

註:這裡git clone我們沒有使用–recursive參數,目的是為了跳過GitHub訪問,因為ncs這個目錄其實是一個指向一個GitHub第三方倉庫的submodule,不加–recursive參數,我們就不需要去clone這個GitHub的第三方倉庫,讓大家對Gitee訪問速度有個直觀的認識。但實際上我們肯定需要加上–recursive參數,這樣才能保證整個倉庫的完整性,但加上–recursive參數後,問題又來了,submodule又指向了GitHub倉庫,訪問速度驟然下降甚至有可能失敗,怎麼辦?還是老辦法,先把這個第三方的submodule://github.com/aiminhua/ncs_samples導入到Gitee,成功後,我們得到它的clone地址://gitee.com/minhua_ai/ncs_samples.git。然後我們更改git submodule的配置,使其指向這個鏡像,即修改.gitmodules文件:

然後我們輸入git submodule update命令:

git submodule update --init

至此整個GitHub倉庫就完全導入到Gitee服務器上,並通過Gitee服務器全部下載到本地。

這裡要特彆強調一點:雖然Gitee上的倉庫可以一直跟GitHub同步,但從根本上來說,這是兩個完全獨立的倉庫,一旦把GitHub倉庫導入到你的Gitee賬號下,你就是這個倉庫的owner,而不是原來的第三方倉庫開發者,你可以認為你自己新建了一個倉庫,只不過倉庫內容跟GitHub上的第三方倉庫內容一模一樣而已。

4.13 pull request – 貢獻自己的代碼

首先說明一下,pull request跟git pull兩者完全不搭架,屬於兩個不同的範疇。Pull request是針對GitHub/Gitee等代碼託管平台來說的,在這些代碼託管平台,有很多開源的倉庫,你會使用他們,使用過程中,你會發現這些開源代碼的問題,然後你自己把這些問題給fix了,由於你不是這些開源倉庫的開發者,你沒法使用git push直接把自己的fix推送到這些開源倉庫,這個問題怎麼解決?這個就是pull request要做的事情。

Pull request(簡寫為PR)用於把自己的修改merge到第三方倉庫,其步驟如下所示:

1. 先fork第三方倉庫到自己的賬號下面,下面是一個把//github.com/nrfconnect/sdk-zephyr倉庫fork到我賬號的示例。

Fork成功後,界面如下所示:

可以看出,sdk-zephyr成了我自己的一個倉庫了,既然它現在是我的倉庫,我當然可以對他做任何修改了。這種情況下,我們把原始的//github.com/nrfconnect/sdk-zephyr稱為//github.com/aiminhua/sdk-zephyr(我自己的倉庫)的upstream倉庫。

2. clone //github.com/aiminhua/sdk-zephyr對應的倉庫

git clone https://github.com/aiminhua/sdk-zephyr.git

clone好之後我們會建一個新分支,然後在這個新分支上進行修改,比如我們新建一個分支:gpiote_pin_fix

git branch gpiote_pin_fix
git checkout gpiote_pin_fix

然後修改這個倉庫某些文件,比如我們修改gpio_nrfx.c(具體修改見下面日誌),並提交相關修改到倉庫

git add .
git commit -s

注意:這條命令之後會跳出一個文本編輯器讓你輸入提交消息,提交消息一定要符合第三方倉庫的書寫要求(第三方倉庫一般都會出相關的書寫指南,你需要自己去查閱一下),否則檢查是過不了的。這裡的-s是sign off的意思,主要是版權方面的考量,加上就好了。

此時我們就可以把最新的branch推送到遠程倉庫了:

git push -u origin gpiote_pin_fix

上面git指令的操作日誌如下所示:

服務器收到新的推送後,自動會提示你要不要創建pull request,你也可以手動選擇創建pull request,如下:

創建pull request的界面如下所示:

Pull request創建成功後,上游倉庫pull requests標籤頁下會自動包含你剛才創建的pull request,如下:

然後等待審核,按照審核意見進行細化修改,直至最後merge成功。

這裏面還有一種特殊情況:就是你的upstream倉庫還有上一級upstream倉庫,這個時候你就要分清此fix是只適用於直接的upstream倉庫,還是更上一級的upstream也適用,如果最最上級的upstream也適用的話,你應該把PR開在它那裡,比如上面例子,//github.com/nrfconnect/sdk-zephyr其實也是一個fork,它的upstream是://github.com/zephyrproject-rtos/zephyr,而且上面例子的fix是適用最原始倉庫的://github.com/zephyrproject-rtos/zephyr,這個時候上面的PR應該直接開在//github.com/zephyrproject-rtos/zephyr這下面,為此我們重複上面的過程(完全再來一次,而且跟上面的PR沒有任何關係),最終PR的樣子是下面這樣的://github.com/zephyrproject-rtos/zephyr/pull/45537

 

5. 常用Git命令說明

不管是Git軟件本身,還是Git官網,都提供了非常詳盡的Git命令使用說明,Git工作原理等等有關Git的一切。雖然我這篇博客非常長,但其主要目的還是幫助大家建立Git的常識,熟悉Git的一些基本用法,了解Git的基本工作原理,有了這些基礎後,大家再去讀Git命令使用說明或者其他Git文章,就容易得多。說到底,Git參考文檔還是以Git官方為準,我們只是為大家做了一個橋樑。

Git官方參考文檔為://git-scm.com/docs,裏面列出了Git所有命令及其詳盡說明。實際上,大家也不需要去訪問網站,Git軟件本身就自帶說明文檔大家直接查看本地說明文檔即可。比如git diff這個命令,我們輸入

git diff --help

自動跳出如下手冊頁面

裏面詳細列出了git diff內涵,使用說明,使用注意事項,舉例等等,可以說,應有盡有,事無巨細,包羅萬象,不過這帶來了一個副作用:太詳細了反而讓讀者抓不到精髓,這也是為什麼市面上有那麼多的Git教程的原因。筆者這篇博客希望能幫助大家理解git的幫助文檔,最終達到一個境界:只需看Git官方幫助文檔,就能解決碰到的所有Git問題,而無需再去網上搜索第三方的參考資料。

最後推薦一本Git專業書籍:Pro Git,整本書可以從這裡獲取://git-scm.com/book/en/v2。Pro Git是一本非常經典的介紹Git的書,我也是在寫作本文的過程中無意發現了這本書,這本書不管是廣度還是深度,都是可圈可點的,而且通俗易懂,深入淺出,非常適合Git學習和鑽研。

介紹Git常用命令之前,我再次把下面這張Git命令操作區域關係圖貼出來,這是一張來自wikipedia的圖,非常直觀地表達了Git常用命令的操作對象。

下面都是一些常用Git命令,建議大家都去了解一下。

5.1 git config

git config用於配置git軟件的一些參數。

//設置郵箱和用戶名(記得換成你自己的郵箱和用戶名)

git config --global user.email "aimh@qq.com"
git config --global user.name "Kevin Ai"

//默認分支名字改為main

git config --global init.defaultBranch main

//設置http代理或者vpn

git config https.proxy 127.0.0.1:7890
git config http.proxy 127.0.0.1:7890

//取消http代理或者vpn設置

git config --unset http.proxy
git config --unset https.proxy

//把c:/mywork(記得換成你自己的目錄哦)設為安全目錄

git config --global --add safe.directory c:/mywork

//所有目錄都是安全的

git config --global --add safe.directory "*"

//通過瀏覽器打開git config的幫助頁面

git config --help

5.2 git init

git init用來創建倉庫。

//在當前目錄創建git倉庫

git init

//在新目錄work/repo(記得換成你自己的目錄哦)下創建git倉庫

git init work/repo

5.3 git clone

git clone用於將遠程倉庫克隆到本地。

//克隆//gitee.com/minhua_ai/ncs_samples.git(記得換成你自己的git服務器地址)倉庫到本地

git clone https://gitee.com/minhua_ai/ncs_samples.git

//遞歸克隆遠程倉庫//github.com/aiminhua/git_demo.git(記得換成你自己的git服務器地址),這個遠程倉庫包含submodule

git clone https://github.com/aiminhua/git_demo.git --recursive

5.4 git add

git add用於把工作目錄更新添加到index區(stage區)。

//把工作目錄所有修改添加到index

git add .

//把指定文件main.c(記得換成你自己的指定文件哦)添加到index

git add main.c

//通過瀏覽器打開git add的幫助頁面

git add --help

5.5 git commit

git commit用於把修改提交到倉庫。

//把stage內容提交到倉庫,自動打開系統編輯器以讓你輸入提交信息

git commit

//把stage內容提交到倉庫,自動打開系統編輯器以讓你輸入提交信息,並署名該提交

git commit -s

//把stage內容提交到倉庫,提交信息為”my first repo”(記得改成你自己的提交信息)

git commit -m "my first repo"

//把working directory裏面的修改直接提交到倉庫,自動打開系統編輯器以讓你輸入提交信息

git commit -a

//把working directory裏面的修改直接提交到倉庫,提交信息為”updated main.c”(記得換成你自己的提交信息)

git commit -a -m "updated main.c"

//commit之後發現還有文件需要修改,修改之後,把本次修改合併在上次commit之中,而不是另起一個新commit,自動打開系統編輯器以讓你更新上次的提交信息

git commit --amend

//commit之後發現還有文件需要修改,修改之後,把本次修改合併在上次commit之中,而不是另起一個新commit,並把提交信息更新為”updated main.c again”(記得換成你自己的提交信息)

git commit --amend -m "updated main.c again"

//在CMD輸入多行提交信息,使用ctrl+z保存並退出輸入界面,使用ctrl+c不保存退出。這個命令用得少,一般直接使用git commit就可以了

git commit -F-

5.6 git diff

git diff用來比較兩個對象。

//顯示所有未添加至index的變更

git diff

//顯示所有已添加到index但還未commit的變更

git diff --cached

//顯示與上一個commit的差異

git diff HEAD~1

//顯示main分支與blinky分支的不同(記得換成你自己的分支名)

git diff main blinky

//顯示master分支與blinky分支的不同(記得換成你自己的分支名)

git diff master blinky

//只顯示main分支與blinky分支src目錄的不同(記得換成你自己的分支和目錄)

git diff main blinky -- src/*

//只顯示master分支與blinky分支src目錄的不同(記得換成你自己的分支和目錄)

git diff master blinky -- src/*

//只統計main分支與blinky分支的文件差異,不比較內容本身差異(記得換成你自己的分支名)

git diff main blinky --stat

//比較HEAD與e6bdd4bfeb1197189e7707df1e52ddb1979cc862(這是一個對象的ID,即hash值,記得換成你自己的)的差異

git diff e6bdd4bfeb1197189e7707df1e52ddb1979cc862

//將更改輸出到patch.diff文件(記得換成你的文件名,文件名和擴展名沒有強制要求)

git diff > patch.diff

//通過瀏覽器打開git diff的幫助頁面

git diff --help

5.7 git reset

git reset用來把HEAD指針複位到前面的commit。

//把HEAD複位到commit:fb863c(這是一個對象的ID,即hash值,記得換成你自己的)狀態

git reset --hard fb863c

//把main分支複位到服務器clone狀態

git reset --hard origin/main

//把master分支複位到服務器clone狀態

git reset --hard origin/master

//複位index區

git reset

//通過瀏覽器打開git reset的幫助頁面

git reset --help

5.8 git status

git status用於查看當前狀態。

//查看working directory哪些文件修改了

git status

//通過瀏覽器打開git status的幫助頁面

git status --help

5.9 git rm

git rm用於刪除工作目錄或者index區的文件。

//同時刪除index和working tree裏面的README.rst文件(記得換成你自己的文件)

git rm README.rst

//只刪除index裏面的README.rst,保留working tree裏面的README.rst(記得換成你自己的文件)

git rm --cached README.rst

如果只刪除working tree裏面的文件,或者還沒有添加到index裏面的文件,請直接使用操作系統的刪除命令

5.10 git log

git log顯示提交歷史。

//顯示提交歷史

git log

//顯示提交歷史,並統計文件修改情況

git log --stat

//顯示src/main.c的提交歷史(記得換成你自己的文件)

git log src/main.c

//顯示src/main.c的內容修改歷史—diff形式 (記得換成你自己的文件)

git log -p src/main.c

//搜索所有提交歷史信息中包含”blinky”關鍵字的提交(記得換成你自己的關鍵字)

git log --grep blinky

5.11 git show

git show用於顯示對象信息。

//顯示e56e8a(這是一個對象的ID,即hash值,記得換成你自己的)的信息

git show e56e8a

//顯示tag v1.0.0的信息(記得換成你自己的tag名稱)

git show v1.0.0

//顯示tag v1.0.0的名字信息(記得換成你自己的tag名稱)

git show --name-only v1.0.0

//顯示提交e56e8a(這是一個對象的ID,即hash值,記得換成你自己的)包含的src/main.c內容(記得換成你自己的文件)

git show e56e8a:src/main.c

5.12 git tag

git tag用於創建,列出,刪除與驗證tag對象。

//列出目前倉庫包含的tag

git tag

//給commit e56e8a(這是一個對象的ID,即hash值,記得換成你自己的)打輕量級的tag,tag名稱為v0.1(記得換成你自己的名稱)

git tag v0.1 e56e8a

//給HEAD打有註解的tag,tag名稱為v1.0.0(記得換成你自己的名稱)

git tag -a v1.0.0 -m "git demo release v1.0.0"

//刪掉tag v0.1(記得換成你自己的tag)

git tag -d v0.1

5.13 git branch

git branch用於列出,創建和刪除分支。

//列出所有本地分支

git branch

//列出所有遠程分支(確切說是遠程跟蹤分支)

git branch -r

//同時列出本地和遠程分支

git branch -a

//創建一個新分支:feature(記得換成你自己的分支名)

git branch feature

//刪除分支:feature(記得換成你自己的分支名)

git branch -d feature

//修改分支feature名字為dev(記得換成你自己的分支名)

git branch -m feature dev

//基於commit e56e8a(這是一個對象的ID,即hash值,記得換成你自己的)建立分支:feature(記得換成你自己的分支名)

git branch feature e56e8a

//設置當前分支跟蹤origin/blinky,即origin/blinky成為它的upstream(記得換成你自己的分支名)

git branch --set-upstream-to origin/blinky

//刪除遠程分支blinky在本地的clone(記得換成你自己的分支名)

git branch -dr origin/blinky

5.14 git checkout

git checkout用於切換分支或者恢復工作目錄內容。

//切換到blinky分支(記得換成你自己的分支名)

git checkout blinky

//切換到tag: v1.0.0(記得換成你自己的tag)

git checkout v1.0.0

//切換到commit e56e8a(這是一個對象的ID,即hash值,記得換成你自己的)

git checkout e56e8a

//創建分支blinky並切換到此分支(記得換成你自己的分支名)

git checkout -b blinky

//把index區的內容覆蓋working tree

git checkout .

//把index區的src/main.c覆蓋working tree裏面的src/main.c(記得換成你自己的文件)

git checkout -- src/main.c

5.15 git merge

git merge用於合併開發歷史。

//將遠程跟蹤分支origin/main合併到當前分支

git merge origin/main

//將遠程跟蹤分支origin/ master合併到當前分支

git merge origin/master

//將blinky分支合併到當前分支(記得換成你自己的分支名)

git merge blinky

//手動解決衝突後,繼續剛才有衝突的merge,完成merge過程

git merge --continue

//中止剛才有衝突的merge

git merge --abort

5.16 git rebase

git rebase用於將當前分支分叉後的所有commit重新提交在新分支的末端。

//將當前分支分叉後的所有commit重新提交在main分支的末端

git rebase main

//將當前分支分叉後的所有commit重新提交在master分支的末端

git rebase master

//手動解決衝突後繼續rebase操作以完成整個操作

git rebase --continue

//跳過本commit繼續rebase過程

git rebase --skip

//中止rebase過程

git rebase --abort

5.17 git remote

git remote用於管理遠程跟蹤倉庫。

//查看當前所有遠程跟蹤倉庫

git remote -v

//顯示遠程倉庫origin的信息

git remote show origin

//將//github.com/aiminhua/git_demo.git(記得換成自己的git服務器地址)設為遠程跟蹤倉庫,並命名為origin

git remote add origin https://github.com/aiminhua/git_demo.git

//刪除遠程跟蹤倉庫origin

git remote remove origin

5.18 git push

git push將本地內容推送到遠程服務器。

//將當前分支推送到遠程origin倉庫的main分支

git push -u origin main

//將當前分支推送到遠程origin倉庫的master分支

git push -u origin master

//將當前分支推送到遠程origin倉庫的blinky分支(記得換成你自己的分支名)

git push -u origin blinky

//推送tag:v1.0.0到遠程服務器(記得換成你自己的tag)

git push origin v1.0.0

//推送所有跟蹤分支到遠程服務器

git push

//刪除遠程服務器的blinky分支(記得換成你自己的分支名)

git push origin --delete blinky

5.19 git fetch

git fetch用於下載遠程倉庫更新到本地。

//下載origin倉庫的更新到本地clone區

git fetch origin

//下載所有關聯的遠程倉庫

git fetch

//下載遠程倉庫的pull request#2518(記得換成你自己的pull request編號)到本地分支:smp_ota(記得換成你自己的分支名)。

git fetch origin pull/2518/head:smp_ota

5.20 git pull

git pull用於集成遠程分支更新到本地分支,是git fetch和git rebase/merge的合體。

//將當前分支rebase到遠程跟蹤分支origin/main

git pull --rebase origin main

//將當前分支rebase到遠程跟蹤分支origin/ master

git pull --rebase origin master

//將遠程origin/main分支merge到當前分支

git pull origin main

//將遠程origin/master分支merge到當前分支

git pull origin master

//取所有遠程分支,並將當前分支的upstream分支merge過來

git pull

//下載遠程倉庫的pull request#2518(記得換成你自己的pull request編號)到本地分支:smp_ota(記得換成你自己的分支名)

git pull origin pull/2518/head:smp_ota

//通過瀏覽器打開git pull的幫助頁面

git pull --help

5.21 git revert

git revert用於覆蓋最近幾次的提交,即把歷史中的一次提交重新在分支頂端提交一次。

//把前一次提交再重新提交一次,相當於前一次提交覆蓋了當前提交

git revert HEAD~1

//把commit:fb863c(這是一個對象的ID,即hash值,記得換成你自己的)重新提交在分支最頂端

git revert fb863c

5.22 git restore

git restore用於恢復工作目錄或者index區裏面的文件。

//將index中的src/main.c內容恢復到工作區的src/main.c(記得換成你自己的文件)

git restore src/main.c

//恢復index中的README.rst至HEAD狀態(記得換成你自己的文件)

git restore --staged README.rst

//將tag:v1.0.0(記得換成你自己的tag)中的src/main.c內容恢復到工作區的src/main.c(記得換成你自己的文件)

git restore --source=v1.0.0 src/main.c

5.23 git reflog

git reflog用於管理引用日誌信息,包括多個子命令,默認git reflog等價於git reflog show。

//顯示HEAD的所有更新操作日誌,包括那些刪除了或者不可達的commit

git reflog

//顯示main分支的所有更新操作日誌,包括那些刪除了或者不可達的commit

git reflog main

//顯示master分支的所有更新操作日誌,包括那些刪除了或者不可達的commit

git reflog master

5.24 git stash

git stash用於將working tree的修改暫時壓棧。

//將working tree裏面的修改壓入stash棧

git stash

//將stash棧中的內容彈出到工作區

git stash pop

//顯示stash棧裏面的內容

git stash show

5.25 git submodule

git submodule用於初始化,更新和查看submodule。

//把//github.com/aiminhua/ncs_samples.git(記得換成自己的git服務器地址)添加為本倉庫的子模塊並放在ncs(記得換成自己的目錄)目錄下

git submodule add https://github.com/aiminhua/ncs_samples.git ncs

//從服務器下載submodule的更新,如果沒有初始化,則對其先進行初始化

git submodule update --init

//反初始化ncs(記得換成自己的目錄)這個submodule

git submodule deinit ncs

//通過瀏覽器打開git submodule的幫助頁面

git submodule --help

5.26 git subtree

git subtree用於包含第三方庫到主工程中,這個第三方庫的歷史記錄成了主工程的一部分。

//包含//gitee.com/minhua_ai/ncs_samples.git(記得換成自己的git服務器地址)為當前倉庫下的一個子庫,所有代碼放在ncs(記得換成自己的目錄)目錄下,–squash表示只把最後一條提交記錄加在主庫的歷史記錄中

git subtree add --prefix ncs https://gitee.com/minhua_ai/ncs_samples.git master --squash

//更新這個subtree子庫,參數說明同上

git subtree pull --prefix ncs https://gitee.com/minhua_ai/ncs_samples.git master --squash

//通過瀏覽器打開git subtree的幫助頁面

git subtree --help

5.27 git cherry-pick

git cherry-pick用於merge指定的commit到當前分支。

//將提交b03a94fa9245abf41538028450a9c675067d6e4f(這是一個對象的ID,即hash值,記得換成你自己的)的修改merge到當前分支

git cherry-pick b03a94fa9245abf41538028450a9c675067d6e4f

5.28 git grep

git grep用於使用正則表達式搜索整個倉庫。

//在工作目錄中搜索包含”blinky”關鍵字的文件(記得換成自己的關鍵字)

git grep "blinky"

//通過瀏覽器打開git grep的幫助頁面

git grep --help

5.29 git apply

git apply用於把補丁(diff文件)應用在當前工作目錄,該工作目錄可以不關聯一個git倉庫。

//把patch.diff(記得換成你自己的文件)文件應用在當前目錄及子目錄,當前目錄以外的忽略。註:diff文件的生成請見git diff命令說明

git apply patch.diff

//通過瀏覽器打開git apply的幫助頁面

git apply --help

5.30 git cat-file

git cat-file用於查看倉庫對象的內容。

//查看fb863cc95ca4a80fe64e51addd1db6a3910ee69e(這是一個對象的ID,即hash值,記得換成你自己的)這個對象的內容

git cat-file -p fb863cc95ca4a80fe64e51addd1db6a3910ee69e

5.31 git ls-files

git ls-files用於查看index或者工作目錄中的文件。

//列出index中的所有文件

git ls-files -s

//列出工作目錄中的所有文件

git ls-files

5.32 git merge-file

git merge-file只merge一個文件,這個文件可以不關聯git倉庫。

//把other.c文件相對於base.c文件的diff輸出到current.c文件(記得換成你自己的文件)

git merge-file current.c base.c other.c

//通過瀏覽器打開git merge-file的幫助頁面

git merge-file --help

5.33 git gc

git gc用於清理git倉庫不需要的文件以及對Git倉庫進行壓縮優化,一般不需要你自己去跑這條指令,系統自動會去做git gc操作。

git gc

 

6. Git重要術語列表

下面是Git重要術語列表,他們都直接摘自Git官網,原文鏈接為://git-scm.com/docs/gitglossary#def_parent

branch

A “branch” is a line of development. The most recent commit on a branch is referred to as the tip of that branch. The tip of the branch is referenced by a branch head, which moves forward as additional development is done on the branch. A single Git repository can track an arbitrary number of branches, but your working tree is associated with just one of them (the “current” or “checked out” branch), and HEAD points to that branch.

checkout

The action of updating all or part of the working tree with a tree object or blob from the object database, and updating the index and HEAD if the whole working tree has been pointed at a new branch.

cherry-picking

In SCM jargon, “cherry pick” means to choose a subset of changes out of a series of changes (typically commits) and record them as a new series of changes on top of a different codebase. In Git, this is performed by the “git cherry-pick” command to extract the change introduced by an existing commit and to record it based on the tip of the current branch as a new commit.

commit

As a noun: A single point in the Git history; the entire history of a project is represented as a set of interrelated commits. The word “commit” is often used by Git in the same places other revision control systems use the words “revision” or “version”. Also used as a short hand for commit object.

As a verb: The action of storing a new snapshot of the project’s state in the Git history, by creating a new commit representing the current state of the index and advancing HEAD to point at the new commit.

core Git

Fundamental data structures and utilities of Git. Exposes only limited source code management tools.

detached HEAD

Normally the HEAD stores the name of a branch, and commands that operate on the history HEAD represents operate on the history leading to the tip of the branch the HEAD points at. However, Git also allows you to check out an arbitrary commit that isn’t necessarily the tip of any particular branch. The HEAD in such a state is called “detached”.

Note that commands that operate on the history of the current branch (e.g. git commit to build a new history on top of it) still work while the HEAD is detached. They update the HEAD to point at the tip of the updated history without affecting any branch. Commands that update or inquire information about the current branch (e.g. git branch –set-upstream-to that sets what remote-tracking branch the current branch integrates with) obviously do not work, as there is no (real) current branch to ask about in this state.

fast-forward

A fast-forward is a special type of merge where you have a revision and you are “merging” another branch‘s changes that happen to be a descendant of what you have. In such a case, you do not make a new merge commit but instead just update your branch to point at the same revision as the branch you are merging. This will happen frequently on a remote-tracking branch of a remote repository.

fetch

Fetching a branch means to get the branch’s head ref from a remote repository, to find out which objects are missing from the local object database, and to get them, too. See also git-fetch[1].

hash

In Git’s context, synonym for object name.

head

A named reference to the commit at the tip of a branch. Heads are stored in a file in $GIT_DIR/refs/heads/ directory, except when using packed refs. (See git-pack-refs[1].)

HEAD

The current branch. In more detail: Your working tree is normally derived from the state of the tree referred to by HEAD. HEAD is a reference to one of the heads in your repository, except when using a detached HEAD, in which case it directly references an arbitrary commit.

head ref

A synonym for head.

index

A collection of files with stat information, whose contents are stored as objects. The index is a stored version of your working tree. Truth be told, it can also contain a second, and even a third version of a working tree, which are used when merging.

index entry

The information regarding a particular file, stored in the index. An index entry can be unmerged, if a merge was started, but not yet finished (i.e. if the index contains multiple versions of that file).

master

The default development branch. Whenever you create a Git repository, a branch named “master” is created, and becomes the active branch. In most cases, this contains the local development, though that is purely by convention and is not required.

merge

As a verb: To bring the contents of another branch (possibly from an external repository) into the current branch. In the case where the merged-in branch is from a different repository, this is done by first fetching the remote branch and then merging the result into the current branch. This combination of fetch and merge operations is called a pull. Merging is performed by an automatic process that identifies changes made since the branches diverged, and then applies all those changes together. In cases where changes conflict, manual intervention may be required to complete the merge.

As a noun: unless it is a fast-forward, a successful merge results in the creation of a new commit representing the result of the merge, and having as parents the tips of the merged branches. This commit is referred to as a “merge commit”, or sometimes just a “merge”.

object

The unit of storage in Git. It is uniquely identified by the SHA-1 of its contents. Consequently, an object cannot be changed.

object database

Stores a set of “objects”, and an individual object is identified by its object name. The objects usually live in $GIT_DIR/objects/.

object identifier

Synonym for object name.

object name

The unique identifier of an object. The object name is usually represented by a 40 character hexadecimal string. Also colloquially called SHA-1.

object type

One of the identifiers “commit“, “tree“, “tag” or “blob” describing the type of an object.

index

A collection of files with stat information, whose contents are stored as objects. The index is a stored version of your working tree. Truth be told, it can also contain a second, and even a third version of a working tree, which are used when merging.

index entry

The information regarding a particular file, stored in the index. An index entry can be unmerged, if a merge was started, but not yet finished (i.e. if the index contains multiple versions of that file).

master

The default development branch. Whenever you create a Git repository, a branch named “master” is created, and becomes the active branch. In most cases, this contains the local development, though that is purely by convention and is not required.

merge

As a verb: To bring the contents of another branch (possibly from an external repository) into the current branch. In the case where the merged-in branch is from a different repository, this is done by first fetching the remote branch and then merging the result into the current branch. This combination of fetch and merge operations is called a pull. Merging is performed by an automatic process that identifies changes made since the branches diverged, and then applies all those changes together. In cases where changes conflict, manual intervention may be required to complete the merge.

As a noun: unless it is a fast-forward, a successful merge results in the creation of a new commit representing the result of the merge, and having as parents the tips of the merged branches. This commit is referred to as a “merge commit”, or sometimes just a “merge”.

object

The unit of storage in Git. It is uniquely identified by the SHA-1 of its contents. Consequently, an object cannot be changed.

object database

Stores a set of “objects”, and an individual object is identified by its object name. The objects usually live in $GIT_DIR/objects/.

object identifier

Synonym for object name.

object name

The unique identifier of an object. The object name is usually represented by a 40 character hexadecimal string. Also colloquially called SHA-1.

object type

One of the identifiers “commit“, “tree“, “tag” or “blob” describing the type of an object.

octopus

To merge more than two branches.

origin

The default upstream repository. Most projects have at least one upstream project which they track. By default origin is used for that purpose. New upstream updates will be fetched into remote-tracking branches named origin/name-of-upstream-branch, which you can see using git branch -r.

overlay

Only update and add files to the working directory, but don’t delete them, similar to how cp -R would update the contents in the destination directory. This is the default mode in a checkout when checking out files from the index or a tree-ish. In contrast, no-overlay mode also deletes tracked files not present in the source, similar to rsync –delete.

pack

A set of objects which have been compressed into one file (to save space or to transmit them efficiently).

pack index

The list of identifiers, and other information, of the objects in a pack, to assist in efficiently accessing the contents of a pack.

plumbing

Cute name for core Git.

porcelain

Cute name for programs and program suites depending on core Git, presenting a high level access to core Git. Porcelains expose more of a SCM interface than the plumbing.

pull

Pulling a branch means to fetch it and merge it. See also git-pull[1].

push

Pushing a branch means to get the branch’s head ref from a remote repository, find out if it is an ancestor to the branch’s local head ref, and in that case, putting all objects, which are reachable from the local head ref, and which are missing from the remote repository, into the remote object database, and updating the remote head ref. If the remote head is not an ancestor to the local head, the push fails.

rebase

To reapply a series of changes from a branch to a different base, and reset the head of that branch to the result.

ref

A name that begins with refs/ (e.g. refs/heads/master) that points to an object name or another ref (the latter is called a symbolic ref). For convenience, a ref can sometimes be abbreviated when used as an argument to a Git command; see gitrevisions[7] for details. Refs are stored in the repository.

The ref namespace is hierarchical. Different subhierarchies are used for different purposes (e.g. the refs/heads/ hierarchy is used to represent local branches).

There are a few special-purpose refs that do not begin with refs/. The most notable example is HEAD.

reflog

A reflog shows the local “history” of a ref. In other words, it can tell you what the 3rd last revision in this repository was, and what was the current state in this repository, yesterday 9:14pm. See git-reflog[1] for details.

refspec

A “refspec” is used by fetch and push to describe the mapping between remote ref and local ref.

remote repository

A repository which is used to track the same project but resides somewhere else. To communicate with remotes, see fetch or push.

remote-tracking branch

A ref that is used to follow changes from another repository. It typically looks like refs/remotes/foo/bar (indicating that it tracks a branch named bar in a remote named foo), and matches the right-hand-side of a configured fetch refspec. A remote-tracking branch should not contain direct modifications or have local commits made to it.

repository

A collection of refs together with an object database containing all objects which are reachable from the refs, possibly accompanied by meta data from one or more porcelains. A repository can share an object database with other repositories via alternates mechanism.

resolve

The action of fixing up manually what a failed automatic merge left behind.

revision

Synonym for commit (the noun).

SCM

Source code management (tool).

SHA-1

“Secure Hash Algorithm 1”; a cryptographic hash function. In the context of Git used as a synonym for object name.

shallow repository

A shallow repository has an incomplete history some of whose commits have parents cauterized away (in other words, Git is told to pretend that these commits do not have the parents, even though they are recorded in the commit object). This is sometimes useful when you are interested only in the recent history of a project even though the real history recorded in the upstream is much larger. A shallow repository is created by giving the –depth option to git-clone[1], and its history can be later deepened with git-fetch[1].

stash entry

An object used to temporarily store the contents of a dirty working directory and the index for future reuse.

submodule

A repository that holds the history of a separate project inside another repository (the latter of which is called superproject).

superproject

A repository that references repositories of other projects in its working tree as submodules. The superproject knows about the names of (but does not hold copies of) commit objects of the contained submodules.

tag

A ref under refs/tags/ namespace that points to an object of an arbitrary type (typically a tag points to either a tag or a commit object). In contrast to a head, a tag is not updated by the commit command. A Git tag has nothing to do with a Lisp tag (which would be called an object type in Git’s context). A tag is most typically used to mark a particular point in the commit ancestry chain.

tag object

An object containing a ref pointing to another object, which can contain a message just like a commit object. It can also contain a (PGP) signature, in which case it is called a “signed tag object”.

tree

Either a working tree, or a tree object together with the dependent blob and tree objects (i.e. a stored representation of a working tree).

tree object

An object containing a list of file names and modes along with refs to the associated blob and/or tree objects. A tree is equivalent to a directory.

unmerged index

An index which contains unmerged index entries.

unreachable object

An object which is not reachable from a branch, tag, or any other reference.

upstream branch

The default branch that is merged into the branch in question (or the branch in question is rebased onto). It is configured via branch.<name>.remote and branch.<name>.merge. If the upstream branch of A is origin/B sometimes we say “A is tracking origin/B“.

working tree

The tree of actual checked out files. The working tree normally contains the contents of the HEAD commit’s tree, plus any local changes that you have made but not yet committed.