Coding WebIDE 是 Coding.net 自主研發(fā)的在線集成開發(fā)環(huán)境 (IDE)。你可以通過 WebIDE 創(chuàng)建項目的工作空間, 進行在線開發(fā), 調試等操作,有功能健全的 Terminal。由于 Git 使用門檻偏高, WebIDE 提供了便利的 GUI 界面,在此前,WebIDE 實現(xiàn)了基本的 Git 客戶端特性。本次更新,增加了 merge,stash,rebase,reset, tags 幾個高級特性,使得開發(fā)者使用 WebIDE 的效率大大提升!以下為 Coding.net 工程師在實現(xiàn) WebIDE 中 Git 功能的心得分享。
管理文檔、程序、配置等文件內容變化的的系統(tǒng)。
其實版本控制很想 并不難理解,其實即使不是編程人員對他也不會陌生,比如 windows 的系統(tǒng)還原,mac 的 timemachine。他們在某一時刻,記錄下系統(tǒng)的狀態(tài)或文件的內容,然后在需要的時候可以恢復。
對于程序員來說,他有以下好處:
恢復:當不小心刪除了文件、或者改錯了文件,可以恢復文件內容
回滾:新版本出現(xiàn)了重大問題,可以回滾到上一正確的狀態(tài)。
協(xié)作:不同開發(fā)者根據(jù)同一個版本進行開發(fā),形成不同版本可以方便的合并在一起。
常見的版本控制系統(tǒng)有 CVS、SVN、Mercurial、Git 等。
這四個版本控制系統(tǒng)可以根據(jù)對網(wǎng)絡的要求分成兩組,一組是CVS、SVN,一組是 Mercurial、Git。
CVS、SVN:使用中央倉庫,開發(fā)者需要從中央倉庫中取出代碼
Mercurial、Git:使用本地倉庫,開發(fā)者可以本地開發(fā)
第一組要求必須連到公司的網(wǎng)絡才能辦公,而第二組倉庫在本地,意味著不用連接到公司的網(wǎng)絡,進一步可以說是離線就可以辦公。
像Git、Mercurial這樣的分布式的版本控制系統(tǒng)變得越來越流行,正在慢慢取代像CVS、SVN“中央式“的版本控制系統(tǒng)。
是什么原因讓 git 從這么多的版本控制系統(tǒng)中脫穎而出呢?
本地提交: 這意味著無論你是在家里、還是地鐵上都可以離線工作了,不需要連到公司的網(wǎng)絡。
輕量級分支: git 的輕量級分支使得你可以快速的切換項目版本。這種特性在某些場景下特別重要,尤其是當我們正在開發(fā)過程中,突然發(fā)現(xiàn)一個緊急bug需要修復,我們可以快速切換分支,修復bug。
解決沖突方便: 正因為有輕量級的分支,git也鼓勵我們使用分支進行開發(fā)。但是當我們將分支合并到主干時,不可避免的會出現(xiàn)沖突,而 git 解決沖突的方式對用戶非常的友好
有 Github、Coding 這樣強大的代碼托管平臺支持: 在 Github 和 Coding 上有非常多的開源代碼,而且這兩個平臺上的用戶非常的活躍,使用 git,有助于接觸更多優(yōu)秀的項目、優(yōu)秀的開發(fā)者,對我們的成長有非常大的幫助。
一段經(jīng)典的 git 操作。
touch README.mdgit add README.mdgit commit -m "add readme"
touch READEME.md
可以代表創(chuàng)建、修改文件操作git add README.md
表示將對文件的改動添加到暫存區(qū) git commit -m "add readme"
表示將改動提交到倉庫
這些我們都已經(jīng)知道了,那么添加到暫存區(qū)、提交到倉庫具體是什么意思?
git 有三種狀態(tài):工作區(qū)、暫存區(qū)、本地倉庫。
add: 工作區(qū) -> 暫存區(qū)
commit: 暫存區(qū) -> 本地倉庫
工作目錄我們是知道的,我們平時編寫代碼,就是在工作目錄中完成的。
暫存區(qū)也叫做索引,保存了下次將提交的文件列表。
本地倉庫是 Git 用來保存項目的數(shù)據(jù)的地方。提交代碼,意味著將文件內容永久保存到數(shù)據(jù)庫中。
首先看一下本地倉庫,項目中的文件在本地倉庫中是以快照的形式來保存的。
每一個 version,都是項目的一次完整快照。而快照中沒有修改的文件,Git 使用鏈接指向之前存儲的文件。
這就帶來了一個問題,鏈接是什么?怎么快速的知道文件內容是否發(fā)生了改變?git 中的方案是使用 SHA-1。
echo 'test content' | git hash-object --stdind670460b4b4aece5915caf5c68d12f560a9fe3e4
特點:
SHA-1將文件中的內容通過算法生成一個 160bit 的報文摘要,即40個十六進制數(shù)字。SHA-1的一個重要特征就是幾乎可以保證,如果兩個文件的SHA-1值是相同的,那么它們確是完全相同的內容。
上面的代碼,無論運行幾次,得到的 hash 值都是一樣的。這個hash值可以看作是該文件的唯一id。
Git 中所有數(shù)據(jù)在存儲前都計算該hash值,然后用該hash值來引用。因此這個 id 除了可以唯一表示任何版本中的文件,還可以表示任何一次提交、任何一次代碼的快照。
find .git/objects -type f
我們來看一下實際存儲在git中的數(shù)據(jù),看起來比較亂,這些數(shù)據(jù)存放在 .git/objects,然后使用sha-1計算的hash值的前兩位作為文件夾的名字,后面的38位作為文件的名字。
在這么多的文件中,其實可以分為4種類型,分別是 blob、commit、tree 和 tag。
將上面的內容經(jīng)過按照這些類型整理可以得到類似下面的關系(忽略 tag)。
每一個線框表示了一個object,也就是 objects 目錄下的一個文件。
每個 object 上面的這個字母與數(shù)字組合的字符串,就是object的上一目錄名+文件名,也就是 sha-1 hash 值。
每個 object 的第一行格式是一致的,都由兩列組成,第一列表示了 object 的類型,第二列是文件內容的長度。
接下來我們分別看一下每種類型:
blob: 用來存放項目文件的內容,項目的任意文件的任意版本都是以 blob 的形式存放的。但是不包括文件的路徑、名字、格式等其它描述信息。
tree: 用來表示項目中的目錄,我們知道,目錄中有文件、有子目錄。因此 tree 中有 blob、子 tree。這是與目錄的對應。tree 中還包含了文件的路徑以及名稱。從頂層的 tree 縱覽整個樹狀的結構,葉子結點就是blob,表示文件的內容,非葉子結點表示項目的目錄,那么頂層的 tree 對象就代表了當前項目的快照。
commit: 一個commit表示一次提交。里面的 tree 的值指向了項目的快照。還有一些其它的信息,比如 parent,committer、author、message 等信息。
tree 看成一個樹狀的結構,blob 可以作為其中的葉子結點出現(xiàn)。commit 可以看作是一個DAG,有向無環(huán)圖。因為 commit 可以有一個 parent,也可以有兩個或者多個parent。
至此,本地倉庫我們就了解完了。接下來看一下暫存區(qū)。
暫存區(qū)是工作區(qū)與本地倉庫之間的一個緩沖,它保存了下次將提交的文件列表信息。它其實是一個文件,路徑為: .git/index
。由于該文件是一個二進制文件,沒辦法直接看它的內容,但是可以使用 git 命令查看。
每列的含義依次為,文件權限、文件 blob、文件狀態(tài)、文件名。
第二列指的是文件的 blob。這個 blob 存放了文件暫存時的內容。
我們操作暫存區(qū)的場景是這樣的,每當編輯好一個或幾個文件后,把它加入到暫存區(qū),然后接著修改其他文件,改好后放入暫存區(qū),循環(huán)反復。直到修改完畢,最后使用 commit 命令,將暫存區(qū)的內容永久保存到本地倉庫。
這個過程其實就是構建項目快照的過程,因此可以說暫存區(qū)是用來構建項目快照的區(qū)域。
接下來看一下分支的概念,首先看一張圖:
這張圖中的每一個點表示了一個commit。從這張圖中我們可以看出的信息有:
從任意一點分歧出來的線都可以叫做分支
分支可以合并
在 .git/HEAD 文件中,保存了當前的分支。
cat .git/HEAD=>ref: refs/heads/master
其實這個 ref 表示的就是一個分支,它也是一個文件,我們可以繼續(xù)看一下這個文件的內容:
cat .git/refs/heads/master=> 2b388d2c1c20998b6233ff47596b0c87ed3ed8f8
可以看到分支存儲了一個 object,我們可以使用 cat-file 命令繼續(xù)查看該 object 的內容。
git cat-file -p 2b388d2c1c20998b6233ff47596b0c87ed3ed8f8=> tree 15f880be0567a8844291459f90e9d0004743c8d9=> parent 3d885a272478d0080f6d22018480b2e83ec2c591=> author Hehe Tan <xiayule148@gmail.com> 1460971725 +0800=> committer Hehe Tan <xiayule148@gmail.com> 1460971725 +0800=> => add branch paramter for rebase
從上面的內容,我們知道了分支指向了一次提交。為什么分支指向一個提交的原因,其實也是git中的分支為什么這么輕量的答案。
因為分支就是指向了一個 commit 的指針,當我們提交新的commit,這個分支的指向只需要跟著更新就可以了,而創(chuàng)建分支僅僅是創(chuàng)建一個指針。
至此git的原理就講完了,接下來看一下 JGit。
JGit 是一個用 Java 實現(xiàn)的比較健全的 git 實現(xiàn),Eclipse IDE 中的 git 插件 Egit,就是基于 JGit 開發(fā)的。同 git 一樣,它提供了底層命令和高層命令。
高層命令的入口是 Git 類。高層命令好理解,我們使用 git 的客戶端絕大多數(shù)命令都是高層命令。
比如 add、commit、checkout 等都是高層命令,他們提供了友好的交互,往往一條命令就能完成你所想要的效果。
底層命令的入口是 Repository 類。底層命令不同于高層命令,它們直接作用域 倉庫(Repository)。比如 AbstractTreeIterator,就是用來遍歷 Tree 結構的,DirCache 是用來操作暫存區(qū)的,RevWalk 是用來遍歷 commit 的,ObjectInsert 是用來生成 obj的,ObjectLoader 是用來加載 object。
一條高層命令往往是由多條底層命令組成的。
作為一切的開始,你需要一個 Repository。
Repository repository = new FileRepositoryBuilder() .setGitDir(new File("/home/tan/GitTest/.git")) .readEnvironment() .build();
使用時只需要將倉庫的路徑傳進來就可以了,它會自動讀取一些必要的環(huán)境變量。
ObjectInserter用來將數(shù)據(jù)插入到git數(shù)據(jù)庫中,也就是 objects 目錄下。插入的類型為我們剛才提到的四種,分別是 Blob、Tree、Commit、Tag。
try (ObjectInserter inserter = repo.newObjectInserter()) { ObjectId objectId = inserter.insert(Constants.OBJ_BLOB, new String("test").getBytes()); inserter.flush();}
第二個參數(shù)表示要插入的數(shù)據(jù),該數(shù)據(jù)會自動使用 zlib 壓縮。
用來遍歷目錄結構,可以為工作區(qū)、暫存區(qū)或項目快照(版本庫)。
try (TreeWalk treeWalk = new TreeWalk(repo)) { treeWalk.setRecursive(true); treeWalk.addTree(new FileTreeIterator(repo)); treeWalk.addTree(new DirCacheIterator(repo.readDirCache())); while (treeWalk.next()) { AbstractTreeIterator treeIterator = treeWalk.getTree(0, AbstractTreeIterator.class); DirCacheIterator dirCacheIterator = treeWalk.getTree(1, DirCacheIterator.class); }}
TreeWalk 是用來遍歷樹這種結構的,它比較厲害的一點是可以同時遍歷多棵樹,遍歷多課樹的思路為將文件列表做一個合并,然后遍歷這個列表,沒有的調用 getTree 會返回 null 值。
其實 git status 就是這種原理來做的:
RevWalk 用來遍歷 Commit。
try (RevWalk revWalk = new RevWalk(repository)) { revWalk.markStart(one); revWalk.markStart(two); revWalk.setRevFilter(RevFilter.MERGE_BASE); RevCommit base = revWalk.next();}
我們這個例子,標記了兩個 commit,我們設置的 filter 是 MERGE_BASE, 它會自動查找這兩個 commit 所在分支的 MERGE_BASE。其中 MERGE_BASE 可以看作是分支的分岔點,合并的時候 MERGE_BASE 會作為參照。
高層命令其實是由多條底層命令組成的,比如我們最常使用的 add、commit:
add
commit
上面的復雜操作,可以簡單的用底層命令替代。
git.add().addFilepattern("README").call();git.commit().setMessage("add readme").call();
高層命令使用起來方便,但是它所提供的功能有限。這里我們拿 merge 舉例。
使用 JGit 提供的接口進行 merge 十分的方便,只需要指定要合并的 branch 就可以了。
MergeCommand merge = git.merge();merge.include(branch);MergeResult result = merge.call();
但是 merge 之后呢,文件沖突了怎么辦,怎么解決沖突呢?實際上除了 merge,stash、rebase 等等操作也都會產(chǎn)生沖突。也就是說 git 沖突文件的處理是客戶端的重要功能之一。
遺憾的是 jgit 并沒有提供解決沖突的方案,所以這就需要我們自己來解決這個問題。
一種比較理想的解決沖突的方案是,將沖突的文件根據(jù)本地修改、基礎版本、要合并分支的修改分成三欄。
通過這樣的方式,我們可以直觀的對照沖突的內容,并且可以方便的選取或者要拋棄修改。
計算 merge base
第一個就是計算這兩個分支的 MERGE_BASE。這樣我們獲得了三個 commit,每個 commit 都都紀錄了提交時的文件快照。而我們只要將沖突文件的內容從快照中取出來就好了。但是這個方案有個缺點,那就是我們只有在合并的那一瞬間才能知道要合并的分支,之后想要知道只能去 .git 下面的 MERGE_HEAD 去查,而且其它方式比如 stash、rebase 等操作引起的沖突是不會生成該文件的。
使用暫存區(qū)的信息
想想我們 當我們有合并沖突狀態(tài)時,使用 git status,會列出沖突文件,以及沖突的類型,比如 “雙方修改”、“由我們刪除”,“雙方添加”等這樣的字眼,git 如果獲得這些信息的呢?
如果存在沖突文件,我們查看暫存區(qū),可以看到類似下面的內容:
git ls-files --stage100644 6e9f0da13f19b444ec3a9c3d6e795ad35c0554a2 1 Readme100644 29d460866c44ad72cc08ef4983fc6ebd48053bab 2 Readme100644 12892f544e81ef2170034392f63c7fc5e6c6ccd9 3 Readme
原來暫存區(qū)中有四種狀態(tài)用于標示文件:
* 0: standard stage
* 1: base tree revision
* 2: first tree revision (usually called "ours")
* 3: second tree revision (usually called "theirs")
接下來我們專門看一下這4種狀態(tài)是如何表示沖突狀態(tài)的。
假設當前我們處于 master 分支,要合并的分支為 test,開發(fā)歷史如下圖:
現(xiàn)在假設合并過程中有個文件(Readme)發(fā)生了沖突,我們查詢暫存區(qū)該文件的狀態(tài)(可以有多個):
我們拿第一種情況舉例,文件(Readme)有兩種狀態(tài) 1 和 2,1 表示該文件存在于 commit 1(也就是MERGE_BASE),2 表示該文件在 commit 2 (master 分支)中被修改了,沒有狀態(tài) 3,也就是該文件在 commit 3(test分支)被刪除了,總結來說這種狀態(tài)就是 DELETED_BY_THEM。
可以再看一下第四種情況,文件(Readme)有三種狀態(tài) 1、2、3,1 表示 commit 1(MERGE_BASE)中存在,2 表示 commit 2(master 分支)進行了修改,3 表示(test 分支)也就行了修改,總結來說就是 BOTH_MODIFIED(雙方修改)。
知道了沖突文件的狀態(tài),就能在暫存區(qū)獲得沖突文件的三個版本了。代碼如下:
DirCache dirCache = repository.readDirCache();// 在暫存區(qū)中,所有文件是按照字母順序排列的,因此文件的不同狀態(tài)是連著的int eIdx = dirCache.findEntry(path);// nextEntry 會自動調過文件名相同的文件,找到下一個文件。int lastIdx = dirCache.nextEntry(eIdx);// 在 [eIdx, lastIdx) 區(qū)間的也就是文件的沖突的不同版本for (int i=0; i<lastIdx - eIdx; i++) { DirCacheEntry entry = dirCache.getEntry(eIdx + i); // 如果是 MERGE_BASE if (entry.getStage() == DirCacheEntry.STAGE_1) readBlobContent(entry.getObjectId()); // 如果是 當前分支 else if (entry.getStage() == DirCacheEntry.STAGE_2) readBlobContent(entry.getObjectId()); // 如果是 要合并的分支 else if (entry.getStage() == DirCacheEntry.STAGE_3) readBlobContent(entry.getObjectId());}
至此我們得到了解決合并沖突的一個方案。
Happy Coding;)