Git 入门
文章目录
- [Git 入门](#Git 入门)
-
- 一、基础篇
-
- [1.1 Git Commit](#1.1 Git Commit)
- [1.2 Git Branch](#1.2 Git Branch)
- [1.3 Git Merge](#1.3 Git Merge)
- [1.4 Git Rebase](#1.4 Git Rebase)
- 二、高级篇
-
- [2.1 分离 HEAD](#2.1 分离 HEAD)
- [2.2 相对引用(`^` 和 `~`)](#2.2 相对引用(
^
和~
)) - 强制修改分支位置
- [2.3 撤销变更](#2.3 撤销变更)
-
- [Git Reset](#Git Reset)
- [Git Revert](#Git Revert)
- 三、整理提交记录
-
- [3.1 Git Cherry-pick](#3.1 Git Cherry-pick)
- [3.2 交互式 Rebase](#3.2 交互式 Rebase)
- [四、Git 技巧 Tips](#四、Git 技巧 Tips)
-
- [4.1 筛选提交记录](#4.1 筛选提交记录)
- [4.2 提交的技巧](#4.2 提交的技巧)
-
- [4.2.1 技巧1:`git rebase -i`](#4.2.1 技巧1:
git rebase -i
) - [4.2.2 技巧2:`git cherry-pick`](#4.2.2 技巧2:
git cherry-pick
)
- [4.2.1 技巧1:`git rebase -i`](#4.2.1 技巧1:
- [4.3 Git Tags](#4.3 Git Tags)
- [4.4 Git Describe](#4.4 Git Describe)
- [4.5 选择父提交记录](#4.5 选择父提交记录)
- 五、与远程仓库协作
-
- [5.1 克隆远程仓库](#5.1 克隆远程仓库)
- [5.2 Git Fetch](#5.2 Git Fetch)
- [5.3 Git Pull](#5.3 Git Pull)
- [5.4 Git Push](#5.4 Git Push)
- [5.5 偏离的提交](#5.5 偏离的提交)
- [5.6 合并特性分支](#5.6 合并特性分支)
- [5.7 为什么不用 `merge` 呢?](#5.7 为什么不用
merge
呢?) - [5.8 远程跟踪分支](#5.8 远程跟踪分支)
- [5.9 Git Remote](#5.9 Git Remote)
- [5.10 远程仓库相关的名词、参数](#5.10 远程仓库相关的名词、参数)
- [5.11 `refspec` 中的`<src>` 为空](#5.11
refspec
中的<src>
为空) -
- [`push :<dst>` 会删除远程分支](#
push :<dst>
会删除远程分支) - [`fetch :<dst>` 会新建或提取到指定本地分支](#
fetch :<dst>
会新建或提取到指定本地分支)
- [`push :<dst>` 会删除远程分支](#
一、基础篇
1.1 Git Commit
Git 仓库中的提交记录保存的是你的目录下所有文件的快照,就像是把整个目录复制,然后再粘贴一样,但比复制粘贴优雅许多!
Git 希望提交记录尽可能地轻量,因此在你每次进行提交时,它并不会盲目地复制整个目录。条件允许的情况下,它会将当前版本与仓库中的上一个版本进行对比,并把所有的差异打包到一起作为一个提交记录。
Git 还保存了提交的历史记录。这也是为什么大多数提交记录的上面都有父节点的原因 ------ 我们会在图示中用箭头来表示这种关系。对于项目组的成员来说,维护提交历史对大家都有好处。
关于提交记录太深入的东西咱们就不再继续探讨了,现在你可以把提交记录看作是项目的快照。提交记录非常轻量,可以快速地在这些提交记录之间切换!
1.2 Git Branch
Git 的分支也非常轻量。它们只是简单地指向某个提交纪录 ------ 仅此而已。所以许多 Git 爱好者传颂:
早建分支!多用分支!
这是因为即使创建再多分的支也不会造成储存或内存上的开销,并且按逻辑分解工作到不同的分支要比维护那些特别臃肿的分支简单多了。
在将分支和提交记录结合起来后,我们会看到两者如何协作。现在只要记住使用分支其实就相当于在说:"我想基于这个提交以及它所有的父提交进行新的工作。"
分支右上角的那个星号(*
)表示该分支为当前所在的分支。
使用 git checkout <name>
来切换到 name 分支上。
执行 git checkout -b <your-branch-name>
,创建一个新的分支同时切换到新创建的分支。
1.3 Git Merge
在 Git 中合并两个分支时会产生一个特殊的提交记录,它有两个父节点。
假设有两个分支,每个分支上各有一个独有的提交。这意味着没有一个分支包含了我们修改的所有内容。
首先,master 现在指向了一个拥有两个父节点的提交记录。假如从 master 开始沿着箭头向上看,在到达起点的路上会经过所有的提交记录。这意味着 master 包含了对代码库的所有修改。
一定要注意当前分支是哪个。
例如:
-
git checkout master;git merge bugFix;
是把 master 与 bugFix 的更改合并形成一个新的提交记录 ,合并后当前分支没变,仍是 master ,指向该提交记录。master 更新了,而 bugFix 没有更新。 -
接着,执行
git checkout bugFix;git merge master;
,当前分支为 bugFix ,是现在的 master 的父节点,因而将子节点 master 合并到父节点 bugFix 就相当于仅向前移动 bugFix 指向 master 当前指向的提交记录。此时,bugFix 也更新了,与 master 指向相同的提交记录。 -
如果合并时 bugFix 是基于 master 的最新提交创建的,则会触发
fast-forward
(默认开启)选项,直接将 bugFix 合并到 master 中而不创建新的提交记录 。可以在合并的时候指定--no-ff
的参数禁用fast-forward
。
如果你细心一点,便会发现这和上面 "2. " 举的例子解释的是同一种操作:"2. " 中 bugFix 是 master 的父节点,当前节点为 bugFix ;而本例中 master 是 bugFix 的父节点,当前节点为 master 。两个例子中都没有创建新的提交记录,只是简单地前移了当前分支的指向位置到子分支。
1.4 Git Rebase
git rebase
实际上就是取出一系列的提交记录,"复制"它们,然后在另外一个地方逐个的放下去。(rebase
中文有的翻译为"变基")
Rebase 的优势就是可以创造更线性的提交历史。如果只允许使用 Rebase 的话,代码库的提交历史将会变得异常清晰。
假设当前分支为 bugFix ,我们想要把 bugFix 分支里的工作直接移到 master 分支上。移动以后会使得两个分支的功能看起来像是按顺序开发,但实际上它们是并行开发的。执行 git checkout bugFix;git rebase master
。rebase
在 master 的提交记录后面产生了一个 bugFix 提交记录的副本,该副本继承 master 的提交记录。bugFix 更新了,而 master 没有更新。
接着,我们切换到了 master 上。把它 rebase
到 bugFix 分支上,执行 git checkout master;git rebase bugFix
。由于 bugFix 继承自 master ,所以 Git 只是简单的把 master 分支的引用向前移动了一下而已,与上一节 merge
类似。至此,master 也更新了,与 bugFix 指向相同的提交记录。
二、高级篇
2.1 分离 HEAD
HEAD
是一个对当前检出记录的符号引用 ------ 也就是指向你正在其基础上进行工作的提交记录。
HEAD
总是指向当前分支上最近一次提交记录。大多数修改提交树的 Git 命令都是从改变 HEAD
的指向开始的。
HEAD
通常情况下是指向分支名的(如 bugFix )。在你提交时,改变了 bugFix 的状态,这一变化通过 HEAD
变得可见。
如果想看 HEAD
指向,可以通过 cat .git/HEAD
查看, 如果 HEAD
指向的是一个引用,还可以用 git symbolic-ref HEAD
查看它的指向。
分离 HEAD 就是让其指向了某个具体的提交记录而不是分支名。在命令执行之前的状态如下所示:
HEAD -> master -> C1
HEAD 指向 master, master 指向 C1
执行
bash
git checkout C1
现在变成了
HEAD -> C1
上例中提交记录为 C1 ,而真实的提交记录使用的是一长串哈希值(例如基于 SHA-1
,共 40 位),如果前缀唯一,可以只取前缀表示某一提交记录。
2.2 相对引用(^
和 ~
)
通过哈希值指定提交记录很不方便,所以 Git 引入了相对引用。
使用相对引用的话,你就可以从一个易于记忆的地方(比如 bugFix 分支或 HEAD
)开始计算。
相对引用非常给力,这里我介绍两个简单的用法:
- 使用
^
向上移动 1 个提交记录; - 使用
~<num>
向上移动多个提交记录,如~3
。单独使用~
,等价于^
。
首先看看操作符 (^)。把这个符号加在引用名称的后面,表示让 Git 寻找指定提交记录的父提交。
所以 master^
相当于"master 的父节点"。
master^^
是 master 的第二个父节点
也可以将 HEAD
作为相对引用的参照。
强制修改分支位置
使用相对引用最多的就是移动分支。可以直接使用 -f
选项让分支指向另一个提交。例如:
bash
git branch -f master HEAD~3
上面的命令会将 master 分支强制指向 HEAD
的第 3 级父提交。
2.3 撤销变更
主要有两种方法用来撤销变更 ------ git reset
和 git revert
。
Git Reset
git reset
通过把分支记录回退几个提交记录来实现撤销改动。你可以将这想象成"改写历史"。git reset
向上移动分支,原来指向的提交记录就跟从来没有提交过一样。
例如:
初始状态为
执行 git reset HEAD~1
后
Git Revert
虽然在你的本地分支中使用 git reset
很方便,但是这种"改写历史"的方法对大家一起使用的远程分支是无效的哦!
例如:
初始状态
为了撤销更改并分享给别人,我们需要使用 git revert
。来看演示:
bash
git revert HEAD
奇怪!在我们要撤销的提交记录后面居然多了一个新提交!这是因为新提交记录 C2' 引入了更改 ------ 这些更改刚好是用来撤销 C2 这个提交的。也就是说 C2' 的状态与 C1 是相同的。相当于数据库中的 Undo Log
,C2' 执行了 C2 的逆操作。
revert
之后就可以把你的更改推送到远程仓库与别人分享啦。
三、整理提交记录
到现在我们已经学习了 Git 的基础知识 ------ 提交、分支以及在提交树上移动。 这些概念涵盖了 Git 90% 的功能,同样也足够满足开发者的日常需求。
然而, 剩余的 10% 在处理复杂的工作流时(或者当你陷入困惑时)可能就显得尤为重要了。接下来要讨论的这个话题是"整理提交记录" ------ 开发人员有时会说"我想要把这个提交放到这里, 那个提交放到刚才那个提交的后面", 而接下来就讲的就是它的实现方式,非常清晰、灵活,还很生动。
看起来挺复杂, 其实是个很简单的概念。
3.1 Git Cherry-pick
本系列的第一个命令是 git cherry-pick
,命令形式为:
bash
git cherry-pick <提交号>...
如果你想将指定的提交复制到当前所在的位置(HEAD
)下面的话,cherry-pick
是最直接、最简单的方式了。
执行git cherry-pick C2 C4
后,
注意,cherry-pick
仅将指定的提交记录复制到 HEAD
下,而不是范围 [C2,C4]
,C3 并没有被复制。
3.2 交互式 Rebase
当你知道你所需要的提交记录(并且还知道这些提交记录的哈希值)时, 用 cherry-pick 再好不过了 ------ 没有比这更简单的方式了。
但是如果你不清楚你想要的提交记录的哈希值呢? 幸好 Git 帮你想到了这一点, 我们可以利用交互式的 rebase ------ 如果你想从一系列的提交记录中找到想要的记录, 这就是最好的方法了。
交互式 Rebase 指的是使用带参数 --interactive
的 rebase
命令, 简写为 -i
。
如果你在命令后增加了这个选项, Git 会打开一个 UI 界面并列出将要被复制到目标分支的备选提交记录,它还会显示每个提交记录的哈希值和提交说明,提交说明有助于你理解这个提交进行了哪些更改。在实际使用时,所谓的 UI 窗口一般会在文本编辑器 ------ 如 Vim ------ 中打开一个文件。
当 rebase UI界面打开时, 你能做3件事:
- 调整提交记录的顺序
- 删除你不想要的提交
- 合并提交。
四、Git 技巧 Tips
4.1 筛选提交记录
来看一个在开发中经常会遇到的情况:我正在解决某个特别棘手的 Bug,为了便于调试而在代码中添加了一些调试命令并向控制台打印了一些信息。
这些调试 和打印语句都在它们各自的提交记录里。最后我终于找到了造成这个 Bug 的根本原因,解决掉以后觉得沾沾自喜!
最后就差把 bugFix 分支里的工作合并回 master 分支了。你可以选择通过 fast-forward
快速合并到 master 分支上,但这样的话 master 分支就会包含我这些调试 和打印语句了。你肯定不想这样,应该还有更好的方式......
实际我们只要让 Git 复制解决问题的那一个提交记录就可以了。跟之前我们在"三、整理提交记录"中学到的一样,我们可以使用
git rebase -i
git cherry-pick
来达到目的。
前提:
当前分支为 bugFix 。
目标:
解决方式:
-
git rebase -i
bash# 在 UI窗口中删除 C2、C3,仅保留 C4 git rebase -i master git rebase bugFix master
-
git cherry-pick
bashgit checkout master git cherry-pick C4
如果不考虑 bugFix 只关注 master ,则做到这就可以完成目标。否则,如需与目标完全一样,则需要再执行
bashgit branch -f bugFix master
强制修改 bugFix 的位置到 master 。
4.2 提交的技巧
接下来这种情况也是很常见的:你之前在 newImage 分支上进行了一次提交,然后又基于它创建了 caption 分支,然后又提交了一次。
此时你想对的某个以前的提交记录进行一些小小的调整。比如设计师想修改一下 newImage 中图片的分辨率,尽管那个提交记录并不是最新的了。
4.2.1 技巧1:git rebase -i
我们可以通过下面的方法来克服困难:
- 先用
git rebase -i
将提交重新排序,然后把我们想要修改的提交记录挪到最前 - 然后用
commit --amend
来进行一些小修改 - 接着再用
git rebase -i
来将他们调回原来的顺序 - 最后我们把 master 移到修改的最前端(用你自己喜欢的方法),就大功告成啦!
当然完成这个任务的方法不止上面提到的一种(我知道你在看 cherry-pick
啦),之后我们会多点关注这些技巧啦,但现在暂时只专注上面这种方法。 最后有必要说明一下目标状态中的那几个'
------ 我们把这个提交移动了两次,每移动一次会产生一个 '
;而 C2 上多出来的那个是我们在使用了 amend
参数提交时产生的,所以最终结果就是这样了。
前提:
当前分支为 caption 。
目标:
解决方式:
bash # 颠倒 C2、C3 的顺序,让 C2 位于最新提交的位置 git rebase -i master # 修改 C2 提交,更改分辨率 git commit --amend # 恢复 C2、C3 的顺序 git rebase -i master # 修改 master 的位置 git rebase caption master
4.2.2 技巧2:git cherry-pick
正如你在上一节所见到的,我们可以使用 rebase -i
对提交记录进行重新排序。只要把我们想要的提交记录挪到最前端,我们就可以很轻松的用 --amend
修改它,然后把它们重新排成我们想要的顺序。
但这样做就唯一的问题就是要进行两次排序,而这有可能造成由 rebase
而导致的冲突。下面还是看看 git cherry-pick
是怎么做的吧。
要在心里牢记 cherry-pick
可以将提交树上任何地方的提交记录取过来追加到 HEAD
上(只要不是 HEAD
上游的提交就没问题)。
前提:
当前分支为 caption 。
目标:
解决方式:
bash
git checkout master
git cherry-pick C2
git commit --amend
git cherry-pick C3
4.3 Git Tags
相信通过前面课程的学习你已经发现了:分支很容易被人为移动,并且当有新的提交时,它也会移动。分支很容易被改变,大部分分支还只是临时的,并且还一直在变。
你可能会问了:有没有什么可以永远指向某个提交记录的标识呢,比如软件发布新的大版本,或者是修正一些重要的 Bug 或是增加了某些新特性,有没有比分支更好的可以永远指向这些提交的方法呢?
当然有了!Git 的 tag
就是干这个用的啊,它们可以(在某种程度上 ------ 因为标签可以被删除后重新在另外一个位置创建同名的标签)永久地将某个特定的提交命名为里程碑,然后就可以像分支一样引用了。
更难得的是,它们并不会随着新的提交而移动。你也不能检出到某个标签上面进行修改提交,它就像是提交树上的一个锚点,标识了某个特定的位置。
git tag
命令用法如下:
git tag [-a | -s | -u <key-id>] [-f] [-m <msg> | -F <file>] [-e]
<tagname> [<commit> | <object>]
其中:
<commit>
<object>
新标签引用的对象,通常是一个提交。默认为 HEAD 。
4.4 Git Describe
由于标签在代码库中起着"锚点"的作用,Git 还为此专门设计了一个命令用来描述离你最近的锚点(也就是标签),它就是 git describe
!
Git Describe 能帮你在提交历史中移动了多次以后找到方向;当你用 git bisect
(用于查找产生 Bug 的提交记录的命令)找到某个提交记录时,或者是当你坐在你那刚刚度假回来的同事的电脑前时, 可能会用到这个命令。
git describe 的语法是:
git describe <ref>
<ref> 可以是任何能被 Git 识别成提交记录的引用,如果你没有指定的话,Git 会使用当前检出的位置(HEAD)。
它输出的结果是这样的:
<tag>_<numCommits>_g<hash>
tag 表示的是离 ref 最近的标签, numCommits 是表示这个 ref 与 tag 相差有多少个提交记录, hash 表示的是你所给定的 ref 所表示的提交记录哈希值的前几位。
当 ref 提交记录上有某个标签时,则只输出标签名称。
4.5 选择父提交记录
操作符 ^
与 ~
符一样,后面也可以跟一个数字。
但是该操作符后面的数字与 ~
后面的不同,并不是用来指定向上返回几代,而是指定合并提交记录的某个父提交。当一个合并提交有多个父提交时,该选择哪条路径就不是很清晰了。
Git 默认选择合并提交的"第一个"父提交,在操作符 ^ 后跟一个数字可以改变这一默认行为。
更厉害的是,Git 还支持 ^
与 ~
联合的链式操作。
五、与远程仓库协作
远程仓库并不复杂, 在如今的云计算盛行的世界很容易把远程仓库想象成一个富有魔力的东西, 但实际上它们只是你的仓库在另个一台计算机上的拷贝。你可以通过因特网与这台计算机通信 ------ 也就是增加或是获取提交记录
话虽如此, 远程仓库却有一系列强大的特性。
首先也是最重要的的点, 远程仓库是一个强大的备份。本地仓库也有恢复文件到指定版本的能力, 但所有的信息都是保存在本地的。有了远程仓库以后,即使丢失了本地所有数据, 你仍可以通过远程仓库拿回你丢失的数据。
还有就是, 远程让代码社交化了!既然你的项目被托管到别的地方了, 你的朋友可以更容易地为你的项目做贡献(或者拉取最新的变更)。
现在用网站来对远程仓库进行可视化操作变得越发流行了(像 Github , Phabricator 或 Gitee),但远程仓库永远是这些工具的顶梁柱, 因此理解其概念非常的重要!
5.1 克隆远程仓库
直到现在, 教程都聚焦于本地仓库的操作(branch
、merge
、rebase
等等)。但我们现在需要学习远程仓库的操作 ------ 我们需要一个配置这种环境的命令, 它就是 git clone
。 从技术上来讲,git clone
命令在真实的环境下的作用是在本地创建一个远程仓库的拷贝(比如从 https://github.com )。
git clone
默认会在我们的本地仓库创建一个名为 origin/master
的分支,这种类型的分支就叫远程分支。由于远程分支的特性导致其拥有一些特殊属性。
远程分支反映了远程仓库(在你上次和它通信时)的状态。这会有助于你理解本地的工作与公共工作的差别 ------ 这是你与别人分享工作成果前至关重要的一步.
远程分支有一个特别的属性,在你检出时自动进入分离 HEAD 状态。Git 这么做是出于不能直接在这些分支上进行操作的原因, 你必须在别的地方完成你的工作, (更新了远程分支之后)再用远程分享你的工作成果。
git clone
命令语法如下:
git clone [--template=<template-directory>]
[-l] [-s] [--no-hardlinks] [-q] [-n] [--bare] [--mirror]
[-o <name>] [-b <name>] [-u <upload-pack>] [--reference <repository>]
[--dissociate] [--separate-git-dir <git-dir>]
[--depth <depth>] [--[no-]single-branch] [--no-tags]
[--recurse-submodules[=<pathspec>]] [--[no-]shallow-submodules]
[--[no-]remote-submodules] [--jobs <n>] [--sparse] [--[no-]reject-shallow]
[--filter=<filter> [--also-filter-submodules]] [--] <repository>
[<directory>]
远程分支的命名规范是:
<remote name>/<branch name>
5.2 Git Fetch
git fetch
命令的用途是从远程仓库下载对象或引用。其命令语法如下:
git fetch [<options>] [<repository> [<refspec>...]]
git fetch [<options>] <group>
git fetch --multiple [<options>] [(<repository> | <group>)...]
git fetch --all [<options>]
git fetch
可以从一个指定的仓库、URL,或同时从多个仓库(如果给定 <group>
并且在配置文件中有 remotes.<group>
条目)中提取。
如果未指定远程(remote
),默认情况下将使用 origin
远程,除非为当前分支配置了上游(upstream
)分支。
当我们使用 git fetch
从远程仓库获取数据时,实际完成了以下两步:
- 从远程仓库下载本地仓库中缺失的提交记录
- 更新远程分支指针(如
origin/master
)
git fetch
实际上将本地仓库中的远程分支更新成了远程仓库相应分支最新的状态。
git fetch
通常通过互联网(使用 http://
或 git://
协议)与远程仓库通信。
但 git fetch
并不会改变你本地仓库的状态。它不会更新你的 master 分支,也不会修改你磁盘上的文件。理解这一点很重要,因为许多开发人员误以为执行了 git fetch
以后,他们本地仓库就与远程仓库同步了。它可能已经将进行这一操作所需的所有数据都下载了下来,但是并没有修改你本地的文件。所以, 你可以将 git fetch
的理解为单纯的下载操作。
5.3 Git Pull
git pull
相当于 git fetch;git merge
,先抓取一个远程仓库分支的更新,再将更新合并到本地的远程跟踪(Remote-Tracking)分支。命令语法如下:
git pull [<options>] [<repository> [<refspec>...]]
5.4 Git Push
git push
负责将你的变更上传到指定的远程仓库,并在远程仓库上合并你的新提交记录。远程仓库设置为公开时,可以将你提交的变更与他人分享。
git push
命令语法如下:
git push [--all | --branches | --mirror | --tags] [--follow-tags] [--atomic] [-n | --dry-run] [--receive-pack=<git-receive-pack>]
[--repo=<repository>] [-f | --force] [-d | --delete] [--prune] [-q | --quiet] [-v | --verbose]
[-u | --set-upstream] [-o <string> | --push-option=<string>]
[--[no-]signed|--signed=(true|false|if-asked)]
[--force-with-lease[=<refname>[:<expect>]] [--force-if-includes]]
[--no-verify] [<repository> [<refspec>...]]
如果在命令行中未使用 <repository>
参数指定推送到哪,则会参考当前分支的 branch.*.remote
配置来决定推送位置。如果缺失该配置,则默认为 origin
。
如果命令行中未使用 refspec>
参数,或 --all
、--mirror
、--tags
选项指定推送内容,命令通过参考 remote.*.push
配置来查找默认 <refspec>
,如果没有找到,则使用 push.default
配置决定推送内容。
当命令行和配置都没有指定推送内容时,将使用默认行为,该行为对应于 push.default
的 simple
值:推送当前分支到相应的上游分支,但作为安全措施,如果上游分支与本地分支的名称不相同,则中止推送。
5.5 偏离的提交
使用 Git 操作远程仓库与他人协同工作时,主要困难来自于远程库提交历史的偏离。
假设你周一克隆了一个仓库,然后开始研发某个新功能。到周五时,你新功能开发测试完毕,可以发布了。但是 ------ 天啊!你的同事这周写了一堆代码,还改了许多你的功能中使用的 API,这些变动会导致你新开发的功能变得不可用。但是他们已经将那些提交推送到远程仓库了,因此你的工作就变成了基于项目旧版的代码,与远程仓库最新的代码不匹配了。
这种情况下,git push
就不知道该如何操作了。如果你执行 git push
,Git 应该让远程仓库回到星期一那天的状态吗?还是直接在新代码的基础上添加你的代码,异或由于你的提交已经过时而直接忽略你的提交?
因为这情况(历史偏离)有许多的不确定性,Git 是不会允许你 push
变更的。实际上它会强制你先合并远程最新的代码,然后才能分享你的工作。
线性提交
那该如何解决这个问题呢?很简单,你需要做的就是使你的工作基于最新的远程分支。有许多方法做到这一点呢,不过最直接的方法就是通过 rebase
调整你的工作。
bash
git fetch;git rebase origin/master;git push
我们用 git fetch
更新了本地仓库中的远程分支,然后用 rebase 将我们的工作移动到最新的提交记录下,最后再用 git push 推送到远程仓库。
合并提交
还有其它的方法可以在远程仓库变更了以后更新我的工作吗? 当然有,我们还可以使用 merge
。
尽管 git merge
不会移动你的工作(它会创建新的合并提交),但是它会告诉 Git 你已经合并了远程仓库的所有变更。这是因为远程分支现在是你本地分支的祖先,也就是说你的提交已经包含了远程分支的所有变化。
bash
git fetch;git merge origin/master;git push
回忆前面的内容,我们可以使用 git pull
替换 git fetch;git merge
。同样,git pull --rebase
等价于 git fetch;git rebase
。这样,命令就更简短了!
5.6 合并特性分支
在大型项目中开发人员通常会在(从 master 上分出来的)特性分支上工作,工作完成后只做一次集成。这跟前面课程的描述很相像(把 side 分支推送到远程仓库),不过本节我们会深入一些。
但是有些开发人员只在 master 上做 push
、pull
------ 这样的话 master 总是最新的,始终与远程分支(origin/master
)保持一致。
对于接下来这个工作流,我们集成了两个步骤:
- 将特性分支集成到 master 上
- 推送并更新远程分支
让我们看看如何快速的更新 master 分支并推送到远程。
bash
git pull --rebase;git push
我们执行了两个命令:
- 将我们的工作
rebase
到远程分支的最新提交记录 - 向远程仓库推送我们的工作
5.7 为什么不用 merge
呢?
为了 push
新变更到远程仓库,你要做的就是包含远程仓库中最新变更。意思就是只要你的本地分支包含了远程分支(如 origin/master
)中的最新变更就可以了,至于具体是用 rebase
还是 merge
,并没有限制。
那么既然没有规定限制,为何前面几节都在着重于 rebase
呢?为什么在操作远程分支时不喜欢用 merge
呢?
在开发社区里,有许多关于 merge
与 rebase
的讨论。以下是关于 rebase
的优缺点:
- 优点:
rebase
使你的提交树变得很干净, 所有的提交都在一条线上 - 缺点:
rebase
修改了提交树的历史
比如,提交 C1 可以被rebase
到 C3 之后。这看起来 C1 中的工作是在 C3 之后进行的,但实际上是在 C3 之前。
一些开发人员喜欢保留提交历史,因此更偏爱 merge
。而其他人可能更喜欢干净的提交树,于是偏爱 rebase
。仁者见仁,智者见智。
5.8 远程跟踪分支
直接了当地讲,master 和 origin/master 的关联关系就是由分支的"remote tracking "属性决定的。master 被设定为跟踪 origin/master ------ 这意味着为 master 分支指定了推送的目的地以及拉取后合并的目标。
你可能想知道 master 分支上这个属性是怎么被设定的,你并没有用任何命令指定过这个属性呀!好吧, 当你克隆仓库的时候, Git 就自动帮你把这个属性设置好了。
当你克隆时, Git 会为远程仓库中的每个分支在本地仓库中创建一个远程分支(比如 origin/master
)。然后再创建一个跟踪远程仓库中活动分支的本地分支,默认情况下这个本地分支会被命名为 master
。
克隆完成后,你会得到一个本地分支(如果没有这个本地分支的话,你的目录就是"空白"的),但是可以查看远程仓库中所有的分支(如果你好奇心很强的话)。这样做对于本地仓库和远程仓库来说,都是最佳选择。
这也解释了为什么会在克隆的时候会看到下面的输出:
bash
local branch "master" set to track remote branch "origin/master"
有两种方法设置这个属性,第一种就是通过远程分支检出一个新的分支,执行:
bash
git checkout -b totallyNotMaster origin/master
就可以创建一个名为 totallyNotMaster 的分支,它跟踪远程分支 origin/master。
另一种设置远程追踪分支的方法就是使用:git branch -u
命令,执行:
bash
git branch -u origin/master foo
这样 foo 就会跟踪 origin/master 了。如果当前就在 foo 分支上, 还可以省略 foo:
bash
git branch -u origin/master
5.9 Git Remote
git remote
命令用于管理跟踪的远程仓库设置,用法如下:
git remote [-v | --verbose]
git remote add [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] <name> <URL>
git remote rename [--[no-]progress] <old> <new>
git remote remove <name>
git remote set-head <name> (-a | --auto | -d | --delete | <branch>)
git remote set-branches [--add] <name> <branch>...
git remote get-url [--push] [--all] <name>
git remote set-url [--push] <name> <newurl> [<oldurl>]
git remote set-url --add [--push] <name> <newurl>
git remote set-url --delete [--push] <name> <URL>
git remote [-v | --verbose] show [-n] <name>...
git remote prune [-n | --dry-run] <name>...
git remote [-v | --verbose] update [-p | --prune] [(<group> | <remote>)...]
假如我们 git clone
克隆了某个远程仓库,执行 git remote -v
则可看见一个名为 origin
的远程信息:
5.10 远程仓库相关的名词、参数
: 即远程仓库(repository
)。
下列之一的名称可以用来取代 URL 作为 `<repository>` 参数:
* Git 配置文件中的 `remote` :`$GIT_DIR/config` ,
* 一个 `$GIT_DIR/remotes` 目录中的文件,或
* 一个 `$GIT_DIR/branches` 目录中的文件。
所有这些也允许你在命令行中省略 `refspec` 因为它们每个都包含一个 Git 默认会使用的 `refspec` 。
: 即分支(branch
)和标签(tag
)的总称。
: "远程"仓库,为 fetch
和 pull
操作的来源。该参数可以是一个 URL 或一个远程的名称。
: 引用配置文件内 remotes.<group>
值中的仓库列表的名称。
: 指定要获取的引用和要更新的本地引用。当命令行上没有 <refspec>
时,从 remote.<repository>.fetch
变量中读取要提取的引用。<refspec>
参数的格式是可选的加号(+
,表示强制更新,不管是不是 fast-forward
),后跟源 <src>
,后跟冒号(:
),后跟目标引用 <dst>
。当 <dst>
为空时,可以省略冒号。<src>
通常是一个引用,但也可以是一个完全拼写的十六进制对象名。
可以在 `<src>` 和 `<dst>` 中使用 `*` 和 `^` 做模式匹配。`*` 作为正操作,模式匹配至少一次即成功;而 `^` 作为负操作,将其加在 `<refspec>`前则表示不匹配该 `<refspec>` 。
`<refspec>` 可以只包含 `<src>` 和 `<dst>` 之一,也不支持完全拼写的十六进制对象名称。
5.11 refspec
中的<src>
为空
push :<dst>
会删除远程分支
如果存在 <dst>
远程分支时,push :<dst>
会删除远程分支。
因而我们可以使用该技巧来删除远程分支。另一个删除远程分支的命令是:
bash
git push <repository> --delete <branchname>
删除本地分支的命令为:
bash
# -D 是 --delete --force 的简写,-d 是 --delete 的简写。-r 表示删除的是本地的远程跟踪分支。可一次删除多个分支。
git branch (-d | -D) [-r] <branchname>...
如果不存在 <dst>
远程分支时,push :<dst>
会报错。
bash
$ git push origin :vvv
error: unable to delete 'vvv': remote ref does not exist
error: failed to push some refs to 'gitee.com:XXX.git'
fetch :<dst>
会新建或提取到指定本地分支
**如果本地不存在 <dst>
分支时,fetch :<dst>
会在本地新建指定分支;否则,fetch :<dst>
会提取到指定本地分支。
git pull
也可使用类似的技巧,相当于 git fetch
+ git merge
,如 git pull origin :bugFix
。
以上内容基于 Learn Git Branching 和 Git Manual Page (git help <command>
)修改创作。推荐 Git 初学者使用 Learn Git Branching 互动学习,生动形象,易于理解。