Subject: [PATCH] Port git-tag.sh to C.

From: Kristian Høgsberg <[email-masked]>
Date: Fri, 8 Jun 2007 17:38:50 -0400

A more or less straight-forward port of git-tag.sh to C.

Signed-off-by: Kristian Høgsberg <[email-masked]>
---

There's two behavioral changes here: git tag with no arguments behaves like
git branch with no arguments; it lists all tags.  The other change is
an empty line in the tag message template to make it look more like the
commit message template.

cheers,
Kristian

 Makefile      |    3 +-
 builtin-tag.c |  364 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 builtin.h     |    1 +
 git-tag.sh    |  183 -----------------------------
 git.c         |    1 +
 5 files changed, 368 insertions(+), 184 deletions(-)
 create mode 100644 builtin-tag.c
 delete mode 100755 git-tag.sh

diff --git a/Makefile b/Makefile
index 0f75955..bb1bed1 100644
--- a/Makefile
+++ b/Makefile
@@ -205,7 +205,7 @@ SCRIPT_SH = \
 	git-pull.sh git-rebase.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 \
@@ -372,6 +372,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..06eafc0
--- /dev/null
+++ b/builtin-tag.c
@@ -0,0 +1,364 @@
+/*
+ * Builtin "git tag"
+ *
+ * Copyright (c) 2007 Kristian Høgsberg <[email-masked]>
+ * Based on git-tag.sh and mktag.c by Linus Torvalds.
+ */
+
+#include "cache.h"
+#include "refs.h"
+#include "commit.h"
+#include "builtin.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 launch_editor(const char *path, const char *template,
+			  char *buffer, size_t size)
+{
+	struct child_process child;
+	const char *editor;
+	const char *args[3];
+	char *eol;
+	int len, fd, blank_lines, i, j;
+
+	fd = open(path, O_CREAT | O_TRUNC | O_WRONLY, 0644);
+	if (fd < 0)
+		die("could not create file %s.", path);
+
+	len = strlen(template);
+	write_or_die(fd, template, len);
+	close(fd);
+
+	editor = getenv("VISUAL");
+	if (!editor)
+		editor = getenv("EDITOR");
+	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, 0644);
+	if (fd == -1)
+		die("could not read %s.", path);
+	len = read_in_full(fd, buffer, size);
+	if (len < 0)
+		die("failed to read '%s', %m", path);
+	close(fd);
+
+	blank_lines = 1;
+	for (i = 0, j = 0; i < len; i++) {
+		if (blank_lines > 0 && buffer[i] == '#') {
+			eol = strchr(buffer + i, '\n');
+			if (!eol)
+				break;
+
+			i = eol - buffer;
+			continue;
+		}
+
+		if (buffer[i] == '\n') {
+			blank_lines++;
+			if (blank_lines > 1)
+				continue;
+		} else {
+			if (blank_lines > 2)
+				buffer[j++] = '\n';
+			blank_lines = 0;
+		}
+
+		buffer[j++] = buffer[i];
+	}
+
+	if (buffer[j - 1] != '\n')
+		buffer[j++] = '\n';
+
+	unlink(path);
+
+	return j;
+}
+
+static int show_reference(const char *refname, const unsigned char *sha1,
+			  int flag, void *cb_data)
+{
+	const char *pattern = cb_data;
+
+	if (pattern == NULL || !fnmatch(pattern, refname, 0))
+		printf("%s\n", refname);
+
+	return 0;
+}
+
+static int list_tags(const char *pattern)
+{
+	for_each_tag_ref(show_reference, (void *) pattern);
+
+	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))
+			printf("Deleted tag '%s'\n", *p);
+	}
+			
+	return had_error;
+}
+
+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;
+		}
+
+		printf("FIXME: verify tag '%s'\n", *p);
+	}
+
+	return had_error;
+}
+
+static int do_sign(char *buffer, size_t size, size_t max)
+{
+	struct child_process gpg;
+	const char *args[5];
+	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] = "-bsa";
+	args[2] = "-u";
+	args[3] = signingkey;
+	args[4] = 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";
+
+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);
+}
+
+int cmd_tag(int argc, const char **argv, const char *prefix)
+{
+	char buffer[4096];
+	unsigned char object[20], prev[20], result[20];
+	int annotate = 0, is_signed = 0, force = 0, lines = 0;
+	const char *message = NULL;
+	char ref[PATH_MAX];
+	const char *object_ref, *tag;
+	int i, body, header, total, fd;
+	enum object_type type;
+	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;
+			is_signed = 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
+				/* FIXME, fallback to 1 on invalid integer */
+				lines = atoi(argv[i + 1]);
+			continue;
+		}
+		if (!strcmp(arg, "-m")) {
+			annotate = 1;
+			i++;
+			if  (i == argc)
+				die("option -m needs an argument.");
+			message = argv[i];
+			continue;
+		}
+		if (!strcmp(arg, "-F")) {
+			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]);
+
+			message = xmalloc(4096);
+			if (read_in_full(fd, (char *) message, 4096) < 0)
+				die("cannot read %s", argv[1]);
+			continue;
+		}
+		if (!strcmp(arg, "-u")) {
+			annotate = 1;
+			is_signed = 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++];
+
+	if (i < argc)
+		object_ref = argv[i];
+	else
+		object_ref = "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)) {
+		if (!force)
+			die("tag '%s' already exists", tag);
+	} else {
+		hashclr(prev);
+	}
+
+	type = sha1_object_info(object, NULL);
+	if (type <= 0)
+	    die("bad object type.");
+
+	header = snprintf(buffer, sizeof buffer,
+			  "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 (annotate && message == NULL)
+		body = launch_editor(git_path("TAGMSG"), tag_template,
+				     buffer + header, sizeof buffer - header);
+	else if (annotate)
+		body = snprintf(buffer + header, sizeof buffer - header,
+				"%s\n", message);
+	else
+		body = 0;
+
+	if (annotate && body == 0)
+		die("no tag message?");
+
+	if (header + body > sizeof buffer)
+		die("tag message too big.");
+
+	if (is_signed)
+		total = do_sign(buffer, header + body, sizeof buffer);
+	else
+		total = header + body;
+
+	if (write_sha1_file(buffer, total, tag_type, result) < 0)
+		die("unable to write tag file");
+
+	lock = lock_any_ref_for_update(ref, prev, 0);
+	if (!lock)
+		die("%s: cannot lock the ref", ref);
+	if (write_ref_sha1(lock, result, NULL) < 0)
+		die("%s: cannot update the ref", ref);
+
+	return 0;
+}
diff --git a/builtin.h b/builtin.h
index 39290d1..91166e1 100644
--- a/builtin.h
+++ b/builtin.h
@@ -72,6 +72,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 37cee97..0000000
--- a/git-tag.sh
+++ /dev/null
@@ -1,183 +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
-	;;
-    -s)
-	annotate=1
-	signed=1
-	;;
-    -f)
-	force=1
-	;;
-    -n)
-        case $2 in
-	-*)	LINES=1 	# no argument
-		;;
-	*)	shift
-		LINES=$(expr "$1" : '\([0-9]*\)')
-		[ -z "$LINES" ] && LINES=1 # 1 line is default when -n is used
-		;;
-	esac
-	;;
-    -l)
-	list=1
-	shift
-	PATTERN="$1"	# select tags by shell pattern, not re
-	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' \
-					   -e '/^-----BEGIN PGP SIGNATURE-----$/Q' )
-			printf "%-15s %s\n" "$TAG" "$ANNOTATION" |
-			  sed -e '2,$s/^/    /' \
-			      -e "${LINES}q"
-			;;
-		*)      echo "$TAG"
-			;;
-		esac
-	    done
-	;;
-    -m)
-    	annotate=1
-	shift
-	message="$1"
-	if test "$#" = "0"; then
-	    die "error: option -m needs an argument"
-	else
-	    message_given=1
-	fi
-	;;
-    -F)
-	annotate=1
-	shift
-	if test "$#" = "0"; then
-	    die "error: option -F needs an argument"
-	else
-	    message="$(cat "$1")"
-	    message_given=1
-	fi
-	;;
-    -u)
-	annotate=1
-	signed=1
-	shift
-	username="$1"
-	;;
-    -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
-    shift
-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 29b55a1..c9c20fb 100644
--- a/git.c
+++ b/git.c
@@ -285,6 +285,7 @@ static void handle_internal_command(int argc, const char **argv, char **envp)
 		{ "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.2.1.134.g447c2