一次git事故

在开发过程中,我们的版本管理方式是,一版本一分支。之所以这样,是因为随着开发的进行,版本随时会分叉(这里分叉是指,两个版本都需要后续开发,但是对同一模块的功能需要不一样的微调)。

随着分叉之后的持续开发,就会导致多个分支之间差别越来越大。因此,如果所有版本都添加同样功能或修改同一个Bug时,是不可以采用git merge的方式来进行的。因为很难找到他相同的父节点。这时patch会是一个很好的工具。

而这次事故正是git patch功能使用不当引起的,下面来模拟出一个完整的事故现场。

在最开始我们有一段原始代码如下,可以明显看到,在第18行代码中,将dst错打成了src。

void foo(std::vector<int> &src, std::vector<int> &dst, int id)
{
	for (auto iter = src.begin(); iter != src.end(); )
	{
		if (*iter == id)
		{
			iter = src.erase(iter);
		}
		else
		{
			++iter;
		}
	}
	for (auto iter = dst.begin(); iter != dst.end(); )
	{
		if (*iter == id)
		{
			iter = src.erase(iter);
		}
		else
		{
			++iter;
		}
	}
	dst.push_back(id);
	return ;
}

以此次commit为节点,分切出分支ver1和ver2。

然后在ver1分支上修改上述错误并使用git commit提交,由于也许有很多并行版本都有这个问题,一个一个去改很消耗时间还容易错。因此可以使用`git formatch-patch HEAD~1`来制作一个patch文件,我们得到的patch文件内容如下:

From 3c3a434d131c8cea38eb77b7b0f4681b78ae4172 Mon Sep 17 00:00:00 2001
From: findstr <findstr@sina.com>
Date: Sun, 4 Mar 2018 15:43:43 +0800
Subject: [PATCH] bugfix

---
 a.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/a.c b/a.c
index 241c07f..1157ab4 100644
--- a/a.c
+++ b/a.c
@@ -15,7 +15,7 @@ void foo(std::vector<int> &src, std::vector<int> &dst, int id)
 	{
 		if (*iter == id)
 		{
-			iter = src.erase(iter);
+			iter = dst.erase(iter);
 		}
 		else
 		{
--
2.4.6

切到ver2分支后,先手动将18行修改提交一次,再使用`git am 0001-bugfix.patch`来重复修复这个Bug。

预期中,执行完git am之后,只有两种情况。一是这个文件完全没有变化,二是会出现conflict。

然而,事实并没有想象中的那么美好,执行完git am之后,第7行的src被改成了dst。

以上基本就是整个事故的全部还原过程。

这种事故很难发现,一旦发现却很容易就知道原因。

分析一下patch文件,就会立即发现,整个patch行为是靠以下7行代码来定位的。

 	{
 		if (*iter == id)
 		{
-			iter = src.erase(iter);
+			iter = dst.erase(iter);
 		}
 		else
 		{

恰巧,由于在git am之前,我手动修改了这一行,导致在执行git am时,不可能会匹配到这一块出错的代码。更巧的是,git可以使用个patch中的代码块匹配到第4行的代码,即然匹配成功了,git自然就会将修改应用。

从这个事故中,可以得到几点启示。

1. patch并不是100%可靠的,执行之后最好查看一下结果
2. 大扩号另起一行,对版本管理工具不友好:D
3. DRY原则 不但对人适用,对版本管理工具同样适用

ps. 如果实在没有办法,就尽量扩大git diff输出的代码块大小,`git config –global diff.context 30`即可把代码块增加致30行

Git之坑

最近在使用Git进行多人协作时遇到了一些坑,在些处记录一下。

在使用git的过程中,最频繁使用的应该就是git pull了。在git pull命令时,一般会遇到下面几种情况。

如果是本地完全没有修改,则可以顺利进行git pull,如果有本地提交会与git pull下来的commit进行自动merge(当然自动merge也有可能失败,手动解决一下冲突即可)。

如果本地的修改与被git pull下来的commit中修改有相同的文件,这时git会报错提示,这时只让你先把本地修改commit或stash一下,也不会有太大问题。

最坑的就是是本地有修改,并且与git pull下来的commit并没有修改相同的文件。但是本地的commit(即没有push到远程仓库的提交)与git pull下来的commit自动合并时冲突了,这里git会提醒你要使用git commit -a来提交,但是根据git commit -a的语义,其实他不仅仅是把冲突内容全部合并,还会把本地的其他没有完成的修改一并提交上去。

一般来说,本地的修改大都是不完整的修改,这里如果提交上去,很容易造成其他问题。因此为了保险起见,如果本地有修改一定要先使用git stash将本地修改暂存之后,再执行git pull命令去拉取代码。

上面的坑,最多只会造成不完整的修改被提交,很容易及时发现。

但是另外一个关于submodule的坑,在超过一个人协作的情况下如果不注意是必然会发生的事。

假设有A和B两个人进行协作修改工程P。在P中引用了submodule S。

最初P-A(A的本地仓库)与P-B(B的本地仓库)中对submodule S的引用都是commit 1(SHA-1:xxxxxxxxxxx)。

这里S仓库有一个bug被修复了,P-A将自己的本地仓库中S的引用更新至commit 2(SHA-1:xxxxxxxxxxxxx)。然后提交并push到远程仓库。

这之后,B对P-B仓库成功执行了git pull命令。理论上P-B的仓库此时对S仓库的引用已经变更为commit 2了。但是由于S在P-B目录中的最新commit依然为commit 1。

在这之后,其实P-B中的S仓库并没有任何变动,依然为commit1,并且除了使用git status可以主动查看外,git 在合并之后并没有任何主动提交。这时P-B在完成某项修改之后,直接使用git commit -a就会重新将对S仓库的引用变更为commit 1。并且git 不会有任何提示。

如果要解决这个问题,必须要每次git pull之后,立即执行git submodule update –init来将仓库中的S目录更新到当前最新引用值。