git原理解密

前言
近几年技术发展十分迅猛,让部分同学养成了一种学习知识停留在表面,只会调用一些指令的习惯。我们时常有一种“我会用这个技术、这个框架”的错觉,等到真正遇到问题,才发现事情没有那么简单。

而Git也是一个大部分人都知道如何去使用它,知道有哪些命令,却只有少部分人知道具体原理的东西。了解一些底层的东西,可以更好的帮你理清思路,知道你真正在操作什么,不会迷失在Git大量的指令和参数上面。

Git是怎么储存信息的

这里会用一个简单的例子让大家直观感受一下git是怎么储存信息的。

首先我们先创建两个文件

$ git init
$ echo '111' > a.txt
$ echo '222' > b.txt
$ git add *.txt

Git会将整个数据库储存在.git/目录下,如果你此时去查看.git/objects目录,你会发现仓库里面多了两个object。

$ tree .git/objects
.git/objects
├── 58
│   └── c9bdf9d017fcd178dc8c073cbfcbb7ff240d6c
├── c2
│   └── 00906efd24ec5e783bee7f23b5d7c941b0c12c
├── info
└── pack

好奇的我们来看一下里面存的是什么东西

$ cat .git/objects/58/c9bdf9d017fcd178dc8c073cbfcbb7ff240d6c
xKOR0a044K%

怎么是一串乱码?这是因为Git将信息压缩成二进制文件。但是不用担心,因为Git也提供了一个能够帮助你探索它的api git cat-file [-t] [-p], -t可以查看object的类型,-p可以查看object储存的具体内容。

$ git cat-file -t 58c9
blob
$ git cat-file -p 58c9
111

可以发现这个object是一个blob类型的节点,他的内容是111,也就是说这个object储存着a.txt文件的内容。

这里我们遇到第一种Git object,blob类型,它只储存的是一个文件的内容,不包括文件名等其他信息。然后将这些信息经过SHA1哈希算法得到对应的哈希值
58c9bdf9d017fcd178dc8c073cbfcbb7ff240d6c,作为这个object在Git仓库中的唯一身份证。

也就是说,我们此时的Git仓库是这样子的:
在这里插入图片描述
我们继续探索,我们创建一个commit。

$ git commit -am '[+] init'
$ tree .git/objects
.git/objects
├── 0c
│   └── 96bfc59d0f02317d002ebbf8318f46c7e47ab2
├── 4c
│   └── aaa1a9ae0b274fba9e3675f9ef071616e5b209
...

我们会发现当我们commit完成之后,Git仓库里面多出来两个object。同样使用cat-file命令,我们看看它们分别是什么类型以及具体的内容是什么。

$ git cat-file -t 4caaa1
tree
$ git cat-file -p 4caaa1
100644 blob 58c9bdf9d017fcd178dc8c0...     a.txt
100644 blob c200906efd24ec5e783bee7...    b.txt

这里我们遇到了第二种Git object类型——tree,它将当前的目录结构打了一个快照。从它储存的内容来看可以发现它储存了一个目录结构(类似于文件夹),以及每一个文件(或者子文件夹)的权限、类型、对应的身份证(SHA1值)、以及文件名。

此时的Git仓库是这样的:
在这里插入图片描述

$ git cat-file -t 0c96bf
commit
$ git cat-file -p 0c96bf
tree 4caaa1a9ae0b274fba9e3675f9ef071616e5b209
author lzane 李泽帆  1573302343 +0800
committer lzane 李泽帆  1573302343 +0800
[+] init

接着我们发现了第三种Git object类型——commit,它储存的是一个提交的信息,包括对应目录结构的快照tree的哈希值,上一个提交的哈希值(这里由于是第一个提交,所以没有父节点。在一个merge提交中还会出现多个父节点),提交的作者以及提交的具体时间,最后是该提交的信息。

此时我们去看Git仓库是这样的:
在这里插入图片描述
到这里我们就知道Git是怎么储存一个提交的信息的了,那有同学就会问,我们平常接触的分支信息储存在哪里呢?

$ cat .git/HEAD
ref: refs/heads/master
$ cat .git/refs/heads/master
0c96bfc59d0f02317d002ebbf8318f46c7e47ab2

在Git仓库里面,HEAD、分支、普通的Tag可以简单的理解成是一个指针,指向对应commit的SHA1值。
在这里插入图片描述

其实还有第四种Git object,类型是tag,在添加含附注的tag(git tag -a)的时候会新建,这里不详细介绍,有兴趣的朋友按照上文中的方法可以深入探究。

至此我们知道了Git是什么储存一个文件的内容、目录结构、commit信息和分支的。其本质上是一个key-value的数据库加上默克尔树形成的有向无环图(DAG)。

接下来我们来看一下Git的三个分区(工作目录、Index 索引区域、Git仓库),以及Git变更记录是怎么形成的。了解这三个分区和Git链的内部原理之后可以对Git的众多指令有一个“可视化”的理解,不会再经常搞混。

接着上面的例子,目前的仓库状态如下:
在这里插入图片描述

这里有三个区域,他们所储存的信息分别是:

工作目录 ( working directory ):操作系统上的文件,所有代码开发编辑都在这上面完成。
索引( index or staging area ):可以理解为一个暂存区域,这里面的代码会在下一次commit被提交到Git仓库。
Git仓库( git repository ):由Git object记录着每一次提交的快照,以及链式结构记录的提交变更历史。
我们来看一下更新一个文件的内容这个过程会发生什么事。

在这里插入图片描述
运行echo “333” > a.txt将a.txt的内容从111修改成333,此时如上图可以看到,此时索引区域和git仓库没有任何变化。

在这里插入图片描述
运行git add a.txt将a.txt加入到索引区域,此时如上图所示,git在仓库里面新建了一个blob object,储存了新的文件内容。并且更新了索引将a.txt指向了新建的blob object。

在这里插入图片描述
运行git commit -m 'update’提交这次修改。如上图所示

Git首先根据当前的索引生产一个tree object,充当新提交的一个快照。
创建一个新的commit object,将这次commit的信息储存起来,并且parent指向上一个commit,组成一条链记录变更历史。
将master分支的指针移到新的commit结点。
至此我们知道了Git的三个分区分别是什么以及他们的作用,以及历史链是怎么被建立起来的。基本上Git的大部分指令就是在操作这三个分区以及这条链。

Git分支

Git基础 中介绍了Git提交时会保存一个提交对象,这个对象记录着提交版本的目录结构指针以及文件快照指针。

Git 的分支,其实本质上仅仅是指向提交对象的可变指针。 Git 的默认分支名字是 master。 在多次提交操作之后,你其实已经有一个指向最后那个提交对象的 master 分支。 它会在每次的提交操作中自动向前移动。
在这里插入图片描述

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

分支创建

Git 创建新分支很简单,它只是为你创建了一个可以移动的新的指针。 比如,创建一个 feature 分
支:

$ git branch feature

这会在当前所在的提交对象上创建一个指针。
在这里插入图片描述
那么,Git 又是怎么知道当前在哪一个分支上呢? 也很简单,它有一个名为 HEAD 的特殊指针。
HEAD指针指向当前所在的本地分支(可以将 HEAD 想象为当前分支的别名)。 在上述例子中,你仍然在 master 分支上。 因为 git branch 命令仅仅创建 一个新分支,并不会自动切换到新分支中去。
在这里插入图片描述

既然HEAD指针是指向当前分支的,那么要切换到一个已存在的分支只需要将HEAD指针移动指向这个分支就可以了,使用 git checkout 命令切换到新创建的 feature 分支去:

$ git checkout feature

这样 HEAD 就指向 feature 分支了。

在这里插入图片描述

在分支上进行提交时,当前分支向前移动,其它分支保持不动。

在这里插入图片描述

此时切换到其它分支,使用 git checkout 命令。这个命令做了两件事, 一是使 HEAD 指向目标分支,二是将工作目录恢复成目标分支所指向的快照内容。这样就支持了多分支的非线性开发,而且分支间的切换也非常快。

在这里插入图片描述

同时开发多个分支,提交历史就会出现分叉,因为可能有多个commit对象指向了同一个父对象

在这里插入图片描述

分支合并

Tree-Way Merge
假设有两个同学在各自的分支上对同一个文件进行修改,如下图:
在这里插入图片描述
这个时候我们需要合并两个分支成一个分支,如果我们只对这两个文件进行对比,那么在代码合并时,只知道这两个文件在第30行有差异,却不知道应该采纳谁的版本。

如果我知道这个文件的“原件”,那么通过和“原件”代码的对比就能推算出应该采用谁的版本:
在这里插入图片描述

图示可以看出,Mine中的代码和Base一样,说明Mine中并没有对这行代码做修改,而Yours中的代码和Base不一样,说明Yours在Base的基础上对这行代码做了修改,那么Yours和Mine合并应该采用Yours中的内容。

当然还有一种情况是三个文件的代码都不相同,这就需要我们自己手动去解决冲突了:

在这里插入图片描述
从上面的例子可以看出采用Tree-Way-Merge(也称为三向合并)原理来合并代码有个重要前提是可以找到两份代码的“原件”,而git因为记录了文件的提交历史,再通过自身的合并策略就可以找到两个commit的公共commit是哪个,从而通过比对代码来进行合并。

那么后面我们就来详细说一下git的合并策略是怎么推算出公共commit的。

git的合并策略
git会有很多合并策略,最常见的几种是 Fast-foward,Recursice,Ours,Octopus 。默认git会帮你自动挑选合适的合并策略,也可以通过git merge -s 策略名字来强指定使用的策略类型。

Fast-foward
在这里插入图片描述

Fast -foward是最简单的一种合并策略,如图将dev分支合并到master分支上,git只需要将master分支的ref指向最后一个commit节点上:

在这里插入图片描述

Fast-forward是git在合并两个没有分叉的分支时的默认行为,如果你想禁用掉这种行为,明确拥有一次合并的commit记录,可以使用git merge --no-ff命令来禁用掉。

Recursive
Recursive是git中最重要也是最常用的合并策略,简单概述为:通过算法寻找两个分支的最近公共祖先节点,再将找到的公共祖先节点作为base节点使用三向合并的策略来进行合并。

举个例子:圆圈里的字母为当前commit中的内容,当我们要合并2,3两个分支时,先找到他们的公共祖先节点1,接着和节点1的内容进行对比,因为1的内容是A,所以2并没有修改内容,而3将内容改成B,所以最后的合并结果的内容也是B。

在这里插入图片描述

但实际的情况总是复杂的多的,会出现几个分支相互交叉的情况(Criss-Cross现象)

在这里插入图片描述

如上图所示,当我们在寻找最近公共祖先时,可以找到两个节点:节点2和节点3。

如果我们以节点2作为base节点,如下图:

在这里插入图片描述

此时通过三向合并策略合并(base节点的内容是A,两个待合并分支节点的内容是B和C)我们是无法得出应该使用哪个节点内容的,需要自己手动解决冲突。

而如果使用节点3作为base节点,那么通过三向合并策略合并(base节点的内容是B,两个待合并分支节点的内容是B和C)可以得出应该使用C来作为最终结果:

在这里插入图片描述

查看两个分支的最近公共祖先可以是使用命令 git merge-base --all branch_1 branch_2
作为人类,其实我们很容易看出正确的合并结果应该是C,那么git要如何保证自己能找到正确的base节点,尽可能的减少代码的合并冲突呢?

实际上git在合并时,如果查找发现满足条件的祖先节点不唯一,那么git会首先合并满足条件的祖先节点们,将合并完的结果作为一个虚拟的base节点来参与接下来的合并。

如下图:git会首先合并节点2和节点3,找到他们的公共祖先节点1,在通过三项合并策略得到一个虚拟的节点8,内容是B,再将节点8作为base节点,和节点5,节点6合并,比较完后得出最终版本的内容应该是C。

在这里插入图片描述

Ours & Theirs参数
在合并时我们可以带上-Xours, -Xtheirs参数,表明合并遇到冲突时全部使用其中一方的更改。如下图在master分支下执行git merge -Xours dev,最后产生的节点内容将自动采取master分支上的内容而不需要你再手动解决冲突。

在这里插入图片描述

-Xtheirs参数和-Xours完全相反,遇到冲突时他会自动采取dev上的内容。注意这两个参数只有遇到冲突时才会生效,这和我们下面提到的Ours策略不一样

Ours
Ours 策略和上文提到的-Xours参数非常相像,不同的是-Xours参数是只有合并遇到冲突时,git会自动丢弃被合并分支的更改保留原有分支上的内容,如果没有冲突,git还是会帮我们自动合并的。

而Ours策略是无论有没有冲突,git会完全丢弃被合并分支上的内容,只保留合并分支的上的修改,只是在commit的记录上会保留另一个分支的记录。

如下图在master分支下执行git merge -s ours dev,最后产生的合并节点其内容和master分支上一个节点完全一样。

在这里插入图片描述

这种策略的应用场景一般是为了实现某个功能,同时尝试了两种方案,最终决定选择其中一个方案,而又希望把另一个方案的commit记录合进主分支里方便日后的查看。

为什么没有Theirs策略
既然合并的时候即有-Xtheirs参数又有-Xours参数,所以下意识的觉得git即有 Ours 策略也会有 Theirs 策略,实际上git曾经有过这个策略后来舍弃了,因为Theirs会完全丢掉当前分支的更改,是一个十分危险的操作,如果你真的想丢弃掉自己的修改,可以使用reset命令来代替它。

Octopus
Octopus 策略可以让我们优雅的合并多个分支。前面我们介绍的策略都是针对两个分支的,如果现在有多个分支需要合并,使用Recursive策略进行两两合并会产生大量的合并记录:每合并其中两个分支就会产生一个新的记录,过多的合并提交出现在提交历史里会成为一种“杂音“,对提交历史造成不必要的”污染“。

Octopus在合并多个分支时只会生成一个合并记录,这也是git合并多个分支的默认策略。如下图:在master分支下执行git merge dev1 dev2:

在这里插入图片描述

git rebase
看完git merge 的策略后,再看看另一个合并代码时常用的命令git rebase。git rebase和merge最大的不同是它会改变提交的历史。

如下图:在dev上rebase master时,git会以master分支对应的commit节点作为起点,将dev上commit节点”平移“至master commit的后面,并且会创建全新的commit节点来替代之前commit:
在这里插入图片描述

接下来我们再来看一下”平移“的过程中git需要做的事情:首先我们需要以commit5作为base节点,commit1和commit6进行合并生成新的commit3,然后再将commit3的parent指向commit6。commit2到commit4转变进行了同样的步骤。因为相比较之前的commit,新的commit的parent变了,对应的hash值自然也变了。

所以我们在rebase的时候,你当前分支有几个commit记录那么git就需要进行合并几次。如果你当前分支比较”干净“,只有一个commit记录的话,那么你rebase需要解的冲突其实和merge是一样的,区别就是rebase不会单独生成一个新的commit来记录这次合并。

关于git pull master --rebase和git rebase master的区别,git pull --rebase相当于git fetch + git rebase,正常的git pull相当于git fetch + git merge。

至于什么时候用rebase什么时候用merge,我的理解是:开发只属于自己的分支时尽量使用rebase,减少无用的commit合到主分支里,多人合作时尽量使用merge,一方面减少冲突,另一个方面也让每个人的提交有迹可循。

git rebase还有一个功能是可以合并commit记录:git rbase -i HEAD~n。合并分支还有一个办法是git merge --squash,区别是merge --squash会将你之前所有的记录压缩成一个新的commit,而rebase具体要怎么压缩可操作性比较高,这里就不多展开论述了。

总结
最后我们来看一个新手合并代码经常会遇到的问题:

小明在他的开发分支上完成了一个开发功能并且合上了master分支,后来发现新开发的代码有点问题,于是小明执行了revert操作:将master需要回退到没有合并时的版本,并继续在之前的开发分支上修复了问题:

在这里插入图片描述

这个时候他再试图把dev分支往master上合并时,会发现B节点上新增的内容莫名其妙的就丢失了。根据git的合并策略我们就很容里理解这个问题:

在合并两个有分叉的分支(上图中的D和A),git会默认选择Recursive策略来进行合并,对于D和A他们的最近父节点是B,以B为base节点,对D和A做三项合并,B中拥有“B”的内容,D中也拥有“B”的内容,A中将“B”的内容丢弃,所以合并的结果就是将“B”的内容丢弃。

根据原理解决的方案也有很多,最简单的一种在节点D合并A前,先revert一下生成A(revert的revert),再继续合并就没问题了,或者修复问题时从A节点单独拉一个新分支修复,而不是在之前dev分支上继续开发。

远程分支

1、远程仓库目前提交历史:

在这里插入图片描述

因为远程仓库没有进行过推送所以目前的提交历史还是上图的状态。

张三和李四本地提交历史中的origin/master就是对应着远程仓库master分支。

2、张三执行push操作
张三执行命令git push origin master
在这里插入图片描述

因为本地的master跟远程分支origin/master是关联起来的,origin/master就对应着远程仓库的master分支,所以执行上述命令之后,实际上会做两件事情:

 第一是修改本地的origin/master分支。

 第二是将本地的commit提交历史推送到远程仓库,修改远程仓库的master指针。

上图中注意origin/master所在的commit,发现它到了fb7c243这个commit上面了。提交历史如下图所示:

在这里插入图片描述

因为origin/master和master是关联起来的,只要一执行git push origin master命令,首先在本地就会将origin/master的指针移动到本地的master分支一样。同时会将本地的commit提交历史推送到远程仓库里面去,远程仓库的master也指向了最新的commit。

3、李四执行push操作
a. 李四执行命令git push origin master后会报错。
在这里插入图片描述
报错的意思就是说,远程的仓库里的master分支的代码已经有人提交过了,然后此时是不能直接推送新的代码到master分支的需要先执行git pull命令;

b. 李四执行git pull
此时的git pull会做两件事情:

第一件事情,将远程仓库的commit提交历史拉取下来跟本地的提交历史进行合并。

第二件事情,将本地的master分支对应的commit跟远程仓库的master分支对应的commit进行合并

在这里插入图片描述

此时git pull会执行一下merge操作,我们来看一下李四的log

在这里插入图片描述

李四把master中的commit树拉倒了本地,然后会做一个merge,因为本地的master与origin/master不在一条commit线上所以说要进行一个合并操作。

李四的提交历史图,如下图所示:

在这里插入图片描述

因为要进行合并操作,所以会多出来一个commit,是origin/master与本地master对应的commit合并而来的commit。
在这里插入图片描述

c. 李四再次执行命令git push origin master

此时远程仓库的提交历史就会把李四的commit树加入进去:
在这里插入图片描述

4、张三执行git pull
张三做最后一个操作执行一次git pull,将远程仓库的提交历史拉取到本地进行合并,让本地仓库跟远程仓库保持一致。

在这里插入图片描述

远程分支冲突解决
常见于这样的一种场景,同一个分支中张三与李四修改了同一行代码,张三提交并push到了远程仓库。李四此时要push报错,要先git pull。李四跟张三的master分支代码合并的时候发生了冲突,不知道该听谁的。所以说,这个时候就需要进行冲突的解决。

李四进行push操作:

在这里插入图片描述

李四进行pull操作:
在这里插入图片描述
发现有冲突在TestController中。

在这里插入图片描述

<<<<<<< HEAD  System.out.println("我是李四 发现了bug");======  System.out.println("我是张三发现bug");>>>>>> 23a5e60e6b8fe8ad916bc01cf5ab7f0595be8e87

这里的HEAD指的意思是说,李四本地的HEAD指针指向的分支对应的commit中的代码。

23a5e60e6b8fe8ad916bc01cf5ab7f0595be8e87,指的是张三修改后提交的commit对应的hash值。

删除特殊符号即可解决冲突:

在这里插入图片描述

再进行提交和push即可。

解决冲突的原理其实就是李四pull下来的commit树发现有冲突后根据当前分支与远程分支进行合并在本地形成一个新的commit。再对这个commit进行push到远程仓库中。

Git常用命令总结

git init
在本地新建一个repo,进入一个项目目录,执行git init,会初始化一个repo,并在当前文件夹下创建一个.git文件夹.

git clone
获取一个url对应的远程Git repo, 创建一个local copy.
一般的格式是git clone [url].
clone下来的repo会以url最后一个斜线后面的名称命名,创建一个文件夹,如果想要指定特定的名称,可以git clone [url] newname指定.

git status
查询repo的状态.
git status -s: -s表示short, -s的输出标记会有两列,第一列是对staging区域而言,第二列是对working目录而言.

git log
show commit history of a branch.
git log --oneline --number: 每条log只显示一行,显示number条.
git log --oneline --graph:可以图形化地表示出分支合并历史.
git log branchname可以显示特定分支的log.
git log --oneline branch1 branch2,可以查看在分支1,却不在分支2中的提交.表示排除这个分支(Window下可能要给^branch2加上引号).
git log --decorate会显示出tag信息.
git log --author=[author name] 可以指定作者的提交历史.
git log --since --before --until --after 根据提交时间筛选log.
–no-merges可以将merge的commits排除在外.
git log --grep 根据commit信息过滤log: git log --grep=keywords
默认情况下, git log --grep --author是OR的关系,即满足一条即被返回,如果你想让它们是AND的关系,可以加上–all-match的option.
git log -S: filter by introduced diff.
比如: git log -SmethodName (注意S和后面的词之间没有等号分隔).
git log -p: show patch introduced at each commit.
每一个提交都是一个快照(snapshot),Git会把每次提交的diff计算出来,作为一个patch显示给你看.
另一种方法是git show [SHA].
git log --stat: show diffstat of changes introduced at each commit.
同样是用来看改动的相对信息的,–stat比-p的输出更简单一些.

git add
在提交之前,Git有一个暂存区(staging area),可以放入新添加的文件或者加入新的改动. commit时提交的改动是上一次加入到staging area中的改动,而不是我们disk上的改动.
git add .
会递归地添加当前工作目录中的所有文件.

git diff
不加参数的git diff:
show diff of unstaged changes.
此命令比较的是工作目录中当前文件和暂存区域快照之间的差异,也就是修改之后还没有暂存起来的变化内容.

 若要看已经暂存起来的文件和上次提交时的快照之间的差异,可以用:
 git diff --cached 命令.
 show diff of staged changes.
 (Git 1.6.1 及更高版本还允许使用 git diff --staged,效果是相同的).

 git diff HEAD
 show diff of all staged or unstated changes.
 也即比较woking directory和上次提交之间所有的改动.

 如果想看自从某个版本之后都改动了什么,可以用:
 git diff [version tag]
 跟log命令一样,diff也可以加上--stat参数来简化输出.

 git diff [branchA] [branchB]可以用来比较两个分支.
 它实际上会返回一个由A到B的patch,不是我们想要的结果.
 一般我们想要的结果是两个分支分开以后各自的改动都是什么,是由命令:
 git diff [branchA]…[branchB]给出的.
 实际上它是:git diff $(git merge-base [branchA] [branchB]) [branchB]的结果.

git commit
提交已经被add进来的改动.
git commit -m “the commit message"
git commit -a 会先把所有已经track的文件的改动add进来,然后提交(有点像svn的一次提交,不用先暂存). 对于没有track的文件,还是需要git add一下.
git commit --amend 增补提交. 会使用与当前提交节点相同的父节点进行一次新的提交,旧的提交将会被取消.

git reset
undo changes and commits.
这里的HEAD关键字指的是当前分支最末梢最新的一个提交.也就是版本库中该分支上的最新版本.
git reset HEAD: unstage files from index and reset pointer to HEAD
这个命令用来把不小心add进去的文件从staged状态取出来,可以单独针对某一个文件操作: git reset HEAD - - filename, 这个- - 也可以不加.
git reset --soft
move HEAD to specific commit reference, index and staging are untouched.
git reset --hard
unstage files AND undo any changes in the working directory since last commit.
使用git reset —hard HEAD进行reset,即上次提交之后,所有staged的改动和工作目录的改动都会消失,还原到上次提交的状态.
这里的HEAD可以被写成任何一次提交的SHA-1.
不带soft和hard参数的git reset,实际上带的是默认参数mixed.

 总结:
 git reset --mixed id,是将git的HEAD变了(也就是提交记录变了),但文件并没有改变,(也就是working tree并没有改变). 取消了commit和add的内容.
 git reset --soft id. 实际上,是git reset –mixed id 后,又做了一次git add.即取消了commit的内容.
 git reset --hard id.是将git的HEAD变了,文件也变了.
 按改动范围排序如下:
 soft (commit) < mixed (commit + add) < hard (commit + add + local working)

git revert
反转撤销提交.只要把出错的提交(commit)的名字(reference)作为参数传给命令就可以了.
git revert HEAD: 撤销最近的一个提交.
git revert会创建一个反向的新提交,可以通过参数-n来告诉Git先不要提交.

git rm
git rm file: 从staging区移除文件,同时也移除出工作目录.
git rm --cached: 从staging区移除文件,但留在工作目录中.
git rm --cached从功能上等同于git reset HEAD,清除了缓存区,但不动工作目录树.

git clean
git clean是从工作目录中移除没有track的文件.
通常的参数是git clean -df:
-d表示同时移除目录,-f表示force,因为在git的配置文件中, clean.requireForce=true,如果不加-f,clean将会拒绝执行.

git mv
git rm - - cached orig; mv orig new; git add new

git stash
把当前的改动压入一个栈.
git stash将会把当前目录和index中的所有改动(但不包括未track的文件)压入一个栈,然后留给你一个clean的工作状态,即处于上一次最新提交处.
git stash list会显示这个栈的list.
git stash apply:取出stash中的上一个项目(stash@{0}),并且应用于当前的工作目录.
也可以指定别的项目,比如git stash apply stash@{1}.
如果你在应用stash中项目的同时想要删除它,可以用git stash pop

 删除stash中的项目:
 git stash drop: 删除上一个,也可指定参数删除指定的一个项目.
 git stash clear: 删除所有项目.

git branch
git branch可以用来列出分支,创建分支和删除分支.
git branch -v可以看见每一个分支的最后一次提交.
git branch: 列出本地所有分支,当前分支会被星号标示出.
git branch (branchname): 创建一个新的分支(当你用这种方式创建分支的时候,分支是基于你的上一次提交建立的).
git branch -d (branchname): 删除一个分支.
删除remote的分支:
git push (remote-name) :(branch-name): delete a remote branch.
这个是因为完整的命令形式是:
git push remote-name local-branch:remote-branch
而这里local-branch的部分为空,就意味着删除了remote-branch

git checkout
  git checkout (branchname)

切换到一个分支.
git checkout -b (branchname): 创建并切换到新的分支.
这个命令是将git branch newbranch和git checkout newbranch合在一起的结果.
checkout还有另一个作用:替换本地改动:
git checkout –
此命令会使用HEAD中的最新内容替换掉你的工作目录中的文件.已添加到暂存区的改动以及新文件都不会受到影响.
注意:git checkout filename会删除该文件中所有没有暂存和提交的改动,这个操作是不可逆的.

git merge
把一个分支merge进当前的分支.
git merge [alias]/[branch]
把远程分支merge到当前分支.

 如果出现冲突,需要手动修改,可以用git mergetool.
 解决冲突的时候可以用到git diff,解决完之后用git add添加,即表示冲突已经被resolved.

git tag
tag a point in history as import.
会在一个提交上建立永久性的书签,通常是发布一个release版本或者ship了什么东西之后加tag.
比如: git tag v1.0
git tag -a v1.0, -a参数会允许你添加一些信息,即make an annotated tag.
当你运行git tag -a命令的时候,Git会打开一个编辑器让你输入tag信息.

 我们可以利用commit SHA来给一个过去的提交打tag:
 git tag -a v0.9 XXXX

 push的时候是不包含tag的,如果想包含,可以在push时加上--tags参数.
 fetch的时候,branch HEAD可以reach的tags是自动被fetch下来的, tags that aren’t reachable from branch heads will be skipped.如果想确保所有的tags都被包含进来,需要加上--tags选项.

git remote
list, add and delete remote repository aliases.
因为不需要每次都用完整的url,所以Git为每一个remote repo的url都建立一个别名,然后用git remote来管理这个list.
git remote: 列出remote aliases.
如果你clone一个project,Git会自动将原来的url添加进来,别名就叫做:origin.
git remote -v:可以看见每一个别名对应的实际url.
git remote add [alias] [url]: 添加一个新的remote repo.
git remote rm [alias]: 删除一个存在的remote alias.
git remote rename [old-alias] [new-alias]: 重命名.
git remote set-url [alias] [url]:更新url. 可以加上—push和fetch参数,为同一个别名set不同的存取地址.

git fetch
download new branches and data from a remote repository.
可以git fetch [alias]取某一个远程repo,也可以git fetch --all取到全部repo
fetch将会取到所有你本地没有的数据,所有取下来的分支可以被叫做remote branches,它们和本地分支一样(可以看diff,log等,也可以merge到其他分支),但是Git不允许你checkout到它们.

git pull
fetch from a remote repo and try to merge into the current branch.
pull == fetch + merge FETCH_HEAD
git pull会首先执行git fetch,然后执行git merge,把取来的分支的head merge到当前分支.这个merge操作会产生一个新的commit.
如果使用–rebase参数,它会执行git rebase来取代原来的git merge.

git rebase
–rebase不会产生合并的提交,它会将本地的所有提交临时保存为补丁(patch),放在”.git/rebase”目录中,然后将当前分支更新到最新的分支尖端,最后把保存的补丁应用到分支上.
rebase的过程中,也许会出现冲突,Git会停止rebase并让你解决冲突,在解决完冲突之后,用git add去更新这些内容,然后无需执行commit,只需要:
git rebase --continue就会继续打余下的补丁.
git rebase --abort将会终止rebase,当前分支将会回到rebase之前的状态.

git push
push your new branches and data to a remote repository.
git push [alias] [branch]
将会把当前分支merge到alias上的[branch]分支.如果分支已经存在,将会更新,如果不存在,将会添加这个分支.
如果有多个人向同一个remote repo push代码, Git会首先在你试图push的分支上运行git log,检查它的历史中是否能看到server上的branch现在的tip,如果本地历史中不能看到server的tip,说明本地的代码不是最新的,Git会拒绝你的push,让你先fetch,merge,之后再push,这样就保证了所有人的改动都会被考虑进来.

git reflog
git reflog是对reflog进行管理的命令,reflog是git用来记录引用变化的一种机制,比如记录分支的变化或者是HEAD引用的变化.
当git reflog不指定引用的时候,默认列出HEAD的reflog.
HEAD@{0}代表HEAD当前的值,HEAD@{3}代表HEAD在3次变化之前的值.
git会将变化记录到HEAD对应的reflog文件中,其路径为.git/logs/HEAD, 分支的reflog文件都放在.git/logs/refs目录下的子目录中.

特殊符号:
代表父提交,当一个提交有多个父提交时,可以通过在后面跟上一个数字,表示第几个父提交: 相当于1.
~相当于连续的个^.

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值