Git 是一个版本管理软件,由 Linux 之父花了 三天 搞出来的东西。

手动惭愧 (〃’▽’〃)

一 clone(项目)

1
git clone https://github.com/CalmCenter/UiStarge.git ui-starge

最后那个 ui-starge 表示手动指定本地仓库目录名称。

使用 git clone 时,除了从远程仓库把 .git 这个仓库目录下载到工作目录中,还会 checkout (签出) mastercheckout 的意思就是把某个 commit 作为当前 commit,把 HEAD 移动过去,并把工作目录的文件内容替换成这个 commit 所对应的内容)。

二 push(提交)

add(添加至暂存区)

push 做的事是:把当前 branch 的位置(即它指向哪个 commit)上传到远端仓库,并把它的路径上的 commits 一并上传。

基础步骤 add -> commit -> push ,接下来一个一个看

1
git add test.txt

add 后面可以使文件或者文件夹,如果是文件夹 test/ 。如果文件或文件夹名字有空格,比如 text 1.txt 文件 或 text 2 文件夹 ,需要写成 get add test\ 1.txtget add test\ 2/ ,将空格转义一下。

还有一个更方便的方法

1
git add .

add 指令除了 git add 文件名 这种用法外,还可以使用 add . 来直接把工作目录下的所有改动全部放进暂存区。这个用法没什么特别的好处,但就一个字:方便 (^_−)☆。

添加或删除的文件都需要 add ,也包括改名前的文件。

你在 add 后,如果再次修改的已经添加的文件,需要再次 add ,因为通过 add 添加进暂存区的不是文件名,而是具体的对改动内容的一个快照。

commit(提交到本地仓库)

正常提交 -m 后面写提交信息

1
git commit -m "test"

当提交后,发现内容有误,需要怎么办?修改后重新提交一个 commit ? 当然可以,不过还有一个更加优雅和简单的解决方法:

1
git commit -—amend

"amend" 是「修正」的意思。在提交时,如果加上 --amend 参数,Git 不会在当前 commit 上增加 commit,而是会把当前 commit 里的内容和暂存区里的内容合并起来后创建一个新的 commit用这个新的 commit 把当前 commit 替换掉

需要注意:commit --amend 并不是直接修改原 commit 的内容,而是生成一条新的 commit ,替换掉旧的 commit 。如果旧的 commit 已经 push 到远程仓库,替换后 push 的时候需要在 push 后加 -fgit push -f 强制更新远程仓库历史。

这种方式只能修改最新的 commit ,那如果要修改倒数第二个、第三个 commit 怎么办呢?八 rebase 4.1 中会详细说明。

push

提交到远程

1
git push

git push 是提交当前分支已经 commit 的文件。分支提交请查看 Branch(分支) 部分

push 上去了才发现写错怎么办?

出错的内容在你自己的 branch,不会影响到其他人,那没关系用本文的方法把写错的 commit 修改或者删除掉,然后再 push 上去就好了。

如果是修改或删掉了本地的 commit 需要用下面的命令强制更新远程才可以 ~

1
git push origin branch1 -f

当出错的内容已经合并到 master ,同事的工作都在 master 上,永远不知道一次强制 push 会不会洗掉同事刚发上去的新提交。
除非人员数量和行为都完全可控的超小团队,可以和同事做到无死角的完美沟通,不然一定别在 master 上强制 push

在这种时候,你只能退一步,选用另一种策略:
增加一个新的提交,把之前提交的内容抹掉。例如之前你增加了一行代码,你希望撤销它,那么你就做一个删掉这行代码的提交;
如果你删掉了一行代码,你希望撤销它,那么你就做一个把这行代码还原回来的提交。

这种事做起来也不算麻烦,因为 Git 有一个对应的指令:revert

1
git revert HEAD^

上面这行代码就会增加一条新的 commit,它的内容和倒数第二个 commit 是相反的,从而和倒数第二个 commit 相互抵消,达到撤销的效果。

revert 完成之后,把新的 commitpush 上去,这个 commit 的内容就被撤销了。它和前面所介绍的撤销方式相比,最主要的区别是,这次改动只是被「反转」了,并没有在历史中消失掉,你的历史中会存在两条 commit :一个原始 commit ,一个对它的反转 commit

三 pull(拉取)

1
git pull

这个命令可以 拉取文件 比如其他人提交或合并了文件,还可以 拉取远程分支 比如其他人将分支提交到了远程仓库,pull 的实际操作其实是把远端仓库的内容用 fetch 取下来之后,用 merge 来合并。

四 查看提交记录

1
git log

结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ git log
commit 2c796942b103740176aae9eab1e2ead364c75787 (HEAD -> master, origin/master, origin/HEAD)
Author: Pc
Date: Thu Jul 16 16:04:31 2020 +0800

delete

commit efc90131928c01ff3c8db1feae58461c766c78cd
Author: Calm Lv
Date: Thu Jul 16 16:02:28 2020 +0800

Delete test 1.txt

commit 24078eeecdaae6091e65170d86dcd4e20a474817
Author: Pc
Date: Thu Jul 16 16:00:17 2020 +0800

test

第一行的 commit 后面括号里的 HEAD -> master, origin/master, origin/HEAD ,是几个指向这个 commit「引用」

  • commit 唯一标识

    每一个 commit 都有一个它唯一的指定方式,它的 SHA-1 校验和,也就是上图中每个 commit 右边的那一长串字符。

    两个 SHA-1 值的重复概率极低,所以可以使用这个 SHA-1 值来指代 commit,也可以只使用它的前几位来指代它(例如第一个 2c796942b...75787,你使用 ``2c79694甚至2c79` 来指代它)

    但毕竟这种没有任何含义的字符串是很难记忆的,所以 Git 提供了「引用」的机制:使用固定的字符串作为「引用」,指向某个 commit,作为操作 commit 时的快捷方式。

  • HEAD

    HEAD 是「引用」中最特殊的一个:它是指向当前 commit 的「引用」,也就是当前工作目录所对应的 commit

  • origin/master、origin/HEAD

    它们是对远端仓库的 masterHEAD 的本地镜像

git log 可以查看提交记录,但是看不到更多的细节,如何查看更多的细节呢?

查看每个 commit 单行显示

只显示 commit SHA-1 校验和 ,以及 提交说明。

1
git log --oneline

查看每个 commit 图标显示

1
git log --oneline --graph --all

查看每个 commit 大致改动

如果你只想大致看一下改动内容,但并不想深入每一行的细节(例如你想回顾一下自己是在哪个 commit 中修改了 xxx.txt 文件)可以使用

1
git log --stat

查看每个 commit 详细改动

-p--patch 的缩写,通过 -p 参数,你可以看到具体每个 commit 的改动细节

1
git log -p

查看当前 commit 的改动内容

1
git show

查看任意一个 commit

show 后面加上这个 commit 的引用(branchHEAD 标记)或它的 SHA-1 码:

1
git show 03c8

查看指定 commit 中的指定文件改动内容

1
git show 03c8 branch1.txt

查看未提交的内容

使用 git diff --staged 可以显示 暂存区( add 后的内容) 和 上一条提交 之间的不同,换句话说,这条指令可以让你看到「输入 git commit 后,你将会提交什么」

比对暂存区和上一条提交

1
git diff --staged

--staged 有一个等价的选项叫做 --cached。这里所谓的「等价」,是真真正正的等价,它们的意思完全相同。

比对工作目录和暂存区

使用 git diff (不加选项参数)可以显示 工作目录 和 暂存区之间 的不同。换句话说,这条指令可以让你看到「执行 add . ,你会向暂存区中增加哪些内容」

1
git diff

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

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

1
git diff HEAD

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

Reflog 查看引用移动记录

reflog"reference log" 的缩写,使用它可以查看 Git 仓库中的引用的移动记录。如果不指定引用,它会显示 HEAD 的移动记录。

1
2
3
$ git reflog
347cd98 (HEAD -> master, origin/master, origin/HEAD) HEAD@{0}: checkout: moving from feature2 to master
3a10357 (feature2) HEAD@{1}: checkout: moving from master to feature2

这里可以查看之前分支的 commit 信息,如果 feature2 在这之后被删除了,可以查看之前 feature2 最后的切换信息,例如上面的 347cd98 表示 HEAD 的最后一次移动行为是「从 feature2 移动到 master」,如果之后 feature2 被删除了,我们可以切换回 347cd98,然后重新创建 feature2

1
2
git checkout 347cd98
git checkout -b feature2

这样就实现了分支追回。

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

五 Branch(分支)

HEADGit 中一个独特的「引用」,它是唯一的。而除了 HEAD 之外,Git 还有一种「引用」,叫做 branch(分支)。

HEAD 除了可以指向 commit,还可以指向一个 branch,当它指向某个 branch 的时候,会通过这个 branch 来间接地指向某个 commit

上面的 git log 输出中,HEAD -> master 中的 master 就是一个 branch 的名字,并且它是默认的 branch,而它左边的箭头 -> 表示 HEAD 正指向它(当然,也会间接地指向它所指向的 commit)。

当我们执行 git commit 后 ,HEADmaster 这两个「引用」都指向了新的 commit,而 origin/masterorigin/HEAD 则依然停留在原先的位置。

1
2
3
4
5
6
7
8
9
10
11
12
$ git log
commit 0b4d1da7f998bc5fb724b0a74b4034c5dab9e35a (HEAD -> master)
Author: Pc
Date: Thu Jul 16 16:46:58 2020 +0800

add

commit 2c796942b103740176aae9eab1e2ead364c75787 (origin/master, origin/HEAD)
Author: Pc
Date: Thu Jul 16 16:04:31 2020 +0800

delete

创建分支

1
git branch <分支名>

提交分支

这里会将本地创建的分支更新到远程,并提交分支中的 commits

如果本地当前 HEAD 处于新分支并且没有和远程关联,需要输入

1
git push origin <分支名>

才能提交当前分支以及分支中的文件到远程仓库

origin 是远程仓库的别名,是你在 git clone 的时候 Git 自动帮你起的,然后指明分支名称。这两个参数合起来指定了你要 push 到的目标仓库和目标分支。

如果进行了关联 (设置本地分支追踪远程分支)

1
2
git push --set-upstream origin <分支名>
git push -u origin <分支名>

关联后就可以在当前分支使用 git push 直接提交。

What exactly does the “u” do? “git push -u origin master” vs “git push origin master”

git push 的 -u 参数具体适合含义?

查看分支

1
git branch -a

切换分支(checkout)

1
git checkout <分支名>

还有一种方式是创建分支,创建并切换到刚创建的分支。

1
git checkout -b <分支名>

checkout 本质:

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

git checkout branch名 的本质,其实是把 HEAD 指向指定的 branch,然后签出这个 branch 所对应的 commit 的工作目录。所以同样的,checkout 的目标也可以不是 branch,而直接指定某个 commit

1
2
git checkout HEAD^^
git checkout 78a4bc

git status 的提示语中,Git 会告诉你可以用 checkout -- 文件名 的格式,通过「签出」的方式来撤销工作目录中指定文件的修改:

1
2
3
4
5
6
7
8
9
10
11
12
$ git status
On branch master
Your branch is ahead of 'origin/master' by 8 commits.
(use "git push" to publish your local commits)

Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)

modified: branch1.txt

no changes added to commit (use "git add" and/or "git commit -a")
1
git checkout -- branch1.txt

然后刚刚对这个文件的修改就都被撤回了 ~

checkoutreset 的不同

reset 后面会讲,但是这里先说一下不同,既然说不同那肯定就有相同的地方:

checkoutreset 都可以切换 HEAD 的位置,它们除了有许多细节的差异外,最大的区别在于:reset 在移动 HEAD 时会带着它所指向的 branch(引用) 一起移动,而 checkout 不会。

checkout 有一个专门用来只让 HEADbranch 脱离而不移动 HEAD 的用法:

1
git checkout --detach

现在提交 commit 之前所依赖的 branch 将不会一起移动。

1
2
3
4
5
6
7
8
9
10
11
commit 347cd9856e1edf4c0a96cb15bad227ab8d255cda (HEAD)
Author: Pc
Date: Tue Sep 29 18:27:18 2020 +0800

head

commit 81bb6988fc9698699f697613876f5ad43a698564 (master)
Author: Pc
Date: Tue Sep 29 18:26:43 2020 +0800

c

应用场景:
比如想在某个 commit 出写一些临时的新的 commits,但不确定是否保留这些内容(或者倾向于不保留),那么就:

  1. 不希望把当前的 branch 带跑;
  2. 不想新建一个别的 branch。

那么就可以 detach 一下,这样就可以在后面的 commits 中只有 HEAD 在跟着 commits 跑,而 branch 一直留在原地。

删除分支

1
git branch -d <分支名>
  • HEAD 指向的 branch 不能删除。如果要删除 HEAD 指向的 branch,需要先用 checkoutHEAD 指向其他地方。
  • 由于 Git 中的 branch 只是一个「引用」,所以删除 branch 的操作也只会删掉这个「引用」,并不会删除任何的 commit(分支提交的内容)。(不过如果一个 commit 不在任何一个 branch 的「路径」上,或者换句话说,如果没有任何一个 branch 可以回溯到这条 commit(也许可以称为野生 commit?),那么在一定时间后,它会被 Git 的回收机制删除掉。)
  • 出于安全考虑,没有被合并到 masterbranch 在删除时会失败(前提是有新内容),这种情况如果你确认是要删除这个 branch (例如某个未完成的功能被团队确认永久毙掉了,不再做了),可以把 -d 改成 -D,小写换成大写,就能删除了。

「引用」的本质

所谓「引用」(reference),其实就是一个个的字符串。这个字符串可以是一个 commitSHA-1 码(例:c08de9a4d8771144cd23986f9f76c4ed729e69b0),也可以是一个 branch(例:ref: refs/heads/feature3)。拿到这个字符串,就知道当前引用指向谁。

Git 中的 HEAD 和每一个 branch 以及其他的引用,都是以文本文件的形式存储在本地仓库 .git 目录中,而 Git 在工作的时候,就是通过这些文本文件的内容来判断这些所谓的「引用」是指向谁的。

六 merge(合并)

merge 的意思是「合并」,它做的事也是合并:指定一个 commit,把它合并到当前的 commit 来。

把目标 commit 的路径上的所有 commit 的内容一并应用到当前 commit (即 HEAD 所指向的 commit),然后自动生成一个新的 commit

1
git merge <目标分支名>

适用场景

  • 合并分支

    当一个 branch 的开发已经完成,需要把内容合并回去时,用 merge 来进行合并。

  • pull 的内部操作

    pull 的实际操作其实是把远端仓库的内容用 fetch 取下来之后,用 merge 来合并。

特殊情况 1:冲突

merge 在做合并的时候,是有一定的自动合并能力的:
如果一个分支改了 A 文件,另一个分支改了 B 文件,那么合并后就是既改 A也改 B,这个动作会自动完成;
如果两个分支都改了同一个文件,但一个改的是第 1 行,另一个改的是第 2 行,那么合并后就是第 1 行和第 2 行都改,也是自动完成。

但是,如果两个分支修改了同一部分内容,merge 的自动算法就搞不定了。这种情况 Git 称之为:冲突(Conflict)。

两个分支改了相同的内容,Git 不知道应该以哪个为准。如果在 merge 的时候发生了这种情况,Git 就会把问题交给你来决定。它会告诉你 merge 失败,以及失败的原因。

我再 feature1 分支 mergeTest.txt 文件第一行写了几个字,并 commit 提交,然后切换到 master 分支,这时 mergeTest.txt 文件是没东西的,又在第一行写了几个字,并 commit 提交,最后在 master 分支去合并 feature1 分支,提示如下:

1
2
3
4
$ git merge feature1
Auto-merging mergeTest.txt
CONFLICT (content): Merge conflict in mergeTest.txt
Automatic merge failed; fix conflicts and then commit the result.

提示信息中心说 mergeTest.txt 中出现合并冲突,自动合并失败,要求把冲突解决后再提交。意思就是需要先 解决冲突 ,然后手动 commit 一下

1.解决冲突

现在打开冲突的文件,会发现是这样的:

1
2
3
4
5
<<<<<<< HEAD
烦烦烦
=======
奥德赛
>>>>>>> feature1

Git 虽然没有帮你完成自动 merge,但它对文件还是做了一些工作:它把两个分支冲突的内容放在了一起,并用 ======= 符号标记出了它们的边界以及它们的出处。

HEAD 中的内容是 烦烦烦,而 feature1 中的内容则是 奥德赛。这两个改动 Git 不知道应该怎样合并,于是把它们放在一起,由你来决定。假设你决定保留 HEAD 的修改,那么只要删除掉 feature1 的修改,再把 Git 添加的那三行 <<< === >>> 辅助文字也删掉,保存文件退出,所谓的「解决掉冲突」就完成了。

修改后只能剩下需要的内容:

1
烦烦烦

2.提交

解决完冲突以后,就可以进行第二步—— commit 了。

1
2
git add mergeTest.txt
git commit

被冲突中断的 merge,在手动 commit 的时候依然会自动填写提交信息。这是因为在发生冲突后,Git 仓库处于一个「merge 冲突待解决」的中间状态,在这种状态下 commitGit 就会自动地帮你添加「这是一个 merge commit」的提交信息。

1
2
3
4
Merge branch 'feature1' into master

# Conflicts:
# mergeTest.txt

3.放弃解决冲突,取消 merge

1
git merge --abort

输入这行代码,你的 Git 仓库就会回到 merge 前的状态。

特殊情况 2:HEAD 领先于目标 commit

如果 merge 时的目标 commitHEAD 处的 commit 并不存在分叉,而是 HEAD 领先于目标 commit

1
2
3
1          2          3
↑ ↑
feature1 HEAD/master

那么 merge 就没必要再创建一个新的 commit 来进行合并操作,因为并没有什么需要合并的。在这种情况下, Git 什么也不会做,merge 是一个空操作。

特殊情况 3:HEAD 落后于目标 commit

这种又叫 fast-forward

另一种情况:如果 HEAD 和目标 commit 依然是不存在分叉,但 HEAD 不是领先于目标 commit,而是落后于目标 commit

1
2
3
1          2          3          4
↑ ↑
HEAD/master origin/master

那么 Git 会直接把 HEAD 移动到目标 commit

这种操作有一个专有称谓,叫做 "fast-forward"(快速前移)。

一般情况下,创建新的 branch 都是会和原 branch 并行开发的,不然没必要开 branch ,直接在原 branch 上开发就好。

但事实上,这种情形其实很常见,因为这其实是 pull 操作的一种经典情形:本地的 master 没有新提交,而远端仓库中有同事提交了新内容到 master
那么这时如果在本地执行一次 pull 操作,就会由于 HEAD 落后于目标 commit (也就是远端的 master)而造成 "fast-forward"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ git pull
remote: Enumerating objects: 2, done.
remote: Counting objects: 100% (2/2), done.
remote: Total 2 (delta 1), reused 2 (delta 1), pack-reused 0
Unpacking objects: 100% (2/2), done.
From https://github.com/CalmCenter/UiStarge
195efe8..61213f7 master -> origin/master
Updating 195efe8..61213f7
Fast-forward
branch1.txt | 1 +
mergeTest.txt | 1 +
test1.txt | 1 +
3 files changed, 3 insertions(+)
create mode 100644 branch1.txt
create mode 100644 mergeTest.txt

现在把 git pull 拆分执行:

第一步

1
git fetch

下载远端仓库内容时,这两个镜像引用得到了更新,origin/masterorigin/HEAD 移动到了最新的 commit

在输入 git log 后你会发现找不到 (origin/master, origin/HEAD) 这个引用了,因为到更新到最新的 commit 了,这里只能打印当前 commit 以下的信息。

1
2
3
4
5
6
7
8
$ git log
commit 61213f7813ad116abdd498bd3575e7b4173b4c8e (HEAD -> master)
Merge: 195efe8 75aa327
Author: Pc
Date: Wed Sep 23 14:32:33 2020 +0800

Merge branch 'feature2'

git pull 的第二步操作 merge 的目标 commit ,是远端仓库的 HEAD,也就是 origin/HEAD ,所以 git pull 的第二步的完整内容是:

1
2
3
4
5
$ git merge origin/HEAD
Updating 61213f7..6c2bcc2
Fast-forward
mergeTest.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ git log
commit 6c2bcc2a8ca9d872338d36df45dee8a95d900148 (HEAD -> master, origin/master, origin/HEAD)
Author: Pc
Date: Wed Sep 23 14:39:43 2020 +0800

test

commit 61213f7813ad116abdd498bd3575e7b4173b4c8e
Merge: 195efe8 75aa327
Author: Pc
Date: Wed Sep 23 14:32:33 2020 +0800

Merge branch 'feature2'

因此 HEAD 就会带着 master 一起,也指向了最新 commit

如果不想用 fast-forward ,可以使用如下命令 --no-ff:不使用 fast-forward 方式合并,保留分支的 commit 历史

1
git merge --no-ff feature

七 Feature Branch(最流行的工作流)

目前最流行(不论是中国还是世界)的团队开发的工作流:Feature Branch

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

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

这种工作流似乎没什么特别之处。但实质上,Feature Branching 这种工作流,为团队开发时两个关键的问题—— 代码分享一人多任务 ——提供了解决方案。

1.代码分享

假设需要开发一个新功能,于是创建了一个新的 branch 叫做 books,然后开始在 books 上进行开发工作。

1
git checkout -b books

在十几个 commit 过后,功能开发完毕,把代码 push 到中央仓库(例如 GitHub)去,然后告诉 review 的同事:「嘿,功能写完了,分支名是 books,谁有空的话帮我 review 一下吧。」

1
git push origin books

然后负责 review 的同事就可以从中央仓库拉下来你的代码:

1
2
git pull
git chekcout books

然后可以根据同事要求修改、更新,review 通过后,就可以合并到 master 上了。

1
2
3
git checkout master
git pull
git merge books

merge 之前 pull 一下,让 master 更新到和远程仓库同步

然后把合并后的结果 push 到了中央仓库,并删掉了 books 这个 branch

1
2
3
git push
git branch -d books
git push origin -d books

git push origin -d books-d参数把远程仓库的 branch 也删了

2.Pull Request

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

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

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

首先创建一个 pull request

这是在 push 分支之后,会有一个这么个按钮快捷创建 pull request,但我现在试着不是每次都出~

下面是正常创建流程。

点击 New pull request 然后选择需要分享的分支,这里是 feature2 ,选择后就可以看到修改内容,并且 create pull request 也可以点击了。

这里需要写修改内容 md 格式的。

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

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

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

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

3.一人多任务

除了代码分享的便捷,基于 Feature Branch 的工作流对于一人多任务的工作需求也提供了很好的支持。

安安心心做事不被打扰,做完一件再做下一件自然是很美好的事,但现实往往不能这样。对于程序员来说,一种很常见的情况是,你正在认真写着代码,忽然紧急需要新填另外一个功能!

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

如果有一天需要回来继续做这个 branch,你只要用 checkout 切回来,就可以继续了。

八 rebase(变基)

rebase ,又是一个中国人看不懂的词。这个词的意思,你如果查一下的话是 变基 (〃’▽’〃) 。 不忍直视 ~ (/ω\)

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

一般情况下是在子分支去 rebase 主分支

1. rebasemerge 的区别

先看一下 rebase 的情况。

当前分支情况:

1
2
3
4
5
6
7
8
                      master

1 → 2 → 3 → 4


5 → 6

feature2
1
2
git checkout feature2
git rebase master

执行完后:

1
2
3
4
5
6
7
                      master         feature2
↓ ↓
1 → 2 → 3 → 4 → 7 → 8


5 → 6

通过 rebasefeature2 把基础点从 2 换成了 4feature2 中的 56 两条 commit 也 变成了新的 commit 78 ,但是内容是没有变的。
通过这样的方式,就让本来分叉了的提交历史重新回到了一条线。这种「重新设置基础点」的操作,就是 rebase 的含义。

更直观一点:

这是 sourTree 的历史日志,feature2 当前的在 delete branch 这个 commit 的基础下,更改了一些内容,并且当前 master 也有了新的更新,现在需要把 master 的内容合并到 feature2

使用 rebase 合并后

feature2 的基准点变了,成了当前 master 指向的 commit ,并且他们的提交历史变成了一条直线。

merge 呢?

feature2 当前的在 branch 更新 这个 commit 的基础下,更改了一些内容,并且当前 master 也有了新的更新,现在需要把 master 的内容合并到 feature2

使用 merge 合并后

这里可以明显的看到,多了一个 commit ,并且提交历史也是分叉的,如果分支多的情况下,会很乱的!

现在,基本明白了 rebase 的作用,可以减少 commit 的数量以及减少分叉数量,使得提交历史干净整洁。

当然这只是其中一部分作用。

2.rebase 时出现了冲突怎么办?

如果出现了冲突,在 git 页面中会出现 REBASE 1/1merge 冲突时也会出现类似的提示,解决办法相同,把 rebase 改成 merge 即可。

需要手动解决,解决完后,用 git add . 或者某个文件名,来标记已解决,最后执行

1
git rebase --continue

继续执行 rebase 完成变基。

如果想回到 rebase 执行之前的状态

1
git rebase --abort

3. 使用 rebase 需要注意什么?

rebase 在为理解透彻之前,是不能随便使用的!

但是,有一个黄金定律,只要满足,就可以安全使用,并且比 merge 要好用很多。

只要你的分支上需要 rebase 的所有 commits 历史还没有被 push 过,就可以安全地使用 git rebase 来操作。

也就是说永远不要 rebase 一个已经在中央库中存在的 commit ,只能 rebase 你自己使用的私有 commit

rebase 了一个已经 push 过的 commit 会怎么办呢?

1
2
3
4
5
6
7
8
9
                      master

1 → 2 → 3 → 4


5 → 6
↑ ↑
origin/feature2 feature2

还是这张图,假设 5 已经被 push 到远程,现在 rebasemaster

1
2
3
4
5
6
7
8
                      master         feature2
↓ ↓
1 → 2 → 3 → 4 → 7 → 8


5 → 6

origin/feature2

现在看着没什么问题,但是你查看远程时,发现 5 那个 commit 是可以 pull 下来的。因为本地的 5 已经变成了新的 commit 7 ,本地已经没有 5 的记录了,但是远程有。

如果你在 5 的时候添加的一些文件并 push 到了远程,在 6 的时候删除了那些文件,执行 rebase 后你是可以 pull 的, pull 远程后,你会发现那些删除的文件又回来了!

这里这种问题,可以通过下面的命令,强制提交,不需要 pull 而是直接覆盖中央仓库的历史。(--force)

1
git push -f

这只是本人亲身经历过的一种情况,所以一定要遵循 rebase 黄金定律

4.交互式 rebase -i

偏移符号

在了解交互式 rebase -i 之前,先了解下偏移符号。

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

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

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

下面详细说明。

1
2
3
4
5
6
7
8
9
$ git log --oneline --graph --all
* 1699603 (HEAD -> master) Merge branch 'feature2'
|\
| * 2b21ec9 (feature2) feature2 第二次提交
| * 2fb7de8 feature2 第一次提交
* | 528eb23 (feature1) feature1 第二次提交
* | d4faecf feature1 第一次提交
|/
* 9001713 (origin/master, origin/feature2, origin/feature1, origin/HEAD) 第二次添加

先看一下 ~

1
2
3
4
5
6
7
8
$ git show --oneline HEAD~1
528eb23 (feature1) feature1 第二次提交

$ git show --oneline HEAD~2
d4faecf feature1 第一次提交

$ git show --oneline HEAD~3
9001713 (origin/master, origin/feature2, origin/feature1, origin/HEAD) 第二次添加

然后看 ^

1
2
3
4
5
6
7
8
$ git show --oneline HEAD^
528eb23 (feature1) feature1 第二次提交

$ git show --oneline HEAD^^
d4faecf feature1 第一次提交

$ git show --oneline HEAD^^^
9001713 (origin/master, origin/feature2, origin/feature1, origin/HEAD) 第二次添加

可以看到这里并没有打印到 feature2 的提交,那如何就可以打印了呢?

切换分支 ~

1
2
3
4
5
6
7
8
$ git show --oneline HEAD^2
2b21ec9 (feature2) feature2 第二次提交

$ git show --oneline HEAD^2^
2fb7de8 feature2 第一次提交

$ git show --oneline HEAD^2~1
2fb7de8 feature2 第一次提交

^2 这里没有偏移,只是切换分支。当然,还可以切换引用

1
2
$ git show --oneline feature2^
2fb7de8 feature2 第一次提交

总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 当前提交
HEAD = HEAD~0 = HEAD^0

# 主线回溯(最靠左边的即为主线)
HEAD~1 = HEAD^ 主线的上一次提交
HEAD~2 = HEAD^^ 主线的上二次提交
HEAD~3 = HEAD^^^ 主线的上三次提交

# 如果某个节点有其他分支并入
HEAD^1 主线提交(第一个父提交)
HEAD^2 切换到了第2个并入的分支并得到最近一次的提交
HEAD^2~3 切换到了第2个并入的分支并得到最近第 4 次的提交
HEAD^3~2 切换到了第3个并入的分支并得到最近第 3 次的提交

# ^{n} 和 ^ 重复 n 次的区别
HEAD~1 = HEAD^
HEAD~2 = HEAD^^
HEAD~3 = HEAD^^^

# 切换父级
HEAD^1~3 = HEAD~4
HEAD^2~3 = HEAD^2^^^
HEAD^3~3 = HEAD^3^^^

修改之前的 commit

之前讲过 git commit -—amend 可以 修复/替换 最新 commit 的错误,那倒数第二个,第三个 commit 需要修改怎么办呢?

那就需要用到 rebase -i 了,它是 rebase --interactive 的缩写形式,意为「交互式 rebase」。

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

当前我提交了几个 commit

1
2
3
4
5
6
7
8
*   1699603 (HEAD -> master) Merge branch 'feature2'
|\
| * 2b21ec9 (feature2) feature2 第二次提交
| * 2fb7de8 feature2 第一次提交
* | 528eb23 (feature1) feature1 第二次提交
* | d4faecf feature1 第一次提交
|/
*

但是我发现在 feature1 第二次提交commit 中需要修改一些内容,这时使用 commit --amend 已经晚了。我们需要使用下面的命令

1
git rebase -i HEAD^^

会进入编辑界面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
pick 528eb23 feature1 第二次提交
pick 2fb7de8 feature2 第一次提交
pick 2b21ec9 feature2 第二次提交

# Rebase d4faecf..5b7f84f onto d4faecf (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified). Use -c <commit> 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.

这里可以看到, rebase -i HEAD^^ 和我们之前讲的 git show --oneline HEAD^^ 预期是不一样的。

为什么不一样呢?

rebase -i HEAD^^ 表示列出倒数第三个 commit 之前的提交记录,不包括倒数第三个

git show --oneline HEAD^^ 表示显示倒数第三个 commit 的记录。

又因为这里用了 merge ,在 rebase -i HEAD^ 时,会列出 merge 操作中合并的所有 commit ,这里正好 merge 了两个 commit
所以第一个 ^ 会列出主线倒数第一个 commit,但是这个 commit 是个 merge 操作,然后就去寻找 merge 了那些 commit 并列出 ,第二个 ^ 是主线倒数第二个 commit

这个编辑界面的最顶部,列出了将要「被 rebase」的所有 commit 。需要注意,这个排列是正序的,旧的 commit 会排在上面,新的排在下面。

这两行指示了两个信息:

  1. 需要处理哪些 commit
  2. 怎么处理它们。

每个 commit 默认的操作都是 pick ,表示「直接应用这个 commit」。如果直接退出的话,那么这次就是一次空操作。

各个操作的意思:

  1. pick:直接应用这个 commit
  2. reword:使用此 commit, 编辑 commit messagemessage 就是 commit -m 命令后面写到说明,下同。
  3. edit:使用此 commit,但是不编辑 commit message,保持原有 commit message
  4. squash:使用此 commit,但是合并到前一个 commit 中去
  5. fixup: 和 squash 类似,但是放弃此 commitmessage
  6. drop:删除 commit
  7. 其余的没用过 ~

点击 i 键进行编辑。我们的目的是修改 feature1 第二次提交 ,我们需要把那一行的 pick 改成 edit

1
2
3
edit 528eb23 feature1 第二次提交
pick 2fb7de8 feature2 第一次提交
pick 2b21ec9 feature2 第二次提交

修改完后,按 esc 然后 :wq 这是三个按键依次按,之前我也不懂,但是学习 linux 之后就懂了 ~ 保存退出的意思 ~

1
2
3
4
5
6
7
8
9
10
$ git rebase -i HEAD^^
Stopped at 528eb23... feature1 第二次提交
You can amend the commit now, with

git commit --amend

Once you are satisfied with your changes, run

git rebase --continue

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

修改完成之后,用 commit --amend 来把修正应用到当前最新的 commit

1
2
git add .
git commit --amend

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

1
git rebase --continue

这里可能出现冲突,解决办法之前讲过 ~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ git log --oneline
2d77504 (HEAD -> master) feature2 第二次提交
82d685f feature2 第一次提交
5729656 feature1 第二次提交
d4faecf feature1 第一次提交

$ git log --oneline --graph --all
* 2d77504 (HEAD -> master) feature2 第二次提交
* 82d685f feature2 第一次提交
* 5729656 feature1 第二次提交
| * 2b21ec9 (feature2) feature2 第二次提交
| * 2fb7de8 feature2 第一次提交
| | * 528eb23 (feature1) feature1 第二次提交
| |/
|/|
* | d4faecf feature1 第一次提交
|/
* 9001713 (origin/master, origin/feature2, origin/feature1, origin/HEAD) 第二次添加

再次查看,log 发现后三个 commitSHA-1 值都变了,说明是新的 commit 。查看图表会更明确一点,之前的 commit 都还在但是 merge 的那个 commit 不见了,子分支的 commit 的内容,都在主分支都复制了一份,说明使用 rebase -i 不仅可以执行我们制定的操作,还会重新组织历史记录合并在一条主线上,然后清理无用的 commit

删除之前的 commit

和修改的炒作类似,只是在编辑的时候删除我们需要删除的那个 commit 即可

1
2
3
pick 528eb23 feature1 第二次提交
pick 2fb7de8 feature2 第一次提交
pick 2b21ec9 feature2 第二次提交

删掉 feature1 第二次提交,或者将 pick 改为 drop

1
2
pick 2fb7de8 feature2 第一次提交
pick 2b21ec9 feature2 第二次提交

如果把这一行删掉,那就相当于在 rebase 的过程中跳过了这个 commit,从而也就把这个 commit 撤销掉了。

那这两种操作有什么不同呢? emmm…. 表面上也没啥区别,具体实质性的却别就不清楚了 ~
Git rebase interactive drop vs deleting the commit line

剩下的操作和上面一样。

5. rebase –onto

我们之前讲过 git rebase masterrebase 的「起点」是自动判定的:选取当前 commit 和目标 commit 在历史上的交叉点作为起点。

1
2
3
4
5
6
7
8
                      master

1 → 2 → 3 → 4


5 → 6

HEAD/feature2

比如这里执行 git rebase master ,那么 Git 会自动选取 46 的历史交叉点 2 作为 rebase 的起点,依次将 56 重新提交到 4 的路径上去。

--onto 参数,就可以额外给 rebase 指定它的起点。例如同样以上图为例,如果我只想把 6 提交到 4 上,不想附带上 5,那么我可以执行:

1
git rebase --onto 4(commit) 5(commit) feature2

--onto 参数后面有三个附加参数:目标 commit、起点 commit(注意:rebase 的时候会把起点排除在外)、终点 commit

1
2
3
4
5
6
                      master HEAD/feature2
↓ ↓
1 → 2 → 3 → 4 → 7(6的内容)


5 → 6

还可以撤销提交:

1
2
3
4
5
6
$ git log --oneline --graph --all
* 0b6cb82 (HEAD -> feature2) add
* b8b5146 add new File
* 8b2c2f7 new File
* e657879 rebase
* 99854db feature

这是生成的一点测试记录,执行下面的命令:

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

上面的意思为,以倒数第二个 commit 为起点(不包括起点),feature2 指向的 commit 为终点,rebase 到倒数第四个 commit 上, 结果:

1
2
3
4
$ git log --oneline --graph --all
* 3a10357 (HEAD -> feature2) add
* e657879 rebase
* 99854db feature

HEAD^^^ 目标 commit 之后的 commit 都会被清除掉,起点(HEAD^) 到 终点(feature2) 之间的 commit (不包括 起点HEAD^ 本身),会提前复制出一份生成新的 commit,最后连接到目标 commit 后面。
相当于 目标(HEAD^^^) 到 起点(HEAD^) 之间的 commit 都会被删除,不包括目标 commit ,包括起点 commit

九 reset(重置)

reset 的本质:移动 HEAD 以及它所指向的 branch

这样就可以起到撤销某个 commit 的作用 ,不是删除,只要记下 SHA-1 还可以再撤回来 ~

1
git reset --hard HEAD^

偏移符号之前讲过,如果移动后想回往回移动,但是已经没有了 SHA-1,可以使用 git reflog 来查看操作记录。

Git 的历史只能往回看,不能向未来看,所以把 HEADbranch 往回移动,就能起到撤回 commit 的效果。

所以同理,reset --hard 不仅可以撤销提交,还可以用来把 HEADbranch 移动到其他的任何地方。

reset –hard:重置工作目录

reset --hard 会在重置 HEADbranch 的同时,重置工作目录里的内容。当你在 reset 后面加了 --hard 参数时,你的工作目录里的内容会被完全重置为和 HEAD 的新位置相同的内容。换句话说,就是未提交(commit)的修改会被全部擦除,不管它们是否被放进暂存区。添加到暂存区的新增也会被擦除,但是没有添加到缓存区的新增不会!

modifiedFile.txtaddModifiedFile.txt 是当前 commit 新增的(证明已经 commit 上去了),
然后修改 addModifiedFile.txt 文件并添加缓存区,然后修改 modifiedFile.txt 文件,不添加到缓存区。这里表示修改操作添加暂存区和不添加暂存区的情况。
添加 newFile.txtaddNewFile.txt 并把 addNewFile.txt 添加到缓存区。这里表示新增操作添加暂存区和不添加暂存区的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ git status
On branch feature2
Your branch and 'origin/feature2' have diverged,
and have 1 and 2 different commits each, respectively.
(use "git pull" to merge the remote branch into yours)

Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

new file: addNewFile.txt
modified: addModifiedFile.txt

Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)

modified: modifiedFile.txt

Untracked files:
(use "git add <file>..." to include in what will be committed)

newFile.txt

然后执行:

1
git reset --hard HEAD^

工作目录里的新改动也一起全都消失了,不管它们是否被放进暂存区
只有没有添加到缓冲区新创建的文件,不会清除,如下:

1
2
3
4
5
6
7
8
9
10
11
12
$ git status
On branch feature2
Your branch is behind 'origin/feature2' by 2 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)

Untracked files:
(use "git add <file>..." to include in what will be committed)

newFile.txt

nothing added to commit but untracked files present (use "git add" to track)

只留下了新创建的并且没有添加缓存区addHard 文件。其他的修改和添加都被清除了。

reset –soft:保留工作目录

reset --soft 会在重置 HEADbranch 时,保留工作目录和暂存区中的内容,并把重置 HEAD 所带来的新的差异放进暂存区。

「重置 HEAD 所带来的新的差异」 就是当前 HEADreset 目标之间的 commit 提交的文件。

初始文件状态和上面一样。

执行

1
git reset --soft HEAD^

重点观看 hard.txtreset.txt ,它俩被重新添加到暂存区,并且标记 new file 。这两个文件就是两个 commit 的差异。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ git status
On branch feature2
Your branch is behind 'origin/feature2' by 2 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)

Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

new file: addModifiedFile.txt
new file: addNewFile.txt
new file: modifiedFile.txt

Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)

modified: modifiedFile.txt

Untracked files:
(use "git add <file>..." to include in what will be committed)

newFile.txt

这就是 --soft--hard 的区别:--hard 会清空暂存目录的改动和新增,以及工作目录的改动,而 --soft 则会保留工作目录的内容,并把因为保留工作目录内容所带来的新的文件差异放进暂存区。

reset 不加参数:保留工作目录,并清空暂存区(–mixed)

reset 如果不加参数,那么默认使用 --mixed 参数。它的行为是:保留工作目录,并且清空暂存区。

初始文件状态和上面一样。

执行

1
git reset HEAD^

将差异文件、暂存目录的文件、工作目录的文件都保存了下来,但是都放在了工作目录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ git status
On branch feature2
Your branch is behind 'origin/feature2' by 2 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)

Untracked files:
(use "git add <file>..." to include in what will be committed)

addModifiedFile.txt
addNewFile.txt
modifiedFile.txt
newFile.txt

nothing added to commit but untracked files present (use "git add" to track)

十 stash(临时存储)

"stash" 这个词,和它意思比较接近的中文翻译是「藏匿」,是「把东西放在一个秘密的地方以备未来使用」的意思。

Git 中,stash 指令可以帮你把工作目录的内容全部放在你本地的一个独立的地方,它不会被提交,也不会被删除,你把东西放起来之后就可以去做你的临时工作了,做完以后再来取走,就可以继续之前手头的事了。

具体说来,stash 的用法很简单。当你手头有一件临时工作要做,需要把工作目录暂时清理干净,那么可以:

1
git stash

现在工作目录的改动就被清空了,所有改动都被存了起来。

当手头工作做完后,

1
git stash pop

之前的代码又回来了!

注意:没有被 track 的文件(即从来没有被 add 过的文件不会被 stash 起来,因为 Git 会忽略它们。如果想把这些文件也一起 stash,可以加上 -u 参数,它是 --include-untracked 的简写。就像这样:

十一 tag(引用)

添加 Tag

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

1
$ git tag -a v1.1 -m "my Tag 1.1"

这是在当前 commit 创建一个 名为 v1.1 的标签。-m 选项指定了一条将会存储在标签中的信息。 如果没有为附注标签指定一条信息,Git 会启动编辑器要求你输入信息。

如何给之前的 commit 添加 tag 呢?

1
$ git tag -a v1.0 81bb698 -m "my Tag 1.0"

只需要在标签名后面加入 commitSHA-1 校验和 。

查看 Tag

1
2
3
$ git log --oneline
347cd98 (HEAD -> master, tag: v1.1, origin/master, origin/HEAD) head
81bb698 (tag: v1.0) c

也可以使用如下只查看标签

1
2
3
$ git tag
v1.0
v1.1

通过使用 git show 命令可以看到标签信息和与之对应的提交信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ git show v1.1
tag v1.1
Tagger: companyPc <lvyanv@163.com>
Date: Fri Oct 9 10:59:57 2020 +0800

my Tag 1.1

commit 347cd9856e1edf4c0a96cb15bad227ab8d255cda (HEAD -> master, tag: v1.1, origin/master, origin/HEAD)
Author: companyPc <lvyanv@163.com>
Date: Tue Sep 29 18:27:18 2020 +0800

head

diff --git a/branch1.txt b/branch1.txt
index b3a843b..b9176c0 100644
--- a/branch1.txt
+++ b/branch1.txt
@@ -4,7 +4,7 @@
11
11
11
-
+22

输出显示了打标签者的信息、打标签的日期时间、附注信息,然后显示具体的提交信息。

删除 Tag

1
2
$ git tag -d v1.0
Deleted tag 'v1.0' (was a390a5f)

注意上述命令并不会从任何远程仓库中移除这个标签,通过如下命令可以更新远程分支情况。

1
$ git push origin : refs/tags/v1.0

第二种更直观的删除远程标签的方式是:

1
$ git push origin --delete v1.0

检出标签

tag 创建后,就相当于一个应用,可以同个 checkout 检出到当前标签的位置 ~

1
2
3
4
5
6
7
8
9
10
11
12
13
$ git checkout v1.0
Note: checking out 'v1.0'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

git checkout -b <new-branch-name>

HEAD is now at 81bb698 c

十二 Gitflow 工作流

Git-flowGit 的包装器

DevelopMaster 分支

master

  • 主分支 , 产品的功能全部实现后 , 最终在 master 分支对外发布
  • 该分支为只读唯一分支 , 只能从其他分支 (release/hotfix) 合并 , 不能在此分支修改
  • 另外所有在 master 分支的推送应该打标签做记录,方便追溯
  • 例如 release 合并到 master , 或 hotfix 合并到 master

develop

  • 主开发分支 , 基于 master 分支克隆
  • 包含所有要发布到下一个 release 的代码
  • 该分支为只读唯一分支 , 只能从其他分支合并
  • feature 功能分支完成 , 合并到 develop (不推送)
  • develop 拉取 release 分支 , 提测
  • release/hotfix 分支上线完毕 , 合并到 develop 并推送

图片来源于文末链接中的 Gitflow

这个工作流使用两个分支来记录项目的历史,而不是单一的 mastermaster 存储官方发布历史记录,而 develop 分支充当功能的集成分支。用版本号标记 master 中的所有提交也很方便。

第一步是用一个 develop 分支来补充默认的 master 。一个简单的方法是一个开发人员在本地创建一个空的 develop 分支,并将其推到服务器:

1
2
git branch develop 
git push -u origin develop

该分支将包含项目的完整历史记录,而master将包含简化版本。现在,其他开发人员现在应该 clone 中央存储库,并为 develop 创建 tracking 分支。

当使用 git-flow 扩展库时,在已有的 repo 上执行 git flow init 将创建开发分支:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ git flow init

Which branch should be used for bringing forth production releases?
- develop
- master
Branch name for production releases: [master]

Which branch should be used for integration of the "next release"?
- develop
Branch name for "next release" development: [develop]

How to name your supporting branch prefixes?
Feature branches? [] feature/
Bugfix branches? [] bugfix/
Release branches? [] release/
Hotfix branches? [] hotfix/
Support branches? [] support/
Version tag prefix? [] version/
Hooks and filters directory? [C:/Users/Beepay/Desktop/新建文件夹/ui-starge/.git/hooks]

git flow init 命令是缺省 git init 命令的扩展,除了为您创建分支外,它不会更改存储库中的任何内容。

设置前缀的时候加上 / 相当于分组了。试一试下面的过程就知道啦。

如果使用 Sourcetree ,也可以点击工具中右上角 Git 工作流 切换到 git flow 工作流。

之后的操作就是弹框中的,之后就不一一列出 Sourcetree 的操作了。

Feature(功能) 分支

feature

  • 功能开发分支 , 基于 develop 分支克隆 , 主要用于新需求新功能的开发
  • 功能开发完毕后合到 develop 分支(未正式上线之前不推送到远程中央仓库!!!)
  • feature 分支可同时存在多个 , 用于团队中多个功能同时开发 , 属于临时分支 , 功能完成后可选删除

每一个新功能的开发都应该各自使用独立的分支,可以推送到中央存储库进行备份/协作。但是,feature 分支使用 develop 作为它们的父分支,而不是 master 分支。当一个 功能 完成时,它会被合并回到 feature 中。feature 不应该直接与 master 交互。

图片来源于文末链接中的 Gitflow

注意:组合使用 feature 分支和 develop 分支的这种设计,其实完全就是 Feature Branch Workflow的理念。然而,Gitflow 流程并不止于此。且看下文分解。

创建一个工作分支:

1
2
git checkout develop 
git checkout -b feature_branch

使用 git-flow 扩展时:(这里注意 init 时的前缀加 / )

1
git flow feature start feature_branch

执行这个命令不需要切换分支,会自动已本地 develop 最新 commit 为基点, 之后像往常一样使用继续 Git 就可以。

完成/合并 一个工作分支:

(执行之前记得 add 以及 commit 更新的内容)当完成了 feature 的开发工作后,下一步是将 feature_branch 合并到 develop 中。

1
2
3
git checkout develop 
git merge feature_branch
git branch -d feature_branch

使用 git-flow 扩展时:

1
git flow feature finish feature_branch

Release 分支

release

  • 测试分支 , 基于 feature 分支合并到 develop 之后 , 从 develop 分支克隆
  • 主要用于提交给测试人员进行功能测试 , 测试过程中发现的 BUG 在本分支进行修复 , 修复完成上线后合并到 develop/master 分支并推送(完成功能) , 打Tag
  • 属于临时分支 , 功能上线后可选删除

图片来源于文末链接中的 Gitflow

一旦 develop 分支积聚了足够多的新功能(或者预定的发布日期临近了),你可以基于 develop 分支建立一个用于产品发布的分支。这个分支的创建意味着一个发布周期的开始,也意味着本次发布不会再增加新的功能,在这个分支上只能修复 bug ,做一些文档工作或者跟发布相关的任务。在一切准备就绪的时候,这个分支会被合并入 master ,并且用版本号打上 tag
另外,release 分支上的改动还应该合并入 develop 分支,在发布周期内,develop 分支仍然在被使用(一些开发者会把其他功能集成到 develop 分支)。

使用专门的一个分支来为发布做准备的好处是,在一个团队忙于当前的发布的同时,另一个团队可以继续为接下来的一次发布开发新功能。

创建 release 分支是另一个简单的分支操作。与 feature 分支一样,release 分支也基于 develop 分支。可以使用以下方法创建一个新的 release 分支。

1
2
git checkout develop 
git checkout -b release/0.1.0

使用 git-flow 扩展时:(这里注意 init 时的前缀加 / )

1
git flow release start 0.1.0

要 完成/合并 一个发布分支(执行之前记得 add 以及 commit 更新的内容),使用以下方法:

1
2
3
4
5
git checkout master 
git merge release/0.1.0
git checkout develop
git merge release/0.1.0
git branch -d release/0.1.0

使用 git-flow 扩展时:

1
git flow release finish '0.1.0' -m "my version 0.1.0"

这里可以不写 -m ,但是在合并 develop 的时候弹出的编辑页面需要再次写入提交信息,否者会导致提交失败。如果写了 -m 只需要两次 :wq 即可。

Hotfix 分支

hotfix

  • 补丁分支 , 基于 master 分支克隆 , 主要用于对线上的版本进行 BUG 修复
  • 修复完毕后合并到 develop/master 分支并推送 , 打 Tag
  • 属于临时分支 , 补丁修复上线后可选删除
  • 所有 hotfix 分支的修改会进入到下一个 release

图片来源于文末链接中的 Gitflow

hotfix 分支用于快速对生产版本进行补丁。hotfix 分支很像 release 分支和 feature 分支,除了 hotfix 是基于 master 分支而不是 develop。这是唯一一个从主分支 fork 的。一旦修复完成,它应该被合并到 masterdevelop (或者当前的 release 分支)中,并且 master 应该被标记为一个更新的版本号。

拥有专门的 bug 修复开发线可以让团队解决问题同时,并且不会中断其余的工作流程或等待下一个发布周期。

可以将维护分支视为直接与 master一起工作的临时 release 分支。

创建 hotifx 分支

1
2
git checkout master 
git checkout -b hotfix_branch

使用 git-flow 扩展时:(这里注意 init 时的前缀加 / )

1
git flow hotfix start hotfix_branch

完成/合并 一个 hotfix 分支(执行之前记得 add 以及 commit 更新的内容)

1
2
3
4
5
git checkout master 
git merge hotfix_branch
git checkout develop
git merge hotfix_branch
git branch -d hotfix_branch

使用 git-flow 扩展时:

1
git flow hotfix finish hotfix_branch  -m "my hotfix commit"

这里可以不写 -m ,但是在合并 develop 的时候弹出的编辑页面需要再次写入提交信息,否者会导致提交失败。如果写了 -m 只需要两次 :wq 即可。

Gitflow 的总体流程为:

  1. master 创建一个 develop分支
  2. releasedevelop 分支创建
  3. featuredevelop 分支创建
  4. feature完成后,会合并到develop 分支
  5. release分支完成后,会合并到developmaster
  6. 如果master检测到问题,则从 master 创建 hotfix 程序分支
  7. hotfix完成后,会被合并到两个developmaster

感谢:

Gitflow

Git

git HEAD / HEAD^ / HEAD~ 的含义

git rebase vs git merge详解

Git 原理详解及实用指南

更多:

Git+Gerrit如何永久删除历史文件(大文件/私密文件)