Subject: [PATCH] Add column layout skeleton and git-column

From: Nguyễn Thái Ngọc Duy <[email-masked]>
Date: Mon, 16 Apr 2012 20:26:24 +0700

A column option string consists of many token separated by either
a space or a  comma. A token belongs to one of three groups:

 - enabling: always, never and auto
 - layout mode: currently plain (which does not layout at all)
 - other future tuning flags

git-column can be used to pipe output to from a command that wants
column layout, but not to mess with its own output code. Simpler output
code can be changed to use column layout code directly.

Thanks-to: Ramsay Jones <[email-masked]>
Signed-off-by: Nguyễn Thái Ngọc Duy <[email-masked]>
---
 2012/4/16 Junio C Hamano <[email-masked]>:
 > Nguyễn Thái Ngọc Duy  <[email-masked]> writes:
 >
 >> +static int column_config(const char *var, const char *value,
 >> +                      const char *key, unsigned int *colopts)
 >> +{
 >> +     if (parse_config(colopts, value))
 >> +             return error("invalid %s mode %s", key, value);
 >> +     return 0;
 >> +}
 >> +
 >> +int git_column_config(const char *var, const char *value,
 >> +                   const char *command, unsigned int *colopts)
 >> +{
 >> +     if (!strcmp(var, "column.ui"))
 >> +             return column_config(var, value, "column.ui", colopts);
 >
 > I do not think there is anything that reads column.ui from a configuration
 > file (or "git -c column.ui") in this step, but later patches seem to use
 > it from their configuration callback (e.g. git_branch_config()) and at
 > that point you will segfault because you ignore the case where value is
 > NULL.

 fixed

 >> +     if (command) {
 >> +             struct strbuf sb = STRBUF_INIT;
 >> +             int ret = 0;
 >> +             strbuf_addf(&sb, "column.%s", command);
 >> +             if (!strcmp(var, sb.buf))
 >> +                     ret = column_config(var, value, sb.buf, colopts);
 >> +             strbuf_release(&sb);
 >> +             return ret;
 >> +     }
 >
 > This whole thing looks overly wasteful.  How about doing it this way?
 >
 >        const char *it = skip_prefix(var, "column.");
 >        if (!it)
 >                return 0;
 >        if (!strcmp(it, "ui"))
 >                parse "ui";
 >        else if (!strcmp(it, command))
 >                parse "command";
 >
 > and make the third parameter to column_config() be without the constant
 > prefix "column."?

 Nice, and thanks I didn't know about skip_prefix. How about this?

 .gitignore                   |    1 +
 Documentation/config.txt     |   18 +++++
 Documentation/git-column.txt |   53 +++++++++++++
 Makefile                     |    3 +
 builtin.h                    |    1 +
 builtin/column.c             |   59 +++++++++++++++
 column.c                     |  167 ++++++++++++++++++++++++++++++++++++++++++
 column.h                     |   35 +++++++++
 command-list.txt             |    1 +
 git.c                        |    1 +
 parse-options.h              |    2 +
 t/t9002-column.sh            |   45 +++++++++++
 12 files changed, 386 insertions(+), 0 deletions(-)
 create mode 100644 Documentation/git-column.txt
 create mode 100644 builtin/column.c
 create mode 100644 column.c
 create mode 100644 column.h
 create mode 100755 t/t9002-column.sh

diff --git a/.gitignore b/.gitignore
index 87fcc5f..2540264 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,6 +26,7 @@
 /git-cherry-pick
 /git-clean
 /git-clone
+/git-column
 /git-commit
 /git-commit-tree
 /git-config
diff --git a/Documentation/config.txt b/Documentation/config.txt
index abeb82b..147c41c 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -821,6 +821,24 @@ color.ui::
 	`never` if you prefer git commands not to use color unless enabled
 	explicitly with some other configuration or the `--color` option.
 
+column.ui::
+	Specify whether supported commands should output in columns.
+	This variable consists of a list of tokens separated by spaces
+	or commas:
++
+--
+`always`;;
+	always show in columns
+`never`;;
+	never show in columns
+`auto`;;
+	show in columns if the output is to the terminal
+`plain`;;
+	show in one column
+--
++
+	This option defaults to 'never'.
+
 commit.status::
 	A boolean to enable/disable inclusion of status information in the
 	commit message template when using an editor to prepare the commit
diff --git a/Documentation/git-column.txt b/Documentation/git-column.txt
new file mode 100644
index 0000000..9be16ee
--- /dev/null
+++ b/Documentation/git-column.txt
@@ -0,0 +1,53 @@
+git-column(1)
+=============
+
+NAME
+----
+git-column - Display data in columns
+
+SYNOPSIS
+--------
+[verse]
+'git column' [--command=<name>] [--[raw-]mode=<mode>] [--width=<width>]
+	     [--indent=<string>] [--nl=<string>] [--pading=<n>]
+
+DESCRIPTION
+-----------
+This command formats its input into multiple columns.
+
+OPTIONS
+-------
+--command=<name>::
+	Look up layout mode using configuration variable column.<name> and
+	column.ui.
+
+--mode=<mode>::
+	Specify layout mode. See configuration variable column.ui for option
+	syntax.
+
+--raw-mode=<n>::
+	Same as --mode but take mode encoded as a number. This is mainly used
+	by other commands that have already parsed layout mode.
+
+--width=<width>::
+	Specify the terminal width. By default 'git column' will detect the
+	terminal width, or fall back to 80 if it is unable to do so.
+
+--indent=<string>::
+	String to be printed at the beginning of each line.
+
+--nl=<N>::
+	String to be printed at the end of each line,
+	including newline character.
+
+--padding=<N>::
+	The number of spaces between columns. One space by default.
+
+
+Author
+------
+Written by Nguyen Thai Ngoc Duy <[email-masked]>
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Makefile b/Makefile
index a0de4e9..0998f0d 100644
--- a/Makefile
+++ b/Makefile
@@ -646,6 +646,7 @@ LIB_OBJS += bulk-checkin.o
 LIB_OBJS += bundle.o
 LIB_OBJS += cache-tree.o
 LIB_OBJS += color.o
+LIB_OBJS += column.o
 LIB_OBJS += combine-diff.o
 LIB_OBJS += commit.o
 LIB_OBJS += compat/obstack.o
@@ -774,6 +775,7 @@ BUILTIN_OBJS += builtin/checkout-index.o
 BUILTIN_OBJS += builtin/checkout.o
 BUILTIN_OBJS += builtin/clean.o
 BUILTIN_OBJS += builtin/clone.o
+BUILTIN_OBJS += builtin/column.o
 BUILTIN_OBJS += builtin/commit-tree.o
 BUILTIN_OBJS += builtin/commit.o
 BUILTIN_OBJS += builtin/config.o
@@ -2166,6 +2168,7 @@ builtin/prune.o builtin/reflog.o reachable.o: reachable.h
 builtin/commit.o builtin/revert.o wt-status.o: wt-status.h
 builtin/tar-tree.o archive-tar.o: tar.h
 connect.o transport.o url.o http-backend.o: url.h
+column.o help.o pager.o: column.h
 http-fetch.o http-walker.o remote-curl.o transport.o walker.o: walker.h
 http.o http-walker.o http-push.o http-fetch.o remote-curl.o: http.h url.h
 
diff --git a/builtin.h b/builtin.h
index 857b9c8..338f540 100644
--- a/builtin.h
+++ b/builtin.h
@@ -61,6 +61,7 @@ extern int cmd_cherry(int argc, const char **argv, const char *prefix);
 extern int cmd_cherry_pick(int argc, const char **argv, const char *prefix);
 extern int cmd_clone(int argc, const char **argv, const char *prefix);
 extern int cmd_clean(int argc, const char **argv, const char *prefix);
+extern int cmd_column(int argc, const char **argv, const char *prefix);
 extern int cmd_commit(int argc, const char **argv, const char *prefix);
 extern int cmd_commit_tree(int argc, const char **argv, const char *prefix);
 extern int cmd_config(int argc, const char **argv, const char *prefix);
diff --git a/builtin/column.c b/builtin/column.c
new file mode 100644
index 0000000..5ea798a
--- /dev/null
+++ b/builtin/column.c
@@ -0,0 +1,59 @@
+#include "builtin.h"
+#include "cache.h"
+#include "strbuf.h"
+#include "parse-options.h"
+#include "string-list.h"
+#include "column.h"
+
+static const char * const builtin_column_usage[] = {
+	"git column [options]",
+	NULL
+};
+static unsigned int colopts;
+
+static int column_config(const char *var, const char *value, void *cb)
+{
+	return git_column_config(var, value, cb, &colopts);
+}
+
+int cmd_column(int argc, const char **argv, const char *prefix)
+{
+	struct string_list list = STRING_LIST_INIT_DUP;
+	struct strbuf sb = STRBUF_INIT;
+	struct column_options copts;
+	const char *command = NULL, *real_command = NULL;
+	struct option options[] = {
+		OPT_STRING(0, "command", &real_command, "name", "lookup config vars"),
+		OPT_COLUMN(0, "mode", &colopts, "layout to use"),
+		OPT_INTEGER(0, "raw-mode", &colopts, "layout to use"),
+		OPT_INTEGER(0, "width", &copts.width, "Maximum width"),
+		OPT_STRING(0, "indent", &copts.indent, "string", "Padding space on left border"),
+		OPT_INTEGER(0, "nl", &copts.nl, "Padding space on right border"),
+		OPT_INTEGER(0, "padding", &copts.padding, "Padding space between columns"),
+		OPT_END()
+	};
+
+	/* This one is special and must be the first one */
+	if (argc > 1 && !prefixcmp(argv[1], "--command=")) {
+		command = argv[1] + 10;
+		git_config(column_config, (void *)command);
+	} else
+		git_config(column_config, NULL);
+
+	memset(&copts, 0, sizeof(copts));
+	copts.width = term_columns();
+	copts.padding = 1;
+	argc = parse_options(argc, argv, "", options, builtin_column_usage, 0);
+	if (argc)
+		usage_with_options(builtin_column_usage, options);
+	if (real_command || command) {
+		if (!real_command || !command || strcmp(real_command, command))
+			die(_("--command must be the first argument"));
+	}
+	finalize_colopts(&colopts, -1);
+	while (!strbuf_getline(&sb, stdin, '\n'))
+		string_list_append(&list, sb.buf);
+
+	print_columns(&list, colopts, &copts);
+	return 0;
+}
diff --git a/column.c b/column.c
new file mode 100644
index 0000000..c2b887d
--- /dev/null
+++ b/column.c
@@ -0,0 +1,167 @@
+#include "cache.h"
+#include "column.h"
+#include "string-list.h"
+#include "parse-options.h"
+
+/* Display without layout when not enabled */
+static void display_plain(const struct string_list *list,
+			  const char *indent, const char *nl)
+{
+	int i;
+
+	for (i = 0; i < list->nr; i++)
+		printf("%s%s%s", indent, list->items[i].string, nl);
+}
+
+void print_columns(const struct string_list *list, unsigned int colopts,
+		   const struct column_options *opts)
+{
+	struct column_options nopts;
+
+	if (!list->nr)
+		return;
+	assert(COL_ENABLE(colopts) != COL_AUTO);
+
+	memset(&nopts, 0, sizeof(nopts));
+	nopts.indent = opts && opts->indent ? opts->indent : "";
+	nopts.nl = opts && opts->nl ? opts->nl : "\n";
+	nopts.padding = opts ? opts->padding : 1;
+	nopts.width = opts && opts->width ? opts->width : term_columns() - 1;
+	if (!COL_ENABLE(colopts)) {
+		display_plain(list, "", "\n");
+		return;
+	}
+	switch (COL_LAYOUT(colopts)) {
+	case COL_PLAIN:
+		display_plain(list, nopts.indent, nopts.nl);
+		break;
+	default:
+		die("BUG: invalid layout mode %d", COL_LAYOUT(colopts));
+	}
+}
+
+int finalize_colopts(unsigned int *colopts, int stdout_is_tty)
+{
+	if (COL_ENABLE(*colopts) == COL_AUTO) {
+		if (stdout_is_tty < 0)
+			stdout_is_tty = isatty(1);
+		*colopts &= ~COL_ENABLE_MASK;
+		if (stdout_is_tty)
+			*colopts |= COL_ENABLED;
+	}
+	return 0;
+}
+
+struct colopt {
+	const char *name;
+	unsigned int value;
+	unsigned int mask;
+};
+
+#define LAYOUT_SET 1
+#define ENABLE_SET 2
+
+static int parse_option(const char *arg, int len, unsigned int *colopts,
+			int *group_set)
+{
+	struct colopt opts[] = {
+		{ "always", COL_ENABLED,  COL_ENABLE_MASK },
+		{ "never",  COL_DISABLED, COL_ENABLE_MASK },
+		{ "auto",   COL_AUTO,     COL_ENABLE_MASK },
+		{ "plain",  COL_PLAIN,    COL_LAYOUT_MASK },
+	};
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(opts); i++) {
+		int arg_len = len, name_len;
+		const char *arg_str = arg;
+
+		name_len = strlen(opts[i].name);
+		if (arg_len != name_len ||
+		    strncmp(arg_str, opts[i].name, name_len))
+			continue;
+
+		switch (opts[i].mask) {
+		case COL_ENABLE_MASK:
+			*group_set |= ENABLE_SET;
+			break;
+		case COL_LAYOUT_MASK:
+			*group_set |= LAYOUT_SET;
+			break;
+		}
+
+		if (opts[i].mask)
+			*colopts = (*colopts & ~opts[i].mask) | opts[i].value;
+		return 0;
+	}
+
+	return error("unsupported option '%s'", arg);
+}
+
+static int parse_config(unsigned int *colopts, const char *value)
+{
+	const char *sep = " ,";
+	int group_set = 0;
+
+	while (*value) {
+		int len = strcspn(value, sep);
+		if (len) {
+			if (parse_option(value, len, colopts, &group_set))
+				return -1;
+
+			value += len;
+		}
+		value += strspn(value, sep);
+	}
+	/*
+	 * Setting layout implies "always" if neither always, never
+	 * nor auto is specified.
+	 *
+	 * Current COL_ENABLE() value is disregarded. This means if
+	 * you set column.ui = auto and pass --column=row, then "auto"
+	 * will become "always".
+	 */
+	if ((group_set & LAYOUT_SET) && !(group_set & ENABLE_SET))
+		*colopts = (*colopts & ~COL_ENABLE_MASK) | COL_ENABLED;
+	return 0;
+}
+
+static int column_config(const char *var, const char *value,
+			 const char *key, unsigned int *colopts)
+{
+	if (value && parse_config(colopts, value))
+		return error("invalid column.%s mode %s", key, value);
+	return 0;
+}
+
+int git_column_config(const char *var, const char *value,
+		      const char *command, unsigned int *colopts)
+{
+	const char *it = skip_prefix(var, "column.");
+	if (!it)
+		return 0;
+
+	if (!strcmp(it, "ui"))
+		return column_config(var, value, "ui", colopts);
+
+	if (command && !strcmp(it, command))
+		return column_config(var, value, it, colopts);
+
+	return 0;
+}
+
+int parseopt_column_callback(const struct option *opt,
+			     const char *arg, int unset)
+{
+	unsigned int *colopts = opt->value;
+	*colopts |= COL_PARSEOPT;
+	*colopts &= ~COL_ENABLE_MASK;
+	if (unset)		/* --no-column == never */
+		return 0;
+	/* --column == always unless "arg" states otherwise */
+	*colopts |= COL_ENABLED;
+	if (arg)
+		return parse_config(colopts, arg);
+
+	return 0;
+}
diff --git a/column.h b/column.h
new file mode 100644
index 0000000..778a71c
--- /dev/null
+++ b/column.h
@@ -0,0 +1,35 @@
+#ifndef COLUMN_H
+#define COLUMN_H
+
+#define COL_LAYOUT_MASK   0x000F
+#define COL_ENABLE_MASK   0x0030   /* always, never or auto */
+#define COL_PARSEOPT      0x0040   /* --column is given from cmdline */
+
+#define COL_ENABLE(c) ((c) & COL_ENABLE_MASK)
+#define COL_DISABLED      0x0000   /* must be zero */
+#define COL_ENABLED       0x0010
+#define COL_AUTO          0x0020
+
+#define COL_LAYOUT(c) ((c) & COL_LAYOUT_MASK)
+#define COL_PLAIN             15   /* one column */
+
+#define explicitly_enable_column(c) \
+	(((c) & COL_PARSEOPT) && COL_ENABLE(c) == COL_ENABLED)
+
+struct column_options {
+	int width;
+	int padding;
+	const char *indent;
+	const char *nl;
+};
+
+struct option;
+extern int parseopt_column_callback(const struct option *, const char *, int);
+extern int git_column_config(const char *var, const char *value,
+			     const char *command, unsigned int *colopts);
+extern int finalize_colopts(unsigned int *colopts, int stdout_is_tty);
+
+extern void print_columns(const struct string_list *list, unsigned int colopts,
+			  const struct column_options *opts);
+
+#endif
diff --git a/command-list.txt b/command-list.txt
index a36ee9b..fe06f15 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -20,6 +20,7 @@ git-cherry-pick                         mainporcelain
 git-citool                              mainporcelain
 git-clean                               mainporcelain
 git-clone                               mainporcelain common
+git-column                              purehelpers
 git-commit                              mainporcelain common
 git-commit-tree                         plumbingmanipulators
 git-config                              ancillarymanipulators
diff --git a/git.c b/git.c
index 3805616..ee727cb 100644
--- a/git.c
+++ b/git.c
@@ -348,6 +348,7 @@ static void handle_internal_command(int argc, const char **argv)
 		{ "cherry-pick", cmd_cherry_pick, RUN_SETUP | NEED_WORK_TREE },
 		{ "clean", cmd_clean, RUN_SETUP | NEED_WORK_TREE },
 		{ "clone", cmd_clone },
+		{ "column", cmd_column, RUN_SETUP_GENTLY },
 		{ "commit", cmd_commit, RUN_SETUP | NEED_WORK_TREE },
 		{ "commit-tree", cmd_commit_tree, RUN_SETUP },
 		{ "config", cmd_config, RUN_SETUP_GENTLY },
diff --git a/parse-options.h b/parse-options.h
index 2e811dc..56fcafd 100644
--- a/parse-options.h
+++ b/parse-options.h
@@ -238,5 +238,7 @@ extern int parse_opt_noop_cb(const struct option *, const char *, int);
 	  PARSE_OPT_OPTARG, &parse_opt_abbrev_cb, 0 }
 #define OPT__COLOR(var, h) \
 	OPT_COLOR_FLAG(0, "color", (var), (h))
+#define OPT_COLUMN(s, l, v, h) \
+	{ OPTION_CALLBACK, (s), (l), (v), "style", (h), PARSE_OPT_OPTARG, parseopt_column_callback }
 
 #endif
diff --git a/t/t9002-column.sh b/t/t9002-column.sh
new file mode 100755
index 0000000..a7f3cd9
--- /dev/null
+++ b/t/t9002-column.sh
@@ -0,0 +1,45 @@
+#!/bin/sh
+
+test_description='git column'
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+	cat >lista <<\EOF
+one
+two
+three
+four
+five
+six
+seven
+eight
+nine
+ten
+eleven
+EOF
+'
+
+test_expect_success 'never' '
+	git column --indent=Z --mode=never <lista >actual &&
+	test_cmp lista actual
+'
+
+test_expect_success 'always' '
+	cat >expected <<\EOF &&
+Zone
+Ztwo
+Zthree
+Zfour
+Zfive
+Zsix
+Zseven
+Zeight
+Znine
+Zten
+Zeleven
+EOF
+	git column --indent=Z --mode=plain <lista >actual &&
+	test_cmp expected actual
+'
+
+test_done
-- 
1.7.3.1.256.g2539c.dirty