Git 笔记系列(九)—— Git进阶
时间 更新备注
2018-04-26 新建文章
2018-09-09 添加git账号切换
2019-01-18 更新链接

Git 飞行规则

git-flight-rules/README_zh-CN.md at master · k88hudson/git-flight-rules

git-tips: Git的奇技淫巧

521xueweihan/git-tips: Git的奇技淫巧

目录

Git fetch

fetch从远程存储库下载更改,但不会将它们集成到工作副本中。因此,Fetch是用来了解远程上发生的事情的概况.

一般如果不确定是否想要合并代码,可以先使用git fetch查看远端的和本地的diff改动.

1
2
git fetch origin master
git difftool origin

之后可以进入图形化的 对比界面

Where to find changes due to git fetch - Stack Overflow

FETCH_HEAD

理解 fetch 的关键, 是理解 FETCH_HEAD.
FETCH_HEAD: 是一个版本链接,记录在本地的一个文件中,指向着目前已经从远程仓库取下来的分支的末端版本。

常见的git fetch 使用方式

  1. git fetch
  • 创建并更新所有远程分支的本地远程分支.
  • 设定当前分支的FETCH_HEAD为远程服务器的master分支 (上面说的第一种情况)

需要注意的是: 和push不同, fetch会自动获取远程`新加入’的分支.

  1. git fetch origin
  • 同上, 只不过手动指定了remote.
  1. git fetch origin branch1
  • 设定当前分支的 FETCH_HEAD’ 为远程服务器的branch1分支`.
  • 注意: 在这种情况下, 不会在本地创建本地远程分支, 这是因为:
  • 这个操作是git pull origin branch1的第一步, 而对应的pull操作,并不会在本地创建新的branch.

与git pull的区别

git pull 会获取所有远程索引,并把它们的数据都合并到本地分支中来。

git add 跟踪文件变化

  • git add -A 提交所有变化

  • git add -u 提交被修改(modified)和被删除(deleted)文件,不包括新文件(new)

  • git add . 提交新文件(new)和被修改(modified)文件,不包括被删除(deleted)文件

一.版本导致的差别:

1.x版本:

(1)git add all可以提交未跟踪、修改和删除文件。

(2)git add .可以提交未跟踪和修改文件,但是不处理删除文件。

2.x版本:

两者功能在提交类型方面是相同的。

二.所在目录不同导致的差异:

(1)git add all无论在哪个目录执行都会提交相应文件。

(2)git add .只能够提交当前目录或者它后代目录下相应文件。

一个文件仅仅changed是不能被commit的,Git要求只能提交Index里的东西。

所以需要git add。这个命令的意思是,把Changed的文件的内容同步到Index区域里。这样Working Directory和Index区域的内容就一致了。这个过程被称之为stage

这个时候git status的结果是:

1
# Changes to be committed:
  • git add filename_1 filename_2 用一个命令向暂存区(staging)添加多个文件的方法
  • git add -A 提交所有变化,包括删除添加和改动。
  • git add -u 提交被修改(modified)和被删除(deleted)文件,不包括新文件(new)
  • git add . 提交新文件(new)和被修改(modified)文件,不包括被删除(deleted)文件

交互式暂存

Git提供了很多脚本来辅助某些命令行任务。这里,你将看到一些交互式命令,它们帮助你方便地构建只包含特定组合和部分文件的提交。在你修改了一大批文件然后决定将这些变更分布在几个各有侧重的提交而不是单个又大又乱的提交时,这些工具非常有用。用这种方法,你可以确保你的提交在逻辑上划分为相应的变更集,以便于供和你一起工作的开发者审阅。如果你运行git add时加上-i或者--interactive选项,Git就进入了一个交互式的shell模式,显示一些类似于下面的信息:

1
2
3
4
5
6
7
8
9
10
$ git add -i
staged unstaged path
1: unchanged +0/-1 TODO
2: unchanged +1/-1 index.html
3: unchanged +5/-1 lib/simplegit.rb

*** Commands ***
1: status 2: update 3: revert 4: add untracked
5: patch 6: diff 7: quit 8: help
What now>

Git 比较差异

git difftol

最方便的使用kaleidoscope进行节点的查看

1
git difftool <local commit>  <another commit>

使用log来打印diff

1
2
3
git log -p <branch>  a<another-branch> -<log number>

git log origin/master ^master

分支跟踪

从远程分支 checkout 出来的本地分支,称为 跟踪分支 (tracking branch)。跟踪分支是一种和某个远程分支有直接联系的本地分支。在跟踪分支里输入 git push,Git 会自行推断应该向哪个服务器的哪个分支推送数据。同样,在这些分支里运行 git pull 会获取所有远程索引,并把它们的数据都合并到本地分支中来。

在克隆仓库时,Git 通常会自动创建一个名为 master 的分支来跟踪 origin/master。这正是 git push 和 git pull 一开始就能正常工作的原因。当然,你可以随心所欲地设定为其它跟踪分支,比如 origin 上除了 master 之外的其它分支。刚才我们已经看到了这样的一个例子:git checkout -b [分支名] [远程名]/[分支名]。如果你有 1.6.2 以上版本的 Git,还可以用 –track 选项简化:

1
2
3
$ git checkout --track origin/serverfix
Branch serverfix set up to track remote branch refs/remotes/origin/serverfix.
Switched to a new branch "serverfix"

合并多个commits

使用squash

squash = use commit, but meld into previous commit

Git的撤销、回退

Reset、Checkout、Revert-的选择

Reset

在提交层面上,reset 将一个分支的末端指向另一个提交。这可以用来移除当前分支的一些提交。比如,下面这两条命令让 hotfix 分支向后回退了两个提交。

1
2
git checkout hotfix
git reset HEAD~2

hotfix 分支末端的两个提交现在变成了悬挂提交。也就是说,下次 Git 执行垃圾回收的时候,这两个提交会被删除。换句话说,如果你想扔掉这两个提交,你可以这么做。reset 操作如下图所示:


Checkout

你应该已经非常熟悉提交层面的 git checkout。当传入分支名时,可以切换到那个分支。

git checkout hotfix
上面这个命令做的不过是将HEAD移到一个新的分支,然后更新工作目录。因为这可能会覆盖本地的修改,Git 强制你提交或者缓存工作目录中的所有更改,不然在 checkout 的时候这些更改都会丢失。和 git reset 不一样的是,git checkout 没有移动这些分支。

将 HEAD 从 master 移到 hotfix

除了分支之外,你还可以传入提交的引用来 checkout 到任意的提交。这和 checkout 到另一个分支是完全一样的:把 HEAD 移动到特定的提交。比如,下面这个命令会 checkout 到当前提交的祖父提交。

1
git checkout HEAD~2

这对于快速查看项目旧版本来说非常有用。但如果你当前的 HEAD 没有任何分支引用,那么这会造成 HEAD 分离。这是非常危险的,如果你接着添加新的提交,然后切换到别的分支之后就没办法回到之前添加的这些提交。因此,在为分离的 HEAD 添加新的提交的时候你应该创建一个新的分支。

Revert

Revert 撤销一个提交的同时会创建一个新的提交。这是一个安全的方法,因为它不会重写提交历史。比如,下面的命令会找出倒数第二个提交,然后创建一个新的提交来撤销这些更改,然后把这个提交加入项目中。

1
2
git checkout hotfix
git revert HEAD~2

如下图所示:

revert到倒数第二个commit

相比 git reset,它不会改变现在的提交历史。因此,git revert 可以用在公共分支上,git reset 应该用在私有分支上。你也可以把 git revert 当作撤销已经提交的更改,而 git reset HEAD 用来撤销没有提交的更改。

就像 git checkout 一样,git revert 也有可能会重写文件。所以,Git 会在你执行 revert 之前要求你提交或者缓存你工作目录中的更改。

Git如何回滚一次错误的合并 - 掘金

Reset、Revert 跟 Rebase 指令有什麼差別? - 為你自己學 Git | 高見龍

這三个指令差別

表格归纳一下:

指令 改变历史记录 说明
Reset 把目前的狀態設定成某個指定的 Commit 的狀態,通常適用於尚未推出去的 Commit。
Rebase 不管是新增、修改、刪除 Commit 都相當方便,用來整理、編輯還沒有推出去的 Commit 相當方便,但通常也只適用於尚未推出去的 Commit。
Revert 新增一個 Commit 來反轉(或說取消)另一個 Commit 的內容,原本的 Commit 依舊還是會保留在歷史紀錄中。雖然會因此而增加 Commit 數,但通常比較適用於已經推出去的 Commit,或是不允許使用 Reset 或 Rebase 之修改歷史紀錄的指令的場合。

忽略文件

在git中如果想忽略掉某个文件,不让这个文件提交到版本库中,可以使用修改根目录中 .gitignore 文件的方法(如果没有这个文件,则需自己手工建立此文件)。这个文件每一行保存了一个匹配的规则例如:

1
2
3
4
5
6
7
# 此为注释 – 将被 Git 忽略

*.sample    # 忽略所有 .sample 结尾的文件
!lib.sample    # 但 lib.sample 除外
/TODO    # 仅仅忽略项目根目录下的 TODO 文件,不包括 subdir/TODO
build/    # 忽略 build/ 目录下的所有文件
doc/*.txt   # 会忽略 doc/notes.txt 但不包括 doc/server/arch.txt

.gitignore规则不生效的解决办法

把某些目录或文件加入忽略规则,按照上述方法定义后发现并未生效,原因是.gitignore只能忽略那些原来没有被追踪的文件,如果某些文件已经被纳入了版本管理中,则修改.gitignore是无效的。那么解决方法就是先把本地缓存删除(改变成未被追踪状态),然后再提交:

1
2
3
git rm -r --cached .
git add .
git commit -m 'update .gitignore'

Diff tool in Git GUI

  • 设置合并工具
    Kaleidoscope是一款很不错的diff工具
    如果您在使用tower,请查看

Diff & Merge Tools - Tower Help

create a file named “CompareTools.plist” and put it into “~/Library/Application Support/com.fournova.Tower3/CompareTools/“.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
<dict>
<key>ApplicationIdentifier</key>
<string>com.madebysofa.Kaleidoscope</string>
<key>ApplicationName</key>
<string>Kaleidoscope</string>
<key>DisplayName</key>
<string>Kaleidoscope</string>
<key>LaunchScript</key>
<string>kaleidoscope.sh</string>
<key>Identifier</key>
<string>kaleidoscope</string>
<key>SupportsMergeTool</key>
<false/>
<key>SupportsDiffChangeset</key>
<true/>
</dict>
</array>
</plist>

SSH Key

使用远程服务进行身份验证通常是使用 SSH 密钥完成的。虽然是一个非常安全和专业的方法, 但它可能是一个有点繁琐的设置和管理。通过允许您从应用程序内部管理您的帐户的 SSH 密钥,

Using and Managing SSH Keys - Tower Help

Git Flow

我说的以下流程,sourceTree等工具已经完美的支持了,鼠标点两下就完成了。简直是完美。

简介

Feature Branch Workflow是一种非常灵活的开发方式。对于一些规模比较大的团队,最好就是给特定的分支赋予不同的角色。除了功能分支(feature branch),Gitflow Workflow还使用独立的分支来准备发布(preparing)维护(maintaining), 和记录版本(recording releases)

分支类型和流程

下图能说明整个流程,只要你看得懂的话。该模式来自 Nvie

  • feature(多个、玫红)。主要是自己玩了,差不多的时候要合并回develop去。从不与master交互。
  • develop(1个、黄色)。主要是和feature以及release交互。
  • release(同一时间1个、绿色)。总是基于develop,最后又合并回develop。当然对应的tag跑到master这边去了。生命周期很短,只是为了发布
  • hotfix(同一时间1个、红色)。总是基于master,并最后合并到master和develop。生命周期较短,用了修复bug或小粒度修改发布。
  • master(1个蓝色)。没有什么东西,仅是一些关联的tag,因从不在master上开发。

在这个模型中,master和develop都具有象征意义。master分支上的代码总是稳定的(stable build),随时可以发布出去。develop上的代码总是从feature上合并过来的,可以进行Nightly Builds,但不直接在develop上进行开发。当develop上的feature足够多以至于可以进行新版本的发布时,可以创建release分支。

release分支基于develop,进行很简单的修改后就被合并到master,并打上tag,表示可以发布了。紧接着release将被合并到develop;此时develop可能往前跑了一段,出现合并冲突,需要手工解决冲突后再次合并。这步完成后就删除release分支。

当从已发布版本中发现bug要修复时,就应用到hotfix分支了。hotfix基于master分支,完成bug修复或紧急修改后,要merge回master,打上一个新的tag,并merge回develop,删除hotfix分支。

由此可见release和hotfix的生命周期都较短,master/develop虽然总是存在但却不常使用。

分支详解

主分支 (Historical Branches)

主分支是所有开发活动的核心分支。所有的开发活动产生的输出物最终都会反映到主分支的代码中。主分支分为master分支和development分支。

master 分支

master分支上存放的应该是随时可供在生产环境中部署的代码(Production Ready state)。当开发活动告一段落,产生了一份新的可供部署的代码时,master分支上的代码会被更新。同时,每一次更新,最好添加对应的版本号标签(TAG)。

develop分支

develop分支是保存当前最新开发成果的分支。通常这个分支上的代码也是可进行每日夜间发布的代码(Nightly build)。因此这个分支有时也可以被称作整合分支(integration branch)。

辅助分支

辅助分支是用于组织解决特定问题的各种软件开发活动的分支。辅助分支主要用于组织软件新功能的并行开发、简化新功能开发代码的跟踪、辅助完成版本发布工作以及对生产代码的缺陷进行紧急修复工作。这些分支与主分支不同,通常只会在有限的时间范围内存在。

辅助分支包括:

  1. 用于开发新功能时所使用的feature分支;
  2. 用于辅助版本发布的release分支;
  3. 用于修正生产代码中的缺陷的hotfix分支。

以上这些分支都有固定的使用目的和分支操作限制。从单纯技术的角度说,这些分支与Git其他分支并没有什么区别,但通过命名,我们定义了使用这些分支的方法。

feature 分支

使用规范:

  1. 可以从develop分支发起feature分支
  2. 代码必须合并回develop分支
  3. feature分支的命名可以使用除masterdeveloprelease-*hotfix-*之外的任何名称

feature分支(有时也可以被叫做“topic分支”)通常是在开发一项新的软件功能的时候使用,这个分支上的代码变更最终合并回develop分支或者干脆被抛弃掉(例如实验性且效果不好的代码变更)。

release分支(preparing)

使用规范:

  • 可以从develop分支派生
  • 必须合并回develop分支和master分支
  • 分支命名惯例:release-*

release分支是为发布新的产品版本而设计的。在这个分支上的代码允许做小的缺陷修正、准备发布版本所需的各项说明信息(版本号、发布时间、编译时间等等)。通过在release分支上进行这些工作可以让develop分支空闲出来以接受新的feature分支上的代码提交,进入新的软件开发迭代周期。

当develop分支上的代码已经包含了所有即将发布的版本中所计划包含的软件功能,并且已通过所有测试时,我们就可以考虑准备创建release分支了。而所有在当前即将发布的版本之外的业务需求一定要确保不能混到release分支之内(避免由此引入一些不可控的系统缺陷)。

成功的派生了release分支,并被赋予版本号之后,develop分支就可以为“下一个版本”服务了。所谓的“下一个版本”是在当前即将发布的版本之后发布的版本。版本号的命名可以依据项目定义的版本号命名规则进行。

hotfix分支(maintaining)

使用规范:

  • 可以从master分支派生
  • 必须合并回master分支和develop分支
  • 分支命名惯例:hotfix-*

除了是计划外创建的以外,hotfix分支与release分支十分相似:都可以产生一个新的可供在生产环境部署的软件版本。

当生产环境中的软件遇到了异常情况或者发现了严重到必须立即修复的软件缺陷的时候,就需要从master分支上指定的TAG版本派生hotfix分支来组织代码的紧急修复工作。

这样做的显而易见的好处是不会打断正在进行的develop分支的开发工作,能够让团队中负责新
功能开发的人与负责代码紧急修复的人并行的开展工作。

git cherry-pick 详解

初识 git cherry-pick(拣选)
拣选会提取某次提交的补丁,之后尝试将其重新应用到当前分支上。 这种方式在你只想引入特性分支中的某个提交时很有用。

假设你的项目提交历史如下:

如果你希望将提交 e43a6 拉取到 master 分支,你可以运行:

当前处于 master 分支

$ git cherry-pick e43a6
Finished one cherry-pick.
[master]: created a0a41a9: “More friendly message when locking the index
fails.”
3 files changed, 17 insertions(+), 3 deletions(-)
这样会拉取和 e43a6 相同的更改,但是因为应用的日期不同,你会得到一个新的提交 SHA-1 值。 现在你的历史会变成这样:

现在你可以删除这个特性分支(ruby_client),并丢弃不想拉入的提交(5ddae)。

需要说明的是,提取某次提交的“补丁”,这个补丁是基于其父提交的。

下图可以说明:

我们要拣选提交 C4 到 maint 分支(maint 指向 C7),Git 会生成一个补丁(Δ=C4−C3 \Delta = C4-C3Δ=C4−C3),然后把Δ \DeltaΔ应用到C7上,也就是说把 C4 对 C3 的变化在 C7 上重放一遍。

为何会产生冲突
同 merge 操作一样,拣选操作也可能产生冲突。有人会问:不会吧,打个补丁也能冲突?

当然能。

用 diff 工具生成 patch 时,我们所做的每一处修改都会连同它的“定位信息”(原始文件中的行号、修改处前三行和后三行的原始文本)一并保存到 patch 文件中。patch 被应用时,会在目标文件中寻找“定位信息”,找到后再实施修改。可是,当我们把补丁应用到 C7 上时,有可能找不到那些定位信息了:在master分支上,C2变成了C3,在maint分支上,C2变成了C6,又变成了C7,也许C3和C7相差越来越远,C3中的上下文在C7中早已面目全非,不见踪迹。于是应用patch失败,即发生冲突。

冲突了怎么办
当拣选发生冲突的时候,GIT 会采用三路合并算法。还是以上面的图为例子,

当你运行命令 git cherry-pick C4 的时候,Local是C7,Remote是C4,Base是C3(即C4的父提交)。总结:

当你运行命令

git cherry-pick

如果冲突了,那么Git会尝试三方合并

LOCAL: the commit you’re merging on top of (i.e. the HEAD of your branch)
REMOTE: the commit you’re cherry picking (i.e. commit C)
BASE: the parent of the commit you’re cherry-picking (i.e. C^, ie the parent of C)
如果三方合并的时候又冲突了怎么办?那只能靠我们人工解决了。

问题

管理冲突

  1. 多个Feature 分支开发完成,提交到develop 分支时,修改了共同的模块。
  2. release 分支或者hotfix 分支修改了bug合并回develop时,发现develop分支已经往前面走了一截。

在执行可能有冲突的操作前,先查看一下 暂存区 和 工作目录,保证其中没有修改。

比如使用git stash就可以把暂存区 和 工作目录的修改保存起来,让暂存区 和 工作目录处于干净的状态。

更进一步

Gitflow workflow 和pull request 组合起来使用,代码审查机制的加入,会让这个模式大放异彩。下一篇文章中, 我会详细介绍 代码审查😘😘😘😘。

扩展:Forking Workflow

Forking Workflow与以上讨论的工作流很不同,一个很重要的区别就是它不只是多个开发共享一个远程仓库(central repository),而是每个开发者都拥有一个独立的服务端仓库。也就是说每个contributor都有两个仓库:本地私有的仓库和远程共享的仓库。

Forking Workflow这种工作流主要好处就是每个开发者都拥有自己的远程仓库,可以将提交的commits推送到自己的远程仓库,但只有工程维护者才有权限push提交的commits到官方的仓库,其他开发者在没有授权的情况下不能push。Github很多开源项目都是采用Forking Workflow工作流。

文章来源

Git版本控制与工作流

[基于git的源代码管理模型——git flow](https://link.jianshu.com/?t=http://www.ituring.com.cn/article/56870

Git CI集成

自动化的单元测试
Continuous Integration and Delivery - CircleCI

代码Review — Reviewable

Reviewable - GitHub Code Reviews Done Right

Pull Request的code review

Git工作流指南:Pull Request工作流 - 文章 - 伯乐在线

保持你的 Git 提交记录的整洁

Git 提交记录很容易变得混乱不堪,现在教你怎么保持整洁!

提交功能是 Git 仓库的关键部分之一,不仅如此,提交信息也是仓库的生命日志。项目或者仓库在随着时间的推移不断演变(新功能不断加入,Bug 被不断修复,体系架构也被重构),提交信息成为了人们查看仓库所发生的变化或者怎么发生变化的地方。因此使用简短精确的提交信息反映出内部的变化是非常重要的。

为什么有意义的提交记录非常重要?

Git 提交信息是你在你所写的代码上所留下的指纹。不管你今天提交了什么代码,一年之后你再看到这个变化;你会非常感谢你所写的有意义的、干净整洁的提交信息,这也会使得你的同事工作更轻松。当根据上下文分开提交时,可以更快地找到 Bug 是在哪一次提交中被引入的,将首先引起 Bug 的这次提交进行回退可以非常简便的修复 Bug。

当开发大型项目时,我们经常处理一大堆部件变动,包括更新、添加和移除。在这种场景中确保好好维护提交信息是很艰难的,尤其是当开发周期是数天、数周、甚至数月时。因此为了简化维护提交记录的工作,这篇文章会使用许多开发人员在 Git 仓库上工作时可能会经常遇到的常见场景。

  • 场景 1:我需要修改最近一次的提交
  • 场景 2:我需要修改一次特定的提交
  • 场景 3:我需要添加、移除或者合并提交
  • 场景 4:我的提交记录没啥有用的内容,我需要重新开始!
    但是在我们深入了解之前,让我们快速浏览一下我们假设的 Ruby 应用程序中典型的开发工作流程。

注意: 这篇文章默认你已经掌握 Git 基础,分支如何工作,如何将分支的未提交更改添加到暂存区以及如何提交更改。如果你不太熟悉这些流程,我们的文档是一个好的起点。

生活中的某天

现在,我们正在开发一个小型的 Ruby on Rails 项目,在这个项目中我们需要在首页添加一个导航视图,这需要更新和添加许多文件。下面是整个流程分解的每个步骤:

  • 你开始开发某个功能,更新了一个文件;让我们称它为 application_controller.rb
  • 这个功能还需要你更新一个视图:index.html.haml
  • 你添加了索引页所使用的一个部分:_navigation.html.haml
  • 为了体现你所添加的那一部分,样式表也需要被更新:styles.css.scss
  • 改完这些模块,功能已经完成了,是时候更新测试文件了;要更新的文件如下:
    • application_controller_spec.rb
    • navigation_spec.rb
  • 测试也更新了,并且如期地通过了所有的测试案例,现在是时候提交更改了!

因为所有的这些文件属于不同的架构领域,我们彼此隔离地提交这些文件的更改,以确保每次提交代表了特定的上下文,并且按照特定顺序进行提交。我通常偏向于后端 -> 前端的提交顺序:首先提交以后端为中心的更改,其次提交中间层文件的更改,最后提交以前端为中心的更改。

  1. application_controller.rb & application_controller_spec.rb添加导航路由
  2. _navigation.html.haml & navigation_spec.rb页面导航视图
  3. index.html.haml渲染导航部分
  4. styles.css.scss为导航添加样式

在提交更改之后,我们会为分支创建一个合并请求。一旦创建了合并请求,在被合并到仓库的 master 分支之前,通常会由你的同事对代码进行审查。现在我们了解一下代码审查过程中可能会遇到的不同情况。

场景 1:我需要修改最近一次的提交

想象一下代码审查者在审查 styles.css.scss 时提出了一个修改建议。这种情况,修改起来非常简单,因为样式表修改是你分支上的最后一次提交。下面是我们应该怎样处理这种情况:

  • 你直接在你的分支上对 styles.css.scss 做必要的修改。
  • 一旦你完成了修改,将这些修改添加到暂存区进行暂存;运行命令 git add styles.css.scss
  • 一旦修改被添加到暂存区,我们需要将这些修改添加到我们的最后一次提交;运行命令: git commit --amend
  • 命令分解:这里,我们使用 git commit 命令修改最近一次提交,把暂存中的任何修改合并到最近一次提交。
  • 这会在你的 Git 定义的文本编辑器中打开你最后一次的提交,它具有提交信息为导航添加样式
  • 因为我们只更新了 CSS 声明,所以我们不需要修改提交信息。你可以只做保存然后退出 Git 为你打开的文本编辑器,你的更改会被反映到提交上。

由于你修改了一个已经存在的提交,你需要使用 git push --force-with-lease <remote_name> <branch_name> 命令将这些修改强制推送到你的远程仓库。这个命令会使用我们本地仓库中所做的修改来覆盖远程仓库中为导航添加样式这个提交。

当你强制推送分支时,有一点需要注意,那就是当你所在分支是一个多人协作的分支时,你的强制推送可能会给其他人的正常推送造成麻烦,因为远程分支上有一些强制推送的新的提交。因此,你应该合理地使用这个功能。你可以在这里学习到更多有关 Git 强制推送选项的信息。

场景 2:我需要修改一次特定的提交

在上一个场景中,因为我们只需要修改最近的一次提交,所以做起来非常简单,但是想象一下如果代码审查者建议修改 _navigation.html.haml 文件中的某些部分。在这种场景下,它是第二次提交,所以修改起来不像第一个场景中那么直接。让我们看看怎么处理这种情况:

每次在分支上提交更改,都会有一个独一无二的 SHA1 哈希字符串作为更改提交的标志。可以把它看做区分每次提交的独特 ID。可以通过运行 git log 命令查看某个分支上的所有提交以及它们分别的 SHA1 哈希值。运行命令之后,可以看到类似下面的输出,其中最近一次的提交在顶部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
commit aa0a35a867ed2094da60042062e8f3d6000e3952 (HEAD -> add-page-navigation)
Author: Kushal Pandya <[email protected]>
Date: Wed May 2 15:24:02 2018 +0530

为导航添加样式

commit c22a3fa0c5cdc175f2b8232b9704079d27c619d0
Author: Kushal Pandya <[email protected]>
Date: Wed May 2 08:42:52 2018 +0000

渲染导航部分

commit 4155df1cdc7be01c98b0773497ff65c22ba1549f
Author: Kushal Pandya <[email protected]>
Date: Wed May 2 08:42:51 2018 +0000

页面导航视图

commit 8d74af102941aa0b51e1a35b8ad731284e4b5a20
Author: Kushal Pandya <[email protected]>
Date: Wed May 2 08:12:20 2018 +0000

添加导航路由

现在轮到 git rebase 命令表演了。不管什么时候我们想要用 git rebase 命令修改一个特定的更改提交,我们首先要将我们分支的 HEAD 变基到我们想要修改的更改提交之前。在这个场景中,我们需要修改页面导航视图的更改提交。

现在,注意我们想要修改的更改提交之前的一个更改提交的哈希值;复制这个哈希值然后按照一下步骤进行操作:

  • 通过运行命令 git rebase -i 8d74af102941aa0b51e1a35b8ad731284e4b5a20 来将分支变基到我们要修改的更改提交的前一个更改提交
  • 命令分解:现在我们正在使用 Git 的 rebase 命令的交互模式,通过提交 SHA1 哈希值我们可以将分支进变基。
  • 这条命令会运行 Git 变基命令的交互模式,并且会打开文本编辑器展示你所变基到的更改提交之后的所有更改提交。它看起来是这样的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pick 4155df1cdc7 页面导航视图
pick c22a3fa0c5c 渲染导航部分
pick aa0a35a867e 为导航添加样式

# Rebase 8d74af10294..aa0a35a867e onto 8d74af10294 (3 commands)
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

注意到每个更改提交之前都有一个单词 pick,并且在它下面的内容里面,有所有的我们可以使用的关键字。因为我们想要编辑一个更改提交,所以我们将命令 pick 4155df1cdc7 页面导航视图修改为 edit 4155df1cdc7 页面导航视图。保存更改并退出编辑器。

现在你的分支已经被变基到包含 _navigation.html.haml 的更改提交之前了。打开文件并完成每个审查反馈中的修改需求。一旦你完成了修改,使用命令 git add _navigation.html.haml 将它们暂存起来。

因为我们已经暂存了这些更改,所以现在应该把分支 HEAD 重新移动到我们原来的更改提交(同时包含我们所有的新的更改的提交),运行 git rebase --continue,这将会在终端中打开你的默认编辑器并且向你展示变基期间我们所做的更改的提交信息;页面导航视图。如果需要你可以修改这个提交信息,但现在我们保留它,因此接下来保存修改然后退出编辑器。这个时候,Git 会重新展示你刚刚修改的更改提交之后的所有更改提交并且分支的 HEAD 已经回到了我们原来的所有更改提交的顶部,它包含所有你对其中某个更改提交所做的所有新的更改。

因为我们又一次修改了远程仓库中的一个提交,我们需要再次使用 git push --force-with-lease <remote_name> <branch_name> 命令将分支强制提交。

场景 3:我需要添加、移除或者合并提交

一个常见的场景就是当我们刚刚修改了一些之前的提交并重新提交了一些新的更改。现在让我们尽可能的精简一下这些提交,用原来的提交合并它们。

你所要做的就是像其它场景中所做的那样开始交互性的变基操作。

1
2
3
4
5
6
7
pick 4155df1cdc7 页面导航视图
pick c22a3fa0c5c 渲染导航部分
pick aa0a35a867e 为导航添加样式
pick 62e858a322 Fix a typo
pick 5c25eb48c8 Ops another fix
pick 7f0718efe9 Fix 2
pick f0ffc19ef7 Argh Another fix!

现在假设你想要合并所有的那些提交到 c22a3fa0c5c 渲染导航部分。你只需要做:

  1. 把你想要合并的那些更改提交往上移动,以使得它们位于最终合并的更改提交之下。
  2. 将每一个更改提交的模式由 pick 改为 squash 或者 fixup

注意: squash 模式会在描述中保留修改时的信息。而fixup 不会,它只会保留原来的提交信息。

你会以下面这种结果结束实验:

1
2
3
4
5
6
7
pick 4155df1cdc7 页面导航视图
pick c22a3fa0c5c 渲染导航部分
fixup 62e858a322 Fix a typo
fixup 5c25eb48c8 Ops another fix
fixup 7f0718efe9 Fix 2
fixup f0ffc19ef7 Argh Another fix!
pick aa0a35a867e 为导航添加样式

保存更改并退出编辑器,你就完成了!这就是完成之后的历史提交记录:

1
2
3
pick 4155df1cdc7 页面导航视图
pick 96373c0bcf 渲染导航部分
pick aa0a35a867e 为导航添加样式

像之前一样,你现在所要做的所有工作只是运行 git push --force-with-lease <remote_name> <branch_name> 命令,然后所有的修改都被强制推送了。

如果你想要完全地移除一个更改提交,而不是 squash 或者 fixup,你只需要输入 drop 或者简单地删除这一行。

避免冲突

为避免发生冲突,请确保您在时间线上上移的提交不会触及其后的提交所触及的相同文件。

1
2
3
4
5
6
7
pick 4155df1cdc7 页面导航视图
pick c22a3fa0c5c 渲染导航部分
fixup 62e858a322 Fix a typo # this changes styles.css
fixup 5c25eb48c8 Ops another fix # this changes image/logo.svg
fixup 7f0718efe9 Fix 2 # this changes styles.css
fixup f0ffc19ef7 Argh Another fix! # this changes styles.css
pick aa0a35a867e 为导航添加样式 # this changes index.html (no conflict)

附加提示:快速 fixup

如果你很清楚你想要修改哪一个更改提交,在提交更改时不必浪费脑力思考一些 “Fix 1”, “Fix 2”, …, “Fix 42” 这样的名字。

步骤 1:初识 --fixup

在你暂存那些更改之后,使用以下命令提交更改:

1
git commit --fixup c22a3fa0c5c

(注意到这个哈希值是属于 c22a3fa0c5c 渲染导航部分这个更改提交的)

这会产生这样的提交信息:fixup! 渲染导航部分

步骤 2:还有这个小伙伴 --autosquash

通过简单的使用交互变基操作。你可以让 git 自动的把所有 fixup 放到正确的位置。

git rebase -i 4155df1cdc7 --autosquash

历史提交记录会变成下面这样:

1
2
3
4
5
6
7
pick 4155df1cdc7 页面导航视图
pick c22a3fa0c5c 渲染导航部分
fixup 62e858a322 Fix a typo
fixup 5c25eb48c8 Ops another fix
fixup 7f0718efe9 Fix 2
fixup f0ffc19ef7 Argh Another fix!
pick aa0a35a867e 为导航添加样式

一切就绪,你只需要审查并继续。

如果你觉得自己喜欢冒险,你可以做一个非交互式的变基 git rebase --autosquash,但前提是你喜欢过这种有风险的生活,因为你没有机会在这些合并应用前检查它们。

场景 4:我的提交记录没啥有用的内容,我需要重新开始!

如果你在开发一个大型的功能,那通常会有许多修复和代码审查反馈的修改频繁的被提交。我们可以将提交的清理工作留到开发结束,而不是不断重新设计分支。

这是创建补丁文件非常方便的地方。实际上,在开发人员可以使用以 Git 为基础的服务比如 GitLab 之前,补丁文件一直是开发大型开源项目通过邮件分享代码与合并代码的主要方式。假设你有一个提交量非常巨大的分支(例如;add-page-navigation),它对于仓库的变更历史表述得不是很清晰。以下是如何为您在此分支中所做的所有更改创建补丁文件:

  • 创建补丁文件的第一步是确保您的分支具有来自 master 分支的所有更改并且与这些更改没有冲突。
  • 您可以在 add-page-navigation 分支中签出时运行 git rebase mastergit merge master,以将所有从 master 进行的更改转移到您的分支上。
  • 现在创建补丁文件;运行 git diff master add-page-navigation > ~/add_page_navigation.patch
    • 命令分解:在这里我们使用了 Git 的 diff 特性,查询 master 分支和 add-page-navigation 分支之间的差异,然后重定向输出(通过 > 符号)到一个文件,在我们的用户主目录(在 *nix 系操作系统中通常是 ~/)中命名为 add_page_navigation.patch
  • 你可以指定你想保存这个文件的路径,文件名和扩展名为任意你想要的值。
  • 一旦命令运行并且没有看到任何错误,就会生成补丁文件。
  • 现在签出 master 分支;运行 git checkout master
  • 从本地仓库删除分支 add-page-navigation;运行 git branch -D add-page-navigation。请记住,我们已经在创建的补丁文件中更改了此分支。
  • 现在创建一个具有相同名称的新分支(master 被签出);运行 git checkout -b add-page-navigation
  • 现在,这是一个新的分支,没有任何你所做的修改。
  • 最后,从修补程序文件中应用您的更改;git apply ~/add_page_navigation.patch
  • 在这里,所有更改都会应用到分支中,并且它们将显示为未提交,就好像您所做的所有修改都完成了,但没有任何修改是在分支中实际提交的。
  • 现在,您可以继续并按照所需顺序提交按影响区域分组的单个文件或文件,并使用简单明了的提交信息。

跟之前的场景一样,我们修改了整个分支,现在是时候强制推送了!

结论

虽然我们已经介绍了使用 Git 进行日常工作流程中出现的大多数常见和基本情况,但重写 Git 提交历史是一个巨大的话题,如果你已经熟悉上述建议,您可以在 Git 官方文档。快乐的 git’ing!

总结

个人觉得最重要的一点就是每次的 commit 粒度和 commit 信息一定要合理清晰,这样才能更为精准的进行版本控制管理。

参考

  1. Git 官方文档
  2. How (and why!) to keep your Git commit history clean
  3. GotGitHub — GotGitHub
文章作者: MichaelMao
文章链接: http://frizzlefur.com/2018/04/26/Git 笔记系列(九)—— Git进阶/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 MMao
我要吐槽下