如何在不向上游推送的情况下保留本地版本并在 Git 上提交更改?

How do I keep a local version and commit changes on Git without pushing upstream?

我是学生,所以我是新手。我从我实习的地方克隆了一个 repo,并想建立我自己的开发分支作为我自己的沙箱。我希望能够提交更改并在它们之间来回切换,但我不想将我的分支推到上游。

我创建了一个新分支,提交了到目前为止的更改。但是当我尝试推送时,Git 希望我将它发送到上游。我如何为自己保留所有这些而不是将其推送到远程位置?我已经在本地设置了所有内容吗?如果是这样,那么我如何查看提交的历史记录并在它们之间切换?

这里您真正需要的是一个很好的 Git 教程,但是代替它,让我们试试这个:

  • Git 就是关于提交的。 Git 新手(甚至有一些经验的人)经常认为它是关于文件或 b运行ches,但实际上不是:它是关于 提交
  • 每个 Git 存储库都是提交的完整集合。也就是说,如果您有 last 提交,那么您也有所有 earlier 提交。1
  • 提交已编号,但数字不是简单的计数:它们不会提交 #1、#2、#3 等。相反,每个提交都有一个丑陋的 哈希 ID 数字,例如 675a4aaf3b226c0089108221b96559e0baae5de9。这个数字在 每个 存储库副本中都是唯一的,所以要么你有一个提交,要么你没有;当你进行新的提交时,它会得到一个新的、唯一的编号,这是其他提交从未有过的。2 这样,就可以连接两个 Git:它们只需互相提交提交编号,而不是整个提交,另一个 Git 可以轻松检查:我有这个提交吗? 只需查找编号即可。
  • 每个提交都包含 Git 知道的每个文件的完整快照。提交不包含更改,尽管当您 show 提交时,Git shows 更改。
  • 上面的工作方式是每个提交还包含一些元数据,或者关于提交本身的信息。这包括提交人的姓名和电子邮件地址、date-and-time-stamp 等;但它还包括原始哈希 ID——提交编号——在此提交之前提交的提交。 Git 将其称为提交的 父级
  • 一旦 Git 提交,其中的任何内容都不能更改,并且提交(大部分)是永久性的。3

因为每个提交都持有前一个(父)提交的哈希 ID,如果愿意,我们可以将提交绘制在一个小型的 3 提交存储库中,如下所示:

 A <-B <-C

这里的A代表第一次提交的哈希ID,B代表第二次,C代表第三次。 last 提交是提交 C 并且是我们通常使用的提交。由于 C 持有早期提交 B 的哈希 ID,但是,Git 可以轻松读取两个提交,并比较两个快照。无论 有什么不同,这就是 Git 将向您展示的内容 — 当然,还有显示提交者 C 等的元数据。

这也意味着,从最后一次提交开始,Git 可以向后一直工作到第一次提交。也就是说,Git 从最后一次提交开始作为要显示的提交。然后 Git 显示它,然后 Git 移动到它的父级,并显示它,依此类推。在 Git 看来,第一个提交是“第一个”的原因是它没有父级:A 没有父级,所以 Git 现在可以停止向后走通过这条链。


1A so-called shallow clone故意弱化了这卦运行tee,但只要你没有使用 git clone --depth <em>number</em> 或类似的,你不会有浅克隆,也不需要担心这个。

2Pigeonhole Principle tells us that this scheme must eventually fail. The reason commit hash IDs are so big is to make the "eventually" take long enough that it doesn't matter. In practice, collisions don't occur, but someone could theoretically hand-craft one. Also, two Git repositories that never actually meet each other could safely have hash collisions. For more about this see How does the newly found SHA-1 collision affect Git?

3这个“不可改变的”属性实际上对所有Git的内部对象都是正确的,所有这些对象都得到这些哈希ID,因为哈希 ID 只是内部对象内容的加密校验和。如果您从 Git 的数据库中取出这些对象之一,对其进行一些更改,然后将其放回原处,更改后的对象将获得一个 新的哈希 ID。旧对象仍然存在,具有旧内容。所以即使 Git 也不能 改变 一个对象:如果我们想替换一个提交,例如,用 git commit --amend,我们得到的并不是真正的 changed 提交,而是 new 提交。旧的还在仓库里!

“大部分永久”中的“大部分”部分是因为 无法 的提交或其他内部对象 any name——git fsck 调用 danglingunreachable——最终会被 Git 的 垃圾收集器git gc。由于篇幅原因,我们不会在这里详细介绍,但 git commit --amend 通常会导致旧的(错误的,现在已被替换)提交稍后被垃圾收集。


B运行切

这里缺少的是 Git 找到 最后一次提交的原始哈希 ID 的简单方法。这就是 b运行ch 名称的用武之地。像 master 这样的 b运行ch 名称只包含 last-commit 哈希 ID:

A--B--C   <-- master

请注意,我已将提交之间的内部箭头替换为连接ines:既然提交不能改变,那没关系,只要我们记住Git不能轻易向前,只能向后。也就是说,A 不知道 B 的散列 ID 是什么,即使 B 已经硬连入了 A 的散列 ID。但是我们会保留 b运行ch 名称中的箭头,这是有充分理由的:这些名称(或箭头)move.

如果我们现在创建一个新的 b运行ch 名称,例如 develop,默认是使用这个新的 b运行ch 名称 also 指向当前提交 C,像这样:

A--B--C   <-- develop, master

现在我们还需要一件事:一种方法来记住我们正在使用的名称。这就是特殊名称 HEAD 的用武之地。名称 HEAD 通常 附加到 b运行ch 名称之一:

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

这表明即使提交 C 有两个名称——所有三个提交都在两个 b运行ches 上——我们使用的名称master.

git checkout 或(自 Git 2.23 起)git switch 命令是更改 HEAD 所附加的名称的方式。所以如果我们 git checkout developgit switch develop,我们得到这个:

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

我们仍在使用提交C;我们刚刚改变了 Git 查找提交 C 的方式。 Git 不是使用名称 master 来查找它,而是使用名称 develop 来查找它。

假设我们现在进行新的提交 D。无需深入探讨如何,我们就假设我们已经完成了。 Git 已经为这个新提交分配了一个新的唯一哈希 ID,并且新提交 D 指向现有提交 C 作为其父项——因为我们在 C我们做了 D。那么让我们画出那部分:

A--B--C
       \
        D

git commit的最后一步有点棘手:Git将新提交的哈希ID写入任何b运行ch名称HEAD 附加到 。所以现在的图表是:

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

git log 通常以 HEAD 开始并向后工作

假设我们现在 运行 git log。 Git 将:

  • 显示提交 D(以及 -p,显示 D 与其父 C 相比 不同 );然后
  • 向后退一步 C 并显示;然后
  • 向后退一步 B 并证明

等等。 Git 以提交 D 开始,因为名称 HEAD 附加到名称 develop 并且 b运行ch 名称 develop 定位提交 D.

假设我们 运行 git checkout mastergit switch master,得到这个:

A--B--C   <-- master (HEAD)
       \
        D   <-- develop
再次

和 运行 git log。这次HEAD附加到mastermaster指向提交C,所以git log会显示C,然后后退一步到 B 并显示,依此类推。 Commit D 好像不见了!但它没有:它就在那里,可以使用名称 develop.

找到

因此,这就是 b运行ch 名称为我们所做的:每个 b运行ch 名称找到“on”的 last 提交b运行ch。较早的提交也在那个 b运行ch 上,即使它们在其他一些 b运行ch 或 b运行ches 上。许多提交在许多 b运行ches 上,在典型的存储库中,第一个提交在 every b运行ch.4

您甚至可以提交根本不在任何 b运行ch 上的提交。5 Git 有一个叫做 的东西分离的 HEAD 模式,您可以在其中进行此类提交,但通常您不会在此模式下进行任何实际工作。在需要解决冲突的 git rebase 期间,您将处于这种分离的 HEAD 模式,但我们也不会在此处介绍。


4您可以在存储库中进行多次“首次提交”。 Git 将这些无父提交称为 根提交 ,如果您有多个提交,则可以拥有彼此独立的提交链。这不是特别有用,但它简单明了,所以Git支持它。

5例如,git stash 进行了此类提交。 Git 发现这些提交使用的名称不是 b运行ch 名称。不过,我们不会在这里详细介绍这些内容。


Git 的索引和您的 work-tree,或者,关于进行新提交的须知

早些时候,我跳过了进行新提交的“如何”部分 D,但现在是时候谈谈这个了。不过,首先,让我们仔细看看提交中的快照。

我们介绍了这样一个事实,即提交的文件——Git 在每次提交中保存的快照中的文件——是 read-only。从字面上看,它们 无法 更改。它们还以压缩和 de-duplicated 格式存储,只有 Git 可以读取。6 de-duplication 处理了大多数提交的事实只是一些早期提交的 re-use 文件。如果 README.md 没有改变,就不需要存储new 复制:每次提交可以只保留 re-using 前一个。

不过,这意味着 Git 提交中的文件 而不是 您将看到和处理的文件。您将要处理的文件采用计算机的普通日常格式,并且可写和可读。这些文件包含在您的 工作树 work-tree 中。当你检查一些特定的提交时——通过选择一个 b运行ch 名称,它指向那个 b运行ch 上的最后一次提交——Git 将填充你的 work-tree使用来自该提交的文件。

这意味着 当前提交的每个文件实际上有两个副本:

  • 在提交本身中有一个,即 read-only 和 Git-only,在冻结的 Git-ified 形式中,我喜欢称之为 freeze-dried.

  • 你的work-tree里有一个,你可以看到并工作with/on。

许多版本控制系统使用相同的模式,每个文件只有这两个副本,但 Git 实际上更进一步。每个文件都有一个 third copy7 Git 不同地调用 index,或暂存区,或者——现在很少见——缓存。这第三个副本是freeze-dried格式,准备进入下一个提交,但与提交的副本不同,您可以替换任何时候,甚至完全删除它。

因此,当您检出提交时,Git 确实填充了 它的索引 (使用 freeze-dried 文件)和 你的work-tree(有可用的副本)。当您进行新的提交时,Git 实际上根本不会查看您的 work-tree。 Git 只是通过打包每个文件的已经 freeze-dried index 副本来进行新提交。

这导致了对 Git 索引的一个很好、简单的描述:该索引包含您提议的 next 提交。 这个描述其实有点简单了,因为索引还有其他作用。特别是,它在解决合并冲突时发挥了更大的作用。不过,我们不会在这里讨论那部分。简单的描述效果很好,可以开始使用 Git。

这意味着在编辑 work-tree 文件后,您需要告诉 Git 将 work-tree 副本复制回其索引。 git add 命令就是这样做的:它告诉 Git 制作这个文件或所有这些文件的索引副本,匹配 work-tree 副本 . Git 将在此时压缩和 de-duplicate work-tree 副本,比下一个 git commit 提前。这使 git commit 的工作变得容易得多:它根本不需要查看您的 work-tree。8

无论如何,这里要记住的是,在任何时候,每个“活动”文件都有 三个 个副本,在 Git 中:

  • frozen-forever提交HEAD复制;
  • 冻结-格式但可替换索引/暂存区副本;和
  • 你的 work-tree 副本。

Git 构建新的提交,不是从您的 work-tree 副本,而是从每个文件的索引副本。因此,索引包含 Git 在您 运行 git commit 时知道的所有文件,并且提交的快照是 索引中的任何内容 当时.


6有多种格式,称为loose objectspacked objects,loose对象实际上很容易直接读取。打包的对象有点难以阅读。但无论如何,Git保留自己在未来随时更改格式的权利,所以最好让Git阅读它们。

7因为这第三个副本是de-duplicated之前的副本,所以根本不是副本

8请注意 git commit 通常 运行 是一个快速的 git status,而 git status 不过看看你的 work-tree。


git status 的作用

在你运行git commit之前,你通常应该运行git status:

  • 状态命令首先告诉您当前的 b运行ch 名称——这是 git commit 更改的名称,以便它指向新的提交——通常还有一些我们将在此处跳过的其他有用的东西。

  • 接下来,git status 告诉您 暂存以提交 的文件。不过,这里真正要做的是将HEAD中的所有文件与索引中的所有文件进行比较。当这两个文件相同时,git status 什么都不说。当它们 不同 时,git status 宣布此文件已 暂存用于提交

  • 经过HEAD-vs-index比较git status 告诉您 未暂存提交 的文件。不过,这里真正要做的是比较索引中的所有文件与work-tree中的所有文件。当它们相同时,git status 什么都不说。当它们 不同 git status 宣布此文件 未暂存提交

  • 最后,git status 会告诉您 未跟踪的文件 。我们将把它留到另一节。

git status命令非常有用。经常使用!它将向您显示索引中的内容以及 work-tree 中的内容,这比您直接查看它们要实用得多。 not-staged-for-commit 文件可以被 git add 编辑,因此索引副本与 work-tree 副本匹配。 staged-for-commit 文件在新提交中将与当前提交中的不同。

未跟踪的文件和 .gitignore

因为您的work-tree是您的,您可以在这里创建Git一无所知的文件。也就是说,work-tree 中的新文件还没有 Git 的索引中,因为索引已从您选择的提交中填充.

Git 调用这样的文件 untracked。也就是说,未跟踪的文件很简单,就是存在于您的 work-tree 但不在 Git 的索引中的文件。 git status 命令抱怨这些文件,提醒您 git add 它们。 git add 命令有一个 en-masse “添加所有文件”模式,例如 git add .,它将通过将所有这些未跟踪的文件复制到 Git 的索引中来添加它们,所以他们在下一次提交中。

但有时,您知道 work-tree 文件根本不应该提交。要使 git status 停止抱怨它们,并使 git add not 自动 add 它们,您可以列出文件名或 .gitignore 文件中的模式。

如果文件已经在 Git 的索引中,则在此处列出文件无效。 也就是说,这些文件并不是真正的 忽略。将此文件命名为 .git-do-not-complain-about-these-files-and-do-not-automatically-add-them-with-any-en-masse-git-add-command 或类似名称可能更好,而不是 .gitignore。但是那个文件名是荒谬的,所以 .gitignore 它是。

如果一个文件进入了 Git 的索引,但它不应该在那里——不应该在新的提交中——你可以从 Git 的索引中删除该文件。 小心 因为执行此操作的命令默认从 Git 的索引 你的work-tree!这个命令是 git rm,你可以,例如,使用 git rm database.db 来删除重要内容的 accidentally-added 数据库......但是如果你这样做,Git 会删除 两份.

要仅删除索引副本,可以:

  • 移动或复制 work-tree 文件,使 Git 无法触碰它,或者
  • 使用git rm --cached,它告诉Git 只删除索引副本.

但请注意,如果您将文件放在较早的提交中,并从以后的提交中删除它,Git 现在会有不同的问题。每次你签出 old 提交时,Git 需要将文件放入 Git 的索引和你的 work-tree ... 和每次你从那个旧提交切换到 没有 的新提交时,Git 需要从两个 Git 中删除该文件index 和你的 work-tree.

最好不要一开始就不小心提交这些文件,这样你就不会遇到上面的问题。如果你确实点击了它,请记住有一个文件的副本——可能已经过时了,但仍然是一个副本——那个旧的提交中;您可以随时获得 that 复制回来,因为提交的文件是 read-only,并且与提交本身一样永久。

还剩下什么

我们根本没有涵盖 git pushgit fetch。我们没有触及 git merge,只是提到 Git 的索引在合并期间发挥了扩展的作用。我们没有提到 git pull,但我会说 git pull 确实是一个方便的命令:它表示 运行 git fetch,然后 运行第二个 Git 命令,通常是 git merge。我建议分别学习这两个命令,然后 运行 分别使用它们,至少一开始是这样。我们也没有涵盖 git rebase。但是这个答案已经足够长了!

关于 Git 有很多知识需要了解,但以上内容应该可以帮助您入门。最重要的几点是:

  • 每个 Git 存储库都是完整的(浅克隆除外)。您可以在本地 Git 完成所有工作。当您希望 Git 与其他 Git.

    交换提交时,您只需要获取和推送
  • 每个 Git 存储库都有 自己的 b运行ch 名称。 names 只是找到最后 次提交。这很重要(因为您还能如何找到最后一次提交?),但提交本身才是真正的关键。

  • 每个提交都包含“freeze-dried”(压缩和 de-duplicated)文件的完整快照,根据您当时的 Git 索引构建,或任何人 运行 git commit。每个提交还保存其 parent commit 的哈希 ID(或者,对于合并——我们没有在这里介绍——parents,复数)。

  • 您处理的文件实际上不在 Git 中,在您的 work-tree 中。你的 work-tree 和 Git 的索引都是 临时的; 只有提交本身(大部分)是永久的,只有提交本身才能得到 t 运行从一个 Git 转移到另一个

所以,也许为时已晚,简短回答:

How do I keep all this for myself and NOT push it to a remote location? Do I have everything set locally already?

is: 是的,一切都已经准备好了。要查看提交,请使用 git log。它默认从您的 current 提交开始并向后工作,但是:

git log --branches

它将从 所有 b运行ch names 开始并向后工作。这增加了一堆复杂性:git log 一次只能显示 一个提交 并且现在可能一次显示多个提交。它也值得尝试:

git log --all --decorate --oneline --graph

--all标志告诉Git使用所有引用(所有b运行ch名称、标签名称和其他名称我们这里没有介绍)。 --decorate 选项使 Git 显示哪些名称指向哪些提交。 --oneline 选项使 Git 以 one-line 紧凑形式显示每个提交,--graph 选项使 Git 绘制相同类型的 connection-graph我一直在上面画图,除了 Git 将较新的提交放在图表的 top,而不是向右。