如何在git中"rebase tags"?

10 git rebase git-rebase git-tag

假设我有以下简单的git存储库:一个分支,一些一个接一个地提交,其中一些已经在提交它们之后被标记(带有注释标签),然后有一天我决定我想要更改第一个提交(顺便说一句,如果这改变了什么的话,它没有被标记).所以我运行git rebase --interactive --root并为初始提交标记'edit',更改其中的内容git rebase --continue.现在我的存储库中的所有提交都已重新创建,因此它们的sha1已经更改.但是,我创建的标签完全没有变化,仍然指向先前提交的sha1.

是否有自动方式将标签更新为重新定位时创建的对应提交?

有些人建议使用,git filter-branch --tag-name-filter cat -- --tags但首先警告我,我的每个标签都没有改变,然后说我的每个标签都改为自己(相同的标签名称和相同的提交哈希).而且,仍然git show --tags说,标签仍然指向旧的提交.

tor*_*rek 14

从某种意义上说,为时已晚(但坚持下去,有好消息).该filter-branch代码能够调整标签,因为它保持,它的过滤,老-SHA1的映射到新-SHA1中.

事实上,无论是filter-branchrebase使用相同的基本的想法,这是每次提交被复制,通过扩大原有的内容,进行任何所需的更改,然后又做了新的提交出结果的.这意味着在每个复制步骤中将<old-sha1,new-sha1>对写入文件是微不足道的,然后一旦完成,通过从old-sha1查找new-sha1来修复引用.完成所有引用后,您将致力于新编号并删除映射.

地图现在已经消失了,因此"从某种意义上来说,已经太晚了".

幸运的是,现在还为时不晚.:-)你的rebase是可重复的,或者至少是它的关键部分.此外,如果您的rebase足够简单,您可能根本不需要重复它.

让我们来看看"重复"思想.我们有一个任意形状的原始图G:

     o--o
    /    \
o--o--o---o--o   <-- branch-tip
 \          /
  o--o--o--o
Run Code Online (Sandbox Code Playgroud)

(哇,一个飞碟!).我们已经完成了git rebase --root(某些部分)它,复制(部分或全部)提交(保留合并与否)以获得一些新图G':

    o--o--o--o   <-- branch-tip
   /
  /  o--o
 /  /    \
o--o--o---o--o
 \          /
  o--o--o--o
Run Code Online (Sandbox Code Playgroud)

我画的这只是原始的根节点(现在它是一艘带有起重机的帆船,而不是飞碟).可能会有更多共享,或更少.一些旧节点可能已经完全未被引用,因此被垃圾收集(可能不是:reflogs应该使所有原始节点保持活动至少30天).但无论如何,我们仍然有标签指向G'的一些"旧G部分",这些参考保证那些节点及其所有父节点仍然在新的G'中.

因此,如果我们知道原始rebase是如何完成的,我们可以在G'的子图上重复它,这是G的重要部分.这是多么困难或容易,以及使用什么命令来执行它,取决于所有原始G是否在G'中,rebase命令是什么,G'覆盖原始G多少,以及更多(因为git rev-list,这是我们获取节点列表的关键,可能无法区分"原始,在G中"和"新到G"节点.但它可能已经完成:此时它只是一个小编程问题.

如果你重复它,这次你想要保持映射,特别是如果结果图G''没有完全重叠G',因为你现在需要的不是地图本身,而是这个地图的投影,从G到G'.

我们只是给原始G中的每个节点一个唯一的相对地址(例如,"从提示中找到父提交#2;从该提交中,找到父提交#1;从该提交...")然后找到相应的G'中的相对地址.这允许我们重建地图的关键部分.

根据原始rebase的简单性,我们可能会直接跳到此阶段.例如,如果我们确定整个图形被复制而没有展平(这样我们就有两个独立的飞碟)那么TG中标签的相对地址就是我们想要的G'中的相对地址,现在使用它是微不足道的相对地址,以使新标记指向复制的提交.

基于新信息的大更新

使用原始图形完全线性的附加信息,以及我们复制每个提交的附加信息,我们可以使用非常简单的策略.我们仍然需要重建地图,但现在它很容易,因为每个旧的提交只有一个新的提交,它有一些线性距离(很容易表示为单个数字)从原始图的任一端(我会使用距离尖端).

也就是说,旧图形看起来像这样,只有一个分支:

A <- B <- C ... <- Z   <-- master
Run Code Online (Sandbox Code Playgroud)

标签只是指向其中一个提交(通过带注释的标签对象),例如,可能标记foo指向指向提交的带注释标签的对象W.然后我们注意到W有四个提交回来了Z.

新图表看起来完全相同,只是每个提交已被其副本替换.让我们称之为A',B'等等Z'.(单个)分支指向最尖端的提交,即Z'.我们要调整原始标记,foo以便我们有一个新的带注释标记的对象指向W'.

我们需要原始提示最多的SHA-1 ID.这应该很容易在(单个)分支的reflog中找到,并且可能很简单master@{1}(虽然这取决于你从那时起调整分支的次数;并且如果你自从重新定位后添加了新的提交,我们需要考虑到这些因素).它也可能在特殊的引用中ORIG_HEAD,git rebase如果你决定不喜欢rebase结果,它会留下.

我们假设这master@{1}是正确的ID,并且没有这样的新提交.然后:

orig_master=$(git rev-parse master@{1})
Run Code Online (Sandbox Code Playgroud)

将保存此ID $orig_master.

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

$ git rev-list $orig_master > /tmp/orig_list
$ git rev-list master > /tmp/new_list
$ wc -l /tmp/orig_list /tmp/new_list
Run Code Online (Sandbox Code Playgroud)

(这两个文件的输出应该是相同的;如果没有,这里的一些假设出错了;同时我也会$在下面留下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
Run Code Online (Sandbox Code Playgroud)

(这是非常未经测试的,意味着将两个文件粘贴在一起 - zip在两个列表上的一个shell版本的Python - 来获取映射).但我们实际上并不需要映射,我们只需要那些"距离小费"的数量,所以我假装我们没有在这里打扰.

现在我们需要迭代所有标签:

# 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
Run Code Online (Sandbox Code Playgroud)

我们仍然需要编写两个adj_anno_tagadj_lightweight_tagshell函数.首先,让我们编写一个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))
}
Run Code Online (Sandbox Code Playgroud)

WARNING案例永远不应该发生,并且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
}
Run Code Online (Sandbox Code Playgroud)

更新带注释的标签更加困难,但我们可以从中窃取代码git filter-branch.我不会在这里引用它; 相反,我只是给你这一点:

$ vim $(git --exec-path)/git-filter-branch
Run Code Online (Sandbox Code Playgroud)

和这些指令:搜索第二次出现git for-each-ref,并注意git cat-file管道sed传递给传递给的结果git mktag,它设置了shell变量new_sha1.

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

(顺便说一下,写下这个答案并查看过滤器分支脚本,我发现过滤器分支中存在一个错误,我们将导入到我们的post-rebase标签修复代码中:如果现有的带注释的标签指向到另一个标签,我们不修复它.我们只修复直接指向提交的轻量级标签和标签.)

请注意,上面的示例代码都没有实际测试过,并将其转换为更通用的脚本(例如,可以在任何rebase之后运行,或者更好,并入交互式rebase本身)需要相当多的额外的工作.


nac*_*ker 6

您可以使用 git rebasetags

您可以像使用一样使用 git rebase

git rebasetags <rebase args>

如果 rebase 是交互式的,您将看到一个 bash shell,您可以在其中进行更改。退出该外壳后,标签将被恢复。

在此处输入图片说明

这个帖子


小智 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
Run Code Online (Sandbox Code Playgroud)