Git 全功能介紹
Git 歷史和現(xiàn)狀
Git 是 Linux 作者 Linus 的另一個(gè)作品。2002 年他還在使用 Bitkeeper 作為 Linux 內(nèi)核的版本管理,但因?yàn)樗?Copyright 有版權(quán)的軟件備受質(zhì)疑,然后 Andrew Tridgell 對(duì) Bitkeeper 進(jìn)行逆向工程,導(dǎo)致 BitMover 要回收 Linux 開(kāi)發(fā)者的 Bitkeeper 的免費(fèi)使用權(quán),Linus 一怒之下花了 10 天寫(xiě)出了 Git。
名字的意思是:egotistical bastard
如今 Git 已經(jīng)成為絕大多數(shù)開(kāi)發(fā)者的選擇, Tom Preston-Werner、Chris Wanstrath 和 PJ Hyett 在 2007 年 10 月推出的 Github 已經(jīng)成為了全球最大的開(kāi)發(fā)者網(wǎng)站,我們廠在上面也是貢獻(xiàn)頗多。
Tencent at Github
更有甚者,一向自己造輪子的的微軟,也打算把巨達(dá) 300G 的 Windows 源代碼遷移到 Git 上進(jìn)行管理,他們?yōu)?Git 提供了新的 GVFS 實(shí)現(xiàn),有效地改善了 Git 對(duì)巨大代碼倉(cāng)庫(kù)的性能。
Microsoft will migrate windows source code to git
另外說(shuō)一句:Docker 的二進(jìn)制 image 管理,也是基于 git 實(shí)現(xiàn)的。
集中式版本管理和分布式版本管理
Git 和 SVN 是從設(shè)計(jì)理念上就不一樣的版本工具,SVN 將代碼進(jìn)行中心化管理,擁有更好的穩(wěn)定性和安全性,但是去中心化的 Git 卻是從 Linux 操作系統(tǒng)的開(kāi)發(fā)需求而來(lái),更加適合多人協(xié)作的開(kāi)源項(xiàng)目,可以以任何一個(gè)點(diǎn)為 remote 將他的代碼與本地代碼合并,隨著時(shí)間發(fā)展,還衍生出了更多強(qiáng)大功能和一整套操縱流程,讓它也可以適應(yīng)了商業(yè)軟件的開(kāi)發(fā)。
Central and distribution
Git 和 SVN 代碼歷史的不同
SVN 的代碼歷史相對(duì)比較簡(jiǎn)單,因?yàn)樗侵行幕?,所有人的代碼都直接提交到某個(gè) repository 上,所以它的 Reversion ID 號(hào)是一個(gè)按順序增加的數(shù)字類(lèi)型,一般情況下不能在兩個(gè)數(shù)字之間插入別的 reversion。
SVN History
Git 的看起來(lái)就是雜亂多了,它的 Reversion ID 號(hào)是一個(gè) 40 位長(zhǎng)度的 hash 值,通常也可以縮寫(xiě)為 7 位,這樣做的原因是因?yàn)?Git 的最小單位是代碼修改的歷史,即為補(bǔ)丁 Patch,而分支、 Tag、Remote(一會(huì)兒會(huì)說(shuō)到這些概念)等都只是分支的集合,互相之間可以隨意拆分、合并。
我更愿意把分支、Tag、Remote 想象成不同的平行宇宙,因?yàn)槟承C(jī)緣導(dǎo)致產(chǎn)生了分裂,走向了不同的歷史,也可能因?yàn)槟承C(jī)緣又合并到了一起,變得更加強(qiáng)大。
Git history
Git 基礎(chǔ)命令
Git sences
Git 按照?qǐng)鼍翱梢苑譃橐韵聢?chǎng)景(Scence):
- Workspace:當(dāng)前工作區(qū),修改的的最初狀態(tài)。
- Staging:修改后,添加到準(zhǔn)備提交的緩存狀態(tài)。
- Local repository:本地的代碼倉(cāng)庫(kù),只對(duì)自己的代碼生效。這也是和 svn 區(qū)別之一,svn commit 之后就直接提交到遠(yuǎn)程服務(wù)器了,git commit 之后只是到本地代碼庫(kù)。
- Remote repository:遠(yuǎn)程代碼庫(kù),將自己的本地代碼庫(kù)同步到遠(yuǎn)程代碼庫(kù)上,這樣可以供別的開(kāi)發(fā)者分享自己的成果。
具體流程看圖即可,下面對(duì)幾個(gè)常用命令進(jìn)行簡(jiǎn)單介紹
PS: 圖中沒(méi)有提到 rebase 和 cherry-pick 命令,這兩個(gè)命令也非常強(qiáng)大,后面有提到,有時(shí)間可以關(guān)注一下。
補(bǔ)丁 diff
之前有提到過(guò),補(bǔ)丁是 Git/SVN 代碼版本管理的基礎(chǔ)概念,它其實(shí)是以行為單位的文件修改歷史,增加行以 + 號(hào)開(kāi)頭 ,刪除行以 - 號(hào)開(kāi)頭,而修改一行,就是先 - 后 +。
在 Git 里可以通過(guò) git diff 或者 Linux/Mac/Conemu 中,也可以通過(guò) diff -Naur 來(lái)生成文件對(duì)比結(jié)果,有點(diǎn)類(lèi)似下圖。
這是整個(gè)代碼管理的基礎(chǔ)概念,所有的分支、Tag、Remote 都是在此基礎(chǔ)上衍生的。
Git diff
基本流程
1. 克隆代碼到本地開(kāi)發(fā)環(huán)境 - Clone
$ git clone [REPOSITORY_URL]
對(duì)應(yīng)到了 svn checkout 命令,用于把遠(yuǎn)程代碼克隆到本地,跟 svn 一樣,REPOSITORY_URL 的協(xié)議非常靈活,之前流行用 ssh/scp 協(xié)議,但現(xiàn)在 https/http 協(xié)議漸漸流行起來(lái)了。
2. 更新代碼 - Status/Commit/Log
剛有提到過(guò) Git 的四種場(chǎng)景,其中前兩種場(chǎng)景需要通過(guò)
$ git status
命令查看,代碼剛剛新建可以看到是 Untracked files(Workspace) 狀態(tài),執(zhí)行 add 之后變成 Changes to be commited(Stage) 狀態(tài),修改了 Stage 中的文件,又會(huì)變成 Changes not staged for commit 狀態(tài)。執(zhí)行 commit 之后就從 Stage 中轉(zhuǎn)移到了 Local repository 中,可以通過(guò)
$ git log
查看到代碼提交。
3. Branch 和 Tag
如剛從所說(shuō),Branch 和 Tag 都可以看成是補(bǔ)丁的時(shí)序化集合,branch 可以互相合并,在 clone 完 repository 后有一個(gè)主線分支叫做 master。而 Tag 用于發(fā)布后標(biāo)記版本,這兩個(gè)只是從名字上不一樣,功能(我感覺(jué)實(shí)現(xiàn)上)并沒(méi)有太大區(qū)別。
和 SVN 不同, SVN 的 Branch 和 Tag 都是把 Trunk 整個(gè)代碼庫(kù)拷貝出來(lái),Git 只是將補(bǔ)丁引用重新對(duì)當(dāng)前代碼應(yīng)用一下,所以 Git 的 Branch/Tag 都非常輕量,切換起來(lái)非常輕松,使用 Git 要盡量多使用它的分支來(lái)提高開(kāi)發(fā)效率,一會(huì)兒提到 Git flow 時(shí)會(huì)描述一下如果用分支進(jìn)行代碼功能開(kāi)發(fā)管理。
3.1 新建分支的兩種辦法
$ git checkout -b [BRANCH_NAME] # 在當(dāng)前版本切換并新建分支
$ git branch [BRANCH_NAME] # 直接新建分支
3.2 切換分支
$ git checkout [BRANCH_NAME]
3.3 合并分支的兩種辦法
$ git merge [BRANCH_NAME] # 將另外一個(gè)分支的代碼,打到當(dāng)前分支之后。
$ git rebase [BRANCH_NAME] # 不推薦,對(duì)代碼進(jìn)行比較,將本分支修改后的代碼打到另外一個(gè)分支之后
rebase 通常情況下不推薦使用,因?yàn)?rebase 完下游分支,再?gòu)纳嫌畏种?merge 的時(shí)候會(huì)丟失分支合并的 commit,但是對(duì)于部分有 history mysophobia 的人來(lái)說(shuō),它是保持代碼提交歷史記錄干凈的神器,那個(gè) Merge branch 'xxx' of http://git.code.oa.com/xxx into yyy 的 commit 看起來(lái)也挺討厭的。
對(duì)于已經(jīng)推到 remote repository 的 commit,是不建議 rebase 的,因?yàn)橐坏?rebase 了,別人再 pull 就會(huì)出一大堆的沖突 conflict,而且基本沒(méi)法修,通常情況下還是建議用 merge 穩(wěn)妥一些。
PS: rebase 還有一個(gè)強(qiáng)大的功能是配合 --interactive 參數(shù)修改之前的補(bǔ)丁,具體自己查一下,但是改完了 push --force 上去,別人再 pull 回來(lái)出 conflict 是必然的,但是這招是修改 Github 上的 Pull request(后面有提到)必備技能。
3.4 刪除分支
$ git branch -d [BRANCH_NAME] # 已經(jīng)合并到 master
$ git branch -D [BRANCH_NAME] # 該分支未合并到 master,強(qiáng)制刪除
PS: 即使刪除了分支等,也可以用 git reflogs 找回來(lái)喔
3.5 取消修改
git stash # 取消全部修改,很強(qiáng)大的是它可以恢復(fù)過(guò)來(lái),具體自己查一下
git reset —soft [REV] # 保留修改內(nèi)容,從 Local repository 中撤銷(xiāo),也可以用于回退歷史記錄
git reset —hard [REV] # 丟掉修改內(nèi)容,從 Local repository 中撤銷(xiāo),也可以用于回退歷史記錄
推送本地代碼到遠(yuǎn)程倉(cāng)庫(kù)
推送代碼是為了跟別人一起合作,命令行非常簡(jiǎn)單
$ git push [REMOTE] [BRANCH]
remote 默認(rèn)為 origin,如果不填的話就推送到它上面,branch 默認(rèn)為當(dāng)前分支,其實(shí)可以不加,加了就把指定的分支推送到遠(yuǎn)程了。如果要將推送本地功能分支,建議 push 后面加上 —set-upstream 參數(shù)。
鄭重警告:永遠(yuǎn)不要對(duì)主線 master 分支執(zhí)行 —force
獲取遠(yuǎn)程分支更新
$ git pull # 把代碼更新到 workspace
$ git fetch # 把代碼更新到 Local repository,可能需要通過(guò) merge 再合并到 worksapce 一次。
遠(yuǎn)程倉(cāng)庫(kù)
Clone 之后會(huì)有一個(gè)默認(rèn)的遠(yuǎn)程倉(cāng)庫(kù)為 origin,但如果還要增加別的遠(yuǎn)程倉(cāng)庫(kù),就需要用到下面命令了:
$ git remote add [REMOTE_NAME] [URL] # 添加原創(chuàng)倉(cāng)庫(kù)
$ git fetch [REMOTE_NAME] # 獲取遠(yuǎn)程倉(cāng)庫(kù)更新
$ git branch -a # 查看包括遠(yuǎn)程倉(cāng)庫(kù)以內(nèi)的所有分支
$ git push [REMOTE_NAME] [BRANCH_NAME] # 推送到遠(yuǎn)程倉(cāng)庫(kù)
Github Pull Request & Gitlab Merge Request
Github 在 Git Remote 的基礎(chǔ)上為了方便大家參與開(kāi)源項(xiàng)目,衍生出的一套機(jī)制,目前常規(guī)開(kāi)源項(xiàng)目的參與流程是,先注冊(cè)一個(gè) Github 賬號(hào),然后將感興趣的開(kāi)源項(xiàng)目 Fork 一份到自己的 namespace 下,然后拆分分支進(jìn)行修改,然后提交到自己的 Github repository 下,再發(fā)起一個(gè) Pull Request,讓項(xiàng)目維護(hù)者來(lái)合并你的代碼(Pull request 名副其實(shí)),在這個(gè)過(guò)程中,項(xiàng)目維護(hù)者會(huì)對(duì)你的代碼進(jìn)行 Review 和點(diǎn)評(píng),你得按照維護(hù)者要求進(jìn)行修改(這里 rebase 會(huì)用得很勤),修改通過(guò),維護(hù)者同意后,就有他將代碼合并進(jìn)項(xiàng)目中。
具體流程自己走一圈就明白了,Gitlab 的 Merge Request 原理是一模一樣的。
PS: 范例圖片在 PPT 的第 22 頁(yè)起
Git flow
Git flow 本來(lái)應(yīng)該是本文的重點(diǎn)內(nèi)容的,它是在 Git branch 的基礎(chǔ)上實(shí)現(xiàn)了一套簡(jiǎn)單的功能模塊化開(kāi)發(fā)流程,主要思想是把分支分成了上下游幾個(gè)層級(jí),然后通過(guò)一套命令行工具進(jìn)行維護(hù)。
- master 分支 - 與線上版本保持一致,當(dāng)發(fā)生線上問(wèn)題后可以很輕松地修復(fù)。
- develop 分支 - 功能開(kāi)發(fā)基線分支,功能開(kāi)發(fā)完之后合并到上面,所有功能開(kāi)發(fā)完畢后經(jīng)過(guò)測(cè)試上線,然后合并回 master。
- feature/* 功能開(kāi)發(fā)分支 - 從 develop 上拆分,也需要隨時(shí)把 develop 的更新 merge 回來(lái),跟上游的分支保持一致,等功能開(kāi)發(fā)完畢之后,即可合并到 develop。通過(guò)和上游分支保持一致,這樣可以避免對(duì)誤刪別人的代碼,所有代碼沖突必須在下游分支修好,測(cè)試完畢后才可合并到上游分支。
- hotfix/* 熱修復(fù)分支 - 主要用于線上 bug 修復(fù),但是修復(fù)后應(yīng)該同時(shí)合并到 master 和 develop 兩個(gè)分支上。
Git flow
然后 git flow 提供了套命令行工具來(lái)更加輕松地去做這些代碼合并的事情。
$ git flow init # 初始化 git flow 分支模型
$ git flow feature start [NAME] # 開(kāi)始一個(gè)功能分支
$ git flow feature finish [NAME] # 將功能分支合并進(jìn) develop
$ git flow hotfix start [NAME] # 開(kāi)始一個(gè)熱修復(fù)分支
$ git flow hotfix finish [NAME] # 將補(bǔ)丁合并進(jìn) develop 和 master
$ git flow release [NAME] # 發(fā)布一個(gè)新版本,打 tag
感覺(jué) Git flow 得有個(gè)篇幅,下次有機(jī)會(huì)再來(lái)詳述。
其它內(nèi)容
有興趣可以繼續(xù)看一下別的相關(guān)內(nèi)容,非常有意思:
- git svn - Git 可以以 svn 為代碼后端,通過(guò) Giit 來(lái)對(duì) SVN 里的代碼進(jìn)行版本管理。
- git reflogs - 引用記錄,在 git 中誤刪除某提交是不用害怕的,只要 commit 了,就可以通過(guò) reflogs 找到,用 reset 或者 cherry pick 恢復(fù)。
- git cherry pick - 摘櫻桃(commit),從另一個(gè)分支中單獨(dú)將某個(gè) patch 摘回來(lái)。
- git bare repository - 建立 Git 服務(wù)器
- git submodule - 子模塊,一個(gè)大項(xiàng)目可以通過(guò) submodule 進(jìn)行拆分,可以隨時(shí)進(jìn)行子模塊的版本更新和回溯。
- git hooks - 鉤子,當(dāng) git 在 repository 上發(fā)生某種行為的時(shí)候,可以通過(guò)鉤子觸發(fā)一些行為,像目前 Github 上的一些第三方持續(xù)集成服務(wù),就是在此基礎(chǔ)上實(shí)現(xiàn)的。
- git signature - 簽名,通過(guò) gpg 在 commit 時(shí)對(duì) patch 進(jìn)行簽名,證明那個(gè)補(bǔ)丁確實(shí)是自己提交的,可以參考一下 Github 的文檔。
- Source Tree - Source Tree 是一套跨平臺(tái)的 Git 圖形界面,簡(jiǎn)單方便,我目前主要用它進(jìn)行基本的 Patch 閱讀,比命令行更加舒服。