如何在Eclipse IDE中将分支恢复到原始状态?

aiqt4smr  于 2022-11-04  发布在  Eclipse
关注(0)|答案(1)|浏览(136)

我有一个正在开发的特性,因此创建了一个分支,我们将其命名为Branch A。我有一个针对分支A的提取请求,我正尝试将其合并到main。我想开发其他特性,因此创建了一个基于分支A的Branch B。我需要根据收到的评论在分支A中进行一些更改,但是不知怎么的,我在Branch B中所做的更改在分支A中得到了反映。那么,我如何才能让分支A恢复到原来的状态,同时保留我在分支B中所做的工作呢?还是我注定要将我的工作保存到其他地方,然后将所有内容还原回来?我还没有将分支B中的任何更改推送到github。

xzlaal3s

xzlaal3s1#

你一直在用一种错误的思维模式来理解Git的工作原理。(这并不奇怪:很多人并没有立刻“理解”Git模型。当我第一次使用Git的时候,在2006年,或者不管是哪一年,我遇到了同样的问题。
诀窍是要意识到分支在Git中基本上是“无关紧要的”。它们并非完全无用:它们有一个非常特殊的功能。但是除了这个特殊的功能之外,它们并没有任何意义,甚至没有做任何事情。相反,Git是关于提交的--不是分支,不是文件,而是提交。直到你实际上做了一个新的提交,通常是运行git commit,你实际上还没有在Git中做任何事情。
当你在评论中说:
实际上,我需要做的就是把我的零钱藏起来...
这说明您使用了git branchgit switch -cgit checkout -b来创建一个新的 * 分支名称 *,但您从未运行过git commit
git stash所做的是 * 两 * 次提交(有时是三次)。git stash所做的提交是在 no 分支上。在Git中分支不是必需的。只有 commits 才是真正重要的。
认识到这是如何运作的是非常重要的。事实上,这是至关重要的,因为如果你不知道这一点,你很容易失去你已经做的工作。
1这是一个稍微夸大的效果;在“Git”中做一些事情而不实际承诺是可能的,但那是以后的事,在你学会了早期和经常承诺之后。😀

提交在Git中如何工作

提交是Git存在的原因。它们是基本的构建块。如果你真的在使用Git,那么提交很可能就是你使用Git的 * 原因 *。(唯一的另一个原因是“因为老板告诉我这么做”或类似的--基本上是xkcd 1597中取笑的东西。)因此,你需要知道提交是什么,对你有什么作用。
每个Git提交:

  • 编号:它有一个看起来是随机的(但实际上不是)唯一的数字,而且非常大,非常难看,非常不适合人类。
  • 是只读的。一旦提交,就永远不能更改。这是魔术编号方案工作所必需的。
  • 包含两个部分:一些 * 元数据 ,或者关于提交本身的信息,例如提交者的姓名和电子邮件地址,以及-间接地- 每个文件的完整快照 *。

每次提交的快照都以一种特殊的、神奇的、压缩的和 * 内容去重复 * 的方式存储,这样Git存储库--由提交和它们的支持对象组成--就不会随着提交的增加而膨胀。大多数提交大多重用之前提交的大部分或全部文件当他们这样做的时候,这些文件的 * 内容 * 会被删除重复,以便在 * 所有 * 拥有它的提交之间共享。(这是通过使神奇的编号系统工作所需的只读特性来实现的。它真的具有惊人的自引用性,其中Git的一部分依赖于Git的另一部分,而Git的另一部分又依赖于第一部分,就像Ouroboros一样。
任何给定提交的 * 元数据 * 都包含该提交的父提交的 * 原始哈希ID *(唯一编号)。大多数提交(Git称之为 * 普通提交 *)都只包含一个父哈希ID。这形成了一个简单的反向链,每个提交链接到它的(单个)父提交,父提交又反向链接到 * 它的 * 父提交,依此类推。
这意味着Git只需要知道一个hash ID--最近一次提交的hash ID--就能找到所有的提交。
为了理解这一点,我们需要稍微回顾一下 repository。大多数Git仓库都由一个大的key-value database组成,Git称之为 objects database。Git通过哈希ID在这个大数据库中查找内容。由于提交的哈希ID是 unique,如果我们知道提交的哈希ID,Git可以快速地从这个大对象数据库中提取提交本身,但Git * 需要 * 哈希ID来完成。2
假设我们已经记住了 latest 提交的哈希ID,它有一个很大的难看的hexadecimal表达式,比如dda7228a83e2e9ff584bf6adbf55910565b41e14;如果我们真的要记住它的话,我们就得在脑子里记下来(或者写在纸上、白板上或其他什么东西上)。我们把这个哈希ID * 输入到 * Git,Git很快就在那个大数据库里找到了这个提交。让我们把这个提交命名为H,表示哈希,并把它画成这样:

<-H

H中伸出的向后指的箭头表示存储在H的元数据中的 * 父哈希ID*。(在本例中为279ebd47614f182152fec046c0697037a4efbecd),它是提交的父提交,因此Git可以使用 that hash ID来查找更早的提交,就在H之前的那个。让我们调用这个提交G并把它画进去:

<-G <-H

现在,假设G也是一个普通提交,3它也会有一个父哈希ID,我用G中的箭头表示它,它指向另一个父提交F

... <-F <-G <-H

通过沿着这些箭头,一次一跳,Git可以找到 * 每一个提交 *。我们所要做的就是把 * 最后一个 * 提交的哈希ID H给它。

**这样做的问题很明显:我们必须记住一些随机的、丑陋的、人类不可能记住的哈希ID。**那么我们该怎么做才能解决这个问题呢?

2请注意,有些维护命令会(缓慢而痛苦地)搜索整个数据库以查找各种问题。这样的命令可以找到所有“最新”的提交。然而,在任何相当大的仓库中,这都需要花费几分钟的时间:太慢了,不能用于日常工作。
3我一直在使用Git仓库中的哈希ID,如果你看一下279ebd47614f182152fec046c0697037a4efbecd,你会发现这根本不是一个普通的提交,但我们在这里不打算讨论这个。

分支名称

这里有一个好主意:我们有一台 * 计算机 *。让 * 计算机 * 记住最新的哈希ID。我们将使用一些人类可以处理的东西,比如 * 分支名称 *。我们将添加第二个数据库--实际上是另一个键值存储--就在大型的所有对象数据库旁边。在这个 * 名称 * 数据库中,我们将存储名称:分支名称、标记名称和所有其他类型的名称。在每个名称下,我们只存储一个哈希ID。
(That一个哈希ID可能看起来有点限制,事实的确如此,但对Git来说已经足够了。就像 * 分支 * name只需要记住 latest 的哈希ID一样,tag name只需要记住一个哈希ID。Git在需要的时候使用 annotated tag objects 来处理这个问题。不过我们在这里也不会涉及这些。
当你在Git中创建一个 * 新分支名 * 时,你基本上是在设置一些东西,这样你就可以有多个“最新”提交。也就是说,我们从一个分支名开始,比如mastermain--你使用哪个对Git来说并不重要--然后我们有一系列的提交,从一个非常特殊的提交开始,Git称之为(或)root 提交,该提交 * 没有 * 父提交:

A--B--C   <-- main

这里我画了一个只有三个提交的小仓库。Commit A是我们特殊的根提交,没有父提交。Commit B是第二个提交,它指向A;并且提交C是第三次也是迄今为止的最后一次提交,指回C
如果我们现在进行一次新的提交--先别管 * 如何 *,想象一下我们进行了一次新的提交--Git将产生一个新的、从未使用过的哈希ID,Git将通过保存每个文件的完整快照来提交D-这些文件来自哪里是关键,但也是令人惊讶的,我们将回到这一点-新提交的元数据将指向已有的提交C,因为C是在我们进行D时的最新提交,但是D一旦被进行,就是最新的提交,所以Git只需要把D的哈希ID填充到名称数据库中的名称main中,瞧:

A--B--C--D   <-- main

我们说分支名称main指向分支中的最后一次提交,这实际上是一个定义:无论名称main中存储了什么哈希ID,它都是分支中的最后一次提交。
如果我们认为提交D很糟糕,并且我们想摆脱它,那么,我们只需让Git将C的哈希ID存储回main,如下所示:

D   ???
       /
A--B--C   <-- main

提交D时会发生什么情况?什么都不做:它仍然存在于大数据库中,只是坐在几乎 * 找不到 * 的地方,因为名称main不再指向它。5如果你记住了哈希ID--或者把它写下来了--你可以把它输入到Git中,并且仍然可以看到提交D,至少在维护删除之前是这样的(再次参见脚注5),否则你就看不到它了。
但是,我们不要 * 擦除 * D,而是做一些不同的事情。

A--B--C   <-- main

并且产生一个 * 新的分支名称 ,例如develop。这也将指向提交C 所有三个提交现在都在两个分支上 *。

A--B--C   <-- develop, main

为了记住 * 我们使用哪个分支名称来查找提交C *,我们让Git将特殊名称HEAD“附加”到这两个分支名称中的一个。这就是 * 当前分支 *,也就是git status在表示on branch masteron branch develop时列出的名称:

A--B--C   <-- develop, main (HEAD)

如果我们现在使用git switch develop,我们从提交C切换到提交C--这根本不做任何事情,因为它没有切换 commits--但是我们现在使用的是C,名称是develop

A--B--C   <-- develop (HEAD), main

当我们现在提交D时,Git会把新的哈希ID写入 * 当前分支名称 * 中,因为它是develop,而不是main,所以develop现在指向D,而另一个名称main仍然指向C

A--B--C   <-- main
       \
        D   <-- develop (HEAD)

这样,我们就可以创建多个 * 分支名称 *,每个分支名称都指向 * 任何一个现有的提交 *。例如,我们可以返回到提交B,并为该提交创建一个新名称:

A--B   <-- old
    \
     C   <-- main
      \
       D   <-- develop (HEAD)

我们可以在 * 任何时间 * 添加和删除 * 任何 * 分支名称,但有一个约束条件,即不允许删除我们“在”的分支名称,无论该名称是什么。因此,如果我现在想删除develop,我必须运行git switch maingit switch old

4这个哈希ID必须是之前从未在 * 任何 * 仓库 * 中使用过的,也必须是从未被再次使用过的,而且Git必须在不联系任何其他Git软件或Git仓库的情况下做到这一点。这是如何工作的?它是magic...或者,好吧,根本不是真正的魔法,总有一天它会崩溃,而是not for a long time, we hope
5这就是维护命令稍后要用到的地方。它们会搜索整个数据库,发现D,发现D * 找不到 *,然后 * 删除它 *。也许,最终会。我们不知道确切的时间。

你的 * 工作树 * 和Git的 * 索引 *

我之前提到过,Git使用什么文件来进行一次 * 新的提交 * 是令人惊讶的。原因很简单:

  • 您无法 * 查看 * 这些文件;和
  • 其他版本控制系统甚至没有这些文件。

换句话说,Git在这里很特别。
Git的“正常”之处在于:保存在任何一个提交中的文件都是只读的。不仅如此,它们的格式是你的计算机的其他部分无法使用的。除了Git之外,没有任何东西可以读取这些文件,什至Git本身也无法覆盖这些文件。但是要在你的计算机上完成工作,你需要普通的日常文件,几乎所有的版本控制系统都有这个问题,而且它们几乎都以同样的方式处理它: checkout 一个提交的行为会复制已保存快照中的文件。
当你选择一个提交时,例如git switch *branch-name*,Git * 会提取该提交的文件 *(当然,除非你不 * 修改 * 提交,在这种情况下Git什么都不做)。6这些文件的 * 可用 * 副本进入一个工作区,Git称之为“工作树”或“工作树”。这些都是普通的日常文件!你可以看到它们。你可以在编辑器或IDE中打开它们。你可以对这些文件做任何你想做的事情。**这些文件不是 in Git。*它们 out of Git,但它们现在只是普通的文件。
这就是为什么卡德武问道:
你确定来自分支B的提交在分支A上吗?或者你说的修改是指那些来自工作区的...
当你切换到一个新的分支A并做了一些提交时,这些都是新的提交。但是当你切换到一个新的分支B时,
没有提交 *。你修改了工作树文件,但是 * 仍然在同一个提交上 *。然后你切换回分支A......这改变了 * HEAD所附加的 * 名称 * 但未更改提交,也未更改任何文件。
[当]我做一个git status...
现在我们来看看Git在检查提交时所做的一些偷偷摸摸的事情。
当Git用每个文件的 * 可用 * 副本填充你的 * 工作树 * 时,Git也会填充每个文件的 * 第三个 * 副本。这个第三个副本实际上位于 * 提交副本(Git的特殊提交格式)和工作树中的可用副本之间。每个文件的中间副本都是 * 去重格式 *。但是-与存储在commit中的文件不同-它不是完全只读的。7使用git add,可以 * 替换 * 这个副本。
每个文件的这个额外的中间副本位于Git所称的 indexstaging area 或者--现在很少这样称呼--cache 中。这三个名字都代表同一个东西。事实上,有这三个名字主要反映了原来的名字很糟糕。你可以忽略 cache 这个名字。我喜欢 index 这个名称,因为它没有意义,但是 staging area 这个名称很有用,因为它反映了您如何 * 使用 * 索引。
当你运行git commit时,Git会 * 获取 * Git索引中的所有文件 *,并将它们用于新的提交。**你看不到这些文件!**它们在Git的索引中,这是 * 不可见的 *。8如果你修改了某个工作树文件,你必须在它上面运行git add
git add的功能非常简单:它

  • 读取工作树副本;
  • 将其压缩为特殊的Git专用格式;
  • 检查内容是否已作为副本存在:
  • 如果是副本,git add丢弃 * 新 * 压缩版本并使用旧版本;
  • 如果不是副本,则git add保存 * 新的 * 压缩版本并使用该版本;
  • 在任何情况下,X1 M81 N1 X更新索引条目,使得更新的文件是将要提交的文件。

无论哪种方式,* 在 * 你运行git add之前,文件已经在Git的索引中,准备提交。* 在 * 你运行git add之后,文件再次在Git的索引中,准备提交-只是带有不同的 * 压缩和去重内容 *。
因此,无论Git的索引中有什么,都 * 随时可以提交 *。这就是git commit如此(相对)快的原因。

如果你git add一个新的Git文件,Git仍然像往常一样压缩内容,但当它把Git化的对象写入Git的索引时,它会进入一个 new 索引条目,为新的文件名命名。该索引以完整路径名保存文件名-path/to/file.ext,注意,即使在Windows系统上,Git也会在这里使用正斜杠,其中操作系统将其存储为file.ext,文件夹为path\to\file.ext,文件夹为path,文件夹为to。Git在索引中只有 files,没有任何文件夹。9
类似地,如果你使用git rm删除一个文件,Git会同时从工作树和索引中删除该文件。如果没有索引副本,下一个git commit将保存一个完整的快照,并将该文件删除。相对于上一次提交,新的提交将“删除”该文件。

**所有这些都意味着很容易记住:这个索引代表了 * 下一个你计划做的提交 *。*就是这样-这就是索引的意义!它是 * 下一个 * 提交。它从 * 这个 * 提交开始填充。当你在工作树中做改变时, Git的索引还没有发生任何变化 *。你必须运行git add(或git rm),让Git根据你在工作树中所做的更新来更新它的索引。

作为一种捷径,您 * 可以 * 使用git commit -a,但它有一个缺陷--好吧,不止一个缺陷,但其中一些缺陷不会咬到您,除非您有不了解Git如何使索引复杂化的人编写的预提交钩子,有时候,包括当您使用git commit -a时。* 主要 * 缺陷是git commit -a大致相当于运行git add -u,* 而不是 * git add --allgit add-u选项只会更新 * 已经在Git索引中的文件 *。任何 * 新 * 的文件都不会被添加。
6 Git的“如果不改变提交,就不要改变任何文件”福尔斯一个更一般的优化,它是“不要改变任何你不需要改变的文件”。我们在这里也不会讨论这个问题,但是请注意,从提交C切换到提交C,就像我们之前做的那样,不会切换出底层的 commit,因此不会改变 * 任何文件 *。因此,在这种情况下,优化完全不会触及任何内容。这就是为什么,例如,你可以在开始修改文件后创建一个新的分支。创建一个新的分支 name 使用 current commit,所以它不会修改提交,因此不需要修改任何文件,也不会。
7从技术上讲,Git的索引/暂存区 * 中的 * 内容 * 是 * 只读的,以Git内部 blob 对象的形式存在。你要做的就是用另一个blob对象覆盖它。
8 git ls-files命令可以相当直接地显示索引中的内容,但这个命令的用处相对较小:git status是最终要使用的命令。
9这就是导致the problem of storing an empty folder的原因,而Git做得并不好。如果索引可以保存一个目录而不需要“保持变成gitlink”的bug,Git * 就可以 * 通过empty tree来保存空目录。但是它(索引)不能(保存目录),所以它(Git)也不能(保存空文件夹)。

了解git status,并对.gitignore有一点了解

我之前提到过,你无法 * 看到 * Git的索引/暂存区中有什么。因为Git * 会从Git索引中的文件进行新的提交 *,这是一个问题!如果你查看你的工作树,你所看到的 * 不在Git中 也不是将要提交的 *。将要提交的东西是Git索引中的任何东西,你无法 * 看到 *。
不过,你 * 能 * 做的就是运行git status。这个命令实际上运行了 * 两次 * 比较。首先,git status告诉你 * 当前分支名称 ,例如on branch develop。这非常有用:这是Git在 * 存储新提交哈希ID 时所使用的 * 分支名称 *。你可能会得到更多关于分支名称的信息,例如,在它的 * 上游 * 之前和/或之后。我们在这里不讨论这个(由于篇幅的原因)。
接下来,Git会在 * 当前提交 ,也就是HEAD和索引之间做一个比较--实际上是git diff --name-status。通常情况下, 这里几乎所有的文件都没有改变 *。对于那些文件,git status * 什么都没有说 *。所以对于大多数文件,你会得到 * 根本没有输出 *,这真的很容易理解。你只会得到那些有不同的文件的输出!
这意味着这一节列出了 * 为提交而进行的修改 *,这就是这一节的标题,Changes staged for commit。这里打印的任何文件名都是被打印出来的 *,因为 * 这个文件在索引中 * 与HEAD提交中的 * 不同 *。也许它是全新的!也许它被删除了!也许它只是被修改了。尽管它肯定是 * 不同 * 的。

列出了这些“staged for commit”的修改--或者如果Git的索引仍然与HEAD commit匹配,就什么也不说, -git status命令现在开始进行第二次比较。它基本上运行了另一个git diff,也使用--name-status来避免显示修改的 * 行 *,以找出哪些文件(如果有的话),在Git的索引和工作树中是 * 不同的 *。
如果某个工作树文件与该文件的索引副本 * 不同 *,git status将 * 在此处列出该文件 *。这些文件将在git status输出的Changes not staged for commit部分中列出。如果您没有触及1000个文件中的999个,则仅 * 一个 * 文件将在此处列出:一旦你在这个被修改的文件上使用git add,这个 * 索引副本 * 就会与 * 工作树 * 副本匹配,它将不再是“未暂存”的。但是现在索引副本可能不再与HEAD副本匹配,它将开始被“暂存”。
因此:

  • 第一个diff告诉您 * 是 * 为提交而暂存的文件;
  • 第二个差异告诉您 * 不是但可以 * 暂存文件。
  • 这两组文件都是通过 * 比较 * 每个副本的内容来发现的。首先Git比较HEAD-file-contents和index-file-contents,得到“暂存提交”列表。然后Git比较index-file-contents和工作树-file的内容,得到“未暂存提交”列表。

就这么简单......嗯,* 几乎 *。当然,Git必须在这里添加一个额外的皱纹。
如果你在索引中添加了一个全新的文件,Git会告诉你有一个新的文件被添加到了索引中,并等待提交。这是有道理的。但是如果你在工作树中添加了一个全新的文件,你可能会认为Git会告诉你有一个新的文件被添加到了索引中,但并没有等待提交。
但是没有!**相反,Git告诉你有一个 untracked 文件。**这 * 是怎么回事?嗯,有时候这个新文件 * 应该 * 被git add-ed。然后它就变成了一个 tracked 文件,它将进入下一次提交。
有时候--尤其是在某些编程语言中--你会得到一大堆根本不应该提交的文件。例如,对于C和C++代码,你会得到.o(对象代码)文件。对于Python,你会得到.pyc或类似的文件,有时候在一个子目录中(Python 3)。这些文件都不应该提交。10
如果Git抱怨所有这些文件,那将是非常烦人的。所以你可以让Git * 闭嘴 * 某些 untracked 文件,方法是在.gitignore文件中列出这些文件名或模式。在.gitignore中列出 untracked 文件会让git status闭嘴。这是主要目的,真的。
现在,列出这些未被跟踪的文件也有一些次要的影响,特别是,你现在可以使用en-mass git add .操作来添加 * 所有 * 文件,包括新文件,而不添加这些未被跟踪但被忽略的,安静地没有抱怨的,不应该被提交的文件。
不过,你最需要知道的是:**如果一个文件被跟踪,则不能忽略它。**在.gitignore中列出一个 tracked 文件没有任何效果。幸运的是,tracked 有一个简单的定义:当且仅当一个文件 * 现在 * 就在Git的索引中时,它才被 * 跟踪 *。
我们知道可以使用git rm(同时删除工作树和索引副本)或git rm --cached(只删除索引副本)从Git的索引中删除文件。一旦我们删除了这样一个文件,它就 untracked 了(如果我们忘记使用--cached,它可能会完全消失)。
但是我们不能改变任何一个已经存在的提交。如果一个不应该进入Git的文件进入了某个已经存在的提交,那么它就永远停留在那里了。只要我们有那个提交,如果我们 checkout 那个提交,Git就会把这个文件复制到Git的索引中(和我们的工作树),它将被跟踪。我们需要再次删除它,每次,取消跟踪它。唯一的解决办法是完全停止使用该提交。
因此,确保 * 应该 * 取消跟踪的文件 * 保持 * 该状态非常重要:永远不要在任何提交中被提交,因此永远不要通过基本的check-out-a-commit动作潜入Git的索引。如果你做了一个 bad 提交,其中有一些不应该的文件,尽量避免传递该提交。在它污染其他Git仓库之前将其删除。我们不会在这里讨论 * 如何 * 做,但最终你可能需要学习这一点,因为这种情况经常发生。
10有时候“构建工件”需要归档。不过,将它们放入 Git 通常是不明智的,因为Git的算法在处理大的二进制文件时往往会崩溃,尤其是压缩文件。

相关问题