utf8_length: add reserve flag for reserving a trailing character
[tig] / tig.c
diff --git a/tig.c b/tig.c
index cb8f6be..25cf31b 100644 (file)
--- a/tig.c
+++ b/tig.c
@@ -42,6 +42,9 @@
 #include <langinfo.h>
 #include <iconv.h>
 
+/* ncurses(3): Must be defined to have extended wide-character functions. */
+#define _XOPEN_SOURCE_EXTENDED
+
 #include <curses.h>
 
 #if __GNUC__ >= 3
 #endif
 
 static void __NORETURN die(const char *err, ...);
+static void warn(const char *msg, ...);
 static void report(const char *msg, ...);
 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);
+static size_t utf8_length(const char *string, size_t max_width, int *trimmed, bool reserve);
 
 #define ABS(x)         ((x) >= 0  ? (x) : -(x))
 #define MIN(x, y)      ((x) < (y) ? (x) :  (y))
@@ -72,6 +76,7 @@ static size_t utf8_length(const char *string, size_t max_width, int *coloffset,
 #define REVGRAPH_MERGE 'M'
 #define REVGRAPH_BRANCH        '+'
 #define REVGRAPH_COMMIT        '*'
+#define REVGRAPH_BOUND '^'
 #define REVGRAPH_LINE  '|'
 
 #define SIZEOF_REVGRAPH        19      /* Size of revision ancestry graphics. */
@@ -105,13 +110,13 @@ static size_t utf8_length(const char *string, size_t max_width, int *coloffset,
        "git ls-remote $(git rev-parse --git-dir) 2>/dev/null"
 
 #define TIG_DIFF_CMD \
-       "git show --root --patch-with-stat --find-copies-harder -B -C %s 2>/dev/null"
+       "git show --pretty=fuller --no-color --root --patch-with-stat --find-copies-harder -C %s 2>/dev/null"
 
 #define TIG_LOG_CMD    \
-       "git log --cc --stat -n100 %s 2>/dev/null"
+       "git log --no-color --cc --stat -n100 %s 2>/dev/null"
 
 #define TIG_MAIN_CMD \
-       "git log --topo-order --pretty=raw %s 2>/dev/null"
+       "git log --no-color --topo-order --boundary --pretty=raw %s 2>/dev/null"
 
 #define TIG_TREE_CMD   \
        "git ls-tree %s %s"
@@ -352,8 +357,8 @@ sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
        REQ_(TOGGLE_REV_GRAPH,  "Toggle revision graph visualization"), \
        REQ_(STATUS_UPDATE,     "Update file status"), \
        REQ_(STATUS_MERGE,      "Merge file using external tool"), \
+       REQ_(TREE_PARENT,       "Switch to parent directory in tree view"), \
        REQ_(EDIT,              "Open in editor"), \
-       REQ_(CHERRY_PICK,       "Cherry-pick commit to current branch"), \
        REQ_(NONE,              "Do nothing")
 
 
@@ -407,22 +412,14 @@ get_request(const char *name)
 static const char usage[] =
 "tig " TIG_VERSION " (" __DATE__ ")\n"
 "\n"
-"Usage: tig [options]\n"
-"   or: tig [options] [--] [git log options]\n"
-"   or: tig [options] log  [git log options]\n"
-"   or: tig [options] diff [git diff options]\n"
-"   or: tig [options] show [git show options]\n"
-"   or: tig [options] <    [git command output]\n"
+"Usage: tig        [options] [revs] [--] [paths]\n"
+"   or: tig show   [options] [revs] [--] [paths]\n"
+"   or: tig status\n"
+"   or: tig <      [git command output]\n"
 "\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"
-"  -v, --version               Show version and exit\n"
-"  -h, --help                  Show help message and exit\n";
+"  -v, --version   Show version and exit\n"
+"  -h, --help      Show help message and exit\n";
 
 /* Option and state variables. */
 static bool opt_line_number            = FALSE;
@@ -440,7 +437,7 @@ 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]    = "";
-static char opt_is_inside_work_tree    = -1; /* set to TRUE or FALSE */
+static signed char opt_is_inside_work_tree     = -1; /* set to TRUE or FALSE */
 static char opt_editor[SIZEOF_STR]     = "";
 
 enum option_type {
@@ -490,45 +487,41 @@ check_option(char *opt, char short_name, char *name, enum option_type type, ...)
 static bool
 parse_options(int argc, char *argv[])
 {
+       char *altargv[1024];
+       int altargc = 0;
+       char *subcommand = NULL;
        int i;
 
        for (i = 1; i < argc; i++) {
                char *opt = argv[i];
 
                if (!strcmp(opt, "log") ||
-                   !strcmp(opt, "diff") ||
-                   !strcmp(opt, "show")) {
+                   !strcmp(opt, "diff")) {
+                       subcommand = opt;
                        opt_request = opt[0] == 'l'
                                    ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
+                       warn("`tig %s' has been deprecated", opt);
                        break;
                }
 
-               if (opt[0] && opt[0] != '-')
-                       break;
-
-               if (!strcmp(opt, "-l")) {
-                       opt_request = REQ_VIEW_LOG;
-                       continue;
-               }
-
-               if (!strcmp(opt, "-d")) {
+               if (!strcmp(opt, "show")) {
+                       subcommand = opt;
                        opt_request = REQ_VIEW_DIFF;
-                       continue;
+                       break;
                }
 
-               if (!strcmp(opt, "-S")) {
+               if (!strcmp(opt, "status")) {
+                       subcommand = opt;
                        opt_request = REQ_VIEW_STATUS;
-                       continue;
+                       break;
                }
 
-               if (check_option(opt, 'n', "line-number", OPT_INT, &opt_num_interval)) {
-                       opt_line_number = TRUE;
-                       continue;
-               }
+               if (opt[0] && opt[0] != '-')
+                       break;
 
-               if (check_option(opt, 'b', "tab-size", OPT_INT, &opt_tab_size)) {
-                       opt_tab_size = MIN(opt_tab_size, TABSIZE);
-                       continue;
+               if (!strcmp(opt, "--")) {
+                       i++;
+                       break;
                }
 
                if (check_option(opt, 'v', "version", OPT_NONE)) {
@@ -541,29 +534,59 @@ parse_options(int argc, char *argv[])
                        return FALSE;
                }
 
-               if (!strcmp(opt, "--")) {
-                       i++;
-                       break;
+               if (!strcmp(opt, "-S")) {
+                       warn("`%s' has been deprecated; use `tig status' instead", opt);
+                       opt_request = REQ_VIEW_STATUS;
+                       continue;
                }
 
-               die("unknown option '%s'\n\n%s", opt, usage);
+               if (!strcmp(opt, "-l")) {
+                       opt_request = REQ_VIEW_LOG;
+               } else if (!strcmp(opt, "-d")) {
+                       opt_request = REQ_VIEW_DIFF;
+               } else if (check_option(opt, 'n', "line-number", OPT_INT, &opt_num_interval)) {
+                       opt_line_number = TRUE;
+               } else if (check_option(opt, 'b', "tab-size", OPT_INT, &opt_tab_size)) {
+                       opt_tab_size = MIN(opt_tab_size, TABSIZE);
+               } else {
+                       if (altargc >= ARRAY_SIZE(altargv))
+                               die("maximum number of arguments exceeded");
+                       altargv[altargc++] = opt;
+                       continue;
+               }
+
+               warn("`%s' has been deprecated", opt);
        }
 
+       /* Check that no 'alt' arguments occured before a subcommand. */
+       if (subcommand && i < argc && altargc > 0)
+               die("unknown arguments before `%s'", argv[i]);
+
        if (!isatty(STDIN_FILENO)) {
                opt_request = REQ_VIEW_PAGER;
                opt_pipe = stdin;
 
-       } else if (i < argc) {
+       } else if (opt_request == REQ_VIEW_STATUS) {
+               if (argc - i > 1)
+                       warn("ignoring arguments after `%s'", argv[i]);
+
+       } else if (i < argc || altargc > 0) {
+               int alti = 0;
                size_t buf_size;
 
                if (opt_request == REQ_VIEW_MAIN)
                        /* XXX: This is vulnerable to the user overriding
                         * options required for the main view parser. */
-                       string_copy(opt_cmd, "git log --pretty=raw");
+                       string_copy(opt_cmd, "git log --no-color --pretty=raw --boundary");
                else
                        string_copy(opt_cmd, "git");
                buf_size = strlen(opt_cmd);
 
+               while (buf_size < sizeof(opt_cmd) && alti < altargc) {
+                       opt_cmd[buf_size++] = ' ';
+                       buf_size = sq_quote(opt_cmd, buf_size, altargv[alti++]);
+               }
+
                while (buf_size < sizeof(opt_cmd) && i < argc) {
                        opt_cmd[buf_size++] = ' ';
                        buf_size = sq_quote(opt_cmd, buf_size, argv[i++]);
@@ -627,6 +650,7 @@ LINE(MAIN_DELIM,   "",                      COLOR_MAGENTA,  COLOR_DEFAULT,  0), \
 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(MAIN_REVGRAPH,"",                 COLOR_MAGENTA,  COLOR_DEFAULT,  0), \
 LINE(TREE_DIR,     "",                 COLOR_DEFAULT,  COLOR_DEFAULT,  A_NORMAL), \
 LINE(TREE_FILE,    "",                 COLOR_DEFAULT,  COLOR_DEFAULT,  A_NORMAL), \
 LINE(STAT_SECTION, "",                 COLOR_CYAN,     COLOR_DEFAULT,  0), \
@@ -695,15 +719,15 @@ get_line_info(char *name, int namelen)
 static void
 init_colors(void)
 {
-       int default_bg = COLOR_BLACK;
-       int default_fg = COLOR_WHITE;
+       int default_bg = line_info[LINE_DEFAULT].bg;
+       int default_fg = line_info[LINE_DEFAULT].fg;
        enum line_type type;
 
        start_color();
 
-       if (use_default_colors() != ERR) {
-               default_bg = -1;
-               default_fg = -1;
+       if (assume_default_colors(default_fg, default_bg) == ERR) {
+               default_bg = COLOR_BLACK;
+               default_fg = COLOR_WHITE;
        }
 
        for (type = 0; type < ARRAY_SIZE(line_info); type++) {
@@ -788,8 +812,8 @@ static struct keybinding default_keybindings[] = {
        { ':',          REQ_PROMPT },
        { 'u',          REQ_STATUS_UPDATE },
        { 'M',          REQ_STATUS_MERGE },
+       { ',',          REQ_TREE_PARENT },
        { 'e',          REQ_EDIT },
-       { 'C',          REQ_CHERRY_PICK },
 
        /* Using the ncurses SIGWINCH handler. */
        { KEY_RESIZE,   REQ_SCREEN_RESIZE },
@@ -915,10 +939,30 @@ get_key_value(const char *name)
 }
 
 static char *
+get_key_name(int key_value)
+{
+       static char key_char[] = "'X'";
+       char *seq = NULL;
+       int key;
+
+       for (key = 0; key < ARRAY_SIZE(key_table); key++)
+               if (key_table[key].value == key_value)
+                       seq = key_table[key].name;
+
+       if (seq == NULL &&
+           key_value < 127 &&
+           isprint(key_value)) {
+               key_char[1] = (char) key_value;
+               seq = key_char;
+       }
+
+       return seq ? seq : "'?'";
+}
+
+static char *
 get_key(enum request request)
 {
        static char buf[BUFSIZ];
-       static char key_char[] = "'X'";
        size_t pos = 0;
        char *sep = "";
        int i;
@@ -927,27 +971,12 @@ get_key(enum request request)
 
        for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
                struct keybinding *keybinding = &default_keybindings[i];
-               char *seq = NULL;
-               int key;
 
                if (keybinding->request != request)
                        continue;
 
-               for (key = 0; key < ARRAY_SIZE(key_table); key++)
-                       if (key_table[key].value == keybinding->alias)
-                               seq = key_table[key].name;
-
-               if (seq == NULL &&
-                   keybinding->alias < 127 &&
-                   isprint(keybinding->alias)) {
-                       key_char[1] = (char) keybinding->alias;
-                       seq = key_char;
-               }
-
-               if (!seq)
-                       seq = "'?'";
-
-               if (!string_format_from(buf, &pos, "%s%s", sep, seq))
+               if (!string_format_from(buf, &pos, "%s%s", sep,
+                                       get_key_name(keybinding->alias)))
                        return "Too many keybindings!";
                sep = ", ";
        }
@@ -955,6 +984,67 @@ get_key(enum request request)
        return buf;
 }
 
+struct run_request {
+       enum keymap keymap;
+       int key;
+       char cmd[SIZEOF_STR];
+};
+
+static struct run_request *run_request;
+static size_t run_requests;
+
+static enum request
+add_run_request(enum keymap keymap, int key, int argc, char **argv)
+{
+       struct run_request *tmp;
+       struct run_request req = { keymap, key };
+       size_t bufpos;
+
+       for (bufpos = 0; argc > 0; argc--, argv++)
+               if (!string_format_from(req.cmd, &bufpos, "%s ", *argv))
+                       return REQ_NONE;
+
+       req.cmd[bufpos - 1] = 0;
+
+       tmp = realloc(run_request, (run_requests + 1) * sizeof(*run_request));
+       if (!tmp)
+               return REQ_NONE;
+
+       run_request = tmp;
+       run_request[run_requests++] = req;
+
+       return REQ_NONE + run_requests;
+}
+
+static struct run_request *
+get_run_request(enum request request)
+{
+       if (request <= REQ_NONE)
+               return NULL;
+       return &run_request[request - REQ_NONE - 1];
+}
+
+static void
+add_builtin_run_requests(void)
+{
+       struct {
+               enum keymap keymap;
+               int key;
+               char *argv[1];
+       } reqs[] = {
+               { KEYMAP_MAIN,    'C', { "git cherry-pick %(commit)" } },
+               { KEYMAP_GENERIC, 'G', { "git gc" } },
+       };
+       int i;
+
+       for (i = 0; i < ARRAY_SIZE(reqs); i++) {
+               enum request req;
+
+               req = add_run_request(reqs[i].keymap, reqs[i].key, 1, reqs[i].argv);
+               if (req != REQ_NONE)
+                       add_keybinding(reqs[i].keymap, req, reqs[i].key);
+       }
+}
 
 /*
  * User config file handling.
@@ -1087,7 +1177,7 @@ option_bind_command(int argc, char *argv[])
        int keymap;
        int key;
 
-       if (argc != 3) {
+       if (argc < 3) {
                config_msg = "Wrong number of arguments given to bind command";
                return ERR;
        }
@@ -1105,6 +1195,21 @@ option_bind_command(int argc, char *argv[])
 
        request = get_request(argv[2]);
        if (request == REQ_NONE) {
+               const char *obsolete[] = { "cherry-pick" };
+               size_t namelen = strlen(argv[2]);
+               int i;
+
+               for (i = 0; i < ARRAY_SIZE(obsolete); i++) {
+                       if (namelen == strlen(obsolete[i]) &&
+                           !string_enum_compare(obsolete[i], argv[2], namelen)) {
+                               config_msg = "Obsolete request name";
+                               return ERR;
+                       }
+               }
+       }
+       if (request == REQ_NONE && *argv[2]++ == '!')
+               request = add_run_request(keymap, key, argc - 2, argv + 2);
+       if (request == REQ_NONE) {
                config_msg = "Unknown request name";
                return ERR;
        }
@@ -1124,9 +1229,10 @@ set_option(char *opt, char *value)
        /* Tokenize */
        while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
                argv[argc++] = value;
-
                value += valuelen;
-               if (!*value)
+
+               /* Nothing more to tokenize or last available token. */
+               if (!*value || argc >= ARRAY_SIZE(argv))
                        break;
 
                *value++ = 0;
@@ -1187,27 +1293,47 @@ read_option(char *opt, size_t optlen, char *value, size_t valuelen)
        return OK;
 }
 
-static int
-load_options(void)
+static void
+load_option_file(const char *path)
 {
-       char *home = getenv("HOME");
-       char buf[SIZEOF_STR];
        FILE *file;
 
-       config_lineno = 0;
-       config_errors = FALSE;
-
-       if (!home || !string_format(buf, "%s/.tigrc", home))
-               return ERR;
-
        /* It's ok that the file doesn't exist. */
-       file = fopen(buf, "r");
+       file = fopen(path, "r");
        if (!file)
-               return OK;
+               return;
+
+       config_lineno = 0;
+       config_errors = FALSE;
 
        if (read_properties(file, " \t", read_option) == ERR ||
            config_errors == TRUE)
-               fprintf(stderr, "Errors while loading %s.\n", buf);
+               fprintf(stderr, "Errors while loading %s.\n", path);
+}
+
+static int
+load_options(void)
+{
+       char *home = getenv("HOME");
+       char *tigrc_user = getenv("TIGRC_USER");
+       char *tigrc_system = getenv("TIGRC_SYSTEM");
+       char buf[SIZEOF_STR];
+
+       add_builtin_run_requests();
+
+       if (!tigrc_system) {
+               if (!string_format(buf, "%s/tigrc", SYSCONFDIR))
+                       return ERR;
+               tigrc_system = buf;
+       }
+       load_option_file(tigrc_system);
+
+       if (!tigrc_user) {
+               if (!home || !string_format(buf, "%s/.tigrc", home))
+                       return ERR;
+               tigrc_user = buf;
+       }
+       load_option_file(tigrc_user);
 
        return OK;
 }
@@ -1330,6 +1456,43 @@ static struct view views[] = {
 #define view_is_displayed(view) \
        (view == display[0] || view == display[1])
 
+static int
+draw_text(struct view *view, const char *string, int max_len, int col,
+         bool use_tilde, int tilde_attr)
+{
+       int n;
+
+       n = 0;
+       if (max_len > 0) {
+               int len;
+               int trimmed = FALSE;
+
+               if (opt_utf8) {
+                       len = utf8_length(string, max_len, &trimmed, use_tilde);
+                       n = len;
+               } else {
+                       len = strlen(string);
+                       if (len > max_len) {
+                               if (use_tilde) {
+                                       max_len -= 1;
+                               }
+                               len = max_len;
+                               trimmed = TRUE;
+                       }
+                       n = len;
+               }
+               waddnstr(view->win, string, n);
+               if (trimmed && use_tilde) {
+                       if (tilde_attr != -1)
+                               wattrset(view->win, tilde_attr);
+                       waddch(view->win, '~');
+                       n++;
+               }
+       }
+
+       return n;
+}
+
 static bool
 draw_view_line(struct view *view, unsigned int lineno)
 {
@@ -2194,6 +2357,56 @@ open_editor(bool from_root, const char *file)
        }
 }
 
+static void
+open_run_request(enum request request)
+{
+       struct run_request *req = get_run_request(request);
+       char buf[SIZEOF_STR * 2];
+       size_t bufpos;
+       char *cmd;
+
+       if (!req) {
+               report("Unknown run request");
+               return;
+       }
+
+       bufpos = 0;
+       cmd = req->cmd;
+
+       while (cmd) {
+               char *next = strstr(cmd, "%(");
+               int len = next - cmd;
+               char *value;
+
+               if (!next) {
+                       len = strlen(cmd);
+                       value = "";
+
+               } else if (!strncmp(next, "%(head)", 7)) {
+                       value = ref_head;
+
+               } else if (!strncmp(next, "%(commit)", 9)) {
+                       value = ref_commit;
+
+               } else if (!strncmp(next, "%(blob)", 7)) {
+                       value = ref_blob;
+
+               } else {
+                       report("Unknown replacement in run request: `%s`", req->cmd);
+                       return;
+               }
+
+               if (!string_format_from(buf, &bufpos, "%.*s%s", len, cmd, value))
+                       return;
+
+               if (next)
+                       next = strchr(next, ')') + 1;
+               cmd = next;
+       }
+
+       open_external_viewer(buf);
+}
+
 /*
  * User request switch noodle
  */
@@ -2208,6 +2421,11 @@ view_driver(struct view *view, enum request request)
                return TRUE;
        }
 
+       if (request > REQ_NONE) {
+               open_run_request(request);
+               return TRUE;
+       }
+
        if (view && view->lines) {
                request = view->ops->request(view, request, &view->line[view->lineno]);
                if (request == REQ_NONE)
@@ -2369,9 +2587,6 @@ view_driver(struct view *view, enum request request)
                report("Nothing to edit");
                break;
 
-       case REQ_CHERRY_PICK:
-               report("Nothing to cherry-pick");
-               break;
 
        case REQ_ENTER:
                report("Nothing to enter");
@@ -2415,7 +2630,6 @@ pager_draw(struct view *view, struct line *line, unsigned int lineno, bool selec
 {
        char *text = line->data;
        enum line_type type = line->type;
-       int textlen = strlen(text);
        int attr;
 
        wmove(view->win, lineno, 0);
@@ -2468,13 +2682,9 @@ pager_draw(struct view *view, struct line *line, unsigned int lineno, bool selec
                }
 
        } else {
-               int col = 0, pos = 0;
-
-               for (; pos < textlen && col < view->width; pos++, col++)
-                       if (text[pos] == '\t')
-                               col += TABSIZE - (col % TABSIZE) - 1;
+               int tilde_attr = get_line_attr(LINE_MAIN_DELIM);
 
-               waddnstr(view->win, text, pos);
+               draw_text(view, text, view->width, 0, TRUE, tilde_attr);
        }
 
        return TRUE;
@@ -2512,7 +2722,7 @@ static void
 add_pager_refs(struct view *view, struct line *line)
 {
        char buf[SIZEOF_STR];
-       char *commit_id = line->data + STRING_SIZE("commit ");
+       char *commit_id = (char *)line->data + STRING_SIZE("commit ");
        struct ref **refs;
        size_t bufpos = 0, refpos = 0;
        const char *sep = "Refs: ";
@@ -2623,7 +2833,7 @@ static void
 pager_select(struct view *view, struct line *line)
 {
        if (line->type == LINE_COMMIT) {
-               char *text = line->data + STRING_SIZE("commit ");
+               char *text = (char *)line->data + STRING_SIZE("commit ");
 
                if (view != VIEW(REQ_VIEW_PAGER))
                        string_copy_rev(view->ref, text);
@@ -2660,6 +2870,8 @@ help_open(struct view *view)
                if (!req_info[i].request)
                        lines++;
 
+       lines += run_requests + 1;
+
        view->line = calloc(lines, sizeof(*view->line));
        if (!view->line)
                return FALSE;
@@ -2688,6 +2900,30 @@ help_open(struct view *view)
                add_line_text(view, buf, LINE_DEFAULT);
        }
 
+       if (run_requests) {
+               add_line_text(view, "", LINE_DEFAULT);
+               add_line_text(view, "External commands:", LINE_DEFAULT);
+       }
+
+       for (i = 0; i < run_requests; i++) {
+               struct run_request *req = get_run_request(REQ_NONE + i + 1);
+               char *key;
+
+               if (!req)
+                       continue;
+
+               key = get_key_name(req->key);
+               if (!*key)
+                       key = "(no key defined)";
+
+               if (!string_format(buf, "    %-10s %-14s `%s`",
+                                  keymap_table[req->keymap].name,
+                                  key, req->cmd))
+                       continue;
+
+               add_line_text(view, buf, LINE_DEFAULT);
+       }
+
        return TRUE;
 }
 
@@ -2859,6 +3095,16 @@ tree_request(struct view *view, enum request request, struct line *line)
 {
        enum open_flags flags;
 
+       if (request == REQ_TREE_PARENT) {
+               if (*opt_path) {
+                       /* fake 'cd  ..' */
+                       request = REQ_ENTER;
+                       line = &view->line[1];
+               } else {
+                       /* quit view if at top of tree */
+                       return REQ_VIEW_CLOSE;
+               }
+       }
        if (request != REQ_ENTER)
                return request;
 
@@ -2906,7 +3152,7 @@ tree_request(struct view *view, enum request request, struct line *line)
 static void
 tree_select(struct view *view, struct line *line)
 {
-       char *text = line->data + STRING_SIZE("100644 blob ");
+       char *text = (char *)line->data + STRING_SIZE("100644 blob ");
 
        if (line->type == LINE_TREE_FILE) {
                string_copy_rev(ref_blob, text);
@@ -2954,12 +3200,13 @@ struct 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;
-       char name[SIZEOF_STR];
 };
 
 static struct status stage_status;
@@ -2977,7 +3224,7 @@ status_get_diff(struct status *file, char *buf, size_t bufsize)
        char *new_rev  = buf + 56;
        char *status   = buf + 97;
 
-       if (bufsize != 99 ||
+       if (bufsize < 99 ||
            old_mode[-1] != ':' ||
            new_mode[-1] != ' ' ||
            old_rev[-1]  != ' ' ||
@@ -2993,7 +3240,7 @@ status_get_diff(struct status *file, char *buf, size_t bufsize)
        file->old.mode = strtoul(old_mode, NULL, 8);
        file->new.mode = strtoul(new_mode, NULL, 8);
 
-       file->name[0] = 0;
+       file->old.name[0] = file->new.name[0] = 0;
 
        return TRUE;
 }
@@ -3060,7 +3307,7 @@ status_run(struct view *view, const char cmd[], bool diff, enum line_type type)
                                        unmerged = file;
 
                                } else if (unmerged) {
-                                       int collapse = !strcmp(buf, unmerged->name);
+                                       int collapse = !strcmp(buf, unmerged->new.name);
 
                                        unmerged = NULL;
                                        if (collapse) {
@@ -3071,10 +3318,26 @@ status_run(struct view *view, const char cmd[], bool diff, enum line_type type)
                                }
                        }
 
+                       /* Grab the old name for rename/copy. */
+                       if (!*file->old.name &&
+                           (file->status == 'R' || file->status == 'C')) {
+                               sepsize = sep - buf + 1;
+                               string_ncopy(file->old.name, buf, sepsize);
+                               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);
+                       string_ncopy(file->new.name, buf, sepsize);
+                       if (!*file->old.name)
+                               string_copy(file->old.name, file->new.name);
                        bufsize -= sepsize;
                        memmove(buf, sep + 1, bufsize);
                        file = NULL;
@@ -3095,13 +3358,16 @@ error_out:
 }
 
 /* Don't show unmerged entries in the staged section. */
-#define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached HEAD"
-#define STATUS_DIFF_FILES_CMD "git diff-files -z"
+#define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached -M HEAD"
+#define STATUS_DIFF_FILES_CMD "git update-index -q --refresh && 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"
+#define STATUS_DIFF_INDEX_SHOW_CMD \
+       "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s %s 2>/dev/null"
+
+#define STATUS_DIFF_FILES_SHOW_CMD \
+       "git diff-files --root --patch-with-stat -C -M -- %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
@@ -3156,12 +3422,14 @@ static bool
 status_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
 {
        struct status *status = line->data;
+       int tilde_attr = get_line_attr(LINE_MAIN_DELIM);
 
        wmove(view->win, lineno, 0);
 
        if (selected) {
                wattrset(view->win, get_line_attr(LINE_CURSOR));
                wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
+               tilde_attr = -1;
 
        } else if (!status && line->type != LINE_STAT_NONE) {
                wattrset(view->win, get_line_attr(LINE_STAT_SECTION));
@@ -3195,7 +3463,7 @@ status_draw(struct view *view, struct line *line, unsigned int lineno, bool sele
                        return FALSE;
                }
 
-               waddstr(view->win, text);
+               draw_text(view, text, view->width, 0, TRUE, tilde_attr);
                return TRUE;
        }
 
@@ -3203,8 +3471,10 @@ status_draw(struct view *view, struct line *line, unsigned int lineno, bool sele
        if (!selected)
                wattrset(view->win, A_NORMAL);
        wmove(view->win, lineno, 4);
-       waddstr(view->win, status->name);
+       if (view->width < 5)
+               return TRUE;
 
+       draw_text(view, status->new.name, view->width - 5, 5, TRUE, tilde_attr);
        return TRUE;
 }
 
@@ -3212,7 +3482,8 @@ static enum request
 status_enter(struct view *view, struct line *line)
 {
        struct status *status = line->data;
-       char path[SIZEOF_STR] = "";
+       char oldpath[SIZEOF_STR] = "";
+       char newpath[SIZEOF_STR] = "";
        char *info;
        size_t cmdsize = 0;
 
@@ -3222,8 +3493,15 @@ status_enter(struct view *view, struct line *line)
                return REQ_NONE;
        }
 
-       if (status && sq_quote(path, 0, status->name) >= sizeof(path))
-               return REQ_QUIT;
+       if (status) {
+               if (sq_quote(oldpath, 0, status->old.name) >= sizeof(oldpath))
+                       return REQ_QUIT;
+               /* Diffs for unmerged entries are empty when pasing the
+                * new path, so leave it empty. */
+               if (status->status != 'U' &&
+                   sq_quote(newpath, 0, status->new.name) >= sizeof(newpath))
+                       return REQ_QUIT;
+       }
 
        if (opt_cdup[0] &&
            line->type != LINE_STAT_UNTRACKED &&
@@ -3232,8 +3510,8 @@ status_enter(struct view *view, struct line *line)
 
        switch (line->type) {
        case LINE_STAT_STAGED:
-               if (!string_format_from(opt_cmd, &cmdsize, STATUS_DIFF_SHOW_CMD,
-                                       "--cached", path))
+               if (!string_format_from(opt_cmd, &cmdsize,
+                                       STATUS_DIFF_INDEX_SHOW_CMD, oldpath, newpath))
                        return REQ_QUIT;
                if (status)
                        info = "Staged changes to %s";
@@ -3242,8 +3520,8 @@ status_enter(struct view *view, struct line *line)
                break;
 
        case LINE_STAT_UNSTAGED:
-               if (!string_format_from(opt_cmd, &cmdsize, STATUS_DIFF_SHOW_CMD,
-                                       "", path))
+               if (!string_format_from(opt_cmd, &cmdsize,
+                                       STATUS_DIFF_FILES_SHOW_CMD, oldpath, newpath))
                        return REQ_QUIT;
                if (status)
                        info = "Unstaged changes to %s";
@@ -3261,12 +3539,12 @@ status_enter(struct view *view, struct line *line)
                        return REQ_NONE;
                }
 
-               opt_pipe = fopen(status->name, "r");
+               opt_pipe = fopen(status->new.name, "r");
                info = "Untracked file %s";
                break;
 
        default:
-               die("w00t");
+               die("line type %d not handled in switch", line->type);
        }
 
        open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | OPEN_SPLIT);
@@ -3278,7 +3556,7 @@ status_enter(struct view *view, struct line *line)
                }
 
                stage_line_type = line->type;
-               string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.name);
+               string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.new.name);
        }
 
        return REQ_NONE;
@@ -3305,7 +3583,7 @@ status_update_file(struct view *view, struct status *status, enum line_type type
                if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
                                        status->old.mode,
                                        status->old.rev,
-                                       status->name, 0))
+                                       status->old.name, 0))
                        return FALSE;
 
                string_add(cmd, cmdsize, "git update-index -z --index-info");
@@ -3313,14 +3591,14 @@ status_update_file(struct view *view, struct status *status, enum line_type type
 
        case LINE_STAT_UNSTAGED:
        case LINE_STAT_UNTRACKED:
-               if (!string_format_from(buf, &bufsize, "%s%c", status->name, 0))
+               if (!string_format_from(buf, &bufsize, "%s%c", status->new.name, 0))
                        return FALSE;
 
                string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
                break;
 
        default:
-               die("w00t");
+               die("line type %d not handled in switch", type);
        }
 
        pipe = popen(cmd, "w");
@@ -3373,14 +3651,18 @@ status_request(struct view *view, enum request request, struct line *line)
                break;
 
        case REQ_STATUS_MERGE:
-               open_mergetool(status->name);
+               if (!status || status->status != 'U') {
+                       report("Merging only possible for files with unmerged status ('U').");
+                       return REQ_NONE;
+               }
+               open_mergetool(status->new.name);
                break;
 
        case REQ_EDIT:
                if (!status)
                        return request;
 
-               open_editor(status->status != '?', status->name);
+               open_editor(status->status != '?', status->new.name);
                break;
 
        case REQ_ENTER:
@@ -3411,7 +3693,7 @@ status_select(struct view *view, struct line *line)
        char *text;
        char *key;
 
-       if (status && !string_format(file, "'%s'", status->name))
+       if (status && !string_format(file, "'%s'", status->new.name))
                return;
 
        if (!status && line[1].type == LINE_STAT_NONE)
@@ -3435,7 +3717,7 @@ status_select(struct view *view, struct line *line)
                break;
 
        default:
-               die("w00t");
+               die("line type %d not handled in switch", line->type);
        }
 
        if (status && status->status == 'U') {
@@ -3464,7 +3746,7 @@ status_grep(struct view *view, struct line *line)
                char *text;
 
                switch (state) {
-               case S_NAME:    text = status->name;    break;
+               case S_NAME:    text = status->new.name;        break;
                case S_STATUS:
                        buf[0] = status->status;
                        text = buf;
@@ -3629,10 +3911,10 @@ stage_request(struct view *view, enum request request, struct line *line)
                break;
 
        case REQ_EDIT:
-               if (!stage_status.name[0])
+               if (!stage_status.new.name[0])
                        return request;
 
-               open_editor(stage_status.status != '?', stage_status.name);
+               open_editor(stage_status.status != '?', stage_status.new.name);
                break;
 
        case REQ_ENTER:
@@ -3680,6 +3962,7 @@ struct rev_graph {
        size_t size;
        struct commit *commit;
        size_t pos;
+       unsigned int boundary:1;
 };
 
 /* Parents of the commit being visualized. */
@@ -3752,7 +4035,9 @@ get_rev_graph_symbol(struct rev_graph *graph)
 {
        chtype symbol;
 
-       if (graph->parents->size == 0)
+       if (graph->boundary)
+               symbol = REVGRAPH_BOUND;
+       else if (graph->parents->size == 0)
                symbol = REVGRAPH_INIT;
        else if (graph_parent_is_merge(graph))
                symbol = REVGRAPH_MERGE;
@@ -3834,7 +4119,7 @@ prepare_rev_graph(struct rev_graph *graph)
        }
 
        /* Interleave the new revision parent(s). */
-       for (i = 0; i < graph->parents->size; i++)
+       for (i = 0; !graph->boundary && i < graph->parents->size; i++)
                push_rev_graph(graph->next, graph->parents->rev[i]);
 
        /* Lastly, put any remaining revisions. */
@@ -3871,68 +4156,78 @@ main_draw(struct view *view, struct line *line, unsigned int lineno, bool select
        enum line_type type;
        int col = 0;
        size_t timelen;
-       size_t authorlen;
-       int trimmed = 1;
+       int tilde_attr;
+       int space;
 
        if (!*commit->author)
                return FALSE;
 
+       space = view->width;
        wmove(view->win, lineno, col);
 
        if (selected) {
                type = LINE_CURSOR;
                wattrset(view->win, get_line_attr(type));
                wchgat(view->win, -1, 0, type, NULL);
-
+               tilde_attr = -1;
        } else {
                type = LINE_MAIN_COMMIT;
                wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
+               tilde_attr = get_line_attr(LINE_MAIN_DELIM);
        }
 
-       timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
-       waddnstr(view->win, buf, timelen);
-       waddstr(view->win, " ");
+       {
+               int n;
 
-       col += DATE_COLS;
-       wmove(view->win, lineno, col);
-       if (type != LINE_CURSOR)
-               wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
+               timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
+               n = draw_text(
+                       view, buf, view->width - col, col, FALSE, tilde_attr);
+               draw_text(
+                       view, " ", view->width - col - n, col + n, FALSE,
+                       tilde_attr);
 
-       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;
-               }
+               col += DATE_COLS;
+               wmove(view->win, lineno, col);
+               if (col >= view->width)
+                       return TRUE;
        }
+       if (type != LINE_CURSOR)
+               wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
 
-       if (trimmed) {
-               waddnstr(view->win, commit->author, authorlen);
-               if (type != LINE_CURSOR)
-                       wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
-               waddch(view->win, '~');
-       } else {
-               waddstr(view->win, commit->author);
+       {
+               int max_len;
+
+               max_len = view->width - col;
+               if (max_len > AUTHOR_COLS - 1)
+                       max_len = AUTHOR_COLS - 1;
+               draw_text(
+                       view, commit->author, max_len, col, TRUE, tilde_attr);
+               col += AUTHOR_COLS;
+               if (col >= view->width)
+                       return TRUE;
        }
 
-       col += AUTHOR_COLS;
-       if (type != LINE_CURSOR)
-               wattrset(view->win, A_NORMAL);
-
        if (opt_rev_graph && commit->graph_size) {
+               size_t graph_size = view->width - col;
                size_t i;
 
+               if (type != LINE_CURSOR)
+                       wattrset(view->win, get_line_attr(LINE_MAIN_REVGRAPH));
                wmove(view->win, lineno, col);
+               if (graph_size > commit->graph_size)
+                       graph_size = commit->graph_size;
                /* Using waddch() instead of waddnstr() ensures that
                 * they'll be rendered correctly for the cursor line. */
-               for (i = 0; i < commit->graph_size; i++)
+               for (i = 0; i < graph_size; i++)
                        waddch(view->win, commit->graph[i]);
 
-               waddch(view->win, ' ');
                col += commit->graph_size + 1;
+               if (col >= view->width)
+                       return TRUE;
+               waddch(view->win, ' ');
        }
+       if (type != LINE_CURSOR)
+               wattrset(view->win, A_NORMAL);
 
        wmove(view->win, lineno, col);
 
@@ -3948,27 +4243,31 @@ main_draw(struct view *view, struct line *line, unsigned int lineno, bool select
                                wattrset(view->win, get_line_attr(LINE_MAIN_REMOTE));
                        else
                                wattrset(view->win, get_line_attr(LINE_MAIN_REF));
-                       waddstr(view->win, "[");
-                       waddstr(view->win, commit->refs[i]->name);
-                       waddstr(view->win, "]");
+
+                       col += draw_text(
+                               view, "[", view->width - col, col, TRUE,
+                               tilde_attr);
+                       col += draw_text(
+                               view, commit->refs[i]->name, view->width - col,
+                               col, TRUE, tilde_attr);
+                       col += draw_text(
+                               view, "]", view->width - col, col, TRUE,
+                               tilde_attr);
                        if (type != LINE_CURSOR)
                                wattrset(view->win, A_NORMAL);
-                       waddstr(view->win, " ");
-                       col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
+                       col += draw_text(
+                               view, " ", view->width - col, col, TRUE,
+                               tilde_attr);
+                       if (col >= view->width)
+                               return TRUE;
                } while (commit->refs[i++]->next);
        }
 
        if (type != LINE_CURSOR)
                wattrset(view->win, get_line_attr(type));
 
-       {
-               int titlelen = strlen(commit->title);
-
-               if (col + titlelen > view->width)
-                       titlelen = view->width - col;
-
-               waddnstr(view->win, commit->title, titlelen);
-       }
+       col += draw_text(
+               view, commit->title, view->width - col, col, TRUE, tilde_attr);
 
        return TRUE;
 }
@@ -3992,7 +4291,13 @@ main_read(struct view *view, char *line)
                if (!commit)
                        return FALSE;
 
-               string_copy_rev(commit->id, line + STRING_SIZE("commit "));
+               line += STRING_SIZE("commit ");
+               if (*line == '-') {
+                       graph->boundary = 1;
+                       line++;
+               }
+
+               string_copy_rev(commit->id, line);
                commit->refs = get_refs(commit->id);
                graph->commit = commit;
                add_line_data(view, commit, LINE_MAIN_COMMIT);
@@ -4083,20 +4388,6 @@ main_read(struct view *view, char *line)
        return TRUE;
 }
 
-static void
-cherry_pick_commit(struct commit *commit)
-{
-       char cmd[SIZEOF_STR];
-       char *cherry_pick = getenv("TIG_CHERRY_PICK");
-
-       if (!cherry_pick)
-               cherry_pick = "git cherry-pick";
-
-       if (string_format(cmd, "%s %s", cherry_pick, commit->id)) {
-               open_external_viewer(cmd);
-       }
-}
-
 static enum request
 main_request(struct view *view, enum request request, struct line *line)
 {
@@ -4104,8 +4395,6 @@ main_request(struct view *view, enum request request, struct line *line)
 
        if (request == REQ_ENTER)
                open_view(view, REQ_VIEW_DIFF, flags);
-       else if (request == REQ_CHERRY_PICK)
-               cherry_pick_commit(line->data);
        else
                return request;
 
@@ -4193,6 +4482,9 @@ unicode_width(unsigned long c)
            || (c >= 0x30000 && c <= 0x3fffd)))
                return 2;
 
+       if (c == '\t')
+               return opt_tab_size;
+
        return 1;
 }
 
@@ -4260,19 +4552,16 @@ utf8_to_unicode(const char *string, size_t length)
 
 /* Calculates how much of string can be shown within the given maximum width
  * and sets trimmed parameter to non-zero value if all of string could not be
- * shown.
- *
- * Additionally, adds to coloffset how many many columns to move to align with
- * the expected position. Takes into account how multi-byte and double-width
- * characters will effect the cursor position.
+ * shown. If the reserve flag is TRUE, it will reserve at least one
+ * trailing character, which can be useful when drawing a delimiter.
  *
  * Returns the number of bytes to output from string to satisfy max_width. */
 static size_t
-utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
+utf8_length(const char *string, size_t max_width, int *trimmed, bool reserve)
 {
        const char *start = string;
        const char *end = strchr(string, '\0');
-       size_t mbwidth = 0;
+       unsigned char last_bytes = 0;
        size_t width = 0;
 
        *trimmed = 0;
@@ -4298,27 +4587,16 @@ utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
                width  += ucwidth;
                if (width > max_width) {
                        *trimmed = 1;
+                       if (reserve && width - ucwidth == max_width) {
+                               string -= last_bytes;
+                       }
                        break;
                }
 
-               /* The column offset collects the differences between the
-                * number of bytes encoding a character and the number of
-                * columns will be used for rendering said character.
-                *
-                * So if some character A is encoded in 2 bytes, but will be
-                * represented on the screen using only 1 byte this will and up
-                * adding 1 to the multi-byte column offset.
-                *
-                * Assumes that no double-width character can be encoding in
-                * less than two bytes. */
-               if (bytes > ucwidth)
-                       mbwidth += bytes - ucwidth;
-
                string  += bytes;
+               last_bytes = bytes;
        }
 
-       *coloffset += mbwidth;
-
        return string - start;
 }
 
@@ -4735,6 +5013,18 @@ die(const char *err, ...)
        exit(1);
 }
 
+static void
+warn(const char *msg, ...)
+{
+       va_list args;
+
+       va_start(args, msg);
+       fputs("tig warning: ", stderr);
+       vfprintf(stderr, msg, args);
+       fputs("\n", stderr);
+       va_end(args);
+}
+
 int
 main(int argc, char *argv[])
 {