创建分支和进行软重置之间的区别?回到旧版工作的最佳方法是什么?

问题描述 投票:1回答:4

说我的提交历史是A - B - C而我只有这个分支。

B完全正常工作。我开始在C中添加一些功能,但它不起作用,所以我需要回到B,但我也想保留我在C中编写的代码,因为我想查看它并稍后修复它。最好的方法是什么?

从B开始创建新分支的最佳方法是什么?

这与软复位有什么区别?我理解软复位不会删除更改(这是正确的吗?)但我不清楚如何恢复这些更改(C中的代码),也不清楚软复位和创建分支之间的区别。

在旁边

Git看起来似乎不必要的奥术和模糊。我的意思是,官方文档将推送定义为:

https://git-scm.com/docs/git-push

git-push - 更新远程引用以及相关对象

我相信它在技术上是正确的,但它不是最方便用户的解释。他们是否可以添加一条评论,说明它将本地存储库上传到远程存储库,或类似的东西?

git git-branch git-reset
4个回答
4
投票

这里的所有答案都可以。缺少的是,嗯...这是你的咆哮进来的地方.-)你的教授引用这里是相当apposite

我在大学的最好的教授之一总是说:要小心那些试图愚弄非常复杂的概念,但也要注意复杂性的人:那些无法以简单的方式解释简单概念的人要么炫耀要么做不是真的了解这个概念本身!

或者,as Einstein supposedly put it,“让一切变得尽可能简单,但并不简单。”

不幸的是,Git所做的 - 分布式源代码控制 - 本质上很复杂。幸运的是,有一些简单的方法可以开始。不幸的是,传统书籍和Git的文档本身在我看来并不是那么好。我认为Pro Git book相当不错(并且具有通常最新的优势),并且还有一些其他书籍现在已经非常过时了,但是大多数介绍都试图在没有一个适当的基础。

该基础也需要一些术语。这可能是Git手册页面最难破解的地方。他们只是喷洒术语 - 有时是不一致的术语,尽管随着时间的推移,这种术语已经得到了改善。这导致了一些非常有趣的web pages。 (我认为很多Git的介绍都避免使用术语,因为Git的核心基础在于图论和散列理论,人们发现那些可怕的数学方面。)

Git本身使事情变得更加艰难。一个简单的存在证明就是Mercurial。至少就他们对源代码所做的事情来说,Mercurial和Git同样强大 - 但是那些刚接触分布式源代码控制的人在Mercurial中开始的问题远远少于在Git中的问题。它不是100%清楚为什么会这样,但我认为Mercurial有两个不同的关键因素产生了这个结果:

  • 在Mercurial,分支机构是全球性和永久性的。这对于开始工作非常方便,但至少有时候证明是一个陷阱。 Mercurial最终添加了像Git分支机构一样的书签。
  • Mercurial没有Git称之为索引的东西。

这些并不是唯一的东西--Git还有许多其他较小的烦恼,而Mercurial也不存在 - 但我认为它们是最重要的两个。例如,git reset的整个问题在Mercurial中没有出现,因为git reset(a)操纵分支指针 - Mercurial有这些书签,如果你选择使用它们 - 和(b)操纵Mercurial甚至没有的索引。

My own answer: what's going on

无论如何,这里的关键是这三件事。 (这里有一些术语!)

  1. 在Git中,分支名称只不过是名称到哈希ID的映射。重要的是提交。
  2. 提交是一个唯一的实体,由唯一的哈希ID(如b5101f929789889c2e536d915698f58d5c5c6b7a)标识,它存储永久1和不可交换的文件快照和一些元数据,包括其他一些提交的哈希ID。
  3. 索引是Git实际用于构建新提交的区域。

1无论如何,作为提交的永久性。如果没有办法找到它们,提交最终会消失。这就是分支名称和图论的结果 - 但我们稍后会讨论。


What to know about the index

让我们从这个观察开始:当提交存储所有文件的快照时,它会将它们保存为压缩的只读Git存储形式。它们有点像冷冻或冷冻干燥。没有人可以改变它们。这对于保存档案的旧源代码来说很好 - 但对于完成任何新工作完全没用。

要完成工作,您需要一个文件解冻,补水,可读和可写的地方,以正常的日常形式。那个地方就是Git所说的工作树。 Git可以在这里停止 - 冻结提交和灵活的工作树 - 这就是Mercurial所做的并且它工作正常。但无论出于何种原因,Git都会添加它所谓的索引,有时甚至是暂存区域,甚至是缓存。 (使用的名称取决于谁/什么做命名,但这三个是相同的。而且,索引本身比我将要复杂,但我们不需要担心这些复杂性。 )

索引存储的是Git-ified文件副本。它们并没有完全冷冻,但它们的格式相同 - 冻干形式,就像它一样。它们对你没用;它们只对Git有用。为什么这样做是有争议的,但它确实如此,你需要了解它。它的作用是,索引是Git如何进行新提交的。

当你运行:

git commit -m "this is a terrible log message"

Git将立即打包索引中的所有内容,以及您的元数据 - 您的姓名和电子邮件地址以及日志消息等等 - 并将其转换为新的提交。工作树中你正在做工作的东西完全无关紧要!事实上,一切都已经准备好 - 已经冻干,就像它一样 - 是使git commit如此之快的原因。 Mercurial的hg commit提交工作树中的内容,必须检查工作树中的每个文件,看它是否与前一个文件相同,如果没有,则为提交准备冻干表单。所以在一个大项目中你运行hg commit然后出去喝咖啡或者其他什么。但是使用Git,如果你在工作树中更改一个文件,Git会让你运行:

git add file

每次。这会将文件 - 冻干或Git-ify-ing-复制到索引中。

因此,索引始终包含您要提出的下一个提交。如果对工作树进行了一些更改,并在下次提交时想要它们,则必须在运行git commit之前将它们显式复制到索引中。您可以使用git commit -a让Git扫描您的工作树并为您执行adds,如果您使用Mercurial,Git会像Mercurial那样行事。这当然很方便,让你不要考虑索引,甚至假装它不存在。但我认为这是一个糟糕的计划,因为那时git reset变得莫名其妙。


2通常情况并非那么糟糕,在一个小项目中差异几乎无法察觉。 Mercurial使用了大量的缓存技巧来尽可能地加快速度,但是 - 与Git不同 - 它可以阻止那些用户使用它们。


Commits

现在让我们仔细看看究竟是什么进入了提交。我认为看到这个的最好方法是查看实际的提交。你可以看看你自己:

git cat-file -p HEAD

但是我会在Git的Git存储库中显示这个,就像这样:

$ git cat-file -p b5101f929789889c2e536d915698f58d5c5c6b7a | sed 's/@/ /'
tree 3f109f9d1abd310a06dc7409176a4380f16aa5f2
parent a562a119833b7202d5c9b9069d1abb40c1f9b59a
author Junio C Hamano <gitster pobox.com> 1548795295 -0800
committer Junio C Hamano <gitster pobox.com> 1548795295 -0800

Fourth batch after 2.20

Signed-off-by: Junio C Hamano <gitster pobox.com>

请注意treeparent行,它们引用其他哈希ID。 tree行表示已保存的源代码快照。它可能不是唯一的!假设您进行了提交,然后再回到旧版本,但将其保存为新的提交。新提交可以重用原始提交的tree,Git会自动执行此操作。这是Git用于压缩存档快照的众多技巧之一。

然而,parent线是Git提交成为图形的方式。这个特别的提交是b5101f929789889c2e536d915698f58d5c5c6b7a。在此提交之前提交的是a562a119833b7202d5c9b9069d1abb40c1f9b59a,它是一个合并提交:

$ git cat-file -p a562a119833b7202d5c9b9069d1abb40c1f9b59a | sed 's/@/ /'
tree 9e2e07ce274b0a5a070d837c865f6844b1dc0de8
parent 7fa92ba40abbe4236226e7d91e664bbeab8c43f2
parent ad6f028f067673cadadbc2219fcb0bb864300a6c
author Junio C Hamano <gitster pobox.com> 1548794876 -0800
committer Junio C Hamano <gitster pobox.com> 1548794877 -0800

Merge branch 'it/log-format-source'

Custom userformat "log --format" learned %S atom that stands for
the tip the traversal reached the commit from, i.e. --source.

* it/log-format-source:
  log: add %S option (like --source) to log --format

这个提交有两个parent行,给出两个提交。这就是首先让它成为合并提交的原因。

所有这一切意味着,如果我们抛出查看源代码的概念(我们可以随时通过使用每个提交中的tree行将其返回 - 每个提交都有一个),我们可以将提交本身视为一个图表中链接的一系列节点,每个节点都有自己唯一的哈希ID,每个哈希ID都记住一些前任或父节点的哈希ID。

我们可以这样绘制:

A <-B <-C

对于一个简单的三提交存储库,或者:

...--I--J--M--N
  \       /
   K-----L

对于一个更复杂的存储库,合并为最后一次提交的父级(在右侧)。我们使用一个大写字母代表实际的,显然是随机的哈希ID,因为哈希ID很笨重(但是单个字母非常易于使用)。从子项提交回其父项的箭头或连接线是实际提交中的parent行。

再次记住,所有这些提交都会在时间上永久冻结。我们无法改变其中任何一个方面。我们当然可以进行新的提交(像往常一样从索引)。如果我们不喜欢提交C或提交N,我们可以替换它,例如:

     D
    /
A--B--C

然后我们可以弯曲C并使用D代替:

A--B--D
    \
     C

这些是相同的图表,我们只是以不同的方式看待它。

Branch names (and other names but we won't cover them here)

这些图形图形简洁明了,我认为,这是推理Git存储库的方法。它们显示提交,并隐藏了我们丑陋的哈希ID。但是Git确实需要哈希ID - 这就是Git检索提交的方式 - 我们需要记住这些链中任何一个链的最后一个哈希ID。我们现在只需要最后一个的原因应该是显而易见的:如果我们抓住,比如提交D,那么,提交D会将commit B的实际哈希ID存储在其自身内部。所以一旦我们知道D的哈希,我们使用D来找到B。然后我们使用B找到A,并且 - 因为A是第一次提交,因此没有父级 - 我们可以停下来休息。

所以我们在这里需要再添加一个图纸。我们需要的是一个分支名称。该名称只是指向(即包含最后一次提交的实际哈希ID)!我们可以将其绘制为:

A--B--D   <-- master
    \
     C

名称master保存最后一次提交的哈希ID。从那里我们找到了之前的提交。 Git为我们存储的是:

  • 所有提交,按哈希ID
  • 一组名称,每个名称都包含一个哈希ID

除了索引和工作树的所有复杂性之外 - Git是如何工作的。要创建新的提交E,我们只需对索引进行快照,添加元数据(我们的名称,电子邮件地址等),包括commit D的哈希ID,并将其写入提交数据库:

        E
       /
A--B--D   <-- master
    \
     C

然后让Git自动更新名称qa​​zxswpoi以指向我们刚刚做出的新提交:

master

现在我们可以解决问题:

        E   <-- master
       /
A--B--D
    \
     C

然而,可怜的孤独承诺A--B--D--E <-- master \ C 怎么样?它没有名字。它有一些实际的大丑陋的哈希ID,但如果没有名称或记住该哈希ID,我们将如何找到提交C

答案是Git最终将完全删除C,除非我们给它起一个名字。显而易见的名称是另一个分支名称,所以让我们这样做:

C

现在我们有两个分支,A--B--D--E <-- master \ C <-- dev master。名称dev的意思是“提交master”,而名称E的意思是“提交dev”,目前。当我们使用存储库并向其添加新提交时,存储在这两个名称下的哈希ID将会更改。这导致了我们的关键观察:在Git中,提交是永久的(大多数)和不可更改的(完全),但分支名称移动。 Git存储图形 - 这些提交链及其内部箭头连接它们,以这种向后看的方式为我们。我们可以通过添加更多提交随时添加。并且,Git为我们存储了一个名称到哈希ID的映射表,其中分支名称在图中包含起始点(或结束点?)的哈希ID。

这些起点/终点的Git术语是提示。分支名称标识提示提交。

C, and HEAD and the index and the work-tree

既然我们的存储库中有多个分支名称,我们需要一些方法来记住我们正在使用的分支。这是特殊名称git checkout的主要功能。在Git中,我们使用HEAD来选择一些现有的分支名称,例如git checkoutmaster

dev

结果是:

$ git checkout dev

通过将名称A--B--D--E <-- master \ C <-- dev (HEAD) 附加到像HEAD这样的分支名称,Git知道我们现在正在处理哪个分支。

作为一个关键的副作用,Git还:

  • dev中的所有文件复制到索引中,为下一次提交做好准备,并且
  • C / the-index中的所有文件复制到工作树中,以便我们可以查看和使用它们。

Git可能还需要删除一些文件,如果我们在提交C并且它有E中没有的文件。它将从索引和工作树中删除它们。像往常一样,Git确保每个文件的所有三个副本都匹配。例如,如果在提交C中有一个名为README的文件,我们有:

  • C:这是提交HEAD:README中的冻结Git-ified副本,现在可以使用特殊名称C访问。
  • HEAD:这是索引副本。它目前与:README匹配,但我们可以用HEAD:README覆盖它。
  • git add:这是一个普通文件。我们可以使用它。 Git并不十分关心这一点 - 如果我们改变它,我们需要将它复制回README

所以,有一个动作 - :READMEgit checkout master-我们:

  • 重新加上git checkout dev;
  • 填写索引;和
  • 填写工作树

现在可以工作了,HEAD文件将它们复制回索引,git add创建一个新的快照,添加到分支并使分支名称引用新的提交。让我们在git commit上做一个新的提交F

dev

现在我们将:

... edit some file(s) including README ...
git add README                    # or git add ., or git add -u, etc
git commit -m "another terrible log message"

Git知道更新A--B--D--E <-- master \ C--F <-- dev (HEAD) ,而不是dev,因为master附加到HEAD,而不是dev。还要注意,因为我们现在从我们索引中的任何内容提交了master,并且我们只是使索引与工作树匹配,现在F,索引和工作树都匹配。如果我们刚刚运行F,那就是我们所拥有的!

This is where git checkout dev comes in

除了最终被删除的无法访问的提交的特殊情况之外,图表本身只能被添加到。但是,分支名称,我们可以随时随地移动。执行此操作的主要命令是git reset

例如,假设提交git reset很糟糕 - 这是一个错误,我们只想完全忘记它。我们需要做的是移动名称F,而不是指向dev,它再次指向F-C的父母。

我们可以找到commit F的哈希ID,并且粗略地将它直接写入分支名称。但是,如果我们这样做,那么我们的索引和工作树呢?它们仍将匹配commit C的内容。我们将有图表:

F

但索引和工作树将不匹配A--B--D--E <-- master \ C <-- dev (HEAD) \ F 。如果我们再次运行C,我们将得到一个与git commit几乎完全相同的提交 - 它将共享F,并且只是有一个不同的日期戳,可能是更好的日志消息。但也许这就是我们想要的!也许我们想要修复我们可怕的日志消息。在这种情况下,从当前指数中创建一个新的tree就是答案。

这就是G所做的:它允许我们将分支名称移动到指向不同的提交,而不更改索引和工作树。我们丢弃git reset --soft,然后制作一个新的F,就像G但有正确的信息。 F没有名字,最终消失了。

但是,如果我们只是想完全摆脱F呢?然后我们希望索引和工作树匹配提交F。我们会像以前一样让C消失。但是为了使索引和工作树匹配F,我们需要C

因为索引和工作树是独立的实体,所以我们可以选择中途走。我们可以将名称git reset --hard移动到指向dev,用C替换索引内容,但是单独留下工作树。这就是C所做的,git reset --mixed实际上是git reset --mixed的默认值,因此我们甚至不需要git reset部分。

所有这三个行动都有不同的最终目标:--mixed用于重新执行提交,git reset --soft用于完全丢弃提交,而git reset --hard在这个特定示例中没有明确的用法。那他们为什么拼写git reset --mixed?这就是你的咆哮再次适用的地方:他们可能不应该。它们之间的关系是Git有这三件事可以用branch-name-to-commit-hash,索引和工作树内容:

  1. 移动分支名称
  2. 替换或保留索引内容
  3. 替换或保留工作树内容

git reset将执行第1步并停止(git reset),或者执行步骤1和2并停止(git reset --soft /默认值),或者执行所有三个并停止(git reset --mixed)。但他们的目的并不相关:Git是一个令人困惑的机制(“我们如何从这里到达那里”)与目标(“到那里”)。

Conclusion

说我的提交历史是A - B - C而我只有这个分支。

好:

git reset --hard

我需要回到B,但我也想保留我在C中编写的代码

好。显然,我们想要的是一个识别提交A--B--C <-- branch (HEAD) 的名称和另一个识别提交B的名称。但我们还需要关注索引和工作树!

只有一个索引和一个工作树,3并且C不会复制这些索引。只有提交是永久性的。因此,如果您的索引和/或工作树中有任何未保存的内容,您可能应该立即保存它。 (通过提交,可能 - 并且你可以使用git clone进行不在任何分支上的提交,但是不要去那里,至少现在还没有。)让我们假设你不这样做,以便完全删除这个问题。

图表不会改变。您只需添加一个新名称。有很多方法可以做到这一点,但为了说明,让我们这样做:让我们首先创建一个新的分支名称,也指向提交git stash,我们称之为C。为此,我们将使用save,它可以创建指向现有提交的新名称:

git branch

新名称指向的默认值是使用当前提交(通过$ git branch save 和当前分支名称),所以现在我们有:

HEAD

A--B--C <-- branch (HEAD), save 没有动过:它仍然依附于HEAD,仍然指向branch。请注意,两个分支都标识相同的提交C,并且所有三个提交都在两个分支上

现在我们有了名称C保存save的哈希ID,我们可以自由地将名称C移动到指向提交branch。要做到这一点,我们将使用B。我们希望我们的索引和工作树匹配也提交git reset,所以我们需要B - 它将取代我们的索引和工作树,这就是为什么确保我们不需要保存任何内容他们:

git reset --hard

赠送:

$ git reset --hard <hash-of-B>

当然,还有很多其他选择。例如,我们可以让A--B <-- branch (HEAD) \ C <-- save 指向branch并创建一个指向C的新名称:

B

为此,我们可以使用:

A--B   <-- start-over
    \
     C   <-- branch (HEAD)

由于我们没有移动$ git branch start-over <hash-of-B> ,因此无需以任何方式干扰索引和工作树。如果我们有未提交的工作,我们现在可以根据需要运行HEAD(如果需要更新索引)和git add进行新的提交git commit,将D作为其父级。


3实际上这不是真的。有一个主要的工作树,它有一个主要索引。您可以根据需要创建任意数量的临时索引文件,从Git 2.5开始,您可以随时添加辅助工作树。每个添加的工作树都有自己独立的索引 - 索引索引/缓存工作树,毕竟它自己的C,以便每个可以,实际上必须在不同的分支上。但同样,这不是你需要担心的事情。创建一个临时索引实际上只是用于特殊目的的操作:例如,这就是HEAD如何在不弄乱其他东西的情况下提交当前工作树的方法。

4这就是Git和Mercurial的巨大差异:在Mercurial中,每个提交都只在一个分支上,它永远存在。你实际上不能创建两个标识相同提交的分支名称。 Mercurial也不使用此分支名称等于提示提交,并且通过走图形技巧暗示其他提交。


There's a trick for hash IDs

我只想在这里提到这一点。在上面,我们有很多情况你可能需要运行git stash并剪切和粘贴大丑陋的哈希ID。我们已经知道名称(如分支名称)允许我们使用名称而不是ID。不是像git logC所指出的那样写出branch的哈希ID,而是使用名称:

save

例如,将提取提交git show save ,然后提取提交C,比较两者,并向我们展示BB中快照的不同之处。但我们可以做得更好:

C

意思是:找到提交git show save~1 。然后,退回一个父链接。这是承诺C。所以B现在将在git show及其父级B中提取快照,比较两者,并向我们展示我们在A中所做的改变。波浪号B和hat ~字符可用作任何修订说明符的后缀。 ^中记录了如何指定修订(提交或提交范围)的完整描述。有很多方法可以做到!


几年前,我试着创办一本既使用Git又使用Mercurial的书,让人们开始使用基于图形和哈希的分布式源代码控制。不幸的是,大部分关于工作的工作发生在工作之间,而且我多年来一直没有工作,所以它已经停滞不前并变得陈旧。但对于那些想要看到它们的人来说,它是the gitrevisions manual


1
投票

如果你想保持提交here作为提交并再次从C工作,你将需要一个新的分支。我建议从B和硬重置C(或任何你的主要工作分支)到master做一个新的分支。

然后你会离开这个(为了清楚起见我添加了一个新的提交):

B

D master | C review-c branch |/ B | A 进行软重置将删除提交B并暂存您在C中所做的更改(就像您自己对C进行了更改并对其执行了C一样)。


1
投票

在您的情况下,我建议1)您在git add创建分支或标记,以便您可以在需要时查看它,并且2)将您的分支重置为C,以便B的更改从分支中排除。

C

在某种程度上,标签比分支更稳定。 # create a branch from C git branch foo C # create a tag at C git branch bar C # reset your branch to B git checkout <your_branch> git reset B --hard # review C git show foo git show bar 可能会被一个指挥官转移到另一个提交。 foo总是指向bar,除非你打算将它移动到另一个提交。 C被认为是bar的别名。


1
投票

C

你已经唤醒了<rant-adressing on>。非常害怕。

the Ancients

;-)


对于手头的问题,如果你想在以后保留C的变化但是继续从更好的B状态开始工作,你有很多方法可以做到这一点。我会做一个新的分支*并重置旧分支。

分支创建/硬重置方法

<rant-adressing off>

但是你也可以考虑使用# create your backup branch for these failed changes git checkout -b to-be-reviewed # take your branch to its previous state git checkout - git reset --hard HEAD^ 。如果您想比较用途,请按以下步骤进行比较:

分支创建/软重置方法

git reset --soft

然后你原来的分支点在B和# undo last commit (C) but keep the changes in the working tree git reset --soft HEAD^ # create a new branch and commit on it git checkout -b to-be-reviewed git commit -m "Your message" 有所有最近(虽然不工作)的变化。


最后,这是to-be-reviewed的用例:

没有新的分支/存储方法

git stash

此时你可以用# reset your branch to state B git reset --soft HEAD^ # stash your changes with a title for easier reuse git stash save "Failed changes XYZ" / git stash list检查这个藏匿点。

*(作为ElpieKay git stash show非常合适,可以考虑使用标签代替分支用于此用途。无论如何,整体推理是相同的)

© www.soinside.com 2019 - 2024. All rights reserved.