Code

Handle blaming beyond the creation of file more gracefully
[tig.git] / tig.c
diff --git a/tig.c b/tig.c
index c5e3380bbd5a267f38639a02647b66e76f14d29e..13c167d3c49f772d12dcd680eda2658c8d5181bb 100644 (file)
--- a/tig.c
+++ b/tig.c
@@ -188,20 +188,6 @@ string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
 #define string_add(dst, from, src) \
        string_ncopy_do(dst + (from), sizeof(dst) - (from), src, sizeof(src))
 
-static size_t
-string_expand_length(const char *line, int tabsize)
-{
-       size_t size, pos;
-
-       for (pos = 0; line[pos]; pos++) {
-               if (line[pos] == '\t' && tabsize > 0)
-                       size += tabsize - (size % tabsize);
-               else
-                       size++;
-       }
-       return size;
-}
-
 static void
 string_expand(char *dst, size_t dstlen, const char *src, int tabsize)
 {
@@ -676,20 +662,24 @@ io_write(struct io *io, const void *buf, size_t bufsize)
 }
 
 static bool
-run_io_buf(const char **argv, char buf[], size_t bufsize)
+io_read_buf(struct io *io, char buf[], size_t bufsize)
 {
-       struct io io = {};
        bool error;
 
-       if (!run_io_rd(&io, argv, FORMAT_NONE))
-               return FALSE;
+       io->buf = io->bufpos = buf;
+       io->bufalloc = bufsize;
+       error = !io_get(io, '\n', TRUE) && io_error(io);
+       io->buf = NULL;
 
-       io.buf = io.bufpos = buf;
-       io.bufalloc = bufsize;
-       error = !io_get(&io, '\n', TRUE) && io_error(&io);
-       io.buf = NULL;
+       return done_io(io) || error;
+}
 
-       return done_io(&io) || error;
+static bool
+run_io_buf(const char **argv, char buf[], size_t bufsize)
+{
+       struct io io = {};
+
+       return run_io_rd(&io, argv, FORMAT_NONE) && io_read_buf(&io, buf, bufsize);
 }
 
 static int
@@ -1360,6 +1350,10 @@ add_builtin_run_requests(void)
  * User config file handling.
  */
 
+static int   config_lineno;
+static bool  config_errors;
+static const char *config_msg;
+
 static struct enum_map color_map[] = {
 #define COLOR_MAP(name) ENUM_MAP(#name, COLOR_##name)
        COLOR_MAP(DEFAULT),
@@ -1384,12 +1378,31 @@ static struct enum_map attr_map[] = {
        ATTR_MAP(UNDERLINE),
 };
 
-#define set_color(color, name)         map_enum(color, color_map, name)
 #define set_attribute(attr, name)      map_enum(attr, attr_map, name)
 
-static int   config_lineno;
-static bool  config_errors;
-static const char *config_msg;
+static int
+parse_int(int *opt, const char *arg, int min, int max)
+{
+       int value = atoi(arg);
+
+       if (min <= value && value <= max) {
+               *opt = value;
+               return OK;
+       }
+
+       config_msg = "Integer value out of bound";
+       return ERR;
+}
+
+static bool
+set_color(int *color, const char *name)
+{
+       if (map_enum(color, color_map, name))
+               return TRUE;
+       if (!prefixcmp(name, "color"))
+               return parse_int(color, name + 5, 0, 255) == OK;
+       return FALSE;
+}
 
 /* Wants: object fgcolor bgcolor [attribute] */
 static int
@@ -1404,19 +1417,18 @@ option_color_command(int argc, const char *argv[])
 
        info = get_line_info(argv[0]);
        if (!info) {
-               if (!string_enum_compare(argv[0], "main-delim", strlen("main-delim"))) {
-                       info = get_line_info("delimiter");
-
-               } else if (!string_enum_compare(argv[0], "main-date", strlen("main-date"))) {
-                       info = get_line_info("date");
-
-               } else if (!string_enum_compare(argv[0], "main-author", strlen("main-author"))) {
-                       info = get_line_info("author");
+               static struct enum_map obsolete[] = {
+                       ENUM_MAP("main-delim",  LINE_DELIMITER),
+                       ENUM_MAP("main-date",   LINE_DATE),
+                       ENUM_MAP("main-author", LINE_AUTHOR),
+               };
+               int index;
 
-               } else {
+               if (!map_enum(&index, obsolete, argv[0])) {
                        config_msg = "Unknown color name";
                        return ERR;
                }
+               info = &line_info[index];
        }
 
        if (!set_color(&info->fg, argv[1]) ||
@@ -1440,16 +1452,6 @@ static int parse_bool(bool *opt, const char *arg)
        return OK;
 }
 
-static int
-parse_int(int *opt, const char *arg, int min, int max)
-{
-       int value = atoi(arg);
-
-       if (min <= value && value <= max)
-               *opt = value;
-       return OK;
-}
-
 static int
 parse_string(char *opt, const char *arg, size_t optsize)
 {
@@ -1543,23 +1545,16 @@ option_bind_command(int argc, const char *argv[])
 
        request = get_request(argv[2]);
        if (request == REQ_NONE) {
-               struct {
-                       const char *name;
-                       enum request request;
-               } obsolete[] = {
-                       { "cherry-pick",        REQ_NONE },
-                       { "screen-resize",      REQ_NONE },
-                       { "tree-parent",        REQ_PARENT },
+               static struct enum_map obsolete[] = {
+                       ENUM_MAP("cherry-pick",         REQ_NONE),
+                       ENUM_MAP("screen-resize",       REQ_NONE),
+                       ENUM_MAP("tree-parent",         REQ_PARENT),
                };
-               size_t namelen = strlen(argv[2]);
-               int i;
+               int alias;
 
-               for (i = 0; i < ARRAY_SIZE(obsolete); i++) {
-                       if (namelen != strlen(obsolete[i].name) ||
-                           string_enum_compare(obsolete[i].name, argv[2], namelen))
-                               continue;
-                       if (obsolete[i].request != REQ_NONE)
-                               add_keybinding(keymap, obsolete[i].request, key);
+               if (map_enum(&alias, obsolete, argv[2])) {
+                       if (alias != REQ_NONE)
+                               add_keybinding(keymap, alias, key);
                        config_msg = "Obsolete request name";
                        return ERR;
                }
@@ -1667,11 +1662,8 @@ load_options(void)
 
        add_builtin_run_requests();
 
-       if (!tigrc_system) {
-               if (!string_format(buf, "%s/tigrc", SYSCONFDIR))
-                       return ERR;
-               tigrc_system = buf;
-       }
+       if (!tigrc_system)
+               tigrc_system = SYSCONFDIR "/tigrc";
        load_option_file(tigrc_system);
 
        if (!tigrc_user) {
@@ -2253,6 +2245,30 @@ toggle_view_option(bool *option, const char *help)
  * Navigation
  */
 
+static bool
+goto_view_line(struct view *view, unsigned long offset, unsigned long lineno)
+{
+       if (lineno >= view->lines)
+               lineno = view->lines > 0 ? view->lines - 1 : 0;
+
+       if (offset > lineno || offset + view->height <= lineno) {
+               unsigned long half = view->height / 2;
+
+               if (lineno > half)
+                       offset = lineno - half;
+               else
+                       offset = 0;
+       }
+
+       if (offset != view->offset || lineno != view->lineno) {
+               view->offset = offset;
+               view->lineno = lineno;
+               return TRUE;
+       }
+
+       return FALSE;
+}
+
 /* Scrolling backend */
 static void
 do_scroll_view(struct view *view, int lines)
@@ -2464,20 +2480,18 @@ static void search_view(struct view *view, enum request request);
 static void
 select_view_line(struct view *view, unsigned long lineno)
 {
-       if (lineno - view->offset >= view->height) {
-               view->offset = lineno;
-               view->lineno = lineno;
-               if (view_is_displayed(view))
-                       redraw_view(view);
-
-       } else {
-               unsigned long old_lineno = view->lineno - view->offset;
+       unsigned long old_lineno = view->lineno;
+       unsigned long old_offset = view->offset;
 
-               view->lineno = lineno;
+       if (goto_view_line(view, view->offset, lineno)) {
                if (view_is_displayed(view)) {
-                       draw_view_line(view, old_lineno);
-                       draw_view_line(view, view->lineno - view->offset);
-                       wnoutrefresh(view->win);
+                       if (old_offset != view->offset) {
+                               redraw_view(view);
+                       } else {
+                               draw_view_line(view, old_lineno - view->offset);
+                               draw_view_line(view, view->lineno - view->offset);
+                               wnoutrefresh(view->win);
+                       }
                } else {
                        view->ops->select(view, &view->line[view->lineno]);
                }
@@ -2669,26 +2683,11 @@ restore_view_position(struct view *view)
                return FALSE;
        }
 
-       if (view->p_lineno >= view->lines) {
-               view->p_lineno = view->lines > 0 ? view->lines - 1 : 0;
-               if (view->p_offset >= view->p_lineno) {
-                       unsigned long half = view->height / 2;
-
-                       if (view->p_lineno > half)
-                               view->p_offset = view->p_lineno - half;
-                       else
-                               view->p_offset = 0;
-               }
-       }
-
-       if (view_is_displayed(view) &&
-           view->offset != view->p_offset &&
-           view->lineno != view->p_lineno)
+       if (goto_view_line(view, view->p_offset, view->p_lineno) &&
+           view_is_displayed(view))
                werase(view->win);
 
-       view->offset = view->p_offset;
        view->yoffset = view->p_yoffset;
-       view->lineno = view->p_lineno;
        view->p_restore = FALSE;
 
        return TRUE;
@@ -2931,9 +2930,7 @@ add_line_format(struct view *view, enum line_type type, const char *fmt, ...)
 enum open_flags {
        OPEN_DEFAULT = 0,       /* Use default view switching. */
        OPEN_SPLIT = 1,         /* Split current view. */
-       OPEN_BACKGROUNDED = 2,  /* Backgrounded. */
        OPEN_RELOAD = 4,        /* Reload view even if it is the current. */
-       OPEN_NOMAXIMIZE = 8,    /* Do not maximize the current view. */
        OPEN_REFRESH = 16,      /* Refresh view using previous command. */
        OPEN_PREPARED = 32,     /* Open already prepared command. */
 };
@@ -2941,10 +2938,9 @@ enum open_flags {
 static void
 open_view(struct view *prev, enum request request, enum open_flags flags)
 {
-       bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
        bool split = !!(flags & OPEN_SPLIT);
        bool reload = !!(flags & (OPEN_RELOAD | OPEN_REFRESH | OPEN_PREPARED));
-       bool nomaximize = !!(flags & (OPEN_NOMAXIMIZE | OPEN_REFRESH));
+       bool nomaximize = !!(flags & OPEN_REFRESH);
        struct view *view = VIEW(request);
        int nviews = displayed_views();
        struct view *base_view = display[0];
@@ -2961,8 +2957,7 @@ open_view(struct view *prev, enum request request, enum open_flags flags)
 
        if (split) {
                display[1] = view;
-               if (!backgrounded)
-                       current_view = 1;
+               current_view = 1;
        } else if (!nomaximize) {
                /* Maximize the current view. */
                memset(display, 0, sizeof(display));
@@ -3001,7 +2996,7 @@ open_view(struct view *prev, enum request request, enum open_flags flags)
        }
 
        if (prev && view != prev) {
-               if (split && !backgrounded) {
+               if (split) {
                        /* "Blur" the previous view. */
                        update_view_title(prev);
                }
@@ -3019,11 +3014,6 @@ open_view(struct view *prev, enum request request, enum open_flags flags)
                redraw_view(view);
                report("");
        }
-
-       /* If the view is backgrounded the above calls to report()
-        * won't redraw the view title. */
-       if (backgrounded)
-               update_view_title(view);
 }
 
 static void
@@ -3325,6 +3315,22 @@ view_driver(struct view *view, enum request request)
  * View backend utilities
  */
 
+static void
+parse_timezone(time_t *time, const char *zone)
+{
+       long tz;
+
+       tz  = ('0' - zone[1]) * 60 * 60 * 10;
+       tz += ('0' - zone[2]) * 60 * 60;
+       tz += ('0' - zone[3]) * 60;
+       tz += ('0' - zone[4]);
+
+       if (zone[0] == '-')
+               tz = -tz;
+
+       *time -= tz;
+}
+
 /* Parse author lines where the name may be empty:
  *     author  <email@address.tld> 1138474660 +0100
  */
@@ -3352,20 +3358,8 @@ parse_author_line(char *ident, char *author, size_t authorsize, struct tm *tm)
                char *zone = strchr(secs, ' ');
                time_t time = (time_t) atol(secs);
 
-               if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
-                       long tz;
-
-                       zone++;
-                       tz  = ('0' - zone[1]) * 60 * 60 * 10;
-                       tz += ('0' - zone[2]) * 60 * 60;
-                       tz += ('0' - zone[3]) * 60;
-                       tz += ('0' - zone[4]) * 60;
-
-                       if (zone[0] == '-')
-                               tz = -tz;
-
-                       time -= tz;
-               }
+               if (zone && strlen(zone) == STRING_SIZE(" +0700"))
+                       parse_timezone(&time, zone + 1);
 
                gmtime_r(&time, tm);
        }
@@ -3390,11 +3384,11 @@ select_commit_parent_handler(void *data, char *buf, int c)
 }
 
 static bool
-select_commit_parent(const char *id, char rev[SIZEOF_REV])
+select_commit_parent(const char *id, char rev[SIZEOF_REV], const char *path)
 {
        char buf[SIZEOF_STR * 4];
        const char *revlist_argv[] = {
-               "git", "rev-list", "-1", "--parents", id, NULL
+               "git", "rev-list", "-1", "--parents", id, "--", path, NULL
        };
        int parents;
 
@@ -3405,7 +3399,10 @@ select_commit_parent(const char *id, char rev[SIZEOF_REV])
                return FALSE;
 
        } else if (parents == 0) {
-               report("The selected commit has no parents");
+               if (path)
+                       report("Path '%s' does not exist in the parent", path);
+               else
+                       report("The selected commit has no parents");
                return FALSE;
        }
 
@@ -4183,6 +4180,7 @@ struct blame_commit {
 
 struct blame {
        struct blame_commit *commit;
+       unsigned long lineno;
        char text[1];
 };
 
@@ -4246,14 +4244,16 @@ parse_blame_commit(struct view *view, const char *text, int *blamed)
 {
        struct blame_commit *commit;
        struct blame *blame;
-       const char *pos = text + SIZEOF_REV - 1;
+       const char *pos = text + SIZEOF_REV - 2;
+       size_t orig_lineno = 0;
        size_t lineno;
        size_t group;
 
-       if (strlen(text) <= SIZEOF_REV || *pos != ' ')
+       if (strlen(text) <= SIZEOF_REV || pos[1] != ' ')
                return NULL;
 
-       if (!parse_number(&pos, &lineno, 1, view->lines) ||
+       if (!parse_number(&pos, &orig_lineno, 1, 9999999) ||
+           !parse_number(&pos, &lineno, 1, view->lines) ||
            !parse_number(&pos, &group, 1, view->lines - lineno + 1))
                return NULL;
 
@@ -4267,6 +4267,7 @@ parse_blame_commit(struct view *view, const char *text, int *blamed)
 
                blame = line->data;
                blame->commit = commit;
+               blame->lineno = orig_lineno + group - 1;
                line->dirty = 1;
        }
 
@@ -4294,14 +4295,15 @@ blame_read_file(struct view *view, const char *line, bool *read_file)
                return FALSE;
 
        } else {
-               size_t linelen = string_expand_length(line, opt_tab_size);
+               size_t linelen = strlen(line);
                struct blame *blame = malloc(sizeof(*blame) + linelen);
 
                if (!blame)
                        return FALSE;
 
                blame->commit = NULL;
-               string_expand(blame->text, linelen + 1, line, opt_tab_size);
+               strncpy(blame->text, line, linelen);
+               blame->text[linelen] = 0;
                return add_line_data(view, blame, LINE_BLAME_ID) != NULL;
        }
 }
@@ -4354,17 +4356,7 @@ blame_read(struct view *view, char *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;
+               parse_timezone(&author_time, line);
                gmtime_r(&author_time, &commit->time);
 
        } else if (match_blame_header("summary ", &line)) {
@@ -4387,6 +4379,7 @@ blame_draw(struct view *view, struct line *line, unsigned int lineno)
        struct blame *blame = line->data;
        struct tm *time = NULL;
        const char *id = NULL, *author = NULL;
+       char text[SIZEOF_STR];
 
        if (blame->commit && *blame->commit->filename) {
                id = blame->commit->id;
@@ -4406,7 +4399,8 @@ blame_draw(struct view *view, struct line *line, unsigned int lineno)
        if (draw_lineno(view, lineno))
                return TRUE;
 
-       draw_text(view, LINE_DEFAULT, blame->text, TRUE);
+       string_expand(text, sizeof(text), blame->text, opt_tab_size);
+       draw_text(view, LINE_DEFAULT, text, TRUE);
        return TRUE;
 }
 
@@ -4422,6 +4416,42 @@ check_blame_commit(struct blame *blame)
        return FALSE;
 }
 
+static void
+setup_blame_parent_line(struct view *view, struct blame *blame)
+{
+       const char *diff_tree_argv[] = {
+               "git", "diff-tree", "-U0", blame->commit->id,
+                       "--", blame->commit->filename, NULL
+       };
+       struct io io = {};
+       int parent_lineno = -1;
+       int blamed_lineno = -1;
+       char *line;
+
+       if (!run_io(&io, diff_tree_argv, NULL, IO_RD))
+               return;
+
+       while ((line = io_get(&io, '\n', TRUE))) {
+               if (*line == '@') {
+                       char *pos = strchr(line, '+');
+
+                       parent_lineno = atoi(line + 4);
+                       if (pos)
+                               blamed_lineno = atoi(pos + 1);
+
+               } else if (*line == '+' && parent_lineno != -1) {
+                       if (blame->lineno == blamed_lineno - 1 &&
+                           !strcmp(blame->text, line + 1)) {
+                               view->lineno = parent_lineno ? parent_lineno - 1 : 0;
+                               break;
+                       }
+                       blamed_lineno++;
+               }
+       }
+
+       done_io(&io);
+}
+
 static enum request
 blame_request(struct view *view, enum request request, struct line *line)
 {
@@ -4432,14 +4462,21 @@ blame_request(struct view *view, enum request request, struct line *line)
        case REQ_VIEW_BLAME:
                if (check_blame_commit(blame)) {
                        string_copy(opt_ref, blame->commit->id);
+                       string_copy(opt_file, blame->commit->filename);
+                       if (blame->lineno)
+                               view->lineno = blame->lineno;
                        open_view(view, REQ_VIEW_BLAME, OPEN_REFRESH);
                }
                break;
 
        case REQ_PARENT:
                if (check_blame_commit(blame) &&
-                   select_commit_parent(blame->commit->id, opt_ref))
+                   select_commit_parent(blame->commit->id, opt_ref,
+                                        blame->commit->filename)) {
+                       string_copy(opt_file, blame->commit->filename);
+                       setup_blame_parent_line(view, blame);
                        open_view(view, REQ_VIEW_BLAME, OPEN_REFRESH);
+               }
                break;
 
        case REQ_ENTER:
@@ -4728,6 +4765,55 @@ status_restore(struct view *view)
        view->p_restore = FALSE;
 }
 
+static void
+status_update_onbranch(void)
+{
+       static const char *paths[][2] = {
+               { "rebase-apply/rebasing",      "Rebasing" },
+               { "rebase-apply/applying",      "Applying mailbox" },
+               { "rebase-apply/",              "Rebasing mailbox" },
+               { "rebase-merge/interactive",   "Interactive rebase" },
+               { "rebase-merge/",              "Rebase merge" },
+               { "MERGE_HEAD",                 "Merging" },
+               { "BISECT_LOG",                 "Bisecting" },
+               { "HEAD",                       "On branch" },
+       };
+       char buf[SIZEOF_STR];
+       struct stat stat;
+       int i;
+
+       if (is_initial_commit()) {
+               string_copy(status_onbranch, "Initial commit");
+               return;
+       }
+
+       for (i = 0; i < ARRAY_SIZE(paths); i++) {
+               char *head = opt_head;
+
+               if (!string_format(buf, "%s/%s", opt_git_dir, paths[i][0]) ||
+                   lstat(buf, &stat) < 0)
+                       continue;
+
+               if (!*opt_head) {
+                       struct io io = {};
+
+                       if (string_format(buf, "%s/rebase-merge/head-name", opt_git_dir) &&
+                           io_open(&io, buf) &&
+                           io_read_buf(&io, buf, sizeof(buf))) {
+                               head = chomp_string(buf);
+                               if (!prefixcmp(head, "refs/heads/"))
+                                       head += STRING_SIZE("refs/heads/");
+                       }
+               }
+
+               if (!string_format(status_onbranch, "%s %s", paths[i][1], head))
+                       string_copy(status_onbranch, opt_head);
+               return;
+       }
+
+       string_copy(status_onbranch, "Not currently on any branch");
+}
+
 /* 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). */
@@ -4737,12 +4823,7 @@ status_open(struct view *view)
        reset_view(view);
 
        add_line_data(view, NULL, LINE_STAT_HEAD);
-       if (is_initial_commit())
-               string_copy(status_onbranch, "Initial commit");
-       else if (!*opt_head)
-               string_copy(status_onbranch, "Not currently on any branch");
-       else if (!string_format(status_onbranch, "On branch %s", opt_head))
-               return FALSE;
+       status_update_onbranch();
 
        run_io_bg(update_index_argv);
 
@@ -5460,7 +5541,7 @@ stage_request(struct view *view, enum request request, struct line *line)
        }
 
        VIEW(REQ_VIEW_STATUS)->p_restore = TRUE;
-       open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD | OPEN_NOMAXIMIZE);
+       open_view(view, REQ_VIEW_STATUS, OPEN_REFRESH);
 
        /* Check whether the staged entry still exists, and close the
         * stage view if it doesn't. */
@@ -6908,7 +6989,17 @@ main(int argc, const char *argv[])
                {
                        char *cmd = read_prompt(":");
 
-                       if (cmd) {
+                       if (cmd && isdigit(*cmd)) {
+                               int lineno = view->lineno + 1;
+
+                               if (parse_int(&lineno, cmd, 1, view->lines + 1) == OK) {
+                                       select_view_line(view, lineno - 1);
+                                       report("");
+                               } else {
+                                       report("Unable to parse '%s' as a line number", cmd);
+                               }
+
+                       } else if (cmd) {
                                struct view *next = VIEW(REQ_VIEW_PAGER);
                                const char *argv[SIZEOF_ARG] = { "git" };
                                int argc = 1;