珠峰培训

git实用教程(三)历史穿越

作者:

2015-11-23 12:05:26

120

前面我们已经成功地添加并提交了一个index.txt文件,修改index.txt如下:

echo diff >> index.html

运行git status命令看看结果:

On branch master  //在分支master上
Changes not staged for commit:  //变化还没有添加到暂存区以备提交
(use "git add <file>..." to update what will be committed) //使用git add 命令把修改添加到暂存区
(use "git checkout -- <file>..." to discard changes in working directory) //使用git checkout 丢弃工作区中的丢改
     modified:   index.html   //修改了index.txt文件
     no changes added to commit (use "git add" and/or "git commit -a") //没有变化需要被提交( 可以使用git add 或者 git commit -a)

git status -s 加上 -s 参数可以用精简方式

其中标识有两列,拿状态 M 来说

  • 第一列绿色字母含义是:暂存区中和版本库中的文件的改动
  • 第二列红色字母含义是:工作区和暂存区的文件改动

git diff这个命令看看内容差异:

$ git diff //查看工作区和暂存区中的文件区别
diff --git a/index.html b/index.html //比较两个index.html
index 5f5fbe7..34cbd62 100644 这个5f5fbe7是暂存区中的index.html文件 可以用 git show 5f5fbe7 查看
--- a/index.html //---代表源文件
+++ b/index.html //+++代表目标文件  
@@ -1,3 +1,4 @@ //差异按照差异区域进行组织,每个差异区域的第一行都是定位语句,由@@开头,@@结尾。
// -1,3表示差异区域从第一行开始,一共有3行,+1,4表示从第1行开始,一共有4行
 1 //空格开头的行,是源文件和目标文件中都出现的行
 2 //空格开头的行,是源文件和目标文件中都出现的行
 -3 //-开头的行,是只出现在源文件中的行
 +3  //+开头的行,是只出现在目标文件中的行
+diff 增加了一行 //+开头的行,是只出现在目标文件中的行

git diff顾名思义就是查看difference,显示的格式正是Unix通用的diff格式,可以从上面的命令输出看到,我们在第一行添加了一个"1"。 index.txt作了什么修改后,再把它提交到仓库,提交修改和提交新文件是一样的两步,git add和git commit:

$ git add index.html
$ git commit -m "add diff"

注意

  • 要随时掌握工作区的状态,使用git status命令。
  • 如果git status告诉你有文件被修改过,用git diff可以查看修改内容。

提交后

我们再用git status命令看看仓库的当前状态:

$ git status
# On branch master
nothing to commit (working directory clean)

Git告诉我们当前没有需要提交的修改,而且,工作目录是干净**(working directory clean)**的。

再看历史

$ git log
commit 521cb3d9b496632b32dab77d3428598ad449b5e3
Author: zhangrenyang-t510 <zhang_renyang@>
Date:   Mon Oct 12 17:19:48 2015 +0800

    add diff

commit 728bab96b4e3bbc33e6c9123ff4513778cdb175b
Author: zhangrenyang-t510 <zhang_renyang@>
Date:   Mon Oct 12 17:02:12 2015 +0800

    add index.js

commit da19241f37a548af4522012c9286f99b0048e1eb
Author: zhangrenyang-t510 <zhang_renyang@>
Date:   Sat Oct 3 20:54:23 2015 +0800

    在index.html中增加了 1 2

小结

要随时掌握工作区和暂存区的状态,使用git status命令。

如果git status告诉你有文件被修改过, 
git diff 可查看工作区和暂存区的差异。 
git diff HEAD 可查看工作区和HEAD(当前工作分支)相比的差异 
git diff --cached 可查看暂存区和历史区的差异.

查看最新的一次提交的对象状态

git log -1 --pretty=raw
tree 本次提交的目录树
parent 父提交(上一次提交)

只知道ID想知道类型可以用

git cat-file -t a2d38866b7a9

查知道ID的内容可以用

git cat-file -p a2d38866b7a9

显示最新的提交

git log -1 HEAD master refs/heads/master
cat .git/HEAD

真实的结构图

查看暂存区目录树

git ls-files

查看历史区目录树

 git ls-tree HEAD
 100644 blob 5d342d83dcdbd77938984285c6e4feb04fc4706c    index.html

参考

Git学习笔记(一) Git的安装与使用

Git学习笔记(二) Git初始化

Git学习笔记(三) Git暂存区

Git学习笔记(四) Git对象

Git学习笔记(五) Git重置

Git学习笔记(六) Git检出

Git学习笔记(七) 恢复进度

Git学习笔记(八) Git基本操作

Git学习笔记(九) 历史穿梭

Git学习笔记(十) 改变历史

Git学习笔记(十一) Git克隆

Git学习笔记(十二) 前几章的补充

Git教程

你不断对文件进行修改,然后不断提交修改到版本库里。每当你觉得文件修改到一定程度的时候,就可以“**保存一个快照**”,这个快照在Git中被称为**commit**。一旦你把文件改乱了,或者误删了文件,还可以从最近的一个**commit**恢复,然后继续工作,而不是把几个月的工作成果全部丢失。

现在,我们回顾一下 index.html文件一共有几个版本被提交到Git仓库里了:

  • 版本1:在index.html中增加了 1 2
  • 版本2:add index.js
  • 版本3:add diff

当然了,在实际工作中,我们脑子里怎么可能记得一个几千行的文件每次都改了什么内容,不然要版本控制系统干什么。版本控制系统肯定有某个命令可以告诉我们历史记录,在Git中,我们用git log命令查看:

$ git log
commit 521cb3d9b496632b32dab77d3428598ad449b5e3
Author: zhangrenyang-t510 <zhang_renyang@>
Date:   Mon Oct 12 17:19:48 2015 +0800

    add diff

commit 728bab96b4e3bbc33e6c9123ff4513778cdb175b
Author: zhangrenyang-t510 <zhang_renyang@>
Date:   Mon Oct 12 17:02:12 2015 +0800

    add index.js

commit da19241f37a548af4522012c9286f99b0048e1eb
Author: zhangrenyang-t510 <zhang_renyang@>
Date:   Sat Oct 3 20:54:23 2015 +0800

    在index.html中增加了 1 2

git log命令显示从最近到最远的提交日志,我们可以看到3次提交,最近的一次是在add diff,上一次是add index.js,最早的一次是index.html中增加了 1 2。 如果嫌输出信息太多,看得眼花缭乱的,可以试试加上--oneline参数:

$ git log --oneline
521cb3d add diff
728bab9 add index.js
da19241 在index.html中增加了 1 2

需要友情提示的是,你看到的一大串类似521cb3d的是commit id(版本号),和SVN不一样,Git的commit id不是1,2,3……递增的数字,而是一个SHA1计算出来的一个非常大的数字,用十六进制表示,而且你看到的commit id和我的肯定不一样,以你自己的为准。为什么commit id需要用这么一大串数字表示呢?因为Git是分布式的版本控制系统,后面我们还要研究多人在同一个版本库里工作,如果大家都用1,2,3……作为版本号,那肯定就冲突了。

每提交一个新版本,实际上Git就会把它们自动串成一条时间线。如果使用可视化工具查看Git历史,就可以更清楚地看到提交历史的时间线:

好了,现在我们启动时光穿梭机,准备把**readme.html**回退到上一个版本,也就是“**add index.js**”的那个版本,怎么做呢?

首先,Git必须知道当前版本是哪个版本,在Git中,用HEAD表示当前版本,也就是最新的提交**521cb3d9b496632b32dab77d3428598ad449b5e3**(注意我的提交ID和你的肯定不一样),上一个版本就是**HEAD^**,上上一个版本就是**HEAD^^**,当然往上100个版本写100个^比较容易数不过来,所以写成**HEAD~100**。

现在,我们要把当前版本add diff回退到上一个版本add index.js,就可以使用git reset命令:

$ git reset --hard HEAD^ //把历史区重置到上一个提交,请注意在windows的cmd下面此语句不行,只能在git bash下执行,windows下可以把HEAD^换成上一个 commit id
HEAD is now at 728bab9 add index.js

--hard参数有啥意义?这个后面再讲,现在你先放心使用。

看看index.html的内容是不是版本add index.js:

$ cat index.html
1
2
3

看到了吧,第三个提交添加的第四行diff已经没有了.

还可以继续回退到上一个版本 在index.html中增加了 1 2,不过且慢,我们用git log再看看现在版本库的状态:

$ git log
commit 728bab96b4e3bbc33e6c9123ff4513778cdb175b
Author: zhangrenyang-t510 <zhang_renyang@>
Date:   Mon Oct 12 17:02:12 2015 +0800

    add index.js

commit da19241f37a548af4522012c9286f99b0048e1eb
Author: zhangrenyang-t510 <zhang_renyang@>
Date:   Sat Oct 3 20:54:23 2015 +0800

    在index.html中增加了 1 2

最新的那个版本add diff已经看不到了!好比你从21世纪坐时光穿梭机来到了19世纪,想再回去已经回不去了,肿么办?

办法其实还是有的,只要上面的命令行窗口还没有被关掉,你就可以顺着往上找啊找啊,找到那个add diff的commit id是521cb3d9b496632b32dab77d3428598ad449b5e3,于是就可以指定回到未来的某个版本:

$ git reset --hard 521cb3d
HEAD is now at 521cb3d add diff

版本号没必要写全,前7位就可以了,Git会自动去找。当然也不能只写前一两位,因为Git可能会找到多个版本号,就无法确定是哪一个了。

再看看readme.txt的内容:

$ cat index.html
1
2
3
diff

可以看到diff又回来了,历史也回来了。 Git的版本回退速度非常快,因为Git在内部有个指向当前版本的HEAD指针,当你回退版本的时候,Git仅仅是把HEAD从指向add diff:

然后顺便把工作区的文件更新了。所以你让HEAD指向哪个版本号,你就把当前版本定位在哪。

现在,你回退到了某个版本,关掉了电脑,第二天早上就后悔了,想恢复到新版本怎么办?找不到新版本的 commit id 怎么办?

在Git中,总是有后悔药可以吃的。当你用git reset --hard HEAD^回退到add index.js版本时,再想恢复到add diff,就必须找到add diff的 commit id 。Git提供了一个命令git reflog用来记录你的每一次命令:

$ git reflog
521cb3d HEAD@{0}: reset: moving to 521cb3d
728bab9 HEAD@{1}: reset: moving to HEAD^
521cb3d HEAD@{2}: commit: add diff
728bab9 HEAD@{3}: commit: add index.js
da19241 HEAD@{4}: commit (initial): 在index.html中增加了 1 2

终于舒了口气,第二行显示add diff的commit id 是`521cb3d·,现在,你又可以乘坐时光机回到未来了。

$ git reset --hard 521cb3d
HEAD is now at 521cb3d add diff

现在总结一下:

HEAD 指向的版本就是当前版本,因此,Git允许我们在版本的历史之间穿梭,使用命令git reset --hard commit_id。

穿梭前,用git log可以查看提交历史,以便确定要回退到哪个版本。

git log --oneline -2 --grep='index.html' 过滤

要重返未来,用git reflog查看命令历史,以便确定要回到未来的哪个版本。

git reset扩展

  • git reset –mixed:此为默认方式,不带任何参数的git reset,即时这种方式,它回退到某个版本, 工作区 不变,回退历史区 和 暂存区
  • git reset –soft:回退到某个版本,只回退了历史区的信息,工作区 和 暂存区 都不变
  • git reset –hard:彻底回退到某个版本,回退 工作区 、历史区 和 暂存区 。

现在,假定你已经完全掌握了暂存区的概念。下面,我们要讨论的就是,为什么Git比其他版本控制系统设计得优秀,因为Git跟踪并管理的是修改,而非文件。

你会问,什么是修改?比如你新增了一行,这就是一个修改,删除了一行,也是一个修改,更改了某些字符,也是一个修改,删了一些又加了一些,也是一个修改,甚至创建一个新文件,也算一个修改。

为什么说Git管理的是修改,而不是文件呢?我们还是做实验。第一步,对readme.txt做一个修改,比如加一行内容:

 $ echo edit1 >> index.html

然后,添加:

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

        modified:   index.html

然后,再修改readme.html:

$ echo edit2 >> index.html

直接提交:

$ git commit -m"only commit edit1"
[master 51f74a9] only commit edit1
 1 file changed, 1 insertion(+), 1 deletion(-)

提交后,再看看状态: $ git status On branch master Changes not staged for commit: (use "git add ..." to update what will be committed) (use "git checkout -- ..." to discard changes in working directory)

         modified:   index.html

 no changes added to commit (use "git add" and/or "git commit -a")

咦,怎么第二次的修改没有被提交?

别激动,我们回顾一下操作过程:

第一次修改 -> git add -> 第二次修改 -> git commit

你看,我们前面讲了,Git管理的是修改,当你用git add命令后,在工作区的第一次修改被放入暂存区,准备提交,但是,在工作区的第二次修改并没有放入暂存区,所以,git commit只负责把暂存区的修改提交了,也就是第一次的修改被提交了,第二次的修改不会被提交。

提交后,用`git diff HEAD`命令可以查看工作区和版本库里面最新版本的区别:

$ git diff HEAD
diff --git a/index.html b/index.html
index 88713e9..ae0a04c 100644
--- a/index.html
+++ b/index.html
@@ -2,3 +2,4 @@
 2
 3
 diff
 edit1
+edit2 //第二次的修改表示出了差异

可见,第二次修改确实没有被提交。


那怎么提交第二次修改呢?你可以继续`git add`再`git commit`,也可以别着急提交第一次修改,先`git add`第二次修改,再`git commit`,就相当于把两次修改合并后一块提交了:

第一次修改 -> `git add` -> 第二次修改 -> `git add` -> `git commit`

好,现在,把第二次修改提交了,然后开始小结。

小结

现在,你又理解了Git是如何跟踪修改的,每次修改,如果不`add`到暂存区,那就不会加入到`commit`中。

自然,你是不会犯错的。不过现在是凌晨两点,你在readme.html中添加了一行:

$ echo bug >> index.html

在你准备提交前,一杯咖啡起了作用,你猛然发现了bug!

$ cat index.html
1
2
3
diff
edit1
edit2
bug

既然错误发现得很及时,就可以很容易地纠正它。 先用git status查看一下:

$ git status
On branch master
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:   index.html

no changes added to commit (use "git add" and/or "git commit -a")

你可以发现,Git会告诉你,git checkout -- file可以丢弃工作区的修改:也就是从暂存区中检出

$ git checkout -- index.html

命令git checkout -- index.html意思就是,把index.html文件在工作区的修改全部撤销, 使用暂存区里的index.html覆盖工作区的 index.html。

现在,看看`index.html`的文件内容:

$ cat index.html
1
2
3
diff
edit1
edit2

文件内容果然复原了。

git checkout -- file命令中的--很重要,没有--,就变成了“切换到另一个分支”的命令,我们在后面的分支管理中会再次遇到git checkout命令。

现在假定是凌晨3点,你不但写了一些胡话,还git add到暂存区了:

$ echo bug >> index.html
$ git add index.html

庆幸的是,在commit之前,你发现了这个问题。用git status查看一下,修改只是添加到了暂存区,还没有提交:

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

        modified:   index.html

Git同样告诉我们,用命令git reset HEAD file可以把暂存区的修改撤销掉(unstage),重新放回工作区:

$ git reset HEAD index.html
Unstaged changes after reset:
M       index.html

git reset命令既可以回退版本,也可以把暂存区的修改回退到工作区。当我们用HEAD时,表示最新的版本。

再用git status查看一下,现在暂存区是干净的,工作区有修改:

$ git status On branch master Changes not staged for commit: (use "git add ..." to update what will be committed) (use "git checkout -- ..." to discard changes in working directory)

      modified:   index.html

no changes added to commit (use "git add" and/or "git commit -a")

还记得如何丢弃工作区的修改吗?

$ git checkout -- index.html
$ git status
On branch master
nothing to commit, working directory clean

整个世界终于清静了!

现在,假设你不但改错了东西,还从暂存区提交到了版本库,怎么办呢?还记得版本回退一节吗?可以回退到上一个版本。不过,这是有条件的,就是你还没有把自己的本地版本库推送到远程。 还记得Git是分布式版本控制系统吗?我们后面会讲到远程版本库,一旦你把“bug”提交推送到远程版本库,你就真的惨了……

小结

  • 场景1:当你改乱了工作区某个文件的内容,想直接丢弃工作区的修改时,用命令git checkout -- file。
  • 场景2:当你不但改乱了工作区某个文件的内容,还添加到了暂存区时,想丢弃修改,分两步,第一步用命令git reset HEAD file,就回到了场景1,第二步按场景1操作。
  • 场景3:已经提交了不合适的修改到版本库时,想要撤销本次提交,参考版本回退一节,不过前提是没有推送到远程库。

在Git中,删除也是一个修改操作,我们实战一下,先把前面添加和index.js删除,可以在资源管理器中删除也可以用 rm

$ rm index.js

这个时候,Git知道你删除了文件,因此,工作区和版本库就不一致了,git status命令会立刻告诉你哪些文件被删除了:

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

        deleted:    index.js

no changes added to commit (use "git add" and/or "git commit -a")

现在你有两个选择

一是误删除了

但暂存区里还有呢,所以可以很轻松地把误删的文件从暂存区恢复到工作区:

$ git checkout -- index.js

无论工作区是修改还是删除,git checkout都可以“一键还原”。

二是确实要删除

一是确实要从版本库中删除该文件,那就用命令git rm删掉,并且git commit:

$ git rm index.js
rm 'index.js'
$ git commit -m "delete index.js"
[master d4b9890] delete index.js
 1 file changed, 0 insertions(+), 0 deletions(-)
 delete mode 100644 index.js

现在,文件就从版本库中被删除了。

小结

命令git rm用于删除一个文件。如果一个文件已经被提交到版本库,那么你永远不用担心误删,但是要小心,你只能恢复文件到最新版本,你会丢失最近一次提交后你修改的内容。