chiark / gitweb /
Add TODO about using non-blocking I/O for incremental view loading
[tig] / tig.c
diff --git a/tig.c b/tig.c
index dde4369f183744548ce57e4c3b98c707919d387b..6adfb334e9d3546511ffc737a2e89621f50631fc 100644 (file)
--- a/tig.c
+++ b/tig.c
@@ -11,8 +11,8 @@
  * GNU General Public License for more details.
  */
 
-#ifndef        VERSION
-#define VERSION        "tig-0.6.git"
+#ifndef VERSION
+#define VERSION "unknown-version"
 #endif
 
 #ifndef DEBUG
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <sys/types.h>
+#include <sys/stat.h>
 #include <unistd.h>
 #include <time.h>
 
-#include <sys/types.h>
 #include <regex.h>
 
 #include <locale.h>
@@ -47,7 +48,7 @@
 
 static void __NORETURN die(const char *err, ...);
 static void report(const char *msg, ...);
-static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, int, char *, int));
+static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, size_t, char *, size_t));
 static void set_nonblocking_input(bool loading);
 static size_t utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed);
 
@@ -110,6 +111,7 @@ static size_t utf8_length(const char *string, size_t max_width, int *coloffset,
 /* XXX: Needs to be defined to the empty string. */
 #define TIG_HELP_CMD   ""
 #define TIG_PAGER_CMD  ""
+#define TIG_STATUS_CMD ""
 
 /* Some ascii-shorthands fitted into the ncurses namespace. */
 #define KEY_TAB                '\t'
@@ -168,7 +170,7 @@ string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
 /* Shorthands for safely copying into a fixed buffer. */
 
 #define string_copy(dst, src) \
-       string_ncopy_do(dst, sizeof(dst), src, sizeof(dst))
+       string_ncopy_do(dst, sizeof(dst), src, sizeof(src))
 
 #define string_ncopy(dst, src, srclen) \
        string_ncopy_do(dst, sizeof(dst), src, srclen)
@@ -176,6 +178,9 @@ string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
 #define string_copy_rev(dst, src) \
        string_ncopy_do(dst, SIZEOF_REV, src, SIZEOF_REV - 1)
 
+#define string_add(dst, from, src) \
+       string_ncopy_do(dst + (from), sizeof(dst) - (from), src, sizeof(src))
+
 static char *
 chomp_string(char *name)
 {
@@ -293,6 +298,7 @@ sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
        REQ_(VIEW_BLOB,         "Show blob view"), \
        REQ_(VIEW_HELP,         "Show help page"), \
        REQ_(VIEW_PAGER,        "Show pager view"), \
+       REQ_(VIEW_STATUS,       "Show status view"), \
        \
        REQ_GROUP("View manipulation") \
        REQ_(ENTER,             "Enter current line and scroll"), \
@@ -382,7 +388,7 @@ get_request(const char *name)
  */
 
 static const char usage[] =
-VERSION " (" __DATE__ ")\n"
+"tig " VERSION " (" __DATE__ ")\n"
 "\n"
 "Usage: tig [options]\n"
 "   or: tig [options] [--] [git log options]\n"
@@ -394,6 +400,7 @@ VERSION " (" __DATE__ ")\n"
 "Options:\n"
 "  -l                          Start up in log view\n"
 "  -d                          Start up in diff view\n"
+"  -S                          Start up in status view\n"
 "  -n[I], --line-number[=I]    Show line numbers with given interval\n"
 "  -b[N], --tab-size[=N]       Set number of spaces for tab expansion\n"
 "  --                          Mark end of tig options\n"
@@ -414,6 +421,8 @@ static bool opt_utf8                        = TRUE;
 static char opt_codeset[20]            = "UTF-8";
 static iconv_t opt_iconv               = ICONV_NONE;
 static char opt_search[SIZEOF_STR]     = "";
+static char opt_cdup[SIZEOF_STR]       = "";
+static char opt_git_dir[SIZEOF_STR]    = "";
 
 enum option_type {
        OPT_NONE,
@@ -488,6 +497,11 @@ parse_options(int argc, char *argv[])
                        continue;
                }
 
+               if (!strcmp(opt, "-S")) {
+                       opt_request = REQ_VIEW_STATUS;
+                       break;
+               }
+
                if (check_option(opt, 'n', "line-number", OPT_INT, &opt_num_interval)) {
                        opt_line_number = TRUE;
                        continue;
@@ -595,7 +609,12 @@ LINE(MAIN_TAG,     "",                     COLOR_MAGENTA,  COLOR_DEFAULT,  A_BOLD), \
 LINE(MAIN_REMOTE,  "",                 COLOR_YELLOW,   COLOR_DEFAULT,  A_BOLD), \
 LINE(MAIN_REF,     "",                 COLOR_CYAN,     COLOR_DEFAULT,  A_BOLD), \
 LINE(TREE_DIR,     "",                 COLOR_DEFAULT,  COLOR_DEFAULT,  A_NORMAL), \
-LINE(TREE_FILE,    "",                 COLOR_DEFAULT,  COLOR_DEFAULT,  A_NORMAL)
+LINE(TREE_FILE,    "",                 COLOR_DEFAULT,  COLOR_DEFAULT,  A_NORMAL), \
+LINE(STAT_SECTION, "",                 COLOR_DEFAULT,  COLOR_BLUE,     A_BOLD), \
+LINE(STAT_NONE,    "",                 COLOR_DEFAULT,  COLOR_DEFAULT,  0), \
+LINE(STAT_STAGED,  "",                 COLOR_CYAN,     COLOR_DEFAULT,  0), \
+LINE(STAT_UNSTAGED,"",                 COLOR_YELLOW,   COLOR_DEFAULT,  0), \
+LINE(STAT_UNTRACKED,"",                        COLOR_MAGENTA,  COLOR_DEFAULT,  0)
 
 enum line_type {
 #define LINE(type, line, fg, bg, attr) \
@@ -706,6 +725,7 @@ static struct keybinding default_keybindings[] = {
        { 'f',          REQ_VIEW_BLOB },
        { 'p',          REQ_VIEW_PAGER },
        { 'h',          REQ_VIEW_HELP },
+       { 'S',          REQ_VIEW_STATUS },
 
        /* View manipulation */
        { 'q',          REQ_VIEW_CLOSE },
@@ -758,7 +778,8 @@ static struct keybinding default_keybindings[] = {
        KEYMAP_(TREE), \
        KEYMAP_(BLOB), \
        KEYMAP_(PAGER), \
-       KEYMAP_(HELP) \
+       KEYMAP_(HELP), \
+       KEYMAP_(STATUS)
 
 enum keymap {
 #define KEYMAP_(name) KEYMAP_##name
@@ -1023,7 +1044,7 @@ option_set_command(int argc, char *argv[])
                                        break;
                                }
                default:
-                       string_copy(opt_encoding, arg);
+                       string_ncopy(opt_encoding, arg, strlen(arg));
                        return OK;
                }
        }
@@ -1101,7 +1122,7 @@ set_option(char *opt, char *value)
 }
 
 static int
-read_option(char *opt, int optlen, char *value, int valuelen)
+read_option(char *opt, size_t optlen, char *value, size_t valuelen)
 {
        int status = OK;
 
@@ -1120,7 +1141,7 @@ read_option(char *opt, int optlen, char *value, int valuelen)
 
        }  else {
                /* Look for comment endings in the value. */
-               int len = strcspn(value, "#");
+               size_t len = strcspn(value, "#");
 
                if (len < valuelen) {
                        valuelen = len;
@@ -1234,10 +1255,12 @@ struct view {
 struct view_ops {
        /* What type of content being displayed. Used in the title bar. */
        const char *type;
-       /* Draw one line; @lineno must be < view->height. */
-       bool (*draw)(struct view *view, struct line *line, unsigned int lineno, bool selected);
+       /* Open and reads in all view content. */
+       bool (*open)(struct view *view);
        /* Read one line; updates view->line. */
        bool (*read)(struct view *view, char *data);
+       /* Draw one line; @lineno must be < view->height. */
+       bool (*draw)(struct view *view, struct line *line, unsigned int lineno, bool selected);
        /* Depending on view, change display based on current line. */
        bool (*enter)(struct view *view, struct line *line);
        /* Search for regex in a line. */
@@ -1250,6 +1273,8 @@ static struct view_ops pager_ops;
 static struct view_ops main_ops;
 static struct view_ops tree_ops;
 static struct view_ops blob_ops;
+static struct view_ops help_ops;
+static struct view_ops status_ops;
 
 #define VIEW_STR(name, cmd, env, ref, ops, map) \
        { name, cmd, #env, ref, ops, map}
@@ -1259,13 +1284,14 @@ static struct view_ops blob_ops;
 
 
 static struct view views[] = {
-       VIEW_(MAIN,  "main",  &main_ops,  ref_head),
-       VIEW_(DIFF,  "diff",  &pager_ops, ref_commit),
-       VIEW_(LOG,   "log",   &pager_ops, ref_head),
-       VIEW_(TREE,  "tree",  &tree_ops,  ref_commit),
-       VIEW_(BLOB,  "blob",  &blob_ops,  ref_blob),
-       VIEW_(HELP,  "help",  &pager_ops, ""),
-       VIEW_(PAGER, "pager", &pager_ops, ""),
+       VIEW_(MAIN,   "main",   &main_ops,   ref_head),
+       VIEW_(DIFF,   "diff",   &pager_ops,  ref_commit),
+       VIEW_(LOG,    "log",    &pager_ops,  ref_head),
+       VIEW_(TREE,   "tree",   &tree_ops,   ref_commit),
+       VIEW_(BLOB,   "blob",   &blob_ops,   ref_blob),
+       VIEW_(HELP,   "help",   &help_ops,  ""),
+       VIEW_(PAGER,  "pager",  &pager_ops,  ""),
+       VIEW_(STATUS, "status", &status_ops, ""),
 };
 
 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
@@ -1778,8 +1804,6 @@ end_update(struct view *view)
 static bool
 begin_update(struct view *view)
 {
-       const char *id = view->id;
-
        if (view->pipe)
                end_update(view);
 
@@ -1800,11 +1824,12 @@ begin_update(struct view *view)
                else if (sq_quote(path, 0, opt_path) >= sizeof(path))
                        return FALSE;
 
-               if (!string_format(view->cmd, format, id, path))
+               if (!string_format(view->cmd, format, view->id, path))
                        return FALSE;
 
        } else {
                const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
+               const char *id = view->id;
 
                if (!string_format(view->cmd, format, id, id, id, id, id))
                        return FALSE;
@@ -1813,7 +1838,7 @@ begin_update(struct view *view)
                 * member. This is needed by the blob view. Most other
                 * views sets it automatically after loading because the
                 * first line is a commit line. */
-               string_copy(view->ref, id);
+               string_copy_rev(view->ref, view->id);
        }
 
        /* Special case for the pager view. */
@@ -1832,7 +1857,7 @@ begin_update(struct view *view)
        view->offset = 0;
        view->lines  = 0;
        view->lineno = 0;
-       string_copy_rev(view->vid, id);
+       string_copy_rev(view->vid, view->id);
 
        if (view->line) {
                int i;
@@ -1980,65 +2005,30 @@ end:
 }
 
 static struct line *
-add_line_text(struct view *view, char *data, enum line_type type)
+add_line_data(struct view *view, void *data, enum line_type type)
 {
-       struct line *line = &view->line[view->lines];
-
-       if (!data)
-               return NULL;
-
-       line->data = strdup(data);
-       if (!line->data)
-               return NULL;
+       struct line *line = &view->line[view->lines++];
 
+       memset(line, 0, sizeof(*line));
        line->type = type;
-       view->lines++;
+       line->data = data;
 
        return line;
 }
 
-
-/*
- * View opening
- */
-
-static void open_help_view(struct view *view)
+static struct line *
+add_line_text(struct view *view, char *data, enum line_type type)
 {
-       char buf[BUFSIZ];
-       int lines = ARRAY_SIZE(req_info) + 2;
-       int i;
-
-       if (view->lines > 0)
-               return;
-
-       for (i = 0; i < ARRAY_SIZE(req_info); i++)
-               if (!req_info[i].request)
-                       lines++;
-
-       view->line = calloc(lines, sizeof(*view->line));
-       if (!view->line) {
-               report("Allocation failure");
-               return;
-       }
-
-       add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
-
-       for (i = 0; i < ARRAY_SIZE(req_info); i++) {
-               char *key;
+       if (data)
+               data = strdup(data);
 
-               if (!req_info[i].request) {
-                       add_line_text(view, "", LINE_DEFAULT);
-                       add_line_text(view, req_info[i].help, LINE_DEFAULT);
-                       continue;
-               }
+       return data ? add_line_data(view, data, type) : NULL;
+}
 
-               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);
-       }
-}
+/*
+ * View opening
+ */
 
 enum open_flags {
        OPEN_DEFAULT = 0,       /* Use default view switching. */
@@ -2062,8 +2052,11 @@ open_view(struct view *prev, enum request request, enum open_flags flags)
                return;
        }
 
-       if (view == VIEW(REQ_VIEW_HELP)) {
-               open_help_view(view);
+       if (view->ops->open) {
+               if (!view->ops->open(view)) {
+                       report("Failed to load %s view", view->name);
+                       return;
+               }
 
        } else if ((reload || strcmp(view->vid, view->id)) &&
                   !begin_update(view)) {
@@ -2172,6 +2165,7 @@ view_driver(struct view *view, enum request request)
        case REQ_VIEW_LOG:
        case REQ_VIEW_TREE:
        case REQ_VIEW_HELP:
+       case REQ_VIEW_STATUS:
                open_view(view, request, OPEN_DEFAULT);
                break;
 
@@ -2251,7 +2245,7 @@ view_driver(struct view *view, enum request request)
                break;
 
        case REQ_SHOW_VERSION:
-               report("%s (built %s)", VERSION, __DATE__);
+               report("tig-%s (built %s)", VERSION, __DATE__);
                return TRUE;
 
        case REQ_SCREEN_RESIZE:
@@ -2517,8 +2511,63 @@ pager_select(struct view *view, struct line *line)
 
 static struct view_ops pager_ops = {
        "line",
-       pager_draw,
+       NULL,
        pager_read,
+       pager_draw,
+       pager_enter,
+       pager_grep,
+       pager_select,
+};
+
+
+/*
+ * Help backend
+ */
+
+static bool
+help_open(struct view *view)
+{
+       char buf[BUFSIZ];
+       int lines = ARRAY_SIZE(req_info) + 2;
+       int i;
+
+       if (view->lines > 0)
+               return TRUE;
+
+       for (i = 0; i < ARRAY_SIZE(req_info); i++)
+               if (!req_info[i].request)
+                       lines++;
+
+       view->line = calloc(lines, sizeof(*view->line));
+       if (!view->line)
+               return FALSE;
+
+       add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
+
+       for (i = 0; i < ARRAY_SIZE(req_info); i++) {
+               char *key;
+
+               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,
@@ -2700,8 +2749,9 @@ tree_select(struct view *view, struct line *line)
 
 static struct view_ops tree_ops = {
        "file",
-       pager_draw,
+       NULL,
        tree_read,
+       pager_draw,
        tree_enter,
        pager_grep,
        tree_select,
@@ -2715,14 +2765,375 @@ blob_read(struct view *view, char *line)
 
 static struct view_ops blob_ops = {
        "line",
-       pager_draw,
+       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 ls-files -z --others --exclude-per-directory=.gitignore"
+
+/* 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 bool
+status_enter(struct view *view, struct line *line)
+{
+       struct status *status = line->data;
+       char cmd[SIZEOF_STR];
+       char buf[SIZEOF_STR];
+       size_t cmdsize = 0;
+       size_t bufsize = 0;
+       size_t written = 0;
+       FILE *pipe;
+
+       if (!status)
+               return TRUE;
+
+       if (opt_cdup[0] &&
+           line->type != LINE_STAT_UNTRACKED &&
+           !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
+               return FALSE;
+
+       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;
+
+               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;
+
+       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
  */
@@ -2809,7 +3220,7 @@ push_rev_graph(struct rev_graph *graph, char *parent)
                        return;
 
        if (graph->size < SIZEOF_REVITEMS) {
-               string_ncopy(graph->rev[graph->size++], parent, SIZEOF_REV);
+               string_copy_rev(graph->rev[graph->size++], parent);
        }
 }
 
@@ -3045,8 +3456,7 @@ main_read(struct view *view, char *line)
 {
        static struct rev_graph *graph = graph_stacks;
        enum line_type type;
-       struct commit *commit = view->lines
-                             ? view->line[view->lines - 1].data : NULL;
+       struct commit *commit;
 
        if (!line) {
                update_rev_graph(graph);
@@ -3054,26 +3464,25 @@ main_read(struct view *view, char *line)
        }
 
        type = get_line_type(line);
-
-       switch (type) {
-       case LINE_COMMIT:
+       if (type == LINE_COMMIT) {
                commit = calloc(1, sizeof(struct commit));
                if (!commit)
                        return FALSE;
 
-               line += STRING_SIZE("commit ");
-
-               view->line[view->lines++].data = commit;
-               string_copy_rev(commit->id, line);
+               string_copy_rev(commit->id, line + STRING_SIZE("commit "));
                commit->refs = get_refs(commit->id);
                graph->commit = commit;
-               break;
+               add_line_data(view, commit, LINE_MAIN_COMMIT);
+               return TRUE;
+       }
+
+       if (!view->lines)
+               return TRUE;
+       commit = view->line[view->lines - 1].data;
 
+       switch (type) {
        case LINE_PARENT:
-               if (commit) {
-                       line += STRING_SIZE("parent ");
-                       push_rev_graph(graph->parents, line);
-               }
+               push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
                break;
 
        case LINE_AUTHOR:
@@ -3085,7 +3494,7 @@ main_read(struct view *view, char *line)
                char *nameend = strchr(ident, '<');
                char *emailend = strchr(ident, '>');
 
-               if (!commit || !nameend || !emailend)
+               if (!nameend || !emailend)
                        break;
 
                update_rev_graph(graph);
@@ -3099,7 +3508,7 @@ main_read(struct view *view, char *line)
                                ident = "Unknown";
                }
 
-               string_copy(commit->author, ident);
+               string_ncopy(commit->author, ident, strlen(ident));
 
                /* Parse epoch and timezone */
                if (emailend[1] == ' ') {
@@ -3127,9 +3536,6 @@ main_read(struct view *view, char *line)
                break;
        }
        default:
-               if (!commit)
-                       break;
-
                /* Fill in the commit title if it has not already been set. */
                if (commit->title[0])
                        break;
@@ -3148,7 +3554,7 @@ main_read(struct view *view, char *line)
                /* FIXME: More graceful handling of titles; append "..." to
                 * shortened titles, etc. */
 
-               string_copy(commit->title, line);
+               string_ncopy(commit->title, line, strlen(line));
        }
 
        return TRUE;
@@ -3205,8 +3611,9 @@ main_select(struct view *view, struct line *line)
 
 static struct view_ops main_ops = {
        "commit",
-       main_draw,
+       NULL,
        main_read,
+       main_draw,
        main_enter,
        main_grep,
        main_select,
@@ -3593,7 +4000,7 @@ get_refs(char *id)
 }
 
 static int
-read_ref(char *id, int idlen, char *name, int namelen)
+read_ref(char *id, size_t idlen, char *name, size_t namelen)
 {
        struct ref *ref;
        bool tag = FALSE;
@@ -3652,10 +4059,10 @@ load_refs(void)
 }
 
 static int
-read_repo_config_option(char *name, int namelen, char *value, int valuelen)
+read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
 {
        if (!strcmp(name, "i18n.commitencoding"))
-               string_copy(opt_encoding, value);
+               string_ncopy(opt_encoding, value, valuelen);
 
        return OK;
 }
@@ -3667,9 +4074,28 @@ load_repo_config(void)
                               "=", read_repo_config_option);
 }
 
+static int
+read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
+{
+       if (!opt_git_dir[0])
+               string_ncopy(opt_git_dir, name, namelen);
+       else
+               string_ncopy(opt_cdup, name, namelen);
+       return OK;
+}
+
+/* XXX: The line outputted by "--show-cdup" can be empty so the option
+ * must be the last one! */
+static int
+load_repo_info(void)
+{
+       return read_properties(popen("git rev-parse --git-dir --show-cdup 2>/dev/null", "r"),
+                              "=", read_repo_info);
+}
+
 static int
 read_properties(FILE *pipe, const char *separators,
-               int (*read_property)(char *, int, char *, int))
+               int (*read_property)(char *, size_t, char *, size_t))
 {
        char buffer[BUFSIZ];
        char *name;
@@ -3747,14 +4173,23 @@ main(int argc, char *argv[])
        signal(SIGINT, quit);
 
        if (setlocale(LC_ALL, "")) {
-               string_copy(opt_codeset, nl_langinfo(CODESET));
+               char *codeset = nl_langinfo(CODESET);
+
+               string_ncopy(opt_codeset, codeset, strlen(codeset));
        }
 
+       if (load_repo_info() == ERR)
+               die("Failed to load repo info.");
+
+       /* Require a git repository unless when running in pager mode. */
+       if (!opt_git_dir[0])
+               die("Not a git repository");
+
        if (load_options() == ERR)
                die("Failed to load user config.");
 
        /* Load the repo config file so options can be overwritten from
-        * the command line.  */
+        * the command line. */
        if (load_repo_config() == ERR)
                die("Failed to load repo config.");
 
@@ -3770,10 +4205,6 @@ main(int argc, char *argv[])
        if (load_refs() == ERR)
                die("Failed to load refs.");
 
-       /* Require a git repository unless when running in pager mode. */
-       if (refs_size == 0 && opt_request != REQ_VIEW_PAGER)
-               die("Not a git repository");
-
        for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
                view->cmd_env = getenv(view->cmd_env);
 
@@ -3827,7 +4258,7 @@ main(int argc, char *argv[])
                        char *search = read_prompt(prompt);
 
                        if (search)
-                               string_copy(opt_search, search);
+                               string_ncopy(opt_search, search, strlen(search));
                        else
                                request = REQ_NONE;
                        break;