threads / patch / 8990

patch, 2 partsMake git-tag a builtin.

Subject: [PATCH 2/2] Make git-tag a builtin.

## tl;dr

3 messages between Jul 11, 2007 and Jul 12, 2007. Diffs are folded; open one to read it.

replies: 2people: 2as markdown or json

Carlos Rica· Jul 11, 2007, 18:54 UTC · lore

This replaces the script "git-tag.sh" with "builtin-tag.c". It is based in a previous work on it from Kristian Høgsberg.

There are some minor changes in the behaviour of "git tag" here: "git tag -v" now can get more than one tag to verify, like "git tag -d" did, "git tag" with no arguments prints all tags, more like "git branch" does, and the template for the edited message adds an empty line, like in "git commit".

The program is now calling to the script "git verify-tag" for verify, something that should be changed porting it to C and calling its functions directly from builtin-tag.c.

Signed-off-by: Carlos Rica <jasampler@gmail.com>
---
  As said, "git verify-tag" should be replaced with "git tag" for
  verify tags, since both do the same job.  The builtin "git mktag"
  is another candidate to be replaced with "git tag", however
  it is now an essential part of the "plumbing" family doing
  a neat thing not easily replaceable.
  The function launch_editor is copied and modified from the initial work
  from Kristian on builtin-commit.c, so we expect share that code on
  both builtins when it is finnished.
 Makefile      |    3 +-
 builtin-tag.c |  430 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 builtin.h     |    1 +
 git-tag.sh    |  205 ---------------------------
 git.c         |    1 +
 5 files changed, 434 insertions(+), 206 deletions(-)
 create mode 100644 builtin-tag.c
 delete mode 100755 git-tag.sh
Show changes to 5 files +434 −206

Makefile, builtin-tag.c, builtin.h, git-tag.sh, git.c

diff --git a/Makefile b/Makefile
index d7541b4..f0da8f7 100644
--- a/Makefile
+++ b/Makefile
@@ -207,7 +207,7 @@ SCRIPT_SH = \
 	git-pull.sh git-rebase.sh git-rebase--interactive.sh \
 	git-repack.sh git-request-pull.sh git-reset.sh \
 	git-sh-setup.sh \
-	git-tag.sh git-verify-tag.sh \
+	git-verify-tag.sh \
 	git-am.sh \
 	git-merge.sh git-merge-stupid.sh git-merge-octopus.sh \
 	git-merge-resolve.sh git-merge-ours.sh \
@@ -376,6 +376,7 @@ BUILTIN_OBJS = \
 	builtin-show-branch.o \
 	builtin-stripspace.o \
 	builtin-symbolic-ref.o \
+	builtin-tag.o \
 	builtin-tar-tree.o \
 	builtin-unpack-objects.o \
 	builtin-update-index.o \
diff --git a/builtin-tag.c b/builtin-tag.c
new file mode 100644
index 0000000..1824379
--- /dev/null
+++ b/builtin-tag.c
@@ -0,0 +1,430 @@
+/*
+ * Builtin "git tag"
+ *
+ * Copyright (c) 2007 Kristian HÞgsberg <krh@redhat.com>,
+ *                    Carlos Rica <jasampler@gmail.com>
+ * Based on git-tag.sh and mktag.c by Linus Torvalds.
+ */
+
+#include "cache.h"
+#include "builtin.h"
+#include "refs.h"
+#include "tag.h"
+#include "run-command.h"
+
+static const char builtin_tag_usage[] =
+  "git-tag [-n [<num>]] -l [<pattern>] | [-a | -s | -u <key-id>] [-f | -d | -v] [-m <msg>] <tagname> [<head>]";
+
+static char signingkey[1000];
+static int lines;
+
+static void launch_editor(const char *path, char **buffer, unsigned long *len)
+{
+	const char *editor, *terminal;
+	struct child_process child;
+	const char *args[3];
+	int fd;
+
+	editor = getenv("VISUAL");
+	if (!editor)
+		editor = getenv("EDITOR");
+
+	terminal = getenv("TERM");
+	if (!editor && (!terminal || !strcmp(terminal, "dumb"))) {
+		fprintf(stderr,
+		"Terminal is dumb but no VISUAL nor EDITOR defined.\n"
+		"Please supply the message using either -m or -F option.\n");
+		exit(1);
+	}
+
+	if (!editor)
+		editor = "vi";
+
+	memset(&child, 0, sizeof(child));
+	child.argv = args;
+	args[0] = editor;
+	args[1] = path;
+	args[2] = NULL;
+
+	if (run_command(&child))
+		die("could not launch editor %s.", editor);
+
+	fd = open(path, O_RDONLY);
+	if (fd == -1)
+		die("could not read %s.", path);
+	if (read_pipe(fd, buffer, len))
+		die("could not read message file '%s': %s",
+		    path, strerror(errno));
+	close(fd);
+}
+
+#define PGP_SIGNATURE "-----BEGIN PGP SIGNATURE-----"
+
+static int show_reference(const char *refname, const unsigned char *sha1,
+			  int flag, void *cb_data)
+{
+	const char *pattern = cb_data;
+
+	if (!fnmatch(pattern, refname, 0)) {
+		int i;
+		unsigned long size;
+		enum object_type type;
+		char *buf, *sp, *eol;
+		size_t len;
+
+		if (!lines) {
+			printf("%s\n", refname);
+			return 0;
+		}
+		printf("%-15s ", refname);
+
+		sp = buf = read_sha1_file(sha1, &type, &size);
+		if (!buf || !size)
+			return 0;
+		/* skip header */
+		while (sp + 1 < buf + size &&
+				!(sp[0] == '\n' && sp[1] == '\n'))
+			sp++;
+		/* only take up to "lines" lines, and strip the signature */
+		for (i = 0, sp += 2; i < lines && sp < buf + size &&
+				prefixcmp(sp, PGP_SIGNATURE "\n");
+				i++) {
+			if (i)
+				printf("\n    ");
+			eol = memchr(sp, '\n', size - (sp - buf));
+			len = eol ? eol - sp : size - (sp - buf);
+			fwrite(sp, len, 1, stdout);
+			if (!eol)
+				break;
+			sp = eol + 1;
+		}
+		putchar('\n');
+		free(buf);
+	}
+
+	return 0;
+}
+
+static int list_tags(const char *pattern)
+{
+	char *newpattern;
+
+	if (pattern == NULL)
+		pattern = "";
+
+	/* prepend/append * to the shell pattern: */
+	newpattern = xmalloc(strlen(pattern) + 3);
+	sprintf(newpattern, "*%s*", pattern);
+
+	for_each_tag_ref(show_reference, (void *) newpattern);
+
+	free(newpattern);
+
+	return 0;
+}
+
+
+static int delete_tags(const char **argv)
+{
+	const char **p;
+	char ref[PATH_MAX];
+	int had_error = 0;
+	unsigned char sha1[20];
+
+	for (p = argv; *p; p++) {
+		if (snprintf(ref, sizeof ref, "refs/tags/%s", *p) > sizeof ref)
+			die("tag name '%s' too long.", *p);
+		if (!resolve_ref(ref, sha1, 1, NULL)) {
+			fprintf(stderr, "tag '%s' not found.\n", *p);
+			had_error = 1;
+			continue;
+		}
+
+		if (delete_ref(ref, sha1))
+			had_error = 1;
+		else
+			printf("Deleted tag '%s'\n", *p);
+	}
+
+	return had_error;
+}
+
+static int run_verify_tag_command(unsigned char *sha1)
+{
+	int ret;
+	const char *argv_verify_tag[] = {"git-verify-tag",
+					"-v", "SHA1_HEX", NULL};
+	argv_verify_tag[2] = sha1_to_hex(sha1);
+
+	ret = run_command_v_opt(argv_verify_tag, 0);
+
+	if (ret <= -10000)
+		die("unable to run %s\n", argv_verify_tag[0]);
+	return -ret;
+}
+
+static int verify_tags(const char **argv)
+{
+	const char **p;
+	char ref[PATH_MAX];
+	int had_error = 0;
+	unsigned char sha1[20];
+
+	for (p = argv; *p; p++) {
+		if (snprintf(ref, sizeof ref, "refs/tags/%s", *p) > sizeof ref)
+			die("tag name '%s' too long.", *p);
+
+		if (!resolve_ref(ref, sha1, 1, NULL)) {
+			fprintf(stderr, "tag '%s' not found.\n", *p);
+			had_error = 1;
+			continue;
+		}
+		if (run_verify_tag_command(sha1))
+			had_error = 1;
+	}
+
+	return had_error;
+}
+
+static int do_sign(char *buffer, size_t size, size_t max)
+{
+	struct child_process gpg;
+	const char *args[4];
+	char *bracket;
+	int len;
+
+	if (signingkey[0] == '\0') {
+		strlcpy(signingkey, git_committer_info(1), sizeof signingkey);
+		bracket = strchr(signingkey, '>');
+		if (bracket)
+			bracket[1] = '\0';
+	}
+
+	memset(&gpg, 0, sizeof(gpg));
+	gpg.argv = args;
+	gpg.in = -1;
+	gpg.out = -1;
+	args[0] = "gpg";
+	args[1] = "-bsau";
+	args[2] = signingkey;
+	args[3] = NULL;
+
+	if (start_command(&gpg))
+		die("could not run gpg.");
+
+	write_or_die(gpg.in, buffer, size);
+	close(gpg.in);
+	gpg.close_in = 0;
+	len = read_in_full(gpg.out, buffer + size, max - size);
+
+	finish_command(&gpg);
+
+	return size + len;
+}
+
+static const char tag_template[] =
+	"\n"
+	"#\n"
+	"# Write a tag message\n"
+	"#\n";
+
+static int git_tag_config(const char *var, const char *value)
+{
+	if (!strcmp(var, "user.signingkey")) {
+		if (!value)
+			die("user.signingkey without value");
+		strlcpy(signingkey, value, sizeof signingkey);
+		return 0;
+	}
+
+	return git_default_config(var, value);
+}
+
+#define MAX_SIGNATURE_LENGTH 1024
+/* message must be NULL or allocated, it will be reallocated and freed */
+static void create_tag(const unsigned char *object, const char *tag,
+		       char *message, int sign, unsigned char *result)
+{
+	enum object_type type;
+	char header_buf[1024], *buffer;
+	int header_len, max_size;
+	unsigned long size;
+
+	type = sha1_object_info(object, NULL);
+	if (type <= 0)
+	    die("bad object type.");
+
+	header_len = snprintf(header_buf, sizeof header_buf,
+			  "object %s\n"
+			  "type %s\n"
+			  "tag %s\n"
+			  "tagger %s\n\n",
+			  sha1_to_hex(object),
+			  typename(type),
+			  tag,
+			  git_committer_info(1));
+
+	if (header_len >= sizeof header_buf)
+		die("tag header too big.");
+
+	if (!message) {
+		char *path;
+		int fd;
+
+		/* write the template message before editing: */
+		path = xstrdup(git_path("TAG_EDITMSG"));
+		fd = open(path, O_CREAT | O_TRUNC | O_WRONLY, 0600);
+		if (fd < 0)
+			die("could not create file %s.", path);
+		write_or_die(fd, tag_template, strlen(tag_template));
+		close(fd);
+
+		launch_editor(path, &buffer, &size);
+
+		unlink(path);
+		free(path);
+	}
+	else {
+		buffer = message;
+		size = strlen(message);
+	}
+
+	size = stripspace(buffer, size, 1);
+
+	if (!message && !size)
+		die("no tag message?");
+
+	/* insert the header and add the '\n' if needed: */
+	max_size = header_len + size + (sign ? MAX_SIGNATURE_LENGTH : 0) + 1;
+	buffer = xrealloc(buffer, max_size);
+	if (size)
+		buffer[size++] = '\n';
+	memmove(buffer + header_len, buffer, size);
+	memcpy(buffer, header_buf, header_len);
+	size += header_len;
+
+	if (sign)
+		size = do_sign(buffer, size, max_size);
+
+	if (write_sha1_file(buffer, size, tag_type, result) < 0)
+		die("unable to write tag file");
+	free(buffer);
+}
+
+int cmd_tag(int argc, const char **argv, const char *prefix)
+{
+	unsigned char object[20], prev[20];
+	int annotate = 0, sign = 0, force = 0;
+	char *message = NULL;
+	char ref[PATH_MAX];
+	const char *object_ref, *tag;
+	int i, fd;
+	struct ref_lock *lock;
+
+	git_config(git_tag_config);
+
+	for (i = 1; i < argc; i++) {
+		const char *arg = argv[i];
+
+		if (arg[0] != '-')
+			break;
+		if (!strcmp(arg, "-a")) {
+			annotate = 1;
+			continue;
+		}
+		if (!strcmp(arg, "-s")) {
+			annotate = 1;
+			sign = 1;
+			continue;
+		}
+		if (!strcmp(arg, "-f")) {
+			force = 1;
+			continue;
+		}
+		if (!strcmp(arg, "-n")) {
+			if (i + 1 == argc || *argv[i + 1] == '-')
+				/* no argument */
+				lines = 1;
+			else
+				lines = isdigit(*argv[++i]) ?
+					atoi(argv[i]) : 1;
+			continue;
+		}
+		if (!strcmp(arg, "-m")) {
+			annotate = 1;
+			i++;
+			if  (i == argc)
+				die("option -m needs an argument.");
+			message = xstrdup(argv[i]);
+			continue;
+		}
+		if (!strcmp(arg, "-F")) {
+			unsigned long len;
+			annotate = 1;
+			i++;
+			if  (i == argc)
+				die("option -F needs an argument.");
+
+			fd = open(argv[i], O_RDONLY);
+			if (fd < 0)
+				die("cannot open %s", argv[1]);
+
+			len = 1024;
+			message = xmalloc(len);
+			if (read_pipe(fd, &message, &len))
+				die("cannot read %s", argv[1]);
+			message = xrealloc(message, len + 1);
+			message[len] = '\0';
+			continue;
+		}
+		if (!strcmp(arg, "-u")) {
+			annotate = 1;
+			sign = 1;
+			i++;
+			if  (i == argc)
+				die("option -u needs an argument.");
+			strlcpy(signingkey, argv[i], sizeof signingkey);
+			continue;
+		}
+		if (!strcmp(arg, "-l")) {
+			return list_tags(argv[i + 1]);
+		}
+		if (!strcmp(arg, "-d")) {
+			return delete_tags(argv + i + 1);
+		}
+		if (!strcmp(arg, "-v")) {
+			return verify_tags(argv + i + 1);
+		}
+		usage(builtin_tag_usage);
+	}
+
+	if (i == argc)
+		return list_tags(NULL);
+	tag = argv[i++];
+
+	object_ref = i < argc ? argv[i] : "HEAD";
+
+	if (get_sha1(object_ref, object))
+		die("Failed to resolve '%s' as a valid ref.", object_ref);
+
+	if (snprintf(ref, sizeof ref, "refs/tags/%s", tag) > sizeof ref)
+		die("tag '%s' too long.", tag);
+	if (check_ref_format(ref))
+		die("'%s' is not a valid tag name.", tag);
+
+	if (!resolve_ref(ref, prev, 1, NULL))
+		hashclr(prev);
+	else if (!force)
+		die("tag '%s' already exists", tag);
+
+	if (annotate)
+		create_tag(object, tag, message, sign, object);
+
+	lock = lock_any_ref_for_update(ref, prev, 0);
+	if (!lock)
+		die("%s: cannot lock the ref", ref);
+	if (write_ref_sha1(lock, object, NULL) < 0)
+		die("%s: cannot update the ref", ref);
+
+	return 0;
+}
diff --git a/builtin.h b/builtin.h
index 4cc228d..ac7417f 100644
--- a/builtin.h
+++ b/builtin.h
@@ -70,6 +70,7 @@ extern int cmd_show(int argc, const char **argv, const char *prefix);
 extern int cmd_show_branch(int argc, const char **argv, const char *prefix);
 extern int cmd_stripspace(int argc, const char **argv, const char *prefix);
 extern int cmd_symbolic_ref(int argc, const char **argv, const char *prefix);
+extern int cmd_tag(int argc, const char **argv, const char *prefix);
 extern int cmd_tar_tree(int argc, const char **argv, const char *prefix);
 extern int cmd_unpack_objects(int argc, const char **argv, const char *prefix);
 extern int cmd_update_index(int argc, const char **argv, const char *prefix);
diff --git a/git-tag.sh b/git-tag.sh
deleted file mode 100755
index 1c25d88..0000000
--- a/git-tag.sh
+++ /dev/null
@@ -1,205 +0,0 @@
-#!/bin/sh
-# Copyright (c) 2005 Linus Torvalds
-
-USAGE='[-n [<num>]] -l [<pattern>] | [-a | -s | -u <key-id>] [-f | -d | -v] [-m <msg>] <tagname> [<head>]'
-SUBDIRECTORY_OK='Yes'
-. git-sh-setup
-
-message_given=
-annotate=
-signed=
-force=
-message=
-username=
-list=
-verify=
-LINES=0
-while case "$#" in 0) break ;; esac
-do
-    case "$1" in
-    -a)
-	annotate=1
-	shift
-	;;
-    -s)
-	annotate=1
-	signed=1
-	shift
-	;;
-    -f)
-	force=1
-	shift
-	;;
-    -n)
-        case "$#,$2" in
-	1,* | *,-*)
-		LINES=1 	# no argument
-		;;
-	*)	shift
-		LINES=$(expr "$1" : '\([0-9]*\)')
-		[ -z "$LINES" ] && LINES=1 # 1 line is default when -n is used
-		;;
-	esac
-	shift
-	;;
-    -l)
-	list=1
-	shift
-	case $# in
-	0)	PATTERN=
-		;;
-	*)
-		PATTERN="$1"	# select tags by shell pattern, not re
-		shift
-		;;
-	esac
-	git rev-parse --symbolic --tags | sort |
-	    while read TAG
-	    do
-	        case "$TAG" in
-		*$PATTERN*) ;;
-		*)	    continue ;;
-		esac
-		[ "$LINES" -le 0 ] && { echo "$TAG"; continue ;}
-		OBJTYPE=$(git cat-file -t "$TAG")
-		case $OBJTYPE in
-		tag)
-			ANNOTATION=$(git cat-file tag "$TAG" |
-				sed -e '1,/^$/d' |
-				sed -n -e "
-					/^-----BEGIN PGP SIGNATURE-----\$/q
-					2,\$s/^/    /
-					p
-					${LINES}q
-				")
-			printf "%-15s %s\n" "$TAG" "$ANNOTATION"
-			;;
-		*)      echo "$TAG"
-			;;
-		esac
-	    done
-	;;
-    -m)
-	annotate=1
-	shift
-	message="$1"
-	if test "$#" = "0"; then
-	    die "error: option -m needs an argument"
-	else
-	    message="$1"
-	    message_given=1
-	    shift
-	fi
-	;;
-    -F)
-	annotate=1
-	shift
-	if test "$#" = "0"; then
-	    die "error: option -F needs an argument"
-	else
-	    message="$(cat "$1")"
-	    message_given=1
-	    shift
-	fi
-	;;
-    -u)
-	annotate=1
-	signed=1
-	shift
-	if test "$#" = "0"; then
-	    die "error: option -u needs an argument"
-	else
-	    username="$1"
-	    shift
-	fi
-	;;
-    -d)
-	shift
-	had_error=0
-	for tag
-	do
-		cur=$(git show-ref --verify --hash -- "refs/tags/$tag") || {
-			echo >&2 "Seriously, what tag are you talking about?"
-			had_error=1
-			continue
-		}
-		git update-ref -m 'tag: delete' -d "refs/tags/$tag" "$cur" || {
-			had_error=1
-			continue
-		}
-		echo "Deleted tag $tag."
-	done
-	exit $had_error
-	;;
-    -v)
-	shift
-	tag_name="$1"
-	tag=$(git show-ref --verify --hash -- "refs/tags/$tag_name") ||
-		die "Seriously, what tag are you talking about?"
-	git-verify-tag -v "$tag"
-	exit $?
-	;;
-    -*)
-        usage
-	;;
-    *)
-	break
-	;;
-    esac
-done
-
-[ -n "$list" ] && exit 0
-
-name="$1"
-[ "$name" ] || usage
-prev=0000000000000000000000000000000000000000
-if git show-ref --verify --quiet -- "refs/tags/$name"
-then
-    test -n "$force" || die "tag '$name' already exists"
-    prev=`git rev-parse "refs/tags/$name"`
-fi
-shift
-git check-ref-format "tags/$name" ||
-	die "we do not like '$name' as a tag name."
-
-object=$(git rev-parse --verify --default HEAD "$@") || exit 1
-type=$(git cat-file -t $object) || exit 1
-tagger=$(git-var GIT_COMMITTER_IDENT) || exit 1
-
-test -n "$username" ||
-	username=$(git repo-config user.signingkey) ||
-	username=$(expr "z$tagger" : 'z\(.*>\)')
-
-trap 'rm -f "$GIT_DIR"/TAG_TMP* "$GIT_DIR"/TAG_FINALMSG "$GIT_DIR"/TAG_EDITMSG' 0
-
-if [ "$annotate" ]; then
-    if [ -z "$message_given" ]; then
-        ( echo "#"
-          echo "# Write a tag message"
-          echo "#" ) > "$GIT_DIR"/TAG_EDITMSG
-        ${VISUAL:-${EDITOR:-vi}} "$GIT_DIR"/TAG_EDITMSG || exit
-    else
-        printf '%s\n' "$message" >"$GIT_DIR"/TAG_EDITMSG
-    fi
-
-    grep -v '^#' <"$GIT_DIR"/TAG_EDITMSG |
-    git stripspace >"$GIT_DIR"/TAG_FINALMSG
-
-    [ -s "$GIT_DIR"/TAG_FINALMSG -o -n "$message_given" ] || {
-	echo >&2 "No tag message?"
-	exit 1
-    }
-
-    ( printf 'object %s\ntype %s\ntag %s\ntagger %s\n\n' \
-	"$object" "$type" "$name" "$tagger";
-      cat "$GIT_DIR"/TAG_FINALMSG ) >"$GIT_DIR"/TAG_TMP
-    rm -f "$GIT_DIR"/TAG_TMP.asc "$GIT_DIR"/TAG_FINALMSG
-    if [ "$signed" ]; then
-	gpg -bsa -u "$username" "$GIT_DIR"/TAG_TMP &&
-	cat "$GIT_DIR"/TAG_TMP.asc >>"$GIT_DIR"/TAG_TMP ||
-	die "failed to sign the tag with GPG."
-    fi
-    object=$(git-mktag < "$GIT_DIR"/TAG_TMP)
-fi
-
-git update-ref "refs/tags/$name" "$object" "$prev"
diff --git a/git.c b/git.c
index a647f9c..eb9e5ca 100644
--- a/git.c
+++ b/git.c
@@ -363,6 +363,7 @@ static void handle_internal_command(int argc, const char **argv)
 		{ "show", cmd_show, RUN_SETUP | USE_PAGER },
 		{ "stripspace", cmd_stripspace },
 		{ "symbolic-ref", cmd_symbolic_ref, RUN_SETUP },
+		{ "tag", cmd_tag, RUN_SETUP },
 		{ "tar-tree", cmd_tar_tree },
 		{ "unpack-objects", cmd_unpack_objects, RUN_SETUP },
 		{ "update-index", cmd_update_index, RUN_SETUP },
-- 
1.5.0
Junio C Hamano· Jul 12, 2007, 05:46 UTC · re: Carlos Rica · lore

Re: [PATCH 2/2] Make git-tag a builtin.

Carlos Rica <jasampler@gmail.com> writes:
> Mime-Version: 1.0
> Content-Type: text/plain; charset=windows-1252
Hmmmmmm.....
Show 7 quoted lines
> This replaces the script "git-tag.sh" with "builtin-tag.c".
> It is based in a previous work on it from Kristian Høgsberg.
>
> There are some minor changes in the behaviour of "git tag" here:
> "git tag -v" now can get more than one tag to verify, like "git tag -d" did,
> "git tag" with no arguments prints all tags, more like "git branch" does, and
> the template for the edited message adds an empty line, like in "git commit".

These probably are good changes (except that "no argument" case might be a bit annoying, but I guess it's Ok). Is there any need for documentation updates with these changes?

Show 10 quoted lines
> diff --git a/builtin-tag.c b/builtin-tag.c
> new file mode 100644
> index 0000000..1824379
> --- /dev/null
> +++ b/builtin-tag.c
> @@ -0,0 +1,430 @@
> +/*
> + * Builtin "git tag"
> + *
> + * Copyright (c) 2007 Kristian H??gsberg <krh@redhat.com>,

I do not know how the above would come out, but it surely was not Høgsberg in the copy I received...

We probably should make sure our sources are in UTF-8; I suspect builtin-branch.c is not.

Show 34 quoted lines
> + *                    Carlos Rica <jasampler@gmail.com>
> + * Based on git-tag.sh and mktag.c by Linus Torvalds.
> + */
> +
> +#include "cache.h"
> +#include "builtin.h"
> +#include "refs.h"
> +#include "tag.h"
> +#include "run-command.h"
> +
> +static const char builtin_tag_usage[] =
> +  "git-tag [-n [<num>]] -l [<pattern>] | [-a | -s | -u <key-id>] [-f | -d | -v] [-m <msg>] <tagname> [<head>]";
> +
> +static char signingkey[1000];
> +static int lines;
> +
> +static void launch_editor(const char *path, char **buffer, unsigned long *len)
> +{
> +	const char *editor, *terminal;
> +	struct child_process child;
> +	const char *args[3];
> +	int fd;
> +
> +	editor = getenv("VISUAL");
> +	if (!editor)
> +		editor = getenv("EDITOR");
> +
> +	terminal = getenv("TERM");
> +	if (!editor && (!terminal || !strcmp(terminal, "dumb"))) {
> +		fprintf(stderr,
> +		"Terminal is dumb but no VISUAL nor EDITOR defined.\n"
> +		"Please supply the message using either -m or -F option.\n");
> +		exit(1);
> +	}

Ah, this is taken from git-commit.sh ;-) Does your "git tag" support the -F option (builtin_tag_usage[] does not seem to mention it)? I wonder what happens when this function migrates to editor.c and a new program, other than git-tag and git-commit, that is without -F nor -m options wants to call this.

Show 11 quoted lines
> +	if (!editor)
> +		editor = "vi";
> +
> +	memset(&child, 0, sizeof(child));
> +	child.argv = args;
> +	args[0] = editor;
> +	args[1] = path;
> +	args[2] = NULL;
> +
> +	if (run_command(&child))
> +		die("could not launch editor %s.", editor);

This message is not quite true, isn't it? The editor might have been launched but exited with non-zero status.

> +	fd = open(path, O_RDONLY);
> +	if (fd == -1)
> +		die("could not read %s.", path);

And this is "could not open", with probably strerror(errno) to be helpful.

Show 13 quoted lines
> +	if (read_pipe(fd, buffer, len))
> +		die("could not read message file '%s': %s",
> +		    path, strerror(errno));
> +	close(fd);
> +}
> +
> +#define PGP_SIGNATURE "-----BEGIN PGP SIGNATURE-----"
> +
> +static int show_reference(const char *refname, const unsigned char *sha1,
> +			  int flag, void *cb_data)
> +{
> ...
> +}
Ok.
> +static int list_tags(const char *pattern)
> +{
> ...
> +}
Ok.
Show 10 quoted lines
> +static int delete_tags(const char **argv)
> +{
> +	const char **p;
> +	char ref[PATH_MAX];
> +	int had_error = 0;
> +	unsigned char sha1[20];
> +
> +	for (p = argv; *p; p++) {
> +		if (snprintf(ref, sizeof ref, "refs/tags/%s", *p) > sizeof ref)
> +			die("tag name '%s' too long.", *p);

"sizeof foo" may be proper ANSI C, but somehow many people seem to find "sizeof(foo)" much easier to read, including me.

I think the overflow check is wrong. snprintf() does not write more than sizeof(ref) including the training NUL, but when its output is truncated, it returns the number of bytes that would have been written to the buffer, excluding the NUL termination. What this means is that if it returns sizeof(ref), it also is a case of overflow. Further, if C library is not C99, e.g. older glibc (say 2.0.6), it could return negative status indicating an error condition upon truncated output.

I suspect you inherited these problems from builtin-branch.c, but we'd better fix them there as well.

Show 5 quoted lines
> +		if (!resolve_ref(ref, sha1, 1, NULL)) {
> +			fprintf(stderr, "tag '%s' not found.\n", *p);
> +			had_error = 1;
> +			continue;
> +		}

We might want to call error("tag '%s' not found.") instead for later libification...

Show 16 quoted lines
> ...
> +}
> +
> +static int run_verify_tag_command(unsigned char *sha1)
> +{
> +	int ret;
> +	const char *argv_verify_tag[] = {"git-verify-tag",
> +					"-v", "SHA1_HEX", NULL};
> +	argv_verify_tag[2] = sha1_to_hex(sha1);
> +
> +	ret = run_command_v_opt(argv_verify_tag, 0);
> +
> +	if (ret <= -10000)
> +		die("unable to run %s\n", argv_verify_tag[0]);
> +	return -ret;
> +}

I wonder why you need to differentiate between ERR_RUN_COMMAND_* and non-zero exit status... Also do you need to "return -ret", instead of not negating? In C programs error returns are often negative and finish_command() follows that convention.

> +static int verify_tags(const char **argv)
> +{
> ...
> +}
Ok.
Show 13 quoted lines
> +static int do_sign(char *buffer, size_t size, size_t max)
> +{
> +	struct child_process gpg;
> +	const char *args[4];
> +	char *bracket;
> +	int len;
> +
> +	if (signingkey[0] == '\0') {
> +		strlcpy(signingkey, git_committer_info(1), sizeof signingkey);
> +		bracket = strchr(signingkey, '>');
> +		if (bracket)
> +			bracket[1] = '\0';
> +	}

Hmph. Silently truncate with strlcpy instead of erroring out on insanely long committer info?

Show 16 quoted lines
> +	memset(&gpg, 0, sizeof(gpg));
> +	gpg.argv = args;
> +	gpg.in = -1;
> +	gpg.out = -1;
> +	args[0] = "gpg";
> +	args[1] = "-bsau";
> +	args[2] = signingkey;
> +	args[3] = NULL;
> +
> +	if (start_command(&gpg))
> +		die("could not run gpg.");
> +
> +	write_or_die(gpg.in, buffer, size);
> +	close(gpg.in);
> +	gpg.close_in = 0;
> +	len = read_in_full(gpg.out, buffer + size, max - size);

Bi-di pipes makes me nervous, but this case should be Ok. It could try writing "---BEGIN PGP SIGNATURE---" before reading from you and get stuck because you are not reading from it but that is only in theory.

I wonder why our read_in_full and write_in_full do not return ssize_t although that's how they count things internally. At least you could make do_sign() to return ssize_t, and change the die() to "return error()" to have the caller deal with it, which would be easier for people other than "git tag -s" to call this if they later want to.

What happens when gpg gave more than you expect? We get truncated signature and I do not see any code to notice the breakage...

Show 16 quoted lines
> +
> +	finish_command(&gpg);
> +
> +	return size + len;
> +}
> +
> +static const char tag_template[] =
> +	"\n"
> +	"#\n"
> +	"# Write a tag message\n"
> +	"#\n";
> +
> +static int git_tag_config(const char *var, const char *value)
> +{
> ...
> +}
Ok, except for the same strlcpy comment above.
Show 26 quoted lines
> +#define MAX_SIGNATURE_LENGTH 1024
> +/* message must be NULL or allocated, it will be reallocated and freed */
> +static void create_tag(const unsigned char *object, const char *tag,
> +		       char *message, int sign, unsigned char *result)
> +{
> +	enum object_type type;
> +	char header_buf[1024], *buffer;
> +	int header_len, max_size;
> +	unsigned long size;
> +
> +	type = sha1_object_info(object, NULL);
> +	if (type <= 0)
> +	    die("bad object type.");
> +
> +	header_len = snprintf(header_buf, sizeof header_buf,
> +			  "object %s\n"
> +			  "type %s\n"
> +			  "tag %s\n"
> +			  "tagger %s\n\n",
> +			  sha1_to_hex(object),
> +			  typename(type),
> +			  tag,
> +			  git_committer_info(1));
> +
> +	if (header_len >= sizeof header_buf)
> +		die("tag header too big.");

This comparison '>=' is correct (i.e. not '>'), as opposed to the one I commented way above.

Show 12 quoted lines
> +	if (!message) {
> ...
> +	}
> +	else {
> +		buffer = message;
> +		size = strlen(message);
> +	}
> +
> +	size = stripspace(buffer, size, 1);
> +
> +	if (!message && !size)
> +		die("no tag message?");
Why check 'message' here?
> ...
> +	free(buffer);
> +}
The rest is Ok.
Show 13 quoted lines
> +int cmd_tag(int argc, const char **argv, const char *prefix)
> +{
> ...
> +		if (!strcmp(arg, "-F")) {
> +			unsigned long len;
> +			annotate = 1;
> +			i++;
> +			if  (i == argc)
> +				die("option -F needs an argument.");
> +
> +			fd = open(argv[i], O_RDONLY);
> +			if (fd < 0)
> +				die("cannot open %s", argv[1]);

The shell script version relies on the magic "cat -" to read from standard input upon "git tag -F -". It is understandable that both of you and Dscho missed it, though.

Show 8 quoted lines
> +			len = 1024;
> +			message = xmalloc(len);
> +			if (read_pipe(fd, &message, &len))
> +				die("cannot read %s", argv[1]);
> +			message = xrealloc(message, len + 1);
> +			message[len] = '\0';
> +			continue;
> +		}

We might be better off if read_pipe() is renamed to read_fd() and made internally always NUL-terminate the buffer (but not count that NUL as part of length). I dunno.

Show 8 quoted lines
> +		if (!strcmp(arg, "-u")) {
> +			annotate = 1;
> +			sign = 1;
> +			i++;
> +			if  (i == argc)
> +				die("option -u needs an argument.");
> +			strlcpy(signingkey, argv[i], sizeof signingkey);
> +			continue;
Extra space "if  (expr".  Return from strlcpy() not checked?
> +	if (snprintf(ref, sizeof ref, "refs/tags/%s", tag) > sizeof ref)
> +		die("tag '%s' too long.", tag);

This use of snprintf() and checking the overflow as error is much nicer, don't you think, instead of silently ignoring truncation?

> ...
> +}
The remainder of cmd_tag() looks Ok.
Carlos Rica· Jul 12, 2007, 21:01 UTC · re: Junio C Hamano · lore

Re: [PATCH 2/2] Make git-tag a builtin.

Thank you for your review, Junio, I'm studing all of your suggestions.
2007/7/12, Junio C Hamano <gitster@pobox.com>:
Show 6 quoted lines
> Carlos Rica <jasampler@gmail.com> writes:
>
> > Mime-Version: 1.0
> > Content-Type: text/plain; charset=windows-1252
>
> Hmmmmmm.....
Show 13 quoted lines
> > diff --git a/builtin-tag.c b/builtin-tag.c
> > new file mode 100644
> > index 0000000..1824379
> > --- /dev/null
> > +++ b/builtin-tag.c
> > @@ -0,0 +1,430 @@
> > +/*
> > + * Builtin "git tag"
> > + *
> > + * Copyright (c) 2007 Kristian H??gsberg <krh@redhat.com>,
>
> I do not know how the above would come out, but it surely was
> not Høgsberg in the copy I received...

I think it is my fault. My original builtin-tag.c file has the name encoded in UTF-8, and the patch too, except because I also added his name in the commit's message in Latin-1 encoding, and then, programs cannot recode or recognize both. Sorry.

Show 42 quoted lines
>
> > + *                    Carlos Rica <jasampler@gmail.com>
> > + * Based on git-tag.sh and mktag.c by Linus Torvalds.
> > + */
> > +
> > +#include "cache.h"
> > +#include "builtin.h"
> > +#include "refs.h"
> > +#include "tag.h"
> > +#include "run-command.h"
> > +
> > +static const char builtin_tag_usage[] =
> > +  "git-tag [-n [<num>]] -l [<pattern>] | [-a | -s | -u <key-id>] [-f | -d | -v] [-m <msg>] <tagname> [<head>]";
> > +
> > +static char signingkey[1000];
> > +static int lines;
> > +
> > +static void launch_editor(const char *path, char **buffer, unsigned long *len)
> > +{
> > +     const char *editor, *terminal;
> > +     struct child_process child;
> > +     const char *args[3];
> > +     int fd;
> > +
> > +     editor = getenv("VISUAL");
> > +     if (!editor)
> > +             editor = getenv("EDITOR");
> > +
> > +     terminal = getenv("TERM");
> > +     if (!editor && (!terminal || !strcmp(terminal, "dumb"))) {
> > +             fprintf(stderr,
> > +             "Terminal is dumb but no VISUAL nor EDITOR defined.\n"
> > +             "Please supply the message using either -m or -F option.\n");
> > +             exit(1);
> > +     }
>
> Ah, this is taken from git-commit.sh ;-) Does your "git tag"
> support the -F option (builtin_tag_usage[] does not seem to
> mention it)?  I wonder what happens when this function migrates
> to editor.c and a new program, other than git-tag and
> git-commit, that is without -F nor -m options wants to call
> this.

I will include that -F option in the builtin_tag_usage and I will also find if documentation already has it.

As I said above, launch_editor is copied and modified from the builtin-commit.c that Kristian is currently writing. Indeed, I removed every mention on it related specifically to commits, so it will be difficult to share the code between both versions in the current state. Perhaps a raw function not printing out errors (and returning error codes instead) could be the only option to reuse it. I need to ask Kristian for this.

Show 18 quoted lines
> > +static int run_verify_tag_command(unsigned char *sha1)
> > +{
> > +     int ret;
> > +     const char *argv_verify_tag[] = {"git-verify-tag",
> > +                                     "-v", "SHA1_HEX", NULL};
> > +     argv_verify_tag[2] = sha1_to_hex(sha1);
> > +
> > +     ret = run_command_v_opt(argv_verify_tag, 0);
> > +
> > +     if (ret <= -10000)
> > +             die("unable to run %s\n", argv_verify_tag[0]);
> > +     return -ret;
> > +}
>
> I wonder why you need to differentiate between ERR_RUN_COMMAND_*
> and non-zero exit status...  Also do you need to "return -ret",
> instead of not negating?  In C programs error returns are often
> negative and finish_command() follows that convention.
The point here was to mimic the behaviour of git-tag.sh,
returning the exit status returned by the called script: "git-verify-tag.sh".
However, I didn't realize that git-verify tag.sh was exiting only with 1 or 0,
so "return ret;" will also work since the call to the function is:
   if (run_verify_tag_command(sha1))
      had_error = 1;

When die is called (because of an ERR_RUN_COMMAND_* error condition), it will return 128 to the system, and won't do more processing to other tags. Do yo mean that it would be better to remove this "die" call here?

Show 15 quoted lines
>
> > +     if (!message) {
> > ...
> > +     }
> > +     else {
> > +             buffer = message;
> > +             size = strlen(message);
> > +     }
> > +
> > +     size = stripspace(buffer, size, 1);
> > +
> > +     if (!message && !size)
> > +             die("no tag message?");
>
> Why check 'message' here?

Because the behaviour of git-tag.sh here is not allow empty messages when the editor is executed, but allow them when -m or -F options are given (message then will be not NULL).

Show 17 quoted lines
> > +int cmd_tag(int argc, const char **argv, const char *prefix)
> > +{
> > ...
> > +             if (!strcmp(arg, "-F")) {
> > +                     unsigned long len;
> > +                     annotate = 1;
> > +                     i++;
> > +                     if  (i == argc)
> > +                             die("option -F needs an argument.");
> > +
> > +                     fd = open(argv[i], O_RDONLY);
> > +                     if (fd < 0)
> > +                             die("cannot open %s", argv[1]);
>
> The shell script version relies on the magic "cat -" to read
> from standard input upon "git tag -F -".  It is understandable
> that both of you and Dscho missed it, though.
It should be easy to implement, I will do it.
Show 12 quoted lines
> > +                     len = 1024;
> > +                     message = xmalloc(len);
> > +                     if (read_pipe(fd, &message, &len))
> > +                             die("cannot read %s", argv[1]);
> > +                     message = xrealloc(message, len + 1);
> > +                     message[len] = '\0';
> > +                     continue;
> > +             }
>
> We might be better off if read_pipe() is renamed to read_fd()
> and made internally always NUL-terminate the buffer (but not
> count that NUL as part of length).  I dunno.

I saw an implementation for read_pipe named read_fd from Kristian: http://article.gmane.org/gmane.comp.version-control.git/51366 but it is only focused on allowing NULL and 0 in pointer and size parameters. It should be harmless NUL-terminating the buffer since the routine is already allocating more memory than needed for efficiency purposes.

← back to recent threads