整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

30 个纯 HTML5 实现的游戏

览器和 JavaScript 的功能逐年不断的变强变大。曾几何时,任何类型的游戏都需要Flash。但随着 HTML5 发展,HTML5 + WebGL 游戏式就慢慢占领着这个舞台。以下是30款流行的游戏,它们可以在所有现代浏览器中运行,并且只使用web技术构建。

1. HexGL

地址:http://hexgl.bkcore.com/

类型:街机,赛车


HexGL 是一款基于HTML5,JavaScript和WebGL的快节奏的赛车游戏。玩家可以使用键盘,触摸屏设备或leap motion(体感控制器)来控制太空飞船。

2. CrossCode

地址:http://www.cross-code.com/en/home

类型:动作,角色扮演

一个复古灵感的2D游戏设定在遥远的未来。这是一个充满伟大的游戏机制,如组合,拼图,技能树,任务,物品等等。

3. Sketchout

地址:https://sketch-out.appspot.com/

类型:街机

Sketchout的任务保护你的行星,并通过改变流星的方向来消灭对手,通过使流星偏转来保护您的星球并消灭对方,这款游戏有很棒的视觉效果和音乐特效。

4. Treasure Arena

地址:http://www.treasurearena.com/类型:多人,角色扮演,动作

Treasure Arena 是一款动态的竞技场战斗游戏,最多可容纳4名玩家。它具有不同的游戏模式,出色的帧率和配乐,是一个非常有趣的游戏。

5. Bejeweled

地址:http://bejeweled.popcap.com/html5/

类型:街机,解谜,娱乐

HTML5格式的经典“宝石迷阵”游戏。这是一个官方克隆,因此可以正常运行且外观完美。

6. Missile Game

地址:http://missile-game.bwhmather.com/类型:街机

这是一款非常具有挑战性的游戏,游戏中我们扮演的是一枚被发射进隧道的导弹。游戏有很酷的黑白图像,玩的时候会有很强的场景效果。

7. Gods Will Be Watching

地址:http://www.deconstructeam.com/games/gods-will-be-watching/类型:拼图

在这个令人毛骨悚然(但又很棒)的游戏中,我和自己团队必须独自生存40天。团队有六名成员,其中包括一只狗,一名精神病医生和一个机器人,您必须与他们互动,以使其保持温暖,温饱和理智的状态。

8. Sinuous

地址:http://www.sinuousgame.com/类型:街机

一个简单的游戏,极简的图形和流畅的帧率。拾取电源时避免与红点碰撞。此外,如果你想要那些额外的积分,就需要不停向前移动

9. Swooop

地址:http://swooop.playcanvas.com/类型:街机

在一个美丽多彩的3D世界里,到处飞翔,收集宝石和星星。

10. Free Rider HD

地址:http://www.freeriderhd.com/

Free Rider HD 是一款令人上瘾的游戏,你可以在其他玩家绘制的赛道上骑自行车。可以在成千上万的播放器曲目中选择一个播放,也可以创建自己的曲目并分享。

11. Entanglement

地址:http://entanglement.gopherwoodstudios.com/zh-CN-index.html类型:拼图,娱乐

这个游戏的目的是通过在网格上放置线段来创建一条尽可能长的路径。你可以单独玩,也可以和朋友一起玩。

12. Escape from XP

地址:https://www.modern.ie/en-us/ie6countdown#escape-from-xp

类型:动作,街机

用“Escape from XP”来庆祝 Windows XP 的终结。你的任务是拯救最后一个陷入Clippy暴政的开发人员。

13. Polycraft

地址:http://polycraftgame.com/类型:角色扮演,塔防,动作

在这个很棒的3D游戏中,你到处收集资源,建造东西,完成任务。关于它的所有东西都经过抛光,并且运行也非常顺畅。

14. 2048

地址:https://gabrielecirulli.github.io/2048/类型:拼图

一个非常上瘾的游戏,你可能已经玩过了。在 2048 ,你移动编号的图块并合并它们。当界面中最大数字是`2048 时,游戏胜利。

15. Onslaught Arena

地址:http://arcade.lostdecadegames.com/onslaught_arena/

类型:动作

一种快节奏的复古生存游戏,您可以使用不同的武器与成群的敌人作战。

16. Angry Birds

地址:http://chrome.angrybirds.com/类型:游戏

《愤怒的小鸟》游戏,这就不用介绍了。

17. Cube Slam

地址:https://www.cubeslam.com/mcycrs

类型:街机,多人

具有丰富的色彩和炫酷的3D图形乒乓球游戏。我们可以通过向朋友发送一个URL来挑战他们,还可以通过网络摄像头看到对方。

18. The Wizard

地址:http://hypnoticowl.com/games/the-wizard/类型:动作,角色扮演,策略

Wizard 是基于回合的地牢爬行者,在里面会遇到神话般的怪物并找到奇妙的咒语。该游戏具有酷炫的战斗机制,有时可能会带来很大挑战。

19. X-Type

地址:http://phoboslab.org/xtype/类型:动作,街机

在这款酷炫的太空射击游戏中,你目的就是要起战胜 Boss。

20. Cookie Clicker

地址:http://orteil.dashnet.org/cookieclicker/类型:休闲,搞笑

Cookie clicker 是一款可能为了开玩笑而创建的游戏,但仍然提供了大量的乐趣。你可以从0个cookie开始,然后单击一些有效率的cookie,最后你可能会发现自己拥有数十亿个cookie。

21. Elevator Saga

地址:http://play.elevatorsaga.com/类型:拼图,编码

这类属于程序员类型游戏 。在电梯中的任务是通过对电梯的运动进行编程,以最有效的方式运送人员,这些都是用 JavaScript 来完成的。

22. Game of Bombs

地址:http://gameofbombs.com/landing类型:动作,角色扮演,多人

Game of Bombs是一个轰炸机类型的游戏,在广阔地图上,都有着敌方玩家。收集力量,皮肤和成就,以成为最佳轰炸机玩家的方式。

23. Olympia Rising

地址:http://or.paleozoic.com/类型:平台游戏,动作

Olympia Rising具有漂亮复古外观图形的游戏。它坐落在古希腊,在那里我们扮演的女人被赋予了重新的机会,所以我们的任务就是逃离死者的世界。

24. Pixel Race

地址: https://ned.im/pixel-race-game/类型:街机,赛车

Pixel Race是一款简单概念概念,你可以在收集硬币的同时控制汽车以避开障碍物。如果有足够的耐心和空闲时间,那么你可能会打破记录(记录为36309个硬币)。

25. Little Alchemy

地址:https://littlealchemy.com/类型:拼图

从这四个基本元素开始,将它们组合起来,创建510种可能的组合。

26. Arena5

地址:http://www.kevs3d.co.uk/dev/arena5/类型:街机

在数字领域中飞行并射击几何敌人以获得高分。

27.Vector Runner Remix

地址:https://vector-runner-remix.tresensa.com/

类型:街机

在这个充满色彩和几何形状的平台游戏中,尽你所能奔跑吧。

28. Biolab Disaster

地址:http://playbiolab.com/类型:动作

一款出色的像素艺术平台游戏,你必须在这里逃脱充满突变生物和其他不良生物的实验室。

29. World's Biggest PAC-MAN

地址:http://worldsbiggestpacman.com/#类型:街机

30. New Super Resident Raver

地址:http://games.jessefreeman.com/new-super-resident-raver/

从即将到来的僵尸入侵中拯救惊慌失措的人们。收集钱,升级你的武器和战斗僵尸。


作者:Danny Markov 来源:tutorialzin 译者:前端小智

原文:https://tutorialzine.com/2015/02/30-amazing-games-made-only-with-html5

单实现2048小游戏

想实现2048游戏书写代码时可以分为三个步骤

一、HTML部分

先书写HTML把游戏结构搭建出来

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="index.css">
</head>
<body>
<!--最外部的大框-->
<div class="outermost"> //包裹游戏全局的大盒子
    <!--title-->
    <span class="top"><b>SCORE:<span id="score01"></span></b></span>//顶部实时显示的游戏分数
    <!--游戏大框框-->
    <div class="big">//2048游戏为四行四列因此需要16个div盒子
        <div class="cell" id="c00"></div>
        <div class="cell" id="c01"></div>
        <div class="cell" id="c02"></div>
        <div class="cell" id="c03"></div>
 
        <div class="cell" id="c10"></div>
        <div class="cell" id="c11"></div>
        <div class="cell" id="c12"></div>
        <div class="cell" id="c13"></div>
 
        <div class="cell" id="c20"></div>
        <div class="cell" id="c21"></div>
        <div class="cell" id="c22"></div>
        <div class="cell" id="c23"></div>
 
        <div class="cell" id="c30"></div>
        <div class="cell" id="c31"></div>
        <div class="cell" id="c32"></div>
        <div class="cell" id="c33"></div>
    //游戏结束时会弹出的提示框
    </div>
    <!--提示框-->
    <div class="tips" id="gameover">
       <p>GAME OVER!!! <br>
           SCORE: <span id="score02">0</span><br>
           <button class="startbtn">重新开始</button>
       </p>
    </div>
    <!--重玩一遍-->
    <div class="foot">
        <button class="replay"><a>重玩一遍</a></button>
    </div>
</div>
<script type="text/javascript" src="index.js"></script>
</body>
</html>

二、css部分

经过了第一步的搭建游戏框架,第二部就是给游戏添加样式,使它能显示出来

*{
    padding: 0px;
    margin: 0px auto;
    font-family: Arial;
 
}
/*最外部的大框*/
.outermost{
    width: 480px;
    height: 600px;
    font-size: 40px;
    margin-top: 120px;
}
/*title*/
<!--顶部显示分数的样式-->
.top{
    margin: auto;
}
.top span{
    color: red;
}
/*游戏大框框*/
.big{
    width: 480px;
    height: 480px;
    background:pink;
    border-radius: 8px;
}
<!--给每一个盒子包裹的小框子添加样式-->
.cell{
    list-style: none;
    float: left;
    display: inline-block;
    width: 100px;
    height: 100px;
    line-height: 100px;
    text-align: center;
    background-color: #fbf8cd;
    margin-left: 16px;
    margin-top: 16px;
    border-radius: 6px;
}
<!--提前把出现的数2、4、8、16等的数所在的格子给添加好样式增加游戏体验感-->
.n2{background-color:#f65e3b;color:#776e65}
.n4{background-color:#33b5e5;color:#776e65}
.n8{background-color:#f2b179;color:#776e65}
.n16{background-color:#f59563;color:#776e65}
.n32{background-color:#f67c5f;color:#776e65}
.n64{background-color:#f65e3b;color:#776e65}
.n128{background-color:#edcf72;color:#776e65}
.n256{background-color:#edcc61;color:#776e65}
.n512{background-color:#9c0;color:#776e65}
.n1024{background-color:#33b5e5;color:#776e65;font-size:40px}
.n2048{background-color:#09c;color:#776e65;font-size:40px}
/*提示框样式*/
.tips{
    border: 1px solid #cccccc;
    background: #FFFFFF;
    width: 400px;
    height: 200px;
    border-radius: 10px;
    color: #ff4456;
    text-align: center;
    line-height: 60px;
    position: absolute;
    top: 50%;
    left: 50%;
    margin-left: -200px;
    margin-top: -100px;
    display: none;
}
.tips .startbtn{
    height: 50px;
    width: 200px;
    color: #FFFFFF;
    font-size: 20px;
    line-height: 50px;
    border-radius: 10px;
    background: cornflowerblue;
    border: none;
}
/*重玩一遍*/
.foot{
    width: 200px;
    height: 50px;
}
.foot>.replay{
    width: 200px;
    height: 50px;
    background: aquamarine;
    margin-top: 60px;
    color: lightpink;
    border:0;
    font-size: 24px;
    font-weight: bold;
    border-radius: 6px;
}

书写好了HTML+CSS部分游戏的模样也就出来了,如下图所示:

三、JS部分

下面就到了最后也是最关键的一步----添加行为,也就是JS部分的书写,给其添加效果

//创建一个对象,里面存储所有的游戏数据及游戏方法
var game = {
	data : [],   //定义一个数组,用来存所有的游戏的数据
	score : 0,   //定义一个分数的属性
	gamerunning : 1,   //定义一个游戏运行的状态,将其设置为1与其他状态区分开
	gameover : 0,     //定义一个游戏结束的状态
	status : 0,      //这个是目前游戏的状态,时刻的跟上面两个状态做比较,确定游戏处于运行或者结束
	start : function(){   //游戏开始时候的方法
//		游戏开始的时候肯定是要把游戏的状态设置成游戏运行的状态
//		this == game
		this.status = this.gamerunning;
//		游戏开始的时候分数清空
		this.score = 0;
//		数组中的所有元素全部设置成0
		this.data = [
			[0,0,0,0],
			[0,0,0,0],
			[0,0,0,0],
			[0,0,0,0]
		];
		this.randomNum();//调用下面自定义的随机函数,可以在移动和开始的时候随机出来一个数
		this.randomNum();//调用两次是因为这是游戏开始时的方法,开局随机出现两个数和位置,因此需要调用两次
		this.dataView();//调用下面所写的更新视图的方法
	},
//	随机数的函数,开始的时候随机生成,移动的时候随机生成
	randomNum: function(){
		while(true){
			//		随机生成行和列 0 - 3随机整数
			var r = Math.floor( Math.random() * 4 );   //随机生成一个行
			var c = Math.floor( Math.random() * 4 );   //随机生成一个列
			
			if(this.data[r][c] == 0){
				var num = Math.random() > 0.5 ? 2 : 4;//随机出现2或4
				this.data[r][c] = num;
				break;
			}
		}
	},
//	更新试图的方法
	dataView: function(){
//		大的循环,然后把所有的元素全部遍历一遍
		for(var r = 0; r < 4; r++){
			for(var c = 0; c < 4; c++){
//				找到对应的div
				var div = document.getElementById("c" + r + c);  //字符串拼接
				if(this.data[r][c] != 0){
//					数组中对应的内容放到格子上面去
					div.innerHTML = this.data[r][c];
//					样式也写成对应的
					div.className = "cell n" + this.data[r][c];
				}else{
					div.innerHTML = "";
					div.className = "cell"
				}
			}
		}
//		更新分数
		document.getElementById("score01").innerHTML = this.score;
		//游戏没有结束的时候 弹出层时刻都是隐藏的
		if(this.status == this.gamerunning){
			document.getElementById("gameover").style.display = "none";
		}else{
			document.getElementById("gameover").style.display = "block";
			document.getElementById("score02").innerHTML = this.score;
		}
	},
//	判断游戏是否结束的方法
	isgameover: function(){
		for(var r = 0; r < 4; r++){
			for(var c = 0; c < 4; c++){
				if(this.data[r][c] == 0){  //里面有空格子的时候,游戏还是可以运行
					return false;   //表示游戏还没有结束
				}
				if(c < 3){//判断左右是否有相同的
					if(this.data[r][c] == this.data[r][c+1]){
						return false;
					}
				}
				if(r < 3){
					if(this.data[r][c] == this.data[r+1][c]){
						return false;
					}
				}
			}
		}
		return true;
	},

//移动的方法,左 右 上 下四个部分

//	左 右 上 下
//	左移的方法
	moveLeft: function(){
		var before = String(this.data);   //之前做一次转换
//		具体的移动需要处理的逻辑,直接处理好每一行即可
		for(var r = 0;r < 4;r ++){
			this.moveLeftInRow(r);
		}
		var after = String(this.data);  //移动之后再做一次转换
//		如果说移动之前不等于移动之后,肯定是发生了移动
		if(before != after){
			this.randomNum();   //生成随机数
//			生成的随机数可能会造成游戏的gameover
			if(this.isgameover()){
//				改变游戏的状态
				this.status = this.gameover
			}
//			更新视图
			this.dataView();
		}
	},
	moveLeftInRow: function(r){   //只去做处理每一行的逻辑
		for(var c = 0; c < 3; c++){
			var nextc = this.getNextinRow(r,c);
			if(nextc != -1){
				if(this.data[r][c] == 0){
//					如果等于0,直接替换
					this.data[r][c] = this.data[r][nextc];
					this.data[r][nextc] = 0;  //位置恢复成0
					c --;   //要让位置恢复到原地
				}else if(this.data[r][c] == this.data[r][nextc]){
					this.data[r][c] *= 2;   //位置直接翻一倍
					this.data[r][nextc] = 0;
					this.score += this.data[r][c];  //更新分数
				}
			}else{   //没有找到
				break;   //直接退出循环
			}
		}
	},
	getNextinRow: function(r,c){
		for(var i = c + 1; i < 4; i++){
			if(this.data[r][i] != 0){
				return i;    //表示已经找到位置,并且把位置返回出来
			}
		}
		return -1;   //返回一个标识符
	},
//	右移的方法
	moveRight: function(){
		var before = String(this.data);
		for(var r = 0; r < 4; r++){
			this.moveRightInRow(r);
		}
		var after = String(this.data);
		if(before != after){
			this.randomNum();
			if(this.isgameover()){
				this.status = this.gameover;
			}
			this.dataView();
		}
	},
	moveRightInRow: function(r){
		for(var c = 4; c > 0; c--){
			var prevc = this.getPrevInRow(r,c);
			if(prevc != -1){
				if(this.data[r][c] == 0){
					this.data[r][c] = this.data[r][prevc];
					this.data[r][prevc] = 0;
					c ++
				}else if(this.data[r][c] == this.data[r][prevc]){
					this.data[r][c] *= 2;
					this.data[r][prevc] = 0;
					this.score += this.data[r][c];
				}
			}else{
				break;
			}
		}
	},
	getPrevInRow: function(r,c){
		for(var i = c - 1; i >= 0; i--){
			if(this.data[r][i] != 0){
				return i;
			}
		}
		return -1;
	},
//	上移
	moveUp: function(){
		var before = String(this.data);
		for(var c = 0; c < 4; c++){
			this.moveUpInCol(c);
		}
		var after = String(this.data);
		if(before != after){
			this.randomNum();
			if(this.isgameover()){
				this.status = this.gameover;
			}
			this.dataView();
		}
	},
	moveUpInCol: function(c){
		for(var r = 0;r < 4; r++){
			var nextr = this.getNextInCol(r,c);
			if(nextr != -1){
				if(this.data[r][c] == 0){
					this.data[r][c] = this.data[nextr][c];
					this.data[nextr][c] = 0;
					r -- ;
				}else if(this.data[r][c] == this.data[nextr][c]){
					this.data[r][c] *= 2;
					this.data[nextr][c] = 0;
					this.score += this.data[r][c];
				}
			}else{
				break;
			}
		}
	},
	getNextInCol: function(r,c){
		for(var i = r + 1; i < 4; i++){
			if(this.data[i][c] != 0){
				return i;
			}
		}
		return -1;
	},
//	下移的方法
	moveDown: function(){
		var before = String(this.data);
		for(var c = 0;c < 4; c++){
			this.moveDownInCol(c);
		}
		var after = String(this.data);
		if(before != after){
			this.randomNum();
			if(this.isgameover()){
				this.status = this.gameover;
			}
			this.dataView();
		}
	},
	moveDownInCol: function(c){
		for(var r = 3; r > 0; r--){
			var prev = this.getPrevIncol(r,c);
			if(prev != -1){
				if(this.data[r][c] == 0){
					this.data[r][c] = this.data[prev][c];
					this.data[prev][c] = 0;
					r -- ;
				}else if(this.data[r][c] == this.data[prev][c]){
					this.data[r][c] *= 2;
					this.data[prev][c] = 0;
					this.score += this.data[r][c];
				}
			}else{
				break;
			}
		}
	},
	getPrevIncol: function(r,c){
		for(var i = r - 1; i >= 0; i--){
			if(this.data[i][c] != 0){
				return i;
			}
		}
		return -1;
	},
}
game.start();
console.log(game.data)
console.log(game.status);
console.log(game.score);
//键盘事件
document.onkeydown = function(){
	if(event.keyCode == 37){
		//console.log("左")
		game.moveLeft();
	}else if(event.keyCode == 38){
		//console.log("上")
		game.moveUp()
	}else if(event.keyCode == 39){
		//console.log("右")
		game.moveRight()
	}else if(event.keyCode == 40){
		//console.log("下")
		game.moveDown()
	}
}
//touch事件
//手指按下
var startX;//设定开始起始位置的x坐标
var startY;//设定开始起始位置的y坐标
var endX;//设定结束滑动位置的x坐标
var endY;//设定结束滑动位置的y坐标
document.addEventListener('touchstart',function(){
//	console.log("手指按下了屏幕")
	console.log(event);
	startX = event.touches[0].pageX;
	startY = event.touches[0].pageY;
})
//手指移动
//document.addEventListener('touchmove',function(){
//	console.log("手指的移动")
//})
//手指松开
document.addEventListener("touchend",function(){
//	console.log("手指松开")
	console.log(event);
	endX = event.changedTouches[0].pageX;//如何获取结束时的位置x
	endY = event.changedTouches[0].pageY;
	var X = endX - startX;
	var Y = endY - startY
	var absX = Math.abs(X) > Math.abs(Y);
	var absY = Math.abs(Y) > Math.abs(X);
	if(X > 0 && absX){
		console.log("右滑动")
		game.moveRight()
	}else if(X < 0 && absX){
		console.log("左滑动")
		game.moveLeft()
	}if(Y > 0 && absY){
		console.log("下滑动")
		game.moveDown()
	}if(Y < 0 && absY){
		console.log("上滑动")
		game.moveUp()
	}
})

如下为游戏效果图

就这样一个简单的2048游戏就完成啦~


最后

非常感谢您能看到这里~

关注我~带给你更多惊喜~

、背景



一方面想对git底层工作原理有更多理解,另外观摩下Linus大神的编码思路和风格

二、版本选择

git项目地址:https://github.com/git/git.git

开源库随着功能增加代码越来越庞大,并且主干思想也会被越来越多的分支细节所淹没,所以直接选择git第一个版本代码进行参考。时间回到2005年4月7号的下午。

commit e83c5163316f89bfbde7d9ab23ca2e25604af290 (HEAD)
Author: Linus Torvalds <torvalds@ppc970.osdl.org>
Date:   Thu Apr 7 15:13:13 2005 -0700

进到目录里看一下,只有几个文件,总共代码行才1000出头

三、代码运行

review+debug是学习代码库非常有效的方法,所以先让代码跑起来。mac上尝试编译,出现一些警告以及错误。本地做一些修改后编译通过。有同学要自己尝试动手,可以参照以下修改:

1、安装openssl库以及zlib库

brew install openssl

brew install zlib

2、修改编译以及链接选项并指定头文件以及库位置同时关闭弃用函数报警

CFLAGS=-g -Wno-deprecated -I/usr/local/opt/openssl/include/

LDFLAGS=-L/usr/local/opt/openssl/lib/ -L/usr/local/opt/zlib/lib/

3、链接库修改,从 -lssl 改为 -lcrypto -lz

4、main函数增加返回值、修改时间相关结构体

5、m1芯片mac没有找到可用gdb版本,可以使用lldb代替

以下是具体修改点:

[graypig:]$ git diff
diff --git a/Makefile b/Makefile
index a6bba79ba1..fe779bdb75 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,5 @@
-CFLAGS=-g
+CFLAGS=-g -Wno-deprecated -I/usr/local/opt/openssl/include/
+LDFLAGS=-L/usr/local/opt/openssl/lib/ -L/usr/local/opt/zlib/lib/
 CC=gcc
 
 PROG=update-cache show-diff init-db write-tree read-tree commit-tree cat-file
@@ -8,27 +9,27 @@ all: $(PROG)
 install: $(PROG)
        install $(PROG) $(HOME)/bin/
 
-LIBS= -lssl
+LIBS= -lcrypto -lz
 
 init-db: init-db.o
 
 update-cache: update-cache.o read-cache.o
-       $(CC) $(CFLAGS) -o update-cache update-cache.o read-cache.o $(LIBS)
+       $(CC) $(CFLAGS) $(LDFLAGS) -o update-cache update-cache.o read-cache.o $(LIBS)

 show-diff: show-diff.o read-cache.o
-       $(CC) $(CFLAGS) -o show-diff show-diff.o read-cache.o $(LIBS)
+       $(CC) $(CFLAGS) $(LDFLAGS) -o show-diff show-diff.o read-cache.o $(LIBS)
 
 write-tree: write-tree.o read-cache.o
-       $(CC) $(CFLAGS) -o write-tree write-tree.o read-cache.o $(LIBS)
+       $(CC) $(CFLAGS) $(LDFLAGS) -o write-tree write-tree.o read-cache.o $(LIBS)
 
 read-tree: read-tree.o read-cache.o
-       $(CC) $(CFLAGS) -o read-tree read-tree.o read-cache.o $(LIBS)
+       $(CC) $(CFLAGS) $(LDFLAGS) -o read-tree read-tree.o read-cache.o $(LIBS)
 
 commit-tree: commit-tree.o read-cache.o
-       $(CC) $(CFLAGS) -o commit-tree commit-tree.o read-cache.o $(LIBS)
+       $(CC) $(CFLAGS) $(LDFLAGS) -o commit-tree commit-tree.o read-cache.o $(LIBS)
 
 cat-file: cat-file.o read-cache.o
-       $(CC) $(CFLAGS) -o cat-file cat-file.o read-cache.o $(LIBS)
+       $(CC) $(CFLAGS) $(LDFLAGS) -o cat-file cat-file.o read-cache.o $(LIBS)
 
 read-cache.o: cache.h
 show-diff.o: cache.h
diff --git a/cache.h b/cache.h
index 98a32a9ad3..161a5aff90 100644
--- a/cache.h
+++ b/cache.h
@@ -12,6 +12,7 @@
 
 #include <openssl/sha.h>
 #include <zlib.h>
+#include <string.h>
 
 /*
  * Basic data structures for the directory cache
diff --git a/init-db.c b/init-db.c
index 25dc13fe10..d11b16bff5 100644
--- a/init-db.c
+++ b/init-db.c
@@ -19,8 +19,8 @@ int main(int argc, char **argv)
        sha1_dir = getenv(DB_ENVIRONMENT);
        if (sha1_dir) {
                struct stat st;
-               if (!stat(sha1_dir, &st) < 0 && S_ISDIR(st.st_mode))
-                       return;
+               if (!(stat(sha1_dir, &st) < 0) && S_ISDIR(st.st_mode))
+                       return 0;
                fprintf(stderr, "DB_ENVIRONMENT set to bad directory %s: ", sha1_dir);
        }
 
diff --git a/show-diff.c b/show-diff.c
index b8522886a1..6d00ba2a6f 100644
--- a/show-diff.c
+++ b/show-diff.c
@@ -11,11 +11,11 @@ static int match_stat(struct cache_entry *ce, struct stat *st)
 {
        unsigned int changed = 0;
 
-       if (ce->mtime.sec  != (unsigned int)st->st_mtim.tv_sec ||
-           ce->mtime.nsec != (unsigned int)st->st_mtim.tv_nsec)
+       if (ce->mtime.sec  != (unsigned int)st->st_mtimespec.tv_sec ||
+           ce->mtime.nsec != (unsigned int)st->st_mtimespec.tv_nsec)
                changed |= MTIME_CHANGED;
-       if (ce->ctime.sec  != (unsigned int)st->st_ctim.tv_sec ||
-           ce->ctime.nsec != (unsigned int)st->st_ctim.tv_nsec)
+       if (ce->ctime.sec  != (unsigned int)st->st_ctimespec.tv_sec ||
+           ce->ctime.nsec != (unsigned int)st->st_ctimespec.tv_nsec)
                changed |= CTIME_CHANGED;
        if (ce->st_uid != (unsigned int)st->st_uid ||
            ce->st_gid != (unsigned int)st->st_gid)
diff --git a/update-cache.c b/update-cache.c
index 5085a5cb53..f9c8e0fc69 100644
--- a/update-cache.c
+++ b/update-cache.c
@@ -139,9 +139,9 @@ static int add_file_to_cache(char *path)
        memset(ce, 0, size);
        memcpy(ce->name, path, namelen);
        ce->ctime.sec = st.st_ctime;
-       ce->ctime.nsec = st.st_ctim.tv_nsec;
+       ce->ctime.nsec = st.st_ctimespec.tv_nsec;
        ce->mtime.sec = st.st_mtime;
-       ce->mtime.nsec = st.st_mtim.tv_nsec;
+       ce->mtime.nsec = st.st_mtimespec.tv_nsec;
        ce->st_dev = st.st_dev;
        ce->st_ino = st.st_ino;
        ce->st_mode = st.st_mode;

四、源码分析

1、 init-db.c

核心逻辑:创建缓存目录.dircache/objects,并且在此目录下预创建256个目录,命名规则

.dircache/objects/00 .dircache/objects/01 .dircache/objects/... .dircache/objects/ff

#include "cache.h"

int main(int argc, char **argv)
{
    char *sha1_dir = getenv(DB_ENVIRONMENT), *path;
    int len, i, fd;

    if (mkdir(".dircache", 0700) < 0) {
        perror("unable to create .dircache");
        exit(1);
    }

    /*
     * If you want to, you can share the DB area with any number of branches.
     * That has advantages: you can save space by sharing all the SHA1 objects.
     * On the other hand, it might just make lookup slower and messier. You
     * be the judge.
     */
    sha1_dir = getenv(DB_ENVIRONMENT);
    if (sha1_dir) {
        struct stat st;
        if (!stat(sha1_dir, &st) < 0 && S_ISDIR(st.st_mode))
            return;
        fprintf(stderr, "DB_ENVIRONMENT set to bad directory %s: ", sha1_dir);
    }

    /*
     * The default case is to have a DB per managed directory. 
     */
    sha1_dir = DEFAULT_DB_ENVIRONMENT;
    fprintf(stderr, "defaulting to private storage area\n");
    len = strlen(sha1_dir);
    if (mkdir(sha1_dir, 0700) < 0) {
        if (errno != EEXIST) {
            perror(sha1_dir);
            exit(1);
        }
    }
    //注意malloc申请内存后不会清零,但是使用sprintf格式化会在末尾添加\0,所以不存在越界问题
    path = malloc(len + 40);
    memcpy(path, sha1_dir, len);
    for (i = 0; i < 256; i++) {
        //两个16进制字符格式打印
        sprintf(path+len, "/%02x", i);
        if (mkdir(path, 0700) < 0) {
            if (errno != EEXIST) {
                perror(path);
                exit(1);
           }
        }
    }
    return 0;
}

2、update-cache.c

缓存项设计经过仔细考量,可以直接利用文件字节流还原内存缓存项结构,省掉了拷贝动作。核心逻辑:

首先读取.dircache/index的文件内容,对要加入缓存的文件进行校验后,进行zlib压缩并计算sha1值,按照sha1计算文件的路径.dircache/objects/xx/xx{19}, 保存文件然后更新全局cache信息,并将全局cache保存到磁盘上生成新的.dircache/index。

文件内容索引文件格式:"blob " + size + null + zlib压缩后的文件内容

int main(int argc, char **argv)
{
    int i, newfd, entries;

    entries = read_cache();
    if (entries < 0) {
        perror("cache corrupted");
        return -1;
    }

    newfd = open(".dircache/index.lock", O_RDWR | O_CREAT | O_EXCL, 0600);
    if (newfd < 0) {
        perror("unable to create new cachefile");
        return -1;
    }
    for (i = 1 ; i < argc; i++) {
        char *path = argv[i];
        // 判断路径是否合法,排除: . .. //结尾
        if (!verify_path(path)) {
            fprintf(stderr, "Ignoring path %s\n", argv[i]);
              continue;
        }
        if (add_file_to_cache(path)) {
            fprintf(stderr, "Unable to add %s to database\n", path);
            goto out;
        }
    }
    if (!write_cache(newfd, active_cache, active_nr) && !rename(".dircache/index.lock", ".dircache/index"))
        return 0;
out:
    unlink(".dircache/index.lock");
}

2.1 缓存读取逻辑

read_cache读取缓存逻辑:打开缓存文件.dircache/index,通过mmap将文件映射到内存,校验文件sha1,根据头enty个数还原缓存数据。

   int read_cache(void)
{
    int fd, i;
    struct stat st;
    unsigned long size, offset;
    void *map;
    struct cache_header *hdr;

    errno = EBUSY;
    if (active_cache)
        return error("more than one cachefile");
    errno = ENOENT;
    sha1_file_directory = getenv(DB_ENVIRONMENT);
    if (!sha1_file_directory)
        sha1_file_directory = DEFAULT_DB_ENVIRONMENT;
    if (access(sha1_file_directory, X_OK) < 0)
        return error("no access to SHA1 file directory");
    fd = open(".dircache/index", O_RDONLY);
    if (fd < 0)
        return (errno == ENOENT) ? 0 : error("open failed");

    map = (void *)-1;
    if (!fstat(fd, &st)) {
        map = NULL;
        size = st.st_size;
        errno = EINVAL;
        if (size > sizeof(struct cache_header))
            map = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
    }
    close(fd);
    if (-1 == (int)(long)map)
        return error("mmap failed");

    hdr = map;
    if (verify_hdr(hdr, size) < 0)
        goto unmap;

    // 根据缓存数量来申请内存,预留1.5倍空间
    active_nr = hdr->entries;
    active_alloc = alloc_nr(active_nr);
    active_cache = calloc(active_alloc, sizeof(struct cache_entry *));
    // 通过文件字节直接还原内存结构
    offset = sizeof(*hdr);
    for (i = 0; i < hdr->entries; i++) {
        struct cache_entry *ce = map + offset;
        offset = offset + ce_size(ce);
        active_cache[i] = ce;
    }
    return active_nr;

unmap:
    munmap(map, size);
    errno = EINVAL;
    return error("verify header failed");
}

verify_hdr校验缓存头:通过缓存重新计算sha1,跟缓存头sha1对比进行校验

static int verify_hdr(struct cache_header *hdr, unsigned long size)
{
   SHA_CTX c;
   unsigned char sha1[20];
   // 基础校验, 签名&版本
   if (hdr->signature != CACHE_SIGNATURE)
      return error("bad signature");
   if (hdr->version != 1)
      return error("bad version");
   SHA1_Init(&c);
   // 提取缓存头中除了sha1部分的数据
   SHA1_Update(&c, hdr, offsetof(struct cache_header, sha1));
   // 提取缓存内容数据,hdr+1是指跳过缓存头
   SHA1_Update(&c, hdr+1, size - sizeof(*hdr));
   // 计算sha1
   SHA1_Final(sha1, &c);
   // 对比sha1
   if (memcmp(sha1, hdr->sha1, 20))
      return error("bad header sha1");
   return 0;
}

特殊宏函数说明:

struct cache_entry {
   struct cache_time ctime;
   struct cache_time mtime;
   unsigned int st_dev;
   unsigned int st_ino;
   unsigned int st_mode;
   unsigned int st_uid;
   unsigned int st_gid;
   unsigned int st_size;
   unsigned char sha1[20];
   unsigned short namelen;
   // 0长度字符数组,并不占用空间
   unsigned char name[0];
};

// 计算缓存项的长度
#define ce_size(ce) cache_entry_size((ce)->namelen)

/*
 offsetof(struct cache_entry,name)获取name在结构体中的偏移,即除去name之外的缓存项目大小
  & ~7 将最低3位置0,也就是说将最终的长度对8对齐
  +8  为了防止将最低3位置0后大小变小,因此提前+8来预留空间
  
 */
#define cache_entry_size(len) ((offsetof(struct cache_entry,name) + (len) + 8) & ~7)

2.2 文件加入缓存逻辑

获取文件meta信息,给文件建立索引,将文件加入缓存entry.


static int add_file_to_cache(char *path)
{
   int size, namelen;
   struct cache_entry *ce;
   struct stat st;
   int fd;

   fd = open(path, O_RDONLY);
   if (fd < 0) {
      if (errno == ENOENT)
         return remove_file_from_cache(path);
      return -1;
   }
   if (fstat(fd, &st) < 0) {
      close(fd);
      return -1;
   }
   namelen = strlen(path);
   size = cache_entry_size(namelen);
   ce = malloc(size);
   memset(ce, 0, size);
   memcpy(ce->name, path, namelen);
   ce->ctime.sec = st.st_ctime;
   ce->ctime.nsec = st.st_ctimespec.tv_nsec;
   ce->mtime.sec = st.st_mtime;
   ce->mtime.nsec = st.st_mtimespec.tv_nsec;
   ce->st_dev = st.st_dev;
   ce->st_ino = st.st_ino;
   ce->st_mode = st.st_mode;
   ce->st_uid = st.st_uid;
   ce->st_gid = st.st_gid;
   ce->st_size = st.st_size;
   ce->namelen = namelen;

   if (index_fd(path, namelen, ce, fd, &st) < 0)
      return -1;

   return add_cache_entry(ce);
}

文件建索引流程:将文件mmap到内存,使用zlib压缩meta信息(blob size + null byte), 压缩文件内容,计算sha1,根据sha1计算缓存文件名,写入缓存文件。


static int index_fd(const char *path, int namelen, struct cache_entry *ce, int fd, struct stat *st)
{
   z_stream stream;
   int max_out_bytes = namelen + st->st_size + 200;
   void *out = malloc(max_out_bytes);
   void *metadata = malloc(namelen + 200);
   void *in = mmap(NULL, st->st_size, PROT_READ, MAP_PRIVATE, fd, 0);
   SHA_CTX c;

   close(fd);
   if (!out || (int)(long)in == -1)
      return -1;

   memset(&stream, 0, sizeof(stream));
   deflateInit(&stream, Z_BEST_COMPRESSION);

   // 压缩meta信息
   /*
    * ASCII size + nul byte
    */    
   stream.next_in = metadata;
   stream.avail_in = 1+sprintf(metadata, "blob %lu", (unsigned long) st->st_size);
   stream.next_out = out;
   stream.avail_out = max_out_bytes;
   while (deflate(&stream, 0) == Z_OK)
      /* nothing */;

   /*
    * File content
    */
   // 压缩文件内容
   stream.next_in = in;
   stream.avail_in = st->st_size;
   while (deflate(&stream, Z_FINISH) == Z_OK)
      /*nothing */;

   deflateEnd(&stream);
   
   SHA1_Init(&c);
   SHA1_Update(&c, out, stream.total_out);
   // 计算sha1
   SHA1_Final(ce->sha1, &c);

   // 文件内容写入缓存
   return write_sha1_buffer(ce->sha1, out, stream.total_out);
}
int write_sha1_buffer(unsigned char *sha1, void *buf, unsigned int size)
{
   char *filename = sha1_file_name(sha1);
   int i, fd;

   fd = open(filename, O_WRONLY | O_CREAT | O_EXCL, 0666);
   if (fd < 0)
      return (errno == EEXIST) ? 0 : -1;
   write(fd, buf, size);
   close(fd);
   return 0;
}

根据哈希值计算文件名: 第一个哈希值决定目录,剩余的19个哈希值决定文件名

/*
 * NOTE! This returns a statically allocated buffer, so you have to be
 * careful about using it. Do a "strdup()" if you need to save the
 * filename.
 */
char *sha1_file_name(unsigned char *sha1)
{
   int i;
   static char *name, *base;

   if (!base) {
      char *sha1_file_directory = getenv(DB_ENVIRONMENT) ? : DEFAULT_DB_ENVIRONMENT;
      int len = strlen(sha1_file_directory);
      base = malloc(len + 60);
      memcpy(base, sha1_file_directory, len);
      memset(base+len, 0, 60);
      // .dircache/objects/xx/xx{19}
      base[len] = '/';
      base[len+3] = '/';
      name = base + len + 1;
   }
   for (i = 0; i < 20; i++) {
      static char hex[] = "0123456789abcdef";
      unsigned int val = sha1[i];
      //根据哈希值计算文件名。第一个哈希值决定目录,剩余的19个哈希值决定文件名
      // i > 0是用来跳过"/", 第一个哈希值在"/"前,剩余的19个哈希值在"/"后
      char *pos = name + i*2 + (i > 0);
      *pos++ = hex[val >> 4];
      *pos = hex[val & 0xf];
   }
   return base;
}

add_cache_entry将文件加入缓存: 缓存按照文件路径排序,二分查找。


static int add_cache_entry(struct cache_entry *ce)
{
   int pos;

   pos = cache_name_pos(ce->name, ce->namelen);

   /* existing match? Just replace it */
   if (pos < 0) {
      active_cache[-pos-1] = ce;
      return 0;
   }

   /* Make sure the array is big enough .. */
   if (active_nr == active_alloc) {
      active_alloc = alloc_nr(active_alloc);
      active_cache = realloc(active_cache, active_alloc * sizeof(struct cache_entry *));
   }

   /* Add it in.. */
   active_nr++;
   // 要插入的位置不在最后,从pos开始元素向后移动
   if (active_nr > pos)
      memmove(active_cache + pos + 1, active_cache + pos, (active_nr - pos - 1) * sizeof(ce));
   active_cache[pos] = ce;
   return 0;
}

cache_name_pos根据名字获取缓存项的位置,二分查找。 这个函数返回值比较特殊, 没有找到返回最后一次查找first(>0),找到了返回 -p -1(<0) 。这样设计,基于性能考虑,找到时返回了位置,在没有找到的时候返回了要插入的位置。

 static int cache_name_pos(const char *name, int namelen)
{
   int first, last;

   first = 0;
   last = active_nr;
   while (last > first) {
      int next = (last + first) >> 1;
      struct cache_entry *ce = active_cache[next];
      int cmp = cache_name_compare(name, namelen, ce->name, ce->namelen);
      if (!cmp)
         return -next-1;
      if (cmp < 0) {
         last = next;
         continue;
      }
      first = next+1;
   }
   return first;
}

cache_name_compare,先比较名称,再比较长度, 0相等,-1 小于,1 大于

static int cache_name_compare(const char *name1, int len1, const char *name2, int len2)
{
   int len = len1 < len2 ? len1 : len2;
   int cmp;

   cmp = memcmp(name1, name2, len);
   if (cmp)
      return cmp;
   if (len1 < len2)
      return -1;
   if (len1 > len2)
      return 1;
   return 0;
}

3、show-diff.c

核心逻辑:首先读取缓存,针对缓存中的每个entry,根据meta判断文件当前是否有变更,如果有打印文件路径以及sha1,并且根据sha1找到文件并解压文件内容,并调用系统的diff(diff -u - ${name})命令打印差异。 对diff命令,-代表标准输入


int main(int argc, char **argv)
{
   int entries = read_cache();
   int i;

   if (entries < 0) {
      perror("read_cache");
      exit(1);
   }
   for (i = 0; i < entries; i++) {
      struct stat st;
      struct cache_entry *ce = active_cache[i];
      int n, changed;
      unsigned int mode;
      unsigned long size;
      char type[20];
      void *new;

      if (stat(ce->name, &st) < 0) {
         printf("%s: %s\n", ce->name, strerror(errno));
         continue;
      }
      changed = match_stat(ce, &st);
      if (!changed) {
         printf("%s: ok\n", ce->name);
         continue;
      }
      printf("%.*s:  ", ce->namelen, ce->name);
      for (n = 0; n < 20; n++)
         printf("%02x", ce->sha1[n]);
      printf("\n");
      new = read_sha1_file(ce->sha1, type, &size);
      show_differences(ce, &st, new, size);
      free(new);
   }
   return 0;
}

show_differences 执行系统命令diff打印差异

static void show_differences(struct cache_entry *ce, struct stat *cur,
   void *old_contents, unsigned long long old_size)
{
   static char cmd[1000];
   FILE *f;

   snprintf(cmd, sizeof(cmd), "diff -u - %s", ce->name);
   f = popen(cmd, "w");
   fwrite(old_contents, old_size, 1, f);
   pclose(f);
}

4、cat-file.c

核心逻辑:按照sha1计算缓存文件名,读取文件解压将内容写入临时文件,并且打印类型以及长度

int main(int argc, char **argv)
{
   unsigned char sha1[20];
   char type[20];
   void *buf;
   unsigned long size;
   char template[] = "temp_git_file_XXXXXX";
   int fd;

   if (argc != 2 || get_sha1_hex(argv[1], sha1))
      usage("cat-file: cat-file <sha1>");
   buf = read_sha1_file(sha1, type, &size);
   if (!buf)
      exit(1);
   fd = mkstemp(template);
   if (fd < 0)
      usage("unable to create tempfile");
   if (write(fd, buf, size) != size)
      strcpy(type, "bad");
   printf("%s: %s\n", template, type);
}


5、write-tree.c

核心逻辑:读取文件缓存数据,组成树内容。内容格式:tree size null mode name null [mode name null] mode name null。然后根据文件内容计算sha1,根据sha1计算文件路径,将压缩后的数据写入文件


int main(int argc, char **argv)
{
   unsigned long size, offset, val;
   int i, entries = read_cache();
   char *buffer;

   if (entries <= 0) {
      fprintf(stderr, "No file-cache to create a tree of\n");
      exit(1);
   }

   /* Guess at an initial size */
   size = entries * 40 + 400;
   buffer = malloc(size);
   offset = ORIG_OFFSET;

   for (i = 0; i < entries; i++) {
      struct cache_entry *ce = active_cache[i];
      if (check_valid_sha1(ce->sha1) < 0)
         exit(1);
      // 空间不够重新申请
      if (offset + ce->namelen + 60 > size) {
         size = alloc_nr(offset + ce->namelen + 60);
         buffer = realloc(buffer, size);
      }
      // 格式:十进制权限 文件名 NULL sha1
      offset += sprintf(buffer + offset, "%o %s", ce->st_mode, ce->name);
      buffer[offset++] = 0;
      memcpy(buffer + offset, ce->sha1, 20);
      offset += 20;
   }
   /*
      offset - ORIG_OFFSET 数据长度
      ORIG_OFFSET 数据偏移
      将数据长度写到预留空间的尾部,向前填入"tree ",并调整buffer offset位置
      整体数据格式: tree size null mode name null sha1 [mode name null sha1] ... mode name null sha1
   */
   i = prepend_integer(buffer, offset - ORIG_OFFSET, ORIG_OFFSET);
   i -= 5;
   memcpy(buffer+i, "tree ", 5);

   buffer += i;
   offset -= i;

   write_sha1_file(buffer, offset);
   return 0;
}

prepend_integer 从i个位置向前以字符串形式填写val,并返回新的i

static int prepend_integer(char *buffer, unsigned val, int i)
{
   buffer[--i] = '\0';
   do {
      buffer[--i] = '0' + (val % 10);
      val /= 10;
   } while (val);
   return i;
}

数据样例

x buf x/60b buf

https://wenku.baidu.com/view/62a4aea6e63a580216fc700abb68a98271feacb0.html?_wkts_=1676432746759&bdQuery=lldb+%E8%BF%9E%E7%BB%AD%E5%86%85%E5%AD%98


6、commit-tree.c

基础逻辑:校验参数后,获取当前登录用户的密码相关信息,用来获取用户名、email,记录changgelog。记录当前commit sha1,parent sha1 、author 、committer以及 评论信息,调整缓存头"commit size." 根据文件内容sha1计算文件名,并保存到object目录。


int main(int argc, char **argv)
{
   int i, len;
   int parents = 0;
   unsigned char tree_sha1[20];
   unsigned char parent_sha1[MAXPARENT][20];
   char *gecos, *realgecos;
   char *email, realemail[1000];
   char *date, *realdate;
   char comment[1000];
   struct passwd *pw;
   time_t now;
   char *buffer;
   unsigned int size;

   if (argc < 2 || get_sha1_hex(argv[1], tree_sha1) < 0)
      usage("commit-tree <sha1> [-p <sha1>]* < changelog");

   for (i = 2; i < argc; i += 2) {
      char *a, *b;
      a = argv[i]; b = argv[i+1];
      if (!b || strcmp(a, "-p") || get_sha1_hex(b, parent_sha1[parents]))
         usage("commit-tree <sha1> [-p <sha1>]* < changelog");
      parents++;
   }
   if (!parents)
      fprintf(stderr, "Committing initial tree %s\n", argv[1]);
   // 读取当前用户密码信息,用来记录changelog
   pw = getpwuid(getuid());
   if (!pw)
      usage("You don't exist. Go away!");
   realgecos = pw->pw_gecos;
   len = strlen(pw->pw_name);
   memcpy(realemail, pw->pw_name, len);
   realemail[len] = '@';
   gethostname(realemail+len+1, sizeof(realemail)-len-1);
   time(&now);
   realdate = ctime(&now);

   gecos = getenv("COMMITTER_NAME") ? : realgecos;
   email = getenv("COMMITTER_EMAIL") ? : realemail;
   date = getenv("COMMITTER_DATE") ? : realdate;

   remove_special(gecos); remove_special(realgecos);
   remove_special(email); remove_special(realemail);
   remove_special(date); remove_special(realdate);

   init_buffer(&buffer, &size);
   add_buffer(&buffer, &size, "tree %s\n", sha1_to_hex(tree_sha1));

   /*
    * NOTE! This ordering means that the same exact tree merged with a
    * different order of parents will be a _different_ changeset even
    * if everything else stays the same.
    */
   for (i = 0; i < parents; i++)
      add_buffer(&buffer, &size, "parent %s\n", sha1_to_hex(parent_sha1[i]));

   /* Person/date information */
   add_buffer(&buffer, &size, "author %s <%s> %s\n", gecos, email, date);
   add_buffer(&buffer, &size, "committer %s <%s> %s\n\n", realgecos, realemail, realdate);

   /* And add the comment */
   while (fgets(comment, sizeof(comment), stdin) != NULL)
      add_buffer(&buffer, &size, "%s", comment);

   finish_buffer("commit ", &buffer, &size);

   write_sha1_file(buffer, size);
   return 0;
} 

缓存处理逻辑:初始化了16K基本缓存大小,预留了40字节头信息,每32k realloc一次内存。代码存在BUG,应该是笔误,16k 32k应该设置成一样大小,否则特殊场景会崩。


#define BLOCKING (1ul << 14)
#define ORIG_OFFSET (40)

/*
 * Leave space at the beginning to insert the tag
 * once we know how big things are.
 *
 * FIXME! Share the code with "write-tree.c"
 */
static void init_buffer(char **bufp, unsigned int *sizep)
{
   char *buf = malloc(BLOCKING);
   memset(buf, 0, ORIG_OFFSET);
   *sizep = ORIG_OFFSET;
   *bufp = buf;
}

static void add_buffer(char **bufp, unsigned int *sizep, const char *fmt, ...)
{
   char one_line[2048];
   va_list args;
   int len;
   unsigned long alloc, size, newsize;
   char *buf;

   va_start(args, fmt);
   len = vsnprintf(one_line, sizeof(one_line), fmt, args);
   va_end(args);
   size = *sizep;
   newsize = size + len;
   alloc = (size + 32767) & ~32767;
   buf = *bufp;
   if (newsize > alloc) {
      alloc = (newsize + 32767) & ~32767;   
      buf = realloc(buf, alloc);
      *bufp = buf;
   }
   *sizep = newsize;
   memcpy(buf + size, one_line, len);
}


五、总结

设计巧妙,代码简洁工整,注重性能,注释自由

1、基础模型

git里两个基本概念:The Object Database、Current Directory Cache

The Object Database

对象数据库,对象内容采用zlib压缩,对象名采用sha1,包含三类对象,BLOB(普通文件内容)、TREE(文件权限/名称/sha1集合,表示一次提交的内容)、

CHANGESET(TREE父子链,表示变更历史)。

Current Directory Cache

git暂存区,当前缓存的文件的META信息

2、功能维度

git第一版代码保持了linux工具链风格,每个工具只干一件事情,底层工具组合在一起完成代码管理功能

1)update-cache:git add雏形,保存最新文件内容到objects里,并更新本地目录缓存

2)show-diff:git status雏形,实现了缓存中的文件与最新状态差异对比

3)write-tree: git commit雏形1,保存工作区最新缓存树到objects目录并生成sha1

4)commit-tree: git commit雏形2,保存提交的树的sha1以及的parent树的sha1到object目录并生成sha1

理解了这些工具实现逻辑,不难想象目前git的各种命令和概念的原理。比如分支,分支本质只是一个changeset的sha1,基于sha1可以反向追溯每一次提交的tree。要实现两次提交diff,对比两个tree可以找到目录差异以及变化的文件,基于文件的sha1可以找到文件进而对比出文件的变化。分支拷贝,底层操作只需要拷贝一个sha1值,等等。

3、性能维度

实现功能同时充分考量性能,缓存项头格式设计、二分查找返回值的设计、文件内容头信息、文件访问采用mmap避免内核缓冲区到用户缓冲区数据拷贝

虽然对常规业务来讲,可读性高于性能,但随手可得的优化是程序员基本素养