git命令与使用_learn_git_branching_notes

chaocc0xs 2020-05-25

基础篇

Commit

commit即一次提交。git仓库中的提交记录保存的东西可以看作是目录下所有文件的快照。git希望提交记录能够尽可能的轻量,因此如果条件允许,在一次提交中,提交记录不会真的是所有文件的复制,而是将本次提交与上次提交对比,将所有的差异内容打包到一起,作为本次提交记录。

使用命令git commit在当前分支上创建一个新的提交记录。

Branch

git的分支也非常轻量,因此很多git爱好者提倡开发者尽早引入分支。
这是因为,即使创建再多分支也不会造成存储或者内存上的开销,而且按照逻辑将工作分解到不同的分支好过维护一个臃肿的主分支。

使用git branch branchname来创建一个名为branchname的新分支。
该命令只是创建了这样的一个分支,并不会切换到新分支。如果希望切换到新分支,执行命令git checkout branchname
当然,有些时候创建新分支的同时希望切换到新分支,这个时候可以运行命令git checkout -b branchname,就会创建一个名为branchname的新分支并且切换到该分支。

新创建的分支头保持和创建该分支时所在的分支头一样的位置。

Merge

开发中时常会遇到需要合并分支的操作,比如在一个新分支上开发的功能,可能会被合并到主分支。

使用git merge branchname来将branchname分支合并到当前所在分支,假设是master
该命令执行后,branchname分支头位置不变,而master分支相当于新执行了一次commit,该commit包含原masterbranchname的提交记录。

为了更直观,将执行git merge bugFix前后的代码库展示如下:

git命令与使用_learn_git_branching_notes

Rebase

除了merge,还有一种合并分支的方法,就是rebaserebase做的事情是,取出一系列的提交记录,“复制”他们,然后再另一个地方按顺序逐个放下去。
rebase的作用,是能够使整个提交记录更加线性。使用git rebase branchname能够把当前分支所有与branchname不同的提交记录按照原顺序放在branchname分支之后,然后更新当前分支的头。

给出执行git rebase master前后的变化(当前在bugFix分支:
git命令与使用_learn_git_branching_notes

接着再执行git checkout master; git rebase bugFix就可以把master移动到和bugFix一样的位置,或者直接使用git rebase tocommitpointer fromcommitpointerfromcommitpointer所指分支移动到tocommitpointer之后,随后把当前分支切换为fromcommitpointer分支。
上述的git rebase branchname实际上是省略了可选的目标分支fromcommitpointer,而使用了默认的目标分支:当前分支。

一个小技巧是:git rebase nomove或者git rebase nomove move

高级篇

HEAD

在每一个分支中,存在一个HEAD指针。默认情况下,HEAD指针总是指向分支名。

使用git checkout commithash可以将HEAD指针分离出来,并且使HEAD指针指向给定的一次提交,随后切换到HEAD指示的分支。

commithash可以使用git log来查看。通常可以使用前几位来代替整个哈希值。不过不要慌,接下来马上就会看到,如何不用冗长的哈希值也能完成HEAD的移动。

为什么会使用git checkout指令呢?
我认为,应该是把HEAD指针看作是和分支名同等的存在。不同的是,当该指令后接commithash时,该指令不去切换分支,而是直接修改HEAD这一个分支的指向。

相对引用^

使用git checkout branchname^来使得HEAD指向branchname指示的提交的前一次提交,并切换分支到HEAD分支。
使用git checkout branchname^^来使得HEAD指向branchname指示的提交的前一次提交的前一次提交,并切换分支到HEAD分支。
以此类推。
branchname也可以换为HEAD或者一个commithash。总之该参数需要指向一个提交。

bugFix分支上执行git checkout HEAD^代码后的示意图:
git命令与使用_learn_git_branching_notes

相对引用~

相对引用^只允许一个一个移动,而git checkout branchname~[num]允许向前num次移动。
num为可选项,如果不填写,效果就和^是一样的。

这样向前的引用一个重要的作用就是强制移动分支指向的提交。使用命令git branch -f branchname commitpointer来使分支branchname强制指向commitpointercommitpointer是一个提交的指针,它可以是commit的哈希值,可以是一个相对引用,也可以是HEAD或者其他分支名。

撤销变更

git对变更的撤销包括对暂存区的文件的改变以及撤销变更的方法。这里主要关注是用什么命令可以撤销变更。

Reset

第一种撤销变更的方法是git reset commitpointerreset的效果是,直接把当前分支强制移动到所指示的提交位置。

git reset HEAD~执行前后结果如下:
git命令与使用_learn_git_branching_notes

Revert

第二种方式是git revert commitpointerrevert的效果是,新增一个提交,该提交的效果是撤销commitpointer的提交,移动当前分支到最新提交。

git revert HEAD执行前后结果如下:
git命令与使用_learn_git_branching_notes

为什么有两种方式呢?因为reset的效果是,一旦撤销就不再有被撤销的记录了,一般用于本地开发时进行回退撤销;而revert则保留了所有的提交记录,一般用于多人协同开发时的代码修改。

移动提交记录

Cherry-pick

使用git cherry-pick commitpointer1 commitpointer2 ...来将一系列提交按照顺序添加到当前分支(在当前分支做提交修改的操作总是会移动分支名指向,永远指向最近的一个提交记录)。
这些commitpointer不能是当前HEAD的祖宗提交。

交互式Rebase

使用git rebase -i来使用交互式的rebase。交互式的rebase像是rebasecherry-pick的结合。

当使用git rebase -i commitpointer 的时候,打开一个交互式界面,在该交互式界面中可以从指定的这些提交(从当前分支的最近一次提交到commitpointer指向的那个提交之前的一个提交,不包括commitpointer指向的提交)中选择一些,并决定他们的排列顺序或是否合并(merge)。
cherry-pick不同的是,rebase -i选择出提交序列之后,不是直接将提交添加到当前分支,而是将这些提交以commitpointer作为父节点,按顺序添加,并将当前分支切换到该提交序列的最近一个。

执行git rebase -i HEAD~4,不改变默认顺序与提交的前后对比:
git命令与使用_learn_git_branching_notes

杂项

只做一个提交记录

现在有一个场景:在正常编程中,发现了BUG,于是程序员新建一个分支debug,在该分支中进行DEBUG。在这个过程中,程序员认为需要输出信息,于是在写输出信息的代码之前进行了一次提交。
提交之后,程序员立即创建了新分支pintf,在该分支中进行错误信息输出调试。程序员认为输出信息打印语句写完了,于是再一次提交。
提交之后立即创建了新分支bugFix,在该分支中进行代码修改。历经千辛万苦,终于把这个bug修复了,程序员立即提交。

这时,只给出每一个分支的最后一次提交,可视化为:
git命令与使用_learn_git_branching_notes

现在,程序员想把BUG处理合并到主分支,但是面临一个问题:程序员不希望把错误信息输出也合并。

这个时候,就可以使用git rebase -i或者git cherry-pick来解决问题了。

首先考虑git rebase -i
git rebase -i只能对当前分支之前的提交做操作,因此首先需要在bugFix分支执行命令git rebase -i master来调出UI界面,在该界面中,程序员选取所有pintf之后,bugFix之前的提交,按照原顺序,添加到指定的位置——master之后。这样,就相当于提交C4C2C3毫无关系了,它是直接接在C1提交之后的。

为什么C4变成了C4‘呢?这是因为没一次提交都可能是与前一次提交的差异信息,当C4的前置节点变化,存储的变更信息也变化。无论存储信息是否变化,这里只要对提交做了顺序、内容上的修改,都是用加了的表示。

这样,就变成:
git命令与使用_learn_git_branching_notes

这个时候,就可以使用git branch -f master bugFix来强制移动master的位置,或者使用git checkout master;git rebase bugFix或者使用git checkout master;git merge bugFix等很多操作都可以使master前进一步。
最终结果就是(使用git branch -f master bugFix):
git命令与使用_learn_git_branching_notes

下面再说使用cherry-pick
很简单,首先使用git checkout master切换到主分支,然后使用git cherry-pick bugFix即可。

提交技巧:修改以前的提交

一个场景是,已经提交过一次的代码,由于某些原因需要修改小小的参数。这个时候需要把这次修改添加到之前的某一次提交中。但是最新的提交已经不是那次提交了。
初始的时候,视图如下:
git命令与使用_learn_git_branching_notes

程序员希望在newImage这个分支中添加新的提交信息,但是最新的提交已经是caption分支指向的提交了。
首先,程序员需要把newImage移动到最新提交的位置,可以在caption分支使用git rebase -i newImage~
随后使用git commit --amend在最新的提交中进行一些修改,最后使用git rebase -i把修改过的提交顺序还原。最后把master挪动到最新提交位置即可。

下面还是具体看一下可视化流程:

首先执行git rebase -i newImage~命令,新建另一个C1提交后其他提交的顺序,在可视化界面中把newImage提交拉到最后。相当于不再使用原来的那个分支顺序C2->C3,而是使用一个全新的分支顺序C3‘->C2‘
视图变为:
git命令与使用_learn_git_branching_notes

随后使用git commit --amend提交新内容,本次加了选项--amend的提交不会创建一个新的提交记录,而是更改最近一个提交记录。或者说是以最近一个提交记录的父记录为前置记录创建一个新提交记录,原来的那个提交记录就不再使用了。
执行git commit --amend之后效果如下:
git命令与使用_learn_git_branching_notes

随后再次使用git rebase -i newImage~或者git rebase -i master来重排提交记录,把顺序还原。如下:
git命令与使用_learn_git_branching_notes

最后使用一种方法把mater移动到最前。使用git checkout master;git merge caption或者git branch -f master caption或者使用git checkout master;git rebase caption等等都可以。

使用上述方法,可能会产生由于两次重排序而带来的冲突。除了使用git rebase -i来完成这一任务,还可以使用git cherry-pick来完成,可以避免重排序。下面来看看git cherry-pick如何完成这一任务。

首先,切换到master分支。
随后,执行git cherry-pick newImage重组织一个newImage的提交记录,如下所示:
git命令与使用_learn_git_branching_notes

然后提交新的修改git commit --amend。在rebase -i已经展示过效果,这里就不放图片了。

最后将最新的提交拿到新的提交分支上,使用git cherry-pick caption即可,如下图:
git命令与使用_learn_git_branching_notes

Tag

tag类似于branch,也是对某一个提交记录的指向,但是。正如前面一直讨论的,分支往往只是一个临时的对某一个提交记录的指向,很容易被更改。tag则相当于一个里程碑,其指向不能随意更改,往往用于标志程序设计中的重大版本发布。
使用git checkout tagname将会使得HEAD指针分离到tagname所指示的提交,而不是“切换”到该tag。以C++中的术语比喻,tag只是一个高层常指针,它本身不能更换指向。从这个角度来看,tagbranch有明显的区别。
下面来探寻使用tag来为一个特定的提交命名,并且像使用branch一样使用tag

使用git tag tagname [commitpointer]来创建一个指向commitpointer的标签(tag)。如果不指定一个提交,那么默认指向当前分支的HEAD

Describe

使用git describe [commitpointer]指令来找到距离commitpointer最近的一次tagtag一定在commitpointer之前。)同样的,不指定commitpointer的时候,默认是HEAD
该指令输出<tag>_<numCommits>_g<hash>
其中,<tag>就是那个距离commitpointer最近的标签名称,<numCommits>tagcommitpointer之间相差的提交个数,<hash>就是commitpointer的前几位哈希。
如果commitpointer就存在一个标签,那么命令只返回<tag>

高级话题

多次Rebase

将一个杂乱的提交树按提交顺序整理成线性的过程。
注意rebase只移动两个指定分支中不同的提交。

这个关卡比较简单,只需要多次执行git rebase nomove move。没有更多的技巧,但还是比较锻炼对于git rebase命令的熟练度。

如果未来的某一天你还是感兴趣,就点击中的链接自己去体验一下吧!

两个父节点

merge中,一个提交记录可能会有两个父节点,这样当使用相对引用的时候,可能无法确定究竟要去到哪一个父节点。这个时候可以使用^来完成父节点的选择工作。

如果使用git checkout HEAD^,不加其他东西,那么将将HEAD分离到第一个提交,而如果使用git checkout HEAD^2,就将会分离HEAD到第二个提交。

另外,符号^~支持链式操作。
比如,master~^2~2相当于把HEAD分离到下图所示位置:
git命令与使用_learn_git_branching_notes

再罗嗦一下,这个commitpointer~^2~2是一个提交的引用,不止可以使用在checkout上面。

纠缠不清的分支

是一个关于提交记录的修改,需要把master上面的一些特定提交添加到其他几个分支上。
是一个对git checkoutgit cherry-pickgit branch -f的一个练习。

同样,如果未来的某一天你还是感兴趣,就点击中的链接自己去体验一下吧~


下面我们来讨论一下对远程仓库处理的问题吧!

远程仓库初级:Push & Pull

Clone

使用git clone来获取远程仓库的本地副本,可以使用https://或者git://协议来进行通信。

一个例子:git clone https://github.com/yayi2456/Example.git

远程分支

本地仓库如果连接到远程,将会存在远程分支,远程分支的命名方式是<remote name>/<branch name>。远程仓库一般都会被命名为origin
远程分支是本地分支上一次与远程仓库交互时远程仓库状态的反映,因此远程分支的一个重要特点是,在检出(checkout)到该分支的时候自动分离HEAD。这是因为不能直接在远程分支上进行操作,必须在其他地方操作,随后对远程分支在远程仓库中进行更新,这样本地的远程分支才能更新。也就是:远程分支只跟随远程仓库更新。
给一个这样的例子:看到,在本地对远程分支commit的时候,HEAD分离,分支本身不变。
git命令与使用_learn_git_branching_notes

Fetch

使用git fetch从远程仓库获取数据。该命令将会从默认远程仓库拉取提交记录更新,提交记录的伴随着本地仓库远程分支的指向更新。
但是git fetch不会改变本地仓库的状态。换言之,本地分支是不会被改变的。

git fetch使用https://或者git://协议来进行通信。

Pull

使用git pull完成远程仓库中分支抓取以及与本地分支的合并。
这一操作也可以通过git fetch;git merge origin/master完成。不过既然提供了git pull,建议使用git pull

模拟团队合作

一个练习。模拟多人在本地以及远程仓库上的操作,更像实际的git使用。包括git clonegit fetchgit pull等使用。无新知识,不多介绍。

同样,如果未来的某一天你还是感兴趣,就点击中的链接自己去体验一下吧~

Push

git pushgit pull相对,它从本地仓库推送更新到远程仓库,并合并。
注意,当远程仓库更新的时候,本地仓库的远程分支也更新。

偏离的提交历史

远程仓库是多人协作的,那么就免不了有冲突的提交记录。当程序员想将自己的本地分支push到远程分支的时候,可能因为其他协作者已经更改了远程仓库的远程分支,远程仓库的远程分支的最新提交已经和程序员本地的远程分支不一致了,本次push将会失败。
如下图所示:(虚线代表远程仓库)

git命令与使用_learn_git_branching_notes
这个时候更新远程仓库就需要git fetch的帮助了。
为了成功更新远程仓库,需要更改本地仓库的提交顺序,使得本地仓库的更新提交依赖于最新的远程分支。

首先使用git fetch把本地仓库的远程分支更新,随后使用git rebase origin/master来使得自己的master基于远程分支的提交,最后使用git push

或者可以:使用git fetch获取最新的远程分支状态,随后使用git merge origin/master把本地主分支合并到远程主分支,最后git push

或者可以使用git pull,随后git push

再复习一遍,git pull=git fetch+git merge origin/master

或者使用git pull --rebase,随后git push
git pull --rebase,是git fetchgit rebase origin/master的组合效果。

锁定的Master

在大项目中,为了保持整个远程仓库不至于混乱,经常不允许直接将本地提交直接push,通常通过锁定master来实现。而是只允许使用Pull Request来更新分支。

如果收到了错误信息:! [远程服务器拒绝] master -> master (TF402455: 不允许推送(push)这个分支; 你必须使用pull request来更新这个分支.),那么就意味着程序员不能使用git push来更新远程仓库了。

使用Pull Request更新,首先需要把更新全部放在一个新建分支,随后push该分支,并且申请Pull Request

如果程序员忘记使用新分支,而是把所有commit都放在了主分支,需要首先新建一个分支feature指向当前HEAD,随后把master分支运行git reset和远程服务器保持一致,最后切换到新分支featuregit checkout feature,使用git push origin feature把分支feature推送到远程服务器。

这个过程稍微复杂,结合图来看一下:
首先,程序员不小心在master分支提交:

git命令与使用_learn_git_branching_notes

为了能够申请Pull Request,首先新建一个分支,叫做feature,执行git branch feature
git命令与使用_learn_git_branching_notes

随后把主分支撤销到远程主分支的位置,运行git reset o/master
git命令与使用_learn_git_branching_notes

最后切换到feature分支,并推送到远程仓库,运行git checkout feature;git push origin feature

git命令与使用_learn_git_branching_notes

远程仓库进阶:origin与其周边

合并远程仓库

在开发中,经常会有从主分支分出特性分支,对特性分支开发完成后再合并到主分支的操作。
但是也有一些开发者只在主分支开发。
不过目标是一致的,那就是合并到主分支。
这个过程中,可能还需要从远程仓库拉取本地没有的主分支提交记录。

这个游戏可以锻炼对git pull --rebasegit rebase nomove movegit push origin master的熟练程度。

同样,如果未来的某一天你还是感兴趣,就点击中的链接自己去体验一下吧~

为了把本地更新push到远程仓库,程序员需要做的就是包含远程仓库中的最新变更。为了包含最新变更,无论是使用rebase还是使用merge是没有限制的。

rebase的优点是:提交树十分干净;缺点是:修改了提交的历史。
于是,有些程序员喜欢使用rebase,因为显得历史记录干净整洁,另一些喜欢使用merge,因为可以保留完整的提交顺序历史。

远程跟踪

本地分支master与本地的远程分支origin/master以及远程仓库的分支origin/master是关联的。后两个很容易理解,那么前两个是如何被关联的呢?

早在克隆仓库的时候,git首先在本地为远程仓库的每个分支创建一个远程分支,然后再创建一个跟踪远程仓库中活动分支的本地分支。本地仓库的这两个分支通过属性remote tracking决定。

可以通过手动设置的方式关联本地分支和远程分支。
使用git checkout -b newbranchname origin/branchname来将两个分支关联。也可以使用git branch -u origin/branchname newbranchname来将二者关联。
这样关联之后,原来的branchnameorigin/branchname就取消关联了。在本地分支branchname所做的pullpush等操作也与origin/branchname无关了。

git push的参数

git push [remote] [place]

若指定remote以及place,则忽略此时检出的本地分支,将本地分支place支的所有提交拿出来,将place所关联的远程分支中没有的提交添加到远程分支。

若不指定,则将本地检出分支的提交拿出,添加所有默认远程仓库中与当前检出分支关联的远程分支中没有的提交到该远程分支。

也就是说:place是指本地分支名称,remote是指远程仓库名称。

如果希望同时指定本地分支的位置和远程分支的位置,可以使用命令git push remotename sourcepointer:destbranchname,其中sourcepointer是一个commitpointerdestbranchname是一个远程分支名。该指令允许远程仓库在分支名destbranchname不存在的时候在远程仓库新建该分支。
另外,sourcepointer可以留空,也即使用git push remotename :destbranchname,代表删除远程仓库中的destbranchname分支。当然本地仓库的对应远程分支也会被删除。

git fetch的参数

可以使用类似于git pushgit fetch [remote] [place]git fetch remotename sourcepointer:destbranchname

pushplace是针对本地分支而言,但fetchplace是针对远程分支而言的。git fetch origin branchname将会从远程仓库寻找分支branchname,拉取数据到本地的origin/branchname分支。

git fetch remotename sourcepointer:destbranchname将从远程分支sourcepointer拿数据,放在本地分支destbranchname。不过尽量不要这样使用git fetch默认更改本地仓库的远程分支是有原因的。
同样的,留空sourcepointer,使用命令git fetch remotename :destbranchname可以在本地仓库创建一个本地分支destbranchname

如果不给git fetch加参数,那么默认下载所有分支。这与git push不同,git pushpush当前检出的分支。

git pull的参数

正如之前说的,git pull = git fetch + git merge

git pull [remote] [place]代表从远程仓库中拿到远程分支remote/place并更新本地仓库的远程分支,并将remote/place``merge当前检出的本地分支(不一定是place本地分支)。

同样可使用git pull remotename sourcepointer:destbranchname,就相当于git fetch remotename sourcepointer:destbranchname; git merge destbranchname

?? TADA!完成撒花!??????????

REF

本文整理自:LEARN GIT BRANCHING,是一个帮助你快速掌握常见、不常见的GIT命令的小游戏
如果感兴趣,也可以访问他们的GIT REPOSITORY来获得源码,参与贡献。

查看markdown语法参考

相关推荐