什麼是分散式版本控制系統?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重要術語列表
當我們開發軟體的時候,會建立很多原始碼檔案,這些原始碼檔案一般會放在一個目錄裡面,這個就是我們的code base,對這些原始碼檔案,我們每天都在迭代開發,因此需要對其進行管理,這樣我們就能知道這些原始碼檔案的歷史,比如前天改了什麼,今天又改了什麼。一個人自己寫程式碼對軟體開發管理感覺還不深,如果是多個人一起開發一個專案,這個時候怎麼保證每個人的修改別人可以獲取到而又不影響到其他人的程式碼,這就需要軟體版本管理工具或者軟體,Git就是這樣一款軟體。
Git是一款分散式版本控制系統(Distributed Version Control System),它可以用來追蹤任何檔案的修改,協調多個開發者之間的工作,Git具有速度快,資料完整性校驗,分散式開發,以及支援非線性工作流等眾多特點。Git最初由Linus Torvalds創作(Linus Torvalds還創作了大名鼎鼎的Linux作業系統,我們後面會對Linus進行一個簡單介紹,以示對大神的敬意),它是一款開源的免費的GPL2.0版權的軟體。Git的官網地址是:https://git-scm.com/,scm表示source code management,跟前面提及的版本控制系統一個意思。
前面說過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評論:想想我們一生,想做的事情太多了,不過到最後,我們大部分人回顧自己一生:生於哪年,卒於哪年,妻或者夫是誰,小孩是誰,僅此而已)
以上純粹是節外生枝,下面迴歸正題。
現在我們開始建立自己的第一個Git倉庫。
首先前往Git官網下載Git工具:https://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不熟也沒關係的,你就把它當成一個目錄下包含了幾個檔案,僅此而已。這個工程程式碼獲取連結為:https://github.com/zephyrproject-rtos/zephyr/tree/main/samples/hello_world,將其下載下來(大家可以一個檔案一個檔案複製貼上下來),放在自己的目錄下:(注:如果GitHub存取不了,請多試幾次。如果還是不行,請存取https://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 "[email protected]" 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程式程式碼獲取連結為:https://github.com/zephyrproject-rtos/zephyr/tree/main/samples/basic/blinky(注:如果GitHub存取不了,請多試幾次。如果還是不行,請存取https://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工作原理進行說明。
如果你對Git不是很熟,建議你按照第2章"2. 建立第一個Git倉庫(Repository)"先實際操作一遍,然後再看本章,效果會更好。
Git是一個分散式版本控制系統(distributed version control system),這意味著每一個開發者或者電腦都擁有一個本地倉庫,而且每個倉庫(包含伺服器上的那個倉庫)關係同等,即每個倉庫都可以獨立工作,同步之後,每個倉庫都擁有所有倉庫的修改歷史記錄。跟分散式版本控制系統相對的是集中式版本控制系統(central version control system),比如SVN(Subversion)就是集中式版本控制系統,集中式版本控制系統對伺服器依賴比較高,伺服器一旦出問題,版本歷史就會丟失,當多個人同時修改一個檔案的時候,還需要檔案鎖機制。根據最新的統計結果,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區,起到一個緩衝的作用。
如大家熟悉的,對一般使用者來說,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包含的主要物件如下所示:
除了上述資料物件(他們都是儲存在.git/objects中),Git還會儲存參照(references,簡寫refs),refs就是對commit的參照,用來找到需要的commit,refs儲存在.git/refs目錄,主要有如下參照型別:
如前所述,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的內部物件非常有幫助。
最初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命令進行統一說明。
從前面描述可知,我們可以隨時隨地建立一個新的branch(分支),然後新分支又可以引出其他分支,最後我們又可以把新分支merge(合併,後面會講述它的含義)到main分支。隨著開發的不斷髮展,branch也將不斷髮展,這裡面就有一個git的工作流問題,即該建哪些分支?這些分支該如何演變?有人總結了如下的branch模型(原文連結為:https://geekiam.io/how-to-use-git-flow/),可供大家參考。
上圖每一個圓點就是一次commit,每一條橫線表示一個分支,總共有6個分支:main,hotfixes,release,develop,feature和feature',main分支用來發布,開發主要在develop分支,每次增添新功能的時候,另起一個feature分支,在把develop分支推播到main之前,先合併到release分支。從上面的分支演化圖,我們可以看出,分支可以相互參照,錯綜複雜,但從根本上來說,我們的開發從main分支開始,也從main分支結束。
我們現在在第2章Git工程的基礎上,繼續演示Git的一些高階應用。
我們首先檢視一下分支情況,並切換到主分支:
這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。
為了下面演示另一種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。
git checkout,git revert和git reset這三者的區別示意圖如下所示:
如果你一直按照我們的步驟來操作,那麼前面的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三者內容都不一樣的段落,即是存在衝突的段落,這個就需要使用者手工去調整和修改。
我們現在把這個工程編譯一下(注:沒有安裝Zephyr編譯系統的,大家可以只觀看,不用實際操作),編譯成功後,我們會有如下build目錄:
此時git status輸出也會顯示我們新增了這個build目錄:
這個build目錄裡面的內容都是編譯系統自動生成,我們去追蹤他是毫無意義的,更重要的是,一旦追蹤了這個build目錄,那麼每次執行git diff或者git status,你都將得到一個長長的diff表,無用資訊將掩蓋有用資訊,閱讀特別費勁。為此需要引入.gitignore檔案,忽略某些特定檔案或者目錄,我們可以直接把zephyr工程裡面的.gitignore檔案拷到我們工程:https://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"
我們在第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輸出紀錄檔如下所示:
我們現在進入開發者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完成了共同作業開發。
假設我們有如下分支,即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分支。
現在我們回到開發者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工程中參照https://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演示就結束了。
前面提過,我們還可以使用git subtree來包含第三方模組。git submodule是參照第三方模組,第三方模組是一個獨立的git 倉庫,跟主倉庫完全剝離。git subtree則是包含第三方模組,第三方模組程式碼是主倉庫的一部分,對第三方模組的修改會全部體現在主倉庫的歷史記錄中,使用git subtree推播第三方模組,第三方模組程式碼會完整clone到伺服器上。
我們現在把https://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
到目前為止,我們的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上進行修改。
GitHub有很多非常好的開原始碼,但是目前國記憶體取GitHub有點慢,當這個GitHub倉庫比較大的時候,直接使用git clone去下載它,經常會報超時等各種莫名其妙的錯誤。此時我們可以把這個GitHub倉庫匯入到Gitee,由Gitee通過云云對接的方式去clone(這個過程其實就是建立倉庫的映象),然後我們再從Gitee伺服器把映象倉庫clone下來。下面我們將演示如何把我們剛剛創立的倉庫:https://github.com/aiminhua/git_demo匯入到gitee。
首先確保你有gitee賬號,然後登入,選擇如下選單選項:
然後輸入GitHub倉庫地址:https://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:https://github.com/aiminhua/ncs_samples匯入到Gitee,成功後,我們得到它的clone地址:https://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上的第三方倉庫內容一模一樣而已。
首先說明一下,pull request跟git pull兩者完全不搭架,屬於兩個不同的範疇。Pull request是針對GitHub/Gitee等程式碼託管平臺來說的,在這些程式碼託管平臺,有很多開源的倉庫,你會使用他們,使用過程中,你會發現這些開原始碼的問題,然後你自己把這些問題給fix了,由於你不是這些開源倉庫的開發者,你沒法使用git push直接把自己的fix推播到這些開源倉庫,這個問題怎麼解決?這個就是pull request要做的事情。
Pull request(簡寫為PR)用於把自己的修改merge到第三方倉庫,其步驟如下所示:
1. 先fork第三方倉庫到自己的賬號下面,下面是一個把https://github.com/nrfconnect/sdk-zephyr倉庫fork到我賬號的範例。
Fork成功後,介面如下所示:
可以看出,sdk-zephyr成了我自己的一個倉庫了,既然它現在是我的倉庫,我當然可以對他做任何修改了。這種情況下,我們把原始的https://github.com/nrfconnect/sdk-zephyr稱為https://github.com/aiminhua/sdk-zephyr(我自己的倉庫)的upstream倉庫。
2. clone https://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成功。
注意:PR建立的時候是在自己的fork下,成功後跑到上游倉庫,最終它是屬於上游倉庫的PR,而不是你自己的PR,尋找PR必須去上游倉庫尋找哦。
這裡面還有一種特殊情況:就是你的upstream倉庫還有上一級upstream倉庫,這個時候你就要分清此fix是隻適用於直接的upstream倉庫,還是更上一級的upstream也適用,如果最最上級的upstream也適用的話,你應該把PR開在它那裡,比如上面例子,https://github.com/nrfconnect/sdk-zephyr其實也是一個fork,它的upstream是:https://github.com/zephyrproject-rtos/zephyr,而且上面例子的fix是適用最原始倉庫的:https://github.com/zephyrproject-rtos/zephyr,這個時候上面的PR應該直接開在https://github.com/zephyrproject-rtos/zephyr這下面,為此我們重複上面的過程(完全再來一次,而且跟上面的PR沒有任何關係),最終PR的樣子是下面這樣的:https://github.com/zephyrproject-rtos/zephyr/pull/45537。
不管是Git軟體本身,還是Git官網,都提供了非常詳盡的Git命令使用說明,Git工作原理等等有關Git的一切。雖然我這篇部落格非常長,但其主要目的還是幫助大家建立Git的常識,熟悉Git的一些基本用法,瞭解Git的基本工作原理,有了這些基礎後,大家再去讀Git命令使用說明或者其他Git文章,就容易得多。說到底,Git參考檔案還是以Git官方為準,我們只是為大家做了一個橋樑。
Git官方參考檔案為:https://git-scm.com/docs,裡面列出了Git所有命令及其詳盡說明。實際上,大家也不需要去存取網站,Git軟體本身就自帶說明檔案,大家直接檢視本地說明檔案即可。比如git diff這個命令,我們輸入
git diff --help
自動跳出如下手冊頁面:
裡面詳細列出了git diff內涵,使用說明,使用注意事項,舉例等等,可以說,應有盡有,事無鉅細,包羅萬象,不過這帶來了一個副作用:太詳細了反而讓讀者抓不到精髓,這也是為什麼市面上有那麼多的Git教學的原因。筆者這篇部落格希望能幫助大家理解git的幫助檔案,最終達到一個境界:只需看Git官方幫助檔案,就能解決碰到的所有Git問題,而無需再去網上搜尋第三方的參考資料。
最後推薦一本Git專業書籍:Pro Git,整本書可以從這裡獲取:https://git-scm.com/book/en/v2。Pro Git是一本非常經典的介紹Git的書,我也是在寫作本文的過程中無意發現了這本書,這本書不管是廣度還是深度,都是可圈可點的,而且通俗易懂,深入淺出,非常適合Git學習和鑽研。
介紹Git常用命令之前,我再次把下面這張Git命令操作區域關係圖貼出來,這是一張來自wikipedia的圖,非常直觀地表達了Git常用命令的操作物件。
下面都是一些常用Git命令,建議大家都去了解一下。
git config用於設定git軟體的一些引數。
//設定郵箱和使用者名稱(記得換成你自己的郵箱和使用者名稱)
git config --global user.email "[email protected]" 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
git init用來建立倉庫。
//在當前目錄建立git倉庫
git init
//在新目錄work/repo(記得換成你自己的目錄哦)下建立git倉庫
git init work/repo
git clone用於將遠端倉庫克隆到本地。
//克隆https://gitee.com/minhua_ai/ncs_samples.git(記得換成你自己的git伺服器地址)倉庫到本地
git clone https://gitee.com/minhua_ai/ncs_samples.git
//遞迴克隆遠端倉庫https://github.com/aiminhua/git_demo.git(記得換成你自己的git伺服器地址),這個遠端倉庫包含submodule
git clone https://github.com/aiminhua/git_demo.git --recursive
git add用於把工作目錄更新新增到index區(stage區)。
//把工作目錄所有修改新增到index
git add .
//把指定檔案main.c(記得換成你自己的指定檔案哦)新增到index
git add main.c
//通過瀏覽器開啟git add的幫助頁面
git add --help
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-
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
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
git status用於檢視當前狀態。
//檢視working directory哪些檔案修改了
git status
//通過瀏覽器開啟git status的幫助頁面
git status --help
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裡面的檔案,請直接使用作業系統的刪除命令
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
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
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
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
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
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
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
git remote用於管理遠端跟蹤倉庫。
//檢視當前所有遠端跟蹤倉庫
git remote -v
//顯示遠端倉庫origin的資訊
git remote show origin
//將https://github.com/aiminhua/git_demo.git(記得換成自己的git伺服器地址)設為遠端跟蹤倉庫,並命名為origin
git remote add origin https://github.com/aiminhua/git_demo.git
//刪除遠端跟蹤倉庫origin
git remote remove origin
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
git fetch用於下載遠端倉庫更新到本地。
//下載origin倉庫的更新到本地clone區
git fetch origin
//下載所有關聯的遠端倉庫
git fetch
//下載遠端倉庫的pull request#2518(記得換成你自己的pull request編號)到本地分支:smp_ota(記得換成你自己的分支名)。
git fetch origin pull/2518/head:smp_ota
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
git revert用於覆蓋最近幾次的提交,即把歷史中的一次提交重新在分支頂端提交一次。
//把前一次提交再重新提交一次,相當於前一次提交覆蓋了當前提交
git revert HEAD~1
//把commit:fb863c(這是一個物件的ID,即hash值,記得換成你自己的)重新提交在分支最頂端
git revert fb863c
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
git reflog用於管理參照紀錄檔資訊,包括多個子命令,預設git reflog等價於git reflog show。
//顯示HEAD的所有更新操作紀錄檔,包括那些刪除了或者不可達的commit
git reflog
//顯示main分支的所有更新操作紀錄檔,包括那些刪除了或者不可達的commit
git reflog main
//顯示master分支的所有更新操作紀錄檔,包括那些刪除了或者不可達的commit
git reflog master
git stash用於將working tree的修改暫時壓棧。
//將working tree裡面的修改壓入stash棧
git stash
//將stash棧中的內容彈出到工作區
git stash pop
//顯示stash棧裡面的內容
git stash show
git submodule用於初始化,更新和檢視submodule。
//把https://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
git subtree用於包含第三方庫到主工程中,這個第三方庫的歷史記錄成了主工程的一部分。
//包含https://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
git cherry-pick用於merge指定的commit到當前分支。
//將提交b03a94fa9245abf41538028450a9c675067d6e4f(這是一個物件的ID,即hash值,記得換成你自己的)的修改merge到當前分支
git cherry-pick b03a94fa9245abf41538028450a9c675067d6e4f
git grep用於使用正規表示式搜尋整個倉庫。
//在工作目錄中搜尋包含"blinky"關鍵字的檔案(記得換成自己的關鍵字)
git grep "blinky"
//通過瀏覽器開啟git grep的幫助頁面
git grep --help
git apply用於把修補程式(diff檔案)應用在當前工作目錄,該工作目錄可以不關聯一個git倉庫。
//把patch.diff(記得換成你自己的檔案)檔案應用在當前目錄及子目錄,當前目錄以外的忽略。注:diff檔案的生成請見git diff命令說明
git apply patch.diff
//通過瀏覽器開啟git apply的幫助頁面
git apply --help
git cat-file用於檢視倉庫物件的內容。
//檢視fb863cc95ca4a80fe64e51addd1db6a3910ee69e(這是一個物件的ID,即hash值,記得換成你自己的)這個物件的內容
git cat-file -p fb863cc95ca4a80fe64e51addd1db6a3910ee69e
git ls-files用於檢視index或者工作目錄中的檔案。
//列出index中的所有檔案
git ls-files -s
//列出工作目錄中的所有檔案
git ls-files
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
git gc用於清理git倉庫不需要的檔案以及對Git倉庫進行壓縮優化,一般不需要你自己去跑這條指令,系統自動會去做git gc操作。
git gc
下面是Git重要術語列表,他們都直接摘自Git官網,原文連結為:https://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.