X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=tig.c;h=fb6231f256fbbfbcbb1545dc7caaa6d0a69dd3c1;hb=ba7c7d30bbed330fe7e9ceb4dc9c65a22dc8d6dc;hp=ec7f26732597b98e5f6d36d4c78692cee9cf8839;hpb=fa6db1b4558800ecdeb1554943b7ccb701cafa4d;p=tig.git diff --git a/tig.c b/tig.c index ec7f267..fb6231f 100644 --- a/tig.c +++ b/tig.c @@ -68,9 +68,8 @@ static void __NORETURN die(const char *err, ...); static void warn(const char *msg, ...); static void report(const char *msg, ...); static void set_nonblocking_input(bool loading); -static size_t utf8_length(const char *string, int *width, size_t max_width, int *trimmed, bool reserve); -static bool prompt_yesno(const char *prompt); static int load_refs(void); +static size_t utf8_length(const char **string, size_t col, int *width, size_t max_width, int *trimmed, bool reserve); #define ABS(x) ((x) >= 0 ? (x) : -(x)) #define MIN(x, y) ((x) < (y) ? (x) : (y)) @@ -110,6 +109,7 @@ static int load_refs(void); /* The default interval between line numbers. */ #define NUMBER_INTERVAL 5 +#define SCROLL_INTERVAL 1 #define TAB_SIZE 8 @@ -121,7 +121,7 @@ static int load_refs(void); #define GIT_CONFIG "config" #endif -/* Some ascii-shorthands fitted into the ncurses namespace. */ +/* Some ASCII-shorthands fitted into the ncurses namespace. */ #define KEY_TAB '\t' #define KEY_RETURN '\r' #define KEY_ESC 27 @@ -148,29 +148,17 @@ enum format_flags { static bool format_argv(const char *dst[], const char *src[], enum format_flags flags); -struct int_map { - const char *name; - int namelen; - int value; +enum input_status { + INPUT_OK, + INPUT_SKIP, + INPUT_STOP, + INPUT_CANCEL }; -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; -} +typedef enum input_status (*input_handler)(void *data, char *buf, int c); +static char *prompt_input(const char *prompt, input_handler handler, void *data); +static bool prompt_yesno(const char *prompt); /* * String helpers @@ -200,6 +188,27 @@ 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 void +string_expand(char *dst, size_t dstlen, const char *src, int tabsize) +{ + size_t size, pos; + + for (size = pos = 0; size < dstlen - 1 && src[pos]; pos++) { + if (src[pos] == '\t') { + size_t expanded = tabsize - (size % tabsize); + + if (expanded + size >= dstlen - 1) + expanded = dstlen - size - 1; + memcpy(dst + size, " ", expanded); + size += expanded; + } else { + dst[size++] = src[pos]; + } + } + + dst[size] = 0; +} + static char * chomp_string(char *name) { @@ -259,6 +268,33 @@ string_enum_compare(const char *str1, const char *str2, int len) return 0; } +struct enum_map { + const char *name; + int namelen; + int value; +}; + +#define ENUM_MAP(name, value) { name, STRING_SIZE(name), value } + +static bool +map_enum_do(struct enum_map *map, size_t map_size, int *value, const char *name) +{ + size_t namelen = strlen(name); + int i; + + for (i = 0; i < map_size; i++) + if (namelen == map[i].namelen && + !string_enum_compare(name, map[i].name, namelen)) { + *value = map[i].value; + return TRUE; + } + + return FALSE; +} + +#define map_enum(attr, map, name) \ + map_enum_do(map, ARRAY_SIZE(map), attr, name) + #define prefixcmp(str1, str2) \ strncmp(str1, str2, STRING_SIZE(str2)) @@ -282,7 +318,7 @@ argv_from_string(const char *argv[SIZEOF_ARG], int *argc, char *cmd) cmd[valuelen] = 0; argv[(*argc)++] = chomp_string(cmd); - cmd += valuelen + advance; + cmd = chomp_string(cmd + valuelen + advance); } if (*argc < SIZEOF_ARG) @@ -625,24 +661,75 @@ io_write(struct io *io, const void *buf, size_t bufsize) return written == bufsize; } +static bool +io_read_buf(struct io *io, char buf[], size_t bufsize) +{ + bool error; + + 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; +} + static bool run_io_buf(const char **argv, char buf[], size_t bufsize) { struct io io = {}; - bool error; - if (!run_io_rd(&io, argv, FORMAT_NONE)) - return FALSE; + return run_io_rd(&io, argv, FORMAT_NONE) && io_read_buf(&io, buf, bufsize); +} - io.buf = io.bufpos = buf; - io.bufalloc = bufsize; - error = !io_get(&io, '\n', TRUE) && io_error(&io); - io.buf = NULL; +static int +io_load(struct io *io, const char *separators, + int (*read_property)(char *, size_t, char *, size_t)) +{ + char *name; + int state = OK; - return done_io(&io) || error; + if (!start_io(io)) + return ERR; + + while (state == OK && (name = io_get(io, '\n', TRUE))) { + char *value; + size_t namelen; + size_t valuelen; + + name = chomp_string(name); + namelen = strcspn(name, separators); + + if (name[namelen]) { + name[namelen] = 0; + value = chomp_string(name + namelen + 1); + valuelen = strlen(value); + + } else { + value = ""; + valuelen = 0; + } + + state = read_property(name, namelen, value, valuelen); + } + + if (state != ERR && io_error(io)) + state = ERR; + done_io(io); + + return state; +} + +static int +run_io_load(const char **argv, const char *separators, + int (*read_property)(char *, size_t, char *, size_t)) +{ + struct io io = {}; + + return init_io_rd(&io, argv, NULL, FORMAT_NONE) + ? io_load(&io, separators, read_property) : ERR; } -static int read_properties(struct io *io, const char *separators, int (*read)(char *, size_t, char *, size_t)); /* * User requests @@ -666,6 +753,7 @@ static int read_properties(struct io *io, const char *separators, int (*read)(ch REQ_(ENTER, "Enter current line and scroll"), \ REQ_(NEXT, "Move to next"), \ REQ_(PREVIOUS, "Move to previous"), \ + REQ_(PARENT, "Move to parent"), \ REQ_(VIEW_NEXT, "Move focus to next view"), \ REQ_(REFRESH, "Reload and refresh"), \ REQ_(MAXIMIZE, "Maximize the current view"), \ @@ -677,7 +765,6 @@ static int read_properties(struct io *io, const char *separators, int (*read)(ch REQ_(STATUS_REVERT, "Revert file changes"), \ REQ_(STATUS_MERGE, "Merge file using external tool"), \ REQ_(STAGE_NEXT, "Find next chunk to stage"), \ - REQ_(TREE_PARENT, "Switch to parent directory in tree view"), \ \ REQ_GROUP("Cursor navigation") \ REQ_(MOVE_UP, "Move cursor one line up"), \ @@ -688,6 +775,8 @@ static int read_properties(struct io *io, const char *separators, int (*read)(ch REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \ \ REQ_GROUP("Scrolling") \ + REQ_(SCROLL_LEFT, "Scroll two columns left"), \ + REQ_(SCROLL_RIGHT, "Scroll two columns right"), \ REQ_(SCROLL_LINE_UP, "Scroll one line up"), \ REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \ REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \ @@ -762,19 +851,6 @@ get_request(const char *name) * Options */ -static const char usage[] = -"tig " TIG_VERSION " (" __DATE__ ")\n" -"\n" -"Usage: tig [options] [revs] [--] [paths]\n" -" or: tig show [options] [revs] [--] [paths]\n" -" or: tig blame [rev] path\n" -" or: tig status\n" -" or: tig < [git command output]\n" -"\n" -"Options:\n" -" -v, --version Show version and exit\n" -" -h, --help Show help message and exit"; - /* Option and state variables. */ static bool opt_date = TRUE; static bool opt_author = TRUE; @@ -797,6 +873,7 @@ static char opt_codeset[20] = "UTF-8"; static iconv_t opt_iconv = ICONV_NONE; static char opt_search[SIZEOF_STR] = ""; static char opt_cdup[SIZEOF_STR] = ""; +static char opt_prefix[SIZEOF_STR] = ""; static char opt_git_dir[SIZEOF_STR] = ""; static signed char opt_is_inside_work_tree = -1; /* set to TRUE or FALSE */ static char opt_editor[SIZEOF_STR] = ""; @@ -805,89 +882,6 @@ static FILE *opt_tty = NULL; #define is_initial_commit() (!*opt_head_rev) #define is_head_commit(rev) (!strcmp((rev), "HEAD") || !strcmp(opt_head_rev, (rev))) -static enum request -parse_options(int argc, const char *argv[], const char ***run_argv) -{ - enum request request = REQ_VIEW_MAIN; - const char *subcommand; - bool seen_dashdash = FALSE; - /* XXX: This is vulnerable to the user overriding options - * required for the main view parser. */ - static const char *custom_argv[SIZEOF_ARG] = { - "git", "log", "--no-color", "--pretty=raw", "--parents", - "--topo-order", NULL - }; - int i, j = 6; - - if (!isatty(STDIN_FILENO)) - return REQ_VIEW_PAGER; - - if (argc <= 1) - return REQ_VIEW_MAIN; - - subcommand = argv[1]; - if (!strcmp(subcommand, "status") || !strcmp(subcommand, "-S")) { - if (!strcmp(subcommand, "-S")) - warn("`-S' has been deprecated; use `tig status' instead"); - if (argc > 2) - warn("ignoring arguments after `%s'", subcommand); - return REQ_VIEW_STATUS; - - } else if (!strcmp(subcommand, "blame")) { - if (argc <= 2 || argc > 4) - die("invalid number of options to blame\n\n%s", usage); - - i = 2; - if (argc == 4) { - string_ncopy(opt_ref, argv[i], strlen(argv[i])); - i++; - } - - string_ncopy(opt_file, argv[i], strlen(argv[i])); - return REQ_VIEW_BLAME; - - } else if (!strcmp(subcommand, "show")) { - request = REQ_VIEW_DIFF; - - } else if (!strcmp(subcommand, "log") || !strcmp(subcommand, "diff")) { - request = subcommand[0] == 'l' ? REQ_VIEW_LOG : REQ_VIEW_DIFF; - warn("`tig %s' has been deprecated", subcommand); - - } else { - subcommand = NULL; - } - - if (subcommand) { - custom_argv[1] = subcommand; - j = 2; - } - - for (i = 1 + !!subcommand; i < argc; i++) { - const char *opt = argv[i]; - - if (seen_dashdash || !strcmp(opt, "--")) { - seen_dashdash = TRUE; - - } else if (!strcmp(opt, "-v") || !strcmp(opt, "--version")) { - printf("tig version %s\n", TIG_VERSION); - return REQ_NONE; - - } else if (!strcmp(opt, "-h") || !strcmp(opt, "--help")) { - printf("%s\n", usage); - return REQ_NONE; - } - - custom_argv[j++] = opt; - if (j >= ARRAY_SIZE(custom_argv)) - die("command too long"); - } - - custom_argv[j] = NULL; - *run_argv = custom_argv; - - return request; -} - /* * Line-oriented content detection. @@ -918,7 +912,7 @@ LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \ 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(AUTHOR, "author ", COLOR_GREEN, COLOR_DEFAULT, 0), \ LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \ LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \ LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \ @@ -927,10 +921,10 @@ LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \ LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \ LINE(DELIMITER, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \ LINE(DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \ +LINE(MODE, "", COLOR_CYAN, COLOR_DEFAULT, 0), \ LINE(LINE_NUMBER, "", COLOR_CYAN, COLOR_DEFAULT, 0), \ LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \ LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \ -LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \ LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \ LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \ LINE(MAIN_LOCAL_TAG,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \ @@ -939,7 +933,8 @@ LINE(MAIN_TRACKED, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \ LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, 0), \ LINE(MAIN_HEAD, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \ LINE(MAIN_REVGRAPH,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \ -LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \ +LINE(TREE_HEAD, "", COLOR_DEFAULT, COLOR_DEFAULT, A_BOLD), \ +LINE(TREE_DIR, "", COLOR_YELLOW, COLOR_DEFAULT, A_NORMAL), \ LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \ LINE(STAT_HEAD, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \ LINE(STAT_SECTION, "", COLOR_CYAN, COLOR_DEFAULT, 0), \ @@ -1087,6 +1082,8 @@ static struct keybinding default_keybindings[] = { { '-', REQ_MOVE_PAGE_UP }, /* Scrolling */ + { KEY_LEFT, REQ_SCROLL_LEFT }, + { KEY_RIGHT, REQ_SCROLL_RIGHT }, { KEY_IC, REQ_SCROLL_LINE_UP }, { KEY_DC, REQ_SCROLL_LINE_DOWN }, { 'w', REQ_SCROLL_PAGE_UP }, @@ -1113,7 +1110,7 @@ static struct keybinding default_keybindings[] = { { '!', REQ_STATUS_REVERT }, { 'M', REQ_STATUS_MERGE }, { '@', REQ_STAGE_NEXT }, - { ',', REQ_TREE_PARENT }, + { ',', REQ_PARENT }, { 'e', REQ_EDIT }, }; @@ -1136,14 +1133,13 @@ enum keymap { #undef KEYMAP_ }; -static struct int_map keymap_table[] = { -#define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name } +static struct enum_map keymap_table[] = { +#define KEYMAP_(name) ENUM_MAP(#name, KEYMAP_##name) KEYMAP_INFO #undef KEYMAP_ }; -#define set_keymap(map, name) \ - set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name)) +#define set_keymap(map, name) map_enum(map, keymap_table, name) struct keybinding_table { struct keybinding *data; @@ -1354,8 +1350,12 @@ add_builtin_run_requests(void) * User config file handling. */ -static struct int_map color_map[] = { -#define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name } +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), COLOR_MAP(BLACK), COLOR_MAP(BLUE), @@ -1367,11 +1367,8 @@ static struct int_map color_map[] = { COLOR_MAP(YELLOW), }; -#define set_color(color, name) \ - set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name)) - -static struct int_map attr_map[] = { -#define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name } +static struct enum_map attr_map[] = { +#define ATTR_MAP(name) ENUM_MAP(#name, A_##name) ATTR_MAP(NORMAL), ATTR_MAP(BLINK), ATTR_MAP(BOLD), @@ -1381,14 +1378,33 @@ static struct int_map attr_map[] = { ATTR_MAP(UNDERLINE), }; -#define set_attribute(attr, name) \ - set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(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 [attr] */ +/* Wants: object fgcolor bgcolor [attribute] */ static int option_color_command(int argc, const char *argv[]) { @@ -1401,25 +1417,27 @@ 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"); + 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]) == ERR || - set_color(&info->bg, argv[2]) == ERR) { + if (!set_color(&info->fg, argv[1]) || + !set_color(&info->bg, argv[2])) { config_msg = "Unknown color"; return ERR; } - if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) { + if (argc == 4 && !set_attribute(&info->attr, argv[3])) { config_msg = "Unknown attribute"; return ERR; } @@ -1427,18 +1445,30 @@ option_color_command(int argc, const char *argv[]) return OK; } -static bool parse_bool(const char *s) +static int parse_bool(bool *opt, const char *arg) { - return (!strcmp(s, "1") || !strcmp(s, "true") || - !strcmp(s, "yes")) ? TRUE : FALSE; + *opt = (!strcmp(arg, "1") || !strcmp(arg, "true") || !strcmp(arg, "yes")) + ? TRUE : FALSE; + return OK; } static int -parse_int(const char *s, int default_value, int min, int max) +parse_string(char *opt, const char *arg, size_t optsize) { - int value = atoi(s); + int arglen = strlen(arg); - return (value < min || value > max) ? default_value : value; + switch (arg[0]) { + case '\"': + case '\'': + if (arglen == 1 || arg[arglen - 1] != arg[0]) { + config_msg = "Unmatched quotation"; + return ERR; + } + arg += 1; arglen -= 2; + default: + string_ncopy_do(opt, optsize, arg, strlen(arg)); + return OK; + } } /* Wants: name = value */ @@ -1455,68 +1485,35 @@ option_set_command(int argc, const char *argv[]) return ERR; } - if (!strcmp(argv[0], "show-author")) { - opt_author = parse_bool(argv[2]); - return OK; - } + if (!strcmp(argv[0], "show-author")) + return parse_bool(&opt_author, argv[2]); - if (!strcmp(argv[0], "show-date")) { - opt_date = parse_bool(argv[2]); - return OK; - } - - if (!strcmp(argv[0], "show-rev-graph")) { - opt_rev_graph = parse_bool(argv[2]); - return OK; - } + if (!strcmp(argv[0], "show-date")) + return parse_bool(&opt_date, argv[2]); - if (!strcmp(argv[0], "show-refs")) { - opt_show_refs = parse_bool(argv[2]); - return OK; - } + if (!strcmp(argv[0], "show-rev-graph")) + return parse_bool(&opt_rev_graph, argv[2]); - if (!strcmp(argv[0], "show-line-numbers")) { - opt_line_number = parse_bool(argv[2]); - return OK; - } + if (!strcmp(argv[0], "show-refs")) + return parse_bool(&opt_show_refs, argv[2]); - if (!strcmp(argv[0], "line-graphics")) { - opt_line_graphics = parse_bool(argv[2]); - return OK; - } + if (!strcmp(argv[0], "show-line-numbers")) + return parse_bool(&opt_line_number, argv[2]); - if (!strcmp(argv[0], "line-number-interval")) { - opt_num_interval = parse_int(argv[2], opt_num_interval, 1, 1024); - return OK; - } + if (!strcmp(argv[0], "line-graphics")) + return parse_bool(&opt_line_graphics, argv[2]); - if (!strcmp(argv[0], "author-width")) { - opt_author_cols = parse_int(argv[2], opt_author_cols, 0, 1024); - return OK; - } + if (!strcmp(argv[0], "line-number-interval")) + return parse_int(&opt_num_interval, argv[2], 1, 1024); - if (!strcmp(argv[0], "tab-size")) { - opt_tab_size = parse_int(argv[2], opt_tab_size, 1, 1024); - return OK; - } + if (!strcmp(argv[0], "author-width")) + return parse_int(&opt_author_cols, argv[2], 0, 1024); - if (!strcmp(argv[0], "commit-encoding")) { - const char *arg = argv[2]; - int arglen = strlen(arg); + if (!strcmp(argv[0], "tab-size")) + return parse_int(&opt_tab_size, argv[2], 1, 1024); - switch (arg[0]) { - case '"': - case '\'': - if (arglen == 1 || arg[arglen - 1] != arg[0]) { - config_msg = "Unmatched quotation"; - return ERR; - } - arg += 1; arglen -= 2; - default: - string_ncopy(opt_encoding, arg, strlen(arg)); - return OK; - } - } + if (!strcmp(argv[0], "commit-encoding")) + return parse_string(opt_encoding, argv[2], sizeof(opt_encoding)); config_msg = "Unknown variable name"; return ERR; @@ -1548,16 +1545,18 @@ option_bind_command(int argc, const char *argv[]) request = get_request(argv[2]); if (request == REQ_NONE) { - const char *obsolete[] = { "cherry-pick", "screen-resize" }; - size_t namelen = strlen(argv[2]); - int i; + static struct enum_map obsolete[] = { + ENUM_MAP("cherry-pick", REQ_NONE), + ENUM_MAP("screen-resize", REQ_NONE), + ENUM_MAP("tree-parent", REQ_PARENT), + }; + int alias; - for (i = 0; i < ARRAY_SIZE(obsolete); i++) { - if (namelen == strlen(obsolete[i]) && - !string_enum_compare(obsolete[i], argv[2], namelen)) { - config_msg = "Obsolete request name"; - return ERR; - } + if (map_enum(&alias, obsolete, argv[2])) { + if (alias != REQ_NONE) + add_keybinding(keymap, alias, key); + config_msg = "Obsolete request name"; + return ERR; } } if (request == REQ_NONE && *argv[2]++ == '!') @@ -1627,8 +1626,8 @@ read_option(char *opt, size_t optlen, char *value, size_t valuelen) } if (status == ERR) { - fprintf(stderr, "Error on line %d, near '%.*s': %s\n", - config_lineno, (int) optlen, opt, config_msg); + warn("Error on line %d, near '%.*s': %s", + config_lineno, (int) optlen, opt, config_msg); config_errors = TRUE; } @@ -1641,16 +1640,16 @@ load_option_file(const char *path) { struct io io = {}; - /* It's ok that the file doesn't exist. */ + /* It's OK that the file doesn't exist. */ if (!io_open(&io, path)) return; config_lineno = 0; config_errors = FALSE; - if (read_properties(&io, " \t", read_option) == ERR || + if (io_load(&io, " \t", read_option) == ERR || config_errors == TRUE) - fprintf(stderr, "Errors while loading %s.\n", path); + warn("Errors while loading %s.", path); } static int @@ -1663,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) { @@ -1692,9 +1688,6 @@ struct view_ops; static struct view *display[2]; static unsigned int current_view; -/* Reading from the prompt? */ -static bool input_mode = FALSE; - #define foreach_displayed_view(view, i) \ for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++) @@ -1724,14 +1717,16 @@ struct view { /* Navigation */ unsigned long offset; /* Offset of the window top */ + unsigned long yoffset; /* Offset from the window side. */ unsigned long lineno; /* Current line number */ unsigned long p_offset; /* Previous offset of the window top */ + unsigned long p_yoffset;/* Previous offset from the window side */ unsigned long p_lineno; /* Previous current line number */ bool p_restore; /* Should the previous position be restored. */ /* Searching */ char grep[SIZEOF_STR]; /* Search string */ - regex_t *regex; /* Pre-compiled regex */ + regex_t *regex; /* Pre-compiled regexp */ /* If non-NULL, points to the view that opened this view. If this view * is closed tig will switch back to the parent view. */ @@ -1747,6 +1742,8 @@ struct view { struct line *curline; /* Line currently being drawn. */ enum line_type curtype; /* Attribute currently used for drawing. */ unsigned long col; /* Column when drawing. */ + bool has_scrolled; /* View was scrolled. */ + bool can_hscroll; /* View can be scrolled horizontally. */ /* Loading */ struct io io; @@ -1768,7 +1765,7 @@ struct view_ops { bool (*draw)(struct view *view, struct line *line, unsigned int lineno); /* Depending on view handle a special requests. */ enum request (*request)(struct view *view, enum request request, struct line *line); - /* Search for regex in a line. */ + /* Search for regexp in a line. */ bool (*grep)(struct view *view, struct line *line); /* Select line */ void (*select)(struct view *view, struct line *line); @@ -1840,12 +1837,13 @@ draw_chars(struct view *view, enum line_type type, const char *string, int len = 0; int col = 0; int trimmed = FALSE; + size_t skip = view->yoffset > view->col ? view->yoffset - view->col : 0; if (max_len <= 0) return 0; if (opt_utf8) { - len = utf8_length(string, &col, max_len, &trimmed, use_tilde); + len = utf8_length(&string, skip, &col, max_len, &trimmed, use_tilde); } else { col = len = strlen(string); if (len > max_len) { @@ -1858,13 +1856,17 @@ draw_chars(struct view *view, enum line_type type, const char *string, } set_view_attr(view, type); - waddnstr(view->win, string, len); + if (len > 0) + waddnstr(view->win, string, len); if (trimmed && use_tilde) { set_view_attr(view, LINE_DELIMITER); waddch(view->win, '~'); col++; } + if (view->col + col >= view->width + view->yoffset) + view->can_hscroll = TRUE; + return col; } @@ -1889,6 +1891,7 @@ draw_space(struct view *view, enum line_type type, int max, int spaces) static bool draw_lineno(struct view *view, unsigned int lineno) { + size_t skip = view->yoffset > view->col ? view->yoffset - view->col : 0; char number[10]; int digits3 = view->digits < 3 ? 3 : view->digits; int max_number = MIN(digits3, STRING_SIZE(number)); @@ -1912,29 +1915,31 @@ draw_lineno(struct view *view, unsigned int lineno) col = draw_space(view, LINE_LINE_NUMBER, max_number, max_number); } - if (col < max) { + if (col < max && skip <= col) { set_view_attr(view, LINE_DEFAULT); waddch(view->win, line_graphics[LINE_GRAPHIC_VLINE]); - col++; } + col++; - if (col < max) - col += draw_space(view, LINE_DEFAULT, max - col, 1); view->col += col; + if (col < max && skip <= col) + col = draw_space(view, LINE_DEFAULT, max - col, 1); + view->col++; - return view->width - view->col <= 0; + return view->width + view->yoffset <= view->col; } static bool draw_text(struct view *view, enum line_type type, const char *string, bool trim) { - view->col += draw_chars(view, type, string, view->width - view->col, trim); + view->col += draw_chars(view, type, string, view->width + view->yoffset - view->col, trim); return view->width - view->col <= 0; } static bool draw_graphic(struct view *view, enum line_type type, chtype graphic[], size_t size) { + size_t skip = view->yoffset > view->col ? view->yoffset - view->col : 0; int max = view->width - view->col; int i; @@ -1944,14 +1949,13 @@ draw_graphic(struct view *view, enum line_type type, chtype graphic[], size_t si set_view_attr(view, type); /* Using waddch() instead of waddnstr() ensures that * they'll be rendered correctly for the cursor line. */ - for (i = 0; i < size; i++) + for (i = skip; i < size; i++) waddch(view->win, graphic[i]); view->col += size; - if (size < max) { + if (size < max && skip <= size) waddch(view->win, ' '); - view->col++; - } + view->col++; return view->width - view->col <= 0; } @@ -1967,8 +1971,9 @@ draw_field(struct view *view, enum line_type type, const char *text, int len, bo else col = draw_space(view, type, max - 1, max - 1); - view->col += col + draw_space(view, LINE_DEFAULT, max - col, max - col); - return view->width - view->col <= 0; + view->col += col; + view->col += draw_space(view, LINE_DEFAULT, max - col, max - col); + return view->width + view->yoffset <= view->col; } static bool @@ -1985,12 +1990,58 @@ draw_date(struct view *view, struct tm *time) return draw_field(view, LINE_DATE, date, DATE_COLS, FALSE); } +static bool +draw_author(struct view *view, const char *author) +{ + bool trim = opt_author_cols == 0 || opt_author_cols > 5 || !author; + + if (!trim) { + static char initials[10]; + size_t pos; + +#define is_initial_sep(c) (isspace(c) || ispunct(c) || (c) == '@') + + memset(initials, 0, sizeof(initials)); + for (pos = 0; *author && pos < opt_author_cols - 1; author++, pos++) { + while (is_initial_sep(*author)) + author++; + strncpy(&initials[pos], author, sizeof(initials) - 1 - pos); + while (*author && !is_initial_sep(author[1])) + author++; + } + + author = initials; + } + + return draw_field(view, LINE_AUTHOR, author, opt_author_cols, trim); +} + +static bool +draw_mode(struct view *view, mode_t mode) +{ + static const char dir_mode[] = "drwxr-xr-x"; + static const char link_mode[] = "lrwxrwxrwx"; + static const char exe_mode[] = "-rwxr-xr-x"; + static const char file_mode[] = "-rw-r--r--"; + const char *str; + + if (S_ISDIR(mode)) + str = dir_mode; + else if (S_ISLNK(mode)) + str = link_mode; + else if (mode & S_IXUSR) + str = exe_mode; + else + str = file_mode; + + return draw_field(view, LINE_MODE, str, sizeof(file_mode), FALSE); +} + static bool draw_view_line(struct view *view, unsigned int lineno) { struct line *line; bool selected = (view->offset + lineno == view->lineno); - bool draw_ok; assert(view_is_displayed(view)); @@ -2014,11 +2065,7 @@ draw_view_line(struct view *view, unsigned int lineno) view->ops->select(view, line); } - scrollok(view->win, FALSE); - draw_ok = view->ops->draw(view, line, lineno); - scrollok(view->win, TRUE); - - return draw_ok; + return view->ops->draw(view, line, lineno); } static void @@ -2039,11 +2086,7 @@ redraw_view_dirty(struct view *view) if (!dirty) return; - redrawwin(view->win); - if (input_mode) - wnoutrefresh(view->win); - else - wrefresh(view->win); + wnoutrefresh(view->win); } static void @@ -2051,16 +2094,15 @@ redraw_view_from(struct view *view, int lineno) { assert(0 <= lineno && lineno < view->height); + if (lineno == 0) + view->can_hscroll = FALSE; + for (; lineno < view->height; lineno++) { if (!draw_view_line(view, lineno)) break; } - redrawwin(view->win); - if (input_mode) - wnoutrefresh(view->win); - else - wrefresh(view->win); + wnoutrefresh(view->win); } static void @@ -2123,12 +2165,7 @@ update_view_title(struct view *view) mvwaddnstr(view->title, 0, 0, buf, bufpos); wclrtoeol(view->title); - wmove(view->title, 0, view->width - 1); - - if (input_mode) - wnoutrefresh(view->title); - else - wrefresh(view->title); + wnoutrefresh(view->title); } static void @@ -2166,7 +2203,7 @@ resize_display(void) if (!view->win) die("Failed to create %s view", view->name); - scrollok(view->win, TRUE); + scrollok(view->win, FALSE); view->title = newwin(1, 0, offset + view->height, 0); if (!view->title) @@ -2196,19 +2233,6 @@ redraw_display(bool clear) } } -static void -update_display_cursor(struct view *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); - } -} - static void toggle_view_option(bool *option, const char *help) { @@ -2221,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) @@ -2252,19 +2300,19 @@ do_scroll_view(struct view *view, int lines) int line = lines > 0 ? view->height - lines : 0; int end = line + ABS(lines); + scrollok(view->win, TRUE); wscrl(view->win, lines); + scrollok(view->win, FALSE); - for (; line < end; line++) { - if (!draw_view_line(view, line)) - break; - } + while (line < end && draw_view_line(view, line)) + line++; if (redraw_current_line) draw_view_line(view, view->lineno - view->offset); + wnoutrefresh(view->win); } - redrawwin(view->win); - wrefresh(view->win); + view->has_scrolled = TRUE; report(""); } @@ -2277,6 +2325,27 @@ scroll_view(struct view *view, enum request request) assert(view_is_displayed(view)); switch (request) { + case REQ_SCROLL_LEFT: + if (view->yoffset == 0) { + report("Cannot scroll beyond the first column"); + return; + } + if (view->yoffset <= SCROLL_INTERVAL) + view->yoffset = 0; + else + view->yoffset -= SCROLL_INTERVAL; + redraw_view_from(view, 0); + report(""); + return; + case REQ_SCROLL_RIGHT: + if (!view->can_hscroll) { + report("Cannot scroll beyond the last column"); + return; + } + view->yoffset += SCROLL_INTERVAL; + redraw_view(view); + report(""); + return; case REQ_SCROLL_PAGE_DOWN: lines = view->height; case REQ_SCROLL_LINE_DOWN: @@ -2397,8 +2466,7 @@ move_view(struct view *view, enum request request) /* Draw the current line */ draw_view_line(view, view->lineno - view->offset); - redrawwin(view->win); - wrefresh(view->win); + wnoutrefresh(view->win); report(""); } @@ -2412,21 +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); + unsigned long old_lineno = view->lineno; + unsigned long old_offset = view->offset; - } else { - unsigned long old_lineno = view->lineno - 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); - redrawwin(view->win); - wrefresh(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]); } @@ -2520,10 +2585,12 @@ reset_view(struct view *view) free(view->line); view->p_offset = view->offset; + view->p_yoffset = view->yoffset; view->p_lineno = view->lineno; view->line = NULL; view->offset = 0; + view->yoffset = 0; view->lines = 0; view->lineno = 0; view->line_alloc = 0; @@ -2616,25 +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->lineno = view->p_lineno; + view->yoffset = view->p_yoffset; view->p_restore = FALSE; return TRUE; @@ -2758,7 +2811,7 @@ update_view(struct view *view) if (view->lines == 0) { time_t secs = time(NULL) - view->start_time; - if (secs > view->update_secs) { + if (secs > 1 && secs > view->update_secs) { if (view->update_secs == 0) redraw_view(view); update_view_title(view); @@ -2783,8 +2836,11 @@ update_view(struct view *view) line = out_buffer; } - if (!view->ops->read(view, line)) - goto alloc_error; + if (!view->ops->read(view, line)) { + report("Allocation failure"); + end_update(view, TRUE); + return FALSE; + } } { @@ -2826,11 +2882,6 @@ update_view(struct view *view) * commit reference in view->ref it'll be available here. */ update_view_title(view); return TRUE; - -alloc_error: - report("Allocation failure"); - end_update(view, TRUE); - return FALSE; } static struct line * @@ -2879,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. */ }; @@ -2889,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]; @@ -2909,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)); @@ -2949,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); } @@ -2967,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 @@ -3072,6 +3114,8 @@ view_driver(struct view *view, enum request request) move_view(view, request); break; + case REQ_SCROLL_LEFT: + case REQ_SCROLL_RIGHT: case REQ_SCROLL_LINE_DOWN: case REQ_SCROLL_LINE_UP: case REQ_SCROLL_PAGE_DOWN: @@ -3267,6 +3311,114 @@ 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 1138474660 +0100 + */ +static void +parse_author_line(char *ident, char *author, size_t authorsize, struct tm *tm) +{ + char *nameend = strchr(ident, '<'); + char *emailend = strchr(ident, '>'); + + if (nameend && emailend) + *nameend = *emailend = 0; + ident = chomp_string(ident); + if (!*ident) { + if (nameend) + ident = chomp_string(nameend + 1); + if (!*ident) + ident = "Unknown"; + } + + string_ncopy_do(author, authorsize, ident, strlen(ident)); + + /* Parse epoch and timezone */ + if (emailend && emailend[1] == ' ') { + char *secs = emailend + 2; + char *zone = strchr(secs, ' '); + time_t time = (time_t) atol(secs); + + if (zone && strlen(zone) == STRING_SIZE(" +0700")) + parse_timezone(&time, zone + 1); + + gmtime_r(&time, tm); + } +} + +static enum input_status +select_commit_parent_handler(void *data, char *buf, int c) +{ + size_t parents = *(size_t *) data; + int parent = 0; + + if (!isdigit(c)) + return INPUT_SKIP; + + if (*buf) + parent = atoi(buf) * 10; + parent += c - '0'; + + if (parent > parents) + return INPUT_SKIP; + return INPUT_OK; +} + +static bool +select_commit_parent(const char *id, char rev[SIZEOF_REV]) +{ + char buf[SIZEOF_STR * 4]; + const char *revlist_argv[] = { + "git", "rev-list", "-1", "--parents", id, NULL + }; + int parents; + + if (!run_io_buf(revlist_argv, buf, sizeof(buf)) || + !*chomp_string(buf) || + (parents = (strlen(buf) / 40) - 1) < 0) { + report("Failed to get parent information"); + return FALSE; + + } else if (parents == 0) { + report("The selected commit has no parents"); + return FALSE; + } + + if (parents > 1) { + char prompt[SIZEOF_STR]; + char *result; + + if (!string_format(prompt, "Which parent? [1..%d] ", parents)) + return FALSE; + result = prompt_input(prompt, select_commit_parent_handler, &parents); + if (!result) + return FALSE; + parents = atoi(result); + } + + string_copy_rev(rev, &buf[41 * parents]); + return TRUE; +} + /* * Pager backend */ @@ -3274,11 +3426,12 @@ view_driver(struct view *view, enum request request) static bool pager_draw(struct view *view, struct line *line, unsigned int lineno) { - char *text = line->data; + char text[SIZEOF_STR]; if (opt_line_number && draw_lineno(view, lineno)) return TRUE; + string_expand(text, sizeof(text), line->data, opt_tab_size); draw_text(view, line->type, text, TRUE); return TRUE; } @@ -3485,22 +3638,13 @@ static struct view_ops diff_ops = { static bool help_open(struct view *view) { - int lines = ARRAY_SIZE(req_info) + 2; + char buf[SIZEOF_STR]; + size_t bufpos; int i; if (view->lines > 0) return TRUE; - for (i = 0; i < ARRAY_SIZE(req_info); i++) - if (!req_info[i].request) - lines++; - - lines += run_requests + 1; - - view->line = calloc(lines, sizeof(*view->line)); - if (!view->line) - return FALSE; - add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT); for (i = 0; i < ARRAY_SIZE(req_info); i++) { @@ -3519,8 +3663,14 @@ help_open(struct view *view) if (!*key) key = "(no key defined)"; - add_line_format(view, LINE_DEFAULT, " %-25s %s", - key, req_info[i].help); + for (bufpos = 0; bufpos <= req_info[i].namelen; bufpos++) { + buf[bufpos] = tolower(req_info[i].name[bufpos]); + if (buf[bufpos] == '_') + buf[bufpos] = '-'; + } + + add_line_format(view, LINE_DEFAULT, " %-25s %-20s %s", + key, buf, req_info[i].help); } if (run_requests) { @@ -3531,8 +3681,6 @@ help_open(struct view *view) for (i = 0; i < run_requests; i++) { struct run_request *req = get_run_request(REQ_NONE + i + 1); const char *key; - char cmd[SIZEOF_STR]; - size_t bufpos; int argc; if (!req) @@ -3543,12 +3691,12 @@ help_open(struct view *view) key = "(no key defined)"; for (bufpos = 0, argc = 0; req->argv[argc]; argc++) - if (!string_format_from(cmd, &bufpos, "%s%s", + if (!string_format_from(buf, &bufpos, "%s%s", argc ? " " : "", req->argv[argc])) return REQ_NONE; add_line_format(view, LINE_DEFAULT, " %-10s %-14s `%s`", - keymap_table[req->keymap].name, key, cmd); + keymap_table[req->keymap].name, key, buf); } return TRUE; @@ -3616,25 +3764,33 @@ push_tree_stack_entry(const char *name, unsigned long lineno) /* Parse output from git-ls-tree(1): * - * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile - * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c - * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf */ #define SIZEOF_TREE_ATTR \ - STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t") + STRING_SIZE("100644 blob f931e1d229c3e185caad4449bf5b66ed72462657\t") -#define TREE_UP_FORMAT "040000 tree %s\t.." +#define SIZEOF_TREE_MODE \ + STRING_SIZE("100644 ") + +#define TREE_ID_OFFSET \ + STRING_SIZE("100644 blob ") + +struct tree_entry { + char id[SIZEOF_REV]; + mode_t mode; + struct tm time; /* Date from the author ident. */ + char author[75]; /* Author of the commit. */ + char name[1]; +}; static const char * tree_path(struct line *line) { - const char *path = line->data; - - return path + SIZEOF_TREE_ATTR; + return ((struct tree_entry *) line->data)->name; } + static int tree_compare_entry(struct line *line1, struct line *line2) { @@ -3643,30 +3799,126 @@ tree_compare_entry(struct line *line1, struct line *line2) return strcmp(tree_path(line1), tree_path(line2)); } +static struct line * +tree_entry(struct view *view, enum line_type type, const char *path, + const char *mode, const char *id) +{ + struct tree_entry *entry = calloc(1, sizeof(*entry) + strlen(path)); + struct line *line = entry ? add_line_data(view, entry, type) : NULL; + + if (!entry || !line) { + free(entry); + return NULL; + } + + strncpy(entry->name, path, strlen(path)); + if (mode) + entry->mode = strtoul(mode, NULL, 8); + if (id) + string_copy_rev(entry->id, id); + + return line; +} + +static bool +tree_read_date(struct view *view, char *text, bool *read_date) +{ + static char author_name[SIZEOF_STR]; + static struct tm author_time; + + if (!text && *read_date) { + *read_date = FALSE; + return TRUE; + + } else if (!text) { + char *path = *opt_path ? opt_path : "."; + /* Find next entry to process */ + const char *log_file[] = { + "git", "log", "--no-color", "--pretty=raw", + "--cc", "--raw", view->id, "--", path, NULL + }; + struct io io = {}; + + if (!view->lines) { + tree_entry(view, LINE_TREE_HEAD, opt_path, NULL, NULL); + report("Tree is empty"); + return TRUE; + } + + if (!run_io_rd(&io, log_file, FORMAT_NONE)) { + report("Failed to load tree data"); + return TRUE; + } + + done_io(view->pipe); + view->io = io; + *read_date = TRUE; + return FALSE; + + } else if (*text == 'a' && get_line_type(text) == LINE_AUTHOR) { + parse_author_line(text + STRING_SIZE("author "), + author_name, sizeof(author_name), &author_time); + + } else if (*text == ':') { + char *pos; + size_t annotated = 1; + size_t i; + + pos = strchr(text, '\t'); + if (!pos) + return TRUE; + text = pos + 1; + if (*opt_prefix && !strncmp(text, opt_prefix, strlen(opt_prefix))) + text += strlen(opt_prefix); + if (*opt_path && !strncmp(text, opt_path, strlen(opt_path))) + text += strlen(opt_path); + pos = strchr(text, '/'); + if (pos) + *pos = 0; + + for (i = 1; i < view->lines; i++) { + struct line *line = &view->line[i]; + struct tree_entry *entry = line->data; + + annotated += !!*entry->author; + if (*entry->author || strcmp(entry->name, text)) + continue; + + string_copy(entry->author, author_name); + memcpy(&entry->time, &author_time, sizeof(entry->time)); + line->dirty = 1; + break; + } + + if (annotated == view->lines) + kill_io(view->pipe); + } + return TRUE; +} + static bool tree_read(struct view *view, char *text) { - size_t textlen = text ? strlen(text) : 0; + static bool read_date = FALSE; + struct tree_entry *data; struct line *entry, *line; enum line_type type; + size_t textlen = text ? strlen(text) : 0; + char *path = text + SIZEOF_TREE_ATTR; + + if (read_date || !text) + return tree_read_date(view, text, &read_date); - if (!text) - return TRUE; if (textlen <= SIZEOF_TREE_ATTR) return FALSE; - - type = text[STRING_SIZE("100644 ")] == 't' - ? LINE_TREE_DIR : LINE_TREE_FILE; - if (view->lines == 0 && - !add_line_format(view, LINE_DEFAULT, "Directory path /%s", opt_path)) + !tree_entry(view, LINE_TREE_HEAD, opt_path, NULL, NULL)) return FALSE; /* Strip the path part ... */ if (*opt_path) { size_t pathlen = textlen - SIZEOF_TREE_ATTR; size_t striplen = strlen(opt_path); - char *path = text + SIZEOF_TREE_ATTR; if (pathlen > striplen) memmove(path, path + striplen, @@ -3674,14 +3926,15 @@ tree_read(struct view *view, char *text) /* Insert "link" to parent directory. */ if (view->lines == 1 && - !add_line_format(view, LINE_TREE_DIR, TREE_UP_FORMAT, view->ref)) + !tree_entry(view, LINE_TREE_DIR, "..", "040000", view->ref)) return FALSE; } - entry = add_line_text(view, text, type); + type = text[SIZEOF_TREE_MODE] == 't' ? LINE_TREE_DIR : LINE_TREE_FILE; + entry = tree_entry(view, type, path, text, text + TREE_ID_OFFSET); if (!entry) return FALSE; - text = entry->data; + data = entry->data; /* Skip "Directory ..." and ".." line. */ for (line = &view->line[1 + !!*opt_path]; line < entry; line++) { @@ -3690,7 +3943,7 @@ tree_read(struct view *view, char *text) memmove(line + 1, line, (entry - line) * sizeof(*entry)); - line->data = text; + line->data = data; line->type = type; for (; line <= entry; line++) line->dirty = line->cleareol = 1; @@ -3705,6 +3958,29 @@ tree_read(struct view *view, char *text) return TRUE; } +static bool +tree_draw(struct view *view, struct line *line, unsigned int lineno) +{ + struct tree_entry *entry = line->data; + + if (line->type == LINE_TREE_HEAD) { + if (draw_text(view, line->type, "Directory path /", TRUE)) + return TRUE; + } else { + if (draw_mode(view, entry->mode)) + return TRUE; + + if (opt_author && draw_author(view, entry->author)) + return TRUE; + + if (opt_date && draw_date(view, *entry->author ? &entry->time : NULL)) + return TRUE; + } + if (draw_text(view, line->type, entry->name, TRUE)) + return TRUE; + return TRUE; +} + static void open_blob_editor() { @@ -3746,7 +4022,7 @@ tree_request(struct view *view, enum request request, struct line *line) } return REQ_NONE; - case REQ_TREE_PARENT: + case REQ_PARENT: if (!*opt_path) { /* quit view if at top of tree */ return REQ_VIEW_CLOSE; @@ -3768,7 +4044,7 @@ tree_request(struct view *view, enum request request, struct line *line) switch (line->type) { case LINE_TREE_DIR: - /* Depending on whether it is a subdir or parent (updir?) link + /* Depending on whether it is a subdirectory or parent link * mangle the path buffer. */ if (line == &view->line[1] && *opt_path) { pop_tree_stack_entry(); @@ -3791,13 +4067,12 @@ tree_request(struct view *view, enum request request, struct line *line) break; default: - return TRUE; + return REQ_NONE; } open_view(view, request, flags); - if (request == REQ_VIEW_TREE) { + if (request == REQ_VIEW_TREE) view->lineno = tree_lineno; - } return REQ_NONE; } @@ -3805,17 +4080,17 @@ tree_request(struct view *view, enum request request, struct line *line) static void tree_select(struct view *view, struct line *line) { - char *text = (char *)line->data + STRING_SIZE("100644 blob "); + struct tree_entry *entry = line->data; if (line->type == LINE_TREE_FILE) { - string_copy_rev(ref_blob, text); + string_copy_rev(ref_blob, entry->id); string_format(opt_file, "%s%s", opt_path, tree_path(line)); } else if (line->type != LINE_TREE_DIR) { return; } - string_copy_rev(view->ref, text); + string_copy_rev(view->ref, entry->id); } static const char *tree_argv[SIZEOF_ARG] = { @@ -3827,7 +4102,7 @@ static struct view_ops tree_ops = { tree_argv, NULL, tree_read, - pager_draw, + tree_draw, tree_request, pager_grep, tree_select, @@ -3902,6 +4177,7 @@ struct blame_commit { struct blame { struct blame_commit *commit; + unsigned long lineno; char text[1]; }; @@ -3965,14 +4241,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; @@ -3986,6 +4264,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; } @@ -4016,6 +4295,9 @@ blame_read_file(struct view *view, const char *line, bool *read_file) size_t linelen = strlen(line); struct blame *blame = malloc(sizeof(*blame) + linelen); + if (!blame) + return FALSE; + blame->commit = NULL; strncpy(blame->text, line, linelen); blame->text[linelen] = 0; @@ -4071,17 +4353,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)) { @@ -4104,6 +4376,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; @@ -4114,8 +4387,7 @@ blame_draw(struct view *view, struct line *line, unsigned int lineno) if (opt_date && draw_date(view, time)) return TRUE; - if (opt_author && - draw_field(view, LINE_MAIN_AUTHOR, author, opt_author_cols, TRUE)) + if (opt_author && draw_author(view, author)) return TRUE; if (draw_field(view, LINE_BLAME_ID, id, ID_COLS, FALSE)) @@ -4124,7 +4396,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; } @@ -4150,10 +4423,19 @@ 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)) + open_view(view, REQ_VIEW_BLAME, OPEN_REFRESH); + break; + case REQ_ENTER: if (!blame->commit) { report("No commit loaded yet"); @@ -4318,7 +4600,6 @@ status_get_diff(struct status *file, const char *buf, size_t bufsize) static bool status_run(struct view *view, const char *argv[], char status, enum line_type type) { - struct status *file = NULL; struct status *unmerged = NULL; char *buf; struct io io = {}; @@ -4329,6 +4610,8 @@ status_run(struct view *view, const char *argv[], char status, enum line_type ty add_line_data(view, NULL, type); while ((buf = io_get(&io, 0, TRUE))) { + struct status *file = unmerged; + if (!file) { file = calloc(1, sizeof(*file)); if (!file || !add_line_data(view, file, type)) @@ -4341,7 +4624,7 @@ status_run(struct view *view, const char *argv[], char status, enum line_type ty if (status == 'A') string_copy(file->old.rev, NULL_ID); - } else if (!file->status) { + } else if (!file->status || file == unmerged) { if (!status_get_diff(file, buf, strlen(buf))) goto error_out; @@ -4349,21 +4632,13 @@ status_run(struct view *view, const char *argv[], char status, enum line_type ty if (!buf) break; - /* Collapse all 'M'odified entries that follow a - * associated 'U'nmerged entry. */ - if (file->status == 'U') { - unmerged = file; - - } else if (unmerged) { - int collapse = !strcmp(buf, unmerged->new.name); - + /* Collapse all modified entries that follow an + * associated unmerged entry. */ + if (unmerged == file) { + unmerged->status = 'U'; unmerged = NULL; - if (collapse) { - free(file); - file = NULL; - view->lines--; - continue; - } + } else if (file->status == 'U') { + unmerged = file; } } @@ -4433,18 +4708,67 @@ status_restore(struct view *view) while (view->p_lineno > 0 && !view->line[view->p_lineno].data) view->p_lineno--; - /* If the above fails, always skip the "On branch" line. */ - if (view->p_lineno < view->lines) - view->lineno = view->p_lineno; - else - view->lineno = 1; + /* If the above fails, always skip the "On branch" line. */ + if (view->p_lineno < view->lines) + view->lineno = view->p_lineno; + else + view->lineno = 1; + + if (view->lineno < view->offset) + view->offset = view->lineno; + else if (view->offset + view->height <= view->lineno) + view->offset = view->lineno - view->height + 1; + + 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 (view->lineno < view->offset) - view->offset = view->lineno; - else if (view->offset + view->height <= view->lineno) - view->offset = view->lineno - view->height + 1; + 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/"); + } + } - view->p_restore = FALSE; + 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 @@ -4456,12 +4780,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); @@ -4509,7 +4828,7 @@ status_draw(struct view *view, struct line *line, unsigned int lineno) case LINE_STAT_NONE: type = LINE_DEFAULT; - text = " (no files)"; + text = " (no files)"; break; case LINE_STAT_HEAD: @@ -4802,13 +5121,20 @@ status_revert(struct status *status, enum line_type type, bool has_none) return FALSE; } else { + char mode[10] = "100644"; + const char *reset_argv[] = { + "git", "update-index", "--cacheinfo", mode, + status->old.rev, status->old.name, NULL + }; const char *checkout_argv[] = { "git", "checkout", "--", status->old.name, NULL }; if (!prompt_yesno("Are you sure you want to overwrite any changes?")) return FALSE; - return run_io_fg(checkout_argv, opt_cdup); + string_format(mode, "%o", status->old.mode); + return (status->status != 'U' || run_io_fg(reset_argv, opt_cdup)) && + run_io_fg(checkout_argv, opt_cdup); } } @@ -5172,7 +5498,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. */ @@ -5449,8 +5775,7 @@ main_draw(struct view *view, struct line *line, unsigned int lineno) if (opt_date && draw_date(view, &commit->time)) return TRUE; - if (opt_author && - draw_field(view, LINE_MAIN_AUTHOR, commit->author, opt_author_cols, TRUE)) + if (opt_author && draw_author(view, commit->author)) return TRUE; if (opt_rev_graph && commit->graph_size && @@ -5556,56 +5881,13 @@ main_read(struct view *view, char *line) break; case LINE_AUTHOR: - { - /* Parse author lines where the name may be empty: - * author 1138474660 +0100 - */ - char *ident = line + STRING_SIZE("author "); - char *nameend = strchr(ident, '<'); - char *emailend = strchr(ident, '>'); - - if (!nameend || !emailend) - break; - + parse_author_line(line + STRING_SIZE("author "), + commit->author, sizeof(commit->author), + &commit->time); update_rev_graph(view, graph); graph = graph->next; - - *nameend = *emailend = 0; - ident = chomp_string(ident); - if (!*ident) { - ident = chomp_string(nameend + 1); - if (!*ident) - ident = "Unknown"; - } - - string_ncopy(commit->author, ident, strlen(ident)); - view->line[view->lines - 1].dirty = 1; - - /* Parse epoch and timezone */ - if (emailend[1] == ' ') { - char *secs = emailend + 2; - 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; - } - - gmtime_r(&time, &commit->time); - } break; - } + default: /* Fill in the commit title if it has not already been set. */ if (commit->title[0]) @@ -5625,7 +5907,7 @@ main_read(struct view *view, char *line) /* FIXME: More graceful handling of titles; append "..." to * shortened titles, etc. */ - string_ncopy(commit->title, line, strlen(line)); + string_expand(commit->title, sizeof(commit->title), line, 1); view->line[view->lines - 1].dirty = 1; } @@ -5734,15 +6016,11 @@ static struct view_ops main_ops = { /* * Unicode / UTF-8 handling * - * NOTE: Much of the following code for dealing with unicode is derived from + * NOTE: Much of the following code for dealing with Unicode is derived from * ELinks' UTF-8 code developed by Scrool . Origin file is - * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28. + * src/intl/charset.c from the UTF-8 branch commit elinks-0.11.0-g31f2c28. */ -/* I've (over)annotated a lot of code snippets because I am not entirely - * confident that the approach taken by this small UTF-8 interface is correct. - * --jonas */ - static inline int unicode_width(unsigned long c) { @@ -5780,7 +6058,7 @@ static const unsigned char utf8_bytes[256] = { 3,3,3,3,3,3,3,3, 3,3,3,3,3,3,3,3, 4,4,4,4,4,4,4,4, 5,5,5,5,6,6,1,1, }; -/* Decode UTF-8 multi-byte representation into a unicode character. */ +/* Decode UTF-8 multi-byte representation into a Unicode character. */ static inline unsigned long utf8_to_unicode(const char *string, size_t length) { @@ -5821,7 +6099,7 @@ utf8_to_unicode(const char *string, size_t length) unicode += (string[5] & 0x3f); break; default: - die("Invalid unicode length"); + die("Invalid Unicode length"); } /* Invalid characters could return the special 0xfffd value but NUL @@ -5836,9 +6114,9 @@ utf8_to_unicode(const char *string, size_t length) * * Returns the number of bytes to output from string to satisfy max_width. */ static size_t -utf8_length(const char *string, int *width, size_t max_width, int *trimmed, bool reserve) +utf8_length(const char **start, size_t skip, int *width, size_t max_width, int *trimmed, bool reserve) { - const char *start = string; + const char *string = *start; const char *end = strchr(string, '\0'); unsigned char last_bytes = 0; size_t last_ucwidth = 0; @@ -5859,11 +6137,15 @@ utf8_length(const char *string, int *width, size_t max_width, int *trimmed, bool * it is a single- or double-width character. */ unicode = utf8_to_unicode(string, bytes); - /* FIXME: Graceful handling of invalid unicode character. */ + /* FIXME: Graceful handling of invalid Unicode character. */ if (!unicode) break; ucwidth = unicode_width(unicode); + if (skip > 0) { + skip -= ucwidth <= skip ? ucwidth : skip; + *start += bytes; + } *width += ucwidth; if (*width > max_width) { *trimmed = 1; @@ -5876,11 +6158,11 @@ utf8_length(const char *string, int *width, size_t max_width, int *trimmed, bool } string += bytes; - last_bytes = bytes; + last_bytes = ucwidth ? bytes : 0; last_ucwidth = ucwidth; } - return string - start; + return string - *start; } @@ -5891,10 +6173,17 @@ utf8_length(const char *string, int *width, size_t max_width, int *trimmed, bool /* Whether or not the curses interface has been initialized. */ static bool cursed = FALSE; +/* Terminal hacks and workarounds. */ +static bool use_scroll_redrawwin; +static bool use_scroll_status_wclear; + /* The status window is used for polling keystrokes. */ static WINDOW *status_win; -static bool status_empty = TRUE; +/* Reading from the prompt? */ +static bool input_mode = FALSE; + +static bool status_empty = FALSE; /* Update status and title window. */ static void @@ -5926,6 +6215,8 @@ report(const char *msg, ...) va_start(args, msg); wmove(status_win, 0, 0); + if (view->has_scrolled && use_scroll_status_wclear) + wclear(status_win); if (*msg) { vwprintw(status_win, msg, args); status_empty = FALSE; @@ -5933,13 +6224,12 @@ report(const char *msg, ...) status_empty = TRUE; } wclrtoeol(status_win); - wrefresh(status_win); + wnoutrefresh(status_win); va_end(args); } update_view_title(view); - update_display_cursor(view); } /* Controls when nodelay should be in effect when polling user input. */ @@ -5956,6 +6246,7 @@ set_nonblocking_input(bool loading) static void init_display(void) { + const char *term; int x, y; /* Initialize the curses library */ @@ -5973,10 +6264,10 @@ init_display(void) if (!cursed) die("Failed to initialize curses"); - nonl(); /* Tell curses not to do NL->CR/NL on output */ + nonl(); /* Disable conversion and detect newlines from input. */ cbreak(); /* Take input chars one at a time, no wait for \n */ noecho(); /* Don't echo input */ - leaveok(stdscr, TRUE); + leaveok(stdscr, FALSE); if (has_colors()) init_colors(); @@ -5994,41 +6285,77 @@ init_display(void) if (opt_line_graphics) { line_graphics[LINE_GRAPHIC_VLINE] = ACS_VLINE; } + + term = getenv("XTERM_VERSION") ? NULL : getenv("COLORTERM"); + if (term && !strcmp(term, "gnome-terminal")) { + /* In the gnome-terminal-emulator, the message from + * scrolling up one line when impossible followed by + * scrolling down one line causes corruption of the + * status line. This is fixed by calling wclear. */ + use_scroll_status_wclear = TRUE; + use_scroll_redrawwin = FALSE; + + } else if (term && !strcmp(term, "xrvt-xpm")) { + /* No problems with full optimizations in xrvt-(unicode) + * and aterm. */ + use_scroll_status_wclear = use_scroll_redrawwin = FALSE; + + } else { + /* When scrolling in (u)xterm the last line in the + * scrolling direction will update slowly. */ + use_scroll_redrawwin = TRUE; + use_scroll_status_wclear = FALSE; + } } static int -get_input(bool prompting) +get_input(int prompt_position) { struct view *view; - int i, key; + int i, key, cursor_y, cursor_x; - if (prompting) + if (prompt_position) input_mode = TRUE; - while (true) { - foreach_view (view, i) + while (TRUE) { + foreach_view (view, i) { update_view(view); + if (view_is_displayed(view) && view->has_scrolled && + use_scroll_redrawwin) + redrawwin(view->win); + view->has_scrolled = FALSE; + } + + /* Update the cursor position. */ + if (prompt_position) { + getbegyx(status_win, cursor_y, cursor_x); + cursor_x = prompt_position; + } else { + view = display[current_view]; + getbegyx(view->win, cursor_y, cursor_x); + cursor_x = view->width - 1; + cursor_y += view->lineno - view->offset; + } + setsyx(cursor_y, cursor_x); /* Refresh, accept single keystroke of input */ + doupdate(); key = wgetch(status_win); /* wgetch() with nodelay() enabled returns ERR when * there's no input. */ if (key == ERR) { - doupdate(); } else if (key == KEY_RESIZE) { int height, width; getmaxyx(stdscr, height, width); - /* Resize the status view and let the view driver take - * care of resizing the displayed views. */ - resize_display(); - redraw_display(TRUE); wresize(status_win, 1, width); mvwin(status_win, height - 1, 0); - wrefresh(status_win); + wnoutrefresh(status_win); + resize_display(); + redraw_display(TRUE); } else { input_mode = FALSE; @@ -6037,76 +6364,38 @@ get_input(bool prompting) } } -static bool -prompt_yesno(const char *prompt) -{ - enum { WAIT, STOP, CANCEL } status = WAIT; - bool answer = FALSE; - - while (status == WAIT) { - int key; - - mvwprintw(status_win, 0, 0, "%s [Yy]/[Nn]", prompt); - wclrtoeol(status_win); - - key = get_input(TRUE); - switch (key) { - case 'y': - case 'Y': - answer = TRUE; - status = STOP; - break; - - case KEY_ESC: - case KEY_RETURN: - case KEY_ENTER: - case KEY_BACKSPACE: - case 'n': - case 'N': - case '\n': - default: - answer = FALSE; - status = CANCEL; - } - } - - /* Clear the status window */ - status_empty = FALSE; - report(""); - - return answer; -} - static char * -read_prompt(const char *prompt) +prompt_input(const char *prompt, input_handler handler, void *data) { - enum { READING, STOP, CANCEL } status = READING; + enum input_status status = INPUT_OK; static char buf[SIZEOF_STR]; - int pos = 0; + size_t pos = 0; - while (status == READING) { + buf[pos] = 0; + + while (status == INPUT_OK || status == INPUT_SKIP) { int key; mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf); wclrtoeol(status_win); - key = get_input(TRUE); + key = get_input(pos + 1); switch (key) { case KEY_RETURN: case KEY_ENTER: case '\n': - status = pos ? STOP : CANCEL; + status = pos ? INPUT_STOP : INPUT_CANCEL; break; case KEY_BACKSPACE: if (pos > 0) - pos--; + buf[--pos] = 0; else - status = CANCEL; + status = INPUT_CANCEL; break; case KEY_ESC: - status = CANCEL; + status = INPUT_CANCEL; break; default: @@ -6115,7 +6404,8 @@ read_prompt(const char *prompt) return NULL; } - if (isprint(key)) + status = handler(data, buf, key); + if (status == INPUT_OK) buf[pos++] = (char) key; } } @@ -6124,7 +6414,7 @@ read_prompt(const char *prompt) status_empty = FALSE; report(""); - if (status == CANCEL) + if (status == INPUT_CANCEL) return NULL; buf[pos++] = 0; @@ -6132,21 +6422,43 @@ read_prompt(const char *prompt) return buf; } -/* - * Repository properties - */ +static enum input_status +prompt_yesno_handler(void *data, char *buf, int c) +{ + if (c == 'y' || c == 'Y') + return INPUT_STOP; + if (c == 'n' || c == 'N') + return INPUT_CANCEL; + return INPUT_SKIP; +} -static int -git_properties(const char **argv, const char *separators, - int (*read_property)(char *, size_t, char *, size_t)) +static bool +prompt_yesno(const char *prompt) { - struct io io = {}; + char prompt2[SIZEOF_STR]; - if (init_io_rd(&io, argv, NULL, FORMAT_NONE)) - return read_properties(&io, separators, read_property); - return ERR; + if (!string_format(prompt2, "%s [Yy/Nn]", prompt)) + return FALSE; + + return !!prompt_input(prompt2, prompt_yesno_handler, NULL); +} + +static enum input_status +read_prompt_handler(void *data, char *buf, int c) +{ + return isprint(c) ? INPUT_OK : INPUT_SKIP; +} + +static char * +read_prompt(const char *prompt) +{ + return prompt_input(prompt, read_prompt_handler, NULL); } +/* + * Repository properties + */ + static struct ref *refs = NULL; static size_t refs_alloc = 0; static size_t refs_size = 0; @@ -6270,7 +6582,7 @@ read_ref(char *id, size_t idlen, char *name, size_t namelen) } if (check_replace && !strcmp(name, refs[refs_size - 1].name)) { - /* it's an annotated tag, replace the previous sha1 with the + /* 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 before the commit id * it points to. */ @@ -6321,7 +6633,23 @@ load_refs(void) while (id_refs_size > 0) free(id_refs[--id_refs_size]); - return git_properties(ls_remote_argv, "\t", read_ref); + return run_io_load(ls_remote_argv, "\t", read_ref); +} + +static void +set_repo_config_option(char *name, char *value, int (*cmd)(int, const char **)) +{ + const char *argv[SIZEOF_ARG] = { name, "=" }; + int argc = 1 + (cmd == option_set_command); + int error = ERR; + + if (!argv_from_string(argv, &argc, value)) + config_msg = "Too many option arguments"; + else + error = cmd(argc, argv); + + if (error == ERR) + warn("Option 'tig.%s': %s", name, config_msg); } static int @@ -6333,6 +6661,15 @@ read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen if (!strcmp(name, "core.editor")) string_ncopy(opt_editor, value, valuelen); + if (!prefixcmp(name, "tig.color.")) + set_repo_config_option(name + 10, value, option_color_command); + + else if (!prefixcmp(name, "tig.bind.")) + set_repo_config_option(name + 9, value, option_bind_command); + + else if (!prefixcmp(name, "tig.")) + set_repo_config_option(name + 4, value, option_set_command); + /* branch..remote */ if (*opt_head && !strncmp(name, "branch.", 7) && @@ -6363,7 +6700,7 @@ load_git_config(void) { const char *config_list_argv[] = { "git", GIT_CONFIG, "--list", NULL }; - return git_properties(config_list_argv, "=", read_repo_config_option); + return run_io_load(config_list_argv, "=", read_repo_config_option); } static int @@ -6379,8 +6716,12 @@ read_repo_info(char *name, size_t namelen, char *value, size_t valuelen) * the option else either "true" or "false" is printed. * Default to true for the unknown case. */ opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE; - } else { + + } else if (*name == '.') { string_ncopy(opt_cdup, name, namelen); + + } else { + string_ncopy(opt_prefix, name, namelen); } return OK; @@ -6394,7 +6735,7 @@ load_repo_info(void) }; const char *rev_parse_argv[] = { "git", "rev-parse", "--git-dir", "--is-inside-work-tree", - "--show-cdup", NULL + "--show-cdup", "--show-prefix", NULL }; if (run_io_buf(head_argv, opt_head, sizeof(opt_head))) { @@ -6406,45 +6747,7 @@ load_repo_info(void) } } - return git_properties(rev_parse_argv, "=", read_repo_info); -} - -static int -read_properties(struct io *io, const char *separators, - int (*read_property)(char *, size_t, char *, size_t)) -{ - char *name; - int state = OK; - - if (!start_io(io)) - return ERR; - - while (state == OK && (name = io_get(io, '\n', TRUE))) { - char *value; - size_t namelen; - size_t valuelen; - - name = chomp_string(name); - namelen = strcspn(name, separators); - - if (name[namelen]) { - name[namelen] = 0; - value = chomp_string(name + namelen + 1); - valuelen = strlen(value); - - } else { - value = ""; - valuelen = 0; - } - - state = read_property(name, namelen, value, valuelen); - } - - if (state != ERR && io_error(io)) - state = ERR; - done_io(io); - - return state; + return run_io_load(rev_parse_argv, "=", read_repo_info); } @@ -6452,6 +6755,19 @@ read_properties(struct io *io, const char *separators, * Main */ +static const char usage[] = +"tig " TIG_VERSION " (" __DATE__ ")\n" +"\n" +"Usage: tig [options] [revs] [--] [paths]\n" +" or: tig show [options] [revs] [--] [paths]\n" +" or: tig blame [rev] path\n" +" or: tig status\n" +" or: tig < [git command output]\n" +"\n" +"Options:\n" +" -v, --version Show version and exit\n" +" -h, --help Show help message and exit"; + static void __NORETURN quit(int sig) { @@ -6489,12 +6805,90 @@ warn(const char *msg, ...) va_end(args); } +static enum request +parse_options(int argc, const char *argv[]) +{ + enum request request = REQ_VIEW_MAIN; + const char *subcommand; + bool seen_dashdash = FALSE; + /* XXX: This is vulnerable to the user overriding options + * required for the main view parser. */ + const char *custom_argv[SIZEOF_ARG] = { + "git", "log", "--no-color", "--pretty=raw", "--parents", + "--topo-order", NULL + }; + int i, j = 6; + + if (!isatty(STDIN_FILENO)) { + io_open(&VIEW(REQ_VIEW_PAGER)->io, ""); + return REQ_VIEW_PAGER; + } + + if (argc <= 1) + return REQ_NONE; + + subcommand = argv[1]; + if (!strcmp(subcommand, "status")) { + if (argc > 2) + warn("ignoring arguments after `%s'", subcommand); + return REQ_VIEW_STATUS; + + } else if (!strcmp(subcommand, "blame")) { + if (argc <= 2 || argc > 4) + die("invalid number of options to blame\n\n%s", usage); + + i = 2; + if (argc == 4) { + string_ncopy(opt_ref, argv[i], strlen(argv[i])); + i++; + } + + string_ncopy(opt_file, argv[i], strlen(argv[i])); + return REQ_VIEW_BLAME; + + } else if (!strcmp(subcommand, "show")) { + request = REQ_VIEW_DIFF; + + } else { + subcommand = NULL; + } + + if (subcommand) { + custom_argv[1] = subcommand; + j = 2; + } + + for (i = 1 + !!subcommand; i < argc; i++) { + const char *opt = argv[i]; + + if (seen_dashdash || !strcmp(opt, "--")) { + seen_dashdash = TRUE; + + } else if (!strcmp(opt, "-v") || !strcmp(opt, "--version")) { + printf("tig version %s\n", TIG_VERSION); + quit(0); + + } else if (!strcmp(opt, "-h") || !strcmp(opt, "--help")) { + printf("%s\n", usage); + quit(0); + } + + custom_argv[j++] = opt; + if (j >= ARRAY_SIZE(custom_argv)) + die("command too long"); + } + + if (!prepare_update(VIEW(request), custom_argv, NULL, FORMAT_NONE)) + die("Failed to format arguments"); + + return request; +} + int main(int argc, const char *argv[]) { - const char **run_argv = NULL; + enum request request = parse_options(argc, argv); struct view *view; - enum request request; size_t i; signal(SIGINT, quit); @@ -6514,10 +6908,6 @@ main(int argc, const char *argv[]) if (load_git_config() == ERR) die("Failed to load repo config."); - request = parse_options(argc, argv, &run_argv); - if (request == REQ_NONE) - return 0; - /* Require a git repository unless when running in pager mode. */ if (!opt_git_dir[0] && request != REQ_VIEW_PAGER) die("Not a git repository"); @@ -6539,17 +6929,12 @@ main(int argc, const char *argv[]) init_display(); - if (request == REQ_VIEW_PAGER || run_argv) { - if (request == REQ_VIEW_PAGER) - io_open(&VIEW(request)->io, ""); - else if (!prepare_update(VIEW(request), run_argv, NULL, FORMAT_NONE)) - die("Failed to format arguments"); + if (request != REQ_NONE) open_view(NULL, request, OPEN_PREPARED); - request = REQ_NONE; - } + request = request == REQ_NONE ? REQ_VIEW_MAIN : REQ_NONE; while (view_driver(display[current_view], request)) { - int key = get_input(FALSE); + int key = get_input(0); view = display[current_view]; request = get_keybinding(view->keymap, key); @@ -6561,7 +6946,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; @@ -6591,6 +6986,10 @@ main(int argc, const char *argv[]) if (search) string_ncopy(opt_search, search, strlen(search)); + else if (*opt_search) + request = request == REQ_SEARCH ? + REQ_FIND_NEXT : + REQ_FIND_PREV; else request = REQ_NONE; break;