内容


面向 Subversion 用户的 Git,第 1 部分

入门指南

对于 Subversion 版本控制系统用户 Git 不再神秘

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: 面向 Subversion 用户的 Git,第 1 部分

敬请期待该系列的后续内容。

此内容是该系列的一部分:面向 Subversion 用户的 Git,第 1 部分

敬请期待该系列的后续内容。

对于任何不熟悉免费的开源版本控制系统(VCS)的人来说,Subversion 开始成为一种标准的非商业性质的 VCS,取代了旧的并发版本系统(Concurrent Versions System,CVS)。CVS 仍然适用于使用有限的场景,但是 Subversion 的魅力在于它只需要对 Web 服务器进行少量设置。Subversion 的确存在一些问题,我也会在本文中进行讨论,但是从大体上来说,它仍然是一种有效的系统。

那么,为什么还需要另一种系统呢?Git(大写 G;git 是命令行工具)在许多方面要优于 Subversion。它是众多分布式 VCS 中的一种。我自己使用过 Arch/tla,以及 Mercurial、Bazaar、darcs 和一些其他系统。在诸多因素的影响下(我将按照相关性进行讨论),Git 变得很流行,并且经常被认为是和 Subversion 并列的用于个人和企业 VCS 的首选。

如果您是一名 Subversion 用户的话,那么有两个重要的原因会使您对 Git 产生兴趣。

  • 您尝试迁移到 Git,因为 Subversion 在某种程度上会限制您。
  • 您对 Git 感到好奇,并且希望将它与 Subversion 进行比较。

好吧,也许还有第三个原因:Git 是一种比较热门的技术,您希望将它写到自己的简历中。我希望这并不是您的主要目标;学习 Git 是一名开发人员可以做的最有益的事情之一。即使您现在没有用到 Git,这个分布式 VCS 所蕴含的概念和工作流对于未来 10 年中 IT 产业的大部分领域都是非常宝贵的知识,因为 IT 行业将在范围和地理分布方面发生重大的变革。

从根本上说,如果您不是一名 Linux 内核开发人员的话,可能算不上是一个有说服力的理由,但是内核以及大量其他重要项目都将使用 Git 进行维护,因此如果您计划做出贡献的话,那么将需要熟悉它。

本文面向的是初中级 Subversion 用户。需要具备初级 Subversion 知识和一些有关版本控制系统的一般知识。本文介绍的信息主要针对 UNIX® 类(Linux® 和 Mac OS X)系统用户,并涉及少量针对 Windows® 用户的知识。

本系列的第 2 部分将讨论 Git 的更高级应用:合并分支、生成差异(diff)和其他常见任务。

Subversion 和 Git 基础

在后文中,我将 “Subversion” 缩写为 “SVN”,以便节省些时间并减少键盘磨损。

那么,SVN 有哪些优势?您可能已经有所了解,但是 VCS 与文件无关;它关注的是修改。SVN 运行在中央服务器上,向它的数据存储库中添加修改,并在每次修改后为您提供一个快照。这个快照有一个修订号;修订号对于 SVN 以及 SVN 用户非常重要。如果您在我之后继续修改,那么您必定会得到一个更高的修订号。

Git 具有类似的目标 — 跟踪修改 — 但是没有使用集中式服务器。这一差别非常关键。SVN 是集中式的,而 Git 是分布式的;因此,Git 不会提供一个逐渐变大的修订号,因为不存在 “最新的修订”。它仍然使用唯一的修订 ID;只不过唯一修订 ID 本身并没有 SVN 修订号那么有用。

对于 Git,比较关键的操作不再是提交(commit);而是合并(merge)。任何人都可以克隆存储库并提交给 clone。存储库所有者可以选择合并更改。或者,开发人员可以将修改推回到存储库中。我将只讨论后一种情况,即经过授权的 push 模型。

在 SVN 下放置一个目录

让我们从一个简单的示例开始:使用 SVN 跟踪目录的内容。将需要一个 SVN 服务器,当然,还需要一个文件目录,以及该服务器上的一个帐户,并能够通过至少一种方式进行提交。在开始之前,首先添加并提交目录:

清单 1. 在 SVN 下设置一个目录
% svn co http://svnserver/...some path here.../top
% cd top
% cp -r ~/my_directory .
% svn add my_directory
% svn commit -m 'added directory'

这些操作有什么作用?现在,您可以从这个目录中获得已提交文件的最新版本,重命名它们,创建新的文件或目录,将更改提交到现有文件,等等:

清单 2. SVN 下的基本文操作
# get latest
% svn up
# what's the status?
% svn st
# delete files
% svn delete
# rename files (really a delete + add that keeps history)
% svn rename
# make directory
% svn mkdir
# add file
% svn add
# commit changes (everything above, plus any content changes)
% svn commit

我不会详细解释这些命令,但是需要牢记它们。要获得有关这些命令的帮助,只需要输入 svn help COMMAND,Subversion 将显示一些基本帮助;可以从参考手册中了解更多。

在 Git 下放置一个目录

我将遵循和 SVN 示例相同的思路。和前面一样,我假设您已经具有一个填充了数据的目录。

对于远程服务器,我将使用免费的 github.com 服务,当然,您也可以设置自己的服务器。使用 GitHub 可以很轻松地处理远程 Git 存储库。在撰写本文时,对于免费帐户,数据被限制为 300MB,您的存储库必须是公开的。我注册了一个用户名为 “tzz” 的帐户,并创建了名为 “datatest” 的公共存储库;可以随便使用它。我生成了一个公共 SSH 键;如果您还没有的话,那么应当生成一个键。您可能还希望尝试 Gitorious 服务器或 repo.or.cz。您将在 git.or.cz Wiki 上找到一长串 Git 宿主服务(从 参考资料 部分可获得链接)。

GitHub 的一个优点就是它非常友好。它会准确地告诉您需要哪些命令来设置 Git 并初始化库。我将向您介绍这些内容。

首先,需要安装 Git,Git 安装在每个平台上都有所不同,然后对它执行初始化。Git 下载页面(见 参考资料)列出了大量与平台有关的选项。(在 Mac OS X 上,我使用 port install git-core 命令,但是您需要首先设置 MacPorts。Git 下载页面还提供了一个独立的 MacOS X Git 安装程序链接;这个工具可能更适合大多数人使用)。

一旦安装完毕后,使用下面的命令进行初步设置(当然,选择您自己的用户名和电子邮件地址):

清单 3. 基本 Git 设置
% git config --global user.name "Ted Zlatanov"
% git config --global user.email "tzz@bu.edu"

您已经观察到与 SVN 的一个不同之处;在 SVN 中,您的用户身份属于服务器端,并且您的身份由服务器决定。在 Git 中,您可以是 Wittgenstein 的那只奇妙的猴子,如果您愿意的话(我克制着这种诱惑)。

接下来,我将设置数据文件并使用它们初始化我的存储库。(GitHub 还将从一个公共 SVN 存储库导入,这将非常有帮助)。

清单 4. 目录设置和首次提交
# grab some files
% cp -rp ~/.gdbinit gdbinit
% mkdir fortunes
% cp -rp ~/.fortunes.db fortunes/data.txt
# initialize
% git init
# "Initialized empty Git repository in /Users/tzz/datatest/.git/"
# add the file and the directory
% git add gdbinit fortunes
% git commit -m 'initializing'
#[master (root-commit) b238ddc] initializing
# 2 files changed, 2371 insertions(+), 0 deletions(-)
# create mode 100644 fortunes/data.txt
# create mode 100644 gdbinit

在上面的输出当中,Git 向我们显示有关文件模式的信息;100644 表示这些文件的权限的八进制版本。您不需要更新这些内容,但是 2371 insertions 有些费解。它只修改两个文件,不是吗?这个数字实际上指的是插入的行的数量。当然,我们没有删除任何行。

将我们的新更改推入到 GitHub 服务器怎么样?这个文档告诉我们如何添加一个名为 “origin” 的远程服务器(您可以使用任何名称)。在这里我应当提一下,如果希望了解更多有关任何 Git 命令的信息,比如 git remote,您可以输入 git remote --helpgit help remote。这是命令行工具的典型行为,并且 SVN 也执行一些非常相似的行为。

清单 5. 将更改拖入到远程服务器
# remember the remote repository is called "datatest"?
% git remote add origin git@github.com:tzz/datatest.git
# push the changes
% git push origin master
#Warning: Permanently added 'github.com,65.74.177.129' (RSA) to the list of known hosts.
#Counting objects: 5, done.
#Delta compression using 2 threads.
#Compressing objects: 100% (4/4), done.
#Writing objects: 100% (5/5), 29.88 KiB, done.
#Total 5 (delta 0), reused 0 (delta 0)
#To git@github.com:tzz/datatest.git
# * [new branch]      master -> master

OpenSSH 将发出一个警告,因为 github.com 不是一个此前已经知道的主机。不过不需要担心。

Git 消息可以认为是非常详细的。与易于理解的 SVN 消息不同,Git 由专业人员专门针对同行编写。如果您来自 Frank Herbert 的沙丘 魔堡并被训练为一台人类计算机,您很可能编写了自己的 Git 版本,仅仅因为您能够做到这一点。对于其他人而言,对 delta 压缩和涉及到的线程的数量并不是很关心(并且它们会让我们感到头痛)。

将更改推入服务器是通过 SSH 完成的,但是也可以使用其他协议,比如 HTTP、HTTPS、rsync 和文件。参考 git push --help

下面介绍 SVN 和 Git 之间最关键也是最基本的差异。SVN 的提交操作表示 “将更改推入到中央服务器”。在 SVN 中执行提交之前,您所做的变更都没有实际生效。而对于 Git,您的提交是本地化的,并且您拥有一个本地存储库,不管远程服务器上发生什么操作。您可以取消一项更改,进行分支,提交到分支,等等,并且不需要与远程服务器进行交互。使用 Git 推入更改可以非常有效地同步存储库状态和远程服务器。

一切良好,那么让我们看看记录刚才发生的操作的 Git 日志:

清单 6. Git 日志
% git log
#commit b238ddca99ee582e1a184658405e2a825f0815da
#Author: Ted Zlatanov <tzz@lifelogs.com>
#Date:   ...commit date here...
#
#    initializing

日志中只记录了提交(注意比较一下冗长的随机提交 ID 和 SVN 的修订号)。日志中没有提到通过 git push 实现的同步。

通过 Git 实现协作

目前为止,我们一直将 Git 用作 SVN 的替代品。当然,要增加趣味性,必须获得多个用户和修改集合。我将把这个存储库签出到另一台机器(本例中运行的是 Ubuntu GNU/Linux;您将需要安装 git-core 而不是 git):

清单 7. 设置另一个 Git 身份并签出存储库
% git config --global user.name "The Other Ted"
% git config --global user.email "tzz@bu.edu"
% git clone git@github.com:tzz/datatest.git
#Initialized empty Git repository in /home/tzz/datatest/.git/
#Warning: Permanently added 'github.com,65.74.177.129' (RSA) to the list of known hosts.
#remote: Counting objects: 5, done.
#remote: Compressing objects: 100% (4/4), done.
#Indexing 5 objects...
#remote: Total 5 (delta 0), reused 0 (delta 0)
# 100% (5/5) done
% ls datatest
#fortunes  gdbinit
% ls -a datatest/.git
# .  ..  branches  config  description  HEAD  hooks  index  info  logs  objects  refs
% ls -a datatest/.git/hooks
# .  ..  applypatch-msg  commit-msg  post-commit  post-receive post-update
#  pre-applypatch  pre-commit  pre-rebase  update

注意,OpenSSH 警告表示我们此前没有从这台机器上通过 SSH 处理 GitHub。git clone 命令类似于一个 SVN 签出(checkout),但是与获取同步内容不同的是(特定修订的快照,或者是最新修订),您将获得完整的存储库。

我包含了 datatest/.git 目录以及其下的 hooks 子目录的内容,以表明您确实获得了所有内容。Git 默认情况下不会加密,这点与 SVN 不同,后者默认情况下将存储库设置为私有,仅允许访问快照。

顺便提一下,如果您希望对 Git 存储库施加某些规则,不管是针对每一个提交或是在其他情况下,都可以使用钩子(hook)。它们都是 shell 脚本,非常类似于 SVN 钩子,并且使用相同的标准 UNIX 约定:“返回 0 表示成功,其他则表示失败”。我在本文不会详细地介绍钩子,但是如果您准备在团队中使用 Git,那么一定要了解一下这些内容。

好的,现在让我们将一个新文件添加到主分支中(相当于 SVN 的 TRUNK)并通过对 gdbinit 文件作出一些修改来生成一个新分支。

清单 8. 添加一个文件并生成一个新分支
# get a file to add...
% cp ~/bin/encode.pl .
% git add encode.pl
% git commit -m 'adding encode.pl'
#Created commit 6750342: adding encode.pl
# 1 files changed, 1 insertions(+), 0 deletions(-)
# create mode 100644 encode.pl
% git log
#commit 675034202629e5497ed10b319a9ba42fc72b33e9
#Author: The Other Ted <tzz@bu.edu>
#Date:   ...commit date here...
#
#    adding encode.pl
#
#commit b238ddca99ee582e1a184658405e2a825f0815da
#Author: Ted Zlatanov <tzz@lifelogs.com>
#Date:   ...commit date here...
#
#    initializing
% git branch empty-gdbinit
% git branch
#  empty-gdbinit
#* master
% git checkout empty-gdbinit
#Switched to branch "empty-gdbinit"
% git branch
#* empty-gdbinit
#  master
% git add gdbinit
% git commit -m 'empty gdbinit'
#Created commit 5512d0a: empty gdbinit
# 1 files changed, 0 insertions(+), 1005 deletions(-)
% git push
#updating 'refs/heads/master'
#  from b238ddca99ee582e1a184658405e2a825f0815da
#  to   675034202629e5497ed10b319a9ba42fc72b33e9
#Generating pack...
#Done counting 4 objects.
#Result has 3 objects.
#Deltifying 3 objects...
# 100% (3/3) done
#Writing 3 objects...
# 100% (3/3) done
#Total 3 (delta 0), reused 0 (delta 0)

这个示例太长了,希望您没有打瞌睡;如果睡着了的话,希望您能梦到 Git 存储库在不断地同步修改集合(不用担心,您一定会梦到的)。

首先,我添加了一个文件(encode.pl,只包含一行)并提交它。提交文件后,GitHub 上的远程存储库并不知道我已经作出更改。随后我生成一个名为 empty-gdbinit 的新分支,然后切换到该分支(我也可以使用 git checkout -b empty-gdbinit 完成这个操作)。在新的分支中,我清空 gdbinit 文件并提交这个更改。最后,我将更改推入到远程服务器。

如果我切换到主分支,那么在日志中不会看到空的 gdbinit。因此,每个分支都有它自己的日志,这是很有意义的。

清单 9. 查看分支之间的日志
# we are still in the empty-gdbinit branch
% git log
#commit 5512d0a4327416c499dcb5f72c3f4f6a257d209f
#Author: The Other Ted <tzz@bu.edu>
#Date:   ...commit date here...
#
#    empty gdbinit
#
#commit 675034202629e5497ed10b319a9ba42fc72b33e9
#Author: The Other Ted <tzz@bu.edu>
#Date:   ...commit date here...
#
#    adding encode.pl
#
#commit b238ddca99ee582e1a184658405e2a825f0815da
#Author: Ted Zlatanov <tzz@lifelogs.com>
#Date:   ...commit date here...
#
#    initializing
% git checkout master
#Switched to branch "master"
% git log
#commit 675034202629e5497ed10b319a9ba42fc72b33e9
#Author: The Other Ted <tzz@bu.edu>
#Date:   ...commit date here...
#
#    adding encode.pl
#
#commit b238ddca99ee582e1a184658405e2a825f0815da
#Author: Ted Zlatanov <tzz@lifelogs.com>
#Date:   ...commit date here...
#
#    initializing

当我执行推入操作时,Git 会在 GitHub 的服务器上显示一个名为 encode.pl 的新文件。

GitHub 的 Web 界面现在将显示 encode.pl。但是 GitHub 上仍然只有一个分支。为什么 empty-gdbinit 分支没有被同步?这是因为 Git 在默认情况下并没有假设您希望推入分支及其更改。因此,您需要推入所有内容:

清单 10. 推入所有内容
% git push -a
#updating 'refs/heads/empty-gdbinit'
#  from 0000000000000000000000000000000000000000
#  to   5512d0a4327416c499dcb5f72c3f4f6a257d209f
#updating 'refs/remotes/origin/HEAD'
#  from 0000000000000000000000000000000000000000
#  to   b238ddca99ee582e1a184658405e2a825f0815da
#updating 'refs/remotes/origin/master'
#  from 0000000000000000000000000000000000000000
#  to   b238ddca99ee582e1a184658405e2a825f0815da
#Generating pack...
#Done counting 5 objects.
#Result has 3 objects.
#Deltifying 3 objects...
# 100% (3/3) done
#Writing 3 objects...
# 100% (3/3) done
#Total 3 (delta 1), reused 0 (delta 0)

再一次出现了面向专业人士的界面。但是我们仍然可以使用,不是吗?也许我们并不是专业人士,但是至少懂得 0000000000000000000000000000000000000000 表示某种特殊的初始标记。我们还可以从 清单 9 的日志看出,标记 5512d0a4327416c499dcb5f72c3f4f6a257d209f 表示 empty-gdbinit 分支中的最后一个也是唯一一个提交。其余则大多数用户都难以理解。这些内容不需要考虑。GitHub 将显示新的分支和其中的修改。

可以分别使用 git mvgit rm 管理文件,重命名并移除它们。

结束语

在本文中,我解释了基本的 Git 概念并使用 Git 对一个简单目录的内容实施版本控制,并在此过程中对 Git 和 Subversion 进行了对比。我使用一个简单示例解释了分支功能。

在本系列第 2 部分中,我将研究合并、差异生成以及一些其他 Git 命令。我强烈建议您阅读简单易懂的 Git 手册,或者至少阅览一下教程。所有这些内容都可以从 Git 主页获得,因此请花些时间研究它。(下面的 参考资料 小节提供一些链接)。对于 SVN 用户而言,这些已经足够了。

另一方面,Git 是一种非常丰富的 DVCS;进一步了解其特性几乎肯定会简化并改善您的 VCS 工作流。并且,您甚至会梦到 Git 存储库。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Linux, Open source
ArticleID=422534
ArticleTitle=面向 Subversion 用户的 Git,第 1 部分: 入门指南
publish-date=08242009