查看本地分支与远程分支的对应关系

1
2
3
4
# 查看远程分支与本地分支的对应关系
$ git branch -vv
# 查看所有分支(远程 + 本地)
$ git branch -a

新建远程分支 | push

1
2
3
4
$ git checkout <local-branch>
# 本地分支和远程分支的名字可不一样,但一般是同名分支 | 关联后 (--set-upstream) 可直接 push
# git push origin master 这个master指的是远程分支,因为本地分支默认为当前分支
$ git push origin <local-branch>:<remote-branch>

Tip: push 的时候只会上传当前的 branch 的指向,并不会把本地的 HEAD 的指向也一起上传到远程仓库。事实上,远程仓库的 HEAD 是永远指向它的默认分支(即 master,如果不修改它的名称的话),并会随着默认分支的移动而移动的。

--set-upstream 设置远程分支与本地分支关联

1
2
3
4
# 关联远程仓库的 master 分支与本地的 master 分支,该方法在 push 中可设置,如果要直接绑定分支但不 push 呢,见下面命令行
$ git push --set-upstream origin master:master
# 通过 branch 命令直接设置
$ git branch --set-upstream-to=origin/feature2 feature2

拉取远程分支 | pull

1
2
3
# 说明:关联后 (--set-upstream) 可直接 push
# git pull origin master 这个master指的是远程分支,因为本地分支默认为当前分支
$ git pull origin <remote-branch>:<local-branch>

根据远程分支上创建本地分支

1
2
# 只有远程分支的时候,根据远程分支创建本地分支即可
$ git checkout -b <local-branch> origin/<remote-branch>

删除远程分支

1
2
3
4
5
6
7
# 说明:git push origin <local-branch>/<remote-branch>
# Method1: 推送空分支到远程分支 remote-branch,相当于删除远程分支
$ git push origin :<remote-branch>
# Method2: 直接删除远程分支
$ git push origin --delete <remote-branch>
# Tip: 删除本地分支
$ git branch -d <local-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 checkout --detach

git 各式后悔药

git 管理仓库时,往往需要撤销某些操作/提交/暂存内容。

1. 撤销工作区的文件修改

如果工作区的某个文件被改乱了,但还没有执行 git add,可以用 git checkout 命令找回本次修改之前的文件

1
$ git checkout -- [filename]

⭐它的原理是先找暂存区,如果该文件有暂存的版本,则恢复该版本,否则恢复上一次提交的版本。

注意:工作区的文件变化一旦被撤销,就无法找回了。

2. 从暂存区撤销文件

如果不小心把一个文件添加到暂存区,可以用下面的命令撤销。

1
$ git rm --cached [filename]

上面的命令不影响已经提交的内容。

3. 替换上一次提交信息

💔情况一:提交以后,发现提交信息写错了,这时可以使用 git commit 命令的 --amend 参数,可以修改上一次的提交信息。

1
2
$ git commit --amend -m "Fixes bug #42"
# git commit --amend 也可

它的原理是产生一个新的提交对象,替换掉上一次提交产生的提交对象。

💔情况二:提交之后,发现提交的文件需要修改!这时先修改好工作区,然后再执行 add 后执行 git commit --amend,因为这时暂存区有发生变化的文件,会一起提交到仓库。

1
2
$ git commit --amend -m "append info"
# git commit --amend 也可

所以,--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
2
3
$ git reset --hard [last good SHA]
# 或者
$ git reset --hard HEAD^1

执行 git reset 命令之后,如果想找回那些丢弃掉的提交,可以使用 git reflog 命令,具体做法参考这里 。不过,这种做法有时效性,时间长了可能找不回来。

6. 撤销当前分支的变化

你在当前 error 分支上做了几次提交,突然发现放错了分支,这几个提交本应该放到 master 分支。

1
2
3
4
5
6
7
# 将 error 分支上的<最新一次>提交转移到 master 分支
$ git checkout master
$ git cherry-pick error

# 将 error 分支上的多次提交转移到 master 分支,注意:SHA1 比 SHA2 来得早!
$ git checkout master
$ git cherry-pick [SHA1] [SHA2]

目标:将 error 分支上的最新两次 commit 转移到 master 分支

先执行 git checkout master

再执行 git cherry-pick [SHA1] [SHA2]

有冲突解决冲突,没冲突即可完成

OK!error 分支想要 reset 就 reset~

合并分支 | merge

由于现在 Git 仓库处于冲突待解决的中间状态(已执行 merge 操作),所以如果你最终决定放弃这次 merge,也需要执行一次 merge --abort 来手动取消它。

1
2
3
4
5
# 回到 merge 前的状态
$ git merge --abort
# 在 master 分支上合并 feature 分支
$ git checkout master
$ git merge feature

主流工作流 Feature Branching

这种工作流的核心内容可以总结为两点:

  1. 任何新的功能(feature)或 bug 修复全都新建一个 branch 来写;
  2. branch 写完后,合并到 master,然后删掉这个 branch

Feature Branching

这就是这种工作流最基本的模型。

从上面的动图来看,这种工作流似乎没什么特别之处。但实质上,Feature Branching 这种工作流,为团队开发时两个关键的问题提供了解决方案:

  • 代码分享
  • 一人多任务

1. 代码分享

1
2
3
4
5
6
7
8
9
10
11
12
13
# 本地电脑
$ git checkout -b books
$ git push origin books
# 同事电脑
$ git pull
$ git checkout books
$ git checkout master
$ git pull # merge 之前 pull 一下,让 master 更新到和远程仓库同步
$ git merge books
$ git push
# 删除本地 books 分支,删除远程 books 分支
$ git branch -d books
$ git push origin -d books

借助 GitHub 的 Pull Request 简化 Feature Branching 工作流

事实上,上面所说的这个流程,还可以利用 Pull Request 来进一步简化。

Pull Request 并不是 Git 的内容,而是一些 Git 仓库服务提供方(例如 GitHub)所提供的一种便捷功能,它可以让团队的成员方便地讨论一个 branch ,并在讨论结束后一键合并这个 branchmaster

同样是把写好的 branch 给同事看,使用 Pull Request 的话你可以这样做:

  1. branch push 到中央仓库;
  2. 在中央仓库处创建一个 Pull Request。以 GitHub 为例:

然后你的同事就可以在 GitHub 上看到你创建的 Pull Request 了。他们可以在 GitHub 的这个页面查看你的 commits,也可以给你评论表示赞同或提意见,你接下来也可以根据他们的意见把新的 commits push 上来,这也页面会随着你新的 push 而展示出最新的 commits

在讨论结束以后,你们一致认为这个 branch 可以合并了,你只需要点一下页面中那个绿色的 “Merge pull request” 按钮,GitHub 就会自动地在中央仓库帮你把 branch 合并到 master 了:

然后你只要在本地 pull 一下,把最新的内容拉到你的电脑上,这件事情就算完成了。

另外,GitHub 还设计了一个贴心的 “Delete branch” 按钮,方便你在合并之后一键删除 branch


完整的例子:

1
2
3
4
5
6
7
8
9
10
# 拉取 feature2 分支
$ git pull origin feature2:feature2
$ git branch --set-upstream-to=origin/feature2 feature2
$ git branch -vv
feature1 c16362b [origin/feature1] Merge branch 'master' into feature1
* feature2 db3024c [origin/feature2] create branch feature2
master 55c2952 [origin/master: behind 2] Merge pull request
$ git checkout master
# master 分支也要 pull
$ git pull origin master:master

2. 一人多任务

你正在认真写着代码,忽然同事过来跟你说:「内个……你这个功能先放一放吧,我们最新讨论出要做另一个更重要的功能,你来做一下吧。」

如果你是在独立的 branch 上做事,切换任务是很简单的。你只要稍微把目前未提交的代码简单收尾一下,然后做一个带有「未完成」标记的提交(例如,在提交信息里标上「TODO」),然后回到 master 去创建一个新的 branch 就好了。

1
2
3
# 切换回 master 主分支!!!因为需要从主分支上创建分支
$ git checkout master
$ git checkout -b new_feature

查改历史改动记录

git log

1
2
3
4
5
6
7
8
9
# 在 .bashrc 中设置 git-log 别名,可图形化输出 git log 的信息
$ alias git-log='git log --pretty=oneline --all --graph --abbrev-commit'
$ git-log
# 查看历史改动信息
$ git log
# 查看详细的历史记录,可以看到每个 commit 的具体改动细节(-p 是 --patch 的缩写)
$ git log -p
# 查看简要统计,只想大致看一下改动内容
$ git log --stat

git show

1
2
3
# 查看某个具体的 commit: $ git show 查看当前 commit
$ git show <SHA-1>
$ git show <SHA-1> <filename> # 具体到某个指定文件

git diff

比对本地工作目录与暂存区的内容(即显示未使用 git add 加入暂存区的内容与暂存区的内容的不同之处)

1
2
3
4
5
6
7
8
9
10
$ git diff
diff --git a/README.md b/README.md
index 4b572c1..0f8ceb4 100644
--- a/README.md
+++ b/README.md
@@ -5,3 +5,4 @@
4. `git branch feature1` && `git checkout feature1` && `git checkout master` && `git merge feature1` (feature1)
5. master branch (feature branching, solve conflict by `pull request`)
6. feature2
+7. 哈哈哈哈

比对暂存区与上一条提交的内容(即显示 git add 后的内容与上次 commit 之间内容的不同之处)

1
2
3
# 二者命令完全等价
$ git diff --staged
$ git diff --cached

比对工作目录和上一条提交的内容

使用 git diff HEAD 可以显示工作目录和上一条提交之间的不同,它是上面这二者的内容相加。换句话说,这条指令可以让你看到「如果你现在把所有文件都 add 然后 git commit,你将会提交什么」(不过需要注意,没有被 Git 记录在案的文件(即从来没有被 add 过 的文件,untracked files 并不会显示出来。为什么?因为对 Git 来说它并不存在啊)。

1
2
$ git diff HEAD
# 也可 git diff <SHA-1>

实质上,如果你把 HEAD 换成别的 commit,也可以显示当前工作目录和这条 commit 的区别。

不喜欢 merge 的分叉,那就用 rebase 吧

rebase —— 变基?!

其实这个翻译还是比较准确的。rebase 的意思是,给你的 commit 序列重新设置基础点(也就是父 commit)。展开来说就是,把你指定的 commit 以及它所在的 commit 串,以指定的目标 commit 为基础,依次重新提交一次。例如下面这个 merge

1
2
# merge 过来;rebase 过去
$ git merge branch1

master: 1-2-3-4-7

branch1: 1-2-5-6-7

img

如果把 merge 换成 rebase,可以这样操作:

1
2
3
$ git checkout branch1
# merge 过来;rebase 过去
$ git rebase master

master: 1-2-3-4-7-8

branch1: 1-2-5-6

master 上的 7、8 是 branch1 上的 5、6 rebase 过去的~

img

可以看出,通过 rebase56 两条 commits 把基础点从 2 换成了 4 。通过这样的方式,就让本来分叉了的提交历史重新回到了一条线。这种「重新设置基础点」的操作,就是 rebase 的含义。

另外,在 rebase 之后,记得切回 mastermerge 一下,把 master 移到最新的 commit

1
2
$ git checkout master
$ git merge branch1

master/branch1: 1-2-3-4-7-8

img

为什么要从 branch1rebase,然后再切回 mastermerge 一下这么麻烦,而不是直接在 master 上执行 rebase

从图中可以看出,rebase 后的 commit 虽然内容和 rebase 之前相同,但它们已经是不同的 commits 了。如果直接从 master 执行 rebase 的话,就会是下面这样:

img

这就导致 master 上之前的两个最新 commit 被剔除了。如果这两个 commit 之前已经在中央仓库存在,这就会导致没法 push 了:

img

所以,为了避免和远端仓库发生冲突,一般不要从 master 向其他 branch 执行 rebase 操作。而如果是 master 以外的 branch 之间的 rebase(比如 branch1branch2 之间),就不必这么多费一步,直接 rebase 就好。


⭐以上情况是不发生冲突的 rebase,如果发生冲突了,那么就需要先解决冲突再执行如下命令:

以下展示另外一个例子

1
2
3
4
# 1.修改冲突文件
# 2.git add. & git commit -m "fix conflict"
# 3.rebase continue
$ git rebase --continue

初始状态

git checkout rebase-branch

git rebase master:发生 conflict 在 **”step 3”**!

解决冲突后,再执行 addcommit(⭐甚至这一步都不需要 commit,在 add 之后即可)

git rebase --continue

git checkout master

git merge rebase-branch:相当于 fast-forward!

⭐总结:需要说明的是,rebase 是站在需要被 rebasecommit 上进行操作,这点和 merge 是不同的(相反)。

如何修复倒数第 2 个 commit | 交互式 rebase

commit --amend 可以修复最新 commit 的错误,但如果是倒数第二个 commit 写错了,怎么办?

如果不是最新的 commit 写错,就不能用 commit --amend 来修复了,而是要用 rebase。不过需要给 rebase 也加一个参数:-i

rebase -irebase --interactive 的缩写形式,意为「交互式 rebase」。

所谓「交互式 rebase」,就是在 rebase 的操作执行之前,你可以指定要 rebasecommit 链中的每一个 commit 是否需要进一步修改。

那么你就可以利用这个特点,进行一次「原地 rebase」。

例如你是在写错了 commit 之后,又提交了一次才发现之前写错了:

1
$ git log

img

现在再用 commit --amend 已经晚了,但可以用 rebase -i

1
$ git rebase -i HEAD^^

说明:在 Git 中,有两个「偏移符号」: ^~

^ 的用法:在 commit 的后面加一个或多个 ^ 号,可以把 commit 往回偏移,偏移的数量是 ^ 的数量。例如:master^ 表示 master 指向的 commit 之前的那个 commitHEAD^^ 表示 HEAD 所指向的 commit 往前数两个 commit

~ 的用法:在 commit 的后面加上 ~ 号和一个数,可以把 commit 往回偏移,偏移的数量是 ~ 号后面的数。例如:HEAD~5 表示 HEAD 指向的 commit往前数 5 个 commit

上面这行代码表示,把当前 commitHEAD 所指向的 commitrebaseHEAD 之前 2 个的 commit 上:

img

如果没有 -i 参数的话,这种「原地 rebase」相当于空操作,会直接结束。而在加了 -i 后,就会跳到一个新的界面:

img

pick 修改成 edit 后,就可以退出编辑界面了:

img

上图的提示信息说明,rebase 过程已经停在了第二个 commit 的位置,那么现在你就可以去修改你想修改的内容了。

修改完成之后,和上节里的方法一样,用 commit --amend 来把修正应用到当前最新的 commit

1
2
$ git add 笑声
$ git commit --amend

img

在修复完成之后,就可以用 rebase --continue 来继续 rebase 过程,把后面的 commit 直接应用上去。

1
$ git rebase --continue

img

然后,这次交互式 rebase 的过程就完美结束了,你的那个倒数第二个写错的 commit 就也被修正了:

img

实质上,交互式 rebase 并不是必须应用在「原地 rebase」上来修改写错的 commit ,这只不过是它最常见的用法。你同样也可以把它用在分叉的 commit 上,不过这个你就可以自己去研究一下了。

丢弃倒数第二个提交 | 强大的 rebase

如果要丢弃刚提交的 commit,那只需要执行 git reset --hard HEAD^ 即可,那如果是倒二个呢?

git rebase -i

1
2
$ git rebase -i HEAD^^
# 上文是修改 pick 为 edit;而该操作只需要将 pick 当行删除即可

git rebase --onto

除了用交互式 rebase ,你还可以用 rebase --onto 来更简便地撤销提交。

rebase 加上 --onto 选项之后,可以指定 rebase 的「起点」。一般的 rebase,告诉 Git 的是「我要把当前 commit 以及它之前的 commits 重新提交到目标 commit 上去,这其中,rebase 的「起点」是自动判定的:选取当前 commit 和目标 commit 在历史上的交叉点作为起点。

例如下面这种情况:

img

如果在这里执行:

1
$ git rebase <第3个commit>

那么 Git 会自动选取 35 的历史交叉点 2 作为 rebase 的起点,依次将 45 重新提交到 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 上去。

img

1
$ git rebase --onto HEAD^^ HEAD^ branch1

上面这行代码的意思是:以倒数第二个 commit 为起点(起点不包含在 rebase 序列里哟),branch1 为终点,rebase 到倒数第三个 commit 上。

也就是这样:

img

reset 的本质 | 参数解析

reset 的三种参数:

  1. --hard:重置位置的同时,清空工作目录的所有改动
  2. --soft:重置位置的同时,保留工作目录和暂存区的内容,并把重置 HEAD 的位置所导致的新的文件差异放进暂存区。
  3. --mixed(默认 git reset):重置位置的同时,保留工作目录的内容,并清空暂存区

checkout 的本质 | 除了切换分支还可签出某个提交

不过实质上,checkout 并不止可以切换 branchcheckout 本质上的功能其实是:签出( checkout )指定的 commit

直接上案例:

1
2
3
4
$ git checkout HEAD^^
$ git checkout master~5
$ git checkout <SHA>
$ git checkout <SHA>^^^

另外,如果你留心的话可能会发现,在 git status 的提示语中,Git 会告诉你可以用 checkout -- 文件名 的格式,通过「签出」的方式来撤销指定文件的修改:

即撤销工作目录下的修改(此时未添加到暂存区)

img

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 就找回来了。

注意:不再被引用直接或间接指向的 commits 会在一定时间后被 Git 回收,所以使用 reflog 来找回删除的 branch 的操作一定要及时,不然有可能会由于 commit 被回收而再也找不回来。


reflog 默认查看 HEAD 的移动历史,除此之外,也可以手动加上分支名称查看其他分支的引用移动历史,例如 master 分支:

1
$ git reflog master

不可移动的 branch | tag

tag 是一个和 branch 非常相似的概念,它和 branch 最大的区别是:tag 不能移动。所以在很多团队中,tag 被用来在关键版本处打标记用。

更多关于 taggit-scm.com/docs/git-ta…

Git Flow:复杂又高效的工作流

除了前面讲到的 “Feature Branching”,还有一个也很流行的工作流:Git Flow。Git Flow 的机制非常完善,很适合大型团队的代码管理。不过由于它的概念比较复杂(虽然难度并不高),所以并不适合新手直接学习,而更适合在不断的自我研究中逐渐熟悉,或者在团队合作中慢慢掌握。基于这个原因,我最终也没有在这本小册里讲 Git Flow,但我推荐你自己在有空的时候了解一下它。


✍️ Yikun Wu 已发表了 69 篇文章 · 总计 293k 字,采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处

🌀 本站总访问量