git命令与使用_learn_git_branching_notes
基础篇
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
包含原master
和branchname
的提交记录。
为了更直观,将执行git merge bugFix
前后的代码库展示如下:
Rebase
除了merge
,还有一种合并分支的方法,就是rebase
。rebase
做的事情是,取出一系列的提交记录,“复制”他们,然后再另一个地方按顺序逐个放下去。rebase
的作用,是能够使整个提交记录更加线性。使用git rebase branchname
能够把当前分支所有与branchname
不同的提交记录按照原顺序放在branchname
分支之后,然后更新当前分支的头。
给出执行git rebase master
前后的变化(当前在bugFix
分支:
接着再执行git checkout master; git rebase bugFix
就可以把master
移动到和bugFix
一样的位置,或者直接使用git rebase tocommitpointer fromcommitpointer
把fromcommitpointer
所指分支移动到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 checkout branchname~[num]
允许向前num
次移动。num
为可选项,如果不填写,效果就和^
是一样的。
这样向前的引用一个重要的作用就是强制移动分支指向的提交。使用命令git branch -f branchname commitpointer
来使分支branchname
强制指向commitpointer
。commitpointer
是一个提交的指针,它可以是commit
的哈希值,可以是一个相对引用,也可以是HEAD
或者其他分支名。
撤销变更
git对变更的撤销包括对暂存区的文件的改变以及撤销变更的方法。这里主要关注是用什么命令可以撤销变更。
Reset
第一种撤销变更的方法是git reset commitpointer
,reset
的效果是,直接把当前分支强制移动到所指示的提交位置。
git reset HEAD~
执行前后结果如下:
Revert
第二种方式是git revert commitpointer
,revert
的效果是,新增一个提交,该提交的效果是撤销commitpointer
的提交,移动当前分支到最新提交。
git revert HEAD
执行前后结果如下:
为什么有两种方式呢?因为reset
的效果是,一旦撤销就不再有被撤销的记录了,一般用于本地开发时进行回退撤销;而revert
则保留了所有的提交记录,一般用于多人协同开发时的代码修改。
移动提交记录
Cherry-pick
使用git cherry-pick commitpointer1 commitpointer2 ...
来将一系列提交按照顺序添加到当前分支(在当前分支做提交修改的操作总是会移动分支名指向,永远指向最近的一个提交记录)。
这些commitpointer
不能是当前HEAD
的祖宗提交。
交互式Rebase
使用git rebase -i
来使用交互式的rebase
。交互式的rebase
像是rebase
和cherry-pick
的结合。
当使用git rebase -i commitpointer
的时候,打开一个交互式界面,在该交互式界面中可以从指定的这些提交(从当前分支的最近一次提交到commitpointer
指向的那个提交之前的一个提交,不包括commitpointer
指向的提交)中选择一些,并决定他们的排列顺序或是否合并(merge
)。
与cherry-pick
不同的是,rebase -i
选择出提交序列之后,不是直接将提交添加到当前分支,而是将这些提交以commitpointer
作为父节点,按顺序添加,并将当前分支切换到该提交序列的最近一个。
执行git rebase -i HEAD~4
,不改变默认顺序与提交的前后对比:
杂项
只做一个提交记录
现在有一个场景:在正常编程中,发现了BUG,于是程序员新建一个分支debug
,在该分支中进行DEBUG。在这个过程中,程序员认为需要输出信息,于是在写输出信息的代码之前进行了一次提交。
提交之后,程序员立即创建了新分支pintf
,在该分支中进行错误信息输出调试。程序员认为输出信息打印语句写完了,于是再一次提交。
提交之后立即创建了新分支bugFix
,在该分支中进行代码修改。历经千辛万苦,终于把这个bug修复了,程序员立即提交。
这时,只给出每一个分支的最后一次提交,可视化为:
现在,程序员想把BUG处理合并到主分支,但是面临一个问题:程序员不希望把错误信息输出也合并。
这个时候,就可以使用git rebase -i
或者git cherry-pick
来解决问题了。
首先考虑git rebase -i
。git rebase -i
只能对当前分支之前的提交做操作,因此首先需要在bugFix
分支执行命令git rebase -i master
来调出UI界面,在该界面中,程序员选取所有pintf
之后,bugFix
之前的提交,按照原顺序,添加到指定的位置——master
之后。这样,就相当于提交C4
与C2
、C3
毫无关系了,它是直接接在C1
提交之后的。
为什么
C4
变成了C4‘
呢?这是因为没一次提交都可能是与前一次提交的差异信息,当C4
的前置节点变化,存储的变更信息也变化。无论存储信息是否变化,这里只要对提交做了顺序、内容上的修改,都是用加了‘
的表示。
这样,就变成:
这个时候,就可以使用git branch -f master bugFix
来强制移动master
的位置,或者使用git checkout master;git rebase bugFix
或者使用git checkout master;git merge bugFix
等很多操作都可以使master
前进一步。
最终结果就是(使用git branch -f master bugFix
):
下面再说使用cherry-pick
。
很简单,首先使用git checkout master
切换到主分支,然后使用git cherry-pick bugFix
即可。
提交技巧:修改以前的提交
一个场景是,已经提交过一次的代码,由于某些原因需要修改小小的参数。这个时候需要把这次修改添加到之前的某一次提交中。但是最新的提交已经不是那次提交了。
初始的时候,视图如下:
程序员希望在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 commit --amend
提交新内容,本次加了选项--amend
的提交不会创建一个新的提交记录,而是更改最近一个提交记录。或者说是以最近一个提交记录的父记录为前置记录创建一个新提交记录,原来的那个提交记录就不再使用了。
执行git commit --amend
之后效果如下:
随后再次使用git rebase -i newImage~
或者git rebase -i master
来重排提交记录,把顺序还原。如下:
最后使用一种方法把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 commit --amend
。在rebase -i
已经展示过效果,这里就不放图片了。
最后将最新的提交拿到新的提交分支上,使用git cherry-pick caption
即可,如下图:
Tag
tag
类似于branch
,也是对某一个提交记录的指向,但是。正如前面一直讨论的,分支往往只是一个临时的对某一个提交记录的指向,很容易被更改。tag
则相当于一个里程碑,其指向不能随意更改,往往用于标志程序设计中的重大版本发布。
使用git checkout tagname
将会使得HEAD
指针分离到tagname
所指示的提交,而不是“切换”到该tag
。以C++
中的术语比喻,tag
只是一个高层常指针,它本身不能更换指向。从这个角度来看,tag
与branch
有明显的区别。
下面来探寻使用tag
来为一个特定的提交命名,并且像使用branch
一样使用tag
。
使用git tag tagname [commitpointer]
来创建一个指向commitpointer
的标签(tag
)。如果不指定一个提交,那么默认指向当前分支的HEAD
。
Describe
使用git describe [commitpointer]
指令来找到距离commitpointer
最近的一次tag
(tag
一定在commitpointer
之前。)同样的,不指定commitpointer
的时候,默认是HEAD
。
该指令输出<tag>_<numCommits>_g<hash>
。
其中,<tag>
就是那个距离commitpointer
最近的标签名称,<numCommits>
是tag
与commitpointer
之间相差的提交个数,<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分离到下图所示位置:
再罗嗦一下,这个commitpointer~^2~2
是一个提交的引用,不止可以使用在checkout
上面。
纠缠不清的分支
是一个关于提交记录的修改,需要把master
上面的一些特定提交添加到其他几个分支上。
是一个对git checkout
,git cherry-pick
和git 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
分离,分支本身不变。
Fetch
使用git fetch
从远程仓库获取数据。该命令将会从默认远程仓库拉取提交记录更新,提交记录的伴随着本地仓库远程分支的指向更新。
但是git fetch
不会改变本地仓库的状态。换言之,本地分支是不会被改变的。
git fetch
使用https://
或者git://
协议来进行通信。
Pull
使用git pull
完成远程仓库中分支抓取以及与本地分支的合并。
这一操作也可以通过git fetch;git merge origin/master
完成。不过既然提供了git pull
,建议使用git pull
。
模拟团队合作
一个练习。模拟多人在本地以及远程仓库上的操作,更像实际的git
使用。包括git clone
、git fetch
、git pull
等使用。无新知识,不多介绍。
同样,如果未来的某一天你还是感兴趣,就点击中的链接自己去体验一下吧~
Push
git push
与git pull
相对,它从本地仓库推送更新到远程仓库,并合并。
注意,当远程仓库更新的时候,本地仓库的远程分支也更新。
偏离的提交历史
远程仓库是多人协作的,那么就免不了有冲突的提交记录。当程序员想将自己的本地分支push
到远程分支的时候,可能因为其他协作者已经更改了远程仓库的远程分支,远程仓库的远程分支的最新提交已经和程序员本地的远程分支不一致了,本次push
将会失败。
如下图所示:(虚线代表远程仓库)
这个时候更新远程仓库就需要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 fetch
和git 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
和远程服务器保持一致,最后切换到新分支feature
,git checkout feature
,使用git push origin feature
把分支feature
推送到远程服务器。
这个过程稍微复杂,结合图来看一下:
首先,程序员不小心在master
分支提交:
为了能够申请Pull Request
,首先新建一个分支,叫做feature
,执行git branch feature
:
随后把主分支撤销到远程主分支的位置,运行git reset o/master
:
最后切换到feature
分支,并推送到远程仓库,运行git checkout feature;git push origin feature
:
远程仓库进阶:origin与其周边
合并远程仓库
在开发中,经常会有从主分支分出特性分支,对特性分支开发完成后再合并到主分支的操作。
但是也有一些开发者只在主分支开发。
不过目标是一致的,那就是合并到主分支。
这个过程中,可能还需要从远程仓库拉取本地没有的主分支提交记录。
这个游戏可以锻炼对git pull --rebase
、git rebase nomove move
、git 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
来将二者关联。
这样关联之后,原来的branchname
与origin/branchname
就取消关联了。在本地分支branchname
所做的pull
、push
等操作也与origin/branchname
无关了。
git push
的参数
git push [remote] [place]
。
若指定remote
以及place
,则忽略此时检出的本地分支,将本地分支place
支的所有提交拿出来,将place
所关联的远程分支中没有的提交添加到远程分支。
若不指定,则将本地检出分支的提交拿出,添加所有默认远程仓库中与当前检出分支关联的远程分支中没有的提交到该远程分支。
也就是说:
place
是指本地分支名称,remote
是指远程仓库名称。
如果希望同时指定本地分支的位置和远程分支的位置,可以使用命令git push remotename sourcepointer:destbranchname
,其中sourcepointer
是一个commitpointer
。destbranchname
是一个远程分支名。该指令允许远程仓库在分支名destbranchname
不存在的时候在远程仓库新建该分支。
另外,sourcepointer
可以留空,也即使用git push remotename :destbranchname
,代表删除远程仓库中的destbranchname
分支。当然本地仓库的对应远程分支也会被删除。
git fetch
的参数
可以使用类似于git push
的git fetch [remote] [place]
、git fetch remotename sourcepointer:destbranchname
。
push
的place
是针对本地分支而言,但fetch
的place
是针对远程分支而言的。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 push
只push
当前检出的分支。
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来获得源码,参与贡献。