Git是一個(gè)分布式的版本控制系統(tǒng)。
注意上面的”分布式的“這個(gè)限定詞,這一點(diǎn)是Git和CVS,VSN等其他版本控制系統(tǒng)最大的分別。
集中式版本控制系統(tǒng)最大的毛病就是必須聯(lián)網(wǎng)才能夠工作,也就是各個(gè)客戶端必須連接到中央倉庫才能夠工作。
分布式的版本控制系統(tǒng)中,不必有中央倉庫,每個(gè)人的電腦上都有一個(gè)完整的版本庫。我們可以在自己的電腦上修改、提交。如果兩個(gè)人需要交換修改的代碼,則需要將自己的修改推送給對(duì)方。
在實(shí)際的應(yīng)用中,為了方便一個(gè)團(tuán)隊(duì)之間交換代碼修改,我們還是有一個(gè)中央倉庫。注意這個(gè)倉庫是提供便利的,并不是沒有它就不行(雖然會(huì)很不方便)。
這個(gè)教程的主要目的是讓大家了解使用Git進(jìn)行版本管理的一些基本操作和邏輯,讓大家對(duì)Git的使用有一個(gè)基本的了解。如果想要詳細(xì)了解每個(gè)命令的用法,或者要了解一些“高級(jí)”的命令,建議大家還是去看看Git的手冊(cè)。
2. 創(chuàng)建本地倉庫
關(guān)于Git的安裝,這里就不講了,網(wǎng)上有很多文章,大家根據(jù)自己的系統(tǒng),找一篇文章照著做就可以了。這里主要講解Git的一些概念和操作。
前面已經(jīng)說過,Git是一個(gè)分布式的版本控制系統(tǒng),每個(gè)人的電腦上都有一個(gè)完整的版本庫,我們把這個(gè)版本庫稱為本地倉庫。所有其他的倉庫,無論是同事電腦上的,還是中央服務(wù)器的,我們都稱為遠(yuǎn)程倉庫。
創(chuàng)建本地倉庫有兩種方式,第一種是我們從零開始創(chuàng)建,第二種是從遠(yuǎn)程倉庫克隆一份。
我們可以把版本倉庫理解為一個(gè)目錄,其中保存了我們所有讓git管理的文件。Git能夠跟蹤這些文件的修改、刪除,能夠追蹤這些文件的歷史,并且可以還原。
2.1 從頭開始創(chuàng)建本地倉庫
從頭開始創(chuàng)建本地倉庫非常簡單。
- 選擇你想要?jiǎng)?chuàng)建版本倉庫的地方,創(chuàng)建一個(gè)空的目錄。
- 進(jìn)入該目錄,在命令行中執(zhí)行g(shù)it init命令,把這個(gè)目錄變成Git可以管理的版本倉庫。
- 把文件添加到Git倉庫中,一共需要三步:
- 把文件放到Git倉庫的目錄中,無論是新創(chuàng)建一個(gè)文件,還是從其他地方copy一個(gè)文件。也可以放在子目錄中
- 運(yùn)行
git add <filepath>
,告訴Git把文件添加到倉庫中。 - 使用
git commit
命令,告訴Git把改變提交到倉庫。
2.2 從遠(yuǎn)程倉庫克隆
相比從頭開始創(chuàng)建本地倉庫,我們做得更多的是從遠(yuǎn)程倉庫克隆一份。理論上來說遠(yuǎn)程倉庫可以是任何一臺(tái)電腦上的倉庫,但是通常來說,我們會(huì)在github,或自己搭建的git服務(wù)器上創(chuàng)建中央倉庫,團(tuán)隊(duì)中的所有成員都從該倉庫上克隆,并在后續(xù)將自己的修改上傳上去,或者從中央倉庫讀取其他同事的修改。因此我們這樣只講怎樣中央倉庫克隆到本地倉庫。
- 創(chuàng)建本機(jī)的ssh-key,在類linux系統(tǒng)上,只需要運(yùn)行ssh-keygen命令就可以了。
- 將ssh-key的公鑰id_rsa.pub上傳到github或者Git服務(wù)器上,具體的步驟要參考你們是要的Git服務(wù)器是哪一個(gè)。
- 執(zhí)行命令
git clone <版本庫的網(wǎng)址> [本地目錄名]
來創(chuàng)建本地倉庫。如果不指定目錄,則會(huì)在當(dāng)前目錄下創(chuàng)建一個(gè)目錄,名字與遠(yuǎn)程版本庫的名字相同。
上面的命令中,版本庫的網(wǎng)址中是可以帶上協(xié)議、用戶名等信息的。Git支持http(s),ssh,git,ftp(s),本地文件協(xié)議等不同的協(xié)議。
3 使用本地倉庫進(jìn)行版本管理
3.1 工作區(qū)和暫存區(qū)
為了能夠更好的使用本地倉庫進(jìn)行版本管理,我們先了解一下工作區(qū)和暫存區(qū)的概念。
無論我們是從遠(yuǎn)程倉庫克隆,還是使用git init
命令新建一個(gè)本地倉庫,最終在本地倉庫的目錄中,都有一個(gè)隱藏的子目錄.git
。
本地倉庫目錄是我們的工作區(qū),我們要管理的文件都在這個(gè)目錄中。但是.git
目錄不是工作區(qū),這個(gè)是版本庫。我們不要手動(dòng)去修改這個(gè)目錄中的內(nèi)容。
在這個(gè)版本庫中,保存了很多的東西,其中最重要的是暫存區(qū),以及分支。關(guān)于分支的概念,我們以后再解釋。目前來說,我們也只有一個(gè)分支,也就是Git自動(dòng)為我們創(chuàng)建的master分區(qū)。還有一個(gè)指向master分區(qū)的指針,叫做HEAD。
前面我們已經(jīng)提到過,在添加一個(gè)文件到Git本地倉庫中的時(shí)候,需要先執(zhí)行git add
命令,然后執(zhí)行git commit
命令。其中git add
命令就是將變化的代碼從工作區(qū)拷貝到暫存區(qū)。 git commit
則是將代碼從暫存區(qū)提交到版本庫。一旦提交后,暫存區(qū)會(huì)被清空。
所以,我們其實(shí)可以執(zhí)行多次git add
之后,執(zhí)行一次git commit
,一次性的將多次修改的結(jié)果進(jìn)行提交。
可以使用git status來查看有哪些修改沒有add到暫存區(qū),有哪些修改在暫存區(qū)中,沒有提交到版本庫。
3.2 提交版本和查看歷史
我們修改了一個(gè)或多個(gè)文件之后,可以使用git add
和git commit
命令將修改提交到版本庫。在使用git commit
命令時(shí),可以使用-m選項(xiàng)來添加備注。該命令會(huì)得到類似下面的返回:
1 2 3
| $ git commit -m "Fixed the type mistake in readme.txt" [master 0db896a] Fixed the type mistake in readme.txt 1 file changed, 1 insertion(+), 1 deletion(-)
|
這個(gè)命令返回的信息中,包含了提交的分支,本次提交自動(dòng)生成的版本號(hào)(commit id),輸入的備注,以及修改內(nèi)容的統(tǒng)計(jì)信息。 注意這里的版本號(hào)可能不是完整的版本號(hào),而只是版本號(hào)的前幾位。
我們可以看到,Git的版本號(hào)是一個(gè)用十六進(jìn)制表示的隨機(jī)數(shù)字,這是為了避免各自在本地?cái)?shù)據(jù)庫中提交時(shí)版本號(hào)的沖突。
Git會(huì)記錄下我們所有的版本歷史,可以使用git log
命令來查看。
3.3 版本回退
版本回退是版本管理系統(tǒng)最基本的功能。如果不能回退,要這系統(tǒng)何用。
要回退,首先需要知道回退到哪個(gè)版本。可以使用git log
命令來查看版本歷史信息。然后使用git rest --hard <版本號(hào)>
來進(jìn)行回退。這里的版本號(hào)不需要輸全,只需要輸入前面幾位,能夠唯一確認(rèn)一個(gè)版本就可以了。
使用git log
的時(shí)候,我們看到的會(huì)是一堆密密麻麻的信息。如果我們使用圖形化的Git工具,就能夠很直觀的看到Git是將所有的提交按照時(shí)間順序串成了一條線,在這個(gè)時(shí)間線上每一個(gè)點(diǎn)就是一次提交,每一次提交都有很多的信息,比如提交人是誰,版本號(hào)是多少,備注信息是什么。
這下就體現(xiàn)了提交的時(shí)候使用備注的好處了,我們可以通過備注知道每一次修改的原因和內(nèi)容,這樣才知道需要回退到哪個(gè)版本。
如果我們從今天的版本回退到了昨天的版本,還能不能回到今天的版本呢?可以的,但是前提要是你還記得今天的版本的版本號(hào)。如果我們前面的窗口沒有關(guān)閉,可以從git log
命令的輸出中找到今天的版本號(hào)。如果已經(jīng)關(guān)閉了,則可以重新使用git reflog
命令查看我們執(zhí)行的每一條命令。這樣就可以使用git rest
恢復(fù)到今天的版本了。
3.4 管理修改
前面講過了工作區(qū)和暫存區(qū)。因此Git相比其他的版本管理系統(tǒng)多了一層。這一層有什么作用呢?
我們?cè)谔峤坏臅r(shí)候,提交的是暫存區(qū)中的內(nèi)容,而不是工作區(qū)中的內(nèi)容。因此,我們?cè)谛薷牡臅r(shí)候,可以多次將中間代碼添加到暫存區(qū)。我們既不需要產(chǎn)生大量的中間版本號(hào)和提交記錄,也可以保證不會(huì)因?yàn)楹罄m(xù)的修改弄丟了前面的代碼。我們可以大膽的試錯(cuò),發(fā)現(xiàn)不合適了,很容易回滾到前一個(gè)版本。
我們可以使用git checkout -- <file>
來撤銷工作區(qū)的修改。這個(gè)時(shí)候,如果暫存區(qū)中有還沒有提交的修改,那么會(huì)使用暫存區(qū)中的內(nèi)容覆蓋工作區(qū)中的內(nèi)容。如果暫存區(qū)中的修改都已經(jīng)提交了,那么會(huì)用版本庫中的內(nèi)容覆蓋工作區(qū)中的內(nèi)容。 注意--
不能省略,省略了就變成切換分支的命令了。
當(dāng)然,刪除文件也是一種修改。一般我們把文件從Git的目錄中刪除之后,Git會(huì)檢測(cè)到,并且使用git status
命令能夠看到。這個(gè)時(shí)候,如果確實(shí)要?jiǎng)h除文件,可以使用git rm <file>
來告訴暫存區(qū)我們要?jiǎng)h除這文件,然后用git commit
命令提交修改,從版本庫中刪除文件。
如果是誤刪除的,可以使用git checkout -- <file>
命令從版本庫中恢復(fù)文件。
4 分支管理
前面我們提到了,從git log
可以看到我們所有的提交的歷史記錄,并且按照先后順序串成了一條線,這條線就是一個(gè)“分支”。只不過我們現(xiàn)在只有一個(gè)分支,就是創(chuàng)建本地倉庫時(shí),默認(rèn)為我們創(chuàng)建一個(gè)master分支,通常稱為主分支。
這個(gè)分支目前的情況如下面的示意圖:
分支允許我們創(chuàng)建另外一條”路徑“,同時(shí)管理兩個(gè)版本的代碼。例如,我們完成了1.0版本,進(jìn)入2.0版本的開發(fā)。但是我們同時(shí)需要進(jìn)行1.0版本的維護(hù),修復(fù)bug。這個(gè)時(shí)候,我們就可以同時(shí)維護(hù)1.0分支和2.0分支。
4.1 創(chuàng)建與合并分支
分支就是提交記錄組成的一個(gè)時(shí)間線,因此一個(gè)分支可以表示如下:

分支master指向該分支中的最后一次提交。然后Git還會(huì)用HEAD指向master,表明當(dāng)前的分支是master分支。每次提交,master都會(huì)向前移動(dòng)一步,一直指向最新的提交,master分支的時(shí)間線也越來越長。

如果我們需要?jiǎng)?chuàng)建一個(gè)新的分支,例如從當(dāng)前最新提交創(chuàng)建一個(gè)dev分支,那么其實(shí)只是創(chuàng)建了一個(gè)名為dev的指針,指向最新的提交,同時(shí)將HEAD修改為指向dev。使用的命令是git branch dev
和 git checkout dev
,得到的結(jié)果如下圖所示:

因此,在Git中創(chuàng)建分支非常的快,因?yàn)橹皇莿?chuàng)建一個(gè)指針,然后修改做一個(gè)指針。
我們可以使用git branch
命令來查看當(dāng)前的分支,該命令會(huì)列出所有的分支,并在當(dāng)前使用的分支前使用*
來標(biāo)注。
從現(xiàn)在開始,所有的提交操作都是針對(duì)dev分支了,因此,如果做了一次新的提交,dev會(huì)向前移動(dòng)一步,但是master指針不變,如下圖:

這個(gè)時(shí)候,要合并分支也很容易,只需要先使用git checkout master
命令切換到master分支,然后在master分支中執(zhí)行git merge dev
來講dev合并到當(dāng)前分支就可以了。實(shí)際Git要做的只是將master指針指向最新的提交就可以了(這種合并叫做fast-forward,快速向前。當(dāng)然并不是所有的合并都這么簡單,我們后面會(huì)講到)。如下圖所示:

當(dāng)一個(gè)分支完成了歷史使命的時(shí)候,我們可以將其刪除。例如,如果我們要?jiǎng)h除dev分支,只需要執(zhí)行git branch -d dev
就可以了。刪除后就只剩下master分支了。
4.2 解決分支之間的沖突
前面介紹的合并分支是最簡單的一種情況,當(dāng)然現(xiàn)實(shí)世界通常不會(huì)這么簡單。現(xiàn)實(shí)中常見的情況時(shí)在兩個(gè)分支上我們都有提交,如下圖所示:

這個(gè)時(shí)候,如果我們執(zhí)行git merge dev
命令,就無法執(zhí)行fast-forward合并,Git會(huì)試圖把各自的修改合并起來。如果在dev分支上和在master分支上沒有修改相同的文件,Git能夠自動(dòng)進(jìn)行合并。如果有修改相同的文件,Git就處理不了,會(huì)告訴我們有沖突,需要我們手動(dòng)解決沖突之后再提交。 使用git status
命令可以看到?jīng)_突的文件。
如果我們打開沖突的文件,會(huì)看到類似下面的內(nèi)容:
1 2 3 4 5 6
| ...... <<<<<<< HEAD HEAD中的內(nèi)容(當(dāng)前分支中的內(nèi)容) ========== dev分支中的內(nèi)容。 >>>>>>> dev
|
對(duì)于每一個(gè)沖突,Git中會(huì)用<<<<<<<
,=======
,>>>>>>>
來標(biāo)記不同分支中的內(nèi)容。我們需要將這一段修改為合適的內(nèi)容,并再次提交。如下圖所示:

5 通過遠(yuǎn)程倉庫合作
一個(gè)團(tuán)隊(duì)中有多個(gè)開發(fā)人員,他們一起協(xié)助來完成軟件開發(fā)的工作。因此,不可能大家都只在自己的本地倉庫中修修改改。通常來說,我們會(huì)配置一個(gè)”中央倉庫“,大家都把自己的本地倉庫中的代碼要提交到中央倉庫。
5.1 日常的工作流程
在軟件開發(fā)過程中,我們首先是從中央倉庫下載代碼到本地倉庫,這個(gè)操作在前面的第2.2節(jié)中已經(jīng)描述過了。
有了本地倉庫之后,日常的工作流程通常上:
- 從中央倉庫拉取最新的代碼。
- 根據(jù)開發(fā)任務(wù),在工作區(qū)中修改和測(cè)試代碼。
- 將代碼提交到本地倉庫。
- 重復(fù)2和3,直到任務(wù)開發(fā)和測(cè)試完成。
- 將代碼推送到中央倉庫。
5.2 從中央倉庫拉取代碼
從中央倉庫拉取代碼,使用的是git pull
命令。 git pull的時(shí)候,會(huì)試圖自動(dòng)合并本地與遠(yuǎn)程之間的沖突,如果無法自動(dòng)合并,則需要手動(dòng)解決沖突。解決的方式和我們前面第4.2節(jié)中說過的方法一樣。
還有一種情況,是從中央倉庫抓取一個(gè)新的分支。我們使用git clone
命令從中央倉庫克隆的時(shí)候,獲取的是master分支。這個(gè)時(shí)候,中央倉庫就是我們的遠(yuǎn)程倉庫,默認(rèn)的名稱是origin。我們可以使用git remote -v
來查看遠(yuǎn)程倉庫的詳細(xì)信息。
如果要從中央倉庫獲取一個(gè)新的分支,比如dev分支,則需要先執(zhí)行git checkout -b <本地分支名稱> <遠(yuǎn)程庫名稱>/<遠(yuǎn)程分支名稱>
命令來獲取新的分支。
5.3 推送分支到中央倉庫
我們?cè)诒镜刈龅男薷模枰蟼鞯街醒雮}庫,使用的是git push <遠(yuǎn)程庫名稱> <分支名稱>
命令。這個(gè)過程被稱為推送。
如果從上次下載代碼到本次推送之間,遠(yuǎn)程倉庫上的代碼沒有變化,那么本次推送就沒有問題。但是通常來說,在團(tuán)隊(duì)合作的時(shí)候,在這段時(shí)間內(nèi)會(huì)有其他人推送了新的代碼到中央倉庫。這個(gè)時(shí)候使用git push
命令就會(huì)報(bào)錯(cuò),并且會(huì)提示我們使用git pull
獲取最新的代碼,合并代碼后(無論是自動(dòng)還是手動(dòng))再推送。
6 其他
6.1 標(biāo)簽管理
在Git中,每一次提交都有一個(gè)版本號(hào),但是這個(gè)版本號(hào)對(duì)人類很不友好。沒有任何含義,并且又長又難記。我們?cè)谌粘5慕涣髦袩o法使用這個(gè)版本號(hào)。
這種情況下,引入了tag的功能。Tag就是一個(gè)標(biāo)簽,我們可以在重要的提交版本上添加一個(gè)tag,這個(gè)tag其實(shí)就是指向這一次提交的版本。我們可以給tag賦予有意義的名稱,例如V1.0這樣的,這樣方便我們?nèi)粘5慕涣鳎蛯淼牟檎摇?/p>
在Git中打標(biāo)簽非常簡單,執(zhí)行git tag <tagname>
就可以了。(我們暫時(shí)不討論分支,后續(xù)的分支管理中在討論。)
執(zhí)行上面的命令的時(shí)候,默認(rèn)是將標(biāo)簽打在分支的最后一次提交上。如果要將標(biāo)簽打在其他的提交上,可以使用git tag <tagname> <版本號(hào)>
的方式來執(zhí)行命令。
可以使用git tag
命令來查看所有的標(biāo)簽,使用git show <tagname>
來查看指定標(biāo)簽的信息。
我們創(chuàng)建的標(biāo)簽都是在本地,如果需要將標(biāo)簽推送到遠(yuǎn)程倉庫,則可以使用git push <遠(yuǎn)程倉庫名> <標(biāo)簽名>
命令來推送。也可以一次性的將所有標(biāo)簽推送到遠(yuǎn)程,只需要使用命令git push <遠(yuǎn)程倉庫名> --tags
命令。
標(biāo)簽是不能移動(dòng)和修改的,但是可以刪除。因此,如果標(biāo)簽打錯(cuò)了,修改的方式就是刪除錯(cuò)誤的標(biāo)簽,重新創(chuàng)建正確的標(biāo)簽。git tag -d <name>
就是用來刪除指定名稱的tag的命令。
如果標(biāo)簽已經(jīng)被推送到了遠(yuǎn)程倉庫,則需要先刪除本地標(biāo)簽,然后使用如下形式的push命令來刪除遠(yuǎn)程倉庫中的標(biāo)簽。
git push <遠(yuǎn)程倉庫名> :refs/tags/<標(biāo)簽名>
6.2 git stash操作
有些時(shí)候,我們?cè)诠ぷ鞯闹型荆瑫?huì)接到一些緊急的任務(wù),比如生產(chǎn)環(huán)境的bug修復(fù)。這個(gè)時(shí)候我們手頭的工作才做了一半,既不能提交,也不能丟棄。但是我們又需要切換到另外的分支中來處理緊急任務(wù),該怎么辦?
這個(gè)時(shí)候就可以用到git stash操作了。
git stash命令會(huì)將工作區(qū)的修改“隱藏”起來。如下例所示:
1 2 3
| $git stash Saved working directory and index state WIP on master: 3793459 add new line in readme.txt HEAD is now at 3793459 add new line in readme.txt
|
這個(gè)命令會(huì)將工作區(qū)中的修改創(chuàng)建一個(gè)存根,并保存到堆棧中。我們可以使用git stash list
命令來查看堆棧中的存根。這個(gè)時(shí)候我們?cè)儆?code style="font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; padding: 2px 4px; overflow-wrap: break-word; background: #eeeeee; border-radius: 3px;">git status命令來查看的話,就會(huì)發(fā)現(xiàn)在工作區(qū)中沒有新的修改。這個(gè)時(shí)候我們就可以切換分支,完成緊急任務(wù)。
當(dāng)緊急任務(wù)完成之后,我們?cè)谇袚Q回原來的分支,這個(gè)時(shí)候可以使用git stash pop
,可以從堆棧中彈出修改并應(yīng)用到工作區(qū),這樣我們就可以繼續(xù)以前的工作了。
6.3 git配置
配置用戶信息: 使用命令git config --global user.name="<用戶名>"
來配置用戶的名字。使用命令git config --global user.name="<郵箱地址>"
來配置用戶的email地址。這兩個(gè)命令配置的是本機(jī)的全局配置。
忽略特殊的文件:在有些時(shí)候,我們希望不要提交工作區(qū)中的某些文件,比如java編譯時(shí)生成的class文件等。我們可以將不希望提交的文件的名稱放在Git工作區(qū)的根目錄下的.gitignore文件中,Git就會(huì)忽略這些文件。該文件中可以使用通配符。