1.4 使用 Git 做版本控制
最后更新于:2022-04-01 22:27:45
# 1.4 使用 Git 做版本控制
我们已经开发了一个可以运行的 Rails 应用,接下来要花点时间来做一件事。虽然这件事不是必须的,但是经验丰富的软件开发者都认为这是最基本的事情,即把应用的源代码纳入版本控制。版本控制系统可以跟踪项目中代码的变化,便于和他人协作,如果出现问题(例如不小心删除了文件)还可以回滚到以前的版本。每个专业级软件开发者都应该学习使用版本控制系统。
版本控制系统种类很多,Rails 社区基本都使用 [Git](http://git-scm.com/)。Git 由 Linus Torvalds 开发,最初目的是存储 Linux 内核代码。Git 相关的知识很多,本书只会介绍一些皮毛。网络上有很多免费的资料,我特别推荐 Scott Chacon 写的《[Pro Git](http://git-scm.com/book)》。[[9](#fn-9)]之所以强烈推荐使用 Git 做版本控制,不仅因为 Rails 社区都在用,还因为使用 Git 分享代码更简单([1.4.3 节](#bitbucket)),而且也便于应用的部署([1.5 节](#deploying))。
## 1.4.1 安装和设置
[1.2.1 节](#development-environment)推荐使用的云端 IDE 默认已经集成 Git,不用再安装。如果你没使用云端 IDE,可以参照 [InstallRails.com](http://installrails.com/) 中的说明,在自己的系统中安装 Git。
### 第一次运行前要做的系统设置
使用Git 前,要做一些一次性设置。这些设置对整个系统都有效,因此一台电脑只需设置一次:
```
$ git config --global user.name "Your Name"
$ git config --global user.email your.email@example.com
$ 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](#fn-10)]
加入仓库的文件一开始位于“暂存区”(staging area),这一区用于存放待提交的内容。执行 `status` 命令可以查看暂存区中有哪些文件:
```
$ git status
On branch master
Initial commit
Changes to be committed:
(use "git rm --cached ..." 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 节](#branch-edit-commit-merge)会介绍如何把改动推送(使用 `git push` 命令)到远程仓库中。
顺便说一下,可以使用 `log` 命令查看提交的历史:
```
$ git log
commit df0a62f3f091e53ffa799309b3e32c27b0b38eb4
Author: Michael Hartl
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 ..." to update what will be committed)
(use "git checkout -- ..." 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](http://www.bitbucket.com/) 中。Bitbucket 是一个专门用来托管和分享 Git 仓库的网站。(本书前几版使用 [GitHub](http://www.github.com/),换用 Bitbucket 的原因参见[旁注 1.4](#aside-github-bitbucket)。)在 Bitbucket 中放一份 Git 仓库的副本有两个目的:其一,对代码做个完整备份(包括所有提交历史);其二,便于以后协作。
##### 旁注 1.4:GitHub 和 Bitbucket
目前,托管 Git 仓库最受欢迎的网站是 GitHub 和 Bitbucket。这两个网站有很多相似之处:都可托管仓库,也可以协作,而且浏览和搜索仓库很方便。但二者之间有个重要的区别(对本书而言):GitHub 为开源项目提供无限量的免费仓库,但私有仓库收费;而 Bitbucket 提供了无限量的私有仓库,仅当协作者超过一定数量时才收费。所以,选择哪个网站,取决于具体的需求。
本书前几版使用 GitHub,因为它对开源项目来说有很多好用的功能,但我越来越关注安全,所以推荐所有 Web 应用都放在私有仓库中。因为 Web 应用的仓库中可能包含潜在的敏感信息,例如密钥和密码,可能会威胁到使用这份代码的网站的安全。当然,这类信息也有安全的处理方法,但是容易出错,而且需要很多专业知识。
本书开发的演示应用可以安全地公开,但这只是特例,不能推广。因此,为了尽量提高安全,我们不能冒险,还是默认就使用私有仓库保险。既然 GitHub 对私有仓库收费,而 Bitbucket 提供了不限量的免费私有仓库,就我们的需求来说,Bitbucket 比 Github 更合适。
Bitbucket 的使用方法很简单:
1. 如果没有账户,先[注册一个 Bitbucket 账户](https://bitbucket.org/account/signup/);
2. 把[公钥](https://en.wikipedia.org/wiki/Public-key_cryptography)复制到剪切板。云端 IDE 用户可以使用 `cat` 命令查看公钥,如[代码清单 1.11](#listing-cat-public-key) 所示,然后选中公钥,复制。如果你在自己的系统中,执行[代码清单 1.11](#listing-cat-public-key) 中的命令后没有输出,请参照“[如何在你的 Bitbucket 账户中设定公钥](https://confluence.atlassian.com/display/BITBUCKET/How+to+install+a+public+key+on+your+Bitbucket+account;jsessionid=C5CF09B1641344FE876045B35D720C94.node1)”;
3. 点击右上角的头像,选择“Manage account”(管理账户),然后点击“SSH keys”(SSH 密钥),如[图 1.13](#fig-add-public-key) 所示。
![add public key](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-11_5732bcae1cb51.png)图 1.13:添加 SSH 公钥
##### 代码清单 1.11:使用 `cat` 命令打印公钥
```
$ cat ~/.ssh/id_rsa.pub
```
添加公钥之后,点击“Create”(创建)按钮,[新建一个仓库](https://bitbucket.org/repo/create),如[图 1.14](#fig-create_first_repository) 所示。填写项目的信息时,记得要选中“This is a private repository”(这是私有仓库)。填完后点击“Create repository”(创建仓库)按钮,然后按照“Command line > I have an existing project”(命令行 > 现有项目)下面的说明操作,如[代码清单 1.12](#listing-bitbucket-add-push) 所示。(如果与代码清单 1.12 不同,可能是公钥没正确添加,我建议你再试一次。)推送仓库时,如果询问“Are you sure you want to continue connecting (yes/no)?”(确定继续连接吗?),输入“yes”。
##### 代码清单 1.12:添加 Bitbucket,然后推送仓库
```
$ git remote add origin git@bitbucket.org:/hello_app.git
$ git push -u origin --all # 第一次推送仓库
```
这段代码的意思是,先告诉 Git,你想添加 Bitbucket,作为这个仓库的源,然后再把仓库推送到这个远端的源。(别管 `-u` 旗标的意思,如果好奇,可以搜索“git set upstream”。)当然了,你要把 `<username>` 换成你自己的用户名。例如,我运行的命令是:
```
$ git remote add origin git@bitbucket.org:mhartl/hello_app.git
```
推送完毕后,在 Bitbucket 中会显示一个 `hello_app` 仓库的页面。在这个页面中可以浏览文件、查看完整的提交信息,除此之外还有很多其他功能(如[图 1.15](#fig-bitbucket-repository-page) 所示)。
![create first repository bitbucket](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-11_5732bcae39f7f.png)图 1.14:在 Bitbucket 中创建存放这个应用的仓库![bitbucket repository page](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-11_5732bcae5551d.png)图 1.15:一个 Bitbucket 仓库的页面
## 1.4.4 分支,编辑,提交,合并
如果你跟着 [1.4.3 节](#bitbucket)中的步骤做,可能注意到了,Bitbucket 没有自动识别仓库中的 `README.rdoc` 文件,而在仓库的首页提醒没有 README 文件,如[图 1.16](#fig-bitbucket-no-readme) 所示。这说明 `rdoc` 格式不常见,所以 Bitbucket 不支持。其实,我以及我认识的几乎所有人都使用 Markdown 格式。这一节,我们要把 `README.rdoc` 文件改成 `README.md`,顺便还要在其中添加一些针对本书的内容。在这个过程中,我们将首次演示我推荐在 Git 中使用的工作流程,即“分支,编辑,提交,合并”。[[11](#fn-11)]
![bitbucket no readme](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-11_5732bcae780b5.png)图 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 节](#version-control-with-git)中设置了别名 `co`,就要使用 `git co -b modify-README`。)
只有多个开发者协作开发一个项目时,才能看出分支的全部价值。[[12](#fn-12)]如果只有一个开发者,分支也有作用。一般情况下,要把主题分支的改动和主分支隔离开,这样即便搞砸了,随时都可以切换到主分支,然后删除主题分支,丢掉改动。本节末尾会介绍具体做法。
顺便说一下,像这种小改动,我一般不会新建分支。现在我这么做是为了让你养成好习惯。
### 编辑
创建主题分支后,我们要编辑 README 文件,让其更好地描述我们的项目。较之默认的 RDoc 格式,我更喜欢 [Markdown 标记语言](http://daringfireball.net/projects/markdown/)。如果文件扩展名是 `.md`,Bitbucket 会自动排版其中的内容。首先,使用 Git 提供的 Unix `mv` 命令修改文件名:
```
$ git mv README.rdoc README.md
```
然后把[代码清单 1.13](#listing-new-readme) 中的内容写入 `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 ..." to unstage)
renamed: README.rdoc -> README.md
Changes not staged for commit:
(use "git add ..." to update what will be committed)
(use "git checkout -- ..." to discard changes in working directory)
modified: README.md
```
这里,我们本可以使用“[第一次使用仓库前要做的设置](#first-time-repository-setup)”一节用过的 `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](https://github.com/blog/926-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
$
$ 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 节](#bitbucket)),在大多数系统中都可以省略 `origin master`,直接执行 `git push`:
```
$ git push
```
正如前面所说,Bitbucket 对使用 Markdown 编写的文件做了精美排版,如[图 1.17](#fig-new-readme) 所示。
![new readme bitbucket](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-11_5732bcae8fd74.png)图 1.17:使用 Markdown 格式重写的 `README` 文件
';