陈颂光
全栈工程师,承接从编译器到网站的各类软件开发与咨询,也可以聊历史哲学。
关注我的 GitHub

Git版本控制系统概览

Git是近年比较流行的分布式版本管理系统。

基本概念

版本控制

版本控制就是记录一个或若干文件内容变化,以便将来查阅特定版本修订情况。最常见是对软件源代码作版本控制管理,但实际上可以对任何类型的文件进行版本控制。如果你是位图或网页设计师,可能会需要保存某一幅图片或页面布局文件的所有修订版本。

为了实现这效果,许多人习惯用复制整个项目目录的方式来保存不同的版本,或许还会改名加上备份时间以示区别,但手工流程既不方便又容易出。相比之下,采用版本控制系统(VCS)是个明智的选择。有了它你就可以:

  • 将某个文件回溯到之前的状态,甚至将整个项目都回退到过去某个时间点的状态,从而可以更放心地进行试验
  • 比较文件的变化细节,查出最后是谁修改了哪个地方,从而找出导致怪异问题出现的原因 当然这远不是现代版本控制系统的全部威力。早年的Unix工具SCCS和其GNU替代品RCS已经能做到这点。

现代的版本控制系统还要让在不同系统上的开发者协同工作。传统的想法自然是客户端-服务端架构,在单一的服务器保存所有文件的修订版本,而协同工作的人们都通过客户端连到这台服务器以取出最新的文件或者提交更新,这就是集中式版本控制系统,常用的有CVS及其后继者Subversion,还有商业的Perforce。这种架构简单但有单点故障和效率瓶颈问题。

相反,在分布式版本控制系统如Git、Mercurial和Bazaar中,每个客户端并不只提取最新版本的文件快照,而是把代码仓库完整地镜像下来。这样不仅提高了容错性和一些常用操作的效率,而且可以根据需要设定不同的协作流程。

Git

Linux 内核开源项目有着为数众广的参与者。绝大多数的 Linux 内核维护工作都花在了提交补丁和保存归档的繁琐事务上(1991-2002年间)。到 2002 年,整个项目组开始启用分布式版本控制系统 BitKeeper 来管理和维护代码。到了 2005 年,开发 BitKeeper 的商业公司同 Linux 内核开源社区的合作关系结束,他们收回了免费使用BitKeeper 的权力。这就迫使 Linux 开源社区(特别是 Linux 的缔造者 Linus Torvalds )不得不吸取教训,只有开发一套属于自己的版本控制系统才不至于重蹈覆辙。他们对新的系统制订了若干目标:

  • 速度
  • 简单的设计
  • 对非线性开发模式的强力支持(允许上千个并行开发的分支)
  • 完全分布式
  • 有能力高效管理类似 Linux 内核一样的超大规模项目(速度和数据量)

自诞生于 2005 年以来,Git 日臻成熟完善,在高度易用的同时,仍然保留着初期设定的目标。它的速度飞快,极其适合管理大项目,它还有着令人难以置信的非线性分支管理系统,可以应付各种复杂的项目开发需求。Git的特点包括:

  • 有效的压缩技术,节省存储空间
  • 近乎所有操作都是本地执行,从而速度飞快且不用连网
  • 使用SHA-1校验和而非文件名和时间戵作为数据的唯一标识和索引
  • 多数操作仅添加数据到数据库而不会删除数据,减低丢失数据的可能性

对于任何一个已跟踪的文件,在 Git 内为三种状态之一:

  • 已提交,即文件已经被保存在本地仓库中
  • 已暂存,即已经保存于暂存区(也称索引),在下次提交时会变成已提交
  • 已修改,即只保存于工作树中 其中前两者放在.git目录中。

基本的 Git 工作流程如下:

  1. 在工作目录中修改某些文件
  2. 对修改后的文件进行快照,然后保存到暂存区域
  3. 提交更新

安装和配置

Git的安装方法没有什么特别,例如在Windows下可从http://git-scm.com下载安装包;在GNU/Linux常见的发行版中可用包管理器,如基于Debian的发行版中只用:

apt-get install git

一般在新的系统上,我们都需要先配置下自己的 Git 工作环境。配置工作只需一次,以后升级时还会沿用现在的配置。当然,如果需要,你随时可以用相同的命令修改已有的配置。

第一个要配置的是你个人的用户名称和电子邮件地址。这两条配置很重要,每次 Git 提交时都会引用这两条信息,说明是谁提交了更新,所以会随更新内容一起被永久纳入历史记录。设置命令如:

git config --global user.name "John Doe"
git config --global user.email johndoe@example.com

接下来要设置的是默认使用的文本编辑器。Git 需要你输入一些额外消息的时候,会自动调用一个外部文本编辑器给你用。默认会使用操作系统指定的默认编辑器,一般可能会是 Nano 或者 Vim。如果你有其他偏好,比如 Emacs 的话,可以重新设置:

git config --global core.editor emacs

还有一个比较常用的是,在解决合并冲突时使用哪种差异分析工具。比如要改用 vimdiff 的话:

git config --global merge.tool vimdiff

Git 可以理解 kdiff3,tkdiff,meld,xxdiff,emerge,vimdiff,gvimdiff,ecmerge,和 opendiff 等合并工具的输出信息。当然,你也可以指定使用自己开发的工具。

如果用了 –global 选项,那么更改的配置文件只适用于当前用户,如果用 –system 选项则适用于系统中所有用户,都不加则只选用当前项目。

要检查已有的配置信息,可以使用 git config --list 命令。有时候会看到重复的变量名,那就说明它们来自不同的配置文件,不过最终 Git 实际采用的是最后一个。也可以直接查阅某个环境变量的设定,只要把特定的名字跟在后面即可,例如:git config user.name

基本使用

建立或获取仓库

想对一个项目开始使用Git进行版本管制,只用在项目的根目录运行:

git init

如果要从克隆现有仓库可以用下面的命令:

git clone  <repository> [<directory>]

这会在当前目录下创建一个目录作为工作目录。

文件操作

工作树中的文件要纳入版本控制可用命令:

git add 路径

它会使文件变成已暂存,未跟踪的话也变成已跟踪。要删除则可用git rm(同时从工作树和暂存区删除)。

想知道当前各文件的状态,可用git status或者GUI的git gui。利用git diff可比较暂存区中版本与工作树中的版本。

要把暂存区的文件提交,可用:

git commit

想知道提交,可用git log或者GUI的gitk

分支

分支用于表示一个发展方向。一种常见用法是维持一个主分支,然后每开发一个特性或补丁时创建一个特性分支,在分支中进行开发,直到足够稳定时才把成果合并回主分支。Git中的分支实际上为指向提交的指针,每当提交时当前分支就指向最新的提交。由于Git中分支操作代价很低,需要时尽管用。

git branch

可以列出所有分支,并标注当前分支。

要创建分支用命令git branch <分支名>,要删除分支则用命令git branch -d <分支名>

要切换当前分支,用git checkout <分支名>。要把一个分支的变化合并到当前分支,用git merge <分支名>

标签

有时分支在个别时间点(例如发布时)上的状态特别重要,于是我们会想给它一个名字以方便引用。这时可用git tag -a <标签名> [-m <说明>]

git tag可列出已有标签的名字。

远程仓库

如果通过git clone创建仓库,则应有远程仓库origin。如果要增加远程仓库,可用git remote add <远程仓库名字> <URL>

想列出当前的远程仓库,用git remote -v

要把远程仓库的重新拉取回来,可用git fetch [<远程仓库名字>],用git pull [<远程仓库名字>]进一步把更新合并到当前分支。

相反要把本地更新推给远程仓库,可用git pull [<远程仓库名字>] [<分支>]。不过,你需要有写权限。

主要命令概述

命令 用途
git add [<pathspec>...] 跟踪文件
git am [(<mbox> | <Maildir>)...] 解析邮件中补丁并应用到当前分支
git archive [--format=<fmt>] [-o <file> | --output=<file>] <tree-ish> [<path>...] 创建归档
git bisect 用二分查找定位导致bug的提交
git branch [-r | -a] [<pattern>...] 列出分支(-r表示远程,-a表示所有)
git branch [--set-upstream | --track | --no-track] [-l] [-f] <branchname> [<start-point>] 创建分支
git branch (--set-upstream-to=<upstream> | -u <upstream>) [<branchname>] 设置分支的上游
git branch --unset-upstream [<branchname>] 取消设置分支的上游
git branch -m [<oldbranch>] <newbranch> 重命名分支
git branch -d [-r] <branchname>... 删除分支(-r表示远程)
git branch --edit-description [<branchname>] 设置分支的描述
git bundle 通过归档移动对象和引用
git checkout <branch> 检出分支
git checkout <commit> 检出提交
git cherry-pick <commit>... 应用指定提交引入的变化
git citool git commit的图形版本
git clean 删除未跟踪的文件
git clone <repository> [<directory>] 克隆仓库到一个新目录
git commit [-a] [-F <file> | -m <msg>] 把变化记录到仓库
git describe [<commit-ish>...] 用它前面的最近标签形容提交
git diff [--] [<path>...] 比较工作树与暂存区
git diff --no-index [--] [<path>...] 比较文件系统中两个树
git diff --cached [<commit>] [--] [<path>...] 比较暂存区与一个提交(默认HEAD)
git diff <commit> [--] [<path>...] 比较工作树与一个提交或分支
git diff <commit> <commit> [--] [<path>...] 比较两个提交
git diff <commit>..<commit> [--] [<path>...] 比较两个提交,省略提交表示HEAD
git diff <commit>...<commit> [--] [<path>...] 比较第二个提交所在分支从两个提交共同祖先到第二个提交间变化,省略提交表示HEAD
git fetch [<repository> [<refspec>...]] 拉取另一仓库的数据
git format-patch [ <since> | <revision range> ] 把补丁格式化为邮件格式
git gc 清除不必要文件和优化仓库
git grep <pattern> [--and|--or|--not|(|)|-e <pattern>...] [ [--[no-]exclude-standard] [--cached | --no-index | --untracked] | <tree>...] [--] [<pathspec>...] 寻找匹配指定模式的行
git gui Git的图形界面
git init 创建空Git仓库或重新初始化现有的仓库
git log 显示提交日志
git merge [-m <msg>] [<commit>...] 合并分支
git mv <source> <destination> 移动或重命名文件
git mv <source> ... <destination directory> 移动文件
git notes 阅读或增删对象的附加注记
git pull [<repository> [<refspec>...]] 拉取另一仓库的数据并与之合并
git push [<repository> [<refspec>...]] 把数据推入到远程分支
git rebase [--exec <cmd>] [--onto <newbase>] [<upstream> [<branch>]] 把本地变化回放到上游
git rebase [--exec <cmd>] [--onto <newbase>] --root [<branch>] 把本地变化回放到主分支
git reset [-q] [<tree-ish>] [--] <paths>... 把指定条目复制到暂存区
git reset (--patch | -p) [<tree-ish>] [--] [<paths>...] 把指定条目选择性复制到暂存区
git reset [--soft | --mixed [-N] | --hard | --merge | --keep] [-q] [<commit>] 把HEAD设为指定提交
git revert [-m parent-number] <commit>... 回滾指定提交引入的变化
git rm [-r] [--cached] [--] <file> 从工作目录(若没有–cached)和索引移除文件,-r表示递归
git shortlog 列出精简的日志
git show <object>... 显示对象
git stash 记录当前工作树
git status [<pathspec>...] 显示工作树状态(HEAD与暂存区间变化,暂存区与工作树间变化)
git submodule 管理子模块
git tag [-a] [-f] [-m <msg> | -F <file>] <tagname> [<commit> | <object>] 增加标签
git tag -d <tagname>... 删除标签
git tag [-n[<num>]] -l [<pattern>...] 列出标签
git worktree 支持多工作树
gitk 图形化的Git仓库浏览器

其中各种对象可用以下方式表示:

  • SHA-1值的十六进制字符串表示的前面若干个字符(不必全部40个,只要不引起歧义)
  • git describe的输出
  • 符号引用名字<refname>,它会依次查找: 1. $GIT_DIR/<refname>(如HEADFETCH_HEADORIG_HEADMERGE_HEADCHERRY_PICK_HEAD); 2. refs/<refname> 3. refs/tags/<refname> 4. refs/heads/<refname> 5. refs/remotes/<refname> 6. refs/remotes/<refname>/HEAD
  • @相当于HEAD
  • <refname>@{<date>}表示提交<refname>之前某时间的提交,其中<date>形如yesterday1 month 2 weeks 3 days 1 hour 1 second ago1979-02-26 18:30:00
  • <refname>@{<n>},表示提交<refname>之前每n个提交
  • @{<n>}相当于<当前分支>@{<date>}
  • @{-<n>}表示此前检出的第n个提交
  • <branchname>@{upstream}表示指定分支或当前分支的上游
  • <branchname>@{push}表示指定分支或当前分支的默认推入的远程分支
  • <rev>^表示提交<rev>的第一个前驱
  • <rev>^{n}表示提交<rev>的第n个前驱
  • <rev>~<n>表示提交<rev>的第n代祖先,每代都选首个前驱
  • <rev>^{<type>}表示把<rev>看作指定类型(否则不断解引用),如commit、tree、object或tag
  • <rev>^{}表示标签<rev>
  • <rev>^{/<text>}表示<rev>的祖先中最近提交信息有子串匹配正则表达式<text>的提交
  • :/<text>表示最近提交信息有子串匹配正则表达式<text>的提交
  • <rev>:<path>表示指定提交中的blob对象或树对象
  • [:<n>]:<path>表示暂存区中的blob对象,其中在合并时n为1、2、3分别表示共同祖先、目标分支版本、被合并分支版本

而为指定提交的范围,可用以下之一:

  • <rev>表示<rev>的祖先
  • ^<rev>表示非<rev>的祖先
  • <rev1>..<rev2>表示<rev2>的祖先但不是<rev1>的祖先,省略提交则视为HEAD
  • <rev1>...<rev2>表示<rev1><rev2>的祖先但不是它们的共同祖先,省略提交则视为HEAD
  • <rev>^@表示<rev>的祖先但不包括<rev>
  • <rev>^!表示提交<rev>本身

其中仓库通常用URL表示,形如以下之一:

  • ssh://[user@]host.xz[:port]/path/to/repo.git/
  • git://host.xz[:port]/path/to/repo.git/
  • http[s]://host.xz[:port]/path/to/repo.git/
  • ftp[s]://host.xz[:port]/path/to/repo.git/
  • rsync://host.xz/path/to/repo.git/
  • file:///path/to/repo.git/,通常也可省略file://

帮助

如有疑问,请用git help命令查看文档。

关键词 git 版本控制