git add 和 git reset

2017-10-26 15:43:00
lyjshen
转贴:
CSDN
3047

git add 和 git reset牵扯到很多对.git目录下文件的修改和操作,从大体上讲,这两个操作是互逆的。想要搞懂它们之间的关系和互逆性,就先要搞懂这两个操作的底层操作。

使用$ git add –help查看git的具体含义。



git-add - Add file contents to the index ----意为将文件内容添加到索引。



如果这个文件已经有索引了,add还有一层意思,如果这个add的文件是刚刚创建的,意味着使用命令 git add 开始跟踪一个文件。 

那么问题来了,index—索引是个什么鬼,在git中扮演着怎样的角色?

基本概念

Git有一个很著名的工作栈,被划分为“The Three States”或者叫 “三棵树”。 
来看git官方给的一张图:


git areas
Figure 1-6. 工作目录、暂存区域以及 Git 仓库.


说明:

  1. Git 仓库目录是 Git 用来保存项目的元数据和对象数据库的地方。 这是 Git 中最重要的部分,从其它计算机克隆仓库时,拷贝的就是这里的数据。

  2. 工作目录是对项目的某个版本独立提取出来的内容。 这些从 Git 仓库的压缩数据库中提取出来的文件,放在磁盘上供你使用或修改。

  3. Staging area

    暂存区域是一个文件,保存了下次将提交的文件列表信息,一般在 Git 仓库目录中。 有时候也被称作“索引”(index),不过一般说法还是叫暂存区域。

基本的 Git 工作流程如下:

  1. 在工作目录中修改文件。
  2. 暂存文件,将文件的快照放入暂存区域。
  3. 提交更新,找到暂存区域的文件,将快照永久性存储到 Git 仓库目录。

不过,在我们理解了提交(commit)和分支(branch)的概念之后,上边这个图,其实在某一时刻,是可以这样表述的:

work-flow

git add理解

由于索引又称为“暂存区”,因此,我们在stackoverflow上,还看到另外一张相似的图:

git-states

对照着上边的图,我们来看看add的说明:

git add 命令将内容从工作目录(working tree)添加到暂存区(或称为索引(index)区),以备下次提交。 当 git commit 命令执行时,默认情况下它只会检查暂存区域,因此 git add 是用来确定下一次提交时快照的样子的。

索引(index)持有当前工作区(working tree)的一个快照,这个快照用来作为下次提交的内容。这样,对工作区做了任何改变之后,在执行commit命令之前,你必须使用add命令来添加任何新增或者改动的文件到索引中。

这个命令在commit之前,可以被执行多次。它仅添加指定文件内容;如果你想在下次commit中包含随后的改变,你必须再次执行git add来添加新内容到索引中。

git status命令可以用来获取当前哪些文件被修改,并暂存作为下次要commit的一个总结。

git reset 说明

git reset 命令主要用来根据你传递给动作的参数来执行撤销操作。 它可以移动 HEAD 指针并且可选的改变 index 或者暂存区,如果你使用 –hard 参数的话你甚至可以改变工作区。 如果错误地为这个命令附加后面的参数,你可能会丢失你的工作,所以在使用前你要确定你已经完全理解了它。

git 某一个时刻状态:

git-start

让我们跟着 reset 看看它都做了什么。 它以一种简单可预见的方式直接操纵这三棵树。 它做了三个基本操作。

第 1 步:移动 HEAD 
reset 做的第一件事是移动 HEAD 的指向。 这与改变 HEAD 自身不同(checkout 所做的);reset 移动 HEAD 指向的分支。 这意味着如果 HEAD 设置为 master 分支(例如,你正在 master 分支上),运行 git reset 9e5e64a 将会使 master 指向 9e5e64a

git-reset

无论你调用了何种形式的带有一个提交的 reset,它首先都会尝试这样做。 使用 reset –soft,它将仅仅停在那儿。

现在看一眼上图,理解一下发生的事情:它本质上是撤销了上一次 git commit 命令。 当你在运行 git commit 时,Git 会创建一个新的提交,并移动 HEAD 所指向的分支来使其指向该提交。 当你将它 reset 回 HEAD~(HEAD 的父结点)时,其实就是把该分支移动回原来的位置,而不会改变索引和工作目录。 现在你可以更新索引并再次运行 git commit 来完成 git commit –amend 所要做的事情了(见 修改最后一次提交)。

第 2 步:更新索引(–mixed) 
注意,如果你现在运行 git status 的话,就会看到新的 HEAD 和以绿色标出的它和索引之间的区别。

接下来,reset 会用 HEAD 指向的当前快照的内容来更新索引。

git-mixed

如果指定 –mixed 选项,reset 将会在这时停止。 这也是默认行为,所以如果没有指定任何选项(在本例中只是 git reset HEAD~),这就是命令将会停止的地方。

现在再看一眼上图,理解一下发生的事情:它依然会撤销一上次 提交,但还会 取消暂存 所有的东西。 于是,我们回滚到了所有 git add 和 git commit 的命令执行之前。

第 3 步:更新工作目录(–hard) 
reset 要做的的第三件事情就是让工作目录看起来像索引。 如果使用 –hard 选项,它将会继续这一步。

git-hard

现在让我们回想一下刚才发生的事情。 你撤销了最后的提交、git add 和 git commit 命令以及工作目录中的所有工作。

必须注意,–hard 标记是 reset 命令唯一的危险用法,它也是 Git 会真正地销毁数据的仅有的几个操作之一。 其他任何形式的 reset 调用都可以轻松撤消,但是 –hard 选项不能,因为它强制覆盖了工作目录中的文件。 在这种特殊情况下,我们的 Git 数据库中的一个提交内还留有该文件的 v3 版本,我们可以通过 reflog 来找回它。但是若该文件还未提交,Git 仍会覆盖它从而导致无法恢复。

回顾 
reset 命令会以特定的顺序重写这三棵树,在你指定以下选项时停止:

  1. 移动 HEAD 分支的指向 (若指定了 –soft,则到此停止)

  2. 使索引看起来像 HEAD (若未指定 –hard,则到此停止)

  3. 使工作目录看起来像索引

reset 与 checkout

要理解这两个的区别,就得明白HEAD是个什么东西,HEAD我们说它只是个指向其他分支最后一次提交对象的一个指针,或者是一个符号链接,因此可以将其理解为一个代理。

执行 $ git checkout xxxx, 就是修改HEAD自身的内容。

git-checkout

可以,看到执行后,它指向的链接改变了。

而执行 $git reset 【commit】是改变HEAD指向的分支指向的commit对象,HEAD文件本身内容不变。

其中,【commit】可以是提交哈希值的简写模式,也可以使用HEAD代替。HEAD代表最后一次提交,HEAD^为最后一个提交的父提交,等同于HEAD~1,HEAD~2代表倒数第二次提交

想要查看你可以书写哪些revision,可以使用$git revisions –help来查看。

首次提交如何回退?

一般我们在讨论回退的时候,当前的commit对象已经有一个父commit或者两个父commit对象,此时,在reset的时候,你可以指定任意一个父commit对象,作为回退点。

但是,如果你尝试回退第一次(initial commit)commit,你会发现,–soft HEAD…什么的都不好使了,不过,歪果仁又一次展现了他们的机智:

How to revert initial git commit?

I commit a git repository at first time, I 
then regret the commit and want to revert it. I try

# git reset --hard HEAD~1

I get this message:

fatal: ambiguous argument ‘HEAD~1’: unknown revision or path not in 
the working tree. This commit is first commit of repository, any idea 
how to undo git initial commit?

解决方法:

For me, the most secure way is to use the update-ref command:

# git update-ref -d HEAD

It will delete the named reference HEAD, so it 
will reset (softly, you will not lose your work) all your commits of 
your current branch. If what you want is to merge the first with the 
second one, you can use the rebase command:

git rebase -i –root A last way could be to create an empty branch and 
commit your new content on it:

git checkout –orphan

那么git update-ref是干嘛的呢?



git-update-ref —— 安全更新存储在ref中的对象名称



给定两个参数,存储<newValue> 到<ref>中,可能会将符号引用refs解引用。例如,git update-ref HEAD <newValue>,会更新当前分支head为新的对象。

给定三个参数,存储<newValue> 到<ref>中,可能会将符号引用refs解引用,验证ref的当前值是否匹配<oldvalue>。例如,git update-ref refs/heads/master <newvalue> <oldvalue>,仅在当前值<oldvalue>更新master分支head为<newvalue>。你可以指定40 “0” 或者一个空字符串作为<oldvalue>来确保你正在创建的这个ref不存在。

它还允许一个“ref”是到其他ref文件的一个符号链接,开头为四字节的字符序列:"ref:"。

更重要的是,它允许更新一个ref文件使其跟踪这些符号指针,不管它们是符号链接还是“常规文件符号引用”。它只跟踪以"refs/"开头的,真正的符号链接:否则,它仅会尝试将其作为一个常规文件读取和更新(例如,它将允许文件系统跟踪他们,但会将一个符号链接覆写到其他含有一个常规文件名的地方)。

如果为给定 --no-deref,<ref> 自身会被覆写,不再是跟踪符号指针的结果。

通常, 使用



git update-ref HEAD "$head"



比下边这样做更加安全:



echo "$head" > "$GIT_DIR/HEAD"



这俩一个是符号链接跟踪的观点和一个是错误检查的观点。对于符号链接,"refs/"意味着指向 “外部”树 的链接是安全的:它们将会被跟踪用来读取,但是不会被写(如果你已经通过创建一个符号链接树拷贝了整个层级,这样我们将不会通过一个符号链接引用到一些其他树)。

使用 -d 标识,它会去验证当前是否仍然包含<oldvalue>,则删除给定的<ref>。

发表评论
评论通过审核后显示。