X-Git-Url: https://git.distorted.org.uk/~mdw/tig/blobdiff_plain/f97f4012315074190e64aa558c04d1659ec6d622..965537fa357e94c1fe6fda5cf26a93c1b06e9d3a:/tig.c diff --git a/tig.c b/tig.c index 05fb370..94af086 100644 --- a/tig.c +++ b/tig.c @@ -1,5 +1,15 @@ /* Copyright (c) 2006 Jonas Fonseca - * 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 as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * 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) * ====== @@ -54,6 +64,7 @@ static void die(const char *err, ...); static void report(const char *msg, ...); +static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, int, char *, int)); static void set_nonblocking_input(bool loading); static size_t utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed); @@ -138,6 +149,29 @@ struct ref { static struct ref **get_refs(char *id); +struct int_map { + const char *name; + int namelen; + int value; +}; + +static int +set_from_int_map(struct int_map *map, size_t map_size, + int *value, const char *name, int namelen) +{ + + int i; + + for (i = 0; i < map_size; i++) + if (namelen == map[i].namelen && + !strncasecmp(name, map[i].name, namelen)) { + *value = map[i].value; + return OK; + } + + return ERR; +} + /* * String helpers @@ -155,6 +189,21 @@ string_ncopy(char *dst, const char *src, int dstlen) #define string_copy(dst, src) \ string_ncopy(dst, src, sizeof(dst)) +static char * +chomp_string(char *name) +{ + int namelen; + + while (isspace(*name)) + name++; + + namelen = strlen(name) - 1; + while (namelen > 0 && isspace(name[namelen])) + name[namelen--] = 0; + + return name; +} + /* Shell quoting * @@ -216,7 +265,7 @@ VERSION " (" __DATE__ ")\n" " -l Start up in log view\n" " -d Start up in diff view\n" " -n[I], --line-number[=I] Show line numbers with given interval\n" -" -t[N], --tab-size[=N] Set number of spaces for tab expansion\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"; @@ -227,6 +276,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. */ @@ -280,14 +331,14 @@ parse_options(int argc, char *argv[]) } /** - * -t[NSPACES], --tab-size[=NSPACES]:: + * -b[NSPACES], --tab-size[=NSPACES]:: * Set the number of spaces tabs should be expanded to. **/ - if (!strncmp(opt, "-t", 2) || + if (!strncmp(opt, "-b", 2) || !strncmp(opt, "--tab-size", 10)) { char *num = opt; - if (opt[1] == 't') { + if (opt[1] == 'b') { num = opt + 2; } else if (opt[STRING_SIZE("--tab-size")] == '=') { @@ -425,46 +476,260 @@ parse_options(int argc, char *argv[]) } + if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8")) + opt_utf8 = FALSE; + return TRUE; } -/* - * Line-oriented content detection. - */ +/** + * 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" + +/** + * [[history-commands]] + * History 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 \ + "" + + +/** + * FILES + * ----- + * '~/.tig':: + * User configuration file. See "<>" + * section for examples. + * + * '.git/config':: + * Repository config file. Read on startup with the help of + * git-repo-config(1). + **/ +/** + * [[config-options]] + * User Configuration file + * ----------------------- + * You can permanently set an option by putting it in the `~/.tig` file. + * The file consists of a series of 'commands'. Each + * line of the file may contain only one command. + * + * The hash mark ('#'), or semi-colon (';') is used as a 'comment' character. + * All text after the comment character to the end of the line is ignored. + * You can use comments to annotate your initialization file. + * + * Some sample options: + * + * ========================================================================== + * # Diff colors + * color diff-header yellow default + * color diff-index blue default + * color diff-chunk magenta default + * # UI colors + * color title-blur white blue + * color title-focus white blue bold + * ========================================================================== + * + * [[color-options]] + * Color options + * ~~~~~~~~~~~~~ + * Color options control highlighting and the user interface styles. + * If your terminal supports color, these commands can be used to assign + * foreground/backgound combinations to certain areas. Optionally, an + * attribute can be given as the last parameter. The syntax is: + * + * [verse] + * .......................................................................... + * *color* 'area' 'fgcolor' 'bgcolor' '[attributes]' + * .......................................................................... + * + * Valid colors include: *white*, *black*, *green*, *magenta*, *blue*, *cyan*, + * *yellow*, *red*, *default*. Use *default* to refer to the default terminal + * colors. + **/ + +static struct int_map color_map[] = { +#define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name } + COLOR_MAP(DEFAULT), + COLOR_MAP(BLACK), + COLOR_MAP(BLUE), + COLOR_MAP(CYAN), + COLOR_MAP(GREEN), + COLOR_MAP(MAGENTA), + COLOR_MAP(RED), + COLOR_MAP(WHITE), + COLOR_MAP(YELLOW), +}; + +/** + * Valid attributes include: *normal*, *blink*, *bold*, *dim*, *reverse*, *standout*, + * and *underline*. Note, not all attributes may be supported by the terminal. + **/ + +static struct int_map attr_map[] = { +#define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name } + ATTR_MAP(NORMAL), + ATTR_MAP(BLINK), + ATTR_MAP(BOLD), + ATTR_MAP(DIM), + ATTR_MAP(REVERSE), + ATTR_MAP(STANDOUT), + ATTR_MAP(UNDERLINE), +}; + +/** + * Valid area names are described below. Note, all names are case-insensitive, + * and you may use '-', '_', and '.' interchangeably. So "Diff-Header", + * "DIFF_HEADER", and "diff.header" are the same. + * + * -- + **/ #define LINE_INFO \ -/* Line type String to match Foreground Background Attributes - * --------- --------------- ---------- ---------- ---------- */ \ -/* Diff markup */ \ -LINE(DIFF, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ -LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \ +/** + * Diff markup:: + * + * Options concerning diff start, chunks and lines added and deleted. + * + * *diff-header*, *diff-chunk*, *diff-add*, *diff-del* + **/ \ +LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \ LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \ LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \ -LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ -LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ -LINE(DIFF_COPY, "copy ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ -LINE(DIFF_RENAME, "rename ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ -LINE(DIFF_SIM, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ -LINE(DIFF_DISSIM, "dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ -/* Pretty print commit header */ \ +/** + * Enhanced git diff markup:: + * + * Extra diff information emitted by the git diff machinery, such as mode + * changes, rename detection, and similarity. + * + * *diff-oldmode*, *diff-newmode*, *diff-copy-from*, *diff-copy-to*, + * *diff-rename-from*, *diff-rename-to*, *diff-similarity* *diff-dissimilarity* + * *diff-tree*, *diff-index* + **/ \ +LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \ +LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ +LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ +LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \ +LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \ +LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \ +LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \ +LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ +LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ +LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \ +/** + * Pretty print commit headers:: + * + * Commit diffs and the revision logs are usually formatted using pretty + * printed headers , unless `--pretty=raw` was given. This includes lines, + * such as merge info, commit ID, and author and comitter date. + * + * *pp-author*, *pp-commit*, *pp-merge*, *pp-date*, *pp-adate*, *pp-cdate* + **/ \ LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \ LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \ LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \ LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ -/* Raw commit header */ \ +/** + * Raw commit header:: + * + * Usually shown when `--pretty=raw` is given, however 'commit' is pretty + * much omnipresent. + * + * *commit*, *parent*, *tree*, *author*, *committer* + **/ \ LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \ LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \ LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \ LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \ LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \ -/* Misc */ \ -LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \ +/** + * Commit message:: + * + * For now only `Signed-off-by lines` are colorized. + * + * *signoff* + **/ \ LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \ -/* UI colors */ \ +/** + * UI colors:: + * + * Colors for text not matching any of the above: *default* + * + * Status window colors: *status* + * + * Title window colors: *title-blur*, *title-focus* + * + * Cursor line colors: *cursor* + * + * Main view specific: *main-date*, *main-author*, *main-commit*, *main-delim*, + * *main-tag*, *main-ref* + **/ \ LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \ LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \ LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \ @@ -475,7 +740,15 @@ 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_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), +LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \ +/** + * -- + **/ + + +/* + * Line-oriented content detection. + */ enum line_type { #define LINE(type, line, fg, bg, attr) \ @@ -485,6 +758,8 @@ enum line_type { }; struct line_info { + const char *name; /* Option name. */ + int namelen; /* Size of option name. */ const char *line; /* The start of line to match. */ int linelen; /* Size of string to match. */ int fg, bg, attr; /* Color and text attributes for the lines. */ @@ -492,7 +767,7 @@ struct line_info { static struct line_info line_info[] = { #define LINE(type, line, fg, bg, attr) \ - { (line), STRING_SIZE(line), (fg), (bg), (attr) } + { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) } LINE_INFO #undef LINE }; @@ -519,6 +794,28 @@ get_line_attr(enum line_type type) return COLOR_PAIR(type) | line_info[type].attr; } +static struct line_info * +get_line_info(char *name, int namelen) +{ + enum line_type type; + int i; + + /* Diff-Header -> DIFF_HEADER */ + for (i = 0; i < namelen; i++) { + if (name[i] == '-') + name[i] = '_'; + else if (name[i] == '.') + name[i] = '_'; + } + + for (type = 0; type < ARRAY_SIZE(line_info); type++) + if (namelen == line_info[type].namelen && + !strncasecmp(line_info[type].name, name, namelen)) + return &line_info[type]; + + return NULL; +} + static void init_colors(void) { @@ -542,81 +839,125 @@ init_colors(void) } } +struct line { + enum line_type type; + void *data; /* User data */ +}; -/** - * 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" +/* + * User config file handling. + */ -/** - * [[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 set_color(color, name, namelen) \ + set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, namelen) -#define TIG_DIFF_CMD \ - "git show --patch-with-stat --find-copies-harder -B -C %s" +#define set_attribute(attr, name, namelen) \ + set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, namelen) -#define TIG_LOG_CMD \ - "git log --cc --stat -n100 %s" +static int config_lineno; +static bool config_errors; +static char *config_msg; -#define TIG_MAIN_CMD \ - "git log --topo-order --stat --pretty=raw %s" +static int +set_option(char *opt, int optlen, char *value, int valuelen) +{ + /* Reads: "color" object fgcolor bgcolor [attr] */ + if (!strcmp(opt, "color")) { + struct line_info *info; + + value = chomp_string(value); + valuelen = strcspn(value, " \t"); + info = get_line_info(value, valuelen); + if (!info) { + config_msg = "Unknown color name"; + return ERR; + } -/* ... silently ignore that the following are also exported. */ + value = chomp_string(value + valuelen); + valuelen = strcspn(value, " \t"); + if (set_color(&info->fg, value, valuelen) == ERR) { + config_msg = "Unknown color"; + return ERR; + } -#define TIG_HELP_CMD \ - "man tig 2>/dev/null" + value = chomp_string(value + valuelen); + valuelen = strcspn(value, " \t"); + if (set_color(&info->bg, value, valuelen) == ERR) { + config_msg = "Unknown color"; + return ERR; + } -#define TIG_PAGER_CMD \ - "" + value = chomp_string(value + valuelen); + if (*value && + set_attribute(&info->attr, value, strlen(value)) == ERR) { + config_msg = "Unknown attribute"; + return ERR; + } + + return OK; + } + + return ERR; +} + +static int +read_option(char *opt, int optlen, char *value, int valuelen) +{ + config_lineno++; + config_msg = "Internal error"; + + optlen = strcspn(opt, "#;"); + if (optlen == 0) { + /* The whole line is a commend or empty. */ + return OK; + + } else if (opt[optlen] != 0) { + /* Part of the option name is a comment, so the value part + * should be ignored. */ + valuelen = 0; + opt[optlen] = value[valuelen] = 0; + } else { + /* Else look for comment endings in the value. */ + valuelen = strcspn(value, "#;"); + value[valuelen] = 0; + } + + if (set_option(opt, optlen, value, valuelen) == ERR) { + fprintf(stderr, "Error on line %d, near '%.*s' option: %s\n", + config_lineno, optlen, opt, config_msg); + config_errors = TRUE; + } + + /* Always keep going if errors are encountered. */ + return OK; +} + +static int +load_options(void) +{ + char *home = getenv("HOME"); + char buf[1024]; + FILE *file; + + config_lineno = 0; + config_errors = FALSE; + + if (!home || + snprintf(buf, sizeof(buf), "%s/.tig", home) >= sizeof(buf)) + return ERR; + + /* It's ok that the file doesn't exist. */ + file = fopen(buf, "r"); + if (!file) + return OK; + + if (read_properties(file, " \t", read_option) == ERR || + config_errors == TRUE) + fprintf(stderr, "Errors while loading %s.\n", buf); + + return OK; +} /** @@ -638,6 +979,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 +1005,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 +1031,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 +1039,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 +1100,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; } @@ -789,6 +1140,10 @@ redraw_view(struct view *view) * [main] c622eefaa485995320bc743431bae0d497b1d875 - commit 1 of 61 (1%) * * By default, the title of the current view is highlighted using bold font. + * For long loading views (taking over 3 seconds) the time since loading + * started will be appended: + * + * [main] 77d9e40fbcea3238015aea403e06f61542df9a31 - commit 1 of 779 (0%) 5s **/ static void @@ -807,12 +1162,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) { @@ -823,7 +1182,7 @@ update_view_title(struct view *view) wprintw(view->title, " %lds", secs); } - + wmove(view->title, 0, view->width - 1); wrefresh(view->title); } @@ -857,11 +1216,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,10 +1228,9 @@ 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); } offset += view->height + 1; @@ -894,6 +1249,20 @@ redraw_display(void) } } +static void +update_display_cursor(void) +{ + struct view *view = display[current_view]; + + /* Move the cursor to the right-most column of the cursor line. + * + * XXX: This could turn out to be a bit expensive, but it ensures that + * the cursor does not jump around. */ + if (view->lines) { + wmove(view->win, view->lineno - view->offset, view->width - 1); + wrefresh(view->win); + } +} /* * Navigation @@ -920,7 +1289,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 +1297,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 +1307,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 +1416,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 +1439,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 +1511,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 +1528,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 +1551,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) @@ -1309,17 +1682,15 @@ open_view(struct view *prev, enum request request, enum open_flags flags) } if (prev && view != prev) { - /* Continue loading split views in the background. */ - if (!split) - end_update(prev); - else if (!backgrounded) + if (split && !backgrounded) { /* "Blur" the previous view. */ update_view_title(prev); + } view->parent = prev; } - if (view->pipe) { + if (view->pipe && view->lines == 0) { /* Clear the old view and let the incremental updating refill * the screen. */ wclear(view->win); @@ -1396,7 +1767,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: { @@ -1425,9 +1796,10 @@ view_driver(struct view *view, enum request request) break; case REQ_STOP_LOADING: - foreach_view (view, i) { + for (i = 0; i < ARRAY_SIZE(views); i++) { + view = &views[i]; if (view->pipe) - report("Stopped loaded the %s view", view->name), + report("Stopped loading the %s view", view->name), end_update(view); } break; @@ -1448,11 +1820,15 @@ view_driver(struct view *view, enum request request) return TRUE; case REQ_VIEW_CLOSE: - if (view->parent) { + /* XXX: Mark closed views by letting view->parent point to the + * view itself. Parents to closed view should never be + * followed. */ + if (view->parent && + view->parent->parent != view->parent) { memset(display, 0, sizeof(display)); current_view = 0; display[current_view] = view->parent; - view->parent = NULL; + view->parent = view; resize_display(); redraw_display(); break; @@ -1476,24 +1852,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 +1874,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 +1893,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; } @@ -1595,8 +1962,8 @@ pager_enter(struct view *view) scroll_view(view, REQ_SCROLL_LINE_DOWN); /* FIXME: A minor workaround. Scrolling the view will call report("") - * but if we are scolling a non-current view this won't properly update - * the view title. */ + * but if we are scrolling a non-current view this won't properly + * update the view title. */ if (split) update_view_title(view); @@ -1624,20 +1991,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 +2027,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 +2091,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 +2104,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 +2114,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 +2157,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 +2181,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; @@ -2175,15 +2548,7 @@ report(const char *msg, ...) } update_view_title(view); - - /* Move the cursor to the right-most column of the cursor line. - * - * XXX: This could turn out to be a bit expensive, but it ensures that - * the cursor does not jump around. */ - if (view->lines) { - wmove(view->win, view->lineno - view->offset, view->width - 1); - wrefresh(view->win); - } + update_display_cursor(); } /* Controls when nodelay should be in effect when polling user input. */ @@ -2295,73 +2660,116 @@ get_refs(char *id) } static int +read_ref(char *id, int idlen, char *name, int namelen) +{ + struct ref *ref; + bool tag = FALSE; + bool tag_commit = FALSE; + + /* Commits referenced by tags has "^{}" appended. */ + if (name[namelen - 1] == '}') { + while (namelen > 0 && name[namelen] != '^') + namelen--; + if (namelen > 0) + tag_commit = TRUE; + name[namelen] = 0; + } + + if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) { + if (!tag_commit) + return OK; + name += STRING_SIZE("refs/tags/"); + tag = TRUE; + + } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) { + name += STRING_SIZE("refs/heads/"); + + } else if (!strcmp(name, "HEAD")) { + return OK; + } + + refs = realloc(refs, sizeof(*refs) * (refs_size + 1)); + if (!refs) + return ERR; + + ref = &refs[refs_size++]; + ref->name = strdup(name); + if (!ref->name) + return ERR; + + ref->tag = tag; + string_copy(ref->id, id); + + return OK; +} + +static int load_refs(void) { const char *cmd_env = getenv("TIG_LS_REMOTE"); const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE; - FILE *pipe = popen(cmd, "r"); - char buffer[BUFSIZ]; - char *line; - if (!pipe) - return ERR; + return read_properties(popen(cmd, "r"), "\t", read_ref); +} - while ((line = fgets(buffer, sizeof(buffer), pipe))) { - char *name = strchr(line, '\t'); - struct ref *ref; - int namelen; - bool tag = FALSE; - bool tag_commit = FALSE; +static int +read_repo_config_option(char *name, int namelen, char *value, int valuelen) +{ + if (!strcmp(name, "i18n.commitencoding")) { + string_copy(opt_encoding, value); + } - if (!name) - continue; + return OK; +} - *name++ = 0; - namelen = strlen(name) - 1; +static int +load_repo_config(void) +{ + return read_properties(popen("git repo-config --list", "r"), + "=", read_repo_config_option); +} - /* Commits referenced by tags has "^{}" appended. */ - if (name[namelen - 1] == '}') { - while (namelen > 0 && name[namelen] != '^') - namelen--; - if (namelen > 0) - tag_commit = TRUE; - } - name[namelen] = 0; +static int +read_properties(FILE *pipe, const char *separators, + int (*read_property)(char *, int, char *, int)) +{ + char buffer[BUFSIZ]; + char *name; + int state = OK; - if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) { - if (!tag_commit) - continue; - name += STRING_SIZE("refs/tags/"); - tag = TRUE; + if (!pipe) + return ERR; - } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) { - name += STRING_SIZE("refs/heads/"); + while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) { + char *value; + size_t namelen; + size_t valuelen; - } else if (!strcmp(name, "HEAD")) { - continue; - } + name = chomp_string(name); + namelen = strcspn(name, separators); - refs = realloc(refs, sizeof(*refs) * (refs_size + 1)); - if (!refs) - return ERR; + if (name[namelen]) { + name[namelen] = 0; + value = chomp_string(name + namelen + 1); + valuelen = strlen(value); - ref = &refs[refs_size++]; - ref->tag = tag; - ref->name = strdup(name); - if (!ref->name) - return ERR; + } else { + value = ""; + valuelen = 0; + } - string_copy(ref->id, line); + state = read_property(name, namelen, value, valuelen); } - if (ferror(pipe)) - return ERR; + if (state != ERR && ferror(pipe)) + state = ERR; pclose(pipe); - return OK; + return state; } + /* * Main */ @@ -2406,6 +2814,14 @@ main(int argc, char *argv[]) signal(SIGINT, quit); + if (load_options() == ERR) + die("Failed to load user config."); + + /* Load the repo config file so options can be overwritten from + * the command line. */ + if (load_repo_config() == ERR) + die("Failed to load repo config."); + if (!parse_options(argc, argv)) return 0; @@ -2448,7 +2864,9 @@ main(int argc, char *argv[]) memcpy(opt_cmd, "git ", 4); opt_request = REQ_VIEW_PAGER; } else { - request = ERR; + report("Prompt interrupted by loading view, " + "press 'z' to stop loading views"); + request = REQ_SCREEN_UPDATE; } noecho(); @@ -2490,7 +2908,8 @@ main(int argc, char *argv[]) * * You can tune the interaction with git by making use of the options * explained in this section. For example, by configuring the environment - * variables described in the <> section. + * variables described in the <> + * section. * * Limit by path name * ~~~~~~~~~~~~~~~~~~ @@ -2583,6 +3002,11 @@ 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. + * + * - The prompt doesn't work while loading. + * * TODO * ---- * Features that should be explored. @@ -2591,9 +3015,11 @@ main(int argc, char *argv[]) * * - Locale support. * + * - Make '?' show a one page keybinding cheat sheet. + * * COPYRIGHT * --------- - * Copyright (c) Jonas Fonseca , 2006 + * Copyright (c) 2006 Jonas Fonseca * * 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 +3028,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) **/