首页 > 解决方案 > 将下载的 repo 的提交添加到原始提交历史并允许拉取请求

问题描述

我是 git 新手,在没有完全了解 git 是如何工作的情况下开始了我的项目,如果有人能指出我解决这个问题的正确方法,我将不胜感激。

我有一个现有的 Github 存储库(又名 ClientMaster),在 master 分支上有 100 多个提交,我“下载”了存储库,解压缩并开始我的工作(这使我失去了提交历史,愚蠢的举动)并进行了更改,结果在 10 次提交中,都在一个主分支上。

现在我想将下载的 repo 作为新分支推送回 ClientMaster 并发送拉取请求,我可以使用 -f 推送它,但如果我尝试发送,它会显示“没什么可比较的,因为两个分支具有完全不同的历史记录”公关。我的问题是,无论如何,我是否允许我将我的本地仓库与原始仓库合并,同时保留我所有新提交的内容。

以图形方式:

ClientMaster: -0-0-0-0-0-0-0-0-0-0-0-0
Downloaded local Repo:                -1-2-3-4-5-6-7-8-9-10
                                      |
                                      Downloaded to local(no previous 100+ commit history)

如果我做对了,我必须有一个提交历史为 ...-0-0-0-0-0-0-1-2-3-4-5-6-7-8-9- 的仓库10 所以我可以推回 ClientMaster 并发送拉取请求,对吗?

我尝试过合并、变基、樱桃挑选,--allow-unrelated-histories但它们似乎都不起作用。

请指出我正确的方法。

标签: git

解决方案


正如Ferrybig 评论的那样,当您使用“下载 zip 文件”选项然后创建自己的存储库时git init,您根本没有得到他们的 存储库。你刚刚得到一张快照

此时您需要做的是将两个存储库合并为一个。这总是至少有点混乱。如果运气好的话,只会有点乱,但还是有点痛。如果你有更多的提交——比如说,你自己的 500 个而不是 10 个——你会想要自动化这个或使用其他方法,但是你自己的提交只有 10 个左右,你可以用一个相对简单的方法来做到这一点,无论如何,从概念上讲,方法。

我们在这里要做的是将两个存储库中的所有提交放到第三个存储库中,然后进行一些手术。究竟要做什么手术取决于你,但我会使用 using git cherry-pick,如果需要的话,可能会使用第一次手动提交(我们稍后会看到原因)。不过,您需要一些背景知识才能了解发生了什么。

存储库中有什么

首先,让我们快速了解一下什么是存储库。大致来说,Git 存储库是一个包含大量提交的数据库。也就是说,Git 是关于提交的。提交保存文件,因此存储库间接保存文件,但关键项是提交。每个提交都包含一个快照——一组文件,当你(或任何人)提交时它们的状态——以及一些元数据:提交作者的姓名和电子邮件地址,等等。

存储库也有分支名称。它们很有用——尤其是对人类来说——它们在存储库中形成了一种辅助数据库,但它们并不像提交那么重要。大多数情况下,他们所做的是让你(和 Git)找到提交。提交的问题在于它们的真名是大而丑陋的哈希 ID,没有人能真正处理。

克隆存储库时,您将获得其所有提交。这些进入一个的存储库(最初是空的,然后填充了他们的提交)。你也可以得到它的分支名称,但是你的 Git 会立即重命名它们的所有分支名称:如果它们有master,你会得到origin/master. 如果他们有develop,你就会得到origin/develop。然后,作为 a 的最后一步git clone,您的 Git 运行git checkout _____时会在空白处填充一些名称:通常master,但这取决于您的git clone命令和/或来自其存储库的指令。最后git checkout一步根据您的 Git 刚刚从他们的(或其他)创建的(或来源/其他)创建您的本地master(或其他任何名称)。origin/master master

你最初没有这样做,所以你没有得到他们的承诺。这意味着您在存储库中所做的第一次提交他们的所有提交完全无关。这就是加入你的存储库和提交的原因,通过一个新的克隆,你可以使他们的存储库变得如此混乱:它们不会轻易加入。

请注意,任何现有提交无法更改。但是,我们可以进行新的提交,有时通过复制提交或其效果。我们将利用它来复制您的 10 个提交中的 9 个或全部 10 个。

提交如何相互关联

我们已经提到提交的“真实名称”是它的哈希 ID。任何提交的哈希 ID 对于该一次提交都是唯一的:没有其他提交将拥有该哈希 ID。但是宇宙中的每个 Git 都会同意那个提交——那个提交,而不是其他提交——得到那个哈希 ID。Git 这样做的方式是哈希 ID 是提交内容的加密哈希,包括提交人的姓名和提交时间的时间戳。您可以再次使用相同的内容进行提交,但如果您的名字不是他们的名字,和/或您在不同的时间提交,那么您已经做出了新的不同的提交。获得相同提交 ID 的唯一方法是使用相同的名称同时进行相同的提交,并且- 至关重要的是 - 具有与他们的提交相同的历史记录或父哈希 ID。

现在,已经进行了一些初始提交——它得到了一些看似随机的哈希 ID——如果你进行第二次提交,那么第二次提交的哈希 ID 不仅取决于内容、你的姓名和当前时间,还取决于第一次提交的哈希 ID。您的第三次提交也部分取决于您的第二次提交的哈希 ID。

因此:每个提交都存储其直接父提交或提交的原始哈希 ID。这不仅会影响该特定提交的哈希 ID,它还允许 Git 将提交串在一起,尽管是向后的。如果他们的存储库有 1000 次提交,其中一个是第一个,最后几个提交中的一个是您用来进行第一次提交的。

假设他们所有的提交都在一个很好的简单线性链中(这在现实生活中从未发生过,但它对示例很有用,在这里几乎肯定足够了,并且假设你的十个提交仍然很好并且线性 - 如果它们不是我们有一个更大的问题),我们可以像这样画它们,用字母代表实际的提交:

A <-B ... <-G <-H

(好吧,这只是 8 次提交——你可以看到为什么 Git 使用大而丑陋的哈希 ID,而不是单个大写字母,因为我们会在 26 个之后用完!)。此表示将内部父链接显示为箭头。只要我们记得(在需要时)箭头都向后而不是向前,就更容易把它画成线。

他们的名字master将标识他们的最后一次提交:

A--B--...--G--H   <-- master

该名称master标识或指向他们的最后一次提交H;该提交标识(指向)它的 parent G,它指向它自己的 parent,依此类推。只有在到达第一次提交时,链才会停止向后指向A

同时,在您的存储库中,您的十次提交看起来很相似,但要绘制它们,我将从字母开始N

N--O--...--V--W   <-- master

请注意,这N是您的存储库中的第一次提交。它没有父母。

我们将如何将它们粘合在一起

我们要做的是创建第三个存储库,开始时为空:

mkdir third; cd third; git init

现在我们将添加通过两个新遥控器访问其他两个存储库的功能。远程只是一个 URL 的简称——那里还有其他 Git——它还提供了记住其他 Git 分支的能力。所以我们会这样做:

git remote add one ssh://git@github.com/path/to/repo.git

(或使用https或任何你会用来克隆它的东西),然后:

git remote add two /path/to/tencommitrepo

我们现在有了名为oneand的遥控器two。我们将使用它们git fetch来获取所有两组提交:

git fetch one
git fetch two

现在,在第三个存储库中,我们没有master,但我们确实有one/masterand two/master——这些是分别记住'和' 主的远程跟踪名称——我们可以像这样绘制它们:onemastertwo

A--B--...--H   <-- one/master

N--O--...--W   <-- two/master

请记住,我们无法更改任何这些现有提交。但是我们可以进行新的提交!让我们从建立一个本地master分支开始。理想情况下,我们应该指出这一点,无论哪个提交可以从名称one/master中找到,该名称与 commit 中的快照完全、完全 100% 相同N

开始重建,如果你找不到 100% 匹配

如果我们找不到,使用 commit 可能就足够了H。如果我们找到一个,我们可以让它包含提交N——比如说,100% 匹配的快照G与 中的快照一样好N,或者实际上更好,因为它背后有正确的历史。但是如果我们找不到它,我们会这样做:

git branch master one/master
git checkout master

这将填充这个新的第三个存储库的索引(从中进行新的提交)和他们的 commit 的工作树H。现在,由于HN并不完全相同,我们将创建一个我们rebuild现在调用的新分支,指向 commit H

git checkout -b rebuild

这给了我们这个:

A--B--...--H   <-- one/master, master, rebuild

N--O--...--W   <-- two/master

现在我们将提交的内容N放入我们的索引和工作树中,以便我们准备好进行新的提交。为此,我们将使用所谓的管道命令,git read-tree. 我们需要找到 commit 的哈希 ID N,或者(如果您的计数准确并且确实是十次提交)使用名称two/master~9

 N--O--P--Q--R--S--T--U--V--W
~9 ~8 ~7 ~6 ~5 ~4 ~3 ~2 ~1

这里的每个数字都会后退那么多父链接,所以~9倒数 9 次,从WNgit log --oneline two/master不过,在命令行窗口中运行并剪切和粘贴原始哈希 ID 可能更容易(尽管需要小心) :

git read-tree -u <hash>

Agit status现在将显示一些准备好提交的更改,因为它将提交H与我们从 commit 放入索引(和工作树)的内容进行比较N。所以现在我们将提交这个,即使它回滚了一些重要的更改——你只需要稍后修复它:

git commit -C <hash-of-N>

-C选项从 commit 复制提交消息N。我们刚刚在当前分支上做了一个全新的提交,rebuild所以现在我们的第三个存储库有:

A--B--...--H   <-- one/master, master
            \
             I   <-- rebuild

N--O--...--W   <-- two/master

CommitI的快照与 commit 的快照完全匹配N。它的提交信息也是提交信息N。(如果您想有机会编辑消息,请使用git commit -c <hash>小写 c 。)-C

如果您确实找到了完全匹配的提交,我们根本不需要这一步:我们会以不同的方式开始。

从 100% 匹配开始重建

如果我们有一个 100% 的快照匹配N,比如 commit G,我们要做的就是master像以前一样创建我们的快照,但是创建rebuild指向 commit的指向G

git branch master one/master
git checkout <hash-of-G>
git checkout -b rebuild

现在,由于 中的快照与 中的快照G匹配N,我们根本不需要复制N,现在重建方法与我们必须复制NI之后所做的事情相结合H

我们现在可以复制其余的提交git cherry-pick

现在,它rebuild指向了一个与 commit 完全匹配的提交N——无论是G,还是一个新的I——我们将运行:

git cherry-pick <hash-of-O>

(或者如果计数准确,则再次git cherry-pick two/master~8)。

Git 将在这里做的是比较提交NO, 以查看您所做的更改。然后它将尽最大努力将这些相同的更改应用于当前提交。

(从技术上讲,Git 实际上在这里进行了完整的三向合并,使用N作为合并基础,O作为另一个提交,G或者I- 无论是当前提交 - 作为当前提交。这种三向合并可能会产生冲突,在一般情况下,但由于GorI完全匹配N,它根本不会有任何冲突,并且樱桃挑选会很容易成功。)

结果是:

A--B--...--H   <-- one/master, master
            \
             I--J   <-- rebuild

N--O--...--W   <-- two/master

或者:

A--B--...--G--H   <-- one/master, master
            \
             J   <-- rebuild

N--O--...--W   <-- two/master

无论哪种方式,新提交都是 commit的J副本O就像它的父级(无论是)G是commitI副本N一样。

这些副本与原件之间的区别在于,这些副本具有正确的父级。从每个提交到其父级的链接Git 存储库中的历史记录,因此这个新存储库正在构建您想要的历史记录。

我们现在只需对每个剩余的提交重复樱桃挑选。我们可以通过运行让 cherry-pick 自己完成所有工作:

git cherry-pick <hash-of-O>..two/master

选择O直到并包括 commit之后的每个提交W。(事实上​​,我们本可以使用第一个git cherry-pick命令来完成此操作,但是通过手动操作,我们确保我们已经获得了正确的哈希值并且一切正常,然后我们将所有 Git 的自动机器都松散在其顶部速度。另外,如果你有一个真正古老的 Git 版本,它可能不支持ranged cherry-pick;在这种情况下,你真的必须在这里手工完成每一个。)

所有这一切的最终结果是——好吧,我们在这里画它有点问题,因为一个字母的名字很快就用完了,但是:

A--B--...--H   <-- one/master, master
            \
             I--J--K--L--M--o--o--...--o   <-- rebuild

N--O--...--W   <-- two/master

whereIN或的副本OJ是链中以 结束的下一个提交的副本,two/master依此类推。

在这一点上,你已经完成了,或者几乎完成了

在这一点上,如果你从 commitH中“丢失”了一些好的东西,是时候修复它了。这将是混乱的,没有乐趣,只是修复它的问题,也许git rebase -i用来把修复放在正确的地方,或者其他什么。这是一个单独问题的问题(但已经有一堆可以让你到达那里)。

如果一切都很完美,或者在你解决了问题之后,此时你已经准备好将第三个存储库作为你的存储库:只需删除two远程,并将远程重命名oneorigin

git remote remove two
git remote rename one origin

第一个命令two/master完全删除远程跟踪名称。第二个更改one/masterorigin/master,您现在可以重命名rebuild为您想要的任何功能名称:

git branch -m rebuild feature-X

并运行git push origin feature-X以将新的(复制的)提交发送到 GitHub 并准备好发出拉取请求。

尾声:为什么我们不能挑选N

当我们从以下内容开始时:

A--B--...--G--H   <-- one/master

N--O--...--V--W   <-- two/master

您可能很想创建master(或rebuild)指向 commit H,然后运行:

git cherry-pick <hash-of-N>

这不行!git cherry-pick当您考虑其工作方式时,原因就变得清晰了。它通过您告诉它的提交与该提交的父级进行比较来工作。

提交N 没有父级。Cherry-pick 可以直接放弃——它可以说我没有那个提交的父节点并停止——但是,它玩了一个假装游戏:它假装提交N的父节点根本没有文件

这意味着为了复制 commit N,cherry-pick 需要重播的操作是创建其中的每个文件。大多数这些文件,甚至可能是所有文件,可能已经存在于 commitH中,这也与空提交进行比较,因此合并操作试图将“添加所有H文件”与“添加所有文件”N结合起来文件”。结果是大量的“添加/添加冲突”,以至于 Git 还不如一开始就放弃了。


推荐阅读