Git 如何";“重新设置标签的基址”;用吉特?

Git 如何";“重新设置标签的基址”;用吉特?,git,rebase,git-rebase,git-tag,Git,Rebase,Git Rebase,Git Tag,假设我有以下简单的git存储库:一个分支,一些提交一个接一个,其中两个在提交每个分支后都被标记(带有注释的标记),然后有一天我决定更改第一次提交(顺便说一句,如果这改变了什么,那么没有标记)。因此,我运行了git-rebase--interactive--root,只需为初始提交标记“edit”,在其中更改一些内容,然后git-rebase--continue。现在,我的存储库中的所有提交都已重新创建,因此它们的sha1已更改。但是,我创建的标记完全没有改变,仍然指向以前提交的sha1 是否有一

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

是否有一种自动方式将标记更新到重定基时创建的相应提交


有些人建议使用
git filter branch--tag name filter cat--tags
,但首先警告我,我的每个标记都没有改变,然后说我的每个标记都更改为它们自己(相同的标记名和相同的提交散列)。尽管如此,
gitshow--tags
说这些标记仍然指向旧的提交。

从某种意义上说,已经太晚了(不过,等一下,有个好消息)。
filter分支
代码能够调整标记,因为它在过滤过程中保持old-sha1到new-sha1的映射

事实上,
filter branch
rebase
都使用相同的基本思想,即通过扩展原始内容、进行任何所需的更改,然后对结果进行新的提交来复制每个提交。这意味着在每个复制步骤中,将这对数据写入一个文件是很简单的,然后一旦完成,就可以通过从旧的sha1中查找新的sha1来修复引用。完成所有引用后,您将使用新编号并删除映射

地图现在已经不见了,因此“从某种意义上说,为时已晚”

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

让我们看看“重复”的想法。我们有一个任意形状的原始图G:

     o--o
    /    \
o--o--o---o--o   <-- branch-tip
 \          /
  o--o--o--o
我只绘制了原始根节点的共享图(现在它是一艘带起重机的帆船,而不是飞碟)。分享可能更多,也可能更少。一些旧节点可能完全未被引用,因此被垃圾收集(可能不是:重新登录应该使所有原始节点保持活动状态至少30天)。但在任何情况下,我们仍然有指向G'的某个“旧G部分”的标记,这些引用保证这些节点及其所有父节点仍然在新G'中

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

如果您重复它,这次您希望保留映射,特别是如果生成的图G“”没有完全重叠G',因为您现在需要的不是映射本身,而是映射的投影,从G到G'

我们只需给原始G中的每个节点一个唯一的相对地址(例如,“从提示中,找到父提交#2;从该提交中,找到父提交#1;从该提交…”),然后在G“”中找到相应的相对地址。这允许我们重建映射的关键部分

取决于原始重基的简单性,我们可能可以直接跳到这个阶段。例如,如果我们确定整个图形是在没有展平的情况下复制的(因此我们有两个独立的飞碟)那么G中标记
T
的相对地址就是我们想要的G'中的相对地址,现在使用该相对地址来创建一个指向复制提交的新标记就很简单了

基于新信息的大更新 使用原始图形完全线性的附加信息,以及我们复制了每个提交,我们可以使用一个非常简单的策略。我们仍然需要重建映射,但现在很容易,因为每个旧提交都有一个新提交,具有一定的线性距离(很容易用单个数字表示)从原始图形的任一端开始(我将使用距尖端的距离)

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

A <- B <- C ... <- Z   <-- master
将此ID保存在
$orig\u 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
$
前缀,因为这剩下的部分实际上应该放在脚本中,即使是一次性使用,以防打字错误和需要调整)

我们仍然需要编写两个
adj_anno_tag
adj_lightweight_tag
shell函数。不过,首先,让我们编写一个shell函数,根据旧ID生成新ID,即查找映射。如果我们使用真实的映射文件,我们将grep或awk作为第一个条目,然后打印第二个。使用sleazy sin然而,在旧的文件方法中,我们需要的是匹配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解析永远不会失败,但是我们可能应该检查这个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
}
更新带注释的标记比较困难,但我们可以
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
# 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
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))
}
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
}
$ vim $(git --exec-path)/git-filter-branch
#!/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