git 进阶

仓库概念

在git中,仓库的概念十分简单,也是最核心的概念之一。
仓库用一个文件夹来存储和描述,这个文件夹就在你项目的那个目录下,是个隐藏文件.git/。除此以外没有其他的位置存储这个仓库想关的信息。
这种仓库的概念非常的简约,会有一个问题,就是不小心删除的话就丢失了。因此,git的一般使用要至少进行多机备份仓库,这个备份是很简单的,在git的系统中,只是一个命令。

概念的简约,是Linus设计作品的一个特点,linux的操作系统的很多概念都非常简约。例如文件系统的概念,无论是文件夹还是设备,统一使用文件的概念去设计和使用。在这里,一个版本控制的单元使用仓库的概念,且没有那么多复杂的控制文件,就在一个小小的文件夹里,拷贝走也完全可以使用。

这种分布式仓库的概念中,没有主从关系,也没有中心关系,彼此是独立的,可以做仓库同步和融合,但是都是可控制的。

工作区 & 暂存区 & 版本库

这三个概念是git的核心逻辑。

工作区 : 就是你的实际的目录和文件。
暂存区 : 就是中间的一个过渡区,连接了版本库和工作区。
版本库 : 就是实际存放各个版本和分支的地方。

其原理是这样的,暂存区维护一份目录树,没有实际的文件。这个目录树与工作区的目录树是不一样的。工作区的目录树是操作系统中文件系统的目录树。暂存区就是存了一些文件名,时间戳,文件长度等信息。

很多时候我们会有这样的经历,我想把一些密码设置的东西,或是笔记放在我当前的目录下,但是我并不想把这部分放到仓库中,因为我会把仓库共享出去的嘛。这个时候,这个文件是存在我的工作区的,但是并不在我的暂存区,由于仓库的目录树必须由暂存区的内容来写入,所以在版本库中也不会有。

当我们对一些问件做了修改之后,使用git status -s就可以查看我们的当前哪些文件被修改但是没有提交到暂存区。

  • 使用git add XXX 可以将当前在工作区修改的部分提交到暂存区,这个时候暂存区的目录树会做出修改,更新相关文件的时间戳等信息。
    同时,我们的文件内容会被写入到对象库中,生成一个新的对象。并且这个对象会有和ID,这个ID会被写入到暂存区的目录树中。

  • 使用git commit 的时候,会把暂存区的目录树更新到版本库中对应的分支的目录树中。
    这个时候才是真正的写到了版本库中。

  • 使用git reset HEAD 的时候,暂存区的目录树会被版本库中的目录树覆盖,也就是还原到了上次你提交的版本。但是此时工作区的修改是不会受到影响的。

  • 使用git rm --cache <file> 的时候,会直接把暂存区的目录中对应的文件删除,但是工作区不受影响,不加--cache会将工作区的一并删除。

  • 使用git checkoutgit checkout -- <file> 命令的时候,会用暂存区中的文件去替换工作区的文件。
    也就是当你写一个东西,写着写着发现不好,需要去掉,但是你已经修改了很多个文件的很多个部分,手动删除不可能做到,你可以使用这个命令来把你工作区的内容回退到你之前提交到暂存区的时候的样子。
    因此,选择合适的时机进行提交操作十分重要。要在自己的开发节点上及时add,及时commit。这样可以方便回退。

  • 使用git checkout HEADgit checkout HEAD <file>,会用版本库中的对应分支的文件来替换暂存区和工作区的文件,这个动作可以参考上面一条,都是十分危险的动作。

有了这三个东西, 会发现,你好像什么都可以得到。
你可以历史查看你每次提交的改动,每个文件的每次提交后的样子你都可以得到。你拥有了一个结构合理,体积很小,速度很快的,无所不知的版本控制系统。
优秀的设计!

Git 对象

贯穿在git的各个元素中,都有40位的SHA1字符串的ID。每个ID都标志了一个对象,每个对象其实都是一个文件。
在git仓库的/objects/ 文件夹下,有一堆两个字母命名的文件夹,打开这些文件夹,里面是一些38位字符命名的文件,所以就是这40位的用法。

  • 使用git log -1 --pretty=raw 命令可以查看我们上一次提交的时候都做有哪些东西
    可以看到如下的内容:
    1
    2
    3
    4
    5
    commit xxxxxx...
    tree xxxxxx...
    parent xxxxxx...
    author ...
    commiter ...

其中包含了三个对象:

+ 一个是commit,这是本次提交的唯一ID标志。
+ tree, 这是本次提交所对应的目录树
+ parent, 这是本次提交上一次提交的ID
  • 使用git cat-file -t <ID> 来查看每个ID指代的对象的类型,分别会是: commit, tree, commit

  • 使用git cat-file -p <ID> 来查看每个对象的内容。

    • 查看一个commit,包含的信息有:tree, parent, author, committer, message.
    • 查看一个tree, 会包含很多个blob对象的信息。每个blob对象是你这次修改的每个文件。
    • 如果再对这个blob查看内容,就是文件的内容了。

通过这几个对象,就可以完整的描述一次提交,并且可以形成一个追踪链。

Git reset

在上面的记录中已经使用过这个命令。目前我们知道git有一个提交链的东西来记录提交历史,并且会记录HEAD为当前最新的一次提交。
reset就是可以进行版本回退的工具。

  • 使用git reset --hard HEAD^ 可以回退到上一个commit的时候。工作区会随之改变,如果存在没有提交的内容就会丢失。
    HEAD^代表了Head的父提交。

这种重置,会将中间的一段历史记录丢掉。使提交的历史也改变了,当我们使用git log的时候,会只显示此前的commit,但是之后的就丢失了。

下面是找回的方法。

Git reflog

由于这些丢失的commit的ID都已经不见了,我们要做的就是找到这些ID,所谓的丢失,并没有真正的删除。而是由于数据结构的需要,这部分不会直接出现在数据结构中,无法用正常的方式访问到,但是依旧存在于文件中。这个时候就相当于去修复数据结构。

  • tail -5 .git/logs/refs/heads/master 可以查看到这个分支对应的log文件,这个文件记录了这个分支所有的commit信息。以文本的形式,并不是以数据结构的形式。

  • 使用git reflog show master | head -5 可以查看最新的5次commit,当然我们的reset操作也是在其中的。
    其基本的数据格式是这样的:

    1
    2
    ID master@{0}: type : message
    ID master@{1}: type : message

其中type会标注这个操作是什么类型的,正常的都是commit, 不正常的就如我们做reset操作。这个并不重要。重要的是这个命令给每个状态都有一个别称master@{1},我们可以继续使用reset来恢复之前的reset。
git reset --hard master@{1}, 就可以跳跃到对应的状态,并且这之间的记录也会回来。

总结git reset的使用

  • git reset [-q] [<commit>] [--] <paths> 只恢复暂存区成为仓库最新提交的状态,不更改工作区。可以操作单个文件。
  • git reset [--soft | --hard | --mixed | --merge | --keep] [-q] [<commit>]
    • --soft,只会更改head指向的commit,并不会更改暂存区和工作区
    • --mixed, 会更改暂存区,也是默认的方式。
    • --hard, 会更改暂存区和工作区。

举几个案例:

  • git reset: 清空暂存区的内容。
  • git reset HEAD: 同上。
  • git reset -- filename: 撤出暂存区add的某个文件,逆add操作。
  • git reset --soft HEAD^: 对上一次提交不满意,直接删掉上一次提交。比如你手快做了一次提交,但是这个小功能块并没有完成,不应该有一个commit出现,所以你把这个commit这样忽略掉,下次重新提交。
  • git reset HEAD^: 工作区不受影响,只将版本库和暂存区往前回退一次。
  • git reset --hard HEAD^: 工作区,版本库和暂存区往前回退一次。

如何自己摸索git的结构和原理

学会使用上面用到的常用命令就可以自己去摸索git的原理。
最主要的是git cat-file 来查看对应的ID中的内容,一切都在其中了。
其次就是git log,保存了很多信息。

从此git不再神秘。

Talk is not cheap.