玩转Git三剑客——第一章Git基础
02、安装Git
Git官网:https://git-scm.com
https://git-scm.com/book/zh/v2/
https://git-scm.com/book/zh/v2/起步-安装-Git
git --version
在Windows上的安装:https://git-scm.com/download/win 能够自动识别你的系统进行安装。
03、使用Git之前需要做的最小配置
04、创建第一个仓库并配置local用户信息
.git是个裸仓库,是个隐藏的文件夹,使用ls -al
可以看到,这个是git的核心文件夹。
设置作用域范围是local的参数(作为例子,设置local的用户名、邮箱跟global的不同):
参数-m是说你为这次变更的理由是什么,执行后出现上述错误信息,请耐心的看完提示错误信息,上述是说我们的readme还没有被git管控,所以你用git commit是不生效的,所以我们要对新加入进来的文件执行git add readme
,之后通过命令git status
可以看到git开始管理这个文件了,你的这个文件已经在暂存区(stage)当中了。
下一步生成正式的commit,如下图所示,master是它的分支,就是说root-commit这个根部的commit已经创建出来了,看一下git的日志:
上图黄色的显示的是commit的ID号;通过上述我们可以推测出来,在global和local这两个作用域范围当中,应该是local当前设置的这些属性优先级是第一的,它比global的优先级要高,当前仓库当中的这个配置起作用。
05、通过几次commit来认识工作区和暂存区
用git工作有一个习惯:
咱们在工作目录上修改的变更,首先咱们要先把它添加到暂存区,这个暂存区是git的比较有特色的一个功能。
暂存区的作用:
例如,在工作目录里有一个变更,假设你还有其他的方案,那你不妨把这次的变更先放到暂存区,然后再尝试着用另外一套方案,假如第二次的方案还没有第一次好的话,这个时候暂存区的东西就可以覆盖工作区的东西;所以暂存区在各种各样的场景当中还是蛮有作用的,也就是它可以暂时的存放着,还不是作为一个正式的提交,但是它已经被git管理了,暂存区里的东西可以很容易被git正式的提交,同时如果暂存区里的东西不合适的话,还可以回退。
我们跟git打交道的话,经常要看git工作目录和暂存区是什么状态的:
Untracked是说这些文件(红色)没被git管理的意思,那怎么样把它们变成git管理的呢?
Changes to be committed是说下面这些文件已经放到暂存区了,如果你认为它合适的话,就可以通过commit的方式生成一个commit了:
git commit -m 'Add index + logo'
因为页面太简陋了,所以我们把css引入进来,美化一下页面:
我们再给页面添加一些js动画:
教大家一个新的用法,就是对git已经跟踪了的这些文件,我们可以用参数 -u
,这个update代表了所有工作区当中被git管理的、已经控制了的这些文件,一起提交到暂存区,就不需要写文件名了:
在一条分支上面进行开发的这种直线型的版本历史,就完整的呈现出来了。
回顾:咱们跟git打交道,就要记住这样一个事情,我们在工作目录上面修改的东西,最好通过git add
的方式先把它添加到暂存区,如果通过挑挑捡捡文件的方式都放到暂存区了,比如说你修改了5个文件,这5个文件都是为一个目的而做的,那当这5个文件都修改完毕的时候,我们对这个集合做一个统一的提交,我们通过git commit
的方式把它纳入到git的版本库里面,这样一个正式的commit就生成了。这个习惯是比较好的,先在工作目录中修改,然后添加到暂存区,最后将变更的集合放到git的历史库里面去。
06、给文件重命名的简便方法
我们先演示在工作目录中修改文件名的方式:
我们把readme.md文件添加进暂存区,同时我们还要把已被git管理的readme文件还要从git里面删除:
这种方式太麻烦了,步骤太多,相当于执行了3个步骤。
我们还有更简便更好用的方法,直接用git的命令的方式,先把环境复原一下,我不想保留刚才的操作(这个复原命令是比较危险的,就是说这个命令一执行的话,暂存区工作路径下面里面的所有变更都会被清理掉):
这样我们工作路径是干净的,暂存区是没有什么东西要提交的,而且我们并没有破坏git的历史,还是完整的历史。
我们可以用一条git命令还替代刚才的3个步骤:
现在就可以用一个步骤来替代了。
7、通过git log查看版本演变历史
除了git log
,该命令还可以带参数:
还可以以一行简洁的方式查看最近的两次:
可以看本地到底有多少分支:
还可以基于以前的版本(例如上图中的temp 415c5c8086e16399)创建几个临时的分支来演示该branch命令。
修改一下readme文件后,再提交:
平时会用到git commit -am
这种方式,就是说工作区的东西直接创建到版本历史库里面去了。
如上图所示,此时就有两个分支了。
如果此时我们执行git log
这种不加任何参数的话,我只要看当前分支的历史:
可以看到HEAD指向的是temp分支,那我们的master分支去哪了呢?
可能会有人说,我所有的版本的分支历史都想看:
git log --all
就可以看到master分支也出现了,master最新的commit是指向commit ace1ed38a92b9这个,temp分支对应的commit 5bf3fb1900这个commit信息。
通过上图你会觉得这个版本历史不存在父子关系,你看起来非常累,就是它的这个演进的历史你看不出来,可以使用图形化的用法:
git log --all --graph
我们可以看到,temp分支是由commit 415c5c8086这个commit演进过来的,master是绿颜色的这块,master有3个commit,然后跟下面的commit 415c5c8086汇合,也是由commit 415c5c8086演进过来的;
只不过temp分支是从commit 415c5c8086那里开始,它的下一个commit直接就是commit 5bf3fb1900;
temp分支和master分支是从commit 415c5c8086那里开始分开、各自向上演进过去的。
参数 --all
是所有分支的演进历史,但是我不想看它的所有,我只想看就近的4个的话:
所以我蛮喜欢后面加个–graph参数的,可以看整个版本的演进历史是怎么样的;而这个-n4不是说每个分支给我取4个,而是说所有分支最近的4个commit你给我取出来。
你如果用–all的话,是所有分支都呈现出来的。
如果想用浏览器看git帮助文档的话:
git help --web log
08、gitk:通过图形界面工具来查看版本历史
命令:gitk
左上角的这块就是历史树。
这个Patch是针对某一次commit的变更,它不是有个变更集么,这个Patch就是指的这个变更集,我们点击某个文件的时候,左边栏显示的就是Patch里面某个文件的变更信息,这个加号+就是说你这次变更增加了哪些内容。
切换成Tree的方式,我们可以看到当前的这个commit下面是怎么样的一个文件目录结构,你点击某个文件,左边栏显示的文件的所有内容。
如果你想看某些指定的功能的话,我们gitk是可以定制的,点击View菜单;如果你点击View菜单不起作用的话,你可以切换到命令窗口,然后再切回来它这个View就生效了。
如上图所示,所有的Branch信息都呈现出来了,还可以鼠标右击某个commit弹出更多功能的选项,比如我们给Add js创建一个tag:
09、探秘.git目录
我们知道git具有最优的存储能力,我们在使用git的过程当中,我既没有远端服务器的支持,我本地就能够做一个版本的管控系统呢,大家会不会觉得很神奇呢,我们今天就进入到.git
这个裸仓库里面去看个究竟,这个.git
文件夹里面就装了git的最核心的东西。
我们查看一下HEAD里面的内容:
HEAD里面的内容是个引用,引用的具体内容是很多的heads里面的一个temp,通过git branch -av
可以看到现在正在工作在temp分支上面,HEAD里面的内容说明我们现在的这个git仓库,正在工作的分支是temp。
我们实验以下切换分支后,看看HEAD里面的内容会不会变:
我们整个仓库现在正在工作在master分支上。
我们再看看config文件:
原来当我们执行了git config --local
的时候,就把相应的user.name和user.email信息记录在.git文件夹中的config这个配置文件里面了。
我们直接修改config文件,修改name这一项,然后看看是否直接影响了–local的配置:
我们再修改回用户名suling:
再到.git/config文件当中看看用户名是否发生变化了:
也就是说,这个config文件存放了跟本地仓库相关的配置信息。
还有一个平时接触比较多的文件是refs,刚才我们看到HEAD文件里面的内容是个引用,是不是指向的refs这个文件呢?
这个tags是软件开发阶段当中的里程碑,当到某一个阶段后给这个关键的commit打上一个标签;
heads其实对应的就是我们的分支,分支其实说白了就是一个独立的开发空间,比如说有些时候你要做前端的开发,有些时候你要做后端的开发,那么你可以在这个仓库里为前端建一个分支,为后端建一个分支,那么彼此在不同的分支里面工作的时候,工作是互不影响的,当它们需要集成的时候,又可以把这两个分支集成到公共的一个分支上面去。
我们可以用git cat-file -t
命令看一下这个哈希值代表什么,是个commit,在通过git branch -av
可以看到当前仓库正在工作在哪个分支上,就是说我们这个master指针指向的是哪一个commit,temp这个文件同理。
我们再看看tags文件夹里面的内容:
我们再通过git cat-file -p
看看它里面有什么内容:
它说这个tag指向的是27d2f8146
这个object,我们又通过参数 -t
看到这个27d2f8146
哈希值指向的是一个commit;
所以说,tag本身有一个哈希值,js01里面存放的是tag的这个哈希值,而tag的这个哈希值里面存放的是一个commit对象。
我们看一下,.git仓库里面还有一个至关重要的,git文件系统最核心的内容是objects文件夹,我们打开可以看到里面有很多2个字母组合的文件夹,我们随便进入一个文件夹e8里面的内容:
两字母组成的目录名加上该目录里面的文件名,共同组成了一个哈希值,我们查看该哈希值发现它代表一个tree。
git仓库当中它有树的概念,还有commit,这已经是两种类型的对象了。
看这个树的内容用git cat-file -p
这个命令:
这个树里面有一个文件叫style.css,这个文件的类型叫blob,我们再通过参数 -p
可以看到它的内容就是style.css文件里面的内容。
git核心的对象也就commit、tree、blob这3类对象。
10、commit、tree和blob三个对象之间的关系
Git对象彼此关系
一个commit肯定会对应一棵树tree,一个commit不可能对应两颗树、多颗树的,这棵树代表了我取出某一个commit,这个commit对应的一个视图,这个视图里面存放的就是一个快照,这个快照集合里面就是放了当前commit对应的本项目仓库所有的文件夹以及文件的一个快照,也就是那个时间点你这个文件夹长什么样,你这个文件是什么样,它就是通过这棵树给呈现出来的。
文件夹也是树,blob指的就是具体的某一个文件;
blob跟文件名一点关系都没有,git这样设计是非常棒的,只要咱们文件的内容相同,在git的眼里它就是唯一的一个blob,它不会有多余的,在git那里不会存放两份内容相同名字不同的文件,git不管你文件名叫什么,在它的眼里这个blob只有一份,这样设计的好处就是可以大大的节约存储的空间,在它的眼里只要这些文件内容相同它们就是一个东西。
有些文件夹tree下面又嵌套了一个文件夹tree,有些文件夹下面只有文件blob。
那么我们提到了,commit对象里面肯定有一棵树,这棵树里面包含了那个时间点我们的项目到底包含了什么文件夹、什么文件,这个大的树下面呢里面嵌套了小的树,小的树里面一层层展开,最后这个叶子的节点就是到文件;
git的文件跟文件名没关系,git的文件它的识别就是根据文件的内容产生一个blob。
上面那幅图就取自我们自己产生的仓库,我们先看下当前在哪个branch里面:
我们这幅图取自415c5c8086e这个commit,我们用git -cat-file -p
的方式看看这个commit的内容,这个commit里面包含有一棵树,这棵树长什么样呢?
可以看到跟图中所示的一样包含有两棵树和两个blob。
11、小练习:数一数tree的个数
今天我们基于一个新的仓库,来看一看刚创建一个commit的时候,究竟这里面的object是怎么样增加进来的,我们只限一个commit,这个commit下面只包含一个文件夹,这个文件夹下面也只有一个文件readme,请问当这个commit创建出来的时候,git的仓库当中总共到底有多少个tree,多少个blob?
我们来做这个演示:
我们从上图发现创建没有带文件的文件夹,git是不理会的。
现在git理我们了,我们先不要急着把它加入暂存区,我们先到.git目录里面看一看:
可以看到.git/objects里面没有东西,那么我们把doc文件夹加入暂存区之后,再看一下这个时候objects里面有没有东西呢?
假如有,说明这块的变更、就是说readme的信息放到暂存区去,git已经帮我们创建了什么对象了,我们检验一下obejcts文件夹里面有没有产生什么对象出来:
说明新的东西加入暂存区,git就会主动的把暂存区的东西先创建出来、以文件加进去的就会创建出blob。
目前有一个新增的文件readme,还没有创建commit,那我们把它创建一下:
当这条指令执行后,我们猜一猜我们的.git/objects这个文件夹下面到底有多少个文件创建了出来,而且这些文件它们的类型究竟有多少颗树、有多少个commit以及多少个blob:
切记,如上图所示,将08目录名和里面的文件名3e18d286组合起来,形成一个正确的哈希值后再进行查询。
12、分离头指针情况下的注意事项
detached HEAD
我们切换分支的时候,有可能不小心输入了一个blob,此时会怎么样呢?
git说了,正在基于’415c5c8086e16’这个commit做了一个checkout的活动,You are in ‘detached HEAD’ state,咱们现在正处在分离头指针的这样一个状态,你可以做一些变更然后产生commit,或者你也可以把你这个生成的commit丢弃掉,就是在分离头指针的情况下你可以继续做开发,然后继续产生commit,而且不会影响其他分支。
分离头指针,本质上就是说我们现在正工作在一个没有分支的状态下,如果我们现在正处于这种没有对应任何分支的情况下,你做了commit,并产生了很多很多的变更,假设有一天你突然接收到了一个新任务,说有一个fix要修复,于是你切换分支到master上面去做修复了,这个时候一切换,那个分离头指针那个地方开发出来的那些commit,不让去跟它挂钩,这样的话,当你切到master的时候,那些没有挂钩的这些变更呢,最后很可能会被git当做垃圾给清除掉,所以这个是它危险的地方。
也就是说,如果你想做变更,请你要跟某个分支做挂钩,在那个分支的基础上对分支进行变更,那这样的commit,git是永远不会把它清除掉的。
这就是分离头指针使用的时候需要注意的地方,当然分离头指针这个事物它是具有两面性的,用得不好会产生后果的一方面,那什么时候我们可以用好分离头指针呢?
有这样一种情况,咱们想做一个变更,但是呢这个变更很可能你只是尝试性的变更,没准你做下来之后你发现效果不好,那你完全可以随时把它扔掉,就是你不要去理会它,你做checkout把它切换到新的分支就可以了,这就是分离头指针带来的好处。
我们接下来做一下这个演示,比如说我们现在已经处于分离头指针了:
我们修改styles/style.css中的背景色为绿色,咱们可以看到git又在提示我们了,咱们这个头HEAD没有指向任何的分支,而是基于某一个commit做的,我个人很喜欢绿色背景,所以我打算把它存起来产生一个commit,偷懒的方式就是git commit -am
,就是不到暂存区去了,工作目录的这个变更我直接就生成commit,虽然这个不推荐,假设你是经过了认真的检测,你自己觉得暂存区你是不需要的,那你就这样做吧,不推荐这样做。
我习惯了commit的消息第一个首字母是大写的:
我们通过git log
来看一下现在到底是什么状况:
请大家注意,之前我们在讲git log的时候,我们发现这个HEAD总是跟某个分支绑在一起的,咱们这边只有一个HEAD,这边没有把HEAD指向任何的分支,这个就叫分离头指针的状态。
我们现在就是在造一个分离头指针造成危害的这么一个例子,这个时候我接到任务了,你必须给master去修复,所以我就切换分支了,切换到master分支上面去了:
git会发出警告,它说有一个commit在后面,还没有加到我们master里面去,这个就是分离头指针导致的;
git还再次提醒你,要不要为3d4731d这个commit建branch,大家要学会看git命令的提示,如果你不去看的话,那么这个3d4731d就会丢掉,我们再用之前学过的gitk --all
来看看,就看我们刚才的那个Background to green那个commit在我们的历史树里面到底出不出现:
仔细的看一下,究竟有没有Background to green这个commit,我们看了又看发现没有,说明在git的眼里,你没有把这个commit跟某个分支绑着,也没有把这个commit跟某个tag绑着,在git的眼里这种commit都是不重要的,日后都是要清除的。
假如我醒悟过来了,认为这个commit很重要,那么我会干嘛呢?
我会按照git的指示信息建一个分支出来,我就建一个fix_css这么一个分支把它保留下来:
这个时候我们再用gitk看一看,这个时候咱们的Backgroud to green就出现了。
分离头指针就是咱们的变更没有基于某一个branch去做,所以当你进行分支切换的时候,在分离头指针上产生的commit很可能会被git当做垃圾给清理掉,如果你认为这些变更是重要的,那么切记要跟某个分支绑在一起。
13、进一步理解HEAD和branch
我们今天演示一下,在新建分支的情况下这个HEAD究竟会发生什么样的变化,其次,HEAD它有一些专业的用法,有一些专业的标识符,我们也可以给大家做一个讲解。
当这个HEAD指向具体的某一个commit的时候,大家看过分离头指针那节课的话,就应该知道了,HEAD不仅仅可以指向分支,它还可以指向具体的某一个commit,这种状态就处于分离头指针的状态;
那么我们现在就基于创建一个新的分支,创建完毕后我们切换到那个分支,看看HEAD会发生什么样的变化:
git checkout -b fix_readme fix_css
这个命令一执行的话呢,它不仅仅可以创建新分支,而且还可以直接切换到那个新分支上面去,我们这次就是基于已存在fix_css这个分支来创建一个新分支。
刚才我们看到了HEAD是指向分离头那个commit的,如果这条指令下去会怎么样呢?
它利马就告诉我们切换到新创建的fix_readme这个分支了。
我们看看大写的HEAD究竟去哪里了?
此时大写的HEAD不再指向分离头指针那个commit了,而是指向我们的新分支了。
那我们再用gitk看一下具体的情况:
我们原来分离头是在9c6861f3a71b4那个位置,当时HEAD是指向这里的;
现在这个HEAD是已经指向fix_css、fix_readme了。
我们再看看.git/HEAD发现它的内容已经指向fix_readme了。
所以,HEAD不仅仅可以指代新分支的最后一次提交,同时HEAD还可以不跟分支挂钩,它处于分离头状态的时候呢,这整个大写的HEAD它就只呆到了某个具体的commit上面去了,它没有跟任何的分支挂钩;
其次,我们倒过来,那么如果我们新建branch的话、或者说切换了branch,对HEAD有什么影响呢,当我们做这个分支切换的时候,HEAD是会跟着变得,它这个指针就指向新的分支了,这是它两者之间的关系。
关于HEAD的指代:
HEAD肯定是落脚于某一个commit或者是先是落脚于某一个分支,它不是指代整个分支所有的commit的,HEAD唯一的只能定位到某一个commit,就拿上图刚才我们举的例子,HEAD是指向某一个分支了,但是这个分支里面的内容究竟是什么呢,归根结底分支最后也是要落脚到某一个commit的:
所以说这个HEAD最终是要落脚于某一个commit的,不管它是处于分离头状态,还是说HEAD指向分支的最新一次的提交这个commit。
你知道了HEAD之后呢,有些指代就非常有用了,比如说我想比较下3d4731d80和415c5c808这两个commit到底有什么差异:
HEAD是可以指代commit的,你还可以用HEAD~1
,或者HEAD^1
这种箭头的方式指代HEAD的父亲,如果要指代HEAD父亲的父亲,你就可以用HEAD^1^1,所以还可以这样比较:
HEAD^^等同于HEAD~2