+blame_read_file(struct view *view, char *line)
+{
+ if (!line) {
+ FILE *pipe = NULL;
+
+ if (view->lines > 0)
+ pipe = popen(view->cmd, "r");
+ view->cmd[0] = 0;
+ if (!pipe) {
+ report("Failed to load blame data");
+ return TRUE;
+ }
+
+ fclose(view->pipe);
+ view->pipe = pipe;
+ return FALSE;
+
+ } else {
+ size_t linelen = strlen(line);
+ struct blame *blame = malloc(sizeof(*blame) + linelen);
+
+ if (!line)
+ return FALSE;
+
+ blame->commit = NULL;
+ strncpy(blame->text, line, linelen);
+ blame->text[linelen] = 0;
+ return add_line_data(view, blame, LINE_BLAME_COMMIT) != NULL;
+ }
+}
+
+static bool
+match_blame_header(const char *name, char **line)
+{
+ size_t namelen = strlen(name);
+ bool matched = !strncmp(name, *line, namelen);
+
+ if (matched)
+ *line += namelen;
+
+ return matched;
+}
+
+static bool
+blame_read(struct view *view, char *line)
+{
+ static struct blame_commit *commit = NULL;
+ static int blamed = 0;
+ static time_t author_time;
+
+ if (*view->cmd)
+ return blame_read_file(view, line);
+
+ if (!line) {
+ /* Reset all! */
+ commit = NULL;
+ blamed = 0;
+ string_format(view->ref, "%s", view->vid);
+ if (view_is_displayed(view)) {
+ update_view_title(view);
+ redraw_view_from(view, 0);
+ }
+ return TRUE;
+ }
+
+ if (!commit) {
+ commit = parse_blame_commit(view, line, &blamed);
+ string_format(view->ref, "%s %2d%%", view->vid,
+ blamed * 100 / view->lines);
+
+ } else if (match_blame_header("author ", &line)) {
+ string_ncopy(commit->author, line, strlen(line));
+
+ } else if (match_blame_header("author-time ", &line)) {
+ author_time = (time_t) atol(line);
+
+ } else if (match_blame_header("author-tz ", &line)) {
+ long tz;
+
+ tz = ('0' - line[1]) * 60 * 60 * 10;
+ tz += ('0' - line[2]) * 60 * 60;
+ tz += ('0' - line[3]) * 60;
+ tz += ('0' - line[4]) * 60;
+
+ if (line[0] == '-')
+ tz = -tz;
+
+ author_time -= tz;
+ gmtime_r(&author_time, &commit->time);
+
+ } else if (match_blame_header("summary ", &line)) {
+ string_ncopy(commit->title, line, strlen(line));
+
+ } else if (match_blame_header("filename ", &line)) {
+ string_ncopy(commit->filename, line, strlen(line));
+ commit = NULL;
+ }
+
+ return TRUE;
+}
+
+static bool
+blame_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
+{
+ struct blame *blame = line->data;
+ int col = 0;
+
+ wmove(view->win, lineno, 0);
+
+ if (selected) {
+ wattrset(view->win, get_line_attr(LINE_CURSOR));
+ wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
+ } else {
+ wattrset(view->win, A_NORMAL);
+ }
+
+ if (opt_date) {
+ int n;
+
+ if (!selected)
+ wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
+ if (blame->commit) {
+ char buf[DATE_COLS + 1];
+ int timelen;
+
+ timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &blame->commit->time);
+ n = draw_text(view, buf, view->width - col, FALSE, selected);
+ draw_text(view, " ", view->width - col - n, FALSE, selected);
+ }
+
+ col += DATE_COLS;
+ wmove(view->win, lineno, col);
+ if (col >= view->width)
+ return TRUE;
+ }
+
+ if (opt_author) {
+ int max = MIN(AUTHOR_COLS - 1, view->width - col);
+
+ if (!selected)
+ wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
+ if (blame->commit)
+ draw_text(view, blame->commit->author, max, TRUE, selected);
+ col += AUTHOR_COLS;
+ if (col >= view->width)
+ return TRUE;
+ wmove(view->win, lineno, col);
+ }
+
+ {
+ int max = MIN(ID_COLS - 1, view->width - col);
+
+ if (!selected)
+ wattrset(view->win, get_line_attr(LINE_BLAME_ID));
+ if (blame->commit)
+ draw_text(view, blame->commit->id, max, FALSE, -1);
+ col += ID_COLS;
+ if (col >= view->width)
+ return TRUE;
+ wmove(view->win, lineno, col);
+ }
+
+ {
+ if (!selected)
+ wattrset(view->win, get_line_attr(LINE_BLAME_LINENO));
+ col += draw_lineno(view, lineno, view->width - col, selected);
+ if (col >= view->width)
+ return TRUE;
+ }
+
+ col += draw_text(view, blame->text, view->width - col, TRUE, selected);
+
+ return TRUE;
+}
+
+static enum request
+blame_request(struct view *view, enum request request, struct line *line)
+{
+ enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
+ struct blame *blame = line->data;
+
+ switch (request) {
+ case REQ_ENTER:
+ if (!blame->commit) {
+ report("No commit loaded yet");
+ break;
+ }
+
+ if (!strcmp(blame->commit->id, NULL_ID)) {
+ char path[SIZEOF_STR];
+
+ if (sq_quote(path, 0, view->vid) >= sizeof(path))
+ break;
+ string_format(opt_cmd, "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s 2>/dev/null", path);
+ }
+
+ open_view(view, REQ_VIEW_DIFF, flags);
+ break;
+
+ default:
+ return request;
+ }
+
+ return REQ_NONE;
+}
+
+static bool
+blame_grep(struct view *view, struct line *line)
+{
+ struct blame *blame = line->data;
+ struct blame_commit *commit = blame->commit;
+ regmatch_t pmatch;
+
+#define MATCH(text) \
+ (*text && regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
+
+ if (commit) {
+ char buf[DATE_COLS + 1];
+
+ if (MATCH(commit->title) ||
+ MATCH(commit->author) ||
+ MATCH(commit->id))
+ return TRUE;
+
+ if (strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time) &&
+ MATCH(buf))
+ return TRUE;
+ }
+
+ return MATCH(blame->text);
+
+#undef MATCH
+}
+
+static void
+blame_select(struct view *view, struct line *line)
+{
+ struct blame *blame = line->data;
+ struct blame_commit *commit = blame->commit;
+
+ if (!commit)
+ return;
+
+ if (!strcmp(commit->id, NULL_ID))
+ string_ncopy(ref_commit, "HEAD", 4);
+ else
+ string_copy_rev(ref_commit, commit->id);
+}
+
+static struct view_ops blame_ops = {
+ "line",
+ blame_open,
+ blame_read,
+ blame_draw,
+ blame_request,
+ blame_grep,
+ blame_select,
+};
+
+/*
+ * Status backend
+ */
+
+struct status {
+ char status;
+ struct {
+ mode_t mode;
+ char rev[SIZEOF_REV];
+ char name[SIZEOF_STR];
+ } old;
+ struct {
+ mode_t mode;
+ char rev[SIZEOF_REV];
+ char name[SIZEOF_STR];
+ } new;
+};
+
+static char status_onbranch[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->old.name[0] = file->new.name[0] = 0;
+
+ return TRUE;
+}
+
+static bool
+status_run(struct view *view, const char cmd[], char status, enum line_type type)