开篇碎碎念
上一篇,我谈了一下《为什么我建议微服务项目不用 SVN 而用 Git 进行版本管理?》。使用 Git 作为代码管理工具之后,开发的效率提高了很多。正如 Git 分支简介 阐述的那样:
几乎所有的版本控制系统都以某种形式支持分支。 使用分支意味着你可以把你的工作从开发主线上分离开来,以免影响开发主线。 在很多版本控制系统中,这是一个略微低效的过程——常常需要完全创建一个源代码目录的副本。对于大项目来说,这样的过程会耗费很多时间。
有人把 Git 的分支模型称为它的“必杀技特性”,也正因为这一特性,使得 Git 从众多版本控制系统中脱颖而出。为何 Git 的分支模型如此出众呢? Git 处理分支的方式可谓是难以置信的轻量,创建新分支这一操作几乎能在瞬间完成,并且在不同分支之间的切换操作也是一样便捷。与许多其它版本控制系统不同,Git 鼓励在工作流程中频繁地使用分支与合并,哪怕一天之内进行许多次。理解和精通这一特性,你便会意识到 Git 是如此的强大而又独特,并且从此真正改变你的开发方式。
用上 Git 之后,我们的工作目录不用再创建分支目录和版本目录,IDE 也中始终只有一份项目源码。但是,我们对于 Git 的摸索常常就止步于此了,很多人包括之前的我似乎并“不敢”频繁地使用分支与合并,不是因为别的,就是担心出错之后自己无法解决,影响开发效率和年终绩效。
接下来,我们一起来直面内心的这份恐惧,从掌控本地分支开始,理解和精通 Git 分支特性。
但是,看完这篇文章之后,答应我 “一起做 Git 的舔狗,好吗?”。
1. Git 简介
Git 版本库是一个简单的数据库,其中包含所有用来维护与管理项目的修订版本和历史的信息。
Git 维护两个主要的数据结构:对象库(object store)和索引(index)。所有这些版本库数据存放在工作目录根目录下一个名为 .git 的隐藏子目录中。
Git 对象库中有一个重要的对象 Commit(提交),画图时,我们通常用“圆圈”来表示一个 Commit 对象。
Branch (分支)不是对象库中的对象,而是一个指针,指向 Commit(提交)。
理解 Git 不仅仅是一个版本控制系统(VCS)是很重要的,Git 同时还是一个内容追踪系统(content tracking system)。Git 的对象库基于其对象内容的散列计算的值,而不是基于用户原始文件布局的文件名或目录名设置。
Commit 是对象库中的对象,当然也有其散列值,你可以用 git show-branch --sha1-name
查看分支:
如上图所示,3fc6dda 只是散列值的前缀部分,不是完整的散列值。“缩写前缀” 可以方便程序员查看和使用。想要得到完整的散列值,可以查看路径为 .git/refs/heads/master
的文件内容:
所以,完善一下 Branch 指向 Commit 的那张图:
完整的散列值,就是 Git 对象库中对象的寻址地址。
如果你没学过 git,你可能又要好奇,想知道 Commit 里面的内容是什么呀?
你可以用
git cat-file -p 3fc6dda
查看 Commit 的内容。如果你以前不了解 Git 这些特征和命令,但是现在有很多问号,那我可能成功勾起你的兴趣了。[奸笑]
2. 远程版本库
在我们的日常开发中,通常都会有一个“权威的”中央仓库。通常在本地仓库中会将其命名为 origin。
我们通过查看路径为 .git/config
的文件内容,可以知道我们的 origin 对应的 url。例如:
origin 就成为了我们配置的远程版本库的代名词。
同时还可以查看文件路径为 .git/refs/remotes/origin
的文件内容,来知道远程版本库上的 master 分支指向的 Commit 对象的散列值:
如图所示,远程代码库上 master 分支指向的 Commit 对象的寻址路径是 a414310...
3. 分支模型
关于分支模型,我从 gitee.com 创建仓库看到的可选分支模型:
有一个问题一直困扰我,怎么判定我们的项目用的是哪种模型?
我现在习惯观察中央仓库中具有无限的生命周期的主要分支有哪些,来判断这个 Git 项目用的是哪种分支模型。
接下来,以我个人经验谈谈主流的常用的分支模型:
3.1 单分支模型
以我现在的常见工作流为例。远程版本库(或者说中央仓库)origin 一直存在的只有 master 这一个分支。但是我们每次创建一个版本时,会从 master 切出“临时”开发分支。
为什么说是“临时”呢? 因为每次发版完成后,我们会将代码合并到 master 主干分支,然后删除该分支。
假如已确定版本 1.0.0 的需求。
-
首先,版本负责人在远程版本库创建分支 Dev_br1.0.0
-
接着,参与版本开发的程序员从中央仓库 git fetch,本地仓库多出了远程仓库分支 origin/Dev_br1.0.0
-
根据 origin/Dev_br1.0.0 切出对应的本地分支 Dev_br1.0.0 (使用相同的名称可以免除一些不必要的麻烦)
经过以上的操作,现在我们的 IDEA - Git Branches 显示如下:
git branch
命令可以帮助我们确定当前本地分支情况。然后再执行切出命令,从当前分支创建新的分支,并切换当前分支为新建的分支:
git checkout -b Ft_ziyu_1.0.0
命令行操作,如下图所示:
查看所有分支的命令如下:
git branch --all
IDEA 直接看到右下角,就可以知道当前工作的是哪个分支,点击右下角分支名字,则会显示 Git Branches:
3.2 生产/开发模型
中央仓库拥有两个主要分支,具有无限的生命周期:
- master
- develop
在中央仓库中,master 是主干分支。这个分支 HEAD 始终反映生产就绪状态 ,简单来说,master 分支上的代码与生产使用的代码始终保持一致。
另一个与 master 并行的分支是开发分支 develop,其 HEAD 始终反映了最新交付的开发代码。通常要求 develop 的代码至少能够通过构建时的单元测试且能正常启动。
develop 向 master 分支的合并工作通常由唯一的版本负责人完成。并且合并完成后,通常会在 master 分支上打上 tag 表示里程碑。
由于多个开发人员,可以同时向 develop 分支发起合并,所以一般每个开发人员也需要从 develop 分支切出属于自己的分支进行开发。
3.3 特性/发布模型
我没有接触过这类模型,但是我感觉该模型一般用于多个团队或者多个人负责同一项目的维护,且每个团队或者每个人负责着项目的不同特性的开发。
这种模型的复杂度显然要高于 2.2 生产/开发模型,比较正式的开源项目或者复杂度极高的系统才会使用该模型。
- master 分支表示已经发布的代码,develop 分支表示测试中的代码,feature 则表示每一项正在开发的特性。
- feature 分支通常会由一个团队来负责,feature 分支是否合并到 develop 分支,一般会有评审委员会来决策。
- feature 分支存在“流产”的可能性,即放弃继续开发,也不准备合并到 develop 分支。
- feature 分支在某些情况下,虽然已经明确评估过不能合并到 develop 分支,但是仍然作为完全独立的分支继续开发。
- develop 分支不直接接受开发者代码的提交,仅用来接受来自特性分支或者 hotfix(修复分支,特殊命名的特性分支)的合并。
- develop 分支在合并后,需要进行发布前的验证。
4. 个人专属分支的管理
为了开展试验,在不同的文件夹,克隆同一个远程版本库,建立两个不同的本地仓库:
-
F:gitcode rouble_makergit-test
以下简称 本地仓库 tm -
F:gitcodegiteegit-test
以下简称 本地仓库 ge
这两个本地仓库,分别是在 F:gitcode rouble_maker
和 F:gitcodegitee
执行 git clone 得到的。(我这个测试库 git-test 没有公开,建议读者自己申请 Gitee 账号,自己建立私有仓库)
git clone https://gitee.com/kendoziyu/git-test.git
我先在 master 分支提交了 Main,并且 push 到了中央仓库中。
public class Main {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
查看 IDEA 底部控制栏 - 版本控制 Version Control:
如上图所示,版本库目前有初始提交 README.md 和 Main.java 文件提交,这两个提交。
4.1 checkout OR branch
接着我在 本地仓库 tm 中,创建本地分支 Ft_ming_1.0.0。
git checkout Dev_br1.0.0
git checkout -b Ft_ming_1.0.0
在 本地仓库 ge 中,创建本地分支 Ft_ziyu_1.0.0
git checkout Dev_br1.0.0
git checkout -b Ft_ziyu_1.0.0
但是本地分支 Ft_ming_1.0.0 和 Ft_ziyu_1.0.0 对于中央仓库是不可以见的,因为我没有 push 到中央仓库。
IDEA 的 Checkout As 功能和 checkout -b
效果相同:都可以从“当前工作分支”创建一个新的“分支”,并把“当前工作分支”移动到新建的分支。
从远程分支 origin/Dev_br1.0.0 切出本地分支 Dev_br1.0.0 ,通常都是保持相同的名称的。
假如当前正在 Dev_br1.0.0 , 可以选择 + New Branch :
接着输入分支名就可以从 Dev_br1.0.0 切出一个新的本地分支 Ft_ziyu_1.0.0 了:
如果勾选 Checkout Branch:使用的是 checkout 命令,当前分支会切换到新创建的分支上。
git checkout -b Ft_ziyu_1.0.0
如果不勾选 Checkout Branch:使用的是 branch 命令,虽然创建新的分支,但是当前分支不会切换到新创建的分支上。
git branch Ft_ziyu_1.0.0
4.2 merge 操作
“程序员 ming” 在 本地仓库 tm 中,修改 Main 方法:
import java.util.Scanner;
public class Main {
static Scanner scanner;
public static void main(String[] args) {
scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String value = scanner.next();
System.out.print("echo: ");
System.out.println(value);
}
}
}
他开发了“回声”功能,进行一次提交。但是 ming 还是觉得需要“退出循环”的方法,于是修改代码,再次提交:
import java.util.Scanner;
public class Main {
static Scanner scanner;
public static void main(String[] args) {
scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String value = scanner.next();
if ("q".equals(value) || "exit".equals(value)) {
break;
}
System.out.print("echo: ");
System.out.println(value);
}
}
}
现在本地仓库 tm 的 Ft_ming_1.0.0 新增了两次提交:
ming 打算把 Ft_ming_1.0.0 的代码合并到本地的 Dev_br1.0.0 , 可以使用 Git 命令:
git checkout Dev_br1.0.0
git merge Ft_ming_1.0.0
当然,我还是要给大家展示 IDEA 的 Git checkout 功能:
然后是 IDEA 的 merge 功能:
Git 合并两个分支时,如果顺着一个分支走下去可以到达另一个分支的话,那么 Git 在合并两者时,只会简单地把指针右移,叫做“快进”(fast-forward)。
在这次合并中,顺着 Dev_br1.0.0 可以走到 Ft_ming_1.0.0 的 HEAD 处。因此,直接移动 Dev_br1.0.0 分支指针到和分支 Ft_ming_1.0.0 的分支指针相同的位置。
“快进”合并不会创建新的 Commit 对象。即“快进”合并不会产生一个新的“合并提交”。
4.3 撤销合并
合并出错时,我们会希望有手段可以补救。下面介绍如何撤销合并。
首先通过 git log 来查看
git log --abbrev-commit --oneline
我们想让 本地仓库 tm 的本地分支 Dev_br1.0.0 回到和远程分支 origin/Dev_br1.0.0 相同的位置,此时缩写键值为 a414310接着,那个“散列值前缀”作为回退的参数。因此,执行 reset 指令:
git reset --hard a414310
回退成功。reset 有几种模式,默认模式为mixed,也就是将版本库和缓存区里的重置,但本地文件不动。
参考博客:git使用笔记(七)版本回退和撤销
但是,这还是挺麻烦的。接下来介绍 IDEA 的简便操作方式:
-
进入你需要回退的分支
-
右击你希望当前分支回退到的 Commit 对象位置
-
点击 Reset Current Branch to Here...
IDEA 的 Reset 操作,帮我们屏蔽了键值细节,通过在指定 Commit 对象位置来代替指定键值!不过你还是需要选择模式:
4.4 merge --no-ff
我希望在合并代码时,引入一个新的“合并提交”,那么可以选择不采用“快进”合并:
git merge --no-ff Ft_ming_1.0.0
然后我们可以看到 IDEA - Version Control 的 Log 如下所示:
图中有些看不清的灰色文字, 内容是:Merge branch ‘Ft_ming_1.0.0’ into Dev_br_1.0.0
在 IDEA 中,右击项目根目录,再移动到 Git,再移动到 Repository。
在打开的子菜单中,点击 Merge Changes
:
在这个页面,你可以为 merge
指定更多参数:
--no-commit
--no-ff
--squash
--log
了解参数信息 git-merge
4.5 git push
git push origin Dev_br1.0.0
或者在 IDEA 顶部菜单栏 VCS -> Git -> Push
这个“冲突”的种子已经埋下了,接下来就去操作 本地仓库 ge
4.6 ziyu 提交代码
ziyu 在本地仓库 ge 中开发 Ft_ziyu_1.0.0 分支时,重构了代码,新增了 Printer 接口,
public interface Printer {
void print(String message);
}
并且将 System.out.println 的实现搬到了实现类 StdPrinter 中,
public class StdPrinter implements Printer {
@Override
public void print(String message) {
System.out.println(message);
}
}
然后,ziyu 在 本地仓库 ge 提交了一次。接着又想用 Jul 来实现 Printer 接口
import java.util.logging.Logger;
public class JulPrinter implements Printer {
Logger logger = Logger.getLogger("julPrinter");
@Override
public void print(String message) {
logger.info(message);
}
}
实现完成后又一次提交。然后,ziyu 修改了 Main 方法,又提交了一次:
public class Main {
public static void main(String[] args) {
Printer printer;
if (args.length > 0 && "jul".equals(args[0])) {
printer = new StdPrinter();
} else {
printer = new JulPrinter();
}
printer.print("Hello World!");
}
}
ziyu 总共新增了 3 个提交。因此,本地仓库 ge 的提交历史记录如下:
4.7 git pull
ziyu 的功能开发完成,想要先从远程版本库更新一下 Dev_br1.0.0 ,然后再把本地 Ft_ziyu_1.0.0 分支的代码合并到 Dev_br1.0.0 :
git checkout Dev_br1.0.0
我们发现 Dev_br1.0.0 旁边有个蓝色的向下箭头,这个表示远程仓库有最新的代码可以拉取。
此时可以使用:
git pull origin Dev_br1.0.0
参考文档:菜鸟教程:git pull 命令
或者在 IDEA 的菜单项 VCS -> Git -> Pull :
这里 Strategy 是合并策略,有兴趣可以去查阅 《Git 版本控制管理(第2版)》的 9.3 合并策略。一般来说,保持默认就好。
4.8 变基
我们的 Ft_ziyu_1.0.0 和 Dev_br1.0.0 都是从 “创建Main函数” 分出去的。所以 “创建Main函数”就是 Ft_ziyu_1.0.0 和 Dev_br1.0.0 的 merge-base 合并基础。
假如使用 Merge 的方式,Ft_ziyu_1.0.0 和 Dev_br1.0.0 重新归为一点:
IDEA 自然也是提供了方便的变基操作 Rebase Current Onto Selected:
变基后的提交记录历史:
变基时,Ft_ziyu_1.0.0 分支上的提交不再是原来的提交了。而是基于原来的提交 X 产生的提交 X' :
变基前,当前分支是 Ft_ziyu_1.0.0 :
A---B---C Ft_ziyu_1.0.0
/
D---E---F---G Dev_br1.0.0
执行下面两条命令的其中一个:
git rebase Dev_br1.0.0
git rebase Dev_br1.0.0 Ft_ziyu_1.0.0
变基后:
A'--B'--C' Ft_ziyu_1.0.0
/
D---E---F---G Dev_br1.0.0
参考 git-rebase
4.9 撤销变基
但是,rebase 的回退有些麻烦,如果 rebase 没有完全完成,可以通过 git rebase --abort
中止变基 。如果 rebase 已经完成,则采用下面的方法:
git reflog --abbrev-commit --oneline
在:
后面输入 q
可以退出 reflog 的查看。接着可以输入:
git checkout Ft_ziyu_1.0.0
git reset HEAD@{7}
或者
git checkout Ft_ziyu_1.0.0
git reset 9d12371
4.10 拣樱桃
关于拣樱桃 cherry-pick
,我觉得如果你想用上这个功能的话,你可能需要在你的个人分支上再拓展分支,把互相不相关的功能点放到一个小分支去处理。
首先,我删除了刚才的本地分支,然后重新切出分支
git checkout Dev_br1.0.0
git branch -d Ft_ziyu_1.0.0
git checkout -d Ft_ziyu_1.0.0
使用 IDEA 的话,不能直接删除“当前工作分支”,所以要 checkout 到其他分支,然后再删除:
然后,我新增了 Util 工具类:
public class Util {
public static void a() {
// do something
}
}
提交一下,再增加 b() 方法,然后提交一下:
public class Util {
public static void a() {
// do something
}
public static void b() {
// do something
}
}
依次类推,增加 c 方法提交一下,增加 d 方法提交一下:
接着 checkout 到 Dev_br1.0.0 分支,从 Ft_ziyu_1.0.0 挑一些功能提交上来
使用 Git Bash 查看 Ft_ziyu_1.0.0 的记录:
git log Ft_ziyu_1.0.0 --oneline --abbrev-commit
然后就可以拿这个 392f7b1 来“拣樱桃”:
git cherry-pick 392f7b1
如果没有发生需要解决的冲突,它直接会为我们在 Dev_br1.0.0 上新建一个和 392f7b1 内容相同的提交。
在 IDEA 中,右击项目根目录 -> Git -> Show History :
我们不用关心提交的键值,直接右击选中某个提交,然后选择 Cherry-Pick:
如果出现冲突,此时就会认为你正在 Cherry-Pick 中,并且会弹出解决冲突的指引:
然后,解决完冲突后,你就可以手动进行提交:
手动提交后,Cherry-Pick 的工程就结束了。
4.11 撤销 Cherry-Pick
假如你正在 Cherry-Pick 过程中,你的 .git
文件夹下面会有一个 CHERRY_PICK_HEAD
的文件存在,此时你可以通过 abort 来中止。
git cherry-pick --abort
在 IDEA 中,右击项目根目录,再移动到 Git,再移动到 Repository。
在这个子菜单中:可以找到 Abort Cherry-Pick。(前提是你正在 Cherry-Pick过程中)
参考博客
花了好长时间整理这篇文章,但是感觉还是有很多地方没能表达全面,希望读者勿怪。我想表达的是,不用害怕出错,Git 有各种方法回退,勇敢尝试吧!