New config options show-author, show-date, show-refs, show-line-numbers.
[tig] / tig.c
diff --git a/tig.c b/tig.c
index 71ddbb5..e5741d6 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
@@ -55,7 +58,7 @@ 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))
@@ -107,7 +110,7 @@ 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 --no-color --root --patch-with-stat --find-copies-harder -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 --no-color --cc --stat -n100 %s 2>/dev/null"
@@ -137,6 +140,7 @@ struct ref {
        char *name;             /* Ref name; tag or head names are shortened. */
        char id[SIZEOF_REV];    /* Commit SHA1 ID */
        unsigned int tag:1;     /* Is it a tag? */
+       unsigned int ltag:1;    /* If so, is the tag local? */
        unsigned int remote:1;  /* Is it a remote ref? */
        unsigned int next:1;    /* For ref lists: are there more refs? */
 };
@@ -351,7 +355,10 @@ sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
        REQ_(SHOW_VERSION,      "Show version information"), \
        REQ_(STOP_LOADING,      "Stop all loading views"), \
        REQ_(TOGGLE_LINENO,     "Toggle line numbers"), \
+       REQ_(TOGGLE_DATE,       "Toggle date display"), \
+       REQ_(TOGGLE_AUTHOR,     "Toggle author display"), \
        REQ_(TOGGLE_REV_GRAPH,  "Toggle revision graph visualization"), \
+       REQ_(TOGGLE_REFS,       "Toggle reference display (tags/branches)"), \
        REQ_(STATUS_UPDATE,     "Update file status"), \
        REQ_(STATUS_MERGE,      "Merge file using external tool"), \
        REQ_(TREE_PARENT,       "Switch to parent directory in tree view"), \
@@ -419,8 +426,11 @@ static const char usage[] =
 "  -h, --help      Show help message and exit\n";
 
 /* Option and state variables. */
+static bool opt_date                   = TRUE;
+static bool opt_author                 = TRUE;
 static bool opt_line_number            = FALSE;
 static bool opt_rev_graph              = FALSE;
+static bool opt_show_refs              = TRUE;
 static int opt_num_interval            = NUMBER_INTERVAL;
 static int opt_tab_size                        = TABSIZE;
 static enum request opt_request                = REQ_VIEW_MAIN;
@@ -434,7 +444,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 {
@@ -645,6 +655,7 @@ LINE(MAIN_AUTHOR,  "",                      COLOR_GREEN,    COLOR_DEFAULT,  0), \
 LINE(MAIN_COMMIT,  "",                 COLOR_DEFAULT,  COLOR_DEFAULT,  0), \
 LINE(MAIN_DELIM,   "",                 COLOR_MAGENTA,  COLOR_DEFAULT,  0), \
 LINE(MAIN_TAG,     "",                 COLOR_MAGENTA,  COLOR_DEFAULT,  A_BOLD), \
+LINE(MAIN_LOCAL_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), \
@@ -805,7 +816,10 @@ static struct keybinding default_keybindings[] = {
        { 'v',          REQ_SHOW_VERSION },
        { 'r',          REQ_SCREEN_REDRAW },
        { '.',          REQ_TOGGLE_LINENO },
+       { 'D',          REQ_TOGGLE_DATE },
+       { 'A',          REQ_TOGGLE_AUTHOR },
        { 'g',          REQ_TOGGLE_REV_GRAPH },
+       { 'F',          REQ_TOGGLE_REFS },
        { ':',          REQ_PROMPT },
        { 'u',          REQ_STATUS_UPDATE },
        { 'M',          REQ_STATUS_MERGE },
@@ -1112,6 +1126,12 @@ option_color_command(int argc, char *argv[])
        return OK;
 }
 
+static bool parse_bool(const char *s)
+{
+       return (!strcmp(s, "1") || !strcmp(s, "true") ||
+               !strcmp(s, "yes")) ? TRUE : FALSE;
+}
+
 /* Wants: name = value */
 static int
 option_set_command(int argc, char *argv[])
@@ -1126,10 +1146,28 @@ option_set_command(int argc, char *argv[])
                return ERR;
        }
 
+       if (!strcmp(argv[0], "show-author")) {
+               opt_author = parse_bool(argv[2]);
+               return OK;
+       }
+
+       if (!strcmp(argv[0], "show-date")) {
+               opt_date = parse_bool(argv[2]);
+               return OK;
+       }
+
        if (!strcmp(argv[0], "show-rev-graph")) {
-               opt_rev_graph = (!strcmp(argv[2], "1") ||
-                                !strcmp(argv[2], "true") ||
-                                !strcmp(argv[2], "yes"));
+               opt_rev_graph = parse_bool(argv[2]);
+               return OK;
+       }
+
+       if (!strcmp(argv[0], "show-refs")) {
+               opt_show_refs = parse_bool(argv[2]);
+               return OK;
+       }
+
+       if (!strcmp(argv[0], "show-line-numbers")) {
+               opt_line_number = parse_bool(argv[2]);
                return OK;
        }
 
@@ -1453,6 +1491,40 @@ 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 len = 0;
+       int trimmed = FALSE;
+
+       if (max_len <= 0)
+               return 0;
+
+       if (opt_utf8) {
+               len = utf8_length(string, max_len, &trimmed, use_tilde);
+       } else {
+               len = strlen(string);
+               if (len > max_len) {
+                       if (use_tilde) {
+                               max_len -= 1;
+                       }
+                       len = max_len;
+                       trimmed = TRUE;
+               }
+       }
+
+       waddnstr(view->win, string, len);
+       if (trimmed && use_tilde) {
+               if (tilde_attr != -1)
+                       wattrset(view->win, tilde_attr);
+               waddch(view->win, '~');
+               len++;
+       }
+
+       return len;
+}
+
 static bool
 draw_view_line(struct view *view, unsigned int lineno)
 {
@@ -2503,11 +2575,26 @@ view_driver(struct view *view, enum request request)
                redraw_display();
                break;
 
+       case REQ_TOGGLE_DATE:
+               opt_date = !opt_date;
+               redraw_display();
+               break;
+
+       case REQ_TOGGLE_AUTHOR:
+               opt_author = !opt_author;
+               redraw_display();
+               break;
+
        case REQ_TOGGLE_REV_GRAPH:
                opt_rev_graph = !opt_rev_graph;
                redraw_display();
                break;
 
+       case REQ_TOGGLE_REFS:
+               opt_show_refs = !opt_show_refs;
+               redraw_display();
+               break;
+
        case REQ_PROMPT:
                /* Always reload^Wrerun commands from the prompt. */
                open_view(view, opt_request, OPEN_RELOAD);
@@ -2590,7 +2677,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);
@@ -2643,13 +2729,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;
@@ -2687,7 +2769,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: ";
@@ -2798,7 +2880,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);
@@ -3117,7 +3199,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);
@@ -3324,7 +3406,7 @@ error_out:
 
 /* Don't show unmerged entries in the staged section. */
 #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_DIFF_FILES_CMD "git diff-files -z"
 #define STATUS_LIST_OTHER_CMD \
        "git ls-files -z --others --exclude-per-directory=.gitignore"
 
@@ -3368,6 +3450,8 @@ status_open(struct view *view)
                        return FALSE;
        }
 
+       system("git update-index -q --refresh");
+
        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))
@@ -3387,12 +3471,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));
@@ -3426,7 +3512,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;
        }
 
@@ -3434,8 +3520,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->new.name);
+       if (view->width < 5)
+               return TRUE;
 
+       draw_text(view, status->new.name, view->width - 5, 5, TRUE, tilde_attr);
        return TRUE;
 }
 
@@ -4117,106 +4205,120 @@ 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, " ");
+       if (opt_date) {
+               int n;
 
-       col += DATE_COLS;
-       wmove(view->win, lineno, col);
+               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);
+
+               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 (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;
-               }
-       }
+       if (opt_author) {
+               int max_len;
 
-       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);
+               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 (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);
 
-       if (commit->refs) {
+       if (opt_show_refs && commit->refs) {
                size_t i = 0;
 
                do {
                        if (type == LINE_CURSOR)
                                ;
+                       else if (commit->refs[i]->ltag)
+                               wattrset(view->win, get_line_attr(LINE_MAIN_LOCAL_TAG));
                        else if (commit->refs[i]->tag)
                                wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
                        else if (commit->refs[i]->remote)
                                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;
 }
@@ -4431,6 +4533,9 @@ unicode_width(unsigned long c)
            || (c >= 0x30000 && c <= 0x3fffd)))
                return 2;
 
+       if (c == '\t')
+               return opt_tab_size;
+
        return 1;
 }
 
@@ -4498,19 +4603,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;
@@ -4536,27 +4638,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;
 }
 
@@ -4800,15 +4891,19 @@ read_ref(char *id, size_t idlen, char *name, size_t namelen)
 {
        struct ref *ref;
        bool tag = FALSE;
+       bool ltag = FALSE;
        bool remote = FALSE;
+       bool check_replace = FALSE;
 
        if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
-               /* Commits referenced by tags has "^{}" appended. */
-               if (name[namelen - 1] != '}')
-                       return OK;
-
-               while (namelen > 0 && name[namelen] != '^')
-                       namelen--;
+               if (!strcmp(name + namelen - 3, "^{}")) {
+                       namelen -= 3;
+                       name[namelen] = 0;
+                       if (refs_size > 0 && refs[refs_size - 1].ltag == TRUE)
+                               check_replace = TRUE;
+               } else {
+                       ltag = TRUE;
+               }
 
                tag = TRUE;
                namelen -= STRING_SIZE("refs/tags/");
@@ -4827,6 +4922,16 @@ read_ref(char *id, size_t idlen, char *name, size_t namelen)
                return OK;
        }
 
+       if (check_replace && !strcmp(name, refs[refs_size - 1].name)) {
+               /* it's an annotated tag, replace the previous sha1 with the
+                * resolved commit id; relies on the fact git-ls-remote lists
+                * the commit id of an annotated tag right beofre the commit id
+                * it points to. */
+               refs[refs_size - 1].ltag = ltag;
+               string_copy_rev(refs[refs_size - 1].id, id);
+
+               return OK;
+       }
        refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
        if (!refs)
                return ERR;
@@ -4839,6 +4944,7 @@ read_ref(char *id, size_t idlen, char *name, size_t namelen)
        strncpy(ref->name, name, namelen);
        ref->name[namelen] = 0;
        ref->tag = tag;
+       ref->ltag = ltag;
        ref->remote = remote;
        string_copy_rev(ref->id, id);