GIT系列二:手动提交一个commit


想要深入的了解一个工具,就必须完一些比较hack的用法。
为了更好的了解git内部的工作机制,本文就试图通过手动的编辑.git目录下的
文件,来完成一次commit的提交。


.git目录下面的文件

# git --version  // 当前git版本
    git version 2.3.2 (Apple Git-55)
# ls -1 .git
objects     <= 
branches    description info    refs

HEAD            <= 存放当前branch的HEAD指针
branches        <= 新版git没有使用该目录
config          <= 本地git仓库的配置文件
description     <= 仅用于gitweb程序
hooks           <= 一定在特定时间发生后被调用的脚本,可理解为钩子函数
index           <= 文件暂存区信息
info/exclude    <= 功能类似.gitignore的全局性排除文件
objects         <= 存放真实的数据文件的地方,文件名是SHA1哈希值
refs/heads      <= 存放各个分支的HEAD指针
refs/tags       <= 存放各个tag的commit指针
refs/remotes    <= 存放remote分支的HEAD指针

通过分析可以看到,对于一个普通的commit而言,比较相关的应该是objects和HEAD指针


正常的commit过程

# echo "hello world" > hello
# git add hello
# git commit -am "add file"

在接下来的内容中,将介绍如何在直接编辑.git目录文件的情况下达到与上面命令一样的效果。


手动提交一个commit的步骤

git存储内容时,会有一个头部信息一并被保存。
比如如果是要存储”hello world\n”字符串,可以通过以下ruby脚本得到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/ruby
require 'digest/sha1'
require 'zlib'
require 'fileutils'
content = "hello world\n"
header = "blob #{content.length}\0"
store = header + content
sha1 = Digest::SHA1.hexdigest(store)
puts "sha1:" + sha1
zlib_content = Zlib::Deflate.deflate(store)
puts "zlib_contet:" + zlib_content
path = '.git/objects/' + sha1[0,2] + '/' + sha1[2,38]
FileUtils.mkdir_p(File.dirname(path))
File.open(path, 'w') { |f| f.write zlib_content}

由于hello文件的内容是”hello world\n”,因此可以通过以上脚本首先生成
hello文件内容对应的object文件,文件类型是blob。可以通过一下命令判断生成的文件内容是否正确

# git cat-file -p 3b18e512dba79e4c8300dd08aeb37f8e728b8dad  
# git cat-file -t 3b18e512dba79e4c8300dd08aeb37f8e728b8dad

以上的object也可以通过一下一条命令得到

# echo "hello world" | git hash-object -w --stdin

随后更新生成.git/index文件

# git update-index --add --cacheinfo 100644 3b18e512dba79e4c8300dd08aeb37f8e728b8dad hello

以上步骤仅是生成了”hello world” 对应的blob文件,但并没有制定这个内容对应的
文件名叫什么,也就是少了tree类型的object。接着执行一下命令

# git write-tree        // 生成指向3b18e5的tree文件,可以用下面两条命令验证
# git cat-file -p 7604755fe13e27f5327d6d13dc6663d44847562d
# git cat-file -t 7604755fe13e27f5327d6d13dc6663d44847562d

一个真正的commit还需要创建一个commit类型的object指向一个特定的tree节点

# git commit-tree 7604755fe -m "add file"
# git cat-file -p df36f6b4884ecf2ec519ddec85f959a83b4adec8
# git cat-file -t df36f6b4884ecf2ec519ddec85f959a83b4adec8

接着更新master的HEAD指针

# git update-ref refs/heads/master df36f6b4884ecf2ec519ddec85f959a83b4adec8
# git log  <= 至此就能看到一个完整的commit log
# git checkout hello  <= 将hello文件从.git库checkout出来,就算彻底的完成了一个commit

总的来说

// 第一部分是完成git add的操作  
# echo "hello world" | git hash-object -w --stdin
    3b18e512dba79e4c8300dd08aeb37f8e728b8dad    // 生成blob文件
# git update-index --add --cacheinfo 100644 3b18e512dba79e4c8300dd08aeb37f8e728b8dad hello

// 第二部分是完成git  commit的操作
# git write-tree
    7604755fe13e27f5327d6d13dc6663d44847562d    // 生成tree文件
# git commit-tree 7604755fe -m "add file"
    df36f6b4884ecf2ec519ddec85f959a83b4adec8    // 生成commit

可以看出一个commit对应会有三种object文件生成,每种object文件的命名都是以sha1哈希值为依据的。
除了commit文件由于带有日期信息所以hash值会变化之外,其他两个文件的hash值都是固定不变的。

参考资料

Git internal
Git 内部原理 - Git 对象
Git commit without commit