Make UTF-8 handling optional but still default
[tig] / tig.c
diff --git a/tig.c b/tig.c
index 05fb370..49c2492 100644 (file)
--- a/tig.c
+++ b/tig.c
@@ -1,5 +1,14 @@
 /* Copyright (c) 2006 Jonas Fonseca <fonseca@diku.dk>
- * See license info at the bottom. */
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
 /**
  * TIG(1)
  * ======
@@ -227,6 +236,8 @@ static int opt_num_interval = NUMBER_INTERVAL;
 static int opt_tab_size                = TABSIZE;
 static enum request opt_request = REQ_VIEW_MAIN;
 static char opt_cmd[SIZEOF_CMD]        = "";
+static char opt_encoding[20]   = "";
+static bool opt_utf8           = TRUE;
 static FILE *opt_pipe          = NULL;
 
 /* Returns the index of log or diff command or -1 to exit. */
@@ -429,6 +440,82 @@ parse_options(int argc, char *argv[])
 }
 
 
+/**
+ * ENVIRONMENT VARIABLES
+ * ---------------------
+ * Several options related to the interface with git can be configured
+ * via environment options.
+ *
+ * Repository references
+ * ~~~~~~~~~~~~~~~~~~~~~
+ * Commits that are referenced by tags and branch heads will be marked
+ * by the reference name surrounded by '[' and ']':
+ *
+ *     2006-03-26 19:42 Petr Baudis         | [cogito-0.17.1] Cogito 0.17.1
+ *
+ * If you want to filter out certain directories under `.git/refs/`, say
+ * `tmp` you can do it by setting the following variable:
+ *
+ *     $ TIG_LS_REMOTE="git ls-remote . | sed /\/tmp\//d" tig
+ *
+ * Or set the variable permanently in your environment.
+ *
+ * TIG_LS_REMOTE::
+ *     Set command for retrieving all repository references. The command
+ *     should output data in the same format as git-ls-remote(1).
+ **/
+
+#define TIG_LS_REMOTE \
+       "git ls-remote . 2>/dev/null"
+
+/**
+ * [[view-commands]]
+ * View commands
+ * ~~~~~~~~~~~~~
+ * It is possible to alter which commands are used for the different views.
+ * If for example you prefer commits in the main view to be sorted by date
+ * and only show 500 commits, use:
+ *
+ *     $ TIG_MAIN_CMD="git log --date-order -n500 --pretty=raw %s" tig
+ *
+ * Or set the variable permanently in your environment.
+ *
+ * Notice, how `%s` is used to specify the commit reference. There can
+ * be a maximum of 5 `%s` ref specifications.
+ *
+ * TIG_DIFF_CMD::
+ *     The command used for the diff view. By default, git show is used
+ *     as a backend.
+ *
+ * TIG_LOG_CMD::
+ *     The command used for the log view. If you prefer to have both
+ *     author and committer shown in the log view be sure to pass
+ *     `--pretty=fuller` to git log.
+ *
+ * TIG_MAIN_CMD::
+ *     The command used for the main view. Note, you must always specify
+ *     the option: `--pretty=raw` since the main view parser expects to
+ *     read that format.
+ **/
+
+#define TIG_DIFF_CMD \
+       "git show --patch-with-stat --find-copies-harder -B -C %s"
+
+#define TIG_LOG_CMD    \
+       "git log --cc --stat -n100 %s"
+
+#define TIG_MAIN_CMD \
+       "git log --topo-order --stat --pretty=raw %s"
+
+/* ... silently ignore that the following are also exported. */
+
+#define TIG_HELP_CMD \
+       "man tig 2>/dev/null"
+
+#define TIG_PAGER_CMD \
+       ""
+
+
 /*
  * Line-oriented content detection.
  */
@@ -542,81 +629,10 @@ init_colors(void)
        }
 }
 
-
-/**
- * ENVIRONMENT VARIABLES
- * ---------------------
- * Several options related to the interface with git can be configured
- * via environment options.
- *
- * Repository references
- * ~~~~~~~~~~~~~~~~~~~~~
- * Commits that are referenced by tags and branch heads will be marked
- * by the reference name surrounded by '[' and ']':
- *
- *     2006-03-26 19:42 Petr Baudis         | [cogito-0.17.1] Cogito 0.17.1
- *
- * If you want to filter out certain directories under `.git/refs/`, say
- * `tmp` you can do it by setting the following variable:
- *
- *     $ TIG_LS_REMOTE="git ls-remote . | sed /\/tmp\//d" tig
- *
- * Or set the variable permanently in your environment.
- *
- * TIG_LS_REMOTE::
- *     Set command for retrieving all repository references. The command
- *     should output data in the same format as git-ls-remote(1).
- **/
-
-#define TIG_LS_REMOTE \
-       "git ls-remote . 2>/dev/null"
-
-/**
- * [[view-commands]]
- * View commands
- * ~~~~~~~~~~~~~
- * It is possible to alter which commands are used for the different views.
- * If for example you prefer commits in the main view to be sorted by date
- * and only show 500 commits, use:
- *
- *     $ TIG_MAIN_CMD="git log --date-order -n500 --pretty=raw %s" tig
- *
- * Or set the variable permanently in your environment.
- *
- * Notice, how `%s` is used to specify the commit reference. There can
- * be a maximum of 5 `%s` ref specifications.
- *
- * TIG_DIFF_CMD::
- *     The command used for the diff view. By default, git show is used
- *     as a backend.
- *
- * TIG_LOG_CMD::
- *     The command used for the log view. If you prefer to have both
- *     author and committer shown in the log view be sure to pass
- *     `--pretty=fuller` to git log.
- *
- * TIG_MAIN_CMD::
- *     The command used for the main view. Note, you must always specify
- *     the option: `--pretty=raw` since the main view parser expects to
- *     read that format.
- **/
-
-#define TIG_DIFF_CMD \
-       "git show --patch-with-stat --find-copies-harder -B -C %s"
-
-#define TIG_LOG_CMD    \
-       "git log --cc --stat -n100 %s"
-
-#define TIG_MAIN_CMD \
-       "git log --topo-order --stat --pretty=raw %s"
-
-/* ... silently ignore that the following are also exported. */
-
-#define TIG_HELP_CMD \
-       "man tig 2>/dev/null"
-
-#define TIG_PAGER_CMD \
-       ""
+struct line {
+       enum line_type type;
+       void *data;             /* User data */
+};
 
 
 /**
@@ -638,6 +654,7 @@ init_colors(void)
  **/
 
 struct view;
+struct view_ops;
 
 /* The display array of active views and the index of the current view. */
 static struct view *display[2];
@@ -663,24 +680,13 @@ static unsigned int current_view;
 static char ref_commit[SIZEOF_REF]     = "HEAD";
 static char ref_head[SIZEOF_REF]       = "HEAD";
 
-
 struct view {
        const char *name;       /* View name */
        const char *cmd_fmt;    /* Default command line format */
        const char *cmd_env;    /* Command line set via environment */
        const char *id;         /* Points to either of ref_{head,commit} */
 
-       struct view_ops {
-               /* What type of content being displayed. Used in the
-                * title bar. */
-               const char *type;
-               /* Draw one line; @lineno must be < view->height. */
-               bool (*draw)(struct view *view, unsigned int lineno);
-               /* Read one line; updates view->line. */
-               bool (*read)(struct view *view, char *line);
-               /* Depending on view, change display based on current line. */
-               bool (*enter)(struct view *view);
-       } *ops;
+       struct view_ops *ops;   /* View operations */
 
        char cmd[SIZEOF_CMD];   /* Command buffer */
        char ref[SIZEOF_REF];   /* Hovered commit reference */
@@ -700,7 +706,7 @@ struct view {
 
        /* Buffering */
        unsigned long lines;    /* Total number of lines */
-       void **line;            /* Line index; each line contains user data */
+       struct line *line;      /* Line index */
        unsigned int digits;    /* Number of digits in the lines member. */
 
        /* Loading */
@@ -708,6 +714,17 @@ struct view {
        time_t start_time;
 };
 
+struct view_ops {
+       /* What type of content being displayed. Used in the title bar. */
+       const char *type;
+       /* Draw one line; @lineno must be < view->height. */
+       bool (*draw)(struct view *view, struct line *line, unsigned int lineno);
+       /* Read one line; updates view->line. */
+       bool (*read)(struct view *view, struct line *prev, char *data);
+       /* Depending on view, change display based on current line. */
+       bool (*enter)(struct view *view, struct line *line);
+};
+
 static struct view_ops pager_ops;
 static struct view_ops main_ops;
 
@@ -758,13 +775,22 @@ static struct view views[] = {
 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
 
 
+static bool
+draw_view_line(struct view *view, unsigned int lineno)
+{
+       if (view->offset + lineno >= view->lines)
+               return FALSE;
+
+       return view->ops->draw(view, &view->line[view->offset + lineno], lineno);
+}
+
 static void
 redraw_view_from(struct view *view, int lineno)
 {
        assert(0 <= lineno && lineno < view->height);
 
        for (; lineno < view->height; lineno++) {
-               if (!view->ops->draw(view, lineno))
+               if (!draw_view_line(view, lineno))
                        break;
        }
 
@@ -807,12 +833,16 @@ update_view_title(struct view *view)
        else
                wprintw(view->title, "[%s]", view->name);
 
-       if (view->lines) {
+       if (view->lines || view->pipe) {
+               unsigned int lines = view->lines
+                                  ? (view->lineno + 1) * 100 / view->lines
+                                  : 0;
+
                wprintw(view->title, " - %s %d of %d (%d%%)",
                        view->ops->type,
                        view->lineno + 1,
                        view->lines,
-                       (view->lineno + 1) * 100 / view->lines);
+                       lines);
        }
 
        if (view->pipe) {
@@ -857,11 +887,8 @@ resize_display(void)
        offset = 0;
 
        foreach_view (view, i) {
-               /* Keep the height of all view->win windows one larger than is
-                * required so that the cursor can wrap-around on the last line
-                * without scrolling the window. */
                if (!view->win) {
-                       view->win = newwin(view->height + 1, 0, offset, 0);
+                       view->win = newwin(view->height, 0, offset, 0);
                        if (!view->win)
                                die("Failed to create %s view", view->name);
 
@@ -872,7 +899,7 @@ resize_display(void)
                                die("Failed to create title window");
 
                } else {
-                       wresize(view->win, view->height + 1, view->width);
+                       wresize(view->win, view->height, view->width);
                        mvwin(view->win,   offset, 0);
                        mvwin(view->title, offset + view->height, 0);
                        wrefresh(view->win);
@@ -920,7 +947,7 @@ do_scroll_view(struct view *view, int lines, bool redraw)
                wscrl(view->win, lines);
 
                for (; line < end; line++) {
-                       if (!view->ops->draw(view, line))
+                       if (!draw_view_line(view, line))
                                break;
                }
        }
@@ -928,7 +955,7 @@ do_scroll_view(struct view *view, int lines, bool redraw)
        /* Move current line into the view. */
        if (view->lineno < view->offset) {
                view->lineno = view->offset;
-               view->ops->draw(view, 0);
+               draw_view_line(view, 0);
 
        } else if (view->lineno >= view->offset + view->height) {
                if (view->lineno == view->offset + view->height) {
@@ -938,7 +965,7 @@ do_scroll_view(struct view *view, int lines, bool redraw)
                        wclrtoeol(view->win);
                }
                view->lineno = view->offset + view->height - 1;
-               view->ops->draw(view, view->lineno - view->offset);
+               draw_view_line(view, view->lineno - view->offset);
        }
 
        assert(view->offset <= view->lineno && view->lineno < view->lines);
@@ -1047,7 +1074,7 @@ move_view(struct view *view, enum request request, bool redraw)
 
                wmove(view->win, prev_lineno, 0);
                wclrtoeol(view->win);
-               view->ops->draw(view, prev_lineno);
+               draw_view_line(view,  prev_lineno);
        }
 
        /* Check whether the view needs to be scrolled */
@@ -1070,7 +1097,7 @@ move_view(struct view *view, enum request request, bool redraw)
        }
 
        /* Draw the current line */
-       view->ops->draw(view, view->lineno - view->offset);
+       draw_view_line(view, view->lineno - view->offset);
 
        if (!redraw)
                return;
@@ -1142,8 +1169,8 @@ begin_update(struct view *view)
                int i;
 
                for (i = 0; i < view->lines; i++)
-                       if (view->line[i])
-                               free(view->line[i]);
+                       if (view->line[i].data)
+                               free(view->line[i].data);
 
                free(view->line);
                view->line = NULL;
@@ -1159,7 +1186,7 @@ update_view(struct view *view)
 {
        char buffer[BUFSIZ];
        char *line;
-       void **tmp;
+       struct line *tmp;
        /* The number of lines to read. If too low it will cause too much
         * redrawing (and possible flickering), if too high responsiveness
         * will suffer. */
@@ -1182,10 +1209,14 @@ update_view(struct view *view)
        while ((line = fgets(buffer, sizeof(buffer), view->pipe))) {
                int linelen = strlen(line);
 
+               struct line *prev = view->lines
+                                 ? &view->line[view->lines - 1]
+                                 : NULL;
+
                if (linelen)
                        line[linelen - 1] = 0;
 
-               if (!view->ops->read(view, line))
+               if (!view->ops->read(view, prev, line))
                        goto alloc_error;
 
                if (lines-- == 1)
@@ -1396,7 +1427,7 @@ view_driver(struct view *view, enum request request)
                        report("Nothing to enter");
                        break;
                }
-               return view->ops->enter(view);
+               return view->ops->enter(view, &view->line[view->lineno]);
 
        case REQ_VIEW_NEXT:
        {
@@ -1476,24 +1507,18 @@ view_driver(struct view *view, enum request request)
  */
 
 static bool
-pager_draw(struct view *view, unsigned int lineno)
+pager_draw(struct view *view, struct line *line, unsigned int lineno)
 {
-       enum line_type type;
-       char *line;
-       int linelen;
+       char *text = line->data;
+       enum line_type type = line->type;
+       int textlen = strlen(text);
        int attr;
 
-       if (view->offset + lineno >= view->lines)
-               return FALSE;
-
-       line = view->line[view->offset + lineno];
-       type = get_line_type(line);
-
        wmove(view->win, lineno, 0);
 
        if (view->offset + lineno == view->lineno) {
                if (type == LINE_COMMIT) {
-                       string_copy(view->ref, line + 7);
+                       string_copy(view->ref, text + 7);
                        string_copy(ref_commit, view->ref);
                }
 
@@ -1504,8 +1529,6 @@ pager_draw(struct view *view, unsigned int lineno)
        attr = get_line_attr(type);
        wattrset(view->win, attr);
 
-       linelen = strlen(line);
-
        if (opt_line_number || opt_tab_size < TABSIZE) {
                static char spaces[] = "                    ";
                int col_offset = 0, col = 0;
@@ -1525,66 +1548,65 @@ pager_draw(struct view *view, unsigned int lineno)
                        col_offset = view->digits + 2;
                }
 
-               while (line && col_offset + col < view->width) {
+               while (text && col_offset + col < view->width) {
                        int cols_max = view->width - col_offset - col;
-                       char *text = line;
+                       char *pos = text;
                        int cols;
 
-                       if (*line == '\t') {
+                       if (*text == '\t') {
+                               text++;
                                assert(sizeof(spaces) > TABSIZE);
-                               line++;
-                               text = spaces;
+                               pos = spaces;
                                cols = opt_tab_size - (col % opt_tab_size);
 
                        } else {
-                               line = strchr(line, '\t');
-                               cols = line ? line - text : strlen(text);
+                               text = strchr(text, '\t');
+                               cols = line ? text - pos : strlen(pos);
                        }
 
-                       waddnstr(view->win, text, MIN(cols, cols_max));
+                       waddnstr(view->win, pos, MIN(cols, cols_max));
                        col += cols;
                }
 
        } else {
                int col = 0, pos = 0;
 
-               for (; pos < linelen && col < view->width; pos++, col++)
-                       if (line[pos] == '\t')
+               for (; pos < textlen && col < view->width; pos++, col++)
+                       if (text[pos] == '\t')
                                col += TABSIZE - (col % TABSIZE) - 1;
 
-               waddnstr(view->win, line, pos);
+               waddnstr(view->win, text, pos);
        }
 
        return TRUE;
 }
 
 static bool
-pager_read(struct view *view, char *line)
+pager_read(struct view *view, struct line *prev, char *line)
 {
        /* Compress empty lines in the help view. */
        if (view == VIEW(REQ_VIEW_HELP) &&
-           !*line &&
-           view->lines &&
-           !*((char *) view->line[view->lines - 1]))
+           !*line && prev && !*((char *) prev->data))
                return TRUE;
 
-       view->line[view->lines] = strdup(line);
-       if (!view->line[view->lines])
+       view->line[view->lines].data = strdup(line);
+       if (!view->line[view->lines].data)
                return FALSE;
 
+       view->line[view->lines].type = get_line_type(line);
+
        view->lines++;
        return TRUE;
 }
 
 static bool
-pager_enter(struct view *view)
+pager_enter(struct view *view, struct line *line)
 {
-       char *line = view->line[view->lineno];
        int split = 0;
 
-       if ((view == VIEW(REQ_VIEW_LOG) ||
-            view == VIEW(REQ_VIEW_PAGER)) &&
-           get_line_type(line) == LINE_COMMIT) {
+       if (line->type == LINE_COMMIT &&
+          (view == VIEW(REQ_VIEW_LOG) ||
+           view == VIEW(REQ_VIEW_PAGER))) {
                open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
                split = 1;
        }
@@ -1624,20 +1646,16 @@ struct commit {
 };
 
 static bool
-main_draw(struct view *view, unsigned int lineno)
+main_draw(struct view *view, struct line *line, unsigned int lineno)
 {
        char buf[DATE_COLS + 1];
-       struct commit *commit;
+       struct commit *commit = line->data;
        enum line_type type;
        int col = 0;
        size_t timelen;
        size_t authorlen;
-       int trimmed;
+       int trimmed = 1;
 
-       if (view->offset + lineno >= view->lines)
-               return FALSE;
-
-       commit = view->line[view->offset + lineno];
        if (!*commit->author)
                return FALSE;
 
@@ -1664,8 +1682,15 @@ main_draw(struct view *view, unsigned int lineno)
        if (type != LINE_CURSOR)
                wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
 
-       /* FIXME: Make this optional, and add i18n.commitEncoding support. */
-       authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
+       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 (trimmed) {
                waddnstr(view->win, commit->author, authorlen);
@@ -1721,7 +1746,7 @@ main_draw(struct view *view, unsigned int lineno)
 
 /* Reads git log --pretty=raw output and parses it into the commit struct. */
 static bool
-main_read(struct view *view, char *line)
+main_read(struct view *view, struct line *prev, char *line)
 {
        enum line_type type = get_line_type(line);
        struct commit *commit;
@@ -1734,7 +1759,7 @@ main_read(struct view *view, char *line)
 
                line += STRING_SIZE("commit ");
 
-               view->line[view->lines++] = commit;
+               view->line[view->lines++].data = commit;
                string_copy(commit->id, line);
                commit->refs = get_refs(commit->id);
                break;
@@ -1744,12 +1769,16 @@ main_read(struct view *view, char *line)
                char *ident = line + STRING_SIZE("author ");
                char *end = strchr(ident, '<');
 
+               if (!prev)
+                       break;
+
+               commit = prev->data;
+
                if (end) {
                        for (; end > ident && isspace(end[-1]); end--) ;
                        *end = 0;
                }
 
-               commit = view->line[view->lines - 1];
                string_copy(commit->author, ident);
 
                /* Parse epoch and timezone */
@@ -1783,13 +1812,12 @@ main_read(struct view *view, char *line)
                break;
        }
        default:
-               /* We should only ever end up here if there has already been a
-                * commit line, however, be safe. */
-               if (view->lines == 0)
+               if (!prev)
                        break;
 
+               commit = prev->data;
+
                /* Fill in the commit title if it has not already been set. */
-               commit = view->line[view->lines - 1];
                if (commit->title[0])
                        break;
 
@@ -1808,7 +1836,7 @@ main_read(struct view *view, char *line)
 }
 
 static bool
-main_enter(struct view *view)
+main_enter(struct view *view, struct line *line)
 {
        enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
 
@@ -2362,6 +2390,44 @@ load_refs(void)
        return OK;
 }
 
+static int
+load_config(void)
+{
+       FILE *pipe = popen("git repo-config --list", "r");
+       char buffer[BUFSIZ];
+       char *name;
+
+       if (!pipe)
+               return ERR;
+
+       while ((name = fgets(buffer, sizeof(buffer), pipe))) {
+               char *value = strchr(name, '=');
+               int valuelen, namelen;
+
+               /* No boolean options, yet */
+               if (!value)
+                       continue;
+
+               namelen  = value - name;
+
+               *value++ = 0;
+               valuelen = strlen(value);
+               if (valuelen > 0)
+                       value[valuelen - 1] = 0;
+
+               if (!strcmp(name, "i18n.commitencoding")) {
+                       string_copy(opt_encoding, value);
+               }
+       }
+
+       if (ferror(pipe))
+               return ERR;
+
+       pclose(pipe);
+
+       return OK;
+}
+
 /*
  * Main
  */
@@ -2416,9 +2482,15 @@ main(int argc, char *argv[])
        if (refs_size == 0 && opt_request != REQ_VIEW_PAGER)
                die("Not a git repository");
 
+       if (load_config() == ERR)
+               die("Failed to load repo config.");
+
        for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
                view->cmd_env = getenv(view->cmd_env);
 
+       if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
+               opt_utf8 = FALSE;
+
        request = opt_request;
 
        init_display();
@@ -2583,6 +2655,9 @@ main(int argc, char *argv[])
  *   outside the current view causing bad wrapping. Same goes
  *   for title and status windows.
  *
+ * - The cursor can wrap-around on the last line and cause the
+ *   window to scroll.
+ *
  * TODO
  * ----
  * Features that should be explored.
@@ -2593,7 +2668,7 @@ main(int argc, char *argv[])
  *
  * COPYRIGHT
  * ---------
- * Copyright (c) Jonas Fonseca <fonseca@diku.dk>, 2006
+ * Copyright (c) 2006 Jonas Fonseca <fonseca@diku.dk>
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -2602,10 +2677,12 @@ main(int argc, char *argv[])
  *
  * SEE ALSO
  * --------
- * [verse]
- * link:http://www.kernel.org/pub/software/scm/git/docs/[git(7)],
- * link:http://www.kernel.org/pub/software/scm/cogito/docs/[cogito(7)]
- * gitk(1): git repository browser written using tcl/tk,
- * qgit(1): git repository browser written using c++/Qt,
- * gitview(1): git repository browser written using python/gtk.
+ * - link:http://www.kernel.org/pub/software/scm/git/docs/[git(7)],
+ * - link:http://www.kernel.org/pub/software/scm/cogito/docs/[cogito(7)]
+ *
+ * Other git repository browsers:
+*
+ *  - gitk(1)
+ *  - qgit(1)
+ *  - gitview(1)
  **/