Code

Rewrite the revision graph renderer
authorJonas Fonseca <jonas.fonseca@savoirfairelinux.com>
Fri, 11 Dec 2009 22:05:23 +0000 (17:05 -0500)
committerJonas Fonseca <fonseca@diku.dk>
Mon, 7 Mar 2011 01:45:34 +0000 (20:45 -0500)
The new renderer is more 'square' but comes with three modes: UTF-8,
ncurses (using chtype graphical characters), and ASCII. The three modes
can be toggled. Enable revision graph rendering by default.

12 files changed:
.gitignore
Makefile
NEWS
graph.c [new file with mode: 0644]
graph.h [new file with mode: 0644]
io.c
io.h
manual.txt
test-graph.c [new file with mode: 0644]
tig.c
tig.h
tigrc.5.txt

index bd0c3a9d41063499663728b0174a960868def51a..d8dcc0cad25dfe0dea0c89cec275cfce02d35e5f 100644 (file)
@@ -16,6 +16,7 @@ tig.1
 tig.spec
 tigrc.5
 tigmanual.7
+test-graph
 *.html
 *.o
 *.xml
index 78131f515223e224935d942ab638886c5654ee54..044348294ab8be34cf37f7ac25a85f3b83d10889 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -35,8 +35,8 @@ RPM_RELEASE = $(word 2,$(RPM_VERLIST))$(if $(WTDIRTY),.dirty)
 LDLIBS ?= -lcurses
 CFLAGS ?= -Wall -O2
 DFLAGS = -g -DDEBUG -Werror -O0
-PROGS  = tig
-SOURCE = tig.c tig.h io.c io.h
+PROGS  = tig test-graph
+SOURCE = tig.c tig.h io.c io.h graph.c graph.h
 TXTDOC = tig.1.txt tigrc.5.txt manual.txt NEWS README INSTALL BUGS TODO
 MANDOC = tig.1 tigrc.5 tigmanual.7
 HTMLDOC = tig.1.html tigrc.5.html manual.html README.html NEWS.html
@@ -150,8 +150,11 @@ configure: configure.ac acinclude.m4
        install-doc-man install-doc-html clean spell-check dist rpm
 
 io.o: io.c io.h tig.h
+graph.o: tig.h
 tig.o: tig.c tig.h io.h
-tig: tig.o io.o
+tig: tig.o io.o graph.o
+test-graph.o: test-graph.c io.h tig.h graph.h
+test-graph: io.o graph.o
 
 tig.spec: contrib/tig.spec.in
        sed -e 's/@@VERSION@@/$(RPM_VERSION)/g' \
diff --git a/NEWS b/NEWS
index 0926f669d53c7f3a03823f07a2ce01f04bf33597..9a4439dc8b49afa8f90de2a8f898b0eaeaac7e4f 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -4,6 +4,12 @@ Release notes
 tig master
 ----------
 
+Improvements:
+
+ - Start rewrite of the revision graph renderer. Three modes are
+   supported UTF-8, ncurses line graphics, and ASCII. Also, enable
+   revision graph rendering by default.
+
 Bug fixes:
 
  - Fix opening of diffs when browsing branches.
diff --git a/graph.c b/graph.c
new file mode 100644 (file)
index 0000000..53aae2c
--- /dev/null
+++ b/graph.c
@@ -0,0 +1,410 @@
+/* Copyright (c) 2006-2010 Jonas Fonseca <fonseca@diku.dk>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+
+#include "tig.h"
+#include "graph.h"
+
+DEFINE_ALLOCATOR(realloc_graph_columns, struct graph_column, 32)
+DEFINE_ALLOCATOR(realloc_graph_symbols, struct graph_symbol, 1)
+
+static size_t get_free_graph_color(struct graph *graph)
+{
+       size_t i, free_color;
+
+       for (free_color = i = 0; i < ARRAY_SIZE(graph->colors); i++) {
+               if (graph->colors[i] < graph->colors[free_color])
+                       free_color = i;
+       }
+
+       graph->colors[free_color]++;
+       return free_color;
+}
+
+void
+done_graph(struct graph *graph)
+{
+       free(graph->row.columns);
+       free(graph->parents.columns);
+       memset(graph, 0, sizeof(*graph));
+}
+
+#define graph_column_has_commit(col) ((col)->id[0])
+
+static size_t
+graph_find_column_by_id(struct graph_row *row, const char *id)
+{
+       size_t free_column = row->size;
+       size_t i;
+
+       for (i = 0; i < row->size; i++) {
+               if (!graph_column_has_commit(&row->columns[i]))
+                       free_column = i;
+               else if (!strcmp(row->columns[i].id, id))
+                       return i;
+       }
+
+       return free_column;
+}
+
+static struct graph_column *
+graph_insert_column(struct graph *graph, struct graph_row *row, size_t pos, const char *id)
+{
+       struct graph_column *column;
+
+       if (!realloc_graph_columns(&row->columns, row->size, 1))
+               return NULL;
+
+       column = &row->columns[pos];
+       if (pos < row->size) {
+               memmove(column + 1, column, sizeof(*column) * (row->size - pos));
+       }
+
+       row->size++;
+       memset(column, 0, sizeof(*column));
+       string_copy_rev(column->id, id);
+       column->symbol.boundary = !!graph->is_boundary;
+
+       return column;
+}
+
+struct graph_column *
+graph_add_parent(struct graph *graph, const char *parent)
+{
+       return graph_insert_column(graph, &graph->parents, graph->parents.size, parent);
+}
+
+static bool
+graph_needs_expansion(struct graph *graph)
+{
+       if (graph->position + graph->parents.size > graph->row.size)
+               return TRUE;
+       return graph->parents.size > 1
+           && graph->position < 0
+           && graph->expanded < graph->parents.size;
+}
+
+static bool
+graph_expand(struct graph *graph)
+{
+       while (graph_needs_expansion(graph)) {
+               if (!graph_insert_column(graph, &graph->row, graph->position + graph->expanded, ""))
+                       return FALSE;
+               graph->expanded++;
+       }
+
+       return TRUE;
+}
+
+static bool
+graph_needs_collapsing(struct graph *graph)
+{
+       return graph->row.size > 1
+           && !graph_column_has_commit(&graph->row.columns[graph->row.size - 1]);
+}
+
+static bool
+graph_collapse(struct graph *graph)
+{
+       while (graph_needs_collapsing(graph)) {
+               graph->row.size--;
+       }
+
+       return TRUE;
+}
+
+static void
+graph_reorder_parents(struct graph *graph)
+{
+       struct graph_row *row = &graph->row;
+       struct graph_row *parents = &graph->parents;
+       int i;
+
+       if (parents->size == 1)
+               return;
+
+       for (i = 0; i < parents->size; i++) {
+               struct graph_column *column = &parents->columns[i];
+               size_t match = graph_find_column_by_id(row, column->id);
+
+               if (match < graph->position && graph_column_has_commit(&row->columns[match])) {
+                       //die("Reorder: %s -> %s", graph->commit->id, column->id);
+//                     row->columns[match].symbol.initial = 1;
+               }
+       }
+}
+
+static void
+graph_canvas_append_symbol(struct graph *graph, struct graph_symbol *symbol)
+{
+       struct graph_canvas *canvas = graph->canvas;
+
+       if (realloc_graph_symbols(&canvas->symbols, canvas->size, 1))
+               canvas->symbols[canvas->size++] = *symbol;
+}
+
+static bool
+graph_insert_parents(struct graph *graph)
+{
+       struct graph_row *row = &graph->row;
+       struct graph_row *parents = &graph->parents;
+       size_t orig_size = row->size;
+       bool branched = FALSE;
+       bool merge = parents->size > 1;
+       int pos;
+
+       assert(!graph_needs_expansion(graph));
+
+       for (pos = 0; pos < graph->position; pos++) {
+               struct graph_column *column = &row->columns[pos];
+               struct graph_symbol symbol = column->symbol;
+
+               if (graph_column_has_commit(column)) {
+                       size_t match = graph_find_column_by_id(parents, column->id);
+
+                       if (match < parents->size) {
+                               column->symbol.initial = 1;
+                       }
+
+                       symbol.branch = 1;
+               }
+               symbol.vbranch = !!branched;
+               if (!strcmp(column->id, graph->id)) {
+                       branched = TRUE;
+                       column->id[0] = 0;
+               }
+
+               graph_canvas_append_symbol(graph, &symbol);
+       }
+
+       for (; pos < graph->position + parents->size; pos++) {
+               struct graph_column *old = &row->columns[pos];
+               struct graph_column *new = &parents->columns[pos - graph->position];
+               struct graph_symbol symbol = old->symbol;
+
+               symbol.merge = !!merge;
+
+               if (pos == graph->position) {
+                       symbol.commit = 1;
+                       /*
+                       if (new->symbol->boundary) {
+                               symbol.boundary = 1;
+                       } else*/
+                       if (!graph_column_has_commit(new)) {
+                               symbol.initial = 1;
+                       }
+
+               } else if (!strcmp(old->id, new->id) && orig_size == row->size) {
+                       symbol.vbranch = 1;
+                       symbol.branch = 1;
+                       //symbol.merge = 1;
+
+               } else if (parents->size > 1) {
+                       symbol.merge = 1;
+                       symbol.vbranch = !(pos == graph->position + parents->size - 1);
+
+               } else if (graph_column_has_commit(old)) {
+                       symbol.branch = 1;
+               }
+
+               graph_canvas_append_symbol(graph, &symbol);
+               if (!graph_column_has_commit(old))
+                       new->symbol.color = get_free_graph_color(graph);
+               *old = *new;
+       }
+
+       for (; pos < row->size; pos++) {
+               bool too = !strcmp(row->columns[row->size - 1].id, graph->id);
+               struct graph_symbol symbol = row->columns[pos].symbol;
+
+               symbol.vbranch = !!too;
+               if (row->columns[pos].id[0]) {
+                       symbol.branch = 1;
+                       if (!strcmp(row->columns[pos].id, graph->id)) {
+                               symbol.branched = 1;
+                               if (too && pos != row->size - 1) {
+                                       symbol.vbranch = 1;
+                               } else {
+                                       symbol.vbranch = 0;
+                               }
+                               row->columns[pos].id[0] = 0;
+                       }
+               }
+               graph_canvas_append_symbol(graph, &symbol);
+       }
+
+       graph->parents.size = graph->expanded = graph->position = 0;
+
+       return TRUE;
+}
+
+bool
+graph_render_parents(struct graph *graph)
+{
+       if (!graph_expand(graph))
+               return FALSE;
+       graph_reorder_parents(graph);
+       graph_insert_parents(graph);
+       if (!graph_collapse(graph))
+               return FALSE;
+
+       return TRUE;
+}
+
+bool
+graph_add_commit(struct graph *graph, struct graph_canvas *canvas,
+                const char *id, const char *parents, bool is_boundary)
+{
+       graph->position = graph_find_column_by_id(&graph->row, id);
+       graph->id = id;
+       graph->canvas = canvas;
+       graph->is_boundary = is_boundary;
+
+       while ((parents = strchr(parents, ' '))) {
+               parents++;
+               if (!graph_add_parent(graph, parents))
+                       return FALSE;
+               graph->has_parents = TRUE;
+       }
+
+       if (graph->parents.size == 0 &&
+           !graph_add_parent(graph, ""))
+               return FALSE;
+
+       return TRUE;
+}
+
+const char *
+graph_symbol_to_utf8(struct graph_symbol *symbol)
+{
+       if (symbol->commit) {
+               if (symbol->boundary)
+                       return " ◯";
+               else if (symbol->initial)
+                       return " ◎";
+               else if (symbol->merge)
+                       return " ●";
+               return " ●";
+       }
+
+       if (symbol->merge) {
+               if (symbol->branch) {
+                       return "━┪";
+               }
+               if (symbol->vbranch)
+                       return "━┯";
+               return "━┑";
+       }
+
+       if (symbol->branch) {
+               if (symbol->branched) {
+                       if (symbol->vbranch)
+                               return "─┴";
+                       return "─┘";
+               }
+               if (symbol->vbranch)
+                       return "─│";
+               return " │";
+       }
+
+       if (symbol->vbranch)
+               return "──";
+
+       return "  ";
+}
+
+const chtype *
+graph_symbol_to_chtype(struct graph_symbol *symbol)
+{
+       static chtype graphics[2];
+
+       if (symbol->commit) {
+               graphics[0] = ' ';
+               if (symbol->boundary)
+                       graphics[1] = 'o';
+               else if (symbol->initial)
+                       graphics[1] = 'I';
+               else if (symbol->merge)
+                       graphics[1] = 'M';
+               else
+                       graphics[1] = 'o'; //ACS_DIAMOND; //'*';
+               return graphics;
+       }
+
+       if (symbol->merge) {
+               graphics[0] = ACS_HLINE;
+               if (symbol->branch)
+                       graphics[1] = ACS_RTEE;
+               else
+                       graphics[1] = ACS_URCORNER;
+               return graphics;
+       }
+
+       if (symbol->branch) {
+               graphics[0] = ACS_HLINE;
+               if (symbol->branched) {
+                       if (symbol->vbranch)
+                               graphics[1] = ACS_BTEE;
+                       else
+                               graphics[1] = ACS_LRCORNER;
+                       return graphics;
+               }
+
+               if (!symbol->vbranch)
+                       graphics[0] = ' ';
+               graphics[1] = ACS_VLINE;
+               return graphics;
+       }
+
+       if (symbol->vbranch) {
+               graphics[0] = graphics[1] = ACS_HLINE;
+       } else
+               graphics[0] = graphics[1] = ' ';
+
+       return graphics;
+}
+
+const char *
+graph_symbol_to_ascii(struct graph_symbol *symbol)
+{
+       if (symbol->commit) {
+               if (symbol->boundary)
+                       return " o";
+               else if (symbol->initial)
+                       return " I";
+               else if (symbol->merge)
+                       return " M";
+               return " *";
+       }
+
+       if (symbol->merge) {
+               if (symbol->branch)
+                       return "-+";
+               return "-.";
+       }
+
+       if (symbol->branch) {
+               if (symbol->branched) {
+                       if (symbol->vbranch)
+                               return "-+";
+                       return "-'";
+               }
+               if (symbol->vbranch)
+                       return "-|";
+               return " |";
+       }
+
+       if (symbol->vbranch)
+               return "--";
+
+       return "  ";
+}
diff --git a/graph.h b/graph.h
new file mode 100644 (file)
index 0000000..505c6c5
--- /dev/null
+++ b/graph.h
@@ -0,0 +1,73 @@
+
+/* Copyright (c) 2006-2010 Jonas Fonseca <fonseca@diku.dk>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+
+#ifndef TIG_GRAPH_H
+#define TIG_GRAPH_H
+
+#define GRAPH_COLORS   7
+
+struct graph_symbol {
+       unsigned int color:8;
+       unsigned int bold:1;
+
+       unsigned int commit:1;
+       unsigned int branch:1;
+
+       unsigned int boundary:1;
+       unsigned int initial:1;
+       unsigned int merge:1;
+
+       unsigned int vbranch:1;
+       unsigned int branched:1;
+};
+
+struct graph_canvas {
+       size_t size;                    /* The width of the graph array. */
+       struct graph_symbol *symbols;   /* Symbols for this row. */
+};
+
+struct graph_column {
+       struct graph_symbol symbol;
+       char id[SIZEOF_REV];            /* Parent SHA1 ID. */
+};
+
+struct graph_row {
+       size_t size;
+       struct graph_column *columns;
+};
+
+struct graph {
+       struct graph_row row;
+       struct graph_row parents;
+       size_t position;
+       size_t expanded;
+       const char *id;
+       struct graph_canvas *canvas;
+       size_t colors[GRAPH_COLORS];
+       bool has_parents;
+       bool is_boundary;
+};
+
+void done_graph(struct graph *graph);
+
+bool graph_render_parents(struct graph *graph);
+bool graph_add_commit(struct graph *graph, struct graph_canvas *canvas,
+                     const char *id, const char *parents, bool is_boundary);
+struct graph_column *graph_add_parent(struct graph *graph, const char *parent);
+
+const char *graph_symbol_to_ascii(struct graph_symbol *symbol);
+const char *graph_symbol_to_utf8(struct graph_symbol *symbol);
+const chtype *graph_symbol_to_chtype(struct graph_symbol *symbol);
+
+#endif
diff --git a/io.c b/io.c
index 685ffb6cfeb77d7573156f4cec6332fc0cabdce7..edd6e85cfe110a45b1ee76a05ca476ccc8ef755b 100644 (file)
--- a/io.c
+++ b/io.c
@@ -276,7 +276,7 @@ io_strerror(struct io *io)
 }
 
 bool
-io_can_read(struct io *io)
+io_can_read(struct io *io, bool can_block)
 {
        struct timeval tv = { 0, 500 };
        fd_set fds;
@@ -284,7 +284,7 @@ io_can_read(struct io *io)
        FD_ZERO(&fds);
        FD_SET(io->pipe, &fds);
 
-       return select(io->pipe + 1, &fds, NULL, NULL, &tv) > 0;
+       return select(io->pipe + 1, &fds, NULL, NULL, can_block ? NULL : &tv) > 0;
 }
 
 ssize_t
diff --git a/io.h b/io.h
index 9a68a5ec336bac8fbcbb96a5e84f9971a18a227d..8a766ca94c467e5f35012af11290eb2590978b35 100644 (file)
--- a/io.h
+++ b/io.h
@@ -62,7 +62,7 @@ bool io_run_append(const char **argv, int fd);
 bool io_eof(struct io *io);
 int io_error(struct io *io);
 char * io_strerror(struct io *io);
-bool io_can_read(struct io *io);
+bool io_can_read(struct io *io, bool can_block);
 ssize_t io_read(struct io *io, void *buf, size_t bufsize);
 char * io_get(struct io *io, int c, bool can_read);
 bool io_write(struct io *io, const void *buf, size_t bufsize);
index ffea349d2537fa66f80f914c4e57473db63f64b1..c9b18932fd98705bff238aa1af9b897094f6a16d 100644 (file)
@@ -386,6 +386,7 @@ Misc
 |D     |Toggle date display on/off/short/relative/local.
 |A     |Toggle author display on/off/abbreviated.
 |g     |Toggle revision graph visualization on/off.
+|~     |Toggle (line) graphics mode
 |F     |Toggle reference display on/off (tag and branch names).
 |:     |Open prompt. This allows you to specify what git command
         to run. Example `:log -p`. You can also use this to jump
diff --git a/test-graph.c b/test-graph.c
new file mode 100644 (file)
index 0000000..48650e6
--- /dev/null
@@ -0,0 +1,106 @@
+/* Copyright (c) 2006-2010 Jonas Fonseca <fonseca@diku.dk>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * Example usage:
+ *
+ *     # git log --pretty=raw --parents | ./test-graph
+ *     # git log --pretty=raw --parents | ./test-graph --ascii
+ */
+
+#include "tig.h"
+#include "io.h"
+#include "graph.h"
+
+static void __NORETURN
+die(const char *err, ...)
+{
+       va_list args;
+
+       endwin();
+
+       va_start(args, err);
+       fputs("tig: ", stderr);
+       vfprintf(stderr, err, args);
+       fputs("\n", stderr);
+       va_end(args);
+
+       exit(1);
+}
+
+struct commit {
+       char id[SIZEOF_REV];
+       struct graph_canvas canvas;
+};
+
+DEFINE_ALLOCATOR(realloc_commits, struct commit *, 8)
+
+int
+main(int argc, const char *argv[])
+{
+       struct graph graph = { };
+       struct io io = { };
+       char *line;
+       struct commit **commits = NULL;
+       size_t ncommits = 0;
+       struct commit *commit = NULL;
+       bool is_boundary;
+       const char *(*graph_fn)(struct graph_symbol *) = graph_symbol_to_utf8;
+
+       if (argc > 1 && !strcmp(argv[1], "--ascii"))
+               graph_fn = graph_symbol_to_ascii;
+
+       if (!io_open(&io, ""))
+               die("IO");
+
+       while (!io_eof(&io)) {
+               bool can_read = io_can_read(&io, TRUE);
+
+               for (; (line = io_get(&io, '\n', can_read)); can_read = FALSE) {
+                       if (!prefixcmp(line, "commit ")) {
+                               line += STRING_SIZE("commit ");
+                               is_boundary = *line == '-';
+
+                               if (is_boundary)
+                                       line++;
+
+                               if (!realloc_commits(&commits, ncommits, 1))
+                                       die("Commits");
+
+                               commit = calloc(1, sizeof(*commit));
+                               if (!commit)
+                                       die("Commit");
+                               commits[ncommits++] = commit;
+                               string_copy_rev(commit->id, line);
+                               graph_add_commit(&graph, &commit->canvas, commit->id, line, is_boundary);
+                               graph_render_parents(&graph);
+
+                       } else if (!prefixcmp(line, "    ")) {
+                               int i;
+
+                               if (!commit)
+                                       continue;
+
+                               for (i = 0; i < commit->canvas.size; i++) {
+                                       struct graph_symbol *symbol = &commit->canvas.symbols[i];
+                                       const char *chars = graph_fn(symbol);
+
+                                       printf("%s", chars + (i == 0));
+                               }
+                               printf("%s\n", line + 3);
+
+                               commit = NULL;
+                       }
+               }
+       }
+
+       return 0;
+}
diff --git a/tig.c b/tig.c
index 6ad9c57953182e4d54d4ad3fdca66d61e8dfc4b3..2cf4360ebff7ec72d1bf66233441dedbe68c819b 100644 (file)
--- a/tig.c
+++ b/tig.c
@@ -1,4 +1,4 @@
-/* Copyright (c) 2006-2010 Jonas Fonseca <fonseca@diku.dk>
+/* Copyrsght (c) 2006-2010 Jonas Fonseca <fonseca@diku.dk>
  *
  * This program is free software; you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -13,6 +13,7 @@
 
 #include "tig.h"
 #include "io.h"
+#include "graph.h"
 
 static void __NORETURN die(const char *err, ...);
 static void warn(const char *msg, ...);
@@ -60,6 +61,20 @@ struct menu_item {
 
 static bool prompt_menu(const char *prompt, const struct menu_item *items, int *selected);
 
+enum graphic {
+       GRAPHIC_ASCII = 0,
+       GRAPHIC_DEFAULT,
+       GRAPHIC_UTF8
+};
+
+static const struct enum_map graphic_map[] = {
+#define GRAPHIC_(name) ENUM_MAP(#name, GRAPHIC_##name)
+       GRAPHIC_(ASCII),
+       GRAPHIC_(DEFAULT),
+       GRAPHIC_(UTF8)
+#undef GRAPHIC_
+};
+
 #define DATE_INFO \
        DATE_(NO), \
        DATE_(DEFAULT), \
@@ -258,6 +273,7 @@ get_author_initials(const char *author)
        REQ_(TOGGLE_DATE,       "Toggle date display"), \
        REQ_(TOGGLE_AUTHOR,     "Toggle author display"), \
        REQ_(TOGGLE_REV_GRAPH,  "Toggle revision graph visualization"), \
+       REQ_(TOGGLE_GRAPHIC,    "Toggle (line) graphics mode"), \
        REQ_(TOGGLE_REFS,       "Toggle reference display (tags/branches)"), \
        REQ_(TOGGLE_SORT_ORDER, "Toggle ascending/descending sort order"), \
        REQ_(TOGGLE_SORT_FIELD, "Toggle field to sort by"), \
@@ -319,11 +335,11 @@ get_request(const char *name)
  */
 
 /* Option and state variables. */
+static enum graphic opt_line_graphics  = GRAPHIC_DEFAULT;
 static enum date opt_date              = DATE_DEFAULT;
 static enum author opt_author          = AUTHOR_DEFAULT;
+static bool opt_rev_graph              = TRUE;
 static bool opt_line_number            = FALSE;
-static bool opt_line_graphics          = TRUE;
-static bool opt_rev_graph              = FALSE;
 static bool opt_show_refs              = TRUE;
 static bool opt_untracked_dirs_content = TRUE;
 static int opt_num_interval            = 5;
@@ -417,7 +433,15 @@ LINE(STAT_UNSTAGED,"",                     COLOR_MAGENTA,  COLOR_DEFAULT,  0), \
 LINE(STAT_UNTRACKED,"",                        COLOR_MAGENTA,  COLOR_DEFAULT,  0), \
 LINE(HELP_KEYMAP,  "",                 COLOR_CYAN,     COLOR_DEFAULT,  0), \
 LINE(HELP_GROUP,   "",                 COLOR_BLUE,     COLOR_DEFAULT,  0), \
-LINE(BLAME_ID,     "",                 COLOR_MAGENTA,  COLOR_DEFAULT,  0)
+LINE(BLAME_ID,     "",                 COLOR_MAGENTA,  COLOR_DEFAULT,  0), \
+LINE(GRAPH_LINE_0, "",                 COLOR_MAGENTA,  COLOR_DEFAULT,  0), \
+LINE(GRAPH_LINE_1, "",                 COLOR_YELLOW,   COLOR_DEFAULT,  0), \
+LINE(GRAPH_LINE_2, "",                 COLOR_CYAN,     COLOR_DEFAULT,  0), \
+LINE(GRAPH_LINE_3, "",                 COLOR_GREEN,    COLOR_DEFAULT,  0), \
+LINE(GRAPH_LINE_4, "",                 COLOR_DEFAULT,  COLOR_DEFAULT,  0), \
+LINE(GRAPH_LINE_5, "",                 COLOR_WHITE,    COLOR_DEFAULT,  0), \
+LINE(GRAPH_LINE_6, "",                 COLOR_RED,      COLOR_DEFAULT,  0), \
+LINE(GRAPH_COMMIT, "",                 COLOR_BLUE,     COLOR_DEFAULT,  0)
 
 enum line_type {
 #define LINE(type, line, fg, bg, attr) \
@@ -589,6 +613,7 @@ static struct keybinding default_keybindings[] = {
        { 'D',          REQ_TOGGLE_DATE },
        { 'A',          REQ_TOGGLE_AUTHOR },
        { 'g',          REQ_TOGGLE_REV_GRAPH },
+       { '~',          REQ_TOGGLE_GRAPHIC },
        { 'F',          REQ_TOGGLE_REFS },
        { 'I',          REQ_TOGGLE_SORT_ORDER },
        { 'i',          REQ_TOGGLE_SORT_FIELD },
@@ -1111,7 +1136,7 @@ option_set_command(int argc, const char *argv[])
                return parse_bool(&opt_line_number, argv[2]);
 
        if (!strcmp(argv[0], "line-graphics"))
-               return parse_bool(&opt_line_graphics, argv[2]);
+               return parse_enum(&opt_line_graphics, argv[2], graphic_map);
 
        if (!strcmp(argv[0], "line-number-interval"))
                return parse_int(&opt_num_interval, argv[2], 1, 1024);
@@ -1552,7 +1577,7 @@ draw_text(struct view *view, enum line_type type, const char *string)
 }
 
 static bool
-draw_graphic(struct view *view, enum line_type type, chtype graphic[], size_t size)
+draw_graphic(struct view *view, enum line_type type, const chtype graphic[], size_t size, bool separator)
 {
        size_t skip = view->yoffset > view->col ? view->yoffset - view->col : 0;
        int max = view->width + view->yoffset - view->col;
@@ -1568,9 +1593,11 @@ draw_graphic(struct view *view, enum line_type type, chtype graphic[], size_t si
                waddch(view->win, graphic[i]);
 
        view->col += size;
-       if (size < max && skip <= size)
-               waddch(view->win, ' ');
-       view->col++;
+       if (separator) {
+               if (size < max && skip <= size)
+                       waddch(view->win, ' ');
+               view->col++;
+       }
 
        return view->width + view->yoffset <= view->col;
 }
@@ -1654,7 +1681,7 @@ draw_lineno(struct view *view, unsigned int lineno)
                view->col += draw_chars(view, LINE_LINE_NUMBER, text, max, TRUE);
        else
                view->col += draw_space(view, LINE_LINE_NUMBER, max, digits3);
-       return draw_graphic(view, LINE_DEFAULT, &separator, 1);
+       return draw_graphic(view, LINE_DEFAULT, &separator, 1, TRUE);
 }
 
 static bool
@@ -1873,6 +1900,7 @@ redraw_display(bool clear)
        TOGGLE_(LINENO,    '.', "line numbers",      &opt_line_number, NULL) \
        TOGGLE_(DATE,      'D', "dates",             &opt_date,   date_map) \
        TOGGLE_(AUTHOR,    'A', "author names",      &opt_author, author_map) \
+       TOGGLE_(GRAPHIC,   '~', "graphics",          &opt_line_graphics, graphic_map) \
        TOGGLE_(REV_GRAPH, 'g', "revision graph",    &opt_rev_graph, NULL) \
        TOGGLE_(REFS,      'F', "reference display", &opt_show_refs, NULL)
 
@@ -2514,7 +2542,7 @@ update_view(struct view *view)
        if (!view->pipe)
                return TRUE;
 
-       if (!io_can_read(view->pipe)) {
+       if (!io_can_read(view->pipe, FALSE)) {
                if (view->lines == 0 && view_is_displayed(view)) {
                        time_t secs = time(NULL) - view->start_time;
 
@@ -2936,6 +2964,7 @@ view_driver(struct view *view, enum request request)
        case REQ_TOGGLE_LINENO:
        case REQ_TOGGLE_DATE:
        case REQ_TOGGLE_AUTHOR:
+       case REQ_TOGGLE_GRAPHIC:
        case REQ_TOGGLE_REV_GRAPH:
        case REQ_TOGGLE_REFS:
                toggle_option(request);
@@ -5656,224 +5685,84 @@ static struct view_ops stage_ops = {
  * Revision graph
  */
 
-struct commit {
-       char id[SIZEOF_REV];            /* SHA1 ID. */
-       char title[128];                /* First line of the commit message. */
-       const char *author;             /* Author of the commit. */
-       struct time time;               /* Date from the author ident. */
-       struct ref_list *refs;          /* Repository references. */
-       chtype graph[SIZEOF_REVGRAPH];  /* Ancestry chain graphics. */
-       size_t graph_size;              /* The width of the graph array. */
-       bool has_parents;               /* Rewritten --parents seen. */
+static const enum line_type graph_colors[] = {
+       LINE_GRAPH_LINE_0,
+       LINE_GRAPH_LINE_1,
+       LINE_GRAPH_LINE_2,
+       LINE_GRAPH_LINE_3,
+       LINE_GRAPH_LINE_4,
+       LINE_GRAPH_LINE_5,
+       LINE_GRAPH_LINE_6,
 };
 
-/* Size of rev graph with no  "padding" columns */
-#define SIZEOF_REVITEMS        (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
-
-struct rev_graph {
-       struct rev_graph *prev, *next, *parents;
-       char rev[SIZEOF_REVITEMS][SIZEOF_REV];
-       size_t size;
-       struct commit *commit;
-       size_t pos;
-       unsigned int boundary:1;
-};
-
-/* Parents of the commit being visualized. */
-static struct rev_graph graph_parents[4];
-
-/* The current stack of revisions on the graph. */
-static struct rev_graph graph_stacks[4] = {
-       { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
-       { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
-       { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
-       { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
-};
-
-static inline bool
-graph_parent_is_merge(struct rev_graph *graph)
+static enum line_type get_graph_color(struct graph_symbol *symbol)
 {
-       return graph->parents->size > 1;
+       if (symbol->commit)
+               return LINE_GRAPH_COMMIT;
+       assert(symbol->color < ARRAY_SIZE(graph_colors));
+       return graph_colors[symbol->color];
 }
 
-static inline void
-append_to_rev_graph(struct rev_graph *graph, chtype symbol)
+static bool
+draw_graph_utf8(struct view *view, struct graph_symbol *symbol, enum line_type color, bool first)
 {
-       struct commit *commit = graph->commit;
-
-       if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
-               commit->graph[commit->graph_size++] = symbol;
-}
+       const char *chars = graph_symbol_to_utf8(symbol);
 
-static void
-clear_rev_graph(struct rev_graph *graph)
-{
-       graph->boundary = 0;
-       graph->size = graph->pos = 0;
-       graph->commit = NULL;
-       memset(graph->parents, 0, sizeof(*graph->parents));
+       return draw_text(view, color, chars + !!first); 
 }
 
-static void
-done_rev_graph(struct rev_graph *graph)
+static bool
+draw_graph_ascii(struct view *view, struct graph_symbol *symbol, enum line_type color, bool first)
 {
-       if (graph_parent_is_merge(graph) &&
-           graph->pos < graph->size - 1 &&
-           graph->next->size == graph->size + graph->parents->size - 1) {
-               size_t i = graph->pos + graph->parents->size - 1;
-
-               graph->commit->graph_size = i * 2;
-               while (i < graph->next->size - 1) {
-                       append_to_rev_graph(graph, ' ');
-                       append_to_rev_graph(graph, '\\');
-                       i++;
-               }
-       }
+       const char *chars = graph_symbol_to_ascii(symbol);
 
-       clear_rev_graph(graph);
+       return draw_text(view, color, chars + !!first); 
 }
 
-static void
-push_rev_graph(struct rev_graph *graph, const char *parent)
+static bool
+draw_graph_chtype(struct view *view, struct graph_symbol *symbol, enum line_type color, bool first)
 {
-       int i;
+       const chtype *chars = graph_symbol_to_chtype(symbol);
 
-       /* "Collapse" duplicate parents lines.
-        *
-        * FIXME: This needs to also update update the drawn graph but
-        * for now it just serves as a method for pruning graph lines. */
-       for (i = 0; i < graph->size; i++)
-               if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
-                       return;
-
-       if (graph->size < SIZEOF_REVITEMS) {
-               string_copy_rev(graph->rev[graph->size++], parent);
-       }
+       return draw_graphic(view, color, chars + !!first, 2 - !!first, FALSE); 
 }
 
-static chtype
-get_rev_graph_symbol(struct rev_graph *graph)
-{
-       chtype symbol;
-
-       if (graph->boundary)
-               symbol = REVGRAPH_BOUND;
-       else if (graph->parents->size == 0)
-               symbol = REVGRAPH_INIT;
-       else if (graph_parent_is_merge(graph))
-               symbol = REVGRAPH_MERGE;
-       else if (graph->pos >= graph->size)
-               symbol = REVGRAPH_BRANCH;
-       else
-               symbol = REVGRAPH_COMMIT;
+typedef bool (*draw_graph_fn)(struct view *, struct graph_symbol *, enum line_type, bool);
 
-       return symbol;
-}
-
-static void
-draw_rev_graph(struct rev_graph *graph)
+static bool draw_graph(struct view *view, struct graph_canvas *canvas)
 {
-       struct rev_filler {
-               chtype separator, line;
+       static const draw_graph_fn fns[] = {
+               draw_graph_ascii,
+               draw_graph_chtype,
+               draw_graph_utf8
        };
-       enum { DEFAULT, RSHARP, RDIAG, LDIAG };
-       static struct rev_filler fillers[] = {
-               { ' ',  '|' },
-               { '`',  '.' },
-               { '\'', ' ' },
-               { '/',  ' ' },
-       };
-       chtype symbol = get_rev_graph_symbol(graph);
-       struct rev_filler *filler;
-       size_t i;
-
-       fillers[DEFAULT].line = opt_line_graphics ? ACS_VLINE : '|';
-       filler = &fillers[DEFAULT];
-
-       for (i = 0; i < graph->pos; i++) {
-               append_to_rev_graph(graph, filler->line);
-               if (graph_parent_is_merge(graph->prev) &&
-                   graph->prev->pos == i)
-                       filler = &fillers[RSHARP];
-
-               append_to_rev_graph(graph, filler->separator);
-       }
-
-       /* Place the symbol for this revision. */
-       append_to_rev_graph(graph, symbol);
-
-       if (graph->prev->size > graph->size)
-               filler = &fillers[RDIAG];
-       else
-               filler = &fillers[DEFAULT];
-
-       i++;
-
-       for (; i < graph->size; i++) {
-               append_to_rev_graph(graph, filler->separator);
-               append_to_rev_graph(graph, filler->line);
-               if (graph_parent_is_merge(graph->prev) &&
-                   i < graph->prev->pos + graph->parents->size)
-                       filler = &fillers[RSHARP];
-               if (graph->prev->size > graph->size)
-                       filler = &fillers[LDIAG];
-       }
-
-       if (graph->prev->size > graph->size) {
-               append_to_rev_graph(graph, filler->separator);
-               if (filler->line != ' ')
-                       append_to_rev_graph(graph, filler->line);
-       }
-}
+       draw_graph_fn fn = fns[opt_line_graphics];
+       int i;
 
-/* Prepare the next rev graph */
-static void
-prepare_rev_graph(struct rev_graph *graph)
-{
-       size_t i;
+       for (i = 0; i < canvas->size; i++) {
+               struct graph_symbol *symbol = &canvas->symbols[i];
+               enum line_type color = get_graph_color(symbol);
 
-       /* First, traverse all lines of revisions up to the active one. */
-       for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
-               if (!strcmp(graph->rev[graph->pos], graph->commit->id))
-                       break;
-
-               push_rev_graph(graph->next, graph->rev[graph->pos]);
+               if (fn(view, symbol, color, i == 0))
+                       return TRUE;
        }
 
-       /* Interleave the new revision parent(s). */
-       for (i = 0; !graph->boundary && i < graph->parents->size; i++)
-               push_rev_graph(graph->next, graph->parents->rev[i]);
-
-       /* Lastly, put any remaining revisions. */
-       for (i = graph->pos + 1; i < graph->size; i++)
-               push_rev_graph(graph->next, graph->rev[i]);
+       return draw_text(view, LINE_MAIN_REVGRAPH, " ");
 }
 
-static void
-update_rev_graph(struct view *view, struct rev_graph *graph)
-{
-       /* If this is the finalizing update ... */
-       if (graph->commit)
-               prepare_rev_graph(graph);
-
-       /* Graph visualization needs a one rev look-ahead,
-        * so the first update doesn't visualize anything. */
-       if (!graph->prev->commit)
-               return;
-
-       if (view->lines > 2)
-               view->line[view->lines - 3].dirty = 1;
-       if (view->lines > 1)
-               view->line[view->lines - 2].dirty = 1;
-       draw_rev_graph(graph->prev);
-       done_rev_graph(graph->prev->prev);
-}
-
-
 /*
  * Main view backend
  */
 
+struct commit {
+       char id[SIZEOF_REV];            /* SHA1 ID. */
+       char title[128];                /* First line of the commit message. */
+       const char *author;             /* Author of the commit. */
+       struct time time;               /* Date from the author ident. */
+       struct ref_list *refs;          /* Repository references. */
+       struct graph_canvas graph;      /* Ancestry chain graphics. */
+};
+
 static const char *main_argv[SIZEOF_ARG] = {
        "git", "log", "--no-color", "--pretty=raw", "--parents",
                "--topo-order", "%(diffargs)", "%(revargs)",
@@ -5894,8 +5783,7 @@ main_draw(struct view *view, struct line *line, unsigned int lineno)
        if (opt_author && draw_author(view, commit->author))
                return TRUE;
 
-       if (opt_rev_graph && commit->graph_size &&
-           draw_graphic(view, LINE_MAIN_REVGRAPH, commit->graph, commit->graph_size))
+       if (opt_rev_graph && draw_graph(view, &commit->graph))
                return TRUE;
 
        if (opt_show_refs && commit->refs) {
@@ -5936,13 +5824,11 @@ main_draw(struct view *view, struct line *line, unsigned int lineno)
 static bool
 main_read(struct view *view, char *line)
 {
-       static struct rev_graph *graph = graph_stacks;
+       static struct graph graph;
        enum line_type type;
        struct commit *commit;
 
        if (!line) {
-               int i;
-
                if (!view->lines && !view->prev)
                        die("No revisions match the given arguments.");
                if (view->lines > 0) {
@@ -5951,38 +5837,30 @@ main_read(struct view *view, char *line)
                        if (!commit->author) {
                                view->lines--;
                                free(commit);
-                               graph->commit = NULL;
                        }
                }
-               update_rev_graph(view, graph);
 
-               for (i = 0; i < ARRAY_SIZE(graph_stacks); i++)
-                       clear_rev_graph(&graph_stacks[i]);
+               done_graph(&graph);
                return TRUE;
        }
 
        type = get_line_type(line);
        if (type == LINE_COMMIT) {
+               bool is_boundary;
+
                commit = calloc(1, sizeof(struct commit));
                if (!commit)
                        return FALSE;
 
                line += STRING_SIZE("commit ");
-               if (*line == '-') {
-                       graph->boundary = 1;
+               is_boundary = *line == '-';
+               if (is_boundary)
                        line++;
-               }
 
                string_copy_rev(commit->id, line);
                commit->refs = get_ref_list(commit->id);
-               graph->commit = commit;
                add_line_data(view, commit, LINE_MAIN_COMMIT);
-
-               while ((line = strchr(line, ' '))) {
-                       line++;
-                       push_rev_graph(graph->parents, line);
-                       commit->has_parents = TRUE;
-               }
+               graph_add_commit(&graph, &commit->graph, commit->id, line, is_boundary);
                return TRUE;
        }
 
@@ -5992,16 +5870,14 @@ main_read(struct view *view, char *line)
 
        switch (type) {
        case LINE_PARENT:
-               if (commit->has_parents)
-                       break;
-               push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
+               if (!graph.has_parents)
+                       graph_add_parent(&graph, line + STRING_SIZE("parent "));
                break;
 
        case LINE_AUTHOR:
                parse_author_line(line + STRING_SIZE("author "),
                                  &commit->author, &commit->time);
-               update_rev_graph(view, graph);
-               graph = graph->next;
+               graph_render_parents(&graph);
                break;
 
        default:
diff --git a/tig.h b/tig.h
index e6cd9a19e5c8ffc312cbea2bc5493f7cef9a34d5..85b639bea72a0649869e27383bb056bb16b26936 100644 (file)
--- a/tig.h
+++ b/tig.h
 #define SIZEOF_REV     41      /* Holds a SHA-1 and an ending NUL. */
 #define SIZEOF_ARG     32      /* Default argument array size. */
 
-/* Revision graph */
-
-#define REVGRAPH_INIT  'I'
-#define REVGRAPH_MERGE 'M'
-#define REVGRAPH_BRANCH        '+'
-#define REVGRAPH_COMMIT        '*'
-#define REVGRAPH_BOUND '^'
-
-#define SIZEOF_REVGRAPH        19      /* Size of revision ancestry graphics. */
-
 /* This color name can be used to refer to the default term colors. */
 #define COLOR_DEFAULT  (-1)
 
index 2d9afddd2dff47198d85112cfede22d164c78a46..b373ef2595f22819de4c24a3d71fb43f54d45f0c 100644 (file)
@@ -87,10 +87,14 @@ Variables
 The following variables can be set:
 
 'show-rev-graph' (bool)::
+
+       Whether to show revision graph in the main view on start-up.
+       Can be toggled. See also line-graphics options.
+
 'show-refs' (bool)::
 
-       Whether to show revision graph, and references (branches, tags, and
-       remotes) in the main view on start-up. Can all be toggled.
+       Whether to show references (branches, tags, and remotes) in the main
+       view on start-up. Can be toggled.
 
 'show-author' (mixed) ["abbreviated" | "default" | bool]::
 
@@ -108,9 +112,9 @@ The following variables can be set:
        Width of the author column. When set to 5 or below, the author name
        will be abbreviated to the author's initials.
 
-'line-graphics' (bool)::
+'line-graphics' (mixed) [ "ascii" | "default" | "utf-8" | bool]::
 
-       Whether to use graphic characters for line drawing.
+       What type of character graphics for line drawing.
 
 'line-number-interval' (int)::
 
@@ -337,6 +341,7 @@ put in either the .gitconfig or .git/config file:
 |toggle-date           |Toggle date display
 |toggle-author         |Toggle author display
 |toggle-rev-graph      |Toggle revision graph visualization
+|toggle-graphic                |Toggle (line) graphics mode
 |toggle-refs           |Toggle reference display
 |edit                  |Open in editor
 |none                  |Do nothing