如何在git仓库中添加过去的提交记录?

26

我收到了一些源代码并决定使用git进行管理,因为我的同事使用了 mkdir $VERSION 等方法。尽管过去的代码目前似乎不重要,但我仍然希望将它放在git控制下以更好地理解开发过程。因此:

有没有便捷的方法将那些过去的版本放入我已经存在的git仓库中?当前没有远程仓库,所以我不介意重写历史记录,但当然会优先考虑需要考虑到远程仓库的解决方案,除非它比较复杂。如果能提供一个脚本,可以基于目录或者存档文件的历史记录,不需要任何交互就可以进行操作,则额外加分。


@Cupcake 谢谢,我没有看到你的链接通知。 - Tobias Kienzler
1
警告:Git 2.18(2018年第二季度)中已删除嫁接。请参见“什么是.git/info/grafts?”。 - VonC
4个回答

27

如果要导入旧的快照,你可以在Git的contrib/fast-import目录中找到一些有用的工具。或者,如果你已经将每个旧的快照放在一个目录中,你可以像这样做:

# Assumes the v* glob will sort in the right order
# (i.e. zero padded, fixed width numeric fields)
# For v1, v2, v10, v11, ... you might try:
#     v{1..23}     (1 through 23)
#     v?{,?}       (v+one character, then v+two characters)
#     v?{,?{,?}}   (v+{one,two,three} characters)
#     $(ls -v v*)  (GNU ls has "version sorting")
# Or, just list them directly: ``for d in foo bar baz quux; do''
(git init import)
for d in v*; do
    if mv import/.git "$d/"; then
        (cd "$d" && git add --all && git commit -m"pre-Git snapshot $d")
        mv "$d/.git" import/
    fi
done
(cd import && git checkout HEAD -- .)

然后将旧的历史记录提取到您的工作存储库中:
cd work && git fetch ../import master:old-history

一旦你把旧的历史记录和基于Git的历史记录放在同一个仓库中,你就有几个选项可以进行前置操作:嫁接和替换。

嫁接是一种每个仓库都有的机制,用于(可能是暂时地)编辑各种现有提交的父级关系。嫁接由$GIT_DIR/info/grafts文件控制(在gitrepository-layout manpage的“info/grafts”下有描述)。

INITIAL_SHA1=$(git rev-list --reverse master | head -1)
TIP_OF_OLD_HISTORY_SHA1=$(git rev-parse old-history)
echo $INITIAL_SHA1 $TIP_OF_OLD_HISTORY_SHA1 >> .git/info/grafts

将嫁接操作完成后(最初的初始提交没有任何父节点,嫁接操作为其提供了一个父节点),您可以使用所有正常的Git工具搜索和查看扩展历史记录(例如,git log现在应该会在您提交后显示旧的历史记录)。

嫁接操作的主要问题是它们仅限于您的存储库。但是,如果您决定它们应成为历史记录的永久部分,您可以使用git filter-branch来实现这一点(首先备份您的.git目录的tar / zip备份;git filter-branch将保存原始引用,但有时只使用普通备份更容易)。

git filter-branch --tag-name-filter cat -- --all
rm .git/info/grafts

替换机制是较新的特性(Git 1.6.5+),但可以在每个命令中基于需要禁用它们(git --no-replace-objects …),并且它们可以被推送以便更易于共享。替换功能适用于单独的对象(blob、tree、commit或带注释的tag),因此这种机制也更为通用。替换机制在git replace手册页中有详细记录。由于其通用性,所谓的“前置”设置涉及到了一些额外的操作(我们需要创建一个新的提交而不只是指定新的父提交):

# the last commit of old history branch
oldhead=$(git rev-parse --verify old-history)
# the initial commit of current branch
newinit=$(git rev-list master | tail -n 1)
# create a fake commit based on $newinit, but with a parent
# (note: at this point, $oldhead must be a full commit ID)
newfake=$(git cat-file commit "$newinit" \
        | sed "/^tree [0-9a-f]\+\$/aparent $oldhead" \
        | git hash-object -t commit -w --stdin)
# replace the initial commit with the fake one
git replace -f "$newinit" "$newfake"

分享这个替换并不是自动的。您需要推送refs/replace的一部分(或全部)来共享替换。

git push some-remote 'refs/replace/*'

如果您决定将替换操作永久化,使用git filter-branch(与嫁接一样;首先备份.git目录的tar/zip文件):

git filter-branch --tag-name-filter cat -- --all
git replace -d $INITIAL_SHA1

2
没有 git filter branch,无论是嫁接还是替换都不会真正改写历史(它们只会对提交 DAG 产生影响,就像它们已经改写了历史一样)。替换的好处在于:1)可以通过命令行参数或环境变量禁用;2)可以被推送/获取;3)可以作用于任何对象,而不仅仅是提交的父“属性”。能够推送替换使它们易于通过常规 Git 协议共享(您可以共享嫁接条目,但必须使用某些“out of band”机制(即不是 push/fetch)来传播它们)。 - Chris Johnsen
@Chris 我刚刚注意到一个旧版本的文件被删除了,而我没有这个文件,因此也不在我的历史记录中,有可能恢复这个文件吗?基本上我正在寻找如何从git的历史记录中删除敏感文件的相反操作。顺便说一下:使用嫁接时,删除发生在原始的初始提交处,使用替换则在第二个原始提交处进行... - Tobias Kienzler
1
阅读您的“undelete”问题,我发现所涉及的文件在快照中,但不在您的Git历史记录中。如果您有标签与Git历史记录的某些部分相关联,那么我会这样做:从原始的Git存储库(在任何过滤或重写之前;如果您没有纯备份/克隆,请参见refs/original/),使用filter-branch --tag-name-filter cat --tree-filter … -- --all(或--index-filter)将文件添加到您的历史记录中,同时重写其标签,然后进行嫁接/替换和git filter-branch --tag-name-filter cat -- --all以永久建立嫁接/替换。 - Chris Johnsen
谢谢,这太棒了!顺便提一下,你可以轻松地将过去的历史“建议”作为注释放在README文件或不引人注意的.gitignore文件中,然后将过去的部分记录为已合并,如果您无法发布经过精简的变基历史记录(这也不建议)。 - mirabilos
2
警告:Git 2.18(2018年第二季度)中已删除了嫁接。请参阅“.git/info/grafts是什么?”。 - VonC
显示剩余9条评论

3
如果您不想更改存储库中的提交,则可以使用移植覆盖提交的父级信息。这是Linux内核存储库在开始使用Git之前获取历史记录的方法。这条信息:http://marc.info/?l=git&m=119636089519572似乎是我能找到的最好的文档。您需要创建一系列与您的git之前的历史相关的提交,然后使用.git/info/grafts文件使Git使用该序列中的最后一个提交作为第一个使用Git生成的提交的父提交。

1
啊,是的,我明白了,谢谢。这在Chris Johnsen的回答中被详细地称为graft-option。 - Tobias Kienzler

2

当然,最简单的方法是创建一个新的git repo,将历史记录首先提交,然后重新应用旧repo的补丁。但我更喜欢一种通过自动化来节省时间的解决方案。


0

如果您只想永久合并两个存储库,最好的解决方案是从第二个存储库中导出所有提交(除了初始提交,该提交将创建存储库作为另一个存储库的延续)。

我认为这是最好的方法,因为当您按照Chris Johnsen所解释的步骤执行时,它将将第二个存储库上的初始提交转换为删除提交,从而删除多个文件。如果您尝试跳过初始提交,它将将您的第二个提交转换为删除所有文件的提交(当然,我不得不尝试一下)。我不确定它如何影响git在命令行中跟踪文件历史的能力,例如git log --follow -- file/name.txt

您可以导出第二个存储库的整个历史记录(除了第一个提交,该提交已经存在于第一个存储库中),并运行以下命令将其导入到第一个存储库中:

在您的第二个存储库上打开Linux命令行(以导出最新提交)。 ``` commit_count=$(git rev-list HEAD --count) git format-patch --full-index -$(($commit_count - 1)) ``` 将所有在第二个存储库根目录创建的git补丁`.patch`文件移动到名为`patches`的新目录中,在第一个存储库根目录的旁边。 现在,在您的第一个存储库上打开Linux命令行(以导入最新提交)。 ``` git am ../patches/*.patch ``` 如果应用git补丁时遇到问题,请运行`git am --abort`,然后查看 git:patch does not apply,并尝试像链接的StackOverflow问题建议的那样运行`git am ../patches/*.patch --ignore-space-change --ignore-whitespace`。

除了使用命令行中的 git,您还可以使用像 SmartGitGitExtensions 这样的 git 接口。

参考资料:

  1. https://www.ivankristianto.com/create-patch-files-from-multiple-commits-in-git/从Git中创建多个提交的补丁文件
  2. Git:如何为合并创建补丁?
  3. https://www.ivankristianto.com/create-patch-files-from-multiple-commits-in-git/从Git中创建多个提交的补丁文件
  4. 如何一次性应用多个git补丁
  5. https://davidwalsh.name/git-export-patchGit导出补丁的方法

为了完整起见,这里提供一个自动化的shell脚本,遵循Chris Johnsen的步骤来永久合并两个仓库。您需要在第一个仓库上运行此脚本,其中您想要将第二个仓库的历史记录集成到第一个仓库中,第二个仓库继续第一个仓库的历史记录。经过几个小时的实验,我发现这是最好的方法。如果您知道如何改进某些内容,请修复/共享/评论。

在运行此操作之前,请完全备份您的第一个和第二个仓库到.zip文件中。

old_history=master
new_history=master-temp

old_remote_name=deathaxe
old_remote_url=second_remote_url

git remote add $old_remote_name $old_remote_url
git fetch $old_remote_name
git branch --no-track $new_history refs/remotes/$old_remote_name/$old_history
git branch --set-upstream-to=origin/$old_history $new_history

# the last commit of old history branch
oldhead=$(git rev-parse --verify $old_history)

# the initial commit of current branch
# newinit=$(git rev-list $new_history | tail -n 2 | head -n -1)
newinit=$(git rev-list $new_history | tail -n 1)

# create a fake commit based on $newinit, but with a parent
# (note: at this point, $oldhead must be a full commit ID)
newfake=$(git cat-file commit "$newinit" \
        | sed "/^tree [0-9a-f]\+\$/aparent $oldhead" \
        | git hash-object -t commit -w --stdin)

# replace the initial commit with the fake one
# git replace <last commit> <first commit>
# git replace <object> <replacement>
git replace -f "$newinit" "$newfake"

# If you decide to make the replacement permanent, use git filter-branch
# (make a tar/zip backup of your .git directory first)
git filter-branch --tag-name-filter cat -- --all
git replace -d $newinit

git push -f --tags
git push -f origin $new_history

git checkout $old_history
git branch -d $new_history
git pull --rebase

参考资料:

  1. https://feeding.cloud.geek.nz/posts/combining-multiple-commits-into-one/
  2. https://mirrors.edge.kernel.org/pub/software/scm/git/docs/git-replace.html
  3. Bash中删除文件的最后一行
  4. 强制 "git push" 覆盖远程文件
  5. 当标签已存在于远程时,Git 强制推送标签

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