Git详细讲解(本地)

一、基本介绍

首先,Git作为版本控制系统,他的原理与SVN为首的集中式版本控制系统差别较大。

简史

很多人都知道,Linus在1991年创建了开源的Linux,从此,Linux系统不断发展,已经成为最大的服务器系统软件了。

Linus Torvalds在2002年起,使用BitMover的版本控制软件BitKeeper管理Linux核心开发,而因为BitKeeper除商业付费版本,仅提供可免费使用但不允许修改重新编译的精简版本,引起了开源社区的不满,如自由软件之父Richard stallman也敢严厉批评Linux Torvalds使用非自由软件开发Linux核心。
在2005年,Samba档案服务器开发人Andrew Tridgell写了连接BitKeeper存储库的简单程序,被BitMover创办人Larry McVoy指控对BitKeeper进行逆向工程,因为决定停止BitKeeper对Linux的支持。
顿时Linux核心开发受到了严峻的挑战,而Linus Torvalds秉承自己的版本自己写的精神,整个周末都不见人影,隔周却如变戏法般带着Git出现。
Linus花了近10天时间自己用C写了一个分布式版本控制系统,这就是Git!一个月之内,Linux系统的源码已经由Git管理了!牛是怎么定义的呢?大家可以体会一下。
Git迅速成为最流行的分布式版本控制系统,尤其是2008年,GitHub网站上线了,它为开源项目免费提供Git存储,无数开源项目开始迁移至GitHub,包括jQuery,PHP,Ruby等等。

1.集中式版本控制系统

集中式版本控制系统,其将版本库集中存放在中央服务器上,所有的版本控制也都在中央服务器,所以在项目分支功能上略显无力,所以对于这类项目的版本控制,只有通过多次的项目、文件网络提交来形成版本控制。并且在中央服务器上发生故障时,对整个团队的协同工作效率略有下降。甚至在最坏的情况下发生项目丢失现象。
当然,他的强大也是毋庸置疑的,在统一的版本控制器上可以清楚看到该项目上的所有开发者在做什么,集中化项目也可以很好的管理项目版本,所以集中式版本控制系统是以项目的视角进行控制版本。管理员也轻松掌控着每个开发者的权限。

github
pdfView

2.分布式版本控制系统

相对于上述系统来讲,Git则以分布式的形式完成版本控制功能。开发者并不是在原始代码上进行加工并上传形成版本树,而是将整个项目镜像下来,将项目分布式管理。所以在发生上述故障时,其他开发者也有相对完整项目版本库。而其强大的分支管理更是Git的版本控制特点。Git作为版本控制是以开发者的视角进行版本控制,形成与开发者相匹配的版本树。

github

二、工作特性

1.保存方式

首先,Git保存数据是以快照的形式展现。如果此次版本中的文件没有做任何修改,则会保存上个版本的地址链接。他与SVN的记录文件差异的方法不同。当保存一个文件的时候,Git把这个文件的信息和内容存储在一个文件里,然后求得这个文件的sha-1作为文件名(.git/objects)。在保存在Git中的数据都是用这些sha-1算法生成的哈希值作为唯一索引进行引用,这一方式可以节省项目存储的空间。
sha-1算法是将文件中的内容计算生成一个40位的哈希值,sha-1的前两位为文件夹名/sha1剩余部分为该文件的文件名。他就像每一个文件的指纹记录,在文件进入暂存区中生成,当该文件进入本地仓库后,指针便指向该文件。
0c85f9f5ce1d60376130469d4e455166c041ea

Git更像是把数据看作是对小型文件系统的一组快照,将所有数据以哈希值作为引用key,合理保存并使用我们的项目文件。
github

2.区域划分

其次需要清除Git的工作区、暂存区、本地仓库和远程仓库概念。

  • 工作区:项目目录
  • 暂存区:在英文解释中使用index/stage来表示暂存区(在.git/index中),此时会将文件生成快照,在工作区的文件与本地最新的版本库文件发生变化后,便用指针指向这个文件。
  • 本地仓库:相对于每个开发者本地Git的项目版本(在git/objects中)
  • 远程仓库:个人理解为项目的统一版本库

github

3.文件分类

我们所有的项目文件就在这四个域中进行各种操作。而不同的域中会以不同的文件状态展示已提交(committed),已修改(modified)和已暂存(staged)。

  • 已修改:表示修改了某个文件,但还没有提交保存,处于修改后的工作区中
  • 已暂存:已暂存表示把已修改的文件放在下次提交时要保存的清单上
  • 已提交:表示该文件已经被安全地保存在本地数据库,处于本地仓库中

4.git对象

从根本上讲,git是一套内容寻址的文件系统,存储的是key-value键值对,然后根据key值来查找value的,说到寻址就会想到指针,git也是根据指针来寻址的,这些指针就存储在git的对象中。git一共有3种对象,commit对象,tree对象和blob对象,最后应该还有一个tag

  • commit 该对象指向一个”tree”对象,并且带有相关的描述信息,标记项目某一个特定时间点的状态
  • tree 像一个目录,管理一些”tree”对象或是”blob”对象
  • blob 一个”blob”通常用来存储文件的内容
  • tag 给某个提交增添一个标记。一个”tag”对象包括一个对象名(SHA1签名)、对象类型、标签名、标签创建人的名字(“tagger”), 还有一条可能包含有签名(signature)的消息
    github

5.目录介绍

打开”.git”目录可以看到
github

  • COMMIT_EDITMSG 保存最新的commit message,git系统不会用到这个文件,只是给用户一个参考
  • config 这个是git仓库的配置文件
  • description 仓库的描述信息,主要给gitweb等git托管系统使用
  • HEAD 这个文件包含了一个档期分支(branch)的引用,通过这个文件Git可以得到下一次commit的parent
  • hooks 这个目录存放一些shell脚本,可以设置特定的git命令后触发相应的脚本;在搭建gitweb系统或其他git托管系统会经常用到hook script
  • index 这个文件就是我们前面提到的暂存区(stage),是一个二进制文件
  • info 包含仓库的一些信息
  • logs 保存所有更新的引用记录
  • objects 所有的git对象都会存放在这个目录中,对象的SHA1哈希值的前两位是文件夹名称,后38位作为对象文件名
  • refs 这个目录一般包括三个子文件夹,heads、remotes和tags,heads中的文件标识了项目中的各个分支指向的当前commit

git中的引用是一个非常重要的概念,对于理解分支、HEAD指针以及reflog非常有帮助。

* 认识HEAD

HEAD是一个引用,一般情况下间接指向你当前所在的分支的最新的commit上。HEAD跟Git中一般的引用不同,它并不包含某个commit的SHA1哈希值,而是包含当前所在的分支,所有HEAD直接执行当前所在的分支,然后间接指向当前所在分支的最新提交。
可以看一下”.git/HEAD”的内容:
ref: refs/heads/master
这表示HEAD是一个指向master分支的引用,根据路径查看到一下内容:
74be835ef696faed6ca9e7ee43474253b695a4bf
一个哈希值,前两位为文件夹名,后38位及时文件名,根据上述讲到文件存储都在object目录下可以看到
github

三、基本用法

目前为止,文章依旧没有涉及命令行操作,依旧在解释Git基础逻辑。

1.基本配置

  • git config --list 显示所有配置信息

  • git config --global user.name yourname 配置全局用户名,如果不要”--global”或者使用”--local”,则表示配置为局部用户名

  • git config --global user.email youremail 配置全局电子邮箱
  • git config --global alias.cm commit 为git命令配置别名。他可以降低工作繁琐性,还可以配置复合操作

  • git help 显示帮助信息

当然,你也可以生成你的ssh公钥来配置你的git本地与github远程仓库。

2.创建及基本本地操作版本库

1.基本指令

  • git init <dir> 在当前目录(不添加dir参数)或dir目录下创建为Git仓库,初始化后会在指定目录下生成一个.git的隐藏目录

github

上面的4条指令实在工作区、暂存区和本地仓库之间进行复制文件。

  • git add <files> 将此文件添加至暂存区
    • . :提交新文件(new)和被修改(modified)文件,不包括被删除(deleted)文件
    • -u :提交被修改(modified)和被删除(deleted)文件,不包括新文件(new)
    • -A :是上面两个功能的合集
  • git commit 将暂存区中文件生成快照,并提交至本地仓库
    • -m “提交内容描述” :如果这里不用-m参数的话,git将调到一个文本编译器(通常是vim)来让你输入提交的描述信息
    • -a :可只将所有被修改或者已删除的且已经被git管理的文档提交倒仓库中
    • —amend :对于已提交过文件形成版本后,如过需要在该版本上添加或修改文件,则需要使用,该提交会使就提交取消
  • git reset -- <files> 用来完成版本回退工作
    • —hard :stage与工作区都将回退,该命令危险度较高,因为在回退版本之后会很难找回回退前的版本,之后可以选择文件、HEAD、HEAD~版本号、提交的sha-1值
    • —soft :工作区回退,而不会恢复到stage中,所以如果将数据还原只需将stage中的数据commit就可以了
    • —mixed :此为默认方式,将stage和本地仓库代码还原至上个版本,但将还原前的内容保留在工作区中
  • git checkout -- <files> 将stage的文件版本取出并覆盖至工作区)(‘—‘表示为在此版本分支下的文件,或使用版本号及commitID代替,表示为使用该版本文件或该次提交时的文件来覆盖工作区)。如果files是版本名的话就会将当前分支切换到该分支上。
    • . :将本地仓库中的文件来覆盖工作区与stage区域
    • -b:生成一个版本分支,并切换到该分支上,类似git branch branchname & git checkout branchname的合并命令

2.查阅指令

  • git status 在进行文件的上传后,可以查看暂存区文件的状态

    Untracked files 未被跟踪的文件,路径为工作目录
    Changes to be committed 已添加至暂存区
    Changes not staged for commit 跟踪的已发生改变的文件

github
此时可以清楚看到在git add test3.txt后,Changes to be committed下就有个该文件,表示该文件已添加至暂存区;而在删除文件后也会在此提示。

git log <files>
此时可以清楚看到每次提交的日志信息

  • commit为此次提交的sha-1唯一码
  • Author为此次提交的用户信息
  • Date为此次提交的日期
  • 最后下方为此次提交的日志。

git log --pretty=oneline <files>
但是每次显示的信息太多,我们也并不关心这些,所以我们也可以使用这个指令来查看提交日志。
github

git ls-files --stage
这里可以查看index暂存区的文件名与其快照
github

git reflog
可以查看项目此时head指针指向的版本(当前版本),及所有的版本号
github

3.差异指令

git diff <files>
可以查看工作区与暂存区快照之间的差异,也就是修改文件还没有暂存起来变化的内容。

  • 第一行显示为变化前后的文件名称
  • 第二行显示变化前后的快照sha-1ID号
  • @@中间为文件-修改前版本与+修改后版本
  • —-为修改前的文件名;+++为修改后的文件名
  • 最后显示未修改内容(白)、删除内容(红)及添加内容(绿)颜色不同的地方可能是我使用的是Cmder来运行git
    github

git diff --cached <files>
这里可以查看暂存区与最新版本库的代码差异,显示也同上。

git diff --staged <files>
这里可以查看下一次提交到本地代码库的内容

git diff head <files>
这里可以查看工作区与最新版本库的代码差异,如果此处的head指向master分支,则可以使用master代替head。

git diff <branch1> <branch2> <files>
git diff <branch1> <branch2> --stat <files>
而这里是比较不同版本或单独文件的差异详情,而后者则不会显示差异详情内容

4.文件删除

git rm <files>
用于删除版本库中的文件,在删除之后会在stage中形成差异,可以在git status中查看。在需要将文件彻底删除则使用git commit指令;如需恢复则可以用上述的git checkout指令

  • —cached :当我们需要删除暂存区或分支上的文件, 但本地又需要使用, 只是不希望这个文件被版本控制

github
这里可以看到使用git rm删除后git commit就完成版本文件的删除。
而在下面因为在删除后本地的情况与stage中一样,所以直接使用git check是无效的,而是先使用git reset将本地仓库中的文件目录与stage区域同步,再使用git checkout将stage区域与工作区同步,完成文件恢复。
github

3.分支

我们知道在使用git的时候,每次提交(commit)都会形成一个版本。git将他们串成一条时间线,而作为初始化版本的master就是一条时间线。而HEAD严格来说不是指向提交,而是指向master,master才是指向提交的,所以HEAD指向的就是当前分支。

1.基本操作

git branch 查看本地分支

  • -a :查看所有分支
  • -r :查看远程分支
  • -d :删除已经参与了合并的分支
  • -D :强制删除分支
  • -m :重命名当前分支
  • -v :查看各个分支最后一个提交对象的信息
  • —no-merged :查看尚未合并的分支。在删除这些尚未合并的分支可能会需要-D参数,来保护分支中还包含着尚未合并进来的工作成果

在此之前,我们先创建一个分支:
git branch <branchname> 创建一个分支。当然,如果想创建分支并切换到那个分支上,可以使用命令git checkout -b <branchname>来完成
git checkout <branchname> 切换分支

git merge <branchname> 合并分支,将branchname分支与所在分支合并成一次新的提交

  • —abort :中断一次合并,会尝试恢复到你运行合并前的状态。但当运行命令前,在工作目录中有未储藏、未提交的修改时它不能完美处理,除此之外它都工作地很好。
  • -Xignore-space-change :忽略所有空白修改。如果你的团队中的某个人可能不小心重新格式化空格为制表符或者相反的操作,这会是一个救命稻草。

完成分支工作后,我们就可以合并分支了,合并后我们可以查看
github

git 作了合并,但没有提交,它会停下来等你解决冲突。vim打开文件可以看到下文件:
github
可以看到 =======隔开的上半部分,是 HEAD(即 master 分支,在运行 merge 命令时所切换到的分支)中的内容,下半部分是在test2分支中的内容。解决冲突的办法无非是二者选其一或者由你亲自整合到一起。

当然,git允许你使用其他的合并工具或图形化界面。在合并之后可以使用下面的命令开启图形化冲突解决:
git mergetool
github
这里显示默认的合并工具是tortoisemerge,在后面可以输入opendiff,就可以打开一个图形合并工具。

在完成合并后,如何没有冲突则不需要提交,如果冲突解决后就可以git addgit commit来完成此次的合并提交工作。

git log --graph 可以看到分支合并图

2.merge与rebase

首先,git merge与git rebase所做的事是一样的。都被设计来将一个分支的更改并入另一个分支,只不过方式有些不同。
经常会在工作中创建分支:
github

现在,如果master中新的提交和你的工作是相关的。为了将新的提交并入你的分支,你有两个选择:merge或rebase。

2.merge

关于merge合并的方法我们也已经知道,命令git checkout featuregit merge master 或者 git merge master feature可以在feature分支中生成新的合并提交。得到以下结构:
github

Merge是一个安全的操作。现有的分支不会被更改,避免了rebase潜在的缺点。
但是这意味着每次合并上游更改时feature分支都会引入一个外来的合并提交。如果master非常活跃的话,这或多或少会污染你的分支历史。当大量分支合并时就需要高级git log来查看项目历史提交。

fast-forward和no fast forward

当前分支合并到另一分支时,如果没有分歧解决,就会直接移动文件指针。这个过程叫做fastforward。。属于快进方式,不过这种情况如果删除分支,则会丢失分支信息。因为在这个过程中没有创建commit。

github
可以看到蓝框就是master的直接后代(无分叉)的结果,这时git会默认在merge时是执行一个fast-forward的merge策略,git并不会创建一个merge commit而是简单地把T1分支标签移动到master分支tip所指向的commit;这时合并后master分支就变换成一个透明分支,我们在提交图谱中无法得知其存在(途中绿色部分),并且一旦这个branch被删除,那么从历史上我们再也无法看到任何关于这个开发分支曾经存在的历史渊源。
这不是我们所想要的,所以我们通过强制git产生一个真正的merge—-通过使用—no-ff参数(no fast forward的意思)。
git merge --no-ff brenchname no fast forward的意思,使得每一次的合并都创建一个新的commit记录。即使这个commit只是fast-foward,用来避免丢失信息。
git merge --squash brenchname 是用来把一些不必要commit进行压缩。需要进行一次额外的commit来“总结”一下,然后完成最终的合并。

3.rebase

作为merge的替代选择,你可以像merge一样使用rebase。
git checkout branchgit rebase basebranch topicbranch
github

merge试讲两个分支进行合并并完成一次提交,而rebase是在当前分支上重演另一个分支的历史。会联想到一次剪切效果。
其最大特点就是会使你的项目历史非诚干净,呈现出一条线性提交,因为它不会引入合并提交,因此在合并后显示的提交日志和 gitk 结构清晰。但是你需要注意使用rebase的黄金定律:
永远不要rebase一个已经分享的分支(到非remote分支,比如rebase到master,develop,release分支上),也就是说永远不要rebase一个已经在中央库中存在的分支.只能rebase你自己使用的私有分支。
因为在进行多人并行开发时,在rebase后没有合并提交历史,这样会给整个项目历史带来灾难性的影响。

在下图可以看到有两个分支,master和t1;两个分支都对其父节点做了commit提交。
github

首先切换至t1分支,并开始于master分支rebase;在完成分支衍合后,可以看到我们目前进入游离分支,一旦切换分支该分支则会消失。
git status查看工作空间后可以看到已添加至f9d1184...的commitid上,查看上图的分支图就知道这个分支是master分支。
github

最后命令git add git rebase —continue就完成了rebase的衍合操作。再进入master分支merge,git checkout mastergit merge branchname就完成了在主分支上的衍合。查看log日志可以看到记录和merge合并完全不一样。
github

交互式rebase

git rebase -i <master> 该功能可以在rebase的基础上进行历史重写效果。
下图中可以看到两条分支:master和t,两者的分支都对文件有修改,现在开始对t分支进行历史修改并rebase到master分支上:
github

可以看到这里会给出t分支上的历史版本:
github

  • pick: 正常选中
  • reword: 选中,并且修改提交信息;
  • edit: 选中,rebase时会暂停,允许你修改这个commit(参考这里)
  • squash: 选中,会将当前commit与上一个commit合并
  • fixup: 与squash相同,但不会保存当前commit的提交信息
  • exec: 执行其他shell命令

这里使用fixup测试一下:
github

在修改完成并提交后,完成相应修改后就可以完成合并。
github

最后将分支合并至master上,查看就可以看到将t分支的修改版修改完成后,rebase到master分支上。
github

4.标签

git可以给历史中的某一个提交打上标签,表示及其重要。

git使用两种主要类型的标签:轻量标签(lightweight)与附注标签(annotated)。
一个轻量标签很像一个不会改变的分支 - 它只是一个特定提交的引用。

git tag -a V1.4 -m “message” 这里创建一个附注标签。-m 选项指定了一条将会存储在标签中的信息。如果没有为附注标签指定一条信息,Git 会运行编辑器要求你输入信息
git show <tagname> 这条命令可以看到标签信息与对应的提交信息。其中会显示打标签者、日期、附注信息等

git tag <CreateTagName> 创建轻量标签,只需要提供标签名字
下图可以看到两个tag详情,左侧为附注标签,右侧为轻量标签。可以很清楚看到两者的差别;当然差别不知是表面。轻量标签本质上是将提交校验和存储到一个文件中,没有保存任何其他信息。而附注标签是存储在 git 数据库中的一个完整对象。
github

git show <commitID> 在我们过去的版本中也可以打标签,寻找到提交ID就可以完成

git tag 列出已有的标签列表。

  • -d :删除tag