Git学习笔记
所谓“版本控制系统”(VCS)是指一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。通常我们使用的VCS都是集中式的版本控制系统(CVCS)——在服务器上集中管理所有文件的所有修订版本,而所有需要协同工作的人们都通过客户点连接到CVCS来获取更新或者提交修订。CVCS著名产品包括VSS、CVS、SVN等。与CVCS相对,本地版本控制系统(LVCS)仅在本地计算机存储文件修订版,这种系统包括RCS,以及很多IDE提供的Local History组件。
CVCS和LVCS面临的共同问题是单点故障,如果系统所在机器的磁盘发生损坏,你将丢失所有数据。为了解决此问题,分布式版本控制系统(DVCS)开始出现,DVCS客户端不仅仅提取最新版本的快照,而且会把整个版本库(Repository,也叫仓库)完整的镜像克隆下来,你本地相当于拥有一个完整的仓库。每一次克隆操作都相当于对版本库进行了一次备份。
Git是一个目前非常流行的分布式版本控制系统,它的最初用途是管理Linux的源代码,它是由Linux之父Linus用C语言开发的,Git诞生不久就开始流行。2008年,提供免费Git存储的网站GitHub上线,无数开源项目从Google Code、Source Forge等仓库转到GitHub。
Git有着很多独到的设计理念,这使得它和SVN之类传统的VCS非常不同。
大部分VCS基于Diff的方式来存储信息——记录文件的Base版本,后续版本仅仅记录修改的增量:
而Git不是这样,它的每个版本都包含了对版本库全部文件的快照。为了提高存储效率,没有修改的文件不会再次存储,只会保存指向前一版本文件的链接:
在Git中,绝大部分操作都只需要访问本地文件和资源,这意味着你不必忍受网络延时,甚至可以离线工作。
举例来说,使用SVN时,如果你向查看某个文件的修订历史,里必须联网到SVN服务器,而使用Git时,你只需要查询本地磁盘即可。
Git在存储任何数据之前,都会计算散列值(SHA-1),在Git数据库中,也是用此散列值而非文件名来引用目标文件。这保证了文件损坏、篡改能够被发现。
此散列值也被作为提交标识符(commit id),由于Git是DVCS,因此不能使用SVN那样的整数作为提交标识符。
你执行的Git操作,几乎都是往Git数据库中插入新的数据,你很难让Git执行任何不可逆的操作或者以任何方式删除数据。文件一旦提交(committed),你就不用担心数据丢失,特别是在定期推送数据库到其它仓库的情况下。
在Git中,你的每个文件具有三种状态:
- 已提交(committed):数据已经安全的保存在本地仓库中
- 已修改(modified):修改了文件,但还没保存到本地仓库中
- 已暂存(staged):对一个已修改文件的当前版本做了标记,使之包含在下次提交(下个版本)的快照中。暂存区域也被称为索引
与这三种状态相关的是Git的三个区域:
- 工作目录(Working Directory):针对仓库特定版本提取出来的目录,供使用和修改
- 仓库目录:即 .git 目录,用来保存项目的元数据和对象数据库,这是Git最重要的部分,从其它机器克隆仓库时,拷贝的就是该目录的数据
- 暂存区域(Stage):仅仅包含一个文件,它记录下一次将要提交的文件列表信息,一般存放在.git目录中
修改文件并提交的基本流程是:
- 在工作目录中修改文件
- 暂存文件,将其纳入暂存区域
- 提交变更,获取暂存文件列表,永久的保存到Git仓库目录
这个区域是Git不同于其它VCS的特殊概念,所有需要提交到版本库的文件,都先存放在暂存区。提交时一次性的提交暂存区中列出的所有文件。
修订版表示法 | 说明 |
SHA-1散列 | 每次Commit的唯一标识符 |
HEAD | 最后一次提交的修订版 |
HEAD^ | 倒数第二次提交的修订版 |
HEAD^^ | 倒数第三次提交的修订版 |
HEAD~100 | 倒数第100次提交的修订版 |
最初Git是在Linux上开发的,很长一段时间内它只能在Unix-like的系统上运行,但是现在已经被移植到Windows上。
Linux下执行下面的命令安装:
1 2 3 4 |
# CentOS sudo yum install git # Ubuntu sudo apt-get install git |
Mac OS X的安装包到这里下载:https://git-scm.com/download/mac
Windows版本的安装包到这里下载:https://git-scm.com/download/win,注意Windows版本实际上内置了一个最小化的模拟的Linux环境。你完全可以使用Cygwin、MinGW,在其中安装Git。
你也可以选择从源码构建Git:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# 安装依赖 # CentOS sudo yum install curl-devel expat-devel gettext-devel openssl-devel zlib-devel sudo yum install asciidoc xmlto docbook2x # Ubuntu sudo apt-get install libcurl4-gnutls-dev libexpat1-dev gettext libz-dev libssl-dev sudo apt-get install asciidoc xmlto docbook2x # 配置和构建 tar -zxf git-2.0.0.tar.gz cd git-2.0.0 ./configure --prefix=/usr make all doc info sudo make install install-doc install-html install-info |
安装完毕后,你需要进行个性化的配置, git config 命令用于执行这些配置。配置信息会保存在以下位置:
- /etc/gitconfig 包含系统每个用户、每个仓库的通用配置,通过 git config --system 进行配置时,从此文件读写
- ~/.gitconfig 或者 ~/.config/git/config 针对当前用户,通过 git config --global 进行配置时,从此文件读写
- .git/config 针对当前仓库
每一级别的配置,会覆盖上一级别的配置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
# 列出当前的配置项 git config --list # 设置Git使用的文本编辑器 git config --global core.edito # 设置用户信息 git config --global user.name "alex" git config --global user.email alex@gmem.cc # 设置凭证缓存时间为1年 git config --global credential.helper 'cache --timeout 31536000' # 为子命令创建别名 git config --global alias.co checkout git config --global alias.br branch git config --global alias.ci commit git config --global alias.st status # 禁用压缩 # 大于此尺寸的文件扁平化的存储,不会尝试进行delta压缩 git config --add core.bigFileThreshold 1 # 禁止HTTPS证书合法性验证 git config http.sslVerify false # 仅当速度小于1KB,且持续600秒,才导致操作中断 git config --global http.lowSpeedLimit 1000 git config --global http.lowSpeedTime 600 |
开发过程中,某些工程文件无需纳入版本管理,例如日志文件、IDE设置文件、构建产生的临时文件。我们需要告诉Git,不去跟踪它们,也不让它们出现在未跟踪文件的列表中。这时可以使用 .gitignore 文件。
该文件中的内容,满足以下规则:
- 所有空行或者以 # 开头的行都会被忽略
- 可以使用标准的glob模式匹配。glob是Shell使用的简化版正则式:
- * 匹配0-N个任意字符
- [A-Z0-9] 匹配枚举中的单个字符
- ? 匹配一个任意字符
- ** 匹配任意0-N级中间目录
- 模式可以以 / 开头防止递归
- 模式可以以 /结尾,表示被排除的是目录
- 可以在模式前面加上 ! 表示取反匹配
按优先级从高到低:
- 命令行提供的忽略设置
- 仓库根目录下的 .gitignore 文件:需要被纳入版本控制、并且在clone时分发给其它仓库的忽略设置
- $GIT_DIR/info/exclude :针对特定仓库,但是不需要共享的忽略设置
- ~/.gitconfig 中 core.excludesFile 配置项所指出的文件:针对所有情况。默认位置 $HOME/.config/git/ignore
模式 | 说明 |
*.a | 排除当前目录、子目录下任何扩展名为a的文件 |
!lib.a | 不排除lib.a文件,即使上面指定了*.a这个模式 |
/TODO | 排除当前目录下的TODO文件 |
build/ | 排除build目录下的所有文件 |
doc/*.txt | 排除doc目录下的所有文本文件,注意 doc/server/arch.txt不被排除 |
doc/**/*.pdf | 排除doc目录、子目录下所有PDF文件 |
与Git仓库的日常交互,主要通过 git 命令进行:
子命令 | 说明 | ||||||||||||||
init |
初始化一个Git仓库。该命令会在当前目录下创建一个.git子目录,该目录包含初始化Git仓库所需的全部文件:
|
||||||||||||||
clone |
获得已经存在的Git仓库的拷贝,这个命令与其它VCS,例如SVN的checkout不同,它会获取目标版本库的几乎所有文件(除了某些钩子设置):
上述命令会在当前目录下创建一个libgit2目录,其中包含一个.git子目录。你可以定制新建目录的名称。刚克隆后的工作目录,其所有文件都为已跟踪、未修改状态 Git支持多种通信协议,除了上面例子中的HTTPS,还包括GIT、SSH协议等,例如:
|
||||||||||||||
add |
把指定文件纳入版本控制(文件变为已跟踪),下次提交,这些文件将被保存到仓库中:
对于已跟踪的文件,修改后未纳入暂存区的,也可以通过该子命令,将其纳入暂存区 添加所有文件:
|
||||||||||||||
commit |
执行提交操作,把暂存文件提交到版本库的当前分支(初始化Git库时,会自动创建唯一的默认分支master):
如果提交后,你发现提交说明写错了,或者遗漏了几个文件(忘记git add),可以进行“改正”:
如果上次提交以来,你没有修改任何文件,那么,改正和上一次提交会被记录为单次提交 |
||||||||||||||
checkout |
以 git checkout -- README.md 的形式使用该子命令时:
注意 -- ,如果缺少这两个横线,checkout的功能变为切换到另外一个分支:
当切换分支时:
|
||||||||||||||
clean |
从工作树中移除没有被跟踪的文件 格式:
选项: -d 移除未跟踪文件和目录。如果未跟踪目录由另外一个Git仓库管理,则默认不会移除,你需要用-f两次来移除这种目录 示例:
|
||||||||||||||
reset |
可以用来实现:撤销对文件的跟踪(暂存)、撤销并重新提交、永久撤销提交、撤销合并/拉取 格式:
选项: -q 安静模式,仅报告错误 示例:
|
||||||||||||||
revert |
撤销现有的提交。你可以给定若干commit,撤销这些commit对分支的修改,同时用新的commit来记录这些这些撤销行为。执行该命令需要工作树是干净的(从HEAD没有任何修改) 如果你需要撤销工作目录中所有尚未提交的修改,请使用git reset 示例:
|
||||||||||||||
status |
显示工作区树的状态,列出以下信息:
如果工作目录没有未跟踪、已修改的文件,则会显示类似下面的信息:
|
||||||||||||||
show |
可以显示多种类型的对象,示例:
|
||||||||||||||
diff |
显示文件内容改变的详情,被比较的文件可以是:
举例:
该子命令默认只显示尚未暂存的改动,而不是上次提交以来的所有改动。要比较已暂存区域和上次提交版本之间的差异,需要:
|
||||||||||||||
rm |
要从Git仓库移除某个文件,需要将其从已跟踪文件清单(暂存区域)中移除,然后提交。你可以使用 git rm 子命令完成该工作,连带将文件从工作目录删除:
如果通过Shell手工删除一个文件,则执行status命令会显示“Changes not staged for commi”,执行status -s显示红色D |
||||||||||||||
mv |
与其它VCS不同,Git不会显式的跟踪文件移动操作。如果在Git中重命名了某个文件,仓库中的元数据被不会体现这是一个重命名操作。尽管如此,Git能够推断出发生了什么:
|
||||||||||||||
log |
显示版本提交的历史记录,默认按照时间降序排列 格式:
选项: --all 就好像所有refs/下的Ref,以及HEAD,都列在<commit>中了。也就是显示所有日志
|
||||||||||||||
reflog |
管理reflog信息。reflog中记录了你所有的命令历史,你可以通过它来查找修订版的标识符(前缀):
查找到标识符(前缀)后,你可以任意的git reset到特定的修订版 |
||||||||||||||
remote |
管理远程仓库,远程仓库是指托管在因特网或其他网络中的你的项目的版本库。 你可以有若干个远程仓库,通常有些仓库对你只读,有些则可以读写。 与他人协作涉及管理远程仓库以及根据需要推送(push)或拉取(fetch)数据 执行下面的命令,可以查看远程仓库:
执行下面的命令,可以添加一个远程仓库:
如果你通过git clone创建了本地仓库,那么原始仓库自动命名为origin,并作为远程仓库看待 执行下面的命令,可以重命名一个远程仓库:
执行下面的命令,可以移除一个远程仓库:
你可以用下面的命令显示远程仓库的更多信息:
|
||||||||||||||
fetch |
从远程仓库下载对象,示例:
执行完上述命令后,gs库的master分支可以在本地通过gs/master看到,你可以将其合并到自己的某个分支中去 |
||||||||||||||
pull |
拉取并整合(integrate)远程仓库或者本地分支。使用该命令可以自动fetch并合并 如果你通过git clone创建了本地仓库,那么本地的master分支自动被设置为跟踪(tracked)远程仓库的默认分支,运行 git pull 通常会拉取数据并合并 相当于git fetch + git merge |
||||||||||||||
push |
推送你本地仓库的变更到远程仓库,你必须具有远程仓库的写入权限。此外,你必须确保已经把其他人的push预先pull下来。示例:
|
||||||||||||||
tag |
与其它VCS一样,Git支持给某一次提交打上标签,以示其重要性。Git 使用两种主要类型的标签:
执行下面的命令,可以列出现有的标签:
执行下面的命令可以创建一个附注标签:
不使用任何-a、-s、-m选项,则自动创建轻量标签:
指定修订版,你可以对历史版本打标签:选项
下面的命令用于删除本地标签:
默认情况下,git push不会把标签推送到远程仓库。如果你需要共享标签,可以显式的推送之:
你可以从一个标签上创建出分支:
|
||||||||||||||
branch |
列出、创建或者删除分支。示例:
|
||||||||||||||
merge |
执行分支合并——把两个或者更多的开发历史合并到一起。如果合并成功,自动保存到版本库,不需要额外commit调用 在dev分支上执行git merge master,则git会选取master,dev的共同祖先,然后进行三方合并,并形成一个新的commit,最终应用到dev分支 遇见冲突后会直接停止,等待手动解决冲突并重新提交 commit 后,才能再次 merge |
||||||||||||||
rebase |
变基,类似于merge,但是不会产生额外的commits 在dev分支上执行git rebase master,会发生:
完毕后,dev分支发生变化,master不变。也就是说在dev上执行git rebase master的意思是将dev的基变为master rebase 遇见冲突后会暂停当前操作,开发者可以:
rebase 操作会丢弃当前分支已提交的 commit,故不要在已经 push 到远程,和其他人正在协作开发的分支上执行 rebase 操作 |
||||||||||||||
cherry-pick |
挑选某些现存的提交,将其中的变更应用到当前分支。该命令会把每个提交引入的变化应用到当前分支,并且为每个提交录制一个新的提交对象。执行该命令要求工作区是干净的 此命令可以避免直接合并两个分支导致的混乱 格式: git cherry-pick [--edit] [-n] [-m parent-number] [-s] [-x] [--ff] <commit>... |
||||||||||||||
submodule add |
子模块用于引用在独立版本库中管理的,当前项目需要依赖的项目。主项目和子模块的提交是相互独立的。 添加一个子模块:
执行上述命令后,git status会显示 .gitmodules、tke文件等待提交 |
||||||||||||||
submodule init submodule update |
克隆带有子模块的项目时,子模块最初仅是空白的目录,你需要做初始化和更新操作:
快捷方式 git clone --recurse-submodules可以初始化、更新所有子模块,包括嵌套的子模块 你可以在子模块中手工fetch / merge,但是最简单的是通过下面的命令来抓取子模块的更新:
git pull会抓取子模块的修改,但不会更新子模块,git log会显示 Submodules changed but not updated。你需要执行submodule update命令来更新子模块。如果希望自动化此过程,你可以:
默认情况下,submodule update会抓取更新并更新子目录,但是子模块的仓库处于“游离的 HEAD“状态 —— 没有本地工作分支来跟踪改动 如果希望对子模块进行修改,并发布这些修改到子模块的远程仓库。你需要:
|
||||||||||||||
submodule sync | 在父项目中拉取更新后,可能出现.gitmodules中记录的子模块仓库URL变化的情况。这种情况下需要将新的URL复制到本地:
然后从新的URL来更新子模块: git submodule update --init --recursive |
||||||||||||||
describe |
根据可用的ref,给予对象一个易读的名字。该命令会寻找一个从对象(默认当前commit)可达的、最近的Tag:
格式:
选项: <commit-ish> Commit-ish对象名,默认HEAD 示例: git describe --dirty --always --tags |
分支是绝大部分VCS都支持的功能。使用分支,你可以把你的工作从开发主线上分离,避免对其产生干扰。
Git的分支功能非常轻量高效,创建一个分支几乎在瞬间就可以完成,在不同分支之间进行切换,也一样的便捷。Git鼓励在工作流程中频繁的使用分支与合并。
要理解Git如何处理方式,需要首先明白Git是如何保存数据的。前面我们提到过,Git提交后,保存的是那个时刻文件系统的快照,而不是各文件的差异。
执行提交操作时,Git会创建并保存一个提交对象(Commit object),该对象包含了:
- 提交者姓名、邮箱,以及提交的说明
- 指向一棵描述本次提交时文件变动状态的SHA-1散列树(快照树)根的指针:在Stage时,每个被暂存文件的SHA-1散列被计算,文件本身以Blob形式保存到Git仓库;在commit时,每个子树的SHA-1散列被计算,并形成树对象保存到Git仓库,该树对象引用了每个暂存文件的SHA-1散列。有了这棵树,可以随时重现本次提交时的状态
- 指向父提交(直接位于当前提交之前的提交)对象的指针。首次提交产生的提交对象,没有父对象;后续提交产生的提交对象,具有一个父对象;多个分支合并产生的提交对象,具有多个父对象
举例来说,我们现在有一个目录,其中包含三个文件,现在stage、commit它们,执行完毕后,提交对象与SHA-1散列树、当前版本文件的Blob的关系如下图:
注意提交对象的tree属性指向了那棵树,后者引用了本次提交所有文件的快照。
此时,执行一些修改,提交;再次执行一些修改,再次提交,每次产生的提交对象,指向了不同的tree:
Git中的分支,仅仅是轻量级的、可移动的指向这些提交对象的指针。 Git默认的分支是master,因此你每一次提交,都让master分支指向新的提交对象。
当你创建一个分支testing时,只是创建了另外一个可以独立移动的指针(这是Git分支轻量高效的直接原因):
那么,Git是如何知道工作目录此时处于master还是testing分支呢?当前所处分支记录在HEAD指针中(注意Git的HEAD和其它VCS的HEAD相当的不同):
可以看到,虽然我们创建了testing分支,但是HEAD还是指向master分支,这是因为我们没有切换到testing分支。现在我们切换到testing分支并执行一些修改:
1 2 3 4 |
git checkout testing vim test.rb git add * git commit -m "Modify test.rb" |
注意checkout会做两件事情:
- 移动HEAD指针到testing分支
- 回退工作区中的文件,让它与testing指向的提交对象的tree一致
提交后,分支——提交对象关系图变成这样:
这相当有趣,你现在位于testing分支,并且向前继续移动了。而master分支仍然停留在执行分支切换前的那个地方。
那么,我们重新切换回master分支、修改文件并提交,关系图会变成什么样呢?很明显,f30ab会成为两个提交对象共同的父对象。
假设你经历如下的工作流:
- 你正在开发某个网站
- 为实现某个新的需求(iss-53),创建一个分支
- 在这个分支上进行开发
此时,忽然运营部报回一个严重的缺陷,需要紧急修补。你执行以下流程:
- 切换到主分支
- 为此严重缺陷创建一个紧急修复分支(hotfix),并在其中修复它
- 测试通过后,切换到主分支,然后合并2中的紧急修复分支
- 切换回开发分支,继续工作
我们看看,如何结合Git完成上述工作流。
当前,你正在解决公司问题跟踪系统的53号问题,这是一个新增需求。此时Git状态如下:
为了实现此新需求,你新建一个分支,并切换到它:
1 2 |
git checkout -b iss53 # 等价于git branch iss53 && git checkout iss53 |
你开始为53号问题编码,并且执行了一次提交:
1 2 3 4 |
vim fix53.c vim main.c git add * git commit -m 'fix53 v1' |
此时Git的状态变为: 就在这个时候,电话响了,运营部报告了一个紧急的问题,必须立刻修复。该修复应当是针对master分支的,不应该把处于新需求实现阶段的iss53牵扯进来,因此你需要切换回master分支。在切换前,你应当保证工作目录处于一个干净的状态,你可以:
- 最好的处理方式,提交所有iss53的修改,避免未提交的代码在切换后混入master
- 或者,保存进度(stashing)
处理好iss53分支后,执行下面的命令切换回master分支,并创建hotfix分支,用于处理紧急问题:
1 2 3 4 5 6 7 8 9 10 |
git checkout master # Switched to branch 'master' git checkout -b hotfix # Switched to a new branch 'hotfix' # 修复问题 vim main.c git add * git commit -m 'main.c v4 hotfix' |
提交hotfix后,Git的状态变成:
测试完毕后,你需要把hotfix合并到master,以便部署到线上。你可以使用git merge完成合并:
1 2 3 4 5 6 7 8 |
# 先切换到接受合并的那个分支,这里是主分支 git checkout master # 然后,执行merge子命令,指定合并的来源 git merge hotfix # Updating 3b50562..f9b71c2 # Fast-forward 所谓“快进”,表示master分支是被并入分支的直接上游,因此Git仅仅把master的指针简单的向前移动即可,没有需要解决的冲突 # main.c | 2 +- 针对main.c有两行变动,新增了一行,删除了一行 # 1 file changed, 1 insertion(+), 1 deletion(-) |
合并后,Git的状态变为:
当发布了紧急修复后,你应该删除hotfix分支,因为master和它指向了同一个提交对象:
1 2 |
git branch -d hotfix # Deleted branch hotfix (3a0874c). |
注意:hotfix中的修改并没有合并到iss53中,你可以切换到iss53,然后把master合并进去。或者,完成iss53的开发后,将其合并到master中去。
你现在回到iss53,继续实现新需求:
1 2 3 4 |
git checkout iss53 vim fix53.c git add * git commit -m 'fix53 v2' |
提交后,Git的状态变为:
你现在已经修复了53号问题,需要将其合并到master分支上去:
1 2 3 4 5 |
git checkout master git merge iss53 # Merge made by the 'recursive' strategy. 递归的合并 # main.c | 1 + # 1 file changed, 1 insertion(+) |
可以注意到,merge子命令的输出和上一次合并不同。这是因为开发历史从更早的地方分叉(diverged)开来,导致master分支(对应的提交对象)不是iss53(对应的提交对象)的祖先,为了完成合并,Git必须进行额外的工作,而不是简单的移动指针。Git会使用两个分支对应的快照,加上它们共同的祖先,执行三方合并:
上涂中蓝色边框的,是参与合并的快照。与Fast-forward不同的是,三方合并的结果会形成一个新的快照,并自动创建一个新的提交指向该快照。这个提交的特别之处在于,它具有不止一个父提交:
需要注意的是,Git会自动决定把哪个提交作为最优的共同祖先,将其作为合并的基础。而SVN 1.5-则需要手工选择最佳的合并基础。
此时,你已经不需要iss53分支,删除它。
很多时候合并不能顺利的自动完成。如果你在两个不同的分支中,对同一个文件的同一部分进行了不同的修改,则Git无法干净的合并它们:
1 2 3 4 5 |
git checkout master git merge iss53 # Auto-merging main.c # CONFLICT (content): Merge conflict in main.c # Automatic merge failed; fix conflicts and then commit the result. |
在上面的例子中,Git提示冲突,需要人工介入处理。其时Git已经做了合并,但不是“干净”的,因而它不会执行提交。
在你处理冲突的过程中,可以随时执行 git status -s 来查看那些因为冲突而没有成功合并的文件,那些文件前面显示 UU 标记。
当你打开存在冲突的文件后,可以看到类似下面的内容:
1 2 3 4 5 6 7 8 9 10 |
int main(){ // <<<<<<< HEAD HEAD版本(即master)对此文件的改动,位于 === 的上半部分 int i = -1; i+=10; // ======= int i = FIX53; i++; // >>>>>>> iss53 iss53分支版本对此文件的改动,位于 === 的下半部分 return i; } |
解决完冲突后, <<< 、 === 、 >>> 都应该被删除。 确认无误后,使用 git add 命令标记冲突已解决。
你也可以利用图形化的工具完成冲突解决,例如: git mergetool 。解决完冲突后,你需要手工完成提交操作。
本节我们列出基于分支的典型工作模式。
基于Git的“三方合并”,即使长期、反复的把一个分支合并入另外一个分支,也是可行的。你可以在项目生命周期的不同阶段,拥有多个开放的分支,你可以定期的对它们进行合并。
很多Git用户喜欢所谓渐进稳定分支的分支模型:
- 只在 master 分支上保留完全稳定的代码——有可能仅仅是已经发布或即将发布的代码
- 创建一些名为develop或者next的平行分支,用来执行后续开发、稳定性测试。这些分支不保证绝对稳定,但是一旦到达稳定状态,它们就可以被合并到master分支
- 开发时引入一个新特性(Topic)时,在develop上建立短期的特性分支
这种模型的好处在于,你可以维护不同层次的稳定性。下面的流水线图,形象的描述这种分支模型:
特性分支是一种短期分支,对任何规模的项目均适用。使用Git时,每天创建、使用、合并、删除多个分支的情况很常见。上面章节的iss53、hotfix就是这样的分支。使用特性分支,你可以快速、完整的进行“上下文切换”——你的工作被分散到不同的流水线中,而每支流水线仅仅与其目标特性相关。你甚至可以基于特性分支创建新的特性分支。
所谓远程引用(Remote reference)是指指向你的远程仓库的引用。远程引用可以包括:分支、标签。远程跟踪(remote-tracking)分支是这些远程引用中最为重要的一种。
远程跟踪分支是远程分支的状态的引用,你在本地不能移动它。当你进行网络通信操作时,它会自动移动。远程跟踪分支就像是你上次连接到远程仓库时,远程分支所处状态的书签。
远程跟踪分支以 (remote-name)/(branch) 形式命名。如果你想查看最后一次与origin仓库通信时master分支的状态,你可以查看origin/master分支。
下面我们用一个例子说明远程跟踪分支如何工作。假设你使用Git服务器git.ourcompany.com,并通过git clone获得仓库的镜像并自动命名远程仓库为origin,自动创建了指向origin/master远程跟踪分支(引用)。Git会在origin/master指向的地方创建本地的master分支,这是你工作的基础:
如果你在本地,基于master分支进行一些工作,这期间,其它人推送到git.ourcompany.com并更新了master分支。在重新和远程仓库同步之前,远程/本地Git的状态如下:
一旦你执行 git fetch origin ,Git就会连接到远程服务器,抓取本地没有的数据、更新本地数据库,移动origin/master指针到新的位置:
从远程跟踪(remote-tracking )分支checkout一个本地分支时,会自动创建一个所谓跟踪分支(tracking branch,也叫上游分支,upstream branch)。跟踪分支是与远程分支直接关联的本地分支。如果在一个跟踪分支上执行 git pull ,Git会自动识别去哪个服务器上去抓取数据、抓取到的数据需要合并到哪个本地分支。
你可以用 @{upstream} 或者 @{u} 来代表上游分支:
1 2 3 |
git merge @{u} # 等价于 git merge origin/master |
当克隆一个仓库时,一般会创建跟踪origin/master的master分支。然后,如果你想跟踪其它远程分支,可以执行:
1 2 |
# 使用-u或者--set-upstream-to,切换当前分支的上游分支 git branch -u origin/serverfix |
你可以手工签出并切换到其它任何远程分支:
1 2 3 4 5 |
# 签出远程分支为serverfix本地分支,并切换到serverfix分支 git checkout --track origin/serverfix # 签出远程serverfix分支为本地分支sf git checkout -b sf origin/serverfix |
如果你想公开分享一个分支,需要将其推送到具有写入权限的远程仓库上去。本地仓库不会与远程仓库自动同步,你必须显式的推送你想分享的分支:
1 2 3 4 5 |
# 向origin服务器推送serverfix分支 git push origin serverfix # 向origin服务器推送serverfix分支,但是在服务器端命名为awesomebranch git push origin serverfix:awesomebranch # 如果不想每一次推送时都输入密码,可以运行git config --global credential.helper cache来设置 |
你的协作者在下一次从服务器抓取数据 git fetch origin 时,会在本地生成一个远程跟踪分支origin/serverfix。新抓取到的远程跟踪分支不会在本地生成可编辑的副本。 协作者可以:
- 通过 git merge origin/serverfix 把你共享的分支合并到他当前的分支中
- 或者,手工签出远程分支为本地分支: git checkout -b serverfix origin/serverfix
通过git fetch从服务器上抓取本地没有的数据时,并不会修改工作目录的内容。你需要手工的完成合并。
另外一种操作git pull则可以进行自动的合并,大部分情况下git pull相当于git fetch && git merge。只要你设置好跟踪分支,不管它是clone自动创建还是checkout手工创建的,git pull都会查找与当前分支匹配的服务器、远程分支,然后抓取数据、合并。
假设远程分支的使命已经完成——你和同事已经完成一个特性,并将其合并到远程master仓库中,可以执行下面的命令删除一个远程分支:
1 2 |
# 从origin服务器上删除serverfix分支 git push origin --delete serverfix |
本节介绍一个成功的分支模型:
- 主分支:
- master,其HEADER必须总是存放production-ready的代码
- develop,针对下一个发布版本的、最新的开发状态
- 支持分支,用于辅助并行开发、简化特性的跟踪、准备产品发布,这些分支最终会被删除:
- 特性分支:开发新的特性/主题。可以从develop衍生,必须合并回develop。命名风格任意(除了master, develop, release-*, hotfix-*),示例:
123456789# 创建分支git checkout -b myfeature develop# 合并回developgit checkout develop# --no-ff禁止fast-forward,避免丢失特性分支存在的历史,并且将特性分支的所有commit合并在一个组里面git merge --no-ff myfeaturegit branch -d myfeaturegit push origin develop - 发布分支:准备新产品级版本的发布。可以从develop衍生,必须合并回develop+master。命名风格release-*,示例:
1234567891011121314git checkout -b release-1.2 develop# 修改反应版本的源码文件./bump-version.sh 1.2# 提交新版本git commit -a -m "Bumped version number to 1.2"# 合并到mastergit checkout mastergit merge --no-ff release-1.2# 可以考虑使用-s或-u来签名Taggit tag -a 1.2# 删除发布分支git branch -d release-1.2 - 热修复分支:非计划的紧急修复,可能从master的标记版本的Tag(例如1.0.0)上衍生,必须合并回develop+master。命名风格hotfix-*,示例:
12345678910111213141516171819# 假设当前版本为1.2,出现严重BUG,需要立即修复git checkout -b hotfix-1.2.1 master# 修改反应版本的源码文件./bump-version.sh 1.2.1git commit -a -m "Bumped version number to 1.2.1"# 进行BUG修复,提交git commit -m "Fixed severe production problem"# 合并到mastergit checkout mastergit merge --no-ff hotfix-1.2.1git tag -a 1.2.1# 合并到developgit checkout developgit merge --no-ff hotfix-1.2.1# 删除热修复分支git branch -d hotfix-1.2.1
- 特性分支:开发新的特性/主题。可以从develop衍生,必须合并回develop。命名风格任意(除了master, develop, release-*, hotfix-*),示例:
尽管从技术上来说,可以使用个人仓库进行pull/push操作,但是这不是好主意。这样很容易弄乱别人的进度,而且你没开机时别人无法存取。多人协作开发时,最佳方法是使用一台公用的服务器作为Git仓库。
要架设Git服务器,首先要选择一种通信协议。Git支持四种不同的协议:本地协议、HTTP协议、SSH协议、Git协议。
这是最基本的协议,远程仓库就是磁盘中的一个目录。 通常用在团队成员对一个共享文件系统(例如一个挂载的NFS)具有访问权限的情况下。
克隆一个本地协议的版本库,可以使用如下命令:
1 |
git clone /opt/git/project.git |
要增加一个基本本地协议的远程仓库到当前Git项目(Git仓库),可以使用:
1 |
git remote add local_proj /opt/git/project.git |
Git 1.6.6引入的一种HTTP协议,是最流行的传输协议。可以像SSH协议那样智能的协商、传输数据,又可以像Git协议那样设置匿名服务。
智能HTTP协议与Git协议、SSH协议类似, 只是运行在标准的HTTP/HTTPS端口上并且使用各种HTTP验证机制。该协议的优势:
- 不同的访问方式只需要一个URL
- 基于口令的认证,避免管理SSH密钥的麻烦
- 与SSH协议一样,HTTP非常快捷高效
- 一般企业防火墙开放了HTTP/HTTPS端口
由于Linux服务器上一般都默认支持SSH服务,因此透过SSH使用Git非常简单:
1 2 3 |
git clone ssh://user@server/project.git # 或者,更简单的scp格式: git clone user@server:project.git |
该协议的缺点是不能实现匿名访问,即使要读取数据,也需要具有访问主机文件系统的权限,不适用与开源软件。
这个协议是最快的,它需要运行一个守护进程,该进场监听9418端口。缺点是没有授权机制。
这里仅仅介绍如何借助Apache 2搭建基于智能HTTP协议的Git服务器。
1 2 3 4 |
# 启用mod_cgi、mod_alias、mod_env、mod_auth_digest、mod_auth_basic等模块 a2enmod cgi alias env auth_digest auth_basic # 重启Apache服务器 service apache2 restart |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
<VirtualHost *:443> DocumentRoot /opt/git ServerName git.gmem.cc ErrorLog ${APACHE_LOG_DIR}/error-git.log CustomLog ${APACHE_LOG_DIR}/access-git.log combined SSLEngine on SSLCipherSuite AES128+EECDH:AES128+EDH SSLCertificateFile /usr/share/ca-certificates/gmem.cc.crt SSLCertificateKeyFile /etc/ssl/private/gmem.cc.key SSLCertificateChainFile /usr/share/ca-certificates/AlphaSSLCA.crt SetEnv GIT_PROJECT_ROOT /opt/git # 如果不设置下面的环境变量,则只有带git-daemon-export-ok文件的版本库才允许未授权客户端使用 SetEnv GIT_HTTP_EXPORT_ALL SetEnv REMOTE_USER=$REDIRECT_REMOTE_USER ScriptAlias / /usr/lib/git-core/git-http-backend/ <Directory "/usr/lib/git-core*"> Options ExecCGI Indexes Order allow,deny Allow from all Require all granted </Directory> # 下面的配置用于实现写操作授权认证: RewriteEngine on RewriteCond %{QUERY_STRING} service=git-receive-pack [OR] RewriteCond %{REQUEST_URI} /git-receive-pack$ RewriteRule ^/ - [E=AUTHREQUIRED:yes] <LocationMatch "^/"> Order Deny,Allow Deny from env=AUTHREQUIRED AuthType Digest AuthName "Git Access" AuthUserFile /opt/git/.htpasswd Require valid-user </LocationMatch> </VirtualHost> |
添加授权用户:
1 2 3 4 |
apt-get install apache2-utils # 添加一个用户 htdigest -c /opt/git/.htpasswd "Git Access" wangzhen # 根据提示输入密码即可 |
启用虚拟主机:
1 2 |
a2ensite git service apache2 reload |
在服务器上创建Git裸仓库:
1 2 3 4 5 |
mkdir /opt/git && cd /opt/git git init --bare test.git # 如果报错:remote: error: insufficient permission for adding an object to repository database ./objects chown -R www-data:www-data /opt/git |
现在可以通过HTTP协议克隆此仓库了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
git clone https://wangzhen@git.gmem.cc/test.git cd test touch README.md git add . git commit -m 'Add README.md' git status # On branch master # Your branch is based on 'origin/master', but the upstream is gone. # (use "git branch --unset-upstream" to fixup) 这个提示是错误的 # 这里提示上游分支不存在。这是因为裸仓库中还没有创建任何分支。我们可以直接推送,在远程仓库上创建分支 git push origin master # To https://wangzhen@git.gmem.cc/test.git # * [new branch] master -> master git status # Your branch is up-to-date with 'origin/master'. |
GitHub是一个提供Git托管服务的网站,你只需要注册一个用户,就可以将其作为免费的远程仓库使用。需要注意的是,你在GitHub上项目将被完全公开,除非你参与它的付费计划。
注册完毕后,进入你的首页,例如https://github.com/gmemcc。点击界面右上角的小箭头,选择New Repository,即可新建一个远程仓库。
你需要输入一个仓库名称、可选的描述信息。如果勾选“Initialize this repository with a README”,则master分支被自动创建,并提交一个README文件。如果你想从其它地方导入master分支,不要勾选它。
你可以通过SSH或者HTTPS协议访问新建的仓库。假设新仓库名为my-autosizer,则:
- SSH协议的URL:git@github.com:gmemcc/my-autosizer.git
- HTTP协议的URL:https://github.com/gmemcc/my-autosizer.git
GitHub建议:任何一个仓库都应该包含 README.md 、 LICENSE.md 、 .gitignore 这三个文件。
如果你本地的my-autosizer项目已经在开发中,最好不要勾选“Initialize this repository with a README”。并执行下面的操作:
1 2 3 4 5 6 7 |
# 初始化本地仓库 git init git add README.md git commit -m "Initial commit" # 关联到远程仓库 git remote add origin https://github.com/gmemcc/my-autosizer.git git push -u origin master |
- Fork原始项目
- 最好创建主题分支,在此分支上进行开发
- 在主题分支下工作,想要导入上游库的更新时,使用 git rebase。这会将当前分支的base推进到新的起点,而不引入多余的commits
- 在主题分支下工作,想要合并其它分支的更新时,可以使用 git merge。注意,如果merge的分支来自远程仓库,每次merge会导致commit操作
- 发送Pull Request
- 在此PR被关闭之前,可以继续向主题分支下Push代码,所有commits都会自动追加到PR
- PR被关闭之后,可以删除本地分支、远程分支
向上箭头:说明当前分支有孩子分支,在其他地方(只是图上不方便画出来)
向下箭头:说明当前分支是从之前某个分支延伸而来
使用命令: git reset 'HEAD@{1}'
Leave a Reply