前言

一直在使用git做版本控制,也一直工作很顺利,直到和别人发生冲突的时候。这才注意到git 工作流并不是那么简单。比如,之前遇到的清理历史。百度到的资料很多,重复性也很多,但实践性操作很少,我很难直接理解其所表达的含义。直接望文生义经常得到错误的结论,只能用时间去检验真理了,不然看到的结果都是似懂非懂,最后还是一团糟。

学习git工作流

1. 最简单的使用,不推荐

1.1.创建仓库

$ pwd
/home/ryan/workspace/l4git-workflow
$ touch readme.md
$ ls
readme.md
$ touch .gitignore
$ git init
初始化空的 Git 仓库于 /home/ryan/workspace/l4git-workflow/.git/
$ touch test.txt
$ git add .
$ git commit -m "init"
[master (根提交) dae77d6] init
3 files changed, 12 insertions(+)
create mode 100644 .gitignore
create mode 100644 readme.md
create mode 100644 test.txt
$ git remote add origin git@github.com:Ryan-Miao/l4git-workflow.git
$ git push -u origin master
对象计数中: 5, 完成.
Delta compression using up to 4 threads.
压缩对象中: 100% (3/3), 完成.
写入对象中: 100% (5/5), 388 bytes | 0 bytes/s, 完成.
Total 5 (delta 0), reused 0 (delta 0)
To git@github.com:Ryan-Miao/l4git-workflow.git
* [new branch] master -> master
分支 master 设置为跟踪来自 origin 的远程分支 master。

1.2. 模拟用户A

git clone git@github.com:Ryan-Miao/l4git-workflow.git
git checkout a
touch a.txt
//write one
//....
$ git add .
$ git commit -m "one"
[a 53ff45e] one
2 files changed, 34 insertions(+), 2 deletions(-)
create mode 100644 a.txt

此时,a还没有提交到origin。 git log 如下:

1.3. 模拟用户B

git clone git@github.com:Ryan-Miao/l4git-workflow.git
git checkout b
$ touch b.txt

//write something

//...

$ git add .
$ git commit -m "b write one"
[b 847078e] b write one
1 file changed, 1 insertion(+)
create mode 100644 b.txt

//write something

//....

$ git add .
$ git commit -m "b write two"
[b 3f30f41] b write two
1 file changed, 2 insertions(+), 1 deletion(-)

此时,git log如下

1.4. 模拟用户A

A和B分别是在本地开发,所以这种顺序是未知的,也许A比B先commit一次,也许B先commit一次。这里的先后是指commit的时间戳。但都是在本地提交的代码。

write something

git add .
git commit -m "a write two"

wirte something

git add .
git commit -m "write three"

A push to server branch a

$ git push origin a:a
Total 0 (delta 0), reused 0 (delta 0)
To git@github.com:Ryan-Miao/l4git-workflow.git
* [new branch] a -> a

A created a Pull Request



1.5. 模拟用户C

C review the PR and then merged it.



此时,github的历史如下:



可以看出,merge的时候多了一次commit,message默认为 Merge pull request #1 from Ryan-Miao/a...

现在看起来,只有a一个人的历史记录,还算清楚,a做了3次提交。

1.6. 模拟用户B

用户B提交前先pull master,更新最新的代码到本地,防止冲突。

 git fetch
git merge origin/master

此时log看起来有点乱。如下:



让人感到混乱的是b原来的历史只有自己的提交,更新了master到本地之后,历史记录被插入了master中的历史。于是,发现原来自己干净的历史被中间插入多次commit。甚至两次merge master的日志显得又长又碍眼。但不管怎么说,B还是要提交的。

于是,B提交到远程分支b:

1.7. 模拟用户C

这时候,A完成了feature a,然后提了PR,然后找他人C merge了。而后,B也完成了feature b,提了PR,需要review and merge。 C review之后,approved, 然后D review, D merge。

此时,项目基本走上正规。feature一个一个添加进去,重复之前的工作流程: fetch -》 work -》 commit -》 push -》 PR -》 merged。

然后,项目历史就变成了这样:

一眼大概看起来还好,每次都能看到提交历史,只要不是message写的特别少,差不多可以理解最近提交的内容。然而,仔细一看,顺序好像不对。目前一共两个feature,但历史却远远超过2个。没关系,保证细粒度更容易体现开发进度。然而,这些历史并不是按照feature的发布顺序,那么,当我想要找到feature a的时候就很难串联起来。如果commit足够多,时间跨度足够大,甚至根本看不出来feature a到底做了哪些修改。

这时候想要使用图形化git 历史工具来帮助理解历史:

这里,还好,还勉强能看出走向。但当10个上百个人同时开发的话,线简直不能看了,时间跨度足够大的话,线也看不完。

因此,这种模式,正是我们自己当前采用的模式。差评。这还不算完,后面更大的困难来了。最先发布的feature a出了问题,必须回滚。怎么做到。关于回滚,就是另一个话题了。 但我们应该知道使用revert而不是reset. 但revert只能回滚指定的commit,或者连续的commit,而且revert不能revert merge操作。这样,想回滚feature a, 我们就要找到a的几次提交的版本号,然后由于不是连续的,分别revert。这会造成复杂到不想处理了。好在github给了方便的东西,PR提供了revert的机会。找到以前的PR。

但是,这绝对不是个好操作!


2. 推荐的工作流程

造成上述现象的原因是因为各自异步编程决定的。因为每个人都可以随时间提交,最后合并起来的时候以提交时间戳来作为序列的依据,就会变成这样。因此,当需要提交的远程服务器的时候,如果能重写下commit的时间为当前时间,然后push到服务端,历史就会序列到最后了。

2.1 模拟用户C

C用户新下载代码。

$ git clone git@github.com:Ryan-Miao/l4git-workflow.git c正克隆到 'c'...
remote: Counting objects: 28, done.
remote: Compressing objects: 100% (17/17), done.
remote: Total 28 (delta 8), reused 22 (delta 4), pack-reused 0
接收对象中: 100% (28/28), 5.90 KiB | 0 bytes/s, 完成.
处理 delta 中: 100% (8/8), 完成.
检查连接... 完成。

然后编辑,提交

$ cd c
$ git config user.name "C"
$ ls
a.txt b.txt readme.md test.txt
$ vim c.txt
$ git add .
$ git commit -m "C write one"
[master cf3f757] C write one
1 file changed, 2 insertions(+)
create mode 100644 c.txt

2.2 模拟用户D

同时,D也需要开发新feature

$ git clone git@github.com:Ryan-Miao/l4git-workflow.git d正克隆到 'd'...
remote: Counting objects: 28, done.
remote: Compressing objects: 100% (17/17), done.
remote: Total 28 (delta 8), reused 22 (delta 4), pack-reused 0
接收对象中: 100% (28/28), 5.90 KiB | 0 bytes/s, 完成.
处理 delta 中: 100% (8/8), 完成.
检查连接... 完成。
$ cd d
/d$ git config user.name "D"
/d$ vim d.txt
/d$ git add .
/d$ git commit -m "d write one"
[master db7a6e9] d write one
1 file changed, 1 insertion(+)
create mode 100644 d.txt

2.3 C继续开发

$ vim c.txt
$ git add .
$ git commit -m "c write two"
[master 01b1210] c write two
1 file changed, 1 insertion(+)

2.4 D继续开发

/d$ vim d.txt
/d$ git add .
/d$ git commit -m "d write two"
[master a1371e4] d write two
1 file changed, 1 insertion(+)

2.5 C 提交

$ vim c.txt
$ git add .
$ git commit -m "c write three"
[master 13b7dde] c write three
1 file changed, 1 insertion(+)

C开发结束,提交到远程

$ git status
位于分支 master
您的分支领先 'origin/master' 共 3 个提交。
(使用 "git push" 来发布您的本地提交)
无文件要提交,干净的工作区
$ git push origin master:C
对象计数中: 9, 完成.
Delta compression using up to 4 threads.
压缩对象中: 100% (6/6), 完成.
写入对象中: 100% (9/9), 750 bytes | 0 bytes/s, 完成.
Total 9 (delta 3), reused 0 (delta 0)
remote: Resolving deltas: 100% (3/3), completed with 1 local object.
To git@github.com:Ryan-Miao/l4git-workflow.git
* [new branch] master -> C

2.6 C 提PR

然后,create a Pull Request.

2.7 C修改再push

然后,发现还有个bug要修复,再次修改提交到远程C

$ vim c.txt
$ git add .
$ git commit -m "C finish something else"
[master 2c5ff94] C finish something else
1 file changed, 1 insertion(+), 1 deletion(-)
$ git push origin master:C
对象计数中: 3, 完成.
Delta compression using up to 4 threads.
压缩对象中: 100% (3/3), 完成.
写入对象中: 100% (3/3), 301 bytes | 0 bytes/s, 完成.
Total 3 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To git@github.com:Ryan-Miao/l4git-workflow.git
13b7dde..2c5ff94 master -> C

2.8 C发现提交次数过多,历史太乱,合并部分历史

这时,发现一个问题,由于C在开发过程中提交了多次,而这几次提交的message其实没有多大意思,只是因为C可能为了保存代码,也可能是暂存。总之,C的前3次提交的message的含义其实是一样的,都是创建C文件,都是一个主题,那么为了维护历史的干净。最好把这3条信息合并成一条C create file c.txt

参考git 合并历史,我们需要将3次历史合并成显示为一次。

查看git历史,找到需要合并的起始区间

$ git log --oneline
2c5ff94 C finish something else
13b7dde c write three
01b1210 c write two
cf3f757 C write one
7151f4c 记录操作。
0bfe562 Merge pull request #2 from Ryan-Miao/b_remote
d81ce20 Merge remote-tracking branch 'origin/master' into b
2d74cfb Merge pull request #1 from Ryan-Miao/a
b90a3dd write three
4b1629e a write two
3f30f41 b write two
847078e b write one
53ff45e one
dae77d6 init

显然,是要合并cf3f75713b7dde。那么找到前一个的版本号为7151f4c

git rebase - i 7151f4c

然后进入交互界面,因为我们想要把第3次和第2次以及第1次提交信息合并。将第3次的类型修改为squash, 意思是和第2次合并。然后将第2次的类型修改为squash, 同样是指合并的前一个commit。



不同git的交互略有不同,之前在windows上的git bash是完全按照vim的命令修改的。本次测试基于Ubuntu,发现存档命令为ctel + X。确认后进入下一个界面,合并3次提交后需要一个message

删除或者anyway you like, 更改message。存档。完成。

$ git rebase -i 7151f4c
[分离头指针 e3764c5] c create file c.txt
Date: Fri Oct 20 22:06:24 2017 +0800
1 file changed, 4 insertions(+)
create mode 100644 c.txt
Successfully rebased and updated refs/heads/master.

Tips

当在rebase过程中出现了失误,可以使用git rebase --abort返回初始状态。如果发现冲突,则可以解决冲突,然后git rebase --continue .

好像已有 rebase-merge 目录,我怀疑您正处于另外一个变基操作

过程中。 如果是这样,请执行

git rebase (--continue | --abort | --skip)

如果不是这样,请执行

rm -fr "/home/ryan/temp/c/.git/rebase-merge"

然后再重新执行变基操作。 为避免丢失重要数据,我已经停止当前操作。

此时,查看log, 显然,C的那三次提交已经合并了。

$ git log --oneline
50b9fe9 C finish something else
e3764c5 c create file c.txt
7151f4c 记录操作。
0bfe562 Merge pull request #2 from Ryan-Miao/b_remote
d81ce20 Merge remote-tracking branch 'origin/master' into b
2d74cfb Merge pull request #1 from Ryan-Miao/a
b90a3dd write three
4b1629e a write two
3f30f41 b write two
847078e b write one
53ff45e one
dae77d6 init

2.9 C再次push

之前的push已经不能用了。需要开新分支推送过去。因为 rebase 只能在本地分支做。不要修改公共分支

$ git push origin master:C
To git@github.com:Ryan-Miao/l4git-workflow.git
! [rejected] master -> C (non-fast-forward)
error: 无法推送一些引用到 'git@github.com:Ryan-Miao/l4git-workflow.git'
提示:更新被拒绝,因为推送的一个分支的最新提交落后于其对应的远程分支。
提示:检出该分支并整合远程变更(如 'git pull ...'),然后再推送。详见
提示:'git push --help' 中的 'Note about fast-forwards' 小节。

选择推送的新分支C2

$ git push origin master:C2
对象计数中: 6, 完成.
Delta compression using up to 4 threads.
压缩对象中: 100% (5/5), 完成.
写入对象中: 100% (6/6), 569 bytes | 0 bytes/s, 完成.
Total 6 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), completed with 1 local object.
To git@github.com:Ryan-Miao/l4git-workflow.git
* [new branch] master -> C2

创建新的PR

2.10 新的merge方式: rebase

通过开始的普通流程发现,每次merge的时候,都会多出一条新的提交信息,这让历史看起来很奇怪。那么,可以选择rebase到master,变基,就是重新以master为基本,把当前的提交直接移动到master的后面。不会因为提交时间的离散导致多次commit的message被拆散。 选择 rebase and merge

这时候,可以看到C提交的两次信息都是最新的,没有发生交叉。而且也没有产生多余的merge信息。

有人会问,那么岂不是看不到PR的地址了。点开C的历史。可以看到message下方是有PR的编号的:

对了,刚开始的PR要记得close

2.11 这时候D也完成了

/d$ git push origin master:D
对象计数中: 10, 完成.
Delta compression using up to 4 threads.
压缩对象中: 100% (7/7), 完成.
写入对象中: 100% (10/10), 4.49 KiB | 0 bytes/s, 完成.
Total 10 (delta 2), reused 4 (delta 1)
remote: Resolving deltas: 100% (2/2), completed with 1 local object.
To git@github.com:Ryan-Miao/l4git-workflow.git
* [new branch] master -> D

提PR, 这时候,如果采用merge:

结果必然发现,1) d提交message被按照时间分散插入历史了(被插入到c的历史之前), 2)多了一次 Merge pull request #5 from Ryan-Miao/D..的提交信息。同开头所述一样,历史开始变得混乱了。那么,这种问题怎么办呢?

2.12 提交前rebase

就像C rebase后merge到master一样。我们一样可以在本地做到这样的事情。在本地rebase,让我们本次feature的提交全部插到master节点之后,有序,而且容易revert。

本次,以新的E和F交叉commit为例子,最终将得到各自分开的历史

E:

$ git clone git@github.com:Ryan-Miao/l4git-workflow.git e
正克隆到 'e'...
remote: Counting objects: 52, done.
remote: Compressing objects: 100% (33/33), done.
remote: Total 52 (delta 18), reused 36 (delta 7), pack-reused 0
接收对象中: 100% (52/52), 7.91 KiB | 0 bytes/s, 完成.
处理 delta 中: 100% (18/18), 完成.
检查连接... 完成。 $ cd e
/e$ vim e.txt
/e$ git add .
/e$ git config user.name "E"
/e$ git commit -m "e commit one"
[master 77ecd73] e commit one
1 file changed, 1 insertion(+)
create mode 100644 e.txt

F:

$ git clone git@github.com:Ryan-Miao/l4git-workflow.git f
正克隆到 'f'...
remote: Counting objects: 52, done.
remote: Compressing objects: 100% (33/33), done.
remote: Total 52 (delta 18), reused 36 (delta 7), pack-reused 0
接收对象中: 100% (52/52), 7.91 KiB | 0 bytes/s, 完成.
处理 delta 中: 100% (18/18), 完成.
检查连接... 完成。 $ cd f
$ vim f.txt
$ git config user.name "F"
$ git add .
$ git commit -m "d write one"
[master b41f8c5] d write one
1 file changed, 2 insertions(+)
create mode 100644 f.txt

E:

/e$ vim e.txt
/e$ git add .
/e$ git commit -m "e write two"
[master 2b8c9fb] e write two
1 file changed, 1 insertion(+)

F:

$ vim f.txt
$ git add .
$ git commit -m "f write two"
[master de9051b] f write two
1 file changed, 1 insertion(+)

E:

/e$ vim e.txt
/e$ git add .
/e$ git commit -m "e write three"
[master b1b9f6e] e write three
1 file changed, 2 insertions(+)

这时候,e完成了,需要提交。提交前先rebase:

/e$ git fetch
/e$ git rebase origin/master
当前分支 master 是最新的。

然后,再提交

/e$ git push origin master:E
对象计数中: 9, 完成.
Delta compression using up to 4 threads.
压缩对象中: 100% (6/6), 完成.
写入对象中: 100% (9/9), 753 bytes | 0 bytes/s, 完成.
Total 9 (delta 3), reused 0 (delta 0)
remote: Resolving deltas: 100% (3/3), completed with 1 local object.
To git@github.com:Ryan-Miao/l4git-workflow.git
* [new branch] master -> E

然后, PR, merge.

同样F:

$ git status
位于分支 master
您的分支领先 'origin/master' 共 2 个提交。
(使用 "git push" 来发布您的本地提交)
无文件要提交,干净的工作区
$ git fetch
remote: Counting objects: 12, done.
remote: Compressing objects: 100% (6/6), done.
remote: Total 12 (delta 6), reused 6 (delta 3), pack-reused 0
展开对象中: 100% (12/12), 完成.
来自 github.com:Ryan-Miao/l4git-workflow
24c6818..f36907c master -> origin/master
* [新分支] E -> origin/E
$ git rebase origin/master
首先,回退分支以便在上面重放您的工作...
应用:d write one
应用:f write two $ git push origin master:F
对象计数中: 6, 完成.
Delta compression using up to 4 threads.
压缩对象中: 100% (4/4), 完成.
写入对象中: 100% (6/6), 515 bytes | 0 bytes/s, 完成.
Total 6 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), completed with 1 local object.
To git@github.com:Ryan-Miao/l4git-workflow.git
* [new branch] master -> F

PR, rebase and merge。 这时候看history:

按照前几次的做法,E和F交叉在本地提交,每次commit的时间戳也是交叉,最终合并到master的时候,历史并没有被拆散。而是像我们期待的一样,顺序下来。这才是我们想要的。通过看图形化界面也能看出区别:

绿色的线是master

那么,操作便是fetch-》rebase。事实上,可以二合一为:

git pull --rebase origin master

最终结果

在都没提交到server的时候, 历史是分散在各个开发者的本地,但commit时间有先后。

按照rebase的用法,提交前rebase一次,就可以使得一个feature的提交串联到一起

最终在github的commit看起来也就是顺畅的多

金科玉律

  1. 想维持树的整洁,方法就是:在git push之前,先git fetch,再git rebase。
git fetch origin master
git rebase origin/master
git push

或者

git pull --rebase origin master

只要你把变基命令当作是在推送前清理提交使之整洁的工具,并且只在从未推送至共用仓库的提交上执行变基命令,就不会有事。 假如在那些已经被推送至共用仓库的提交上执行变基命令,并因此丢弃了一些别人的开发所基于的提交,那你就有大麻烦了,你的同事也会因此鄙视你。

如果你或你的同事在某些情形下决意要这么做,请一定要通知每个人执行 git pull --rebase 命令,这样尽管不能避免伤痛,但能有所缓解。

  1. 绝对不要在公共(远程分支)上rebase,也就是说,如果没有必要就不要在github merge的时候选择rebase,而是用上述的办法,在本地自己的分支推送前rebase
  2. 绝对不可以在公共分支上reset,也不要用--force
  3. 单独功能的多次提交要学会合并提交,保持提交的简洁。
  4. 提交message尽量能概括修改内容。

参考来源

Git 工作流的正确打开方式的更多相关文章

  1. Pull Request的正确打开方式(如何在GitHub上贡献开源项目)

    Pull Request的正确打开方式(如何在GitHub上贡献开源项目) GitHub的官方帮助如下: Fork A Repo: https://help.github.com/articles/f ...

  2. iOS开发小技巧--相机相册的正确打开方式

    iOS相机相册的正确打开方式- UIImagePickerController 通过指定sourceType来实现打开相册还是相机 UIImagePickerControllerSourceTypeP ...

  3. Xcode 的正确打开方式——Debugging(转载)

    Xcode 的正确打开方式——Debugging   程序员日常开发中有大量时间都会花费在 debug 上,从事 iOS 开发不可避免地需要使用 Xcode.这篇博客就主要介绍了 Xcode 中几种能 ...

  4. C#语法——泛型的多种应用 C#语法——await与async的正确打开方式 C#线程安全使用(五) C#语法——元组类型 好好耕耘 redis和memcached的区别

    C#语法——泛型的多种应用   本篇文章主要介绍泛型的应用. 泛型是.NET Framework 2.0 版类库就已经提供的语法,主要用于提高代码的可重用性.类型安全性和效率. 泛型的定义 下面定义了 ...

  5. InnoDB缓冲池预加载在MySQL 5.7中的正确打开方式

    InnoDB缓冲池预加载在MySQL 5.7中的正确打开方式 https://mp.weixin.qq.com/s/HGa_90XvC22anabiBF8AbQ 在这篇文章里,我将讨论在MySQL 5 ...

  6. Console控制台的正确打开方式

    Console控制台的正确打开方式 console对象提供了访问浏览器调试模式的信息到控制台 -- Console对象 |-- assert() 如果第一个参数断言为false,则在控制台输出错误信息 ...

  7. 任务队列和异步接口的正确打开方式(.NET Core版本)

    任务队列和异步接口的正确打开方式 什么是异步接口? Asynchronous Operations Certain types of operations might require processi ...

  8. (一)Redis for Windows正确打开方式

    目录 (一)Redis for Windows正确打开方式 (二)Redis for 阿里云公网连接 (三)Redis for StackExchange.Redis 下载地址 官网.中文网1 及 中 ...

  9. List的remove()方法的三种正确打开方式

    转: java编程:List的remove()方法的三种正确打开方式! 2018年08月12日 16:26:13 Aries9986 阅读数 2728更多 分类专栏: leetcode刷题   版权声 ...

随机推荐

  1. 201521123071《Java程序设计》第五周学习总结

    第5周作业-继承.多态.抽象类与接口 1. 本周学习总结 1.1 思维导图总结: 1.2在本周的学习中,主要学习了以下几点: - 初步接触了接口的定义,用interface关键字定义接口,使用impl ...

  2. 201521123073《Java程序设计》第3周学习总结

    1. 本周学习总结 2. 书面作业 1.代码阅读 public class Test1 { private int i = 1;//这行不能修改 private static int j = 2; p ...

  3. 201521123009 《Java程序设计》第11周学习总结

    1. 本周学习总结 2. 书面作业 本次PTA作业题集多线程 Q1:互斥访问与同步访问 完成题集4-4(互斥访问)与4-5(同步访问) 1.1 除了使用synchronized修饰方法实现互斥同步访问 ...

  4. python之面向对象2

    一.类命名空间与对象.实例的命名空间    常见一个类就会创建一个类的名称空间,用来储存类中定义的所有名字,这些名字成为类的属性  而类有两种属性:静态属性和动态属性 静态属性就是直接在类中定义的变量 ...

  5. arm-linux-gcc 4.3.2编译uboot 1.1.6

    在第三期项目的视频中,官方提供了一整套新的工具链,bootloader, 内核和文件系统(arm-linux-gcc_4.3.2, uboot-2012.04.01, linux-3.4.2)其中ub ...

  6. (转)Unity3D中移动物体位置的几种方法

    1. 简介 在unity3d中,有多种方式可以改变物体的坐标,实现移动的目的,其本质是每帧修改物体的position. 2. 通过Transform组件移动物体 Transform 组件用于描述物体在 ...

  7. ”TCP连接“究竟是什么意思?

    我们经常听到"建立TCP连接","服务器的连接数量有限"等,但仔细一想,连接究竟是个什么东西,是和电话一样两端连起一根线?似乎有点抽象不是么? 1. 久违的分组 ...

  8. unity3D写一个hello world

    unity3D写一个hello world 打开unity并且在assets建立一个新的文件,新的文件命名为hello world.unity.接着创建一个新的C#Sript脚本文件,命名为hello ...

  9. apollo实现c#与android消息推送(一)

    之前做了c#推送消息到手机端,限于网络要求,不能使用百度等现成的推送,查了许多资料,七拼八凑终于凑齐,记录下来,即是复习也是希望对来者有所帮助. 我开发的环境是windows,使用java开发的Apa ...

  10. Javascript写的一个可拖拽排序的列表

    自己常试写了一个可拖拽进行自定义排序的列表,可能写的不太好,欢迎提供意见. 我的思路是将列表中的所有项都放进一个包裹层,将该包裹层设为相对定位,每当点击一个项时,将该项脱离文档并克隆一份重新添加到文档 ...