本篇提要:git的核心理念、結(jié)構(gòu)、代碼庫的創(chuàng)建和Merge
從開始使用git至今看過無數(shù)文章,大多時候是為了解決一個特定問題,尋求一個特定的解決方案,偶爾也會看一些新手向的教程。對于前者,可能需要你具備一定的git能力,這無可厚非。但大多新手向的教程主要鋪陳git的常用命令,看的時候覺得自己懂了,但實際使用時就會問題重重。
磕磕絆絆至今,我對git也算是有了一些了解,當然都是使用向而不是原理向。在此希望能夠以我自己理解的方式,描述我在使用git時的一些心得,希望能夠幫助一些新手或者常常被git困擾的伙伴。
git的核心理念和結(jié)構(gòu)
五花八門的命令是使用git的手段,并不是git的核心。在我看來,與其掌握100條命令,不如先大致了解一下git的思想和大體架構(gòu)。從使用者角度出發(fā),git的目的就是為了方便我們隨時找到需要的代碼并進行處理,而我們常用的大多數(shù)命令,都是為了這一點服務(wù)的,確切的來說這一點就是一條commit。
從結(jié)構(gòu)來說,git主要有工作區(qū)、暫存區(qū)和版本庫三大區(qū)域。工作區(qū)就是我們寫代碼的地方,git會追蹤你干了什么,并告訴你你的操作和版本庫有哪些不同。暫存區(qū)就是對你現(xiàn)在的工作拍照,也就是所謂快照,繼續(xù)在工作區(qū)操作,暫存區(qū)還是會停留在拍照那一刻,就像手機截屏一樣。版本庫就是當你覺得暫存區(qū)的代碼沒問題之后,就可以入庫了,這時候git就真正把你所做的事變成了一條有價值的commit。
使git變得錯綜復(fù)雜的概念是分支,但分支又是必不可少的,很多時候你不得不放下手頭的工作,先去處理其他的問題,處理完后再回來繼續(xù)工作。這種情況下,必須要保證的是被中斷的工作就停留在它剛才的狀態(tài),否則現(xiàn)有的工作就無法繼續(xù)下去了。分支看起來就像一個副本(雖然git肯定不是這么實現(xiàn)的,但我們還是可以簡單的理解它就是復(fù)制了一次),確保了在某個分支上操作,不會影響到其他分支的內(nèi)容。
以上就是我們?nèi)粘J褂胓it的全部了,在工作區(qū)工作,到某個節(jié)點拍照存到暫存區(qū),確認無誤后合并到版本庫。偶爾來個突發(fā)事件,停下手頭的工作到另一個分支處理,結(jié)束后回來繼續(xù)工作。不過這都是概念性的,相當于定義了游戲規(guī)則,接下來就是看git給我們提供了哪些手段來完成此事。
建模,讓git更有趣
在正式操作git命令之前,我覺得對概念建模能對我們理解git提供有效幫助。我們把git看做一片竹林,我是一個園丁,我和其他的園丁一起照料這片林子,只不過這里的每根竹子要生長一節(jié)都是我們園丁說了算,而不是大地(看起來我們都是上帝?)。而我們每個人的工作,就是決定什么時間讓竹子長一節(jié),記錄我們工作的工具是分配給每個人的一臺電腦。
我們的模型就是這么簡單,一堆人一起“造”竹子,接下來跟隨著時間的腳步看下我們的模型如何運作吧。(PS:看起來怪怪的,莫不是我們開了一個竹子蠟像館?問題是能吸引來哪怕一個游客嗎?不管那么多了,老板的想法不是我們普通工人應(yīng)該琢磨的,有錢拿就行了。。。)
git命令和模型結(jié)合,推進時間線
第一天:項目的建立和代碼合并(Merge)
1. 項目啟動儀式
一開始我們很窮,除了錢什么都沒有。要想造竹子,得先有地盤吧,所以我們就拿錢買了塊地,它大概是

好了,不開玩笑,它大概這么大:

打開你的命令行,輸入以下字母,你就得到了一樣大的土地:
mkdir first_git
cd ./first_git/
git init
當你看到這樣的輸出,就成功了:

這里看到了 master,這是git默認給我們創(chuàng)建的分支,也叫主分支,畢竟git需要在某個分支工作。給我們的模型挖一個坑,稍大一點,以后就是我們的明星產(chǎn)品了。

我(飛機醬)和我的小伙伴(姑且叫他路人丙)深得老板器重,所以這塊地目前全部屬于我們。雖說我們只有兩個人,但也要衡量誰做的多做的好,發(fā)給每個人的電腦就是用來記錄我們每天都做了什么,最后每個人的成果還要匯總一下交給老板。所以還需要一臺公共的電腦用來匯總我們的信息,把我們各自的工作整理后發(fā)到這臺電腦上,最終整合成美觀的結(jié)果報告等待老板查閱。既然這臺電腦已經(jīng)開了頭,就把它充公吧。
事情要由老板牽頭,所以老板在這臺電腦上簽了名,并寫下以下內(nèi)容:“竹林001號項目啟動”。接下來就什么都不管了...
使用git完成老板的操作可不簡單,你得依次做以下幾個事情:
git config --global user.name "boss"
git config --global user.email "boss@git.com"
以上就是老板簽名的步驟,--global 是說這個電腦上的全部項目都是老板的,操作者也是他自己,而且他也沒必要每次開一個項目都再次簽名認證了(這樣一來,路人丙如果想搗亂,老板一定會發(fā)現(xiàn)他)。
接下來老板新建了一個README.txt并將內(nèi)容竹林001號項目啟動寫進去保存了起來
echo 竹林001號項目啟動 > README.txt
寫好了開場白,還得按照規(guī)矩把它存到版本庫里,不然事情就白做了。先用 git status 看看現(xiàn)在版本庫的狀態(tài):

那個紅色的 README.txt 就是我們的工作區(qū),git發(fā)現(xiàn)了工作區(qū)改動,但是暫存區(qū)里沒有它,版本庫里也沒有。既然用git工具,就務(wù)必按照規(guī)則來,先把工作區(qū)拍照存到暫存區(qū),再進入版本庫:
// 添加到暫存區(qū)
git add README.txt
再看看狀態(tài),發(fā)現(xiàn)紅色變成綠色了,這時候就進了暫存區(qū):

最后,終于可以入庫了,我們的項目也算正式啟動。
// 并入版本庫
git commit -m "老板寫下了啟動標語"
-m 后邊加的內(nèi)容表示這次做了什么,方便以后查閱。執(zhí)行完成之后,我們就擁有了第一個commit,也就是第一個有價值的節(jié)點。

使用 git log 可以看看我們做過什么:

commit 后邊那個長長的字符串,它是這次commit的唯一標識,后邊我們會明白它的意義。
現(xiàn)在,我們有了第一節(jié)竹子(畫圖不好,勿吐槽):

2. 開工,各自為戰(zhàn)
老板只在公共的電腦上寫下了標語,我們自己的電腦還沒有呢,總不能讓老板挨個寫吧?(老板:……)所以我們要有個辦法把公共電腦的內(nèi)容搞過來,這就是git clone。我們只要在自己的電腦上,執(zhí)行 git clone + 版本庫的路徑,就能把代碼同步過來,我這里只能在同一個電腦操作,所以用倉庫名稱進行 clone(同時在源版本庫執(zhí)行git config receive.denyCurrentBranch warn):

可以看到老板修改的內(nèi)容已經(jīng)過來了。路人丙也執(zhí)行了同樣的操作,這樣就可以各做各的。為了知道是誰做的工作,我們還分別設(shè)置了自己的名字,這樣以后的每個commit都會攜帶作者(路人丙干了壞事,我們看看 git log 就能抓到他?。H绻挥幸粋€項目,可以不加--global。現(xiàn)在我們的土地上有了三根一樣的竹子:

一切準備就緒,接下來就可以開始干活了,飛機醬和路人丙都在全力造竹子,并記錄到自己的電腦里,此時公共電腦并不知道他們干了什么。第一天因為沒有經(jīng)驗,工作中出現(xiàn)了多次失誤,飛機醬只造好了一竹節(jié),路人丙好一些,造好了兩節(jié)。所以他們的竹子是這樣的:

在他們自己的版本庫里,都在 work_01.txt文件里記錄了自己的工作,并保存到了版本庫:


下班后,飛機醬把記錄同步到了公共電腦上,然后開心的回家了。他是這么操作的:
git push origin master:master

origin是遠程版本庫名稱,雖然我們沒有配置它,但它默認就是origin,第一個master是飛機醬電腦上使用的分支,第二個master是遠程版本庫的分支。(飛機醬可能提前補課了,命令寫的很全)
接下來到路人丙同步了,他也使用了類似的操作,但是卻發(fā)生了意外:

一樣的操作,結(jié)果卻迥異,路人丙的內(nèi)心一定是凌亂的,但他還認識幾個單詞,大致明白了提示的意思。原來飛機醬在他前面進行了push操作,導致他這里文件不夠新,git就拒絕了他。所以他按照提示中 (e.g., 'git pull ...') before pushing again. 又試了一次。結(jié)果在git pull時再次發(fā)生意外:

conflict的意思是沖突,conflict出現(xiàn)在對同一個文件的同一行進行了不同修改的時候。我們記得飛機醬和路人丙都創(chuàng)建了 work_01.txt文件并寫入了內(nèi)容,所以git合并時發(fā)現(xiàn)第一行都寫入了內(nèi)容,就沒法決定讓誰的修改在前,就會要求我們手動解決?,F(xiàn)在我們打開路人丙的 work_01.txt,看看它的內(nèi)容:

可以看到,兩人輸入的內(nèi)容用 <<<<HEAD...====...>>>>{commitId}這樣的格式包裹起來了。前面是路人丙的內(nèi)容,后邊是飛機醬的內(nèi)容。想要合并到遠程版本庫,就一定要先解決掉這個問題,否則老板看到就要發(fā)火了~所以路人丙改了它,并把飛機醬的內(nèi)容放在了下邊(老板肯定會先看到前面的內(nèi)容):

因為又發(fā)生了修改,所以內(nèi)容現(xiàn)在還在工作區(qū),就需要再次把它加到暫存區(qū),再入庫。一通操作后,總算合并成功了:

現(xiàn)在讓我們更新一下當前模型的狀態(tài),路人丙為了解決沖突增加了一個commit,這時候公共電腦的log看起來是這樣的:

是不是理所當然的認為竹子長這樣呢:

看起來沒有問題,而且git log顯示也是如此。但是這里有個問題,大家都是今天干完的活,都要把自己的竹子安到明星產(chǎn)品上,憑什么你先放?路人丙的工作又快又好,難道不是更有資格先放嗎?路人丙認為把兩個人的竹子擰在一起,最后再用灰色的那節(jié)整合在一起最公平了,老板一眼就明白我們是同時完成的工作:

看起來很奇怪吧?好好的竹子因為互相爭搶鼓了一個包,看起來十分不美觀(話說這樣老板不會生氣嗎?還是可能會吸引來奇怪的旅客?)。
實際上git pull操作是由兩個命令組成的,第一個是git fetch,表示把遠端版本庫的內(nèi)容拉取到本地,第二步才是執(zhí)行git merge操作把拉取下來的新內(nèi)容和我們自己的內(nèi)容合并在一起。既然都想占第一個位置,干脆git幫你整合一下,然后用一個新的commit代替就好了。要想證明事實上的確如此,使用上面的 git log 是不行的,你要在后邊加上 --pretty=raw:
git log --pretty=raw

commit較多,但最后一條顯示不全的是老板的提交,并不影響我們分析它。我們主要看每條commit的parent字段,可以發(fā)現(xiàn)最上方的那一條有兩個parent,分別指向了飛機醬的第一條和路人丙的第二條,而被指向的這兩條數(shù)據(jù)的parent都是老板的那次提交,這和我們預(yù)期的完全一致。
3. 老板的怒火,推倒重來
解決了問題,路人丙很開心。關(guān)掉電腦,背上背包,正準備回去美餐一頓,老板卻過來視察工作了,先是夸了一頓路人丙工作努力,轉(zhuǎn)眼看到造好的竹子,怒發(fā)沖冠,責令路人丙馬上把問題解決掉!
這問題難不倒他,既然能把竹子合起來,再拆開豈不是相當容易,于是他拿起那臺公共電腦,輸入了以下命令:
// reset --merge 重置merge,后邊跟上之前的commitId,就可以回到過去
git reset --merge 2b0218d74edbea391c8b7853580c99164af8d32a
再看看日志,完蛋,紙飛機的記錄不見了,被路人丙的記錄取而代之!

這下可怎么好,明天飛機醬來了不好交代呀,而且把別人的工作整丟也不符合道義,有后悔藥吃就好了。這時候一邊的老板看不下去了,拿起電腦飛快的敲了一條指令:
git reset --hard HEAD@{1}
然后把電腦交回路人丙,丟下一句:好了,后悔藥吃下去了。(路人丙:我@#$*&^^,發(fā)生了什么事?)再一看,果然又回到剛才的狀態(tài)了,原來老板是大神呀,可惜太冷漠,苦事還得我來做。
檢查一下剛才的 git reset xxx ,后邊跟了路人丙的commitId,結(jié)果就剩下了他的工作,這里一共兩條路,指到飛機醬那邊是不是就好了呢,再試一次,反正有老板在隨時能吃后悔藥(話說這么大膽的么)。
git reset --merge ba65055dab9700d0e814719bbf23713c163ade76

果然是,我路人丙就是個天才,哈哈哈!
等等,這不是飛機醬離開時的狀態(tài)嗎,剛剛的路行不通,我該怎么辦好呢?回想一下剛剛好像是因為沖突才多了一個commit,那如果我不讓它沖突豈不就好了?這工作記錄都寫一個文件里看著也有些亂,我再換個文件也許就好了呢。路人丙用求助的眼神看著老板,意思是希望老板能把他自己的工作記錄刪一下,他已經(jīng)有了好的idea想再試一下。接下來又是老板炫技的時間,不過這次是在路人丙的電腦上:
git log
// 重置到老板自己的commitId
git reset --hard 420e8fe77141a4f6c9cfed2518c65b04d21bcec0
于是路人丙的工作全不見了,一切仿佛回到了剛開始。

這次路人丙給文件起了一個不一樣的名字 lurenbing_01.txt,又把工作記錄寫了進去,再次進行提交:

果然還是需要先pull,再pull一下看看結(jié)果:

這次確實沒有了沖突,但是彈出了一個框,內(nèi)容說明這是一次merge,先不管它,直接下一步,再看git log --pretty=raw:

為什么,都不在一起了還是這樣,路人丙都要崩潰了。一定是剛剛pull的問題,我要是先pull,再寫我的工作記錄是不是就好了。再次讓老板幫忙重置后,路人丙先pull了一次:

這下飛機醬的工作記錄都到我這了,我再寫應(yīng)該沒問題了吧,一頓操作下來,這一次順利的合并了進去,既沒有沖突,也沒有彈出框。到公共電腦上一看,很好很順利:

時間太晚,工作也馬馬虎虎算搞定了,老板打算放過路人丙,但臨走前問了一個令路人丙瞬間凌亂的問題:如果每次都要先pull再開始寫你自己的commit,那你每天都要等飛機醬下班后才開始工作?(路人丙:那我每天豈不是比飛機醬多工作一倍的時間?明天我要找他研究研究,這實在不是人干的事啊~)
最后更新一下我們的模型,可以看到路人丙的竹節(jié)最后還是放在了上邊,所以干的又快又好有什么用呢,快要快在關(guān)鍵時刻。

第一天的工作就到此結(jié)束了,可以看到,只要路人丙希望兩個人的工作能夠平等的占據(jù)第一的位置,就一定會形成鼓包,通過一個新竹節(jié)把這兩條路歸一。而如果他自愿把工作放在后邊,一切就很順暢,有時候真的是退一步海闊天空啊。
我是飛機醬,如果您喜歡我的文章,可以關(guān)注我~
編程之路,道阻且長。唯,路漫漫其修遠兮,吾將上下而求索。