Ben Blank
Ben Blank

Reputation: 56572

How can I rewrite Git tags in an "interactive-rebase-like" manner?

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

Answers (3)

torek
torek

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:

  • The SHA-1 of the commit you want tagged ($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.
  • The new name of the tag. if you want to systematically modify the tag names, set 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

ryanve
ryanve

Reputation: 52531

To rename tags you can delete the old tags and then tag the correct commits with the new names.

Upvotes: 0

Borealid
Borealid

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

Related Questions