git第一版源码(e83c5163)完全注释

1. 综述

学习Git,有很多不错的图书和资料,我通常比较推荐以下资料:

  1. 蒋鑫的《Git权威指南》,这本书虽然有点厚,但讲了GIT的方方面面(一句话说不完),是目前我读过的最全面介绍git的书,适合作为案头常备的参考读物。
  2. Git官方推荐的图书 《Pro Git》,现在在线也可以阅读该书的中文版: Pro Git
  3. Git官方文档 https://git-scm.com/docs,比较严肃,所以读起来会比较晦涩,需要有点耐心,但这个是Git命令最好的参考文档

目前《Git权威指南已经绝版》有需要电子书可加我个人微信,说明Git电子书,个人微信见公众号。

在Git官方文档的 User Manual 中介绍提到,如果你想阅读 Git 源码,早期的第一版是最好的出发点。
在这里插入图片描述

来自: A birds-eye view of Git’s source code

于是阅读了 Git 的第一版代码。

Git 第一版代码很简单, 算上 Makefile 和 README 文件一共才11个文件,按照这篇文章的说明,所有代码总计1089行,虽然代码很小,但已经具有了Git的原型。

这个版本代码编译后生成7个可执行文件,对应于7个命令,这些文件同现在命令的比较如下:

git第一版命令和当前git命令对比

图片来自这篇文章

后面会演示如何下载,编译代码和运行,然后逐文件进行注释。

2. 下载和编译运行

# 下载代码
$ git clone https://github.com/git/git.git .

# 现在最开始的10个提交
$ git log --reverse --oneline | head -10
e83c516 Initial revision of "git", the information manager from hell
8bc9a0c Add copyright notices.
e497ea2 Make read-tree actually unpack the whole tree.
bf0c6e8 Make "cat-file" output the file contents to stdout.
19b2860 Use "-Wall -O2" for the compiler to get more warnings.
24778e3 Factor out "read_sha1_file" into mapping/inflating/unmapping.
2ade934 Add "check_sha1_signature()" helper function
2022211 Add first cut at "fsck-cache" that validates the SHA1 object store.
7660a18 Add new fsck-cache to Makefile.
9426167 Add "-lz" to link line to get in zlib.

# 基于第一个提交 e83c5163 生成分支 git-e83c5163, 并切换到该分支查看代码
$ git checkout -b git-e83c5163 e83c5163

为了编译代码, 还需要在Makefile中链接时添加 zlib 和 crypto 库的支持,如下:

LIBS= -lz -lssl -lcrypto

编译代码

make

3. 使用演示

以下是我使用编译生成的文件进行操作的演示:

# 初始化
$ ./init-db 
defaulting to private storage area

# 添加文件 Makefile (写入 blob 对象)
$ ./update-cache Makefile 
$ find .dircache -type f
.dircache/index
.dircache/objects/b0/4fb99b9a176ff05e03d5e6e739f0a82b83c56c

# 查看上一步写入的 Makefile blob 对象
$ ./cat-file b04fb99b9a176ff05e03d5e6e739f0a82b83c56c
temp_git_file_2MIyAT: blob
$ cat temp_git_file_2MIyAT

# 添加文件 README (写入 blob 对象)
$ ./update-cache README 
$ find .dircache -type f
.dircache/index
.dircache/objects/66/5025b11ce8fb16fadb7daebf77cb54a2ae39a1
.dircache/objects/b0/4fb99b9a176ff05e03d5e6e739f0a82b83c56c

# 将添加的 Makefile 和 README (写入 tree 对象)
$ ./write-tree 
261232a8e0ffbfb43a3bed535c3539d9647901f9

# 查看上一步写入的 tree 对象
$ ./cat-file 261232a8e0ffbfb43a3bed535c3539d9647901f9
temp_git_file_9CFutq: tree
$ cat temp_git_file_9CFutq

# 提交写入的 tree 对象, 第一次提交 (写入 commit 对象)
$ echo "First Commit!" | ./commit-tree 261232a8e0ffbfb43a3bed535c3539d9647901f9
Committing initial tree 261232a8e0ffbfb43a3bed535c3539d9647901f9
76f9697854a64d62f4a511ca09db3aa4879cdc10
$ find .dircache -type f
.dircache/index
.dircache/objects/76/f9697854a64d62f4a511ca09db3aa4879cdc10
.dircache/objects/66/5025b11ce8fb16fadb7daebf77cb54a2ae39a1
.dircache/objects/26/1232a8e0ffbfb43a3bed535c3539d9647901f9
.dircache/objects/b0/4fb99b9a176ff05e03d5e6e739f0a82b83c56c

# 查看上一步写入的 commit 对象
$ ./cat-file 76f9697854a64d62f4a511ca09db3aa4879cdc10
temp_git_file_0EabYS: commit
$ cat temp_git_file_0EabYS

# 修改 Makefile, 写入修改后的 blob 对象, 然后添加到 tree 对象中
$ vim Makefile
$ ./show-diff Makefile b04fb99b9a176ff05e03d5e6e739f0a82b83c56c
$ ./update-cache Makefile 
$ ./write-tree 
0cecb5da03708fd258e4d6b2019a78578e1c9152
$ find .dircache -type f
.dircache/index
.dircache/objects/76/f9697854a64d62f4a511ca09db3aa4879cdc10
.dircache/objects/0c/ecb5da03708fd258e4d6b2019a78578e1c9152
.dircache/objects/66/5025b11ce8fb16fadb7daebf77cb54a2ae39a1
.dircache/objects/7a/5eea9940bd3c11de39c4862767c623867c9374
.dircache/objects/26/1232a8e0ffbfb43a3bed535c3539d9647901f9
.dircache/objects/b0/4fb99b9a176ff05e03d5e6e739f0a82b83c56c

# 提交写入的 tree 对象, 第二次提交 (写入 commit 对象)
$ echo "Second Commit!" | ./commit-tree 0cecb5da03708fd258e4d6b2019a78578e1c9152 -p 76f9697854a64d62f4a511ca09db3aa4879cdc10
6d55fcb1945d84b22cd06c64bfab804ad4ded798
$ find .dircache -type f
.dircache/index
.dircache/objects/76/f9697854a64d62f4a511ca09db3aa4879cdc10
.dircache/objects/0c/ecb5da03708fd258e4d6b2019a78578e1c9152
.dircache/objects/66/5025b11ce8fb16fadb7daebf77cb54a2ae39a1
.dircache/objects/7a/5eea9940bd3c11de39c4862767c623867c9374
.dircache/objects/26/1232a8e0ffbfb43a3bed535c3539d9647901f9
.dircache/objects/6d/55fcb1945d84b22cd06c64bfab804ad4ded798
.dircache/objects/b0/4fb99b9a176ff05e03d5e6e739f0a82b83c56c

# 使用 cat-file 提取最后一次写入的 commit 对象
$ ./cat-file 6d55fcb1945d84b22cd06c64bfab804ad4ded798
temp_git_file_SU60Jd: commit
# 查看提取到的 commit 对象数据
$ cat temp_git_file_SU60Jd 
tree 0cecb5da03708fd258e4d6b2019a78578e1c9152
parent 76f9697854a64d62f4a511ca09db3aa4879cdc10
author guyongqiang,,, <ygu@guyongqiangx> Sat Jul 17 18:18:58 2021
committer guyongqiang,,, <ygu@guyongqiangx> Sat Jul 17 18:18:58 2021

Second Commit!

4. 逐行源码分析

4.1 文件cache.h

cache.h文件定义了版本库目录名称(".dircache/objects")和索引文件(".dircache/index")的格式,包括索引文件头部struct cache_header和索引文件记录struct cache_entry的数据结构。

另外包含了一些给各个模块公用的函数声明。

#ifndef CACHE_H
#define CACHE_H

#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stddef.h>
#include <stdlib.h>
#include <stdarg.h>
#include <errno.h>
#include <sys/mman.h>

#include <openssl/sha.h>
#include <zlib.h>
#include <string.h> /* memcmp,memmove,memset,memcpy,strlen */

/*
 * Basic data structures for the directory cache
 *
 * NOTE NOTE NOTE! This is all in the native CPU byte format. It's
 * not even trying to be portable. It's trying to be efficient. It's
 * just a cache, after all.
 */

/* 暂存区文件(".dircache/index")头部格式 */
#define CACHE_SIGNATURE 0x44495243	/* "DIRC" */
struct cache_header {
	unsigned int signature;
	unsigned int version;
	unsigned int entries;
	unsigned char sha1[20];
};

/*
 * The "cache_time" is just the low 32 bits of the
 * time. It doesn't matter if it overflows - we only
 * check it for equality in the 32 bits we save.
 */
struct cache_time {
	unsigned int sec;
	unsigned int nsec;
};

/*
 * dev/ino/uid/gid/size are also just tracked to the low 32 bits
 * Again - this is just a (very strong in practice) heuristic that
 * the inode hasn't changed.
 */
/*
 * 暂存区文件(".dircache/index")内单条记录格式
 * 最后一个 name 成员是大小为0的数组(用作占位符, 具体依赖于名字长度),
 * 一条 cache entry 由前面定长的部分和最后变长的部分构成, 所以总体长度不是固定的
 */
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;
	/* 每条 cache entry 记录的 name 实际占用的长度由前面的 namelen 决定, 这里的 name[0] 就是一个大小为0的占位符 */
	unsigned char name[0];
};

/* 用于存储版本库目录路径, 默认为 ".dircache/objects", 实际没有使用, 每次都重新设置 */
const char *sha1_file_directory;
/* 内存中的 cache entry 缓存数组指针 */
struct cache_entry **active_cache = NULL;
/*
 *    active_nr: 为暂存区文件 ".dircache/index" 包含的实际 cache entry 数目
 * active_alloc: 为内存中可存放的 cache entry 数目(前面 cative_nr 部分已使用)
 */
unsigned int active_nr = 0, active_alloc = 0;

/*
 * 版本库目录的环境变量和默认名称, 当前git版本库目录已经改为".git/objects"了
 */
#define DB_ENVIRONMENT "SHA1_FILE_DIRECTORY"
#define DEFAULT_DB_ENVIRONMENT ".dircache/objects"

/*
 * 根据传入name字符串的长度len, 计算所在cache entry的大小(8字节对齐)
 */
#define cache_entry_size(len) ((offsetof(struct cache_entry,name) + (len) + 8) & ~7)
/*
 * 根据传入cache entry的记录, 计算cache entry的大小
 */
#define ce_size(ce) cache_entry_size((ce)->namelen)

#define alloc_nr(x) (((x)+16)*3/2)

/* Initialize the cache information */
/* 读取索引文件".dircache/index", 建立缓存, 返回条目数 */
extern int read_cache(void);

/* Return a statically allocated filename matching the sha1 signature */
/* 获取 sha1 值对应的文件名 */
extern char *sha1_file_name(unsigned char *sha1);

/* Write a memory buffer out to the sha file */
/* 将 buf 中大小为 size 的数据写入到 sha1 值对应的文件中 */
extern int write_sha1_buffer(unsigned char *sha1, void *buf, unsigned int size);

/* Read and unpack a sha1 file into memory, write memory to a sha1 file */
/* 提取 sha1 值对应文件的内容(解压缩后返回), 返回内容类型(blob/tree/commit)和 size */
extern void * read_sha1_file(unsigned char *sha1, char *type, unsigned long *size);
/* 压缩 buf 数据, 计算 sha1 值, 并写入对应的 sha1 文件中 */
extern int write_sha1_file(char *buf, unsigned len);

/* Convert to/from hex/sha1 representation */
/* 将 sha1 字符串转换成相应的 sha1 值 */
extern int get_sha1_hex(char *hex, unsigned char *sha1);
/* 将 sha1 值转换成相应的 sha1 字符串 */
extern char *sha1_to_hex(unsigned char *sha1);	/* static buffer! */

/* General helper functions */
extern void usage(const char *err);

#endif /* CACHE_H */

4.2 文件read-cache.c

read-cache.c文件定义了一些通用的函数, 包括 sha1 字符串和 sha1 数据的互转, 版本库中 sha1 文件的读写操作, 暂存区索引文件(".dircache/index")的读取操作。

#include "cache.h"

/*
 * read-cache.c 定义了各组件共用的函数
 */

/* 用于存储版本库目录路径, 默认为 ".dircache/objects", 实际没有使用, 每次都重新设置 */
const char *sha1_file_directory = NULL;
/* 内存中的 cache entry 缓存数组指针 */
struct cache_entry **active_cache = NULL;
/*
 *    active_nr: 为暂存区文件 ".dircache/index" 包含的实际 cache entry 数目
 * active_alloc: 为内存中可存放的 cache entry 数目(前面 cative_nr 部分已使用)
 */
unsigned int active_nr = 0, active_alloc = 0;

void usage(const char *err)
{
	fprintf(stderr, "read-tree: %s\n", err);
	exit(1);
}

/*
 * 将字符c[0-9a-fA-F]转换成对应的16进制数值
 */
static unsigned hexval(char c)
{
	if (c >= '0' && c <= '9')
		return c - '0';
	if (c >= 'a' && c <= 'f')
		return c - 'a' + 10;
	if (c >= 'A' && c <= 'F')
		return c - 'A' + 10;
	return ~0;
}

/*
 * 将 sha1 字符串转换成相应的 sha1 值
 * [hex]"91450428" --> [sha1]0x91,0x45,0x04,0x28
 */
int get_sha1_hex(char *hex, unsigned char *sha1)
{
	int i;
	for (i = 0; i < 20; i++) {
		/* 将相邻的两个 char 转换成一个 byte */
		unsigned int val = (hexval(hex[0]) << 4) | hexval(hex[1]);
		if (val & ~0xff)
			return -1;
		*sha1++ = val;
		hex += 2;
	}
	return 0;
}

/*
 * 将 sha1 值转换成相应的 sha1 字符串
 * [sha1]0x91,0x45,0x04,0x28 --> "91450428"
 */
char * sha1_to_hex(unsigned char *sha1)
{
	static char buffer[50];
	static const char hex[] = "0123456789abcdef";
	char *buf = buffer;
	int i;

	for (i = 0; i < 20; i++) {
		unsigned int val = *sha1++;
		*buf++ = hex[val >> 4];
		*buf++ = hex[val & 0xf];
	}
	return buffer;
}

/*
 * 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.
 */
/*
 * 返回 sha1 值对应的文件名
 * 如16进制数据: 914504285ab1fc2a7fca88dbf15f2b48a20b502d
 *   返回字符串: ".dircache/objects/91/4504285ab1fc2a7fca88dbf15f2b48a20b502d"
 */
char *sha1_file_name(unsigned char *sha1)
{
	int i;
	static char *name, *base;

	/* base 为 static 指针, 第一次分配并设置为 ".dircache/objects/__/________...________" 模式, 下次直接使用 */
	if (!base) {
		/*
		 * char *sha1_file_directory = getenv("SHA1_FILE_DIRECTORY") ? : ".dircache/objects";
		 * base = ".dircache/objects/__/________...________"
		 *                           |
		 *                           name
		 */
		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);
		base[len] = '/';
		base[len+3] = '/';
		name = base + len + 1;
	}
	for (i = 0; i < 20; i++) {
		static char hex[] = "0123456789abcdef";
		unsigned int val = sha1[i];
		char *pos = name + i*2 + (i > 0); /* (i>0)用于调整文件名中的'/'字符带来的偏差 */
		*pos++ = hex[val >> 4];
		*pos = hex[val & 0xf];
	}
	return base;
}

/*
 * 返回 sha1 值对应的文件内容(解压缩后返回)
 */
void * read_sha1_file(unsigned char *sha1, char *type, unsigned long *size)
{
	z_stream stream;
	char buffer[8192];
	struct stat st;
	int i, fd, ret, bytes;
	void *map, *buf;
	/* 将 sha1 值转换成文件名 */
	char *filename = sha1_file_name(sha1);

	/* 打开文件 */
	fd = open(filename, O_RDONLY);
	if (fd < 0) {
		perror(filename);
		return NULL;
	}
	/* 获取文件大小 */
	if (fstat(fd, &st) < 0) {
		close(fd);
		return NULL;
	}
	/* 将文件映射到内存,方便访问 */
	map = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
	close(fd);
	if (-1 == (int)(long)map)
		return NULL;

	/* Get the data stream */
	/* 初始化 zlib stream 结构体 */
	memset(&stream, 0, sizeof(stream));
	stream.next_in = map;
	stream.avail_in = st.st_size;
	stream.next_out = buffer;
	stream.avail_out = sizeof(buffer);

	inflateInit(&stream);
	ret = inflate(&stream, 0);
	/*
	 * 解压数据后, 解析提取头部数据到 type (blob/tree/commit) 和 size 中
	 * 原始数据的头部格式为: <ascii tag without space> + <space> + <ascii decimal size> + <byte\0> + <binary object data>
	 *                即: <type>                    + ' '     + <size>               + '\0'     + <binary data>
	 */
	if (sscanf(buffer, "%10s %lu", type, size) != 2)
		return NULL;
	bytes = strlen(buffer) + 1;
	/* 根据解析得到的 size (最终数据大小), 分配对应大小的 buf */
	buf = malloc(*size);
	if (!buf)
		return NULL;

	/* 复制第一次解压缩后, 头部后面的二进制数据到缓冲区 buf */
	memcpy(buf, buffer + bytes, stream.total_out - bytes);
	/* 设置 bytes 为已经解压缩得到的数据长度值 */
	bytes = stream.total_out - bytes;
	if (bytes < *size && ret == Z_OK) {
		stream.next_out = buf + bytes;
		stream.avail_out = *size - bytes;
		/* 解压剩余数据 */
		while (inflate(&stream, Z_FINISH) == Z_OK)
			/* nothing */;
	}
	/* 解压结束 */
	inflateEnd(&stream);
	return buf;
}

/*
 * 将 buf 数据写入到文件中
 * 1. 压缩 buf 数据;
 * 2. 计算压缩数据的 sha1 值;
 * 3. 将压缩后数据写入 sha1 值对应的文件中;
 */
int write_sha1_file(char *buf, unsigned len)
{
	int size;
	char *compressed;
	z_stream stream;
	unsigned char sha1[20];
	SHA_CTX c;

	/* 压缩传入的 buf 数据 */
	/* Set it up */
	memset(&stream, 0, sizeof(stream));
	deflateInit(&stream, Z_BEST_COMPRESSION);
	/* 根据初始化的算法和数据长度 len, 计算压缩后数据的上限, 实际得到的压缩数据不会超出这个上限 */
	size = deflateBound(&stream, len);
	compressed = malloc(size);

	/* Compress it */
	/* 设置 zlib stream 的输入输出输出数据指针 */
	stream.next_in = buf;
	stream.avail_in = len;
	stream.next_out = compressed;
	stream.avail_out = size;
	/* 压缩数据 */
	while (deflate(&stream, Z_FINISH) == Z_OK)
		/* nothing */;
	deflateEnd(&stream);
	/* 获取最终压缩后数据的大小 */
	size = stream.total_out;

	/* 计算压缩后数据的 sha1 值 */
	/* Sha1.. */
	SHA1_Init(&c);
	SHA1_Update(&c, compressed, size);
	SHA1_Final(sha1, &c);

	/* 将压缩后的数据写入到 sha1 值对应的文件中 */
	if (write_sha1_buffer(sha1, compressed, size) < 0)
		return -1;
	printf("%s\n", sha1_to_hex(sha1));
	return 0;
}

/*
 * 将 buf 中的数据写入到 sha1 值对应的文件中
 */
int write_sha1_buffer(unsigned char *sha1, void *buf, unsigned int size)
{
	/* 将 sha1 值转换成文件名 filename */
	char *filename = sha1_file_name(sha1);
	int i, fd;

	/* 打开文件, 写入 buf 中的数据 */
	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;
}

static int error(const char * string)
{
	fprintf(stderr, "error: %s\n", string);
	return -1;
}

/*
 * 检查暂存区文件(".dircache/index")的 header 数据
 * 1. 检查 header 部分的 signature 和 version
 * 2. 检查 header 部分存储的 sha1 值
 */
static int verify_hdr(struct cache_header *hdr, unsigned long size)
{
	SHA_CTX c;
	unsigned char sha1[20];

	/* 检查 signature 和 version */
	if (hdr->signature != CACHE_SIGNATURE)
		return error("bad signature");
	if (hdr->version != 1)
		return error("bad version");
	SHA1_Init(&c);
	/* 计算 header 部分哈希值 (不包含 sha1 成员本身) */
	SHA1_Update(&c, hdr, offsetof(struct cache_header, sha1));
	/* 计算 header 后面数据的哈希值 (所有的 cache entry) */
	SHA1_Update(&c, hdr+1, size - sizeof(*hdr));
	SHA1_Final(sha1, &c);
	/* 将最终得到的 sha1 值同 hdr 存储的 sha1 值比较 */
	if (memcmp(sha1, hdr->sha1, 20))
		return error("bad header sha1");
	return 0;
}

/* 读取索引文件".dircache/index", 建立缓存, 返回条目数 */
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;
	/* 检查 ".dircache" 是否具有可执行权限, 为什么是可执行权限? */
	if (access(sha1_file_directory, X_OK) < 0)
		return error("no access to SHA1 file directory");
	/* 打开文件 ".dircache/index" */
	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");

	/* 检查映射数据的 header */
	hdr = map;
	if (verify_hdr(hdr, size) < 0)
		goto unmap;

	/* 根据 header 中已有的条目数 */
	active_nr = hdr->entries;
	/* 新的总条目数为已有数据的 3/2 倍 */
	active_alloc = alloc_nr(active_nr);
	/* 根据新的条目数分配内存 */
	active_cache = calloc(active_alloc, sizeof(struct cache_entry *));

	/* offset 为暂存区文件内 cache entry 开始的位置 */
	offset = sizeof(*hdr);
	/* 逐个复制暂存区文件的 cache entry 到 active_cache[] 中 */
	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");
}

4.3 文件init-db.c

init-db.c用于建立版本库的基本结构,初始化生成版本库的256个子目录。

#include "cache.h"

/*
 * 命令: "init-db"
 * 示例: $ ./init-db
 *
 * 创建目录".dircache/objects/{00,01,02,...,fd,fe,ff}"
 * $ tree .dircache
 * .dircache
 * └── objects
 * 	├── 00
 * 	├── 01
 * 	├── 02
 * 	...
 * 	├── fd
 * 	├── fe
 * 	└── ff
 *
 * 257 directories, 0 files
 */
int main(int argc, char **argv)
{
	char *sha1_dir = getenv(DB_ENVIRONMENT), *path;
	int len, i, fd;

	/*
	 * 创建名为".dircache"的文件夹, 权限模式为0700
	 * 如果".dircache"已经存在,则mkdir会在这里失败并返回
	 *
	 * $ ./init-db
	 * unable to create .dircache: File exists
	 */
	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("SHA1_FILE_DIRECTORY"),判断环境变量"SHA1_FILE_DIRECTORY"指向的文件是否存在,其是否为目录 */
	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=".dircache/objects",创建该目录 */
	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);
		}
	}
	/*
	 * 在".dircache/objects"创建256个子目录,结果如下:
	 * $ tree .dircache
	 * .dircache
	 * └── objects
	 * 	├── 00
	 * 	├── 01
	 * 	├── 02
	 * 	...
	 * 	├── fd
	 * 	├── fe
	 * 	└── ff
	 *
	 * 257 directories, 0 files
	 */
	path = malloc(len + 40);
	memcpy(path, sha1_dir, len);
	for (i = 0; i < 256; i++) {
		sprintf(path+len, "/%02x", i);
		if (mkdir(path, 0700) < 0) {
			if (errno != EEXIST) {
				perror(path);
				exit(1);
			}
		}
	}
	return 0;
}

/* #
 * # init-db 使用示例
 * #
 * git-e83c5163$ ./init-db
 * defaulting to private storage area
 * git-e83c5163$ tree .dircache
 * .dircache
 * └── objects
 *     ├── 00
 *     ├── 01
 *     ...
 *     └── ff
 *
 * 257 directories, 0 files
 * git-e83c5163$ ./init-db
 * unable to create .dircache: File exists
 */

4.4 文件update-cache.c

update-cache.c用于添加文件的二进制数据到版本库中,并在暂存区索引文件实现文件的查找、添加和删除操作。

#include "cache.h"

/*
 * 比较字符串 name1 和 name2
 */
static int cache_name_compare(const char *name1, int len1, const char *name2, int len2)
{
	/* 获取 name1 和 name2 的最小长度 */
	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;
}

/*
 * 根据文件名 name, 使用二分查找其在 cache entry 中的位置
 * 找到, 返回 -pos
 * 没有找到, 返回插入位置 pos
 */
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 entry 中的条目
 */
static int remove_file_from_cache(char *path)
{
	/* 根据文件名查找 cache entry 中对应条目的位置 */
	int pos = cache_name_pos(path, strlen(path));
	if (pos < 0) {
		pos = -pos-1;
		active_nr--;
		if (pos < active_nr)
			/* 移动后面所有 cache entry 条目的内存 */
			/* 问题: 每一条 cache_entry 的内容是变长的, 怎么计算总长度? */
			memmove(active_cache + pos, active_cache + pos + 1, (active_nr - pos - 1) * sizeof(struct cache_entry *));
	}
}

/*
 * 使用二分法往内存的 cache entry 中插入新的数据
 */
static int add_cache_entry(struct cache_entry *ce)
{
	int pos;

	/* 使用二分法根据文件名查找 cache entry 中对应条目的位置, 找到了则得到其位置的 -pos, 没找到, 则返回插入该条目应该在的位置 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++;
	/* 移动插入位置后面的内存 */
	if (active_nr > pos)
		memmove(active_cache + pos + 1, active_cache + pos, (active_nr - pos - 1) * sizeof(ce));

	/* 在 cache entry 中存入新的数据 */
	active_cache[pos] = ce;
	return 0;
}

/*
 * 将 fd 指向的文件写入到 blob 数据中
 * 1. 压缩数据("blob 987654" + data)
 * 2. 计算压缩数据 sha1 值
 * 3. 将压缩后数据写入到 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);
	/* 将 fd 指定的文件映射到内存 */
	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);

	/*
	 * 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哈希值 */
	SHA1_Init(&c);
	SHA1_Update(&c, out, stream.total_out);
	SHA1_Final(ce->sha1, &c);

	/* 将压缩数据写入到 sha1 值对应的文件中 */
	return write_sha1_buffer(ce->sha1, out, stream.total_out);
}

/*
 * 将 path 指定的文件内容写入到 blob 数据中, 并将文件信息存放到 cache entry 中
 * 1. 获取 path 指定的文件信息
 * 2. 将文件内容写入到 blob 数据中
 *    1). 添加 "blob 93276" 头部 (93276 为假设的文件长度)
 *    2). 压缩头部和文件数据
 *    4). 计算压缩数据的 sha1 值
 *    3). 将压缩数据写入 sha1 值对应的文件中
 * 3. 将文件信息保存到 cache entry 中
 */
static int add_file_to_cache(char *path)
{
	int size, namelen;
	struct cache_entry *ce;
	struct stat st;
	int fd;

	/* 打开 path 指定文件, 用于后续提取文件信息和内容 */
	fd = open(path, O_RDONLY);
	if (fd < 0) {
		/* 如果打开文件失败的原因是找不到文件 */
		if (errno == ENOENT)
			/* 在内存的 cache entry 中删除该文件 */
			return remove_file_from_cache(path);
		return -1;
	}

	/* 获取文件信息 */
	if (fstat(fd, &st) < 0) {
		close(fd);
		return -1;
	}

	/* 分配 cache entry 条目, 用于存放文件信息 */
	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_ctim.tv_nsec;
	ce->mtime.sec = st.st_mtime;
	ce->mtime.nsec = st.st_mtim.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;

	/* 将文件内容写入到 blob 数据中 */
	if (index_fd(path, namelen, ce, fd, &st) < 0)
		return -1;

	/* 将 cache entry 条目添加到内存的缓存列表中 */
	return add_cache_entry(ce);
}

/*
 * 将 cache entry 中的数据写入到 newfd 指定的文件中
 */
static int write_cache(int newfd, struct cache_entry **cache, int entries)
{
	SHA_CTX c;
	struct cache_header hdr;
	int i;

	hdr.signature = CACHE_SIGNATURE;
	hdr.version = 1;
	hdr.entries = entries;

	/* 计算 cache entry 的哈希值 */
	SHA1_Init(&c);
	SHA1_Update(&c, &hdr, offsetof(struct cache_header, sha1));
	/* 遍历每个 cache entry 条目, 累积计算每个条目的 sha1 */
	for (i = 0; i < entries; i++) {
		struct cache_entry *ce = cache[i];
		int size = ce_size(ce);
		SHA1_Update(&c, ce, size);
	}
	/* 最终得到的 sha1 值写入到 hdr.sha1 中 */
	SHA1_Final(hdr.sha1, &c);

	/* 保存 hdr 数据到 fd */
	if (write(newfd, &hdr, sizeof(hdr)) != sizeof(hdr))
		return -1;

	/* 逐条保存 cache entry 条目数据到文件 newfd */
	for (i = 0; i < entries; i++) {
		struct cache_entry *ce = cache[i];
		int size = ce_size(ce);
		if (write(newfd, ce, size) != size)
			return -1;
	}
	return 0;
}		

/*
 * We fundamentally don't like some paths: we don't want
 * dot or dot-dot anywhere, and in fact, we don't even want
 * any other dot-files (.dircache or anything else). They
 * are hidden, for chist sake.
 *
 * Also, we don't want double slashes or slashes at the
 * end that can make pathnames ambiguous. 
 */
/*
 * 检查 path 字符串中是否包含'.'和'\'字符
 */
static int verify_path(char *path)
{
	char c;

	goto inside;
	for (;;) {
		if (!c)
			return 1;
		if (c == '/') {
inside:
			c = *path++;
			if (c != '/' && c != '.' && c != '\0')
				continue;
			return 0;
		}
		c = *path++;
	}
}

/*
 * "update-cache <file>"
 * 示例: $ ./update-cache Makefile
 *
 * 添加新文件到暂存区(cache)中, 现在叫 staging
 * 1. 文件内容写入到 blob 数据中
 * 2. 文件信息添加到 ".dircache/index" 中
 */
int main(int argc, char **argv)
{
	int i, newfd, entries;

	/* 读取索引文件".dircache/index"到内存, 建立缓存, 返回条目数 */
	entries = read_cache();
	if (entries < 0) {
		perror("cache corrupted");
		return -1;
	}

	/* 创建lock文件: ".dircache/index.lock" */
	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];
		/* 检查文件参数 path 字符串中是否包含'.'和'\'字符 */
		if (!verify_path(path)) {
			fprintf(stderr, "Ignoring path %s\n", argv[i]);
			continue;
		}
		/*
		 * 将 path 指定的文件数据写入 blob 中, 文件信息写入到内存的 cache entry 中
		 */
		if (add_file_to_cache(path)) {
			fprintf(stderr, "Unable to add %s to database\n", path);
			goto out;
		}
	}
	/* 将内存中更新后的 cache entry 写入到 ".dircache/index.lock" 文件, 并命名回 ".dircache/index" */
	if (!write_cache(newfd, active_cache, active_nr) && !rename(".dircache/index.lock", ".dircache/index"))
		return 0;
out:
	unlink(".dircache/index.lock");
}

/* #
 * # update-cache 使用示例
 * #
 *
 * # 1. 使用 update-cache 将 Makefile 添加到暂存区
 * git-e83c5163$ ./update-cache Makefile
 *
 * # 2. 查看新增的 object 数据 (第一次添加文件, 原来的内容为空)
 * git-e83c5163$ tree .dircache/ -a
 * .dircache/
 * ├── index
 * └── objects
 *     ├── 00
 *     ...
 *     ├── b0
 *     │   └── 4fb99b9a176ff05e03d5e6e739f0a82b83c56c
 *     ├── b1
 *     ...
 *     └── ff
 *
 * 257 directories, 2 files
 *
 * # 3. 查看索引文件的内容 (Makefile 文件信息已经添加到该文件)
 * git-e83c5163$ xxd -g 1 .dircache/index
 * 00000000: 43 52 49 44 01 00 00 00 01 00 00 00 1c ee 04 41  CRID...........A
 * 00000010: 4a 8b 00 b2 05 55 e7 6b 2c f6 d6 40 2f c4 2d 21  J....U.k,..@/.-!
 * 00000020: 10 7b ed 60 b6 28 26 27 10 7b ed 60 b6 28 26 27  .{.`.(&'.{.`.(&'
 * 00000030: 03 fc 00 00 86 83 e8 03 a4 81 00 00 8b 63 00 00  .............c..
 * 00000040: 14 00 00 00 ca 03 00 00 b0 4f b9 9b 9a 17 6f f0  .........O....o.
 * 00000050: 5e 03 d5 e6 e7 39 f0 a8 2b 83 c5 6c 08 00 4d 61  ^....9..+..l..Ma
 * 00000060: 6b 65 66 69 6c 65 00 00                          kefile..
 *
 * # 4. 文件存储的数据时以其 sha1 值命名, 这里使用 sha1sum 工具验证下
 * git-e83c5163$ sha1sum .dircache/objects/b0/4fb99b9a176ff05e03d5e6e739f0a82b83c56c
 * b04fb99b9a176ff05e03d5e6e739f0a82b83c56c  .dircache/objects/b0/4fb99b9a176ff05e03d5e6e739f0a82b83c56c
 *
 * # 5. 使用 update-cache 再新增一个文件
 * git-e83c5163$ ./update-cache README
 *
 * # 6. 查看新增的 object 数据 (第二次添加文件, 原来已经有一个文件)
 * git-e83c5163$ tree .dircache/ -a
 * .dircache/
 * ├── index
 * └── objects
 *     ├── 00
 *     ...
 *     ├── 66
 *     │   └── 5025b11ce8fb16fadb7daebf77cb54a2ae39a1
 *     ...
 *     ├── b0
 *     │   └── 4fb99b9a176ff05e03d5e6e739f0a82b83c56c
 *     ...
 *     └── ff
 *
 * 257 directories, 3 files
 *
 * # 7. 查看索引文件内容 (在 Makefile 文件后增加了 README 文件的信息)
 * git-e83c5163$ xxd -g 1 .dircache/index
 * 00000000: 43 52 49 44 01 00 00 00 02 00 00 00 4f 1d b5 af  CRID........O...
 * 00000010: 48 e0 35 5d 65 c2 23 f2 0d 02 2d 4c 83 6b 1d 16  H.5]e.#...-L.k..
 * 00000020: 10 7b ed 60 b6 28 26 27 10 7b ed 60 b6 28 26 27  .{.`.(&'.{.`.(&'
 * 00000030: 03 fc 00 00 86 83 e8 03 a4 81 00 00 8b 63 00 00  .............c..
 * 00000040: 14 00 00 00 ca 03 00 00 b0 4f b9 9b 9a 17 6f f0  .........O....o.
 * 00000050: 5e 03 d5 e6 e7 39 f0 a8 2b 83 c5 6c 08 00 4d 61  ^....9..+..l..Ma
 * 00000060: 6b 65 66 69 6c 65 00 00 31 79 ed 60 0e eb ae 26  kefile..1y.`...&
 * 00000070: 31 79 ed 60 0e eb ae 26 03 fc 00 00 87 83 e8 03  1y.`...&........
 * 00000080: a4 81 00 00 8b 63 00 00 14 00 00 00 c8 20 00 00  .....c....... ..
 * 00000090: 66 50 25 b1 1c e8 fb 16 fa db 7d ae bf 77 cb 54  fP%.......}..w.T
 * 000000a0: a2 ae 39 a1 06 00 52 45 41 44 4d 45 00 00 00 00  ..9...README....
 */

4.5 文件cat-file.c

cat-file.c用于根据 sha1 值从版本库中找到相应的二进制数据文件(blob/tree/commit),解压缩得到原始数据并保存到一个临时文件中。

#include "cache.h"

/*
 * 命令: "cat-file <sha1>"
 * 示例: $ ./cat-file b04fb99b9a176ff05e03d5e6e739f0a82b83c56c
 */
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;

	/* 将 argv[1] 的16进制 sha1 字符串转换成 sha1 值 */
	if (argc != 2 || get_sha1_hex(argv[1], sha1))
		usage("cat-file: cat-file <sha1>");
	/* 获取 sha1 值对应的文件内容(解压缩后返回) */
	/* NOTE!!! 这里的 buf 是在 read_sha1_file 内部 malloc 的, 使用后没有释放会造成内存泄露 */
	buf = read_sha1_file(sha1, type, &size);
	if (!buf)
		exit(1);
	/* 创建临时文件 */
	fd = mkstemp(template);
	if (fd < 0)
		usage("unable to create tempfile");
	/* 将解压缩后的 sha1 值对应的文件内容写入临时文件 temp_git_file_XXXXXX */
	if (write(fd, buf, size) != size)
		strcpy(type, "bad");
	printf("%s: %s\n", template, type);
}

/* #
 * # cat-file 使用示例
 * #
 *
 * # 0. 命令格式
 * # $ cat-file <sha1>
 *
 * # 1. 添加文件, 并查看已有的 sha1 文件对象
 * git-e83c5163$ ./update-cache Makefile
 * git-e83c5163$ ls .dircache/objects/b0/
 * 4fb99b9a176ff05e03d5e6e739f0a82b83c56c
 *
 * # 2. 使用 cat-file 操作提取 sha1 文件对象 (找到 sha1 文件对象, 提取内容并解压缩到临时文件中)
 * git-e83c5163$ ./cat-file b04fb99b9a176ff05e03d5e6e739f0a82b83c56c
 * temp_git_file_Xq29L3: blob
 *
 * # 3. 查看 cat-file 生成的临时文件的内容
 * git-e83c5163$ head -5 temp_git_file_Xq29L3
 * CFLAGS=-g
 * CC= *
 * PROG=update-cache show-diff init-db write-tree read-tree commit-tree cat- *
 * git-e83c5163$
 */

4.6 文件show-diff.c

show-diff.c内部提取版本库的文件内容,并使用 diff 工具同工作区的文件内容进行比较。

#include "cache.h"

#define MTIME_CHANGED	0x0001
#define CTIME_CHANGED	0x0002
#define OWNER_CHANGED	0x0004
#define MODE_CHANGED    0x0008
#define INODE_CHANGED   0x0010
#define DATA_CHANGED    0x0020

/*
 * 比较 cache entry 和 stat 结构体中存放的文件信息
 */
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)
		changed |= MTIME_CHANGED;
	if (ce->ctime.sec  != (unsigned int)st->st_ctim.tv_sec ||
	    ce->ctime.nsec != (unsigned int)st->st_ctim.tv_nsec)
		changed |= CTIME_CHANGED;
	if (ce->st_uid != (unsigned int)st->st_uid ||
	    ce->st_gid != (unsigned int)st->st_gid)
		changed |= OWNER_CHANGED;
	if (ce->st_mode != (unsigned int)st->st_mode)
		changed |= MODE_CHANGED;
	if (ce->st_dev != (unsigned int)st->st_dev ||
	    ce->st_ino != (unsigned int)st->st_ino)
		changed |= INODE_CHANGED;
	if (ce->st_size != (unsigned int)st->st_size)
		changed |= DATA_CHANGED;
	return changed;
}

/*
 * 比较 old_contents 和 cache entry 条目中对应的文件数据
 */
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;

	/* 生成 diff 命令: "diff -u - filename", 比较标准输入中的内容和 cache entry 条目对应的文件 */
	snprintf(cmd, sizeof(cmd), "diff -u - %s", ce->name);
	/* 打开管道 */
	f = popen(cmd, "w");
	/* 往命令管道中写入数据 old_contents */
	fwrite(old_contents, old_size, 1, f);
	pclose(f);
}

/*
 * 命令: "show-diff <file>"
 * 示例: $ ./show-diff Makefile
 */
int main(int argc, char **argv)
{
	/* 读取索引文件".dircache/index"到内存, 建立缓存 */
	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;

		/* 提取 cache entry 条目同名的文件信息 */
		if (stat(ce->name, &st) < 0) {
			printf("%s: %s\n", ce->name, strerror(errno));
			continue;
		}
		/* 将提取的文件信息与 cache entry 条目存储的文件信息, */
		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");
		/* 获取 cache entry 条目对应的文件内容 */
		new = read_sha1_file(ce->sha1, type, &size);
		/* 将 cache entry 条目对应的文件和暂存区已经添加的内容进行比较 */
		show_differences(ce, &st, new, size);
		free(new);
	}
	return 0;
}

/* #
 * # show-diff 使用示例
 * #
 *
 * # 1. 添加文件 Makefile, 然后修改文件
 * git-e83c5163$ ./update-cache Makefile
 * git-e83c5163$ vim Makefile
 *
 * # 2. 使用 show-diff 比较工作区的 Makefile 和暂存区的数据
 * git-e83c5163$ ./show-diff Makefile
 * Makefile:  b04fb99b9a176ff05e03d5e6e739f0a82b83c56c
 * --- -	2021-07-14 23:13:43.918975406 +0800
 * +++ Makefile	2021-07-14 23:13:04.832860681 +0800
 * @@ -1,3 +1,5 @@
 * +.PHONY: all
 * +
 *  CFLAGS=-g
 *  CC=gcc
 *
 */

4.7 文件write-tree.c

write-tree.c将暂存区已添加的文件打包成 tree 结构, 压缩后写入到版本库中。

#include "cache.h"

/*
 * 检查 sha1 值对应的文件是否存在且可以读取
 */
static int check_valid_sha1(unsigned char *sha1)
{
	/* 将 sha1 值转换成相应的文件名 */
	char *filename = sha1_file_name(sha1);
	int ret;

	/* If we were anal, we'd check that the sha1 of the contents actually matches */
	/* 检查对文件 filename 是否有读权限 (R_OK) */
	ret = access(filename, R_OK);
	if (ret)
		perror(filename);
	return ret;
}

/* 将数值 val 填充到 buffer[i] 往前的位置, 并返回开始位置值 */
static int prepend_integer(char *buffer, unsigned val, int i)
{
	/* value = 54321, buffer[] = "____54321\0______"
	 *                                      |
	 *                                      i
	 */
	buffer[--i] = '\0';
	do {
		buffer[--i] = '0' + (val % 10);
		val /= 10;
	} while (val);
	return i;
}

#define ORIG_OFFSET (40)	/* Enough space to add the header of "tree <size>\0" */

/*
 * 命令: "write-tree"
 * 示例: $ ./write-tree
 */
int main(int argc, char **argv)
{
	unsigned long size, offset, val;
	/* 读取索引文件".dircache/index"到内存, 建立缓存, 返回条目数 */
	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];
		/* 根据 cache entry 的 sha1 值检查对应的文件是否存在且可读取 */
		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);
		}
		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 填充到 buffer[ORIG_OFFSET] 往前的位置, 并返回开始位置值 */
	i = prepend_integer(buffer, offset - ORIG_OFFSET, ORIG_OFFSET);
	i -= 5;
	/* 填充 "tree " 字符串, 从 "tree " 开始到结束实际上是一个 tree 对象 */
	memcpy(buffer+i, "tree ", 5);

	buffer += i;
	offset -= i;

	/* 将 tree 对象数据写入到文件中 */
	write_sha1_file(buffer, offset);
	return 0;
}

/* #
 * # write-tree 使用示例
 * #
 *
 * # 1. 查看 .dircache/objects 下的对象 (有两个对象, 分别对应 Makefile 和 README 数据)
 * git-e83c5163$ ./update-cache Makefile
 * git-e83c5163$ ./update-cache README
 * git-e83c5163$ tree .dircache/ -a
 * .dircache/
 * ├── index
 * └── objects
 *     ├── 00
 *     ...
 *     ├── 66
 *     │   └── 5025b11ce8fb16fadb7daebf77cb54a2ae39a1
 *     ...
 *     ├── b0
 *     │   └── 4fb99b9a176ff05e03d5e6e739f0a82b83c56c
 *     ...
 *     └── ff
 *
 * 257 directories, 3 files
 *
 * # 2. 查看 .dircache/index 暂存的内容(包含 Makefile 和 README 两个文件)
 * git-e83c5163$ xxd -g 1 .dircache/index
 * 00000000: 43 52 49 44 01 00 00 00 02 00 00 00 4f 1d b5 af  CRID........O...
 * 00000010: 48 e0 35 5d 65 c2 23 f2 0d 02 2d 4c 83 6b 1d 16  H.5]e.#...-L.k..
 * 00000020: 10 7b ed 60 b6 28 26 27 10 7b ed 60 b6 28 26 27  .{.`.(&'.{.`.(&'
 * 00000030: 03 fc 00 00 86 83 e8 03 a4 81 00 00 8b 63 00 00  .............c..
 * 00000040: 14 00 00 00 ca 03 00 00 b0 4f b9 9b 9a 17 6f f0  .........O....o.
 * 00000050: 5e 03 d5 e6 e7 39 f0 a8 2b 83 c5 6c 08 00 4d 61  ^....9..+..l..Ma
 * 00000060: 6b 65 66 69 6c 65 00 00 31 79 ed 60 0e eb ae 26  kefile..1y.`...&
 * 00000070: 31 79 ed 60 0e eb ae 26 03 fc 00 00 87 83 e8 03  1y.`...&........
 * 00000080: a4 81 00 00 8b 63 00 00 14 00 00 00 c8 20 00 00  .....c....... ..
 * 00000090: 66 50 25 b1 1c e8 fb 16 fa db 7d ae bf 77 cb 54  fP%.......}..w.T
 * 000000a0: a2 ae 39 a1 06 00 52 45 41 44 4d 45 00 00 00 00  ..9...README....
 *
 * # 3. 将 .dircache/index 暂存区内容打包成 tree 对象写入, 生成 tree 数据
 * git-e83c5163$ ./write-tree
 * cb8b8e042b2abdf1070f9f60d83f3fb9cbe204ce
 *
 * # 4. 再查看 .dircache/objects 下的对象, 新增了一个刚写入的数据
 * git-e83c5163$ tree .dircache -a
 * .dircache
 * ├── index
 * └── objects
 *     ├── 00
 *     ...
 *     ├── 66
 *     │   └── 5025b11ce8fb16fadb7daebf77cb54a2ae39a1
 *     ...
 *     ├── b0
 *     │   └── 4fb99b9a176ff05e03d5e6e739f0a82b83c56c
 *     ...
 *     ├── cb
 *     │   └── 8b8e042b2abdf1070f9f60d83f3fb9cbe204ce
 *     ...
 *     └── ff
 *
 * 257 directories, 4 files
 *
 * # 5. 使用 cat-file 提取刚才写入的 objects 数据到临时文件
 * git-e83c5163$ ./cat-file cb8b8e042b2abdf1070f9f60d83f3fb9cbe204ce
 * temp_git_file_8zPbaQ: tree
 *
 * # 6. 检查临时文件的内容 (tree 对象)
 * git-e83c5163$ xxd -g 1 temp_git_file_8zPbaQ
 * 00000000: 31 30 30 36 34 34 20 4d 61 6b 65 66 69 6c 65 00  100644 Makefile.
 * 00000010: b0 4f b9 9b 9a 17 6f f0 5e 03 d5 e6 e7 39 f0 a8  .O....o.^....9..
 * 00000020: 2b 83 c5 6c 31 30 30 36 34 34 20 52 45 41 44 4d  +..l100644 READM
 * 00000030: 45 00 66 50 25 b1 1c e8 fb 16 fa db 7d ae bf 77  E.fP%.......}..w
 * 00000040: cb 54 a2 ae 39 a1                                .T..9.
 */

4.8 文件read-tree.c

read-tree.c根据 tree 对象的 sha1 值,在版本库中找到相应的文件, 解压缩数据,并提取其每一条文件对象的 mode, path 和 sha1 值。

#include "cache.h"

/*
 * 提取 sha1 值对应 tree 对象, 并显示每一个文件的 mode path sha1 数据
 */
static int unpack(unsigned char *sha1)
{
	void *buffer;
	unsigned long size;
	char type[20];

	/* 获取 sha1 值对应的文件内容(解压缩) */
	buffer = read_sha1_file(sha1, type, &size);
	if (!buffer)
		usage("unable to read sha1 file");
	/* 检查 sha1 文件数据的类型是否为 tree */
	if (strcmp(type, "tree"))
		usage("expected a 'tree' node");
	while (size) {
		int len = strlen(buffer)+1;
		unsigned char *sha1 = buffer + len;
		/* 查找空格' '字符 */
		char *path = strchr(buffer, ' ')+1;
		unsigned int mode;
		/* 使用 scanf + %o 提取文件的 mode 数据 */
		if (size < len + 20 || sscanf(buffer, "%o", &mode) != 1)
			usage("corrupt 'tree' file");
		buffer = sha1 + 20;
		size -= len + 20;
		/* 打印展示 tree 对象中每一条数据的 mode, path, sha1 */
		printf("%o %s (%s)\n", mode, path, sha1_to_hex(sha1));
	}
	return 0;
}

/*
 * 命令: "read-tree <key>"
 * 示例: $ ./read-tree cb8b8e042b2abdf1070f9f60d83f3fb9cbe204ce
 */
int main(int argc, char **argv)
{
	int fd;
	unsigned char sha1[20];

	if (argc != 2)
		usage("read-tree <key>");
	/* 将 argv[1] 的 sha1 字符串转换成 sha1 值 */
	if (get_sha1_hex(argv[1], sha1) < 0)
		usage("read-tree <key>");
	/* sha1_file_directory = getenv("SHA1_FILE_DIRECTORY") */
	sha1_file_directory = getenv(DB_ENVIRONMENT);
	if (!sha1_file_directory)
		sha1_file_directory = DEFAULT_DB_ENVIRONMENT;
	/* 解包并打印 sha1 值对应文件的 mode path sha1 数据 */
	if (unpack(sha1) < 0)
		usage("unpack failed");
	return 0;
}

/* #
 * # read-tree 使用示例
 * #
 *
 * # 1. 在暂存区添加文件后写入一个 tree 对象(包含 Makefile 和 README)
 * git-e83c5163$ ./update-cache Makefile
 * git-e83c5163$ ./update-cache README
 * git-e83c5163$ ./write-tree
 * cb8b8e042b2abdf1070f9f60d83f3fb9cbe204ce
 *
 * # 2. 使用 read-tree 读取 tree 对象
 * git-e83c5163$ ./read-tree cb8b8e042b2abdf1070f9f60d83f3fb9cbe204ce
 * 100644 Makefile (b04fb99b9a176ff05e03d5e6e739f0a82b83c56c)
 * 100644 README (665025b11ce8fb16fadb7daebf77cb54a2ae39a1)
 *
 * # 3. 使用 cat-file 提取 tree 对象到临时文件
 * git-e83c5163$ ./cat-file cb8b8e042b2abdf1070f9f60d83f3fb9cbe204ce
 * temp_git_file_13xlrH: tree
 *
 * # 4. 查看 tree 对象内容
 * git-e83c5163$ xxd -g 1 temp_git_file_13xlrH
 * 00000000: 31 30 30 36 34 34 20 4d 61 6b 65 66 69 6c 65 00  100644 Makefile.
 * 00000010: b0 4f b9 9b 9a 17 6f f0 5e 03 d5 e6 e7 39 f0 a8  .O....o.^....9..
 * 00000020: 2b 83 c5 6c 31 30 30 36 34 34 20 52 45 41 44 4d  +..l100644 READM
 * 00000030: 45 00 66 50 25 b1 1c e8 fb 16 fa db 7d ae bf 77  E.fP%.......}..w
 * 00000040: cb 54 a2 ae 39 a1                                .T..9.
 *
 * # 5. 使用 read-tree 尝试读取一个 blob 对象 (Makefile 的 blob 对象)
 * git-e83c5163$ ./read-tree b04fb99b9a176ff05e03d5e6e739f0a82b83c56c
 * read-tree: expected a 'tree' node
 */

4.9 文件commit-tree.c

commit-tree.cwrite-tree.c写入的 tree 对象和提交注释一起打包成 commit 对象,写入到版本库中。

#include "cache.h"

#include <pwd.h>
#include <time.h>

/* 1 BLOCKING = 16384 Bytes (16KB) */
#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"
 */
/* 分配 16KB 的 buffer, 并将 sizep 设置为 40 bytes */
static void init_buffer(char **bufp, unsigned int *sizep)
{
	/* 分配 16KB 的 buffer, 并将前 40 bytes 清 0 */
	char *buf = malloc(BLOCKING);
	memset(buf, 0, ORIG_OFFSET);
	/* sizep 大小为 40 bytes */
	*sizep = ORIG_OFFSET;
	*bufp = buf;
}

/* 将 fmt 后变长参数的内容按 fmt 格式转换成字符串, 添加到 bufp 缓冲区 sizep 开始的位置 */
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;

	/* 将 fmt 后面的变长参数根据 fmt 指定的格式输出到 one_line 中  */
	va_start(args, fmt);
	len = vsnprintf(one_line, sizeof(one_line), fmt, args);
	va_end(args);
	size = *sizep;
	/* 重新根据实际情况计算长度 */
	newsize = size + len;
	/* 将长度对齐到 32KB 边界, 即按 32KB 粒度分配 */
	alloc = (size + 32767) & ~32767;
	buf = *bufp;
	if (newsize > alloc) {
		alloc = (newsize + 32767) & ~32767;
		/* 在原来的位置重新调整分配内存 */
		buf = realloc(buf, alloc);
		*bufp = buf;
	}
	*sizep = newsize;
	/* 将格式化得到的字符串存放到缓冲区 size (40) 指定的开始地方 */
	memcpy(buf + size, one_line, len);
}

/* 将数值 val 填充到 buffer[i] 往前的位置, 并返回开始位置值 */
static int prepend_integer(char *buffer, unsigned val, int i)
{
	/* value = 54321, buffer[] = "____54321\0______"
	 *                                      |
	 *                                      i
	 */
	buffer[--i] = '\0';
	do {
		buffer[--i] = '0' + (val % 10);
		val /= 10;
	} while (val);
	return i;
}

/* 往 bufp 中写入 tag 和附加数据长度 */
static void finish_buffer(char *tag, char **bufp, unsigned int *sizep)
{
	int taglen;
	int offset;
	char *buf = *bufp;
	unsigned int size = *sizep;

	/* 将整个缓冲区填充的长度 size - ORIG_OFFSET, 存放到 ORIG_OFFSET (40) 位置, 返回填充所在位置 offset */
	offset = prepend_integer(buf, size - ORIG_OFFSET, ORIG_OFFSET);
	/* 将 tag 存放到长度字段前面 */
	taglen = strlen(tag);
	offset -= taglen;
	buf += offset;
	size -= offset;
	memcpy(buf, tag, taglen);

	/* 填充后的格式:
	 *                    tag  length  ORIG_OFFSET(40)
	 *                    |       |    |
	 * buffer[] = "_______commiter54321\0________"
	 *             |      |
	 * 返回:       bufp   sizep
	 */

	*bufp = buf;
	*sizep = size;
}

/* 移除字符串中的 '\n', '<'和'>'等特殊字符 */
static void remove_special(char *p)
{
	char c;
	char *dst = p;

	for (;;) {
		c = *p;
		p++;
		switch(c) {
		case '\n': case '<': case '>':
			continue;
		}
		*dst++ = c;
		if (!c)
			break;
	}
}

/*
 * Having more than two parents may be strange, but hey, there's
 * no conceptual reason why the file format couldn't accept multi-way
 * merges. It might be the "union" of several packages, for example.
 *
 * I don't really expect that to happen, but this is here to make
 * it clear that _conceptually_ it's ok..
 */
#define MAXPARENT (16)

/*
 * "commit-tree <sha1> [-p <sha1>]* < changelog"
 * 第一次提交: $ echo "First Commit!" | ./commit-tree cb8b8e042b2abdf1070f9f60d83f3fb9cbe204ce
 *   后续提交: $ echo "Second Commit!" | ./commit-tree 0cecb5da03708fd258e4d6b2019a78578e1c9152 -p 3824d701efdf721efebf5ef04d44817469d7cef4
 */
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;

	/* 将 argv[1]中的 sha1 字符串转换 sha1 值 */
	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];
		/* 提取 -p 选项后的 sha1 字符串, 并将其转换16进制 sha1 值, 存放到 parent_sha1[parents] 中 */
		if (!b || strcmp(a, "-p") || get_sha1_hex(b, parent_sha1[parents]))
			usage("commit-tree <sha1> [-p <sha1>]* < changelog");
		parents++;
	}

	/* 没有指定 -p 参数, 第一次提交 */
	if (!parents)
		fprintf(stderr, "Committing initial tree %s\n", argv[1]);
	/* getpwuid: 返回 passwd 文件中 uid 对应条目的数据, 一个指向 struct passwd 的指针 */
	/* 根据当前用户的 uid 获取用户数据, 构造提交信息: 1. 用户名; 2. 邮件地址; 3. 时间 */
	pw = getpwuid(getuid());
	if (!pw)
		usage("You don't exist. Go away!");
	/* pw_gecos: 用户信息(user information) */
	realgecos = pw->pw_gecos;
	/* pw_name: 用户名(username) */
	len = strlen(pw->pw_name);
	/* 根据用户名和主机名构造用户的邮件信息 user@host */
	memcpy(realemail, pw->pw_name, len);
	realemail[len] = '@';
	gethostname(realemail+len+1, sizeof(realemail)-len-1);
	/* 提取当前时间戳 */
	time(&now);
	realdate = ctime(&now);

	/* 从环境变量提取信息: 1. 用户名; 2. 邮件地址; 3. 时间 */
	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);

	/* 分配 16KB 的 buffer, 并将 size 设置为 40 bytes */
	init_buffer(&buffer, &size);
	/* 将 tree_sha1 值转换成字符串, 以 "tree d486ca60...e83a\n" 的格式, 存放到 buffer 缓冲区 size 开始的位置, 保存后自动调整 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.
	 */
	/* 逐个将 parent_sha1[i] 值转换成字符串, 以 "parent d486ca60...e83a\n" 的格式, 存放到 buffer 缓冲区 size 开始的位置, 保存后自动调整 size */
	for (i = 0; i < parents; i++)
		add_buffer(&buffer, &size, "parent %s\n", sha1_to_hex(parent_sha1[i]));

	/* Person/date information */
	/* 将 author 信息转换成字符串, 以 "author name <email> date\n" 的格式, 存放到 buffer 缓冲区 size 开始的位置, 保存后自动调整 size */
	add_buffer(&buffer, &size, "author %s <%s> %s\n", gecos, email, date);
	/* 将 committer 信息转换成字符串, 以 "committer name <email> date\n\n" 的格式, 存放到 buffer 缓冲区 size 开始的位置, 保存后自动调整 size */
	add_buffer(&buffer, &size, "committer %s <%s> %s\n\n", realgecos, realemail, realdate);

	/* And add the comment */
	/* 将从标准输入获取的 comment 信息存放到 buffer 缓冲区 size 开始的位置, 保存后自动调整 size */
	while (fgets(comment, sizeof(comment), stdin) != NULL)
		add_buffer(&buffer, &size, "%s", comment);

	/* 将 "commit " 和附加数据长度写入到 buffer 中, 从 "commit " 开始到结束实际上是一个 commit 对象 */
	finish_buffer("commit ", &buffer, &size);

	/* 将 commit 对象数据写入到文件中 */
	write_sha1_file(buffer, size);
	return 0;
}

/* #
 * # commit-tree 使用示例
 * #
 *
 * # 1. 在暂存区添加文件后写入一个 tree 对象(包含 Makefile 和 README, 以及一个 tree 对象)
 * git-e83c5163$ ./update-cache Makefile
 * git-e83c5163$ ./update-cache README
 * git-e83c5163$ ./write-tree
 * cb8b8e042b2abdf1070f9f60d83f3fb9cbe204ce
 * git-e83c5163$ tree .dircache -a
 * .dircache
 * ├── index
 * └── objects
 *     ├── 00
 *     ...
 *     ├── 66
 *     │   └── 5025b11ce8fb16fadb7daebf77cb54a2ae39a1
 *     ...
 *     ├── b0
 *     │   └── 4fb99b9a176ff05e03d5e6e739f0a82b83c56c
 *     ...
 *     ├── cb
 *     │   └── 8b8e042b2abdf1070f9f60d83f3fb9cbe204ce
 *     ...
 *     └── ff
 *
 * # 2. 使用 commit-tree 提交写入的对象
 * git-e83c5163$ echo "First Commit!" | ./commit-tree cb8b8e042b2abdf1070f9f60d83f3fb9cbe204ce
 * Committing initial tree cb8b8e042b2abdf1070f9f60d83f3fb9cbe204ce
 * 3824d701efdf721efebf5ef04d44817469d7cef4
 *
 * # 3. 使用 cat-file 解析 commit-tree 写入的对象到临时文件中
 * git-e83c5163$ ./cat-file 3824d701efdf721efebf5ef04d44817469d7cef4
 * temp_git_file_QQADM5: commit
 *
 * # 4. 显示 commit-tree 对象的内容
 * git-e83c5163$ xxd -g 1 temp_git_file_QQADM5
 * 00000000: 74 72 65 65 20 63 62 38 62 38 65 30 34 32 62 32  tree cb8b8e042b2
 * 00000010: 61 62 64 66 31 30 37 30 66 39 66 36 30 64 38 33  abdf1070f9f60d83
 * 00000020: 66 33 66 62 39 63 62 65 32 30 34 63 65 0a 61 75  f3fb9cbe204ce.au
 * 00000030: 74 68 6f 72 20 52 6f 63 6b 79 20 47 75 2c 52 6f  thor Rocky Gu,Ro
 * 00000040: 63 6b 79 20 47 75 2c 75 2c 30 30 39 33 35 37 33  cky Gu,u,0093573
 * 00000050: 39 20 3c 72 67 39 33 35 37 33 39 40 73 74 62 73  9 <rg935739@stbs
 * 00000060: 7a 78 2d 62 6c 64 2d 35 3e 20 54 68 75 20 4a 75  zx-bld-5> Thu Ju
 * 00000070: 6c 20 31 35 20 31 34 3a 33 33 3a 30 37 20 32 30  l 15 14:33:07 20
 * 00000080: 32 31 0a 63 6f 6d 6d 69 74 74 65 72 20 52 6f 63  21.committer Roc
 * 00000090: 6b 79 20 47 75 2c 52 6f 63 6b 79 20 47 75 2c 75  ky Gu,Rocky Gu,u
 * 000000a0: 2c 30 30 39 33 35 37 33 39 20 3c 72 67 39 33 35  ,00935739 <rg935
 * 000000b0: 37 33 39 40 73 74 62 73 7a 78 2d 62 6c 64 2d 35  739@stbszx-bld-5
 * 000000c0: 3e 20 54 68 75 20 4a 75 6c 20 31 35 20 31 34 3a  > Thu Jul 15 14:
 * 000000d0: 33 33 3a 30 37 20 32 30 32 31 0a 0a 46 69 72 73  33:07 2021..Firs
 * 000000e0: 74 20 43 6f 6d 6d 69 74 21 0a                    t Commit!.
 *
 * # 5. 直接显示 commit-tree 对象的内容 (和我们用 'git log commit_id' 看到的一样)
 * git-e83c5163$ cat temp_git_file_QQADM5
 * tree cb8b8e042b2abdf1070f9f60d83f3fb9cbe204ce
 * author Rocky Gu,Rocky Gu,u,00935739 <rg935739@stbszx-bld-5> Thu Jul 15 14:33:07 2021
 * committer Rocky Gu,Rocky Gu,u,00935739 <rg935739@stbszx-bld-5> Thu Jul 15 14:33:07 2021
 *
 * First Commit!
 *
 * # 6. 编辑 Makefile, 使用 update-cache添加到暂存区, 使用 write-tree 写入 tree 对象
 * git-e83c5163$ vim Makefile
 * git-e83c5163$ ./update-cache Makefile
 * git-e83c5163$ ./write-tree
 * 0cecb5da03708fd258e4d6b2019a78578e1c9152
 *
 * # 7. 使用 commit-tree 执行第二次提交, 并使用 cat-file 检查提交内容
 * git-e83c5163$ echo "Second Commit!" | ./commit-tree 0cecb5da03708fd258e4d6b2019a78578e1c9152 -p 3824d701efdf721efebf5ef04d44817469d7cef4
 * 6d55fcb1945d84b22cd06c64bfab804ad4ded798
 * git-e83c5163$ ./cat-file 6d55fcb1945d84b22cd06c64bfab804ad4ded798
 * temp_git_file_SU60Jd: commit
 * git-e83c5163$ cat temp_git_file_SU60Jd
 * tree 0cecb5da03708fd258e4d6b2019a78578e1c9152
 * parent 3824d701efdf721efebf5ef04d44817469d7cef4
 * author guyongqiang,,, <ygu@guyongqiangx> Sat Jul 17 18:18:58 2021
 * committer guyongqiang,,, <ygu@guyongqiangx> Sat Jul 17 18:18:58 2021
 *
 * Second Commit!
 */

5. 推荐文章

本文在操作时参考了以下两篇文章:

尤其是后者,里面关于 index/blob/tree/commit 对象的结构示意图画得很好,我本来开始还想自己画的,但看到这么好的图之后放弃了,因为我没法画得更好了。

在此对这两篇文章的作者表示万分感谢!

6. 其它

洛奇工作中常常会遇到自己不熟悉的问题,这些问题可能并不难,但因为不了解,找不到人帮忙而瞎折腾,往往导致浪费几天甚至更久的时间。

所以我组建了几个微信讨论群(记得微信我说加哪个群,如何加微信见后面),欢迎一起讨论:

  • 一个密码编码学讨论组,主要讨论各种加解密,签名校验等算法,请说明加密码学讨论群。
  • 一个Android OTA的讨论组,请说明加Android OTA群。
  • 一个git和repo的讨论组,请说明加git和repo群。

洛奇自己维护了一个公众号“洛奇看世界”,一个很佛系的公众号,不定期瞎逼逼。公号也提供个人联系方式,一些资源,说不定会有意外的收获,详细内容见公号提示。扫下方二维码关注公众号:

公众号

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

洛奇看世界

一分也是爱~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值