0%

Git进阶(2)

Main Takeaway

Git进阶(2)——了解Git Branching—”killer feature“

希望通过Git - Book (git-scm.com)+Learn Git Branching来进一步掌握Git

Learn to love the command line. Leave the IDE behind.

Git Branching

Branches in a Nutshell

Git保存一系列不同时刻的snapshots,When you make a commit, Git stores a commit object that contains a pointer to the snapshot of the content you staged. 暂存操作会为每一个文件计算校验和。当使用 git commit 进行提交操作时,Git 会先计算每一个子目录(本例中只有项目根目录)的校验和, 然后在 Git 仓库中这些校验和保存为树对象。随后,Git 便会创建一个提交对象, 它除了包含上面提到的那些信息外,还包含指向这个树对象(项目根目录)的指针。 如此一来,Git 就可以在需要的时候重现此次保存的快照。

现在,Git 仓库中有五个对象:三个 blob 对象(保存着文件快照)、一个 对象 (记录着目录结构和 blob 对象索引)以及一个 提交 对象(包含着指向前述树对象的指针和所有提交信息)。

commit-and-tree

Git 的分支,其实本质上仅仅是指向提交对象的可变指针

Git有一个名为 HEAD 的特殊指针,指向当前所在的本地分支(HEAD即当前你所在的位置)

Tips:Git 的分支实质上仅是包含所指对象校验和(长度为 40 的 SHA-1 值字符串)的文件,所以它的创建和销毁都异常高效。

分支创建

使用 git branch 命令:只是为我创建了一个新的指针

1
$ git branch testing
head-to-master

创建新分支的同时切换过去

通常我们会在创建一个新分支后立即切换过去,这可以用 git checkout -b <newbranchname> 一条命令搞定

分支删除

使用带 -d 选项的 git branch 命令来删除分支:

1
$ git branch -d <branch-name>

分支检查

git log 命令查看各个分支当前所指的对象。 提供这一功能的参数是 --decorate。(仅检查当前分支)

1
$ git log --oneline --decorate

查看分叉历史。 运行 git log --oneline --decorate --graph --all 会输出你的提交历史、各个分支的指向以及项目的分支分叉情况。

分支切换

在 Git 2.23 版本中,引入了一个名为 git switch 的新命令,最终会取代 git checkout,因为 checkout 作为单个命令有点超载(它承载了很多独立的功能)

使用 git checkout 命令

1
$ git checkout testing

HEAD会随当前指针移动

1
$ git checkout master
  • HEAD 指回 master 分支

  • 将工作目录恢复成 master 分支所指向的快照内容

Tips:切换前保持干净(留意未提交的修改)

分支合并

git merge 命令来达到上述目的:先切换到主分支,然后 git merge <想要合并的branch>

1
2
$ git checkout master
$ git merge hotfix

一脉相承的分支合并

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

diverged branches:

basic-merging-2

Git 会使用两个分支的末端所指的快照(C4C5)以及这两个分支的公共祖先(C2),做一个简单的三方合并。

Tips:多余的分支记得删除 git branch -d ...

遇到冲突时的分支合并

如果你在两个不同的分支中,对同一个文件的同一个部分进行了不同的修改,Git 就没法干净的合并它们。

1
$ git merge iss53

此时 Git 做了合并,但是没有自动地创建一个新的合并提交。 Git 会暂停下来,等待你去解决合并产生的冲突。 你可以在合并冲突后的任意时刻使用 git status 命令来查看那些因包含合并冲突而处于未合并(unmerged)状态的文件:

1
$ git status

<<<<<<< , ======= , 和 >>>>>>> 这些行被完全删除了。 在你解决了所有文件里的冲突之后,对每个文件使用 git add 命令来将其标记为冲突已解决。 一旦暂存这些原本有冲突的文件,Git 就会将它们标记为冲突已解决。

Rebasing

在 Git 中整合来自不同分支的修改主要有两种方法:merge (最容易)以及 rebase

变基(rebase):使用 rebase 命令将提交到某一分支上的所有修改都移至另一分支上(先转到要rebase的分支上去,master是目标基底分支)

变基是将一系列提交按照原有次序依次应用到另一分支上git rebase <basebranch> <topicbranch>

1
2
$ git checkout experiment
$ git rebase master

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

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

1
2
$ git checkout master
$ git merge experiment

Rebasing 进阶

使用 git rebase 命令的 --onto 选项, 选中在 client 分支里但不在 server 分支里的修改(即 C8C9),将它们在 master 分支上重放:

1
$ git rebase --onto master server client

理解:取出 client 分支,找出它从 server 分支分歧之后的补丁, 然后把这些补丁在 master 分支上重放一遍,让 client 看起来像直接基于 master 修改一样interesting-rebase-2

变基的风险

总的原则是,只对尚未推送或分享给别人的本地修改执行变基操作清理历史, 从不对已推送至别处的提交执行变基操作

分支管理

git branch命令的使用

  • 查看所有分支
1
2
$ git branch
* master

不加任何参数运行它,会得到当前所有分支的一个列表,master 分支前的 * 字符:它代表现在检出的那一个分支(也就是说,当前 HEAD 指针所指向的分支)

  • 查看每一个分支的最后一次提交
1
$ git branch -v
  • --merged与 --no-merged 这两个有用的选项可以过滤这个列表中已经合并或尚未合并到当前分支的分支。 如果要查看哪些分支已经合并到当前分支,可以运行 git branch --merged

    Tips:你总是可以提供一个附加的参数来查看其它分支的合并状态而不必检出它们。 例如,尚未合并到 master 分支的有哪些?

    1
    2
    3
    4
    $ git checkout testing
    $ git branch --no-merged master
    topicA
    featureB

在tree上移动

分离HEAD:git checkout C1(一个snapshot的checksum)

Git识别checksum比较智能,只需能唯一标识的前几个字符即可

相对引用

通过哈希值指定提交记录很不方便,所以 Git 引入了相对引用,这里我介绍两个简单的用法:

  • 使用 ^ 向上移动 1 个提交记录(寻找parent node,可以连续使用eg:main^^),其后也可以跟数字,指明回到哪一个parent node
  • 使用 ~<num> 向上移动多个提交记录,如 ~3(可选,不加数字时与 ^ 相同,向上移动一次)

Tips:modifiers可以复合使用:git checkout HEAD\~2^2~3

强制修改分支位置

使用相对引用最多的就是移动分支。可以直接使用 -f 选项让分支指向另一个提交。例如:

1
git branch -f main HEAD~3

上面的命令会将 main 分支强制指向 HEAD 的第 3 级 parent 提交。

整理提交记录

cherry-pick

“整理提交记录” —— 开发人员有时会说“我想要把这个提交放到这里, 那个提交放到刚才那个提交的后面”

  • git cherry-pick <提交号>...

如果你想将一些提交复制到当前所在的位置(HEAD)下面的话, Cherry-pick 是最直接的方式了。(如果知道提交记录则very simple)

交互式的rebase

if 不清楚你想要的提交记录的哈希值

Tips:COOL!!!

交互式 rebase 指的是使用带参数 --interactive 的 rebase 命令, 简写为 -i,Git 会打开一个 UI 界面(vim)并列出将要被复制到目标分支的备选提交记录,它还会显示每个提交记录的哈希值和提交说明,提交说明有助于你理解这个提交进行了哪些更改。

1
git rebase -i HEAD~4

当 rebase UI界面打开时, 你能做3件事:

  • 调整提交记录的顺序(通过鼠标拖放来完成)
  • 删除你不想要的提交(通过切换 pick 的状态来完成,关闭就意味着你不想要这个提交记录)
  • 合并提交。

如何整理

来看一个在开发中经常会遇到的情况:我正在解决某个特别棘手的 Bug,为了便于调试而在代码中添加了一些调试命令并向控制台打印了一些信息。解决后并不想在commit中包含这些信息,实际我们只要让 Git 复制解决问题的那一个提交记录就可以了。我们可以使用

  • git rebase -i
  • git cherry-pick

来达到目的。

  • 我们可以使用 rebase -i 对提交记录进行重新排序。只要把我们想要的提交记录挪到最前端,我们就可以很轻松的用 --amend 修改它,然后把它们重新排成我们想要的顺序

  • or只是把C2单独提出来,修改后,再用cherry-pick添加进去

    image-20230815110958607

分支开发工作流

长期分支

非必要,但常很有帮助

许多使用 Git 的开发者都喜欢使用这种方式来工作,比如只在 master 分支上保留完全稳定的代码——有可能仅仅是已经发布或即将发布的代码。 他们还有一些名为 develop 或者 next 的平行分支,被用来做后续开发或者测试稳定性——这些分支不必保持绝对稳定,但是一旦达到稳定状态,它们就可以被合并入 master 分支了。

趋于稳定分支的流水线(“silo”)视图:

lr-branches-2

主题分支

主题分支对任何规模的项目都适用。 主题分支是一种短期分支,它被用来实现单一特性或其相关工作。 在 Git 中一天之内多次创建、使用、合并、删除分支都很常见。

远程分支

远程引用是对远程仓库的引用(指针),包括分支、标签等等。 你可以通过 git ls-remote <remote> 来显式地获得远程引用的完整列表, 或者通过 git remote show <remote> 获得远程分支的更多信息。

远程跟踪分支

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

命名形式:/

Git 的 clone 命令会为你自动将其命名为 origin,拉取它的所有数据, 创建一个指向它的 master 分支的指针,并且在本地将其命名为 origin/master。 Git 也会给你一个与 origin 的 master 分支在指向同一个地方的本地 master 分支,这样你就有工作的基础。

remote-branches-1

Tips:运行 git clone -o booyah,那么你默认的远程分支名字将会是 booyah/master。-o远程库的命名。

git remote add name remote(url)

同步远程仓库

程仓库同步数据,运行 git fetch <remote>

remote-branches-3

Tips:本地与远程的工作可以分叉

多个远程仓库可以:git remote add name remote(url)

推送

git push <remote> <branch> Git 自动将 serverfix 分支名字展开为 refs/heads/serverfix:refs/heads/serverfix, 那意味着,“推送本地的 serverfix 分支来更新远程仓库上的 serverfix 分支。”

运行 git push origin serverfix:awesomebranch 来将本地的 serverfix 分支推送到远程仓库上的 awesomebranch 分支

Tips:<避免每次输入密码>使用 HTTPS URL 来推送,Git 服务器会询问用户名与密码。

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

当重新抓取到新的远程跟踪分支时,不会有一个新的 serverfix 分支——只有一个不可以修改的 origin/serverfix 指针。

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

1
2
3
$ 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

跟踪分支

从一个远程跟踪分支检出一个本地分支会自动创建所谓的“跟踪分支”(它跟踪的分支叫做“上游分支”)。 跟踪分支是与远程分支有直接关系的本地分支。 如果在一个跟踪分支上输入 git pull,Git 能自动地识别去哪个服务器上抓取、合并到哪个分支。

git checkout -b <branch> <remote>/<branch>。可以自己命名

这是一个十分常用的操作所以 Git 提供了 --track 快捷方式:

1
$ git checkout --track origin/serverfix

该捷径本身还有一个捷径。 如果你尝试检出的分支 (a) 不存在且 (b) 刚好只有一个名字与之匹配的远程分支,那么 Git 就会为你创建一个跟踪分支:

1
$ git checkout serverfix

设置已有的本地分支跟踪一个刚刚拉取下来的远程分支,或者想要修改正在跟踪的上游分支, 你可以在任意时间使用 -u--set-upstream-to 选项运行 git branch 来显式地设置。

1
2
$ git branch -u origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.

Tips:<上游快捷方式>当设置好跟踪分支后,可以通过简写 @{upstream}@{u} 来引用它的上游分支。 所以在 master 分支时并且它正在跟踪 origin/master 时,如果愿意的话可以使用 git merge @{u} 来取代 git merge origin/master

查看设置的所有跟踪分支,可以使用 git branch-vv 选项。 这会将所有的本地分支列出来并且包含更多的信息

拉取

1
$ git fetch --all; git branch -vv

git fetch 命令从服务器上抓取本地没有的数据时,它并不会修改工作目录中的内容。 它只会获取数据然后让你自己合并。

Tips:git pull 在大多数情况下它的含义是一个 git fetch 紧接着一个 git merge 命令。

由于 git pull 的魔法经常令人困惑所以通常单独显式地使用 fetchmerge 命令会更好一些。

删除远程分支

可以运行带有 --delete 选项的 git push 命令来删除一个远程分支。 如果想要从服务器上删除 serverfix 分支,运行下面的命令:

1
$ git push origin --delete serverfix

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

References