Reputation: 56572
I'm converting an old, multi-project Subversion repo into a series of single-project Git repos and trying to retain as much of the original data as possible. I've got most of the conversion sorted, but am having trouble with the tags.
Due to the way the tags were checked into the Subversion repo, each tag is currently attached to an empty commit which is a child of the commit to which the tag should be attached. In other words, I have a repo with the following commit structure:
A -> B -> C -> D
\ \-> (empty) -> 1.1
\-> (empty) -> 1.0
Ideally, what I'd like to do is something like an interactive rebase, but instead of reordering commits, I want to move those tags up onto the correct commits.
Is anything like this possible? The new repos haven't been pushed anywhere yet, so I have total freedom to rewrite history.
Upvotes: 0
Views: 325
Reputation: 488453
A tag (whether annotated or lightweight) simply points to a commit.1 Hence, as ryanve noted, you can simply force the tag to point to a different commit (since it's still a new private repo there's no one else to mess up; you don't have to worry about remote repos' copies of the tags).
While git filter-branch
can do the whole thing, it tends to be quite a heavy-weight operation. You might want, instead, to snip out the part of filter-branch
that copies annotated tags, and use that to make your own replacements.
To do this, start by opening up the filter branch script in an editor, e.g.:
$ vim $(git --exec-path)/git-filter-branch
Search for git mktag
and you'll find this chunk of code:
if [ "$type" = "tag" ]; then
new_sha1=$( ( printf 'object %s\ntype commit\ntag %s\n' \
"$new_sha1" "$new_ref"
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
warn "gpg signature stripped from tag object $sha1t"
fi
fi
git update-ref "refs/tags/$new_ref" "$new_sha1" ||
die "Could not write tag $new_ref"
Note that $type
is $tag
only when the tag is an actual annotated tag object. Note also the gpg signature stripping—probably not a problem in your case, and if you wanted to sign your new tags you could do that.
The one key input for this code fragment is the existing annotated tag name ($ref
).
The two key things you'd need to compute before using this block of code are:
$new_sha1
): that's the parent commit of the commit to which the existing tag points, so it's simply the output of git rev-parse ${ref}^{commit}^
: find the commit to which the tag (indirectly) points, and traverse the commit graph to that commit's first (and only) parent.new_ref
appropriately. If not, just use $ref
(note that git update-ref
will then update the existing reference in-place, rather than creating a new ref, so the error message for failure becomes a bit wrong).Once you've done that, all you need to do is apply this block of code2 to each annotated tag. Since you'll have already verified that they are all annotated tags, you won't need the if [ "$type" = "tag" ]
test at all: just write the appropriate subset of code to a new shell script, wrap it up as a function, and call the function on all your known annotated tags (which you can get by using git for-each-ref refs/tags
).
Once you've moved all the tags to point away from the empty side commits, the side commits will become invisible due to nothing pointing to them, and will eventually be garbage collected.
1A lightweight tag points directly to a commit—this is what makes it lightweight—while an annotated tag starts with the same thing as a lightweight tag, i.e., a reference in the refs/tags/
name space, but that reference points to a repository object of type "tag", which contains the text described in the git mktag
documentation. The object to which it points is normally a commit, so that the type string is also commit
, and the tag name in the annotated tag object is the same as the lightweight tag's name (minus the refs/tags/
part).
It's possible for a tag (lightweight or annotated) to point to something other than a commit, e.g., you can point one tag to another tag. Of course if you point a lightweight tag to an annotated tag object, you have an annotated tag, although if the annotated tag's internal name doesn't match the lightweight tag's name, you can at least detect this odd situation ... not that there is anything to do about it, other than point out that it's odd.
2Most of the block is regular shell built-ins or commands. The one unusual part is sane_grep
, which is really just running grep
but working around brokenness in some specific cases. You can find that in the git-sh-setup
script in the same exec-path directory, or just use this, which I copied out of git-sh-setup
:
sane_grep () {
GREP_OPTIONS= LC_ALL=C grep "$@"
}
The warn
and die
function-ettes are in git-filter-branch
(there's a die
in the setup script as well), and do the obvious thing.
Upvotes: 1
Reputation: 52531
To rename tags you can delete the old tags and then tag the correct commits with the new names.
Upvotes: 0
Reputation: 98489
You can do this with a filter-branch command.
Filter each tag, and find the relevant child commit. Then put the tag there.
You can also remove the empty commit as part of the filter if you so choose.
Upvotes: 1