Git撤销与合并
1. git init
创建一个空的git repo,也就是创建一个.git的子目录,这个目录包含了几乎所有git存储和操作的东西。新初始化的.git目录的典型结构如下:
.git $ ls -l
total 24
-rw-r--r-- 1 a123 staff 23 3 22 20:57 HEAD
-rw-r--r-- 1 a123 staff 137 3 22 20:57 config
-rw-r--r-- 1 a123 staff 73 3 22 20:57 description
drwxr-xr-x 13 a123 staff 416 3 22 20:57 hooks
drwxr-xr-x 3 a123 staff 96 3 22 20:57 info
drwxr-xr-x 4 a123 staff 128 3 22 20:57 objects
drwxr-xr-x 4 a123 staff 128 3 22 20:57 refs
description文件仅供git web程序使用,平常无需关心。
config文件包含项目特有的配置选项。
info目录包含一个全局性排除文件,用以放置那些不希望被记录在.gitignore文件中的忽略模式。
hooks目录包含客户端或服务端的钩子脚本。
HEAD文件指向目前被检出的分支。
index文件(尚待创建)保存暂存区信息。
objects目录存储所有数据内容。
refs目录存储指向数据的提交对象的指针。
git的默认分支名字是master,git init时默认创建它。
myproject $ cat .git/HEAD
ref: refs/heads/master
myproject $ git status
位于分支 master
尚无提交
无文件要提交(创建/拷贝文件并使用 "git add" 建立跟踪
2. git的三种状态,以及工作区(Working directory),暂存区(Index),HEAD
Git 有三种状态,你的文件可能处于其中之一:已修改(modified)、已暂存(staged)和已提交(committed)
基于刚才init的git project,做一些改动。
myproject $ touch file1.txt
myproject $ git status
位于分支 master
尚无提交
未跟踪的文件:
(使用 "git add <文件>..." 以包含要提交的内容)
file1.txt
提交为空,但是存在尚未跟踪的文件(使用 "git add" 建立跟踪)
myproject $ ls .git/
HEAD description info refs
config hooks objects
会看到在git add之后,.git下面多了一个index文件。
myproject $ git add file1.txt
myproject $ ls .git/
HEAD description index objects
config hooks info refs
这时候,所做的改动就处于已暂存状态,体现在index文件中。
可以利用以下命令查看git缓存了的内容。
myproject $ git ls-files --stage
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 file1.txt
同时,.git/objects下面多了一个子文件夹,并生成了一个新文件。这个新文件就对应了刚才所做的改动。这就是git存储内容的方式--一个文件对应一条内容,以该内容加上特定头部信息一起的SHA-1校验和作为文件名。校验和的前两个字符用于命名子目录,余下的38个字符则作为文件名。后面会详叙。
myproject $ ls .git/objects
e6 info pack
myproject $ ls .git/objects/e6
9de29bb2d1d6434b8b29ae775ad8c2e48c5391
可以通过cat-file命令从git那里查看存储的内容。
git cat-file -p e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
myproject $ git cat-file -p e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
由于file1.txt的内容为空,所以这里显示为空。
这时候可以往file1.txt里添加一些内容,并git add。可以看到.git/objects又多了一个object。
myproject $ vim file1.txt
myproject $ git add file1.txt
myproject $ find .git/objects
.git/objects
.git/objects/pack
.git/objects/info
.git/objects/5e
.git/objects/5e/1187c8883693e5657056087bb5f105337fa0d6
.git/objects/e6
.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
查看这个新的对象的内容以及类型。会发现它是一个blob对象。
myproject $ git cat-file -p 5e1187c8883693e5657056087bb5f105337fa0d6
this is line1.
myproject $ git cat-file -t 5e1187c8883693e5657056087bb5f105337fa0d6
blob
接下来commit这个change。
myProject $ git commit -m "first commit"
myproject $ git commit -m "first commit"
myproject $ git log
commit ec1ddc2cd64f33c13bd376e695987e88a0d35957 (HEAD -> master)
Author: stack <a123@123deMacBook-Pro.local>
Date: Sun Mar 22 21:11:57 2020 +0800
first commit
查看这个commit 对象的类型以及内容,commit的tree对象所指向的内容, 我们会发现,这个tree指向的是一个blob,而这个blob的内容,就是我们刚刚做过改动的文件。
myproject $ git cat-file -t ec1ddc2cd64f33c13bd376e695987e88a0d35957
commit
myproject $ git cat-file -p ec1ddc2cd64f33c13bd376e695987e88a0d35957
tree 4a0475f1371a1df3a7c1ee5d772e782abd072271
author stack <a123@123deMacBook-Pro.local> 1584882717 +0800
committer stack <a123@123deMacBook-Pro.local> 1584882717 +0800
first commit
myproject $ git cat-file -p 4a0475f1371a1df3a7c1ee5d772e782abd072271
100644 blob 5e1187c8883693e5657056087bb5f105337fa0d6 file1.txt
myproject $ git cat-file -t 4a0475f1371a1df3a7c1ee5d772e782abd072271
tree
myproject $ git cat-file -p 5e1187c8883693e5657056087bb5f105337fa0d6
this is line1.
同时,我们查看一下暂存区的内容:
myproject $ git ls-files --stage
100644 5e1187c8883693e5657056087bb5f105337fa0d6 0 file1.txt
会发现,暂存区指向的也是同样的blob对象。
至此,一个commit就提交了,工作区,暂存区,以及head又指向了同样的内容。
它们更新内容的顺序为,工作区->暂存区->head
3. git reset
将做过的change撤销掉,就像没有发生过一样。
git reset 应用的顺序为 head->暂存区->工作区。
(1) git reset --soft
当前,git的状态如下。
myproject $ git log
commit 2e62d3728e289f0c7270ac728b00f036e1c1edff (HEAD -> master)
Author: stack <a123@123deMacBook-Pro.local>
Date: Sun Mar 22 21:23:35 2020 +0800
third commit
commit ee31138368e8994f702902e21edd7d3f46c1a815
Author: stack <a123@123deMacBook-Pro.local>
Date: Sun Mar 22 21:23:13 2020 +0800
second commit
commit ec1ddc2cd64f33c13bd376e695987e88a0d35957
Author: stack <a123@123deMacBook-Pro.local>
Date: Sun Mar 22 21:11:57 2020 +0800
first commit
head指向的内容为:
(head是当前分支引用的指针,总是指向该分支上的最后一次提交。)
myproject $ git cat-file -p HEAD
tree cc90f92f82e86fa02c6dbdf0fe7f6b1b85abdd6c
parent ee31138368e8994f702902e21edd7d3f46c1a815
author stack <a123@123deMacBook-Pro.local> 1584883415 +0800
committer stack <a123@123deMacBook-Pro.local> 1584883415 +0800
third commit
myproject $ git ls-tree -r HEAD
100644 blob 3234a5d219806f3c2b72e2ca4d84c75a636cfcd1 file1.txt
index指向的内容为:
(索引是你的预期的下一个提交)
myproject $ git ls-files --stage
100644 3234a5d219806f3c2b72e2ca4d84c75a636cfcd1 0 file1.txt
我们来进行一次reset。(移动HEAD, --soft)
myproject $ git reset --soft HEAD~
myproject $ git ls-tree -r HEAD
100644 blob 9381eaa3762a236ddfb7ace54dd45e508ff55943 file1.txt
myproject $ git ls-files --stage
100644 3234a5d219806f3c2b72e2ca4d84c75a636cfcd1 0 file1.txt
myproject $ git log
commit ee31138368e8994f702902e21edd7d3f46c1a815 (HEAD -> master)
Author: stack <a123@123deMacBook-Pro.local>
Date: Sun Mar 22 21:23:13 2020 +0800
second commit
commit ec1ddc2cd64f33c13bd376e695987e88a0d35957
Author: stack <a123@123deMacBook-Pro.local>
Date: Sun Mar 22 21:11:57 2020 +0800
first commit
--soft将仅仅移动HEAD的指向,而并不会移动index以及工作区。
HEAD指的是HEAD的父节点。HEAD是父节点的父节点,也可以写成HEAD2.
所以这个命令本质上是撤销了上一次git commit命令。
(2) git reset --mixed
接下来,再通过reset来更新索引。(--mixed,默认行为)
myproject $ git ls-tree -r HEAD
100644 blob 9381eaa3762a236ddfb7ace54dd45e508ff55943 file1.txt
myproject $ git ls-files --stage
100644 3234a5d219806f3c2b72e2ca4d84c75a636cfcd1 0 file1.txt
myproject $ git reset --mixed HEAD~
重置后取消暂存的变更:
M file1.txt
myproject $ git ls-tree -r HEAD
100644 blob 5e1187c8883693e5657056087bb5f105337fa0d6 file1.txt
myproject $ git ls-files --stage
100644 5e1187c8883693e5657056087bb5f105337fa0d6 0 file1.txt
(3) git reset --hard
reset更新工作目录(--hard)
git reset --hard HEAD~
--hard标记是reset命令的危险用法,它也是git会真正销毁数据的几个操作之一。
如果这个commit已经被推送到远端,可以用这个命令使远端也回退到相应的版本。
git push origin <branch> --force
4. git revert
将做过的change撤销掉,通过“反做”某一个版本,用一个新的commit来消除做过的change。
当前git的状态:
myproject $ git log
commit c216d922fd1cc851502bd5a1ed7e92200cae75be (HEAD -> master)
Author: stack <a123@123deMacBook-Pro.local>
Date: Sun Mar 22 21:38:59 2020 +0800
third commit
commit 0bb69921280911ec4a704ce6efcfd3ab1b11425a
Author: stack <a123@123deMacBook-Pro.local>
Date: Sun Mar 22 21:38:32 2020 +0800
second commit
commit af58830f47a7c260dcef186250429e33dcd43715
Author: stack <a123@123deMacBook-Pro.local>
Date: Sun Mar 22 21:37:56 2020 +0800
first commit
revert其中一个commit:
myproject $ git revert -n 0bb69921280911ec4a704ce6efcfd3ab1b11425a
myproject $ git status
位于分支 master
您在执行反转提交 0bb6992 的操作。
(所有冲突已解决:运行 "git revert --continue")
(使用 "git revert --abort" 以取消反转提交操作)
要提交的变更:
(使用 "git reset HEAD <文件>..." 以取消暂存)
删除: file2.txt
myproject $ git commit -m "revert second commit"
再来看,多了一个commit,也就是用来revert的commit:
yproject $ git log --oneline
0bd4d1c (HEAD -> master) revert second commit
c216d92 third commit
0bb6992 second commit
af58830 first commit
而若是想要revert某个版本,但是在这个版本后又做过change,则在revert的过程中可能出现冲突,则需要解决冲突之后再提交。
5. git merge 与git rebase
先来讲讲git merge。
当前master 和 dev branch:
myproject $ git log --graph --all --oneline
* 3c3801f (HEAD -> dev) change file2 on dev
| * 79b700e (master) change file1 on master
|/
* 50b905c first commit
接下来打算将dev的工作并入master分支。
myproject $ git checkout master
切换到分支 'master'
myproject $ git merge dev
Merge made by the 'recursive' strategy.
file2.txt | 1 +
1 file changed, 1 insertion(+)
myproject $ git status
位于分支 master
无文件要提交,干净的工作区
myproject $ git log --graph --all --oneline
* 413dabe (HEAD -> master) Merge branch 'dev'
|\
| * 3c3801f (dev) change file2 on dev
* | 79b700e change file1 on master
|/
* 50b905c first commit
另外,还想将master的工作也并入dev。
git merge之后,会发现dev branch指向了与master相同的commit:
myproject $ git checkout dev
切换到分支 'dev'
myproject $ git merge master
更新 3c3801f..413dabe
Fast-forward
file1.txt | 1 +
1 file changed, 1 insertion(+)
myproject $ git log --graph --all --oneline
* 413dabe (HEAD -> dev, master) Merge branch 'dev'
|\
| * 3c3801f change file2 on dev
* | 79b700e change file1 on master
|/
* 50b905c first commit
所以,git merge是把两个分支的最新快照,以及两者最近的共同祖先进行三方合并,合并的结果是生成一个新的快照。
接下来,用git rebase来合并分支。
当前的git状态
myproject $ git log --graph --all --oneline -n 3
* 5441022 (dev) change file2 again on dev
| * b727be3 (HEAD -> master) change file1 again on master
|/
* 413dabe Merge branch 'dev'
|\
此时,采用git rebase,将dev的工作并入到master。
myproject $ git branch
dev
* master
myproject $ git rebase dev
首先,回退头指针以便在其上重放您的工作...
应用:change file1 again on master
myproject $ git log --graph --all --oneline -n 3
* 4dd73d6 (HEAD -> master) change file1 again on master
* 5441022 (dev) change file2 again on dev
* 413dabe Merge branch 'dev'
|\
当在master branch上执行git rebase dev的时候,实际发生的事情是,找到master和dev两个分支的最近共同祖先,对比当前分支(master分支)相对于该祖先的历次提交,提取相应的修改并存为临时文件,然后将master分支指向目标基底(dev的head指向的commit),最后以此将之前另存为临时文件的修改依序应用。
可以看到,rebase使得提交历史更加整洁。尽管实际的开发工作是并行在不同branch上进行的,但是它们看上去就像是串行的一样,提交历史是一条直线没有分叉。
因此,变基是将一系列提交按照原有次序依次应用到另一分支上,而合并是把最终结果合在一起。这两种方式,整合的最终结果所指向的快照始终是一样的,只不过提交历史不同。