GitHub 的新手:你的分支和 'origin/master' 已经分叉,分别有 1 和 2 个不同的提交。我应该怎么办?
New to GitHub: Your branch and 'origin/master' have diverged, and have 1 and 2 different commits each, respectively. What should I do?
我是 GitHub 的新手(我坚持添加、提交和推送,没有尝试过新分支),今天试图推送一些更改。然而,我提交了一些文件并意识到我搞砸了一些东西并试图通过 运行:
取消提交
git reset --mixed HEAD~;
我又尝试了几次推送和重置。我不太确定我做了什么,但在检查 git 状态时我最终来到这里:
Your branch and 'origin/master' have diverged,
and have 1 and 2 different commits each, respectively.
当我尝试推送时,它显示:
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
所以我认为我现在落后了很多,因为有一些文件在最后两次提交中已经被跟踪,所以 git 状态现在表示未被跟踪。此外,我不想丢失我在本地计算机上取得的任何进展。我怎样才能快进并推动我想做的改变,理想情况下不会丢失任何过去的提交或当前的进展?
So I think I'm now behind quite a bit because there are some files that have been tracked for the last two commits or so that git status is now saying are untracked. Furthermore, I don't want to lose any of my progress that I've made locally on my computer. How can I fast forward and push the changes I'd like to make, ideally without losing any past commits or current progress?
让我们将它们分成不同的部分,这样您就可以正确地理解发生了什么。我们这里关心的组件是:
- 存储库:您的计算机上有一个,GitHub 的有一个。
- 在每个存储库中:
- 提交
- b运行ch 名称
提交确实得到了共享,而 b运行ch 名称却没有——它们以更高级的方式处理。但是,当您 Git 在 GitHub 调用 Git 时,提交只会在特定的 连接点 共享;你的 Git 和他们的 Git 然后就这些 b运行ch 名称和提交进行一些对话。让我们把它留到以后,先关注你的 Git 存储库中的内容:提交和名称。
名字
通常我先从提交开始,但这次,我们只从名称开始。除了 b运行ch 名称之外,还有更多种类的名称,我们稍后会回过头来讨论,但现在,让我们只关心 b[=971= 是什么]ch name is and does.
A b运行ch 名字只是一个名字——一系列字母,最好是 and/or digits,有一些规则阻止你使用像 br..an..ch
作为 b运行ch 名称但允许 bra.nch
。这个 b运行ch 名称的主要功能是保存 one 提交哈希 ID。这是 最新 提交的哈希 ID。因此,如果没有 提交 ,名称实际上对您没有任何好处。
提交
提交是 Git 真正的核心功能。几乎一切都与提交有关。永远提交文件的保存版本——或者至少只要这些提交继续存在——但重要的是要了解如何提交如何做到这一点,以及如何Git 找到 一个提交。
让我们从一个简单的想法开始这部分:每个提交都有编号。然而,这些数字并不是简单的计数。它们不是提交 1、2 和 3。相反,每个数字都是一个丑陋的大 哈希 ID。它 看起来 完全 运行dom(尽管实际上它完全是 non-random)。最新的提交(其编号为 4a0fcf9f760c9774be77f51e1e88a7499b53d2e2
)与其之前的提交没有明显的联系。
要找到 一个提交,您需要知道它的编号。但是这些数字看起来 运行dom,又大又丑,人类无法记住。这就是我们有 b运行ch 名字的原因:他们记得 last 提交的编号。但是等等:只知道 last 提交有什么好处?好吧,让我们看看每个提交里面都有什么。
每个提交都有两部分:它有它的数据,它是所有文件的完整快照。我们稍后会回到这一点。然后它有一些元数据,或者关于提交本身的信息。在此元数据中,您将找到进行提交的人的姓名,他们何时 进行提交,以及为什么 他们进行提交:他们日志消息。但是 Git 也会存储并找到 Git 本身想要的另一条信息,那就是这个提交的 parent[=630= 的编号——哈希 ID ]提交。
任何提交的父项都是在之前的提交。所以对于普通的 single-parent 提交——这往往是其中的大部分——这意味着 Git 可以从 last 提交开始,然后简单地向后工作。 Git就是这样做的,我们可以这样画:
A <-B <-C <--master
这里我们有一个简单的存储库,里面只有三个提交,都在一个名为 master
的 b运行ch 上。 name master
保存最后一次提交的哈希 ID——我们称之为 C
,尽管它有一些丑陋的大哈希 ID——并且那个提交 C
保存早期提交的哈希 ID B
。所以Git可以用name找C
,然后用C
找B
.
同时B
保存了较早提交A
的哈希ID,所以找到B
,Git可以找到A
。 A
是任何人所做的第一个提交,所以它没有父项。这让 Git 停止向后工作。
提交和 b运行ches
这里还有一个有趣的问题,一旦我们有多个 b运行ch 就会出现这种情况。假设我们的存储库中最多有八次提交:
...--G--H <-- master
我已经不再费心在提交之间绘制向后箭头了。这没关系,因为所有提交都必须向后指向,并且提交还有另一件关键的事情:一旦你提交,其中的任何内容都不会改变。1 因此 backwards-pointing 箭头被冻结,无法添加 forwards-pointing 箭头。 b运行ch 名称 并非如此:重新ember,master
用于包含提交的真实哈希 ID C
;现在它包含提交的实际哈希 ID H
.
如果我们现在创建一个 new b运行ch 名称,新名称将 也指向提交 H
。2 让我们画一下:
...--G--H <-- develop, master
我们实际使用的是哪个名称? Git 为我们提供了答案:我们应该在 b运行ch 上附加特殊名称 HEAD
,全部大写,3我们想要使用。所以:
...--G--H <-- develop, master (HEAD)
表示 我们正在使用名称 master
,select 提交 H
,而:
...--G--H <-- develop (HEAD), master
表示 我们正在使用名称 develop
,select 提交 H
.
1它被冻结的原因是提交的编号——它的哈希 ID——是通过计算数据和元数据中所有位的安全哈希来构建的。这意味着实际上不可能更改提交。如果你拿出一个,把它变成普通数据,改变一点,再写回去,你会得到一个新的不同的commit。原始提交保留在存储库中;您只是添加了一个新的、不同的提交。
2实际上,我们可以选择 任何 现有提交来命名。但是,我们必须 选择一些现有的提交:不允许使用不指向某些现有提交的 b运行ch 名称。
3在某些系统上,您可以输入小写的 head
并让它工作。这是一个坏习惯,因为:
- 它不适用于所有系统,并且
- 当您开始使用
git worktree
时它会中断。
如果您不喜欢输入单词 HEAD
,请考虑使用 one-character 同义词 @
。
进行新的提交,第 1 部分
HEAD
非常重要的一个地方是我们何时进行 new 提交。假设我们有:
...--G--H <-- develop (HEAD), master
无论哪种方式,我们都使用 提交H
。但是,如果我们更改一些文件并 git add
它们和 git commit
,这会告诉 Git 进行 new 提交。让我们调用新提交 I
并绘制它,如下所示:
...--G--H
\
I
请注意,新提交 I
的 父级 是现有提交 H
。那是因为我们从提交 H
.
开始
b运行ch 名称发生了什么变化?答案是:Git自动更新HEAD
附上。由于 HEAD
曾经并且仍然附加到 develop
,因此该名称现在指向新提交 I
:
...--G--H <-- master
\
I <-- develop (HEAD)
如果我们现在 git checkout master
返回到 master
,Git 将 return 我们到现有提交 H
,附加特殊名称 HEAD
到 master
,然后给我们:
...--G--H <-- master (HEAD)
\
I <-- develop
提交是 read-only,那么文件是如何工作的?
我们之前提到,提交是 所有 文件的 frozen-in-time 快照。为了使这项工作顺利进行,Git 以特殊的 read-only、Git-only、冻结和 de-duplicated 格式存储每个文件。只有 Git 自己才能 使用 这些文件。因此,当我们选择要使用的提交时,Git 将文件从提交 复制到工作区。那个工作区,里面有普通的日常文件,是你的 work-tree 或者 working tree.
de-duplication 意味着即使每个提交都有 每个 文件的完整快照,这些快照中的大多数只是 re-using 现有文件。也就是说,当我们提交 I
时,我们可能更改了一两个文件,而其他所有文件保持不变。所以提交 I
和提交 H
实际上 共享 他们的大部分文件。在某种程度上,他们可能也与早期提交共享大部分 those。 (事实上 ,如果您将文件 改回 到它在某个早期提交中的方式,新文件会自动与旧提交共享。)
这是每个提交中的数据:所有 文件的完整冻结快照,或者更确切地说,是您告诉 Git 放入的所有文件的快照那张快照。那么这些是哪些文件?
进行新的提交,第 2 部分
每个提交都包含这些 frozen-format 文件,这些文件需要扩展到您的 work-tree 中。那么,您可能会假设 git commit
获取了 work-tree 中的任何内容并提交了它。但实际上 Git 并不是这样工作的。
除了原始文件、in-commit、冻结文件和在-work-tree、日常文件之外,Git 还保留了一个中间副本每个文件。4 这个额外的副本位于一个非常重要的区域,或者最初命名如此糟糕的区域,以至于它有三个名称。 Git 称其为 index,或 staging area,或者有时——现在很少——cache。我将在这里使用术语 index 但请记住 staging 指的是 t这些额外的副本。
每个文件的索引副本都是冻结的格式,准备进入下一次提交。所以这意味着考虑 Git 索引的一个好方法是它包含 提议的下一次提交 。 git add
命令告诉 Git:复制 work-tree 文件的普通格式副本到索引中,或返回到索引中,替换任何以前的副本。 这也以冻结格式准备它(de-duplicating 它也是)以便它为下一个 git commit
.
做好准备
当您执行 运行 git commit
时,Git 会收集元数据所需的任何额外信息——例如您的姓名和日志消息——然后构建一个新的提交。然后它写出索引 中的任何内容然后 作为快照,添加元数据——包括当前提交作为新提交的父提交——并进行新提交,然后更新任何 b 运行通道名称 HEAD
附加到。
如果您喜欢使用 git commit -a
,请注意这只会使 git commit
更新索引 中已有的文件 。它几乎等同于 运行ning git add -u
(更新已知文件)后跟 git commit
.5
您不能直接看到索引,6 但是 git status
会隐式地告诉您索引中的内容。 git status
的工作方式也很简单:
您有一个当前提交。那就是你的 b运行ch 名称(通过查看 HEAD
找到)所说的 last 提交。该提交中有一堆文件,采用 Git 特殊的内部冻结格式。 Git 也称此为 HEAD
提交。
Git有它的索引。其中有一堆文件,采用 Git 的特殊冻结格式——但与提交中的文件不同,它们可以 替换 为新副本。
而且,您有自己的 work-tree,在这里您可以做任何您想做的事情——包括创建 all-new 文件。
git status
命令进行两次单独的比较:
首先,它将 HEAD
提交中的所有文件与索引中的所有文件进行比较。对于每个 相同 的文件,它什么也没说。对于每个不同的文件——包括新的或消失的——它说这个文件是暂存以提交。
然后 git status
将 Git 索引中的所有文件与您 work-tree 中的文件进行比较。对于每个 相同 的文件(从冻结形式展开后),它什么也没说。对于每个 不同 的文件,它表示此文件 未暂存提交 。
这意味着您可以查看索引中可以更新的内容,而不必查看索引中相同的每个文件作为
中的副本
4从技术上讲,索引中的内容不是文件的副本,而是参考 到 frozen-format Git 内部 blob 对象 。但是您通常不需要知道这一点——只有当您开始使用 git ls-files --stage
和 git update-index
来处理 Git 的 low-level 索引时才重要。
5这里的主要区别在于,如果进行新提交失败,git commit -a
方法会回滚索引.使用 git add -u
是一个单独的步骤,因此如果添加成功但提交失败,索引仍会更新。还有很多 more-subtle 区别,但我们将忽略这里所有棘手的极端情况。重要的一点是 Git 从一个索引进行提交,通常只有一个索引—— 索引——其他所有内容都从那个开始。
6实际上,您可以查看索引中的内容:运行 git ls-files --stage
。请注意,这会将大量输出转储到一个大存储库中!此命令不是您通常使用的命令:它是供 Git 程序在内部使用,而不是供用户使用。
跟踪和未跟踪的文件
现在您知道 Git 从其索引进行提交,您终于可以正确理解跟踪和未跟踪的文件了。 跟踪文件的定义非常简单,但仍然很复杂:跟踪文件是指现在在Git索引中的文件。
您可以随时将文件添加到索引中:git 添加<em>newfile</em>
。现在跟踪该文件。您也可以随时从索引中删除 文件:git rm --cached <em>oldfile</em>
. --cached
阻止 git rm
删除您的 work-tree 副本,这样您仍然可以 查看 该文件,但它不再位于 Git 的索引:该文件现在未被跟踪。
但是记住:git checkout <em>b运行ch</em>
告诉 Git 到 填写它的索引和你的work-tree 来自一些现有的提交!所以 Git 将自行更新其索引。如果 Git 的索引和你的 work-tree 中现在有文件,而你 git checkout
提交 没有 有这些文件, Git 将 从其索引和您的 work-tree 中删除 那些文件,以便您可以看到保存在该提交中的内容。
未跟踪的文件 是您 work-tree 中但不在 Git 的索引中的任何文件。当你有这样一个文件——一个不在 Git 的索引中——并且 git checkout
其他一些提交也没有那个文件时,那个文件继续不在 Git' s 索引,因此继续未被跟踪。
(当你有一个未跟踪的文件时,这里会发生一个偷偷摸摸的情况,然后要求切换到 确实 有那个文件的提交。我们不会在这里担心,但您可能会看到这可能是个问题。)
摆脱提交
提交实际上很难摆脱(除了删除整个 .git
目录,这会丢失 一切 并且很少是一个好主意)。那是因为 Git 是为了 添加新提交 而构建的,而不是删除它们。但实际上您可以摆脱提交。
假设一些 b运行ch 名称和一些提交系列:
...--G--H <-- master (HEAD)
现在进一步假设我们可以说服 Git name master
应该持有,而不是提交 H
的哈希 ID,但是 commit G
相反,像这样:
H
/
...--G <-- master (HEAD)
请注意,提交 H
实际上仍然存在于存储库中。但是 Git 显示 我们从 ID 存储在 name 中的提交开始提交,例如 master
。该名称现在表示 commit G
是最新的 commit。提交 G
指向一些较早的提交(可能是 F
)。因此,如果我们询问 Git 关于此存储库中的提交,我们将不会再 看到 提交 H
。
(我们可以,当我们绘制这些时,将“丢弃的”提交向上或向下推以将它们移开。我有点受 Whosebug 文本约定的限制,但是如果您将它们绘制在纸上或一个白板,随意以任何方式绘制它们,包括从 b运行ch 名称到提交的长长的弯曲箭头。)
请注意,这仅适用于“尾部”提交。也就是说,假设我们有:
...--G--H <-- master (HEAD)
\
I <-- develop
如果我们强制名称 master
指向提交 G
,那么,提交 I
仍然指向提交 H
,所以我们得到的是:
...--G <-- master (HEAD)
\
H--I <-- develop
也就是说,现在看起来我们在 b运行ch develop
上提交了 H
。尽管如此,b运行ch master
现在在提交 G
时结束,所以我们确实做了 something.
这就是 git reset
所做的。当你 运行:
git reset --mixed HEAD~
你告诉你的 Git:找到从 HEAD
退一步的提交——从当前 b运行ch 的最后一次提交退一步。然后,强制使用当前的 b运行ch 名称来标识该提交。 如果您有:
...--G--H--I <-- master
你这样做一次,你最终得到:
...--G--H <-- master
\
I
如果您再次这样做,名称 master
将指向提交 G
,并且 H-I
将悬空。默认情况下,它们会在您的存储库中保留一段时间——用户存储库至少有 30 天的时间来取回这些提交。 (这里的机制是Git调用reflogs的东西,但我们就不细说了。)
git reset
的 --mixed
参数告诉 Git 在移动东西时 保持 work-tree 不变 。因此 work-tree 中的文件副本将被单独保留。使用 --hard
,git reset
也会调整这些。使用 --soft
,git reset
单独保留 Git 的索引,但是使用 --mixed
,Git 清空旧索引并从提交中填充它 select.
这——替换索引,但单独保留 work-tree——很容易导致 untracked-files 的情况。特别是,假设提交 I
添加了一个不在提交 H
中的 new 文件。然后上面的 reset
从 Git 的索引中删除新文件,将新文件留在 work-tree 中。该文件现在在您的 work-tree 中,但不在 Git 的索引中,这就是未跟踪文件的定义。
请记住,所有 committed 文件在这些提交中都是安全的,只要您仍然可以 find 那些提交。 通过像提交 I
这样的提交很难找到,你已经设置好了,所以你可能无法获得 those轻松恢复文件的版本。但是 git log
向您展示的任何提交,好吧,那些 提交很容易找到。 (我们跳过了使用 Git 的 detached HEAD 模式来查看历史提交的想法,这样就不必涵盖该模式,但仅此而已查看历史版本的方式。)
添加更多 Git 个存储库
现在你知道怎么做了ommits 和 b运行ch 名称,在 你的 存储库中工作——包括添加新提交,以及重置一些提交——是时候添加 GitHub Git加入组合。
要让您的 Git 调用其他 Git,您需要有一个 URL——类似于 ssh://git@github.com/...
或 https://github.com/...
。您的 Git 将为您 保存 这个 URL,使用一个简短易记的名字。 Git 称其为 远程 。许多 Git 存储库只有一个远程存储库,称为 origin
,我假设您的存储库也是如此。
要让您的 Git 与另一个 Git 连接,您需要 运行 三个命令之一:git fetch
、git push
和 git pull
。 git pull
命令只是一个方便的包装器,首先是 运行s git fetch
,然后是第二个 Git 命令,这是最好的——好吧,I 认为最好——单独学习 git fetch
。所以这只给了我们两个让 Gits 互相交谈的命令。
两条命令本身比较简单:
git fetch
让你的 Git 打电话给他们的 Git,然后问他们有什么,你没有。他们列出了他们的 b运行ch 名称(和其他名称)和他们的提交哈希 ID。您的 Git 可以立即判断您是否有这些提交,因为哈希 ID 在 every Git 存储库中是相同的(再次参见脚注 1)。如果您没有提交,您的 Git 会要求他们的 Git 将它们发送过来。他们这样做了,现在你也有提交了。
现在您已经拥有他们拥有的所有提交(加上您自己未共享的任何提交),您的 Git 创建或更新您的 origin/*
名称,至 记住他们的他们的b运行ch名字。您的每个 origin/*
名称都是一个 remote-tracking 名称。7 这些只是您的 Git在你 运行 git fetch
.
时,他们在 b运行ch 名称中有什么哈希 ID 的记忆
如果他们不改变他们的名字运行ch 名字(曾经,或经常),你的git fetch
将设置你的remote-tracking 每次都正确命名。如果他们确实经常更改它们,您需要经常 运行 git fetch
以获取任何新名称和不同的提交哈希 ID。
你可以 运行 git fetch
就像这样,根本没有参数。
git push
让你的 Git 调用他们的 Git 然后 给他们新的提交 如果需要的话。这比 git fetch
稍微复杂一点,因为他们 记住 任何新的提交,他们将不得不更新 他们的 b 运行ch 名称!他们没有等同于 remote-tracking 的名字。
与 git fetch
一样,您的 Git 列出了提交哈希 ID。他们检查自己是否有这些 ID。如果没有,他们会让你的 Git 发送这些提交(如果需要的话还有他们的文件——这里有很多花哨的东西来避免发送他们已经有副本的文件)。和以前一样,每个 Git 对相同的提交使用 相同的 哈希 ID 的事实使这很容易。
然后,一旦他们的 Git 有任何提交要求,您的 Git 发送一个或多个请求:请,如果没问题,请设置您的 b运行ch name ______(填一个名字)到______(填一个hash ID)。是否服从这种礼貌的要求,由他们自己决定。或者,你的Git可以发一个命令:将你的名字_____设置为_____!是否服从还是由他们决定。
git push
命令需要8,你把名字origin
,因为很久以前,有人说语法是git push <em>remote b运行ch</em>
所以你必须先把 origin
放在那里b运行ch 名称。然后,git push
需要 b运行ch 的名称。这告诉你的 Git 要发送哪个提交——你的 Git 像往常一样从你的 b运行ch 名称中找到 last 提交——并且填补了两个空白。即我们必须将b运行ch name和hash ID都填入两个空格中,无论是礼貌的请求还是强行的命令。你的 Git 从你在这里输入的名字中得到这两个。9
他们告诉我们 Git 他们是否服从了命令。如果是这样,我们的 Git 更新我们的一个 remote-tracking 名称对应于他们的 b运行ch 名称。也就是说,如果我们让他们更新他们的 master
,我们的 Git 会更新存储在 origin/master
中的内存。由于我们没有找到他们的任何 other b运行ch 名称,我们其他 origin/*
名称中的 none 得到更新。
7Git 调用这些东西 remote-tracking b运行ch names, 但我发现这里的单词 b运行ch 使事情变得更加混乱,而不是更少;所以现在我把它放在一边,只称它们为 remote-tracking names.
8你ca 进行设置,使 git push
默认为 推送当前 b运行ch,然后你可以忽略它—Git 将计算出正确的遥控器和当前的 b运行ch——但我喜欢显示显式版本。
9还有一些额外的选项可以让你变得更漂亮。例如,如果您愿意,可以从您的名字 grandpa-simpson
推送到他们的名字 onion-on-my-belt
。可以在每一侧使用完全不同的名称。但是不要在没有强烈需求的情况下这样做:它很快就会变得非常混乱。
Fast-forwards 和 non-fast-forwards
让我们想象一下,现在,我们是一个正在接收 git push
的 Git。其他一些 Git 打电话给我们,问我们是否提交 a123456
。我们没有,所以他们给了我们。 a123456
有父 9876543
,我们确实有,所以这是我们唯一需要的提交。现在他们说:拜托,如果可以的话,把你的master
设置成a123456
。
让我们画出我们所拥有的:
...--G--H <-- master
假设提交H
的哈希ID是9876543
。然后新的提交 a123456
显然是一个新的提交 I
只是 添加到 我们现有的 master
,我们可以像这样把它:
...--G--H--I <-- master
但是如果父 9876543
是 不 提交 H
怎么办?如果提交 G
怎么办?也就是说,我们有:
...--G--H <-- master
他们给了我们:
...--G--H
\
I
他们现在要求我们设置 master
以记住提交 I
?如果我们这样做,我们将失去 我们的 提交 H
。我们最终会得到:
H
/
...--G--I <-- master
我们将无法再找到提交 H
。所以我们会对礼貌的请求说 no,因为这个操作不仅 add 提交给我们的 master
,它还丢弃 一个提交。
如果他们向我们发送强制命令——将你的 master
设置为 a123456
!——我们可能会服从它,并放弃提交 H
。如果 他们 没有保留提交 H
的副本,它可能会很快消失。 Server-side 存储库通常没有 reflogs,几乎可以立即删除被遗弃/悬空的提交。
你自己的情况
我们可以画出你的情况,你的master
是你的origin/master
的ahead 1, behind 2
——你的Git对[=431=的记忆]他们的 Git的master
。它可能看起来像这样:
I--J <-- origin/master
/
...--G--H
\
K <-- master (HEAD)
如果愿意,您可以使用 git push --force origin master
向他们发送您的提交 K
并告诉他们 放弃 他们的 I-J
提交.但是,如果您想保留这些提交,请不要这样做。
如果愿意,您可以放弃自己的提交 K
:
git reset [options] origin/master
会给你:
I--J <-- master (HEAD), origin/master
/
...--G--H
\
K [abandoned]
您的提交 K
会保留一段时间,但要找到它会有点困难。如果你最终不想要它,那可能很好。
您可以使用 git merge
合并提交 H
与提交 K
相比的更改,以及提交 H
与提交 [=] 相比的更改208=],进行新的提交。您可以使用 git rebase
来 复制 现有提交 K
到添加到提交 J
上的新的不同提交。您 可以 做很多事情。每个都有一组不同的结果提交。记住提交是什么以及为您做什么,以及 b运行ch 名称如何找到提交,并决定您想要哪些提交,哪些您希望假装从未发生过——用 git reset
[= 删除811=] git push --force
—然后按照您想要的方式在您自己的本地存储库中进行设置。然后使用 git push
,有或没有 --force
,将新提交发送到 GitHub Git,并让他们设置 他们的 b运行ch 指向新提交的名称,以匹配您在 your Git.
中的设置
我是 GitHub 的新手(我坚持添加、提交和推送,没有尝试过新分支),今天试图推送一些更改。然而,我提交了一些文件并意识到我搞砸了一些东西并试图通过 运行:
取消提交git reset --mixed HEAD~;
我又尝试了几次推送和重置。我不太确定我做了什么,但在检查 git 状态时我最终来到这里:
Your branch and 'origin/master' have diverged,
and have 1 and 2 different commits each, respectively.
当我尝试推送时,它显示:
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
所以我认为我现在落后了很多,因为有一些文件在最后两次提交中已经被跟踪,所以 git 状态现在表示未被跟踪。此外,我不想丢失我在本地计算机上取得的任何进展。我怎样才能快进并推动我想做的改变,理想情况下不会丢失任何过去的提交或当前的进展?
So I think I'm now behind quite a bit because there are some files that have been tracked for the last two commits or so that git status is now saying are untracked. Furthermore, I don't want to lose any of my progress that I've made locally on my computer. How can I fast forward and push the changes I'd like to make, ideally without losing any past commits or current progress?
让我们将它们分成不同的部分,这样您就可以正确地理解发生了什么。我们这里关心的组件是:
- 存储库:您的计算机上有一个,GitHub 的有一个。
- 在每个存储库中:
- 提交
- b运行ch 名称
提交确实得到了共享,而 b运行ch 名称却没有——它们以更高级的方式处理。但是,当您 Git 在 GitHub 调用 Git 时,提交只会在特定的 连接点 共享;你的 Git 和他们的 Git 然后就这些 b运行ch 名称和提交进行一些对话。让我们把它留到以后,先关注你的 Git 存储库中的内容:提交和名称。
名字
通常我先从提交开始,但这次,我们只从名称开始。除了 b运行ch 名称之外,还有更多种类的名称,我们稍后会回过头来讨论,但现在,让我们只关心 b[=971= 是什么]ch name is and does.
A b运行ch 名字只是一个名字——一系列字母,最好是 and/or digits,有一些规则阻止你使用像 br..an..ch
作为 b运行ch 名称但允许 bra.nch
。这个 b运行ch 名称的主要功能是保存 one 提交哈希 ID。这是 最新 提交的哈希 ID。因此,如果没有 提交 ,名称实际上对您没有任何好处。
提交
提交是 Git 真正的核心功能。几乎一切都与提交有关。永远提交文件的保存版本——或者至少只要这些提交继续存在——但重要的是要了解如何提交如何做到这一点,以及如何Git 找到 一个提交。
让我们从一个简单的想法开始这部分:每个提交都有编号。然而,这些数字并不是简单的计数。它们不是提交 1、2 和 3。相反,每个数字都是一个丑陋的大 哈希 ID。它 看起来 完全 运行dom(尽管实际上它完全是 non-random)。最新的提交(其编号为 4a0fcf9f760c9774be77f51e1e88a7499b53d2e2
)与其之前的提交没有明显的联系。
要找到 一个提交,您需要知道它的编号。但是这些数字看起来 运行dom,又大又丑,人类无法记住。这就是我们有 b运行ch 名字的原因:他们记得 last 提交的编号。但是等等:只知道 last 提交有什么好处?好吧,让我们看看每个提交里面都有什么。
每个提交都有两部分:它有它的数据,它是所有文件的完整快照。我们稍后会回到这一点。然后它有一些元数据,或者关于提交本身的信息。在此元数据中,您将找到进行提交的人的姓名,他们何时 进行提交,以及为什么 他们进行提交:他们日志消息。但是 Git 也会存储并找到 Git 本身想要的另一条信息,那就是这个提交的 parent[=630= 的编号——哈希 ID ]提交。
任何提交的父项都是在之前的提交。所以对于普通的 single-parent 提交——这往往是其中的大部分——这意味着 Git 可以从 last 提交开始,然后简单地向后工作。 Git就是这样做的,我们可以这样画:
A <-B <-C <--master
这里我们有一个简单的存储库,里面只有三个提交,都在一个名为 master
的 b运行ch 上。 name master
保存最后一次提交的哈希 ID——我们称之为 C
,尽管它有一些丑陋的大哈希 ID——并且那个提交 C
保存早期提交的哈希 ID B
。所以Git可以用name找C
,然后用C
找B
.
同时B
保存了较早提交A
的哈希ID,所以找到B
,Git可以找到A
。 A
是任何人所做的第一个提交,所以它没有父项。这让 Git 停止向后工作。
提交和 b运行ches
这里还有一个有趣的问题,一旦我们有多个 b运行ch 就会出现这种情况。假设我们的存储库中最多有八次提交:
...--G--H <-- master
我已经不再费心在提交之间绘制向后箭头了。这没关系,因为所有提交都必须向后指向,并且提交还有另一件关键的事情:一旦你提交,其中的任何内容都不会改变。1 因此 backwards-pointing 箭头被冻结,无法添加 forwards-pointing 箭头。 b运行ch 名称 并非如此:重新ember,master
用于包含提交的真实哈希 ID C
;现在它包含提交的实际哈希 ID H
.
如果我们现在创建一个 new b运行ch 名称,新名称将 也指向提交 H
。2 让我们画一下:
...--G--H <-- develop, master
我们实际使用的是哪个名称? Git 为我们提供了答案:我们应该在 b运行ch 上附加特殊名称 HEAD
,全部大写,3我们想要使用。所以:
...--G--H <-- develop, master (HEAD)
表示 我们正在使用名称 master
,select 提交 H
,而:
...--G--H <-- develop (HEAD), master
表示 我们正在使用名称 develop
,select 提交 H
.
1它被冻结的原因是提交的编号——它的哈希 ID——是通过计算数据和元数据中所有位的安全哈希来构建的。这意味着实际上不可能更改提交。如果你拿出一个,把它变成普通数据,改变一点,再写回去,你会得到一个新的不同的commit。原始提交保留在存储库中;您只是添加了一个新的、不同的提交。
2实际上,我们可以选择 任何 现有提交来命名。但是,我们必须 选择一些现有的提交:不允许使用不指向某些现有提交的 b运行ch 名称。
3在某些系统上,您可以输入小写的 head
并让它工作。这是一个坏习惯,因为:
- 它不适用于所有系统,并且
- 当您开始使用
git worktree
时它会中断。
如果您不喜欢输入单词 HEAD
,请考虑使用 one-character 同义词 @
。
进行新的提交,第 1 部分
HEAD
非常重要的一个地方是我们何时进行 new 提交。假设我们有:
...--G--H <-- develop (HEAD), master
无论哪种方式,我们都使用 提交H
。但是,如果我们更改一些文件并 git add
它们和 git commit
,这会告诉 Git 进行 new 提交。让我们调用新提交 I
并绘制它,如下所示:
...--G--H
\
I
请注意,新提交 I
的 父级 是现有提交 H
。那是因为我们从提交 H
.
b运行ch 名称发生了什么变化?答案是:Git自动更新HEAD
附上。由于 HEAD
曾经并且仍然附加到 develop
,因此该名称现在指向新提交 I
:
...--G--H <-- master
\
I <-- develop (HEAD)
如果我们现在 git checkout master
返回到 master
,Git 将 return 我们到现有提交 H
,附加特殊名称 HEAD
到 master
,然后给我们:
...--G--H <-- master (HEAD)
\
I <-- develop
提交是 read-only,那么文件是如何工作的?
我们之前提到,提交是 所有 文件的 frozen-in-time 快照。为了使这项工作顺利进行,Git 以特殊的 read-only、Git-only、冻结和 de-duplicated 格式存储每个文件。只有 Git 自己才能 使用 这些文件。因此,当我们选择要使用的提交时,Git 将文件从提交 复制到工作区。那个工作区,里面有普通的日常文件,是你的 work-tree 或者 working tree.
de-duplication 意味着即使每个提交都有 每个 文件的完整快照,这些快照中的大多数只是 re-using 现有文件。也就是说,当我们提交 I
时,我们可能更改了一两个文件,而其他所有文件保持不变。所以提交 I
和提交 H
实际上 共享 他们的大部分文件。在某种程度上,他们可能也与早期提交共享大部分 those。 (事实上 ,如果您将文件 改回 到它在某个早期提交中的方式,新文件会自动与旧提交共享。)
这是每个提交中的数据:所有 文件的完整冻结快照,或者更确切地说,是您告诉 Git 放入的所有文件的快照那张快照。那么这些是哪些文件?
进行新的提交,第 2 部分
每个提交都包含这些 frozen-format 文件,这些文件需要扩展到您的 work-tree 中。那么,您可能会假设 git commit
获取了 work-tree 中的任何内容并提交了它。但实际上 Git 并不是这样工作的。
除了原始文件、in-commit、冻结文件和在-work-tree、日常文件之外,Git 还保留了一个中间副本每个文件。4 这个额外的副本位于一个非常重要的区域,或者最初命名如此糟糕的区域,以至于它有三个名称。 Git 称其为 index,或 staging area,或者有时——现在很少——cache。我将在这里使用术语 index 但请记住 staging 指的是 t这些额外的副本。
每个文件的索引副本都是冻结的格式,准备进入下一次提交。所以这意味着考虑 Git 索引的一个好方法是它包含 提议的下一次提交 。 git add
命令告诉 Git:复制 work-tree 文件的普通格式副本到索引中,或返回到索引中,替换任何以前的副本。 这也以冻结格式准备它(de-duplicating 它也是)以便它为下一个 git commit
.
当您执行 运行 git commit
时,Git 会收集元数据所需的任何额外信息——例如您的姓名和日志消息——然后构建一个新的提交。然后它写出索引 中的任何内容然后 作为快照,添加元数据——包括当前提交作为新提交的父提交——并进行新提交,然后更新任何 b 运行通道名称 HEAD
附加到。
如果您喜欢使用 git commit -a
,请注意这只会使 git commit
更新索引 中已有的文件 。它几乎等同于 运行ning git add -u
(更新已知文件)后跟 git commit
.5
您不能直接看到索引,6 但是 git status
会隐式地告诉您索引中的内容。 git status
的工作方式也很简单:
您有一个当前提交。那就是你的 b运行ch 名称(通过查看
HEAD
找到)所说的 last 提交。该提交中有一堆文件,采用 Git 特殊的内部冻结格式。 Git 也称此为HEAD
提交。Git有它的索引。其中有一堆文件,采用 Git 的特殊冻结格式——但与提交中的文件不同,它们可以 替换 为新副本。
而且,您有自己的 work-tree,在这里您可以做任何您想做的事情——包括创建 all-new 文件。
git status
命令进行两次单独的比较:
首先,它将
HEAD
提交中的所有文件与索引中的所有文件进行比较。对于每个 相同 的文件,它什么也没说。对于每个不同的文件——包括新的或消失的——它说这个文件是暂存以提交。然后
git status
将 Git 索引中的所有文件与您 work-tree 中的文件进行比较。对于每个 相同 的文件(从冻结形式展开后),它什么也没说。对于每个 不同 的文件,它表示此文件 未暂存提交 。
这意味着您可以查看索引中可以更新的内容,而不必查看索引中相同的每个文件作为
中的副本4从技术上讲,索引中的内容不是文件的副本,而是参考 到 frozen-format Git 内部 blob 对象 。但是您通常不需要知道这一点——只有当您开始使用 git ls-files --stage
和 git update-index
来处理 Git 的 low-level 索引时才重要。
5这里的主要区别在于,如果进行新提交失败,git commit -a
方法会回滚索引.使用 git add -u
是一个单独的步骤,因此如果添加成功但提交失败,索引仍会更新。还有很多 more-subtle 区别,但我们将忽略这里所有棘手的极端情况。重要的一点是 Git 从一个索引进行提交,通常只有一个索引—— 索引——其他所有内容都从那个开始。
6实际上,您可以查看索引中的内容:运行 git ls-files --stage
。请注意,这会将大量输出转储到一个大存储库中!此命令不是您通常使用的命令:它是供 Git 程序在内部使用,而不是供用户使用。
跟踪和未跟踪的文件
现在您知道 Git 从其索引进行提交,您终于可以正确理解跟踪和未跟踪的文件了。 跟踪文件的定义非常简单,但仍然很复杂:跟踪文件是指现在在Git索引中的文件。
您可以随时将文件添加到索引中:git 添加<em>newfile</em>
。现在跟踪该文件。您也可以随时从索引中删除 文件:git rm --cached <em>oldfile</em>
. --cached
阻止 git rm
删除您的 work-tree 副本,这样您仍然可以 查看 该文件,但它不再位于 Git 的索引:该文件现在未被跟踪。
但是记住:git checkout <em>b运行ch</em>
告诉 Git 到 填写它的索引和你的work-tree 来自一些现有的提交!所以 Git 将自行更新其索引。如果 Git 的索引和你的 work-tree 中现在有文件,而你 git checkout
提交 没有 有这些文件, Git 将 从其索引和您的 work-tree 中删除 那些文件,以便您可以看到保存在该提交中的内容。
未跟踪的文件 是您 work-tree 中但不在 Git 的索引中的任何文件。当你有这样一个文件——一个不在 Git 的索引中——并且 git checkout
其他一些提交也没有那个文件时,那个文件继续不在 Git' s 索引,因此继续未被跟踪。
(当你有一个未跟踪的文件时,这里会发生一个偷偷摸摸的情况,然后要求切换到 确实 有那个文件的提交。我们不会在这里担心,但您可能会看到这可能是个问题。)
摆脱提交
提交实际上很难摆脱(除了删除整个 .git
目录,这会丢失 一切 并且很少是一个好主意)。那是因为 Git 是为了 添加新提交 而构建的,而不是删除它们。但实际上您可以摆脱提交。
假设一些 b运行ch 名称和一些提交系列:
...--G--H <-- master (HEAD)
现在进一步假设我们可以说服 Git name master
应该持有,而不是提交 H
的哈希 ID,但是 commit G
相反,像这样:
H
/
...--G <-- master (HEAD)
请注意,提交 H
实际上仍然存在于存储库中。但是 Git 显示 我们从 ID 存储在 name 中的提交开始提交,例如 master
。该名称现在表示 commit G
是最新的 commit。提交 G
指向一些较早的提交(可能是 F
)。因此,如果我们询问 Git 关于此存储库中的提交,我们将不会再 看到 提交 H
。
(我们可以,当我们绘制这些时,将“丢弃的”提交向上或向下推以将它们移开。我有点受 Whosebug 文本约定的限制,但是如果您将它们绘制在纸上或一个白板,随意以任何方式绘制它们,包括从 b运行ch 名称到提交的长长的弯曲箭头。)
请注意,这仅适用于“尾部”提交。也就是说,假设我们有:
...--G--H <-- master (HEAD)
\
I <-- develop
如果我们强制名称 master
指向提交 G
,那么,提交 I
仍然指向提交 H
,所以我们得到的是:
...--G <-- master (HEAD)
\
H--I <-- develop
也就是说,现在看起来我们在 b运行ch develop
上提交了 H
。尽管如此,b运行ch master
现在在提交 G
时结束,所以我们确实做了 something.
这就是 git reset
所做的。当你 运行:
git reset --mixed HEAD~
你告诉你的 Git:找到从 HEAD
退一步的提交——从当前 b运行ch 的最后一次提交退一步。然后,强制使用当前的 b运行ch 名称来标识该提交。 如果您有:
...--G--H--I <-- master
你这样做一次,你最终得到:
...--G--H <-- master
\
I
如果您再次这样做,名称 master
将指向提交 G
,并且 H-I
将悬空。默认情况下,它们会在您的存储库中保留一段时间——用户存储库至少有 30 天的时间来取回这些提交。 (这里的机制是Git调用reflogs的东西,但我们就不细说了。)
git reset
的 --mixed
参数告诉 Git 在移动东西时 保持 work-tree 不变 。因此 work-tree 中的文件副本将被单独保留。使用 --hard
,git reset
也会调整这些。使用 --soft
,git reset
单独保留 Git 的索引,但是使用 --mixed
,Git 清空旧索引并从提交中填充它 select.
这——替换索引,但单独保留 work-tree——很容易导致 untracked-files 的情况。特别是,假设提交 I
添加了一个不在提交 H
中的 new 文件。然后上面的 reset
从 Git 的索引中删除新文件,将新文件留在 work-tree 中。该文件现在在您的 work-tree 中,但不在 Git 的索引中,这就是未跟踪文件的定义。
请记住,所有 committed 文件在这些提交中都是安全的,只要您仍然可以 find 那些提交。 通过像提交 I
这样的提交很难找到,你已经设置好了,所以你可能无法获得 those轻松恢复文件的版本。但是 git log
向您展示的任何提交,好吧,那些 提交很容易找到。 (我们跳过了使用 Git 的 detached HEAD 模式来查看历史提交的想法,这样就不必涵盖该模式,但仅此而已查看历史版本的方式。)
添加更多 Git 个存储库
现在你知道怎么做了ommits 和 b运行ch 名称,在 你的 存储库中工作——包括添加新提交,以及重置一些提交——是时候添加 GitHub Git加入组合。
要让您的 Git 调用其他 Git,您需要有一个 URL——类似于 ssh://git@github.com/...
或 https://github.com/...
。您的 Git 将为您 保存 这个 URL,使用一个简短易记的名字。 Git 称其为 远程 。许多 Git 存储库只有一个远程存储库,称为 origin
,我假设您的存储库也是如此。
要让您的 Git 与另一个 Git 连接,您需要 运行 三个命令之一:git fetch
、git push
和 git pull
。 git pull
命令只是一个方便的包装器,首先是 运行s git fetch
,然后是第二个 Git 命令,这是最好的——好吧,I 认为最好——单独学习 git fetch
。所以这只给了我们两个让 Gits 互相交谈的命令。
两条命令本身比较简单:
git fetch
让你的 Git 打电话给他们的 Git,然后问他们有什么,你没有。他们列出了他们的 b运行ch 名称(和其他名称)和他们的提交哈希 ID。您的 Git 可以立即判断您是否有这些提交,因为哈希 ID 在 every Git 存储库中是相同的(再次参见脚注 1)。如果您没有提交,您的 Git 会要求他们的 Git 将它们发送过来。他们这样做了,现在你也有提交了。现在您已经拥有他们拥有的所有提交(加上您自己未共享的任何提交),您的 Git 创建或更新您的
时,他们在 b运行ch 名称中有什么哈希 ID 的记忆origin/*
名称,至 记住他们的他们的b运行ch名字。您的每个origin/*
名称都是一个 remote-tracking 名称。7 这些只是您的 Git在你 运行git fetch
.如果他们不改变他们的名字运行ch 名字(曾经,或经常),你的
git fetch
将设置你的remote-tracking 每次都正确命名。如果他们确实经常更改它们,您需要经常 运行git fetch
以获取任何新名称和不同的提交哈希 ID。你可以 运行
git fetch
就像这样,根本没有参数。git push
让你的 Git 调用他们的 Git 然后 给他们新的提交 如果需要的话。这比git fetch
稍微复杂一点,因为他们 记住 任何新的提交,他们将不得不更新 他们的 b 运行ch 名称!他们没有等同于 remote-tracking 的名字。与
git fetch
一样,您的 Git 列出了提交哈希 ID。他们检查自己是否有这些 ID。如果没有,他们会让你的 Git 发送这些提交(如果需要的话还有他们的文件——这里有很多花哨的东西来避免发送他们已经有副本的文件)。和以前一样,每个 Git 对相同的提交使用 相同的 哈希 ID 的事实使这很容易。然后,一旦他们的 Git 有任何提交要求,您的 Git 发送一个或多个请求:请,如果没问题,请设置您的 b运行ch name ______(填一个名字)到______(填一个hash ID)。是否服从这种礼貌的要求,由他们自己决定。或者,你的Git可以发一个命令:将你的名字_____设置为_____!是否服从还是由他们决定。
git push
命令需要8,你把名字origin
,因为很久以前,有人说语法是git push <em>remote b运行ch</em>
所以你必须先把origin
放在那里b运行ch 名称。然后,git push
需要 b运行ch 的名称。这告诉你的 Git 要发送哪个提交——你的 Git 像往常一样从你的 b运行ch 名称中找到 last 提交——并且填补了两个空白。即我们必须将b运行ch name和hash ID都填入两个空格中,无论是礼貌的请求还是强行的命令。你的 Git 从你在这里输入的名字中得到这两个。9他们告诉我们 Git 他们是否服从了命令。如果是这样,我们的 Git 更新我们的一个 remote-tracking 名称对应于他们的 b运行ch 名称。也就是说,如果我们让他们更新他们的
master
,我们的 Git 会更新存储在origin/master
中的内存。由于我们没有找到他们的任何 other b运行ch 名称,我们其他origin/*
名称中的 none 得到更新。
7Git 调用这些东西 remote-tracking b运行ch names, 但我发现这里的单词 b运行ch 使事情变得更加混乱,而不是更少;所以现在我把它放在一边,只称它们为 remote-tracking names.
8你ca 进行设置,使 git push
默认为 推送当前 b运行ch,然后你可以忽略它—Git 将计算出正确的遥控器和当前的 b运行ch——但我喜欢显示显式版本。
9还有一些额外的选项可以让你变得更漂亮。例如,如果您愿意,可以从您的名字 grandpa-simpson
推送到他们的名字 onion-on-my-belt
。可以在每一侧使用完全不同的名称。但是不要在没有强烈需求的情况下这样做:它很快就会变得非常混乱。
Fast-forwards 和 non-fast-forwards
让我们想象一下,现在,我们是一个正在接收 git push
的 Git。其他一些 Git 打电话给我们,问我们是否提交 a123456
。我们没有,所以他们给了我们。 a123456
有父 9876543
,我们确实有,所以这是我们唯一需要的提交。现在他们说:拜托,如果可以的话,把你的master
设置成a123456
。
让我们画出我们所拥有的:
...--G--H <-- master
假设提交H
的哈希ID是9876543
。然后新的提交 a123456
显然是一个新的提交 I
只是 添加到 我们现有的 master
,我们可以像这样把它:
...--G--H--I <-- master
但是如果父 9876543
是 不 提交 H
怎么办?如果提交 G
怎么办?也就是说,我们有:
...--G--H <-- master
他们给了我们:
...--G--H
\
I
他们现在要求我们设置 master
以记住提交 I
?如果我们这样做,我们将失去 我们的 提交 H
。我们最终会得到:
H
/
...--G--I <-- master
我们将无法再找到提交 H
。所以我们会对礼貌的请求说 no,因为这个操作不仅 add 提交给我们的 master
,它还丢弃 一个提交。
如果他们向我们发送强制命令——将你的 master
设置为 a123456
!——我们可能会服从它,并放弃提交 H
。如果 他们 没有保留提交 H
的副本,它可能会很快消失。 Server-side 存储库通常没有 reflogs,几乎可以立即删除被遗弃/悬空的提交。
你自己的情况
我们可以画出你的情况,你的master
是你的origin/master
的ahead 1, behind 2
——你的Git对[=431=的记忆]他们的 Git的master
。它可能看起来像这样:
I--J <-- origin/master
/
...--G--H
\
K <-- master (HEAD)
如果愿意,您可以使用 git push --force origin master
向他们发送您的提交 K
并告诉他们 放弃 他们的 I-J
提交.但是,如果您想保留这些提交,请不要这样做。
如果愿意,您可以放弃自己的提交 K
:
git reset [options] origin/master
会给你:
I--J <-- master (HEAD), origin/master
/
...--G--H
\
K [abandoned]
您的提交 K
会保留一段时间,但要找到它会有点困难。如果你最终不想要它,那可能很好。
您可以使用 git merge
合并提交 H
与提交 K
相比的更改,以及提交 H
与提交 [=] 相比的更改208=],进行新的提交。您可以使用 git rebase
来 复制 现有提交 K
到添加到提交 J
上的新的不同提交。您 可以 做很多事情。每个都有一组不同的结果提交。记住提交是什么以及为您做什么,以及 b运行ch 名称如何找到提交,并决定您想要哪些提交,哪些您希望假装从未发生过——用 git reset
[= 删除811=] git push --force
—然后按照您想要的方式在您自己的本地存储库中进行设置。然后使用 git push
,有或没有 --force
,将新提交发送到 GitHub Git,并让他们设置 他们的 b运行ch 指向新提交的名称,以匹配您在 your Git.