如何在 git filter-branch 上为修剪空或子目录过滤器保留标签

chi*_*.de 6 git-filter-branch git-tag

git filter-branch --tag-name-filter cat …通过使用--prune-empty和/或--subdirectory-filter=…您将进入案例重写历史记录时,标记的提交将被删除。到目前为止,这是合理的,并且按设计工作。

问题/目标

我现在想要存档的是:保留附近重写提交的标签

例子:

从...开始 A -> B(tag: foo) -> C -> D -> E

(其中 E 比 D 新,比 C 新……)

跑步git filter-branch我得到

  • 得到A' -> B'(tag: foo)' -> E ( ^ 好案例)

  • 或:(A' -> D' -> E' ^ 坏情况)

我想要得到的是:A'(tag: foo)' -> D' -> E' 因为A'代表了被标记的内容B

一些研究: 我遇到的第一件事是git cherryGit 中:有没有办法找出提交是从哪里挑选出来的?但这似乎对发现差异没有太大帮助,因为树木是不相交的。

相反,我已经找到了一个有用的示例--commit-filter /sf/answers/1034837401/来写重写对象的日志

一些想法:考虑--commit-filter“映射文件”,理论上我可以

  1. 过滤所有未在重写树中设置的标签
    • 找不到如何为该信息过滤树
  2. 迭代有疑问的标签列表
  3. 读取原始提交点 git log --oneline -1 ${tag}
  4. 查找原始树的历史记录以查找任何已知要重写的较新提交
    • 向前查找也很难
    • 或者从任何重写的提交中查找历史以找到标签
  5. 将标签移动到新树中的第一个匹配项
    • 已知问题:如何保存所有信息,我不想重新标记经典方式
  6. 跳过标签,如果只有在另一个标签之后重写的提交
    • 如何确定有问题的提交有标签

我的其他想法是:

  • 通过git log -1 --format="%an%ae%at%cn%ce%ct%s" | sha1sum在原始树中进行比较来找到任何“类似”提交,然后遍历历史记录到下一个已知标签,但这与上面的想法很接近

听起来仍然很困难,即使我没有解决这些步骤的好主意......任何其他想法或已知解决方案(?!)欢迎!

tim*_*kro 2

Deleted:           *    *         *                   *    *         *
Tags:              R    S    T    U                        V         W
Commits: A -> B -> C -> D -> E -> F -> G -> H -> I -> J -> K -> L -> M -> N
Run Code Online (Sandbox Code Playgroud)

预期输出:

Tags:         R    T              V    W
Commits: A -> B -> E -> G -> H -> I -> L -> N
Run Code Online (Sandbox Code Playgroud)

我们将对此进行测试--prune-empty,以便为应删除的提交创建空提交。让我们设置测试存储库。

git init

touch n && git add n && git commit -m "N"
git commit --allow-empty -m "M"
touch l && git add l && git commit -m "L"
git commit --allow-empty -m "K"
git commit --allow-empty -m "J"
touch i && git add i && git commit -m "I"
touch h && git add h && git commit -m "H"
touch g && git add g && git commit -m "G"
git commit --allow-empty -m "F"
touch e && git add e && git commit -m "E"
git commit --allow-empty -m "D"
git commit --allow-empty -m "C"
touch b && git add b && git commit -m "B"
touch a && git add a && git commit -m "A"

git tag W $(git log --pretty=oneline --grep=M | cut -d " " -f1)
git tag V $(git log --pretty=oneline --grep=K | cut -d " " -f1)
git tag U $(git log --pretty=oneline --grep=F | cut -d " " -f1)
git tag T $(git log --pretty=oneline --grep=E | cut -d " " -f1)
git tag S $(git log --pretty=oneline --grep=D | cut -d " " -f1)
git tag R $(git log --pretty=oneline --grep=C | cut -d " " -f1)
Run Code Online (Sandbox Code Playgroud)

首先,我们将创建一个包含所有标签名称及其指向的提交哈希的文件。

for i in $(git tag); do echo $i; git log -1 --pretty=oneline $i | cut -d " " -f1; done > ../tags
Run Code Online (Sandbox Code Playgroud)

运行时git filter-branch提交哈希值将会改变。为了跟踪这些更改,我们创建一个文件,其中包含从旧提交哈希值到新提交哈希值的映射。这里展示了实现这一点的技巧。

--subdirectory-filter=...命令将如下所示:

git filter-branch --subdirectory-filter=... --commit-filter 'echo -n "${GIT_COMMIT}," >>/tmp/commap; git commit-tree "$@" | tee -a /tmp/commap'
Run Code Online (Sandbox Code Playgroud)

由于该--prune-empty选项与--commit-filter我们需要更改一些内容。帮助文档--prune-empty在这里:

某些过滤器将生成空提交,使树保持不变。此选项指示 git-filter-branch 删除此类提交(如果它们恰好有一个或零个未修剪的父项);因此,合并提交将保持不变。此选项不能与 一起使用,尽管通过在提交过滤器中--commit-filter使用提供的函数可以实现相同的效果。git_commit_non_empty_tree

因此,--prune-empty我们将用于此测试的命令如下所示。在运行该命令之前,请确保它/tmp/commap不存在或为空。

git filter-branch --commit-filter 'echo -n "${GIT_COMMIT}," >>/tmp/commap; git_commit_non_empty_tree "$@" | tee -a /tmp/commap'
mv /tmp/commap ../commap
Run Code Online (Sandbox Code Playgroud)

现在我们运行git filter-branch并收集了处理标签所需的所有信息。我们必须删除标签,并且必须更改提交标签指向的位置。我们很幸运,git 将标签指向的提交哈希简单地存储在.git/refs/tags/TAGNAME.

现在剩下的就是编写一个脚本来自动更正标签。这是我用 Python 写的。

def delete(tagname):
    print('git tag -d {}'.format(tagname))

def move(tagname, tagref):
    print('echo "{}" > .git/refs/tags/{}'.format(tagref, tagname))

tags = {}
with open('tags') as tagsfile:
    for i, line in enumerate(tagsfile):
        if i%2 == 0:
            tagname = line[:-1]
        else:
            # if there are multiple tags on one commit
            # we discard all but one
            tagref = line[:-1]
            if tagref in tags:
                delete(tags[tagref])
            tags[tagref] = tagname

commap = []
with open('commap') as commapfile:
    for line in commapfile:
        old, new = line[:-1].split(',')
        commap.append((old, new))

lastnew = None
takentag = None
for old, new in commap:
    if old in tags:
        if takentag:
            delete(takentag)
        takentag = tags[old]
    if new != lastnew:
        # commit was not deleted
        if takentag:
            move(takentag, new)
            takentag = None
    lastnew = new
Run Code Online (Sandbox Code Playgroud)

该脚本输出调整标签所需的命令。在我们的示例中,这是输出:

echo "0593fe3aa7a50d41602697f51f800d34b9887ba3" > .git/refs/tags/W
echo "93e65edf18ec8e33e5cc048e87f8a9c5270dd095" > .git/refs/tags/V
git tag -d U
echo "41d9e45de069df2c8f2fdf9ba1d2a8b3801e49b2" > .git/refs/tags/T
git tag -d S
echo "a0c4c919f841295cfdb536fcf8f7d50227e8f062" > .git/refs/tags/R
Run Code Online (Sandbox Code Playgroud)

将命令粘贴到控制台后,git 存储库看起来符合预期:

$ git log
8945e933c1d8841ffee9e0bca1af1fce84c2977d A
a0c4c919f841295cfdb536fcf8f7d50227e8f062 B
41d9e45de069df2c8f2fdf9ba1d2a8b3801e49b2 E
6af1365157d705bff79e8c024df544fcd24371bb G
108ddf9f5f0a8c8d1e17042422fdffeb147361f2 H
93e65edf18ec8e33e5cc048e87f8a9c5270dd095 I
0593fe3aa7a50d41602697f51f800d34b9887ba3 L
5200d5046bc92f4dbe2aee4d24637655f2af5d62 N
$ git tag
R
T
V
W
$ git log -1 --pretty=oneline R
a0c4c919f841295cfdb536fcf8f7d50227e8f062 B
$ git log -1 --pretty=oneline T
41d9e45de069df2c8f2fdf9ba1d2a8b3801e49b2 E
$ git log -1 --pretty=oneline V
93e65edf18ec8e33e5cc048e87f8a9c5270dd095 I
$ git log -1 --pretty=oneline W
0593fe3aa7a50d41602697f51f800d34b9887ba3 L
Run Code Online (Sandbox Code Playgroud)