目录
一、 Git介绍
参考自: https://blog.csdn.net/xiaoxuantengkong/article/details/41211315
Git 是一个快速、可扩展的分布式版本控制系统,它具有极为丰富的命令集,对内部系统提供了高级操作和完全访问。
Git 像是把变化的文件作快照后,记录在一个微型的文件系统中。每次提交更新时,它会纵览一遍所有文件的指纹信息并对文件作一快照,然后保存一个指向这次快照 的索引。为提高性能,若文件没有变化,Git 不会再次保存,而只对上次保存的快照作一链接。Git 的工作方式就像下图所示:
对于任何一个文件,在 Git 内都只有三种状态:已修改(modified),已暂存(staged)和已提交(committed)。
- 已修改表示修改了某个文件,但还没有提交保存;
- 已暂存表示把已修改的文件放在下次提交时要保存的清单中; add
- 已提交表示该文件已经被安全地保存在本地数据库中了。commit
由此我们看到 Git 管理项目时,文件流转的三个工作区域:Git 的工作目录,暂存区域,以及本地仓库。
二、 关于git仓库和分支的解释
重点参考下面的博客连载(讲解得十分通俗易懂):https://blog.csdn.net/xiaoxuantengkong/article/details/45231331
1 仓库
如果我们使用Git Bash进入一个目录(比如:D:/test/),在这个目录下执行git init命令,就会在这个目录建立一个Repository(仓库),这个仓库就是我们被Git管理的仓库了,如果你在这个目录下做什么操作,都是可以通过Git进行管理的。
比如你新建了一个文件TestFile.txt,那么通过git add TestFile.txt命令就可以把这个文件加入Git的暂存区,再通过git commit -m"增加一个文件TestFile.txt"命令就能够彻底将这个文件纳入Git管理了。
2 分支
当建立一个Repository后,Git会默认为我们建立一个master分支的,我们默认的是在这个名字叫做master的分支上进行操作。
当然,我们可以通过git branch testbranch命令建立一个名字叫做testbranch的分支,这个分支是基于你当前的分支创建的,也就是说,你当前如果处于master分支的话,如果你建立了testbranch分支,那么testbranch分支里的内容会和你在master分支里commit进去的内容是一样的,testbranch里同样会有刚才我commit进的TestFile.txt文件。
我们可以通过git checkout master和git checkout testbranch命令在两个分支上来回切换,切到哪个分支上你的文件目录就会变成哪个分支上的内容。(git checkout -b testbranch2 命令相当于git branch testbranch2+git checkout testbranch2两个命令)。
3 例子解释
假设远程(公司)的服务器上有个仓库Repository,这个项目包含了一个master分支,一个TestA分支,一个TestB分支。我们开发项目时,在自己电脑上直接把项目clone到本地,那么我们的电脑上也会出现一个同样的Repository,同样包括一个master分支,一个TestA分支,一个TestB分支。
目前能了解到的已经有6个分支了,远程有三个,你本地也有三个。为了方便区分,后面我会把本地分支后面加一个(L)。那么当我们使用git checkout master命令的时候,其实我们是在本地切换到了master(L)分支上,要记住哦,我们是不可能切换到远程上的分支的哦,那不是你的Repository~
下面就轮到git pull和git push出场了。
如果我们git checkout TestA之后会切换到TestA(L)分支上,如果我们做了一些改动,git add和git commit之后,意思是讲这些改动提交到了TestA(L)分支中,而远程的TestA分支还是原来的那个样子,如果你想让远程的TestA分支也变成你改动之后的样子,那就会用到git push命令了。当然这里是不出意外的情况,因为大概是在1.9.2之后的Git版本中,Git默认的push方式改成了simple方式,个人比较建议这种方式(即从哪里pull过来,我就push回哪里去)。
可以通过命令git config push.default查看你当前的push方式,可以通过git config --global push.default simple命令把你全局的push方式设置成simple方式的。其它方式读者可以自行搜索在此不再赘述。
所以总结一下,一般情况下,如果你checkout到了TestA(L)分支上执行git pull命令,就会把远程Repository中TestA分支中的内容更新到你本地的TestA(L)分支中;push命令同理。
4 本地与远程建立联系
remote即远程。比如你公司使用Stash管理项目时,你们公司的Stash服务器就是你的remote端;比如你使用Github管理自己的项目时,Github端就是你的remote端。其实你的本地库Repository可以同时对应到很多remote端的哦~也就是说你可以把你本地的库push到任何几个你有权限的remote端!这一点amazing,但是一开始不建议大家这么做,等以后越来越熟悉了再涉足这方面省着出乱子~下面就先对一个本地库对应到一个remote端来进行分析。
就Github进行举例吧,如果我们要在Github这个服务器上维护一个自己的项目,或者叫Repository。首先,我们在Github上用自己的账号新建一个Repository(这个步骤直接上Github按照说明做,简单的几步操作即可完成),建立完成后Github会给我们一个地址(这里的地址其实有两种,一种是SSH的一种是HTTPS的,建议大家直接用HTTPS的比较方便),如https://balabala/testproject。此后就代表Github已经为你在服务器端开辟了一个Repository,而那个地址就是指向这个Repository的。显然,目前这个Repository还是个空的库,里面什么东西都没有,顶多有个说明文件README。
目前,我们仅仅是在服务器上有了一个空仓库,而我们的本地还什么都没做、什么都没有。而我们要实现的状态是:在服务器上的仓库中有我们的项目,在我们本地也有一份同样的项目,并且两者是相互“关联”的(即我们如果pull则能从这个服务器上拉数据下来,如果push则能把本地数据推到这个服务器的这个仓库中去)。
下面就要分两种方法了:
第一种方法:
用Git Bash打开并进入一个目录,使用Git clone https://balabala/testproject命令将Github上的仓库“克隆”下来,就在当前目录。这个过程其实从整体上做了两类事情:1.在当前目录下建立Repository库,将Github上的testproject库中的文件下载下来并按部就班地部署到本地Repository库中;2.既然是clone下来的,那么“自然而然”地就会产生了关联。即本地Repository与Github上的Repository已经产生了联系,我们不管是git pull命令还是git push命令都会互相找到对方。
下一步就是去我们事先已经有了的项目目录,将我们要管理的文件内容拷贝到本地当前的目录下,然后git add进所有要管理的文件,再git commit进所有的文件。现在我们所有想管理的文件都已经在本地纳入了Git的管理了?我们本地的Repository库已经内容丰富了!下一步就是git push啦,将本地Repository里所有新的内容都推到Github服务器上去,这样不管是本地还是remote都已经达到了我们想要的状态了。
第二种方法:
直接用Git Bash进入我们事先已经有了的IDE正在指向的项目目录,这个目录下有我们要管理的所有文件。使用git init命令,直接在当前目录下建立Repository进行项目管理,然后把所有需要管理的文件git add然后git commit进来。这时候我们本地就有了饱满的Repository,与我们的目标唯一的区别就是我们本地的饱满的Repository和Github上的空Repository完全没有关系。
怎么建立联系呢?git remote add myGithubRemote https://balabala/testproject命令就OK了!这个命令的意思就是,我们本地的Repository定义了一个名字叫做myGithubRemote的remote端,这个remote端指向的是https://balabala/testproject地址上的远程Repository。此时你可以用git remote -v 命令查看一下我们的remote状态,在结果信息里就可以看到了myGithubRemote 和 https://balabala/testproject 的对应关系。
简单情况下,这时我们在执行git push命令时Git就知道应该把数据推到哪里了,就是myGithubRemote对应的https://balabala/testproject地址的仓库。当我们push完毕之后,就已经达到了目标状态,而且不用做什么文件迁移。
了解了这些之后,不难猜测第一种方法中git clone的第二类操作到底做了什么,其实Git就是默认为我们git remote add origin https://balabala/testproject了,Git默认给我们建立了这个对应关系,并且给这个远程库起名叫origin!这个origin就相当于我们自己定义的那个myGithubRemote~
5 git merge命令解释
git merge命令时用来合并的,而合并的对象就是branch(分支)。
下面我举个简单的应用场景(只在本地Repository中)来说明:本地库中有两个分支testBranch1和testBranch2。目前两个分支中的文件内容相同,都只有一个fileA.txt文件,而且fileA.txt的内容只有一行文字:HelloWorld!
如果你目前处于testBranch1,并且修改了fileA.txt文件在第二行增加了一句:Hello! 此时你的fileA.txt形如:
HelloWorld!
Hello!
不仅如此,你还新建了另一个空文件fileB.txt,并且把所有的修改commit进来。
然后checkout到testBranch2上,此时testBranch2上应该只有一个fileA.txt文件而且内容只有一行HelloWorld! 此时你又修改了fileA.txt在第二行增加了一句HaHa! ,并且把所有的修改都commit进来。此时你的fileA.txt形如:
HelloWorld!
HaHa!
现在可以想象,testBranch1和testBranch2两个分支分别管理着两套不同版本的内容了。前者有两个文件,后者只有一个文件,而且名字相同的文件内容也有所差别。好了git merge命令马上要出场了,下面请特别注意小编介绍的两个场景:
场景1:
git checkout testBranch1(即切换到testBranch1分支上),执行git merge testBranch2命令。这样代表:站在testBranch1分支上,把testBranch2分支上的内容融合到testBranch1分支上来。这样git会尝试把testBranch2中的内容与testBranch1融合,而这样是不会改变testBranch2分支上的内容的,只会改变testBranch1上的内容。所以此后testBranch1上有两个文件(fileA.txt和fileB.txt),而testBranch2上依然只有1个文件(filtA.txt)
场景2:
git checkout testBranch2 (即切换到testBranch2分支上),执行git merge testBranch1命令。这样代表站在testBranch2分支上,把testBranch1分支上的内容融合到testBranch2分支上来。这样git会尝试把testBranch1中的内容与testBranch2融合,而这样是不会改变testBranch1分支上的内容的,只会改变testBranch2上的内容。所以此后testBranch1上有两个文件(fileA.txt和fileB.txt),而testBranch2上也有了2个文件(fileA.txt和fileB.txt)
场景1和场景2在整体上的变化大家明白以后应该就能理解merge命令的含义了吧?下面说一下另一个细节
如果第一个分支的fileA.txt和第二个分支的fileA.txt进行融合的话,是有可能产生歧义的。前者的第二行希望是Hello!,后者的第二行希望是HaHa!那么最终结果到底听谁的呢?这种情况下Git就“有可能产生冲突”。那么在merge过程中产生冲突是怎样的效果呢?此时你的fileA.txt可能会呈现类似以下的中间状态(此时以场景1的情况为例):
HelloWorld!
>>>>>>>>>
Hello!
=========
Haha!
<<<<<<<<<
类似这样的状态代表Git说:我糊涂了。就需要我们人工来解决这样的冲突。此时我们可以打开fileA.txt文件,发现内容可能如上所示。解决时我们会想:Hello!是我在操作testBranch1时想加的话,HaHa!是我在操作testBranch2是想加的话。两个我都想保存下来,那稍微编辑一下就想搞成如下形式:
HelloWorld!
Hello!
HaHa!
这样解决冲突(即人为编辑)完成后我们保存并关闭文件。
注意!此时我们仍然处于merge的“过程中”,要想完成merge必须告诉Git:刚才冲突的文件就按照我刚才保存的这样解决!怎么告诉Git呢?就是git add fileA.txt然后git commit -m"解决fileA文件的冲突问题"两个命令。说白了就是再把最新的fileA.txt文件加入Git的管理。
这里需要理解一个Git的底层原理,这里称其为原理111:在将testBranch2分支merge到testBranch1分支中时,我们像上面那样解决fileA.txt文件的冲突。这样Git就“知道了”上面这个冲突的解决办法(因为这是刚才主人决定的)。那么如果此后再将testBranch1分支merge到testBranch2分支中时(远程),我们就不必要再手动解决冲突了,因为Git已经知道该怎么做了。
6 Merge Request解释
假设你们公司的服务器是Stash,上面有个正在开发的项目:ComRepository。这个项目有三个分支:master,worker1,worker2。那么如果把这个项目clone下来之后,本地也会有三个分支:master, worker1, worker2。这时有6个分支,服务器上3个,你本地3个。而对于公司来说,他们不在乎每个员工本地的分支是怎样,他们只在乎服务器上的master分支是否运转正常。因为服务器上的master分支有可能就是公司发表项目时用的分支,所以这个分支上的代码至关重要!
此时公司要求你就在worker1分支上开发吧!那么你的开发流程是什么呢?总之最终目的是要让服务器上的master分支有你开发好的代码。如果我是老大,我是不会给你分配master分支的push权限的。因为这样太危险了,master分支是公司至关重要的分支,岂能让你说push就push?这样会让公司的master上的代码时刻处于危险之中。
我们每个开发人员都没有master分支的push权限,但是有pull权限。即只能看,不能写。但是我们有其它分支(如worker1和worker2)的pull和push权限,反正这两个分支也不是太重要的分支,开发者爱怎么糟蹋怎么糟蹋。
在服务器上进行merge动作,即将服务器上的worker1或者worker2分支上的内容merge到master分支上。这个过程就是Merge Request!
服务器上的merge动作不是靠你本地git merge命令来完成的,因为你只能操作你本地的分支,所以这个merge动作基本上是在你们服务器的网页上进行的。比如:你在公司服务器的网站上,发起一个Merge Request,请求把服务器上的worker1分支merge到master分支上去,请求发起要添加一些审核人物,譬如你的老板你的同事。当他们接收到这个Merge Request并同意后,可能会有个“同意”之类的按钮,点击之后就会在服务器上把worker1分支merge到master中。这样做就有效地保护了master分支的安全性。
服务器上的worker1分支的更新就是靠你在本地worker1(L)上更新后push上去的。所以简单的流程就是你早上来上班了,在worker1(L)分支上开发了一天,然后把工作内容都push到服务器的worker1分支上去,再发起一个Merge Request请求把服务器上的worker1分支merge到master上去,然后下班。
7 尽量避免冲突的做法
比如你是在worker1(L)分支上的开发者,做完了一天的工作,先不要提Merge Request,而是先checkout到master(L)上,pull一下,把服务器上master的代码拉下来,使本地的master(L)是最新的master代码,然后checkout回worker1(L)分支,将master(L)分支merge到worker1(L)上,这时候就有可能发生冲突了,因为可能在你下班之前,worker2分支的同事已经提交了代码并且更新了master,而你们恰巧修改了同一个文件。这时候冲突了不要紧,因为冲突是发生在你本地的,你只要在本地把冲突解决了,然后push到worker1分支上去,再提Merge Request,就不会使服务器上的master产生代码冲突了。
原因相信聪明的你已经明白,就是上面提到的原理111。因为你在本地解决冲突的时候,Git就已经知道产生冲突以后该怎么做了,而你又把这些脚本push到了服务器的worker1分支上,那么服务器的worker1分支在往master分支合并的时候自然就知道该怎么做了。怎么样,Git强大吧?
顺便再建议一下,当你早上来上班时,尽量也checkout到master(L),pull一下,再checkout回worker1(L),把master(L)分支的内容merge过来,完成之后再进行开发。这样就是尽量保持在昨晚之后的最新的代码上进行开发,以减少产生冲突的可能性嘛~
8 Upstream概念及使用
git push origin master。origin是远程端代名词(前几篇有讲解),master是这个远程端的一个分支名。所以这句命令的意思可以理解成“推送我的数据到origin端的master分支上去”。为什么很多时候,我们根本没有写这么全,也能够实现呢?而且刚才的解释也没有说明:把什么数据哪里的数据推送上去?这里可以引入一个upstream的概念。可以将upstream理解成一个通道,它用于连接你本地的一个分支和远程的某个分支。
比如远程库A上有3个分支branch1、branch2、branch3。远程库B上有3个分支branchx、branchy、branchz。本地仓库有2个分支local1和local2。那么当初始状态时,local1和local2和任何一个分支都没有关联,也就是没有upstream。当通过git branch --set-upstream-to=A/branch1 local1命令执行后,会给local1和branch1两个分支建立关联,也就是说local1的upstream指向的是branch1。这样的好处就是在local1分支上执行git push(git pull同理)操作时不用附加其它参数,Git就会自动将local1分支上的内容push到branch1上去。同样,local2分支也可以和远程库A和远程库B上的任何一个分支建立关联,只要给local2分支设置了upstream,就可以在local2分支上用git push(git pull同理)方便地与目标分支推拉数据。综上所述,upstream与有几个远程库没有关系,它是分支与分支之间的流通道。
当远程库没有gaga分支时,git branch --set-upstream-to=origin/gaga master这种方法就不可行,连目标分支都不存在,怎么进行关联呢?因此,git push -u origin gaga 更常用,相当于git push origin gaga+git branch --set-upstream-to=origin/gaga master。
另外,git config --global push.default simple这个命令是在电脑上安装完后小编建议大家就须要设置的一个指令。这句命令的意思是“把push命令的全局默认模式设置成:simple”。当然,你也可以设置成其它模式(如:matching),当然小编不建议。由此可以联想到也可以git config --global pull.default simple来给pull动作也设置一下。
simple模式:说白了就是从哪里来到哪里去。假如我的本地分支branchA是从origin端的branchA分支上pull下来的,那么在simple模式下,以后我再站在branchA这个本地分支上执行git push命令时,Git就会自动把我本地branchA上更新的内容推到origin端的branchA分支上去。现在就不难推断,为什么有时候,我们什么都没设置,上来就用git push就能够达到目的?因为你的Git版本的默认push.default是simple模式,而且在你来入职的时候,公司远程服务器上已经为你准备好了属于你的分支branchA,而当你把公司远程库给clone下来的时候,因为simple的模式设置,当你切(checkout)到你本地的branchA分支时,Git就已经知道你本地的这个branchA分支来自何方,而且它也知道该把你的数据推向何方了(这里读者可以把upstream的概念思考进来,就能够明白其实Git是通过设置upstream来实现simple这个模式的功能的)。这样就会导致很多用户上来就可以无脑地使用git pull和git push了。
matching模式:这个模式小编了解不是特别特别详细,可能就是通过分支名称进行匹配。举个例子,如果你的远程分支上有两个分支:branch1和branch2。你的本地分支上有三个分支branch1和branch2和branch3。那么在matching模式下,不管你站在哪个分支上执行git push时,Git都会寻找本地与远程分支名称相同的分支并且全部进行推送数据。所以小编觉得这个模式很危险,不建议大家使用。
9 分支类型解释以及git pull
git pull = git fetch + git merge 。git fetch是获取服务器上的数据,git merge是将数据合并,所以git pull就是获取数据加合并数据的意思。
git branch -a
可以看到三类分支名称:
1.上半部分的白色分支:这部分分支就是本地分支,上述的feature(L)就属于这类分支;(注意绿色的dev_0430左边的*号代表我当前HEAD所处的分支,即目前切换到的分支)。补充:HEAD 就是当前活跃分支的游标。形象的记忆就是:你现在在哪儿,HEAD 就指向哪儿,所以 Git 才知道你在那儿!
2.下半部分的以remotes开头的红色分支:指向远程分支的指针(为了便于理解,就把它们当成远程分支的内容在本地的临时复制品)。这是一个十分重要的概念,后文详述。
3.远程分支:即上图红色分支第一行右边的白色分支名:origin/develop。 这个就代表的远程服务器上的分支,上文提到的feature(R)就属于这个概念。
这样理解的话,目前认知的就有三种类型的分支了,feature(L)、feature(R)还有以remotes开头的分支(如:remotes/origin/feature,后文简称为feature(T))。其实feature(L)和feature(R)从技术实现上根本没有任何交集,它们之间的关联全是通过feature(T)这个桥梁实现的。其实在feature(L)上执行了git fetch命令后,Git会把feature(R)上的改动下载到本地更新到feature(T)分支上,如果此后不执行git merge的话,那么你的feature(L)和feature(T)就会存在差异,只有执行了git merge命令后才会把feature(T)中的内容合并到feature(L)中。
现在清楚了吧?Git与远程仓库之间的交互是通过这个feature(T)进行过渡的。git push也是如此,在simple模式下,我们feature(L)分支的upstream其实指向的就是feature(T),而feature(T)这个指针就会指向真正的远程分支feature(R)。所以如果我们的feature(L)有了改动,执行git push的话,Git会先把改动更新到feature(T)中,然后再讲feature(T)的改动推向远程的feature(R)中。由此才可以理解成:feature(T)就是feature(R)在本地的一个镜像。
重点参考 原本链接:https://blog.csdn.net/xiaoxuantengkong/article/details/45231331