有没有自动更新标记以对应重建时创建的提交的方法?
有些人建议使用“git filter-branch --tag-name-filter cat -- --tags”,但是它首先警告我每个标记都没有更改,然后说每个标记都更改为它们自己(相同的标记名称和相同的提交哈希)。而且,git show --tags 仍然显示标记仍然指向旧提交。
filter-branch
代码能够调整标签,因为它在筛选过程中保留了旧SHA1到新SHA1的映射。filter-branch
和rebase
都使用了相同的基本思想,即通过扩展原始内容、进行任何所需更改并将结果制作成新提交来“复制”每个提交。这意味着在每个复制步骤中,将<old-sha1, new-sha1>对轻松写入文件,然后在完成后通过从其旧SHA1查找新SHA1来修复引用。一旦所有引用完成,您就可以承诺使用新编号,并删除映射。 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
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
adj_anno_tag
和adj_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中存在一个错误,我们将其引入到我们的后重定基标签修复代码中:如果现有的注释标签指向另一个标签,则不会进行修复。我们只修复轻量级标签和直接指向提交的标签。)
请注意,以上示例代码均未经过测试,并将其转换为更通用的脚本(例如,在任何重定基之后都可以运行,或更好的是,合并到交互式重定基本身中)需要进行大量额外的工作。
你可以使用 git rebasetags
使用方法与 git rebase
相同
git rebasetags <rebase args>
感谢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
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
push
到共享存储库中? - Chris