Merge branch 'en/fast-imexport-nested-tags'

Updates to fast-import/export.

* en/fast-imexport-nested-tags:
  fast-export: handle nested tags
  t9350: add tests for tags of things other than a commit
  fast-export: allow user to request tags be marked with --mark-tags
  fast-export: add support for --import-marks-if-exists
  fast-import: add support for new 'alias' command
  fast-import: allow tags to be identified by mark labels
  fast-import: fix handling of deleted tags
  fast-export: fix exporting a tag and nothing else
This commit is contained in:
Junio C Hamano 2019-10-15 13:48:00 +09:00
commit 16d9d7184b
6 changed files with 266 additions and 38 deletions

View File

@ -75,11 +75,20 @@ produced incorrect results if you gave these options.
Before processing any input, load the marks specified in
<file>. The input file must exist, must be readable, and
must use the same format as produced by --export-marks.
--mark-tags::
In addition to labelling blobs and commits with mark ids, also
label tags. This is useful in conjunction with
`--export-marks` and `--import-marks`, and is also useful (and
necessary) for exporting of nested tags. It does not hurt
other cases and would be the default, but many fast-import
frontends are not prepared to accept tags with mark
identifiers.
+
Any commits that have already been marked will not be exported again.
If the backend uses a similar --import-marks file, this allows for
incremental bidirectional exporting of the repository by keeping the
marks the same across runs.
Any commits (or tags) that have already been marked will not be
exported again. If the backend uses a similar --import-marks file,
this allows for incremental bidirectional exporting of the repository
by keeping the marks the same across runs.
--fake-missing-tagger::
Some old repositories have tags without a tagger. The

View File

@ -337,6 +337,13 @@ and control the current import process. More detailed discussion
`commit` command. This command is optional and is not
needed to perform an import.
`alias`::
Record that a mark refers to a given object without first
creating any new object. Using --import-marks and referring
to missing marks will cause fast-import to fail, so aliases
can provide a way to set otherwise pruned commits to a valid
value (e.g. the nearest non-pruned ancestor).
`checkpoint`::
Forces fast-import to close the current packfile, generate its
unique SHA-1 checksum and index, and start a new packfile.
@ -774,6 +781,7 @@ lightweight (non-annotated) tags see the `reset` command below.
....
'tag' SP <name> LF
mark?
'from' SP <commit-ish> LF
original-oid?
'tagger' (SP <name>)? SP LT <email> GT SP <when> LF
@ -913,6 +921,21 @@ a data chunk which does not have an LF as its last byte.
+
The `LF` after `<delim> LF` is optional (it used to be required).
`alias`
~~~~~~~
Record that a mark refers to a given object without first creating any
new object.
....
'alias' LF
mark
'to' SP <commit-ish> LF
LF?
....
For a detailed description of `<commit-ish>` see above under `from`.
`checkpoint`
~~~~~~~~~~~~
Forces fast-import to close the current packfile, start a new one, and to

View File

@ -40,6 +40,7 @@ static int no_data;
static int full_tree;
static int reference_excluded_commits;
static int show_original_ids;
static int mark_tags;
static struct string_list extra_refs = STRING_LIST_INIT_NODUP;
static struct string_list tag_refs = STRING_LIST_INIT_NODUP;
static struct refspec refspecs = REFSPEC_INIT_FETCH;
@ -842,11 +843,9 @@ static void handle_tag(const char *name, struct tag *tag)
free(buf);
return;
case REWRITE:
if (tagged->type != OBJ_COMMIT) {
die("tag %s tags unexported %s!",
oid_to_hex(&tag->object.oid),
type_name(tagged->type));
}
if (tagged->type == OBJ_TAG && !mark_tags) {
die(_("Error: Cannot export nested tags unless --mark-tags is specified."));
} else if (tagged->type == OBJ_COMMIT) {
p = rewrite_commit((struct commit *)tagged);
if (!p) {
printf("reset %s\nfrom %s\n\n",
@ -855,12 +854,29 @@ static void handle_tag(const char *name, struct tag *tag)
return;
}
tagged_mark = get_object_mark(&p->object);
} else {
/* tagged->type is either OBJ_BLOB or OBJ_TAG */
tagged_mark = get_object_mark(tagged);
}
}
}
if (tagged->type == OBJ_TAG) {
printf("reset %s\nfrom %s\n\n",
name, oid_to_hex(&null_oid));
}
if (starts_with(name, "refs/tags/"))
name += 10;
printf("tag %s\nfrom :%d\n", name, tagged_mark);
printf("tag %s\n", name);
if (mark_tags) {
mark_next_object(&tag->object);
printf("mark :%"PRIu32"\n", last_idnum);
}
if (tagged_mark)
printf("from :%d\n", tagged_mark);
else
printf("from %s\n", oid_to_hex(&tagged->oid));
if (show_original_ids)
printf("original-oid %s\n", oid_to_hex(&tag->object.oid));
printf("%.*s%sdata %d\n%.*s\n",
@ -1047,11 +1063,16 @@ static void export_marks(char *file)
error("Unable to write marks file %s.", file);
}
static void import_marks(char *input_file)
static void import_marks(char *input_file, int check_exists)
{
char line[512];
FILE *f = xfopen(input_file, "r");
FILE *f;
struct stat sb;
if (check_exists && stat(input_file, &sb))
return;
f = xfopen(input_file, "r");
while (fgets(line, sizeof(line), f)) {
uint32_t mark;
char *line_end, *mark_end;
@ -1115,7 +1136,9 @@ int cmd_fast_export(int argc, const char **argv, const char *prefix)
struct rev_info revs;
struct object_array commits = OBJECT_ARRAY_INIT;
struct commit *commit;
char *export_filename = NULL, *import_filename = NULL;
char *export_filename = NULL,
*import_filename = NULL,
*import_filename_if_exists = NULL;
uint32_t lastimportid;
struct string_list refspecs_list = STRING_LIST_INIT_NODUP;
struct string_list paths_of_changed_objects = STRING_LIST_INIT_DUP;
@ -1135,6 +1158,10 @@ int cmd_fast_export(int argc, const char **argv, const char *prefix)
N_("Dump marks to this file")),
OPT_STRING(0, "import-marks", &import_filename, N_("file"),
N_("Import marks from this file")),
OPT_STRING(0, "import-marks-if-exists",
&import_filename_if_exists,
N_("file"),
N_("Import marks from this file if it exists")),
OPT_BOOL(0, "fake-missing-tagger", &fake_missing_tagger,
N_("Fake a tagger when tags lack one")),
OPT_BOOL(0, "full-tree", &full_tree,
@ -1149,6 +1176,8 @@ int cmd_fast_export(int argc, const char **argv, const char *prefix)
&reference_excluded_commits, N_("Reference parents which are not in fast-export stream by object id")),
OPT_BOOL(0, "show-original-ids", &show_original_ids,
N_("Show original object ids of blobs/commits")),
OPT_BOOL(0, "mark-tags", &mark_tags,
N_("Label tags with mark ids")),
OPT_END()
};
@ -1182,8 +1211,12 @@ int cmd_fast_export(int argc, const char **argv, const char *prefix)
if (use_done_feature)
printf("feature done\n");
if (import_filename && import_filename_if_exists)
die(_("Cannot pass both --import-marks and --import-marks-if-exists"));
if (import_filename)
import_marks(import_filename);
import_marks(import_filename, 0);
else if (import_filename_if_exists)
import_marks(import_filename_if_exists, 1);
lastimportid = last_idnum;
if (import_filename && revs.prune_data.nr)

View File

@ -2489,18 +2489,14 @@ static void parse_from_existing(struct branch *b)
}
}
static int parse_from(struct branch *b)
static int parse_objectish(struct branch *b, const char *objectish)
{
const char *from;
struct branch *s;
struct object_id oid;
if (!skip_prefix(command_buf.buf, "from ", &from))
return 0;
oidcpy(&oid, &b->branch_tree.versions[1].oid);
s = lookup_branch(from);
s = lookup_branch(objectish);
if (b == s)
die("Can't create a branch from itself: %s", b->name);
else if (s) {
@ -2508,8 +2504,8 @@ static int parse_from(struct branch *b)
oidcpy(&b->oid, &s->oid);
oidcpy(&b->branch_tree.versions[0].oid, t);
oidcpy(&b->branch_tree.versions[1].oid, t);
} else if (*from == ':') {
uintmax_t idnum = parse_mark_ref_eol(from);
} else if (*objectish == ':') {
uintmax_t idnum = parse_mark_ref_eol(objectish);
struct object_entry *oe = find_mark(idnum);
if (oe->type != OBJ_COMMIT)
die("Mark :%" PRIuMAX " not a commit", idnum);
@ -2523,13 +2519,13 @@ static int parse_from(struct branch *b)
} else
parse_from_existing(b);
}
} else if (!get_oid(from, &b->oid)) {
} else if (!get_oid(objectish, &b->oid)) {
parse_from_existing(b);
if (is_null_oid(&b->oid))
b->delete = 1;
}
else
die("Invalid ref name or SHA1 expression: %s", from);
die("Invalid ref name or SHA1 expression: %s", objectish);
if (b->branch_tree.tree && !oideq(&oid, &b->branch_tree.versions[1].oid)) {
release_tree_content_recursive(b->branch_tree.tree);
@ -2540,6 +2536,26 @@ static int parse_from(struct branch *b)
return 1;
}
static int parse_from(struct branch *b)
{
const char *from;
if (!skip_prefix(command_buf.buf, "from ", &from))
return 0;
return parse_objectish(b, from);
}
static int parse_objectish_with_prefix(struct branch *b, const char *prefix)
{
const char *base;
if (!skip_prefix(command_buf.buf, prefix, &base))
return 0;
return parse_objectish(b, base);
}
static struct hash_list *parse_merge(unsigned int *count)
{
struct hash_list *list = NULL, **tail = &list, *n;
@ -2714,6 +2730,7 @@ static void parse_new_tag(const char *arg)
first_tag = t;
last_tag = t;
read_next_command();
parse_mark();
/* from ... */
if (!skip_prefix(command_buf.buf, "from ", &from))
@ -2770,7 +2787,7 @@ static void parse_new_tag(const char *arg)
strbuf_addbuf(&new_data, &msg);
free(tagger);
if (store_object(OBJ_TAG, &new_data, NULL, &t->oid, 0))
if (store_object(OBJ_TAG, &new_data, NULL, &t->oid, next_mark))
t->pack_id = MAX_PACK_ID;
else
t->pack_id = pack_id;
@ -2779,6 +2796,7 @@ static void parse_new_tag(const char *arg)
static void parse_reset_branch(const char *arg)
{
struct branch *b;
const char *tag_name;
b = lookup_branch(arg);
if (b) {
@ -2794,6 +2812,32 @@ static void parse_reset_branch(const char *arg)
b = new_branch(arg);
read_next_command();
parse_from(b);
if (b->delete && skip_prefix(b->name, "refs/tags/", &tag_name)) {
/*
* Elsewhere, we call dump_branches() before dump_tags(),
* and dump_branches() will handle ref deletions first, so
* in order to make sure the deletion actually takes effect,
* we need to remove the tag from our list of tags to update.
*
* NEEDSWORK: replace list of tags with hashmap for faster
* deletion?
*/
struct tag *t, *prev = NULL;
for (t = first_tag; t; t = t->next_tag) {
if (!strcmp(t->name, tag_name))
break;
prev = t;
}
if (t) {
if (prev)
prev->next_tag = t->next_tag;
else
first_tag = t->next_tag;
if (!t->next_tag)
last_tag = prev;
/* There is no mem_pool_free(t) function to call. */
}
}
if (command_buf.len > 0)
unread_command_buf = 1;
}
@ -3060,6 +3104,28 @@ static void parse_progress(void)
skip_optional_lf();
}
static void parse_alias(void)
{
struct object_entry *e;
struct branch b;
skip_optional_lf();
read_next_command();
/* mark ... */
parse_mark();
if (!next_mark)
die(_("Expected 'mark' command, got %s"), command_buf.buf);
/* to ... */
memset(&b, 0, sizeof(b));
if (!parse_objectish_with_prefix(&b, "to "))
die(_("Expected 'to' command, got %s"), command_buf.buf);
e = find_object(&b.oid);
assert(e);
insert_mark(next_mark, e);
}
static char* make_fast_import_path(const char *path)
{
if (!relative_marks_paths || is_absolute_path(path))
@ -3187,6 +3253,8 @@ static int parse_one_feature(const char *feature, int from_stream)
option_import_marks(arg, from_stream, 1);
} else if (skip_prefix(feature, "export-marks=", &arg)) {
option_export_marks(arg);
} else if (!strcmp(feature, "alias")) {
; /* Don't die - this feature is supported */
} else if (!strcmp(feature, "get-mark")) {
; /* Don't die - this feature is supported */
} else if (!strcmp(feature, "cat-blob")) {
@ -3343,6 +3411,8 @@ int cmd_main(int argc, const char **argv)
parse_checkpoint();
else if (!strcmp("done", command_buf.buf))
break;
else if (!strcmp("alias", command_buf.buf))
parse_alias();
else if (starts_with(command_buf.buf, "progress "))
parse_progress();
else if (skip_prefix(command_buf.buf, "feature ", &v))

View File

@ -85,6 +85,36 @@ test_expect_success 'A: create pack from stdin' '
An annotated tag that annotates a blob.
EOF
tag to-be-deleted
from :3
data <<EOF
Another annotated tag that annotates a blob.
EOF
reset refs/tags/to-be-deleted
from 0000000000000000000000000000000000000000
tag nested
mark :6
from :4
data <<EOF
Tag of our lovely commit
EOF
reset refs/tags/nested
from 0000000000000000000000000000000000000000
tag nested
mark :7
from :6
data <<EOF
Tag of tag of our lovely commit
EOF
alias
mark :8
to :5
INPUT_END
git fast-import --export-marks=marks.out <input &&
git whatchanged master
@ -157,12 +187,19 @@ test_expect_success 'A: verify tag/series-A-blob' '
test_cmp expect actual
'
test_expect_success 'A: verify tag deletion is successful' '
test_must_fail git rev-parse --verify refs/tags/to-be-deleted
'
test_expect_success 'A: verify marks output' '
cat >expect <<-EOF &&
:2 $(git rev-parse --verify master:file2)
:3 $(git rev-parse --verify master:file3)
:4 $(git rev-parse --verify master:file4)
:5 $(git rev-parse --verify master^0)
:6 $(git cat-file tag nested | grep object | cut -d" " -f 2)
:7 $(git rev-parse --verify nested)
:8 $(git rev-parse --verify master^0)
EOF
test_cmp expect marks.out
'

View File

@ -53,6 +53,33 @@ test_expect_success 'fast-export | fast-import' '
'
test_expect_success 'fast-export ^muss^{commit} muss' '
git fast-export --tag-of-filtered-object=rewrite ^muss^{commit} muss >actual &&
cat >expected <<-EOF &&
tag muss
from $(git rev-parse --verify muss^{commit})
$(git cat-file tag muss | grep tagger)
data 9
valentin
EOF
test_cmp expected actual
'
test_expect_success 'fast-export --mark-tags ^muss^{commit} muss' '
git fast-export --mark-tags --tag-of-filtered-object=rewrite ^muss^{commit} muss >actual &&
cat >expected <<-EOF &&
tag muss
mark :1
from $(git rev-parse --verify muss^{commit})
$(git cat-file tag muss | grep tagger)
data 9
valentin
EOF
test_cmp expected actual
'
test_expect_success 'fast-export master~2..master' '
git fast-export master~2..master >actual &&
@ -513,10 +540,41 @@ test_expect_success 'tree_tag' '
'
# NEEDSWORK: not just check return status, but validate the output
# Note that these tests DO NOTHING other than print a warning that
# they are ommitting the one tag we asked them to export (because the
# tags resolve to a tree). They exist just to make sure we do not
# abort but instead just warn.
test_expect_success 'tree_tag-obj' 'git fast-export tree_tag-obj'
test_expect_success 'tag-obj_tag' 'git fast-export tag-obj_tag'
test_expect_success 'tag-obj_tag-obj' 'git fast-export tag-obj_tag-obj'
test_expect_success 'handling tags of blobs' '
git tag -a -m "Tag of a blob" blobtag $(git rev-parse master:file) &&
git fast-export blobtag >actual &&
cat >expect <<-EOF &&
blob
mark :1
data 9
die Luft
tag blobtag
from :1
tagger $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
data 14
Tag of a blob
EOF
test_cmp expect actual
'
test_expect_success 'handling nested tags' '
git tag -a -m "This is a nested tag" nested muss &&
git fast-export --mark-tags nested >output &&
grep "^from $ZERO_OID$" output &&
grep "^tag nested$" output >tag_lines &&
test_line_count = 2 tag_lines
'
test_expect_success 'directory becomes symlink' '
git init dirtosymlink &&
git init result &&
@ -567,17 +625,15 @@ test_expect_success 'fast-export quotes pathnames' '
'
test_expect_success 'test bidirectionality' '
>marks-cur &&
>marks-new &&
git init marks-test &&
git fast-export --export-marks=marks-cur --import-marks=marks-cur --branches | \
git --git-dir=marks-test/.git fast-import --export-marks=marks-new --import-marks=marks-new &&
git fast-export --export-marks=marks-cur --import-marks-if-exists=marks-cur --branches | \
git --git-dir=marks-test/.git fast-import --export-marks=marks-new --import-marks-if-exists=marks-new &&
(cd marks-test &&
git reset --hard &&
echo Wohlauf > file &&
git commit -a -m "back in time") &&
git --git-dir=marks-test/.git fast-export --export-marks=marks-new --import-marks=marks-new --branches | \
git fast-import --export-marks=marks-cur --import-marks=marks-cur
git --git-dir=marks-test/.git fast-export --export-marks=marks-new --import-marks-if-exists=marks-new --branches | \
git fast-import --export-marks=marks-cur --import-marks-if-exists=marks-cur
'
cat > expected << EOF