+ 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;
+
+ if (tree_lineno > view->lineno) {
+ view->lineno = tree_lineno;
+ tree_lineno = 0;
+ }
+
+ return TRUE;
+}
+
+static enum request
+tree_request(struct view *view, enum request request, struct line *line)
+{
+ enum open_flags flags;
+
+ if (request != REQ_ENTER)
+ return request;
+
+ /* Cleanup the stack if the tree view is at a different tree. */
+ while (!*opt_path && tree_stack)
+ pop_tree_stack_entry();
+
+ 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) {
+ pop_tree_stack_entry();
+
+ } else {
+ char *data = line->data;
+ char *basename = data + SIZEOF_TREE_ATTR;
+
+ push_tree_stack_entry(basename, view->lineno);
+ }
+
+ /* 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);
+ if (request == REQ_VIEW_TREE) {
+ view->lineno = tree_lineno;
+ }
+
+ return REQ_NONE;
+}
+
+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_request,
+ 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_request,
+ 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];
+};
+
+static struct status stage_status;
+static enum line_type stage_line_type;
+
+/* 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: