我有一种感觉,我用远程的内容覆盖了本地的更改...我该如何恢复它?
我有一个远程分支develop
。我还有一个本地分支,它不是 git 存储库。
我通过执行以下操作将本地目录设为 git 存储库git init
。我想将本地的所有内容添加到远程开发分支,所以我尝试了:
git remote add origin git@github.....
git fetch
git add .
git commit -m "some comment"
git status
Run Code Online (Sandbox Code Playgroud)
但它告诉我:
On branch master
nothing to commit, working tree clean
Run Code Online (Sandbox Code Playgroud)
我觉得很奇怪......所以我尝试了
git push origin develop
Run Code Online (Sandbox Code Playgroud)
但我得到了错误:
error: src refspec develop does not match any
error: failed to push some refs to 'git@github.com:.....'
Run Code Online (Sandbox Code Playgroud)
所以我认为我需要进行开发,所以我这样做了:
git checkout develop
git push
Run Code Online (Sandbox Code Playgroud)
但随后它告诉我一切都是最新的
但是当我去 github 的开发分支时,我没有看到我提交的更改。
我尝试撤消git fetch
此处显示的内容:如何撤消“git fetch”
所以我这么做了,git remote remove origin
但那什么也没做。
我做错了什么,如何恢复原来的更改,以便我可以将其推送到远程开发?
更新:
从git reflog
我看来:
a1be24c (HEAD -> develop) HEAD@{0}: checkout: moving from master to develop
771b082 (master) HEAD@{1}: checkout: moving from develop to master
a1be24c (HEAD -> develop) HEAD@{2}: checkout: moving from master to develop
771b082 (master) HEAD@{3}: commit (initial): trying to keep develop up to date ..
Run Code Online (Sandbox Code Playgroud)
我看到771b082 (master) HEAD@{3}: commit (initial): trying to keep develop up to date ..
这是我的第一次提交。我认为这就是我需要取回本地源代码的东西。
git reflog
显示您最近访问过的每个提交的列表。即使您删除了本地分支,您仍然可以转到最新的提交,然后在该位置创建分支。
所以,先这样做。
完成后,您可以确定是否需要进行任何分支手术以将您的工作与存储库的其余部分集成。
您不必搜索。您的master
分支包含您的一次提交。但是你*确实*必须对你的制作方式做一些事情,这是没有用的。
你到底想做什么取决于很多事情,并且确实需要更长时间的讨论。我的猜测是您会想从 开始git worktree add
。请参阅下面的“如何恢复”部分(但直到您理解长部分)。
\n\nRun Code Online (Sandbox Code Playgroud)\n771b082 (master) HEAD@{3}: commit (initial): trying to keep develop up to date ..\n
这才commit (initial)
是真正的线索。这意味着您创建了一个本地存储库 ( git init
),它将您置于名为 的自己的分支上master
。这个分支还不存在。这是一种令人困惑的情况;见下文。
然后你跑了:
\n\n\nRun Code Online (Sandbox Code Playgroud)\ngit remote add origin git@github.....\ngit fetch\n
这添加了一个远程:在本例中是一个短名称origin
,用于让您的 Git 软件访问另一个单独的 Git 存储库。然后git fetch
确实到达了另一个 Git 存储库(显然是成功的)并从中获取了它的所有提交,但没有任何分支。运行时git fetch
,你的 Git 软件可以看到它们的所有分支名称,而不是仅仅复制它们的分支名称来创建或更新你自己的分支\xe2\x80\x94,这会很糟糕:请参阅下面的更多内容\xe2\x80\x94你的 Git然后软件会重命名每个分支的名称。develop
例如,他们的成为您的origin/develop
。这个名称origin/
前面的 \xe2\x80\x94部分来自\xe2\x80\x94中的origin
名称,是一个远程跟踪名称,您的 Git 软件用它来记住存储库中其存储库的分支名称。origin
git remote add origin ...
因此,您现在拥有他们的所有提交,但没有任何分支:相反,对于他们的每个分支名称,您都有一个远程跟踪名称。这就是全部git fetch
(获取对您来说新的任何提交,并更新您的远程跟踪名称),git fetch
现在已经完成。
然后你跑了:
\n\n\nRun Code Online (Sandbox Code Playgroud)\ngit add .\ngit commit -m "trying to keep develop up to date"\n
由于您(仍然)在您的分支上master
,而该分支仍然不存在,因此这添加了您的所有工作树文件\xe2\x80\x94,其中没有一个来自他们的任何提交\xe2\x80\x94,并制作了一个在您的存储库中进行新的提交,创建名为master
.
你的:
\n\n\nRun Code Online (Sandbox Code Playgroud)\ngit status\n
然后向您展示您的分支主机,没有任何可提交的内容,因为您刚刚提交了所有内容。因此,您的工作树(和 Git 的索引)与您刚刚做出的一次提交相匹配。
\n然后您尝试了,但由于此时git push origin develop
您还没有命名分支,所以失败了。develop
(您确实有origin/develop
,但那是远程跟踪名称,拼写为origin/develop
,而不是develop
)。然后你跑了:
\n\nRun Code Online (Sandbox Code Playgroud)\ngit checkout develop\n
develop
由于此时您还没有命名分支,因此您可能会遇到错误,但两者git checkout
都有git switch
一个特殊功能。如果您不告诉 Git不要这样做--no-guess
,Git 现在会猜测您的意图。Git 的猜测将搜索以develop
. 你有一个!它是origin/develop
。所以搜索成功并且恰好找到了一个。然后 Git 的猜测代码会猜测您打算运行:
git checkout -b develop --track origin/develop\n
Run Code Online (Sandbox Code Playgroud)\n因此,您的 Git 现在创建了一个新的分支名称 ,develop
指向与您的origin/develop
. 您现在有一个名为develop
您可以切换到的分支,git checkout
现在就可以切换。
当然,您develop
现在与您的 持平。origin/develop
它完全独立于您自己的master
,并且不包含来自您的 的提交master
。如果此时您要运行:
git log --all --decorate --graph\n
Run Code Online (Sandbox Code Playgroud)\n(没有,--oneline
因为在这种情况下它具有欺骗性)您将看到两个不相交的图,一个用于develop
/ origin/develop
(两个名称都选择相同的提交),另一个用于master
包含一个提交。
实际上,Git 就是关于提交的。Git 与文件无关,尽管提交保存文件;Git 也与分支无关,尽管分支名称可以帮助您(和 Git)找到提交。这实际上都是关于提交的。
\n存储库中的每个提交都带有通用唯一的哈希 ID 进行编号。如果两个 Git 存储库有两个具有相同编号的提交,则它们必须具有相同的提交。因此,如果您的 Git 存储库有一个带有编号的提交a1be24c
(缩写:实际上是 40 个十六进制数字长),并且他们的Git 存储库有一个具有相同编号的提交,那么您和他们有相同的提交。你从他们那里得到了它,或者他们从你那里得到了它,或者你们俩都从第三个 Git 存储库得到了它,但不管怎样,你们都有相同的commit。
这意味着您的 Git 存储库和他们的 Git 存储库只需比较这些哈希 ID 即可了解谁拥有什么。他们拥有的任何承诺,而你没有的承诺,你都可以通过git fetch
. 任何你拥有而他们没有的提交,你都可以给他们git push
(尽管你不一定会给他们你拥有但他们没有的一切git push
,因为比这里更复杂git fetch
,我们将使用的方式它)。
除了这个相当神奇的编号技巧之外,每个提交还包含两件事:
\n任何给定的提交都包含您或任何人进行提交时 Git 所知道的每个文件的完整快照。例如,您的一次提交master
了解来自 的文件git add .
,因此这些是该提交中的文件。这些文件采用特殊的、只读的、仅限 Git 的、压缩和去重复的格式,因此如果您不断重新提交某些文件的相同版本\xe2\x80\x94,这是很正常的\xe2\x80\x94there\'存储库中实际上只有一份实际副本。但是每个这样的提交都“保存”(共享)副本。(这是安全的,因为每次提交的所有部分都是只读的,这对于神奇的编号技巧是必需的。)
任何给定的提交还包含一些元数据或有关提交本身的信息。这包括提交作者的姓名和电子邮件地址。它包括一些日期和时间戳。它包括您提供的任何日志消息 ( git commit -m ...
)。并且\xe2\x80\x94对于Git\自己的操作至关重要\xe2\x80\x94每个提交在此元数据中存储先前提交哈希ID的列表。该列表通常只有一个元素长,但对于“初始”或“根”提交,它是空的。
如果我们看看它在一个比初始的、完全空的存储库更容易混淆的情况下是如何工作的,我们会看到这样的情况。让我们使用单个大写字母来绘制提交,例如H
代表哈希 ID:
<-H\n
Run Code Online (Sandbox Code Playgroud)\n这里,H
是我们存储库中的最后一次提交,例如在主分支或主分支上(我们此时假设只有一个分支名称,而不是我们更经常看到的两个或三个分支名称)。作为提交,H
同时保存快照和元数据。元数据告诉我们谁创建了H
,但重要的是,它保存了先前提交的哈希 ID,我们将其绘制为向后指向的箭头。这个假装箭头“指向”前一个或父提交,它有一些看起来随机的哈希 ID,但我们将其称为:G
<-G <-H\n
Run Code Online (Sandbox Code Playgroud)\n当然,G
是一个提交,所以它有一个快照和元数据,并且元数据包括一个向后指向的箭头(单个哈希 ID),它让 Git 找到更早的提交F
,这是G
\ 的父级:
... <-F <-G <-H\n
Run Code Online (Sandbox Code Playgroud)\n这会一直持续下去,只是最终它必须停止,因为某些提交(commit A
)是第一个提交。提交A
没有父级,因为没有先前的提交:
A <-B <-C ... <-H\n
Run Code Online (Sandbox Code Playgroud)\nGit 真正的工作方式是,它从末尾开始,从提交开始H
,向后工作。当它到达开头\xe2\x80\x94a没有父级\xe2\x80\x94 的根提交时,它最终会停止。
为了让 Git找到commit H
,某些东西\xe2\x80\x94,也许是你,人类\xe2\x80\x94,必须向Git 提供commit的原始哈希 ID H
。当然,记住哈希 ID 是一项荒谬的任务,但也是完全没有必要的。我们有一台计算机:我们可以让计算机记住最后一个哈希ID。这就是分支名称的用武之地。
1在您运行时,您的 Git 软件如何知道它分配给您的新git commit
提交的哈希 ID与任何其他现有的哈希 ID 不同?答案是否定的:它依赖于概率。然而,这在实践中确实有效。
正如提交向后指向其父提交一样,通过保存父提交的哈希 ID,分支名称通过保存哈希 ID指向最后一次提交。所以我们可以稍微扩展一下我们的绘图:
\n...--G--H <-- master\n
Run Code Online (Sandbox Code Playgroud)\n现在,我们不必记住一些哈希 ID,只需为 Git 指定名称即可master
。这是一个分支名称,根据定义,名称中的任何哈希 ID都是该分支“上”的最后一次提交。
现在假设我们决定br1
使用git branch br1
orgit checkout -b br1
或来创建一个新的分支名称git switch -c br1
。最后两个执行相同的操作,而第一个 \xe2\x80\x94 git branch br1
\xe2\x80\x94 创建名称而不切换到它。不管怎样,新名称也指向H
立即提交,如下所示:
...--G--H <-- br1, master\n
Run Code Online (Sandbox Code Playgroud)\n我们现在需要一种方法来告诉我们使用哪个名称来查找 commitH
。Git 执行此操作的方法是将特殊名称附加HEAD
到两个分支名称之一:
...--G--H <-- br1, master (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n这意味着我们已经“成为”主人了;如果我们现在运行git checkout br1
我们会得到:
...--G--H <-- br1 (HEAD), master\n
Run Code Online (Sandbox Code Playgroud)\n这意味着我们正在“开启” br1
。不管怎样,我们都在使用commit H
,所以目前我们在哪个分支上并不重要。但现在让我们创建一个新的提交(例如,通过更改文件,使用git add
并运行git commit
\xe2\x80\x94 这将创建一个新快照,重用未更改的文件,并添加一个已更改文件的新版本) 。2
不管怎样,为了进行新的提交,Git 保存了快照,添加了元数据\xe2\x80\x94,包括哈希 ID H
,这是当前提交的\xe2\x80\x94,并将所有这些写出作为新的提交,这为新提交分配新的、唯一的哈希 ID;我们将其称为“提交I
”:
I\n /\n...--G--H\n
Run Code Online (Sandbox Code Playgroud)\n由于I
现在是我们所在分支上的最新提交,Git会更新该分支名称\xe2\x80\x94(附加的HEAD
分支名称 \xe2\x80\x94)以指向新提交I
。其他分支名称保持不变,因此如果HEAD
附加到br1
,我们现在有:
I <-- br1 (HEAD)\n /\n...--G--H <-- master\n
Run Code Online (Sandbox Code Playgroud)\n请注意,向上提交H
是在两个分支上,而不仅仅是在分支上master
。提交I
仅在此时生效br1
,尽管将来可能会发生变化。
如果我们现在git checkout master
,Git 将:
I
安全保存,只读,因此这是安全的);I
H
(从commit中提取H
)现在我们有:
\n I <-- br1\n /\n...--G--H <-- master (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n如果您查看工作树中的文件,您将看到来自 的文件H
,而不是来自 的文件I
。当然,其中许多文件将完全相同(您只更改了一个文件),并且 Git 也在幕后秘密地努力工作,不费心撕掉然后重新创建一个不会更改的文件,这使得声称它会删除并替换所有文件的说法有点过于强烈,但以这种方式思考它非常有效,可以帮助您入门。
如果您切换回,br1
您将再次获得提交I
文件:
I <-- br1 (HEAD)\n /\n...--G--H <-- master\n
Run Code Online (Sandbox Code Playgroud)\n如果你现在再次提交,你会得到:
\n I--J <-- br1 (HEAD)\n /\n...--G--H <-- master\n
Run Code Online (Sandbox Code Playgroud)\n新提交J
是最后一个提交,并br1
指向J
; J
向后指向I
,后者向后指向H
,依此类推。
如果您切换回master
并创建br2
并切换到br2
,您将得到:
I--J <-- br1\n /\n...--G--H <-- br2 (HEAD), master\n
Run Code Online (Sandbox Code Playgroud)\n如果您现在再创建两个新提交,您将得到:
\n I--J <-- br1\n /\n...--G--H <-- master\n \\\n K--L <-- br2\n
Run Code Online (Sandbox Code Playgroud)\n这就是 Git 中分支的工作方式。
\n2注意git add
:
所以它不会伤害未更改的git add
文件。不过,它确实需要大量额外的 CPU 工作,因此 Git 会尝试猜测它可以假装已经对文件进行了-ed 操作而实际上并不费心的情况。在极少数情况下,您可能会发现必须使用例如 来覆盖此假装。git add
git add --renormalize
假设我们有一个包含以下内容的存储库:
\n I--J <-- dev (HEAD), origin/dev\n /\n...--G--H <-- master, origin/master\n
Run Code Online (Sandbox Code Playgroud)\n您可以通过克隆一个包含master
和dev
的存储库(创建 yourorigin/master
和 your origin/dev
)然后执行 a来获得此信息git checkout dev
,这将创建您dev
指向与他们的相同提交的指向dev
,即与您的origin/dev
。
现在我们等待一天(或一小时,或一秒,或某个时间间隔),足够长的时间让他们向他们的 dev
. 然后我们就跑git fetch
。我们的 Git 访问他们的 Git 软件和存储库,发现K-L
他们拥有但我们没有的两个提交,因此我们git fetch
获得了这些提交。同时他们的 dev
now 指向(共享) commit L
。因此,我们的 Git 软件现在会更新以记住它们 现在的位置:origin/dev
dev
K--L <-- origin/dev\n /\n I--J <-- dev (HEAD)\n /\n...--G--H <-- master, origin/master\n
Run Code Online (Sandbox Code Playgroud)\n我们dev
落后了,不是因为我们做了什么\xe2\x80\x94我们什么也没做\xe2\x80\x94而是因为他们已经将提交添加到了他们的 dev
. 我们现在可能想强迫我们自己dev
前进L
,去匹配他们dev
。(为了防止这个答案变得太长,我将跳过如何做到这一点。请注意,它看起来像有单独的dev
分支,dev
并且origin/dev
有一些:这完全取决于我们所说的分支的含义。分支是“名称指向的提交”,还是我们存储库中的分支名称,或者是某些提交的集合,还是什么?另请参阅“分支”到底是什么意思? 人类使用这个词含糊不清,所以当你听到“分支”时,你必须问某人\xe2\x80\x94有时甚至你自己\xe2\x80\x94意味着什么。)
在 Git 中,分支名称 必须恰好指向一次提交。但在一个新的空分支中,我们没有提交(计数它们:零、零、虚无、无)。所以我们也不能有任何分支名称。
\n尽管如此,Git 可以附加HEAD
到一个不存在的分支名称,如下所示:
master (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n(master
如果我能做到的话,这个词可能会变灰。)
每当 Git 存储库处于这种状态,并附加到不存在的HEAD
分支名称时,您所做的下一次提交将是根提交(或如引用日志所示的“初始”提交)。因此,如果我们现在添加一些文件 和,我们将获得第一个提交,我们可以将其称为:git add
git commit
A
A <-- master (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n新提交没有parent,使其成为根提交。该名称master
现在作为一个真正的分支名称而出现,并HEAD
仍然依附于它。我们现在已经摆脱了那种疯狂的设置。
我们现在可以进行新的提交,它们将按照我们的预期串联起来:
\nA--B--C <-- master (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n然而,我们可以通过附加一个新的不存在的分支名称,让 Git 回到这种奇怪的情况,即使有提交。HEAD
我们过去常常git checkout --orphan
这样做(自 Git 1.7.2 起),或git switch --orphan
(自 Git 2.23 起)。这两者略有不同,因此请小心,但您几乎永远不想使用其中任何一个,因此我们不会介绍其中的差异;如果我们要使用该名称执行其中一项操作disconnected
,然后创建更多提交,我们将仅说明效果:
A--B--C <-- master\n\nD--E <-- disconnected (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n也就是说,最新的提交disconnected
现在是 commit E
,其父级是D
,但 commitD
是根提交:后退操作在此停止。如果我们现在git checkout master
或git switch master
,Git 将删除所有提交E
文件并提取所有提交C
文件,git log
并将从提交开始向后C
工作到然后然后停止。B
A
Git 中的历史只不过是提交。我们找到来自分支或其他(例如,远程跟踪)名称的提交,这些名称找到最后一次提交,然后从那里开始,我们让 Git 向后工作。使用git log --graph
,Git 将绘制从每个子提交到其父提交的连接。
通过使用像 一样的技巧--orphan
,我们可以创建多个独立的断开连接的图。它们不是很有用,但 Git 背后的理论允许它们,Git 也允许它们。它有点必须,因为还有另一种方法来创建它们,这就是您偶然发现的。
要是我们:
\ngit init
,以便我们可以master
(甚至main
);git remote add
和git fetch
用提交和远程跟踪名称填充存储库;--guess
模式创建一个develop
分支,指向develop
我们的origin/develop
...然后我们将得到一个单提交master
分支,上面有一个根提交,加上我们自己develop
匹配的origin/develop
. 我们将在我们的工作区域看到他们的develop
文件;我们的master
-branch-single-commit 文件在任何地方都不会可见。
我们将会有这样的东西:
\n I--J <-- develop (HEAD), origin/develop\n /\n...--G--H <-- origin/main\n\nK <-- master\n
Run Code Online (Sandbox Code Playgroud)\n取决于他们是否有 amain
或 a (这里我master
称之为main
origin/main
)。
CommitK
有点没用,但确实有用有一堆我们可能希望看到的文件。
我们可以尝试运行:
\ngit cherry-pick master\n
Run Code Online (Sandbox Code Playgroud)\n现在,将这些文件添加到我们工作树中的文件中,但这可能会产生很多添加/添加冲突。原因是尝试从“他们的”提交(我们在上图中的)中git cherry-pick
获取更改,而这些“更改”是“从头开始添加存在的所有文件”。3 Git 尝试将它们与我们的“更改”结合起来,此时将“添加所有文件K
K
J
从头开始“添加所有文件”。结果往往是一个巨大的、大规模的添加/添加冲突:没有用。
更有用的可能是从提交中提取所有文件K
所有文件提取到普通的日常文件中,然后您可以在编辑器中打开这些文件。为此,您有很多选择;最简单的两个是:
git archive master
将生成提交中所有文件的 tar 或 zip 存档K
,您可以按照您喜欢的任何方式提取该存档;或者git worktree add ../master
将添加一个新的工作树,其中分支中的文件master
在新目录中检出../master
(运行此命令时,您应该位于工作树的顶部)。该git worktree add
方法创建此工作树并填充它,因此文件都在那里供您查看,您甚至可以在那里工作并提交(添加到您的master
),如果这比在此处工作更容易develop
分支。
一旦文件在 中真正准备好../master/*
,您可以将它们复制到此处并运行git diff
和/或git add
根据需要,然后git commit
进行添加到提交的新提交J
:
L <-- develop (HEAD)\n /\n I--J <-- origin/develop\n /\n...--G--H <-- origin/main\n\nK <-- master\n
Run Code Online (Sandbox Code Playgroud)\n当您对提交完全满意L
并成功通过git push origin develop
at 将其发送到 Git 存储库时origin
,您可以删除添加的工作树(请参阅 参考资料git worktree remove
),然后删除您的master
分支,现在您不再需要该分支了。你不需要提交K
。
注意:提交K
将在您自己的存储库中保留一段时间(默认情况下至少长达一个月),以防您改变主意并希望将其恢复。要取回它,您必须以某种方式找到它的哈希 ID(例如,通过git reflog
),然后创建一个分支或标记名称来记住该提交哈希 ID。但如果你不想再把它找回来,那么在足够的时间到期后,Git 会注意到你不仅不能正常看到它,而且你再也没有回去找到过它,并且会真正把它扔掉。(这假设你从未git push
在其他地方这样做过:如果你这样做了,就很难永远摆脱它。提交就像病毒:一旦它们感染了几个存储库,你就无法将它们消灭掉容易地。)
3这里的技巧是 Git 将提交K
与其不存在的父级进行比较。为此,Git 假装有一个完全空的提交,使用 Git 的空树来伪造它;这使得所有文件都是“新添加的”。