1.1 背景
现如今,难以想象有创意的人会在没有备份策略的情况下启动一个项目。数据是短暂的,且容易丢失—例如,通过一次错误的代码变更或者一次灾难性的磁盘崩溃。所以说,在整个工作中持续性地备份和存档是非常明智的。
对于文本和代码项目,备份策略通常包括版本控制,或者叫“对变更进行追踪管理”。每个开发人员每天都会进行若干个变更。这些持续增长的变更,加在一起可以构成一个版本库,用于项目描述,团队沟通和产品管理。版本控制具有举足轻重的作用,只要定制好工作流和项目目标,版本控制是最高效的组织管理方式。
一个可以管理和追踪软件代码或其他类似内容的不同版本的工具,通常称为:版本控制系统(VCS),或者源代码管理器(SCM),或者修订控制系统(RCS),或者其他各种和“修订”、“代码”、“内容”、“版本”、“控制”、“管理”和“系统”等相关的叫法。尽管各个工具的作者和用户常常争论得喋喋不休,但是其实每个工具都出于同样的目的:开发以及维护开发出来的代码、方便读取代码的历史版本、记录所有的修改。在本书中,“版本控制系统”(VCS)一词就是泛指一切这样的工具。
本书主要介绍Git这款功能强大、灵活而且低开销的VCS,它可以让协同开发成为一种乐趣。Git由Linus Torvalds发明,起初是为了方便管理Linux{![Linux是Linus Torvalds在美国和其他国家的注册商标。—原注]}内核的开发工作。如今,Git已经在大量的项目中得到了非常成功的应用。
1.2 Git的诞生
通常来说,当工具跟不上项目需求时,开发人员就会开发一个新的工具。实际上,在软件领域里,创造新工具经常看似简单和诱人。然而,鉴于市面上已经有了相当多的VCS,决定再创造一个却应该是要深思熟虑的。不过,如果有着充分的需求、理性的洞察以及良好的动机,则完全可以创造一个新的VCS。
Git就是这样一个VCS。它被它的创造者(Linus,一个脾气急躁又经常爆出冷幽默的人)称作“从地狱来的信息管理工具”。尽管Linux社区内部政治性的争论已经淹没了关于Git诞生的情形和时机的记忆,但是毋庸置疑,这个从烈火中诞生的VCS着实设计优良,能够胜任世界范围内大规模的软件开发工程。
在Git诞生之前,Linux内核开发过程中使用BitKeeper来作为VCS。BitKeeper提供当时的一些开源VCS(如RCS、CVS)所不能提供的高级操作。然而,在2005年春天,当BitKeeper的所有方对他们的免费版BitKeeper加入了额外的限制时,Linux社区意识到,使用BitKeeper不再是一个长期可行的解决方案。
Linus本人开始寻找替代品。这次,他回避使用商业解决方案,在自由软件包中寻找。然而,他却发现,在现有的自由软件解决方案中,那些在选择BitKeeper之前曾经发现的,导致他放弃自由软件解决方案的一些限制和缺陷如今依然存在。那么,这些已经存在的VCS到底存在什么缺陷?Linus没能在现有VCS中找到的有关特性到底是哪些?让我们来看看。
有助于分布式开发
分布式开发有很多方面,Linus希望有一个新的VCS能够尽可能覆盖这些方面。它必须允许并行开发,各人可以在自己的版本库中独立且同时地开发,而不需要与一个中心版本库时刻同步(因为这样会造成开发瓶颈)。它必须允许许多开发人员在不同的地方,甚至是离线的情况下,无障碍地开发,
能够胜任上千开发人员的规模
仅仅支持分布式开发模型还是不够的。Linus深知,每个Linux版本都凝聚了数以千计开发人员的心血。所以新的VCS必须能够很好地支持非常多的开发人员,无论这些开发人员工作在整个项目相同还是不同的部分。当然,新的VCS也必须能够可靠地将这些工作整合起来。
性能优异
Linus决心要确保新的VCS能够快速并且高效地执行。为了支持Linux内核开发中大量的更新操作,他知道不管是个人的更新操作,还是网络传输操作,都需要保证执行速度。为了节约存储空间,从而节约传输时间,需要使用“压缩”和“差异比较”技术。另外,使用分布式开发模型,而非集中式模型,同样也确保了网络的不确定因素不会影响到日常开发的效率。
保持完整性和可靠性
因为Git是一个分布式版本控制系统,所以非常需要能够绝对保证数据的完整性和不会被意外修改。那如何确定,在从一个开发人员到另一个开发人员的过程中,或者从一个版本库到另一个版本库的过程中,数据没有被意外修改呢?又如何确定版本库中的实际数据就是认为的那样?
Git使用一个叫做“安全散列函数”(SHA1)的通用加密散列函数,来命名和识别数据库中的对象。虽然也许理论上不是绝对的,但是在实践中,已经证实这是足够可靠的方式。
强化责任
版本控制系统的一个关键方面,就包括能够定位谁改动了文件,甚至改动的原因。Git对每一个有文件改动的提交(Git把一个历史版本叫做一个“提交”)强制使用“改动日志”。“改动日志”中存储的信息由开发人员、项目需求、管理策略等决定。Git确保被VCS管理的文件不会被莫名地修改,因为Git可以对所有的改动进行责任追踪。
不可变性
Git版本库中存储的数据对象均为不可变的。这意味着,一旦创建数据对象并把它们存放到数据库中,它们便不可修改。当然,它们可以重新创建,但是重新创建只是产生新的数据对象,原始数据对象并不会被替换。Git数据库的设计同时也意味着存储在版本数据库中的整个历史也是不可变的。使用不可变的对象有诸多优势,包括快速比较相同性。
原子事务
有了原子事务,可以让一系列不同但是相关的操作要么全部执行要么一个都不执行。这个特性可以确保在进行更新或者提交操作时,版本数据库不会陷入部分改变或者破损的状态。Git通过记录完整、离散的版本库状态来实现原子事务。而这些版本库状态都无法再分解成更小的独立状态。
支持并且鼓励基于分支的开发
几乎所有的VCS都支持在同一个项目中存在多个“支线”。例如,代码变更的一条支线叫做“开发”,而同时又存在另一条支线叫做“测试”。每个VCS同样可以将一条支线分叉为多条支线,在以后再将差异化后的支线合并。就像大多数VCS一样,Git把这样的支线叫做“分支”,并且给每个分支都命名。
伴随着分支的就是合并。Linus 不仅希望通过简单的分支功能来促进丰富的开发分支,还希望这些分支的合并可以变得简单容易。因为通常来说,分支的合并是各VCS使用中最为困难和痛苦的操作,所以,能够提供一个简单、清晰、快速的合并功能,是非常必要的。
完整的版本库
为了让各个开发人员不需要查询中心服务器就可以得到历史修订信息,每个人的版本库中都有一份关于每个文件的完整历史修订信息就非常重要。
一个清晰的内部设计
即使最终用户也许并不关心是否有一个清晰的内部设计,对于Linus以及其他Git开发人员来说,这确实非常重要。Git的对象模型拥有者简单的结构,并且能够保存原始数据最基本的部分和目录结构,能够记录变更内容等。再将这个对象模型和全局唯一标识符技术相结合,便可以得到一个用于分布式开发环境中的清晰数据对象。
免费自由(Be free, as in freedom)
—Nuff曾说过。
有了创造一个新VCS的清晰理由后,许多天才软件工程师一起创作出了Git。需求是创新之母!
1.3 先例
VCS的完整历史已经超出了本书的讨论范围。然而,有一些具有里程碑、革新意义的系统值得一提。这些系统对Git的开发或者有重要的铺垫意义,或者有引导意义。(本节为可选章节,希望能够记录那些新特性出现的时间,以及在自由软件社区变得流行的时间。)
源代码控制系统(Source Code Control System, SCCS)是UNIX{![UNIX是Open Group在美国和其他国家的注册商标。—原注]}上最初的几个系统之一,由M. J. Rochkind于20世纪70年代早期开发。[“The Source Code Control System,” IEEE Transactions on Software Engineering 1(4) (1975): 364-370.]这是有证可查的可以运行在UNIX系统上的最早的VCS。
SCCS提供的数据存储中心称为“版本库”(repository),而这个基本概念一直沿用至今。SCCS同样提供了一个简单的锁模型来保证开发过程有序。如果一个开发人员需要运行或者测试一个程序,他需要将该程序解锁并检出。然而,如果他想修改某个文件,他则需要锁定并检出(通过UNIX文件系统执行的转换)。当编辑完成以后,他又可以将文件检入到版本库中并解锁它。
修订控制系统(Revision Control System,RCS)由Walter F. Tichy于20世纪80年代早期引入[“RCS: A System for Version Control,”Software Practice and Experience 15(7) (1985): 637-654.]。RCS引入了双向差异的概念,来提高文件不同版本的存储效率。
并行版本系统(Concurrent Version System,CVS)由Dick Grune于1986年设计并最初实现。4年后又被Berliner和他的团队融入RCS模型重新实现,这次实现非常成功。CVS变得非常流行,并且成为开源社(http:www.opensource.org)区许多年的事实标准。CVS相对RCS有多项优势,包括分布式开发和版本库范围内对整个“模块”的更改集。
此外,CVS引入了一个关于“锁”的新范式。而之前的系统需要开发人员在修改某个文件之前先锁定它,一个文件同时只允许一个开发人员进行修改,所有需要修改这个文件的开发人员需要有序等候。CVS给予每个开发人员对于自己的私有版本写的权限。因此,不同开发人员的改动可以自动合并,除非两个开发人员尝试修改同一行。如果出现修改同一行的情况,那这一行将会作为“冲突”被标记出来,由开发人员手动去解决。这个关于“锁”的新规则使得多个开发人员可以并行地编写代码。
就像经常发生的那样,对CVS短处和缺点的改进,促进了新VCS的诞生:Subversion(SVN)。SVN于2001年问世,迅速风靡了开源社区。不像CVS,SVN以原子方式提交改动部分,并且更好地支持分支。
BitKeeper和Mercurial则彻底抛弃了上述所有解决方案。它们淘汰了中心版本库的概念,取而代之的,数据的存储是分布式的,每个开发人员都拥有自己可共享的版本库副本。Git则是从这种端点对端点(Peer to Peer)的模型继承而来。
最后,Mercurial和Monotone首创了用散列指纹来唯一标识文件的内容,而文件名只是个“绰号”,旨在方便用户操作,再没有别的作用。Git沿用了这个概念。从内部实现上来说,Git的文件标识符基于文件的内容,这是一个叫做“内容可寻址文件存储”(Content Addressable File Store,CAFS)的概念。这不是一个新概念。[见“The Venti Filesystem,” (Plan 9), Bell Labs, http://www.usenix.org/events/fast02/quinlan/quinlan_html/index.html.] 据Linus的说法{![私人电子邮件。—原注]}, Git直接从Monotone借用了这个概念。Mercurial也同时实现了这个概念。
1.4 时间线
有了应用场景,有了一点额外的动力,再加上对新VCS的需求迫在眉睫,Git于2005年4月诞生了。
4月7日,Git从以下提交起,正式成为自托管项目。
commit e83c5163316f89bfbde7d9ab23ca2e25604af29
Author: Linus Torvalds <torvalds@ppc970.osdl.org>
Date: Thu Apr 7 15:13:13 2005 –0700
Initial revision of "git", the information manager from hell
不久之后,Linux内核的第一个提交也诞生了。
commit 1da177e4c3f41524e886b7f1b8a0c1fc7321cac2
Author: Linus Torvalds <torvalds@ppc970.osdl.org>
Date: Sat Apr 16 15:20:36 2005 –0700
Linux-2.6.12-rc2
Initial git repository build. I'm not bothering with the full history,
even though we have it. We can create a separate "historical" git
archive of that later if we want to, and in the meantime it's about
3.2GB when imported into git - space that would just make the early
git days unnecessarily complicated, when we don't have a lot of good
infrastructure for it.
Let it rip!
这一次提交将整个Linux内核导入Git版本库中{![关于旧的Bitkeeper日志如何导入Git版本库(2.5之前)的悠久历史的起点,参见http://kerneltrap. org/node/13996。—原注]}。这次提交的统计信息如下:
17291 files changed, 6718755 insertions(+), 0 deletions(-)
是的,这次提交足足引入了670万行代码!
仅仅过了3个小时,Linux内核第一次用Git打上了补丁。Linus在2005年4月20日向Linux内核邮件列表正式宣布,用上Git了!
Linus非常清楚自己想回到Linux内核的开发中去,所以,在2005年7月25日,Linus将Git代码维护工作交接给Junio Hamano。并声称,Junio是显而易见的选择。
大概两个月以后,版本号为2.6.12的Linux内核正式发布,所用VCS正是Git。
1.5 名字有何含义
据Linus宣称,命名为Git,是因为“我是一个自私的混蛋,我照着自己命名我所有的项目,先是Linux,现在是git”{![参见http://www.infoworld.com/article/05/04/19/HNtorvaldswork_1.html。—原注]}。
倘若,Linux这个名字是Linus和Minix的某种结合。那么反用一个表示愚蠢无用之人的英语词汇也不是没可能。
那之后,也有人曾建议,使用一些其他也许更让人舒服的解释。其中最受欢迎的一个就是:全局信息追踪器(Global Information Tracker)。
本文摘自:《Git版本控制管理》
关于《Git版本控制管理》这本书
市面上绝无仅有的Git图书
全面剖析Git的用法
**同时涵盖GitHub **
《Git版本控制管理》这本书可以让读者迅速上手Git,用它来跟踪、分支、合并和管理代码变更。本书通过一系列步骤式教程,引导读者迅速掌握从Git基础知识到高级使用技巧在内的所有知识,并提供友好而严谨的建议,以帮助读者熟悉Git的许多功能。
本书在上一版的基础之上进行了全面更新,包含了操作树的技巧,全面覆盖了reflog和stash的用法,还全面介绍了GitHub仓库。一旦你掌握了Git系统的灵活性之后,你可以以近乎无限的各种方式来管理代码开发,而本书则会告诉你怎么来做。
**本书内容如下: **
学习如何在多个真实的开发场景中使用Git;
深入理解Git的常见用例、最初的任务以及基本功能;
针对集中式和分布式版本控制而使用Git系统;
学习如何管理合并、冲突、补丁和差异;
应用高级技术,比如变基、钩子和处理子模块的方法;
与SVN仓库进行交互——其中包括SVN道Git的转换;
通过GitHub来导航、使用开源项目,并对开源项目做贡献。
关于目录
第1章 介绍 1
1.1 背景 1
1.2 Git的诞生 2
1.3 先例 4
1.4 时间线 5
1.5 名字有何含义 6
第2章 安装Git 7
2.1 使用Linux上的二进制发行版 7
2.1.1 Debian/Ubuntu 7
2.1.2 其他发行版 8
2.2 获取源代码 9
2.3 构建和安装 10
2.4 在Windows上安装Git 11
2.4.1 安装Cygwin版本的Git 12
2.4.2 安装独立的Git(msysGit) 13
第3章 起步 16
3.1 Git命令行 16
3.2 Git使用快速入门 18
3.2.1 创建初始版本库 18
3.2.2 将文件添加到版本库中 19
3.2.3 配置提交作者 21
3.2.4 再次提交 21
3.2.5 查看提交 21
3.2.6 查看提交差异 23
3.2.7 版本库内文件的删除和重命名 23
3.2.8 创建版本库副本 24
3.3 配置文件 25
3.4 疑问 27
第4章 基本的Git概念 28
4.1 基本概念 28
4.1.1 版本库 28
4.1.2 Git对象类型 29
4.1.3 索引 30
4.1.4 可寻址内容名称 30
4.1.5 Git追踪内容 31
4.1.6 路径名与内容 31
4.1.7 打包文件 32
4.2 对象库图示 33
4.3 Git在工作时的概念 35
4.3.1 进入.git目录 35
4.3.2 对象、散列和blob 36
4.3.3 文件和树 37
4.3.4 对Git使用SHA1的一点说明 38
4.3.5 树层次结构 40
4.3.6 提交 40
4.3.7 标签 41
第5章 文件管理和索引 43
5.1 关于索引的一切 44
5.2 Git中的文件分类 44
5.3 使用git add 46
5.4 使用git commit的一些注意事项 48
5.4.1 使用git commit --all 48
5.4.2 编写提交日志消息 50
5.5 使用git rm 50
5.6 使用git mv 52
5.7 追踪重命名注解 54
5.8 .gitignore文件 55
5.9 Git中对象模型和文件的详细视图 56
第6章 提交 61
6.1 原子变更集 62
6.2 识别提交 62
6.2.1 绝对提交名 63
6.2.2 引用和符号引用 64
6.2.3 相对提交名 65
6.3 提交历史记录 67
6.3.1 查看旧提交 67
6.3.2 提交图 70
6.3.3 提交范围 73
6.4 查找提交 77
6.4.1 使用git bisect 78
6.4.2 使用git blame 82
6.4.3 使用Pickaxe 83
第7章 分支 84
7.1 使用分支的原因 84
7.2 分支名 85
7.3 使用分支 86
7.4 创建分支 88
7.5 列出分支名 89
7.6 查看分支 89
7.7 检出分支 91
7.7.1 检出分支的一个简单例子 91
7.7.2 有未提交的更改时进行检出 92
7.7.3 合并变更到不同分支 94
7.7.4 创建并检出新分支 95
7.7.5 分离HEAD分支 96
7.8 删除分支 97
第8章 diff 100
8.1 git diff命令的格式 101
8.2 简单的git diff例子 104
8.3 git diff和提交范围 108
8.4 路径限制的git diff 110
8.5 比较SVN和Git如何产生diff 112
第9章 合并 114
9.1 合并的例子 114
9.1.1 为合并做准备 115
9.1.2 合并两个分支 115
9.1.3 有冲突的合并 117
9.2 处理合并冲突 121
9.2.1 定位冲突的文件 122
9.2.2 检查冲突 122
9.2.3 Git是如何追踪冲突的 126
9.2.4 结束解决冲突 128
9.2.5 中止或重新启动合并 129
9.3 合并策略 130
9.3.1 退化合并 132
9.3.2 常规合并 134
9.3.3 特殊提交 135
9.3.4 应用合并策略 136
9.3.5 合并驱动程序 137
9.4 Git怎么看待合并 138
9.4.1 合并和Git的对象模型 138
9.4.2 压制合并 139
9.4.3 为什么不一个接一个地合并每个变更 140
第10章 更改提交 142
10.1 关于修改历史记录的注意事项 143
10.2 使用git reset 144
10.3 使用git cherry-pick 152
10.4 使用git revert 154
10.5 reset、revert和checkout 154
10.6 修改最新提交 155
10.7 变基提交 158
10.7.1 使用git rebase -i 160
10.7.2 变基与合并 164
第11章 储藏和引用日志 170
11.1 储藏 170
11.2 引用日志 178
第12章 远程版本库 183
12.1 版本库概念 184
12.1.1 裸版本库和开发版本库 184
12.1.2 版本库克隆 185
12.1.3 远程版本库 186
12.1.4 追踪分支 186
12.2 引用其他版本库 187
12.2.1 引用远程版本库 188
12.2.2 refspec 189
12.3 使用远程版本库的示例 191
12.3.1 创建权威版本库 192
12.3.2 制作你自己的origin远程版本库 193
12.3.3 在版本库中进行开发 195
12.3.4 推送变更 196
12.3.5 添加新开发人员 197
12.3.6 获取版本库更新 199
12.4 图解远程版本库开发周期 203
12.4.1 克隆版本库 204
12.4.2 交替的历史记录 205
12.4.3 非快进推送 205
12.4.4 获取交替历史记录 207
12.4.5 合并历史记录 208
12.4.6 合并冲突 208
12.4.7 推送合并后的历史记录 209
12.5 远程版本库配置 209
12.5.1 使用git remote 210
12.5.2 使用git config 211
12.5.3 使用手动编辑 212
12.6 使用追踪分支 212
12.6.1 创建追踪分支 212
12.6.2 领先和落后 215
12.7 添加和删除远程分支 216
12.8 裸版本库和git推送 217
第13章 版本库管理 219
13.1 谈谈服务器 219
13.2 发布版本库 220
13.2.1 带访问控制的版本库 220
13.2.2 允许匿名读取访问的版本库 221
13.2.3 允许匿名写入权限的版本库 225
13.2.4 在GitHub上发布版本库 225
13.3 有关发布版本库的建议 227
13.4 版本库结构 228
13.4.1 共享的版本库结构 228
13.4.2 分布式版本库结构 228
13.4.3 版本库结构示例 229
13.5 分布式开发指南 231
13.5.1 修改公共历史记录 231
13.5.2 分离提交和发布的步骤 232
13.5.3 没有唯一正确的历史记录 232
13.6 清楚你的位置 233
13.6.1 上下游工作流 233
13.6.2 维护者和开发人员的角色 234
13.6.3 维护者-开发人员的交互 234
13.6.4 角色的两面性 235
13.7 多版本库协作 236
13.7.1 属于你自己的工作区 236
13.7.2 从哪里开始你的版本库 237
13.7.3 转换到不同的上游版本库 238
13.7.4 使用多个上游版本库 239
13.7.5 复刻项目 241
第14章 补丁 244
14.1 为什么要使用补丁 245
14.2 生成补丁 246
14.3 邮递补丁 254
14.4 应用补丁 256
14.5 坏补丁 264
14.6 补丁与合并 264
第15章 钩子 265
15.1 安装钩子 267
15.1.1 钩子示例 267
15.1.2 创建第一个钩子 268
15.2 可用的钩子 270
15.2.1 与提交相关的钩子 270
15.2.2 与补丁相关的钩子 271
15.2.3 与推送相关的钩子 272
15.2.4 其他本地版本库的钩子 273
第16章 合并项目 274
16.1 旧解决方案:部分检出 275
16.2 显而易见的解决方案:将代码导入项目 276
16.2.1 手动复制导入子项目 277
16.2.2 通过git pull -s subtree导入子项目 278
16.2.3 将更改提交到上游 282
16.3 自动化解决方案:使用自定义脚本检出子项目 283
16.4 原生解决方案:gitlink和git submodule 284
16.4.1 gitlink 284
16.4.2 git submodule命令 287
第17章 子模块最佳实践 290
17.1 子模块命令 291
17.2 为什么要使用子模块 291
17.3 子模块准备 292
17.4 为什么是只读的 293
17.5 为什么不用只读的 293
17.6 检查子模块提交的散列 293
17.7 凭据重用 294
17.8 用例 295
17.9 版本库的多级嵌套 296
17.10 子模块的未来 296
第18章 结合SVN版本库使用Git 297
18.1 例子:对单一分支的浅克隆 297
18.1.1 在Git中进行修改 300
18.1.2 在提交前进行抓取操作 301
18.1.3 通过git svn rebase提交 302
18.2 在git svn中使用推送、拉取、分支和合并 303
18.2.1 直接使用提交ID 304
18.2.2 克隆所有分支 305
18.2.3 分享版本库 307
18.2.4 合并回SVN 308
18.3 在和SVN一起使用时的一些注意事项 310
18.3.1 svn:ignore与.gitignore 310
18.3.2 重建git-svn的缓存 310
第19章 高级操作 312
19.1 使用git filter-branch 312
19.1.1 使用git filter-branch的例子 314
19.1.2 filter-branch的诱惑 319
19.2 我如何学会喜欢上git rev-list 320
19.2.1 基于日期的检出 320
19.2.2 获取文件的旧版本 323
19.3 数据块的交互式暂存 325
19.4 恢复遗失的提交 336
19.4.1 git fsck命令 336
19.4.2 重新连接遗失的提交 340
第20章 提示、技巧和技术 341
20.1 对脏的工作目录进行交互式变基 341
20.2 删除剩余的编辑器文件 342
20.3 垃圾回收 342
20.4 拆分版本库 344
20.5 恢复提交的小贴士 345
20.6 转换Subversion的技巧 346
20.6.1 普适建议 346
20.6.2 删除SVN导入后的trunk 346
20.6.3 删除SVN提交ID 347
20.7 操作来自两个版本库的分支 348
20.8 从上游变基中恢复 348
20.9 定制Git命令 349
20.10 快速查看变更 350
20.11 清理 351
20.12 使用git-grep来搜索版本库 352
20.13 更新和删除ref 354
20.14 跟踪移动的文件 354
20.15 保留但不追踪文件 355
20.16 你来过这里吗 356
第21章 Git和GitHub 357
21.1 为开源代码提供版本库 358
21.2 创建GitHub的版本库 360
21.3 开源代码的社会化编程 362
21.4 关注者 363
21.5 新闻源 363
21.6 复刻 364
21.7 创建合并请求 365
21.8 管理合并请求 367
21.9 通知 369
21.10 查找用户、项目和代码 371
21.11 维基 373
21.12 GitHub页面(用于网站的Git) 373
21.13 页内代码编辑器 375
21.14 对接SVN 377
21.15 标签自动归档 378
21.16 组织 379
21.17 REST风格的API 379
21.18 闭源的社会化编程 381
21.19 最终开放源代码 381
21.20 开发模型 381
21.21 GitHub企业版 383
21.22 关于GitHub的总结 384
预计3月15日左右上架
人邮IT书坊****ID:ptpressitbooks
喜欢我,关注我,分享我——人邮IT书坊,程序员无悔的选择
网友评论