什么是git
git是写出Linux内核的大神Linus,捣鼓出来的代码版本控制工具。虽然其动机是服务于代码,但其实对非开发者的普通用户,也是很有用的工具。
什么叫做版本控制?
我们可以从比较熟悉的游戏存档谈起。在玩RPG游戏的时候,一个非常重要的基础功能便是能够存储当前的游戏状态。因为你几乎不可能在完全不停止的情况下一次性通关。停止你游戏story展开的因素有很多,例如:
你玩游戏的进度很慢,到了睡觉时间,也就不得不停止,下次再战。
你悲剧地遭遇了电脑的突然中断,不得不重新进入游戏。
一切都很顺利,但你却不幸被游戏中的大boss干掉了。
有了游戏存档功能,一方面你可以再一次从你中断的地方继续游戏,另一方面,如果你的游戏有多条分支线story,你可以从分支的地方继续开始,选择另一条路线体验不同的经历。
那么,git干的其实是同样的事情。git其实就是为特定的文件目录下的所有文件,提供一个存档功能(这个特定的文件目录,就是运行了git init命令的文件目录)。在git的视角之下,每一个时间点的文件目录状态,都可以作为一个存储节点(同游戏存档一样)。而像这样的可以存储一个文件的各个时间节点的文件状态的功能,就叫做版本控制。如果从使用Facebook的角度来讲,git提供的功能就是为这个文件目录提供一个时间线,将这个文件目录下发生的所有事情全部记载下来,起到一个类似于游戏存档的功能。
例如,假设我们在运行了git init命令的文件目录MyDirectory下有两个文件File1.txt和File2.txt。File1.txt和File2.txt这两个文件各包含两行文字,如下图。
图1.0
作为初级程序员的教程,这里需要多说几句,解释一下为什么这个例子可以不失一般性,作为其它所有文件的代表。因为所有的文件在其底层的存储方式都是字节序列。从计算机的角度来看,所有的文件就是一行行的文字而已。所以,如果理解了git如何去处理一个人类可以识别的文本文件。那么,对于任何文件来讲,其原理也是相同的,不过是处理一行行计算机认识的文字,即一堆0、1序列文字流。
如图1.0,这里便可以作为MyDirectory的一个当前时间点的文件状态。当MyDirectory下的任何一个文件有所改动,
图1.1那么此刻的状态,就好比是游戏当中的主角在地图上移动了一个位置,可以作为另一个时间点的文件状态(图1.1)来做存储。
可以发现,到目前为止的讨论,都和GitHub无关、和remote端的服务器没有关系。因为git的使用可以仅仅限于本地,和网路连接无关。因而,对于普通用户来说,完全可以使用git来为你的办公文件、私人文件做存档。存档与备份的不同在于,后者只能够存储一个文件状态,而前者却可以存储多个文件状态,也即是将整个文件的时间线全部存储下来。有了它,你便能够去了解这个文件的整个衍化、生长历史,从而对它有更加深层次的认识和理解。
关于git的优质教程我会推荐两个,一个是Udacity的课程How to Use GitandGitHub,另一个是git的官方文档教程Pro Git,里面提供了可以下载的多种格式的电子版。
常用的git命令
为文件目录提供版本控制功能
最开始的命令当然是git init,用于为一个文件目录提供版本控制的git功能。例如,想要为文件目录/Users/Terence/Documents提供版本控制的功能,那么只需要两步:
将命令行切换到目标文件目录下:cd/Users/Terence/Documents 。
运行git init。
此时,/Users/Terence/Documents目录便具备了版本控制功能(其实质是在这个目录下创建了一个.git隐藏文件夹)。
版本控制下的文件修改和提交
接下来要讨论具备了git监控的文件目录下,一个文件会呈现的几个状态。在git的体系下,一个文件可以具备四种状态:
Untracked
Unmodified
Modified
Staged
不同状态的同一个文件可以同时彼此独立存在。下面根据Pro Git2.2上的一个文件的周期图来做讲解。
Git File Lifecycle
下面以从上到下的顺序讲解这几个箭头相关联的状态切换。
首先是文件的untracked状态,指的是还未被纳入git存档体系的文件。虽然这个文件目录通过git init具备了存档的功能,但还未指定哪些文件的时间线可以被存档。于是这些未被纳入存档的文件的状态便是untracked。 通过命令git add便可以将它纳入待存档(staged)的状态。(这里可以跳跃一个步骤,提前讲解它最后的状态切换,从通过命令git commit 让待存档(staged)状态的文件进入到git库。此时就变成了git库的unmodified状态。)
再来是文件的unmodified(未修改)状态。一般在git界面中将文件名显示为蓝色。一个文件要成为unmodified状态,首要的前提是它已经被纳入了存档体系、并且它已经被存档。也就是说,它一定是从待存档(staged)的状态,通过执行命令git commit(提交到git库,也就是把当前的这个状态存储好)后所处的状态。
然后是文件的modified状态,也就是对一个文件做了修改后的状态。一般在git的界面将文件名显示为红色。例如,假设图1.0都所有文件都是unmodified状态,我们对File2.txt做了修改成为图1.1。此时,File2.txt就是modified状态。
最后是文件的staged状态,也就是待存储状态。一般在git界面中将文件名显示为绿色。这个staged状态是非常微妙的一个状态,它能够与modified状态同时存在。
举个例子:现在我们假设在图1.1的基础上运行了命令git add File2.txt,那么此时的文件状态就变成了:
图1.2也就是Yoga那一行变成了黑色,也就是unmodified状态。如果我们对它再做修改,添加文本L1:
图1.3也就是说,此时添加了L1的那一行全部变成了红色。你或许会提出一个疑问,我明明只添加了字符L1,为什么整行都受到了影响?这是因为,git是以行为单位来考察文件是否有变化。每一行做相应对比,只要有不同,就把整个一行当作有变动的内容处理。
再运行git add File2.txt,得到:
图1.4可以看到,已处于暂存状态staged的做了改动的那一行变成了绿色。
微妙的地方来了,如果此时我们不提交我们这个暂存状态staged,而是继续修改File2.txt,将刚加入的字符串L1删去,则我们会得到这个文件的两个状态:
一个处于staged的图1.4的状态;
另一个则是处于modified的状态图1.5图1.5注意,这里图1.5和图1.1虽然是一样的,但表针的意思完全不同。图1.1是从没有Yoga的状态,添加了Yoga字符串后形成的。而图1.5是从状态Yoga. L1删除了字符串L1后形成的。
此刻,如果我们再运行命令git commit File2.txt,我们提交到git仓库的代码将会成为具备Yoga. L1的图1.6图1.6同时,File2.txt依旧会保持modified的状态图1.5。
希望这个例子能够帮你看出这里的微妙差别。
总结起来,我们这里涉及到的命令有:
git init让文件目录具备git存储功能。
git add将未被跟踪的文件(untracked),或者modified的文件,添加进暂存状态(staged)。
git commit将暂存状态的文件(staged)添加到git仓库,成为unmodified状态。
什么是GitHub
GitHub是一家公司,它提供的服务是为用户提供git的远程(云端)的git仓储。其想法就是,你本地有一份文件的版本控制仓库,里面保存了所有文件的时间线。但是,如果你更换了一台电脑,想要再次重现以前那台机器的版本控制仓库,就会很麻烦。于是,一个解决方案便是,为何不将我的这个仓库系统放到云端。既可以随时将本地的修改放到云端,纳入正式的代码库,也可以在本地保留一份自己的代码仓库,做自己的修改。
更具吸引力的是,一旦你的代码放在了云端,你就可以同其他人合作,在世界的不同地方推进同一个项目。大家保留各自对代码的修改,让云端的代码库成为大家认可的main stream正式存储仓库。大家可以在得到彼此的认同后(也就是后面要讲到的对Pull Request (PR)的处理),将自己的这份代码修改签入到云端的仓储库,也就是GitHub了。
所以,从这个角度讲,GitHub是一个提供了云端的代码版本控制的git仓库平台。从这个意义上讲,GitHub确实是技术人员的社交平台,社交平台中的发朋友圈、发Facebook状态消息等价于不断地展现自己对代码的修改更新。大家是在通过自己的项目状态,来展现各自的工作进展以及最新的项目进展。仔细研究这些版本控制的历史,就能够还原出这个项目、这个工程师的整个成长历史。
在我看来,使用GitHub的目的无非是两个:
学习他人的代码,并希望重现这个项目的结果,或者作出自己的贡献;
或者是将自己的工作迁移到GitHub上,使得自己的工作能够得到一个云端的备份,或者能够更方便与在不同地理位置的人合作。
这里,我想展开谈一下这两点的细节和初学者的疑问点。它们本身并不需要太高深的技术来解决。只是,它们会成为初学者的一道屏障。澄清这些模糊的概念,可以极大地提高初学者对GitHub的掌握效率。
使用GitHub学习他人的代码
写代码如同写文章,要做出好的工作,需要有大量的高质量阅读作前提。GitHub上有大量的优秀项目供人阅读。Programming的各种精髓和技巧,便藏在这一个个优秀的代码细节中。
通过一般的参考资料,很容易将这些优质的代码clone到本地,或者更加简单粗暴直接download整个项目的压缩包到你的本地文件目录。
然后,问题来了:源代码也到手了,可是,怎么把这堆代码变成所需要的目标软件产品呢?编译、运行?可是,从哪里去编译、运行呢?
这个问题的提出,其实涉及到学校的小项目和真正的工业界的大项目的实践区别。或许在学校学习C、Java、Python的时候,只需要在命令行或者IDE 界面点击个运行就可以了。
可是,在真实的复杂项目中,编译代码会涉及到更多的细节和步骤。在编译之前需要更多的环境配置、资源准备以及脚本运行。举一些例子,当你的项目足够复杂后:
需要编译的的源代码本身或者部分,是需要通过某个文本文档去生成的。而如何从这个文本文档中提取出需要的信息再生成相应的代码,又是由一个脚本所控制的。
在编译你的源代码前,需要你提供一些第三方的代码库或者代码包。比如Java的第三方jar包。而这些第三方的资源文件,是需要你提前准备呢?还是可以通过一个脚本自动获得呢?
上面提到的工具脚本,或许是由某个/某几个特定语言编写的。你当前环境中,是否提供了支持这些语言的环境呢?
上面提到的这些问题和可能性,远远不是仅凭借源代码就可以推断出来的。所以,拿到这堆源代码不知所措,其实是很正常的。
那么如何解决这个问题呢?
我的回答是,看你的运气。
如果你恰好遇到的项目是由习惯不大好或者是不喜欢写文档的程序员写的,或者这个项目的贡献者本身不希望你知道编译环境,估计你也就只能看着这堆代码发呆了。
如果你运气不坏,那么,真正的严肃项目,会在它自己的文档(通常是这个项目根目录的README文件)详细说明如何部署编译环境:它包括需要提前准备哪些编译工具、哪些第三方的代码库、如何调用编译的脚本等等。
所以,当你在GitHub上看到一个项目时,先别急着做download的动作。先仔细阅读这个项目的文档,看看它是否足够优秀。通常,文档的质量和项目的质量成正相关。用业界俗语来解释原因就是:写文档就是写代码,文档写不好,代码通常也是一团糟。
如何在GitHub上工作
如果仅仅是简单的把GitHub当作自己的一个代码备份库,其实实践操作并不复杂。无非是不断地在本地修改好代码,再将自己认可的代码签入到远程端的GitHub代码库。
真正有意思的,是在GitHub上与他人合作,一起完成同一个项目。
要能够一起合作,首先需要在你的项目中添加你合作者的GitHub账号。这个只需要在GitHub的项目界面中,点击Settings —>Collaborators,在里面添加你的合作者。这样,你的合作者便获得了对这个项目的更改权限,才有资格对这个GitHub上的项目仓库提交代码。
如果你们同属于某一个GitHub上的Organization,那么这个Organization的管理员通常会设定其成员,自动拥有这个Organization下所有项目的更改权限。
下面谈谈GitHub上的工作流程。作为git的核心,它是以branch做核心驱动的。masterbranch作为主分支,一定要保证它是可以运行的和经过测试的。而你可以在其它的branch上做自己的feature开发、实验和测试,而完全不会影响代码库的主分支masterbranch。
那么,在GitHub上是同样的。当几个人组成一个团队合作一个项目时,每个成员不应该轻易地直接提交代码到master branch,即便是你有这样的权限。作为良好的实践规范,你永远应该在自己的私人分支上做代码修改和实验,然后将这个分支push到GitHub,在GitHub上对这个分支执行Pull Request来让团队的其他人员review你的代码。当reviewer认可了你的代码后,你才能把你的修改分支merge到master。也就是说,任何人对master的代码提交之前,必须有其他人的code review这一步;没有其他人的coder review,你不可以向master分支提交代码即便是你有这个权限。
而在这个Pull Request的过程中,审核者和被审核者可以对代码的每一行做深入的讨论。每一行代码都可以插入相应的评论,供大家去做深入研究。这些讨论,是整个代码设计的精髓,也是代码功底提升的真正关键。再次回到那句老话,写文档即写代码,它传达的精神便是:你的思想和看法才是支撑整个代码的核心。而某种具体编程语言的书写只是这种思想的一种表达。你只有先整理好了思路、要解决的问题的脉络,你才可以写出清晰的代码。
甚至,在所谓的合作而其实是师傅带徒弟的过程中,这些code review的价值会远远高于push code。因为push code只是一个结果,而code review的comments讨论,才是帮助你指正问题、更改坏习惯的核心所在。
而往往初学者又不太明白这一点,误以为这些comments的讨论是配菜,对这些comments的讨论极其不耐烦,一心只想把code签入到代码库。所以,作为mentor他其实是有责任向自己的apprentice指出这个差异与优先级的。
让我们再强调一次,无论作为学徒还是作为平等的合作者,那些review过程中的comments交流才是整个代码的核心,它们是整个项目的指导思想与精神纲领。只有在review的过程中将问题一步步弄清楚了,你才能够更加容易和自信地修改代码,才能够保证代码库的质量水准。任何轻视code review或者review中的comments的行为,都是一种天真的自我欺骗。你需要拿出同写代码一样的严肃认真,来对待整个code review,来对待review过程中的comments交流。