将分阶段更改移至新分支并提交

use*_*586 0 git branch commit

在 Git 上,我目前在分支上有一些暂存但未提交的更改master

我不想提交到 master 分支,而是想

  1. 创建一个新分支,例如development;然后
  2. 将分阶段更改移至新分支,并reset/清除 上的分阶段更改master;然后
  3. 在新分支上提交分阶段的更改;然后
  4. 将提交推送到远程仓库;然后
  5. 将此提交合并developmentmaster远程,并保留development分支;然后
  6. master从远程刷新本地master,而不更改本地现有的未提交文件

请问我应该怎么做呢?我仍然是 git 的初学者,所以分步骤解释一下,以便我可以遵循。

add注1:我的分阶段更改包含 100 多个文件,因此手动将它们逐一手动挑选到新分支会很痛苦。如果可能的话,我试图避免这种容易出错的方式。

注2:有超过30个文件我没有暂存更改。即使从远程刷新后,我也想在本地保留这些更改master

tor*_*rek 7

长话短说

\n

更改不是“在分支上”进行的。事实上,Git根本没有变化:Git 只有快照

\n

这是什么意思?嗯,这意味着你的问题的简短答案是:

\n
    \n
  • 创建新的分支名称 ( git branch); 然后
  • \n
  • 切换到新的分支名称(git switchgit checkout);然后
  • \n
  • 犯罪 (git commit)。
  • \n
\n

您可以将前两个步骤与git switch -c development或结合起来git checkout -b development。这两个命令执行相同的操作:git switch是 Git 2.23 中的新命令,作为将重载git checkout命令拆分为两个单独命令的项目的一部分;旧的git checkout仍然保留在 Git 中,并且可能会在接下来的 20 年中保留,但慢慢迁移到新的是个好主意。

\n

重要的是要认识到这个过程\xe2\x80\x94git switch -c development特别是\xe2\x80\x94 使用了快捷方式。它不适用于某些其他情况,但适用于这种情况。

\n

更长

\n

不过,这确实值得更长的解释。 为什么上面的方法有效?您需要了解的内容从以下内容开始:

\n

Git 就是关于提交

\n

Git 的新手常常认为 Git 是关于文件的,这很自然:我们在 Git 中存储文件。或者,他们可能认为 Git 是关于分支的,这也很自然:我们总是“在”某个分支\xe2\x80\x94 上,如所说的git statuson branch master其他什么。从技术上讲,您也可以不位于任何分支上,即 Git 所谓的“分离 HEAD 模式”,但除了某些特殊情况外,您通常不希望以这种方式工作。

\n

问题是,这些观点都不正确。归根结底,Git 的核心就是提交。确实,每个提交都存储文件,并且我们确实将提交形成分支,我们还发现有branchs\xe2\x80\x94或更准确地说,有分支名称,我们(相当草率地/)懒惰地)调用分支,即使我们也将其他事物称为分支。但最终,Git 是关于提交的

\n

(注意:如果当我们使用分支来查找分支时您感觉到有什么问题,那么您就在正确的轨道上:这个概念问题。事实上,这个词分支定义错误。它(ab)用于多种用途。)

\n

存储库的核心是提交的集合,以及我们稍后将讨论的其他一些内容。这些提交是四种 Git对象之一,Git 将所有这些对象存储在一个大型键值数据库对象数据库)中,该数据库使用哈希 ID(或更正式的对象 ID或 OID)作为键。Git 迫切需要这些对象 ID 才能在数据库中查找提交。

\n

这些哈希 ID 又大又丑,而且看起来很随机。例如,9bf691b78cf906751e65d65ba0c6ffdcd9a5a12c是 Git 本身的 Git 存储库的任何克隆中的特定提交。 每个提交都有一个唯一的哈希 ID:宇宙中任何地方的所有 Git 软件都同意该事物9bf691...blahblah意味着提交,即使这个特定的存储库从未有过该提交并且永远不会获得它。每次提交时,Git 都会生成一个新的唯一哈希 ID。1这意味着查找提交 所需的只是哈希 ID\xe2\x80\x94,但同样,Git 确实需要该哈希 ID,以便它可以在其对象数据库中查找。要么它有该对象,所以它有提交,要么没有。如果您的 Git 存储库缺少提交,您将需要从某个拥有该提交的存储库获取提交。我们将省略细节,但这就是重点。git fetch

\n

不管怎样,考虑到提交如此重要,您需要确切地知道提交是什么以及它对您有什么作用。因此,除了看起来奇怪的随机“数字”(哈希 ID)之外,您还需要了解以下内容:

\n
    \n
  • 每次提交都是只读的。编号系统需要这样做。

    \n
  • \n
  • 每次提交都包含(间接)每个文件。更准确地说,提交具有每个文件(它所拥有的)的完整快照,作为一种存档:源文件的 tarball 或 zip 文件或 WinRAR 或其他文件。Git 非常巧妙地存储这些\xe2\x80\x94,包括对内容进行重复删除\xe2\x80\x94,以便内容在提交之间甚至在提交内共享,因此即使每个提交都有每个文件,但大多数提交确实很小。在新存储库中的第一个存储库不是,因为该存储库必须第一次存储所有文件,但此后,大多数提交大多会重复使用大多数文件,因此那些重复使用的文件不需要空间。

    \n
  • \n
  • 除了快照之外,每个提交还包含一些元数据或有关提交本身的信息。例如,这包括提交人的姓名和电子邮件地址。

    \n
  • \n
\n

除了您的姓名和电子邮件地址(Git 从您的设置中获取user.name)之外user.email,Git 主要自行构建所有元数据。你只需运行git commit,Git 就会生成一个快照\xe2\x80\x94,我们很快就会看到“来自何处”\xe2\x80\x94,并添加元数据。对于 Git 来说,任何一次提交中最重要的元数据之一是先前提交哈希 ID 的列表。大多数提交在此列表中只有一个条目:我们将这些提交称为“普通”提交。

\n

这个单个先前提交哈希 ID 存储在普通提交的元数据中,使提交“指向”其父。也就是说,提交会记住哪个提交出现在该特定提交之前。如果我们喜欢\xe2\x80\x94并且我确实喜欢\xe2\x80\x94,我们可以画这个,将较新的提交放在右侧,将较旧的提交放在左侧,如下所示:

\n
... <-F <-G <-H\n
Run Code Online (Sandbox Code Playgroud)\n

这里H代表链中最后一次提交的“h”ash ID。CommitH有所有文件的完整快照,加上一些元数据,commit中的H元数据包含早期 commit 的哈希 ID G。SoH 指向 G,如向后伸出 o 的小箭头所示H

\n

这意味着如果我们能够获取 Git 提交的哈希 ID H,Git 就可以使用 commitH查找更早的提交G。当然,G它也是一个普通的提交,因此它有一个伸出的箭头,向后指向其父级F。Git 现在可以找到 commit F。只要这也是一个普通的提交,它就向后指向另一个提交。

\n

这样,Git就可以找到链中的每一次提交,只要Git能找到链中的最后一次提交即可。我们所要做的就是记住上次提交的哈希 ID。当然,记忆9bf691b78-ugh-glah-whatever是非常痛苦的,所以 Git 为我们提供了一种避免这种情况的方法。

\n
\n

1我们可以用数学证明这个想法注定会失败。然而,哈希 ID 空间的巨大规模将故障日期置于足够遥远的未来,\xe2\x80\x94我们希望\xe2\x80\x94我们永远不必关心。

\n
\n

分支名称查找最后的提交

\n

为了避免记住提交的哈希 ID H,我们只需告诉 Git 我们想要一个分支名称,例如master。Git 将最后一个哈希 ID 粘贴到名称中:

\n
...--G--H   <-- master\n
Run Code Online (Sandbox Code Playgroud)\n

该名称现在指向最后一次提交,Git 可以从中找到每个较早的提交。这就是它的全部内容\xe2\x80\x94 好吧,几乎全部。

\n

正如我之前提到的,Git 喜欢我们在分支“上”。位于某个分支上意味着特殊名称HEAD附加分支名称上:这就是 Git 如何知道我们实际使用的是众多分支名称中的哪一个。

\n

现在让我们添加一个新的分支名称,并使其也指向 commit H,如下所示:

\n
...--G--H   <-- development, master (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n

这意味着我们已经“开启”了master。两个名称都选择 commit H,而 commit是我们现在正在使用的H提交,但我们正在使用名称来执行此操作。master

\n

如果我们git switch development现在运行,我们会得到:

\n
...--G--H   <-- development (HEAD), master\n
Run Code Online (Sandbox Code Playgroud)\n

我们仍在使用 commit ,但现在我们通过 name 来H这样做。当我们创建新的提交时,这一点很重要。因为我们正在使用 commit ,所以我们的新提交将向后指向,但该新提交的哈希 ID将保存在当前分支名称中,如下所示:developmentH H

\n
...--G--H   <-- master\n         \\\n          I   <-- development (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n

如果我们现在进行另一个新提交,这个新提交J将向后指向I\xe2\x80\x94 因为我们现在正在提交I,通过名称\xe2\x80\x94 并且 Git 将再次development更新名称:development

\n
...--G--H   <-- master\n         \\\n          I--J   <-- development (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n

这就是 Git 中分支的生长方式。我们是否应该切换回master现在:

\n
...--G--H   <-- master (HEAD)\n         \\\n          I--J   <-- development\n
Run Code Online (Sandbox Code Playgroud)\n

Git 将删除development(即 commit- )文件J,并放回所有 commit-H文件。

\n

Git 的索引和你的工作树

\n

我(简要地)提到 Git 的提交是只读的,文件以一种归档方式存储在提交中,并进行压缩和重复数据删除。这对我们来说意味着我们实际上无法读取这些文件\xe2\x80\x94,只有 Git 可以读取它们\xe2\x80\x94,并且实际上没有任何东西,甚至 Git 本身,可以覆盖它们。这对于归档\xe2\x80\x94 来说非常有用,这就是提交正在做的事情,至少在第一级\xe2\x80\x94 但对于完成任何新工作毫无用处。

\n

为了完成工作,Git 必须在提交中取消归档文件。git switch当我们使用或进行切换时,Git 将从我们要移动到的提交git checkout提取所有文件。当然,首先,Git 必须删除我们要删除的所有文件。然后 Git 将所有文件提取到您可以使用的地方。那是你的工作树工作树。您现在可以完成工作了!

\n

Git 的重复数据删除技巧在这里发挥了作用。删除和替换文件有点慢,因此 Git在从一个提交切换到另一个提交之前检查哪些文件是重复的。对于这些文件,Git 根本不需要执行任何操作\x​​e2\x80\x94,然后就不会了。而且,如果我们从 commit 切换H到 commit H,这意味着每个文件都是重复的,因此 Git 不必删除和替换任何文件

\n

这就是为什么创建一个新的分支名称,然后切换到它在这里是安全的。无需返工文件;根本不需要碰任何文件。所以 Git 不会触及任何文件,一切都很好。

\n

不过,关于这一点还有更多要说的。通常,Git确实必须填写工作树。例如,考虑一下这样的情况:您刚刚克隆了一个存储库,Git 正在第一次填充您的工作树。您可能会想:啊,好吧,Git 只是提取所有文件。 例如,这就是其他版本控制系统所做的事情。这就足够了。但这不是 Git 所做的。

\n

相反,Git 有一种文件跟踪系统,Git 通过三个名称来调用该系统:索引暂存区域,有时(现在主要是像这样的标志git rm --cached缓存。这三个名字都指同一件事。

\n

当 Git 提取提交时,Git 会使用该提交中的文件填充其索引和工作树。索引中的副本(或者可能是“副本”)是预先去重复的,以 Git\ 的内部只读格式存储,但与 commit\xe2\x80\x94 中的副本不同,它会被冻结提交本身继续存在\xe2\x80\x94索引副本可以批量替换。由于初始副本(或“副本”)提交中任何内容的重复项\xe2\x80\x94,因此\xe2\x80\x94it\会自动删除重复项,几乎什么都没有。(索引条目本身仍然需要一些空间来保存文件名和一堆缓存数据。)

\n

同样,该索引“暂存区域”:这是同一事物的两个名称。当您修改文件的工作树副本时,索引副本 \xe2\x80\x94 不会发生任何变化!它只是坐在那里,仍然保留着提交中的去重副本。

\n

但是,当您运行时git add,Git 会读取文件的工作树副本,将其压缩为 Git 在提交中使用的内部格式,并检查重复项。如果压缩文件是重复的,Git 会丢弃它刚刚构建的压缩副本,并使用现有的压缩副本。否则,它会保存刚刚制作的压缩副本。无论哪种情况,Git 现在都会将新的或重复使用的副本/“副本”交换到索引中。2

\n

这一切的结果很简单:

\n
    \n
  • before git add,索引保存每个文件的副本(预先去重),准备提交;
  • \n
  • 之后git add,索引仍然保存每个文件的副本(预先去重),准备提交。
  • \n
\n

这意味着索引始终保存每个文件的副本,准备提交。 实际上,索引保存了您建议的下一次提交的快照。

\n

当您运行时git commit,Git 只是以当时的形式打包索引中的所有文件,以便在新快照中使用。这将是新提交的存档。Git 还会在此时收集或生成必要的元数据\xe2\x80\x94,使用当前提交哈希 ID作为父项\xe2\x80\x94,并将所有这些写出以获得新提交的随机哈希 ID,然后git commit将新的提交存储到数据库中,并将其 ID 填充到当前分支名称中。

\n

这一切确实相对简单。那么变化从哪里来呢?

\n
\n

2从技术上讲,索引条目保存文件名、一些缓存数据以及全对象数据库中对象的Blob 哈希 ID 。你真的不需要担心这个。如果您愿意,您可以将索引视为包含完整副本。

\n
\n

如果提交是快照,为什么我们会看到更改?

\n

git show如果您在普通提交上运行,Git 将:

\n
    \n
  1. 吐出元数据;然后
  2. \n
  3. 显示差异以显示发生了什么变化。
  4. \n
\n

Git此时会计算这个差异!Git 使用该普通提交中的元数据来查找该提交的父级。然后,Git 提取两个快照(实际上是在内存中的临时区域)并比较删除重复的文件。由于现在很难发现重复项,因此 Git 实际上只需要比较不同的文件。对于每个这样的文件,Git 都会计算一组更改,如果将这些更改应用于该文件的父级副本,则会生成该文件的子级副本。

\n

这就是您看到的差异:git diff通过将父母与孩子进行比较而得出的差异。(该git show命令调用与此相同的内部代码git diff。它只是git show自动为您找到父级。如果您想使用git diff这种方式,则必须选择两个提交。必须选择两个提交的好处是你可以选择任何一对提交。)

\n

当你运行时git status,Git:

\n
    \n
  • 首先打印有关当前分支的内容(On branch master例如);
  • \n
  • 运行 a将当前提交快照与索引git diff快照进行比较;和
  • \n
  • 再跑一趟git diff
  • \n
\n

第一次比较\xe2\x80\x94当前提交与索引/暂存区域\xe2\x80\x94中建议的下一个快照之间发生了什么变化,可以快速跳过已删除重复的相同文件,并仅比较不同的文件。由于它不会发出一组实际的更改,因此它会短路代码\xe2\x80\x94您可以使用git diff --name-status\xe2\x80\x94自己执行此操作,并仅显示某些文件已更改

\n

此处显示为已更改的任何文件都列在为 commit 暂存的更改下。新文件或删除的文件在这里以相同的方式显示。(Git 也在这里进行重命名检测;我们不会正确介绍这一点。)

\n

列出这些“暂存提交”文件后,git status第一个差异就完成了。现在它继续执行第二次操作git diff --name-status,这次将其索引中的内容与工作树中的内容进行比较。3 对于每个相同的文件,Git 再次什么也没说。但对于不同的文件,Git 现在会提及文件的名称,并在未暂存提交的更改下列出该文件。

\n

不过这里有一点奇怪。假设您使用操作系统的“删除文件”命令(无论适用于您的操作系统)从工作树中删除了一个文件,而不是从 Git 的索引中删除它。Git 会说该文件的删除“未暂存以进行提交”。这是有道理的,并且与第一种 diff 匹配:如果您使用git rm,它会删除索引工作树副本,您会看到删除为“暂存以进行提交”(然后索引和工作树缺少副本)匹配,所以不再提及)。

\n

但是假设您的工作树中有一个尚未编辑的全新文件git add。Git 会在 diff 输出之后保存这些文件名。然后它继续抱怨这些文件未被跟踪

\n
\n

3由于工作树中的文件没有经过压缩和去重\xe2\x80\x94,所以它们只是普通文件\xe2\x80\x94Git 必须在这里更加努力地工作。我们也将跳过所有这些细节。

\n
\n

未跟踪的文件和.gitignore

\n

未跟踪的文件是位于工作树中但不在 Git\ 索引中的文件。git add这就是全部内容,除了索引是您通过和 (部分)控制的事实之外git rm。理解这一点至关重要,因为.gitignore.

\n

.gitignore文件的命名相当错误。这不会忽略文件。Git 提交 Git 索引中的所有内容:您运行git commit,Git 索引中的所有内容都会进入新提交。.gitignore从这个开始是什么: 它让git status 闭嘴.

\n

例如,当您运行时git status,它会对您的所有构建文件发出很多抱怨声。但它们是故意不被追踪的,我们希望它们保持这种状态。抱怨他们只会适得其反。因此列出这些内容.gitignore告诉 Git:关闭____

\n

git add命令还有一些集体“添加所有内容”模式:例如,git add .git add --all它们会查找 Git会抱怨的所有文件,并添加它们。由于列出文件.gitignore使 Git 停止抱怨,因此它也使 Git 停止添加这些文件(如果它们当前未被跟踪)。

\n

.gitignore 不做的是在跟踪文件时阻止 Git 提交文件。如果某个文件在 Git 的索引中,它将提交。“添加全部”或git add -u(更新)模式将从工作树副本更新索引副本。所以这应该被称为.git-do-not-complain-about-these-files-when-they-are-untracked-and-do-not-add-them-with-an-en-masse-git-add-operation-either,或其他什么。但没有人愿意输入这样的文件名,所以.gitignore就是这样。

\n

结论

\n

一旦您了解了索引是什么以及它的作用以及当您更改提交时 Git 如何交换索引和工作树副本,就会清楚只要您更改提交\xe2\x80\x94,例如,当您创建新分支仍然选择当前提交,然后切换到该分支\xe2\x80\x94,通过首先创建新分支来在新分支上进行新提交是非常安全的。

\n

(稍后,一旦您了解了分支名称如何“指向”提交,就很容易了解如何进行提交,然后创建一个分支名称,然后将另一个分支名称向后移动一步,以实现相同的目的。但这是更多的工作,所以你不妨做更简单、更清晰的事情。)

\n