教程目录
一、Git后悔药
撤销
撤销工作目录的修改
# 命令:
git restore 文件名 / git checkout 文件名
作用:将在工作目录中对文件的修改撤销
注意:git restore 文件名是一个危险的命令,这很重要。你对那个文件做的任何修改都会消失 -你只是拷贝了另一个文件来覆盖它。除非你确实清楚不想要那个文件了,否则不要使用这个命令。
撤销暂存区的修改
# 命令:
git restore --staged 文件名 / git reset HEAD 文件名
作用:将文件从暂存区中撤回到工作目录
撤销提交
# 命令:
git commit --amend
作用:这个命令会将暂存区中的文件提交。如果自上次提交以来你还未做任何修改(例如,在上次提交后马上执行了此命令),那么快照会保持不变,而你所修改的只是提交信息。
如果你提交后发现忘记了暂存某些需要的修改,可以像下面这样操作
git commit -m 'initial commit'
git add forgotten_file
git commit --amend
最终你只会有一个提交 -第二次提交将代替第一次提交的结果
HEAD
HEAD是当前分支引用的指针,它总是指向该分支上的最后一次提交。
这表示 HEAD将是下一次提交的父结点。通常,理解HEAD的最简方式,就是将它看做当前提交的快照。
# 查看当前提交对象
git cat-file -p HEAD
# 查看当前提交对象对应的树对象的内容
git ls-tree -r HEAD
暂存区(索引区)
# 查看暂存区当前的样子
git ls-files -s
工作目录
你可以把工作目录当做沙盒(沙箱)。在你将修改提交到暂存区并记录到历史之前,可以随意更改。
细化基本流程
当我们运行git init,这会创建一个Git仓库,其中的HEAD引用指向未创建的分支。
此时,只有工作目录有内容:
现在我们想要提交这个文件,所以用git add来获取工作目录中的内容,并将其复制到索引中
接着运行git commit,它会取得索引中的内容并将它保存为一个永久的快照,然后创建一个指向该快照的提交对象,最后更新master来指向本次提交。
此时如果我们运行git status,会发现没有任何改动,因为现在三棵树完全相同;现在我们想要对文件进行修改然后提交它。我们将会经历同样的过程;首先在工作目录中修改文件。我们称其为该文件的 v2版本,并将它标记为红色:
如果现在运行git status,我们会看到文件显示在 “Changes not staged for commit,”下面并被标记为红色,因为该条目在索引与工作目录之间存在不同。
接着我们运行git add来将它暂存到索引中
此时,由于索引和 HEAD不同,若运行 git status的话就会看到“Changes to be committed”下的该文件变为绿色 ——也就是说,现在预期的下一次提交与上一次提交不同。
最后,我们运行git commit来完成提交。
现在运行git status会没有输出,因为三棵树又变得相同了,切换分支或克隆的过程也类似。当检出一个分支时,它会修改HEAD指向新的分支引用,将索引填充为该次提交的快照,然后将索引的内容复制到工作目录中。
重置 reset
reset三部曲(commithash)
一、移动 HEAD
reset做的第一件事是移动HEAD的指向。
假设我们再次修改了file.txt文件并第三次提交它。现在的历史看起来是这样:
# 移动HEAD指向上一次提交对象
git reset --soft HEAD~
# 移动HEAD指向指定提交对象
git reset --soft 提交对象哈希
这与改变 HEAD自身不同(checkout所做的);reset移动HEAD指向的分支。
看一眼上图,理解一下发生的事情:它本质上是撤销了上一次 git commit命令。当你在运行git commit时,Git会创建一个新的提交,并移动 HEAD所指向的分支来使其指向该提交。
当你将它reset回HEAD~(HEAD的父结点)时,其实就是把该分支移动回原来的位置,而不会改变索引和工作目录。现在你可以更新索引并再次运行git commit来完成git commit --amend所要做的事情了。
回到默认版本:
第一部:git reset --soft HEAD~;
只动HEAD(带着分支一起移动)
二、更新暂存区(索引)
注意 git reset HEAD~等同于 git reset --mixed HEAD~
理解一下发生的事情:它依然会撤销一上次提交,但还会取消暂存所有的东西。于是,我们回滚到了所有 git add和 git commit的命令执行之前。
# 如果通过--mixed命令到该版本,如果还想回去v3,必须也通过--mixed回去
git reset --mixed v3哈希
第二部:git reset --mixed HEAD~;
动HEAD(带着分支一起移动)
动暂存区
三、更新工作目录
你撤销了最后的提交、git add和git commit命令以及工作目录中的所有工作。
第三部:git reset --hard HEAD~;
动HEAD(带着分支一起移动)
动暂存区
动工作目录
注意点
必须注意,--hard标记是reset命令唯一的危险用法,它也是 Git会真正地销毁数据的仅有的几个操作之一。其他任何形式的reset调用都可以轻松撤消,但是--hard选项不能,因为它强制覆盖了工作目录中的文件。
在这种特殊情况下,我们的 Git数据库中的一个提交内还留有该文件的 v3版本,我们可以通过reflog来找回它。但是若该文件还未提交,Git仍会覆盖它从而导致无法恢复。
路径 reset
前面讲述了 reset基本形式的行为,不过你还可以给它提供一个作用路径。若指定了一个路径,reset将会跳过第 1步,并且将它的作用范围限定为指定的文件或文件集合。这样做自然有它的道理,因为 HEAD只是一个指针,你无法让它同时指向两个提交中各自的一部分。不过索引和工作目录可以部分更新,所以重置会继续进行第 2、3步。
现在,假如我们运行 git reset file.txt(这其实是 git reset --mixed HEAD file.txt的简写形式,),它会:移动 HEAD分支的指向(因为是文件这一步忽略)让索引看起来像 HEAD所以它本质上只是将 file.txt从 HEAD复制到索引中
checkout
一、不带路径
git checkout [branch]
运行 git checkout [branch]与运行 git reset --hard [branch]非常相似,它会更新三者使其看起来像[branch],不过有两点重要的区别:
首先不同于 reset --hard,checkout对工作目录是安全的,它会通过检查来确保不会将已更改的文件弄丢。而 reset --hard则会不做检查就全面地替换所有东西。
第二个重要的区别是如何更新 HEAD。reset会移动 HEAD分支的指向,而checkout只会移动 HEAD自身来指向另一个分支。
例如,假设我们有master和develop分支,它们分别指向不同的提交;我们现在在develop上。如果我们运行git reset master,那么develop自身现在会和master指向同一个提交。而如果我们运行git checkout master的话,develop不会移动,HEAD自身会移动。现在HEAD将会指向master。
所以,虽然在这两种情况下我们都移动 HEAD使其指向了提交 A,但做法是非常不同的。reset会移动 HEAD分支的指向,而checkout则移动 HEAD自身。
二、带路径
git checkout <file>
运行 checkout的另一种方式就是指定一个文件路径,这会像 reset一样不会移动 HEAD。它就像是git reset --hard [branch] file。这样对工作目录并不安全,它也不会移动 HEAD将会跳过第 1步更新暂存区和工作目录
git checkout <file> 相比于 git reset – hard commitHash跟文件名的形式第一第二步都没做
log与reflog的比较
用git命令,想看到自己的操作记录,则可以使用log与reflog,它两个的区别如下:
1、git log 命令可以显示所有提交过的版本信息
2、git reflog 可以查看所有分支的所有操作记录(包括已经被删除的 commit 记录和 reset 的操作)
二、数据恢复
在你使用 Git的时候,你可能会意外丢失一次提交。通常这是因为你强制删除了正在工作的分支,但是最后却发现你还需要这个分支;亦或者硬重置了一个分支,放弃了你想要的提交。如果这些事情已经发生,该如何找回你的提交呢?
实例
假设你已经提交了五次
$ git log --pretty=oneline
ab1afef80fac8e34258ff41fc1b867c702daa24b modified repo a bit
484a59275031909e19aadb7c92262719cfcdf19a added repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
现在,我们将master分支硬重置到第三次提
$ git reset --hard 1a410efbd13591db07496601ebc7a059dd55cfe9
HEAD is now at 1a410ef third commit
$ git log --pretty=oneline
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
现在顶部的两个提交已经丢失了 -没有分支指向这些提交。你需要找出最后一次提交的 SHA-1然后增加一个指向它的分支。窍门就是找到最后一次的提交的 SHA-1 -但是估计你记不起来了,对吗?
最方便,也是最常用的方法,是使用一个名叫git reflog的工具。当你正在工作时,Git会默默地记录每一次你改变 HEAD时它的值。每一次你提交或改变分支,引用日志都会被更新;
$ git reflog
1a410ef HEAD@{0}: reset: moving to 1a410ef
ab1afef HEAD@{1}: commit: modified repo.rb a bit
484a592 HEAD@{2}: commit: added repo.rb
。。。
git reflog并不能显示足够多的信息。为了使显示的信息更加有用,我们可以执行git log -g,这个命令会以标准日志的格式输出引用日志
恢复
看起来下面的那个就是你丢失的提交,你可以通过创建一个新的分支指向这个提交来恢复它。例如,你可以创建一个名为recover-branch的分支指向这个提交(ab1afef)
git branch recover-branch ab1afef
现在有一个名为 recover-branch的分支是你的 master分支曾经指向的地方,再一次使得前两次提交可到达了。
三、打tag
Git可以给历史中的某一个提交打上标签,以示重要。比较有代表性的是人们会使用这个功能来标记发布结点(v1.0等等)。
列出标签
git tag
git tag -l 'v1.8.5*'
v1.8.5 v1.8.5-rc0 v1.8.5-rc1 v1.8.5-rc2 v1.8.5-rc3 v1.8.5.1 v1.8.5.2 v1.8.5.3
创建标签
Git使用两种主要类型的标签:轻量标签与附注标签
轻量标签很像一个不会改变的分支 -它只是一个特定提交的引用
git tag v1.4
git tag v1.4 commitHash
附注标签是存储在 Git数据库中的一个完整对象。它们是可以被校验的;其中包含打标签者的名字、电子邮件地址、日期时间;还有一个标签信息;通常建议创建附注标签,这样你可以拥有以上所有信息;但是如果你只是想用一个临时的标签,或者因为某些原因不想要保存那些信息,轻量标签也是可用的
git tag -a v1.4
git tag -a v1.4 commitHash
git tag -a v1.4 commitHash -m 'my version 1.4'
查看特定标签
git show可以显示任意类型的对象(git对象、树对象、提交对象、tag对象)
git show [tagname]
如果想要一次性推送很多标签,也可以使用带有 --tags选项的 git push命令。这将会把所有不在远程仓库服务器上的标签全部传送到那里。
git push origin --tags
删除标签
删除标签要删除掉你本地仓库上的标签,可以使用命令 git tag -d <tagname>。
例如,可以使用下面的命令删除掉一个轻量级标签:
git tag -d v1.4
应该注意的是上述命令并不会从任何远程仓库中移除这个标签,你必须使用git push <remote> :refs/tags/<tagname>
来更新你的远程仓库:
git push origin :refs/tags/v1.4
检出标签
如果你想查看某个标签所指向的文件版本,可以使用 git checkout命令
git checkout tagname
虽然说这会使你的仓库处于“分离头指针(detacthed HEAD)”状态。在“分离头指针”状态下,如果你做了某些更改然后提交它们,标签不会发生变化,但你的新提交将不属于任何分支,并且将无法访问,除非访问确切的提交哈希。因此,如果你需要进行更改——比如说你正在修复旧版本的错误——这通常需要创建一个新分支:
git checkout -b version2
四、Git特点
在开始学习Git的时候,请不要尝试把各种概念和其他版本控制系统(诸如Subversion和 Perforce等)相比拟,否则容易混淆每个操作的实际意义。Git在保存和处理各种信息的时候,虽然操作起来的命令形式非常相近,但它与其他版本控制系统的做法颇为不同。理解这些差异将有助于你准确地使用 Git提供的各种工具。
直接记录快照,而非差异比较
Git和其他版本控制系统的主要差别在于,Git只关心文件数据的整体是否发生变化,而大多数其他系统则只关心文件内容的具体差异。这类系统(CVS,Subversion,Perforce,Bazaar等等)每次记录有哪些文件作了更新,以及都更新了哪些行的什么内容. (下图)其他系统在每个版本中记录着各个文件的具体差异
Git并不保存这些前后变化的差异数据。实际上, Git更像是把变化的文件作快照后,记录在一个微型的文件系统中。每次提交更新时,它会纵览一遍所有文件的指纹信息并对文件作一快照,然后保存一个指向这次快照的索引。为提高性能,若文件没有变化,Git不会再次保存,而只对上次保存的快照作一链接。
Git的工作方式就像下图所示(保存每次更新时的文件快照):
这是 Git同其他系统的重要区别。它完全颠覆了传统版本控制的套路,并对各个环节的实现方式作了新的设计。Git更像是个小型的文件系统,但它同时还提供了许多以此为基础的超强工具,而不只是一个简单的 VCS。
近乎所有操作都是本地执行
在 Git中的绝大多数操作都只需要访问本地文件和资源,不用连网。但如果用 CVCS的话,差不多所有操作都需要连接网络。因为 Git 在本地磁盘上 就保存着所有当前项目的历史更新,所以处理起来速度飞快。
时刻保持数据完整性
在保存到 Git之前,所有数据都要进行内容的校验和计算,并将此结果作为数据的唯一标识和索引。换句话说,不可能在你修改了文件或目录之后,Git一无所知。这项特性作为 Git的设计哲学,建在整体架构的最底层。所以如果文件在传输时变得不完整,或者磁盘损坏导致文件数据缺失,Git都能立即察觉。Git使用 SHA-1算法计算数据的校验,通过对文件的内容或目录的结构计算出一个 SHA-1哈希值,作为指纹字符串。该字串由 40个十六进制字符(0-9及 a-f)组成,看起来就像是:24b9da6552252987aa493b52f8696cd6d3b00373
Git 的工作完全依赖于这类指纹字串,所以你会经常看到这样的哈希值。 实际上,所有保存在 Git 数据库中的东西都是用此哈希值来作索引的,而不 是靠文件名。
多数操作仅添加数据
常用的 Git 操作大多仅仅是把数据添加到数据库。因为任何一种不可逆的操作,比如删除数据,都会使回退或重现历史版本变得困难重重。在别的VCS 中,若还未提交更新,就有可能丢失或者混淆一些修改的内容,但在 Git 里,一旦提交快照之后就完全不用担心丢失数据,特别是养成定期推送到其他仓库的习惯的话。
这种高可靠性令我们的开发工作安心不少,尽管去做各种试验性的尝试好了,再怎样也不会弄丢数据。
文件的三种状态
对于任何一个文件,在 Git 内都只有三种状态(Git 外的状态就是一个普通文件):
已提交(committed),已提交表示该文件已经被安全地保存在本地数据库中了;
已修改(modified) , 已修改表示修改了某个文件,但还没有提交保存;
已暂存(staged) , 已暂存表示把已修改的文件放在下次提交时要保存的清单中
由此我们看到 Git 管理项目时,文件流转的三个工作区域:
Git 的工作目录,暂存区域,本地仓库!!!!
五、Git工作流
每个项目都有一个 Git 目录(.git )它是 Git 用来保存元数据和对象数据库的地方。该目录非常重要,每次克隆镜像仓库的时候,实际拷贝的就是这个目录里面的数据。
1、在工作目录中修改某些文件。
a) 从项目中取出某个版本的所有文件和目录,用以开始后续工作的叫做工作 目录。这些文件实际上都是从 Git 目录中的压缩对象数据库中提取出来的,接下来就可以在工作目录中对这些文件进行编辑。
2、保存到暂存区域,对暂存区做快照
a) 暂存区域只不过是个简单的文件,一般都放在 Git 目录中。有时候人们 会把这个文件叫做索引文件,不过标准说法还是叫暂存区域。
3、提交更新,将保存在暂存区域的文件快照永久转储到本地数据库(Git 目录) 中
我们可以从文件所处的位置来判断状态:如果是 Git 目录中保存着的特定版 本文件,就属于已提交状态;如果作了修改并已放入暂存区域,就属于已暂存 状态;如果自上次取出后,作了修改但还没有放到暂存区域,就是已修改状态。