下面的一些git命令,在使用时常常不知所以,容易混淆,其实原因是对git的机制并不了解,因此在本文中介绍相关。
reset hard/soft/mixed
1
2
3$ git reset --hard
$ git reset --mixed
$ git reset --softfetch/pull
1
2$ git fetch
$ git pullreset/revert
diff cached/HEAD
1
2
3
4
5$ git diff --cached
$ git diff --staged
$ git diff HEAD
$ git diff HEAD~1
$ git diff HEAD^
基本概念图
Git的工作区域
注意,图中的Stage和Index是同一个东西。
下图中,最后一个diff
应该为diff --cached
注意,Git中的branch实际上可以看做是一些列commit的集合。commit是按照repo全局的,这可以最大程度实现复用。
HEAD
HEAD实际上是指向当前分支的指针。观察.git/HEAD
,可以发现
1 | ref: refs/heads/master |
接着打开.git/refs/heads/master
,可以发现
1 | fc30ecc0525b71b0f6bf1ce20fe793aa731ae66c |
执行git log
,可以发现存在指向性关系HEAD -> master -> fc30ecc
1 | git log |
容易想到切换分支指令git checkout
就是通过改变HEAD来实现的。
指针算术
HEAD^1
和HEAD~1
的区别
基础配置
使用vim
1 | git config --global core.editor vim |
支持多个SshKey
在一台机器上使用多个SSH Key
1 | ssh-agent bash -c 'ssh-add ~/.ssh/id_rsa_xxx; git clone git@github.com:yyy.git' |
在提交的时候,需要署名为自己,此时需要
1 | git config user.name xxx |
如果之前提交过了,需要
1 | git commit --amend --author "xxx <xxx@yyy.com>" |
checkout相关
切换分支使用git checkout
指令。加上-b
参数可以基于当前分支创建新分支并切换,相当于整合了git branch
命令。
checkout的指令和reset指令有些类似
我们还可以指定某个文件进行checkout,其中-q
表示quiet。
1 | git checkout [-q] [--] <paths> |
如果我们需要把Index/Stage里面的东西checkout到工作目录中,可以
1 | git checkout-index -a -f |
其中-a
表示全部文件,-f
表示会强制覆盖已存在的文件。可以看出git对所有涉及变动本地目录的操作都很谨慎。
加上-- <path>
就可以checkout单个的文件。需要注意的是,现在checkout就可以直接从Stage/Index检出单个文件了。
1 | git checkout -- 1.txt |
fetch相关
拉取所有分支信息
1 | git fetch |
rebase相关
merge相关
git merge用法
几种merge策略
Git使用的是三路合并,分别是要合并的两方a和b,和这两方的共同祖先c。通过和共同祖先比较,能够自动解决一些冲突,原因如下:
- 假如a对c中的某个文件1.txt进行了修改,而b没有。如果直接合并a和b,我们并不知道是否该采用这个修改;
- 但如果我们参照c去合并a和b,就可以发现b对c上的1.txt并没有修改,所以应该接受a对1.txt的修改。
Recursive
Recursive是默认策略,这种策略只能同时合并两个分支,如果需要同时合并多个分支,就需要反复进行两两合并。这里反复有点奇怪,难道不是n-1
次么?接着往下看。
显而易见,在合并时,我们序号回溯两个分支A和B的共同祖先,从而确定解决冲突的起点。但在Git中,两个分支可能存在有多个共同祖先,即Criss-Cross现象。我们考虑下面的操作方式:
- 在master上提交c0
- 在master上checkout出分支feature1,并在feature1上提交c1
- 在master上提交c2
- 在master上checkout出分支feature2
- 在feature2上merge分支feature1,产生提交c3
此时feature2(指向c3)的祖先是master(指向c2)和feature1(指向c1) - 在master上提交c4
- 将feature1合并到master,产生提交c5
此时会导致Criss-Cross现象。我们来分析一下合并之后的情况:- feature1目前指向c1,c1的祖先是c0。
- feature2目前指向c3,它的祖先是c2和c1。
- master目前指向c5,它的祖先是c4和c1。由于c4的祖先是c2,所以feature2和master有两个共同祖先c1和c2。
下面,我们尝试合并master和feature2。Recursive策略是,首先合并master和feature2的共同祖先,即c1和c2,得到一个虚拟祖先,然后在进行合并。
我们还可以指定不同的diff-algorithm
1 | git merge origin/master -s recursive -X diff-algorithm=patience |
例如patience
策略就能够产生更加优雅的合并结果,例如更好地匹配大括号。
Resolve
Resolve策略是Recursive出现之前旧的合并策略。
Ours
Octopus
这个策略能够同时合并多个分支,但是如果出现需要手工解决的冲突,就会失败。
diff相关
实验:有关diff
我们进行如下操作:
- init仓库,新建文件1.txt
- 变更1.txt的内容为2,并add+commit
- 变更1.txt的内容为3,并add,不commit
- 变更1.txt的内容为4,不做任何操作
下面进行检查:
git diff --cached
比较Index和HEAD1
2
3
4
5
6
7
8
9
10$ git diff --cached
diff --git a/1.txt b/1.txt
index d8263ee..e440e5c 100644
--- a/1.txt
+++ b/1.txt
@@ -1 +1 @@
-2
\ No newline at end of file
+3
\ No newline at end of filegit diff
比较working directory和Index1
2
3
4
5
6
7
8
9
10$ git diff
diff --git a/1.txt b/1.txt
index e440e5c..bf0d87a 100644
--- a/1.txt
+++ b/1.txt
@@ -1 +1 @@
-3
\ No newline at end of file
+4
\ No newline at end of filegit diff HEAD
比较working directory和HEAD1
2
3
4
5
6
7
8
9
10$ git diff HEAD
diff --git a/1.txt b/1.txt
index d8263ee..bf0d87a 100644
--- a/1.txt
+++ b/1.txt
@@ -1 +1 @@
-2
\ No newline at end of file
+4
\ No newline at end of filegit diff HEAD --cached
比较Index和HEAD1
2
3
4
5
6
7
8
9
10$ git diff HEAD --cached
diff --git a/1.txt b/1.txt
index d8263ee..e440e5c 100644
--- a/1.txt
+++ b/1.txt
@@ -1 +1 @@
-2
\ No newline at end of file
+3
\ No newline at end of filegit diff/apply
将HEAD到working的修改输出到patch文件。然后我们回退到HEAD并apply发现会恢复到working中的结果。当然,reset会导致Index区被清空?1
2
3$ git diff HEAD > patch
$ git reset --hard
$ git apply patch那么我们是否可以用HEAD到working,发现会报错
1
2
3
4
5$ git diff HEAD > patch
$ git reset --mixed
$ git apply patch
error: patch failed: 1.txt:1
error: 1.txt: patch does not apply
reset相关
git reset命令修改branch,即指向最新commit的指针HEAD,并不修改任何commit。reset可以带有三个参数:
--hard
参数会改变HEAD、index(stage)和working directory--mixed
参数会改变HEAD、index(stage)--soft
参数只会改变HEAD
常见用法
我有一个文件被git add了,现在我想要从staging里面移出这个修改,怎么做?
下面这个操作能够将它移出staging1
git reset --mixed file
注意,我们通常喜欢使用
git rm --cached
,但是他未必是万精油,例如你可能遇到这个错误1
2error: the following file has staged content different from both the
file and the HEAD:现在我想在working dir中也取消这个修改,怎么做?
git reset --hard
似乎不能对单个文件使用。
这里可以1
git checkout file
实验:有关reset
Step 1
本实验说明soft reset不会改变working directory。
首先我们在空目录执行
1
git init
我们创建a.txt,并且填写其内容为
1
我们执行
1
2
3git add .
git commit -m"a=1"
git log得到输出
1
2
3
4
5commit 6bd3de7a2b1637dcb686f72af415c5d48f4d5dc2 (HEAD -> master)
Author: Calvin Neo <calvinneo1995@gmail.com>
Date: Fri Oct 30 22:46:37 2020 +0800
a=1修改a.txt的内容为
2
执行
1
git reset --soft
查看a.txt的内容为
2
这说明a.txt没有被reset掉
Step 2
本实验说明soft reset不会改变index
此时,a.txt的内容仍然为2
执行
1
git diff --cached a.txt
可以看到,a.txt已经被提交到了index里面
1
2
3
4
5
6
7
8
9diff --git a/a.txt b/a.txt
index 56a6051..d8263ee 100644
--- a/a.txt
+++ b/a.txt
@@ -1 +1 @@
-1
\ No newline at end of file
+2
\ No newline at end of file执行
1
git reset --soft
检查a.txt的内容仍然为2
Step 3
本实验说明soft reset能改变Repo
此时a.txt的内容仍然为2,执行
1
git commit -m"a=2"
检查git log
可以看到,a=2
已经进入了Repo1
2
3
4
5
6
7
8
9
10
11commit c0a4d93cb05eb39e149ff50d9b7e54257a51234b (HEAD -> master)
Author: Calvin Neo <calvinneo1995@gmail.com>
Date: Fri Oct 30 23:03:53 2020 +0800
a=2
commit 6bd3de7a2b1637dcb686f72af415c5d48f4d5dc2
Author: Calvin Neo <calvinneo1995@gmail.com>
Date: Fri Oct 30 22:46:37 2020 +0800
a=1执行
HEAD~1
表示HEAD
向前一个版本。1
git reset --soft HEAD~1
检查git log
发现a=2
的提交被回退了1
2
3
4
5commit 6bd3de7a2b1637dcb686f72af415c5d48f4d5dc2 (HEAD -> master)
Author: Calvin Neo <calvinneo1995@gmail.com>
Date: Fri Oct 30 22:46:37 2020 +0800
a=1检查a.txt
发现内容还是2,没有变。说明即使回退了Repo,也不会改变工作区。执行
1
git checkout
检查a.txt
发现值内容还是2,这个和图2似乎有矛盾。其实应该要加一个-f
1
git checkout -f
bisect相关
实验:有关bisect
Linux内核易于维护的一个原因就是因为Linus要求每一个commit只做一件事,所以他能够通过git bisect
快速地二分出错误的提交。
执行下面语句,得到10个提交
1 | git init |
我们的目标是找到第一个打印出大于等于5的错误提交。
我们写一个predicate脚本
1 | # test.sh |
执行下面语句,自动查找到第一个故障提交
1 | git bisect start |
下面执行git bisect run
,可以得到以下输出
1 | $ git bisect run ./test.sh |
需要注意,如果某个分支不能被test,应当使用git bisect skip来跳过,或者在bash脚本里面返回125
实验:如果涉及merge呢
其实我们还是在一条线上bisect
1 | echo 1 > a |
Pull Request相关
基础
Fork项目A到自己的Repo,并Clone自己的Repo到本地
在本地设置项目A为一个叫upstream的remote,并且fetch
1
2git remote add upstream A
git fetch upstream从项目A中创建一个新分支
非常不推荐在本地的master上修改,这样的坏处是没有办法及时同步远程分支到本地的master。
类似从origin远程分支的方案1
git checkout -b issueXXXX upstream/master
修改
将某个分支和upstream同步
这个通常发生在我们的开发分支me在修改过程中,我们的PR对象upstream/master也在修改。在PR之前,我们可能需要解决冲突。
此时,可以先获取upstream/master到本地
1 | git fetch upstream |
然后将me基于master做rebase
1 | git checkout me |
修改PR
- stash多个commit
- git commit –amend
取消某个历史提交
git revert
将某个分支中的某几个提交应用到另一个分支上
git cherry-pick
git submodule
修改目录
有的时候,我们需要将某个依赖的组件更改为自己的版本。此时可以尝试
1.更新.gitsubmodule
中对应submodule
注意,可能还需要修改对应的branch
1.更新.git/config
中对应submodule
1.执行git submodule sync
注意,如果需要修改远程分支的,那么下面的这个目录也要提交,虽然看起来是空的,但它涉及.git/modules/contrib/yyy
的一个commit id。
1 | $ git status |
git am
git clean
有的时候我们需要checkout,但是提示有untraced files。可以通过下面的命令删除掉
1 | git clean -xdf |