Git学习笔记—原理篇

最近在学习git, 先是了解了git的基本操作Git学习笔记---操作篇,然后阅读Git,由于内容太多,暂时只挑选常见的或我感兴趣的阅读,记录在此,不正确的欢迎批评指正,后续可能会补充其他内容~

起步

git是什么?

我们都知道git是版本控制工具,和其他版本控制工具地主要差异在于对于数据的方式。接下来会详细介绍git的几点特征来帮助你详细地了解这些方面~

记录快照而非差异比较

从概念上来说,大部分系统 (CVS、Subversion、Perforce 等等) 都是以文件变更列表的方式存储数据,数据被看作是基本文件以及随着在不同时间的一些差异diff (基于差异的版本控制)

但是git却把数据看作是对小型文件系统的一系列快照,以快照流的形式对待数据

操作本地执行

git中的大部分操作都是只需要访问本地文件和资源,这点让你对于外部资源的依赖程度大大降低

Git保证完整性

Git在数据存储前都会计算校验和,然后用校验和来引用,这个机制叫做SHA-1散列(hash, 哈希),由40个十六进制字符组成的字符串,这个是基于文件内容或目录结构计算出来的

Git一般只添加数据

Git操作一般只往git数据库中添加数据,它会妥善帮我们管理添加的数据。所以一旦提交快照到git仓库就很难丢失了 (插入一下,在实习的过程中遇到过好几次这个情况,就是自己把同事删除的文件拉下来,然后提上去之后发现删除的文件又被恢复了,现在看来是这个问题,当时觉得巨巨巨尴尬...)

三种状态

git把项目中的文件分为三个状态,分别是已提交(commited),已修改(modified)和已暂存(staged)

git配置

Git自带一个git config工具来帮助设置控制Git外观和行为的配置变量,它存储在三个不同的位置:

  • /etc/gitconfig: 系统配置文件,git config --system
  • ~ /.gitconfig或~/.config/git/config:对于系统上所有仓库都生效

git config --global

  • .git/config:当前项目中的git配置文件 git config --local

可以通过 git config --list --show-origin 查看所有的配置以及所在的文件

用户信息

shell 复制代码
git config --global user.name "John Doe"
git config --global user.email [email protected]

--global适用于所有的项目配置
无--global适用于在特定项目使用不同的用户信息

检查配置信息

git config --list 可以用来列出所有Git当时能找到的配置信息

git config <key> 检查Git某一项配置

获取帮助

git help <verb> | git <verb> --help | man git-<verb>

Eg: git help config可以直接打开离线的网站查看git config指令的具体说明

git <verb> -h

Eg: git add -h 可以看指令的快速说明

Git基础

在这章主要介绍git的基础操作,常见的在这里就不多赘述, 也基本上都整理在Git学习笔记---操作篇了,这里主要梳理本人不知道滴或者新的概念

复制git仓库并更新

git clone <url> <alias>

每个人克隆的都是一个完整的副本,包括完整的提交历史,对比差异,切换分支等等

git clone --depth=1 <url>

浅拷贝,只会拉取最新的一次提交(无历史)

可以使用 git status -s 指令紧凑输出:

对于 .gitignore文件,我们有如下规则:

  1. 忽略所有的 .a 文件 *.a
  2. 但跟踪所有的 lib.a,即便你在前面忽略了 .a 文件 !lib.a
  3. 只忽略当前目录下的 TODO 文件,而不忽略 subdir/TODO /TODO
  4. 忽略任何目录下名为 build 的文件夹 build/
  5. 忽略 doc/notes.txt,但不忽略 doc/server/arch.txt doc/*.txt
  6. 忽略 doc/ 目录及其所有子目录下的 .pdf 文件 doc/**/*.pdf

git diff 使用的说明:

  1. git diff:尚未暂存的文件更新了哪些部分
  2. git diff --staged/--cached:比对已暂存的文件和最后一次提交的文件之间的差异

注意:

  1. git diff是检查尚未暂存的文件,如果一下子暂存了所有的文件(git add .),那么用 git diff 将看不到任何输出
  2. diff只能查看commit之前的文件变化

指令简写 :之前已经追踪过(tracked)的文件可以使用,之前没有追踪过的文件不能使用:git commit -a -m "msg"

重命名git mv xxx yyy:git mv README.md README

Tips: 使用git status查看文件的状态可以帮助我们更好地管理文件

查看提交历史

  • git log:查看提交历史
  • git log -p -2
    • -p 显示每次提交引入的差异
    • -2 只显示最近的两次提交(限制输出)
  • git log --stat:每次提交的简略统计信息
  • git log --pretty=oneline 美化输出
    • git log --pretty=format: "%h - %an, %ar : %s"
    • 这是格式化常用的选项
  • git log --pretty=format:"%h %s --graph"
    • 其他见附录1

撤销操作

shell 复制代码
git commit -m "xxx"
git add .
git commit --amend**

第二次的提交将替代第一次提交的结果,适用于"漏了一个提交"的情况; 使用 git status 指令会输出如何对于不同状态 的文件撤回

Note: 在 Git 中已提交的几乎都是可以恢复的,然而未提交的丢失了很可能再也找不到了

远程仓库

git remote

  • git remote -v
  • git remote add 手动添加某个远程仓库
  • git remote show 显示某个远程仓库详细信息
  • git remote rename pb paul 重命名
  • git remote rm paul 删除

打标签

  • git tag 列出所有标签
  • git tag -l "v1.8.5*" 筛选标签
  • git tag -a v1.0 -m "my version 1.0" 附注标签
  • git tag v1.1 轻量标签
  • git show v1.1
  • git tag -a v1.2 9fceb02 对过往某次commitId打标签

上面 这些都是本地操作,如何推送到远程呢?和分支一样

  • git push origin [tagname]
  • git push origin tags

如何删除呢?

shell 复制代码
* git tag -d v1.0 删除本地标签,但是远程还在
* git push <remote> :refs/tags/<tagname> 同步删除远程仓库标签
* git push <remote> --delete <tagname> 和上面两种方式二选一

Git分支

分支简介

git不记录"修改",而是记录每一次提交项目目录结构的完整快照,这些快照由对象构成,这套机制基于3类核心对象:

  • blob => 文件内容
  • tree => 目录结构
  • commit => 提交信息

有如下的文件结构,当我们执行git add . ,会依次发生什么呢?

创建 Blob 对象(保存文件内容)

  1. Git会依次读取每个文件的当前内容, 比如 hello.txt 是 "Hello Git"。
  2. 对于每个文件的内容会拼上 blob {字节数}\0 的前缀(Git blob 对象的格式),然后计算一个 SHA-1 哈希值,这是该文件快照的唯一标识
shell 复制代码
blob 10\0Hello Git (blob原始数据)
f572d396fae9206628714fb2ce00f72e94f2258f (SHA-1哈
  1. Git 把内容用 gzip(blob原始数据)压缩后,保存在 .git/objects 文件夹下;文件名就是哈希值前两位作为目录,其余部分作为文件名:

.git/objects/f5/72d396fae92066...

这就是 Git 的 blob 对象。

→ 对 readme.md 同理,生成第二个 blob。

创建 Tree 对象(保存目录结构)

现在 Git 要记录:哪些文件,分别对应哪个 blob 哈希,用来构建快照目录。

Tree 对象类似如下格式(二进制形式):

shell 复制代码
100644 hello.txt\0<blob_sha1_binary>
100644 readme.md\0<blob_sha1_binary>

100644 表示文件权限(普通文件) hello.txt 是文件名 \0 后跟 blob 哈希的原始二进制 20 字节 → 这整个结构再次压缩,计算 SHA-1,比如得到:

erlang 复制代码
tree 哈希:abcdef123456...

→ 写入 .git/objects/ab/cdef123456...

这就是 Git 的 tree 对象:记录目录下有哪些文件,各自指向哪个blob。

创建 Commit 对象(记录提交信息)

Git 构造提交对象,格式如下(纯文本):

perl 复制代码
tree abcdef123456...        # 指向上面创建的 tree 对象
author Alice <[email protected]> 1716800000 +0800
committer Alice <[email protected]> 1716800000 +0800

首次提交

然后 Git:

  • 对上面这段文本加上头部 commit <length>\0

  • 整体计算 SHA-1 哈希,比如:

    哈希:4a202b346bb0fb0db7eff3cffeb3c70babbd2045

  • 写入 .git/objects/4a/202b346bb0f...,也就是 commit 对象

更新分支指针(HEAD)

Git 会写入 .git/refs/heads/master(或 main)这个文件,内容是刚才这个提交哈希:

复制代码
4a202b346bb0fb0db7eff3cffeb3c70babbd2045

同时 .git/HEAD 中写着:

bash 复制代码
ref: refs/heads/master

也就是说,当前 HEAD 指向的分支指向这个 commit。

sql 复制代码
[commit]
   |
   v
 [tree]
  ├── hello.txt → [blob] → "Hello Git"
  └── readme.md → [blob] → "This is Git"
  
* 每次提交都是独立完整快照;
* 相同文件内容复用 blob,节省空间;
* 高效比较两个版本:只需对比 tree 或 blob 哈希是否变化。

首次提交Git仓库Belike:

修改之后再提交Belike, 这次产生的提交对象会包含一个指向上次提交对象(父对象)的指针:

使用git branch创建了一个分支,就会创建一个新的指针,Git分支实际上仅是包含所指对象校验和(创建一个新分支就相当于往一个文件中写入 41 个字节),Git分支本质上是指向提交对象的可变指针(HEAD始终指向当前所在的本地分支):

当我们执行 git checkout testing, HEAD就指向testing分支了~

如果我们再切回master分支做了一些修改并且提交,那么我们的提交记录看起来就像下面这样~

使用 git log --oneline --decorate --graph --all 指令可以很清晰地看出来提交历史,分叉情况等~

分支的新建与合并

shell 复制代码
/*新建分支*/
git checkout -b <branchname> 

/*等价于*/
git branch <branchname>
git checkout <branchname>

当切换分支的时候,Git 会重置工作目录,使其看起来像回到了在那个分支上最后一次提交的样子。 Git 会自动添加、删除、修改文件以确保此时你的工作目录和这个分支最后一次提交时的样子一模一样。

1.当我们在修复bug的时候,如果在master分支上拉取一个Bugfix,最后要合入master, 由于这两个分支不存在分叉,所以合并的方式是 快进(fast-forward)

顺利合入之后:使用 git branch -d hotfix 删除bug分支

2.但是当我们在dev分支开发完成之后,我们也需要合入master分子,这是提交历史由于bugfix分支出现了分叉,所以这时的合并我们称之为一次合并提交,特殊之处在于它不止一个父提交:

分支管理

shell 复制代码
git branch (当前分支的全部列表)
git branch -v (附带每个分支最后一次提交的信息)
git branch --merged (过滤出这个列表中已经合并到当前分支的分支)
git branch --no-merged (查看所有包含未合并工作的分支)

git branch --merged/--no-merged <branchname>(查看所有合并/未合并到当前分支的分支)

git branch -d testing(如果之前未合并过的分支会不允许删除,使用 -D 强制删除)

远程分支

shell 复制代码
git fetch <remote> 
查找并抓取远程仓库中本地没有的数据,并且更新本地数据库

推送

shell 复制代码
git push origin serverfix
将本地的分支推到远程,但是不会设置追踪关系

git push -u origin new-test
将本地的分支推到远程,同时设置追踪关系

这句指令相信大家都不陌生,将本地的分支推送到远程,但是其实有些工作被简化了。这条指令相当于 git push origin refs/heads/serverfix:refs/heads/serverfix, 这意味着"推送本地的 serverfix 分支来更新远程仓库上的 serverfix 分支" 。 或者是 git push origin serverfix:serverfix,这意味着"推送本地的 serverfix 分支,将其作为远程仓库的 serverfix 分支"

追踪

shell 复制代码
git checkout -b serverfix origin/serverfix
从一个远程跟踪分支检出一个本地分支会自动创建所谓的"跟踪分支

/*下面两个指令和上面的作用都是等价的*/
git checkout --track origin/serverfix
git checkout serverfix

如果本地已经有了分支,想要显式设置和远程分支的追踪关系(感觉容易出错)

shell 复制代码
git branch -u origin/serverfix

如果想删除远程分支

shell 复制代码
git push origin --delete serverfix
这个命令做的只是从服务器上移除这个指针。 Git 服务器通常会保留数据一段时间直到垃圾回收运行,所以如果不小心删除掉了,通常是很容易恢复的。

变基

一直不太理解的一个概念---

我们执行合并操作有两种方式,一种是merge,一种是rebase变基。

1.当我们执行merge操作的时候,会把两个分支的操作合并会生成一个新的提交C5

(如果有冲突解决冲突再提交,这个C5就是解决冲突的那次提交。如果没有冲突,那么自动生成一次提交)

2.当我们执行rebase变基操作的时候,可以将某个分支上的修改都移至另一个分支,就好像重新播放一样。

shell 复制代码
$ git checkout experiment
$ git rebase master
/*在experiment分支上执行一次变基操作,这样提交历史就变成线型的了*/

$ git checkout master
$ git merge experiment
/*切到master分支,这时只需要执行一次快速合并,不需要执行三方合并*/

但是对于变基我们却不能滥用,下面用一个更加复杂的例子来说明这个问题~

如图所示:当前的提交历史比较复杂:

如果想把C8, C9 迁移到master后续的分支,那么可以这么做:

css 复制代码
git rebase --onto master server client
/**取出 client 分支,找出它从 server 分支分歧之后的补丁, 然后把这些补丁在 master 分支上重放一遍,让 client 看起来像直接基于 master 修改一样**/

整合好了之后,如果我们想把server的内容也迁移到master上面呢?

可以使用 git rebase master server 将server中的修改变基到 master 上面

这时我们打印server分支上的提交历史可以看到:

我们经常说变基是危险的,那么为什么是危险的呢?

Git工具

选择修订版本

使用 git log --abbrev-commit --pretty=oneline 你会得到简略的输出历史,然后使用 git show来检视某次提交

分支引用

同时,由于master分支也指向这次提交,所以也可以使用 git show master 是等价的。 反之,如果想得到某个分支最后一次提交的commitId,也可以使用 git rev-parse master

引用日志

引用日志(reflog)存在于本地仓库,它记录你在自己的仓库中做了什么的日志,比如最近几个月的HEAD和分支引用所指向的历史,同时每个人的引用日志都是不同的 使用git reflog来查看引用历史:

(每次HEAD发生变化,Git都将信息存储到引用历史这个历史记录里)

shell 复制代码
git show HEAD@{5}
/*可以使用 @{n} 来引用 reflog 中输出的提交记录*/

祖先引用

HEAD表示当前的指向,HEAD^表示该引用的上一个引用

同时对于合并的提交,^1表示合并提交的第一父提交(所在的分支),^2表示合并提交的第二父提交(合并的分支)

还有一个语法就是~,HEAD^和HEAD~ 是等价的,区别在于当后面跟数字的时候,^只能表示合并提交的父提交,但是~n表示当前分支向上追溯n次提交

所以我们可以得出这样的结论: ^可以找到当前分支的全部提交,~可以找到被合并到当前分支上的提交

提交区间

在上面的操作中,我们只能看到单次提交,如何查看一定区间的提交:

c 复制代码
$ git log master..experiment
D
C
/*在 experiment 分支中而不在 master 分支中的提交*/

交互式暂存

在批量修改了多个文件,我们希望一次暂存部分文件,可以选择使用 git add -i 启动交互式终端模式,具体操作见Git - 交互式暂存

注意,进入了其中一个commands,按【enter】可以退出重新回到 What now> 模式

贮藏与清理

贮藏工作

stash帮助我们处理工作目录的脏状态,即跟踪文件的修改和暂存的改动,它会将未完成的修改保存到一个栈上,可以在任何时候任何分支应用这些改动

当我们储存了一些内容,然后我们切换了分支之后,首先我们可以使用 git stash list 指令查看储存的内容:

如果想在当前分支上应用某个改动,可以使用 git stash apply stash@{0}(注意在powershell上面需要加引号:"stash@{0}" )

Note: 使用 git stash apply 只会恢复工作区,不会恢复暂存区的文件。需要加上**--index**

如果希望扔掉某个stash记录:git stash drop stash@{0}

如果希望应用贮藏然后扔掉stash记录:git stash pop

追踪范围

stash默认只能贮藏被追踪 (被add过但是修改了的+被add了的)的文件,如果指定了 --include-untracked-u 选项,那么也会贮藏工作区从来没有被追踪过的文件

清理工作目录

git clean -[] 这个命令用来设计为从工作目录移除未被追踪过 的文件,同时这个命令具有破坏性,如果你反悔了你也不一定可以找到了。任何与 .gitignore 或其他忽略文件中的模式匹配的文件都不会被移除(如果也想移除这些文件使用 -x)。

类似作用的是:git stash --all, 这个指令可以移除每一样东西并且存放在栈中

shell 复制代码
git clean -f -d
/*移除工作目录中所有未追踪的文件以及空的子目录*/

git clean -d -n
/*做一次演习然后告诉你 将要 移除什么*/

git clean -n -d -x
/*做一次演习然后告诉你 将要 移除什么,包括ignore中匹配的文件*/

重置揭密

重置两个命令reset和checkout,我们可以从三个概念出发,也可以称为Git中的三棵树:

在这三个概念的基础上, Git工作流程就是通过操纵这三个区域来以更加连续的状态记录项目快照的:

每个文件的提交都会经历从WD到Index再到HEAD的过程

切换分支或克隆的过程也类似。 当检出一个分支时,它会修改 HEAD 指向新的分支引用,将 索引 填充为该次提交的快照, 然后将 索引 的内容复制到 工作目录 中。

重置的作用

现在分步骤研究一下reset指令的作用:

1.移动HEAD

首先移动HEAD的指向,现在在master分支上面HEAD指向上一次提交

撤销上一次git commit命令,将HEAD分支移到上一个位置,但是不会改变索引和工作目录

2.更新索引(--mixed)

使用--mixed,不仅会恢复HEAD, 还会把暂存区恢复,我们可以在此基础上重新commit提交

3.更新工作目录(--hard)

reset 命令会以特定的顺序重写这三棵树,在你指定以下选项时停止:

  1. 移动 HEAD 分支的指向 (若指定了 --soft ,则到此停止)
  2. 使索引看起来像 HEAD (若未指定 --hard ,则到此停止)
  3. 使工作目录看起来像索引 (只有在 --hard 时才执行。)

同时,也可以通过路径重置 = 取消暂存,注意这时不会移动HEAD

shell 复制代码
git reset file.txt

/**取消 file.txt 的暂存状态,让它从"已暂存"变成"已修改但未暂存" **/

如果你没有指定 commit 或分支名(比如 HEAD123abc),Git 会默认用当前 HEAD 作为参照点,所以这条命令其实等同于:

shell 复制代码
git reset --mixed HEAD file.txt

/**
1.不移动 HEAD 指针(因为是对文件,不是对整个提交的回退)
2.把暂存区里的 file.txt 还原成 HEAD 中的版本(也就是上次提交的版本)
3.不动工作目录里的文件内容

效果就是 => 取消暂存这个文件的更改,但保留你写的代码不变。
**/

本质上:和 git add 是反向操作

  • git add file.txt:把文件的更改 加入暂存区
  • git reset file.txt:把文件的更改 从暂存区撤回

进一步,可以指定某个老的提交版本:

shell 复制代码
git reset 123abc file.txt
/*
 file.txt 的 暂存区 变成 123abc 那次提交的样子,然后你可以决定是否再次添加或提交。
*/

--patch 是"选择性撤销"

shell 复制代码
git reset --patch file.txt

这会让你"交互式地选择"文件里的哪些改动想取消暂存(而不是整个文件),跟 git add --patch 的作用相反。适合精细控制提交内容。

压缩合并

利用 reset 我们可以压缩提交,如图所示,可以将HEAD移动到倒数第三次,然后压缩后面两次提交~

检出

checkout也操纵这三棵树,但是很不同的一点是是否给该命令一个文件路径

不带路径
shell 复制代码
git checkout <branch>

行为:

这个命令会切换分支,并进行下面几步:

  1. 移动 HEAD 本身的指向(HEAD → 新分支),而不是移动某个分支指针。
  2. 使索引和工作目录都同步为目标分支的状态(会自动合并未修改的文件)。
  3. 会检查你工作目录是否有未保存的更改,避免丢失未提交内容 (和 reset --hard 不同,checkout 会保护你本地修改)。

类比 reset

  • git reset --hard <branch> 也会把三棵树都替换成 <branch> 的内容,但它直接重写所有文件,不会检查或合并。
  • checkout <branch> 会尝试保留你本地的未修改内容。
带路径
shell 复制代码
git checkout <branch> -- <path>

行为:

这个命令不会切换分支,而是:

  1. 只拉取指定文件** <path> <branch>中的内容。
  2. 更新索引和工作目录中的该文件。
  3. 不会移动 HEAD 的指向,你仍然在当前分支上。

也就是说,它可以让你在当前分支上,把其他分支上的某个文件"检出"过来,用于修复、对比或提取。

类比 reset

这个就像是 git reset --hard <branch> <file>,但 reset 实际上不支持这种路径级别的 --hard 操作;而 checkout 做到了。

高级合并

这章介绍了很多合并的方式,这里暂时只对于revert撤销合并做出理解~

撤消合并

撤销合并有两个方法,一个就是使用reset撤销合并,如图所示,将HEAD指针移到合并之前的提交

但是这种方法的弊端就是如果有人已基于合并提交的M 做出修改,就会比较危险,所以还有一种解决方案就是使用revert:

当使用 git revert -m 1 HEAD, -m l 意思是meanline, 即被保留下来的父结点。它的意思是我们需要撤销C4引入的修改,保留C6引入的修改,所以现在 ^M 的内容就和 C6 一样

如果想继续merge topic不会生效,Git会记得你曾经合并过topic。因此,我们需要撤消还原原始的合并,然后 创建一个新的合并提交:

ruby 复制代码
$ git revert ^M
[master 09f0126] Revert "Revert "Merge branch 'topic'""
$ git merge topic

完整流程:

  1. 合并 topic(→ 创建合并提交 M)
  2. 撤销合并:git revert -m 1 M(→ 创建 ^M,覆盖掉 M 的修改)
  3. 不能再 merge topic(因为 Git 记得你合并过)
  4. 如果你想合并回来,得先 git revert ^M(→ 恢复原来合并的内容)
  5. 然后再 git merge topic(可以合并后续新的 commit)

Git内部原理

Git对象

数据对象(blob object)

git的本质是一个内容寻址的文件系统,核心其实是键值对数据库(.git/objects),可以像这个数据库插入任意类型的内容,会返回一个取回该内容的键;

使用底层命令 git hash-object 可以把任意数据保存在这个数据库,并返回键key

首先init初始化一个仓库,之后使用git hash-object创建数据对象并写入这个仓库

bash 复制代码
 $ git init test
Initialized empty Git repository in /tmp/test/.git/

$ echo  'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4

$ find .git/objects - type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
/*可以看到这条数据已经被存进去了*/

$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content
/*之后可以利用这个commitId 取出对应的内容*/

接着我们再写入两条数据:

bash 复制代码
$ echo  'version 1' > test.txt
$ git hash-object -w test.txt
83baae61804e65cc73a7201a7252750c76066a30

$ echo  'version 2' > test.txt
$ git hash-object -w test.txt
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a

此时我们查看objects文件夹此时的内容:
$ find .git/objects - type f
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

利用 git cat-file -t [commitId] 我们可以很清晰地看出当前储存地数据是什么类型

bash 复制代码
$ git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
blob

树对象(tree object)

通常, Git会根据某一个时刻暂存区所表示的状态创建并记录一个对应的树对象。可以通过底层命令 git update-index 为一个单独的文件创建一个暂存区。

bash 复制代码
 $ git update-index --add --cacheinfo 100644 \
  83baae61804e65cc73a7201a7252750c76066a30 test.txt

之后将这个暂存区的内容写入一个树对象

bash 复制代码
$ git write-tree
d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579
100644 blob 83baae61804e65cc73a7201a7252750c76066a30      test.txt

/*验证一下确实是树对象*/
$ git cat-file -t d8329fc1cc938780ffdd9f94e0d364e0ea74f579
tree

接着,我们创建一个新的树对象,包括test.txt的第二个版本和一个新的文件, 记录下这个树对象:

bash 复制代码
$ echo 'new file' > new.txt
$ git update-index --add --cacheinfo 100644 \
  1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt
$ git update-index --add new.txt

$ git write-tree
0155eb4229851634a0f03eb265b69f5a2d56f341
$ git cat-file -p 0155eb4229851634a0f03eb265b69f5a2d56f341
100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a      test.txt

现在有两个树对象,你可以将第一个树对象加入第二个树对象:

bash 复制代码
$ git read-tree --prefix=bak d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git write-tree
3c4e9cd789d88d8d89c1073707c3585e41b0e614
$ git cat-file -p 3c4e9cd789d88d8d89c1073707c3585e41b0e614
040000 tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579      bak
100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a      test.txt

提交对象(commit object)

现在已经有了三个SHA-1哈希值,但是一些更加详细的信息,提交对象可以为你保存;

我们可以调用 commit-tree创建一个提交对象,需要指定一个树对象的SHA-1值,以及该提交对象的父提交对象(如果有的话)

bash 复制代码
$ echo  'first commit' | git commit-tree d8329f
fdf4fc3344e67ab068f836878b6c4951e3b15f3

查看这个新的提交对象:

bash 复制代码
$ git cat-file -p fdf4fc3
tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author Scott Chacon <[email protected]> 1243040974 -0700
committer Scott Chacon <[email protected]> 1243040974 -0700

first commit

提交对象首先指定一个顶层树对象(代表当前项目快照),然后是可能存在的父提交对象,之后是作者/提交者信息,留空一行,然后是提交注释

bash 复制代码
$ echo 'second commit' | git commit-tree 0155eb -p fdf4fc3
cac0cab538b970a37ea1e769cbbde608743bc96d
$ echo 'third commit'  | git commit-tree 3c4e9c -p cac0cab
1a410efbd13591db07496601ebc7a059dd55cfe9

然后使用git log你已经可以看到提交记录啦~

现在整体的数据结构类似下面:

Git引用

如果有一个名字来代替SHA-1值,在Git中这样的名字指针叫做引用(refs),可以在.git/refs下面找到这类文件

bash 复制代码
 $ find .git/refs
.git/refs
.git/refs/heads
.git/refs/tags
$ find .git/refs - type f

我们不提倡直接编辑引用文件。 如果想更新某个引用,Git 提供了一个更加安全的命令 update-ref 来完成此事:

bash 复制代码
$ git update-ref refs/heads/master 1a410efbd13591db07496601ebc7a059dd55cfe9

这基本就是 Git 分支的本质:一个指向某一系列提交之首的指针或引用。 若想在第二个提交上创建一个分支,可以这么做:

bash 复制代码
$ git update-ref refs/heads/test cac0ca

这个分支将只包含从第二个提交开始往前追溯的记录:

bash 复制代码
$ git log --pretty=oneline test
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit

现在Git数据库看起来像这样:

HEAD引用

HEAD文件是一个符号引用,指向当前所在的分支

查看当前HEAD文件的内容,我们可以看到这样的内容:

bash 复制代码
$ cat .git/HEAD
ref: refs/heads/master

如果执行 git checkout test , Git会像这样更新HEAD文件:

bash 复制代码
$ cat .git/HEAD
ref: refs/heads/test

可以借助 git symbolic-ref 来编辑或查看该文件:

bash 复制代码
$ git symbolic-ref HEAD
refs/heads/master

$ git symbolic-ref HEAD refs/heads/test
$ cat .git/HEAD
ref: refs/heads/test

标签引用

标签对象通常指向一个提交对象,而不是一个树对象。 它像是一个永不移动的分支引用------永远指向同一个提交对象,只不过给这个提交对象加上一个更友好的名字罢了。

可以像这样创建一个轻量标签

bash 复制代码
$ git update-ref refs/tags/v1.0 cac0cab538b970a37ea1e769cbbde608743bc96d

轻量标签就是给一个固定的引用取名,附注对象会更加复杂一些。Git会创建一个标签对象,并创建一个引用指向该标签对象,引用不会直接指向提交对象

创建一个附注对象:

bash 复制代码
$ git tag -a v1.1 1a410efbd13591db07496601ebc7a059dd55cfe9 -m 'test tag'

该标签对象的SHA-1值:

bash 复制代码
$ cat .git/refs/tags/v1.1
9585191f37f7b0fb9444f35a9bf50de191beadc2

现在对该 SHA-1 值运行 git cat-file -p 命令

bash 复制代码
$ git cat-file -p 9585191f37f7b0fb9444f35a9bf50de191beadc2
object 1a410efbd13591db07496601ebc7a059dd55cfe9
type commit
tag v1.1
tagger Scott Chacon <[email protected]> Sat May 23 16:48:58 2009 -0700

test tag

Note: 标签对象并非必须指向某个提交对象;你可以对任意类型的 Git 对象打标签。

远程引用

添加了一个远程版本库并对其执行过推送操作,Git 会记录下最近一次推送操作时每一个分支所对应的值,并保存在 refs/remotes 目录下。

远程引用和分支(位于 refs/heads 目录下的引用)之间最主要的区别在于,远程引用是只读 的。 虽然可以 git checkout 到某个远程引用,但是 Git 并不会将 HEAD 引用指向该远程引用

附录

附录1: git log 输出选项

相关推荐
哆啦刘小洋24 分钟前
HTML Day04
前端·html
再学一点就睡1 小时前
JSON Schema:禁锢的枷锁还是突破的阶梯?
前端·json
从零开始学习人工智能2 小时前
FastMCP:构建 MCP 服务器和客户端的高效 Python 框架
服务器·前端·网络
烛阴2 小时前
自动化测试、前后端mock数据量产利器:Chance.js深度教程
前端·javascript·后端
好好学习O(∩_∩)O2 小时前
QT6引入QMediaPlaylist类
前端·c++·ffmpeg·前端框架
敲代码的小吉米2 小时前
前端HTML contenteditable 属性使用指南
前端·html
testleaf3 小时前
React知识点梳理
前端·react.js·typescript
站在风口的猪11083 小时前
《前端面试题:HTML5、CSS3、ES6新特性》
前端·css3·html5
Xiao_die8883 小时前
前端八股之CSS
前端·css
每天都有好果汁吃3 小时前
基于 react-use 的 useIdle:业务场景下的用户空闲检测解决方案
前端·javascript·react.js