Git 101

来源:互联网 发布:网络表妹啥意思 编辑:程序博客网 时间:2024/05/22 05:28

Git 101




原文:http://blog.dayanjia.com/2014/03/git-101/             

按:这篇文章原本是写了用作公司内部分享的,现在重新整理了部分内容(好歹写了那么多字呢),放在这里以飨各位:)

欢迎来到 Git 101。学习 Git 的第一条规则就是,你不能谈及自己在学习 Git1。因为这个工具由 Linus Torvalds 主导的 Linux 内核社区设计开发,并且由最酷最炫的 GitHub 发扬光大,从头到脚围绕着高大上的光环,全世界聪明的程序员都会用(至少声称自己会用)它来管理自己的代码。如果你在跟人聊天时不经意透露了自己「正在学习使用 Git」,无疑所有人都会投来高贵冷艳充满优越感的目光,并且告诉你:「连我们的设计师都会用 Git 管理他的 PSD 了,你竟然才在学怎么用?」所以,永远不要谈及自己在学习它——是的,你已经熟练地掌握了使用 Git 的所有要领。

学习 Git 的第二条规则是,你不能觉得它概念难懂,命令太多。因为全世界聪明的程序员都在公共场合大谈自己是如何在行驶的飞机上单枪匹马搞定分支合并的冲突,并且一下飞机就风驰电掣地连上机场 Wi-Fi,成功 push 了代码,发起了 pull request。所以,你一定要在心里默念,我是全世界聪明的程序员之一,Git 这点东西显然不在我的话下,少焉,流淌于键盘之上,玩转于股掌之间2

目录

  • Git vs. Subversion —— 解决问题的不同思路
    • 分布式版本控制系统
    • 「真正」的分支
    • 保持数据完整性
    • 文件的三种状态
  • 安装 Git
  • 基础操作
    • 新建仓库
    • 增加文件和提交
    • 修改文件和查看 diff
    • 其他常用命令
  • 分支的魔法
    • 分支和提交引用
    • 新建和切换分支
    • 合并(merge)
    • 衍合(rebase)
      • 交互式 rebase
      • 何时可以使用和不能使用 rebase
  • 远程协作
    • 连接到远程分支
      • 访问基于 SSH 协议的远程仓库
      • 推送(Push)和拉取(Pull)
    • GitHub 式的 Fork/Pull Request 工作流
    • Git Flow 工作流
  • 其他资源和技巧
    • GUI 工具
    • GitHub Pages 和 jekyll
    • 解决 Windows 和 Linux 不同换行符的问题
    • 忽略文件
    • alias:定义命令别名
    • 更好看的 log
    • stash:保存/恢复你的工作状态
    • submodule:子模块
    • Legit:「人文主义」版的 Git
    • 其他

Git vs. Subversion —— 解决问题的不同思路

版本控制系统经过了类似 CVS、ClearCase、Perforce 这类无名利剑的时代后,进入了 Subversion 的紫薇软剑纪元,然而在面对以 Git 为代表的玄铁重剑时,前者往往显得力不从心了。使用 Git 最重要的是要掌握并且领会它的思想,而不是拘泥于工具本身,这样才能达到草木竹石皆可为剑3的境界 :D

分布式版本控制系统

我们熟悉的 Subversion,是一种集中式的版本控制系统。它有一个中央服务器,保存着所有的版本历史,你每次修改代码时,都会从这个中央服务器签出(checkout)一个版本,作为自己的工作目录,修改完成后再向服务器提交(commit)改动。这种十分依靠中央服务器的做法,是一个非常自然的思路。

但是 Git 与之不同,它作为一个分布式的版本控制系统,客户端每次克隆(clone)一个版本库时,不光复制了最新版本的文件内容,也把整个仓库的历史记录镜像了一份。这意味着每个人本地都有一份带有完整历史的备份。正因为如此,使用 Git 进行协作同 SVN 相比,有很大的不同。SVN 中,很多操作都依赖网络中的服务器,但是 Git 中的绝大部分操作(查看历史、提交代码等)都是在本地完成的。

「真正」的分支

在 SVN 常见的 Workflow 中,你总会见到三个目录:trunk、branches 和 tags,其中前两者代表的含义便是主干和分支。可惜的是,SVN 的分支其实只是一份完整的文件拷贝,它和主干中的代码共享一条线性的版本号。而分支的创建和合并,其本质只是在不同目录中相互复制文件,然后在目录的 SVN 属性里做一些特殊的标记。

Git 给我们带来了「真正」的分支体验,你完全可以将其简单地想象成树木的主干和分支。Git 的分支功能可谓是它的「杀手级特性」,它很轻量,却非常强大。在本地创建分支、切换分支都是瞬间完成,合并(merge)和衍合(rebase)功能能给团队协作带来非常大的灵活性,此外和远程分支的交互(push 和 pull)也十分清晰明了。

保持数据完整性

SVN 在记录版本更新时,采用的是一种「增量式」的方法,即只记录和前一版本中不同的信息。而 Git 则完全不理会版本间的差异,它会通过哈希来检测文件是否被修改,如果被修改就会直接将更新后的整个文件存储在版本库中。这使得 Git 更像是个小型的文件系统,能够很好地保持数据完整性。

同时,在 Git 中也不存在类似 SVN 的递增数字版本号。取而代之的是,它会使用 SHA-1 计算出每个版本哈希值,并且将其作为这个版本的唯一代号。

文件的三种状态

SVN 中,你的文件只有两种状态——未提交和已提交,分别对应于工作目录中被修改的文件,和已经 commit 到服务器后的文件。而在 Git 中,一个文件拥有三个状态:已提交(committed),已修改(modified)和已暂存(staged)。前两者和 SVN 中的对应关系是很明确的,而「已暂存」表示把已修改的文件放在下次提交时要保存的清单中。这个状态,看似多余,实则为提交操作提供了一个 Review 的机制,同时还可以支持文件分块提交等灵活的功能。

因此,使用 Git 进行提交的基本流程便是:

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

安装 Git

在 Git 的官方网站,你可以下载到 Windows 版本的安装程序,它会附带安装一个 MinGW MSYS 的环境,使用它我们可以在 Windows 中运行一些 UNIX 工具链。安装过程中,会有选项让用户选择是否将 Git 加入到环境变量中,以及如何处理换行符的问题。

而在 Unix-like 系统上安装 Git 就显得很简单了。大部分 Linux 的发行版和 Mac OS X 要么已经自带了 Git,要么可以方便地使用包管理工具来安装:

$ brew install git

安装完成之后,我们需要对其进行初始配置。Git 的配置文件一共有三个,它会按照系统级($(prefix)/etc/gitconfig)→用户级(~/.gitconfig)→项目级(.git/config)的顺序依次读取,后面的配置文件会覆盖前面的相同配置。在使用之前,我们需要告诉 Git 自己的名字和邮箱,这两个信息会附带在每一次提交日志中。

$ git config --global user.name "Paul McCartney"$ git config --global user.email whoknows@example.com

上面的命令中,我们使用了 --global 参数,代表这修改的是用户级的配置文件。

基础操作

新建仓库

新建仓库可以是一个本地的空仓库,也可以从远程克隆一个现有的仓库。新建本地空仓库的方法十分简单:

$ mkdir my-project$ cd my-project$ git init

从远程克隆仓库的话,你需要知道远程仓库的地址,常见的 URL 有三种类型:

  1. 基于 HTTP/HTTPS 协议的远程仓库,其地址就是一个普通的网址 URL
  2. 基于 SSH 协议的远程仓库,其地址是 user@server:/path.git 这样类似使用 scp 命令时的写法
  3. 基于 Git 协议的远程仓库,其地址类似 git://server/path.git 这样的形态

无论是哪种远程仓库,只需要使用 clone 命令就可以将其克隆到本地:

$ git clone https://github.com/joyent/node.git

增加文件和提交

这部分的操作非常简单,也很类似于 SVN。唯一不同的是,由于 Git 拥有三种文件状态,所以你需要先将文件添加进暂存区,再做提交。看似是一个多余的操作,其实作用是很大的,它不光让你让你很方便地查看即将提交的修改的状态,还可以支持同一个文件按区块进行暂存。假设你修改了一个文件的三行,你可以先将前两行的变更加入暂存区,提交时就只会提交它的前两行,而第三行的变动依然保留在工作目录中。使用 git status 命令,可以清楚地查看每个文件所处的状态。

➜  my-project git:(master) git statusOn branch masterInitial commitnothing to commit (create/copy files and use "git add" to track)➜  my-project git:(master) touch README➜  my-project git:(master) ✗ git statusOn branch masterInitial commitUntracked files:  (use "git add <file>..." to include in what will be committed)    READMEnothing added to commit but untracked files present (use "git add" to track)➜  my-project git:(master) ✗ git add README➜  my-project git:(master) ✗ git statusOn branch masterInitial commitChanges to be committed:  (use "git rm --cached <file>..." to unstage)    new file:   README➜  my-project git:(master) ✗ git commit -m 'init commit'[master (root-commit) cba69a5] init commit 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 README

修改文件和查看 diff

修改文件以后,我们需要再次使用 git add 将其添加到暂存区。添加之前,使用 git diff 可以检查修改内容。这个命令默认是比较工作目录和暂存区之间的差异,如果要比较暂存区和版本库中上次提交的差异,则需要加上一个参数,使用 git diff --cached。Git 默认会使用 less 来显示 diff 信息以方便查看,如果想临时禁用它,可以加上 --no-pager 参数。

➜  my-project git:(master) echo "line one\n" >> README➜  my-project git:(master) ✗ git --no-pager diffdiff --git a/README b/READMEindex e69de29..a7b3537 100644--- a/README+++ b/README@@ -0,0 +1,2 @@+line one+➜  my-project git:(master) ✗ git add README➜  my-project git:(master) ✗ echo "new line" >> README➜  my-project git:(master) ✗ git --no-pager diffdiff --git a/README b/READMEindex a7b3537..feeed59 100644--- a/README+++ b/README@@ -1,2 +1,3 @@ line one+new line➜  my-project git:(master) ✗ git --no-pager diff --cacheddiff --git a/README b/READMEindex e69de29..a7b3537 100644--- a/README+++ b/README@@ -0,0 +1,2 @@+line one+

其他常用命令

我们已经看了最最基础的 initcloneaddstatuscommitdiff 这几个命令,下面还有几个常用命令,其中有些是独立的子命令,有些则是一些常用的参数。

  • git log:查看版本库历史
  • git rm:删除文件
  • git mv:移动文件
  • git commit -a:将所有已跟踪的文件(不是新建的文件)一起加入暂存区并提交,相当于一个快捷方式
  • git commit --amend:撤销并重做上一次提交,如果你发现写错了提交注释,或者漏掉了某个文件,可以使用该命令来修正上一次错误的操作。因为 Git 的版本库是完全本地的,所以你可以轻易地修改提交历史(但是在和远程服务器协作时,要尽量避免修改已经 push 的历史)。
  • git reset HEAD <file>:将暂存区的文件撤回,这条命令的意思是将暂存区中的 <file> 文件重置为版本库中的最新版本(HEAD),因此该文件会从暂存区撤回,但是修改依然保留。
  • git checkout -- <file>:撤销工作目录中对 <file> 文件的修改,这回将所有未暂存的修改抹去,恢复成版本库中的最新版本。这个 checkout 命令也可以用来切换当前工作目录到历史版本中的任意位置,它和 SVN 中的 checkout 没有任何关系,事实上 git checkout 大致相当于 svn update + svn switch 的组合。

下图是 Git 中三种文件状态相互转换的命令示意图。

分支的魔法

Git 的分支是如此强大,以至于要单独拉出来一章来讲讲。前面已经说过,Git 的分支跟 SVN 的是完全不一样的产物,它的实现更加轻量级,但是功能却更强大。而且,这一切都是发生在本地的,无需和服务器进行交互。

分支和提交引用

当你创建了一个 Git 版本库后,它就有了一个默认的分支,名叫 master。每一次提交(Commit),就相当于在这个分支上新增了一个线性的节点,每个节点都有一个 SHA-1 的哈希值,你可以通过这个哈希值来引用到任意一个版本。一个完整的 SHA-1 可以通过一个长度为40的字符串来表示,但是我们一般用不到这么多,往往只需要最开始的五六位就可以区分所有的提交了。

每一次提交的信息中,都会包含它的父节点的名字,那么我们如何知道一个分支里最新的那个提交呢?事实上,Git 会保存很多引用名称,例如分支的名称就是对该分支内最新一次提交的引用名称。这些引用可以在 .git/refs/ 目录中看到。当我们指某个分支时,其实对应的是这个分支的头部的那个提交。通过 git show-ref 命令,可以看到各个引用对应的实际提交名称。

➜  my-project git:(master) ✗ git show-ref       2e5c5f07a14b1a3a4a464a9ac95cad5caecd8e0f refs/heads/master

可以看到,master 的完整引用叫做 refs/heads/master,但是我们一般可以省去前两部分。当一条分支的头部(最新提交)确定了以后,便可以从每次提交信息中回溯构建出整个版本历史信息。

同时,Git 中还有一个 HEAD 指针的概念,一般来说,HEAD 指针指向的是当前分支的最新版本。使用下面介绍的 git checkout 命令实际上就是改变 HEAD 的指向,例如在切换到 master 分支时,HEAD 就会指向 refs/heads/master。甚至,你还可以将 HEAD 定位到版本历史中的任何一个位置上,进入所谓的「身首分离」状态(detached HEAD),听上去优点恐怖?其实还好啦,下面会说到这个问题。

可以看到,Git 中的分支是非常廉价的,因为只需要新建一个40字符长的 SHA-1,就相当于多了一个分支,分支的祖先关系也包含在每一次提交记录中了。而具体的文件,因为每次提交 Git 会将变化的文件整体存储下来,所以哪个版本中有哪个文件,也可以维护得很好。

理解这些关于分支的概念对于使用 Git 进行多人协作是非常重要的。

新建和切换分支

使用 git branch 和 git checkout 命令,我们可以新建和切换分支。

不加任何参数执行 git branch 会列出当前所有分支。使用 git branch <branch> 则会创建一个新的名为 <branch> 的分支(但不会切换到它)。如果想要删除一个分支,可以使用 git branch -d <branch>,它会首先检查该分支是否还有未被合并的提交,如果你想强制删除它,将选项换成大写的 -D 即可。

git checkout 命令可以用来切换工作目录到任意一个版本。如果后面跟的是分支名,就会切换到该分支的最新提交。

前面说到,使用 git branch 新建分支后,并不会自动切换过去,需要再手动 checkout 一下,其实这两步可以合并为一步来完成:

git checkout -b <new-branch>

在指定位置的时候,除了分支名,还可以使用一些特殊的表达方式,主要是 ~ 和 ^的使用:

  • git checkout HEAD~:表示切换到 HEAD 的父节点,HEAD~ 是 HEAD~1 的简写
  • git checkout HEAD~2:表示切换到 HEAD 的父节点的父节点,即爷爷节点,后面的数字可以以此类推
  • git checkout HEAD^:也表示切换到 HEAD 的父节点,HEAD^ 是 HEAD~^ 的简写
  • git checkout HEAD^^:也表示切换到 HEAD 的父节点的父节点
  • git checkout HEAD^2:这就完全不同了,它表示切换到 HEAD 的第二个父节点。在一次合并操作中,一个提交可能会有超过两个父节点

还有一种更直接的方法,便是写相应提交的 SHA-1 名称。这样工作目录就会切换到某个历史版本,往往会处于「身首分离」的状态。你可以用这种方法,很轻松地回到某个历史版本,查看以前版本的状态。

但是,当你在一个分离的 HEAD 上进行提交时,它不会覆盖掉原先后面的提交,而是会产生一个新的。但是这个新的提交没有任何名字,除非你记住它的 SHA-1,否则切换到其他地方以后就没有办法再回来了。这时的一个好方法就是立即在当前位置上新建一个分支,以结束这种身首分离的状态,今后便可以使用分支名称方便地访问到这个提交了。

当 git checkout 指定了文件名时,Git 会从指定的提交中复制该文件覆盖到当前的工作目录中。这也就解释了前文中为什么可以使用这个命令撤销对文件的修改了。

合并(merge)

当你在两个分支中进行了不同的修改后,无法避免的操作就是合并了。合并的两个分支,可以是两个本地分支,也可以本地分支和下文会提到的远程分支。

最简单的一种合并叫做「快速前进」(Fast forward)。当待合并的两个分支 A 和 B 在同一条历史记录线上,但 B 比 A 更新(即 A 是 B 的一个祖先节点),于是将 B 合并到 A 的操作就是所谓的「快速前进」。这种操作往往发生在,别人在我的代码基础上做了修改,现在我要将别人的修改合并进自己的分支中时。

当两条分支是真正地来自于同一个祖先节点,但是之间不是线性关系时,会进行一次真正的合并。这样的合并会自动产生一条新的提交,它的提交信息中会记录两个父节点引用。

Git 的合并命令就是在准备合并的分支下执行 git merge <other_branch> 命令。但合并并不总是如此顺利,冲突在所难免,当自动合并失败时,Git 会显示出每个有冲突的文件名,并将有冲突的位置在文件中打上标准标记。这时,你可以使用各种合并工具来处理这些有冲突的地方,或者直接用文本编辑器打开冲突的文件,手动修改形如下方的冲突块:

<<<<<<< HEADoriginal line=======new line>>>>>>> other

当冲突解决完毕时,保存文件,并且使用 git add 命令将其添加进暂存区,就相当于告诉 Git「我已经手动处理好这个文件的冲突了」。解决完所有冲突后,再使用 git commit 命令,创建一条新的合并提交。

Git 中有一条交互式的命令 git mergetool 会一步步地引导你解决冲突,它会启动系统上的一个合并工具(例如 vimdiff)来辅助你,并在你保存完文件后提示是否解决完成。

衍合(rebase)

除了正常的合并之外,Git 还支持另一种合并分支的方法,叫做衍合(rebase)。这个翻译可能听上去很奇怪,不如直接叫它 rebase 好了(或者你愿意把它直译成「变基」的话 =.=!)。正常的合并操作进行完成后,如果你查看版本历史,会发现在合并之前有一段两条时间线并行的状态,这两条时间线最终会汇集到同一个祖先上。这是一个很自然的关于分支和合并的体现。但是 rebase 不同,使用它合并完完代码后,时间线并不是两条,而是会合成单条:首先从共同祖先开始,然后是对方的所有提交,再然后是自己的所有提交,一直到最新版本。

例如,我们正在 topic 分支中,现在想要合并 master 中的修改,执行 git rebase master 后,Git 会寻找到两个分支的共同祖先,从该祖先到 topic 的最新版本之间的每一次提交都会在 master 的最新提交基础之上「重放」一遍,直到全部完成。

当然,rebase 的时候也会产生冲突。当发生冲突时,rebase 过程会暂停,你需要手动解决冲突,然后使用 git add 标记解决,再执行 git rebase --continue 以继续之后的「重放」。你也可以使用 git rebase --abort 来终止当前的 rebase 过程,一切都会回到原来的状态。

需要注意的是,rebase 中「重放」过的提交记录,虽然修改的文件内容看上去一模一样,但是在 Git 内部已经是完全不同的两个提交了,显然也会拥有完全不同的 SHA-1 名称,而原先的那些提交会被标记为已废弃。

交互式 rebase

rebase 是 Git 中一项比较高级的功能,使用它可以达到天马行空的效果,甚至任意修改提交历史也不费吹灰之力。rebase 提供了一个 --interactive(或 -i)选项,它可以提供一个交互式的 rebase 环境,在这里简单介绍一下,详细使用可以参考手册。

假设你需要修改最近的5次提交记录中的某些对象,首先进入到交互模式:

$ git rebase --interactive HEAD~5

Git 会为你打开一个编辑器(Vim),提供了一些选项:

pick 1fc6c95 Patch Apick 6b2481b Patch Bpick dd1475d something I want to splitpick c619268 A fix for Patch Bpick fa39187 something to add to patch Apick 4ca2acc i cant' typ goodspick 7b36971 something to move before patch B# Rebase 41a72e6..7b36971 onto 41a72e6## Commands:#  p, pick = use commit#  r, reword = use commit, but edit the commit message#  e, edit = use commit, but stop for amending#  s, squash = use commit, but meld into previous commit#  f, fixup = like "squash", but discard this commit's log message#  x, exec = run command (the rest of the line) using shell## If you remove a line here THAT COMMIT WILL BE LOST.# However, if you remove everything, the rebase will be aborted.#

在上面列出的提交日志中,可以针对每一个提交进行不同的操作。例如,默认的 pick 指的就是一般的 rebase 流程,使用这条提交;而 edit 则允许你在 rebase 的过程中暂停,对代码进行修改后使用 git commit --amend 变更提交的内容;squash 的含义是合并两次提交的内容,最终只产生一条记录,它可以让提交历史看上去更加简略。

可以看到,rebase 的功能之强大,可以让用户随意增加、修改和删除历史,这一切都得益于 Git 的分布式特性,在使用中央服务器的 SVN 中,这些功能都是无法想象的。

何时可以使用和不能使用 rebase

可以看到,merge 和 rebase 对分支的合并,最终都可以打到相似的结果,那么在什么场景下,可以考虑使用 rebase 呢?

  • 觉得 merge 产生的提交日志过于混乱,想要 rebase 成一条线性的日志
  • 一次性合并含有大量冲突的代码,使用 merge 可能会让人崩溃,而使用 rebase 可以一点一点地解决每个提交带来的冲突

但是,当分支中的提交被上传到远程公共仓库中去后,就不应该再对其使用 rebase 了。上面提到过,rebase 会丢弃原先的提交信息,产生新的,rebase 过后的代码会和公共仓库中的出现时间线上的冲突。当有其他人已经从公共仓库中合并过代码后,你又把 rebase 后的提交强制上传到远程,最终会带来协作上的噩梦。

远程协作

既然是写代码,就免不了与人合作。Git 最初是用作 Linux kernel 项目的代码仓库,而该项目的开发人员来自全世界,因此 Git 必然对远程协作的支持非常完备。

连接到远程分支

由于 Git 的分布式特性,每个工作拷贝都留有完整的代码仓库提交历史,所以各个机器上的代码都是相互平等的关系。因此,任何机器之间都可以进行相互连接,相互访问各自分支中的代码。

当使用 git clone 命令创建本地版本库时,其实 Git 已经自动添加好了一个远程服务器地址,并取名为 origin。使用 git remote 命令可以用来管理远程仓库。

  • git remote:不带任何参数运行会列出当前已有的远程仓库信息
  • git remote add [shortname] [url]:添加一个远程仓库地址,每个远程仓库可以拥有一个简短的名字
  • git remote rename [old-name] [new-name]:重命名远程仓库
  • git remote rm [remote-name]:删除一个远程仓库的信息

访问基于 SSH 协议的远程仓库

前文说过,常见的远程仓库 URL 有 HTTP(S)、SSH、Git 三种协议,其中 HTTP(S) 和 Git 协议都需要运行额外的 Daemon,所以基于 SSH 协议的远程仓库就显得十分方便,毕竟几乎所有机器都会跑着 sshd。

默认情况下,通过 SSH 访问远程机器需要输入用户名和密码鉴权,使用 Git 时就会频繁地输入这些内容,不胜其烦。我们可以将自己的 SSH 公钥上传至远程机器中,省去了输入密码的过程。这些步骤和通用的 SSH 密钥登录流程一样,在这里简单介绍一下。

首先要在本地机器上生成一个密钥对(如果已经生成可以跳过这一步)。

$ ssh-keygen -t rsa

根据提示一步步进行,最后会生成两个文件,默认位置在 ~/.ssh/id_rsa 和 ~/.ssh/id_rsa.pub

接着你需要登录到远程机器,将 id_rsa.pub 文件的内容附加到远程机器的 ~/.ssh/authorized_keys 文件中,并确保这个文件的权限为600。OpenSSH 中提供了一个方便的脚本 ssh-copy-id 来自动完成这项操作:

$ brew install ssh-copy-id$ ssh-copy-id username@remote-host

推送(Push)和拉取(Pull)

本地仓库和远程仓库之间最普遍的操作便是 Push 和 Pull,对应上传代码和下载合并代码。

Push 操作意味着你会将本地分支的代码上传到远程的一个分支中:

$ git push <remote-name> <remote-branch>

第一次 Push 时,可以加上 -u (--set-upstream) 参数,来将本地的这个分支和远程的那个分支建立对应关系。今后只需要简单地 git push 就可以上传代码了。

当你 Push 过一次代码后,又修改了已经 Push 过的部分的版本历史,例如使用了 git commit --amend 或者 rebase 操作后,再次 Push,由于版本历史不同,远程机器会拒绝这次操作。当然,你可以加上 --force 参数来强行覆盖掉远程机器的分支内容,但是这样非常不推荐做——因为在你两次 Push 之间,可能已经有人 Pull 了代码,这会产生十分糟糕的冲突情况。如果你万不得已想要强制 Push 一次代码,请确保没有人已经下载了会被覆盖掉的版本历史。

而 Pull 操作其实是两个操作的合集:fetch 和 merge。git fetch 命令用来下载远程分支里的内容,而接下来的 merge 命令才是对代码进行合并。也就是说,git pull origin master 其实相当于下面两句命令:

$ git fetch origin$ git merge origin/master

可以看出,当下载了远程机器的版本信息后,你就可以像管理本地分支一样管理这些远程分支(但是它们是只读的)。使用 git branch -r 命令可以查看当前下载回来的远程分支信息。你可以从远程分支上直接新建一个新的本地分支,然后进行开发。

同时我们再次体会了 Git 与 SVN 的不同之处。SVN 中同步远程代码只需要 svn update 一下即可,但是在 Git 中会被分为两个步骤。最后,如果你想 Pull 的时候使用 rebase 而不是普通的 merge,可以加上 --rebase 参数。

GitHub 式的 Fork/Pull Request 工作流

GitHub 作为一个提供公开代码仓库的服务(他们也提供付费的私有仓库),每个用户都可以在上面建立多个代码仓库,用作不同项目的远程仓库。这种略显松散的模式使得它提供的 Fork/Pull Request 式的工作流很适合开源项目的协作。

首先,张三建立了一个项目 A 的公开仓库,并将自己的代码 Push 到远程。这个公开仓库只有张三一个人拥有 Push 的写权限,其他人只能读取,我们姑且称它为「张三的 A」。这时,李四参与到了这个项目中,他需要使用 GitHub 提供的 Fork 功能创建一个只有自己才有写权限的公开仓库——Fork 操作其实就是复制了一整个仓库的内容。李四名下也出现了一个项目 A 的远程仓库(「李四的 A」),他可以将其克隆(clone)到本地,进行开发,并且 Push 回李四的 A。

此时,李四想将自己的修改并入张三的代码库中。他需要在 GitHub 上发起一个 Pull Request,指定李四的 A 中的分支名和张三的 A 中的分支名。GitHub 会自动将这两个远程分支之间的 diff 创建一个 Pull Request,以供张三进行代码评审(Code Review)。当张三评审完毕后,只需要通过该 Pull Request,李四的 A 中相应分支的代码就会被合并进张三的 A 中。张三需要在自己的机器上执行一次 git pull 即可将这次进行在服务器中的代码提交同步到本地。

可以看出,在这些服务器上的项目 A 中,张三的 A 其实是最核心且「官方」的一个,即其他人在自己 Fork 的项目中做出的改动都需要合入张三的 A 中。李四等其他人需要定期同步张三的 A 中的最新代码,因此在它们的本地仓库中,往往需要配置两个或更多的远程仓库地址,其中一个是自己 Fork 的(origin),另一个则是「官方最新版」(upstream)的:

$ git remote add upstream <张三的远程仓库 URL>$ git pull upstream master

Git Flow 工作流

前文说过,Git 的分支功能非常强大,那么如何有效地利用分支进行多人协作,便成为一个课题。Vincent Driessen 提出了一套分支模型,总结为最佳实践,叫做 Git Flow。

上图基本将 Git Flow 的大致思想表达清楚了。Git Flow 和前面介绍的 GitHub 的协作方式并不冲突,因为这只是一些如何建立和管理分支的指导。在 Git Flow 中,有两条主要分支:

  • master: 永远处于可以直接上线的状态
  • develop: 最新的开发状态,包含下一次上线的更新

此外还有一些辅助分支:

  • 新功能分支:开发新功能时,从 develop 分支出来,开发完后再合并回 develop
  • 发布分支:准备要发布的版本,只修 Bug。从 develop 分支出来,完成后合并回 master 和 develop
  • Hotfixes 分支:紧急修复 master 分支中的问题,从 master 分支出来,完成后合并回 master 和 develop 分支

除此之外,作者还提供了辅助实践 Git Flow 工作流的 Git 扩展命令。

其他资源和技巧

GUI 工具

Git 本身是基于命令行的,但是在进行查看版本历史、选择文件块进行暂存等操作时,GUI 的优势非常明显。Git 本身自带了一个比较简陋的 GUI 客户端,可以说聊胜于无。当然,还有很多优秀的第三方客户端,Git 官网列出了一些,这里介绍几个我用过的。

GitHub 客户端

和 Github 整合得非常好,界面友好,基本功能齐全。

  • GitHub for Windows
  • GitHub for Mac

SourceTree

SourceTree 原来是一个收费的客户端,后来被 Atlassian(大名鼎鼎的 JIRA 的开发商)收购后,改为免费软件。功能很全面,同时支持 Git 和 Mercurial 两种版本控制系统。该软件同时在 Windows 和 Mac 上提供。

TortoiseGit

熟悉 SVN 的同学一定知道 TortoiseSVN 这个乌龟客户端,和 Windows 系统的 Shell 整合的特别好。它也有一个 Git 的 Port。其实我觉得不怎么好用,不过习惯的力量是很强大的,乌龟系的软件可以让 SVN 的人迅速上手,

GitHub Pages 和 jekyll

GitHub 除了是一个在线代码仓库之外,还提供了一项非常有意思的功能——托管静态网站,这便是 GitHub Pages。这项功能可以将你的代码仓库中的静态文件组建一个网站,很多用户将自己的主页和开源项目的官网直接建立在 GitHub Pages 上。

GitHub Pages 只支持静态网站,因此 PHP 什么的还是不要想了。它有两种类型:用户网站和项目网站。

建立用户网站,你只需要使用自己的用户名新建一个名为 username.github.io 的仓库,然后便可以向其中添加 HTML 文件和其他资源。每一次 Push 之后,你都可以通过和仓库名相同的域名访问到自己的网站。

第二种项目网站,其访问 URL 是形如 http://username.github.io/repository* 的模式。你需要到 repository* 仓库中新建一个分支,名称固定为 gh-pages。接下来的工作便是向该分支提交上传网页文件了。

事实上,GitHub Pages 并不是简单地将你的静态文件设为一个网站。它在服务器上会运行一个静态网站生成器做一次 Build 的操作,这个静态网站生成器叫做 Jekyll。这个工具用 Ruby 写成,支持用 Markdown 语法写文章,使用 Liquid 模板语言构建网页,可以创建博客风格的静态网站。详细的介绍可以参考 Jekyll 的文档。

解决 Windows 和 Linux 不同换行符的问题

众所周知,Windows 和 Linux/Mac 的文本文件换行符是不一样的,但是在版本仓库中如果混合了两种不同的换行符,就简直是灾难了。一个人明明只编辑了一行,但是因为换行符变了,导致整个 diff 都无法阅读了。

要解决这个问题,就要确保无论提交者使用的是什么操作系统,提交到版本库中的文件要保持统一的换行符。一般来说,保存到 Git 版本库中的代码要统一换行符为 LF,而非 Windows 中的 CRLF。Git 提供了一个设置 core.autocrlf 来处理这样的问题。在 Windows 下,你可以将它设置为 true

$ git config --global core.autocrlf true

这表示 Git 会自动将文件转换换行符为 LF 后再提交,但是不会修改工作目录中的文件本身。而在 Linux/Mac 下,这个选项可以设置为 input

$ git config --global core.autocrlf input

这表示文件的换行符在提交和检出时不会被转换。

这样的设置看似很美好,但是实际上很多 Windows 中的代码编辑器和 IDE 都允许用户设置换行符。为了保持多人协作时的统一性,很多人会把它直接设为 LF,此时这个选项就会出现问题了。于是有一个更好的解决方案被提出,它能够这对某个版本库中的某些文件单独设置它们应当保持的换行符,配置起来更加灵活。这就要引入 .gitattributes 文件了。举个例子,当我们想让版本库中的所有 PHP 文件、JS 文件、CSS 文件都保持为 LF 的换行符,只需要在 .gitattributes 文件中配置如下内容:

# 先设置默认行为* text=auto# 给不同文件单独设置*.php text eol=lf*.js text eol=lf*.css text eol=lf

这样,无论用户本身 Git 的设置如何,在这个版本库中,这些文件都会按照统一的规则保存换行符。至于 .gitattributes 文件,它还可以设置许多其他属性,详细内容可以参考手册。

忽略文件

我们的代码库中往往会有一些不愿意被纳入版本控制的文件,例如编译的中间文件、一些特定的配置文件等。在版本库中新建一个 .gitignore 文件,其中写上想要忽略的文件路径,就可以命令 Git 对其视而不见了。在 Windows 下,直接在资源管理器里是没有办法直接新建一个以句点开头的文件的,你可以使用一些文本编辑器来建立它。详细的使用方法,请参考手册。

alias:定义命令别名

熟悉 SVN 的同学一定知道很多命令都可以简写,比如 svn status 和 svn st 的功能是一样的。但是 Git 本身并没有这个功能,这会让有些懒人感觉很麻烦。其实多打两个字母也没什么啦,不过实在觉得麻烦的可以使用 alias 命令定义一写别名:

git config --global alias.ci commitgit config --global alias.st status

更好看的 log

git log 的默认输出十分简单。其实它的参数非常多,具体可以参考手册。这里提供一个看上去还蛮不错的命令,可以用上面的方法设置一个别名 git lg,这样以后调用起来就方便多了:

git config --global alias.lg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr)%C(bold blue)<%an>%Creset' --abbrev-commit"

其实,不管怎么样,还是用 GUI 看日志方便一些:)

stash:保存/恢复你的工作状态

经常有这样的场景:你正在开发一个新特性,或者重构一部分代码,这时候突然有一个 Bug 需要修复,但是你的工作目录正处于一片混沌状态。这时你是新建一个目录重新签出版本库,还是硬着头皮在改了一半的文件中修 Bug 呢?使用 git stash 可以暂时将你进行到一半的工作保存到一边,将工作目录恢复到干净的状态,当你完成 Bug 的修复后,可以再将一半的工作恢复并继续。更多操作可以参考手册。

submodule:子模块

有时候,在你的版本库中,需要引入其他版本库中的代码。当然你可以手动将其他地方的代码复制一份过来,但是当第三方代码更新了之后该怎么办呢?再手动复制一遍吗?子模块功能就是为了解决这个问题而生的,它可以在一个 Git 版本库中再嵌入另一个版本库。更多信息可以参考手册。

Legit:「人文主义」版的 Git

Legit 是一个由 GitHub for Mac 的灵感而产生的扩展项目,它简化了一些 Git 的逻辑和概念,让新手更容易接受基于 Git 的工作流。

$ git sync# 同步当前分支,自动 merge/rebase, un/stash。$ git switch <branch># 切换分支,并且储存(stash)和恢复未暂存的修改。$ git publish <branch># 向远程服务器公开分支。$ git unpublish <branch># 从远程服务器删除分支。$ git branches# 显示一个漂亮的关于分支和公开发布状态的列表。

其他

Git 还有许许多多的功能,囿于篇幅就不再详述了。网上有许多关于 Git 的资料,勤于搜索,会给你带来惊喜。

本文中的部分图片资源来自于《Pro Git》和 A Visual Git Reference。

XOXO


  1. The first rule of Fight Club is: you do not talk about Fight Club.↩

  2. 东坡居士,如有得罪,还请多多担待。↩

  3. 「四十岁后,不滞于物,草木竹石均可为剑。自此精修,渐进于无剑胜有剑之境。」↩

 Mar 30th, 2014  一家之言

0 0
原创粉丝点击