1. Git是怎么存储信息的
前置知识: 相关Linux命令介绍
echo
: 输出文本echo 'text' > filename
: 将字符串输出重定向,当前目录没有对应文件,则创建,并将字符串输出到文件中tree
: 主要功能是创建文件列表,将所有文件以树的形式列出来cat filename
: 一次性显示整个文件
$ git init
$ echo '111' > a.txt
$ echo '222' > b.txt
$ git add *.txt
以上命令行的意思是:
- 初始化一个git项目
- 将 a.txt 中的内容重写为 ‘111’, 如果没有 a.txt, 新建
- 同上
- 将所有的 .txt 文件添加到暂存区
$ tree .git/objects
.git/objects
├── 58
│ └── c9bdf9d017fcd178dc8c073cbfcbb7ff240d6c
├── c2
│ └── 00906efd24ec5e783bee7f23b5d7c941b0c12c
├── info
└── pack
$ cat .git/objects/58/c9bdf9d017fcd178dc8c073cbfcbb7ff240d6c
xKOR0a044K%
这意味着当我们把文件添加到暂存区的时候,实际上每个文件都在 .git/objects
下生成了一个 blob
对象
这里我们可以借助 git 一个很高级的命令 git cat-file [-t] [-p]
来验证一下
git cat-file
Provide content or type and size information for repository objects.
git cat-file
命令显示版本库对象的内容、类型及大小信息。
-t
Instead of the content, show the object type identified by object.
显示对象的类型。
-p
Pretty-print the contents of object based on its type.
根据对象的类型,以优雅的方式显式对象内容。
$ git cat-file -t 58c9
blob
$ git cat-file -p 58c9
111
那么也就是在 git object 中生成了两个键为文件 hash值, 值为对应文件内容的键值对的 blob 对象
$ git commit -m '[+] init'
$ tree .git/objects
.git/objects
├── 0c
│ └── 96bfc59d0f02317d002ebbf8318f46c7e47ab2
├── 4c
│ └── aaa1a9ae0b274fba9e3675f9ef071616e5b209
...
$ git cat-file -t 4caaa1
tree
$ git cat-file -p 4caaa1
100644 blob 58c9bdf9d017fcd178dc8c0... a.txt
100644 blob c200906efd24ec5e783bee7... b.txt
当我们将暂存区的代码提交到本地仓库的时候, git 在 objects 下生成了两个新的对象, 其中一个是 tree 类型的对象, 对象中包含着此次提交的版本号, 文件名等快照, 相当于一个映射
$ git cat-file -t 0c96bf
commit
$ git cat-file -p 0c96bf
tree 4caaa1a9ae0b274fba9e3675f9ef071616e5b209
author lzane 李泽帆 1573302343 +0800
committer lzane 李泽帆 1573302343 +0800
[+] init
另一个对象是 commit 对象, 里面包含了 提交的版本号,作者,提交者的信息映射
那分支和 tag 在哪里呢?
$ cat .git/HEAD
ref: refs/heads/master
$ cat .git/refs/heads/master
0c96bfc59d0f02317d002ebbf8318f46c7e47ab2
HEAD, 分支,普通的 Tag 可以简单的理解成是一个指针, 指向对应 commits 的 hash 值
Q1: 为什么要把文件的权限和文件名储存在 Tree object 里面而不是 Blob object 呢?
如果我们的这个文件有成千上万行代码, 当我们更改文件名的时候, git 只需要生成一个新的 提交映射就 ok, 而如果是在 blob object 对象中储存, 就要生成一个新的 blob对象 , 体积庞大.
2. Git 的三个分区及变更历史的形成
- 工作目录 ( working directory ):操作系统上的文件,所有代码开发编辑都在这上面完成。
- 索引( index or staging area ):一个暂存区域,会在下一次commit被提交到Git仓库。
- Git仓库( git repository ):由Git object记录着每一次提交的快照,以及链式结构记录的提交变更历史
- 当我们修改 a.txt 文件并 add 之后
- git 会将新的 a.txt 添加到 git 仓库生成一个 blob 节点
- 并更新当前的索引
- 执行 commit 之后
- 将当前索引生成一个 tree 节点
- 新建一个 commit 节点
- 更新分支指向
思考一下git的各种命令,尝试在上图将它们“可视化”出来
Q2:每次commit,Git储存的是全新的文件快照还是储存文件的变更部分?
是全新的, 复杂度上来说更友好
Q3:Git怎么保证历史记录不可篡改?
hash值, 文件一旦发生变化对应的hash值就会改变
3. Git 实用技巧
- 误操作导致分支不见了,如何恢复?不小心删了分支
git reset --hard
: 回退到指定版本
git rebase
: 把本地未push的分叉提交历史整理成直线
当我们本地有多次commit,想要推送到远端而远端有新的提交的时候,我们拉取了新的提交之后就会有本地的提交历史就会分叉,可以使用 git rebase 命令将提交历史整理成一条直线
git reflog
: 查看命令历史,以便确定要回到未来的哪个版本 - 如何获得一个干净的工作空间?
git stash push [-u | --include-untracked]
: 将工作区和暂存区的文件贮藏起来,比如你改到一半的bug
默认情况下, git stash 会缓存下列文件:
- 添加到暂存区的修改;
- Git跟踪的但并未添加到暂存区的修改
但不会缓存以下文件:
- 在工作目录中新的文件;
- 被忽略的文件
使用
-u
或者-- include-untracked
参数可以贮藏未跟踪或忽略的文件
使用-a
或者--all
参数可以贮藏当前目录下的所有修改
- 从Git历史中删除一个文件
git filter-branch --tree-filter 'rm -f passwords.txt' HEAD
- 其他
git commit --amend
: 重新操作上一次的提交说明,在日志是没有操作记录的
git rebase -i origin/master
: 交互式变基
git show-branch
git blame [filename]
: 追溯一个指定文件的历史修改记录
git bisect