最简单的方法是将以前的提交点恢复到Git中的顶部 [英] Easiest way to bring back a previous commit point to the top in Git

查看:172
本文介绍了最简单的方法是将以前的提交点恢复到Git中的顶部的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

好的,这就是我想要的,非常像回到某个以前的提交而不是修改git历史记录



假设我的git日志是这样的:

 绕行C 
绕行B
绕行A
最后一个拐点

我想恢复到最后一点,同时仍然保持历史上的弯路,但不像回到之前的某个提交而不修改git历史记录,我想再次登顶。所以后来我的git日志会喜欢:

 恢复到上一个​​好点
绕行C
绕行B
detour A
最后好点

我知道官方的方式是



git revert HEAD〜3



然而, / p>

 错误:无法还原f755e55 ...最后一个好点
提示:在解决冲突之后,标记已更正的路径
hint:使用'git add<路径>'或'git rm<路径>'



即,我需要解决那些非常混乱的冲突,这是我想尽可能避免的。我知道

  git checkout HEAD〜3 

会马上把我带到那里,但是我读过的话git会处于一个独立的阶段或某种东西,我不知道如何将这个阶段重新复制到顶端。请帮忙。谢谢。 有三种相当直接的方式实现你想要的:


  1. git revert --no-commit HEAD〜3..HEAD&& git commit

  2. git read-tree --reset -u HEAD〜3&& git commit

  3. git rm -rf - 。 &安培;&安培; git checkout HEAD〜3 - 。 &安培;&安培; git commit

这三个都需要你输入新的提交信息;您可以在 git commit 命令中添加 -C HEAD〜3 --edit ,以便您可以从消息在 HEAD〜3 中。这三者中的最后一个要求您在( cd -ed)存储库的顶层。如果你不在那里,你应该首先使用:

$ $ p $ cd $(git rev-parse --show-toplevel)

或者:

  git rev-parse --show-toplevel 

然后剪切并粘贴输出到 cd 命令到达顶层。

更长:为什么上述内容是正确的,从#3开始



关键在于:


我想要恢复 最后一点


(强调我的: revert ,而不仅仅是 revert ,这是一个Git命令,它做了一些不同的操作)。



你也应该小心 stage 这个词,它在Git中有一个技术定义的含义(指复制到临时区域,这是另一个短语对于Git所调用的东西,不同的是,索引 cac他,当然还有暂存区域)。 [/ b>] [/ b>] [/ b>

低级命令这是 git read-tree ,如 PetSerAl的回答。我会推荐 git read-tree --reset -u ,因为 -m 表示执行合并,你需要重置索引。但是有一种方法可以做到这一点,虽然稍微笨拙一些,但使用 git checkout 可能对人类更有意义。正如你注意到的那样, git checkout HEAD〜3 这是命令集3,我们先看看它。 将使期望的提交成为当前的提交 - 但它是通过分离HEAD来实现的,这是一个可怕的短语,意味着你不再处于命名分支上。 (你通过运行 git checkout branchname 来重新连接你的HEAD,它会重新设置,以便你在那个分支上,通过检查分支的提示提交,这当然意味着你不再使用所需的提交。)发生这种情况是因为所有提交或多或少是永久性的,并且完全读取只有:你不能改变过去,你只能重新访问它。



<$ c然而,$ c> git checkout 命令不仅可以重新访问过去(通过检出特定的过去提交),还可以切换到其他分支(通过检出任何已命名的分支)。很可能,这些操作中的很多或大部分都应该有不同的前端命令,因为将它们全部放在 git checkout 下只会让Git更加混乱;但这就是我们所拥有的: git checkout commit-specifier - 路径 告诉 git checkout 将给定的 路径 (文件名或目录名)提取到索引中,然后放入工作树,覆盖索引和工作树中的所有内容,没有改变提交。



因此:

  git checkout HEAD〜3  - 。 

告诉Git从commit HEAD〜3 (从你现在的位置开始的三个步骤),目录。如果您位于Git存储库的顶层,则会为存储库中的每个文件命名。



更精确地说,为存储库中的特定提交中的每个文件命名。这就是为什么你应该先运行:

  git rm -rf  - 。 

告诉Git删除每个文件(Git知道的即,目前在索引中)来自索引和工作树。这一点是......呃,假设在三次绕行提交中,你添加了一个新的文件 newfile.ext 。这个新文件在提交绕行C 中,至少,如果不是这三个文件。但在 HEAD〜3 ,它们的名字是 22769c2 ,这是最后一个好的你想恢复。所以,当你告诉git git checkout 22769c2 - 。或同等的东西时,Git会查看 22769c2 ,找到所有文件 具有 - 其中不包含 newfile.txt - 并用来自好提交的文件替换当前文件,但是离开 newfile.ext 在索引和工作树中。



通过首先删除 Git在绕行C 提交中知道,您给 git checkout ... - 。

因此,命令集3意味着:


  • 删除Git知道的所有内容,生成一个clean-slate索引和工作树。 (Git 没有知道的文件,例如由编译器构建的 .o 文件,或者 .pyc 来自Python的字节码文件,或者其他任何通过 .gitignore 忽略的文件,都不会被删除。)


  • 将所有处于良好提交状态的内容提取到索引和工作树中:使用好的东西填充干净页面。

  • 提交:提交一个新的提交,而不是 22769c2 ,但是其他一些哈希ID,其父元素是绕过C 提交,但其内容是现在索引中的任何内容,这是我们刚刚从 22769c2 中提取的内容。







1 或多或少部分是因为你可以通过改变你的各种名字来放弃 提交,这样就不会有 name 定位这些提交。没有找到它们的名字,这些承诺就会丢失并被遗弃。一旦它们被放弃了足够长的时间 - 通常至少有30天,因为隐藏的 名字仍然可以找到提交,但那些reflog条目最终会过期,通常在30天内完成这些提交 - Git的严峻 Reaper 收集器(也称为垃圾收集器 git gc )将实际移除它们。



$ h
$ b

git读取树方法



什么 git read-tree --reset 的作用是尽可能简单地将> git rm -r - -cached。使用大部分 git checkout HEAD〜3 - 。步骤。当然,这些并不完全在#3中:这种形式,带有 - cached ,仅删除索引条目。而且, git checkout 步骤会填充工作树。这就是命令的 -u 添加项:它更新工作树以匹配对索引所做的更改。删除一些条目(如果有的话)会被删除,导致相应的工作树文件被删除;更新其余条目,包括从正在读取的提交中添加新条目,会导致相应的工作树文件被更新或创建。所以 git read-tree --reset -u HEAD〜3 与我们的移除和签出顺序相同,只是它效率更高。



(您可能不记得它: git read-tree 不是经常使用的命令,而且使用 -m 告诉Git将目标树合并到当前的索引中,这也不是你想要的,尽管它几乎肯定会做正确的)



或者你可以使用 git revert -n



上面的第一个命令使用 git revert --no-commit 。这是拼写 -n 的很长的路,这意味着每次都会返回而不提交结果。通常, git还原的做法是将每个要提交的提交转换为更改集,然后反向应用更改。给定一系列的提交,比如 HEAD〜3..HEAD ,Git首先收集所有涉及到的哈希ID的列表 - 在这种情况下它们是:


  7a6c2cc绕行C 
dc99368绕行B
1cf4eb4绕行A
$ / b

然后Git以倒序的顺序遍历它们,子类最多的是父类,在绕行C ,然后在绕行B ,然后在绕行A

这些提交本身都是一个快照,但每个都有一个父母,它也是一个快照。从绕行C 中的内容中减去绕过B 快照的内容快速告诉Git,改变了 / em>,以便从B到C.然后,Git可以准确地更改这些更改:如果从B到C将一行添加到 README.md README.md 删除那行。如果它从> a.txt 添加行回到 a.txt 。如果它删除了整个文件,请将该文件放回;如果它添加了一个新文件,请将其删除。



一旦所有更改都被退出(结果与绕道B快照中的内容相匹配), git revert - 显然应该被称为 git backout - 通常会从结果中进行新的提交;但是 -n ,它不会。相反,它将结果留在索引和工作树中,准备提交。然后它转到列表中的下一个提交,那就是为了绕过B. Git将它与它的父代进行比较,以查看发生了什么变化,并且解除了这些更改。结果是,在这种情况下和绕道A中的快照是一样的。

如果我们从绕道C快照之外的其他东西开始,尽管如此,退出绕道C的变化将不会匹配绕道B,然后退出绕道B的变化与绕道A中的绕道不匹配。但是我们确实从绕道C快照中的内容开始。所以,现在Git支持绕过A的任何变化,离开 - 这是正确的! - 无论是在最后的好提交。

这个状态现在在索引和工作 - 树,准备提交。所以现在我们简单地把它作为一个新的提交。这就是命令序列1:以相反的顺序恢复(退出)三个不好的想法,这是保证工作的,因为我们从最后一个快照开始。不要提交任何中间结果。然后,一旦索引和工作树匹配上次的良好提交,请进行新的提交。


Ok, here is what I want, very like Going back to certain previous commit and not modifying git history:

Suppose my git log is like this:

detour C
detour B
detour A
Last good point

I want to revert to "Last good point", while still keeping the detours in the history, but unlike Going back to certain previous commit and not modifying git history, I want to make it top again. So afterward my git log would like:

Revert to last good point
detour C
detour B
detour A
Last good point

I know the official way is

git revert HEAD~3

However, I got

error: could not revert f755e55... Last good point
hint: after resolving the conflicts, mark the corrected paths
hint: with 'git add <paths>' or 'git rm <paths>'

I.e., I need to resolve those very messy conflicts, which is what I want to avoid as much as possible. I know

git checkout HEAD~3

will bring me there right away, but I read then the git will be in a detached stage or something, and I don't know how to duplicate this stage back to the top again. Please help. Thx.

解决方案

TL;DR

There are three fairly direct ways to achieve what you want:

  1. git revert --no-commit HEAD~3..HEAD && git commit
  2. git read-tree --reset -u HEAD~3 && git commit
  3. git rm -rf -- . && git checkout HEAD~3 -- . && git commit

All three require that you enter the new commit message; you might add -C HEAD~3 --edit to the git commit command so that you can edit starting from the message that's in HEAD~3. The last of these three require that you be in (cd-ed to) the top level of the repository. If you are not already there, you should first use:

cd $(git rev-parse --show-toplevel)

or:

git rev-parse --show-toplevel

and then cut and paste the output into a cd command to get to the top level.

Longer: why the above are correct, starting with #3

The key phrase is this:

I want to revert to "Last good point"

(emphasis mine: revert to, not just revert, which is a Git command that does something a bit different).

You should also be wary of the word stage, too, which has a technical defined meaning in Git (referring to copying into the staging area, which is another phrase for the thing that Git calls, variously, the index, the cache, and of course the staging area). [edit: removed since the title is adjusted now]

The low level command that does this is git read-tree, as in PetSerAl's answer. I would recommend git read-tree --reset -u, since -m means to perform a merge and you want an index reset. But there is a way to do this that, while slightly clumsier, may make more sense to humans, using git checkout. That's command-set 3, which we will look at first.

As you note, git checkout HEAD~3 will make the desired commit be the current commit—but it does so by "detaching HEAD", which is a scary phrase that just means that you are no longer on a named branch. (You "re-attach" your HEAD by running git checkout branchname, which sets things up again so that you're on that branch, by checking out that branch's tip commit, which of course means you are no longer using the desired commit.) This happens because all commits are more or less permanent,1 and entirely read-only: you cannot change the past, you can only re-visit it.

The git checkout command, though, can do more than re-visit the past (by checking out a specific past commit) or switch to some other branch (by checking out any named branch). Probably, many or most of these operations should have a different front-end command, because lumping them all under git checkout just makes Git more confusing; but that's what we have: git checkout commit-specifier -- paths tells git checkout to extract the given paths (file or directory names), into the index and then on into the work-tree, overwriting whatever is currently in the index and work-tree, without changing commits.

Hence:

git checkout HEAD~3 -- .

tells Git to extract, from commit HEAD~3 (the one three steps back from where you are now), the directory .. If you are in the top level of your Git repository, . names every file in the repository.

More precisely, . names every file in that particular commit of the repository. This is why you should first run:

git rm -rf -- .

That tells Git to remove every file (that Git knows about, i.e., that's in the index right now) from both the index and work-tree. The point of this is ... well, suppose that during the three detour commits, you added a new file newfile.ext. That new file is in commit detour C, at least, if not in all three of those. But it's not in HEAD~3, which names commit 22769c2, the last good one that you want to restore. So when you tell git git checkout 22769c2 -- . or equivalent, Git looks into 22769c2, finds all the files that commit has—which doesn't include newfile.txt—and replaces the current files with the ones from the good commit, but leaves newfile.ext in the index and work-tree.

By first removing everything that Git knows about in the detour C commit, you give the git checkout ... -- . command a clean slate into which to extract everything.

Hence, command set 3 means:

  • Remove everything that Git knows about, to produce a clean-slate index and work-tree. (Files that Git doesn't know about, such as .o files built by a compiler, or .pyc byte-code files from Python, or whatever, that are ignored via a .gitignore, do not get removed.)

  • Extract everything that was in the good commit, into the index and work-tree: fill the clean slate with the good stuff.

  • Commit: make a new commit, not 22769c2 but some other hash ID, whose parent is the detour C commit but whose contents are whatever is in the index right now, which is the stuff we just extracted from 22769c2.


1The "more or less" part is because you can abandon commits, by changing your various names so that no name locates those commits any more. Having no names that find them, the commits become lost and abandoned. Once they have been abandoned sufficiently long—generally at least 30 days as there are hidden reflog entry names that still find the commits, but those reflog entries eventually expire, typically in 30 days for such commits—Git's Grim Reaper Collector, also known as the garbage collector or git gc, will actually remove them.


The git read-tree method

What git read-tree --reset does is, to put it as simply as possible, combine the git rm -r --cached . step with most of the git checkout HEAD~3 -- . step. Of course those aren't quite what are in #3 at all: this form, with --cached, removes only index entries. Moreover, the git checkout step populates the work-tree. That's what the -u addition to the command does: it updates the work-tree to match the changes made to the index. Removing some entries, if any do wind up removed, causes the corresponding work-tree file to be removed; updating the rest of the entries, including adding new entries from the commit being read, causes the corresponding work-tree file to be updated or created. So git read-tree --reset -u HEAD~3 is the same as our remove-and-check-out sequence, except that it's more efficient.

(You might not remember it though: git read-tree is not a command one uses often. Also, using -m tells Git to merge the target tree into the current index, which isn't quite what you want either, although it's almost certainly going to do the right thing here.)

Or you can use git revert -n

The first command above uses git revert --no-commit. This is the long way to spell -n, which means do each revert without committing the result. Normally, what git revert does is to turn each commit-to-be-reverted into a change-set, then "reverse apply" the changes. Given a range of commits like HEAD~3..HEAD, Git first collects a list of all the hash IDs involved—in this case they are:

7a6c2cc detour C
dc99368 detour B
1cf4eb4 detour A

Git then runs through them in backwards order, child-most to parent-most, i.e., first looking at detour C, then at detour B, then at detour A.

Each of these commits is a snapshot in itself, but each has a parent that is also a snapshot. Subtracting what's in the detour B snapshot from what's in detour C tells Git, in effect, what changed in order to go from B to C. Git can then "un-change" exactly those changes: if going from B to C added a line to README.md, remove that line from README.md. If it removed a line from a.txt, add that line back to a.txt. If it removed an entire file, put that file back; if it added a new file, remove it.

Once all of the changes have been backed-out (with the result matching what's in the detour B snapshot), git revert—which obviously should be called git backout—would normally make a new commit from the result; but with -n, it doesn't. Instead, it leaves the result in the index and work-tree, ready to commit. Then it moves on to the next commit in the list, which is that for detour B. Git compares this to its parent to see what changed, and undoes those changes. The result is, in this case, the same snapshot that's in detour A.

Had we started from something other than the detour C snapshot, though, backing out the detour C changes would have not matched detour B, and then backing out the detour B changes would not match what's in detour A. But we did start from what's in the detour C snapshot. So now Git backs out whatever changed in detour A, leaving—that's right!—whatever is in the last good commit.

This state is now in the index and work-tree, ready to commit. So now we simply commit it as a new commit. And that's command-sequence 1: revert (back out) the three bad ideas, in reverse order, which is guaranteed to work since we're starting with the snapshot in the last of them. Don't commit any of the intermediate results. Then, once the index and work-tree match the last good commit, make a new commit.

这篇关于最简单的方法是将以前的提交点恢复到Git中的顶部的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆