+ if (!req_info[i].request) {
+ add_line_text(view, "", LINE_DEFAULT);
+ add_line_text(view, req_info[i].help, LINE_DEFAULT);
+ continue;
+ }
+
+ key = get_key(req_info[i].request);
+ if (!string_format(buf, " %-25s %s", key, req_info[i].help))
+ continue;
+
+ add_line_text(view, buf, LINE_DEFAULT);
+ }
+
+ return TRUE;
+}
+
+static struct view_ops help_ops = {
+ "line",
+ help_open,
+ NULL,
+ pager_draw,
+ pager_enter,
+ pager_grep,
+ pager_select,
+};
+
+
+/*
+ * Tree backend
+ */
+
+/* Parse output from git-ls-tree(1):
+ *
+ * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
+ * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
+ * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
+ * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
+ */
+
+#define SIZEOF_TREE_ATTR \
+ STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
+
+#define TREE_UP_FORMAT "040000 tree %s\t.."
+
+static int
+tree_compare_entry(enum line_type type1, char *name1,
+ enum line_type type2, char *name2)
+{
+ if (type1 != type2) {
+ if (type1 == LINE_TREE_DIR)
+ return -1;
+ return 1;
+ }
+
+ return strcmp(name1, name2);
+}
+
+static bool
+tree_read(struct view *view, char *text)
+{
+ size_t textlen = text ? strlen(text) : 0;
+ char buf[SIZEOF_STR];
+ unsigned long pos;
+ enum line_type type;
+ bool first_read = view->lines == 0;
+
+ if (textlen <= SIZEOF_TREE_ATTR)
+ return FALSE;
+
+ type = text[STRING_SIZE("100644 ")] == 't'
+ ? LINE_TREE_DIR : LINE_TREE_FILE;
+
+ if (first_read) {
+ /* Add path info line */
+ if (!string_format(buf, "Directory path /%s", opt_path) ||
+ !realloc_lines(view, view->line_size + 1) ||
+ !add_line_text(view, buf, LINE_DEFAULT))
+ return FALSE;
+
+ /* Insert "link" to parent directory. */
+ if (*opt_path) {
+ if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
+ !realloc_lines(view, view->line_size + 1) ||
+ !add_line_text(view, buf, LINE_TREE_DIR))
+ return FALSE;
+ }
+ }
+
+ /* Strip the path part ... */
+ if (*opt_path) {
+ size_t pathlen = textlen - SIZEOF_TREE_ATTR;
+ size_t striplen = strlen(opt_path);
+ char *path = text + SIZEOF_TREE_ATTR;
+
+ if (pathlen > striplen)
+ memmove(path, path + striplen,
+ pathlen - striplen + 1);
+ }
+
+ /* Skip "Directory ..." and ".." line. */
+ for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
+ struct line *line = &view->line[pos];
+ char *path1 = ((char *) line->data) + SIZEOF_TREE_ATTR;
+ char *path2 = text + SIZEOF_TREE_ATTR;
+ int cmp = tree_compare_entry(line->type, path1, type, path2);
+
+ if (cmp <= 0)
+ continue;
+
+ text = strdup(text);
+ if (!text)
+ return FALSE;
+
+ if (view->lines > pos)
+ memmove(&view->line[pos + 1], &view->line[pos],
+ (view->lines - pos) * sizeof(*line));
+
+ line = &view->line[pos];
+ line->data = text;
+ line->type = type;
+ view->lines++;
+ return TRUE;
+ }
+
+ if (!add_line_text(view, text, type))
+ return FALSE;
+
+ /* Move the current line to the first tree entry. */
+ if (first_read)
+ view->lineno++;
+
+ return TRUE;
+}
+
+static bool
+tree_enter(struct view *view, struct line *line)
+{
+ enum open_flags flags;
+ enum request request;
+
+ switch (line->type) {
+ case LINE_TREE_DIR:
+ /* Depending on whether it is a subdir or parent (updir?) link
+ * mangle the path buffer. */
+ if (line == &view->line[1] && *opt_path) {
+ size_t path_len = strlen(opt_path);
+ char *dirsep = opt_path + path_len - 1;
+
+ while (dirsep > opt_path && dirsep[-1] != '/')
+ dirsep--;
+
+ dirsep[0] = 0;
+
+ } else {
+ size_t pathlen = strlen(opt_path);
+ size_t origlen = pathlen;
+ char *data = line->data;
+ char *basename = data + SIZEOF_TREE_ATTR;
+
+ if (!string_format_from(opt_path, &pathlen, "%s/", basename)) {
+ opt_path[origlen] = 0;
+ return TRUE;
+ }
+ }
+
+ /* Trees and subtrees share the same ID, so they are not not
+ * unique like blobs. */
+ flags = OPEN_RELOAD;
+ request = REQ_VIEW_TREE;
+ break;
+
+ case LINE_TREE_FILE:
+ flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
+ request = REQ_VIEW_BLOB;
+ break;
+
+ default:
+ return TRUE;
+ }
+
+ open_view(view, request, flags);
+
+ return TRUE;
+}
+
+static void
+tree_select(struct view *view, struct line *line)
+{
+ char *text = line->data + STRING_SIZE("100644 blob ");
+
+ if (line->type == LINE_TREE_FILE) {
+ string_copy_rev(ref_blob, text);
+
+ } else if (line->type != LINE_TREE_DIR) {
+ return;
+ }
+
+ string_copy_rev(view->ref, text);
+}
+
+static struct view_ops tree_ops = {
+ "file",
+ NULL,
+ tree_read,
+ pager_draw,
+ tree_enter,
+ pager_grep,
+ tree_select,
+};
+
+static bool
+blob_read(struct view *view, char *line)
+{
+ return add_line_text(view, line, LINE_DEFAULT);
+}
+
+static struct view_ops blob_ops = {
+ "line",
+ NULL,
+ blob_read,
+ pager_draw,
+ pager_enter,
+ pager_grep,
+ pager_select,
+};
+
+
+/*
+ * Status backend
+ */
+
+struct status {
+ char status;
+ struct {
+ mode_t mode;
+ char rev[SIZEOF_REV];
+ } old;
+ struct {
+ mode_t mode;
+ char rev[SIZEOF_REV];
+ } new;
+ char name[SIZEOF_STR];
+};
+
+/* Get fields from the diff line:
+ * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
+ */
+static inline bool
+status_get_diff(struct status *file, char *buf, size_t bufsize)
+{
+ char *old_mode = buf + 1;
+ char *new_mode = buf + 8;
+ char *old_rev = buf + 15;
+ char *new_rev = buf + 56;
+ char *status = buf + 97;
+
+ if (bufsize != 99 ||
+ old_mode[-1] != ':' ||
+ new_mode[-1] != ' ' ||
+ old_rev[-1] != ' ' ||
+ new_rev[-1] != ' ' ||
+ status[-1] != ' ')
+ return FALSE;
+
+ file->status = *status;
+
+ string_copy_rev(file->old.rev, old_rev);
+ string_copy_rev(file->new.rev, new_rev);
+
+ file->old.mode = strtoul(old_mode, NULL, 8);
+ file->new.mode = strtoul(new_mode, NULL, 8);
+
+ file->name[0] = 0;
+
+ return TRUE;
+}
+
+static bool
+status_run(struct view *view, const char cmd[], bool diff, enum line_type type)
+{
+ struct status *file = NULL;
+ char buf[SIZEOF_STR * 4];
+ size_t bufsize = 0;
+ FILE *pipe;
+
+ pipe = popen(cmd, "r");
+ if (!pipe)
+ return FALSE;
+
+ add_line_data(view, NULL, type);
+
+ while (!feof(pipe) && !ferror(pipe)) {
+ char *sep;
+ size_t readsize;
+
+ readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
+ if (!readsize)
+ break;
+ bufsize += readsize;
+
+ /* Process while we have NUL chars. */
+ while ((sep = memchr(buf, 0, bufsize))) {
+ size_t sepsize = sep - buf + 1;
+
+ if (!file) {
+ if (!realloc_lines(view, view->line_size + 1))
+ goto error_out;
+
+ file = calloc(1, sizeof(*file));
+ if (!file)
+ goto error_out;
+
+ add_line_data(view, file, type);
+ }
+
+ /* Parse diff info part. */
+ if (!diff) {
+ file->status = '?';
+
+ } else if (!file->status) {
+ if (!status_get_diff(file, buf, sepsize))
+ goto error_out;
+
+ bufsize -= sepsize;
+ memmove(buf, sep + 1, bufsize);
+
+ sep = memchr(buf, 0, bufsize);
+ if (!sep)
+ break;
+ sepsize = sep - buf + 1;
+ }
+
+ /* git-ls-files just delivers a NUL separated
+ * list of file names similar to the second half
+ * of the git-diff-* output. */
+ string_ncopy(file->name, buf, sepsize);
+ bufsize -= sepsize;
+ memmove(buf, sep + 1, bufsize);
+ file = NULL;
+ }
+ }
+
+ if (ferror(pipe)) {
+error_out:
+ pclose(pipe);
+ return FALSE;
+ }
+
+ if (!view->line[view->lines - 1].data)
+ add_line_data(view, NULL, LINE_STAT_NONE);
+
+ pclose(pipe);
+ return TRUE;
+}
+
+#define STATUS_DIFF_INDEX_CMD "git diff-index -z --cached HEAD"
+#define STATUS_DIFF_FILES_CMD "git diff-files -z"
+#define STATUS_LIST_OTHER_CMD \
+ "_git_exclude=$(git rev-parse --git-dir)/info/exclude;" \
+ "test -f \"$_git_exclude\" && exclude=\"--exclude-from=$_git_exclude\";" \
+ "git ls-files -z --others --exclude-per-directory=.gitignore \"$exclude\"" \
+
+/* First parse staged info using git-diff-index(1), then parse unstaged
+ * info using git-diff-files(1), and finally untracked files using
+ * git-ls-files(1). */
+static bool
+status_open(struct view *view)
+{
+ size_t i;
+
+ for (i = 0; i < view->lines; i++)
+ free(view->line[i].data);
+ free(view->line);
+ view->lines = view->line_size = 0;
+ view->line = NULL;
+
+ if (!realloc_lines(view, view->line_size + 6))
+ return FALSE;
+
+ if (!status_run(view, STATUS_DIFF_INDEX_CMD, TRUE, LINE_STAT_STAGED) ||
+ !status_run(view, STATUS_DIFF_FILES_CMD, TRUE, LINE_STAT_UNSTAGED) ||
+ !status_run(view, STATUS_LIST_OTHER_CMD, FALSE, LINE_STAT_UNTRACKED))
+ return FALSE;
+
+ return TRUE;
+}
+
+static bool
+status_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
+{
+ struct status *status = line->data;
+
+ wmove(view->win, lineno, 0);
+
+ if (selected) {
+ wattrset(view->win, get_line_attr(LINE_CURSOR));
+ wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
+
+ } else if (!status && line->type != LINE_STAT_NONE) {
+ wattrset(view->win, get_line_attr(LINE_STAT_SECTION));
+ wchgat(view->win, -1, 0, LINE_STAT_SECTION, NULL);
+
+ } else {
+ wattrset(view->win, get_line_attr(line->type));
+ }
+
+ if (!status) {
+ char *text;
+
+ switch (line->type) {
+ case LINE_STAT_STAGED:
+ text = "Changes to be committed:";
+ break;
+
+ case LINE_STAT_UNSTAGED:
+ text = "Changed but not updated:";
+ break;
+
+ case LINE_STAT_UNTRACKED:
+ text = "Untracked files:";
+ break;
+
+ case LINE_STAT_NONE:
+ text = " (no files)";
+ break;
+
+ default:
+ return FALSE;
+ }
+
+ waddstr(view->win, text);
+ return TRUE;
+ }
+
+ waddch(view->win, status->status);
+ if (!selected)
+ wattrset(view->win, A_NORMAL);
+ wmove(view->win, lineno, 4);
+ waddstr(view->win, status->name);
+
+ return TRUE;
+}
+
+static bool
+status_enter(struct view *view, struct line *line)
+{
+ struct status *status = line->data;
+ const char *cmd;
+ char buf[SIZEOF_STR];
+ size_t bufsize = 0;
+ size_t written = 0;
+ FILE *pipe;
+
+ if (!status)
+ return TRUE;
+
+ switch (line->type) {
+ case LINE_STAT_STAGED:
+ if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
+ status->old.mode,
+ status->old.rev,
+ status->name, 0))
+ return FALSE;
+ cmd = "git update-index -z --index-info";
+ break;
+
+ case LINE_STAT_UNSTAGED:
+ case LINE_STAT_UNTRACKED:
+ if (!string_format_from(buf, &bufsize, "%s%c", status->name, 0))
+ return FALSE;
+ cmd = "git update-index -z --add --remove --stdin";
+ break;
+
+ default:
+ die("w00t");
+ }
+
+ pipe = popen(cmd, "w");
+ if (!pipe)
+ return FALSE;
+
+ while (!ferror(pipe) && written < bufsize) {
+ written += fwrite(buf + written, 1, bufsize - written, pipe);
+ }
+
+ pclose(pipe);
+
+ if (written != bufsize)
+ return FALSE;
+
+ open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
+ return TRUE;
+}
+
+static void
+status_select(struct view *view, struct line *line)
+{
+ char *text;
+
+ switch (line->type) {
+ case LINE_STAT_STAGED:
+ text = "Press Enter to unstage file for commit";
+ break;
+
+ case LINE_STAT_UNSTAGED:
+ text = "Press Enter to stage file for commit ";
+ break;
+
+ case LINE_STAT_UNTRACKED:
+ text = "Press Enter to stage file for addition";
+ break;
+
+ case LINE_STAT_NONE:
+ return;
+
+ default:
+ die("w00t");
+ }
+
+ string_ncopy(view->ref, text, strlen(text));
+}
+
+static bool
+status_grep(struct view *view, struct line *line)
+{
+ struct status *status = line->data;
+ enum { S_STATUS, S_NAME, S_END } state;
+ char buf[2] = "?";
+ regmatch_t pmatch;
+
+ if (!status)
+ return FALSE;
+
+ for (state = S_STATUS; state < S_END; state++) {
+ char *text;
+
+ switch (state) {
+ case S_NAME: text = status->name; break;
+ case S_STATUS:
+ buf[0] = status->status;
+ text = buf;
+ break;
+
+ default:
+ return FALSE;
+ }
+
+ if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static struct view_ops status_ops = {
+ "file",
+ status_open,
+ NULL,
+ status_draw,
+ status_enter,
+ status_grep,
+ status_select,
+};
+
+
+/*
+ * Revision graph
+ */
+
+struct commit {
+ char id[SIZEOF_REV]; /* SHA1 ID. */
+ char title[128]; /* First line of the commit message. */
+ char author[75]; /* Author of the commit. */
+ struct tm time; /* Date from the author ident. */
+ struct ref **refs; /* Repository references. */
+ chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
+ size_t graph_size; /* The width of the graph array. */
+};
+
+/* 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;
+};
+
+/* 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)
+{
+ return graph->parents->size > 1;
+}
+
+static inline void
+append_to_rev_graph(struct rev_graph *graph, chtype symbol)
+{
+ struct commit *commit = graph->commit;
+
+ if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
+ commit->graph[commit->graph_size++] = symbol;
+}
+
+static void
+done_rev_graph(struct rev_graph *graph)
+{
+ 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++;
+ }
+ }
+
+ graph->size = graph->pos = 0;
+ graph->commit = NULL;
+ memset(graph->parents, 0, sizeof(*graph->parents));
+}
+
+static void
+push_rev_graph(struct rev_graph *graph, char *parent)
+{
+ int i;
+
+ /* "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_ncopy(graph->rev[graph->size++], parent, SIZEOF_REV);
+ }
+}
+
+static chtype
+get_rev_graph_symbol(struct rev_graph *graph)
+{
+ chtype symbol;
+
+ 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;
+
+ return symbol;
+}
+
+static void
+draw_rev_graph(struct rev_graph *graph)
+{
+ struct rev_filler {
+ chtype separator, line;
+ };
+ enum { DEFAULT, RSHARP, RDIAG, LDIAG };
+ static struct rev_filler fillers[] = {
+ { ' ', REVGRAPH_LINE },
+ { '`', '.' },
+ { '\'', ' ' },
+ { '/', ' ' },
+ };
+ chtype symbol = get_rev_graph_symbol(graph);
+ struct rev_filler *filler;
+ size_t i;
+
+ 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);
+ }
+}
+
+/* Prepare the next rev graph */
+static void
+prepare_rev_graph(struct rev_graph *graph)
+{
+ size_t i;
+
+ /* 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]);
+ }
+
+ /* Interleave the new revision parent(s). */
+ for (i = 0; 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]);
+}
+
+static void
+update_rev_graph(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;
+
+ draw_rev_graph(graph->prev);
+ done_rev_graph(graph->prev->prev);
+}
+
+
+/*
+ * Main view backend
+ */
+
+static bool
+main_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
+{
+ char buf[DATE_COLS + 1];
+ struct commit *commit = line->data;
+ enum line_type type;
+ int col = 0;
+ size_t timelen;
+ size_t authorlen;
+ int trimmed = 1;
+
+ if (!*commit->author)
+ return FALSE;
+
+ wmove(view->win, lineno, col);
+
+ if (selected) {
+ type = LINE_CURSOR;
+ wattrset(view->win, get_line_attr(type));
+ wchgat(view->win, -1, 0, type, NULL);
+
+ } else {
+ type = LINE_MAIN_COMMIT;
+ wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
+ }
+
+ timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
+ waddnstr(view->win, buf, timelen);
+ waddstr(view->win, " ");
+
+ col += DATE_COLS;
+ wmove(view->win, lineno, col);
+ if (type != LINE_CURSOR)
+ wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
+
+ if (opt_utf8) {
+ authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
+ } else {
+ authorlen = strlen(commit->author);
+ if (authorlen > AUTHOR_COLS - 2) {
+ authorlen = AUTHOR_COLS - 2;
+ trimmed = 1;