只使用本地存储库历史记录的子集创建GitHub存储库

46
背景:我正在逐渐开源个人研究代码,这是我已经工作了两年多的项目。它最初是一个SVN存储库,但我一年前转移到了Git,并且我想在GitHub上分享代码。然而,多年来它积累了很多垃圾代码,我希望公共版本从当前状态开始。不过,我仍然想为其做出贡献并整合其他人的潜在贡献。
问题:是否有一种方法可以“分叉”Git存储库,使得分叉(位于GitHub上)上没有保留任何历史记录,但我的本地存储库仍具有完整的历史记录,并且我可以向GitHub推送/拉取?
我没有任何大型存储库管理方面的经验,所以详细信息将非常感激。

好的,我想我现在有一个不错的标题了。我期待着这个问题的答案。如果可行的话,我一定会学到一些 git 的技巧。 - R. Martinho Fernandes
@Martinho 我认为你想要学习的是嫁接技术! - Brian Campbell
实际上,看起来我不得不学习一些Git魔法才能提供这个问题的最简单答案。在回答这个问题的过程中,我学到了两个新功能! - Brian Campbell
我的回答对你有帮助吗?如果我在任何地方让你感到困惑,我很乐意进行澄清。我写作的前提是你已经知道如何在GitHub上创建仓库并将其推送,但如果你不知道,我可以在我的回答中添加这一部分。 - Brian Campbell
@Brian:我刚看到了你的回答,看起来很不错,但我还没有时间测试它,今晚会测试一下。(我可能只是在本地克隆我的存储库来检查它,因为我还没有准备好将整个项目推送到GitHub。)谢谢! - Seth Johnson
3个回答

69

您可以在Git中轻松创建一个新的、全新的历史记录。假设您想要将您的 master 分支作为要推送到GitHub的分支,而您希望完整的历史记录存储在 old-master 中。您只需要将您的 master 分支移动到 old-master,然后使用git checkout --orphan开始一个没有历史记录的全新分支即可:

git branch -m master old-master
git checkout --orphan master
git commit -m "Import clean version of my code"

现在你有一个没有历史记录的新的master分支,你可以将其推送到GitHub。但是,正如你所说,你希望能够在本地仓库中看到所有的旧历史记录;并且可能希望不要断开连接。

你可以使用git replace来实现这一点。替换引用是一种指定任何时候Git查看给定提交的替代提交的方法。因此,当查看历史记录时,你可以告诉Git查看旧分支的最后一次提交,而不是新分支的第一次提交。为了做到这一点,你需要将旧版本库中的断开的历史记录引入。

git replace master old-master

现在您有了新的分支,其中可以看到您所有的历史记录,但实际的提交对象与旧历史记录断开连接,因此您可以将新提交推送到GitHub而不会连带旧提交一起推送。将master分支推送到GitHub,只有新的提交会被推送到GitHub。但是在gitkgit log中查看历史记录,您将看到完整的历史记录。

git push github master:master
gitk --all

需要注意的地方

如果你基于旧的提交记录创建新分支,你需要小心保持这些历史记录的独立性。否则,在这些分支上进行新的提交将会包含旧的提交记录,并且如果你将其推送到GitHub,就会一起推送整个历史记录。只要确保所有的新提交都是基于你的新master而产生的,你就不用担心这个问题。

如果你运行git push --tags github,那么它会推送所有标签,包括旧的标签,这将导致所有旧的历史记录被一起推送。你可以通过删除所有旧的标签(git tag -d $(git tag -l)),或者从未使用git push --tags,仅手动推送标签,或者像下面描述的那样使用两个存储库来解决这个问题。

这两个注意事项的基本问题是,如果你推送任何与旧历史记录相关联的引用(除了通过替换提交之外的引用),你将推送所有旧的历史记录。避免这种情况最好的方法可能是使用两个存储库:一个只包含新提交,另一个包含旧和新的完整历史记录,用于检查完整的历史记录。你在存储只有新提交的存储库中执行所有工作、提交、从GitHub推送和拉取;这样,你就不可能意外地将旧的提交推送出去。

然后,每当需要查看完整历史记录时,你可以将所有新提交拉到具有完整历史记录的存储库中。你可以从GitHub或本地存储库中拉取,哪个更方便就使用哪个。它将成为你的归档,但为了避免意外发布旧历史记录,你不会从中向GitHub推送任何东西。以下是设置方法:

~$ mkdir newrepo
~$ cd newrepo
newrepo$ git init
newrepo$ git pull ~/oldrepo master
# 现在 newrepo 只有新的历史记录;我们可以设置 oldrepo 从中拉取
newrepo$ cd ~/oldrepo
oldrepo$ git remote add newrepo ~/newrepo
oldrepo$ git remote update
oldrepo$ git branch --set-upstream master newrepo/master
# ... 在 newrepo 中完成工作、提交、推送到GitHub等操作
# 现在如果我们想在 oldrepo 中查看完整历史记录:
oldrepo$ git pull

如果你使用的 Git 版本早于 1.7.2

你没有 git checkout --orphan命令,所以你需要通过从现有存储库的当前修订版本创建新存储库,然后拉取旧的断开连接的历史记录来手动完成此操作。你可以使用以下命令:

oldrepo$ mkdir ~/newrepo
oldrepo$ cp $(git ls-files) ~/newrepo
oldrepo$ cd ~/newrepo
newrepo$ git init
newrepo$ git add .
newrepo$ git commit -m "Import clean version of my code"
newrepo$ git fetch ~/oldrepo master:old-master

如果你使用的 Git 版本早于 1.6.5

git replace 和替换引用是在 1.6.5 中添加的,所以你需要使用一个较旧但有些不够灵活的机制,称为grafts,它允许你为

echo $(git rev-parse master) $(git rev-parse old-master) >> .git/info/grafts

这将使得在本地,master 提交看起来像是以 old-master 提交为其父提交,因此您将看到比使用 git replace 更多的提交。


1
你应该优先使用 git replace 而不是 grafts;我相信后者已经被弃用了。http://progit.org/2010/03/17/replace.html http://www.kernel.org/pub/software/scm/git/docs/git-replace.html - Emil Sit
1
git checkout --orphan(Git 1.7.2及更高版本)省去了额外的存储库(以及复制文件等操作):git branch -m master old-master && git checkout --orphan master && git commit -m 'initial public release' - Chris Johnsen
1
啊,是的,那是个坑。我会编辑一下并提到标签,这样其他看到这篇帖子的人就不会被绊倒了。 - Brian Campbell
@Zaz 基本上,在 Git 中,如果提交没有父节点,则被视为 broken 状态。为了方便克隆大型存储库而不必带上所有历史记录,您可以进行浅层拉取,但这样的存储库只能执行完整历史记录的子集。Git 不支持推送浅层历史记录,因为一旦与其他人共享历史记录,而不仅仅是获取最新版本供自己使用,确实需要完整的历史记录,否则很多事情都无法正常工作。 - Brian Campbell
1
@user2375667 在这个例子中,设立newrepo是在你设置old-master和新孤立的master分支于oldrepo之后进行的步骤。这样,oldrepo就拥有了完整的历史记录(可以从newrepo拉取新的提交),而newrepo则是你进行新工作并推向GitHub的地方,以避免意外推送一些旧的历史记录。 - Brian Campbell
显示剩余8条评论

2

Brian的回答 看起来很全面且专业,但有点复杂。

更简单的解决方案是保持两个仓库。

一个私有的GitHub仓库,你在上面工作。你把所有完整历史记录推送到该仓库。

第二个仓库是一个公共的GitHub仓库,你只在想要“发布”新版本时才将其发布到该仓库。你可以使用简单的差异+补丁,然后提交+推送来发布它。


3
虽然我的解决方案设置略微复杂(虽然并不是很复杂;我提供了相当详细的解释,并针对旧版本的Git提供了几种替代方法,看起来很复杂,但实际上非常简单),但一旦设置完成,它就要简单得多。在我的解决方案中,您只需像平常一样提交和推送,不需要额外工作将每个更改应用于一个存储库,然后再作为差异+补丁应用于新存储库。 - Brian Campbell
1
@Brian 谢谢。我猜你的帖子表达得不够清楚。我相信你所说的,并且认为我的观点是错误的。 - Guy

1

一个非常简单有趣的方法如下 -

假设您在REPO-A中有提交C1到C10,其中C1是初始提交,而C10是最新的HEAD。您想创建一个新的REPO-B,使其具有提交C4到C8(一个子集)。

注意:使用此方法将更改提交SHA(例如,在此情况下,将更改为C4'至C8'),但每个提交所包含的更改将保持不变,并且您的第一个提交现在将从所有更改开始 您之前的提交到该点组合起来。

我应该怎么做?


递归地复制本地机器上的所有内容。
cp -R REPO-A REPO-B

可选择从REPO-B中删除所有远程存储库,因为您很可能希望将其用作独立的代码库。

cd REPO-B
git remote -v
git remote remove REMOTE_NAME

强制将分支指针移动到您的子集的后端。对于C4到C8,这将是C8。但很可能您需要子集直到HEAD(例如从C4到C10或从C6到C10),在这种情况下,下面的步骤不是必需的。
git checkout -b temp
git branch -f master C8
git checkout master
git branch -D temp

在文件 .git/info/grafts 目录中输入您的子集中较早一端的提交 SHA。在本例中,它是提交 C4 的 SHA。
git rev-parse --verify C4 >> .git/info/grafts

不使用任何参数进行 Git 分支过滤:

git filter-branch

如果那不起作用:

git filter-branch --all

现在,如果您想要,可以将此推送到单独/新的远程位置:
git remote add origin NEWREMOTE
git push -u origin master

它是如何工作的?

这个链接告诉你如何实际操作 - http://git.661346.n2.nabble.com/how-to-delete-the-entire-history-before-a-certain-commit-td5000540.html

你可以在 git-filter-branch(1) 手册、gitrepository-layout(5) Git 仓库布局说明和 gitglossary(7) Git 词汇表中了解有关嫁接的信息。

简而言之,.git/info/grafts 中的每一行都包含一个对象的 SHA-1 ID,后面是其有效(嫁接)父级的用空格分隔的列表。因此,要剪切历史记录,例如在提交 a3eb250f996bf5e 后,您需要在 .git/info/grafts 文件中放入一个只包含此 SHA-1 的行,例如:

$ git rev-parse --verify a3eb250f996bf5e >> .git/info/grafts


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接