這篇短文將介紹如何用500行的Javascript代碼,寫一個你本身專屬的GIT。 這不是一個如何使用GIT的工具,而是GIT的底層實現。目的是但願能加深對GIT的底層實現原理,而不是想換掉GIT,這只是一個GIT的雛形而已代碼來自開源,也迴流開源,有須要且不嫌棄的能夠上去看看 https://github.com/notechsolution/gitdoujavascript
跟GIT的結緣開始於2011年,公司決定不用原來的IBM Clearcase,改用開源的GIT。做爲當時GIT的內部support,確實有很長一段時間跟它廝混在一塊兒。後來還寫了幾篇如何使用GIT的文章,有空能夠翻翻 GIT七年之癢. 前兩年回一下爐,又寫了幾篇 GIT入門.html
最近看到一個叫Richard Feynman的人說過這麼一句話java
What I cannot create, I do not understand - Richard Feynman
嗯嗯,有點意思,扒拉了一下,還有很多人用Javascript寫GIT。此次的實現主要也是參考了其中一個叫gitlet的git
GIT是Linux Torvalds用C語言寫的。小的不才不懂C,那就用Javascript寫寫吧, ES6 可讓代碼能夠寫得比較簡潔。既然重造輪子,那就儘可能少用框架吧。可是做爲lodash粉,仍是忍不住了,最後仍是用了lodash~~~. github
固然,Pivotal Lab中毒較深,作個練習也離不開TDD,因此此次也用了Ava做爲testing框架。 但功力尚淺,有些case也偷懶了,testcase跟代碼的函數比例只作到1:1, 500行的代碼只有500行的unittest。數據庫
此次的目的是爲了加深對GIT底層實現原理的理解,而不是作出一個真正的產品出來,因此對於用戶操做沒有作出各類友好的提醒,好比沒有像Already up to date
這樣的提醒等等,只要實現了GIT的以下核心命令:服務器
下面嘗試逐一來解釋一下每一個命令是幹什麼的。框架
首先是初始化一個GIT的項目。GIT在某種程度上能夠理解爲一個文件的數據庫,裏面保存着全部文件的全部版本。初始化的過程也就是建立各個文件以及目錄.函數
.gitdou ├── HEAD ├── config ├── objects └── refs ├── heads
ref: refs/heads/master
初始化的過程就是在指定的目錄.gitdou
下生成這些目錄及文件的過程。代碼就比較簡單,根據目錄結構,生成對應的文件樹:工具
init: () => { const gitdouStructure = { HEAD: 'ref: refs/heads/master', objects: {}, refs: { heads: {} }, config: JSON.stringify({core: {bare: false}}, null, 2) } files.writeFilesFromTree({'.gitdou': gitdouStructure}, process.cwd()); },
前面說到了git實際是一個數據庫,存放了全部文件的全部歷史版本。爲了更方便高效地查詢,數據庫都會創建索引。git也不例外,它也有一個index文件,記錄全部文件的路徑,這些文件的狀態以及當前版本的hash值。
add
命令就是將指定路徑的全部文件的路徑,狀態以及當前的hash值記錄保存到index文件裏面。其實現過程就是掃出指定目錄下的全部文件,逐一計算他們的hash值,而後寫到index文件裏面
add: path => { const addedFiles = files.listAllMatchedFiles(path); index.updateFilesIntoIndex(addedFiles, {add: true}); }
有添加命令,對應的也就應該有刪除命令。其過程跟add基本一致,只不過多了一步把要刪除的文件從當前workingCopy裏面刪除掉。
rm: path => { const deletedFiles = files.listAllMatchedFiles(path); index.updateFilesIntoIndex(deletedFiles, {remove: true}); files.removeFiles(deletedFiles); }
當任務已經到一段落,咱們須要給當前版本作一個快照,方便之後找回。這時咱們能夠作一個commit。這個commit將會包含一個hash樹,這棵樹將當前版本的全部文件連起來。固然還包含了一些commit的metadata,好比誰,何時commit,commit的備註是什麼等等。
具體實現大體爲:
commit: option => { // write current index into tree object const treeHash = gitdou.write_tree(); // create commit object based on the tree hash const parentHash = refs.getParentHash(); const commitHash = objects.createCommit({treeHash, parentHash, option}); // point the HEAD to commit hash refs.updateRef({updateToRef: 'HEAD', hash: commitHash}) }
GIT的分支管理是可能稍微複雜一些,不一樣公司,不一樣的開發模式會有不一樣的分支管理,甚至有人將這個上升到分支管理的藝術的高度。最有名的分支管理模型應該就是A successful Git branching model
但... 但... branch在GIT的實現裏面能夠說是最最簡單的一個了,所謂建立branch就是在.gitdou\refs\heads
建立一個用branch名字命名的文件,文件的內容就是當前的hash. 忽然想起某學習機廣告:SO EASY~~~
branch : (name, opts) => { const hash = refs.hash('HEAD'); refs.updateRef({updateToRef:name, hash}); },
不能都是那麼容易的啦!要不也不用花這麼多時間寫!checkout就稍複雜一些。checkout有點相似於還原現場
. 將當前workingCopy還原成指定commit或者branch對應的工做環境。
前面commit命令的時候說到:建立一個hash樹,將全部文件連起來,而且保存到objects數據庫裏面
。全部首先咱們要找出指定commit或者branch的hash樹。再找出當前代碼庫版本的hash樹。而後站在當前代碼庫hash樹的角度,比較這出哪裏改了,哪裏刪了,哪裏新增的。最後將這些不一樣落實到當前代碼庫中。固然,別忘了更新HEAD文件指向checkout的commit或者branch
checkout: (ref) => { const targetCommitHash = refs.hash(ref); const diffs = diff.diff(refs.hash('HEAD'), targetCommitHash); workingCopy.write(diffs); refs.write('HEAD',`ref: ${refs.resolveRef(ref)}`); }
上面的這些命令基本都是在本地本身玩而已,後面這幾個命令就涉及到跟其餘人協做了!不過爲了簡單,協做也是經過文件系統操做而已,沒有通過http,可是原理基本同樣!
remote命令只要是用來管理有遠程代碼庫的配置信息,GIT裏面remote命令實現了不少子命令,好比有remote ls
,remote show
,remote add
,remote remove
。咱們這裏只實現剛需的add
命令
remote add
命令將會讀出代碼庫的配置文件.gitdou\config
,而後在裏面添加remote的屬性
remote : (command, name, path) => { const cfg = config.read(); cfg['remote'] = cfg['remote'] || {}; cfg['remote'][name] = path; config.write(cfg); },
添加後的.gitdou\config
文件內容大體以下 (這裏採用的是JSON格式存取)
{ "core": { "bare": false }, "remote": { "origin": "git@github" } }
remote已經準備好了,接着咱們能夠拉取其餘人的代碼庫了!在真正GIT的實現中,這時就涉及到跟GIT服務器交互的細節,不過咱們這裏都是在本地,全部狀況比較簡單。
首先咱們要在remote的工做目錄下面,讀取他objects數據庫的全部對象,而後將這些對象寫到咱們的objects數據庫裏面,再將最新的hash更新到refs/remotes/origin/${branch}
fetch : (remote, branch) => { const remoteUrl = config.read()['remote'][remote]; const remoteHash = refs.getRemoteHash(remoteUrl, branch); const remoteObjects = refs.getRemoteObjects(remoteUrl); _.each(remoteObjects, content => objects.write(content)); refs.updateRef({updateToRef:refs.getRemoteRef(remote, branch), hash:remoteHash}); refs.write("FETCH_HEAD", `${remoteHash} branch '${branch}' of ${remoteUrl}`); return ["From " + remoteUrl, "Count " + remoteObjects.length, branch + " -> " + remote + "/" + branch].join("\n") + "\n"; }
fetch
的確是拿到了對方的全部對象,可是本地的代碼絲毫沒有變化,由於尚未將這些合併到咱們的代碼庫裏面。merge作的就是這事。
這個版本咱們只實現了沒有衝突的場景,也就是能夠fastforward的狀況。
首先咱們拿到remote的hash樹,再讀取咱們當前的hash樹,而後判斷是否能夠fastforward (也就是判斷remote是否包含了咱們最新的代碼),而後跟checkout相似,站在當前代碼庫的角度,找出兩顆hash樹的異同點,將這些異同點寫到當前代碼庫。最後更新當前代碼庫的當前branch,指向最新的commit
merge: (ref) => { const receiverHash = refs.hash('HEAD'); const giverHash = refs.hash(ref); if(merger.canFastForward({receiverHash, giverHash})){ merger.writeFastForwardMerge({receiverHash, giverHash}); return 'Fast-forward'; } return 'Non Fast Foward, not handle now'; }
有了fetch跟remote命令,pull就躺着數錢了!由於pull(remote, branch) = fetch(remote, branch) + merge('FETCH_HEAD')
pull: function(remote, branch) { gitdou.fetch(remote, branch); return gitdou.merge("FETCH_HEAD"); }
來而不往非禮也!有pull也得有push。push的實現原理有點粗暴!直接跳轉到對方的工做目錄下,而後把本身的objects裏面的全部對象寫到對方的代碼庫裏面,再幫對方更新對方的branch引用! 細思極恐,好在真正的GIT不是這樣處理的!
push: ref => { const onRemote = util.onRemote(remoteUrl); const remoteUrl = config.read()['remote'][ref]; const receiverHash = onRemote(refs.hash, ref); const giverHash = refs.hash('HEAD'); objects.allObjects().forEach(item => onRemote(objects.write, item)); onRemote(gitdou.updateRef, refs.resolveRef(ref), giverHash); }
從有用的角度看,此次GITDOU的實現並沒有卵用!
從無用的角度看,此次GITDOU的實現還挺有用!