git 命令 —— 含变基、分支说明

524 阅读16分钟

【参考】

git官方中文文档:git-scm.com/book/zh/v2/…

理解基础,使用起来才能游刃有余

git init 初始化仓库

在一个尚未进行版本控制的项目目录中初始化git仓库

git init

该命令将创建一个名为 .git 的子目录,这个子目录含有初始化 Git 仓库中所有的必须文件,是 Git 仓库的骨干

此时只是进行了初始化操作,项目中的文件还未被跟踪,需要将文件加入暂存区并进初始化提交

 

git clone 克隆已有仓库

从远端克隆已存在的git仓库到本地

git clone <url>

git克隆的是该仓库服务器上的几乎所有数据,而不仅仅复制最新版本的文件。当你执行 git clone ,默认配置下远端git仓库中每一个文件、每一个版本都将拉取下来。这是git区别于其它版本控制系统的一个重要特性——分布式版本控制。如果远端服务器数据丢失了,你几乎可以使用任何克隆下来的用户端数据重建服务器上的仓库

 

以克隆 libgit2 仓库为例:

git clone https://github.com/libgit2/libgit2


# clone时也可以指定放在本地新目录
git clone https://github.com/libgit2/libgit2 mylibgit

这会在当前目录下创建名为 libgit2 的目录,并在目录中初始化一个 .git 文件夹,从远端仓库拉取所有数据放入 .git 文件夹,然后从中读取最新版本的文件拷贝

例如 git clone github.com/lodash/loda… .git 文件夹。.git 文件夹就是仓库所需的所有文件

git支持多种数据传输协议,上例使用 https:// ,你也可以使用 gitfilessh 传输协议(比如 user@server:path/to/repo.git

 

git add * 将文件加入暂存区

git add *.c README
# 将所有 .c 格式的文件 以及 README 加入暂存区,等待下一次提交

 

git status 查看各文件处于什么状态

该命令列出当前工作目录下文件的状态

git status

你工作目录下的每一个文件不外乎2种主要状态: 已跟踪(包含 未修改、已修改、已放入暂存区)未跟踪

已跟踪 指那些被纳入了版本控制的文件,在上一次快照中有它们的记录。已跟踪 文件的状态可能有 未修改已修改已放入暂存区 3种。简而言之, 已跟踪 的文件就是Git已经知道的文件

其余既不存在于上次快照记录中,也没有被放入暂存区的文件,都属于 未跟踪 文件

以下是4种状态的变化周期图:

image.png

git status -s
git status --short

-s 将得到一种简洁的输出:

 M README
MM Rakefile
A  lib/git.rb
M  lib/simplegit.rb
?? LICENSE.txt

?? :未跟踪文件

A :新加入暂存区的文件

M :修改过的文件

MM :输出中都有两栏,左侧指明了暂存区状态,右侧指明工作区状态。 Rakefile 文件已修改,在加入暂存区后,又做了修改

 

.gitignore 忽略文件

这不是一个命令, .gitignore 是放在项目根目录中的文件,用来让git忽略一些文件

*.[oa]
*~

第一行告诉git,忽略以 .o.a 结尾的文件(这类文件一般是编译过程中出现的)

第二行告诉git,忽略名字已 ~ 结尾的文件(许多文本编辑软件用这样的文件名保存副本)

 

文件 .gitignore 的格式规范如下:

  • 所有空行或者以 # 开头的行都会被 Git 忽略。
  • 可以使用标准的 glob 模式匹配,它会递归地应用在整个工作区中。
  • 匹配模式可以以(/)开头防止递归。
  • 匹配模式可以以(/)结尾指定目录。
  • 要忽略指定模式以外的文件或目录,可以在模式前加上叹号(!)取反。

glob模式

所谓的 glob 模式是指 shell 所使用的简化了的正则表达式。 星号(*)匹配零个或多个任意字符;[abc] 匹配任何一个列在方括号中的字符 (这个例子要么匹配一个 a,要么匹配一个 b,要么匹配一个 c); 问号(?)只匹配一个任意字符;如果在方括号中使用短划线分隔两个字符, 表示所有在这两个字符范围内的都可以匹配(比如 [0-9] 表示匹配所有 0 到 9 的数字)。 使用两个星号(**)表示匹配任意中间目录,比如 a/**/z 可以匹配 a/z 、 a/b/z 或 a/b/c/z 等。

# 忽略所有的 .a 文件
*.a


# 但跟踪所有的 lib.a,即便你在前面忽略了 .a 文件
!lib.a


# 只忽略当前目录下的 TODO 文件,而不忽略 subdir/TODO
/TODO


# 忽略任何目录下名为 build 的文件夹
build/


# 忽略 doc/notes.txt,但不忽略 doc/server/arch.txt
doc/*.txt


# 忽略 doc/ 目录及其所有子目录下的 .pdf 文件
doc/**/*.pdf

git diff 比较文件修改的部分

git status 以文件名的形式列出文件状态,而 git diff 更具体显示了文件哪行内容发生改变

git diff
# 注意:git diff 本身只显示尚未暂存的改动,而不是和上次提交对比

此命令比较的是工作目录中当前文件和暂存区域快照之间的差异。也就是修改之后还没有暂存的变化内容

 

若要比对已暂存文件与最后一次提交的文件差异,可以用 git diff --staged 命令

git diff --staged
或
git diff --cached
# staged和cached是同义词

 

git commit 提交暂存区

将已加入暂存区的文件改动提交,并带上提交信息

git commit -m <message>

提交前,请 git status 确认还有什么已修改或新建的文件没有 git add 过,否则提交时不会记录这些尚未暂存的变化

 

直接运行 git commit ,git将启动你配置的文本编辑器(git config --global core.editor 命令设置)来要求输入提交说明

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# Your branch is up-to-date with 'origin/master'.
#
# Changes to be committed:
#	new file:   README
#	modified:   CONTRIBUTING.md
#
~
~
~
".git/COMMIT_EDITMSG" 9L, 283C

退出编辑器后,git会丢弃注释行,用你输入的提交说明生成一次提交

 

每次运行提交操作,都是对项目做一次快照,以后可以回到这个状态,或进行比较

 

--amend 修正

git commit --amend

有时我们提交完才发现漏了几个文件没有添加、或提交信息写错了

可以使用 --amend 选项来重新提交

这个命令会将暂存区中的文件提交。 如果自上次提交以来你还未做任何修改(例如,在上次提交后马上执行了此命令), 那么快照会保持不变,修改的只是提交信息。新输入的提交信息将覆盖原来的提交信息

 

当你提交后发现忘记暂存某些需要的提交:

$ git commit -m 'initial commit'
$ git add forgotten_file
$ git commit --amend

最终你只会有一个提交——第二次提交将代替第一次提交的结果

当你在修补最后的提交时,与其说是修复旧提交,倒不如说是完全用一个 新的提交 替换旧的提交, 理解这一点非常重要。从效果上来说,就像是旧有的提交从未存在过一样,它并不会出现在仓库的历史中。

修补提交最明显的价值是可以稍微改进你最后的提交,而不会让“啊,忘了添加一个文件”或者 “小修补,修正笔误”这种提交信息弄乱你的仓库历史。

 

注意:

与变基一样,因为--amend操作后,旧提交其实不存在了,如果已经推至远端并被他人拉取使用。就不要再修正了,否则会造成混乱

 

git rm 移除文件

要从git中移除某个文件,就需要从已跟踪文件清单中移除(确切的说,是从暂存区移除),然后提交

如果只是简单地从工作目录中手工删除文件,运行 git status 时就会在 “Changes not staged for commit” 部分(也就是 未暂存清单)看到(即删除文件的行为,还未加入暂存区):

Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
        deleted:    PROJECTS.md

然后再运行 git rm 记录此次移除文件的操作:

git rm PROJECTS.md


git status
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)
    deleted:    PROJECTS.md

提交后,该文件就不再纳入版本管理了

 

另外一种情况是,我们想把文件从 Git 仓库中删除(亦即从暂存区域移除),但仍然希望保留在当前工作目录中。 换句话说,你想让文件保留在磁盘,但是并不想让 Git 继续跟踪。 当你忘记添加 .gitignore 文件,不小心把一个很大的日志文件或一堆 .a 这样的编译生成文件添加到暂存区时,这一做法尤其有用。 为达到这一目的,使用 --cached 选项:

git rm --cached README


# 也可以使用glob模式
log/*.log
注意到星号 * 之前的反斜杠 \, 因为 Git 有它自己的文件模式扩展匹配方式,所以我们不用 shell 来帮忙展开。 此命令删除 log/ 目录下扩展名为 .log 的所有文件

 

git mv 移动文件

git并不显示跟踪文件的移动,如果Git中重命名了某个文件,仓库中存储的元数据不会体现出这是一次更名操作

要在git中对文件改名,可以:

git mv file_from file_to

其实,运行 git mv 就相当于运行了下面三条命令:

mv README.md README
git rm README.md
git add README

如此分开操作,Git 也会意识到这是一次重命名,所以不管何种方式结果都一样。 两者唯一的区别在于, git mv 是一条命令而非三条命令,直接使用 git mv 方便得多。 不过在使用其他工具重命名文件时,记得在提交前 git rm 删除旧文件名,再 git add 添加新文件名

 

git log 查看提交记录

git log

 

git reset 取消暂存的文件

git reset 是一个危险的命令,如果加上了 --hard 就更加危险。下述场景中,工作目录中的文件尚未修改,因此相对安全

 

如果你输入 git add . ,意外纳入了不想暂存的文件,如何取消呢? git status 提示你:

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)
    renamed:    README.md -> README
    modified:   CONTRIBUTING.md

使用 git reset HEAD <file>

git reset HEAD CONTRIBUTING.md

这个命令有点儿奇怪,但是起作用了。 CONTRIBUTING.md 已是修改但未暂存的状态了。

 

git checkout

该命令主要用于分支切换,详见后文 git branch 分支说明

git checkout testing

这样就切换到 testing 分支了

 

要新建一个不存在的分支,并切换至其上,使用 -b 参数:

git checkout -b testing

 

跟踪分支

从一个远程跟踪分支(如 origin/master )检出一个本地分支( master )会自动创建所谓的“跟踪分支”:即一个本地分支设置跟踪了一个远端分支,这个本地分支就可以称为跟踪分支,而它跟踪的远端分支也称为“上游分支”

跟踪分支是与远程分支有直接关系的本地分支。如果在一个跟踪分支上输入 git pull ,Git 能自动地识别去哪个服务器上抓取分支,并合并到本地分支

 

git checkout -b <branch> 只会创建本地分支,该分支不会与远程分支跟踪

要从远程分支创建一个本地的跟踪分支:

git checkout -b <branch> <remote>/<branch>

也可以使用 --track 参数,来为本地分支设置要跟踪的远程分支:

git checkout --track <remote>/<branch>

 

由于这个操作太常用了,还有一个捷径。当你尝试检出一个本地不存在的分支,但刚好有一个与其同名的远程分支是,git会自动创建一个跟踪分支:

# 本地还没有 develop 分支,按理应该检出不过去
# 但本地已经有 origin/develop 远程分支信息
# 则会自动在本地创建与 origin/develop 跟踪的分支 develop,并成功检出到 develop 分支上
$ git checkout develop
Branch develop set up to track remote branch develop from origin.
Switched to a new branch 'develop'

 

设置已有的本地分支跟踪一个远程分支,或修改本地分支所跟踪的上游分支。可以在任意时间使用 -u--set-upstream-to 选项运行 git branch 来显示设置:

git branch -u origin/serverfix

 

用于撤销修改

git checkout -- 是一个危险的命令!你对那个文件在本地的任何修改都会消失——git用最近提交的版本覆盖它。除非你确实清楚不想要对那个本地文件修改了,否者请不要使用这个命令

任何你未提交的东西丢失后就再也找不回来了

如果想将文件的修改还原成上次提交的样子,怎么做呢? git status 也给出了提示:

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)
    modified:   CONTRIBUTING.md

使用:

$ git checkout -- CONTRIBUTING.md

可以看到那些操作已经撤销了

 

git remote 查看远程仓库服务器

git remote

列出当前项目指定了的远程服务器的简写,通常是 origin(仓库服务器的默认名字)

 

git remote -v

-v 还会显示出远程仓库对应的url

 

git clone 会自行添加名为origin的远程仓库,若要自行添加,运行:

git remote add <name> <url>


git remote add pb https://github.com/paulboone/ticgit

现在你可以使用 pb 代替整个url,例如你想拉取远程仓库中有,但你本地还没有的信息:

git pull pb

现在pb的master分支,可以在本地通过 pb/master 访问到,你可以将它合并到自己的某个分支中,或者如果你想要查看到,可以检出一个指向该点的本地分支

查看远程仓库更多信息

git remote show <name>

这个命令列出了当你在特定的分支上执行 git push 会自动地推送到哪一个远程分支

也列出了哪些远程分支不在你的本地,哪些远程分支已经从服务器上移除了

还有当你执行 git pull 时哪些本地分支可以与它跟踪的远程分支自动合并

重命名远程仓库

git remote rename <old-name> <new-name>

值得注意的是这同样也会修改你所有远程跟踪的分支名字。 那些过去引用 pb/master 的现在会引用 paul/master

移除本地的远程仓库信息

git remote remove <name> 

所有和这个远程仓库相关的远程跟踪分支以及配置信息也会一起被删除

删除远端已经没有的分支

当远端删除某一分支后,本地其实还存在该分支的信息

使用 git remote prune origin 来清除

git remote prune <remote-name>

 

git fetch 抓取数据

从远程仓库中获取数据,可执行:

git fetch <remote>

这个命令会访问远程仓库,从中拉取所有你还没有的数据。 执行完成后,你将会拥有那个远程仓库中所有分支的引用,可以随时合并或查看

git fetch 命令只会将数据下载到你的本地仓库——它并不会自动合并或修改你当前的工作。 当准备好时你必须手动将其合并入你的工作

 

git pull 抓取数据,并自动合并

git fetch <remote> 只会将数据下载到本地,不会自动合并或修改你当前的工作

如果你的当前分支,设置了跟踪的远程分支,那么可以使用 git pull 来自动抓取后合并该远程分支到当前分支

 

git pull 会查找当前分支所跟踪的远程分支, 从服务器上抓取最新远程分支信息然后尝试合并入当前分支

git pull 大多数情况的含义是 git fetch 紧接着 git merge

 

git push 推送到远程仓库

当你想分享你的项目时,必须将其推送到上游

git push <remote> <branch>

 

当你想将 master 分支推送到 origin 服务器上( 再次说明git clone 会自动帮你设置好这两个名字):

git push origin master

只有当你有所克隆服务器的写入权限,并且之前没有人推送过时,这条命令才能生效。 当你和其他人在同一时间克隆,他们先推送到上游然后你再推送到上游,你的推送就会毫无疑问地被拒绝。 你必须先抓取他们的工作并将其合并进你的工作后才能推送。

 

git push origin serverfix

上述例子中,Git 自动将 serverfix 分支名字展开为 refs/heads/serverfix:refs/heads/serverfix, 那意味着,“推送本地的 serverfix 分支来更新远程仓库上的 serverfix 分支。

【如何避免每次输入密码】

如果你正在使用 HTTPS URL 来推送,Git 服务器会询问用户名与密码。 默认情况下它会在终端中提示服务器是否允许你进行推送。

如果不想在每一次推送时都输入用户名与密码,你可以设置一个 “credential cache”。 最简单的方式就是将其保存在内存中几分钟,可以简单地运行 git config --global credential.helper cache 来设置它。

下一次其他协作者从服务器上抓取数据时,他们会在本地生成一个远程分支 origin/serverfix,指向服务器的 serverfix 分支的引用:

git fetch origin

要特别注意的一点是当抓取到新的远程跟踪分支时,本地不会自动生成一份可编辑的副本(拷贝)。 换一句话说,这种情况下,不会有一个新的 serverfix 分支——只有一个不可以修改的 origin/serverfix 指针

 

可以运行 git merge origin/serverfix 将这些工作合并到当前所在的分支。 如果想要在自己的 serverfix 分支上工作,可以将其建立在远程跟踪分支之上:

$ git checkout -b serverfix origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch 'serverfix'

这会给你一个用于工作的本地分支,并且起点位于 origin/serverfix

 

删除远程分支

假设一个远程分支的任务结束了,比如已经合入了 master 分支。可以运行 --delete 选项的 git push 命令来删除一个远程分支。

如果想从服务器上删除 serverfix 分支,运行:

git push origin --delete serverfix

基本上这个命令做的只是从服务器上移除这个指针。 Git 服务器通常会保留数据一段时间直到垃圾回收运行,所以如果不小心删除掉了,还是可以找回的

 

git tag 打标签

和其它版本控制系统一样,git可以给仓库历史中某一次提交打上标签,以示重要性。比较有代表性的是人们会使用这个功能来标记发布结点( v1.0 、 v2.0 等等)

 

git tag

列出已有的标签(相当于 -l 参数)。也可以按照特定的模式查找:

$ git tag -l "v1.8.5*"
v1.8.5
v1.8.5-rc0
v1.8.5-rc1
v1.8.5-rc2

 

创建标签

Git 支持两种标签:轻量标签(lightweight)与附注标签(annotated)

轻量标签很像一个不会改变的分支——它只是某个特定提交的引用

而附注标签是存储在 Git 数据库中的一个完整对象, 它们是可以被校验的,其中包含打标签者的名字、电子邮件地址、日期时间, 此外还有一个标签信息,并且可以使用 GNU Privacy Guard (GPG)签名并验证。 通常会建议创建附注标签,这样你可以拥有以上所有信息。但是如果你只是想用一个临时的标签, 或者因为某些原因不想要保存这些信息,那么也可以用轻量标签。

 

附注标签:

使用 -a 参数

$ git tag -a v1.4 -m "my version 1.4"
$ git tag
v0.1
v1.3
v1.4

-m 则指定了一条将会存储在标签中的信息。如果没有指定 -m ,将启动编辑器要求输入

可以使用 git show v1.4 查看标签信息与之对应的提交信息

 

轻量标签:

清凉标签本质上是将提交校验和存储到一个文件中——没有保存仍和其它信息。创建轻量标签不需要 -a、-m 选项,只需要标签名字:

git tag v1.4

此时使用 git show v1.4 不会看到额外信息

 

后期打标签

你可以对过去的提交打上标签,只需在命令最后指定提交的校验和

git tag -a v1.2 9fceb02

 

共享标签

默认情况, git push 命令不会传送标签到远程仓库上。创建标签后,必须显示推送标签,就像共享远程分支一样,运行 git push origin <tagname>

git push origin v1.5

 

--tags 选项可以一次推送很多标签,将所有不再远程仓库上的标签都推送:

git push origin --tags

现在,当其他人从仓库中克隆或拉取,他们也能得到你的那些标签

 

删除标签

删除轻量标签:

gi tag -d <tag-name>

注意,上述命令不会从远程仓库移除该标签,你必须使用 git push <remote> :refs/tags/<tagname> 来更新你的远程仓库:

git push origin :refs/tags/v1.4

上面这种操作的含义是,将冒号前面的空值推送到远程标签名,从而高效地删除它

 

更直观的删除远程标签的方式是:

git push origin --delete <tagname>

 

检出标签

如果你想查看某个标签所指向的文件版本,可以使用 git checkout 命令

通常会从标签版本切出一个新分支(使用 -b 参数创建新分支):

git checkout -b version2 v2.0.0

 

git config 配置git

git 配置文件有3级:

  1. /etc/gitconfig 文件: 包含系统上每一个用户及他们仓库的通用配置。 如果在执行 git config 时带上 --system 选项,那么它就会读写该文件中的配置变量。 (由于它是系统配置文件,因此你需要管理员或超级用户权限来修改它。)
  2. ~/.gitconfig 或 ~/.config/git/config 文件:只针对当前用户。 你可以传递 --global 选项让 Git 读写此文件,这会对你系统上 所有 的仓库生效。
  3. 当前使用仓库的 Git 目录中的 config 文件(即 .git/config):针对该仓库。 你可以传递 --local 选项让 Git 强制读写此文件,虽然默认情况下用的就是它。。 (当然,你需要进入某个 Git 仓库中才能让该选项生效。)

每一个级别会覆盖上一级别的配置,所以 .git/config 的配置变量会覆盖 /etc/gitconfig 中的配置变量。

 

git 别名

如果你不想每次都输入完整git命令,可以轻松为每一个命令设置别名:

$ git config --global alias.co checkout
$ git config --global alias.br branch
$ g it config --global alias.ci commit
$ git config --global alias.st status

这样只需要输入 git ci 就代表 git commit

 

在创建你认为应该存在的命令时这个技术会很有用。 例如,为了解决取消暂存文件的易用性问题,可以向 Git 中添加你自己的取消暂存别名:

$ git config --global alias.unstage 'reset HEAD --'

这会使下面的两个命令等价:

$ git unstage fileA
$ git reset HEAD -- fileA

这样看起来更清楚一些。 通常也会添加一个 last 命令,像这样:

$ git config --global alias.last 'log -1 HEAD'

这样,可以轻松地看到最后一次提交:

$ git last
commit 66938dae3329c7aebe598c2246a8e6af90d04646
Author: Josh Goebel <dreamer3@example.com>
Date:   Tue Aug 26 19:48:51 2008 +0800


    test for current head


    Signed-off-by: Scott Chacon <schacon@example.com>

可以看出,Git 只是简单地将别名替换为对应的命令。 然而,你可能想要执行外部命令,而不是一个 Git 子命令。 如果是那样的话,可以在命令前面加入 ! 符号。 如果你自己要写一些与 Git 仓库协作的工具的话,那会很有用。 我们现在演示将 git visual 定义为 gitk 的别名:

$ git config --global alias.visual '!gitk'

 

git branch 分支

git 分支,本质上仅仅是指向提交对象的可变指针。默认分支为 master ,在多次提交后,你其实已经有一个指向最后那个提交对象的master分支。分支会在每次提交时向前移动

Git 的 master 分支并不是一个特殊分支。 它就跟其它分支完全没有区别。 之所以几乎每一个仓库都有 master 分支,是因为 git init 命令默认创建它,并且大多数人都懒得去改动它

image.png  

查看分支列表

git branch

不带参数的运行,会列出分支列表

-v :同时列出每个分支的最后一次提交

-vv :列出1跟多信息,包括每个分支正在跟踪那个远程分支;以及本地分支是否领先(ahead)、落后(behind)

需要注意,这里领先、落后,比较的都是远端在本地的信息

想获得最新的,需要先抓取所有远程仓库 git fetch --all

 

git branch --merged

--merged--on-merged 可以过滤已经合并或尚未合并到当前分支的分支

上面命令列出了已经合并到当前命令的分支

git branch --merged master

默认与当前分支比较,也可以在不切换到对应分时,列出尚未合并到 master 的分支有哪些

 

创建分支

Git 是怎么创建新分支的呢? 很简单,它只是为你创建了一个可以移动的新的指针。 比如,创建一个 testing 分支, 你需要使用 git branch 命令:

git branch testing

这会在当前所在的提交对象上创建一个指针:

image.png

Git 又是怎么知道当前在哪一个分支上呢? 也很简单,它有一个名为 HEAD 的特殊指针。

请注意它和许多其它版本控制系统(如 Subversion 或 CVS)里的 HEAD 概念完全不同。

Git 中,它是一个指针,指向当前所在的本地分支(译注:将 HEAD 想象为当前分支的别名)。 在本例中,你仍然在 master 分支上。 因为 git branch 命令仅仅 创建 一个新分支,并不会自动切换到新分支中去。

image.png

你可以使用 git log 查看各个分支当前所指的快照对象,提供这一功能的参数是 --decorate

$ git log --oneline --decorate
f30ab (HEAD -> master, testing) add feature #32 - ability to add new formats to the central interface
34ac2 Fixed bug #1328 - stack overflow under certain conditions
98ca9 The initial commit of my project

正如你所见,当前 master 和 testing 分支均指向校验和以 f30ab 开头的提交对象

 

分支切换

git checkout testing

这样, head 就指向 testing 分支了

image.png

如果我们在 testing 分支上做出修改,并提交, testing 分支将往前移动:

image.png

如果再 git checkout master 切换回 master 分支,做出新提交,将会从 f30ab 产生分叉:

image.png

这使得你可以向另一个方向进行开发,并在时机成熟时,将它们合并起来。所有这些工作,你需要的命令只有 branchcheckout 和 commit

 

git log 的图形化示意,可以帮助你简单查看分叉历史:

git log --oneline --decorate --graph --all

输出提交历史、各个分支的指向,以及项目的分支分叉情况:

$ git log --oneline --decorate --graph --all
* c2b9e (HEAD, master) made other changes
| * 87ab2 (testing) made a change
|/
* f30ab add feature #32 - ability to add new formats to the
* 34ac2 fixed bug #1328 - stack overflow under certain conditions
* 98ca9 initial commit of my project

由于git分支实质上只是包含所指对象校验和的文件,所以它的创建、销毁都异常高效(相当于修改41个字节——40个字符的校验和字符串加一个换行符)。与过去大多数版本控制系统形成鲜明对比,那些系统将文件都复制一遍,并保存在一个特定目录。这样高效的特性使得 Git 鼓励开发人员频繁地创建和使用分支

 

删除分支

git branch -d <branch-name>

假设以在 master 分支上,删除其它分支为例

 

如果删除git branch --no-merged 所列出的,没有合入当前分支的分支。会提示失败

如果确认要删除,使用 -D 选项

 

设置、修改本地分支跟踪的上游分支

设置已有的本地分支跟踪一个远程分支,或修改本地分支所跟踪的上游分支。可以在任意时间使用 -u--set-upstream-to 选项运行 git branch 来显示设置:

git branch -u origin/serverfix

 

git merge 合并分支

$ git checkout master
$ git merge hotfix

切换到 master 分支上,将 hotfix 分支合并入 master

 

快进

当你试图合并两个分支时, 如果顺着一个分支走下去能够到达另一个分支,那么 Git 在合并两者的时候, 只会简单的将指针向前推进(指针右移),因为这种情况下的合并操作没有需要解决的分歧——这就叫做 “快进(fast-forward)”。

 

出现分叉时的合并

$ git checkout master
$ git merge iss53

image.png

上述情况,将 iss53 合入 master 时,简单的快进并不能实现。git会使用两个分支末端所知的快照(C4、C5),以及这两个分支的共同祖先(C2),做一个简单的三方合并

与快进不同,三方合并的结果会产生新快照,并自动创建一个合并提交指向它:

image.png

既然以及合并入master,就不在需要iss53分支了:

git branch -d iss53

 

遇到冲突时的合并

有时合并不会如此顺利,上例分叉合并时,如果C4、C5,都对C2同一行进行了修改,就会出现冲突

此时,Git做了合并,但还没有自动创建一个新的合并提交。Git会暂停下来,等待你去解决合并产生的冲突。可以使用 git status 查看因包含冲突而处于未合并状态的文件

 

<<<<<<< HEAD:index.html
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
 please contact us at support@github.com
</div>
>>>>>>> iss53:index.html

冲突上半部分,显示了 merge 是所在的分支内容;下半部分显示了 iss53 分支所指示版本的内容

 

解决冲突后,使用 git add 命令来将其标记为冲突已解决(一旦暂存这些原本有冲突的文件,Git 就会将它们标记为冲突已解决)

 

如果你想使用图形化工具来解决冲突,可以运行 git mergetool ,该命令为你启动一个合适的可视化合并工具,并带领你一步一步解决这些冲突

git mergetool <也可以指定要用的合并工具>

 

请牢记,当你做这么多操作的时候,这些分支全部都存于本地。 当你新建和合并分支的时候,所有这一切都只发生在你本地的 Git 版本库中 —— 还没有与服务器发生交互

 

git ls-remote 列出远程引用

远程引用是对远程仓库的引用(指针),包括分支、标签等

你可以通过:

git ls-remote <remote> 

显式地获得远程引用的完整列表, 或者通过:

git remote show <remote> 

获得远程分支的更多信息。 然而,一个更常见的做法是利用远程跟踪分支

 

远程跟踪分支

远程跟踪分支是远程分支状态的引用。它们是你无法移动的本地引用。一旦你进行了网络通信, Git 就会为你移动它们以精确反映远程仓库的状态。请将它们看做书签, 这样可以提醒你该分支在远程仓库中的位置就是你最后一次连接到它们的位置。

它们以 <remote>/<branch> 的形式命名。 例如,如果你想要看你最后一次与远程仓库 origin 通信时 master 分支的状态,你可以查看 origin/master 分支。 你与同事合作解决一个问题并且他们推送了一个 iss53 分支,你可能有自己的本地 iss53 分支, 然而在服务器上的分支会以 origin/iss53 来表示。

 

这可能有一点儿难以理解,让我们来看一个例子。 假设你的网络里有一个在 git.ourcompany.com 的 Git 服务器。 如果你从这里克隆,Git 的 clone 命令会为你自动将其命名为 origin,拉取它的所有数据, 创建一个指向它的 master 分支的指针,并且在本地将其命名为 origin/master。 Git 也会给你一个与 origin 的 master 分支在指向同一个地方的本地 master 分支,这样你就有工作的基础。

image.png

如果你在本地的 master 分支做了一些工作,在同一段时间内有其他人推送提交到 git.ourcompany.com 并且更新了它的 master 分支,这就是说你们的提交历史已走向不同的方向。 即便这样,只要你保持不与 origin 服务器连接(并拉取数据),你的 origin/master 指针就不会移动

image.png

如果要与给定的远程仓库同步数据,运行 git fetch <remote> 命令(在本例中为 git fetch origin)。 这个命令查找 “origin” 是哪一个服务器(在本例中,它是 git.ourcompany.com), 从中抓取本地没有的数据,并且更新本地数据库,移动 origin/master 指针到更新之后的位置。

image.png

 

git rebase 变基

之前提到过,整合2个分叉分支的方法是 git merge 。在不能快进时,它会把两个分支的最新快照,与二者的共同祖先进行三方合并,合并结果生成一个新的合并提交:

image.png

image.png

  其实,还有一种方法:

你可以提取在 C4 中引入的修改,然后再 C3 上应用一次。这种操作就叫做变基。你可以使用 rebase 命令将提交到某一分支上的所有修改移至另一分支,就好像“重新播放”一样

 

这个例子中,你检出 experiment 分支,然后将它变基到 master 分支上:

$ git checkout experiment
$ git rebase master

它的原理是首先找到这两个分支(即当前分支 experiment、变基操作的目标基底分支 master) 的最近共同祖先 C2,然后对比当前分支相对于该祖先的历次提交,提取相应的修改并存为临时文件, 然后将当前分支指向目标基底 C3, 最后以此将之前另存为临时文件的修改依序应用:

image.png

现在回到 master 分支,进行一次快进合并:

$ git checkout master
$ git merge experiment

image.png

此时的 C4' 分支就和使用 git merge 中的 C5 一样了

这两种方法最终结果没有任何区别,但是变基使提交历史更整洁。你在查看一个经过变基的分支的历史记录时,会发现,尽管实际的开发工作是并行的, 但它们看上去就像是串行的一样,提交历史是一条直线没有分叉。

 

一般我们这样做的目的是为了确保在向远程分支推送时能保持提交历史的整洁——例如向某个其他人维护的项目贡献代码时。 在这种情况下,你首先在自己的分支里进行开发,当开发完成时你需要先将你的代码变基到 origin/master 上,然后再向主项目提交修改。 这样的话,该项目的维护者就不再需要进行整合工作,只需要快进合并便可。

请注意,无论是通过变基,还是通过三方合并,整合的最终结果所指向的快照始终是一样的,只不过提交历史不同罢了。 变基是将一系列提交按照原有次序依次应用到另一分支上,而合并是把最终结果合在一起。

 

更有趣的变基例子 --onto

你创建了一个主题分支 server,为服务端添加了一些功能,提交了 C3 和 C4。 然后从 C3 上创建了主题分支 client,为客户端添加了一些功能,提交了 C8 和 C9。 最后,你回到 server 分支,又提交了 C10

image.png

假设你希望将 client 中的修改合并到主分支并发布,但暂时并不想合并 server 中的修改, 因为它们还需要经过更全面的测试。这时,你就可以使用 git rebase 命令的 --onto 选项, 选中在 client 分支里但不在 server 分支里的修改(即 C8 和 C9),将它们在 master 分支上重放:

git rebase --onto master server client

以上命令的意思是:“取出 client 分支,找出它从 server 分支分歧之后的补丁, 然后把这些补丁在 master 分支上重放一遍,让 client 看起来像直接基于 master 修改一样”。这理解起来有一点复杂,不过效果非常酷。

image.png

master 分支上重播了 C8C9 的修改,得到新的 client 分支)

现在可以切换到 master 上,并使用 git merge 快进到 client 分支处了

git checkout master
git merge client

image.png  

最后,我们再来熟悉一下将 server 也整合进 master

git rebase master server

image.png

server 上的代码被续到了 master

$ git checkout master
$ git merge server
$ git branch -d client
$ git branch -d server

image.png  

变基的风险

奇妙的变基也并非完美无缺,要用它得遵守一条准则:

如果提交存在于你的仓库之外,而别人可能基于这些提交进行开发,那么不要执行变基

如果你遵循这条金科玉律,就不会出差错。 否则,人民群众会仇恨你,你的朋友和家人也会嘲笑你,唾弃你。

 

变基操作的实质是丢弃一些现有的提交,然后相应地新建一些内容一样但实际上不同的提交。 如果你已经将提交推送至某个仓库,而其他人也已经从该仓库拉取提交并进行了后续工作,此时,如果你用 git rebase 命令重新整理了提交并再次推送,你的同伴因此将不得不再次将他们手头的工作与你的提交进行整合,如果接下来你还要拉取并整合他们修改过的提交,事情就会变得一团糟。

 

让我们来看一个在公开的仓库上执行变基操作所带来的问题。 假设你从一个中央服务器克隆然后在它的基础上进行了一些开发。 你的提交历史如图所示:

image.png

然后,某人又向中央服务器提交了一些修改,其中还包括一次合并。 你抓取了这些在远程分支上的修改,并将其合并到你本地的开发分支,然后你的提交历史就会变成这样:

image.png  

接下来,这个人又决定把合并操作回滚,改用变基;继而又用 git push --force 命令覆盖了服务器上的提交历史。 之后你从服务器抓取更新,会发现多出来一些新的提交。

image.png

结果就是你们两人的处境都十分尴尬。 如果你执行 git pull 命令,你将合并来自两条提交历史的内容,生成一个新的合并提交,最终仓库会如图所示:

image.png

你将相同的内容又合并了一次,生成了一个新的提交

此时如果你执行 git log 命令,你会发现有两个提交的作者、日期、日志居然是一样的,这会令人感到混乱。 此外,如果你将这一堆又推送到服务器上,你实际上是将那些已经被变基抛弃的提交又找了回来,这会令人感到更加混乱。 很明显对方并不想在提交历史中看到 C4 和 C6,因为之前就是他把这两个提交通过变基丢弃的。

 

用变基解决变基

如果你 真的 遭遇了类似的处境,Git 还有一些高级魔法可以帮到你。 如果团队中的某人强制推送并覆盖了一些你所基于的提交,你需要做的就是检查你做了哪些修改,以及他们覆盖了哪些修改。

 

实际上,Git 除了对整个提交计算 SHA-1 校验和以外,也对本次提交所引入的修改计算了校验和——即 “patch-id”。

 

如果你拉取被覆盖过的更新并将你手头的工作基于此进行变基的话,一般情况下 Git 都能成功分辨出哪些是你的修改,并把它们应用到新分支上。

 

举个例子,如果遇到前面提到的 有人推送了经过变基的提交,并丢弃了你的本地开发所基于的一些提交 那种情境,如果我们不是执行合并,而是执行 git rebase teamone/master, Git 将会:

  • 检查哪些提交是我们的分支上独有的(C2,C3,C4,C6,C7)
  • 检查其中哪些提交不是合并操作的结果(C2,C3,C4)
  • 检查哪些提交在对方覆盖更新时并没有被纳入目标分支(只有 C2 和 C3,因为 C4 其实就是 C4')
  • 把查到的这些提交应用在 teamone/master 上面

 

从而我们将得到与 你将相同的内容又合并了一次,生成了一个新的提交 中不同的结果,如图 在一个被变基然后强制推送的分支上再次执行变基 所示。

image.png

要想上述方案有效,还需要对方在变基时确保 C4' 和 C4 是几乎一样的。 否则变基操作将无法识别,并新建另一个类似 C4 的补丁(而这个补丁很可能无法整洁的整合入历史,因为补丁中的修改已经存在于某个地方了)。

在本例中另一种简单的方法是使用 git pull --rebase 命令而不是直接 git pull。 又或者你可以自己手动完成这个过程,先 git fetch,再 git rebase teamone/master

如果你习惯使用 git pull ,同时又希望默认使用选项 --rebase,你可以执行这条语句 git config --global pull.rebase true 来更改 pull.rebase 的默认配置。

如果你只对不会离开你电脑的提交执行变基,那就不会有事。 如果你对已经推送过的提交执行变基,但别人没有基于它的提交,那么也不会有事。 如果你对已经推送至共用仓库的提交上执行变基命令,并因此丢失了一些别人的开发所基于的提交, 那你就有大麻烦了,你的同事也会因此鄙视你。

如果你或你的同事在某些情形下决意要这么做,请一定要通知每个人执行 git pull --rebase 命令,这样尽管不能避免伤痛,但能有所缓解。

变基 vs 合并

至此,你已在实战中学习了变基和合并的用法,你一定会想问,到底哪种方式更好。 在回答这个问题之前,让我们退后一步,想讨论一下提交历史到底意味着什么。

有一种观点认为,仓库的提交历史即是 记录实际发生过什么。 它是针对历史的文档,本身就有价值,不能乱改。 从这个角度看来,改变提交历史是一种亵渎,你使用 谎言 掩盖了实际发生过的事情。 如果由合并产生的提交历史是一团糟怎么办? 既然事实就是如此,那么这些痕迹就应该被保留下来,让后人能够查阅。

另一种观点则正好相反,他们认为提交历史是 项目过程中发生的事。 没人会出版一本书的第一版草稿,软件维护手册也是需要反复修订才能方便使用。 持这一观点的人会使用 rebase 及 filter-branch 等工具来编写故事,怎么方便后来的读者就怎么写。

现在,让我们回到之前的问题上来,到底合并还是变基好?希望你能明白,这并没有一个简单的答案。 Git 是一个非常强大的工具,它允许你对提交历史做许多事情,但每个团队、每个项目对此的需求并不相同。 既然你已经分别学习了两者的用法,相信你能够根据实际情况作出明智的选择。

总的原则是,只对尚未推送或分享给别人的本地修改执行变基操作清理历史, 从不对已推送至别处的提交执行变基操作,这样,你才能享受到两种方式带来的便利。