# [PATCH 2nd try] Make git-fmt-merge-msg a builtin

5 messages from 2006-07-03 to 2006-07-04. Participants: Johannes Schindelin, Junio C Hamano.
Thread: https://gitlist.dev/t/4750

## Johannes Schindelin, 2006-07-03 15:18

Subject: [PATCH 2nd try] Make git-fmt-merge-msg a builtin
Message-ID: <Pine.LNX.4.63.0607031717540.29667@wbgn013.biozentrum.uni-wuerzburg.de>
URL: https://gitlist.dev/e/Pine.LNX.4.63.0607031717540.29667%40wbgn013.biozentrum.uni-wuerzburg.de

```

Signed-off-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>

---
	This retires git-fmt-merge-msg.perl, since it passes all the
	tests, but removes the Perl version not now. Did I mention it
	is very, very, very fast? (Even compared to the Git.pm version;
	measured on a cygwin setup.)

 Makefile                |    7 +
 builtin-fmt-merge-msg.c |  355 +++++++++++++++++++++++++++++++++++++++++++++++
 builtin.h               |    1 
 git.c                   |    3 
 4 files changed, 362 insertions(+), 4 deletions(-)

diff --git a/Makefile b/Makefile
index c142bdd..a551c33 100644
--- a/Makefile
+++ b/Makefile
@@ -147,7 +147,7 @@ SCRIPT_SH = \
 
 SCRIPT_PERL = \
 	git-archimport.perl git-cvsimport.perl git-relink.perl \
-	git-shortlog.perl git-fmt-merge-msg.perl git-rerere.perl \
+	git-shortlog.perl git-rerere.perl \
 	git-annotate.perl git-cvsserver.perl \
 	git-svnimport.perl git-mv.perl git-cvsexportcommit.perl \
 	git-send-email.perl
@@ -189,7 +189,8 @@ BUILT_INS = git-log$X git-whatchanged$X 
 	git-ls-files$X git-ls-tree$X git-get-tar-commit-id$X \
 	git-read-tree$X git-commit-tree$X git-write-tree$X \
 	git-apply$X git-show-branch$X git-diff-files$X git-update-index$X \
-	git-diff-index$X git-diff-stages$X git-diff-tree$X git-cat-file$X
+	git-diff-index$X git-diff-stages$X git-diff-tree$X git-cat-file$X \
+	git-fmt-merge-msg$X
 
 # what 'all' will build and 'install' will install, in gitexecdir
 ALL_PROGRAMS = $(PROGRAMS) $(SIMPLE_PROGRAMS) $(SCRIPTS)
@@ -245,7 +246,7 @@ BUILTIN_OBJS = \
 	builtin-apply.o builtin-show-branch.o builtin-diff-files.o \
 	builtin-diff-index.o builtin-diff-stages.o builtin-diff-tree.o \
 	builtin-cat-file.o builtin-mailsplit.o builtin-stripspace.o \
-	builtin-update-ref.o
+	builtin-update-ref.o builtin-fmt-merge-msg.o
 
 GITLIBS = $(LIB_FILE) $(XDIFF_LIB)
 EXTLIBS = -lz
diff --git a/builtin-fmt-merge-msg.c b/builtin-fmt-merge-msg.c
new file mode 100644
index 0000000..6527482
--- /dev/null
+++ b/builtin-fmt-merge-msg.c
@@ -0,0 +1,355 @@
+#include "cache.h"
+#include "commit.h"
+#include "diff.h"
+#include "revision.h"
+#include "tag.h"
+
+static const char *fmt_merge_msg_usage =
+	"git-fmt-merge-msg [--summary] [--no-summary] [--file <file>]";
+
+static int merge_summary = 0;
+
+static int fmt_merge_msg_config(const char *key, const char *value)
+{
+	if (!strcmp("merge.summary", key))
+		merge_summary = git_config_bool(key, value);
+	return 0;
+}
+
+struct list {
+	char **list;
+	void **payload;
+	unsigned nr, alloc;
+};
+
+static void append_to_list(struct list *list, char *value, void *payload)
+{
+	if (list->nr == list->alloc) {
+		list->alloc += 32;
+		list->list = realloc(list->list, sizeof(char *) * list->alloc);
+		list->payload = realloc(list->payload,
+				sizeof(char *) * list->alloc);
+	}
+	list->payload[list->nr] = payload;
+	list->list[list->nr++] = value;
+}
+
+static int find_in_list(struct list *list, char *value)
+{
+	int i;
+
+	for (i = 0; i < list->nr; i++)
+		if (!strcmp(list->list[i], value))
+			return i;
+
+	return -1;
+}
+
+static void free_list(struct list *list)
+{
+	int i;
+
+	if (list->alloc == 0)
+		return;
+
+	for (i = 0; i < list->nr; i++) {
+		free(list->list[i]);
+		if (list->payload[i])
+			free(list->payload[i]);
+	}
+	free(list->list);
+	free(list->payload);
+	list->nr = list->alloc = 0;
+}
+
+struct src_data {
+	struct list branch, tag, r_branch, generic;
+	int head_status;
+};
+
+static struct list srcs = { NULL, NULL, 0, 0};
+static struct list origins = { NULL, NULL, 0, 0};
+
+static int handle_line(char *line)
+{
+	int i, len = strlen(line);
+	unsigned char *sha1;
+	char *src, *origin;
+	struct src_data *src_data;
+
+	if (len < 43 || line[40] != '\t')
+		return 1;
+
+	if (!strncmp(line + 41, "not-for-merge", 13))
+		return 0;
+
+	if (line[41] != '\t')
+		return 2;
+
+	line[40] = 0;
+	sha1 = xmalloc(20);
+	i = get_sha1(line, sha1);
+	line[40] = '\t';
+	if (i)
+		return 3;
+
+	if (line[len - 1] == '\n')
+		line[len - 1] = 0;
+	line += 42;
+
+	src = strstr(line, " of ");
+	if (src) {
+		*src = 0;
+		src += 4;
+	} else
+		src = "HEAD";
+
+	i = find_in_list(&srcs, src);
+	if (i < 0) {
+		i = srcs.nr;
+		append_to_list(&srcs, strdup(src),
+				xcalloc(1, sizeof(struct src_data)));
+	}
+	src_data = srcs.payload[i];
+
+	if (!strncmp(line, "branch ", 7)) {
+		origin = strdup(line + 7);
+		append_to_list(&src_data->branch, origin, NULL);
+		src_data->head_status |= 2;
+	} else if (!strncmp(line, "tag ", 4)) {
+		origin = line;
+		append_to_list(&src_data->tag, strdup(origin + 4), NULL);
+		src_data->head_status |= 2;
+	} else if (!strncmp(line, "remote branch ", 14)) {
+		origin = strdup(line + 14);
+		append_to_list(&src_data->r_branch, origin, NULL);
+		src_data->head_status |= 2;
+	} else if (!strcmp(line, "HEAD")) {
+		origin = strdup(src);
+		src_data->head_status |= 1;
+	} else {
+		origin = strdup(src);
+		append_to_list(&src_data->generic, strdup(line), NULL);
+		src_data->head_status |= 2;
+	}
+
+	if (!strcmp(".", src) || !strcmp(src, origin)) {
+		int len = strlen(origin);
+		if (origin[0] == '\'' && origin[len - 1] == '\'') {
+			char *new_origin = malloc(len - 1);
+			memcpy(new_origin, origin + 1, len - 2);
+			new_origin[len - 1] = 0;
+			origin = new_origin;
+		} else
+			origin = strdup(origin);
+	} else {
+		char *new_origin = malloc(strlen(origin) + strlen(src) + 5);
+		sprintf(new_origin, "%s of %s", origin, src);
+		origin = new_origin;
+	}
+	append_to_list(&origins, origin, sha1);
+	return 0;
+}
+
+static void print_joined(const char *singular, const char *plural,
+		struct list *list)
+{
+	if (list->nr == 0)
+		return;
+	if (list->nr == 1) {
+		printf("%s%s", singular, list->list[0]);
+	} else {
+		int i;
+		printf("%s", plural);
+		for (i = 0; i < list->nr - 1; i++)
+			printf("%s%s", i > 0 ? ", " : "", list->list[i]);
+		printf(" and %s", list->list[list->nr - 1]);
+	}
+}
+
+static void shortlog(const char *name, unsigned char *sha1,
+		struct commit *head, struct rev_info *rev, int limit)
+{
+	int i, count = 0;
+	struct commit *commit;
+	struct object *branch;
+	struct list subjects = { NULL, NULL, 0, 0 };
+	int flags = UNINTERESTING | TREECHANGE | SEEN | SHOWN | ADDED;
+
+	branch = deref_tag(parse_object(sha1), sha1_to_hex(sha1), 40);
+	if (!branch || branch->type != TYPE_COMMIT)
+		return;
+
+	setup_revisions(0, NULL, rev, NULL);
+	rev->ignore_merges = 1;
+	add_pending_object(rev, branch, name);
+	add_pending_object(rev, &head->object, "^HEAD");
+	head->object.flags |= UNINTERESTING;
+	prepare_revision_walk(rev);
+	while ((commit = get_revision(rev)) != NULL) {
+		char *oneline, *bol, *eol;
+
+		/* ignore merges */
+		if (commit->parents && commit->parents->next)
+			continue;
+
+		count++;
+		if (subjects.nr > limit)
+			continue;
+
+		bol = strstr(commit->buffer, "\n\n");
+		if (!bol) {
+			append_to_list(&subjects, strdup(sha1_to_hex(
+							commit->object.sha1)),
+					NULL);
+			continue;
+		}
+
+		bol += 2;
+		eol = strchr(bol, '\n');
+
+		if (eol) {
+			int len = eol - bol;
+			oneline = malloc(len + 1);
+			memcpy(oneline, bol, len);
+			oneline[len] = 0;
+		} else
+			oneline = strdup(bol);
+		append_to_list(&subjects, oneline, NULL);
+	}
+
+	if (count > limit)
+		printf("\n* %s: (%d commits)\n", name, count);
+	else
+		printf("\n* %s:\n", name);
+
+	for (i = 0; i < subjects.nr; i++)
+		if (i >= limit)
+			printf("  ...\n");
+		else
+			printf("  %s\n", subjects.list[i]);
+
+	clear_commit_marks((struct commit *)branch, flags);
+	clear_commit_marks(head, flags);
+	free_commit_list(rev->commits);
+	rev->commits = NULL;
+	rev->pending.nr = 0;
+
+	free_list(&subjects);
+}
+
+int cmd_fmt_merge_msg(int argc, char **argv, char **envp)
+{
+	int limit = 20, i = 0;
+	char line[1024];
+	FILE *in = stdin;
+	const char *sep = "";
+	unsigned char head_sha1[20];
+	const char *head, *current_branch;
+
+	git_config(fmt_merge_msg_config);
+
+	while (argc > 1) {
+		if (!strcmp(argv[1], "--summary"))
+			merge_summary = 1;
+		else if (!strcmp(argv[1], "--no-summary"))
+			merge_summary = 0;
+		else if (!strcmp(argv[1], "-F") || !strcmp(argv[1], "--file")) {
+			if (argc < 2)
+				die ("Which file?");
+			if (!strcmp(argv[2], "-"))
+				in = stdin;
+			else {
+				fclose(in);
+				in = fopen(argv[2], "r");
+			}
+			argc--; argv++;
+		} else
+			break;
+		argc--; argv++;
+	}
+
+	if (argc > 1)
+		usage(fmt_merge_msg_usage);
+
+	/* get current branch */
+	head = strdup(git_path("HEAD"));
+	current_branch = resolve_ref(head, head_sha1, 1);
+	current_branch += strlen(head) - 4;
+	free((char *)head);
+	if (!strncmp(current_branch, "refs/heads/", 11))
+		current_branch += 11;
+
+	while (fgets(line, sizeof(line), in)) {
+		i++;
+		if (line[0] == 0)
+			continue;
+		if (handle_line(line))
+			die ("Error in line %d: %s", i, line);
+	}
+
+	printf("Merge ");
+	for (i = 0; i < srcs.nr; i++) {
+		struct src_data *src_data = srcs.payload[i];
+		const char *subsep = "";
+
+		printf(sep);
+		sep = "; ";
+
+		if (src_data->head_status == 1) {
+			printf(srcs.list[i]);
+			continue;
+		}
+		if (src_data->head_status == 3) {
+			subsep = ", ";
+			printf("HEAD");
+		}
+		if (src_data->branch.nr) {
+			printf(subsep);
+			subsep = ", ";
+			print_joined("branch ", "branches ", &src_data->branch);
+		}
+		if (src_data->r_branch.nr) {
+			printf(subsep);
+			subsep = ", ";
+			print_joined("remote branch ", "remote branches ",
+					&src_data->r_branch);
+		}
+		if (src_data->tag.nr) {
+			printf(subsep);
+			subsep = ", ";
+			print_joined("tag ", "tags ", &src_data->tag);
+		}
+		if (src_data->generic.nr) {
+			printf(subsep);
+			print_joined("commit ", "commits ", &src_data->generic);
+		}
+		if (strcmp(".", srcs.list[i]))
+			printf(" of %s", srcs.list[i]);
+	}
+
+	if (!strcmp("master", current_branch))
+		putchar('\n');
+	else
+		printf(" into %s\n", current_branch);
+
+	if (merge_summary) {
+		struct commit *head;
+		struct rev_info rev;
+
+		head = lookup_commit(head_sha1);
+		init_revisions(&rev);
+		rev.commit_format = CMIT_FMT_ONELINE;
+		rev.ignore_merges = 1;
+		rev.limited = 1;
+
+		for (i = 0; i < origins.nr; i++)
+			shortlog(origins.list[i], origins.payload[i],
+					head, &rev, limit);
+	}
+
+	/* No cleanup yet; is standalone anyway */
+
+	return 0;
+}
+
diff --git a/builtin.h b/builtin.h
old mode 100644
new mode 100755
index f12d5e6..d9e5483
--- a/builtin.h
+++ b/builtin.h
@@ -49,6 +49,7 @@ extern int cmd_cat_file(int argc, const 
 extern int cmd_rev_parse(int argc, const char **argv, char **envp);
 extern int cmd_update_index(int argc, const char **argv, char **envp);
 extern int cmd_update_ref(int argc, const char **argv, char **envp);
+extern int cmd_fmt_merge_msg(int argc, const char **argv, char **envp);
 
 extern int cmd_write_tree(int argc, const char **argv, char **envp);
 extern int write_tree(unsigned char *sha1, int missing_ok, const char *prefix);
diff --git a/git.c b/git.c
index 512fa63..7cc826b 100644
--- a/git.c
+++ b/git.c
@@ -200,7 +200,8 @@ static void handle_internal_command(int 
 		{ "mailinfo", cmd_mailinfo },
 		{ "stripspace", cmd_stripspace },
 		{ "update-index", cmd_update_index },
-		{ "update-ref", cmd_update_ref }
+		{ "update-ref", cmd_update_ref },
+		{ "fmt-merge-msg", cmd_fmt_merge_msg }
 	};
 	int i;
 
-- 
1.4.1.gb2dcd-dirty

```

## Junio C Hamano, 2006-07-03 22:39

Subject: Re: [PATCH 2nd try] Make git-fmt-merge-msg a builtin
Message-ID: <7vwtaunwj1.fsf@assigned-by-dhcp.cox.net>
URL: https://gitlist.dev/e/7vwtaunwj1.fsf%40assigned-by-dhcp.cox.net
In-Reply-To: <Pine.LNX.4.63.0607031717540.29667@wbgn013.biozentrum.uni-wuerzburg.de>

```
Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:

> Signed-off-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
>
> ---
> 	This retires git-fmt-merge-msg.perl, since it passes all the
> 	tests, but removes the Perl version not now.

There is no point not removing the script if you update git.c
and Makefile to point at the new one.

We do not have a test specific for fmt-merge-msg, so it
obviously passes all the tests ;-).  A new one is attached.

I think we should extend boolean to accept 'yes' and 'no', as I
suggested earlier on the list, but other than that things look
good.

Thanks for the patch; no need to resubmit -- I'll munge the
points I raised above.

-- >8 --
diff --git a/t/t6200-fmt-merge-msg.sh b/t/t6200-fmt-merge-msg.sh
new file mode 100755
index 0000000..63e49f3
--- /dev/null
+++ b/t/t6200-fmt-merge-msg.sh
@@ -0,0 +1,163 @@
+#!/bin/sh
+#
+# Copyright (c) 2006, Junio C Hamano
+#
+
+test_description='fmt-merge-msg test'
+
+. ./test-lib.sh
+
+datestamp=1151939923
+setdate () {
+	GIT_COMMITTER_DATE="$datestamp +0200"
+	GIT_AUTHOR_DATE="$datestamp +0200"
+	datestamp=`expr "$datestamp" + 1`
+	export GIT_COMMITTER_DATE GIT_AUTHOR_DATE
+}
+
+test_expect_success setup '
+	echo one >one &&
+	git add one &&
+	setdate &&
+	git commit -m "Initial" &&
+
+	echo uno >one &&
+	echo dos >two &&
+	git add two &&
+	setdate &&
+	git commit -a -m "Second" &&
+
+	git checkout -b left &&
+
+	echo $datestamp >one &&
+	setdate &&
+	git commit -a -m "Common #1" &&
+
+	echo $datestamp >one &&
+	setdate &&
+	git commit -a -m "Common #2" &&
+
+	git branch right &&
+
+	echo $datestamp >two &&
+	setdate &&
+	git commit -a -m "Left #3" &&
+
+	echo $datestamp >two &&
+	setdate &&
+	git commit -a -m "Left #4" &&
+
+	echo $datestamp >two &&
+	setdate &&
+	git commit -a -m "Left #5" &&
+
+	git checkout right &&
+
+	echo $datestamp >three &&
+	git add three &&
+	setdate &&
+	git commit -a -m "Right #3" &&
+
+	echo $datestamp >three &&
+	setdate &&
+	git commit -a -m "Right #4" &&
+
+	echo $datestamp >three &&
+	setdate &&
+	git commit -a -m "Right #5" &&
+
+	git show-branch
+'
+
+cat >expected <<\EOF
+Merge branch 'left'
+EOF
+
+test_expect_success 'merge-msg test #1' '
+
+	git checkout master &&
+	git fetch . left &&
+
+	git fmt-merge-msg <.git/FETCH_HEAD >actual &&
+	diff -u actual expected
+'
+
+cat >expected <<\EOF
+Merge branch 'left' of ../trash
+EOF
+
+test_expect_success 'merge-msg test #2' '
+
+	git checkout master &&
+	git fetch ../trash left &&
+
+	git fmt-merge-msg <.git/FETCH_HEAD >actual &&
+	diff -u actual expected
+'
+
+cat >expected <<\EOF
+Merge branch 'left'
+
+* left:
+  Left #5
+  Left #4
+  Left #3
+  Common #2
+  Common #1
+EOF
+
+test_expect_success 'merge-msg test #3' '
+
+	git repo-config merge.summary true &&
+
+	git checkout master &&
+	setdate &&
+	git fetch . left &&
+
+	git fmt-merge-msg <.git/FETCH_HEAD >actual &&
+	diff -u actual expected
+'
+
+cat >expected <<\EOF
+Merge branches 'left' and 'right'
+
+* left:
+  Left #5
+  Left #4
+  Left #3
+  Common #2
+  Common #1
+
+* right:
+  Right #5
+  Right #4
+  Right #3
+  Common #2
+  Common #1
+EOF
+
+test_expect_success 'merge-msg test #4' '
+
+	git repo-config merge.summary true &&
+
+	git checkout master &&
+	setdate &&
+	git fetch . left right &&
+
+	git fmt-merge-msg <.git/FETCH_HEAD >actual &&
+	diff -u actual expected
+'
+
+test_expect_success 'merge-msg test #5' '
+
+	git repo-config merge.summary yes &&
+
+	git checkout master &&
+	setdate &&
+	git fetch . left right &&
+
+	git fmt-merge-msg <.git/FETCH_HEAD >actual &&
+	diff -u actual expected
+'
+
+test_done

```

## Johannes Schindelin, 2006-07-04 07:50

Subject: Re: [PATCH 2nd try] Make git-fmt-merge-msg a builtin
Message-ID: <Pine.LNX.4.63.0607040948170.29667@wbgn013.biozentrum.uni-wuerzburg.de>
URL: https://gitlist.dev/e/Pine.LNX.4.63.0607040948170.29667%40wbgn013.biozentrum.uni-wuerzburg.de
In-Reply-To: <7vwtaunwj1.fsf@assigned-by-dhcp.cox.net>

```
Hi,

On Mon, 3 Jul 2006, Junio C Hamano wrote:

> Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
> 
> > Signed-off-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
> >
> > ---
> > 	This retires git-fmt-merge-msg.perl, since it passes all the
> > 	tests, but removes the Perl version not now.
> 
> There is no point not removing the script if you update git.c
> and Makefile to point at the new one.

I should have put PATH/RFC. I did not want to remove the script right 
away, so people could compare outputs (like Martin did with 
git-format-patch).

> We do not have a test specific for fmt-merge-msg, so it
> obviously passes all the tests ;-).  A new one is attached.

Well, t4013 showed me two output bugs, but it evidently did not check for 
merge.summary.

> I think we should extend boolean to accept 'yes' and 'no',

In that case, could we have "YesPlease", "oui", "si" and "da", too? ;-)

Ciao,
Dscho

```

## Junio C Hamano, 2006-07-04 07:56

Subject: Re: [PATCH 2nd try] Make git-fmt-merge-msg a builtin
Message-ID: <7vk66tls5o.fsf@assigned-by-dhcp.cox.net>
URL: https://gitlist.dev/e/7vk66tls5o.fsf%40assigned-by-dhcp.cox.net
In-Reply-To: <Pine.LNX.4.63.0607040948170.29667@wbgn013.biozentrum.uni-wuerzburg.de>

```
Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:

> I should have put PATH/RFC. I did not want to remove the script right 
> away, so people could compare outputs (like Martin did with 
> git-format-patch).

Well the stuff is quite straightforward and I think we could fix
it in-tree while in "next".

>> I think we should extend boolean to accept 'yes' and 'no',
>
> In that case, could we have "YesPlease", "oui", "si" and "da", too? ;-)

Hai?  Nah, that's too much.

```

## Johannes Schindelin, 2006-07-04 08:18

Subject: Re: [PATCH 2nd try] Make git-fmt-merge-msg a builtin
Message-ID: <Pine.LNX.4.63.0607041017480.29667@wbgn013.biozentrum.uni-wuerzburg.de>
URL: https://gitlist.dev/e/Pine.LNX.4.63.0607041017480.29667%40wbgn013.biozentrum.uni-wuerzburg.de
In-Reply-To: <7vk66tls5o.fsf@assigned-by-dhcp.cox.net>

```
Hai^H^H^HHi,

On Tue, 4 Jul 2006, Junio C Hamano wrote:

> >> I think we should extend boolean to accept 'yes' and 'no',
> >
> > In that case, could we have "YesPlease", "oui", "si" and "da", too? ;-)
> 
> Hai?  Nah, that's too much.

You know, I always wondered if we could introduce "perhaps", for us 
procrastinators.

Ciao,
Dscho

```
