1.4 使用 Git 做版本控制

我们已经开发了一个可以运行的 Rails 应用,接下来要花点时间来做一件事。虽然这件事不是必须的,但是经验丰富的软件开发者都认为这是最基本的事情,即把应用的源代码纳入版本控制。版本控制系统可以跟踪项目中代码的变化,便于和他人协作,如果出现问题(例如不小心删除了文件)还可以回滚到以前的版本。每个专业级软件开发者都应该学习使用版本控制系统。

版本控制系统种类很多,Rails 社区基本都使用 Git。Git 由 Linus Torvalds 开发,最初目的是存储 Linux 内核代码。Git 相关的知识很多,本书只会介绍一些皮毛。网络上有很多免费的资料,我特别推荐 Scott Chacon 写的《Pro Git》。[9]之所以强烈推荐使用 Git 做版本控制,不仅因为 Rails 社区都在用,还因为使用 Git 分享代码更简单(1.4.3 节),而且也便于应用的部署(1.5 节)。

1.4.1 安装和设置

1.2.1 节推荐使用的云端 IDE 默认已经集成 Git,不用再安装。如果你没使用云端 IDE,可以参照 InstallRails.com 中的说明,在自己的系统中安装 Git。

第一次运行前要做的系统设置

使用Git 前,要做一些一次性设置。这些设置对整个系统都有效,因此一台电脑只需设置一次:

$ git config --global user.name "Your Name"
$ git config --global user.email [email protected]
$ git config --global push.default matching
$ git config --global alias.co checkout

注意,在 Git 配置中设定的名字和电子邮件地址会在所有公开的仓库中显示。(前两个设置必须做。第三个设置是为了向前兼容未来的 Git 版本。第四个设置是可选的,如果设置了,就可以使用 co 代替 checkout 命令。为了最大程度上兼容没有设置 co 的系统,本书仍将继续使用全名 checkout,不过在现实中我基本都用 git co。)

第一次使用仓库前要做的设置

下面的步骤每次新建仓库时都要执行。首先进入第一个应用的根目录,然后初始化一个新仓库:

$ git init
Initialized empty Git repository in /home/ubuntu/workspace/hello_app/.git/

然后执行 git add -A 命令,把项目中的所有文件都放到仓库中:

$ git add -A

这个命令会把当前目录中的所有文件都放到仓库中,但是匹配特殊文件 .gitignore 中模式的文件除外。rails new 命令会自动生成一个适用于 Rails 项目的 .gitignore 文件,而且你还可以添加其他模式。[10]

加入仓库的文件一开始位于“暂存区”(staging area),这一区用于存放待提交的内容。执行 status 命令可以查看暂存区中有哪些文件:

$ git status
On branch master

Initial commit

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)

  new file:   .gitignore
  new file:   Gemfile
  new file:   Gemfile.lock
  new file:   README.rdoc
  new file:   Rakefile
  .
  .
  .

(显示的内容很多,所以我使用竖排点号省略了一些内容。)

如果想告诉 Git 保留这些改动,可以使用 commit 命令:

$ git commit -m "Initialize repository"
[master (root-commit) df0a62f] Initialize repository
.
.
.

旗标 -m 的意思是为这次提交添加一个说明。如果没指定 -m 旗标,Git 会打开系统默认使用的编辑器,让你在其中输入说明。(本书所有的示例都会使用 -m 旗标。)

有一点很重要要注意:Git 提交只发生在本地,也就是说只在执行提交操作的设备中存储内容。1.4.4 节会介绍如何把改动推送(使用 git push 命令)到远程仓库中。

顺便说一下,可以使用 log 命令查看提交的历史:

$ git log
commit df0a62f3f091e53ffa799309b3e32c27b0b38eb4
Author: Michael Hartl <[email protected]>
Date:   Wed August 20 19:44:43 2014 +0000

    Initialize repository

如果仓库的提交历史很多,可能需要输入 q 退出。

1.4.2 使用 Git 有什么好处

如果以前从未用过版本控制,现在可能不完全明白版本控制的好处。那我举个例子说明一下吧。假如你不小心做了某个操作,例如把重要的 app/controllers/ 文件夹删除了:

$ ls app/controllers/
application_controller.rb  concerns/
$ rm -rf app/controllers/
$ ls app/controllers/
ls: app/controllers/: No such file or directory

我们用 Unix 中的 ls 命令列出 app/controllers/ 文件夹里的内容,然后用 rm 命令删除这个文件夹。旗标 -rf 的意思是“强制递归”,无需明确征求同意就递归删除所有文件、文件夹和子文件夹等。

查看一下状态,看看发生了什么:

$ git status
On branch master
Changed but not updated:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

      deleted:    app/controllers/application_controller.rb

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

可以看出,删除了一个文件。但是这个改动只发生在“工作树”中,还未提交到仓库。所以,我们可以使用 checkout 命令,并指定 -f 旗标,强制撤销这次改动:

$ git checkout -f
$ git status
# On branch master
nothing to commit (working directory clean)
$ ls app/controllers/
application_controller.rb  concerns/

删除的文件夹和文件又回来了,这下放心了!

1.4.3 Bitbucket

我们已经把项目纳入 Git 版本控制系统了,接下来可以把代码推送到 Bitbucket 中。Bitbucket 是一个专门用来托管和分享 Git 仓库的网站。(本书前几版使用 GitHub,换用 Bitbucket 的原因参见旁注 1.4。)在 Bitbucket 中放一份 Git 仓库的副本有两个目的:其一,对代码做个完整备份(包括所有提交历史);其二,便于以后协作。

旁注 1.4:GitHub 和 Bitbucket

目前,托管 Git 仓库最受欢迎的网站是 GitHub 和 Bitbucket。这两个网站有很多相似之处:都可托管仓库,也可以协作,而且浏览和搜索仓库很方便。但二者之间有个重要的区别(对本书而言):GitHub 为开源项目提供无限量的免费仓库,但私有仓库收费;而 Bitbucket 提供了无限量的私有仓库,仅当协作者超过一定数量时才收费。所以,选择哪个网站,取决于具体的需求。

本书前几版使用 GitHub,因为它对开源项目来说有很多好用的功能,但我越来越关注安全,所以推荐所有 Web 应用都放在私有仓库中。因为 Web 应用的仓库中可能包含潜在的敏感信息,例如密钥和密码,可能会威胁到使用这份代码的网站的安全。当然,这类信息也有安全的处理方法,但是容易出错,而且需要很多专业知识。

本书开发的演示应用可以安全地公开,但这只是特例,不能推广。因此,为了尽量提高安全,我们不能冒险,还是默认就使用私有仓库保险。既然 GitHub 对私有仓库收费,而 Bitbucket 提供了不限量的免费私有仓库,就我们的需求来说,Bitbucket 比 Github 更合适。

Bitbucket 的使用方法很简单:

  1. 如果没有账户,先注册一个 Bitbucket 账户

  2. 公钥复制到剪切板。云端 IDE 用户可以使用 cat 命令查看公钥,如代码清单 1.11 所示,然后选中公钥,复制。如果你在自己的系统中,执行代码清单 1.11 中的命令后没有输出,请参照“如何在你的 Bitbucket 账户中设定公钥”;

  3. 点击右上角的头像,选择“Manage account”(管理账户),然后点击“SSH keys”(SSH 密钥),如图 1.13 所示。

add public key图 1.13:添加 SSH 公钥

代码清单 1.11:使用 cat 命令打印公钥
$ cat ~/.ssh/id_rsa.pub

添加公钥之后,点击“Create”(创建)按钮,新建一个仓库,如图 1.14 所示。填写项目的信息时,记得要选中“This is a private repository”(这是私有仓库)。填完后点击“Create repository”(创建仓库)按钮,然后按照“Command line > I have an existing project”(命令行 > 现有项目)下面的说明操作,如代码清单 1.12 所示。(如果与代码清单 1.12 不同,可能是公钥没正确添加,我建议你再试一次。)推送仓库时,如果询问“Are you sure you want to continue connecting (yes/no)?”(确定继续连接吗?),输入“yes”。

代码清单 1.12:添加 Bitbucket,然后推送仓库
$ git remote add origin [email protected]:<username>/hello_app.git
$ git push -u origin --all # 第一次推送仓库

这段代码的意思是,先告诉 Git,你想添加 Bitbucket,作为这个仓库的源,然后再把仓库推送到这个远端的源。(别管 -u 旗标的意思,如果好奇,可以搜索“git set upstream”。)当然了,你要把 &lt;username&gt; 换成你自己的用户名。例如,我运行的命令是:

$ git remote add origin [email protected]:mhartl/hello_app.git

推送完毕后,在 Bitbucket 中会显示一个 hello_app 仓库的页面。在这个页面中可以浏览文件、查看完整的提交信息,除此之外还有很多其他功能(如图 1.15 所示)。

create first repository bitbucket图 1.14:在 Bitbucket 中创建存放这个应用的仓库bitbucket repository page图 1.15:一个 Bitbucket 仓库的页面

1.4.4 分支,编辑,提交,合并

如果你跟着 1.4.3 节中的步骤做,可能注意到了,Bitbucket 没有自动识别仓库中的 README.rdoc 文件,而在仓库的首页提醒没有 README 文件,如图 1.16 所示。这说明 rdoc 格式不常见,所以 Bitbucket 不支持。其实,我以及我认识的几乎所有人都使用 Markdown 格式。这一节,我们要把 README.rdoc 文件改成 README.md,顺便还要在其中添加一些针对本书的内容。在这个过程中,我们将首次演示我推荐在 Git 中使用的工作流程,即“分支,编辑,提交,合并”。[11]

bitbucket no readme图 1.16:Bitbucket 提示没有 README 文件

分支

Git 中的分支功能很强大。分支是对仓库的高效复制,在分支中所做的改动(或许是实验性质的)不会影响父级文件。大多数情况下,父级仓库是 master 分支。我们可以使用 checkout 命令,并指定 -b 旗标,创建一个新“主题分支”(topic branch):

$ git checkout -b modify-README Switched to a new branch 'modify-README'
$ git branch
  master
* modify-README

其中,第二个命令 git branch 的作用是列出所有本地分支。星号(*)表示当前所在的分支。注意,git checkout -b modify-README 命令先创建一个新分支,然后再切换到这个新分支——modify-README 分支前面的星号证明了这一点。(如果你在 1.4 节中设置了别名 co,就要使用 git co -b modify-README。)

只有多个开发者协作开发一个项目时,才能看出分支的全部价值。[12]如果只有一个开发者,分支也有作用。一般情况下,要把主题分支的改动和主分支隔离开,这样即便搞砸了,随时都可以切换到主分支,然后删除主题分支,丢掉改动。本节末尾会介绍具体做法。

顺便说一下,像这种小改动,我一般不会新建分支。现在我这么做是为了让你养成好习惯。

编辑

创建主题分支后,我们要编辑 README 文件,让其更好地描述我们的项目。较之默认的 RDoc 格式,我更喜欢 Markdown 标记语言。如果文件扩展名是 .md,Bitbucket 会自动排版其中的内容。首先,使用 Git 提供的 Unix mv 命令修改文件名:

$ git mv README.rdoc README.md

然后把代码清单 1.13 中的内容写入 README.md

代码清单 1.13:新 README 文件,README.md
# Ruby on Rails Tutorial: "hello, world!"

This is the first application for the
[*Ruby on Rails Tutorial*](http://www.railstutorial.org/)
by [Michael Hartl](http://www.michaelhartl.com/).

提交

编辑后,查看一下该分支的状态:

$ git status On branch modify-README
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

  renamed:    README.rdoc -> README.md

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:   README.md

这里,我们本可以使用“第一次使用仓库前要做的设置”一节用过的 git add -A,但是 git commit 提供了 -a 旗标,可以直接提交现有文件中的全部改动(以及使用 git mv 新建的文件,对 Git 来说这不算新文件):

$ git commit -a -m "Improve the README file" 2 files changed, 5 insertions(+), 243 deletions(-)
delete mode 100644 README.rdoc
create mode 100644 README.md

使用 -a 旗标一定要小心,千万别误用了。如果上次提交之后项目中添加了新文件,应该使用 git add -A,先告诉 Git 新增了文件。

注意,我们使用现在时(严格来说是祈使语气)编写提交消息。Git 把提交当做一系列补丁,在这种情况下,说明现在做了什么比说明过去做了什么要更合理。而且这种用法和 Git 命令生成的提交信息相配。详情参阅《Shiny new commit styles》。

合并

我们已经改完了,现在可以把结果合并到主分支了:

$ git checkout master Switched to branch 'master'
$ git merge modify-README Updating 34f06b7..2c92bef
Fast forward
README.rdoc     |  243 --------------------------------------------------
README.md       |    5 +
2 files changed, 5 insertions(+), 243 deletions(-)
delete mode 100644 README.rdoc
create mode 100644 README.md

注意,Git 命令的输出中经常会出现 34f06b7 这样的字符串,这是 Git 内部对仓库的指代。你得到的输出结果不会和我的一模一样,但大致相同。

合并之后,我们可以清理一下分支——如果不用这个主题分支了,可以使用 git branch -d 命令将其删除:

$ git branch -d modify-README Deleted branch modify-README (was 2c92bef).

这一步可做可不做,其实一般都会留着这个主题分支,这样就可以在两个分支之间来回切换,并在合适的时候把改动合并到主分支中。

前面提过,还可以使用 git branch -D 命令放弃主题分支中的改动:

# 仅作演示之用,如果没搞砸,千万别这么做
$ git checkout -b topic-branch
$ <really screw up the branch>
$ git add -A
$ git commit -a -m "Major screw up"
$ git checkout master
$ git branch -D topic-branch

和旗标 -d 不同,如果指定旗标 -D,即使没合并分支中的改动,也会删除分支。

推送

我们已经更新了 README 文件,现在可以把改动推送到 Bitbucket,看看改动的效果。之前我们已经推送过一次(1.4.3 节),在大多数系统中都可以省略 origin master,直接执行 git push

$ git push

正如前面所说,Bitbucket 对使用 Markdown 编写的文件做了精美排版,如图 1.17 所示。

new readme bitbucket图 1.17:使用 Markdown 格式重写的 README 文件