git
0. 掌握git的关键点
0.1 3个区
工作区:.git 的父目录
暂存区:.git/index记录文件元信息 和 .git/objects使用blob记录文件内容
版本库: .git/objects使用commit tree组织blob
0.2 暂存区
它有两部分:
.git/index : 快照概述,包括文件的指纹,文件大小,文件名...
.git/objects : blob
0.2.1 git add
git add 完成对工作区做快照,如工作区有两个文件,两个目录 readme, src/main.c, obj ,
添加到暂存区后,查看 .git/index
$ git ls-files --stage --debug
100644 c200906efd24ec5e783bee7f23b5d7c941b0c12c 0 readme
ctime: 1679136357:783754200
mtime: 1679136373:750504700
dev: 0 ino: 0
uid: 0 gid: 0
size: 4 flags: 0
100644(普通文件,文件权限0644) 58c9bdf9d017fcd178dc8c073cbfcbb7ff240d6c(内容的SHA1值) 0 src/main.c
ctime: 1679136363:909626900
mtime: 1679136369:134322800
dev: 0 ino: 0
uid: 0 gid: 0
size: 4 flags: 0
查看 .git/objects,已经创建了一个blob对象,内容为 readme,src/main.c 的内容(两个文件内容一样),
$ ls .git/objects/
58/ info/ pack/
$ git cat-file -t 58c9bdf9d017fcd178dc8c073cbfcbb7ff240d6c
blob
$ git cat-file -p 58c9bdf9d017fcd178dc8c073cbfcbb7ff240d6c
111
可见暂存区会存放文件内容,只不过blob没有关联的tree和commit,
并且没有子文件的目录obj并没有被记录。
0.2.2 git status
运行 git status 时,会对工作区的文件求SHA1值,与 index 中记录比较,如果SHA1值不同,或文件新增或删除,则报有改变
比如
readme文件内容为 111,将其修改成 222,运行git status后,会报readme被修改,
若改readme为 111,再次运行 git status后,不会报readme被修改。
结论: git status 是比较 文件内容的SHA1值,不在乎文件的元属性修改时间改变。
0.3 版本库
版本库由4个对象构成 blob tree tag commit
他们都是SHA1作为自己的名称,
运行 git commit -m "first commit",git 根据暂存库的描述,在objects下创建commit和 tree对象,并将其和 blob对象关联。
在.git/objects下可以发现多个无明确意义的目录,其下面是一个或多个名称很怪的文件
$ ls .git/objects/
24/ 58/ 75/ 7f/ c2/ info/ pack/
$ ls .git/objects/24/
da758a92d93a001ad8425e2cfd97967b1f5782
这些文件就是对象,
使用 git cat-file -t [SHA1]可以查看对象的类型,
使用 git cat-file -p [SHA1]可以查看对象的内容
SHA1值由目录加文件名拼接得到
如下发现此对象是 commit 对象,且内容包括一个 tree对象和提交信息注释。
$ git cat-file -t 24da758a92d93a001ad8425e2cfd97967b1f5782
commit
$ git cat-file -p 24da758a92d93a001ad8425e2cfd97967b1f5782
tree 7f5e0374b464fab0ad73ee21a2c98934cce8c9bc
author yangxr <yangxr1995@gmail.com> 1679141339 +0800
committer yangxr <yangxr1995@gmail.com> 1679141339 +0800
first commit
使用这个tree对象的SHA1,继续追踪,发现tree的内容为blob对象和另一个tree对象
$ git cat-file -p 7f5e0374b464fab0ad73ee21a2c98934cce8c9bc
100644 blob 58c9bdf9d017fcd178dc8c073cbfcbb7ff240d6c readme
040000 tree 75be4436d22c3e0dfd4eed87629d1f5dbfd4f193 src
查看blob对象,发现blob对象存放内容,但是它自己不知道是谁的内容。
$ git cat-file -p 58c9bdf9d017fcd178dc8c073cbfcbb7ff240d6c
111
如果添加一个文件内容和 readme相同的文件到版本库会怎么样呢?
git是创建两个blob,还是共用一个blob?
添加一个 readme2,内容和readme一样,提交到版本库,用git log可以查看两次commit
$ git log --oneline
a2b4f1a (HEAD -> master) second commit
24da758 first commit
readme 的 blob是知道的为 58c9bdf9d017fcd178dc8c073cbfcbb7ff240d6c,
追踪readme2,从 最近的提交追踪
$ git cat-file -p a2b4
tree 9a64fcf6b554d4a7a8e3012d80714e1ac5a3bf9b
parent 24da758a92d93a001ad8425e2cfd97967b1f5782
author yangxr <yangxr1995@gmail.com> 1679142505 +0800
committer yangxr <yangxr1995@gmail.com> 1679142505 +0800
second commit
$ git cat-file -p 9a64
100644 blob 58c9bdf9d017fcd178dc8c073cbfcbb7ff240d6c readme
100644 blob 58c9bdf9d017fcd178dc8c073cbfcbb7ff240d6c readme2
040000 tree 75be4436d22c3e0dfd4eed87629d1f5dbfd4f193 src
发现readme和 readme2共用一个blob
在追踪时发现 commit还有parent内容,其实这个内容就是上一个commit
$ git cat-file -p 24da
tree 7f5e0374b464fab0ad73ee21a2c98934cce8c9bc
author yangxr <yangxr1995@gmail.com> 1679141339 +0800
committer yangxr <yangxr1995@gmail.com> 1679141339 +0800
first commit
$ git cat-file -p 7f5e
100644 blob 58c9bdf9d017fcd178dc8c073cbfcbb7ff240d6c readme
040000 tree 75be4436d22c3e0dfd4eed87629d1f5dbfd4f193 src
总结
- git commit 时,git会查看暂存区是否有更新,如果有,则按照暂存区的信息创建一个快照
- 版本库中的快照由四种对象组成 blob tree commit tag,他们的关系如下
0.4 引用和分支
前面查看对象时,都是使用其名称也就是SHA1值,非常不方便,为了人更好的使用,git提供引用
相关文件如下
$ ls .git/HEAD
.git/HEAD
$ cat .git/HEAD
ref: refs/heads/master
$ ls .git/refs/
heads/ tags/
$ ls .git/refs/heads/
master
$ cat .git/refs/heads/master
a2b4f1ae30b763c4b30b32a47b2afa4b7ae49d13
说明,.git/refs下的文件记录某个commit对象,.git/HEAD记录某个 .git/refs某个文件
所以通过 .git/refs/ 或 .git/HEAD 都可以得到要找的commit对象,所以要使用该commit时不需要其SHA1,而可以这样做
$ git cat-file -p HEAD
tree 9a64fcf6b554d4a7a8e3012d80714e1ac5a3bf9b
parent 24da758a92d93a001ad8425e2cfd97967b1f5782
author yangxr <yangxr1995@gmail.com> 1679142505 +0800
committer yangxr <yangxr1995@gmail.com> 1679142505 +0800
second commit
$ git cat-file -p $(cat .git/refs/heads/master)
tree 9a64fcf6b554d4a7a8e3012d80714e1ac5a3bf9b
parent 24da758a92d93a001ad8425e2cfd97967b1f5782
author yangxr <yangxr1995@gmail.com> 1679142505 +0800
committer yangxr <yangxr1995@gmail.com> 1679142505 +0800
second commit
$ git cat-file -p master
tree 9a64fcf6b554d4a7a8e3012d80714e1ac5a3bf9b
parent 24da758a92d93a001ad8425e2cfd97967b1f5782
author yangxr <yangxr1995@gmail.com> 1679142505 +0800
committer yangxr <yangxr1995@gmail.com> 1679142505 +0800
second commit
那么HEAD到底记录哪个文件?
首先再创建三个分支(最早的master分支哪里来的?第一次commit时git默认创建的)
$ git branch new-branch1
$ git branch new-branch2
$ git branch new-branch3
当前分支为master时,HEAD指向 master
$ git branch
* master
new-branch1
new-branch2
new-branch3
$ cat .git/HEAD
ref: refs/heads/master
切换分支到new-branch1,HEAD指向new-branch1
$ git checkout new-branch1
Switched to branch 'new-branch1'
$ cat .git/HEAD
ref: refs/heads/new-branch1
现在使用引用或HEAD能方便的查看末端commit,那么前面的commit呢?
首先需要知道自己前面有几个commit,使用git log获得
$ git log --oneline
a2b4f1a (HEAD -> new-branch1, new-branch3, new-branch2, master) second commit
24da758 first commit
使用分支名或HEAD加'^'符号,每加一个'^'向前移动一个commit
$ git cat-file -p master^
tree 7f5e0374b464fab0ad73ee21a2c98934cce8c9bc
author yangxr <yangxr1995@gmail.com> 1679141339 +0800
committer yangxr <yangxr1995@gmail.com> 1679141339 +0800
first commit
$ git cat-file -p HEAD^
tree 7f5e0374b464fab0ad73ee21a2c98934cce8c9bc
author yangxr <yangxr1995@gmail.com> 1679141339 +0800
committer yangxr <yangxr1995@gmail.com> 1679141339 +0800
first commit
结论
- HEAD指向当前分支
- 分支本质是引用,记录末端commit
- 使用'^'得到前面的commit
- tag和branch本质都是引用用于记录commit,不同的是,tag不会随着新增commit而移动,但branch会移动以保证自己指向末端commit
- 还有种重量级tag,本质是对象,存放在objects中,其功能除了能引用commit,还能记录注释信息。
1. 工作区和暂存区
1.1 工作区 -> 暂存区
git add file
运行此命令后,会修改.git/index,添加文件的元信息,在.git/objects/创建blob对象,存储文件内容
1.2 工作区 <- 暂存区
由于.git/objects有blob对象(即使该对象未和commit关联),删除工作区的文件后,也可以恢复
rm file
git restore file
运行此命令后,git会根据文件名在.git/index中找到对应SHA1,再在.git/objects中找到对应的blob,恢复文件
1.3 删除工作区,保留暂存区
rm file
1.4 删除暂存区,保留工作区
git rm --cached file
运行此命令后,git会根据文件名在.git/index中找到对应SHA1,再在.git/objects中找到对应的blob,删除blob和index中元信息
1.5 删除工作区,删除暂存区
git rm -f file
注意工作区的文件也会被删除
1.6 忽略某些文件
如希望忽略所有.o .a文件和隐藏文件,则在工作目录创建 .gitignore文件
$ cat .gitignore
*.[o|a]
a.out
\.*
这时git会默认忽略这些文件,即使显示添加也会报错,提示需要强制操作
$ ls ./
main.c main.o readme
# git 没有略过了main.o
$ git status
On branch master
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
main.c
readme
nothing added to commit but untracked files present (use "git add" to track)
# 添加失败
$ git add ./main.o
The following paths are ignored by one of your .gitignore files:
main.o
hint: Use -f if you really want to add them.
hint: Turn this message off by running
hint: "git config advice.addIgnoredFile false"
1.7 撤销对暂存区的修改
撤销对暂存区的所有修改
git reset
撤销对暂存区中某个文件的修改
git restore --staged file
对暂存区的修改撤销,不会影响工作区
2. 暂存区和版本库
2.1 暂存区 -> 版本库
git commit -m "commit comment"
执行以上命令后,git会根据 .git/index 中文件元信息,创建 commit ,tree对象,并找到相关blob对象,将其关联。
commit对象还会指向上一个commit。
2.1.1 commit 错了,怎么改
比如 git commit -m "xFxx",这个 "xxx" 写错了,
或者提交后暂存区又被修改了,但不想建立新的commit
使用如下命令
git commit --amend
2.2 版本库 -> 暂存区
当提交内容错误或不完整,希望撤销提交,又想不修改工作区的文件。
git reset --soft SHA1
比如有文件 main.c
第一次提交时内容为 111 , commit 为 34dq
第二次提交的内容为 222 , commit 为 qwer
第三次提交的内容为 333 , commit 为 abdc
工作区内容为 444
使用 git reset --soft 34dq,退回到第一次提交后,
使用 git log --oneline,会发现只剩下一个 commit 34dq,
并且 工作区main.c 依旧是 444,
并且 git status 会提示 暂存区已经同步了工作区的修改,待提交
这时可以:
- 如果要提交新的修改
git add main.c // 修改暂存区
git commit -m "second commit" - 如果想恢复main.c
rm main.c
git restore main.c // 因为此时 index中记录的main.c的blob值为第一个commit时的,所以能找到那时的blob
2.2.2 被撤销的commit和相关tree blob还存在吗?
2.3 删除暂存区,保留版本库
git rm --cached file
2.4 删除版本库,保留暂存区
2.5 删除版本库,删除暂存区
cp file file.bak
git rm -f file
git commit --amend
mv file.bak file
注意工作区的file文件也会被删除,所以需要备份
3. 工作区和版本库
3.1 工作区 -> 版本库
工作区无法直接到版本库
3.2 版本库 -> 工作区
运行下面命令,会报告工作区和版本库的差别
git checkout HEAD
如果发现工作区的file文件与版本库中不同,希望改成版本库中的file,则运行
git checkout file
如果希望使用版本库中以前的commit,恢复工作区
git checkout HEAD^
这时 git 会按照该commit修改工作区,并建立你新建一个分支
git chekout HEAD^
运行 git branch,建立了一个临时的引用
$ git branch
* (HEAD detached at 002ec8c)
master
HEAD指向当前commit
$ cat .git/HEAD
002ec8cbec14232648a62032f0000632a7a1fb52
没有建立新分支
$ ls .git/refs/heads/
master
这时可以在此基础上建立新分支,就可以基于老的commit进行开发
$ git switch -c new-branch1
Switched to a new branch 'new-branch1'
$ git branch
master
* new-branch1
如果想在同个分支上,基于老的commit开发,则可以使用 revert
$ git branch
* master
new-branch1
Administrator@DESKTOP-AJ6AG7I MINGW64 ~/Desktop/tmp (master)
$ git log --oneline
10cbc27 (HEAD -> master) thrid commit
9496eaa Revert "second commit"
002ec8c (new-branch1) second commit
c2eef27 first commit
想基于第一个commit进行开发,并且保留后续的commit(即使已被废弃)
git revert HEAD
3.3.1 撤销版本库的commit,并撤销对暂存区的修改,工作区保持不变
git reset --mixed SHA1
仔细分析git reset
撤销暂存区修改
git reset不加参数,撤销对暂存区的修改
撤销commit
当要撤销commit时,使用 git reset --mixed/--soft/--hard SHA1
比如有提交
commit main.c:222 SHA1:eb23 "first commit"
commit main.c:111 SHA1:as1e "second commit"
当前main.c:333
git add main.c, 修改暂存区,保存main.c:333
git reset --mixed SHA1
使用 git reset --mixed as1e,
工作区:main.c:333 工作区保持不变
暂存区:main.c:111 暂存区被修改为第一次提交时的情况
版本库:as1e (HEAD->master) first commit,版本库HEAD和master引用指向第一次提交
git reset --soft SHA1
使用 git reset --soft as1e
工作区:main.c:333 工作区保持不变
暂存区:main.c:333 暂存区保持不变
版本库:as1e(HEAD->master) first commit,版本库HEAD和master引用指向第一次提交
git reset --hard SHA1
使用 git reset --hard as1e
工作区:main.c:111 工作区被修改为第一次提交的情况
暂存区:main.c:111 暂存区被修改为第一次提交的情况
版本库:as1e(HEAD->master) first commit,版本库HEAD和master引用指向第一次提交
版本库的各种操作
合并多个提交
比如有main.c
$ git log --oneline
a5334c7 (HEAD -> master) third commit main.c:333
78ba96f second commit main.c:222
9843b0a first commit main.c:111
我希望将第三次提交和第二次提交合并,
git rebase -i 9843b0a(第一次提交)
进入编辑环境
pick 78ba96f second commit
pick a5334c7 third commit //把pick改为s(squash压缩),保存退出,编辑注释信息
Rebase 9843b0a..a5334c7 onto 9843b0a (2 commands)
Commands:
p, pick = use commit
r, reword = use commit, but edit the commit message
e, edit = use commit, but stop for amending
s, squash = use commit, but meld into previous commit
f, fixup = like "squash", but discard this commit's log message
x, exec = run command (the rest of the line) using shell
b, break = stop here (continue rebase later with 'git rebase --continue')
d, drop = remove commit
l, label
t, reset
m, merge [-C | -c ]
. create a merge commit using the original merge commit's
. message (or the oneline, if no original merge commit was
. specified). Use -c to reword the commit message.
These lines can be re-ordered; they are executed from top to bottom.
If you remove a line here THAT COMMIT WILL BE LOST.
However, if you remove everything, the rebase will be aborted.
完成操作后
$ git log --oneline
133bb24 (HEAD -> master) second thrid commit
9843b0a first commit
查看版本库中main.c:333.
所以合并提交后,版本库使用最后的提交的内容
额外
修改git默认编辑器为vim
编辑 .git/config,在core加入 editor=vim