git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH v7 12/9] column: support grouping entries

From
Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Date
Mar 13, 2012, 12:11 UTC
Message-ID
<c3c9422bcfccad34e7b97c39a267207cfb41c0b7.1331638664.git.pclouds@gmail.com>
In-Reply-To
<25ba0cdc7d4898e65af12b584c0e9ebf4e412557.1331638664.git.pclouds@gmail.com>

If many entries share the same prefix (e.g. "git ls-files Documentation/"), cutting out the prefix helps put more information on the same space.

If "group" is specified, the list of entries will be searched for largest non-overlapping rectangles of text. Estimation is done on each rectangle to see if there are any savings in rows if we group them. Groups are printed first, then the remaining as the last group.

Handling the remaining part this way may not be ideal but I don't want to split all directories like "ls -R". That takes too many lines. Maybe I should prepend ".../" to all grouped items to make it clear they are grouped. There's also problem with ansi escape codes that I'll need to handle if this sounds like a good way to go.

This code may be used for diffstat too (e.g. when most of the diff is in Documentation/).

For demonstration, this is what "COLUMNS=80 git ls-files --column=group -- '*.[ch]'" looks like

builtin/: add.c gc.c read-tree.c annotate.c grep.c receive-pack.c <snip> for-each-ref.c prune.c verify-tag.c fsck.c push.c write-tree.c

compat/: basename.c regex/regexec.c bswap.h setenv.c <snip> regex/regex_internal.c win32mmap.c regex/regex_internal.h winansi.c

contrib/: convert-objects/convert-objects.c credential/osxkeychain/git-credential-osxkeychain.c examples/builtin-fetch--tool.c svn-fe/svn-fe.c

vcs-svn/: fast_export.c line_buffer.h sliding_window.c svndiff.h fast_export.h repo_tree.c sliding_window.h svndump.c line_buffer.c repo_tree.h svndiff.c svndump.h

xdiff/: xdiff.h xemit.c xinclude.h xpatience.c xtypes.h xdiffi.c xemit.h xmacros.h xprepare.c xutils.c xdiffi.h xhistogram.c xmerge.c xprepare.h xutils.h

...: abspath.c pack-check.c advice.c pack-refs.c <snip> object.c xdiff-interface.h object.h zlib.c

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 column.c |  229 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 column.h |    1 +
 2 files changed, 227 insertions(+), 3 deletions(-)
diff --git a/column.c b/column.c
index 80eefa0..8214a9b 100644
--- a/column.c
+++ b/column.c
@@ -19,6 +19,11 @@ struct column_data {
 	int *width;	      /* index to the longest row in column */
 };
 
+struct area {
+	int start, end;		/* in string_list */
+	int size;
+};
+
 /* return length of 's' in letters, ANSI escapes stripped */
 static int item_length(unsigned int colopts, const char *s)
 {
@@ -223,9 +228,9 @@ static int display_table(const struct string_list *list,
 		i = break_long_line(&data);
 		if (i != -1) {
 			printf("%s%s" "%s%s%s" "%s%s",
-			       indent, nl,
-			       indent, list->items[i].string, nl,
-			       indent, nl);
+			       opts->indent, opts->nl,
+			       opts->indent, list->items[i].string, opts->nl,
+			       opts->indent, opts->nl);
 			free(data.len);
 			free(data.width);
 			return i + 1;
@@ -248,6 +253,218 @@ static int display_table(const struct string_list *list,
 	return list->nr;
 }
 
+/*
+ * Find out the contiguous list of entries sharing the same directory
+ * prefix that nr * (prefix_len - skip) is largest, where nr is the
+ * number of entries and prefix_len is the shared directory prefix's
+ * length.
+ */
+static int largest_block(const struct string_list *list, int start, int skip, int *len)
+{
+	const char *str = list->items[start].string;
+	const char *slash;
+	int largest_area = 0;
+
+	for (slash = str + strlen(str) - 1; slash > str + skip; slash--) {
+		int i, area;
+		if (*slash != '/')
+			continue;
+		for (i = start; i < list->nr; i++) {
+			const char *s = list->items[i].string;
+			if (strlen(s) < slash + 1 - str ||
+			    memcmp(str + skip, s + skip, slash + 1 - (str + skip)))
+				break;
+		}
+		area = (i - start) * (slash + 1 - str - skip);
+		if (area > largest_area) {
+			largest_area = area;
+			*len = i - start;
+		}
+	}
+	return largest_area;
+}
+
+static int area_size_cmp(const void *a, const void *b)
+{
+	const struct area *area1 = a;
+	const struct area *area2 = b;
+	return area2->size - area1->size;
+}
+
+/*
+ * Make a sorted list of non-overlapping blocks, largest ones first
+ */
+static struct area *find_large_blocks(const struct string_list *list, int *nr_p)
+{
+	int i, nr = 0, alloc = 16;
+	struct area *areas = xmalloc(sizeof(*areas) * alloc);
+	struct area last;
+	memset(&last, 0, sizeof(last));
+
+	for (i = 0; i < list->nr; i++) {
+		int len, size = largest_block(list, i, 0, &len);
+		if (!size || len == 1)
+			continue;
+		/* the new found area is overlapped with the old one,
+		   but smaller, skip it */
+		if (i < last.end) {
+			if (size < last.size)
+				continue;
+			last.start = i;
+			last.end = i + len;
+			last.size = size;
+			continue;
+		}
+		if (last.size) {
+			if (nr + 1 < alloc)
+				ALLOC_GROW(areas, nr + 1, alloc);
+			areas[nr++] = last;
+		}
+		last.start = i;
+		last.end = i + len;
+		last.size = size;
+	}
+	if (last.size) {
+		if (nr + 1 >= alloc)
+			ALLOC_GROW(areas, nr + 1, alloc);
+		areas[nr++] = last;
+	}
+	qsort(areas, nr, sizeof(*areas), area_size_cmp);
+	*nr_p = nr;
+	return areas;
+}
+
+static int area_start_cmp(const void *a, const void *b)
+{
+	const struct area *area1 = a;
+	const struct area *area2 = b;
+	return area1->start - area2->start;
+}
+
+/*
+ * Assume list is split into two tables: one from "start" to "stop",
+ * where all strings are truncated "skip" bytes, the other the rest of
+ * the strings. Calculate how many rows required if all cells of each
+ * table are of the same width.
+ */
+static int split_layout_gain(const struct string_list *list, int *lengths,
+			     const struct column_options *opts,
+			     int start, int stop, int skip)
+{
+	int i, width0, width1, width2, cols, rows0, rows1;
+	int indent = strlen(opts->indent);
+
+	width0 = width1 = width2 = 0;
+	for (i = 0; i < list->nr; i++) {
+		int len = lengths[i];
+		if (width0 < len)
+			width0 = len;
+		if (i >= start && i < stop) {
+			len -= skip;
+			if (width2 < len)
+				width2 = len;
+		} else {
+			if (width1 < len)
+				width1 = len;
+		}
+	}
+
+	width0 += opts->padding;
+	cols = (opts->width - indent) / width0;
+	if (cols == 0)
+		cols = 1;
+	rows0 = DIV_ROUND_UP(list->nr, cols);
+
+	width1 += opts->padding;
+	cols = (opts->width - indent) / width1;
+	if (cols == 0)
+		cols = 1;
+	rows1 = DIV_ROUND_UP(list->nr - (stop - start), cols);
+
+	width2 += opts->padding;
+	cols = (opts->width - indent) / width2;
+	if (cols == 0)
+		cols = 1;
+	rows1 += DIV_ROUND_UP(stop - start, cols);
+	return rows0 - rows1;
+}
+
+static void group_by_prefix(const struct string_list *list, unsigned int colopts,
+			    const struct column_options *opts)
+{
+	int i, nr;
+	struct area *areas = find_large_blocks(list, &nr);
+	struct string_list new_list = STRING_LIST_INIT_NODUP;
+	struct area *dst;
+	int *len;
+
+	assert(colopts & COL_GROUP);
+	/* avoid inifinite loop when calling print_columns again */
+	colopts &= ~COL_GROUP;
+
+	len = xmalloc(sizeof(*len) * list->nr);
+	for (i = 0; i < list->nr; i++)
+		len[i] = item_length(colopts, list->items[i].string);
+
+	/*
+	 * Calculate and see if there is any saving when print this as
+	 * a group. Base our calculation on non-dense mode even if
+	 * users want dense output because the calculation would be
+	 * less expensive.
+	 */
+	dst = areas;
+	for (i = 0; i < nr; i++) {
+		struct area *area = areas + i;
+		int rows, skip = area->size / (area->end - area->start);
+		rows = split_layout_gain(list, len, opts,
+					 area->start, area->end, skip);
+
+		if (rows > 3) {
+			if (areas + i != dst)
+				*dst = *area;
+			dst++;
+		}
+	}
+	free(len);
+
+	nr = dst - areas;
+	if (!nr) {
+		print_columns(list, colopts, opts);
+		return;
+	}
+	qsort(areas, nr, sizeof(*areas), area_start_cmp);
+
+	/*
+	 * We now have list of worthy groups, sorted by offset. Print
+	 * group by group, then the rest.
+	 */
+	for (i = 0; i < nr; i++) {
+		struct area *area = areas + i;
+		int j, skip = area->size / (area->end - area->start);
+
+		for (j = area->start; j < area->end; j++)
+			string_list_append(&new_list,
+					   list->items[j].string + skip);
+		printf("\n%.*s:\n", skip, list->items[area->start].string);
+		print_columns(&new_list, colopts, opts);
+		string_list_clear(&new_list, 0);
+	}
+
+	printf("\n%s:\n", "...");
+	for (i = 0; i < nr; i++) {
+		struct area *area = areas + i;
+		int j;
+		for (j = i ? area[-1].end : 0; j < area->start; j++)
+			string_list_append(&new_list, list->items[j].string);
+	}
+	for (i = areas[nr-1].end; i < list->nr; i++)
+		string_list_append(&new_list, list->items[i].string);
+	print_columns(&new_list, colopts, opts);
+	string_list_clear(&new_list, 0);
+
+	free(areas);
+}
+
 void print_columns(const struct string_list *list, unsigned int colopts,
 		   const struct column_options *opts)
 {
@@ -264,6 +481,11 @@ void print_columns(const struct string_list *list, unsigned int colopts,
 	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 (colopts & COL_GROUP) {
+		group_by_prefix(list, colopts, &nopts);
+		return;
+	}
 	if (!COL_ENABLE(colopts)) {
 		display_plain(list, "", "\n");
 		return;
@@ -318,6 +540,7 @@ static int parse_option(const char *arg, int len, unsigned int *colopts,
 		{ "row",    COL_ROW,      COL_LAYOUT_MASK },
 		{ "dense",  COL_DENSE,    0 },
 		{ "denser", COL_DENSER,   0 },
+		{ "group",  COL_GROUP,    0 },
 	};
 	int i;
 
diff --git a/column.h b/column.h
index dbc5da2..f3934cf 100644
--- a/column.h
+++ b/column.h
@@ -7,6 +7,7 @@
 #define COL_DENSE         0x0080   /* Shrink columns when possible,
 				      making space for more columns */
 #define COL_DENSER        0x0100
+#define COL_GROUP         0x0200
 
 #define COL_ENABLE(c) ((c) & COL_ENABLE_MASK)
 #define COL_DISABLED      0x0000   /* must be zero */
-- 
1.7.8.36.g69ee2
Previous: Nguyễn Thái Ngọc DuyNext: Junio C Hamano
Message 63 of 66 in “Column display”
  1. 00/11 Column displayNguyễn Thái Ngọc Duy, Feb 25, 2012
  2. 01/11 column: add API to print items in columnsNguyễn Thái Ngọc Duy, Feb 25, 2012
  3. 02/11 Add git-column and column mode parsingNguyễn Thái Ngọc Duy, Feb 25, 2012
  4. Ramsay JonesFeb 27, 2012
  5. Nguyen Thai Ngoc DuyFeb 28, 2012
  6. 03/11 Stop starting pager recursivelyNguyễn Thái Ngọc Duy, Feb 25, 2012
  7. 04/11 column: add columnar layoutNguyễn Thái Ngọc Duy, Feb 25, 2012
  8. 05/11 column: support columns with different widthsNguyễn Thái Ngọc Duy, Feb 25, 2012
  9. Junio C HamanoFeb 26, 2012
  10. 06/11 column: add column.ui for default column output settingsNguyễn Thái Ngọc Duy, Feb 25, 2012
  11. Junio C HamanoFeb 27, 2012
  12. Nguyen Thai Ngoc DuyFeb 27, 2012
  13. 07/11 help: reuse print_columns() for help -aNguyễn Thái Ngọc Duy, Feb 25, 2012
  14. 08/11 branch: add --columnNguyễn Thái Ngọc Duy, Feb 25, 2012
  15. 09/11 status: add --columnNguyễn Thái Ngọc Duy, Feb 25, 2012
  16. 10/11 column: support piping stdout to external git-column processNguyễn Thái Ngọc Duy, Feb 25, 2012
  17. 11/11 tag: add --columnNguyễn Thái Ngọc Duy, Feb 25, 2012
  18. Junio C HamanoFeb 26, 2012
  19. Nguyen Thai Ngoc DuyFeb 27, 2012
  20. Junio C HamanoFeb 27, 2012
  21. Junio C HamanoFeb 27, 2012
  22. Nguyen Thai Ngoc DuyFeb 27, 2012
  23. 00/10 Column displayNguyễn Thái Ngọc Duy, Feb 28, 2012
  24. 01/10 Add git-column for columnar displayNguyễn Thái Ngọc Duy, Feb 28, 2012
  25. Junio C HamanoFeb 28, 2012
  26. Nguyen Thai Ngoc DuyMar 2, 2012
  27. 02/10 Stop starting pager recursivelyNguyễn Thái Ngọc Duy, Feb 28, 2012
  28. Junio C HamanoFeb 28, 2012
  29. Junio C HamanoFeb 28, 2012
  30. Nguyen Thai Ngoc DuyFeb 29, 2012
  31. Junio C HamanoFeb 29, 2012
  32. Nguyen Thai Ngoc DuyFeb 29, 2012
  33. Junio C HamanoFeb 29, 2012
  34. 03/10 column: add columnar layoutNguyễn Thái Ngọc Duy, Feb 28, 2012
  35. Junio C HamanoFeb 28, 2012
  36. 04/10 column: add dense layout supportNguyễn Thái Ngọc Duy, Feb 28, 2012
  37. Junio C HamanoFeb 28, 2012
  38. Nguyen Thai Ngoc DuyMar 2, 2012
  39. Junio C HamanoMar 2, 2012
  40. 05/10 column: add column.ui for default column output settingsNguyễn Thái Ngọc Duy, Feb 28, 2012
  41. Junio C HamanoFeb 28, 2012
  42. 06/10 help: reuse print_columns() for help -aNguyễn Thái Ngọc Duy, Feb 28, 2012
  43. 07/10 branch: add --columnNguyễn Thái Ngọc Duy, Feb 28, 2012
  44. 08/10 status: add --columnNguyễn Thái Ngọc Duy, Feb 28, 2012
  45. 09/10 column: support piping stdout to external git-column processNguyễn Thái Ngọc Duy, Feb 28, 2012
  46. 10/10 tag: add --columnNguyễn Thái Ngọc Duy, Feb 28, 2012
  47. Thomas RastMar 2, 2012
  48. Nguyen Thai Ngoc DuyMar 11, 2012
  49. Junio C HamanoMar 12, 2012
  50. 00/9 Column displayNguyễn Thái Ngọc Duy, Mar 13, 2012
  51. Nguyen Thai Ngoc DuyMar 13, 2012
  52. 01/9 Add column layout skeleton and git-columnNguyễn Thái Ngọc Duy, Mar 13, 2012
  53. 02/9 Stop starting pager recursivelyNguyễn Thái Ngọc Duy, Mar 13, 2012
  54. 03/9 column: add columnar layoutNguyễn Thái Ngọc Duy, Mar 13, 2012
  55. 04/9 column: add dense layout supportNguyễn Thái Ngọc Duy, Mar 13, 2012
  56. 05/9 help: reuse print_columns() for help -aNguyễn Thái Ngọc Duy, Mar 13, 2012
  57. 06/9 branch: add --columnNguyễn Thái Ngọc Duy, Mar 13, 2012
  58. 07/9 status: add --columnNguyễn Thái Ngọc Duy, Mar 13, 2012
  59. 08/9 column: support piping stdout to external git-column processNguyễn Thái Ngọc Duy, Mar 13, 2012
  60. 09/9 tag: add --columnNguyễn Thái Ngọc Duy, Mar 13, 2012
  61. 10/9 ls-files: support --columnNguyễn Thái Ngọc Duy, Mar 13, 2012
  62. 11/9 column: support "denser" modeNguyễn Thái Ngọc Duy, Mar 13, 2012
  63. 12/9 column: support grouping entriesNguyễn Thái Ngọc Duy, Mar 13, 2012
  64. Junio C HamanoMar 13, 2012
  65. Nguyen Thai Ngoc DuyMar 14, 2012
  66. Junio C HamanoMar 14, 2012

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.