查看本地分支与远程分支的对应关系
1 | 查看远程分支与本地分支的对应关系 |
新建远程分支 | push
1 | git checkout <local-branch> |
Tip: push
的时候只会上传当前的 branch
的指向,并不会把本地的 HEAD
的指向也一起上传到远程仓库。事实上,远程仓库的 HEAD
是永远指向它的默认分支(即 master,如果不修改它的名称的话),并会随着默认分支的移动而移动的。
--set-upstream
设置远程分支与本地分支关联
1 | 关联远程仓库的 master 分支与本地的 master 分支,该方法在 push 中可设置,如果要直接绑定分支但不 push 呢,见下面命令行 |
拉取远程分支 | pull
1 | 说明:关联后 (--set-upstream) 可直接 push |
根据远程分支上创建本地分支
1 | 只有远程分支的时候,根据远程分支创建本地分支即可 |
删除远程分支
1 | 说明:git push origin <local-branch>/<remote-branch> |
区分 git reset
& git checkout
⭐Reference: https://blog.csdn.net/longintchar/article/details/82314102
git reset
会带着当前指向的 branch 一起向前推进git checkout
只会修改当前 HEAD 的指向,可先用 checkout 跳转到某次提交处,然后创建某个分支 feature(此时的 HEAD 并未指向 feature1,而是仅仅处于同一处 commit),最后再使用 checkout 切换到该 feature 分支上(即 HEAD 指向 feature 分支)
checkout
签出动作会将 HEAD 与 branch 分离开来,它有一个专门的参数用于让 HEAD 与 branch 脱离且不移动 HEAD 的用法:
1 | git checkout --detach |
git 各式后悔药
git 管理仓库时,往往需要撤销某些操作/提交/暂存内容。
1. 撤销工作区的文件修改
如果工作区的某个文件被改乱了,但还没有执行 git add
,可以用 git checkout
命令找回本次修改之前的文件。
1 | $ git checkout -- [filename] |
⭐它的原理是先找暂存区,如果该文件有暂存的版本,则恢复该版本,否则恢复上一次提交的版本。
注意:工作区的文件变化一旦被撤销,就无法找回了。
2. 从暂存区撤销文件
如果不小心把一个文件添加到暂存区,可以用下面的命令撤销。
1 | $ git rm --cached [filename] |
上面的命令不影响已经提交的内容。
3. 替换上一次提交信息
💔情况一:提交以后,发现提交信息写错了,这时可以使用 git commit
命令的 --amend
参数,可以修改上一次的提交信息。
1 | $ git commit --amend -m "Fixes bug #42" |
它的原理是产生一个新的提交对象,替换掉上一次提交产生的提交对象。
💔情况二:提交之后,发现提交的文件需要修改!这时先修改好工作区,然后再执行 add
后执行 git commit --amend
,因为这时暂存区有发生变化的文件,会一起提交到仓库。
1 | git commit --amend -m "append info" |
所以,--amend
不仅可以修改提交信息,还可以整个把上一次提交替换掉。
4. 撤销某次提交 | 但需新增来覆盖提交
一种常见的场景是,提交代码以后,你突然意识到这个提交有问题,应该撤销掉,这时执行下面的命令就可以了。
1 | $ git revert HEAD |
上面命令的原理是,在当前提交后面,⭐新增一次提交(commit+1),抵消掉上一次提交导致的所有变化(workspace&stage change)。它不会改变过去的历史,所以是首选方式,没有任何丢失代码的风险。
git revert
命令只能抵消上一个提交,如果想抵消多个提交,必须在命令行依次指定这些提交。比如,抵消前两个提交,要像下面这样写。
1 | $ git revert [倒数第一个提交] [倒数第二个提交] |
git revert
命令还有两个参数。
--no-edit
:执行时不打开默认编辑器,直接使用 Git 自动生成的提交信息。--no-commit
:只抵消暂存区(stage)和工作区的文件变化,不产生新的提交。
5. 丢弃提交 | 回溯
如果希望以前的提交在历史中彻底消失,而不是被抵消掉,可以使用git reset
命令,丢弃掉某个提交之后的所有提交。
1 | $ git reset [last good SHA] |
git reset
的原理是,让最新提交的指针回到以前某个时点,该时点之后的提交都从历史中消失。
默认情况下,git reset
不改变工作区的文件(但会改变暂存区),--hard
参数可以让工作区里面的文件也回到以前的状态。
1 | $ git reset --hard [last good SHA] |
执行 git reset
命令之后,如果想找回那些丢弃掉的提交,可以使用 git reflog
命令,具体做法参考这里 。不过,这种做法有时效性,时间长了可能找不回来。
6. 撤销当前分支的变化
你在当前 error 分支上做了几次提交,突然发现放错了分支,这几个提交本应该放到 master 分支。
1 | # 将 error 分支上的<最新一次>提交转移到 master 分支 |
目标:将 error 分支上的最新两次 commit 转移到 master 分支
先执行
git checkout master
再执行
git cherry-pick [SHA1] [SHA2]
有冲突解决冲突,没冲突即可完成
OK!error 分支想要 reset 就 reset~
合并分支 | merge
由于现在 Git 仓库处于冲突待解决的中间状态(已执行 merge
操作),所以如果你最终决定放弃这次 merge
,也需要执行一次 merge --abort
来手动取消它。
1 | 回到 merge 前的状态 |
主流工作流 Feature Branching
这种工作流的核心内容可以总结为两点:
- 任何新的功能(feature)或 bug 修复全都新建一个
branch
来写; branch
写完后,合并到master
,然后删掉这个branch
。
这就是这种工作流最基本的模型。
从上面的动图来看,这种工作流似乎没什么特别之处。但实质上,Feature Branching 这种工作流,为团队开发时两个关键的问题提供了解决方案:
- 代码分享
- 一人多任务
1. 代码分享
1 | 本地电脑 |
借助 GitHub 的 Pull Request 简化 Feature Branching 工作流
事实上,上面所说的这个流程,还可以利用 Pull Request 来进一步简化。
Pull Request 并不是 Git 的内容,而是一些 Git 仓库服务提供方(例如 GitHub)所提供的一种便捷功能,它可以让团队的成员方便地讨论一个 branch
,并在讨论结束后一键合并这个 branch
到 master
。
同样是把写好的 branch
给同事看,使用 Pull Request 的话你可以这样做:
- 把
branch
push
到中央仓库; - 在中央仓库处创建一个 Pull Request。以 GitHub 为例:
然后你的同事就可以在 GitHub 上看到你创建的 Pull Request 了。他们可以在 GitHub 的这个页面查看你的 commit
s,也可以给你评论表示赞同或提意见,你接下来也可以根据他们的意见把新的 commit
s push
上来,这也页面会随着你新的 push
而展示出最新的 commits
。
在讨论结束以后,你们一致认为这个 branch
可以合并了,你只需要点一下页面中那个绿色的 “Merge pull request” 按钮,GitHub 就会自动地在中央仓库帮你把 branch
合并到 master
了:
然后你只要在本地 pull
一下,把最新的内容拉到你的电脑上,这件事情就算完成了。
另外,GitHub 还设计了一个贴心的 “Delete branch” 按钮,方便你在合并之后一键删除 branch
。
完整的例子:
1 | 拉取 feature2 分支 |
2. 一人多任务
你正在认真写着代码,忽然同事过来跟你说:「内个……你这个功能先放一放吧,我们最新讨论出要做另一个更重要的功能,你来做一下吧。」
如果你是在独立的 branch
上做事,切换任务是很简单的。你只要稍微把目前未提交的代码简单收尾一下,然后做一个带有「未完成」标记的提交(例如,在提交信息里标上「TODO」),然后回到 master
去创建一个新的 branch
就好了。
1 | 切换回 master 主分支!!!因为需要从主分支上创建分支 |
查改历史改动记录
git log
1 | 在 .bashrc 中设置 git-log 别名,可图形化输出 git log 的信息 |
git show
1 | 查看某个具体的 commit: $ git show 查看当前 commit |
git diff
比对本地工作目录与暂存区的内容(即显示未使用 git add
加入暂存区的内容与暂存区的内容的不同之处)
1 | git diff |
比对暂存区与上一条提交的内容(即显示 git add
后的内容与上次 commit
之间内容的不同之处)
1 | 二者命令完全等价 |
比对工作目录和上一条提交的内容:
使用 git diff HEAD
可以显示工作目录和上一条提交之间的不同,它是上面这二者的内容相加。换句话说,这条指令可以让你看到「如果你现在把所有文件都 add
然后 git commit
,你将会提交什么」(不过需要注意,没有被 Git 记录在案的文件(即从来没有被 add 过 的文件,untracked files 并不会显示出来。为什么?因为对 Git 来说它并不存在啊)。
1 | git diff HEAD |
实质上,如果你把 HEAD
换成别的 commit
,也可以显示当前工作目录和这条 commit
的区别。
不喜欢 merge 的分叉,那就用 rebase 吧
rebase —— 变基?!
其实这个翻译还是比较准确的。rebase
的意思是,给你的 commit
序列重新设置基础点(也就是父 commit
)。展开来说就是,把你指定的 commit
以及它所在的 commit
串,以指定的目标 commit
为基础,依次重新提交一次。例如下面这个 merge
:
1 | merge 过来;rebase 过去 |
master: 1-2-3-4-7
branch1: 1-2-5-6-7
如果把 merge
换成 rebase
,可以这样操作:
1 | git checkout branch1 |
master: 1-2-3-4-7-8
branch1: 1-2-5-6
master 上的 7、8 是 branch1 上的 5、6 rebase 过去的~
可以看出,通过 rebase
,5
和 6
两条 commit
s 把基础点从 2
换成了 4
。通过这样的方式,就让本来分叉了的提交历史重新回到了一条线。这种「重新设置基础点」的操作,就是 rebase
的含义。
另外,在 rebase
之后,记得切回 master
再 merge
一下,把 master
移到最新的 commit
:
1 | git checkout master |
master/branch1: 1-2-3-4-7-8
为什么要从
branch1
来rebase
,然后再切回master
再merge
一下这么麻烦,而不是直接在master
上执行rebase
?从图中可以看出,
rebase
后的commit
虽然内容和rebase
之前相同,但它们已经是不同的commits
了。如果直接从master
执行rebase
的话,就会是下面这样:
这就导致
master
上之前的两个最新commit
被剔除了。如果这两个commit
之前已经在中央仓库存在,这就会导致没法push
了:
所以,为了避免和远端仓库发生冲突,一般不要从
master
向其他branch
执行rebase
操作。而如果是master
以外的branch
之间的rebase
(比如branch1
和branch2
之间),就不必这么多费一步,直接rebase
就好。
⭐以上情况是不发生冲突的 rebase
,如果发生冲突了,那么就需要先解决冲突再执行如下命令:
以下展示另外一个例子!
1 | 1.修改冲突文件 |
初始状态
git checkout rebase-branch
git rebase master
:发生 conflict 在 **”step 3”**!
解决冲突后,再执行
add
与commit
(⭐甚至这一步都不需要commit
,在add
之后即可)
git rebase --continue
git checkout master
git merge rebase-branch
:相当于 fast-forward!
⭐总结:需要说明的是,rebase
是站在需要被 rebase
的 commit
上进行操作,这点和 merge
是不同的(相反)。
如何修复倒数第 2 个 commit | 交互式 rebase
commit --amend
可以修复最新commit
的错误,但如果是倒数第二个commit
写错了,怎么办?
如果不是最新的 commit
写错,就不能用 commit --amend
来修复了,而是要用 rebase
。不过需要给 rebase
也加一个参数:-i
。
rebase -i
是 rebase --interactive
的缩写形式,意为「交互式 rebase」。
所谓「交互式 rebase」,就是在 rebase
的操作执行之前,你可以指定要 rebase
的 commit
链中的每一个 commit
是否需要进一步修改。
那么你就可以利用这个特点,进行一次「原地 rebase」。
例如你是在写错了 commit
之后,又提交了一次才发现之前写错了:
1 | git log |
现在再用 commit --amend
已经晚了,但可以用 rebase -i
:
1 | git rebase -i HEAD^^ |
说明:在 Git 中,有两个「偏移符号」:
^
和~
。
^
的用法:在commit
的后面加一个或多个^
号,可以把commit
往回偏移,偏移的数量是^
的数量。例如:master^
表示master
指向的commit
之前的那个commit
;HEAD^^
表示HEAD
所指向的commit
往前数两个commit
。
~
的用法:在commit
的后面加上~
号和一个数,可以把commit
往回偏移,偏移的数量是~
号后面的数。例如:HEAD~5
表示HEAD
指向的commit
往前数 5 个commit
。
上面这行代码表示,把当前 commit
( HEAD
所指向的 commit
) rebase
到 HEAD
之前 2 个的 commit
上:
如果没有 -i
参数的话,这种「原地 rebase」相当于空操作,会直接结束。而在加了 -i
后,就会跳到一个新的界面:
把 pick
修改成 edit
后,就可以退出编辑界面了:
上图的提示信息说明,rebase
过程已经停在了第二个 commit
的位置,那么现在你就可以去修改你想修改的内容了。
修改完成之后,和上节里的方法一样,用 commit --amend
来把修正应用到当前最新的 commit
:
1 | git add 笑声 |
在修复完成之后,就可以用 rebase --continue
来继续 rebase
过程,把后面的 commit
直接应用上去。
1 | git rebase --continue |
然后,这次交互式 rebase
的过程就完美结束了,你的那个倒数第二个写错的 commit
就也被修正了:
实质上,交互式 rebase
并不是必须应用在「原地 rebase」上来修改写错的 commit
,这只不过是它最常见的用法。你同样也可以把它用在分叉的 commit
上,不过这个你就可以自己去研究一下了。
丢弃倒数第二个提交 | 强大的 rebase
如果要丢弃刚提交的 commit
,那只需要执行 git reset --hard HEAD^
即可,那如果是倒二个呢?
git rebase -i
1 | git rebase -i HEAD^^ |
git rebase --onto
除了用交互式 rebase
,你还可以用 rebase --onto
来更简便地撤销提交。
rebase
加上 --onto
选项之后,可以指定 rebase
的「起点」。一般的 rebase
,告诉 Git 的是「我要把当前 commit
以及它之前的 commit
s 重新提交到目标 commit
上去,这其中,rebase
的「起点」是自动判定的:选取当前 commit
和目标 commit
在历史上的交叉点作为起点。
例如下面这种情况:
如果在这里执行:
1 | git rebase <第3个commit> |
那么 Git 会自动选取 3
和 5
的历史交叉点 2
作为 rebase
的起点,依次将 4
和 5
重新提交到 3
的路径上去。
而 --onto
参数,就可以额外给 rebase 指定它的起点。例如同样以上图为例,如果我只想把 5
提交到 3
上,不想附带上 4
,那么我可以执行:
1 | git rebase --onto <第3个commit> <第4个commit> branch1 |
--onto
参数后面有三个附加参数:目标 commit
、起点 commit
(注意:rebase 的时候会把起点排除在外)、终点 commit
。所以上面这行指令就会从 4
往下数,拿到 branch1
所指向的 5
,然后把 5
重新提交到 3
上去。
1 | git rebase --onto HEAD^^ HEAD^ branch1 |
上面这行代码的意思是:以倒数第二个 commit
为起点(起点不包含在 rebase
序列里哟),branch1
为终点,rebase
到倒数第三个 commit
上。
也就是这样:
reset 的本质 | 参数解析
reset
的三种参数:
--hard
:重置位置的同时,清空工作目录的所有改动;--soft
:重置位置的同时,保留工作目录和暂存区的内容,并把重置HEAD
的位置所导致的新的文件差异放进暂存区。--mixed
(默认git reset
):重置位置的同时,保留工作目录的内容,并清空暂存区。
checkout 的本质 | 除了切换分支还可签出某个提交
不过实质上,checkout
并不止可以切换 branch
。checkout
本质上的功能其实是:签出( checkout )指定的 commit
。
直接上案例:
1 | git checkout HEAD^^ |
另外,如果你留心的话可能会发现,在 git status
的提示语中,Git 会告诉你可以用 checkout -- 文件名
的格式,通过「签出」的方式来撤销指定文件的修改:
即撤销工作目录下的修改(此时未添加到暂存区)
Emergency!放下你手上的工作 | stash 临时存放工作目录变动
“stash” 这个词,和它意思比较接近的中文翻译是「藏匿」,是「把东西放在一个秘密的地方以备未来使用」的意思。在 Git 中,stash
指令可以帮你把工作目录的内容全部放在你本地的一个独立的地方,它不会被提交,也不会被删除,你把东西放起来之后就可以去做你的临时工作了,做完以后再来取走,就可以继续之前手头的事了。
具体说来,stash
的用法很简单。当你手头有一件临时工作要做,需要把工作目录暂时清理干净,那么你可以:
1 | git stash |
就这么简单,你的工作目录的改动就被清空了,所有改动都被存了起来。
然后你就可以从你当前的工作分支切到 master
去给你的同事打包了……
打完包,切回你的分支,然后:
1 | git stash pop |
你之前存储的东西就都回来了。很方便吧!
注意:没有被 track 的文件(即从来没有被 add 过的文件不会被 stash 起来,因为 Git 会忽略它们。如果想把这些文件也一起 stash,可以加上
-u
参数,它是--include-untracked
的简写。就像这样:
1 git stash -u
从暂存区撤回工作目录 | restore
use “git restore –staged
“ to unstage
如果已经将文件添加到暂存区,然后想要撤销暂存区中该文件的内容(打回工作目录),则使用以下命令:
1 | git restore --staged <file> |
tip:如果文件在工作目录下修改过但未添加到暂存区,则通过前文提到的
git checkout -- <file>
来撤销该修改。
找回丢失的 branch | reflog
reflog
是 “reference log” 的缩写,使用它可以查看 Git 仓库中的引用的移动记录。如果不指定引用,它会显示 HEAD
的移动记录。假如你误删了 branch1
这个 branch
,那么你可以查看一下 HEAD
的移动历史:
1 | git reflog |
从图中可以看出,HEAD
的最后一次移动行为是「从 branch1
移动到 master
」。而在这之后,branch1
就被删除了。所以它之前的那个 commit
就是 branch1
被删除之前的位置了,也就是第二行的 c08de9a
。
所以现在就可以切换回 c08de9a
,然后重新创建 branch1
:
1 | git checkout -b branch1 |
这样,你刚删除的 branch1
就找回来了。
注意:不再被引用直接或间接指向的
commit
s 会在一定时间后被 Git 回收,所以使用reflog
来找回删除的branch
的操作一定要及时,不然有可能会由于commit
被回收而再也找不回来。
reflog
默认查看 HEAD
的移动历史,除此之外,也可以手动加上分支名称查看其他分支的引用移动历史,例如 master
分支:
1 | git reflog master |
不可移动的 branch | tag
tag
是一个和 branch
非常相似的概念,它和 branch
最大的区别是:tag
不能移动。所以在很多团队中,tag
被用来在关键版本处打标记用。
更多关于 tag
:git-scm.com/docs/git-ta…
Git Flow:复杂又高效的工作流
除了前面讲到的 “Feature Branching”,还有一个也很流行的工作流:Git Flow。Git Flow 的机制非常完善,很适合大型团队的代码管理。不过由于它的概念比较复杂(虽然难度并不高),所以并不适合新手直接学习,而更适合在不断的自我研究中逐渐熟悉,或者在团队合作中慢慢掌握。基于这个原因,我最终也没有在这本小册里讲 Git Flow,但我推荐你自己在有空的时候了解一下它。