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

How do I push code from a new local folder to an existing Github repository's main branch and keep commit history?

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

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

如何将我的本地文件夹连接到现有的 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 错误:未能将一些引用推送到远程

首先在新文件夹中进行 git 克隆,这会将所有获取的存储库复制到本地计算机,并使其成为本地存储库。现在将您的新文件夹复制粘贴到该本地存储库中,这将覆盖从 git 下载的文件夹。现在做一个 git 添加。然后说 git 提交和 git 推送。

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

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

graph 选项使 git log 粗略地 ASCII-art 尝试绘制您将在 some of the answers to Pretty Git branch graphs (a question, and set of answers, worth reading carefully several times, though it's quite long). I'm particularly fond of the output from gitdags 中看到的花哨图表教程,尽管它根本不适合自动化工作(它需要仔细地手工构建 LaTeX 输入)。

不过,我认为,想为你的案子做出的决定归结为只有一个问题:你想要线性历史,还是 branch-y 历史? 也就是说,假设 existing 存储库只有一个其中很少有我们可以绘制为 A-B-C-D 的提交,我们将调用您的 new 提交 E。你想让历史看起来像:

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

或者您希望它看起来像:

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

?

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

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

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

到目前为止,您已经完成了我们将称为 E 的提交,或者看起来几乎完全 与我们将称为 的提交完全相同的提交 [=44] =] 最终。在 你的 Git 存储库中定位此提交的分支名称几乎可以肯定是 master;如果您希望它成为 main,现在是重命名它的好时机:

git branch -m main

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

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

git fetch origin

或者只是:

git fetch

你的 Git 现在在 origin 调用另一个 Git 并获得现有的提交(无论实际有多少;我只假设四个,一个分支名称,我假设是 master)。您的 Git 重命名 他们的 分支 名称使您的 remote-tracking 名称,在每个名字前加上 origin/2

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

E   <-- master

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

我们现在可以继续了,但是 下一步 取决于您想要 线性 历史记录还是 branch-y / merge-y 历史.


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

2从技术上讲,您的 Git 将这些 remote-tracking 名称放入一个完全独立的 namespace 中,因此即使您命名一个 ( local) branch origin/foo, Git 不会将其与重命名的 foo-from-origin 混淆,成为 remote-tracking名字origin/foo。你将只有两个名字 显示为 origin/foo,这会混淆 humans,但是 Git 会没事的。但是不要那样做,太混乱了。


要创建 merge-y 历史,我们将使用 git merge

假设您想要:

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

这和我之前画的是同一张图,我只是把E滑得更远;我们也可以把E放在最上面一行,and/or画成:

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

或:

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

当然,我们可以随时将分支名称从 master 更改为 main,再更改为 zebrazorg 或其他名称,因为 Git 对实际的 name 没有兴趣。只是选择一个并坚持下去是个好主意,to avoid confusing humans

不过有一两个问题:

  • git merge,自Git version 2.9,默认拒绝合并“无关历史”。我们可以通过 --allow-unrelated-histories 来克服这个问题(假设 Git 版本为 2.9 或更高版本;在 2.9 之前,此选项不存在但不是必需的)。
  • 合并不相关的历史很麻烦。

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

所以:

git merge -s ours origin/master

(假设您的 remote-tracking 名称在您的存储库中是 origin/master)生成我们想要的图表。那么:

git push -u origin master

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

(请注意,提交是共享的。我们和他们使用相同的提交,具有相同的哈希ID。分支名称不共享:我们有我们的,他们有他们的。我们现在安排将相同的哈希ID存储到我们的master中,因为他们存储到它们的 master 中,但它们是独立的,并且随着时间的推移会独立发展,直到我们有意再次同步它们。每个新提交都会获得一个新的、唯一的哈希 ID,因此两个 Git 总是很容易s 彼此共享 new 提交:它们始终具有唯一的哈希 ID,无论它们是在哪里创建的。两次提交仅具有 相同的 哈希ID 如果它们已经共享,并且一个 Git 已经从另一个 Git 那里得到了那个提交。)


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


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

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

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

我们有一个问题,因为我们已经有提交E并且任何现有提交的任何部分都不能更改。

提交 E 的真实散列 ID,但是,是一些丑陋的大 random-looking 散列 ID,没人能记住,而且 只存在于我们的本地存储库中 此时。假设我们将现有的提交 E 复制 到一个稍微不同的 new-and-improved 提交 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 在 branch-y 的情况下添加 E-和-M。但是我们还是得想办法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

此序列的解释如下;首先,让我们去掉脚注。


4sticking-around是基于三个因素。前两个是 object 最小生命周期 ,默认为 14 天,以及 reflog 条目生命周期 ,默认为 30 天.当我们删除 分支 时,我们删除了分支的 reflog 也是。一些人认为这是 Git 中的错误,并且有一个 very-back-burner 项目,或者至少是想法,来改变它,但现在是这样。但是,Git 中的 HEAD 名称有其自己单独的 reflog,并且 that reflog 将从我们提交之日起将提交保留 30 天,通过默认。第三个因素是 git gc --auto,各种 Git 命令 运行 自动为您执行,实际上 运行 git gc 直到 Git 认为它可能会有利可图。很难准确预测那会是什么时候。


说明

分支重命名很简单:

git branch -m master old-master

这会将我们现有的 master 重命名为 old-master。 (因为我们 on master 我们可以 运行 git branch -m old-master; 我出于习惯输入了完整的命令,上面是cut-and-paste.)

下一个命令同样简单:

git checkout -b master origin/master

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

现在我们想要获取 Git 的 index 和我们的 working tree 以匹配提交 E .这是时髦的 git read-tree 命令的用武之地:

git read-tree -m -u old-master

git read-tree 命令很复杂,是 Git 真正的主力之一,但它非常古老:它实现了 部分 git merge 的一部分 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 选项,只输入 all-new 提交消息。

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

哪个更好:线性历史还是branchy/mergy?

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