国庆节就到,一起写一个Linux初版的Git吧

系统 Linux
各位读者就算不了解git的原理,想必也会用三把斧 git add; git commit; git push,下面就简单说一下git是怎么做的版本管理的:跟踪文件的变化,使用commit作为标记,与远程服务器同步。

[[426802]]

Naive Git

一起写一个简单的Git吧!

前言

我与两个师弟一起成立一个 git org,主要是他们(我需要工作,划水出主意做PM居多)做一些趣味使然的项目,PioneerIncubator[9],这个 git 是第三个项目,第一个项目是 betterGo,我好几个月前就写好初版了,就等他们做一些完善补充工作了,之后会单独介绍。第二个项目是刚动手,他们搜了一下,发现上年十月发现有人做了,那个项目还有500多star了。

Git的原理是怎么样呢?

Git is a distributed version-control system for tracking changes in source code during software development.

各位读者就算不了解git的原理,想必也会用三把斧 git add; git commit; git push,下面就简单说一下git是怎么做的版本管理的:跟踪文件的变化,使用commit作为标记,与远程服务器同步。

跟踪文件变化

假如你来开发git这个工具,在初始化一个文件夹(repository)后,为了记录之后可能的修改,你需要记录当前所有需要跟踪的文件内容,最简单的就是全部复制一份好了。

文件是否变化了?比较一下文件哈希好了。

Commit作标记

顾言思义,就是将当前的 repository 状态存储起来,作为commit。你可以通过 commit 恢复到任意状态,git tag 本质也只是给这个 commit 一个 tag(别名),git branch 也是一样。

恢复到某一个 commit,就是将它所代表的 repository 状态恢复起来,就是将文件全部内容以及当前commit恢复到那个状态。

与远程服务器同步

git说自己是分布式的版本管理系统,是因为假如A、B、C三个人一起合作,理论上每个人都有一份server的版本,而且可以独立开发,解决冲突。

Git具体是怎么做的呢?

原理说完了,但commit的管理是要用东西来存储读取管理的,Git没有用数据库,直接将其内容放到.git 文件夹里。

里面有什么内容呢?

  1.   |-- HEAD //指向branch、tag (ref: refs/heads/devbranch) 
  2.   |-- index 
  3.   |-- objects 
  4.   |   |-- 05 
  5.   |   |   `-- 76fac355dd17e39fd2671b010e36299f713b4d 
  6.   |   |-- 0c 
  7.   |   |   `-- 819c497e4eca8e08422e61adec781cc91d125d 
  8.   |   |-- fe 
  9.   |   |   `-- 897108953cc224f417551031beacc396b11fb0 
  10.   |   |-- fe 
  11.   |   |   `-- 897108953cc224f417551031beacc396b11fb0 
  12.   |   |-- info 
  13.   |   
  14.   `-- refs 
  15.       |-- heads //各个branch的heads 
  16.       |   `-- master //此分支最新的commit id 
  17.       |   `-- devBranch // checkout -b branch就会生成的branch 
  18.       `-- tags 
  19.           `-- v0.1 

各位再结合

下面我展开讲讲:

  • HEAD: 指向branch或者tag,标记当前是在哪个分支或者tag上;
  • index:TODO
  • objects:记录文件的内容,每个文件夹名称是该object的sha1值的前两位,文件夹下的文件名称是sha1值的后18位;(tips:sha1算法,是一种加密算法,会计算当前内容的哈希值,作为object的文件名,得到的哈希值是一个用十六进制数字组成的字符串(长度为40))
  • refs
  • heads: heads 里的就是各个分支的 HEAD 分别指向哪个 commit id;简单说,就是 各个branch分别最新的commit是什么,这样子 git checkout branch 就可以切换到对的地方
  • tags: 同理,这个文件夹里存的都是各个tag

那么,新建一个branch的时候,只要在 refs/heads 文件夹里新建branch 名字的文件,并将当前commit id存进去即可;

新建一个commit时,只要根据 HEAD 文件,找到当前的 branch或者tag 是什么,修改里面的内容即可。

有点不好懂?咱给出一个git的实例,默认在一个文件夹执行 git init 后,添加一个文件并 commit 的信息, commit id为 017aa3d7851e8bbff78a697566b5f827b183483c:

  1. $ cat .git/HEAD 
  2. ref: refs/heads/master 
  3. $ cat .git/refs/heads/master 
  4. 017aa3d7851e8bbff78a697566b5f827b183483c 

如上,HEAD 指向了master,而 master 的commit id正是刚刚commit的id。

存储读取解决了,那么commit怎么组织呢?

将当前的 repository 状态存储起来,作为commit。你可以通过 commit 恢复到任意状态,git tag 本质也只是给这个 commit 一个 tag(别名),git branch 也是一样。

恢复到某一个 commit,就是将它所代表的 repository 状态恢复起来,就是将文件全部内容以及当前commit恢复到那个状态。

上面说了,管理文件夹(repository)状态,但是文件夹是可以嵌套的,与文件不一样,需要有这层级关系,同时也要存文件内容,怎么做来区分呢?

我们可以引入以下概念:

  • Tree:代表文件夹,因为 git init 时,就是把当前文件夹./ 作为项目来管理,那么接下来所有要追踪的项目无非就是./ 里的文件或者文件夹而已;
  •  Blob:文件,Tree里可以包含它;

关系如下图:

给点我们写的数据结构代码你看看,要注意的是,tree 可以拥有 blob 或者 tree,所以用了 union;parent 与 next 作为链表使用,作为文件夹目录管理;

  1. struct tree_entry_list { 
  2.     struct tree_entry_list *next
  3.     union { 
  4.         struct tree *tree; 
  5.         struct blob *blob; 
  6.     } item; 
  7.     struct tree_entry_list *parent; 
  8. }; 
  9.  
  10. struct tree { 
  11.     struct tree_entry_list *entries; 
  12. }; 

而 commit 跟树一样,也是有层级的单链表,不过只有

  1. struct commit { 
  2.     struct commit *parents; 
  3.     struct tree *tree; 
  4.  
  5.     char *commit_id[10]; 
  6.     char *author; 
  7.     char *committer; 
  8.     char *changelog; 
  9. }; 

一图胜千言,看图吧:

如上,有三个commit,先后顺序为:1 -> 2 -> 3, 3是最新的。

  • 画圈的blob是文件内容,代表这个文件在commit 1跟2都没有变化,所以复用了同一个;
  • 画正方形的,也是同一个文件,但是内容有变化了,所以分别指向了不一样的blob;
  • tag 指向了commit 2;
  • HEAD 跟 branch 都在最新的commit 3,新增了一个文件;

于是通过commit记录变动的内容,就是可以从上而下的恢复所有有变更的文件。

如图,checkout 到 v0.1的tag,就是找到此commit id,然后恢复commit下的tree的文件:

云风的游戏资源仓库及升级发布

云风参考过git的原理做过一个游戏资源仓库管理,我下面讲一下它跟git的区别,他的文章[10]我觉得比较绕,没有背景知识的人很难看明白。

背景

我们的引擎的一个重要特性就是,在 PC 上开发,在移动设备上运行调试。我们需要频繁的将资源同步到设备上

程序以 c/s 结构运行时,在移动设备上先建立一个空的镜像仓库,同步 PC 端的资源仓库。运行流程是这样的:

首先在客户端启动的时候,向服务器索取一个根索引的 hash ,在本地镜像上设定根。

客户端请求一个文件路径时,从根开始寻找对应的目录索引文件,逐级查找。如果本地有所需的 hash 对象,就直接使用;否则向服务器请求,直到最后获得目标文件。api 的设计上,open 一个资源路径,要么返回最终的文件,要么返回一个 hash ,表示当前还缺少这个 hash 对象;这样,可以通过网络模块请求这个对象;获得该对象后,无须理会这个对象是什么,简单写入镜像仓库,然后重新前面的过程,再次请求未完成的路径,最终就能打开所需的资源文件。

场景是:Client <- 他的游戏服务器 ,单向同步;

他是这样子做的,客户端的仓库是 key-value 的文件数据库,key是文件的hash,value就是文件内容;

同步时,会从根到具体hash全量同步文件下载到数据库;

假如客户端使用资源时,发现缺乏这个文件,就用hash去服务器拉下来。

换言之,因为不需要管理本地版本,并且同步到上游,所以无需在本地记录全量的版本状态

跟Git的区别:

场景是:Client <-> gitHub ,双向同步;

git 需要本地组织commit,切换本地有但服务器没有的版本(就是离线操作) ,同时还需要将变更同步到上游。

最后的建议

如果看完该文,让你跃跃欲试的话,请不要用C写,请不要用C写,请不要用C写。

从零开始写过几个大一点项目,每次都觉得用C写项目太难受了,这次我写 git commit 时,发现要读写文件,解析内容,我发出了内心的感叹:

太难了,不是写这个难,是C太难用了。。

想到我要遍历这些文件,根据目录得到tree的hash,然后还要update这棵树,把tree跟commit还要blob反序列存到文件里,还要读出来,之后还要组织链表操作,用C写就觉得百般阻挠。。。

具体实现,git rebase,git merge等进阶内容,就要等下一篇了。

本文转载自微信公众号「山尽写东西的cache」,可以通过以下二维码关注。转载本文请联系山尽写东西的cache公众号。

 

责任编辑:武晓燕 来源: 山尽写东西的cache
相关推荐

2021-12-30 06:59:27

视频通话网页

2015-02-09 10:55:50

编程女程序员

2015-02-09 09:26:26

程序员

2023-04-11 07:48:32

WebGLCanvas

2024-08-29 09:18:55

2023-11-06 08:28:43

2024-08-02 09:49:35

Spring流程Tomcat

2021-10-27 06:49:34

线程池Core函数

2024-06-17 11:59:39

2024-08-12 15:55:51

2022-08-29 07:48:27

文件数据参数类型

2012-11-08 17:33:53

智慧云

2022-02-22 10:50:19

IDEAGit工具,

2021-11-15 11:03:09

接口压测工具

2022-06-27 08:00:49

hook工具库函数

2015-10-02 12:36:28

国庆节51cto专题云计算入门

2024-09-04 08:55:56

2022-09-28 13:57:41

鸿蒙开源

2012-04-12 20:50:13

MSN互联网

2012-04-13 16:19:20

MSN
点赞
收藏

51CTO技术栈公众号