Subject: [PATCH v7 01/10] Add git-column for columnar display

From: Nguyễn Thái Ngọc Duy <[email-masked]>
Date: Tue, 28 Feb 2012 18:58:42 +0700

This command is not particularly tied to anything specific to git. The
command is added for supporting column display in various commands,
either by reusing code or piping output to this command for layour.
The command at this stage is just a skeleton, no layout code yet.

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

 - enabling: always, never and auto
 - layout mode: to be implemented
 - other tuning, which could be negated be prefix 'no'

A command line option without argument (e.g. --column) will enable
column output and reuse existing settings (layout mode and options..).
--no-column disables columnar output.

Thanks-to: Ramsay Jones <[email-masked]>
Signed-off-by: Nguyễn Thái Ngọc Duy <[email-masked]>
---
 .gitignore                   |    1 +
 Documentation/git-column.txt |   49 ++++++++++++
 Makefile                     |    3 +
 builtin.h                    |    1 +
 builtin/column.c             |   41 ++++++++++
 column.c                     |  170 ++++++++++++++++++++++++++++++++++++++++++
 column.h                     |   30 ++++++++
 command-list.txt             |    1 +
 git.c                        |    1 +
 parse-options.h              |    2 +
 t/t9002-column.sh            |   27 +++++++
 11 files changed, 326 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/git-column.txt b/Documentation/git-column.txt
new file mode 100644
index 0000000..508b85f
--- /dev/null
+++ b/Documentation/git-column.txt
@@ -0,0 +1,49 @@
+git-column(1)
+=============
+
+NAME
+----
+git-column - Display data in columns
+
+SYNOPSIS
+--------
+[verse]
+'git column' [--mode=<mode> | --rawmode=<n>] [--width=<width>]
+	     [--indent=<string>] [--nl=<string>] [--pading=<n>]
+
+DESCRIPTION
+-----------
+This command formats its input into multiple columns.
+
+OPTIONS
+-------
+--mode=<mode>::
+	Specify layout mode. See configuration variable column.ui for option
+	syntax.
+
+--rawmode=<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..3b0f74e
--- /dev/null
+++ b/builtin/column.c
@@ -0,0 +1,41 @@
+#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;
+
+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;
+	struct option options[] = {
+		OPT_COLUMN(0, "mode", &colopts, "layout to use"),
+		OPT_INTEGER(0, "rawmode", &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()
+	};
+
+	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);
+
+	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..d61da81
--- /dev/null
+++ b/column.c
@@ -0,0 +1,170 @@
+#include "cache.h"
+#include "column.h"
+#include "string-list.h"
+#include "parse-options.h"
+
+#define MODE(mode) ((mode) & COL_MODE)
+
+/* Display without layout when COL_ENABLED is not set */
+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 mode,
+		   struct column_options *opts)
+{
+	const char *indent = "", *nl = "\n";
+	int padding = 1, width = term_columns();
+
+	if (!list->nr)
+		return;
+	if (opts) {
+		if (opts->indent)
+			indent = opts->indent;
+		if (opts->nl)
+			nl = opts->nl;
+		if (opts->width)
+			width = opts->width;
+		padding = opts->padding;
+	}
+	if (width <= 1 || !(mode & COL_ENABLED)) {
+		display_plain(list, indent, nl);
+		return;
+	}
+	die("BUG: invalid mode %d", MODE(mode));
+}
+
+struct colopt {
+	enum {
+		ENABLE,
+		MODE,
+		OPTION
+	} type;
+	const char *name;
+	int value;
+};
+
+/*
+ * Set COL_ENABLED and COL_ENABLED_SET. If 'set' is -1, check if
+ * stdout is tty.
+ */
+static int set_enable_bit(unsigned int *mode, int set, int stdout_is_tty)
+{
+	if (set < 0) {	/* auto */
+		if (stdout_is_tty < 0)
+			stdout_is_tty = isatty(1);
+		set = stdout_is_tty || (pager_in_use() && pager_use_color);
+	}
+	if (set)
+		*mode = *mode | COL_ENABLED | COL_ENABLED_SET;
+	else
+		*mode = (*mode & ~COL_ENABLED) | COL_ENABLED_SET;
+	return 0;
+}
+
+/*
+ * Set COL_MODE_*. mode is intially copied from column.ui. If
+ * COL_ENABLED_SET is not set, then neither 'always', 'never' nor
+ * 'auto' has been used. Default to 'always'.
+ */
+static int set_mode(unsigned int *mode, unsigned int value)
+{
+	*mode = (*mode & ~COL_MODE) | value;
+	if (!(*mode & COL_ENABLED_SET))
+		*mode |= COL_ENABLED | COL_ENABLED_SET;
+
+	return 0;
+}
+
+/* Set or unset other COL_* */
+static int set_option(unsigned int *mode, unsigned int opt, int set)
+{
+	if (set)
+		*mode |= opt;
+	else
+		*mode &= ~opt;
+	return 0;
+}
+
+static int parse_option(const char *arg, int len,
+			unsigned int *mode, int stdout_is_tty)
+{
+	struct colopt opts[] = {
+		{ ENABLE, "always",  1 },
+		{ ENABLE, "never",   0 },
+		{ ENABLE, "auto",   -1 },
+	};
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(opts); i++) {
+		int set = 1, arg_len = len, name_len;
+		const char *arg_str = arg;
+
+		if (opts[i].type == OPTION) {
+			if (arg_len > 2 && !strncmp(arg_str, "no", 2)) {
+				arg_str += 2;
+				arg_len -= 2;
+				set = 0;
+			}
+		}
+
+		name_len = strlen(opts[i].name);
+		if (arg_len != name_len ||
+		    strncmp(arg_str, opts[i].name, name_len))
+			continue;
+
+		switch (opts[i].type) {
+		case ENABLE:
+			return set_enable_bit(mode, opts[i].value,
+					      stdout_is_tty);
+		case MODE:
+			return set_mode(mode, opts[i].value);
+		case OPTION:
+			return set_option(mode, opts[i].value, set);
+		default:
+			die("BUG: Unknown option type %d", opts[i].type);
+		}
+	}
+
+	return error("unsupported style '%s'", arg);
+}
+
+int git_config_column(unsigned int *mode, const char *value,
+		      int stdout_is_tty)
+{
+	const char *sep = " ,";
+
+	while (*value) {
+		int len = strcspn(value, sep);
+		if (len) {
+			if (parse_option(value, len, mode, stdout_is_tty))
+				return -1;
+
+			value += len;
+		}
+		value += strspn(value, sep);
+	}
+	return 0;
+}
+
+int parseopt_column_callback(const struct option *opt,
+			     const char *arg, int unset)
+{
+	unsigned int *mode = opt->value;
+	*mode |= COL_PARSEOPT;
+	if (unset) {
+		*mode = (*mode & ~COL_ENABLED) | COL_ENABLED_SET;
+		return 0;
+	}
+	if (arg)
+		return git_config_column(mode, arg, -1);
+
+	/* no arg, turn it on */
+	*mode |= COL_ENABLED | COL_ENABLED_SET;
+	return 0;
+}
diff --git a/column.h b/column.h
new file mode 100644
index 0000000..67b1c4f
--- /dev/null
+++ b/column.h
@@ -0,0 +1,30 @@
+#ifndef COLUMN_H
+#define COLUMN_H
+
+#define COL_MODE          0x000F
+#define COL_ENABLED      (1 << 4)
+#define COL_ENABLED_SET  (1 << 5)  /* Has COL_ENABLED been set by config? */
+#define COL_PARSEOPT     (1 << 8)  /* --column is given */
+
+#define explicitly_enable_column(c) \
+	(((c) & (COL_PARSEOPT | COL_ENABLED)) == (COL_PARSEOPT | COL_ENABLED))
+
+struct column_options {
+	int width;
+	int padding;
+	const char *indent;
+	const char *nl;
+};
+
+extern int term_columns(void);
+extern void print_columns(const struct string_list *list,
+			  unsigned int mode,
+			  struct column_options *opts);
+extern int git_config_column(unsigned int *mode, const char *value,
+			     int stdout_is_tty);
+
+struct option;
+extern int parseopt_column_callback(const struct option *opt,
+				    const char *arg, int unset);
+
+#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..419e3cc 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 },
 		{ "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..b0b6d62
--- /dev/null
+++ b/t/t9002-column.sh
@@ -0,0 +1,27 @@
+#!/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 --mode=never <lista >actual &&
+	test_cmp lista actual
+'
+
+test_done
-- 
1.7.8.36.g69ee2