Coding WebIDE 是 Coding.net 自主研發的在線集成開發環境 (IDE)。你能夠經過 WebIDE 建立項目的工做空間, 進行在線開發, 調試等操做,有功能健全的 Terminal。因爲 Git 使用門檻偏高, WebIDE 提供了便利的 GUI 界面,在此前,WebIDE 實現了基本的 Git 客戶端特性。本次更新,增長了 merge,stash,rebase,reset, tags 幾個高級特性,使得開發者使用 WebIDE 的效率大大提高!如下爲 Coding.net 工程師在實現 WebIDE 中 Git 功能的心得分享。css
管理文檔、程序、配置等文件內容變化的的系統。git
其實版本控制很想 並不難理解,其實即便不是編程人員對他也不會陌生,好比 windows 的系統還原,mac 的 timemachine。他們在某一時刻,記錄下系統的狀態或文件的內容,而後在須要的時候能夠恢復。程序員
對於程序員來講,他有如下好處:算法
恢復:當不當心刪除了文件、或者改錯了文件,能夠恢復文件內容sql
回滾:新版本出現了重大問題,能夠回滾到上一正確的狀態。數據庫
協做:不一樣開發者根據同一個版本進行開發,造成不一樣版本能夠方便的合併在一塊兒。編程
常見的版本控制系統有 CVS、SVN、Mercurial、Git 等。windows
這四個版本控制系統能夠根據對網絡的要求分紅兩組,一組是CVS、SVN,一組是 Mercurial、Git。api
CVS、SVN:使用中央倉庫,開發者須要從中央倉庫中取出代碼markdown
Mercurial、Git:使用本地倉庫,開發者能夠本地開發
第一組要求必須連到公司的網絡才能辦公,而第二組倉庫在本地,意味着不用鏈接到公司的網絡,進一步能夠說是離線就能夠辦公。
像Git、Mercurial這樣的分佈式的版本控制系統變得愈來愈流行,正在慢慢取代像CVS、SVN「中央式「的版本控制系統。
是什麼緣由讓 git 從這麼多的版本控制系統中脫穎而出呢?
本地提交: 這意味着不管你是在家裏、仍是地鐵上均可以離線工做了,不須要連到公司的網絡。
輕量級分支: git 的輕量級分支使得你能夠快速的切換項目版本。這種特性在某些場景下特別重要,尤爲是當咱們正在開發過程當中,忽然發現一個緊急bug須要修復,咱們能夠快速切換分支,修復bug。
解決衝突方便: 正由於有輕量級的分支,git也鼓勵咱們使用分支進行開發。可是當咱們將分支合併到主幹時,不可避免的會出現衝突,而 git 解決衝突的方式對用戶很是的友好
有 Github、Coding 這樣強大的代碼託管平臺支持: 在 Github 和 Coding 上有很是多的開源代碼,並且這兩個平臺上的用戶很是的活躍,使用 git,有助於接觸更多優秀的項目、優秀的開發者,對咱們的成長有很是大的幫助。
一段經典的 git 操做。
touch README.md git add README.md git commit -m "add readme"
touch READEME.md
能夠表明建立、修改文件操做git add README.md
表示將對文件的改動添加到暫存區 git commit -m "add readme"
表示將改動提交到倉庫
這些咱們都已經知道了,那麼添加到暫存區、提交到倉庫具體是什麼意思?
git 有三種狀態:工做區、暫存區、本地倉庫。
add: 工做區 -> 暫存區
commit: 暫存區 -> 本地倉庫
工做目錄咱們是知道的,咱們平時編寫代碼,就是在工做目錄中完成的。
暫存區也叫作索引,保存了下次將提交的文件列表。
本地倉庫是 Git 用來保存項目的數據的地方。提交代碼,意味着將文件內容永久保存到數據庫中。
首先看一下本地倉庫,項目中的文件在本地倉庫中是以快照的形式來保存的。
每個 version,都是項目的一次完整快照。而快照中沒有修改的文件,Git 使用連接指向以前存儲的文件。
這就帶來了一個問題,連接是什麼?怎麼快速的知道文件內容是否發生了改變?git 中的方案是使用 SHA-1。
echo 'test content' | git hash-object --stdin d670460b4b4aece5915caf5c68d12f560a9fe3e4
特色:
SHA-1將文件中的內容經過算法生成一個 160bit 的報文摘要,即40個十六進制數字。SHA-1的一個重要特徵就是幾乎能夠保證,若是兩個文件的SHA-1值是相同的,那麼它們確是徹底相同的內容。
上面的代碼,不管運行幾回,獲得的 hash 值都是同樣的。這個hash值能夠看做是該文件的惟一id。
Git 中全部數據在存儲前都計算該hash值,而後用該hash值來引用。所以這個 id 除了能夠惟一表示任何版本中的文件,還能夠表示任何一次提交、任何一次代碼的快照。
find .git/objects -type f
咱們來看一下實際存儲在git中的數據,看起來比較亂,這些數據存放在 .git/objects,而後使用sha-1計算的hash值的前兩位做爲文件夾的名字,後面的38位做爲文件的名字。
在這麼多的文件中,其實能夠分爲4種類型,分別是 blob、commit、tree 和 tag。
將上面的內容通過按照這些類型整理能夠獲得相似下面的關係(忽略 tag)。
每個線框表示了一個object,也就是 objects 目錄下的一個文件。
每一個 object 上面的這個字母與數字組合的字符串,就是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 能夠做爲其中的葉子結點出現。commit 能夠看做是一個DAG,有向無環圖。由於 commit 能夠有一個 parent,也能夠有兩個或者多個parent。
至此,本地倉庫咱們就瞭解完了。接下來看一下暫存區。
暫存區是工做區與本地倉庫之間的一個緩衝,它保存了下次將提交的文件列表信息。它實際上是一個文件,路徑爲: .git/index
。因爲該文件是一個二進制文件,沒辦法直接看它的內容,可是可使用 git 命令查看。
每列的含義依次爲,文件權限、文件 blob、文件狀態、文件名。
第二列指的是文件的 blob。這個 blob 存放了文件暫存時的內容。
咱們操做暫存區的場景是這樣的,每當編輯好一個或幾個文件後,把它加入到暫存區,而後接着修改其餘文件,改好後放入暫存區,循環反覆。直到修改完畢,最後使用 commit 命令,將暫存區的內容永久保存到本地倉庫。
這個過程其實就是構建項目快照的過程,所以能夠說暫存區是用來構建項目快照的區域。
接下來看一下分支的概念,首先看一張圖:
這張圖中的每個點表示了一個commit。從這張圖中咱們能夠看出的信息有:
從任意一點分歧出來的線均可以叫作分支
分支能夠合併
在 .git/HEAD 文件中,保存了當前的分支。
cat .git/HEAD =>ref: refs/heads/master
其實這個 ref 表示的就是一個分支,它也是一個文件,咱們能夠繼續看一下這個文件的內容:
cat .git/refs/heads/master => 2b388d2c1c20998b6233ff47596b0c87ed3ed8f8
能夠看到分支存儲了一個 object,咱們可使用 cat-file 命令繼續查看該 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,這個分支的指向只須要跟着更新就能夠了,而建立分支僅僅是建立一個指針。
至此git的原理就講完了,接下來看一下 JGit。
JGit 是一個用 Java 實現的比較健全的 git 實現,Eclipse IDE 中的 git 插件 Egit,就是基於 JGit 開發的。同 git 同樣,它提供了底層命令和高層命令。
高層命令的入口是 Git 類。高層命令好理解,咱們使用 git 的客戶端絕大多數命令都是高層命令。
好比 add、commit、checkout 等都是高層命令,他們提供了友好的交互,每每一條命令就能完成你所想要的效果。
底層命令的入口是 Repository 類。底層命令不一樣於高層命令,它們直接做用域 倉庫(Repository)。好比 AbstractTreeIterator,就是用來遍歷 Tree 結構的,DirCache 是用來操做暫存區的,RevWalk 是用來遍歷 commit 的,ObjectInsert 是用來生成 obj的,ObjectLoader 是用來加載 object。
一條高層命令每每是由多條底層命令組成的。
做爲一切的開始,你須要一個 Repository。
Repository repository = new FileRepositoryBuilder()
.setGitDir(new File("/home/tan/GitTest/.git")) .readEnvironment() .build();
使用時只須要將倉庫的路徑傳進來就能夠了,它會自動讀取一些必要的環境變量。
ObjectInserter用來將數據插入到git數據庫中,也就是 objects 目錄下。插入的類型爲咱們剛纔提到的四種,分別是 Blob、Tree、Commit、Tag。
try (ObjectInserter inserter = repo.newObjectInserter()) {
ObjectId objectId = inserter.insert(Constants.OBJ_BLOB, new String("test").getBytes()); inserter.flush(); }
第二個參數表示要插入的數據,該數據會自動使用 zlib 壓縮。
用來遍歷目錄結構,能夠爲工做區、暫存區或項目快照(版本庫)。
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 等等操做也都會產生衝突。也就是說 git 衝突文件的處理是客戶端的重要功能之一。
遺憾的是 jgit 並無提供解決衝突的方案,因此這就須要咱們本身來解決這個問題。
一種比較理想的解決衝突的方案是,將衝突的文件根據本地修改、基礎版本、要合併分支的修改分紅三欄。
經過這樣的方式,咱們能夠直觀的對照衝突的內容,而且能夠方便的選取或者要拋棄修改。
計算 merge base
第一個就是計算這兩個分支的 MERGE_BASE。這樣咱們得到了三個 commit,每一個 commit 都都紀錄了提交時的文件快照。而咱們只要將衝突文件的內容從快照中取出來就行了。可是這個方案有個缺點,那就是咱們只有在合併的那一瞬間才能知道要合併的分支,以後想要知道只能去 .git 下面的 MERGE_HEAD 去查,並且其它方式好比 stash、rebase 等操做引發的衝突是不會生成該文件的。
使用暫存區的信息
想一想咱們 當咱們有合併衝突狀態時,使用 git status,會列出衝突文件,以及衝突的類型,好比 「雙方修改」、「由咱們刪除」,「雙方添加」等這樣的字眼,git 若是得到這些信息的呢?
若是存在衝突文件,咱們查看暫存區,能夠看到相似下面的內容:
git ls-files --stage
100644 6e9f0da13f19b444ec3a9c3d6e795ad35c0554a2 1 Readme 100644 29d460866c44ad72cc08ef4983fc6ebd48053bab 2 Readme 100644 12892f544e81ef2170034392f63c7fc5e6c6ccd9 3 Readme
原來暫存區中有四種狀態用於標示文件:
* 0: standard stage
* 1: base tree revision
* 2: first tree revision (usually called "ours")
* 3: second tree revision (usually called "theirs")
接下來咱們專門看一下這4種狀態是如何表示衝突狀態的。
假設當前咱們處於 master 分支,要合併的分支爲 test,開發歷史以下圖:
如今假設合併過程當中有個文件(Readme)發生了衝突,咱們查詢暫存區該文件的狀態(能夠有多個):
咱們拿第一種狀況舉例,文件(Readme)有兩種狀態 1 和 2,1 表示該文件存在於 commit 1(也就是MERGE_BASE),2 表示該文件在 commit 2 (master 分支)中被修改了,沒有狀態 3,也就是該文件在 commit 3(test分支)被刪除了,總結來講這種狀態就是 DELETED_BY_THEM。
能夠再看一下第四種狀況,文件(Readme)有三種狀態 一、二、3,1 表示 commit 1(MERGE_BASE)中存在,2 表示 commit 2(master 分支)進行了修改,3 表示(test 分支)也就好了修改,總結來講就是 BOTH_MODIFIED(雙方修改)。
知道了衝突文件的狀態,就能在暫存區得到衝突文件的三個版本了。代碼以下:
DirCache dirCache = repository.readDirCache();
// 在暫存區中,全部文件是按照字母順序排列的,所以文件的不一樣狀態是連着的
int eIdx = dirCache.findEntry(path);
// nextEntry 會自動調過文件名相同的文件,找到下一個文件。
int lastIdx = dirCache.nextEntry(eIdx);
// 在 [eIdx, lastIdx) 區間的也就是文件的衝突的不一樣版本
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;)