Commit Diff


commit - a009df92847bdf738778c064cc50e9c204b6e1d6
commit + 8e7bd50a820730b5f8356254f7a44fa922fab3bc
blob - 5cc02975e9734606b79d513463bbce1e7f4e8fd6
blob + c570e9380338cb7e97047870f80835c0834d134c
--- got/got.1
+++ got/got.1
@@ -520,6 +520,59 @@ Git's garbage collector.
 .It Cm br
 Short alias for
 .Cm branch .
+.It Cm tag Oo Fl m Ar message Oc Oo Fl r Ar repository-path Oc Oo Fl l Oc Ar name Op Ar commit Oc
+Manage tags in a repository.
+.Pp
+Tags are managed via references which live in the
+.Dq refs/tags/
+reference namespace.
+The
+.Cm got tag
+command operates on references in this namespace only.
+.Pp
+Expect one or two arguments and attempt to create a tag with the given
+.Ar name ,
+and make this tag point at the given
+.Ar commit .
+If no commit is specified, default to the latest commit on the work tree's
+current branch if invoked in a work tree, and to a commit resolved via
+the repository's HEAD reference otherwise.
+Otherwise, the expected argument is a commit ID SHA1 hash or an existing
+reference or tag name which will be resolved to a commit ID.
+An abbreviated hash argument will be expanded to a full SHA1 hash
+automatically, provided the abbreviation is unique.
+.Pp
+The options for
+.Cm got tag
+are as follows:
+.Bl -tag -width Ds
+.It Fl m Ar message
+Use the specified tag message when creating the new tag
+Without the
+.Fl m
+option,
+.Cm got import
+opens a temporary file in an editor where a tag message can be written.
+.It Fl r Ar repository-path
+Use the repository at the specified path.
+If not specified, assume the repository is located at or above the current
+working directory.
+If this directory is a
+.Nm
+work tree, use the repository path associated with this work tree.
+.It Fl l
+List all existing tags in the repository instead of creating a new tag.
+If this option is used, no other command-line arguments are allowed.
+.El
+.Pp
+By design, the
+.Cm got tag
+command will not delete tags or change existing tags.
+If a tag must be deleted, the
+.Cm got ref
+command may be used to delete a tag's reference.
+This should only be done if the tag has not already been copied to
+another repository.
 .It Cm add Ar file-path ...
 Schedule unversioned files in a work tree for addition to the
 repository in the next commit.
@@ -1124,7 +1177,10 @@ attempts to reject
 environment variables with a missing email address.
 .It Ev VISUAL , EDITOR
 The editor spawned by
-.Cm got commit .
+.Cm got commit ,
+.Cm got import ,
+or
+.Cm got tag .
 .It Ev GOT_LOG_DEFAULT_LIMIT
 The default limit on the number of commits traversed by
 .Cm got log .
blob - 215ac4b28ec9af15c08d48b2d902dee4ca65fc6a
blob + 792a87a9761babecdde9f39b750d73416cc13418
--- got/got.c
+++ got/got.c
@@ -88,6 +88,7 @@ __dead static void	usage_tree(void);
 __dead static void	usage_status(void);
 __dead static void	usage_ref(void);
 __dead static void	usage_branch(void);
+__dead static void	usage_tag(void);
 __dead static void	usage_add(void);
 __dead static void	usage_remove(void);
 __dead static void	usage_revert(void);
@@ -111,6 +112,7 @@ static const struct got_error*		cmd_tree(int, char *[]
 static const struct got_error*		cmd_status(int, char *[]);
 static const struct got_error*		cmd_ref(int, char *[]);
 static const struct got_error*		cmd_branch(int, char *[]);
+static const struct got_error*		cmd_tag(int, char *[]);
 static const struct got_error*		cmd_add(int, char *[]);
 static const struct got_error*		cmd_remove(int, char *[]);
 static const struct got_error*		cmd_revert(int, char *[]);
@@ -135,6 +137,7 @@ static struct got_cmd got_commands[] = {
 	{ "status",	cmd_status,	usage_status,	"st" },
 	{ "ref",	cmd_ref,	usage_ref,	"" },
 	{ "branch",	cmd_branch,	usage_branch,	"br" },
+	{ "tag",	cmd_tag,	usage_tag,	"" },
 	{ "add",	cmd_add,	usage_add,	"" },
 	{ "remove",	cmd_remove,	usage_remove,	"rm" },
 	{ "revert",	cmd_revert,	usage_revert,	"rv" },
@@ -3293,7 +3296,373 @@ done:
 	return error;
 }
 
+
 __dead static void
+usage_tag(void)
+{
+	fprintf(stderr,
+	    "usage: %s tag [-r repository] -l | -d name | "
+	        "[-m message] name [commit]\n", getprogname());
+	exit(1);
+}
+
+static const struct got_error *
+list_tags(struct got_repository *repo, struct got_worktree *worktree)
+{
+	static const struct got_error *err = NULL;
+	struct got_reflist_head refs;
+	struct got_reflist_entry *re;
+
+	SIMPLEQ_INIT(&refs);
+
+	err = got_ref_list(&refs, repo);
+	if (err)
+		return err;
+
+	SIMPLEQ_FOREACH(re, &refs, entry) {
+		const char *refname;
+		char *refstr, *tagmsg0, *tagmsg, *line, *id_str, *datestr;
+		char datebuf[26];
+		time_t tagger_time;
+		struct got_object_id *id;
+		struct got_tag_object *tag;
+
+		refname = got_ref_get_name(re->ref);
+		if (strncmp(refname, "refs/tags/", 10) != 0)
+			continue;
+		refname += 10;
+		refstr = got_ref_to_str(re->ref);
+		if (refstr == NULL) {
+			err = got_error_from_errno("got_ref_to_str");
+			break;
+		}
+		printf("%stag %s %s\n", GOT_COMMIT_SEP_STR, refname, refstr);
+		free(refstr);
+
+		err = got_ref_resolve(&id, repo, re->ref);
+		if (err)
+			break;
+		err = got_object_open_as_tag(&tag, repo, id);
+		free(id);
+		if (err)
+			break;
+		err = got_object_id_str(&id_str,
+		    got_object_tag_get_object_id(tag));
+		if (err)
+			break;
+		switch (got_object_tag_get_object_type(tag)) {
+		case GOT_OBJ_TYPE_BLOB:
+			printf("%s %s\n", GOT_OBJ_LABEL_BLOB, id_str);
+			break;
+		case GOT_OBJ_TYPE_TREE:
+			printf("%s %s\n", GOT_OBJ_LABEL_TREE, id_str);
+			break;
+		case GOT_OBJ_TYPE_COMMIT:
+			printf("%s %s\n", GOT_OBJ_LABEL_COMMIT, id_str);
+			break;
+		case GOT_OBJ_TYPE_TAG:
+			printf("%s %s\n", GOT_OBJ_LABEL_TAG, id_str);
+			break;
+		default:
+			break;
+		}
+		free(id_str);
+		printf("from: %s\n", got_object_tag_get_tagger(tag));
+		tagger_time = got_object_tag_get_tagger_time(tag);
+		datestr = get_datestr(&tagger_time, datebuf);
+		if (datestr)
+			printf("date: %s UTC\n", datestr);
+		tagmsg0 = strdup(got_object_tag_get_message(tag));
+		got_object_tag_close(tag);
+		if (tagmsg0 == NULL) {
+			err = got_error_from_errno("strdup");
+			break;
+		}
+
+		tagmsg = tagmsg0;
+		do {
+			line = strsep(&tagmsg, "\n");
+			if (line)
+				printf(" %s\n", line);
+		} while (line);
+		free(tagmsg0);
+	}
+
+	got_ref_list_free(&refs);
+	return NULL;
+}
+
+static const struct got_error *
+get_tag_message(char **tagmsg, const char *commit_id_str, const char *repo_path)
+{
+	const struct got_error *err = NULL;
+	char *template = NULL, *initial_content = NULL;
+	char *tagmsg_path = NULL, *editor = NULL;
+	int fd = -1;
+
+	if (asprintf(&template, "/tmp/got-tagmsg") == -1) {
+		err = got_error_from_errno("asprintf");
+		goto done;
+	}
+
+	if (asprintf(&initial_content, "\n# tagging commit %s\n",
+	    commit_id_str) == -1) {
+		err = got_error_from_errno("asprintf");
+		goto done;
+	}
+
+	err = got_opentemp_named_fd(&tagmsg_path, &fd, template);
+	if (err)
+		goto done;
+
+	dprintf(fd, initial_content);
+	close(fd);
+
+	err = get_editor(&editor);
+	if (err)
+		goto done;
+	err = edit_logmsg(tagmsg, editor, tagmsg_path, initial_content);
+done:
+	if (err == NULL || err->code == GOT_ERR_COMMIT_MSG_EMPTY) {
+		unlink(tagmsg_path);
+		free(tagmsg_path);
+		tagmsg_path = NULL;
+	}
+	free(initial_content);
+	free(template);
+	free(editor);
+
+	/* Editor is done; we can now apply unveil(2) */
+	if (err == NULL) {
+		err = apply_unveil(repo_path, 0, NULL);
+		if (err) {
+			free(*tagmsg);
+			*tagmsg = NULL;
+		}
+	}
+	return err;
+}
+
+static const struct got_error *
+add_tag(struct got_repository *repo, const char *tag_name,
+    const char *commit_arg, const char *tagmsg_arg)
+{
+	const struct got_error *err = NULL;
+	struct got_object_id *commit_id = NULL, *tag_id = NULL;
+	char *label = NULL, *commit_id_str = NULL;
+	struct got_reference *ref = NULL;
+	char *refname = NULL, *tagmsg = NULL;
+	const char *tagger;
+
+	/*
+	 * Don't let the user create a tag named '-'.
+	 * While technically a valid reference name, this case is usually
+	 * an unintended typo.
+	 */
+	if (tag_name[0] == '-' && tag_name[1] == '\0')
+		return got_error(GOT_ERR_BAD_REF_NAME);
+
+	err = get_author(&tagger);
+	if (err)
+		return err;
+
+	err = match_object_id(&commit_id, &label, commit_arg,
+	    GOT_OBJ_TYPE_COMMIT, 1, repo);
+	if (err)
+		goto done;
+
+	err = got_object_id_str(&commit_id_str, commit_id);
+	if (err)
+		goto done;
+
+	if (strncmp("refs/tags/", tag_name, 10) == 0) {
+		refname = strdup(tag_name);
+		if (refname == NULL) {
+			 err = got_error_from_errno("strdup");
+			 goto done;
+		}
+		tag_name += 10;
+	} else if (asprintf(&refname, "refs/tags/%s", tag_name) == -1) {
+		 err = got_error_from_errno("asprintf");
+		 goto done;
+	}
+
+	err = got_ref_open(&ref, repo, refname, 0);
+	if (err == NULL) {
+		err = got_error(GOT_ERR_TAG_EXISTS);
+		goto done;
+	} else if (err->code != GOT_ERR_NOT_REF)
+		goto done;
+
+	if (tagmsg_arg == NULL) {
+		err = get_tag_message(&tagmsg, commit_id_str,
+		    got_repo_get_path(repo));
+		if (err)
+			goto done;
+	}
+
+	err = got_object_tag_create(&tag_id, tag_name, commit_id,
+	    tagger, time(NULL), tagmsg ? tagmsg : tagmsg_arg, repo);
+	if (err)
+		goto done;
+
+	err = got_ref_alloc(&ref, refname, tag_id);
+	if (err)
+		goto done;
+
+	err = got_ref_write(ref, repo);
+
+	if (err == NULL) {
+		char *tag_id_str;
+		err = got_object_id_str(&tag_id_str, tag_id);
+		printf("Created tag %s\n", tag_id_str);
+		free(tag_id_str);
+	}
+done:
+	if (ref)
+		got_ref_close(ref);
+	free(commit_id);
+	free(commit_id_str);
+	free(refname);
+	free(tagmsg);
+	return err;
+}
+
+static const struct got_error *
+cmd_tag(int argc, char *argv[])
+{
+	const struct got_error *error = NULL;
+	struct got_repository *repo = NULL;
+	struct got_worktree *worktree = NULL;
+	char *cwd = NULL, *repo_path = NULL, *commit_id_str = NULL;
+	const char *tag_name, *commit_id_arg = NULL, *tagmsg = NULL;
+	int ch, do_list = 0;
+
+	while ((ch = getopt(argc, argv, "m:r:l")) != -1) {
+		switch (ch) {
+		case 'm':
+			tagmsg = optarg;
+			break;
+		case 'r':
+			repo_path = realpath(optarg, NULL);
+			if (repo_path == NULL)
+				err(1, "-r option");
+			got_path_strip_trailing_slashes(repo_path);
+			break;
+		case 'l':
+			do_list = 1;
+			break;
+		default:
+			usage_tag();
+			/* NOTREACHED */
+		}
+	}
+
+	argc -= optind;
+	argv += optind;
+
+	if (do_list) {
+		if (tagmsg)
+			errx(1, "-l and -m options are mutually exclusive\n");
+		if (argc > 0)
+			usage_tag();
+	} else if (argc < 1 || argc > 2)
+		usage_tag();
+	else {
+		tag_name = argv[0];
+		if (argc > 1)
+			commit_id_arg = argv[1];
+	}
+
+#ifndef PROFILE
+	if (do_list) {
+		if (pledge("stdio rpath wpath flock proc exec sendfd unveil",
+		    NULL) == -1)
+			err(1, "pledge");
+	} else {
+		if (pledge("stdio rpath wpath cpath fattr flock proc exec "
+		    "sendfd unveil", NULL) == -1)
+			err(1, "pledge");
+	}
+#endif
+	cwd = getcwd(NULL, 0);
+	if (cwd == NULL) {
+		error = got_error_from_errno("getcwd");
+		goto done;
+	}
+
+	if (repo_path == NULL) {
+		error = got_worktree_open(&worktree, cwd);
+		if (error && error->code != GOT_ERR_NOT_WORKTREE)
+			goto done;
+		else
+			error = NULL;
+		if (worktree) {
+			repo_path =
+			    strdup(got_worktree_get_repo_path(worktree));
+			if (repo_path == NULL)
+				error = got_error_from_errno("strdup");
+			if (error)
+				goto done;
+		} else {
+			repo_path = strdup(cwd);
+			if (repo_path == NULL) {
+				error = got_error_from_errno("strdup");
+				goto done;
+			}
+		}
+	}
+
+	error = got_repo_open(&repo, repo_path);
+	if (error != NULL)
+		goto done;
+
+
+	if (do_list) {
+		error = apply_unveil(got_repo_get_path(repo), 1, NULL);
+		if (error)
+			goto done;
+		error = list_tags(repo, worktree);
+	} else {
+		if (tagmsg) {
+			error = apply_unveil(got_repo_get_path(repo), 1, NULL);
+			if (error)
+				goto done;
+		}
+
+		if (commit_id_arg == NULL) {
+			struct got_reference *head_ref;
+			struct got_object_id *commit_id;
+			error = got_ref_open(&head_ref, repo,
+			    worktree ? got_worktree_get_head_ref_name(worktree)
+			    : GOT_REF_HEAD, 0);
+			if (error)
+				goto done;
+			error = got_ref_resolve(&commit_id, repo, head_ref);
+			got_ref_close(head_ref);
+			if (error)
+				goto done;
+			error = got_object_id_str(&commit_id_str, commit_id);
+			free(commit_id);
+			if (error)
+				goto done;
+		}
+
+		error = add_tag(repo, tag_name,
+		    commit_id_str ? commit_id_str : commit_id_arg, tagmsg);
+	}
+done:
+	if (repo)
+		got_repo_close(repo);
+	if (worktree)
+		got_worktree_close(worktree);
+	free(cwd);
+	free(repo_path);
+	free(commit_id_str);
+	return error;
+}
+
+__dead static void
 usage_add(void)
 {
 	fprintf(stderr, "usage: %s add file-path ...\n", getprogname());
blob - 884a46a8f9776653a7f73ea355dafc30f1f7ff20
blob + b156fbf00a432c54a36fa14acf4c334307dcdd17
--- include/got_error.h
+++ include/got_error.h
@@ -122,6 +122,7 @@
 #define GOT_ERR_STAGED_PATHS	106
 #define GOT_ERR_PATCH_CHOICE	107
 #define GOT_ERR_COMMIT_NO_EMAIL	108
+#define GOT_ERR_TAG_EXISTS	109
 
 static const struct got_error {
 	int code;
@@ -250,6 +251,7 @@ static const struct got_error {
 	{ GOT_ERR_COMMIT_NO_EMAIL,"GOT_AUTHOR environment variable contains "
 	    "no email address; an email address is required for compatibility "
 	    "with Git" },
+	{ GOT_ERR_TAG_EXISTS,"specified tag already exists" },
 };
 
 /*
blob - b0bf900c64d3a893263960c80bff6c6e0e3ac6cb
blob + 93db9af0036c3dc58da8d9f1c76664ddf7fde238
--- include/got_object.h
+++ include/got_object.h
@@ -275,3 +275,8 @@ const char *got_object_tag_get_message(struct got_tag_
 
 const struct got_error *got_object_commit_add_parent(struct got_commit_object *,
     const char *);
+
+/* Create a new tag object in the repository. */
+const struct got_error *got_object_tag_create(struct got_object_id **,
+    const char *, struct got_object_id *, const char *,
+    time_t, const char *, struct got_repository *);
blob - ac485f2d0f28c395fbed5974f9a5a3bd0bfa5e74
blob + e790d9a523cf97c0b9589729659bd763e331c45e
--- lib/object_create.c
+++ lib/object_create.c
@@ -593,6 +593,184 @@ done:
 	free(author_str);
 	free(committer_str);
 	if (commitfile && fclose(commitfile) != 0 && err == NULL)
+		err = got_error_from_errno("fclose");
+	if (err) {
+		free(*id);
+		*id = NULL;
+	}
+	return err;
+}
+
+const struct got_error *
+got_object_tag_create(struct got_object_id **id,
+    const char *tag_name, struct got_object_id *object_id, const char *tagger,
+    time_t tagger_time, const char *tagmsg, struct got_repository *repo)
+{
+	const struct got_error *err = NULL;
+	SHA1_CTX sha1_ctx;
+	char *header = NULL;
+	char *tag_str = NULL, *tagger_str = NULL;
+	char *id_str = NULL, *obj_str = NULL, *type_str = NULL;
+	size_t headerlen, len = 0, n;
+	FILE *tagfile = NULL;
+	char *msg0 = NULL, *msg;
+	const char *obj_type_str;
+	int obj_type;
+
+	*id = NULL;
+
+	SHA1Init(&sha1_ctx);
+
+	err = got_object_id_str(&id_str, object_id);
+	if (err)
+		goto done;
+	if (asprintf(&obj_str, "%s%s\n", GOT_TAG_LABEL_OBJECT, id_str) == -1) {
+		err = got_error_from_errno("asprintf");
+		goto done;
+	}
+
+	err = got_object_get_type(&obj_type, repo, object_id);
+	if (err)
+		goto done;
+
+	switch (obj_type) {
+	case GOT_OBJ_TYPE_BLOB:
+		obj_type_str = GOT_OBJ_LABEL_BLOB;
+		break;
+	case GOT_OBJ_TYPE_TREE:
+		obj_type_str = GOT_OBJ_LABEL_TREE;
+		break;
+	case GOT_OBJ_TYPE_COMMIT:
+		obj_type_str = GOT_OBJ_LABEL_COMMIT;
+		break;
+	case GOT_OBJ_TYPE_TAG:
+		obj_type_str = GOT_OBJ_LABEL_TAG;
+		break;
+	default:
+		err = got_error(GOT_ERR_OBJ_TYPE);
+		goto done;
+	}
+
+	if (asprintf(&type_str, "%s%s\n", GOT_TAG_LABEL_TYPE,
+	    obj_type_str) == -1) {
+		err = got_error_from_errno("asprintf");
+		goto done;
+	}
+
+	if (asprintf(&tag_str, "%s%s\n", GOT_TAG_LABEL_TAG, tag_name) == -1) {
+		err = got_error_from_errno("asprintf");
+		goto done;
+	}
+
+	if (asprintf(&tagger_str, "%s%s %lld +0000\n",
+	    GOT_COMMIT_LABEL_AUTHOR, tagger, tagger_time) == -1)
+		return got_error_from_errno("asprintf");
+
+	msg0 = strdup(tagmsg);
+	if (msg0 == NULL) {
+		err = got_error_from_errno("strdup");
+		goto done;
+	}
+	msg = msg0;
+
+	while (isspace((unsigned char)msg[0]))
+		msg++;
+
+	len = strlen(obj_str) + strlen(type_str) + strlen(tag_str) +
+	    strlen(tagger_str) + 1 + strlen(msg) + 1;
+
+	if (asprintf(&header, "%s %zd", GOT_OBJ_LABEL_TAG, len) == -1) {
+		err = got_error_from_errno("asprintf");
+		goto done;
+	}
+
+	headerlen = strlen(header) + 1;
+	SHA1Update(&sha1_ctx, header, headerlen);
+
+	tagfile = got_opentemp();
+	if (tagfile == NULL) {
+		err = got_error_from_errno("got_opentemp");
+		goto done;
+	}
+
+	n = fwrite(header, 1, headerlen, tagfile);
+	if (n != headerlen) {
+		err = got_ferror(tagfile, GOT_ERR_IO);
+		goto done;
+	}
+	len = strlen(obj_str);
+	SHA1Update(&sha1_ctx, obj_str, len);
+	n = fwrite(obj_str, 1, len, tagfile);
+	if (n != len) {
+		err = got_ferror(tagfile, GOT_ERR_IO);
+		goto done;
+	}
+	len = strlen(type_str);
+	SHA1Update(&sha1_ctx, type_str, len);
+	n = fwrite(type_str, 1, len, tagfile);
+	if (n != len) {
+		err = got_ferror(tagfile, GOT_ERR_IO);
+		goto done;
+	}
+
+	len = strlen(tag_str);
+	SHA1Update(&sha1_ctx, tag_str, len);
+	n = fwrite(tag_str, 1, len, tagfile);
+	if (n != len) {
+		err = got_ferror(tagfile, GOT_ERR_IO);
+		goto done;
+	}
+
+	len = strlen(tagger_str);
+	SHA1Update(&sha1_ctx, tagger_str, len);
+	n = fwrite(tagger_str, 1, len, tagfile);
+	if (n != len) {
+		err = got_ferror(tagfile, GOT_ERR_IO);
+		goto done;
+	}
+
+	SHA1Update(&sha1_ctx, "\n", 1);
+	n = fwrite("\n", 1, 1, tagfile);
+	if (n != 1) {
+		err = got_ferror(tagfile, GOT_ERR_IO);
+		goto done;
+	}
+
+	len = strlen(msg);
+	SHA1Update(&sha1_ctx, msg, len);
+	n = fwrite(msg, 1, len, tagfile);
+	if (n != len) {
+		err = got_ferror(tagfile, GOT_ERR_IO);
+		goto done;
+	}
+
+	SHA1Update(&sha1_ctx, "\n", 1);
+	n = fwrite("\n", 1, 1, tagfile);
+	if (n != 1) {
+		err = got_ferror(tagfile, GOT_ERR_IO);
+		goto done;
+	}
+
+	*id = malloc(sizeof(**id));
+	if (*id == NULL) {
+		err = got_error_from_errno("malloc");
+		goto done;
+	}
+	SHA1Final((*id)->sha1, &sha1_ctx);
+
+	if (fflush(tagfile) != 0) {
+		err = got_error_from_errno("fflush");
+		goto done;
+	}
+	rewind(tagfile);
+
+	err = create_object_file(*id, tagfile, repo);
+done:
+	free(msg0);
+	free(header);
+	free(obj_str);
+	free(tagger_str);
+	if (tagfile && fclose(tagfile) != 0 && err == NULL)
 		err = got_error_from_errno("fclose");
 	if (err) {
 		free(*id);
blob - 010a6bbd9da121a641c0c2f1321b9919f736476f
blob + 535e0b8450f2e076c694aaae464ec319797b783f
--- regress/cmdline/Makefile
+++ regress/cmdline/Makefile
@@ -1,5 +1,6 @@
-REGRESS_TARGETS=checkout update status log add rm diff blame branch ref commit \
-	revert cherrypick backout rebase import histedit stage unstage cat
+REGRESS_TARGETS=checkout update status log add rm diff blame branch tag \
+	ref commit revert cherrypick backout rebase import histedit stage \
+	unstage cat
 NOOBJ=Yes
 
 checkout:
@@ -29,6 +30,9 @@ blame:
 branch:
 	./branch.sh
 
+tag:
+	./tag.sh
+
 ref:
 	./ref.sh
 
blob - 346b80d2069558d1f86a09e0cce41f0e2d7f7a94
blob + 1b91f45a44501291c93f9f5e151ef448f9e31e77
--- regress/cmdline/common.sh
+++ regress/cmdline/common.sh
@@ -56,6 +56,14 @@ function git_show_author_time
 	(cd $repo && git show --no-patch --pretty='format:%at' $object)
 }
 
+function git_show_tagger_time
+{
+	local repo="$1"
+	local tag="$2"
+	(cd $repo && git cat-file tag $tag | grep ^tagger | \
+		sed -e "s/^tagger $GOT_AUTHOR//" | cut -d' ' -f2)
+}
+
 function git_show_parent_commit
 {
 	local repo="$1"
blob - /dev/null
blob + d76ab174ed2d704f7ea8afe42e9e6b7e6736a52e (mode 755)
--- /dev/null
+++ regress/cmdline/tag.sh
@@ -0,0 +1,172 @@
+#!/bin/sh
+#
+# Copyright (c) 2019 Stefan Sperling <stsp@openbsd.org>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+. ./common.sh
+
+function test_tag_create {
+	local testroot=`test_init tag_create`
+	local commit_id=`git_show_head $testroot/repo`
+	local tag=1.0.0
+	local tag2=2.0.0
+
+	# Create a tag based on repository's HEAD reference
+	got tag -m 'test' -r $testroot/repo $tag HEAD > $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got ref command failed unexpectedly"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	tag_id=`got ref -r $testroot/repo -l \
+		| grep "^refs/tags/$tag" | tr -d ' ' | cut -d: -f2`
+	echo "Created tag $tag_id" > $testroot/stdout.expected
+	cmp -s $testroot/stdout $testroot/stdout.expected
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# Ensure that Git recognizes the tag Got has created
+	(cd $testroot/repo && git checkout -q $tag)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "git checkout command failed unexpectedly"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# Ensure Got recognizes the new tag
+	got checkout -c $tag $testroot/repo $testroot/wt >/dev/null
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got checkout command failed unexpectedly"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# Create a tag based on implied worktree HEAD ref
+	(cd $testroot/wt && got tag -m 'test' $tag2 > $testroot/stdout)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	tag_id2=`got ref -r $testroot/repo -l \
+		| grep "^refs/tags/$tag2" | tr -d ' ' | cut -d: -f2`
+	echo "Created tag $tag_id2" > $testroot/stdout.expected
+	cmp -s $testroot/stdout $testroot/stdout.expected
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/repo && git checkout -q $tag2)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "git checkout command failed unexpectedly"
+		test_done "$testroot" "$ret"
+	fi
+
+	# Attempt to create a tag pointing at a non-commit
+	local tree_id=`git_show_tree $testroot/repo`
+	(cd $testroot/wt && got tag -m 'test' foobar $tree_id \
+		2> $testroot/stderr)
+	ret="$?"
+	if [ "$ret" == "0" ]; then
+		echo "git tag command succeeded unexpectedly"
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	echo "got: object not found" > $testroot/stderr.expected
+	cmp -s $testroot/stderr $testroot/stderr.expected
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	got ref -r $testroot/repo -l > $testroot/stdout
+	echo "HEAD: $commit_id" > $testroot/stdout.expected
+	echo -n "refs/got/worktree/base-" >> $testroot/stdout.expected
+	cat $testroot/wt/.got/uuid | tr -d '\n' >> $testroot/stdout.expected
+	echo ": $commit_id" >> $testroot/stdout.expected
+	echo "refs/heads/master: $commit_id" >> $testroot/stdout.expected
+	echo "refs/tags/$tag: $tag_id" >> $testroot/stdout.expected
+	echo "refs/tags/$tag2: $tag_id2" >> $testroot/stdout.expected
+	cmp -s $testroot/stdout $testroot/stdout.expected
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+	fi
+	test_done "$testroot" "$ret"
+}
+
+function test_tag_list {
+	local testroot=`test_init tag_list`
+	local commit_id=`git_show_head $testroot/repo`
+	local tag=1.0.0
+	local tag2=2.0.0
+
+	(cd $testroot/repo && git tag -a -m 'test' $tag)
+	(cd $testroot/repo && git tag -a -m 'test' $tag2)
+
+	tag_id=`got ref -r $testroot/repo -l \
+		| grep "^refs/tags/$tag" | tr -d ' ' | cut -d: -f2`
+	local tagger_time=`git_show_tagger_time $testroot/repo $tag`
+	d1=`env TZ=UTC date -r $tagger_time +"%a %b %d %X %Y UTC"`
+	tag_id2=`got ref -r $testroot/repo -l \
+		| grep "^refs/tags/$tag2" | tr -d ' ' | cut -d: -f2`
+	local tagger_time2=`git_show_tagger_time $testroot/repo $tag2`
+	d2=`env TZ=UTC date -r $tagger_time2 +"%a %b %d %X %Y UTC"`
+
+	got tag -r $testroot/repo -l > $testroot/stdout
+
+	echo "-----------------------------------------------" \
+		> $testroot/stdout.expected
+	echo "tag $tag $tag_id" >> $testroot/stdout.expected
+	echo "commit $commit_id" >> $testroot/stdout.expected
+	echo "from: $GOT_AUTHOR" >> $testroot/stdout.expected
+	echo "date: $d1" >> $testroot/stdout.expected
+	echo " " >> $testroot/stdout.expected
+	echo " test" >> $testroot/stdout.expected
+	echo " " >> $testroot/stdout.expected
+	echo "-----------------------------------------------" \
+		>> $testroot/stdout.expected
+	echo "tag $tag2 $tag_id2" >> $testroot/stdout.expected
+	echo "commit $commit_id" >> $testroot/stdout.expected
+	echo "from: $GOT_AUTHOR" >> $testroot/stdout.expected
+	echo "date: $d2" >> $testroot/stdout.expected
+	echo " " >> $testroot/stdout.expected
+	echo " test" >> $testroot/stdout.expected
+	echo " " >> $testroot/stdout.expected
+	cmp -s $testroot/stdout $testroot/stdout.expected
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+	fi
+	test_done "$testroot" "$ret"
+}
+
+run_test test_tag_create
+run_test test_tag_list