-1

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

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

4

1 回答 1

1

您在这里真正需要的是一个好的 Git 教程,但取而代之的是,让我们试试这个:

  • Git 是关于提交的。Git 新手(甚至有一些经验的人)通常认为它与文件或分支有关,但实际上并非如此:它与提交有关。
  • 每个 Git 存储库都是一个完整的提交集合。也就是说,如果您有最后一次提交,那么您也有所有较早的提交。1
  • 提交是有编号的,但这些数字不是简单的计数:它们不会提交 #1、#2、#3 等等。相反,每个提交都有一个大而丑陋的哈希 ID号,表示为,例如675a4aaf3b226c0089108221b96559e0baae5de9这个数字在每个存储库副本中都是唯一的,因此您要么有提交,要么没有;当您进行新的提交时,它会获得一个新的、唯一的编号,这是其他提交所没有的。2 通过这种方式,可以连接两个Git:他们只是互相传递提交号,而不是整个提交,另一个Git可以轻松检查:我有这个提交吗?只需查找号码。
  • 每个提交都包含 Git 知道的每个文件的完整快照。提交不包含更改,尽管当您显示提交时,Git会显示更改。
  • 上述工作方式是每个提交还包含一些元数据,或有关提交本身的信息。这包括提交人的姓名和电子邮件地址、日期和时间戳等;但它还包括在此提交之前的提交的原始哈希 ID(提交编号) 。Git 将此称为提交的父级。
  • 一旦 Git 提交,其中的任何内容都无法更改,并且提交(大部分)是永久性的。3

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

 A <-B <-C

这里A代表第一个提交、B第二个和C第三个提交的哈希 ID。最后一次提交是提交C,是我们通常使用的提交。不过,由于C保存了较早提交的哈希 ID B,Git 可以轻松读取两个提交,并比较两个快照。无论有什么不同,Git 都会向您展示——当然,还有显示谁提交的元数据C等等。

这也意味着,从最后一次提交开始,Git 可以一直向后工作到第一次提交。也就是说,Git 以最后一次提交作为要显示的提交开始。然后 Git 显示它,然后 Git 移动到它的父级,并显示它,等等。在 Git 眼中,第一次提交“首先”的原因在于它没有父级:A没有父级,所以 Git 现在可以停止在这条链中倒退。


1所谓的浅克隆故意弱化了这个保证,但是只要你不使用或者类似的,就不会有浅克隆,也不需要担心这个。git clone --depth number

2鸽巢原理告诉我们,这个方案最终一定会失败。提交哈希 ID 如此之大的原因是为了使“最终”花费足够长的时间以至无关紧要。在实践中,不会发生碰撞,但理论上有人可以手工制作。此外,两个从未真正相遇的 Git 存储库可能会安全地发生哈希冲突。有关此的更多信息,请参阅新发现的 SHA-1 冲突如何影响 Git?

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

“大部分永久”中的“大部分”部分是因为无法以任何名称找到的提交或其他内部对象 -git fsck称为悬空不可访问- 最终将被 Git 的垃圾收集器清理,git gc. 出于篇幅原因,我们不会在这里详细介绍,但git commit --amend通常会导致旧的(坏的,现在被替换的)提交在以后被垃圾收集。


分支机构

这里缺少的是让 Git找到最后一次提交的原始哈希 ID 的简单方法。这就是分支名称的用武之地。像这样的分支名称master仅包含最后提交的哈希 ID:

A--B--C   <-- master

请注意,我已经用连接线替换了提交之间的内部箭头:因为提交不能更改,所以可以这样做,只要我们记住 Git 不能轻易前进,而只能后退。也就是说,A不知道哈希 IDB是什么,即使B它的哈希 ID 已经硬连线A。但是我们会保留分支名称中的箭头,这是有充分理由的:这些名称(或箭头)move

如果我们现在创建一个新的分支名称,例如develop,默认是让这个新的分支名称指向当前的提交C,如下所示:

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

现在我们还需要一件事:一种记住我们使用的名称的方法。这是特殊名称的HEAD来源。名称HEAD通常附加到分支名称之一:

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

这表明即使提交有两个名称C——并且所有三个提交都在两个分支上——我们使用的名称是master.

or(自git checkoutGit 2.23 起)git switch命令是您更改HEAD附加名称的方式。因此,如果我们git checkout developgit switch develop,我们会得到:

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

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

假设我们现在做了一个新的 commit D。在不深入讨论如何的情况下,我们只是假设我们已经做到了。Git 为这个新提交分配了一个新的唯一哈希 ID,并且新提交D指向C作为其父级的现有提交——因为我们在C创建D. 所以让我们画出那部分:

A--B--C
       \
        D

的最后一步git commit有点棘手:Git 将新提交的哈希 ID 写入附加HEAD. 所以现在的图是:

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

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

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

  • 显示提交D(并使用-p,显示与其父级相比的不同之处);然后DC
  • 后退一步C并表明这一点;然后
  • 后退一步B并表明

等等。Git 以 commit 开头,D因为名称HEAD附加到名称develop并且分支名称develop位于 commit D

假设我们运行git checkout masterorgit switch master来得到这个:

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

git log再次运行。这次HEAD附加到master,并master指向 commit C,所以git log会显示C,然后后退一步B显示 ,以此类推。提交D似乎已经消失了!但它没有:它就在那里,可以使用 name 找到develop

因此,这就是分支名称为我们所做的:每个分支名称都会找到该分支“上”的最后一个提交。较早的提交也在该分支上,即使它们在其他一些或多个分支上。许多提交在许多分支上,在典型的存储库中,第一次提交在每个分支上。4

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


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

5例如,git stash进行此类提交。Git 使用不是分支名称的名称来查找这些提交。不过,我们不会在这里详细介绍这些内容。


Git 的索引和你的工作树,或者,关于进行新提交的事情

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

我们介绍了提交的文件(Git 在每次提交中保存的快照中的文件)是只读的这一事实。从字面上看,它们是无法改变的。它们还以只有 Git 可以读取的压缩和去重格式存储。6 重复数据删除处理了这样一个事实,即大多数提交大多只是重用一些较早提交的文件。如果README.md未更改,则无需存储副本:每次提交都可以继续重复使用前一个。

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

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

  • 提交本身中有一个,它是只读的和仅 Git 的,采用冻结的 Git 化形式,我喜欢称之为freeze-dried

  • 您的工作树中有一个,您可以查看和使用/处理它。

许多版本控制系统使用相同的模式,每个文件只有这两个副本,但 Git 实际上走得更远。每个文件都有第三个副本7在 Git 调用的地方,不同的是indexstaging area,或者——现在很少见—— cache。这第三个副本是冻干格式,准备进入下一次提交,但与已提交的副本不同,您可以随时替换它,甚至完全删除它。

因此,当您检查提交时,Git 确实会填充它的索引(带有冻干文件)和您的工作树(带有可用副本)。当您进行新的提交时,Git 实际上根本不会查看您的工作树。Git 只是通过打包每个文件已经冻干的索引副本来进行新的提交。

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

这意味着在编辑工作树文件后,您需要告诉 Git 将该工作树副本复制回其索引中。该git add命令正是这样做的:它告诉 Git将该文件或所有这些文件的索引副本与工作树副本匹配。Git 将在此时压缩和删除工作树副本,远远早于下一个git commit. 这使git commit's 的工作变得容易得多:它根本不需要查看您的工作树。8

无论如何,这里要记住的是,在 Git 中,每个“活动”文件始终存在三个副本:

  • 永久冻结的提交HEAD副本;
  • 冻结格式但可替换的索引/暂存区副本;和
  • 你的工作树副本。

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


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

7因为这第三个副本是预先去重的,所以它根本不是真正的副本

8请注意,git commit通常会运行 quick git status,并且git status 确实会查看您的工作树。


git status做什么

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

  • status 命令首先告诉您当前的分支名称——这是git commit将要更改的名称,以便它指向新的提交——通常还有一些其他有用的东西,我们将在这里跳过。

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

  • 在 HEAD-vs-index 比较之后,git status告诉您未暂存的文件 commit。不过,这里真正要做的是将索引中的所有文件与工作树中的所有文件进行比较。当这些相同时,git status什么都不说。当它们 git status同时,宣布该文件未暂存为 commit

  • 最后,git status将告诉您有关未跟踪文件的信息。我们将把它留到另一部分。

git status命令非常有用。经常使用它!它将以比直接查看它们更有用的方式向您显示索引中的内容和工作树中的内容。可以对未暂存的提交文件进行git add-ed,以便索引副本与工作树副本匹配。新提交中的暂存提交文件将不同于当前提交中的文件。

未跟踪的文件和.gitignore

因为你的工作树是的,所以你可以在这里创建 Git 一无所知的文件。也就是说,你的工作树中的一个新文件还没有Git 的索引中,因为索引是在之前从你选择的提交中填充的。

Git 将此类文件称为untracked。也就是说,一个未跟踪的文件是一个简单的文件,它存在于你的工作树中,但不在 Git 的索引中。该git status命令会抱怨这些文件,以提醒您注意git add它们。该git add命令有一个整体的“添加所有文件”模式,例如,git add .它将通过将所有这些未跟踪的文件复制到 Git 的索引中来添加它们,以便它们在下一次提交中。

但是,有时,您知道根本不应该提交某些工作树文件。要git status停止抱怨它们,并且git add 自动添加它们,您可以在文件中列出文件的名称或模式.gitignore

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

如果一个文件进入了 Git 的索引,并且它不应该在那里——不应该在新的提交中——你可以从 Git 的索引中删除该文件。 请小心,因为执行此操作的命令默认为从Git的索引工作树中删除文件!这个命令是git rm,你可能,例如,git rm database.db用来删除意外添加的重要资料的数据库......但如果你这样做,Git 会删除两个副本

要仅删除索引副本,请执行以下任一操作:

  • 移动或复制工作树文件,这样 Git 就无法得到它肮脏的爪子,或者
  • use git rm --cached,它告诉 Git只删除索引副本

但是请注意,如果您将文件放在某个较早的提交中,并将其从未来的提交中删除,那么 Git 现在会遇到不同的问题。每次检查提交时,Git 都需要将文件放入 Git 的索引和工作树中……每次从旧提交切换到没有文件的新提交时,Git 都会需要从 Git 的索引和工作树中删除该文件。

最好不要一开始就意外提交这些文件,以免遇到上述问题。如果您确实点击了它,请记住该文件的副本 - 可能已经过时,但仍然有副本 - <em>在那个旧的提交中;您可以随时取回副本,因为提交的文件是只读的,并且与提交本身一样永久。

还剩下什么

git push我们根本没有涵盖git fetchgit merge除了提到 Git 的索引在合并过程中发挥了扩展的作用外,我们还没有涉及到 。我们没有提到git pull,但我会说这git pull确实是一个方便的命令:它意味着run git fetch,然后运行第二个 Git 命令,通常是git merge。我建议分别学习这两个命令,然后分别运行它们,至少一开始是这样。我们也没有涉及git rebase。但是这个答案已经足够长了!

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

  • 每个 Git 存储库都是完整的(浅层克隆除外)。您可以在本地 Git 中完成所有工作。当您希望您的 Git 与其他 Git 交换提交时,您只需要获取和推送。

  • 每个 Git 存储库都有自己的分支名称。名称只是定位最后一次提交。这很重要(因为否则您将如何找到最后一次提交?),但提交本身才是真正的关键。

  • 每个提交都包含“冻干”(压缩和重复数据删除)文件的完整快照,这些文件是根据您或任何人运行时的 Git 索引构建的git commit。每个提交还保存其父提交的哈希 ID(或者,对于合并——我们在这里没有介绍——父母,复数)。

  • 您在工作树中处理实际上不在 Git 中的文件你的工作树和 Git 的索引都是临时的;只有提交本身(大部分)是永久的,而且只有提交本身从一个 Git 转移到另一个 Git。

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

我如何为自己保留所有这些而不是将其推送到远程位置?我是否已经在本地设置了所有内容?

是:是的,一切都已经设置好了。要查看提交,请使用git log. 它默认从您当前的提交开始并向后工作,但是:

git log --branches

它将从所有分支名称开始并向后工作。这增加了一堆复杂性:git log一次只能显示一个提交,现在可能一次显示多个提交。它也值得尝试:

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

--all标志告诉 Git 使用所有引用(所有分支名称、标签名称和我们在此未涉及的其他名称)。该--decorate选项使 Git 显示哪些名称指向哪些提交。该--oneline选项使 Git 以单行紧凑的形式显示每个提交,并且该--graph选项使 Git 绘制与我在上面绘制的相同类型的连接图,除了 Git 将较新的提交放在图的顶部,而不是的向右。

于 2020-08-24T20:47:35.730 回答