500行JS代码打造你的专属GIT
栏目: JavaScript · 发布时间: 5年前
内容简介:跟GIT的结缘开始于2011年,公司决定不用原来的IBM Clearcase,改用开源的GIT。作为当时GIT的内部support,确实有很长一段时间跟它厮混在一起。后来还写了几篇如何使用GIT的文章,有空可以翻翻最近看到一个叫What I cannot create, I do not understand - Richard Feynman
代码来自开源,也回流开源,有需要且不嫌弃的可以上去看看 https://github.com/notechsolution/gitdou
缘起
跟GIT的结缘开始于2011年,公司决定不用原来的IBM Clearcase,改用开源的GIT。作为当时GIT的内部support,确实有很长一段时间跟它厮混在一起。后来还写了几篇如何使用GIT的文章,有空可以翻翻 GIT七年之痒 . 前两年回一下炉,又写了几篇 GIT入门 .
最近看到一个叫 Richard Feynman 的人说过这么一句话
What I cannot create, I do not understand - Richard Feynman
嗯嗯,有点意思,扒拉了一下,还有不少人用Javascript写GIT。这次的实现主要也是参考了其中一个叫 gitlet 的
用什么锤子?| 技术栈
GIT是Linux Torvalds用 C语言 写的。小的不才不懂C,那就用Javascript写写吧, ES6 可以让代码可以写得比较简洁。既然重造轮子,那就尽量少用框架吧。但是作为lodash粉,还是忍不住了,最后还是用了lodash~~~.
当然,Pivotal Lab中毒较深,做个练习也离不开TDD,所以这次也用了Ava作为testing框架。 但功力尚浅,有些case也偷懒了,testcase跟代码的函数比例只做到1:1, 500行的代码只有500行的unittest。
锤出个什么东东?| 实现哪些功能
这次的目的是为了加深对GIT底层实现原理的理解,而不是做出一个真正的产品出来,所以对于用户操作没有做出各种友好的提醒,比如没有像 Already up to date
这样的提醒等等,只要实现了GIT的如下核心命令:
- init
- add
- rm
- commit
- checkout
- branch
- remote
- fetch
- merge
- pull
- push
咋锤的?| 实现过程
下面尝试逐一来解释一下每个命令是干什么的。
gitdou.init
首先是初始化一个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()); },
add
前面说到了git实际是一个数据库,存放了所有文件的所有历史版本。为了更方便高效地查询,数据库都会建立索引。git也不例外,它也有一个index文件,记录所有文件的路径,这些文件的状态以及当前版本的hash值。
add
命令就是将指定路径的所有文件的路径,状态以及当前的hash值记录保存到index文件里面。其实现过程就是扫出指定目录下的所有文件,逐一计算他们的hash值,然后写到index文件里面
add: path => { const addedFiles = files.listAllMatchedFiles(path); index.updateFilesIntoIndex(addedFiles, {add: true}); }
rm
有添加命令,对应的也就应该有删除命令。其过程跟add基本一致,只不过多了一步把要删除的文件从当前workingCopy里面删除掉。
rm: path => { const deletedFiles = files.listAllMatchedFiles(path); index.updateFilesIntoIndex(deletedFiles, {remove: true}); files.removeFiles(deletedFiles); }
commit
当任务已经到一段落,我们需要给当前版本做一个快照,方便以后找回。这时我们可以做一个commit。这个commit将会包含一个hash树,这棵树将当前版本的所有文件连起来。当然还包含了一些commit的metadata,比如谁,什么时候commit,commit的备注是什么等等。
具体实现大致为:
- 创建一个hash树,将所有文件连起来,并且保存到objects数据库里面
- 创建一个commit对象,包含hash树的hash,commit的消息,commit的时间,如果有父亲hash,也包含进来。同样将这个commit的对象保存到objects数据库里面
- 更新当前branch,指向新的commit hash
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}) }
branch
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就稍复杂一些。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)}`); }
remote
上面的这些命令基本都是在本地自己玩而已,后面这几个命令就涉及到跟其他人协作了!不过为了简单,协作也是通过文件系统操作而已,没有经过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" } }
fetch
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"; }
merge
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'; }
pull
有了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"); }
push
来而不往非礼也!有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的实现还挺有用!
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 不到40行 Python 代码打造一个简单的推荐系统
- 使用 CodeMirror 打造属于自己的在线代码编辑器 荐
- 10几行代码,用python打造实时截图识别OCR
- 谷歌重磅开源新技术:5行代码打造无限宽神经网络模型
- 最新 | 用深度强化学习打造不亏钱的交易机器人(附代码)
- 将Vim打造成Python IDE(二):代码格式化的一些问题
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
鸟哥的Linux私房菜 基础学习篇(第二版)
鸟哥 / 人民邮电出版社 / 2007-9 / 65.00元
《鸟哥的Linux私房菜基础学习篇(第二版)》全面而详细地介绍了Linux操作系统。全书分为5个部分:第一部分着重说明Linux的起源及功能,如何规划和安装Linux主机;第二部分介绍Linux的文件系统、文件、目录与磁盘的管理;第三部分介绍文字模式接口shell和管理系统的好帮手shell脚本,另外还介绍了文字编辑器vi和vim的使用方法;第四部分介绍了对于系统安全非常重要的Linux账号的管理......一起来看看 《鸟哥的Linux私房菜 基础学习篇(第二版)》 这本书的介绍吧!