如何在git中"rebase tags"?
How to "rebase tags" in git?
假设我有以下简单的 git 存储库:一个分支,一些提交一个接一个,其中几个已被标记(使用 annotated 标记)在提交了每一个之后,然后有一天我决定我要更改第一个提交(顺便说一下,如果有任何更改,则没有标记)。所以我 运行 git rebase --interactive --root
并标记 'edit' 用于初始提交,更改其中的内容和 git rebase --continue
。现在我存储库中的所有提交都已重新创建,因此它们的 sha1 已更改。但是,我创建的标签完全没有变化,仍然指向之前提交的sha1。
是否有自动更新标签到变基时创建的相应提交的方法?
有些人建议使用 git filter-branch --tag-name-filter cat -- --tags
但这首先警告我我的每个标签都没有改变,然后说我的每个标签都更改为它们自己(相同的标签名称和相同的提交哈希)。而且,git show --tags
说标签仍然指向旧的提交。
从某种意义上说,为时已晚(但等一下,好消息来了)。 filter-branch
代码能够调整标签,因为它在过滤期间保留了旧 sha1 到新 sha1 的映射。
其实filter-branch
和rebase
的基本思路是一样的,就是每次commit都是copied,通过扩展原来的内容,进行任何所需的更改,然后根据结果进行新的提交。这意味着在每个复制步骤中,将 对写入文件是微不足道的,然后一旦完成,您就可以通过从旧 sha1 中查找 new-sha1 来修复引用.完成所有引用后,您将使用新编号并删除映射。
地图现在已经不见了,因此 "in one sense, it's too late"。
幸运的是,现在还不算太晚。 :-) 您的变基是可重复的,或者至少,它的关键部分可能是可重复的。此外,如果你的 rebase 足够简单,你可能根本不需要重复它。
让我们看看"repeat"的想法。我们有一个任意形状的原始图 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
我只画了这个共享的原始根节点(现在它是一个上面有起重机的帆船,而不是飞碟)。共享可能更多,也可能更少。一些旧节点可能已经完全未被引用,因此 garbage-collected(可能不是:reflogs 应该让所有原始节点至少存活 30 天)。但无论如何,我们仍然有指向 G' 的某些 "old G part" 的标签,并且 those 引用保证 those 节点,并且他们所有的 parents,都还在新的 G'。
因此,如果我们知道原来的 rebase 是如何完成的,我们可以在 G' 的 sub-graph 上重复它,这是 G 的重要部分。这有多难或容易,以及什么命令( s) 用来做它,取决于原始 G 是否在 G' 中,rebase 命令是什么,G' 覆盖原始 G 的程度等等(因为 git rev-list
,这是我们的关键要获取节点列表,可能无法区分 "original, was-in-G" 和 "new to G'" 节点)。但它可能是可以做到的:这只是编程的一个小问题,在这一点上。
如果你重复它,这次你会想要保留映射,特别是如果生成的图形 G'' 没有完全重叠 G',因为你现在需要的不是地图本身,而是这张地图的投影,从G到G'。
我们简单地给原G中的每个节点一个唯一的相对地址(例如"from the tip, find parent commit #2; from that commit, find parent commit #1; from that commit..."),然后在G''中找到对应的相对地址。这使我们能够重建地图的关键部分。
根据原来rebase的简单程度,我们或许可以直接跳到这个阶段。例如,如果我们确定整个图是在没有展平的情况下被复制的(这样我们就有了两个独立的飞碟)那么标签 T
在 G 中的相对地址是我们想要在 G' 中的相对地址,现在使用该相对地址创建一个指向复制的提交的新标记是微不足道的。
基于新信息的大更新
使用原始图完全线性的附加信息,以及我们复制每个提交的信息,我们可以使用一个非常简单的策略。我们仍然需要重建地图,但现在很容易,因为每个旧提交都有一个新提交,它与原始图的两端有一些线性距离(很容易表示为单个数字)(我将使用 distance-from-tip).
也就是说,旧图看起来像这样,只有一个分支:
A <- B <- C ... <- Z <-- master
标签只是指向其中一个提交(通过带注释的标签 object),例如,标签 foo
可能指向一个 annotated-tag object 指向提交 W
。然后我们注意到 W
是从 Z
.
返回的四次提交
新图看起来完全一样,只是每个提交都被替换为它的副本。我们通过 Z'
调用这些 A'
、B'
等。 (单个)分支指向 tip-most 提交,即 Z'
。我们要调整原始标签 foo
以便我们有一个新的 annotated-tag object 指向 W'
.
我们需要原始 tip-most 提交的 SHA-1 ID。这应该很容易在(单个)分支的 reflog 中找到,并且可能只是 master@{1}
(尽管这取决于您从那时起调整分支的次数;以及自那以后是否添加了新的提交变基,我们也需要考虑这些)。它也可能在特殊参考 ORIG_HEAD
中,如果您决定不喜欢 rebase 结果,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 $
前缀,因为剩下的这真的应该进入脚本,即使是 one-time 使用,以防打字错误和需要调整)
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 版本——以获得映射) .但我们实际上并不需要映射,我们所需要的只是那些 "distance from tip" 计数,所以我要假装我们没有在这里打扰。
现在我们需要遍历所有标签:
# 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_tag
和adj_lightweight_tag
shell这两个函数。不过,首先,让我们编写一个 shell 函数,在给定旧 ID 的情况下生成新 ID,即查找映射。如果我们使用一个真实的映射文件,我们会为第一个条目使用 grep 或 awk,然后打印第二个条目。但是,使用低级 single-old-file 方法,我们想要的是匹配 ID 的 行号 ,我们可以通过 grep -n
:
获得
map_sha1() {
local grep_result line
grep_result=$(grep -n /tmp/orig_list) || {
echo "WARNING: ID is not mapped" 1>&2
echo
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 函数的 return 状态。
轻量级标签更新器现在非常简单:
adj_lightweight_tag() {
local old_sha1= new_sha1 tag=
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
.
这是我们需要复制的标签object。新副本必须指向通过在旧标记指向的提交上使用 $(map_sha1) 找到的 object。我们可以发现提交的方式与 filter-branch
相同,使用 git rev-parse $old_sha1^{commit}
.
(顺便说一句,写下这个答案并查看 filter-branch 脚本,我发现 filter-branch 中有一个错误,我们将把它导入我们的 post-rebase tag-fixup代码:如果现有的带注释的标签指向另一个标签,我们不会修复它。我们只修复轻量级标签和直接指向提交的标签。)
请注意,上面示例代码的 none 实际上已经过测试,并将其转换为 more-general-purpose 脚本(例如,在任何 rebase 之后可能是 运行,或者更好然而,并入交互式变基本身)需要大量的额外工作。
感谢 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 "" || {
echo "WARNING: ID 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= new_sha1 tag=
new_sha1=$(map_sha1 "${old_sha1}")
if [[ -n "${new_sha1}" ]]
then
git update-ref "${tag}" "${new_sha1}"
fi
}
die () {
echo ""
exit 1
}
adjust_annotated_tag () {
local sha1t=
local ref=
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
您可以使用git rebasetags
你怎么用就怎么用git rebase
git rebasetags <rebase args>
如果 rebase 是交互式的,您将看到一个 bash shell,您可以在其中进行更改。退出 shell 后,标签将被恢复。
假设我有以下简单的 git 存储库:一个分支,一些提交一个接一个,其中几个已被标记(使用 annotated 标记)在提交了每一个之后,然后有一天我决定我要更改第一个提交(顺便说一下,如果有任何更改,则没有标记)。所以我 运行 git rebase --interactive --root
并标记 'edit' 用于初始提交,更改其中的内容和 git rebase --continue
。现在我存储库中的所有提交都已重新创建,因此它们的 sha1 已更改。但是,我创建的标签完全没有变化,仍然指向之前提交的sha1。
是否有自动更新标签到变基时创建的相应提交的方法?
有些人建议使用 git filter-branch --tag-name-filter cat -- --tags
但这首先警告我我的每个标签都没有改变,然后说我的每个标签都更改为它们自己(相同的标签名称和相同的提交哈希)。而且,git show --tags
说标签仍然指向旧的提交。
从某种意义上说,为时已晚(但等一下,好消息来了)。 filter-branch
代码能够调整标签,因为它在过滤期间保留了旧 sha1 到新 sha1 的映射。
其实filter-branch
和rebase
的基本思路是一样的,就是每次commit都是copied,通过扩展原来的内容,进行任何所需的更改,然后根据结果进行新的提交。这意味着在每个复制步骤中,将
地图现在已经不见了,因此 "in one sense, it's too late"。
幸运的是,现在还不算太晚。 :-) 您的变基是可重复的,或者至少,它的关键部分可能是可重复的。此外,如果你的 rebase 足够简单,你可能根本不需要重复它。
让我们看看"repeat"的想法。我们有一个任意形状的原始图 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
我只画了这个共享的原始根节点(现在它是一个上面有起重机的帆船,而不是飞碟)。共享可能更多,也可能更少。一些旧节点可能已经完全未被引用,因此 garbage-collected(可能不是:reflogs 应该让所有原始节点至少存活 30 天)。但无论如何,我们仍然有指向 G' 的某些 "old G part" 的标签,并且 those 引用保证 those 节点,并且他们所有的 parents,都还在新的 G'。
因此,如果我们知道原来的 rebase 是如何完成的,我们可以在 G' 的 sub-graph 上重复它,这是 G 的重要部分。这有多难或容易,以及什么命令( s) 用来做它,取决于原始 G 是否在 G' 中,rebase 命令是什么,G' 覆盖原始 G 的程度等等(因为 git rev-list
,这是我们的关键要获取节点列表,可能无法区分 "original, was-in-G" 和 "new to G'" 节点)。但它可能是可以做到的:这只是编程的一个小问题,在这一点上。
如果你重复它,这次你会想要保留映射,特别是如果生成的图形 G'' 没有完全重叠 G',因为你现在需要的不是地图本身,而是这张地图的投影,从G到G'。
我们简单地给原G中的每个节点一个唯一的相对地址(例如"from the tip, find parent commit #2; from that commit, find parent commit #1; from that commit..."),然后在G''中找到对应的相对地址。这使我们能够重建地图的关键部分。
根据原来rebase的简单程度,我们或许可以直接跳到这个阶段。例如,如果我们确定整个图是在没有展平的情况下被复制的(这样我们就有了两个独立的飞碟)那么标签 T
在 G 中的相对地址是我们想要在 G' 中的相对地址,现在使用该相对地址创建一个指向复制的提交的新标记是微不足道的。
基于新信息的大更新
使用原始图完全线性的附加信息,以及我们复制每个提交的信息,我们可以使用一个非常简单的策略。我们仍然需要重建地图,但现在很容易,因为每个旧提交都有一个新提交,它与原始图的两端有一些线性距离(很容易表示为单个数字)(我将使用 distance-from-tip).
也就是说,旧图看起来像这样,只有一个分支:
A <- B <- C ... <- Z <-- master
标签只是指向其中一个提交(通过带注释的标签 object),例如,标签 foo
可能指向一个 annotated-tag object 指向提交 W
。然后我们注意到 W
是从 Z
.
新图看起来完全一样,只是每个提交都被替换为它的副本。我们通过 Z'
调用这些 A'
、B'
等。 (单个)分支指向 tip-most 提交,即 Z'
。我们要调整原始标签 foo
以便我们有一个新的 annotated-tag object 指向 W'
.
我们需要原始 tip-most 提交的 SHA-1 ID。这应该很容易在(单个)分支的 reflog 中找到,并且可能只是 master@{1}
(尽管这取决于您从那时起调整分支的次数;以及自那以后是否添加了新的提交变基,我们也需要考虑这些)。它也可能在特殊参考 ORIG_HEAD
中,如果您决定不喜欢 rebase 结果,git rebase
会留下它。
让我们假设 master@{1}
是正确的 ID,并且没有这样的新提交。那么:
orig_master=$(git rev-parse master@{1})
会保存这个$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 $
前缀,因为剩下的这真的应该进入脚本,即使是 one-time 使用,以防打字错误和需要调整)
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 版本——以获得映射) .但我们实际上并不需要映射,我们所需要的只是那些 "distance from tip" 计数,所以我要假装我们没有在这里打扰。
现在我们需要遍历所有标签:
# 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_tag
和adj_lightweight_tag
shell这两个函数。不过,首先,让我们编写一个 shell 函数,在给定旧 ID 的情况下生成新 ID,即查找映射。如果我们使用一个真实的映射文件,我们会为第一个条目使用 grep 或 awk,然后打印第二个条目。但是,使用低级 single-old-file 方法,我们想要的是匹配 ID 的 行号 ,我们可以通过 grep -n
:
map_sha1() {
local grep_result line
grep_result=$(grep -n /tmp/orig_list) || {
echo "WARNING: ID is not mapped" 1>&2
echo
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 函数的 return 状态。
轻量级标签更新器现在非常简单:
adj_lightweight_tag() {
local old_sha1= new_sha1 tag=
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
.
这是我们需要复制的标签object。新副本必须指向通过在旧标记指向的提交上使用 $(map_sha1) 找到的 object。我们可以发现提交的方式与 filter-branch
相同,使用 git rev-parse $old_sha1^{commit}
.
(顺便说一句,写下这个答案并查看 filter-branch 脚本,我发现 filter-branch 中有一个错误,我们将把它导入我们的 post-rebase tag-fixup代码:如果现有的带注释的标签指向另一个标签,我们不会修复它。我们只修复轻量级标签和直接指向提交的标签。)
请注意,上面示例代码的 none 实际上已经过测试,并将其转换为 more-general-purpose 脚本(例如,在任何 rebase 之后可能是 运行,或者更好然而,并入交互式变基本身)需要大量的额外工作。
感谢 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 "" || {
echo "WARNING: ID 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= new_sha1 tag=
new_sha1=$(map_sha1 "${old_sha1}")
if [[ -n "${new_sha1}" ]]
then
git update-ref "${tag}" "${new_sha1}"
fi
}
die () {
echo ""
exit 1
}
adjust_annotated_tag () {
local sha1t=
local ref=
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
您可以使用git rebasetags
你怎么用就怎么用git rebase
git rebasetags <rebase args>
如果 rebase 是交互式的,您将看到一个 bash shell,您可以在其中进行更改。退出 shell 后,标签将被恢复。