From: Luke Shumaker <lukeshu@xxxxxxxxxxx> A tag object contains the tag-name in the object, and is also pointed to by a ref named 'refs/tags/{tag-name}'. It's possible to end up with a tag for which the internal name and the refname disagree. This "shouldn't" happen, but sometimes it can: - In the "the coolest merge ever"[1], if Linus had wanted to import existing tags from Paul's gitk repo, he'd likely have wanted to give them a `gitk/` or `gitk-` prefix; you don't want gitk v0.0.1 to appear to be git v0.0.1, so when importing it, you'd rename the tag to `gitk/v0.0.1`. (Less hypothetically, in-the-wild, my employer's repo has _several_ such merges/imports, where the tags from each repo were given a prefix.) - Sometimes tags are "archived" to something like "refs/archived-tags/foo". That'd work fine if they're lightweight tags, but if they're annotated tags, then after the rename the internal name in the tag object (`v0.0.1`) is now different than the refname (`gitk/v0.0.1`). Which is still mostly fine, since not too many tools care if the internal name and the refname disagree. But, fast-export/fast-import are tools that do care: it's currently impossible to represent these tags in a fast-import stream. This patch adds an optional "refname" sub-command to fast-import's "tag" top-level-command, the stream tag foo refname refs/tags/bar ... will create a tag at "refs/tags/bar" that says "tag foo" internally. [1]: https://lore.kernel.org/git/Pine.LNX.4.58.0506221433540.2353@xxxxxxxxxxxxxxx/ Signed-off-by: Luke Shumaker <lukeshu@xxxxxxxxxxx> --- Documentation/git-fast-import.txt | 22 +++++++++++++--------- builtin/fast-export.c | 6 ++++++ builtin/fast-import.c | 10 ++++++++-- t/t9350-fast-export.sh | 11 ++++++++--- 4 files changed, 35 insertions(+), 14 deletions(-) diff --git a/Documentation/git-fast-import.txt b/Documentation/git-fast-import.txt index 39cfa05b28..8c1756bdad 100644 --- a/Documentation/git-fast-import.txt +++ b/Documentation/git-fast-import.txt @@ -822,24 +822,28 @@ Creates an annotated tag referring to a specific commit. To create lightweight (non-annotated) tags see the `reset` command below. .... - 'tag' SP <name> LF + 'tag' SP <tagname> LF mark? + ('refname' SP <refname> LF)? 'from' SP <commit-ish> LF original-oid? 'tagger' (SP <name>)? SP LT <email> GT SP <when> LF data .... -where `<name>` is the name of the tag to create. +where `<tagname>` is the name of the tag that is stored in the tag +object itself. The value of `<tagname>` must be a valid for inclusion +as part of a refname in Git and therefore may contain forward slashes. +As `LF` is not valid in a Git refname, no quoting or escaping syntax +is supported here. -Tag names are automatically prefixed with `refs/tags/` when stored -in Git, so importing the CVS branch symbol `RELENG-1_0-FINAL` would -use just `RELENG-1_0-FINAL` for `<name>`, and fast-import will write the -corresponding ref as `refs/tags/RELENG-1_0-FINAL`. +The `refname` command controls where in the ref hierarchy to put the +reference that points to the resulting tag object. If `refname` is +not given, then `refs/tags/<tagname>` is used. -The value of `<name>` must be a valid refname in Git and therefore -may contain forward slashes. As `LF` is not valid in a Git refname, -no quoting or escaping syntax is supported here. +So, for example, importing the CVS branch symbol `RELENG-1_0-FINAL` +would use just `RELENG-1_0-FINAL` for `<tagname>`, and fast-import +will write the corresponding ref as `refs/tags/RELENG-1_0-FINAL`. The `from` command is the same as in the `commit` command; see above for details. diff --git a/builtin/fast-export.c b/builtin/fast-export.c index 2bf83fe52e..de04aaf241 100644 --- a/builtin/fast-export.c +++ b/builtin/fast-export.c @@ -784,6 +784,7 @@ static void handle_tail(struct object_array *commits, struct rev_info *revs, static void handle_tag(const char *refname, struct tag *tag) { + static struct strbuf autorefname = STRBUF_INIT; unsigned long size; enum object_type type; char *buf; @@ -910,6 +911,9 @@ static void handle_tag(const char *refname, struct tag *tag) } } + strbuf_reset(&autorefname); + strbuf_addf(&autorefname, "refs/tags/%.*s", (int)tagname_len, tagname); + if (tagged->type == OBJ_TAG) { printf("reset %s\nfrom %s\n\n", refname, oid_to_hex(&null_oid)); @@ -919,6 +923,8 @@ static void handle_tag(const char *refname, struct tag *tag) mark_next_object(&tag->object); printf("mark :%"PRIu32"\n", last_idnum); } + if (strcmp(refname, autorefname.buf)) + printf("refname %s\n", refname); if (tagged_mark) printf("from :%d\n", tagged_mark); else diff --git a/builtin/fast-import.c b/builtin/fast-import.c index 43287f929f..49e5a0c159 100644 --- a/builtin/fast-import.c +++ b/builtin/fast-import.c @@ -2794,9 +2794,15 @@ static void parse_new_tag(const char *arg) read_next_command(); parse_mark(); + /* refname ... */ strbuf_reset(&refname); - strbuf_addstr(&refname, "refs/tags/"); - strbuf_addstr(&refname, tagname); + if (skip_prefix(command_buf.buf, "refname ", &v)) { + strbuf_addstr(&refname, v); + read_next_command(); + } else { + strbuf_addstr(&refname, "refs/tags/"); + strbuf_addstr(&refname, tagname); + } /* from ... */ if (!skip_prefix(command_buf.buf, "from ", &from)) diff --git a/t/t9350-fast-export.sh b/t/t9350-fast-export.sh index 0bcc1bd54e..089c0c0a24 100755 --- a/t/t9350-fast-export.sh +++ b/t/t9350-fast-export.sh @@ -34,6 +34,9 @@ test_expect_success 'setup' ' git commit -m sitzt file2 && test_tick && git tag -a -m valentin muss && + git tag -a -m joseph pure && + git update-ref refs/english-tags/rein refs/tags/pure && + git update-ref -d refs/tags/pure && git merge -s ours main ' @@ -42,6 +45,7 @@ test_expect_success 'fast-export | fast-import' ' MAIN=$(git rev-parse --verify main) && REIN=$(git rev-parse --verify rein) && + PURE=$(git rev-parse --verify refs/english-tags/rein) && WER=$(git rev-parse --verify wer) && MUSS=$(git rev-parse --verify muss) && mkdir new && @@ -51,6 +55,7 @@ test_expect_success 'fast-export | fast-import' ' git fast-import && test $MAIN = $(git rev-parse --verify refs/heads/main) && test $REIN = $(git rev-parse --verify refs/tags/rein) && + test $PURE = $(git rev-parse --verify refs/english-tags/rein) && test $WER = $(git rev-parse --verify refs/heads/wer) && test $MUSS = $(git rev-parse --verify refs/tags/muss)) <actual @@ -359,7 +364,7 @@ test_expect_success 'fast-export | fast-import when main is tagged' ' git tag -m msg last && git fast-export -C -C --signed-tags=strip --all > output && - test $(grep -c "^tag " output) = 3 + test $(grep -c "^tag " output) = 4 ' @@ -374,11 +379,11 @@ test_expect_success 'cope with tagger-less tags' ' TAG=$(git hash-object -t tag -w tag-content) && git update-ref refs/tags/sonnenschein $TAG && git fast-export -C -C --signed-tags=strip --all > output && - test $(grep -c "^tag " output) = 4 && + test $(grep -c "^tag " output) = 5 && ! grep "Unspecified Tagger" output && git fast-export -C -C --signed-tags=strip --all \ --fake-missing-tagger > output && - test $(grep -c "^tag " output) = 4 && + test $(grep -c "^tag " output) = 5 && grep "Unspecified Tagger" output ' -- 2.31.1