+/* 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 ls-files -z --others --exclude-per-directory=.gitignore"
+
+#define STATUS_DIFF_SHOW_CMD \
+ "git diff --root --patch-with-stat --find-copies-harder -B -C %s -- %s 2>/dev/null"
+
+/* 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)
+{
+ struct stat statbuf;
+ char exclude[SIZEOF_STR];
+ char cmd[SIZEOF_STR];
+ 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 (!string_format(exclude, "%s/info/exclude", opt_git_dir))
+ return FALSE;
+
+ string_copy(cmd, STATUS_LIST_OTHER_CMD);
+
+ if (stat(exclude, &statbuf) >= 0) {
+ size_t cmdsize = strlen(cmd);
+
+ if (!string_format_from(cmd, &cmdsize, " %s", "--exclude-from=") ||
+ sq_quote(cmd, cmdsize, exclude) >= sizeof(cmd))
+ 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, 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 enum request
+status_enter(struct view *view, struct line *line)
+{
+ struct status *status = line->data;
+ char path[SIZEOF_STR] = "";
+ char *info;
+ size_t cmdsize = 0;
+
+ if (line->type == LINE_STAT_NONE ||
+ (!status && line[1].type == LINE_STAT_NONE)) {
+ report("No file to diff");
+ return REQ_NONE;
+ }
+
+ if (status && sq_quote(path, 0, status->name) >= sizeof(path))
+ return REQ_QUIT;
+
+ if (opt_cdup[0] &&
+ line->type != LINE_STAT_UNTRACKED &&
+ !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
+ return REQ_QUIT;
+
+ switch (line->type) {
+ case LINE_STAT_STAGED:
+ if (!string_format_from(opt_cmd, &cmdsize, STATUS_DIFF_SHOW_CMD,
+ "--cached", path))
+ return REQ_QUIT;
+ if (status)
+ info = "Staged changes to %s";
+ else
+ info = "Staged changes";
+ break;
+
+ case LINE_STAT_UNSTAGED:
+ if (!string_format_from(opt_cmd, &cmdsize, STATUS_DIFF_SHOW_CMD,
+ "", path))
+ return REQ_QUIT;
+ if (status)
+ info = "Unstaged changes to %s";
+ else
+ info = "Unstaged changes";
+ break;
+
+ case LINE_STAT_UNTRACKED:
+ if (opt_pipe)
+ return REQ_QUIT;
+
+
+ if (!status) {
+ report("No file to show");
+ return REQ_NONE;
+ }
+
+ opt_pipe = fopen(status->name, "r");
+ info = "Untracked file %s";
+ break;
+
+ default:
+ die("w00t");
+ }
+
+ open_view(view, REQ_VIEW_DIFF, OPEN_RELOAD | OPEN_SPLIT);
+ if (view_is_displayed(VIEW(REQ_VIEW_DIFF))) {
+ string_format(VIEW(REQ_VIEW_DIFF)->ref, info, status->name);
+ }
+
+ return REQ_NONE;
+}
+
+
+static bool
+status_update_file(struct view *view, struct status *status, enum line_type type)
+{
+ char cmd[SIZEOF_STR];
+ char buf[SIZEOF_STR];
+ size_t cmdsize = 0;
+ size_t bufsize = 0;
+ size_t written = 0;
+ FILE *pipe;
+
+ if (opt_cdup[0] &&
+ type != LINE_STAT_UNTRACKED &&
+ !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
+ return FALSE;
+
+ switch (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;
+
+ string_add(cmd, cmdsize, "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;
+
+ string_add(cmd, cmdsize, "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;
+
+ return TRUE;
+}
+
+static void
+status_update(struct view *view)
+{
+ struct line *line = &view->line[view->lineno];
+
+ assert(view->lines);
+
+ if (!line->data) {
+ while (++line < view->line + view->lines && line->data) {
+ if (!status_update_file(view, line->data, line->type))
+ report("Failed to update file status");
+ }
+
+ if (!line[-1].data) {
+ report("Nothing to update");
+ return;
+ }
+
+ } else if (!status_update_file(view, line->data, line->type)) {
+ report("Failed to update file status");
+ }
+
+ open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
+}
+
+static enum request
+status_request(struct view *view, enum request request, struct line *line)
+{
+ struct status *status = line->data;
+
+ switch (request) {
+ case REQ_STATUS_UPDATE:
+ status_update(view);
+ break;
+
+ case REQ_EDIT:
+ if (!status)
+ return request;
+
+ open_editor(view, status->name);
+ break;
+
+ case REQ_ENTER:
+ status_enter(view, line);
+ break;
+
+ default:
+ return request;
+ }
+
+ return REQ_NONE;
+}
+
+static void
+status_select(struct view *view, struct line *line)
+{
+ struct status *status = line->data;
+ char file[SIZEOF_STR] = "all files";
+ char *text;
+
+ if (status && !string_format(file, "'%s'", status->name))
+ return;
+
+ if (!status && line[1].type == LINE_STAT_NONE)
+ line++;
+
+ switch (line->type) {
+ case LINE_STAT_STAGED:
+ text = "Press %s to unstage %s for commit";
+ break;
+
+ case LINE_STAT_UNSTAGED:
+ text = "Press %s to stage %s for commit";
+ break;
+
+ case LINE_STAT_UNTRACKED:
+ text = "Press %s to stage %s for addition";
+ break;
+
+ case LINE_STAT_NONE:
+ text = "Nothing to update";
+ break;
+
+ default:
+ die("w00t");
+ }
+
+ string_format(view->ref, text, get_key(REQ_STATUS_UPDATE), file);
+}
+
+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_request,
+ 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_copy_rev(graph->rev[graph->size++], parent);
+ }
+}
+
+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)