Git 簡介
Git是目前世界上最先進的分布式版本控制系統,沒有之一。
勤用 git status 查看狀態和提示,準沒錯的。
廖雪峰 Git 教程
集中式 VS 分布式
CVS及SVN都是集中式的版本控制系統,而Git是分布式版本控制系統,集中式和分布式版本控制系統有什么區別呢?
先說集中式版本控制系統,版本庫是集中存放在中央服務器的,而干活的時候,用的都是自己的電腦,所以要先從中央服務器取得最新的版本,然后開始干活,干完活了,再把自己的活推送給中央服務器。中央服務器就好比是一個圖書館,你要改一本書,必須先從圖書館借出來,然后回到家自己改,改完了,再放回圖書館。
集中式版本控制系統最大的毛病就是必須聯網才能工作,如果在局域網內還好,帶寬夠大,速度夠快,可如果在互聯網上,遇到網速慢的話,可能提交一個10M的文件就需要5分鐘,這還不得把人給憋死啊。
那分布式版本控制系統與集中式版本控制系統有何不同呢?首先,分布式版本控制系統根本沒有“中央服務器”,每個人的電腦上都是一個完整的版本庫,這樣,你工作的時候,就不需要聯網了,因為版本庫就在你自己的電腦上。既然每個人電腦上都有一個完整的版本庫,那多個人如何協作呢?比方說你在自己電腦上改了文件A,你的同事也在他的電腦上改了文件A,這時,你們倆之間只需把各自的修改推送給對方,就可以互相看到對方的修改了。
說明
首先這里再明確一下,所有的版本控制系統,其實只能跟蹤文本文件的改動,比如TXT文件,網頁,所有的程序代碼等等,Git也不例外。版本控制系統可以告訴你每次的改動,比如在第5行加了一個單詞“Linux”,在第8行刪了一個單詞“Windows”。而圖片、視頻這些二進制文件,雖然也能由版本控制系統管理,但沒法跟蹤文件的變化,只能把二進制文件每次改動串起來,也就是只知道圖片從100KB改成了120KB,但到底改了啥,版本控制系統不知道,也沒法知道。
不幸的是,Microsoft的Word格式是二進制格式,因此,版本控制系統是沒法跟蹤Word文件的改動的,前面我們舉的例子只是為了演示,如果要真正使用版本控制系統,就要以純文本方式編寫文件。
因為文本是有編碼的,比如中文有常用的GBK編碼,日文有Shift_JIS編碼,如果沒有歷史遺留問題,強烈建議使用標準的UTF-8編碼,所有語言使用同一種編碼,既沒有沖突,又被所有平臺所支持。
使用Windows的童鞋要特別注意:
千萬不要使用Windows自帶的記事本編輯任何文本文件。原因是Microsoft開發記事本的團隊使用了一個非常弱智的行為來保存UTF-8編碼的文件,他們自作聰明地在每個文件開頭添加了0xefbbbf(十六進制)的字符,你會遇到很多不可思議的問題,比如,網頁第一行可能會顯示一個“?”,明明正確的程序一編譯就報語法錯誤,等等,都是由記事本的弱智行為帶來的。建議你下載 Notepad++ 代替記事本,不但功能強大,而且免費!記得把Notepad++的默認編碼設置為UTF-8 without BOM 即可:
Unix的哲學是——沒有消息就是好消息。
廢話說完了,下面開始命令
-
git init
:將一個目錄或文件夾初始化成 Git 可以管理的倉庫( repository ) -
git add readme.txt
:把readme.txt
文件的修改添加到暫存區; -
git add .
:將當前目錄下所有文件添加到暫存區; -
git commit -m 'the commit info'
:將暫存區中的修改提交到倉庫; -
git status
:查看倉庫當前狀態; git diff readme.txt
:查看文件具體不同;-
git log [--pretty=oneline]
:查看提交歷史; -
git reset --hard HEAD^
:回到上一個版本; -
git reset --hard HEAD~n
:回到上 n 個版本; -
git reset --hard commit_id
:回到commit_id
對應的版本; -
git reflog
:查看命令歷史——包括每一次的commit_id
!!這樣就可以任意穿越!! -
git checkout -- readme.txt
:將 工作區中的修改 恢復到最近一次add
或commit
的狀態,即若add
后又作了修改,就恢復到add
時的狀態;若commit
后作了修改,就恢復到commit
時的狀態;(注意這里--
是單獨在中間的,沒有和readme.txt
連起來!) -
git reset HEAD readme.txt
:將 暫存區中readme.txt
的修改 撤銷(unstage)掉,重新放回工作區; -
git rm a.txt
:刪除a.txt
并將修改添加到暫存區; -
git remote add origin https://github.com/xiaogmail/learnit.git
將本地倉庫與遠程倉庫關聯起來; -
git push -u origin master
:將本地倉庫推送到遠程倉庫;由于遠程庫是空的,我們第一次推送
master
分支時,加上了-u
參數,Git不但會把本地的master
分支內容推送的遠程新的master
分支,還會把本地的master
分支和遠程的master
分支關聯起來,在以后的推送或者拉取時就可以簡化命令,git push
搞定。
-
git branch
:查看分支; -
git branch <name>
:創建分支; -
git checkout <name>
:切換分支; -
git checkout -b <name>
:創建并切換分支; -
git branch -d <name>
:刪除分支; -
git merge <name>
:合并某個分支到當前分支; -
git merge --abort
:終止合并(在遇到沖突要你手動合并時); -
git log --graph --pretty=oneline --abbrev-commit
:查看分支圖; -
git merge --no-ff dev -m 'master merge dev with --no-ff'
:強制非快進模式,這才是正確使用方式。不要默認的 Fast-forward。 -
git stash
:保存和隱藏當前工作現場; -
git stash list
:查看隱藏的工作現場; -
git stash apply
:恢復工作現場; -
git stash drop
:刪除隱藏的工作現場; -
git stash pop
:恢復并刪除; -
git remote
:查看遠程倉庫信息; -
git remote -v
:查看更詳細的信息(url 地址); -
git push origin branch-name
:從本地推送分支; -
git checkout -b branch-name origin/branch-name
在本地創建和遠程分支對應的分支; -
git branch --set-upstream-to=origin/branch-name
:建立本地分支和遠程分支的關聯; -
git pull
:從遠程分支抓取。如果有沖突,先解決沖突; -
git tag <tagname>
:在當前最新提交上創建一個標簽; -
git tag <tagname> <commit id>
:在指定 commit id 上創建標簽; -
git tag -a <tagname> -m <'tag information'>
:帶提示信息的標簽; -
git tag
:查看所有已建立的標簽; -
git show <tagname>
:查看某個標簽的詳細信息(如建在哪個 commit id 上); -
git tag -d <tagname>
:刪除標簽(本地); -
git push origin <tagname>
:推送標簽到遠程倉庫;對,標簽也要推送! -
git push origin --tags
:一次性推送所有未推送標簽; -
git push origin :refs/tags/<tagname>
:刪除一個遠程標簽(首先要在本地刪除);
手動新建一個 readme.txt
并寫入內容 aaaaa
后,用git status
查看狀態:
執行git add readme.txt
后再次查看狀態:
在readme.txt
中添加一行bbbbb
并保存后,查看狀態:
Changes to be commited
:暫存區中有未提交的修改;
Changes not staged for commit
:not staged,未將修改添加到暫存區;
再添加一行ccccc
后,用git diff readme.txt
查看區別:
像這樣,你不斷對文件進行修改,然后不斷提交修改到版本庫里,就好比玩RPG游戲時,每通過一關就會自動把游戲狀態存盤,如果某一關沒過去,你還可以選擇讀取前一關的狀態。有些時候,在打Boss之前,你會手動存盤,以便萬一打Boss失敗了,可以從最近的地方重新開始。Git也是一樣,每當你覺得文件修改到一定程度的時候,就可以“保存一個快照”,這個快照在Git中被稱為
commit
。一旦你把文件改亂了,或者誤刪了文件,還可以從最近的一個commit
恢復,然后繼續工作,而不是把幾個月的工作成果全部丟失。
-
git log [--pretty=oneline]
:查看日志;
需要友情提示的是,你看到的一大串類似
3628164...882e1e0
的是commit id
(版本號),和 SVN 不一樣,Git的commit id
不是1,2,3……遞增的數字,而是一個SHA1
計算出來的一個非常大的數字,用十六進制表示,而且你看到的commit id
和我的肯定不一樣,以你自己的為準。為什么commit id
需要用這么一大串數字表示呢?因為Git是分布式的版本控制系統,后面我們還要研究多人在同一個版本庫里工作,如果大家都用1,2,3……作為版本號,那肯定就沖突了。
時光機---回退:將工作區恢復到以前的某個版本
首先,Git必須知道當前版本是哪個版本,在Git中,用
HEAD
表示當前版本,也就是最新的提交3628164...882e1e0
(注意我的提交ID和你的肯定不一樣),上一個版本就是HEAD^
,上上一個版本就是HEAD^^
,當然往上100個版本寫100個^,比較容易數不過來,所以寫成HEAD~100
。
現在會退到上一個版本:git reset --hard HEAD^
看,readme.txt
果然回去了:
但是!如果剛才是手殘,現在想回到add ccccc
的那個版本怎么辦??
——有辦法!前提是你剛才的窗口還沒關!
辦法其實還是有的,只要上面的命令行窗口還沒有被關掉,你就可以順著往上找啊找啊,找到那個
append GPL
的commit id
是3628164...
,于是就可以指定回到未來的某個版本:
$ git reset --hard 3628164 HEAD is now at 3628164 append GPL
版本號沒必要寫全,前幾位就可以了,Git會自動去找。當然也不能只寫前一兩位,因為Git可能會找到多個版本號,就無法確定是哪一個了。
現在,你回退到了某個版本,關掉了電腦,第二天早上就后悔了,想恢復到新版本怎么辦?找不到新版本的commit id
怎么辦?
在Git中,總是有后悔藥可以吃的。當你用git reset --hard HEAD^
回退到add distributed
版本時,再想恢復到append GPL
,就必須找到append GPL
的commit id
。Git提供了一個命令git reflog
用來記錄你的每一次命令:
$ git reflog
ea34578 HEAD@{0}: reset: moving to HEAD^
3628164 HEAD@{1}: commit: append GPL
ea34578 HEAD@{2}: commit: add distributed
cb926e7 HEAD@{3}: commit (initial): wrote a readme file
現在,你又可以用git reset --hard commit_id
回到未來了!
工作區--暫存區--某個分支:
撤銷,工作區中,對某個文件最近的修改——即還未stage
的修改
git checkout -- readme.txt
命令
git checkout -- readme.txt
意思就是,把readme.txt
文件在工作區的修改全部撤銷,這里有兩種情況:
- 一種是
readme.txt
自修改后還沒有被放到暫存區,現在,撤銷修改就回到和版本庫一模一樣的狀態; - 一種是
readme.txt
已經添加到暫存區后,又作了修改,現在,撤銷修改就回到添加到暫存區后的狀態;
總之,就是讓這個文件回到最近一次
git commit
或git add
時的狀態。
上面是撤銷工作區中的修改,若想撤銷暫存區中的呢?
撤銷(unstage)暫存區中,未提交(commit)的修改,重新放回工作區:
git reset HEAD readme.txt
刪除文件
修改若添加到了暫存區,則先要恢復暫存區(git reset HEAD a.txt
),再恢復工作區(git checkout -- a.txt
)。
連接 Github
注冊賬戶之后,首先需要在 Github 上對本機添加信任(SSH, RSA),步驟;
完成后在 Github 上可以看到:
接下來在 Github 上新建一個空的倉庫,然后把本地的倉庫與之關聯,將本地倉庫內容推送到 Github 倉庫:
git remote add origin https://github.com/xiaogmail/learngit.git
git push -u origin master
或者,遠程倉庫已經存在,從遠程倉庫克隆到本地:
git clone https://github.com/xiaogmail/learngit.git
創建與合并分支
git checkout -b dev
:創建并切換到dev
分支;
上面相當于以下兩條命令:
git branch dev
:創建dev
分支;
git checkout dev
:切換到dev
分支;
用git branch
查看分支:
現在,在readme.txt
中加一行new branch
然后提交(到dev
分支),再git branch master
切換回master
分支:
這時你發現,readme.txt
中最后一行new branch
不見了!(用Notepad++需要關閉并重新打開文件)——因為回到了master
分支!
現在,把dev
分支的內容合并到master
分支上:git merge dev
這時會發現readme.txt
中new branch
的內容又回來了;
注意到上面的
Fast-forward
信息,Git告訴我們,這次合并是“快進模式”,也就是直接把master
指向dev
的當前提交,所以合并速度非常快。
當然,也不是每次合并都能Fast-forward
,我們后面會講其他方式的合并。
合并完成后的分支狀態:
合并完成后,就可以放心的刪除dev
分支了:git branch -d dev
;
因為創建、合并和刪除分支非常快,所以 Git 鼓勵你使用分支完成某個任務,合并后再刪掉分支,這和直接在 master 分支上工作效果是一樣的,但過程更安全。
解決沖突
現在,你在dev
上新增一個提交,切換回master
,在master
上也新增一個提交,變成了這樣:
這時你用git merge dev
合并分支:
其中,dev 中 readme.txt 最后一行是 "dev new line",master 中是 "master new line";
這時發現,readme.txt 的內容變了:
Git 用 <<<<<<<,=======,>>>>>>> 標記出不同分支的內容
注意上面最后一句話:“Automatic merge failed; fix conficts and then commit the result.”——現在還處于 merge 狀態,只不過需要你手動完成。
我修改為如下:
然后提交,git add readme.txt
,git commit -m 'fix conflicts.'
這樣,merge 操作才算手動完成,現在,分支圖:
看懂分支圖的變化:綠線表示在 master 分支上將 dev 分支的內容合并進來,但 feature1 是不變的!它是被合并的那一個!不動!
仔細想想合并的過程。同一個文件,稍微有點不一樣,就會發生沖突;這時只能手動決定合并后的內容——亦即,隨便改,隨你。
“ 首先 git branch一下你在哪個分支上,本節的例子出現上面的文本應該時在 master 上。那么對于這個文本,想怎么改就怎么改,改成合并后你想要的文本就行。甚至不修改,直接保持上面原文。只要在 merge 操作提示沖突后再進行一個 add 和 commit ,至于進行這個操作前你對 master 分支里的 readme.txt 進行怎樣的修改都行 修改完畢后,git add 然后 git commit,就已經 merge 了。”——網友討論。
所以,git merge 只是一種嘗試,或者一種提示;根據沖突的提示,由你來決定合并后的版本。合并過程與兩個涉及到的分支都沒關系。
機智網友的討論:
我說一點自己的理解,不知道對不對啊?造成沖突的原因就是多個分支同時對同一文件進行了修改,有點像是樹節點有了多個兒子,當合并時,HEAD就不知道該往哪里走了,所以只能保留某一個分支的修改內容,然后進行合并。所以,在實際中,我們在同一時間應該利用某一個分支最好只操作固定的文件,避免沖突的發生?不知道我以上說的對不對。
你說的這種辦法絕對不會造成沖突,可是我們在工作學習中不能削足適履吧?已經提供了這種在不同分支中沖突的解決辦法,為什么還要害怕沖突產生呢?
當兩條分支對同一個文件的同一個文本塊進行了不同的修改,并試圖合并時,Git不能自動合并的,稱之為沖突(conflict)。解決沖突需要人工處理
廖老師,你好,我 merge 成功之后(已解決了沖突),發現 master 分支和 feature1 分支中的 readme.txt 的內容不一樣....
廖雪峰:對,因為你解決沖突后 master 多了一個新的 commit,正常情況可以把 master 再 merge 到 feature1 使兩者保持一致
正常情況可以把 master 再 merge 到 feature1 使兩者保持一致,不過沒有必要,因為之前 merge 過了,并且已經修改過了(解決沖突)。修改之后 feature1 就沒有作用了,可以刪掉。
master 是穩定版,然后你此時要開發一個功能 a,你開一個新分支去開發,開發完后再合并到 master,這時 master 才有功能a,大概這樣吧…
用帶參數的 git log 查看分支的合并情況:
git log --graph --pretty=oneline --abbrev-commit
分支管理策略
之前合并分支時,默認用的是Fast-forward
模式,但這種模式下,刪除分支后,會丟掉分支信息。
默認的Fast-forward
和用參數--no-ff
關閉快速模式:
刪除dev
分支后再看:
重新做了個實驗,在 master 中新建 a.txt,空的;新建并切換到分支 dev,分3次添加 aaaa,bbbb,cccc;再切換回 master,合并,分別用默認 Fast-forward 和 --no-ff ;最后刪除 dev 分支,查看分支圖:
也許最關鍵的后果是,造成了 dev 分支 commit 記錄和 master 的 commit 記錄的混亂吧。想想 Fast-forward 的實現方式,移動指針。
而用 --no-ff 就可以在即使 dev 刪除后也能保留 dev 的 commit 記錄,清晰的區分開來。
另,當有沖突時,就不再是 Fast-forward 了(你都動手了),這時就轉為了 --no-ff,自然可以保留 dev 分支記錄 or 合并記錄。不要迷惑了。
下面這張圖完美解釋了這個問題!
問題的關鍵點在于 master 有沒有diverged!**
分支策略
在實際開發中,我們應該按照幾個基本原則進行分支管理:
首先,master
分支應該是非常穩定的,也就是僅用來發布新版本,平時不能在上面干活;
那在哪干活呢?干活都在dev
分支上,也就是說,dev
分支是不穩定的,到某個時候,比如1.0版本發布時,再把dev分支合并到master
上,在master
分支發布1.0版本;
你和你的小伙伴們每個人都在dev
分支上干活,每個人都有自己的分支,時不時地往dev
分支上合并就可以了。
所以,團隊合作的分支看起來就像這樣:
合并分支時,加上--no-ff
參數就可以用普通模式合并,合并后的歷史有分支,能看出來曾經做過合并,而Fast forward
合并就看不出來曾經做過合并。
保存當前工作 [現場]
在切換分支前,當前的工作現場必須是“working tree clean”的狀態;即不能有沒 staged 的修改,或未 commit 的 stage;否則在切換前會有提示:
Your local changes to the following files would be overwritten by checkout: a.txt. Please commit your changes or stash them before you switch branches. Aborting.
提示要么提交,要么 stash,隱藏當前工作現場;
git stash
:保存并隱藏當前工作現場,待以后恢復;(stash:隱藏)
然后就可以愉快的切換分支了。
再回來,用git stash list
查看隱藏的工作現場:
然后用git stash pop
恢復并刪除分支:
-
git stash apply
:恢復工作現場; -
git stash drop
:刪除隱藏的工作現場; -
git stash pop
:恢復并刪除;
修復bug時,我們會通過創建新的bug分支進行修復,然后合并,最后刪除;
當手頭工作沒有完成時,先把工作現場git stash一下,然后去修復bug,修復后,再git stash pop,回到工作現場。
刪除分支時的一點小問題
軟件開發中,總有無窮無盡的新的功能要不斷添加進來。
添加一個新功能時,你肯定不希望因為一些實驗性質的代碼,把主分支搞亂了,所以,每添加一個新功能,最好新建一個feature分支,在上面開發,完成后,合并,最后,刪除該feature分支。
但就在 feature 分支的新功能開發完畢,切換回 dev 分支準備合并時,接到上級命令,因經費不足,新功能必須取消!
雖然白干了,但是這個分支還是必須就地銷毀:git branch -d dev
但是提示:dev 分支還沒有被合并,如果確定要刪除,用git branch -D dev
照做,OK.
多人協作
當你從遠程倉庫克隆時,實際上Git自動把本地的 master 分支和遠程的master 分支對應起來了,并且,遠程倉庫的默認名稱是 origin 。
git remote
:查看遠程倉庫信息;
或者git remote -v
查看更詳細的信息:
上面顯示了可以抓取和推送的 origin 的地址。如果沒有推送權限,就看不到 push 的地址。
git push origin <branch-name>
:推送某個分支到遠程倉庫;
原來你的遠程 origin 倉庫里只有 master 分支,現在你推送 dev 分支,那么 origin 里就會新出現一個 dev 分支。origin 是 倉 庫 名 ,可以容納很多分支。
現在,另一個人參與進來,他在他的電腦上 clone 這個倉庫:git clone https://xxxxxx.git
,但是,他用git branch
查看分支時,發現 只 能 [看 到] master 分支,dev 分支 [看] 不 到!:
是的,這時需要你手動把他找出來:
git checkout -b dev origin/dev
我這里為什么猜測是 [看不到],而不是根本沒有拷貝下來?因為這句命令回車后沒有拷貝的過程!瞬間完成。
就行來就可以在本地的 dev 分支上繼續工作了。
多人協作
完整故事看這里。
多人協作的工作模式通常是這樣:
- 首先,可以試圖用
git push origin branch-name
推送自己的修改; - 如果推送失敗,則因為遠程分支比你的本地更新,需要先用
git pull
試圖合并; - 如果合并有沖突,則解決沖突,并在本地提交;
- 沒有沖突或者解決掉沖突后,再用
git push origin branch-name
推送就能成功!
如果git pull
提示 “no tracking information”,則說明本地分支和遠程分支的鏈接關系沒有創建,用命令git branch --set-upstream-to=origin/branch-name
。
這就是多人協作的工作模式,一旦熟悉了,就非常簡單。
使用標簽
標簽(tag)也指向某個 commit,作用是為某次 commit 取個別名——因為 commit id 是一串無規律的數字,記不住。類似于 ip 地址和域名的關系。
其余
- 忽略某些文件時,需要編寫
.gitignore
; -
.gitignore
文件本身要放到版本庫里,并且可以對.gitignore
做版本管理!
為命令配置別名
上面哪個顯示分支圖的命令還記得嗎?
git log --graph --pretty=oneline --abbrev-commit
;
還有一個顏色、格式更好,但更長的版本:
git log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit
太長了,記不住怎么辦?設置一個別名。
git config --global alias.lg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"
lg
就是后面一大串的別名。
然后就可以git lg
了!
git 全局的配置文件在C:\Users\xiao\.gitconfig
:
Done!
終于到了期末總結的時刻了!
經過幾天的學習,相信你對Git已經初步掌握。一開始,可能覺得Git上手比較困難,尤其是已經熟悉SVN的童鞋,沒關系,多操練幾次,就會越用越順手。
Git雖然極其強大,命令繁多,但常用的就那么十來個,掌握好這十幾個常用命令,你已經可以得心應手地使用Git了。