如何" rebase标签"在git?

时间:2015-11-05 00:02:16

标签: git rebase git-rebase git-tag

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

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

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

3 个答案:

答案 0 :(得分:12)

从某种意义上说,为时已晚(但坚持下去,有好消息)。 filter-branch代码能够调整标记,因为它在过滤期间保留了old-sha1到new-sha1的映射。

事实上,filter-branchrebase使用相同的基本思想,即每次提交都是复制,通过展开原始内容,进行任何所需的更改,然后从结果中进行新的提交。这意味着在每个复制步骤中,写下< old-sha1,new-sha1>是很容易的。配对文件,然后一旦完成,你可以通过从old-sha1中查找new-sha1来修复引用。完成所有引用后,您将提交新编号并删除映射。

地图现在已经消失了,因此,从某种意义上来说,它太晚了#34;

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

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

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

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

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

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

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

如果你重复一遍,这次你想要保留映射,特别是如果得到的图G&#39;&#39;并不完全重叠G&#39;,因为你现在需要的不是地图本身,而是这张地图的投影,从G到G&#39;。

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

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

基于新信息的大更新

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

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

A <- B <- C ... <- Z   <-- master

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

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

我们需要原始提示最多提交的SHA-1 ID。这应该很容易在(单个)分支的reflog中找到,并且可能只是master@{1}(尽管这取决于你从那时起调整分支的次数;以及如果有新的提交你添加了变基,我们也需要考虑这些因素。它也可能出现在特殊引用ORIG_HEAD中,如果你决定不喜欢rebase结果,git rebase会留下。{/ p>

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

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

会将此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

(两个文件的输出应该相同;如果没有,这里的一些假设出错了;同时我也会在下面留下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的shell版本 - 以获得映射)。但我们实际上并不需要绘图,我们所需要的只是那些距离提示的距离&#34;很重要,所以我假装我们没有在这里打扰。

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

# 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

我们仍然需要编写两个adj_anno_tagadj_lightweight_tag shell函数。首先,让我们编写一个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))
}

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
}

更新带注释的标签更加困难,但我们可以从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}相同的提交方式。

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

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

答案 1 :(得分:4)

感谢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

答案 2 :(得分:2)

您可以使用git rebasetags

您可以像使用git rebase

一样使用

git rebasetags <rebase args>

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

enter image description here

来自this post