如何将代码从新的本地文件夹推送到现有 Github 存储库的主分支并保留提交历史记录?

Posted

技术标签:

【中文标题】如何将代码从新的本地文件夹推送到现有 Github 存储库的主分支并保留提交历史记录?【英文标题】:How do I push code from a new local folder to an existing Github repository's main branch and keep commit history? 【发布时间】:2021-10-01 07:02:42 【问题描述】:

我有一个现有的存储库,其中主分支有多个提交。

我的桌面上有一个本地文件夹,其中包含我希望提交到主分支并完全替换之前提交中的所有代码的代码。我希望保留主分支的提交历史。

如何将我的本地文件夹连接到现有的 github 存储库,将其指向主分支,提交到此分支,以便它替换上一次提交中的所有代码,同时保留所有提交历史记录?

I have done the following:
# Initialize the local directory as a Git repository.
git init

# Add files
git add .

# Commit your changes
git commit -m "First commit"

# Add remote origin
git remote add origin <Remote repository URL>
# <Remote repository URL> looks like: https://github.com/user/repo.git

# Verifies the new remote URL
git remote -v

# Push your changes to main
git push origin main

但它说 git error: failed to push some refs to remote

【问题讨论】:

本地文件夹是从现有仓库克隆的吗?还是一个新的仓库? @evolutionxbox 不,它是全新的代码,与 repo 无关,也不是从它克隆的。 您是否已将代码提交到该本地存储库? @evolutionxbox 我已经用我已经采取的步骤更新了帖子。当我输入 git branch 时,它当前指向本地主分支,但我想将此代码提交到 github 主分支 您可能必须先合并才能推送。在本地主机上签出时,请尝试git merge origin/master --allow-unrelated-histories。您可能需要解决冲突 【参考方案1】:

Git is a tool, or set of tools, not a solution. (Here is another article, perhaps better, on the difference.)

这有好处。特别是,由于它一组工具,因此您几乎可以使用它构建任何您想要的东西。它不只是变成鸟舍、书柜、桌子或文件柜、双簧管、吉他和小提琴,或者任何你可以在设备齐全的商店里制作的东西:如果你知道你在做什么,它可以制作所有这些。但是你需要成为一个工匠大师,或者至少是经验丰富的工匠,才能用它建造一座好房子——即使它只是一个鸟屋或狗屋。

从好的方面来说,您似乎已经了解在 Git 中,提交历史。这意味着您有了一个良好的开端!

您需要预先做出一个重大决定:您希望历史看起来如何?

一个插图,以git log开头

如果我们查看 Git 本身的 Git 存储库,我们会看到类似这样的内容(经过轻微编辑以删除 @ 标志以减少垃圾邮件):

commit eb27b338a3e71c7c4079fbac8aeae3f8fbb5c687 (HEAD -> master, origin/master)
Author: Junio C Hamano <gitster pobox.com>
Date:   Wed Jul 21 13:32:38 2021 -0700

    The sixth batch
    
    Signed-off-by: Junio C Hamano <gitster pobox.com>

commit fe3fec53a63a1c186452f61b0e55ac2837bf18a1
Merge: 33309e428b d1c5ae78ce
Author: Junio C Hamano <gitster pobox.com>
Date:   Thu Jul 22 13:05:56 2021 -0700

    Merge branch 'bc/rev-list-without-commit-line'
    
    "git rev-list" learns to omit the "commit <object-name>" header
    lines from the output with the `--no-commit-header` option.
    
    * bc/rev-list-without-commit-line:
      rev-list: add option for --pretty=format without header


commit 33309e428bf85a0f06e4d23b448bf5400efe3f17
Merge: bb3a55f6d3 351bca2d1f
Author: Junio C Hamano <gitster pobox.com>
Date:   Thu Jul 22 13:05:56 2021 -0700

    Merge branch 'ab/imap-send-read-everything-simplify'
    
    Code simplification.
    
    * ab/imap-send-read-everything-simplify:
      imap-send.c: use less verbose strbuf_fread() idiom

这个历史子集——只有三个提交的小 sn-p——缺少一些东西:显示的两个提交,fe3fec53a63a1c186452f61b0e55ac2837bf18a133309e428bf85a0f06e4d23b448bf5400efe3f17,是合并提交,每个都有两个父提交而不是一个。中间提交fe3fec53a63a1c186452f61b0e55ac2837bf18a1 的两个父级是(缩写)33309e428bd1c5ae78ce,从上面输出的第一行Merge: 可以看出。这两个提交之一是另一个合并(显示的第三个提交),但是提交d1c5ae78ce 在哪里?它有超过 40 次提交,除非我们添加 --graph 或类似名称,否则我们无法真正看到它在历史中的位置

graph 选项使git log 以您将在some of the answers to Pretty Git branch graphs 中看到的那种花哨的图形绘制粗略的ASCII艺术尝试(一个问题,并设置答案,值得仔细阅读几遍,虽然它很长)。我特别喜欢 gitdags 用于教程的输出,尽管它根本不适合自动化工作(它需要仔细手工构建 LaTeX 输入)。

不过,我认为想要为你的案做出的决定最终归结为只回答一个问题:做你想要一个线性历史,还是一个分支历史?也就是说,假设 现有 存储库中只有几个提交,我们可以绘制为 A-B-C-D,我们将调用你的 new 提交E。您是否希望历史看起来像:

A--B--C--D--E   <-- main (or master)

或者你希望它看起来像:

A--B--C--D
          \
           M   <-- main (or master)
          /
E--------’

?

无论哪种情况,您都需要一个包含所有提交的存储库 H3>

要创建一个包含所有提交的存储库,您可以从您已经做过的事情开始:

git init
git add .
git commit -m "First commit"
git remote add origin <url>

到目前为止,您已经完成了我们将称之为E 的提交,或者一个看起来几乎完全类似于我们最终将称之为E 的提交。在你的 Git 存储库中定位此提交的分支名称几乎肯定是master;如果您希望它改为 main,现在是重命名它的好时机:

git branch -m main

如果您想将其保留为 master(或者如果您将 Git 设置为使用 main,因此它已经是 main1),您可以省略此步骤。实际的分支名称并不重要(嗯,除了对人类......好吧,这很重要? - 但这里的重点是 Git 并不关心你使用什么名称,只要因为有一些名称可以用来查找提交)。

还没有提交 A-B-C-D,所以下一步是获取它们:

git fetch origin

或者只是:

git fetch

你的 Git 现在在 origin 调用另一个 Git 并获得现有的提交(不管确实有多少;我只是假设四个,一个分支名称,我假设是 @ 987654358@)。您的 Git 重命名它们的分支名称,以制作您的远程跟踪名称,方法是在每个名称前添加origin/2

您现在在本地存储库中拥有:

E   <-- master

A--B--C--D   <-- origin/master

我们现在已准备好继续,但 下一步 步骤取决于您是想要 linear 历史记录还是 branch-y / merge-y历史


1这需要一个非常现代的 Git,这样你就可以使用 git init 的参数——这不在你原来引用的命令中,所以我猜你没有这样做— 或配置初始分支名称。

2从技术上讲,您的 Git 将这些远程跟踪名称放入一个完全独立的 namespace,这样即使您将(本地)分支命名为 origin/fooGit em> 不会将其与重命名的 foo-from-origin 混淆,后者成为远程跟踪名称 origin/foo。您将只有两个名称​​显示为origin/foo,这会让人类感到困惑,但Git 会很好。但是不要那样做,太混乱了。


要创建一个合并历史,我们将使用git merge

假设你想要:

A--B--C--D
          \
           M   <-- master
          /
         E

这和我之前画的一样,我只是把E滑得更远了;我们也可以将E 放在第一行,和/或将其绘制为:

          E
           \
A--B--C--D--M   <-- master

或:

        E--M   <-- master
          /
A--B--C--D

当然,我们可以随时将分支名称从 master 更改为 mainzebrazorg 或其他任何名称,因为 Git 对实际的 名称时间>。只是选择一个并坚持下去是个好主意,to avoid confusing humans

不过,有一两个问题:

git merge,从 Git 2.9 版开始,默认拒绝合并“不相关的历史”。我们可以通过--allow-unrelated-histories 克服这个问题(假设 Git 版本为 2.9 或更高版本;在 2.9 之前,此选项不存在但不是必需的)。 合并不相关的历史记录令人担忧。

有一个对我们有利的非常大的逃生舱口,不过,对于后者:-s ours策略3这个策略告诉 Git:完全忽略正在合并的提交中的文件。请改用我们提交的文件。

所以:

git merge -s ours origin/master

(假设您的远程跟踪名称,在您的存储库中,是origin/master)生成我们想要的图形。那么:

git push -u origin master

立即工作并将我们的两个新提交EM 发送到origin,他们将其添加到他们的 master,以便他们的master 标识提交M 通过其原始哈希 ID,就像我们的 master 通过其原始哈希 ID 识别提交 M

(请注意,commits 是共享的。我们和他们使用 相同的提交,具有相同的哈希 ID。分支名称是不共享:我们有我们的,他们也有他们的。我们现在安排将 相同的哈希 ID 存储到我们的 master 中,就像他们存储在 master 中一样,但这些都是独立的,并且会随着时间的推移, 独立进化,直到我们故意再次同步它们。每个新的提交都会获得一个新的、唯一的哈希 ID,因此两个 Git 总是很容易彼此共享 new 提交:它们总是有唯一的哈希 ID,无论它们是在哪里进行的。如果两个提交已经共享,则它们只有 相同 哈希 ID,并且一个 Git 已经从另一个 Git 获得了该提交。)


3不要与-X ours 扩展策略选项混淆,后者在此处根本不起作用。 git merge-s 选项提供了一种策略。将 -X 选项传递给 git merge 将其参数 传递给 该策略,作为一个选项。 The git merge documentation 将此称为 strategy-option,这很容易与 -s strategy 选项混淆。所以我说:称它为扩展选项或扩展策略选项。


要创造线性历史,我们必须更加努力

如果我们希望历史类似于:

A--B--C--D--E

我们遇到了问题,因为我们已经提交了E,并且任何现有提交的任何部分都无法更改。

不过,提交E 的真实哈希 ID 是一些看起来很丑的随机哈希 ID,没有人能记住,并且目前只存在于我们的本地存储库中。假设我们将现有的提交 E 并将其复制到一个稍微不同的、经过改进的新提交 E',我们将绘制如下:

E   <-- master

           E'  <-- new-master
          /
A--B--C--D   <-- origin/master

现在我们重命名我们现有的masterold-master,我们现有的new-mastermaster

E   <-- old-master

           E'  <-- master
          /
A--B--C--D   <-- origin/master

然后,一旦我们确定我们喜欢结果,我们就完全删除名称old-master。提交本身会在我们的存储库中逗留一段时间——默认情况下至少 30 天,尽管这会有点复杂4——但最终它会消失。由于没有人能够记住旧的哈希 ID,并且 git log 不再显示它,我们可以假装我们从未有过 E 并且 E' 是称为E,或者其他什么,这给了我们一个简单的线性历史。不过我会把它画成E'

A--B--C--D--E'  <-- master

origin/master 仍然指向D;我只是没有它)。

然后我们可以运行git push origin masterE' 添加到他们的 master,就像我们运行git push origin master 以添加E-and-M 一样分支-y 案例。但是我们仍然必须以某种方式制作E'

最简单的方法是使用git commit-treegit read-tree,它们都是plumbing 命令并且相当专业。我将在这里的示例中使用git read-tree

git branch -m master old-master
git checkout -b master origin/master
git read-tree -m -u old-master
git commit -C master
git log                     # inspect the result
git push origin master      # note: `-u` not required
git branch -D old-master    # whenever you like

下面是这个序列的解释;首先,让我们把脚注去掉。


4坚持是基于三个因素。前两个是 object minimum lifetime,默认为 14 天,以及 reflog 条目生命周期,默认为 30 天。当我们删除 branch 时,我们也删除了 branch 的 reflog。有些人认为这是 Git 中的一个错误,并且有一个非常落后的项目,或者至少是一个想法来改变这一点,但现在确实如此。但是,Git 中的 HEAD 名称有自己独立的 reflog,默认情况下,that reflog 将在我们提交后将提交保留 30 天。第三个因素是 git gc --auto,各种 Git 命令自动为您运行,在 Git 认为它可能有利可图之前,它实际上并没有运行 git gc。很难准确预测什么时候会发生。


解释

分支重命名很简单:

git branch -m master old-master

这会将我们现有的 master 重命名为 old-master。 (由于我们开启master,我们可以运行git branch -m old-master;出于习惯,我输入了完整的命令,上面是剪切和粘贴。)

下一个命令同样简单:

git checkout -b master origin/master

这会在本地创建一个新的 master 分支名称,使用 origin/master 作为提交哈希 ID。这给了我们一个指向提交 D 的指针(假设在 origin/master 上有四个提交)。作为副作用,新的master 已经有一个上游 集,即origin/master

现在我们想要获取 Git 的 index 和我们的 工作树 来匹配提交 E。这就是时髦的git read-tree 命令的用武之地:

git read-tree -m -u old-master

git read-tree 命令很复杂,是 Git 真正的主力之一,但它非常古老:它实现了 part of git mergepart of git checkout .在这里,我们使用它来运行 git checkout 的等效项,而根本不更新 当前分支名称。完成同样事情的更长但更清晰的方法是使用git rm -rf . 删除每个文件,然后使用git checkout old-master -- . 重新填充Git 的索引和我们的工作树。如果git read-tree 永远消失,那将是做我们想做的事的一种方式。 (另一种方法是使用git checkout,后跟git symbolic-ref,另一种方法是使用git commit-tree,后跟git resetgit merge --ff-only:本研讨会中有许多工具可以让您从A 点到B点)

我们使用的git commit 几乎是普通的git commit,只是添加了一个选项,即-C old-master

git commit -C old-master

-C 选项告诉 Git 从指定的提交中获取 提交消息。在这里,我们将git commit 指向现有的提交E 以获取提交消息。如果愿意,您可以省略 -C 选项,只需输入全新的提交消息即可。

最终的git push 与任何git push 一样,但是因为我们创建了master,其上游已经设置为origin/master,所以我们不需要分支中需要的-u 选项- y / merge-y 构造。

哪个更好:线性历史还是分支/合并?

巧克力冰淇淋和草莓哪个更好吃?学放风筝还是学开直升飞机哪个更好?这部分是意见问题,部分是您想去哪里以及如何到达那里的问题,n'est-ce pas?

【讨论】:

这个超级有深度,感谢您花时间回答!【参考方案2】:

首先在新文件夹中执行 git clone,这会将所有获取的存储库复制到本地计算机,并将其设为本地存储库。现在将新文件夹复制粘贴到该本地存储库中,这将覆盖从 git 下载的文件夹。现在做一个 git add。然后说 git commit 和 git push。

【讨论】:

当历史无关时,拉动会有所帮助吗?不会报错吗? 将其编辑为 git clone。当您执行 git clone 时,它​​还会创建一个名为 .git 的文件夹,然后当您在覆盖文件夹后再次提交时,它会记住历史记录。

以上是关于如何将代码从新的本地文件夹推送到现有 Github 存储库的主分支并保留提交历史记录?的主要内容,如果未能解决你的问题,请参考以下文章

将本地的代码推送到公网的github账号去

Git 提交将本地项目推送到github上

将本地项目首次推送到github

进行后续更改后如何将最后一次提交推送到 Github?

text 将本地存储库推送到现有存储库

将已有项目推送到github上