使用 Git 丢失代码的 N 种操作您中招了吗?
无意翻出这篇写于两年前的手稿,当时交付的几家私有云大客户以及公有云客户均频繁遇到此问题,虽然现象千奇百怪,但是无非是错误的操作导致的代码丢失,秉承着开放、自由、分享的开源精神,把相关的错误操作整理出来并加以说明,对于新老用户都是一种引导,不仅可以避免给团队带来麻烦,也使自己能够更好的理解 Git 的一些运作方式,所以整理成文,希望能够帮助到有需要的人,尤其是公司内部研发流程的培训上,更应该关注这一类误操作的普及和说明,避免「不了解」给团队带来的麻烦。
- 我们丢了好多天的代码,你们靠不靠谱啊?!?!??
- 你们平台怎么搞的!我们代码莫名其妙丢了!
- 我们公司的一些文件莫名其妙被回退了,能帮我们看看吗?
- 本来这些文件夹里面都有代码的,现在没了,你告诉我不是你们平台的问题?
- 昨天本来还好好的,今天代码就都没了”
每当收到这些反馈,我都非常紧张,紧张并不是因为 Gitee 平台真的有什么问题,而是在想该怎么向用户去解释,怎么帮用户复盘问题,怎么帮用户处理这种问题。紧张的同时也在深深的埋怨 Linus 先生,为什么不把 Git 的这些操作设计的更简单一些(虽然已经很简单了)…
Gitee 上线近八年,接收到的类似的反馈数不胜数,但是归根究底,造成这些现象的根源在于对 Git 的不熟悉,对自己操作后的结果没有预期,于是饮了一口浓茶,望着窗外陷入了深深的沉思,复盘这些各种各样的情况,其实无外乎「4」种操作导致的:
- 使用 Git 进行推送的时候进行了强推操作
- 不正确的 Merge 操作造成的文件版本回退
- 将某些目录意外搞成了子模块,也就是 Git SubModule
- 一不小心删除了远程某些分支甚至仓库,刚好本地都没备份,造成丢失
突然我心里一阵激动:作为国内用户最多的速度最快的企业最认可的大家最满意的代码托管和研发协作平台,不能再低调了,我们有义务有责任将这些不正确的操作整理成文,剖析每一种操作,让大家能够了解并且避免这些情况,尤其是对于刚接触 Git 的朋友们,更应该注意和清楚这些操作,避免自己给团队带来不必要的麻烦,好,既然这样,那维护代码世界的和平就靠我了。
于是,抄起电脑,开始了我的复盘之路…
复盘过程中的截图会同时有命令行以及图形化界面,尽可能的符合大部分读者的场景。
No.1 强推操作
问题背景
在推送的时候往往会遇到远程仓库的版本领先于自己本地的版本,这个时候如果我们执行推送操作,就会看到 Git 给到我们的提示:
命令行:
GUI 界面:
这个时候很多刚接触 Git 的新手看到 error
就会懵掉,甚至根本都不会去看提示,就会去网上搜
hint: Updates were rejected because the tip of your current...
于是很兴奋的发现找到了解决方案
跟着答案在命令行加上了 -f 参数,或者图形化界面选中了 Force
git push origin master -f
然后执行命令或者点击 Push 按钮,发现居然成功了,开心!
命令行:
图形界面:
但是,人生大起大落是非常刺激的,还没开心多久,发现工作群已经炸锅了
- 刚刚谁做了操作,我代码呢?
- 我发现我的也没有了?
- 什么情况?
- 我*!谁把代码覆盖了?
- 咋回事???
- @ 新手,Gitee 上的动态显示是你强推的,为什么啊!?!?”
原因剖析
看到自己名字被@ 的那一瞬间,是不是并没有初恋般的心动,而是小时候打碎玻璃被发现的心慌?
没错,就是 -f
的问题,加了这个参数就是强制推送,如果在命令行推送的时候加了 -f 的参数,或者在图形化界面选择了强制推送的选项,就会把远程的版本强制替换为自己本地的版本,而别人之前推送的提交如果不在自己本地的那个版本里,就会造成丢失的现象。
可以观察下强制推送的结果是有 (forced update)
提示的,并且搜索的结果里注意看的话,别人已经说了 with -f tag you will override Remote Branch code.
,加上这个参数将会覆盖掉远程分支的代码。
解决方式
怎么解决这种问题呢,其实 Git 在操作的时候已经给出了提示:
Integrate the remote changes (e.g.'git pull ...') before pushing again.
我们在发现有冲突的时候可以尝试合并远端的变更到本地,合并之后即可正常推送到远端,合并的方式也非常简单,就如正常拉取代码一样,执行 git pull origin master
命令即可,唯一不同的就是需要进行一次合并操作,或者如果出现冲突需要解决完冲突再行进行合并,这里假设无冲突,来执行一下 git pull origin master
终端弹出了合并的提示,这是为当前的一次合并提交填写 Commit 信息的,我们默认即可,保存退出即可完成一次合并操作。
与终端不一样的是,GUI 界面在可以自动合并的情况下,默认是自动合并的:
合并之后我们就可以正常推送了,图形界面也一样的操作,这里不再赘述
当然,这种合并方式会产生一次额外的合并提交,如果你不想有多余的合并提交的话,可以参见 rebase 命令: https://git-scm.com/docs/git-rebase
更好的方式
更好的方式当然是禁止强推,目前 Gitee 平台支持设置仓库禁止强推,从源头避免强推导致的各种问题。
如果您是自建的 Git 服务的话,可以使用 receive.denyNonFastForwards
设置项进行单仓库或者全局的设置,如果要全局禁用强推,可以在服务端执行:
git config --system receive.denyNonFastForwards true
不过,工具只是辅助,最主要的还是要提升团队每个开发者对 Git 的认识,才能更好的协作。
No.2 错误的 Merge 方式
问题背景
上面我们有说到,当遇到冲突的时候需要合并远端到本地,才能继续进行推送的操作,但是上面的例子是并没有任何冲突的理想情况下,但事实经常事与愿违,在开发过程中,文件之间的冲突是不可避免的,所以经常会遇到 Git 不能够自动合并的情况,所以我们需要自己进行代码的合并,通过对代码上下文以及业务的理解,合并出团队所「期望」的代码。
命令行:
图形界面:
正常情况下,我们会对冲突的文件进行内容上的修复,并且在确认修复完成后提交到暂存区,通过提交合并的方式创建一个合并的提交,完成本次合并即可,过程如下图:
稍微对上图做些说明:
- 从第 4 个提交开始,我们衍生出了
dev
分支进行分支开发 - 同时
master
分支也有相应的提交 dev
分支上做了 7、8 两次提交,分别在b.txt
文件新增了第六行L6
和第七行L7
- 同时
master
分支上也做了 5、6 两次提交,在a.txt
新增了第九行L9
,分别在b.txt
文件新增了第六行L5
和第七行L6
- 然后
dev
分支合并到master
分支,由于两个分支都新增了b.txt
文件的第六行L6
,所以需要进行冲突合并,得出合并后的b.txt
文件的第六行L6
- 最后,执行
git add . && git commit -m "merge dev into master"
等操作完成本次合并,生成第 9 次提交 - 推送到远端的
master
整个过程是常规的合并操作,是一种标准实践了,但是上线的那天晚上,当团队复核代码的时候发现,最新版本的 a.txt
的 L9
变更不见了!
- 有人覆盖了 a.txt 代码?
- 喂,你在线?之前提交的 a.txt 代码呢?
- Zoker,帮忙看看我们团队投产的时候,发现有一些代码不见了,紧急!
- ....
现场看起来是这样的:
原因剖析
我记得当年排查这个问题的时候也觉得非常诡异,后来才发现这种问题的复现方式,还是通过现场反向推断出来的,因为后来发现做合并提交 9
的这位同事使用的是图形化工具,于是在模拟了他的一系列操作之后,终于发现了端倪,原因是因为:
在处理完冲突进行合并的时候,并未选中所有的文件,这些未选中的文件就被回退到被合并的分支的版本,而命令行没问题是因为默认无冲突的文件都被添加进暂存区了
不好理解?我们来看看命令行执行完 git pull
之后的状态:
其中 a.txt
由于无冲突,默认添加进暂存区等待提交了,当我们处理完 b.txt
冲突之后,只需要执行 git add b.txt
或者 git add .
就可以进行下一步提交了。但是,如果我们把暂存区的 a.txt
移除掉呢?比如恢复到工作区 git restore --staged a.txt
:
如果是这个状态,我们最终的合并提交的化, a.txt
就会被回退到 dev
分支上的版本,换句话说就是,在 dev
分支衍生出来之后的 master
分支上的 a.txt
改动会丢失。
但是,在命令行合并的时候我们关注的都是冲突的文件,并不会人为的去变更默认加入暂存区的文件,所以一般情况下不会出问题,那么图形化工具在合并的时候是如何造成这种问题呢?看看下面这张图:
在合并完 b.txt
准备提交的时候,把默认选中的 a.txt
取消掉了,再进行提交就会导致 a.txt
所增加的那一行被回退,而且使用 git log a.txt
去查看还看不到那次提交,一次小小的点击,换来一次大大的教训。
再深一层的理解的话,由于合并的时候暂存区的 tree
对象指向的是最新版本的 a.txt^Blob 对象
,把它从暂存区移除,就相当于暂存区的 tree
对象指回了老版本的 a.txt^Blob 对象
,理解起来的话就如下图:
解决方式
解决方式就比较简单了,在进行合并的时候,无论是使用命令行还是图形化界面,一定注意不要对暂存区的文件进行操作,除非你非常清楚的知道你在做什么,否则在处理完冲突之后,请确保添加了所有未冲突的文件以及解决了冲突的文件到暂存区!
No.3 误触子模块
问题背景
经常会收到用户说:
- 为什么我的这个目录在 Gitee 上打不开?
- 为什么我本地这个目录明明有代码,在服务器上拉取的时候这个目录是空的?
他们在 Gitee 上看到的是这样的:
这里的 lib
目录是无法点击的,而且当我们克隆这个仓库的时候,这个目录也是空的,但明明我本地提交的时候是有东西的啊,为什么呢?
原因剖析
所有上述的现象都是因为有 Git Submodule
的存在,因为在往仓库添加 lib
文件夹的时候, lib
文件夹本身就是一个 Git 仓库,当我们直接复制到仓库里面去提交的时候,Git 其实是会有提示的,只不过可惜的是大部分人都不会关注这些警告:
虽然依旧可以提交成功,但是就会造成网页上无法打开,克隆下来的时空目录的问题。
解决方式
仔细查看上图 Git 给的提示,可以知道 lib
本身就是一个 Git 仓库了。
方法一:如果我们本意并不是要以一个单独的 Git 仓库管理 lib
目录的话,我们可以单纯的删除掉 lib
目录下的 .git
文件夹,让 lib
成为一个单纯的目录,再行提交就不会有问题了。
方法二:如果确实需要单独以一个子模块的形式进行 lib
目录的管理,那么我们可以按照提示给 lib
目录配置上子模块:
然后按照常规的提交操作进行提交推送即可,这个时候我们再看看 Gitee 网页上是如何展示的:
克隆的时候,如果需要 lib
目录的话,只需要在克隆之后执行 git submodule update --init
即可
这里需要注意的是 lib
算是一个独立的仓库,它的权限也是独立的,所以说如果有一些目录并不是所有开发都需要,并且是比较核心的库之类的话,试试使用子模块来管理权限也是个不错的选择。
Git Submodule 手册: https://git-scm.com/book/en/v2/Git-Tools-Submodules
No.4 意外删除
问题背景
有时候难免会脑子抽风,一不小心删除了某个分支或者删除了某个仓库,那该怎么办呢?
解决方式
1、分支误删除
在 Gitee 上如果一不小心删除了某个分支,不要慌,只需要找到对应分支的 Commit ID
即可,Gitee 在动态里给出了这个信息,为的就是给操作一点空间,在相关的游离对象没有被 GC 之前,赶紧去找回来吧!
拿到这个 Commit ID
,我们就可以在本地或者网页上重建一个基于此 Commit ID
的分支,这样分支就找回来了。
2、仓库误删除(这是广告)
把 Gitee 上的仓库删除了,刚好本地有没有保存,还有没有办法?
答案是:如果你使用了 Gitee 企业版,那么你是可以找回的,Gitee 企业版提供了仓库快照的功能,能够定期的对仓库打快照,并且能够恢复任一快照的完整的仓库,快照的创建时间为最近三周的周末以及最近半年的每月第一个周末,就算误删,也能找回。
赶快来体验 Gitee 企业版: https://gitee.com/enterprises
最后
其实防止代码出问题的最好的办法,就是每个团队成员都能够了解 Git,了解自己在每一次操作之后发生了什么,自己是否能够知道预期的结果,所以最好是能够在前期就进行充分的培训以及注意事项的引导,团队的开发出现代码版本问题的现象将会大大降低。
那么,除了以上常见的几种方式,你还遇到过哪些丢代码的骚操作?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: 正确处理 HTML 5 标签的关闭
下一篇: 谈谈自己对于 AOP 的了解
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论