GinoBeFunny

《Git权威指南》读书笔记

Git是现在最流行的版本控制工具,其开创性的分布式版本控制特性使其快速拥有一大群粉丝,是Linus Torvalds的又一大作。除了常见的命令,学习其设计理念和原理机制也很有必要。

版本控制的前世和今生

略。

爱上Git的理由

略。

Git的安装和使用

略。

Git初始化

创建版本库及第一次提交

配置用户名和Email

git config --global user.name "Gino Zhang"
git config --global user.email "zf_zmc@163.com"

设置常用的一些命令别名

git config --global alias.st status
git config --global alias.ci commit
git config --global alias.co checkout
git config --global alias.br branch

开启颜色显示

git config --global color.ui true

初始化版本库

git init demo
Initialized empty Git repository in D:/git_learning/demo/.git/

创建第一个文件welcome.txt

echo "Hello." > welcome.txt

将新建的文件添加到暂存区中

git add welcome.txt

将新建的文件添加到版本库中

git ci -m "initialized."
[master (root-commit) 0e2185a] initialized.
 1 file changed, 1 insertion(+)
 create mode 100644 welcome.txt

为什么工作区跟目录下有一个.git目录

  • 对于Git来说,.git目录即为本地的版本库,子目录下不存在其他跟踪文件或目录;
  • 这样设计可以使得大部分的操作(除了远程版本库操作外)都可以在本地完成;
  • 当在子目录执行git命令时会递归查找到根目录的.git目录;

git config命令的各个参数

版本库级别的配置文件

git config -e
/d/git_learning/demo/.git/config 
[core]
        repositoryformatversion = 0
        filemode = false
        bare = false
        logallrefupdates = true
        symlinks = false
        ignorecase = true

全局配置文件

git config -e --global
~/.gitconfig
[user]
        name = Gino Zhang
        email = zf_zmc@163.com
[alias]
        st = status
        ci = commit
        co = checkout
        br = branch
[color]
        ui = true

系统级配置文件

git config -e --system
/mingw64/etc/gitconfig
[credential]
        helper = manager

备份本章的工作成果

/d/git_learning
$ git clone demo demo-step-1
Cloning into 'demo-step-1'...
done.

暂存区

修改不能直接提交吗

给welcome.txt追加一行

echo "Nice to meet you." >> welcome.txt

使用git diff命令比较下差别

git diff
diff --git a/welcome.txt b/welcome.txt
index 18832d3..fd3c069 100644
--- a/welcome.txt
+++ b/welcome.txt
@@ -1 +1,2 @@
 Hello.
+Nice to meet you.

试试直接提交到版本库

$ git ci -m "Append a new line."
On branch master
Changes not staged for commit:
        modified:   welcome.txt
no changes added to commit

提示告诉我们修改的文件也必须先使用git add添加之后才能使用git commit提交。
那就将修改“提交”到提交任务中去。

git add welcome.txt
git diff

但是执行git diff时没有输出,难道是提交成功了?试试git status和git diff HEAD呢。

git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)
        modified:   welcome.txt


git diff HEAD
diff --git a/welcome.txt b/welcome.txt
index 18832d3..fd3c069 100644
--- a/welcome.txt
+++ b/welcome.txt
@@ -1 +1,2 @@
 Hello.
+Nice to meet you.

说明当前与HEAD(或MASTER分支)是存在差别的。这时如果使用git commit就可以提交入库了,但是为了试验,我们继续修改welcome.txt文件试试看。

echo "Bye-Bye." >> welcome.txt
git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)
        modified:   welcome.txt

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)
        modified:   welcome.txt

这就说明不但版本库的最新提交与暂存区存在差别,当前工作区与暂存区也存在差别。即存在三个版本的文件:一个在工作区、一个在等待提交的暂存区、一个是版本库中最新提交的。那三者之间如何比较呢?

git diff      //比较的是工作区与暂存区的差别
git diff HEAD //比较的是工作区和版本库最新提交的差别
git diff --cached //比较的是暂存区和版本库最新提交的差别

这时候使用git commit提交会怎么样呢?试试看

git ci -m "Which version checked in?"
[master 3231bdc] Which version checked in?
 1 file changed, 1 insertion(+)

这时候提交到版本库的会是哪个版本呢?使用上面的diff命令可以看出来,真正提交的是暂存区中的版本。

理解Git暂存区

暂存区的设计是Git最成功的设计之一,也是最难理解的。

.git目录下有一个index文件,我们来玩一个和它有关的小实验。

先把之前未提交的welcome.txt撤销掉。

git co -- welcome.txt

然后查看下.git/index文件的时间戳

ls --full-time .git/index
-rw-r--r-- 1 35360 197609 145 2016-12-14 17:30:32.674746800 +0800 .git/index //注意这个时间17:30:32

我们使用touch命令但是不改变它的内容,然后使用git status -s查看状态,再观察时间戳

touch welcome.txt
git status -s
ls --full-time .git/index
-rw-r--r-- 1 35360 197609 145 2016-12-14 17:35:24.975946900 +0800 .git/index //注意时间戳改变了

这说明执行git status命令扫描工作区变动时,先根据.git/index文件中记录的时间戳、长度等信息来判断工作区文件是否变动,如果有变动则打开文件对比原始文件,否则则将该文件新的时间戳记录到.git/index中。这种方式使得Git的扫描能更加快速高效。

文件.git/index实际上就是一个包含文件索引的目录树,其记录了文件和文件状态而非内容。文件的内容尽量在Git对象库.git/objects目录中,文件索引建立了文件和对象实体之间的关系。

工作区、版本库、暂存区原理图

工作区、版本库、暂存区原理图

  • HEAD实际是指向master分支的一个“游标”;
  • 执行git add时,暂存区目录树会被更新,文件内容被写入到对象库,对象的ID记录在文件索引中;
  • 执行git commit时,暂存区的目录树写到版本库,master分支做相应的更新;
  • 执行git reset HEAD时,暂存区的目录树被master分支指向的目录树替换,工作区不受影响;
  • 执行git rm –cached时,会直接从暂存区删除,工作区不受影响;
  • 执行git checkout .时,工作区的目录树被暂存区的替换;
  • 执行git checkout HEAD . 或git checkout HEAD时,会用HEAD指向的master分支替换工作区和暂存区的文件;

Git Diff魔法

先清理暂存区和工作区,使其和master分支保存一致。

git clean -fd
git checkout .

添加一个文件目录和文件,并修改welcome.txt的内容,然后添加到暂存区

echo "Bye-Bye." >> welcome.txt
mkdir subdir
echo "Hello." > subdir/hello.txt
git add .

然后再次修改hello.txt文件的内容,是的工作区、暂存区和版本库的目录树均不同

echo "Bye-bye." >> subdir/hello.txt
git status -s
AM subdir/hello.txt
M  welcome.txt

下面来看如果用git diff比较他们之间的区别

Git diff

  • git diff:工作区和暂存区的比较;
  • git diff –cached:暂存区和HEAD比较;
  • git diff HEAD:工作区和HEAD比较;

不用使用git commit -a

这个命令可以直接将工作区的修改和删除文件直接提交版本库,但是会有两个问题:一个是没有包括未被版本库跟踪的文件;另外一个是丢掉了暂存区带来的好处(对提交内容进行控制的能力)。

搁置问题,暂存状态

git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        new file:   subdir/hello.txt
        modified:   welcome.txt

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   subdir/hello.txt

git stash
Saved working directory and index state WIP on master: 3231bdc Which version checked in?
HEAD is now at 3231bdc Which version checked in?

git status
On branch master
nothing to commit, working tree clean

Git对象

Git对象库探秘

为什么一个提交里有三个SHA1的对象ID:

git log -1 --pretty=raw
commit 3231bdcee2336b13684a283960c0bba366519dfa      #本次提交标识
tree f58da9a820e3fd9d84ab2ca2f1b467ac265038f9        #本次提交对应的目录树
parent 0e2185a617f030d147e7092d34c478125da861b1      #本次提交的父提交(上一次提交)
author Gino Zhang <zf_zmc@163.com> 1481706190 +0800
committer Gino Zhang <zf_zmc@163.com> 1481706190 +0800
    Which version checked in?

研究Git对象ID的一个重量级武器就是git cat-file命令。

git cat-file -p 3231
tree f58da9a820e3fd9d84ab2ca2f1b467ac265038f9
parent 0e2185a617f030d147e7092d34c478125da861b1
author Gino Zhang <zf_zmc@163.com> 1481706190 +0800
committer Gino Zhang <zf_zmc@163.com> 1481706190 +0800
Which version checked in?

git cat-file -p f58d
100644 blob fd3c069c1de4f4bc9b15940f490aeb48852f3c42    welcome.txt  #blob对应的文件继续看

git cat-file -p fd3c    # 对应的是welcome.txt的文件内容
Hello.
Nice to meet you.

git cat-file -p 0e21
tree 190d840dd3d8fa319bdec6b8112b0957be7ee769
author Gino Zhang <zf_zmc@163.com> 1481703025 +0800
committer Gino Zhang <zf_zmc@163.com> 1481703025 +0800
initialized.

这些对象保存在.git/object目录下(ID的前2位是目录,后38位是文件名)。
这些对象之间形成了一条跟踪链,可以通过git log查看。

git log --pretty=raw --graph 3231
* commit 3231bdcee2336b13684a283960c0bba366519dfa
| tree f58da9a820e3fd9d84ab2ca2f1b467ac265038f9
| parent 0e2185a617f030d147e7092d34c478125da861b1
| author Gino Zhang <zf_zmc@163.com> 1481706190 +0800
| committer Gino Zhang <zf_zmc@163.com> 1481706190 +0800
|
|     Which version checked in?
|
* commit 0e2185a617f030d147e7092d34c478125da861b1
  tree 190d840dd3d8fa319bdec6b8112b0957be7ee769
  author Gino Zhang <zf_zmc@163.com> 1481703025 +0800
  committer Gino Zhang <zf_zmc@163.com> 1481703025 +0800

      initialized.

SHA1哈希值到底是什么,如何生成的?

可以总结为综合了“对象类型(commit, blob, tree)、对象大小(提交说明字符长度、文件大小、目录树内容大小)、对象内容(提交说明、文件内容、目录树内容)”因素生成的一个标识,冲突概率很低很低。

为什么不用顺序的数字来表示提交?

主要是由于Git是分布式的版本库,如果采用顺序的数字来标识提交,不可避免会造成冲突。

Git重置

分支游标master探秘

新增一个文件,观察文件.git/ref/heads/master的内容如何改变

$ cat .git/refs/heads/master
3231bdcee2336b13684a283960c0bba366519dfa

$ touch new-commit.txt
$ git add new-commit.txt

$ git commit -m "does master follow this new commit?"
[master cfa86f4] does master follow this new commit?
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 new-commit.txt

$ cat .git/refs/heads/master
cfa86f4465926576eb3a9e0bc77e115af0467551   #指向了新的提交

$ git log --graph --oneline
* cfa86f4 does master follow this new commit?
* 3231bdc Which version checked in?
* 0e2185a initialized.

重置版本库(危险动作

$ git reset --hard HEAD^
HEAD is now at 3231bdc Which version checked in?

$ git reset --hard 0e2185a
HEAD is now at 0e2185a initialized.

$ cat welcome.txt
Hello.

重置命令很危险,会彻底底丢弃历史。还能通过浏览历史的办法找回历史的提交ID然后再恢复吗?不能!因为提交历史也没了。

用reflog挽救错误的重置

$ tail -10 .git/logs/refs/heads/master
0000000000000000000000000000000000000000 0e2185a617f030d147e7092d34c478125da861b1 Gino Zhang <zf_zmc@163.com> 1481703025 +0800  commit (initial): initialized.
0e2185a617f030d147e7092d34c478125da861b1 3231bdcee2336b13684a283960c0bba366519dfa Gino Zhang <zf_zmc@163.com> 1481706190 +0800  commit: Which version checked in?
3231bdcee2336b13684a283960c0bba366519dfa cfa86f4465926576eb3a9e0bc77e115af0467551 Gino Zhang <zf_zmc@163.com> 1482915083 +0800  commit: does master follow this new commit?
cfa86f4465926576eb3a9e0bc77e115af0467551 3231bdcee2336b13684a283960c0bba366519dfa Gino Zhang <zf_zmc@163.com> 1482915422 +0800  reset: moving to HEAD^
3231bdcee2336b13684a283960c0bba366519dfa 0e2185a617f030d147e7092d34c478125da861b1 Gino Zhang <zf_zmc@163.com> 1482915443 +0800  reset: moving to 0e2185a

To be continued…

Gino Zhang wechat
扫一扫,关注我的微信公众号