From: Jonas Fonseca Date: Fri, 11 Dec 2009 22:05:23 +0000 (-0500) Subject: Rewrite the revision graph renderer X-Git-Url: https://git.tokkee.org/?a=commitdiff_plain;h=607cfb11cd9b6e2beaa36158d77842f8aac7fd67;p=tig.git Rewrite the revision graph renderer 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. --- diff --git a/.gitignore b/.gitignore index bd0c3a9..d8dcc0c 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ tig.1 tig.spec tigrc.5 tigmanual.7 +test-graph *.html *.o *.xml diff --git a/Makefile b/Makefile index 78131f5..0443482 100644 --- 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 0926f66..9a4439d 100644 --- 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 index 0000000..53aae2c --- /dev/null +++ b/graph.c @@ -0,0 +1,410 @@ +/* Copyright (c) 2006-2010 Jonas Fonseca + * + * 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 index 0000000..505c6c5 --- /dev/null +++ b/graph.h @@ -0,0 +1,73 @@ + +/* Copyright (c) 2006-2010 Jonas Fonseca + * + * 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 685ffb6..edd6e85 100644 --- 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 9a68a5e..8a766ca 100644 --- 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); diff --git a/manual.txt b/manual.txt index ffea349..c9b1893 100644 --- a/manual.txt +++ b/manual.txt @@ -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 index 0000000..48650e6 --- /dev/null +++ b/test-graph.c @@ -0,0 +1,106 @@ +/* Copyright (c) 2006-2010 Jonas Fonseca + * + * 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 6ad9c57..2cf4360 100644 --- a/tig.c +++ b/tig.c @@ -1,4 +1,4 @@ -/* Copyright (c) 2006-2010 Jonas Fonseca +/* Copyrsght (c) 2006-2010 Jonas Fonseca * * 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 e6cd9a1..85b639b 100644 --- a/tig.h +++ b/tig.h @@ -76,16 +76,6 @@ #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) diff --git a/tigrc.5.txt b/tigrc.5.txt index 2d9afdd..b373ef2 100644 --- a/tigrc.5.txt +++ b/tigrc.5.txt @@ -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