簡介:相信大部分開發者對 Git 都不陌生,Git 也已成爲大部分開發者平常開發必用的工具。本文分享 Git 使用上的一些基礎知識,通俗易懂,很是有用。git
在 Git Rev News #48 期的 LightReading 中有一篇文章(地址:https://hacker-tools.github.io/version-control/) 寫的不錯,不只乾貨滿滿並且還附帶了操做視頻。其中的內容不只覆蓋了不少 Git 使用上的基礎知識,也從使用角度上解答了不少剛接觸 Git 的開發者的疑問。爲了便於讀者理解,我在翻譯的同時也添加了一些內容。如下爲正文部分。本文內容較長,建議收藏慢慢學習。github
不少人怕使用 Git,我我的以爲主要多是兩部分的緣由:算法
Never Be Afraid To Try Something New.
代碼對於開發者是勞做成果的結晶,對於公司而言是核心資產,有一些擔心也是正常的。但 Git 也並無咱們想象中的那麼複雜,須要讓咱們每次使用都心有餘悸,其實咱們只須要稍微花一點時間嘗試多多瞭解它,在不少時候你會發現,非但 Git 不會讓你產生擔心,並且會讓本身的交付過程更加高效。vim
談及 Git 就不得不提到版本控制,咱們不妨先來看下版本控制是作什麼的,這將有助於後續對 Git 的理解。安全
當你在工做中面對的是一些常常變化的文檔、代碼等交付物的時候,考慮如何去追蹤和記錄這些 changes 就變得很是重要,緣由多是:對於頻繁改動和改進的交付物,很是有必要去記錄下每次變動的內容,每次記錄的內容匯成了一段修改的歷史,有了歷史咱們才知道咱們曾經作了什麼。服務器
記錄的歷史中必需要包含一些重要的信息,這樣追溯才變得有意義,好比:數據結構
最好能夠支持撤銷變動,不讓某一個提交的嚴重問題,去污染整個提交歷史。app
版本控制系統(VCS: Version Control System),正會爲你提供這種記錄和追溯變動的能力。分佈式
大多數的 VCS 支持在多個使用者之間共享變動的提交歷史,這從實質上讓團隊協同變爲了可能,簡單說來就是:工具
VCS 歷經多年的發展,目前業界中有許多 VCS 工具可供咱們選擇。在本文中,咱們將會針對目前最流行的 Git 來介紹。
剛接觸 Git 時,Git 確實有讓人以爲有點像黑魔法同樣神祕,可是又有哪一個技術不是這樣呢?當咱們瞭解其基本的數據結構結構後,會發現 Git 從使用角度來說其實並不複雜,咱們甚至能夠更進一步的學習 Git 的一些優良的軟件設計理論,從中獲益。首先,讓咱們先從 commit 提及。
提交對象(git commit object):每個提交在 Git 中都經過 git commit object 存儲,對象具備一個全局惟一的名稱,叫作 revision hash。它的名字是由 SHA-1 算法生成,形如"998622294a6c520db718867354bf98348ae3c7e2
",咱們一般會取其縮寫方便使用,如"9986222
"。
讓咱們經過實戰來幫助理解,第一步咱們來初始化一個 repository(Git 倉庫),默認初始化以後倉庫是空的,其中既沒有保存任何文本內容也沒有附帶任何提交:
$ git init hackers $ cd hackers $ git status
第二步,讓咱們來看下執行事後 Git 給出的輸出內容,它會指引咱們進行進一步的瞭解:
➜ hackers git:(master) git status On branch master No commits yet nothing to commit (create/copy files anduse "git add" to track)
1)output 1: On branch master
對於剛剛建立空倉庫來講,master 是咱們的默認分支,一個 Git 倉庫下能夠有不少分支 (branches),具體某一個分支的命名能夠徹底由你本身決定,一般會起便於理解的名字,若是用 hash 號的話確定不是一個好主意。
branches 是一種引用 (ref),他們指向了一個肯定的 commit hash 號,這樣咱們就能夠明確咱們的分支當前的內容。
除了 branches 引用之外,還有一種引用叫作 tags,相信你們也不會陌生。
master 一般被咱們更加熟知,由於大多數的分支開發模式都是用 master 來指向「最新」的 commit。
On branch master 表明着咱們當前是在 master 分支下操做,因此每次當咱們在提交新的 commit 時,Git 會自動將 master 指向咱們新的 commit,當工做在其餘分支上時,同理。
有一個很特殊的 ref 名稱叫作 "HEAD",它指向咱們當前正在操做的 branches 或 tags (正常工做時),其命名上很是容易理解,表示當前的引用狀態。
經過git branch
(或git tag
) 命令你能夠靈活的操做和修改 branches 或 tags。
2)output 2:No commits yet
對於空倉庫來講,目前咱們尚未進行任意的提交。
nothing to commit (create/copy files anduse "git add" to track)
output 中提示咱們須要使用git add
命令,說到這裏就必需要提到暫存或索引 (stage),那麼如何去理解暫存呢?
一個文件從改動到提交到 Git 倉庫,須要經歷三個狀態:
git add 的幫助文檔中很詳細的解釋了暫存這一過程:
This command updates the index using thecurrent content found in the working tree, to prepare the content stagedfor the next commit.
git add 命令將更新暫存區,爲接下來的提交作準備。
It typically adds the current content ofexisting paths as a whole, but with some options it can also be used toadd content with only part of the changes made to the working tree filesapplied, or remove paths that do not exist in the working tree anymore.The "index" holds a snapshot ofthe content of the working tree, and it is this snapshot that is taken as thecontents of the next commit.
暫存區的 index 保存的是改動的完整文件和目錄的快照 (非 delta)。
Thus after making any changes to theworking tree, and before running the commit command, you must use the addcommand to add any new or modified files to the index.
暫存是咱們將改動提交到 git 倉庫以前必須經歷的狀態。
對 Git 暫存有必定了解後,其相關操做的使用其實也很是簡單,簡要的說明以下:
1)暫存區操做
git add
命令將改動暫存。git add -p
來依次暫存每個文件改動,過程當中咱們能夠靈活選擇文件。中的變動內容,從而決定哪些改動暫存。git add
不會暫存被 ignore 的文件改動。git rm
命令,咱們能夠刪除文件的同時將其從暫存區中剔除。2)暫存區修正
git reset
命令進行修正,能夠先將暫存區的內容清空,在使用git add -p
命令對改動 review 和暫存。git reset FILE
或git reset -p
命令。3)暫存區狀態
git diff --staged
依次檢查暫存區內每個文件的修改。git diff
查看剩餘的還未暫存內容的修改。當你對須要修改的內容和範圍滿意時,你就能夠將暫存區的內容進行 commit 了,命令爲:git commit
。
若是你以爲須要把全部當前工做空間的修改所有 commit,能夠執行git commit -a
,這至關於先執行git add
後執行git commit
,將暫存和提交的指令合二爲一,這對於一些開發者來講是很高效的,可是若是提交過大這樣作一般不合適。
咱們建議一個提交中只作一件事,這在符合單一職責的同時,也可讓咱們明確的知道每個 commit 中作了一件什麼事情而不是多個事情。因此一般咱們的使用習慣都是執行git add -p
來 review 咱們將要暫存內容是否合理?是否須要更細的拆分提交?這些優秀的工程實踐,將會讓代碼庫中的 commits 更加優雅。
ok,咱們已經在不知不覺中瞭解了不少內容,咱們來回顧下,它們包括了:
附帶的,在瞭解 commit 過程當中咱們知道了從本地改動到提交到 Git 倉庫,經歷的幾個關鍵的狀態:
下圖爲上述過程當中各個狀態的轉換過程:
git commit
以後,暫存區的內容對象將會存儲在 Git 倉庫中,並執行更新 HEAD 指向等後續操做,這樣就完成了引用與提交、提交與改動快照的——對應了。
正是由於 Git 自己對於這幾個區域(狀態)的設計,爲 Git 在本地開發過程帶來了靈活的管理空間。咱們能夠根據本身的狀況,自由的選擇哪些改動暫存、哪些暫存的改動能夠 commit、commit 能夠關聯到那個引用,從而進一步與其餘人進行協同。
咱們已經有了一個 commit,如今咱們能夠圍繞 commit 作更多有趣的事情:
git log
(或git log --oneline
)。diff:git log -p
。git checkout NAME
(若是 NAME 是一個具體的提交哈希值時,Git 會認爲狀態是 「detached (分離的)」,由於git checkout
過程當中重要的一步是將 HEAD 指向那個分支的最後一次 commit。因此若是這樣作,將意味着沒有分支在引用此提交,因此若咱們這時候進行提交的話,沒有人會知道它們的存在)。git revert NAME
來對 commit 進行反轉操做。git diff NAME
將舊版本與當前版本進行比較,查看 diff。git log NAME
查看指定區間的提交。git reset NAME
進行提交重置操做。git reset --hard NAME
:將全部文件的狀態強制重置爲 NAME 的狀態,使用上須要當心。引用 (refs) 包含兩種分別是 branches 和 tags, 咱們接下來簡單介紹下相關操做:
git branch b
命令可讓咱們建立一個名稱爲 b 的分支。git checkout b
能夠切換到b分支上,切換後新的提交都會在b分支上,理所應當。git checkout master
切換回 master 後,b 分支的提交也不會帶回 master 上,分支隔離。分支上提交隔離的設計,可讓咱們很是輕鬆的切換咱們的修改,很是方便的作各種測試。
tags 的名稱不會改變,並且它們有本身的描述信息 (好比能夠做爲 release note 以及標記發佈的版本號等)。
可能不少人的提交歷史是長這個樣子的:
commit 14: add feature x – maybe even witha commit message about x! commit 13: forgot to add file commit 12: fix bug commit 11: typo commit 10: typo2 commit 9: actually fix commit 8: actually actually fix commit 7: tests pass commit 6: fix example code commit 5: typo commit 4: x commit 3: x commit 2: x commit 1: x
單就 Git 而言,這看上去是沒有問題並且合法的,但對於那些對你修改感興趣的人(極可能是將來的你!),這樣的提交在信息在追溯歷史時可能並無多大幫助。可是若是你的提交已經長成這個樣子,咱們該怎麼辦?
不要緊,Git 有辦法能夠彌補這一些:
咱們能夠將新的改動提交到當前最近的提交上,好比你發現少改了什麼,可是又不想多出一個提交時會頗有用。
若是咱們認爲咱們的提交信息寫的並很差,我要修改修改,這也是一種辦法,可是並非最好的辦法。
這個操做會更改先前的提交,併爲其提供新的 hash 值。
這個命令很是強大,能夠說是 Git 提交管理的神器,此命令含義是咱們能夠針對以前的 13 次的提交在 VI 環境中進行從新修改設計:
操做選項 p 意味着保持原樣什麼都不作,咱們能夠經過 vim 中編輯提交的順序,使其在提交樹上生效。
操做選項 r:咱們能夠修改提交信息,這種方式比commit --amend
要好的多,由於不會新生成一個 commit。
操做選項 e:咱們能夠修改 commit,好比新增或者刪除某些文件改動。
操做選項 s:咱們能夠將這個提交與其上一次的提交進行合併,並從新編輯提交信息。
操做選項 f:f表明着 "fixup"。例如咱們若是想針對以前一個老的提交進行 fixup,又不想作一次新的提交破壞提交樹的歷史的邏輯含義,能夠採用這種方式,這種處理方式很是優雅。
版本控制的一個常見功能是容許多我的對一組文件進行更改,而不會互相影響。或者更確切地說,爲了確保若是他們不會踩到彼此的腳趾,不會在提交代碼到服務端時偷偷的覆蓋彼此的變化。
在 Git 中咱們如何保證這一點呢?
Git 與 SVN 不一樣,Git 不存在本地文件存在 lock 的狀況,這是一種避免出現寫做問題的方式,可是並不方便,而 Git 與 SVN 最大的不一樣在於它是一個分佈式 VCS,這意味着:
git push
推送本地內容到任意咱們有權限的 Git 遠端倉庫。
SVN,圖片出自 git-scm
Git,圖片出自 git-scm
衝突的產生幾乎是不可避免的,當衝突產生時你須要將一個分支中的更改與另外一個分支中的更改合併,對應 Git 的命令爲git merge NAME
,通常過程以下:
Git 將會保證這個過程改動不會丟失,另一個命令你可能會比較熟悉,那就是git pull
命令,git pull
命令實際上包含了git merge
的過程,具體過程爲:
git fetch REMOTE
git merge REMOTE/BRANCH
若是每次 merge 都如此順利,那確定是很是完美的,但有時候你會發如今合併時產生了衝突文件,這時候也不用擔憂,如何處理衝突的簡要介紹以下:
<<<<<<<
,這是你須要開始處理衝突的地方,而後找到=======
,等號上面的內容是 HEAD 到共同祖先之間的改動,等號下面是 NAME 到共同祖先之間的改動。用 git mergetool 一般是比較好的選擇,固然如今大多數 IDE 都集成了不錯的衝突解決工具。git add.
來暫存這些改動吧。git commit
,若是你想放棄當前修改從新解決可使用git merge --abort
,很是方便。當你完成了以上這些艱鉅的任務,最後git push
吧!
排除掉遠端的 Git 服務存在問題之外,咱們 push 失敗的大多數緣由都是由於咱們在工做的內容其餘人也在工做的關係。
Git 是這樣判斷的:
1)會判斷 REMOTE 的當前 commit 是否是你當前正在 pushing commit 的祖先。
2)若是是的話,表明你的提交是相對比較新的,push 是能夠成功的 (fast-forwarding)。
3)不然 push 失敗並提示你其餘人已經在你 push 以前執行更新 (push is rejected)。
當發生「push is rejected」後咱們的幾個處理方法以下:
git pull
合併遠程的最新更改(git pull
至關於git fetch
+git merge
)。--force-with-lease
參數,這樣只有遠端的 ref 自上次從 fetch 後沒有改變時纔會強制進行更改,不然「reject the push」,這樣的操做更安全,是一種很是推薦使用的方式。本文只是選取部分 Git 基本命令進行介紹,目的是拋磚引玉,讓你們對 Git 有一個基本的認識。當咱們深刻挖掘 Git 時,你會發現它自己有着如此多優秀的設計理念,值得咱們學習和探究。
不要讓 Git 成爲你認知領域的黑魔法,而是讓 Git 成爲你掌握的魔法。