全棧必備:Git
為什么使用Git
Git 是 Linus Torvalds 為了幫助管理 Linux 內核開發而開發的一個開放源碼的版本控制軟件。大神就是大神,在開發了Linux之后,Git 是又一抗鼎之作。這是唯一的理由么?
Git在軟件開發中位置——配置管理SCM
Software configuration management (SCM, or just plain CM) is an organizational framework — that is, a discipline — for managing the evolution of computer systems throughout all stages of systems development.
軟件配置管理:通過執行版本控制、變更控制的規程,以及使用合適的配置管理軟件,來保證所有配置資源的完整性和可跟蹤性。配置管理是對工作成果的一種有效保護。沒有軟件配置管理,最大的麻煩是工作成果無法回溯。
配置管理的內容和目標
配置管理的內容:
一類是屬于產品的組成部分,例如需求文檔、設計文檔、源代碼、測試用例等等;
另一類是在管理過程中產生的文檔,例如各種計劃、報告等
軟件配置管理是在貫穿整個軟件生命周期中建立和維護項目產品的完整性。它的基本目標包括:
1. 軟件配置管理的各項工作是有計劃進行的。
2. 被選擇的項目產品得到識別,控制并且可以被相關人員獲取。
3. 已識別出的項目產品的更改得到控制。
4. 使相關組別和個人及時了解軟件基準的狀態和內容。
配置管理的主要任務
軟件配置管理的主要任務也就歸結為以下幾條:
(1)制定項目的配置計劃;
(2)對配置項進行標識;
(3)對配置項進行版本控制;
(4)對配置項進行變更控制;
(5)定期進行配置審計;
(6)向相關人員報告配置的狀態。
版本控制
版本控制是軟件配置管理的核心功能。所有位于配置資源庫中的元素都應自動予以版本的標識,并保證版本命名的唯一性。版本在生成過程中,自動依照設定的使用模型自動分支、演進。
版本控制(Revision control)確保由不同人所編輯的同一檔案都得到更新。
版本控制中的基本概念
1)簽入,提交,檢出
2)沖突,解決,合并
3)分支,版本
4)鎖定,hook
常見的版本控制工具
作為一個老碼農,枚舉一下曾經使用過的版本控制工具。
1. VSS: visual source safe, 微軟的東東,97年寫VC程序時使用,人多的時候性能較差,不知道現在的升級版怎樣了
2. clearcase: 99年開發Unix 上分布式式應用時使用,功能強大,不只限于版本控制,有錢的大團隊才去用
3. CVS: 02年在互聯網熱潮的時候使用,開源產品,當時“Copy-Modify-Merge”開發模型眼前一亮。
4. SVN:曾經的摯愛,在曾工作的合資公司使用,權限管理和分支合并等方面做的很出色,并在多個公司推廣使用。還記得TortoiseSVN么?那只可愛的小烏龜。
5. perforce:是一款具有輕便快速的SCM工具、真正的客戶端/服務器系統等特點的商業軟件。高通內部使用的版本管理工具。確實不錯。
6. git:現在的最愛……
比較一下cvs,svn,和git:
Git 簡要
GIT 是一款免費的、開源的、分布式的版本控制系統。每一個GIT克隆都是一個完整的文件庫,含有全部歷史記錄和修訂追蹤能力。其最大特色就是“分支”及“合并”操作快速、簡便。支持離線工作,GIT是整個項目范圍的原子提交,而且GIT中的每個工作樹都包含一個具有完整項目歷史的倉庫。
核心特點:
-
Git 底層自行維護的存儲文件系統:存儲的是文件快照,即整個文件內容,并保存指向快照的索引
-
去中心化的分布式控制
優缺點:
優點:
-
適合分布式開發,強調個體。
-
公共服務器壓力和數據量都不會太大, 速度快、靈活。
-
任意兩個開發者之間可以很容易的解決沖突。
-
離線工作。
缺點:
-
學習周期相對而言比較長。
-
不符合常規思維。
-
代碼保密性差,一旦開發者把整個庫克隆下來就可以完全公開所有代碼和版本信息。
Git 原理
Git的目錄結構
不論通過git init 還是克隆下來的git 倉庫,都有如下的目錄結構:
主要目錄結構描述見下表:
子目錄名 | 簡要描述 |
---|---|
branches | Git 項目分支信息,新版 Git 已經不再使用該目錄 |
config | Git 項目配置信息 |
description | Git 項目描述信息 |
HEAD | 指向 Git 項目當前分支的頭指針 |
hooks | 默認的”hooks”腳本,被特定事件發生前后觸發。 |
info | 里面含一個 exclude 文件,指 Git 項目要忽略的文件。 |
objects | Git 的數據對象,包括:commits, trees, blobs, tags。 |
refs | 指向所有 Git 項目分支的指針 |
所有的分支指針保存在 .git/refs/heads 目錄下,HEAD 在 .git/HEAD 目錄下,標簽在 .git/refs/tags 目錄下。
快照
例如: 一個工程中有兩個文件A和B, 有3個版本:
V1.0 A和B,V1.5 A1和B,V2.0 A1和B1
在Git 的實際存儲中實際存了3個快照 4個文件。
Git對文件進行 SHA-1 計算作為文件的唯一ID,并校驗了文件完整性。
SHA-1 算法將文件中的內容通過計算生成一個 40 位的 Hash 值。SHA-1 算法的特點:
由文件內容計算出的 Hash 值;Hash 值相同,文件內容相同。
使用 SHA-1 的前兩位創建了文件夾,剩下的 38 位為文件名。
這些 Obj 文件,其實分為四種類型,分別是 Blob、Tree、Commit、Tag。
Blob
用來存放項目文件的內容,但是不包括文件的路徑、名字、格式等其它描述信息。項目的任意文件的任意版本都是以 Blob 的形式存放的。
Tree
Tree 用來表示目錄。我們知道項目就是一個目錄,目錄中有文件、有子目錄。因此 Tree 中有 Blob、子 Tree,且都是使用 SHA-1值引用的。這是與目錄對應的。從頂層的 Tree 縱覽整個樹狀的結構,葉子結點就是 Blob,表示文件的內容,非葉子結點表示項目的目錄,頂層的 Tree 對象就代表了當前項目的快照。
Commit
表示一次提交,有 Parent 字段,用來引用父提交。指向了一個頂層 Tree,表示了項目的快照,還有一些其它的信息,比如上一個提交 Committer、Author、Message 等信息。
存儲區
Git中有4個類型的存儲區:遠程倉庫,工作區,本地倉庫和緩存區。
暫存區的好處:
-
為了能夠實現部分提交
-
為了不在工作區創建狀態文件、會污染工作區。
-
暫存區記錄文件的修改時間等信息,提高文件比較的效率。
暫存區是用來構建項目快照的區域。暫存區是一個文件,路徑為: .Git/index
它是一個二進制文件,第四列是文件名,第三列是文件的沖突狀態,第二列指的是文件的 Blob。
Commit 命令,將暫存區的內容永久保存到本地倉庫。提交時 Git 會使用暫存區的這些信息生成 Tree 對象,也就是項目快照,永久保存到數據庫中。
文件的狀態可以分為兩類。一類是暫存區與本地倉庫比較得出的狀態,另一類是工作區與暫存區比較得出的狀態。為什么要分成兩類的愿意也很簡單,因為第一類狀態在提交時,會直接寫入本地倉庫。而第二種則不會。一個文件可以同時擁有兩種狀態。
分支
分支的目的是讓我們可以并行的進行開發。 .Git/HEAD 文件,它保存了當前的分支。
分支指向了一次提交,也是 Git 中的分支為什么這么輕量的原因。
因為分支就是指向了一個 Commit 的指針,當提交新的 Commit,這個分支的指向只需要跟著更新就可以了,而創建分支僅僅是創建一個指針。
Git 必備技能
常見命令速查
git add 和 git commit
Add 操作是將修改保存到暫存區,Commit 是將暫存區的內容永久保存到本地倉庫。
每當將修改的文件加入到暫存區,Git 都會根據文件的內容計算出 SHA-1,并將內容轉換成 Blob,寫入數據庫。然后使用 SHA-1 值更新該列表中的文件項。
在暫存區的文件列表中,每一個文件名,都會對應一個 SHA-1 值,用于指向文件的實際內容。最后提交的那一刻,Git 會將這個列表信息轉換為項目的快照,也就是 Tree 對象。寫入數據庫,并再構建一個 Commit 對象,寫入數據庫。然后更新分支指向。
分支合并: merge 和rebase
沖突的狀態
- DELETED_BY_THEM;
- DELETED_BY_US;
- BOTH_ADDED;
- BOTH_MODIFIED
遇到不可自動合并沖突時,Git 會將這些狀態寫入到暫存區。
merge
在解決完沖突后,我們可以將修改的內容提交為一個新的提交。這就是 Merge。
Merge 之后仍可以做出新的提交。
rebase
Rebase 會把從 Merge Base 以來的所有提交,以補丁的形式一個一個重新達到目標分支上。這使得目標分支合并該分支的時候會直接 Fast Forward,即不會產生任何沖突。
Rebase 主要在 .Git/Rebase-Merge 下生成了兩個文件,分別為 Git-Rebase-todo 和 Done 文件,Git-Rebase-todo 存放了 Rebase 將要操作的 Commit。而 Done 存放正在操作或已經操作完畢的 Commit。
Rebase 的一個缺點,那就是修改了分支的歷史提交。如果已經將分支推送到了遠程倉庫,會導致無法將修改后的分支推送上去,必須使用 -f 參數(Force)強行推送。
所以使用 Rebase 最好不要在公共分支上進行操作。
checkout
經常用來切換分支、或者切換到某一次提交。
Checkout 找到目標提交(Commit),目標提交中的快照也就是 Tree 對象就是我們要檢出的項目版本。
Checkout 首先根據Tree生成暫存區的內容,再根據 Tree 與其包含的 Blob 轉換成我們的項目文件。然后修改 HEAD 的指向,表示切換分支。
Checkout 并沒有修改提交的歷史記錄。只是將對應版本的項目內容提取出來。
revert
revert 實現了反向提交,就是舊版本添加了的內容,要在新版本中刪除;舊版本中刪除了的內容,要在新版本中添加。這在分支已經推送到遠程倉庫的情境下非常有用。
Revert 也不會修改歷史提交記錄,實際的操作相當于是檢出目標提交的項目快照到工作區與暫存區,然后用一個新的提交完成版本的“回退”。
reset
在當前分支進行版本的“回退”,Reset 是會修改歷史提交記錄的。
Reset 常用的選項有三個,分別是 —Soft, —Mixed, —Hard。他們的作用域依次增大。
Soft 會僅僅修改分支指向。而不修改工作區與暫存區的內容,
Mixed 比 Soft 的作用域多了一個 暫存區。實際上 Mixed 選項與 Soft 只差了一個 Add 操作。
Hard 會比 Mixed作用域又多了一個工作區。
注意:在丟失后可以使用 Git Reset –Hard ORIG_HEAD 立即恢復,或者使用 reflog 命令查看之前分支的引用
stash
有時,在一個分支上做了一些工作,修改了很多代碼,而這時需要切換到另一個分支干點別的事。但又不想將只做了一半的工作提交。
Stash 將工作區與暫存區中的內容做一個提交,保存起來,然后使用Reset Hard 選項恢復工作區與暫存區內容。我們可以隨時使用 Stash Apply 將修改應用回來。
Stash 實現思路將我們的修改提交到本地倉庫,使用特殊的分支指針(.Git/refs/Stash)引用該提交,然后在恢復的時候,將該提交恢復即可。
Git 典型實踐
一個典型的git 并行開發的流程模型如下:
主要分支
把origin/master作為主要分支,源碼的HEAD總是表示production-ready(可隨時部署)狀態。
origin/develop上的代碼是為下一次的代碼發布準備的。每日構建也是基于此分支。
當develop分支達到了一個穩定狀態并準備發布時,所有的改變都要合并到master分支,并標上版本號。
輔助分支
Feature branches
繼承與合并都與develop 分支相關,用來開發新特性的(短期,遠期都可以)。
當要創建一個新特性時,從develop分支上再創建一個 Feature branch。
$ git checkout -b myfeature develop
合并feature 到develop
$ gitcheckoutdevelop Switchedto branch 'develop' $ gitmerge --no-ffmyfeature Updatingea1b82a..05e9557 (Summaryofchanges) $ gitbranch -d myfeature Deletedbranchmyfeature (was 05e9557). $ gitpushorigindevelop
Release branches
繼承分支: develop
合并分支:develop 和 master
命名規范:release-*
創建一個release 分支
Release branch是通過develop分支而創建.
$ gitcheckout -b release-1.2 develop Switchedto a new branch "release-1.2" $ ./bump-version.sh 1.2 Filesmodifiedsuccessfully, versionbumpedto 1.2. $ gitcommit -a -m "Bumped version number to 1.2" [release-1.2 74d9424] Bumpedversionnumberto 1.2 1 fileschanged, 1 insertions(+), 1 deletions(-)
完成一個release 分支
當release branch已經準備就緒,需要做幾件事。
- release分支被合并到master分支上(每一個提交到master上的commit都是一個新版 本,切記)。
- master上的commit都要添加tag,方便將來查看和回滾。
- release上所做的修改必須合并到develop分支上,保證bug已被修補。
前兩個步驟:
$ gitcheckoutmaster Switchedto branch 'master' $ gitmerge --no-ffrelease-1.2 Mergemadebyrecursive. (Summaryofchanges) $ gittag -a 1.2
為了把release上的改變保存到develop,需要合并到develop。
$ gitcheckoutdevelop Switchedto branch 'develop' $ gitmerge --no-ffrelease-1.2 Mergemadebyrecursive. (Summaryofchanges)
這個步驟可能會導致沖突,如果這樣的話,解決沖突,然后再提交。
最后,可以刪除release 分支。
$ gitbranch -d release-1.2 Deletedbranchrelease-1.2 (wasff452fe).
Hotfix branches
繼承分支: master
合并分支:develop 和 master
命名規范:hotfix-*
運行過程中發現了bug,就必須快速解決,這時就可以創建一個Hotfix branch,解決完后合并到master分支上。好處是開發人員可以繼續工作,有專人來負責搞定這個bug。
創建hotfix分支
$ gitcheckout -b hotfix-1.2.1 master Switchedto a new branch "hotfix-1.2.1" $ ./bump-version.sh 1.2.1 Filesmodifiedsuccessfully, versionbumpedto 1.2.1. $ gitcommit -a -m "Bumped version number to 1.2.1" [hotfix-1.2.1 41e61bb] Bumpedversionnumberto 1.2.1 1 fileschanged, 1 insertions(+), 1 deletions(-)
fix bug, 解決問題
需要一次或幾次commit
$ gitcommit -m "Fixed severe production problem" [hotfix-1.2.1 abbe5d6] Fixedsevereproductionproblem 5 fileschanged, 32 insertions(+), 17 deletions(-)
完成Hotfix branch
當結束時,bugfix要被合并到master,同時也要合并到develop,保證下個版本發布時該bug已被修復。這跟release branch完成時一樣。
首先更新master和tag release
$ gitcheckoutmaster Switchedto branch 'master' $ gitmerge --no-ffhotfix-1.2.1 Mergemadebyrecursive. (Summaryofchanges) $ gittag -a 1.2.1
接下來與develop合并
$ gitcheckoutdevelop Switchedto branch 'develop' $ gitmerge --no-ffhotfix-1.2.1 Mergemadebyrecursive. (Summaryofchanges)
有一個例外,就是當一個release branch存在時,bugfix要被合并到release而不是develop,因為release最終會被合并到develop。
最后移除branch
$ gitbranch -d hotfix-1.2.1 Deletedbranchhotfix-1.2.1 (wasabbe5d6).
總結
了解Git 在軟件工程及敏捷開發中的地位,明白git與其他版本控制工具之間的區別,掌握Git 工作的基本原理和必備操作,復雜問題可以查找git的相關命令,應用git開發的流程模型,讓Git 成為我們的真正利器。
參考資料:
1) http://nvie.com/posts/a-successful-git-branching-model/
來自:http://blog.jobbole.com/107027/