如何在git中"rebase tags"?

23
假设我有一个简单的 git 存储库:一个分支,一些提交一个接一个地进行,其中几个被标记(使用注释标记),然后有一天我决定要更改第一个提交(顺便说一下,如果这会改变任何内容,则未标记)。所以我运行“git rebase --interactive --root”,只为最初的提交标记'edit',更改其中的一些内容并运行“git rebase --continue”。现在我的存储库中的所有提交都已被重新创建,因此它们的 sha1 已更改。但是,我创建的标记完全没有更改,仍然指向以前提交的 sha1。
有没有自动更新标记以对应重建时创建的提交的方法?
有些人建议使用“git filter-branch --tag-name-filter cat -- --tags”,但是它首先警告我每个标记都没有更改,然后说每个标记都更改为它们自己(相同的标记名称和相同的提交哈希)。而且,git show --tags 仍然显示标记仍然指向旧提交。

你是否与他人分享了你的标签,例如通过将它们push到共享存储库中? - Chris
@Chris:没有,我没有。 - user4256966
4个回答

20
在某种意义上,现在为时已晚了(但是请稍等,好消息会来的)。filter-branch代码能够调整标签,因为它在筛选过程中保留了旧SHA1到新SHA1的映射。
事实上,filter-branchrebase都使用了相同的基本思想,即通过扩展原始内容、进行任何所需更改并将结果制作成新提交来“复制”每个提交。这意味着在每个复制步骤中,将<old-sha1, new-sha1>对轻松写入文件,然后在完成后通过从其旧SHA1查找新SHA1来修复引用。一旦所有引用完成,您就可以承诺使用新编号,并删除映射。
地图现在已经消失了,因此“在某种意义上,现在为时已晚”。
幸运的是,还不算太晚。 :-) 您的变基是可重复的,或者至少,其中的关键部分可能是可重复的。此外,如果您的变基足够简单,您甚至可能不需要重复它。
让我们看看“重复”思想。我们有一个任意形状的原始图G:
     o--o
    /    \
o--o--o---o--o   <-- branch-tip
 \          /
  o--o--o--o

(哇,一个飞碟!) 我们对其进行了git rebase --root操作(针对其中的某个部分),复制了(一些或全部)提交记录(保留合并或不保留),以获得新的图形G':

    o--o--o--o   <-- branch-tip
   /
  /  o--o
 /  /    \
o--o--o---o--o
 \          /
  o--o--o--o

我只分享了原始根节点(现在是带有起重机的帆船,而不是飞碟)。可能会有更多或更少的共享。一些旧节点可能已经完全没有被引用,因此已经被垃圾回收(可能不会:reflog应该至少保留所有原始节点30天)。但无论如何,我们仍然有指向G'某些“旧G部分”的标记,而这些引用保证了那些节点及其所有父节点仍然存在于新的G'中。
因此,如果我们知道如何进行原始变基,我们可以在G'的子图上重复它,这是G的重要部分。这有多难或容易,以及要使用哪些命令来执行它,取决于原始G是否全部在G'中,变基命令是什么,G'覆盖了原始G的程度等等(因为git rev-list,它是获取节点列表的关键,可能没有办法区分“原始的,在G中”和“新的到G'的节点”)。但这可能是可以做到的:在这一点上,这只是一个小的编程问题。
如果你重复了它,这次你需要保留映射,特别是如果结果图G''没有完全重叠G',因为现在你需要的不是映射本身,而是从G到G'的投影。
我们只需要给原始G中的每个节点一个唯一的相对地址(例如,“从尖端找到父提交#2;从该提交,找到父提交#1;从该提交...”),然后在G''中找到相应的相对地址。这使我们能够重建地图的关键部分。
根据原始变基的简单性,我们可能能够直接跳转到此阶段。例如,如果我们确定整个图形被复制而没有展平(因此我们有两个独立的飞碟),则标记T在G中的相对地址就是我们在G'中想要的相对地址,现在使用该相对地址轻松地创建一个指向复制提交的新标记。
基于新信息的大更新
使用额外的信息,即原始图形完全是线性的,并且我们已经复制了每个提交,我们可以使用非常简单的策略。我们仍然需要重建映射,但现在很容易,因为每个旧提交都有一个新提交,它与原始图形的任一端具有某些线性距离(可以表示为单个数字)(我将使用距离-自顶向下)。
也就是说,旧图形看起来像这样,只有一个分支:
A <- B <- C ... <- Z   <-- master

这些标签简单地指向其中一个提交(通过带注释的标签对象),例如,也许标签foo指向指向提交W的一个带注释的标签对象。然后我们注意到W距离Z有四个提交。

新图形看起来和原来一模一样,只是每个提交都被其副本所取代。我们把它们称为A'B',以此类推,直到Z'。这(唯一的)分支指向最顶端的提交,即Z'。我们需要调整原始标签foo,使其指向W'

我们需要原始最顶端提交的SHA-1 ID。这应该很容易在(唯一的)分支的reflog中找到,可能只是master@{1}(尽管这取决于您自那时以来调整分支的次数;如果有您在重演之后添加的新提交,则我们还需要考虑它们)。它很可能也在特殊引用ORIG_HEAD中,这是git rebase留下的引用,以防您认为重演结果不好。

假设master@{1}是正确的ID,并且没有这样的新提交。那么:

orig_master=$(git rev-parse master@{1})

会在$orig_master中保存此ID。

如果我们想要构建完整的映射,可以这样做:

$ git rev-list $orig_master > /tmp/orig_list
$ git rev-list master > /tmp/new_list
$ wc -l /tmp/orig_list /tmp/new_list

两个文件的输出应该相同;如果不是,那么这里的某些假设就错了;同时,我在下面省略了shell的$前缀,因为即使只用一次,为了防止打字错误和需要调整,剩余部分也应该写成脚本形式。

exec 3 < /tmp/orig_list 4 < /tmp/new_list
while read orig_id; do
    read new_id <& 4; echo $orig_id $new_id;
done <& 3 > /tmp/mapping

(这个功能还没有经过充分测试)旨在将两个文件粘合在一起,类似于Python中zip函数操作两个列表并创建一个映射。但是我们实际上不需要这个映射,我们只需要“距离顶部”的计数,因此我假装我们在这里没有费心。

现在我们需要遍历所有标签:

# We don't want a pipe here because it's
# not clear what happens if we update an existing
# tag while `git for-each-ref` is still running.
git for-each-ref refs/tags > /tmp/all-tags

# it's also probably a good idea to copy these
# into a refs/original/refs/tags name space, a la
# git filter-branch.
while read sha1 objtype tagname; do
    git update-ref -m backup refs/original/$tagname $sha1
done < /tmp/all-tags

# now replace the old tags with new ones.
# it's easy to handle lightweight tags too.
while read sha1 objtype tagname; do
    case $objtype in
    tag) adj_anno_tag $sha1 $tagname;;
    commit) adj_lightweight_tag $sha1 $tagname;;
    *) echo "error: shouldn't have objtype=$objtype";;
    esac
done < /tmp/all-tags

我们仍需要编写两个 shell 函数adj_anno_tagadj_lightweight_tag。不过,首先让我们编写一个 shell 函数,以旧 ID 为参数返回新 ID,也就是查找映射关系。如果我们使用真实的映射文件,则可以使用 grep 或 awk 查找第一条记录,然后打印第二条记录。但是,由于使用了不太正规的单个旧文件方法,我们需要匹配 ID 的行号,可以使用grep -n命令实现:
map_sha1() {
    local grep_result line

    grep_result=$(grep -n $1 /tmp/orig_list) || {
        echo "WARNING: ID $1 is not mapped" 1>&2
        echo $1
        return 1
    }
    # annoyingly, grep produces "4:matched-text"
    # on a match.  strip off the part we don't want.
    line=${grep_result%%:*}
    # now just get git to spit out the ID of the (line - 1)'th
    # commit before the tip of the current master.  the "minus
    # one" part is because line 1 represents master~0, line 2
    # is master~1, and so on.
    git rev-parse master~$((line - 1))
}

警告情况不应该发生,rev-parse也不应该失败,但我们可能应该检查此shell函数的返回状态。

轻量级标签更新器现在非常简单:

adj_lightweight_tag() {
    local old_sha1=$1 new_sha1 tag=$2

    new_sha1=$(map_sha1 $old_sha1) || return
    git update-ref -m remap $tag $new_sha1 $old_sha1
}

更新已注释的标签比较困难,但是我们可以从 git filter-branch 中借鉴一些代码。我不会在这里引用所有的代码,只给你这一点:

$ vim $(git --exec-path)/git-filter-branch

需要执行以下指令:搜索第二个出现的git for-each-ref,并注意将git cat-file管道传递到sed,然后将结果传递给git mktag,以设置shell变量new_sha1

这就是我们需要复制标签对象的方法。新副本必须指向在旧标签所指向的提交上使用$(map_sha1)找到的对象。我们可以通过与filter-branch相同的方式,使用git rev-parse $old_sha1^{commit}来查找该提交。

(顺便说一下,编写本回答并查看filter-branch脚本时,我发现filter-branch中存在一个错误,我们将其引入到我们的后重定基标签修复代码中:如果现有的注释标签指向另一个标签,则不会进行修复。我们只修复轻量级标签和直接指向提交的标签。)

请注意,以上示例代码均未经过测试,并将其转换为更通用的脚本(例如,在任何重定基之后都可以运行,或更好的是,合并到交互式重定基本身中)需要进行大量额外的工作。


谢谢你的回答!我认为它比较笼统,但我的具体情况确实很简单。我已经更新了我的答案,提供了更多细节。我的问题不是如何获取旧的和新的sha1映射 - 即使我可以手动完成(如果标签数量很大,那将是不切实际的)。我的真正问题是如何使标签指向在rebase过程中创建的新提交,而不改变标签中的任何其他内容(日期、消息等)。显然,自动执行此操作的方法可能需要知道上述映射。 - user4256966
好的,听起来你的原始结构完全是线性的(没有需要担心的合并),而且你保留了所有原始提交,这使得“相对寻址”变得微不足道:从旧的 tip 到标签的距离与从新的 tip 到标签应该放置的位置的距离相同。主要剩下的问题是这些是否是带注释的标签还是轻量级标签。 - torek
没错,你说对了。它们是带注释的标签。那有什么关系呢? - user4256966
注释标签是实际对象,因此必须使用调整后的提交 ID 进行复制(或重新创建)。如果它们被签署,最简单的方法可能是从头开始重新创建它们,而不是复制它们。(然后,在任何一种情况下,都必须创建一个轻量级标签来指向某个地方,例如这里,指向新的注释标签对象;对于普通的轻量级标签,我们将轻量级标签指向变基提交。)我稍后会回来处理这个问题,现在我有一件事要办... - torek
我明白了。在我的情况下,它们没有签名。难道现有的注释标签不能直接指向新的提交对象吗?(不用着急,感谢您的跟进)。 - user4256966
显示剩余3条评论

8

你可以使用 git rebasetags

使用方法与 git rebase 相同

git rebasetags <rebase args>

如果rebase是交互式的,你将会看到一个bash shell,在那里你可以进行更改。退出该shell后,标签将被恢复。

rebasetags demo

源代码


5

感谢torek详细的步骤指导,我整合了一个实现。

#!/usr/bin/env bash
set -eo pipefail

orig_master="$(git rev-parse ORIG_HEAD)"

sane_grep () {
    GREP_OPTIONS= LC_ALL=C grep "$@"
}

map_sha1() {
    local result line

    # git rev-list $orig_master > /tmp/orig_list
    result="$(git rev-list "${orig_master}" | sane_grep -n "$1" || {
        echo "WARNING: ID $1 is not mapped" 1>&2
        return 1
    })"

    if [[ -n "${result}" ]]
    then
        # annoyingly, grep produces "4:matched-text"
        # on a match.  strip off the part we don't want.
        result=${result%%:*}
        # now just get git to spit out the ID of the (line - 1)'th
        # commit before the tip of the current master.  the "minus
        # one" part is because line 1 represents master~0, line 2
        # is master~1, and so on.
        git rev-parse master~$((result - 1))
    fi
}

adjust_lightweight_tag () {
    local old_sha1=$1 new_sha1 tag=$2

    new_sha1=$(map_sha1 "${old_sha1}")

    if [[ -n "${new_sha1}" ]]
    then
        git update-ref "${tag}" "${new_sha1}"
    fi
}

die () {
    echo "$1"
    exit 1
}

adjust_annotated_tag () {
    local sha1t=$1
    local ref=$2
    local tag="${ref#refs/tags/}"

    local sha1="$(git rev-parse -q "${sha1t}^{commit}")"
    local new_sha1="$(map_sha1 "${sha1}")"

    if [[ -n "${new_sha1}" ]]
    then
        local new_sha1=$(
            (
                printf 'object %s\ntype commit\ntag %s\n' \
                        "$new_sha1" "$tag"
                git cat-file tag "$ref" |
                sed -n \
                        -e '1,/^$/{
                    /^object /d
                    /^type /d
                    /^tag /d
                    }' \
                        -e '/^-----BEGIN PGP SIGNATURE-----/q' \
                        -e 'p'
            ) | git mktag
        ) || die "Could not create new tag object for $ref"

        if git cat-file tag "$ref" | \
                sane_grep '^-----BEGIN PGP SIGNATURE-----' >/dev/null 2>&1
        then
            echo "gpg signature stripped from tag object $sha1t"
        fi

        echo "$tag ($sha1 -> $new_sha1)"
        git update-ref "$ref" "$new_sha1"
    fi
}

git for-each-ref --format='%(objectname) %(objecttype) %(refname)' refs/tags |
while read sha1 type ref
do
    case $type in
    tag)
        adjust_annotated_tag "${sha1}" "${ref}" || true
        ;;
    commit)
        adjust_lightweight_tag "${sha1}" "${ref}" || true
        echo
        ;;
    *)
        echo "ERROR: unknown object type ${type}"
        ;;
    esac
done

0

git-rebase(1)非常适合保持分支最新并进行最近历史重写。但如果要更改某些非常基本的东西(例如根提交),然后让周围的所有内容更新自己,那么它就不太适用了。

我会使用git-filter-repo(1),因为这样您就不必担心在过程中丢失任何东西,而如果您不够勤奋,git-rebase(1)可能会这样做。

git checkout <root commit>
old_sha1=$(git rev-parse HEAD)
git commit --amend
new_sha1=$(git rev-parse HEAD)
git replace $old_sha1 $new_sha1
# It demands `--force` for a reason
git filter-repo --force

演示


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