代码中的软件工程

978-7-115-57850-1
作者: 孟宁
译者:
编辑: 郭媛

图书目录:

详情

本书共五篇:第一篇介绍常用工具VS Code、Git 和正则表达式;第二篇以C 语言代码为例介绍工程化编程的基本方法,涵盖代码的风格和规范、模块化、可复用、可重入函数与线程安全等;第三篇介绍从需求分析到软件设计的基本建模方法——从需求分析开始,以UML 为工具完成用例建模、业务领域建模、对象交互建模,最终形成设计方案;第四篇探讨软件的元素、结构、特性和描述方法,以及高质量软件的内涵等;第五篇回顾软件危机的前因后果,并将之与PSP、TSP、CMM/CMMI、敏捷开发、DevOps 等软件过程和生命周期管理衔接起来。 本书以国家精品在线开放课程——“工程化编程实战”为核心内容,增加了常用工具、需求分析与建模方法、软件结构和软件过程等相关内容,内容全面、新颖,实践性强。本书主要针对在校学生编写,适合开设相关专业的普通高校和高职院校作为主要教材,也可供不同层次的自学者学习参考。

图书摘要

版权信息

书名:代码中的软件工程

ISBN:978-7-115-57850-1

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

编  著 孟 宁

责任编辑 郭 媛

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315

读者服务:

微信扫码关注【异步社区】微信公众号,回复“e57850”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。


本书共五篇:第一篇介绍常用工具VS Code、Git和正则表达式;第二篇以C语言代码为例介绍工程化编程的基本方法,涵盖代码的风格和规范、模块化、可复用、可重入函数与线程安全等;第三篇介绍从需求分析到软件设计的基本建模方法——从需求分析开始,以UML为工具完成用例建模、业务领域建模、对象交互建模,最终形成设计方案;第四篇探讨软件的元素、结构、特性和描述方法,以及高质量软件的内涵等;第五篇回顾软件危机的前因后果,并将之与PSP、TSP、CMM/CMMI、敏捷开发、DevOps等软件过程和生命周期管理衔接起来。

本书以国家精品在线开放课程——“工程化编程实战”为核心内容,增加了常用工具、需求分析与建模方法、软件结构和软件过程等相关内容,内容全面、新颖,实践性强。本书主要针对在校学生编写,适合开设相关专业的普通高校和高职院校作为主要教材,也可供不同层次的自学者学习参考。


目前,国内高校对于软件工程类课程的教授方法是以教师课堂授课为主,学生被动地听课,教学一般重理论而轻实践。中国科学技术大学软件学院软件工程课程在教学改革10年来一直寻找改变这一现状的方法,从中积累了一些教学经验,也在不断引入一些新的教学内容和教学思路,乃至对软件本身进行哲学上的思考。本书即这10年来的经验总结,编者将其整理出版,希望能抛砖引玉,激发软件工程在教学、研究和工程上的活力。

总体而言,本书兼有理论深度和实践厚度,内容丰富、由浅入深、层次鲜明。工欲善其事,必先利其器,本书首先从简单、实用的工具讲起,包括VS Code、Git以及正则表达式;接着以C语言代码为例介绍工程化编程的基本方法,涵盖了代码风格规范、模块化、可复用、可重入函数和线程安全等;然后从需求建模的角度,利用一种从需求分析到软件设计的基本建模方法,从需求分析开始,以UML为工具完成用例建模、业务领域建模、对象交互建模,最终形成设计方案;接着深刻讨论软件的本质和软件的结构,以软件科学化的视角梳理软件的基本构成元素、基本结构、设计模式、软件架构、质量属性,以及描述软件的不同视图等;最后从软件危机的历史讲起,回顾软件危机的前因后果,从还原论过渡到系统论,将软件的生命周期作为讨论的重点,并与业界主流的PSP、TSP、CMM/CMMI、敏捷开发、DevOps 等软件过程和生命周期管理衔接起来。

在软件定义一切的时代,我们该如何定义软件?本书试图以从实践到理论、从还原论到系统论等不同的角度给出编者的思考和观点。毫无疑问软件已经成为人类生存的新一代基础设施,就像农业时代的禾本科农作物和农耕器械、工业时代的机械化工具和化石能源一样。智能软件已经渗透到人们生活的方方面面,成为人类赖以生存的条件之一,未来这一趋势将不断深化。

感谢我的同事白天老师、王超老师和汪炀老师,三位专家对本书内容进行了严谨的审核把关;本书部分内容的编写深受美国得州大学阿灵顿分校龚振和老师的启迪;《软件测试方法和技术》《敏捷测试:以持续测试促进持续交付》作者朱少民老师和《构建之法:现代软件工程》作者邹欣老师的成功经验和倾力指导使我受益良多;编写过程中我的同事华保健老师帮助我解答了一些问题。

本书得到了清华大学软件学院刘强老师、南京大学软件学院陈振宇老师、厦门大学计算机科学与技术系郑炜老师、中国矿业大学计算机科学与技术学院杨文嘉老师、北京电子科技学院网络空间安全系娄嘉鹏老师、淮阴师范学院计算机科学与技术学院桂斌老师、苏州工业园区服务外包学院谷瑞老师、苏州科技大学电子与信息工程学院胡伏原老师、西安交通大学计算机科学与技术学院鲍军鹏老师、浙江大学计算机学院翁恺老师、开课吧合伙人兼首席产品官孙志岗老师,以及我的同事朱宗卫老师和李曦老师等同行专家的评阅和指导,特对以上专家的关心和指导表示衷心的感谢并致以崇高的敬意!

在编写、修改和审校本书的过程中,编者得到人民邮电出版社信息技术出版分社社长陈冀康和编辑郭媛的全方位指导,并由他们对书稿进行了内容审核把关。本书的出版离不开出版社编辑团队所做的大量细致、专业的工作,在此对他们的辛苦工作表示衷心的感谢!

本书的出版得到中国科学技术大学研究生教材出版专项经费支持,感谢学校对教材编写工作的大力支持。

本书得以出版离不开编者的家人、朋友和同事各方面的支持和帮助,在此不一一列举姓名,一并表示诚挚的感谢!

由于本书是10年来讲稿的汇编整理,其间编者参考了大量互联网上的资料,时间跨度大,而且本领域的发展更多是以非正式的工程实践总结为主,严谨的学术论文并没有主导学科的发展,因此本书在参考文献的严谨性、规范性上有诸多不足。当然主要受限于编者水平,书中不当之处在所难免,欢迎广大读者批评指正,请关注微信公众号“读行学”与编者互动并给予反馈信息。另外,本书配套有完善的讲稿PPT、考试试题等资料,需要的读者可发送“所就职的学校+姓名+手机号”到mengning997@163.com索取,或者在公众号直接留言。如想获得本书参考文献和相关信息的电子版,可进入公众号“图书”菜单的本书页面附录1获得。

微信公众号“读行学”

孟宁

2021年11月

献给软件相关从业者和哲学爱好者!

在软件定义一切的时代,我们该如何定义软件?

如果一个现代哲学家不探究软件的哲学意义,那他已经是哲学的外行。

就像我们很难想象会有不去探究人类认知规律的古典哲学家一样。


本书由异步社区出品,社区(https://www.epubit.com)为您提供相关资源和后续服务。

本书提供如下资源:

本书源代码;

书中彩图文件。

要获得以上配套资源,请在异步社区本书页面中点击,跳转到下载界面,按提示进行操作即可。

您还可以扫码右侧二维码, 关注【异步社区】微信公众号,回复“e57850”直接获取,同时可以获得异步社区15天VIP会员卡,近千本电子书免费畅读。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,点击“提交勘误”,输入错误信息,点击“提交”按钮即可。本书的作者和编辑会对您提交的错误进行审核,确认并接受后,您将获赠异步社区的 100 积分。积分可用于在异步社区兑换优惠券、样书或奖品。

扫描下方二维码,您将会在异步社区微信服务号中看到本书信息及相关的服务提示。

我们的联系邮箱是contact@epubit.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线提交投稿(直接访问www.epubit.com/selfpublish/ submission即可)。

如果您所在的学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近40年计算机图书出版的积累和专业编辑团队,相关图书在封面上印有异步图书的Logo。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。

异步社区

微信服务号


“工欲善其事,必先利其器”,本部分精心介绍3种软件开发中常用的工具,即Visual Studio Code代码编辑器、Git分布式版本控制系统和正则表达式。后两种工具都被集成进了Visual Studio Code代码编辑器,因此我们可以围绕Visual Studio Code代码编辑器进行学习和训练。


Visual Studio Code(以下称VS Code)的使用量近年来获得了“爆炸”式增长,成为广大开发者工具库中的必备“神器”。VS Code是一个轻量且强大的代码编辑器,支持Windows、macOS和Linux,内置JavaScript、TypeScript和Node.js支持,而且拥有丰富的插件生态系统,用户可通过安装插件来支持C/C++、C#、Python、PHP等语言。

VS Code安装文件可以通过VS Code官方网站下载[1]。下载完VS Code安装文件,在Windows操作系统和macOS下可图形化安装,在此不赘述;在Linux操作系统(以Ubuntu Linux为例)下可以使用如下命令安装。

[1] 关注微信公众号“读行学”在“图书”菜单进入本书页面附录1,即可获得Visual Studio Code的下载地址。

    sudo apt install ./<filename>.deb

VS Code主界面最左侧有六大主菜单,分别是文件资源管理器(Ctrl+Shift+E)、跨文件搜索(Ctrl+Shift+F)、源代码管理(Ctrl+Shift+G)、启动和调试(Ctrl+Shift+D)、远程资源管理器和管理扩展插件(Ctrl+Shift+X)。单击每个主菜单后都会显示对应的管理面板。图1-1所示为VS Code的界面概览。

通过Ctrl+Shift+P快捷键调出“查找并运行所有命令”工具,能够快速查找和运行所有命令。请务必牢记Ctrl+Shift+P快捷键。

另外,可使用Ctrl+F快捷键调出文件查找面板,以及使用Ctrl+`(Esc键下面的键)快捷键切换集成终端。

图1-1 VS Code的界面概览

VS Code界面语言的配置方法是按Ctrl+Shift+P快捷键调出查找并运行所有命令工具,输入display选择Configure Display Language(配置显示语言),然后选择需要的语言,如en为英文、zh-cn为简体中文。如果没有需要的语言可以选择Install additional languages...(安装语言包),安装完语言包之后重新按Ctrl+Shift+P快捷键调出工具,选择Configure Display Language,就可以看到刚才安装的语言包,选择之后重启VS Code就可完成语言配置。

通过“帮助”菜单打开“欢迎使用”,可以自定义界面颜色主题、安装相关工具和语言插件、设置和按键绑定等。

VS Code作为一款服务于软件开发者的文本编辑器,具备一般的文本编辑器所具有的基本功能,使用方法也基本类似,如新建文件(Ctrl+N)、关闭文件(Ctrl+W)、编辑文件和保存文件(Ctrl+S)、文件内搜索(Ctrl+F)等。

软件开发者在编辑和管理文件时以项目为单位,一个项目往往在一个独立的文件夹工作区(workspace)内,因此VS Code也具有文件资源管理器的特点,如打开文件夹(Ctrl+O)可以在文件资源管理器(Ctrl+Shift+E)管理面板中查看文件夹内的文件和目录,也可以关闭文件夹工作区(Ctrl+K F,注意该操作由两步完成:第一步为按Ctrl+K快捷键,第二步为按F键)。在软件开发过程中常常会同时打开多个源文件。也可以快速关闭所有文件(Ctrl+K W),以及关闭已保存的文件(Ctrl+K U)。

当然,VS Code可以在自定义配置中设置按键绑定,通过安装插件的方式支持大多数编辑器或集成开发环境(Integrated Development Environment,IDE)的快捷键用法,如安装Vim插件后可以在VS Code中通过Vim的操作模式来编辑文本。

特别需要指出的是,注释和取消注释这一功能在软件开发过程经常用到,通过编辑菜单可以找到对应的选项。在VS Code中,注释和取消注释的快捷键为Ctrl+/和Shift+Alt+A,可以快捷地进行代码注释。Ctrl+/用于单行代码注释和取消注释,快捷键Shift+Alt+A用于代码块的注释和取消注释。

[2] 本节内容主要来自微软程序员李少侠的观点和分析。

近年来,VS Code的用户量获得了“爆炸”式增长,成为广大开发者工具库中的必备“神器”。VS Code为什么能这么牛?主要有以下几个方面的原因。

简洁而聚焦的产品定位贯穿始终。

进程隔离的插件模型是“定海神针”。UI渲染与业务逻辑隔离,从而做到一致的用户体验。

代码理解和调试——LSP和DAP两大协议“厥功至伟”。

集大成的VS Code远程开发环境。

你知道VS Code的核心开发团队人数只有20多个吗?难以相信吧!大家都觉得VS Code“无所不能”,如此强大的工具仅20多个人怎么可能做得出来?实际上功能丰富是个“美好的错觉”,因为大部分针对特定编程语言和技术的功能都是第三方插件提供的。VS Code的核心始终非常精简,这很考验产品团队的拿捏能力:做多了,臃肿,人手也不够;做少了,太弱,没人用。

“简洁”说到底是产品的“形态”,更关键的其实是前置问题——产品的定位,即它到底解决什么问题。

有如此多的编辑器和集成开发环境,我们为什么还需要一个新的工具?我们到底是需要一个代码编辑器(editor)还是集成开发环境(IDE)?VS Code与代码编辑器及集成开发环境之间的关系如图1-2所示。

图1-2中keyboard centric的意思是以键盘为中心;lightweight/fast的意思是轻量、快速;file/folders的意思是支持文件和文件夹的管理功能;many languages的意思是支持多种语言;code understanding的意思是能够理解代码的语法规则和关键词;debug的意思是支持代码调试功能;project systems的意思是具有项目级别的管理功能;integrated build的意思是集成了代码构建功能;templates,wizards的意思是具有常用的工程模板和智能创建项目的工具;designers的意思是对界面设计者比较友好;ALM integration的意思是集成了项目的生命周期管理(Application Lifecycle Management,ALM)。

图1-2 VS Code与代码编辑器与集成开发环境之间的关系(源自VS Code官网)

VS Code专注于开发者“最常用”的功能:编辑器+代码理解+版本控制+远程开发+调试。这是非常节制而平衡的选择。同时在产品的形式上VS Code力求简洁、高效。从结果来看,这个定位是相当成功的。

VS Code相对较小的功能集,使开发者们能在代码质量上精益求精,最终使用户也得到一个性能优异的工具。这是VS Code从一众编辑器中脱颖而出的重要原因。

正是因为在产品定位以及团队职责上高度节制,团队成员才能把时间聚焦在开发者“最常用”的功能问题上,写出经得起考验的代码。而团队规模较小也使团队成员更易于做到行为层面的“整齐划一”。

有的人用Node.js开发,有的人用Go开发;有的人做前端开发,有的人做后台开发……VS Code如何满足这些人五花八门的需求呢?机智的你已经抢答了——海量插件。

通过插件来扩展功能的做法已经是司空见惯的了,但如何保证插件的功能和原生工具的功能一样优秀呢?历史经验告诉我们——不能保证!

Eclipse插件模型可以说是做得非常“彻底”了,从功能层面上讲它也是“无所不能”,但它存在几个烦人的问题:不稳定、难用、慢。所以不少用户转投IntelliJ的怀抱。可谓“成也插件,败也插件”。

不同团队写出来的代码,无论是思路还是质量,都不一致。最终可能会导致用户得到一个又乱又卡的产品。所以要让插件在稳定性、速度和用户体验的层面都做到和原生工具的功能一致,只能是一个“美好的愿望”。

享有“宇宙第一IDE”美名的Visual Studio却可以“搞定”所有功能,并且做到优秀,让别人“无事可做”。IntelliJ与之相仿,开箱即用,插件可有可无。这么看起来,自己搞定所有的事情是个好办法,但你是否知道,Visual Studio背后有上千人的工程师团队。

Eclipse核心部分的开发者就是早期的VS Code核心开发团队成员,所以他们没有“两次踏入同一条河流”。与Eclipse不同,VS Code选择了“把插件关进笼子里”,造就了VS Code的“定海神针”——进程隔离的插件模型,如图1-3所示。

图1-3 VS Code进程隔离的插件模型(源自VS Code官网)

图1-3中VS Code process的意思是VS Code进程;RPC的意思是远程过程调用(Remote Procedure Call);Extension Process(Node.js)的意思是用Node.js实现的扩展插件进程;Extension Host和Extensions的意思是扩展插件主机中有多个扩展插件。

稳定性对于VS Code来说尤为重要。VS Code基于Electron.js,但它实质上是个Node.js环境,而Node.js是单线程的,任何代码崩溃都会带来灾难性的后果,所以VS Code干脆不信任任何人,把各个插件放到单独的进程里,某个插件进程崩溃也无法干扰主进程代码的执行,因此主程序的稳定性得到了保障。

解决了稳定性的问题之后,最关键的问题就是影响易用性的问题了,具体来说,就是混乱的界面和流程。究其原因就是海量插件无法做到界面语言统一有序,这导致用户的学习曲线异常陡峭,并且在面临问题时没有统一的解决问题的路径。用两个字概括就是“难用”。

VS Code的做法是根本不给插件开发者“发明”新界面的机会。

VS Code统管所有用户界面交互,制定用户界面交互的标准,所有用户的操作被转化为各种请求发送给插件进程,插件能做的就是响应这些请求。插件进程只能专注于业务逻辑处理,从而做到从始至终插件都不能“决定”或者“影响”界面元素如何被渲染,比如颜色、字体等,至于在用户交互过程中弹出对话框一类的动作更是完全不可能的。这种用户界面(User Interface,UI)渲染与业务逻辑隔离的做法提供了一致的用户体验。

绝大部分代码理解和调试功能都是由第三方插件来实现的,这些用于代码理解和调试的第三方插件与VS Code主进程之间的桥梁就是两大协议——语言服务协议(Language Server Protocol,LSP)和调试适配协议(Debug Adapter Protocol,DAP)。

全栈开发早已成为这个时代的主流,软件开发从业者们越来越不被某种特定的语言或者技术所局限,这对我们手里的开发者工具提出了新的挑战。举个例子,用TypeScript和Node.js做前端,同时用Java写后台,偶尔也用Python做一些数据分析,那么我很有可能需要若干个工具。这样做的问题就在于我需要在不同的工具间频繁切换,无论从系统资源消耗的角度,还是用户体验的角度来看,这都是很低效的。

那么有没有一种工具能在同一个工作区里把3种语言都“搞定”呢?VS Code就是能同时支持多语言的开发环境,而多语言支持的基础就是LSP。

LSP在短短几年内取得了空前的成功,到目前为止,已经有来自微软等公司以及社区的一百多个实现,基本覆盖了所有主流编程语言。同时,它也被其他开发工具所采纳,如Atom、Vim、Sublime、Emacs、Visual Studio和Eclipse,这也从另一个角度证明了LSP的优秀。

更难能可贵的是,LSP还做到了轻量和快速,这可以说是VS Code的“杀手级”特性了。又强大又轻巧,它到底怎么做到的呢?先划重点:①节制的设计;②合理的抽象;③周全的细节。

在产品设计中追求大而全是很常见的。如果让我来设计这么一个用来支持所有编程语言的产品,第一反应很可能会将其设计成涵盖所有语言特性的超集。

微软就有过这样的尝试,如Roslyn——一个语言中立的编译器,C#和VB.NET的编译器都是基于它做的。大家都知道C#在语言特性层面是非常丰富的,Roslyn能支撑起C#足以说明它的强大。

那么问题来了,为什么它没有在社区得到广泛应用呢?我想根本原因是“强大”所带来的副作用——复杂。语法树就已经很复杂了,其他各种特性以及它们之间的关系更是让人望而却步,这样一个“庞然大物”,普通开发者是不敢轻易去碰的。

LSP显然把小巧作为设计目标之一,它选择做最小子集,贯彻了团队一贯“节制”的作风。它关心的是用户在编辑代码时最经常处理的物理实体(如文件、目录)和状态(如光标位置),它根本没有试图去理解语言的特性,当然编译也不是它所关心的问题,所以自然不会涉及语法树一类的复杂概念。

小归小,功能可不能少,所以抽象就非常关键了。LSP最重要的概念是动作和位置,LSP的大部分请求都是在表达“在特定位置执行规定的动作”。将功能抽象成请求(request)和响应(response),同时规定请求和响应的数据规格(schema)。在开发者看来,其概念非常少,交互形式也很简单,实现起来非常容易,学习和理解起来非常轻松。

LSP作为一个经典案例告诉我们,做设计的时候一定要倾向于简单。首先LSP是一个基于文本的协议,文本可降低理解和调试的难度。参考超文本传送协议(Hypertext Transfer Protocol,HTTP)和描述性状态迁移(Representational State Transfer,REST)的成功,很难想象如果它是一个二进制协议会带来什么局面。另外,同样是文本协议的简单对象访问协议(Simple Object Access Protocol,SOAP)也早已“作古”,足以说明“简单”在打造开发者生态里的重要性。

其次LSP是一个基于JSON的协议,JSON可以说是最易读的结构化数据格式。大家看看各个代码仓库里的配置文件都是什么格式就知道这是个多么正确的决定了。现在还有人在新项目里用可扩展标记语言(Extensible Markup Language,XML)吗?几乎没有,人们一般都会倾向于选择更简单的JSON。

最后LSP是一个基于JSON的远程过程调用(Remote Procedure Call,RPC)协议。由于JSON很流行,各大语言都对它有极好的支持,所以开发者根本不需要处理序列化、反序列化一类的问题,这对于实现层面来说也是选择了“简单”。

在VS Code或其他IDE中添加不同语言的调试器是令人沮丧的工作,因为每个调试器都使用不同的API 来实现大致相同的功能,因此无法轻松适配诸多不同的调试器。

DAP协议背后的理念是对VS Code与调试器进行协作的方式抽象到协议中。这样不同的调试器只要针对DAP协议开发调试适配器就可以与VS Code进行协作调试程序。这样DAP协议使VS Code能够通过调试适配器与不同的调试器进行协作成为可能。基于DAP协议的调试适配器不仅在VS Code中,还可以在其他多个开发工具中重复使用,从而显著减少了在不同工具中支持新调试器的工作量。

LSP和DAP两大协议通过合理的抽象使代码理解和调试在VS Code中轻松支持众多编程语言及其调试器。

使用VS Code远程开发环境(VS Code Remote Development,VSCRD)可以在远程环境(如云端虚拟机、容器)里打开一个VS Code工作区,用本地的VS Code连上去就像在本地开发一样方便。VS Code远程开发环境示意图如图1-4所示。

图1-4 VS Code远程开发环境示意图

VS Code远程开发环境所有的交互都在本地UI内完成,响应迅速。而远程桌面由于传输的是截屏画面,数据往返延迟很大,卡顿是常态。

VS Code远程开发环境的UI运行在本地,遵从所有本地设置,所以开发者依然可以使用自己所习惯的快捷键、布局、字体,避免在远程主机上重复配置带来工作效率层面的开销。

在远程工作区里,VS Code的原生功能和所有第三方插件的功能都依然可用,避免了采用远程桌面方式需要在远程工作区重新安装第三方插件。

总之,VS Code远程开发环境将远程文件系统完整地映射到本地,使远程工作区和本地工作区两者在操作逻辑上基本一致。VS Code远程开发环境为什么那么“神奇”,能够实现以上效果呢?VS Code远程开发环境工作原理示意图如图1-5所示。

图1-5 VS Code远程开发环境工作原理示意图(源自VS Code官网)

图1-5中Local OS是本地操作系统;Remote OS是远程操作系统;Theme/UI Extension是界面主题插件;Workspace Extension是工作区插件;Source Code是源代码的文件系统;Terminal Processes是终端控制台相关进程;Running Application是运行中的应用程序,指当前开发的程序;Debugger是调试器。

进程隔离的插件模型中插件主机Extension Host(也就是VS Code Server)与主程序通过 JSON RPC进行网络通信,那么把插件主机Extension Host放在远程或者放在本地运行并没有本质的区别。UI渲染与插件逻辑隔离,所有插件的UI都由VS Code统一渲染,所以插件里面只有纯业务逻辑,行为高度统一,在远程还是本地运行并没有区别。VS Code的两大协议LSP和DAP都非常精简,天然适合网络延迟高的情况,用在远程开发上非常合适。

由此可见,VS Code的开发团队在其架构上的决策无疑是非常有前瞻性的,与此同时,他们对细节的把握也无可挑剔。正因为有如此良好的架构设计和如此扎实的工程基础,VS Code远程开发环境这样神奇的工具才得以诞生。VS Code远程开发环境被认为是VS Code的集大成者。

VS Code远程开发环境有两大非常有用的应用场景。一是开发环境配置起来很烦琐的场景,如物联网开发,需要自己安装和配置各种工具和插件。在VS Code远程开发环境里,使用一个远程工作区的模板即可解决。二是本地计算机配置太低,无法执行某些任务,比如大数据分析、深度学习训练,海量数据或计算资源需求量大的任务需要非常好的机器或云计算环境。在 VS Code远程开发环境里,可以直接操作远程文件系统,使用远程的计算资源。

打开VS Code,单击最左侧的管理扩展插件图标。在扩展插件市场里搜索C++,找到Microsoft C/C++扩展插件C/C++ for Visual Studio Code,单击Install安装即可,如图1-6所示。

图1-6 安装Microsoft C/C++扩展插件

VS Code的C/C++插件并不包含C/C++编译器和调试器,我们需要自己安装C/C++编译器和调试器,如果你在计算机上已经安装了C/C++编译器和调试器可以直接使用。在不同操作系统中常用的C/C++编译器和调试器如下。

GCC on Linux。

MinGW-W64 is GCC for Windows 64 & 32 bits。

Microsoft C++ compiler on Windows。

Clang for XCode on macOS。

不同的C/C++编译器和调试器的用法有所不同,由于GNU编译器套件(GNU Compiler Collection,GCC)在不同操作系统上都可以使用,而且用法基本一致,我们这里选用MinGW-W64用于Windows操作系统,选用GCC和GNU调试器(GNU Debugger,GDB)用于不同的Linux发行版和macOS。

1.Windows操作系统下安装MinGW-W64

MinGW(Minimalist GNU for Windows)是一个适用于Windows操作系统的应用程序的极简开发环境。MinGW提供了一个完整的开源编程工具集,适用于原生MS-Windows应用程序的开发,并且不依赖于任何第三方C运行时动态链接库(Dynamic Link Library,DLL)。MinGW主要供在MS-Windows平台上工作的开发人员使用,也可以跨平台使用。

MinGW-W64是原始的mingw.org项目的升级版,该项目旨在支持Windows操作系统上的GCC编译器。它在2007年有了不同版本,以便为64位和新API提供支持。从那以后,它得到了广泛的使用和开发。

在Windows操作系统下可以下载MinGW-W64的安装文件[3]MinGW-W64-install.exe,运行安装文件,如图1-7所示。

[3] 关注微信公众号“读行学”在“图书”菜单进入本书页面附录1,即可获得MinGW-W64的安装文件下载地址。

图1-7所示安装MinGW-W64-install.exe的过程中有几个选项需要说明。

Version:制定版本号,从4.9.1到9.x.0,按需选择,没有特殊要求,可用最新版。

Architecture:跟操作系统有关,64位系统选择x86_64,32位系统选择i686。

Threads:设置线程标准,可选posix或win32,为了与其他平台保持一致,这里我们选择posix。

Exception:设置异常处理系统,Architecture为x86_64时可选seh或sjlj,为i686时可选dwarf或sjlj。

Build revision:构建版本号,选择最大即可。

图1-7 运行安装文件MinGW-W64-install.exe

添加环境变量后,打开命令提示符窗口,执行命令gcc -v和gdb -v看看是否安装成功。

2.Ubuntu Linux操作系统下安装GCC和GDB

在Ubuntu Linux操作系统下安装GCC和GDB的命令如下。

sudo apt update
sudo apt install build-essential
gcc -v # 看看是否安装成功
sudo apt install gdb
gdb -v # 看看是否安装成功

3.macOS下安装GCC和GDB

在macOS下安装GCC和GDB的命令如下。

xcode-select —install
brew update
brew install gcc
# 因为在 macOS下gcc这个名称被占用,这里 gcc-9 中的 9 为版本号
/usr/local/bin/gcc-9 -v # 看看是否安装成功
brew install gdb
/usr/local/bin/gdb -v # 看看是否安装成功

在命令行下使用“code .”命令可以打开当前文件夹,并将当前文件夹作为工作区。同时VS Code会在当前工作区创建.vscode文件夹并在其中创建3个JSON配置文件,如下。

tasks.json (build instructions)。

launch.json (debugger settings)。

c_cpp_properties.json (compiler path and IntelliSense settings)。

tasks.json配置文件是用来告诉VS Code如何编译程序,这里我们可以通过修改tasks.json配置构建任务,实际上就是调用GCC编译器将源代码编译成可执行程序。

我们可以使用以下hello.c代码测试构建任务是否成功。

#include <stdio.h>

int main()
{
    printf("hello world!\n");
}

通过菜单Terminal选择Configure Default Build Task...或者Configure Tasks...,然后选择C/C++: gcc build active file,在当前项目目录(工作区)下自动生成.vscode/tasks.json配置文件,构建任务的简要配置示例tasks.json如下,其中command表示指明编译器;args表示编译器GCC的参数;isDefault为true表示按Ctrl+Shift+B快捷键将自动执行默认构建任务(default build task)。

{
    "version": "2.0.0",
    "tasks": [
        {
            "type": "shell",
            "label": "gcc build active file",
            "command": "/usr/local/bin/gcc-9",
            "args": [
                "-g",
                "${file}",
                "-o",
                "${fileDirname}/${fileBasenameNoExtension}"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            }
        }
    ]
}

配置文件launch.json用于告诉VS Code如何调用调试器调试程序,我们以GDB为例。配置调试环境可以通过单击VS Code主界面左侧的“启动和调试”或者按快捷键Ctrl+Shift+D进入Debug二级菜单,然后创建一个launch.json配置文件(create a launch.json file),选择C++(GDB/LLDB)。

调试环境配置文件launch.json的简单示例如下。

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "gcc build and debug active file",
            "type": "cppdbg",
            "request": "launch",
            "program": "${fileDirname}/${fileBasenameNoExtension}",
            "args": [],
            "stopAtEntry": true,
            "cwd": "${workspaceFolder}",
            "environment": [],
            "externalConsole": false,
            "MIMode": "gdb",
            "setupCommands": [
                {
                    "description": "Enable pretty-printing for gdb",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                }
            ],
            "preLaunchTask": "gcc build active file",
            "miDebuggerPath": "/usr/local/bin/gdb"
        }
    ]
}

进行程序调试的方法是通过菜单Run选择Start Debugging或者直接按F5键。

VS Code远程开发环境可以在远程主机(包括云主机)、容器(如Docker)、适用于Linux的Windows子系统(Windows Subsystem for Linux,WSL)等搭建。

VS Code远程开发环境支持多种模式,以本地和远程的通信方式及远程环境的类型来划分,如下。

1.Remote-SSH

Remote-SSH是最常见的方案,不论远程环境是何种类型,都可以通用地支持不同类型的操作系统、容器、云主机、WSL等。VS Code远程开发环境Remote-SSH如图1-8所示。

图1-8 VS Code远程开发环境Remote-SSH(源自VS Code官网)

图1-8中Local OS是本地操作系统;Remote Machine/VM是远程计算机或虚拟机;Theme/ UI Extension是界面主题插件;SSH Tunnel是安全外壳协议(Secure Shell,SSH)隧道;Workspace Extension是工作区插件;Source Code是源代码的文件系统;Terminal Processes是终端控制台相关进程;Running Application是运行中的应用程序,指当前开发的程序;Debugger是调试器。

2.Remote-WSL

Remote-WSL仅限于Windows 用户在本机使用WSL方式安装的Linux操作系统下运行,Windows和Linux操作系统之间可以直接挂载文件系统,文件读写速度比较快,但这不是一种通用的解决方案。VS Code远程开发环境Remote-WSL如图1-9所示。

图1-9 VS Code远程开发环境Remote-WSL(源自VS Code官网)

图1-9中,除了前文已经注明的英文,Windows Subsystem for Linux(WSL)指本地Windows主机的子系统Linux;File System是文件系统;Exposed Port是暴露端口;Volume Mount(/mnt/c)是指将本地文件系统挂载到WSL中的/mnt/c目录。

3.Remote-Containers

Remote-Containers这种方式适用于项目运行环境是Docker一类的容器。本地系统中安装了Docker一类的容器,这样本机操作系统和容器环境可以挂载共享源代码,文件读写速度比较快,但这不是一种通用的解决方案。VS Code远程开发环境Remote - Containers如图1-10所示。

图1-10 VS Code 远程开发环境Remote-Containers(源自VS Code官网)

图1-10中,除了前文已经注明的英文,Container是容器,这里指本地系统上运行的容器,因为要将本地的源代码挂载到容器里。

4.GitHub Codespaces

Visual Studio Codespaces是微软提供的云上远程开发环境,注册开通后本地VS Code可以直接连接使用,无须自行配置远程开发环境,这样开发、部署、运维全部可以上云了。

目前Visual Studio Codespaces服务已关闭,它被GitHub Codespaces取代。

5.Web Remote-code-server

code-server是在浏览器打开远程开发环境里的VS Code,无须在本地安装VS Code。

在上述5种远程开发环境中,我们以最为通用的Remote-SSH和Web Remote-code-server为例介绍其具体的安装配置过程。

使用VS Code远程开发环境Remote-SSH配置模式可以打开远程主机、虚拟机、容器或者WSL上的远程目录,只要配置了SSH服务器,VS Code就能像访问本地目录一样访问SSH远程目录。

VS Code远程开发环境Remote-SSH配置大致分为两步:(1)在远程环境配置SSH服务器;(2)在本地VS Code中安装Remote-SSH插件并连接SSH服务器。

下面对这两个步骤进行具体介绍。

(1)在远程环境中配置SSH服务器。

安装SSH服务器openssh-server。

$ sudo apt install openssh-server

如果Ubuntu Linux已经默认安装了openssh-server,但并没有配置和启动SSH服务器。使用sudo/etc/init.d/ssh start命令启动,会提示不能加载host key,如下。

$ sudo /etc/init.d/ssh start
 * Starting OpenBSD Secure Shell server sshd
Could not load host key: /etc/ssh/ssh_host_rsa_key
Could not load host key: /etc/ssh/ssh_host_ecdsa_key
Could not load host key: /etc/ssh/ssh_host_ed25519_key

重新安装openssh-server,并且在安装过程中会更新配置文件/etc/ssh/sshd_config。

$ sudo apt remove openssh-server
$ sudo apt install openssh-server

安装好openssh-server,修改配置文件/etc/ssh/sshd_config中的PasswordAuthentication配置项为yes,然后使用sudo /etc/init.d/ssh start命令启动SSH服务,如下。

$ sudo vi /etc/ssh/sshd_config
PasswordAuthentication yes
$ sudo /etc/init.d/ssh start
 * Starting OpenBSD Secure Shell server sshd

如果没有出错,则用putty、SecureCRT、SSH Secure Shell Client等SSH客户端软件,输入你的服务器的IP地址。如果一切正常,等一会儿就可以连接上了。并且使用现有的Ubuntu Linux用户名和密码就可以登录了。也可以在SSH服务器上通过ssh命令登录,自己测试SSH服务器是否工作正常。这里在Windows PowerShell中使用格式为ssh name@ip的命令登录,测试效果如下。

PS C:\Users\lx> ssh mengning@192.168.0.210
mengning@192.168.0.210's password:
Welcome to Ubuntu 18.04.4 LTS (GNU/Linux 4.4.0-17763-Microsoft x86_64)
...
mengning@DESKTOP-IBF51L8:~$ exit
logout
Connection to 192.168.0.210 closeD.
PS C:\Users\lx>

如果通过证书认证登录SSH服务器,则在SSH服务过程中,所有的内容都是加密传输的,安全性更加有保证,而且经过证书认证设置还能实现自动登录,更方便、更安全。首先需要生成RSA密钥对,可以直接使用ssh-keygen命令生成,如下。

$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/mengning/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/mengning/.ssh/id_rsA.
Your public key has been saved in /home/mengning/.ssh/id_rsA.puB.
The key fingerprint is:
SHA256:2GVPKrRdFxAaD8xoxDmseD0UrOK9af8c1g1XVZJkFG0 mengning@DESKTOP-IBF51L8
The key's randomart image is:
+---[RSA 2048]----+
...
+----[SHA256]-----+

生成的RSA密钥对存放在默认目录下。使用ssh-keygen命令生成密钥对的过程中会提示输入passphrase,这相当于给证书加个密码,是提高安全性的措施,这样即使证书不小心被人复制也不怕。当然如果不输入passphrase,后面即可实现通过证书认证的自动登录,这里留空。

ssh-keygen命令会生成两个密钥,首先将公钥通过ssh-copy-id命令发给SSH服务器,然后将私钥从服务器上复制出来,并删除服务器上的私钥文件。

~$ ssh-copy-id -i ~/.ssh/id_rsA.pub mengning@192.168.0.210
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: 
"/home/mengning/.ssh/id_rsA.pub"
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
mengning@192.168.0.210's password:
Number of key(s) added: 1
Now try logging into the machine, with:   "ssh 'mengning@192.168.0.210'"
and check to make sure that only the key(s) you wanted were added

这里为了测试密钥在SSH服务器是否配置成功,我们就在SSH服务器主机上配置SSH客户端自动登录来测试。创建SSH客户端配置文件~/.ssh/config,如下。

vi ~/.ssh/config
Host start
    HostName 127.0.0.1
    User mengning
    PreferredAuthentications publickey
    identityfile ~/.ssh/id_rsa
    Port 22

配置文件~/.ssh/config的权限需要适当设置,使SSH客户端有权限读取配置文件。类UNIX操作系统下使用以下命令。

sudo chmod 600 ~/.ssh/config

这时就可以使用以下命令自动登录了。

ssh mengning@192.168.0.210

如果是生产环境,建议为了安全修改配置文件/etc/ssh/sshd_config中的PasswordAuthentication配置项为no,关闭密码登录,然后使用sudo /etc/init.d/ssh restart命令启动SSH服务。

到这里SSH服务器就配置好了,而且还有一个私钥id_rsa文件作为通行证,接下来就可以配置VS Code远程开发环境了。

(2)在本地VS Code中安装Remote-SSH插件并连接SSH服务器。

按Ctrl+Shift+X快捷键进入插件菜单,搜索Remote-SSH,如图1-11所示,安装Remote-SSH插件。

图1-11 安装Remote-SSH插件

按Ctrl+Shift+P快捷键或者按F1键进入命令行,搜索ssh,如图1-12所示,选择Remote-SSH:Add New SSH Host...。

图1-12 选择Remote-SSH:Add New SSH Host...

填入格式为ssh name@ip的登录命令,如图1-13所示。

图1-13 填入登录命令

然后在~/.ssh/目录下配置config文件如下,并把私钥id_rsa文件放到~/.ssh/目录下。

Host 192.168.0.210
    HostName 192.168.0.210
    User mengning
        PreferredAuthentications publickey
        IdentityFile ~/.ssh/id_rsa
        Port 22

可以在命令行下使用ssh mengning@192.168.0.210测试使用该配置文件和私钥 id_rsa 文件是否能正常登录远程主机。

接下来就可以按Ctrl+Shift+P快捷键或者按F1键进入命令行,搜索ssh,选择Remote-SSH: Connect to Host...,然后选择192.168.0.210,在连接过程中及时根据提示选择远程主机的操作系统类型,如Linux、Windows或macOS,以便VS Code登录到远程主机安装VS Code Server。

安装完成后,通过VS Code远程开发环境就像在本地主机上一样,可以打开远程目录,也可以直接打开远程主机的终端。这样就可以愉快地使用 VS Code 远程开发环境了,如图 1-14所示。

图1-14 用VS Code打开远程目录和远程主机的终端

对于VS Code远程开发环境Web Remote,只要在服务器端安装配置好code-server,就可以在任何浏览器上使用VS Code,也就是code-server将VS Code打包到了Web服务中,这样也就实现了另外一种VS Code远程开发环境Web Remote。只是这是第三方(coder.com)提供的解决方案,不是VS Code官方提供的解决方案。

code-server是coder.com提供的解决方案,读者可以在GitHub网站搜索code-server项目。code-server支持多种运行环境,如Debian、Ubuntu、Fedora、Red Hat、SUSE、Arch Linux、macOS等,也支持使用yarn和npm方式安装,还支持在Docker环境下运行。我们还是以WSL Ubuntu Linux为例简单介绍code-server的安装配置过程。

可以直接使用官方提供的脚本按以下命令安装,根据提示操作即可。

curl -fsSL https://code-server.dev/install.sh | sh -s -- --dry-run

由于文件较大,在安装过程中可能会断线,也可以按以下步骤安装。

(1)下载code-server安装包。可以在GitHub网站搜索code-server项目,并根据你的系统类型选择合适的安装包。code-server官方提供的下载命令是curl,但是由于安装文件比较大,建议使用支持多线程和断点续传的aria2c命令。

curl -sSOL https://github.com/cdr/code-server/releases/download/v3.3.1/code-server_
           3.3.1_amd64.deb
# 推荐安装 aria2 来下载
sudo apt update
sudo apt install aria2
aria2c https://github.com/cdr/code-server/releases/download/v3.3.1/code-server_
       3.3.1_amd64.deb

(2)安装code-server。在Ubuntu Linux操作系统下,以下两个命令都可以用于安装。

sudo dpkg -i code-server_3.3.1_amd64.deb
sudo apt install ./code-server_3.3.1_amd64.deb

(3)配置启动。

$ systemctl --user enable --now code-server
# 也可以直接执行 code-server
$ code-server
info  Using config file ~/.config/code-server/config.yaml
info  Using user-data-dir ~/.local/share/code-server
info  code-server 3.4.0 69ad52907e8ea109345831d29da5425cb2a55047
info  HTTP server listening on http://127.0.0.1:8080

这样就可以通过浏览器使用VS Code了,如图1-15所示。本机测试网址为http://127.0.0.1:8080(如果8080端口被占用则自动使用8081端口),非本机访问需要换上对应的IP地址。密码在配置文件~/.config/code-server/config.yaml中。

图1-15 通过浏览器使用VS Code

code-server不能在线安装插件,与正常的VS Code相比还是有一些功能限制的,不过它对于专用的在线开发环境比较合适。

一、填空题

1.VS Code中调出查找并运行所有命令工具的快捷键是(    )。

2.VS Code中切换集成终端的快捷键是(    )。

3.VS Code中用于代码注释和取消注释的快捷键是(    )。

二、实验

1.完成基于VS Code的C/C++开发调试环境配置。

2.完成基于Remote-SSH的VS Code远程开发环境配置。


读者服务:

微信扫码关注【异步社区】微信公众号,回复“e57850”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。

关于版本控制,我们应该都比较熟悉,甚至经常用到。比如我们在写一个文档的过程中,经常会将其另存为一个独立文件作为备份,来管理文档的不同版本,这就是版本管理,只是这是用人工的方式来进行版本控制。当版本的数量庞大的时候,人工进行版本管理就比较容易出差错,这时就出现了用软件作为工具来进行版本控制的系统,这就是版本控制系统。

比较常见的版本控制的方式有两种:一种是独立文件或整体备份的方式,比如使用另存为将整个项目或者整个文档整体备份成一个版本;另一种就是补丁包的方式,比如使用diff命令将当前版本与上一个版本对比得出两者之间的差异从而形成一个补丁包,这样上一个版本加上这个补丁包就是当前版本。显然前者会产生大量重复数据,消耗比较多的存储资源,但是每一个版本都是独立且完整的;后者几乎没有重复数据,存储效率更高,但是每一个版本的补丁包无法独立使用,因为它们都需要递归地依赖上一个版本才能合并出一个完整的版本。

版本控制系统大致分为两大类,一类是中心版本控制系统,比如Concurrent Versions System(以下称CVS)和Subversion(以下称SVN);另一类就是分布式版本控制系统,比如我们即将重点介绍的Git,它是目前世界上最先进的分布式版本控制系统之一。

Git的诞生过程大致是这样的。

在2002年以前,Linux内核源代码文件是通过diff的方式生成补丁包发给莱纳斯·托瓦兹(Linus Tovalds),然后由莱纳斯本人通过手动方式合并代码!莱纳斯坚定地反对CVS和SVN,因为这些中心版本控制系统不但速度慢,而且必须联网才能使用。有一些商用的版本控制系统,虽然比CVS和SVN好用,但那是要付费购买的,这和Linux的开源精神不符。

2002年莱纳斯选择了一个商用的版本控制系统BitKeeper——BitMover公司出于“人道主义精神”,授权Linux社区免费使用BitKeeper。其实这应该是莱纳斯与BitMover公司的创始人之间的私人关系促成的。

2005年,BitMover公司要收回Linux社区的免费使用权。为什么BitMover公司要收回Linux社区的免费使用权?根据一些资料显示,大概是因为Linux社区里有人违反授权协议将BitKeeper用于其他项目,从而侵害了BitMover公司的商业利益。

为了替代BitKeeper版本控制系统,莱纳斯花了两周时间自己用C语言写了一个分布式版本控制系统,这就是Git!一个月之内,Linux内核源代码已经用Git管理了!莱纳斯除Linux内核之外又产出了一个“神作”——Git分布式版本控制系统。

大约经过了3年时间的迅猛发展,2008年,GitHub网站上线了,它专为开源项目免费提供Git存储。如今GitHub已经成为全球最大的程序员社交网站,程序员将其戏称为“全球最大的单身男性社交网站”。

2016年,也就是Git诞生11年之后,可能是由于Git太流行了,以致BitKeeper在商业上无法继续运营,BitMover公司只得将BitKeeper的源代码贡献给开源社区。

2018年,GitHub网站被微软以75亿美元收购。

大概了解了Git的历史,接下来我们看看Git的基本操作示意图,如图2-1所示。

图2-1 Git的基本操作示意图

实际上本地Repo(图2-1所示的中间存储库Repository)都存在项目根目录下的.git文件夹中,其内部可能有多个分支,但至少有一个叫master的分支。

本地Repo中的某个分支被checkout到当前Workspace,就能在当前源代码目录中看到一份完整的源代码。

若要在Workspace中新增文件或修改文件,只有完成add和commit两步操作才能将新增或修改的文件纳入本地Repo中进行版本管理。add和commit两步操作中间的Index索引数据应该也是和本地Repo一样存储在项目根目录下的.git文件夹中。

Git与CVS、SVN的操作逻辑大致一致,只是实现了本地的中心化版本控制。接下来看看Git是怎么做到分布式版本控制的。

本地Repo中的分支与一个或多个远程Repo(如图2-1所示的Remote)中的分支存在跟踪关系,这样就构成了Git分布式版本控制的网状结构。

显然Git的分布式网状结构比CVS和SVN在理解和使用上更为复杂一些。比如增加了本地与远程Repo之间数据同步操作clone、fetch、push、pull。

通过git --help命令可以查看Git命令用法及常用的Git命令列表。

$ git --help
usage: git [--version] [--help] [-C <path>] [-c <name>=<value>]
         [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
         [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare]
         [--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
         <command> [<args>]

These are common Git commands used in various situations:

start a working area (see also: git help tutorial)
    clone    Clone a repository into a new directory
    init     Create an empty Git repository or reinitialize an existing one

work on the current change (see also: git help everyday)
    add      Add file contents to the index
    mv       Move or rename a file, a directory, or a symlink
    reset    Reset current HEAD to the specified state
    rm       Remove files from the working tree and from the index

examine the history and state (see also: git help revisions)
    bisect   Use binary search to find the commit that introduced a bug
    grep     Print lines matching a pattern
    log      Show commit logs
    show     Show various types of objects
    status   Show the working tree status

grow, mark and tweak your common history
    branch   List, create, or delete branches
    checkout Switch branches or restore working tree files
    commit   Record changes to the repository
    diff     Show changes between commits, commit and working tree, etc
    merge    Join two or more development histories together
    rebase   Reapply commits on top of another base tip
    tag      Create, list, delete or verify a tag object signed with GPG

collaborate (see also: git help workflows)
    fetch    Download objects and refs from another repository
    pull     Fetch from and integrate with another repository or a local branch
    push     Update remote refs along with associated objects

'git help -a' and 'git help -g' list available subcommands and some
concept guides. See 'git help <command>' or 'git help <concept>'
to read about a specific subcommand or co

接下来我们用五大场景来介绍Git的基本用法,基本遵循从简单到复杂,最终回归到简单的原则,同时这五大场景也基本能够覆盖大多数实际使用的应用场景。

如果你使用了VS Code,那么恭喜你,安装VS Code时已经附带安装了Git。建议Windows用户通过安装VS Code完成Git软件包的安装。

如果你使用的是Linux或者类UNIX的操作系统,以Ubuntu为例,大致可以通过类似以下命令安装Git软件包。

sudo apt install git

当然Linux和macOS也可以通过安装VS Code附带安装Git,但是在没有图形界面的服务器端单独安装Git软件包比较合适。

在VS Code中打开文件夹(Ctrl+O),实际上就是打开一个项目工作区。VS Code的工作区概念与Git的工作区概念基本一致,都是指当前项目文件夹里的整套源代码。

如果项目文件夹里没有Git存储库,这时打开源代码管理(Ctrl+Shift+G),如图2-2所示,可以直接单击“初始化存储库”按钮,初始化一个Git本地版本库。

图2-2 打开源代码管理

如果习惯使用命令行,只需在项目根目录下执行git init命令即可初始化一个Git本地版本库。

如果你已经在Gitee或者GitHub网站上创建了版本库,可以通过git clone命令,将版本库克隆到本地完成本地版本库的初始化。git clone命令的用法如下。

git clone https://DOMAIN_NAME/YOUR_NAME/REPO_NAME.git

不管使用何种方式初始化一个Git本地版本库,实际上都是在项目根目录下创建了一个.git文件夹。若读者感兴趣,可以进入.git文件夹进一步了解Git版本库内部数据的存储结构。

在VS Code中打开源代码管理可以看到与上一个版本相比本项目的所有更改,即当前工作区的状态,比如图2-3所示的源代码管理中左侧,以绿色U标记的文件为没有添加到版本库进行跟踪的文件(untracked files)、以橙色M标记的文件为已修改(modified)未提交(changes not staged for commit)的文件。

图2-3 源代码管理中以绿色U和橙色M标记文件

如果习惯使用命令行,只需在项目目录下执行git status命令即可查看Git本地版本库当前工作区的状态。

$ git status
On branch master
Your branch is up to date with 'origin/master'.

Changes not staged for commit:
    (use "git add <file>..." to update what will be committed)
    (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   lab7/README.md

Untracked files:
    (use "git add <file>..." to include in what will be committed)
"lab1/mybot/\357\202\267\343\200\212\345\267\245\347\250\213\345\214\226\347\274\226\347\250\213\345\256\236\346\210\230\343\200\213\350\242\253\346\225\231\350\202\262\351\203\250\350\256\244\345\256\232\344\270\272\345\233\275\345\256\266\347\262\276\345\223\201\345\234\250\347\272\277\345\274\200\346\224\276\350\257\276\347\250\213 (2018).txt"

no changes added to commit (use "git add" and/or "git commit

在VS Code中打开源代码管理可以看到与上一个版本相比本项目的所有更改,即当前工作区的状态,如图2-4所示。只要在“更改”列表里的文件上单击“+”即可暂存更改(git add FILES),即将更改的文件加入“暂存的更改”列表,单击“”即可放弃更改(git checkout -- FILES / git checkout .),即将该文件中的更改清除。

图2-4 单击“+”即可暂存更改

只要在“暂存的更改”列表里的文件上单击“—”即可取消暂存更改(git reset HEAD FILES),即将暂存更改的文件从“暂存的更改”列表里撤销,使其重新回到“更改”列表,如图 2-5所示。

图2-5 单击“—”即可取消暂存更改

如果习惯使用命令行,需要清楚对应的几个命令的用法,如下。

git add FILES # 指定文件或文件列表
git add .     # 用“.”表示当前目录

以上两个命令是将特定文件或者当前目录下所有文件添加到暂存区。

git reset HEAD FILES # 指定文件或文件列表
git reset HEAD

以上两个命令是取消将特定文件或者当前目录下所有文件添加到暂存区,即将它们从暂存区中删除。只有在暂存区登记的文件才会在提交代码时被存入版本库。

git checkout -- FILES # 不要忘记“--” ,不写系统就会把FILES当分支名
git checkout .

以上两个命令的效果是放弃对特定文件或者所有文件的修改,实际上是重新checkout特定文件或者所有文件到工作区,注意这样会覆盖掉已修改未暂存的内容。不希望被覆盖的文件可以先使用git add将其添加到暂存区。

在VS Code中打开源代码管理,如图2-6所示,只要“暂存的更改”列表里有文件就可以直接单击“√”(Ctrl+Enter)将暂存的文件提交到仓库,只是在提交之前会强制要求输入提交日志消息。

图2-6 单击“√”将暂存的文件提交到仓库

注意:一旦提交到仓库,尽管可以撤销上次提交,但是这样依然会在仓库里留下记录,提交和撤销上次提交的菜单命令如图2-7所示。可以通过git reflog命令查看当前HEAD之后的提交记录,稍后我们通过命令行方式详细解释。

图2-7 提交和撤销上次提交的菜单命令

如果习惯使用命令行,注意把暂存区里的文件提交到仓库主要使用git commit命令,但是还会涉及撤销提交和查看日志的命令。

git commit -m "wrote a commit log"

通过以上命令可以把暂存区里的文件提交到仓库。

git log

通过以上命令可以查看提交日志,可以看到当前HEAD之前的所有提交记录。

git reset --hard HEAD^
git reset --hard HEAD^^
git reset --hard HEAD~100
git reset --hard 128个字符的commit-id
git reset --hard 简写为commit-id的头几个字符

通过以上命令可以让HEAD回退到指定版本,比如HEAD^表示HEAD的前一个版本、HEAD^^表示HEAD的前两个版本、HEAD~100表示HEAD的前100个版本。也可以用版本号字符串来指定任意一个版本。

注意:HEAD只是一个指向特定版本的指针,通过git reset --hard回退之后,HEAD指向的不是最新的版本,而git log只能查看HEAD及其之前(时间更早)的提交记录。这就会产生一个问题,我们可以通过git reset --hard回到过去某个版本,那怎么“回到未来”?那就要想办法查到HEAD指向的版本之后(时间更晚)的提交记录。

git reflog

通过以上命令可以查看当前HEAD之后(时间更晚)的提交记录,从而可以通过git reset --hard回到未来。

场景一主要在本地对源代码进行基本的版本控制,通过git add和git commit -m提交版本。有了提交记录之后可以灵活地将当前工作区里的源代码回退到过去的某个版本,也就是回到过去。回到过去之后,有可能发现之前撤销的某个版本是有价值的,希望找回来,这就需要回到未来。过去和未来之间的分界点就是HEAD,即当前工作区所依赖的版本。

git init # 初始化一个本地版本库
git status # 查看当前工作区的状态
git add [FILES] # 把文件添加到暂存区
git commit -m "wrote a commit log infro" # 把暂存区里的文件提交到仓库
git log # 查看当前HEAD之前的提交记录,便于回到过去
git reset --hard HEAD^^/HEAD~100/commit-id/commit-id的头几个字符 # 回退
git reflog # 可以查看当前HEAD之后的提交记录,便于回到未来
git reset --hard commit-id/commit-id的头几个字符 # 回退
git revert HEAD^^/HEAD~100/commit-id/commit-id # 通过提交新commit的方式回滚

如果你已经在Gitee或者GitHub等网站上创建了Git版本库,可以通过git clone命令,将版本库克隆到本地完成本地版本库的初始化。git clone命令的用法如下。

git clone https://DOMAIN_NAME/YOUR_NAME/REPO_NAME.git

也可以在VS Code中打开源代码管理,如果当前没有打开的项目文件夹,可以看到源代码管理界面上有图2-8所示的“打开文件夹”和“克隆存储库”两个按钮,这时单击“克隆存储库”,即可输入存储库URL,按Enter键选择保存的目录位置,即可完成将远程的版本库克隆到本地的任务。

图2-8 在VS Code中克隆存储库

通过克隆远程版本库从而在本地创建了一个版本库,这时就可以参照场景一的本地版本库基本用法,执行查看工作区状态、暂存更改的文件、把暂存区提交到仓库,以及回到过去、回到未来等本地版本控制的基本操作。

在场景二Git远程版本库的基本用法的介绍中我们假定远程版本库用作远程备份或者公开源代码。还是像场景一一样介绍单人的版本控制,为了循序渐进,暂时不涉及多人项目的协作。

这里使用git clone之后默认的分支,即远程为origin/master、本地为master,没有创建其他分支。查看本地版本库所跟踪的远程存储库的命令为git remote。

$ git remote
origin

执行git remote命令后可以看到克隆之后默认的远程存储库名称为origin。

$ git remote -v
origin  https://DOMAIN_NAME/YOUR_NAME/REPO_NAME.git (fetch)
origin  https://DOMAIN_NAME/YOUR_NAME/REPO_NAME.git (push)

执行git remote -v命令可以查看更详细的远程存储库信息,包括抓取(fetch)的远程存储库URL和推送(push)的远程存储库URL。

git fetch、git push和git clone是3个对远程存储库的基本操作的命令,而git pull实际上是git fetch与git merge的组合。

git clone命令的官方解释是“Clone a repository into a new directory”,即克隆一个存储库到一个新的目录下。

git fetch命令的官方解释是“Download objects and refs from another repository”,即下载一个远程存储库数据对象等信息到本地存储库。

git push命令的官方解释是“Update remote refs along with associated objects”,即将本地存储库的相关数据对象更新到远程存储库。

git merge命令的官方解释是“Join two or more development histories together”,即合并两个或多个开发历史记录。

git pull命令的官方解释是“Fetch from and integrate with another repository or a local branch”,即从其他存储库或分支抓取并合并到当前存储库的当前分支。

如果不理解Git背后的设计理念,或者说不理解其设计原理,记住这些命令及其实际作用还是有一些难度,而且在相对复杂的项目环境下使用容易出错。但是在深入Git背后的设计理念之前,还是可以掌握一些在默认条件下的基本用法,从而快速上手使用。

我们假定场景二所述的工作是串行的并且能及时将本地与远程的项目同步,也就是对于一个单人项目,要么在本地提交代码到仓库,要么通过Web页面更新远程仓库,而且这两种方式不会同时发生。不管是在本地仓库还是在远程仓库,修改代码之前都首先进行代码同步操作,防止产生分叉和冲突。

在VS Code中版本库同步操作已经简化为一个菜单命令,如图2-9所示。

在我们假定的使用场景中,此同步操作会将提交项推送到远程仓库origin/master,并从远程仓库origin/master拉取提交项到本地master。

但是在我们假定的使用场景中,实际上只会有提交项被推送或拉取,不会同时有提交项被推送并被拉取,因此不会产生冲突。

图2-9 在VS Code中版本库同步操作菜单命令

在我们假定的使用场景中,在命令环境下同步操作大致相当于使用以下两条命令。

git pull
git push

同步完成后,不管是在本地仓库还是在远程仓库提交代码,都能再次执行同步操作而不会产生分叉或冲突。

实际操作中难免会产生无法同步的情况,这时就需要在本地解决冲突,情形会稍微复杂一点。首先我们通过git pull拉取远程仓库里的提交项到本地仓库并合并到当前分支,即将origin/master中的提交项抓取到本地仓库并合并到本地master分支。在VS Code中版本库有一个菜单命令——拉取,可以完成git pull的功能,如图2-10所示。

图2-10 VS Code中版本库的拉取菜单命令

拉取过程中有可能产生冲突,无法完成合并,这时需要对产生冲突的代码进行修改并提交到本地仓库,即利用git add.和git commit -m等本地版本库的命令。

这时本地仓库的提交项是领先于远程仓库的,只需要通过git push将本地仓库中的提交项推送到远程仓库,即可完成本地仓库和远程仓库的同步。在VS Code中版本库有一个菜单命令——推送,可以完成git push的功能,如图2-11所示。

图2-11 VS Code中版本库的推送菜单命令

注意:推送过程中一般需要用户名和密码验证身份。

至此,在本地版本库基本命令的基础上,只需要用推送和拉取就可完成本地仓库和远程仓库的同步。但是其中涉及了抓取、合并,以及分支、提交项的概念,要进一步深入学习Git、灵活使用Git,就需要理解Git背后的设计理念,乃至Git仓库的存储方式。

使用场景二所述的单人项目工作,其在时间线上是串行的,如图2-12所示分叉之前的部分。只要及时将本地与远程同步就不会出现分叉的情况。

图2-12 Git中的分叉现象示意图

在实际工作中往往是多人团队开发项目,多人团队的工作时间线常常是“并行”的,团队中的每个人都向远程版本库推送,难免会发生如图2-12所示的分叉现象,这种情况该如何处理呢?只有理解了Git背后的设计理念,才能比较准确地把握分叉合并的处理方法。

首先要清楚Git版本管理中提交的概念。通过按行对比(line diff)将有差异的部分作为增量补丁,使用git add添加到暂存区里的每一个文件都会由按行对比得到它们的增量补丁,而使用git commit将暂存区里的所有文件的增量补丁合并起来存入仓库,这就是一次提交。

通常在提交时,会生成一个SHA-1 Hash值作为commit ID。每个commit ID中有40个十六进制数字(如3d80c612175ce9126cd348446d20d96874e2eba6),就是该次提交在Git仓库中存储的内容和头信息的校验和。Git使用SHA-1并非为了保证安全性,而是为了保证数据的完整性,即可以保证,在很多年后重新校验某次提交时,一定是它多年前的状态,完全一模一样、完全值得信任。

按时间线依次排列的一组提交记录形成一个分支,比如默认分支master,也可以根据某种需要创建分支。

tag是某个提交的标签,比如发布1.0版本时的那次提交被专门打了个标签v1.0。标签就是别名,便于记忆和使用。

我们简要总结一下以上几个关键概念。

按行对比是制作增量补丁的方法,即通过按行对比将有差异的部分制作成增量补丁。

提交是存储到仓库里的一个版本,是整个项目的一个或多个文件的增量补丁合并起来形成的项目的增量补丁,是一次提交记录。每次提交都生成一个唯一的 commit ID。

分支是按时间线依次排列的一组提交记录,理论上可以通过当前分支上最初的提交依次打补丁直到HEAD得到当前工作区里的源代码。

标签是某次提交的commit ID的别名。

了解以上几个概念再来理解合并的概念就有了基础。合并操作常指将远程分支合并到本地master分支,或者某个本地分支合并到master分支。以图2-13为例,项目在A版本处开始分叉,形成了两个分支,分别提交了B、D、F和C、E、G,这时希望将这两个分支合并,只要将F与A的有差异的部分放入工作区,此时C、E、G已经在工作区了,如果有冲突,解决冲突后就可以提交一个版本H,即完成了两个分支的合并。

图2-13 将GitHub远程分支合并到本地分支的示意图

简要总结一下,合并操作可以用一个公式来表示:H = A + (F − A) + (G − A),即F版本与A版本的差异,以及G版本与A版本的差异,与A合并起来,如果有冲突,解决冲突,形成一个新的版本H。

具体技术实现上,因为每一个版本都具有上一个版本的“增量补丁”,只要将要合并的分支里B、D、F的增量补丁,合并到当前工作区的G版本里,解决冲突后即可提交为H版本。

有了前面的基础知识和技能之后,我们可以了解更复杂的团队项目合作的工作流程。如果多人同时向远程分支频繁提交代码,一来可能会有诸多冲突的情况发生;二来整个git log提交记录中多个开发者或多个功能模块、代码模块的提交是交错排列在同一条时间线上的,不利于回顾查看和回退代码,并且会让跟踪代码的成长轨迹变得异常困难。

我们需要考虑以新的方式来独立维护不同开发者或者不同功能模块、代码模块的代码,让一段连续的工作在提交日志的时间线上呈现为一条独立的分支线段,只在关键节点处进行分支合并。

基于以上想法,建议团队项目的每一个开发者采用的工作流程大致如下。

(1)克隆或同步最新的代码到本地存储库。

(2)为自己的工作创建一个分支,该分支应该只负责单一功能模块或代码模块的版本控制。

(3)在该分支上完成某单一功能模块或代码模块的开发工作。

(4)最后,将该分支合并到主分支。

注意:默认的合并方式为“快进式合并”(fast-farward merge),会将分支里的提交合并到主分支里,并列在一条时间线上,与我们期望的呈现为一条独立的分支线段不符,因此合并时需要使用--no-ff参数关闭“快进式合并”。

接下来,让我们了解分支合并的具体方法。

在VS Code中创建分支可使用如图2-14所示的创建分支菜单命令。

也可使用如下Git命令创建分支。

git checkout -b mybranch

图2-14 VS Code中的创建分支菜单命令

实际上不管是使用菜单命令方式还是使用Git命令方式创建分支,都是将当前分支分叉出一个分支(以上命令中分支名称为mybranch),并签出到工作区。这时使用git branch查看分支列表,如下所示,mybranch前面有一个*代表当前工作区处于mybranch分支。

$ git branch
  master
* mybranch
$ git checkout master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
$ git branch
* master
  mybranch

这时要将当前工作区切换到master分支,如上所示使用git checkout master命令即可。在VS Code中可以使用如图2-15所示的“签出到...”菜单命令,然后选择要签出的master分支即可完成分支切换。

图2-15 VS Code中的“签出到...”菜单命令

假如要将mybranch分支合并到master分支,那么首先确保当前工作区处于master分支。可以使用git checkout master命令切换到master分支,也可以使用git branch查看确认,或者在VS Code中源代码管理的消息输入文本框里也能看到。然后可以使用如图2-16所示的“合并分支...”菜单命令选择mybranch分支。

这样就可以将mybranch分支合并到 master分支。如果创建mybranch分支之后master分支更新过,合并可能会因为有冲突而失败,这时mybranch分支的代码已经合并到当前工作区,只要在当前工作区里先解决冲突,然后提交到仓库(使用git add和git commit -m)即可完成合并。

图2-16 VS Code中“合并分支...”菜单命令

可以使用上述“合并分支...”菜单命令合并mybranch分支到当前的master分支,也可以使用以下Git命令。

git merge mybranch

不管是Git命令还是“合并分支...”菜单命令,都使用默认的合并方式,即“快进式合并”。“快进式合并”的合并前后示意图大致如图2-17所示,也就是mybranch分支与master分支会合并到一条时间线中。

图2-17 “快进式合并”的合并前后示意图

如果要保留mybranch分支为一段独立的分支线段,则需要使用--no-ff参数关闭“快进式合并”,Git命令如下。

git merge --no-ff mybranch

使用--no-ff参数后,会进行正常合并,在master分支上生成一个新节点。关闭“快进式合并”的合并示意图如图2-18所示。

图2-18 关闭“快进式合并”的合并示意图

为了保证版本演进路径清晰,我们可采用这种关闭“快进式合并”的合并方法。不过这种方法在VS Code中没有对应的菜单命令,在VS Code中可以通过 Ctrl+`快捷键调出集成终端,使用Git命令来使用这种合并方法。

我们建议团队的每一个开发者都采用的基本工作流程有以下四大步,此处以Git命令方式为例,其中每一步对应的VS Code菜单命令一般都可以在前文中查找到。

(1)克隆或同步最新的代码到本地存储库。

git clone https://DOMAIN_NAME/YOUR_NAME/REPO_NAME.git
git pull

(2)为自己的工作创建一个分支,该分支应该只负责单一功能模块或代码模块的版本控制。

git checkout -b mybranch
git branch

(3)在该分支上完成某单一功能模块或代码模块的开发工作。多次执行以下命令。

git add FILES
git commit -m "commit log"

(4)最后,先切换回master分支,将最新的远程origin/master分支同步到本地存储库,再合并mybranch分支到master分支,推送到远程origin/master分支之后即完成了一项开发工作。

git checkout master
git pull
git merge --no-ff mybranch
git push

这样在 GitHub 上的分支网络图中,该工作将有一段明确的分叉合并路径。如果整个团队的每一项工作都参照这个工作流程进行,那么最终 GitHub 上的分支网络图中就会留下清晰的项目演进成长路径,如图2-19所示,有两次合并到master分支。

图2-19 GitHub上的分支网络图

一般我们在软件开发的流程中,有一个朴素的版本管理“哲学”:开发者的提交要尽量干净、简单。开发者要把自己修改的代码根据功能拆分成一个个相对独立的提交,一个提交对应一个功能点,而且要在对应的commit log message里描述清楚。在合并和推送之前检查修改提交记录时常需要进行此操作。

场景四实际就是在场景三团队项目工作流程中增加一步“Git Rebase”,即在mybranch分支上完成工作之后,为了让我们更容易回顾、参考log记录,用git rebase命令重新整理提交记录。

注意:不要通过git rebase对任何已经提交到远程仓库中的提交记录进行修改。

git rebase命令的格式大致如下。

git rebase -i  [startpoint]  [endpoint]

其中-i的意思是--interactive,即弹出交互式的界面让用户编辑完成合并操作,[startpoint] [endpoint]则指定了一个编辑区间,如果不指定[endpoint],则该区间的终点默认是当前分支的HEAD。

一般只指定[startpoint],即从某一个提交节点开始,可以使用HEAD^^、HEAD~100、commit ID或者commit ID的头几个字符来指定,比如下面的命令指定重新整理HEAD之前的3个提交节点。

$ git rebase -i HEAD^^^

这时打开命令行文本编辑器大致有如下信息。

pick c5fe513 A
pick ec777a8 B
pick 52c5ac5 C

# Rebase 902d019..52c5ac5 onto 902d019 (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .      create a merge commit using the original merge commit's
# .      message (or the oneline, if no original merge commit was
# .      specified). Use -c <commit> to reword the commit message.
#
# 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

该文本编辑器的用法与Vim编辑器的大致相同,按i键进入插入编辑模式,可以删除某个版本,也可以修改提交日志消息;按Esc键退出编辑模式回到一般命令模式(normal mode),这时按:键进入底线命令模式,输入:wq按Enter键可保存退出,输入:q按Enter键可退出,输入:q!按Enter键可强制退出。

不管进行了怎样的编辑操作,退出文本编辑器后,如果想撤销git rebase命令的操作,可以执行以下命令。

git rebase --abort

如果我们删除了B版本,即删除了“pick ec777a8 B”一行,然后输入:wq按Enter键保存退出,可以看到以下提示。

$ git rebase -i HEAD^^^
Auto-merging git2.md
CONFLICT (content): Merge conflict in git2.md
error: could not apply 52c5ac5... C

Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".

Could not apply 52c5ac5...

这时用VS Code打开冲突文件,冲突提示如图2-20所示。

图2-20 VS Code中的冲突提示

可以根据提示选择保留哪个更改,也可以直接编辑文件去掉提示信息。

解决冲突后需要将修改后的文件存入暂存区,最后执行以下命令完成整理。

git add .
git rebase --continue

删除的B版本的内容很可能会合并到C版本,这时往往需要重新修改C版本的提交日志消息,因此在完成操作之前需进入文本编辑器修改C版本的提交日志。按i键进入插入编辑模式,可以修改C版本的提交日志消息;按Esc键退出编辑模式回到一般命令模式,这时按:键进入底线命令模式,输入:wq按Enter键保存退出。保存退出后即完成了整理操作。

$ git rebase --continue
[detached HEAD adcb434] C
 1 file changed, 6 insertions(+), 1 deletion(-)
Successfully rebased and updated refs/heads/mybranch.

这时查看提交日志可以发现B版本已经不存在了。

$ git log
commit adcb434396ca664b11f19ed518f7901a27c81e1f (HEAD -> mybranch)
Author: mengning <mengning@ustC.edu.cn>
Date:   Sun Sep 20 11:19:57 2020 +0800

    C

commit c5fe51360f8bc010ae8de3cabe0f550240ae78f8
Author: mengning <mengning@ustC.edu.cn>
Date:   Sun Sep 20 11:19:07 2020 +0800

最后,和场景三的第4步一样,先切换回master分支,将最新远程origin/master分支同步到本地存储库,再合并mybranch分支到master分支,推送到远程origin/master分支之后即完成了一项开发工作。

Git Rebase的操作较为复杂,这里给出一道练习题如下。

在GitHub或Gitee上新建一个版本库,并实现如图2-21所示的commit分支网络示意图,要求A和B在本地存在过,但并不出现在远程网络图中。

图2-21 commit分支网络示意图

前面我们讨论的场景三和场景四都是在合作紧密的开发团队中使用的,这样的开发团队具有良好的信任关系,具有共同遵守的、规范的项目开发流程。但是开源社区的开发活动往往是松散的,团队成员的技术水平往往参差不齐,开发流程千差万别。这时如果采用场景三和场景四中推荐的参考工作流程,项目仓库的网络图就会“一团糟”。

为了解决开源社区松散团队的协作问题,GitHub提供了Fork+ Pull request的协作开发工作流程。当你想更正别人仓库里的bug或者向别人的仓库贡献代码时可以执行此流程。

(1)先“Fork”(分叉)别人的仓库,相当于复制一份。

(2)做一些漏洞修复或其他的代码贡献。

(3)发起Pull request给原仓库。

(4)原仓库的所有者审核Pull request,如果没有问题,就会合并Pull request到原仓库。

接下来按步骤简要看一下整个Fork + Pull request的过程。

(1)在某个项目页面的右上角单击Fork按钮,如图2-22所示。

图2-22 单击页面右上角的Fork按钮

系统会以该项目仓库为蓝本为你新建一个版本库,然后直接进入新建的版本库,如图2-23所示。

图2-23 进入Fork出来的项目页面

注意新建的版本库页面中有Pull request按钮,如图2-23所示。

(2)可以参考前面场景一、场景二、场景三和场景四的做法,在新建的版本库中独立工作,最终将漏洞修复或其他的代码贡献同步到远程新建的版本库中。

(3)创建Pull request。即在如图2-23所示的Fork的版本库页面中找到Pull request按钮并单击,进入Comparing changes页面,如图2-24所示。

图2-24 进入Comparing changes页面

在Comparing changes页面可以看到新建的版本库与原仓库之间的所有变更信息,单击页面上绿色的Create pull request按钮页面即跳转到原仓库。此时可审核变更信息创建一个Pull request,如图2-25所示。

图2-25 创建一个Pull request

(4)处理Pull request。原仓库的所有者审核Pull request,在图2-26所示的页面可以看到所有的代码变更信息,如果没有问题,就会合并Pull request到原仓库。

图2-26 处理Pull request

学习Fork + Pull request的具体操作,可以两人一组互相“Fork”对方的仓库,然后互相提交Pull request等进行演练。

至此,我们按由简单到复杂、从实际操作到其背后的基本原理的方式进行了讲解,并通过VS Code菜单命令和Git命令行两种方式相互对照,在五大场景下给出了Git命令的参考用法。不管你是希望实际使用时快速参考,还是希望系统地掌握Git版本管理的技能,相信本章都能为你提供必要帮助。

一、填空题

1.在Git命令行下查看当前工作区的状态的命令是(    )。

2.在Git命令行下将更改的文件加入暂存区的命令是(    )。

3.在Git命令行下取消添加到暂存区里的所有文件,即将之从暂存区中删除,所需的命令是(    )。

4.在Git命令行下把暂存区里的文件提交到仓库的命令是(    )。

5.在Git命令行下将仓库回退到一个指定版本的命令是(    )。

6.在Git命令行下从远程仓库中克隆一个仓库到本地的命令是(    )。

7.在GitHub中远程仓库的默认分支名称是(    )。

8.在GitHub中按时间线依次排列的一组提交记录称为(    )。

二、简述题

1.比较常见的版本控制的方式有两种:一种是独立文件或说整体备份的方式;另一种是增量补丁包的方式。Git采用了增量补丁包的方式,请问Git是如何生成增量补丁包的?

2.在Git中默认的分支合并方式为“快进式合并”,但是我们常常使用--no-ff参数关闭“快进式合并”,请你简述两者有何不同。

三、实验

1.参考第2.2.6小节实际演练本地版本库的用法。

2.参考第2.3.3小节实际演练GitHub远程版本库的基本用法。

3.参考第2.5.3小节实际演练团队项目工作流程。


读者服务:

微信扫码关注【异步社区】微信公众号,回复“e57850”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。

相关图书

程序员的README
程序员的README
有限元基础与COMSOL案例分析
有限元基础与COMSOL案例分析
现代控制系统(第14版)
现代控制系统(第14版)
现代软件工程:如何高效构建软件
现代软件工程:如何高效构建软件
GitLab CI/CD 从入门到实战
GitLab CI/CD 从入门到实战
科学知识图谱:工具、方法与应用
科学知识图谱:工具、方法与应用

相关文章

相关课程