借助于版本管理工具,我们可以方便的进行数据管理(历史浏览、数据追责、记录合并、冲突解决)、数据恢复。熟练运用版本管理工具能够有效提高我们的工作效率,也能减少很多不必要的麻烦
为什么 stash 节点有两个父节点
stash 需要同时保存 Git Tree 和 Index Tree,自身的 Tree 中保存了 Working Tree 信息
$ cat .git/refs/stash
cdeb6831
$ cat .git/logs/refs/stash
00000000 ce8a05da wangzihan 1628308344 +0800 WIP on develop: 3b71625 tmp
ce8a05da cdeb6831 wangzihan 1628599766 +0800 WIP on develop: 385b0c2 test
$ git cat-file -p ce8a05da
tree 40cd7e7b
parent 3b71625e
parent d2977e25
author wangzihan 1628308344 +0800
committer wangzihan 1628308344 +0800
WIP on develop: 3b71625 tmp
// git stash pop --index(是否恢复 Index 信息)
通过版本管理工具,可以很容易的进行数据管理、数据恢复、浏览历史、数据合并、冲突解决。版本管理发展主要经历了三个阶段:
- 本地版本管理(文件夹备份)
- 集中式版本管理(单点故障、大部分指令依赖联网)
- 分布式版本管理(本地保存仓库的全部镜像数据、大部分指令可以在本地完成)
Git 工具
对于每次提交或是保存状态,Git 存储的是文件快照,而不是文件的变化。为了减少磁盘占用和提高效率,对于没有变化的文件,Git 不会再次生成 blob,而是一个指向之前 blob 的指针
在 Git 中,所有内容都是经过 SHA-1(40 个字符的字符串) 校验的,由十六进制字符(0-9 和 a-f)组成,并根据 Git 中的文件或目录结构的内容计算得出
24b9da6552252987aa493b52f8696cd6d3b00373
对于上图中的每一次提交,我们通过commit来表示
不同commit通过parent指针来构成链式关系
通过上面的分析,我们了解到Git主要是通过树结构来维护文件快照的,对应的数据结构为:Commit、Tree、Blob
对于 Git 来说,几乎所有操作都是增加数据。不过下面三条指令例外,如果依次执行,你可能会有想哭却哭不出来的时候
// 查看当前有多少悬空对象
git fsck --unreachable --no-reflogs
// 使 reflog 过期
// rewrite 参数作用
// each reflog entry actually contains two SHA1 identifiers: the value
// of the ref before the change and the value of the ref after the change.
// To ensure safe garbage collection, reflog expire simply deletes any entry
// where one of the two SHA1s identifies an unreachable commit.
git reflog expire --expire-unreachable=now --all --rewrite
// 执行 GC 任务
git gc --prune=now
Git通过空间换时间来提高命令执行速度,但也导致了数据膨胀的问题,针对于此Git内部做了一些压缩优化的策略
Git 树
在 Git 中,文件有三种状态,分别为:修改、暂存、已提交
- 修改意味着本地文件的改动,但是没有被 Git 暂存区管理
- 暂存意味着 Git 已经记录了文件的改动,会提交到下次的 Commit 快照中
- 已提交意味着文件已经存入数据库
git status命令大家再熟悉不过了,这时如果执行commit命令,绿色的会被带到下一次提交(commit)中,红色的不会。但是Git是如何区分哪些改动应该显示成绿色,哪些显示成红色?
这引出了 Git 工程三棵树的概念,分别为 Working Tree、Staging / Index Tree、Git / Head Tree,Git中的大部分指令都是围绕三棵树进行的
上面介绍了Git的数据结构以及三棵树的概念,下面我们通过reset和checkout命令来仔细探究一下命令背后的原理
更新整棵树的场景
初始状态(蓝色代表目标commit树)
git reset --soft cd83818a:通过指定的commit节点更新Git Tree的内容
Tips:--soft参数常用于压缩本地commit节点,避免一个需求对应N次提交的问题,有利于后期维护
git reset --mixed cd83818a:通过指定的commit节点更新Git Tree以及Index Tree的内容
Tips:--mixed参数常用于清空暂存区的提交
git reset --hard cd83818a
Tips:--hard参数会把三棵树都恢复到某一次提交,包括本地未提交的内容也会被刷掉
git checkout [branch]
git checkout [branch]和git reset --hard [branch]类似,都是将三棵树更新为某一次的提交状态,但是也有一些区别:
- 和reset hard不同,checkout时目录安全的
- 和reset hard不同,checkout只更新HEAD指针,而reset hard会同时更新Branch指针
更新文件的场景
初始状态
git reset uj38hfue A
reset操作文件默认使用--mixed参数,但由于我们无法改变commit中的某一个文件,因此Git Tree不变,只改变Index Tree
Tips:常用于清空暂存区中的某一个文件,例如,恢复错误执行add命令的场景,避免特定文件被带入下次commit
git checkout uj38hfue A
Git Tree不变,只改变Index Tree和Working Tree中对应的文件
git checkout A
Git Tree不变,用Index Tree更新Working Tree中对应的文件
上面介绍了三棵树的使用场景,另外Git也提供了很多高级参数,比如我们通过add命令的-i参数可以只添加部分内容,很多命令都有interactive模式(精细化操作)
Git 配置
$ git config --list // 查看所有的配置信息
$ git config --global user.name "John Doe"
$ git config --global user.email johndoe@example.com
$ git config --local user.name "John Doe" // 工程内的配置
$ git help <verb> // 查看详细帮助
$ git <verb> --help // 查看详细帮助
$ git add -h // 查看简要帮助
查看文件状态
$ git status -s // git status --short
M README
MM Rakefile
A lib/git.rb
M lib/simplegit.rb
?? LICENSE.txt
查看暂存区和工作区的修改
$ git diff // 查看未放入暂存区的修改
$ git diff --staged // 查看放入暂存区的修改
提交改动
$ git commit -m "fix benchmarks for speed"
$ git commit -m -a “Add new benchmarks”
// -a 参数用来提交全部改动(包括未放入暂存区的改动,前提是文件在 Git Tree 中已经存在)
删除暂存区中的文件
// --cached 参数指明只删除 Index Tree 中的文件,而不删除 Working Tree 中的文件
$ git rm --cached README
查看提交历史
// -p / --patch 用来查看每次提交修改的内容
// -num 用来指定显示几条提交
$ git log -p -1
// --stat 显示文件级别的修改
$ git log --stat -2
// --pretty 格式化输出
$ git log --pretty=oneline
$ git log --oneline
$ git log --pretty=format:"%h - %an, %ar : %s"
// --graph 按照图谱格式输出
$ git log --pretty=format:"%h %s" --graph
* 2d3acf9 Ignore errors from SIGCHLD on trap
* 5e3ee11 Merge branch 'master' of git://github.com/dustin/grit
|\
| * 420eac9 Add method for getting the current branch
* | 30e367c Timeout code and tests
* | 5a09431 Add timeout protection to grit
* | e1193f8 Support for heads with slashes in them
|/
* d6016bc Require time for xmlschema
* 11d191e Merge branch 'defunkt' into local
远程仓库
$ git remote
origin
$ git remote -v
origin https://github.com/schacon/ticgit (fetch)
origin https://github.com/schacon/ticgit (push)
$ git remote add pb https://github.com/paulboone/ticgit
$ git remote -v
origin https://github.com/schacon/ticgit (fetch)
origin https://github.com/schacon/ticgit (push)
pb https://github.com/paulboone/ticgit (fetch)
pb https://github.com/paulboone/ticgit (push)
$ git fetch origin // 拉取最新代码
$ git remote rename pb paul
$ git remote
origin
paul
$ git remote remove paul
$ git remote
origin
Tag 标签
// 列出所有 Tag
$ git tag
$ git tag -l
$ git tag --list
v1.0
v2.0
$ git tag -l "v1.8.5*" // 模糊匹配
v1.8.5
v1.8.5-rc0
v1.8.5-rc1
// 创建 Lightweight Tag
$ git tag v0.1
// 创建 Annotated Tag
$ git tag -a v1.4 -m "my version 1.4"
$ git tag
v0.1
v1.4
// 根据 commitId 创建 Tag
$ git tag -a v1.2 9fceb02
$ git show v1.4
tag v1.4
Tagger: Ben Straub <ben@straub.cc>
Date: Sat May 3 20:19:12 2014 -0700
my version 1.4
commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date: Mon Mar 17 21:52:11 2008 -0700
Change version number
$ git checkout v2.0.0
Note: switching to 'v2.0.0'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.
// 根据 Tag 切换新分支
$ git checkout -b version2 v2.0.0
Switched to a new branch 'version2'
Git 别名
$ git config --global alias.unstage 'reset HEAD --'
分支概念
分支代表一个指向具体 commit 对象的指针
当我们 commit 数据时,git 会生成一个 commit 对象,该对象包含一个指向内容快照的指针,此外还包含作者的信息、commit 信息以及一个或多个父节点的信息:初始提交没有父节点,正常提交包含一个父节点 ,merge commit 包含两个或多个父节点。具体结构见下图,关键数据结构有:commit、tree、blob
如果我们提交多次,则 commit 对象同时会保存父节点的信息
通过 git branch 切换新分支
$ git branch testing
Git 通过 HEAD 指针来识别当前的分支
通过 git checkout 改变 HEAD 指针(前提是分支已存在)
$ git checkout testing
修改分支名
$ git branch --move bad-branch-name corrected-branch-name
$ git push origin corrected-branch-name
$ git push origin --delete bad-branch-name
Merge
Git 会合并两个节点的快照数据并创建一个新的 commit 节点,这个 commit 节点存在两个父节点
类似于 git merge, git pull 相当于执行了两个操作,分别为 git fetch 和 git merge
Rebase
可以有效历史记录避免分叉,不过 rebase 过程会重新生成新的 commit id,所以千万不要 rebase 已经提交到远端并且被其他人依赖的 commit 记录
Rebase vs Merge
除了 rebase 要遵守的那一条外,rebase 和 merge 具体要怎么用,只能看具体情况。首先,merge 能还原历史修改,而 rebase 历史脉络更清晰,但是破坏了原始的历史记录。个人更倾向于本地分支使用 rebase,和写书类似,草稿是不应该被发表的
查看单次历史提交
Git 允许查看历史提交记录(一个或多个),显然我们可以通过 40 个字符的 SHA-1 来引用到具体的一次提交,但是有更友好的方式
// 通过 SHA-1 缩写
$ git show 1c002dd4b536e7479fe34593e72e6c6c1819e53b
$ git show 1c002dd4b536e7479f
$ git show 1c002d
// 通过分支名
$ git show topic1
// 通过 reflog
$ git reflog == git log -g
734713b HEAD@{0}: commit: Fix refs handling, add gc auto, update tests
d921970 HEAD@{1}: merge phedders/rdocs: Merge made by the 'recursive' strategy.
1c002dd HEAD@{2}: commit: Add some blame and merge stuff
1c36188 HEAD@{3}: rebase -i (squash): updating HEAD
95df984 HEAD@{4}: commit: # This is a combination of two commits.
1c36188 HEAD@{5}: rebase -i (squash): updating HEAD
7e05da5 HEAD@{6}: rebase -i (pick): updating HEAD
$ git show HEAD@{5}
// 通过 ^(如果有多个父节点,^标识获取哪一个父节点) 和 ~(上层节点)
$ git show d921970~3^2
$ git show HEAD~3
查看一定范围内的历史提交
// 在 master 分支,但是不在 experiment 分支的提交
$ git log master..experiment
D
C
// 在 refA 和 refB 分支,但不再 refC 分支的提交
$ git log refA refB ^refC
$ git log refA refB --not refC
$ git log --left-right master...experiment
< F
< E
> D
> C
超级编辑器模式
$ git add --interactive
$ git add -i
staged unstaged path
1: unchanged +0/-1 TODO
2: unchanged +1/-1 index.html
3: unchanged +5/-1 lib/simplegit.rb
*** Commands ***
1: [s]tatus 2: [u]pdate 3: [r]evert 4: [a]dd untracked
5: [p]atch 6: [d]iff 7: [q]uit 8: [h]elp
What now>
// 类似命令
$ git add --patch
$ git reset --patch
$ git checkout --patch
$ git stash save --patch
Stash
You can reapply the one you just stashed by using the command shown in the help output of the original stash command: git stash apply
. If you want to apply one of the older stashes, you can specify it by naming it, like this: git stash apply stash@{2}
. If you don’t specify a stash, Git assumes the most recent stash and tries to apply it:
$ git stash apply
$ git stash apply stash@{2}
$ git stash pop
$ git stash drop
$ git stash list
stash@{0}: WIP on master: 049d078 Create index file
stash@{1}: WIP on master: c264051 Revert "Add file_size"
stash@{2}: WIP on master: 21d80a5 Add number to log
$ git stash drop stash@{0}
$ git stash --keep-index // 保留暂存区改动
$ git stash --include-untracked or -u // 包含未跟踪的文件
$ git stash --all or -a // 包含所有文件,包括被 ignore 的
$ git stash branch <new branchname> // 恢复 stash 时,切换新分支
重写历史提交
$ git commit --amend
$ git commit --amend --no-edit
$ git rebase -i HEAD~3 // 对应可以删除、合并、修改、拆分历史提交节点
$ git reset --soft HEAD~2
// 核武器,遍历所有的提交记录,修改提交信息
$ git filter-branch --tree-filter 'rm -f passwords.txt' HEAD
revert
$ git revert -m 1 HEAD // -m 1 标明哪个父分支是主分支,需要保留的
[master b1d8379] Revert "Merge branch 'topic'"
^M 节点和 C6 节点的内容是完全相同的,就好像 merge 操作从来没有发生。如果这时有人基于 C4 节点提交了 C7,我们去 merge topic 时,由于 master 中已经存在了 C3 和 C4 节点,因此,C3 和 C4 的提交会丢失
$ git merge topic
Already up-to-date.
如果我们想要保留 C3 和 C4 节点,需要再 revert 回去
$ git revert ^M
[master 09f0126] Revert "Revert "Merge branch 'topic'""
$ git merge topic
.git 目录结构
$ ls -F1
config
description
HEAD
hooks/
info/
objects/
refs/
底层命令
$ git cat-file -p master^{tree}
100644 blob a906cb2a4a904a152e80877d4088654daad0c859 README
100644 blob 8f94139338f9404f26296befa88755fc2598c289 Rakefile
040000 tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0 lib
Passing -p
to cat-file
instructs the command to first figure out the type of content, then display it appropriately. The master^{tree}
syntax specifies the tree object that is pointed to by the last commit on your master
branch. Notice that the lib
subdirectory isn’t a blob but a pointer to another tree:
$ git cat-file -p 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0
100644 blob 47c6340d6459e05787f644c2447d2595f5d3a54b simplegit.rb
You can have Git tell you the object type of any object in Git, given its SHA-1 key, with git cat-file -t
:
$ git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
blob
git symbolic-ref
. You can read the value of your HEAD via this command:
$ git symbolic-ref HEAD
refs/heads/master
You can also set the value of HEAD using the same command:
$ git symbolic-ref HEAD refs/heads/test
$ cat .git/HEAD
ref: refs/heads/test
$ git update-ref refs/tags/v1.0 cac0cab538b970a37ea1e769cbbde608743bc96d
Occasionally Git packs up several of these objects into a single binary file called a “packfile” in order to save space and be more efficient. Git does this if you have too many loose objects around, if you run the git gc
command manually, or if you push to a remote server.
$ git verify-pack -v .git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.idx
数据恢复
Git 会自动运行 "auto gc",这个指令会完成以下操作:
- 收集松散对象并压缩成 packfiles
- 将多个小的 packfiles 压缩成一个大的 packfile
- 移除若干星期前的悬空对象
$ cat .git/packed-refs
# pack-refs with: peeled fully-peeled
cac0cab538b970a37ea1e769cbbde608743bc96d refs/heads/experiment
ab1afef80fac8e34258ff41fc1b867c702daa24b refs/heads/master
cac0cab538b970a37ea1e769cbbde608743bc96d refs/tags/v1.0
9585191f37f7b0fb9444f35a9bf50de191beadc2 refs/tags/v1.1
^1a410efbd13591db07496601ebc7a059dd55cfe9
对于误删除的对象,最便捷的方式时执行 git reflog 命令
$ git reflog
1a410ef HEAD@{0}: reset: moving to 1a410ef
ab1afef HEAD@{1}: commit: Modify repo.rb a bit
484a592 HEAD@{2}: commit: Create repo.rb
如果 .git/logs/ 目录也被误删除,则可以通过 fsck 命令查找悬空对象
$ git branch -D recover-branch
$ rm -Rf .git/logs/
$ git fsck --full
Checking object directories: 100% (256/256), done.
Checking objects: 100% (18/18), done.
dangling blob d670460b4b4aece5915caf5c68d12f560a9fe3e4
dangling commit ab1afef80fac8e34258ff41fc1b867c702daa24b
dangling tree aea790b9a58f6cf6f2804eeac9f0abbe9631e4c9
dangling blob 7108f7ecb345ee9d0084193f147cdad4d2998293