X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=tig.c;h=c1dd51366a2694bffe61de408c9e3f2c0ce37e61;hb=74323336d4df16c9465109dd72cee4316afbb04f;hp=27342bc2b4403ebd36ad27aee3963a1c8a10aed0;hpb=04cd7add72b518b12d782f935997dda94b6e5828;p=tig.git diff --git a/tig.c b/tig.c index 27342bc..c1dd513 100644 --- a/tig.c +++ b/tig.c @@ -36,6 +36,7 @@ #include #include #include +#include #include #include @@ -68,11 +69,12 @@ 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 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); +static inline unsigned char utf8_char_length(const char *string, const char *end); #define ABS(x) ((x) >= 0 ? (x) : -(x)) #define MIN(x, y) ((x) < (y) ? (x) : (y)) +#define MAX(x, y) ((x) > (y) ? (x) : (y)) #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0])) #define STRING_SIZE(x) (sizeof(x) - 1) @@ -103,25 +105,17 @@ static size_t utf8_length(const char **string, size_t col, int *width, size_t ma /* The format and size of the date column in the main view. */ #define DATE_FORMAT "%Y-%m-%d %H:%M" #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ") +#define DATE_SHORT_COLS STRING_SIZE("2006-04-29 ") -#define AUTHOR_COLS 20 #define ID_COLS 8 +#define AUTHOR_COLS 19 -/* The default interval between line numbers. */ -#define NUMBER_INTERVAL 5 - -#define TAB_SIZE 8 - -#define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3) +#define MIN_VIEW_HEIGHT 4 #define NULL_ID "0000000000000000000000000000000000000000" #define S_ISGITLINK(mode) (((mode) & S_IFMT) == 0160000) -#ifndef GIT_CONFIG -#define GIT_CONFIG "config" -#endif - /* Some ASCII-shorthands fitted into the ncurses namespace. */ #define KEY_TAB '\t' #define KEY_RETURN '\r' @@ -129,18 +123,24 @@ static size_t utf8_length(const char **string, size_t col, int *width, size_t ma struct ref { - char *name; /* Ref name; tag or head names are shortened. */ char id[SIZEOF_REV]; /* Commit SHA1 ID */ unsigned int head:1; /* Is it the current HEAD? */ unsigned int tag:1; /* Is it a tag? */ unsigned int ltag:1; /* If so, is the tag local? */ unsigned int remote:1; /* Is it a remote ref? */ unsigned int tracked:1; /* Is it the remote for the current HEAD? */ - unsigned int next:1; /* For ref lists: are there more refs? */ + char name[1]; /* Ref name; tag or head names are shortened. */ +}; + +struct ref_list { + char id[SIZEOF_REV]; /* Commit SHA1 ID */ + size_t size; /* Number of refs. */ + struct ref **refs; /* References for this ID. */ }; -static struct ref **get_refs(const char *id); -static void foreach_ref(bool (*visitor)(void *data, struct ref *ref), void *data); +static struct ref_list *get_ref_list(const char *id); +static void foreach_ref(bool (*visitor)(void *data, const struct ref *ref), void *data); +static int load_refs(void); enum format_flags { FORMAT_ALL, /* Perform replacement in all arguments. */ @@ -162,6 +162,14 @@ 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); +struct menu_item { + int hotkey; + const char *text; + void *data; +}; + +static bool prompt_menu(const char *prompt, const struct menu_item *items, int *selected); + /* * Allocation helpers ... Entering macro hell to never be seen again. */ @@ -291,6 +299,9 @@ string_enum_compare(const char *str1, const char *str2, int len) return 0; } +#define enum_equals(entry, str, len) \ + ((entry).namelen == (len) && !string_enum_compare((entry).name, str, len)) + struct enum_map { const char *name; int namelen; @@ -299,6 +310,24 @@ struct enum_map { #define ENUM_MAP(name, value) { name, STRING_SIZE(name), value } +static char * +enum_map_name(const char *name, size_t namelen) +{ + static char buf[SIZEOF_STR]; + int bufpos; + + for (bufpos = 0; bufpos <= namelen; bufpos++) { + buf[bufpos] = tolower(name[bufpos]); + if (buf[bufpos] == '_') + buf[bufpos] = '-'; + } + + buf[bufpos] = 0; + return buf; +} + +#define enum_name(entry) enum_map_name((entry).name, (entry).namelen) + static bool map_enum_do(const struct enum_map *map, size_t map_size, int *value, const char *name) { @@ -306,8 +335,7 @@ map_enum_do(const struct enum_map *map, size_t map_size, int *value, const char int i; for (i = 0; i < map_size; i++) - if (namelen == map[i].namelen && - !string_enum_compare(name, map[i].name, namelen)) { + if (enum_equals(map[i], name, namelen)) { *value = map[i].value; return TRUE; } @@ -331,17 +359,129 @@ suffixcmp(const char *str, int slen, const char *suffix) } +#define DATE_INFO \ + DATE_(NO), \ + DATE_(DEFAULT), \ + DATE_(RELATIVE), \ + DATE_(SHORT) + +enum date { +#define DATE_(name) DATE_##name + DATE_INFO +#undef DATE_ +}; + +static const struct enum_map date_map[] = { +#define DATE_(name) ENUM_MAP(#name, DATE_##name) + DATE_INFO +#undef DATE_ +}; + +struct time { + time_t sec; + int tz; +}; + +static inline int timecmp(const struct time *t1, const struct time *t2) +{ + return t1->sec - t2->sec; +} + static const char * -mkdate(const time_t *time) +string_date(const struct time *time, enum date date) { static char buf[DATE_COLS + 1]; + static const struct enum_map reldate[] = { + { "second", 1, 60 * 2 }, + { "minute", 60, 60 * 60 * 2 }, + { "hour", 60 * 60, 60 * 60 * 24 * 2 }, + { "day", 60 * 60 * 24, 60 * 60 * 24 * 7 * 2 }, + { "week", 60 * 60 * 24 * 7, 60 * 60 * 24 * 7 * 5 }, + { "month", 60 * 60 * 24 * 30, 60 * 60 * 24 * 30 * 12 }, + }; struct tm tm; - gmtime_r(time, &tm); + if (date == DATE_RELATIVE) { + struct timeval now; + time_t date = time->sec + time->tz; + time_t seconds; + int i; + + gettimeofday(&now, NULL); + seconds = now.tv_sec < date ? date - now.tv_sec : now.tv_sec - date; + for (i = 0; i < ARRAY_SIZE(reldate); i++) { + if (seconds >= reldate[i].value) + continue; + + seconds /= reldate[i].namelen; + if (!string_format(buf, "%ld %s%s %s", + seconds, reldate[i].name, + seconds > 1 ? "s" : "", + now.tv_sec >= date ? "ago" : "ahead")) + break; + return buf; + } + } + + gmtime_r(&time->sec, &tm); return strftime(buf, sizeof(buf), DATE_FORMAT, &tm) ? buf : NULL; } +#define AUTHOR_VALUES \ + AUTHOR_(NO), \ + AUTHOR_(FULL), \ + AUTHOR_(ABBREVIATED) + +enum author { +#define AUTHOR_(name) AUTHOR_##name + AUTHOR_VALUES, +#undef AUTHOR_ + AUTHOR_DEFAULT = AUTHOR_FULL +}; + +static const struct enum_map author_map[] = { +#define AUTHOR_(name) ENUM_MAP(#name, AUTHOR_##name) + AUTHOR_VALUES +#undef AUTHOR_ +}; + +static const char * +get_author_initials(const char *author) +{ + static char initials[AUTHOR_COLS * 6 + 1]; + size_t pos = 0; + const char *end = strchr(author, '\0'); + +#define is_initial_sep(c) (isspace(c) || ispunct(c) || (c) == '@' || (c) == '-') + + memset(initials, 0, sizeof(initials)); + while (author < end) { + unsigned char bytes; + size_t i; + + while (is_initial_sep(*author)) + author++; + + bytes = utf8_char_length(author, end); + if (bytes < sizeof(initials) - 1 - pos) { + while (bytes--) { + initials[pos++] = *author++; + } + } + + for (i = pos; author < end && !is_initial_sep(*author); author++) { + if (i < sizeof(initials) - 1) + initials[i++] = *author; + } + + initials[i++] = 0; + } + + return initials; +} + + static bool argv_from_string(const char *argv[SIZEOF_ARG], int *argc, char *cmd) { @@ -428,9 +568,22 @@ init_io_rd(struct io *io, const char *argv[], const char *dir, } static bool -io_open(struct io *io, const char *name) +io_open(struct io *io, const char *fmt, ...) { + char name[SIZEOF_STR] = ""; + bool fits; + va_list args; + init_io(io, NULL, IO_FD); + + va_start(args, fmt); + fits = vsnprintf(name, sizeof(name), fmt, args) < sizeof(name); + va_end(args); + + if (!fits) { + io->error = ENAMETOOLONG; + return FALSE; + } io->pipe = *name ? open(name, O_RDONLY) : STDIN_FILENO; if (io->pipe == -1) io->error = errno; @@ -576,9 +729,9 @@ run_io_append(const char **argv, enum format_flags flags, int fd) } static bool -run_io_rd(struct io *io, const char **argv, enum format_flags flags) +run_io_rd(struct io *io, const char **argv, const char *dir, enum format_flags flags) { - return init_io_rd(io, argv, NULL, flags) && start_io(io); + return init_io_rd(io, argv, dir, flags) && start_io(io); } static bool @@ -715,7 +868,8 @@ run_io_buf(const char **argv, char buf[], size_t bufsize) { struct io io = {}; - return run_io_rd(&io, argv, FORMAT_NONE) && io_read_buf(&io, buf, bufsize); + return run_io_rd(&io, argv, NULL, FORMAT_NONE) + && io_read_buf(&io, buf, bufsize); } static int @@ -826,11 +980,15 @@ run_io_load(const char **argv, const char *separators, REQ_(FIND_PREV, "Find previous search match"), \ \ REQ_GROUP("Option manipulation") \ + REQ_(OPTIONS, "Open option menu"), \ REQ_(TOGGLE_LINENO, "Toggle line numbers"), \ REQ_(TOGGLE_DATE, "Toggle date display"), \ + REQ_(TOGGLE_DATE_SHORT, "Toggle short (date-only) dates"), \ REQ_(TOGGLE_AUTHOR, "Toggle author display"), \ REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"), \ REQ_(TOGGLE_REFS, "Toggle reference display (tags/branches)"), \ + REQ_(TOGGLE_SORT_ORDER, "Toggle ascending/descending sort order"), \ + REQ_(TOGGLE_SORT_FIELD, "Toggle field to sort by"), \ \ REQ_GROUP("Misc") \ REQ_(PROMPT, "Bring up the prompt"), \ @@ -876,8 +1034,7 @@ get_request(const char *name) int i; for (i = 0; i < ARRAY_SIZE(req_info); i++) - if (req_info[i].namelen == namelen && - !string_enum_compare(req_info[i].name, name, namelen)) + if (enum_equals(req_info[i], name, namelen)) return req_info[i].request; return REQ_NONE; @@ -889,16 +1046,17 @@ get_request(const char *name) */ /* Option and state variables. */ -static bool opt_date = TRUE; -static bool opt_author = TRUE; +static enum date opt_date = DATE_DEFAULT; +static enum author opt_author = AUTHOR_DEFAULT; static bool opt_line_number = FALSE; static bool opt_line_graphics = TRUE; static bool opt_rev_graph = FALSE; static bool opt_show_refs = TRUE; -static int opt_num_interval = NUMBER_INTERVAL; +static int opt_num_interval = 5; static double opt_hscroll = 0.50; -static int opt_tab_size = TAB_SIZE; -static int opt_author_cols = AUTHOR_COLS-1; +static double opt_scale_split_view = 2.0 / 3.0; +static int opt_tab_size = 8; +static int opt_author_cols = AUTHOR_COLS; static char opt_path[SIZEOF_STR] = ""; static char opt_file[SIZEOF_STR] = ""; static char opt_ref[SIZEOF_REF] = ""; @@ -906,9 +1064,9 @@ static char opt_head[SIZEOF_REF] = ""; static char opt_head_rev[SIZEOF_REV] = ""; static char opt_remote[SIZEOF_REF] = ""; static char opt_encoding[20] = "UTF-8"; -static bool opt_utf8 = TRUE; static char opt_codeset[20] = "UTF-8"; -static iconv_t opt_iconv = ICONV_NONE; +static iconv_t opt_iconv_in = ICONV_NONE; +static iconv_t opt_iconv_out = ICONV_NONE; static char opt_search[SIZEOF_STR] = ""; static char opt_cdup[SIZEOF_STR] = ""; static char opt_prefix[SIZEOF_STR] = ""; @@ -919,6 +1077,7 @@ 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))) +#define mkdate(time) string_date(time, opt_date) /* @@ -980,6 +1139,8 @@ LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \ LINE(STAT_STAGED, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \ LINE(STAT_UNSTAGED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \ LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \ +LINE(HELP_KEYMAP, "", COLOR_CYAN, COLOR_DEFAULT, 0), \ +LINE(HELP_GROUP, "", COLOR_BLUE, COLOR_DEFAULT, 0), \ LINE(BLAME_ID, "", COLOR_MAGENTA, COLOR_DEFAULT, 0) enum line_type { @@ -1034,8 +1195,7 @@ get_line_info(const char *name) enum line_type type; for (type = 0; type < ARRAY_SIZE(line_info); type++) - if (namelen == line_info[type].namelen && - !string_enum_compare(line_info[type].name, name, namelen)) + if (enum_equals(line_info[type], name, namelen)) return &line_info[type]; return NULL; @@ -1071,6 +1231,7 @@ struct line { unsigned int selected:1; unsigned int dirty:1; unsigned int cleareol:1; + unsigned int other:16; void *data; /* User data */ }; @@ -1139,11 +1300,14 @@ static const struct keybinding default_keybindings[] = { { 'z', REQ_STOP_LOADING }, { 'v', REQ_SHOW_VERSION }, { 'r', REQ_SCREEN_REDRAW }, + { 'o', REQ_OPTIONS }, { '.', REQ_TOGGLE_LINENO }, { 'D', REQ_TOGGLE_DATE }, { 'A', REQ_TOGGLE_AUTHOR }, { 'g', REQ_TOGGLE_REV_GRAPH }, { 'F', REQ_TOGGLE_REFS }, + { 'I', REQ_TOGGLE_SORT_ORDER }, + { 'i', REQ_TOGGLE_SORT_FIELD }, { ':', REQ_PROMPT }, { 'u', REQ_STATUS_UPDATE }, { '!', REQ_STATUS_REVERT }, @@ -1295,26 +1459,68 @@ get_key_name(int key_value) return seq ? seq : "(no key)"; } +static bool +append_key(char *buf, size_t *pos, const struct keybinding *keybinding) +{ + const char *sep = *pos > 0 ? ", " : ""; + const char *keyname = get_key_name(keybinding->alias); + + return string_nformat(buf, BUFSIZ, pos, "%s%s", sep, keyname); +} + +static bool +append_keymap_request_keys(char *buf, size_t *pos, enum request request, + enum keymap keymap, bool all) +{ + int i; + + for (i = 0; i < keybindings[keymap].size; i++) { + if (keybindings[keymap].data[i].request == request) { + if (!append_key(buf, pos, &keybindings[keymap].data[i])) + return FALSE; + if (!all) + break; + } + } + + return TRUE; +} + +#define get_key(keymap, request) get_keys(keymap, request, FALSE) + static const char * -get_key(enum request request) +get_keys(enum keymap keymap, enum request request, bool all) { static char buf[BUFSIZ]; size_t pos = 0; - char *sep = ""; int i; buf[pos] = 0; - for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) { - const struct keybinding *keybinding = &default_keybindings[i]; + if (!append_keymap_request_keys(buf, &pos, request, keymap, all)) + return "Too many keybindings!"; + if (pos > 0 && !all) + return buf; - if (keybinding->request != request) - continue; + if (keymap != KEYMAP_GENERIC) { + /* Only the generic keymap includes the default keybindings when + * listing all keys. */ + if (all) + return buf; - if (!string_format_from(buf, &pos, "%s%s", sep, - get_key_name(keybinding->alias))) + if (!append_keymap_request_keys(buf, &pos, request, KEYMAP_GENERIC, all)) return "Too many keybindings!"; - sep = ", "; + if (pos) + return buf; + } + + for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) { + if (default_keybindings[i].request == request) { + if (!append_key(buf, &pos, &default_keybindings[i])) + return "Too many keybindings!"; + if (!all) + return buf; + } } return buf; @@ -1365,6 +1571,7 @@ static void add_builtin_run_requests(void) { const char *cherry_pick[] = { "git", "cherry-pick", "%(commit)", NULL }; + const char *commit[] = { "git", "commit", NULL }; const char *gc[] = { "git", "gc", NULL }; struct { enum keymap keymap; @@ -1373,6 +1580,7 @@ add_builtin_run_requests(void) const char **argv; } reqs[] = { { KEYMAP_MAIN, 'C', ARRAY_SIZE(cherry_pick) - 1, cherry_pick }, + { KEYMAP_STATUS, 'C', ARRAY_SIZE(commit) - 1, commit }, { KEYMAP_GENERIC, 'G', ARRAY_SIZE(gc) - 1, gc }, }; int i; @@ -1471,7 +1679,7 @@ option_color_command(int argc, const char *argv[]) { struct line_info *info; - if (argc != 3 && argc != 4) { + if (argc < 3) { config_msg = "Wrong number of arguments given to color command"; return ERR; } @@ -1498,9 +1706,15 @@ option_color_command(int argc, const char *argv[]) return ERR; } - if (argc == 4 && !set_attribute(&info->attr, argv[3])) { - config_msg = "Unknown attribute"; - return ERR; + info->attr = 0; + while (argc-- > 3) { + int attr; + + if (!set_attribute(&attr, argv[argc])) { + config_msg = "Unknown attribute"; + return ERR; + } + info->attr |= attr; } return OK; @@ -1513,6 +1727,26 @@ static int parse_bool(bool *opt, const char *arg) return OK; } +static int parse_enum_do(unsigned int *opt, const char *arg, + const struct enum_map *map, size_t map_size) +{ + bool is_true; + + assert(map_size > 1); + + if (map_enum_do(map, map_size, (int *) opt, arg)) + return OK; + + if (parse_bool(&is_true, arg) != OK) + return ERR; + + *opt = is_true ? map[1].value : map[0].value; + return OK; +} + +#define parse_enum(opt, arg, map) \ + parse_enum_do(opt, arg, map, ARRAY_SIZE(map)) + static int parse_string(char *opt, const char *arg, size_t optsize) { @@ -1547,10 +1781,10 @@ option_set_command(int argc, const char *argv[]) } if (!strcmp(argv[0], "show-author")) - return parse_bool(&opt_author, argv[2]); + return parse_enum(&opt_author, argv[2], author_map); if (!strcmp(argv[0], "show-date")) - return parse_bool(&opt_date, argv[2]); + return parse_enum(&opt_date, argv[2], date_map); if (!strcmp(argv[0], "show-rev-graph")) return parse_bool(&opt_rev_graph, argv[2]); @@ -1573,6 +1807,9 @@ option_set_command(int argc, const char *argv[]) if (!strcmp(argv[0], "horizontal-scroll")) return parse_step(&opt_hscroll, argv[2]); + if (!strcmp(argv[0], "split-view-height")) + return parse_step(&opt_scale_split_view, argv[2]); + if (!strcmp(argv[0], "tab-size")) return parse_int(&opt_tab_size, argv[2], 1, 1024); @@ -1588,7 +1825,7 @@ static int option_bind_command(int argc, const char *argv[]) { enum request request; - int keymap; + int keymap = -1; int key; if (argc < 3) { @@ -1705,7 +1942,7 @@ load_option_file(const char *path) struct io io = {}; /* It's OK that the file doesn't exist. */ - if (!io_open(&io, path)) + if (!io_open(&io, "%s", path)) return; config_lineno = 0; @@ -1831,6 +2068,8 @@ struct view_ops { bool (*grep)(struct view *view, struct line *line); /* Select line */ void (*select)(struct view *view, struct line *line); + /* Prepare view for loading */ + bool (*prepare)(struct view *view); }; static struct view_ops blame_ops; @@ -1898,6 +2137,7 @@ static int draw_chars(struct view *view, enum line_type type, const char *string, int max_len, bool use_tilde) { + static char out_buffer[BUFSIZ * 2]; int len = 0; int col = 0; int trimmed = FALSE; @@ -1906,22 +2146,28 @@ draw_chars(struct view *view, enum line_type type, const char *string, if (max_len <= 0) return 0; - if (opt_utf8) { - len = utf8_length(&string, skip, &col, max_len, &trimmed, use_tilde); - } else { - col = len = strlen(string); - if (len > max_len) { - if (use_tilde) { - max_len -= 1; + len = utf8_length(&string, skip, &col, max_len, &trimmed, use_tilde); + + set_view_attr(view, type); + if (len > 0) { + if (opt_iconv_out != ICONV_NONE) { + ICONV_CONST char *inbuf = (ICONV_CONST char *) string; + size_t inlen = len + 1; + + char *outbuf = out_buffer; + size_t outlen = sizeof(out_buffer); + + size_t ret; + + ret = iconv(opt_iconv_out, &inbuf, &inlen, &outbuf, &outlen); + if (ret != (size_t) -1) { + string = out_buffer; + len = sizeof(out_buffer) - outlen; } - col = len = max_len; - trimmed = TRUE; } - } - set_view_attr(view, type); - if (len > 0) waddnstr(view->win, string, len); + } if (trimmed && use_tilde) { set_view_attr(view, LINE_DELIMITER); waddch(view->win, '~'); @@ -1997,35 +2243,22 @@ draw_field(struct view *view, enum line_type type, const char *text, int len, bo } static bool -draw_date(struct view *view, time_t *time) +draw_date(struct view *view, struct time *time) { - const char *date = mkdate(time); + const char *date = time && time->sec ? mkdate(time) : ""; + int cols = opt_date == DATE_SHORT ? DATE_SHORT_COLS : DATE_COLS; - return draw_field(view, LINE_DATE, date, DATE_COLS, FALSE); + return draw_field(view, LINE_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++; - } + bool trim = opt_author_cols == 0 || opt_author_cols > 5; + bool abbreviate = opt_author == AUTHOR_ABBREVIATED || !trim; - author = initials; - } + if (abbreviate && author) + author = get_author_initials(author); return draw_field(view, LINE_AUTHOR, author, opt_author_cols, trim); } @@ -2202,6 +2435,15 @@ update_view_title(struct view *view) wnoutrefresh(view->title); } +static int +apply_step(double step, int value) +{ + if (step >= 1) + return (int) step; + value *= step + 0.01; + return value ? value : 1; +} + static void resize_display(void) { @@ -2219,7 +2461,9 @@ resize_display(void) if (view != base) { /* Horizontal split. */ view->width = base->width; - view->height = SCALE_SPLIT_VIEW(base->height); + view->height = apply_step(opt_scale_split_view, base->height); + view->height = MAX(view->height, MIN_VIEW_HEIGHT); + view->height = MIN(view->height, base->height - MIN_VIEW_HEIGHT); base->height -= view->height; /* Make room for the title bar. */ @@ -2267,6 +2511,21 @@ redraw_display(bool clear) } } +static void +toggle_enum_option_do(unsigned int *opt, const char *help, + const struct enum_map *map, size_t size) +{ + *opt = (*opt + 1) % size; + redraw_display(FALSE); + report("Displaying %s %s", enum_name(map[*opt]), help); +} + +#define toggle_enum_option(opt, help, map) \ + toggle_enum_option_do(opt, help, map, ARRAY_SIZE(map)) + +#define toggle_date() toggle_enum_option(&opt_date, "dates", date_map) +#define toggle_author() toggle_enum_option(&opt_author, "author names", author_map) + static void toggle_view_option(bool *option, const char *help) { @@ -2275,6 +2534,29 @@ toggle_view_option(bool *option, const char *help) report("%sabling %s", *option ? "En" : "Dis", help); } +static void +open_option_menu(void) +{ + const struct menu_item menu[] = { + { '.', "line numbers", &opt_line_number }, + { 'D', "date display", &opt_date }, + { 'A', "author display", &opt_author }, + { 'g', "revision graph display", &opt_rev_graph }, + { 'F', "reference display", &opt_show_refs }, + { 0 } + }; + int selected = 0; + + if (prompt_menu("Toggle option", menu, &selected)) { + if (menu[selected].data == &opt_date) + toggle_date(); + else if (menu[selected].data == &opt_author) + toggle_author(); + else + toggle_view_option(menu[selected].data, menu[selected].text); + } +} + static void maximize_view(struct view *view) { @@ -2315,15 +2597,6 @@ goto_view_line(struct view *view, unsigned long offset, unsigned long lineno) return FALSE; } -static int -apply_step(double step, int value) -{ - if (step >= 1) - return (int) step; - value *= step + 0.01; - return value ? value : 1; -} - /* Scrolling backend */ static void do_scroll_view(struct view *view, int lines) @@ -2670,6 +2943,32 @@ free_argv(const char *argv[]) free((void *) argv[argc]); } +static const char * +format_arg(const char *name) +{ + static struct { + const char *name; + size_t namelen; + const char *value; + const char *value_if_empty; + } vars[] = { +#define FORMAT_VAR(name, value, value_if_empty) \ + { name, STRING_SIZE(name), value, value_if_empty } + FORMAT_VAR("%(directory)", opt_path, ""), + FORMAT_VAR("%(file)", opt_file, ""), + FORMAT_VAR("%(ref)", opt_ref, "HEAD"), + FORMAT_VAR("%(head)", ref_head, ""), + FORMAT_VAR("%(commit)", ref_commit, ""), + FORMAT_VAR("%(blob)", ref_blob, ""), + }; + int i; + + for (i = 0; i < ARRAY_SIZE(vars); i++) + if (!strncmp(name, vars[i].name, vars[i].namelen)) + return *vars[i].value ? vars[i].value : vars[i].value_if_empty; + + return NULL; +} static bool format_argv(const char *dst_argv[], const char *src_argv[], enum format_flags flags) { @@ -2694,27 +2993,13 @@ format_argv(const char *dst_argv[], const char *src_argv[], enum format_flags fl len = strlen(arg); value = ""; - } else if (!prefixcmp(next, "%(directory)")) { - value = opt_path; - - } else if (!prefixcmp(next, "%(file)")) { - value = opt_file; - - } else if (!prefixcmp(next, "%(ref)")) { - value = *opt_ref ? opt_ref : "HEAD"; - - } else if (!prefixcmp(next, "%(head)")) { - value = ref_head; - - } else if (!prefixcmp(next, "%(commit)")) { - value = ref_commit; - - } else if (!prefixcmp(next, "%(blob)")) { - value = ref_blob; - } else { - report("Unknown replacement: `%s`", next); - return FALSE; + value = format_arg(next); + + if (!value) { + report("Unknown replacement: `%s`", next); + return FALSE; + } } if (!string_format_from(buf, &bufpos, "%.*s%s", len, arg, value)) @@ -2795,7 +3080,7 @@ prepare_update_file(struct view *view, const char *name) { if (view->pipe) end_update(view, TRUE); - return io_open(&view->io, name); + return io_open(&view->io, "%s/%s", opt_cdup[0] ? opt_cdup : ".", name); } static bool @@ -2804,16 +3089,13 @@ begin_update(struct view *view, bool refresh) if (view->pipe) end_update(view, TRUE); - if (refresh) { - if (!start_io(&view->io)) - return FALSE; - - } else { - if (view == VIEW(REQ_VIEW_TREE) && strcmp(view->vid, view->id)) - opt_path[0] = 0; - - if (!run_io_rd(&view->io, view->ops->argv, FORMAT_ALL)) + if (!refresh) { + if (view->ops->prepare) { + if (!view->ops->prepare(view)) + return FALSE; + } else if (!init_io_rd(&view->io, view->ops->argv, NULL, FORMAT_ALL)) { return FALSE; + } /* Put the current ref_* value to the view title ref * member. This is needed by the blob view. Most other @@ -2822,6 +3104,9 @@ begin_update(struct view *view, bool refresh) string_copy_rev(view->ref, view->id); } + if (!start_io(&view->io)) + return FALSE; + setup_update(view, view->id); return TRUE; @@ -2855,7 +3140,7 @@ update_view(struct view *view) } for (; (line = io_get(view->pipe, '\n', can_read)); can_read = FALSE) { - if (opt_iconv != ICONV_NONE) { + if (opt_iconv_in != ICONV_NONE) { ICONV_CONST char *inbuf = line; size_t inlen = strlen(line) + 1; @@ -2864,7 +3149,7 @@ update_view(struct view *view) size_t ret; - ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen); + ret = iconv(opt_iconv_in, &inbuf, &inlen, &outbuf, &outlen); if (ret != (size_t) -1) line = out_buffer; } @@ -3000,6 +3285,11 @@ open_view(struct view *prev, enum request request, enum open_flags flags) display[current_view] = view; } + /* No parent signals that this is the first loaded view. */ + if (prev && view != prev) { + view->parent = prev; + } + /* Resize the view when switching between split- and full-screen, * or when switching between two different full-screen views. */ if (nviews != displayed_views() || @@ -3030,13 +3320,9 @@ open_view(struct view *prev, enum request request, enum open_flags flags) do_scroll_view(prev, lines); } - if (prev && view != prev) { - if (split) { - /* "Blur" the previous view. */ - update_view_title(prev); - } - - view->parent = prev; + if (prev && view != prev && split && view_is_displayed(prev)) { + /* "Blur" the previous view. */ + update_view_title(prev); } if (view->pipe && view->lines == 0) { @@ -3072,7 +3358,7 @@ open_mergetool(const char *file) } static void -open_editor(bool from_root, const char *file) +open_editor(const char *file) { const char *editor_argv[] = { "vi", file, NULL }; const char *editor; @@ -3088,7 +3374,7 @@ open_editor(bool from_root, const char *file) editor = "vi"; editor_argv[0] = editor; - open_external_viewer(editor_argv, from_root ? opt_cdup : NULL); + open_external_viewer(editor_argv, opt_cdup); } static void @@ -3160,7 +3446,7 @@ view_driver(struct view *view, enum request request) case REQ_VIEW_BLAME: if (!opt_file[0]) { report("No file chosen, press %s to open tree view", - get_key(REQ_VIEW_TREE)); + get_key(view->keymap, REQ_VIEW_TREE)); break; } open_view(view, request, OPEN_DEFAULT); @@ -3169,7 +3455,7 @@ view_driver(struct view *view, enum request request) case REQ_VIEW_BLOB: if (!ref_blob[0]) { report("No file chosen, press %s to open tree view", - get_key(REQ_VIEW_TREE)); + get_key(view->keymap, REQ_VIEW_TREE)); break; } open_view(view, request, OPEN_DEFAULT); @@ -3178,7 +3464,7 @@ view_driver(struct view *view, enum request request) case REQ_VIEW_PAGER: if (!VIEW(REQ_VIEW_PAGER)->pipe && !VIEW(REQ_VIEW_PAGER)->lines) { report("No pager content, press %s to run command from prompt", - get_key(REQ_PROMPT)); + get_key(view->keymap, REQ_PROMPT)); break; } open_view(view, request, OPEN_DEFAULT); @@ -3187,7 +3473,7 @@ view_driver(struct view *view, enum request request) case REQ_VIEW_STAGE: if (!VIEW(REQ_VIEW_STAGE)->lines) { report("No stage content, press %s to open the status view and choose file", - get_key(REQ_VIEW_STATUS)); + get_key(view->keymap, REQ_VIEW_STATUS)); break; } open_view(view, request, OPEN_DEFAULT); @@ -3221,7 +3507,9 @@ view_driver(struct view *view, enum request request) (view == VIEW(REQ_VIEW_STAGE) && view->parent == VIEW(REQ_VIEW_STATUS)) || (view == VIEW(REQ_VIEW_BLOB) && - view->parent == VIEW(REQ_VIEW_TREE))) { + view->parent == VIEW(REQ_VIEW_TREE)) || + (view == VIEW(REQ_VIEW_MAIN) && + view->parent == VIEW(REQ_VIEW_BRANCH))) { int line; view = view->parent; @@ -3263,16 +3551,20 @@ view_driver(struct view *view, enum request request) maximize_view(view); break; + case REQ_OPTIONS: + open_option_menu(); + break; + case REQ_TOGGLE_LINENO: toggle_view_option(&opt_line_number, "line numbers"); break; case REQ_TOGGLE_DATE: - toggle_view_option(&opt_date, "date display"); + toggle_date(); break; case REQ_TOGGLE_AUTHOR: - toggle_view_option(&opt_author, "author display"); + toggle_author(); break; case REQ_TOGGLE_REV_GRAPH: @@ -3283,6 +3575,11 @@ view_driver(struct view *view, enum request request) toggle_view_option(&opt_show_refs, "reference display"); break; + case REQ_TOGGLE_SORT_FIELD: + case REQ_TOGGLE_SORT_ORDER: + report("Sorting is not yet supported for the %s view", view->name); + break; + case REQ_SEARCH: case REQ_SEARCH_BACK: search_view(view, request); @@ -3333,7 +3630,8 @@ view_driver(struct view *view, enum request request) return FALSE; default: - report("Unknown key, press 'h' for help"); + report("Unknown key, press %s for help", + get_key(view->keymap, REQ_VIEW_HELP)); return TRUE; } @@ -3345,6 +3643,42 @@ view_driver(struct view *view, enum request request) * View backend utilities */ +enum sort_field { + ORDERBY_NAME, + ORDERBY_DATE, + ORDERBY_AUTHOR, +}; + +struct sort_state { + const enum sort_field *fields; + size_t size, current; + bool reverse; +}; + +#define SORT_STATE(fields) { fields, ARRAY_SIZE(fields), 0 } +#define get_sort_field(state) ((state).fields[(state).current]) +#define sort_order(state, result) ((state).reverse ? -(result) : (result)) + +static void +sort_view(struct view *view, enum request request, struct sort_state *state, + int (*compare)(const void *, const void *)) +{ + switch (request) { + case REQ_TOGGLE_SORT_FIELD: + state->current = (state->current + 1) % state->size; + break; + + case REQ_TOGGLE_SORT_ORDER: + state->reverse = !state->reverse; + break; + default: + die("Not a sort request"); + } + + qsort(view->line, view->lines, sizeof(*view->line), compare); + redraw_view(view); +} + DEFINE_ALLOCATOR(realloc_authors, const char *, 256) /* Small author cache to reduce memory consumption. It uses binary @@ -3384,7 +3718,13 @@ get_author(const char *name) } static void -parse_timezone(time_t *time, const char *zone) +parse_timesec(struct time *time, const char *sec) +{ + time->sec = (time_t) atol(sec); +} + +static void +parse_timezone(struct time *time, const char *zone) { long tz; @@ -3396,14 +3736,15 @@ parse_timezone(time_t *time, const char *zone) if (zone[0] == '-') tz = -tz; - *time -= tz; + time->tz = tz; + time->sec -= tz; } /* Parse author lines where the name may be empty: * author 1138474660 +0100 */ static void -parse_author_line(char *ident, const char **author, time_t *time) +parse_author_line(char *ident, const char **author, struct time *time) { char *nameend = strchr(ident, '<'); char *emailend = strchr(ident, '>'); @@ -3425,29 +3766,46 @@ parse_author_line(char *ident, const char **author, time_t *time) char *secs = emailend + 2; char *zone = strchr(secs, ' '); - *time = (time_t) atol(secs); + parse_timesec(time, secs); if (zone && strlen(zone) == STRING_SIZE(" +0700")) parse_timezone(time, zone + 1); } } -static enum input_status -select_commit_parent_handler(void *data, char *buf, int c) +static bool +open_commit_parent_menu(char buf[SIZEOF_STR], int *parents) { - size_t parents = *(size_t *) data; - int parent = 0; + char rev[SIZEOF_REV]; + const char *revlist_argv[] = { + "git", "log", "--no-color", "-1", "--pretty=format:%s", rev, NULL + }; + struct menu_item *items; + char text[SIZEOF_STR]; + bool ok = TRUE; + int i; - if (!isdigit(c)) - return INPUT_SKIP; + items = calloc(*parents + 1, sizeof(*items)); + if (!items) + return FALSE; - if (*buf) - parent = atoi(buf) * 10; - parent += c - '0'; + for (i = 0; i < *parents; i++) { + string_copy_rev(rev, &buf[SIZEOF_REV * i]); + if (!run_io_buf(revlist_argv, text, sizeof(text)) || + !(items[i].text = strdup(text))) { + ok = FALSE; + break; + } + } - if (parent > parents) - return INPUT_SKIP; - return INPUT_OK; + if (ok) { + *parents = 0; + ok = prompt_menu("Select parent", items, parents); + } + for (i = 0; items[i].text; i++) + free((char *) items[i].text); + free(items); + return ok; } static bool @@ -3455,12 +3813,13 @@ select_commit_parent(const char *id, char rev[SIZEOF_REV], const char *path) { char buf[SIZEOF_STR * 4]; const char *revlist_argv[] = { - "git", "rev-list", "-1", "--parents", id, "--", path, NULL + "git", "log", "--no-color", "-1", + "--pretty=format:%P", id, "--", path, NULL }; int parents; if (!run_io_buf(revlist_argv, buf, sizeof(buf)) || - (parents = (strlen(buf) / 40) - 1) < 0) { + (parents = strlen(buf) / 40) < 0) { report("Failed to get parent information"); return FALSE; @@ -3472,17 +3831,8 @@ select_commit_parent(const char *id, char rev[SIZEOF_REV], const char *path) 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); - } + if (parents > 1 && !open_commit_parent_menu(buf, &parents)) + return FALSE; string_copy_rev(rev, &buf[41 * parents]); return TRUE; @@ -3526,22 +3876,22 @@ add_pager_refs(struct view *view, struct line *line) { char buf[SIZEOF_STR]; char *commit_id = (char *)line->data + STRING_SIZE("commit "); - struct ref **refs; - size_t bufpos = 0, refpos = 0; + struct ref_list *list; + size_t bufpos = 0, i; const char *sep = "Refs: "; bool is_tag = FALSE; assert(line->type == LINE_COMMIT); - refs = get_refs(commit_id); - if (!refs) { + list = get_ref_list(commit_id); + if (!list) { if (view == VIEW(REQ_VIEW_DIFF)) goto try_add_describe_ref; return; } - do { - struct ref *ref = refs[refpos]; + for (i = 0; i < list->size; i++) { + struct ref *ref = list->refs[i]; const char *fmt = ref->tag ? "%s[%s]" : ref->remote ? "%s<%s>" : "%s%s"; @@ -3550,7 +3900,7 @@ add_pager_refs(struct view *view, struct line *line) sep = ", "; if (ref->tag) is_tag = TRUE; - } while (refs[refpos++]->next); + } if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) { try_add_describe_ref: @@ -3693,80 +4043,129 @@ static struct view_ops diff_ops = { * Help backend */ +static bool help_keymap_hidden[ARRAY_SIZE(keymap_table)]; + static bool -help_open(struct view *view) +help_open_keymap_title(struct view *view, enum keymap keymap) { + struct line *line; + + line = add_line_format(view, LINE_HELP_KEYMAP, "[%c] %s bindings", + help_keymap_hidden[keymap] ? '+' : '-', + enum_name(keymap_table[keymap])); + if (line) + line->other = keymap; + + return help_keymap_hidden[keymap]; +} + +static void +help_open_keymap(struct view *view, enum keymap keymap) +{ + const char *group = NULL; char buf[SIZEOF_STR]; size_t bufpos; + bool add_title = TRUE; int i; - if (view->lines > 0) - return TRUE; - - add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT); - for (i = 0; i < ARRAY_SIZE(req_info); i++) { - const char *key; + const char *key = NULL; if (req_info[i].request == REQ_NONE) continue; if (!req_info[i].request) { - add_line_text(view, "", LINE_DEFAULT); - add_line_text(view, req_info[i].help, LINE_DEFAULT); + group = req_info[i].help; continue; } - key = get_key(req_info[i].request); - if (!*key) - key = "(no key defined)"; + key = get_keys(keymap, req_info[i].request, TRUE); + if (!key || !*key) + continue; - for (bufpos = 0; bufpos <= req_info[i].namelen; bufpos++) { - buf[bufpos] = tolower(req_info[i].name[bufpos]); - if (buf[bufpos] == '_') - buf[bufpos] = '-'; + if (add_title && help_open_keymap_title(view, keymap)) + return; + add_title = FALSE; + + if (group) { + add_line_text(view, group, LINE_HELP_GROUP); + group = NULL; } - add_line_format(view, LINE_DEFAULT, " %-25s %-20s %s", - key, buf, req_info[i].help); + add_line_format(view, LINE_DEFAULT, " %-25s %-20s %s", key, + enum_name(req_info[i]), req_info[i].help); } - if (run_requests) { - add_line_text(view, "", LINE_DEFAULT); - add_line_text(view, "External commands:", LINE_DEFAULT); - } + group = "External commands:"; for (i = 0; i < run_requests; i++) { struct run_request *req = get_run_request(REQ_NONE + i + 1); const char *key; int argc; - if (!req) + if (!req || req->keymap != keymap) continue; key = get_key_name(req->key); if (!*key) key = "(no key defined)"; + if (add_title && help_open_keymap_title(view, keymap)) + return; + if (group) { + add_line_text(view, group, LINE_HELP_GROUP); + group = NULL; + } + for (bufpos = 0, argc = 0; req->argv[argc]; argc++) if (!string_format_from(buf, &bufpos, "%s%s", argc ? " " : "", req->argv[argc])) - return REQ_NONE; + return; - add_line_format(view, LINE_DEFAULT, " %-10s %-14s `%s`", - keymap_table[req->keymap].name, key, buf); + add_line_format(view, LINE_DEFAULT, " %-25s `%s`", key, buf); } +} + +static bool +help_open(struct view *view) +{ + enum keymap keymap; + + reset_view(view); + add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT); + add_line_text(view, "", LINE_DEFAULT); + + for (keymap = 0; keymap < ARRAY_SIZE(keymap_table); keymap++) + help_open_keymap(view, keymap); return TRUE; } +static enum request +help_request(struct view *view, enum request request, struct line *line) +{ + switch (request) { + case REQ_ENTER: + if (line->type == LINE_HELP_KEYMAP) { + help_keymap_hidden[line->other] = + !help_keymap_hidden[line->other]; + view->p_restore = TRUE; + open_view(view, REQ_VIEW_HELP, OPEN_REFRESH); + } + + return REQ_NONE; + default: + return pager_request(view, request, line); + } +} + static struct view_ops help_ops = { "line", NULL, help_open, NULL, pager_draw, - pager_request, + help_request, pager_grep, pager_select, }; @@ -3837,26 +4236,57 @@ push_tree_stack_entry(const char *name, unsigned long lineno) struct tree_entry { char id[SIZEOF_REV]; mode_t mode; - time_t time; /* Date from the author ident. */ + struct time time; /* Date from the author ident. */ const char *author; /* Author of the commit. */ char name[1]; }; static const char * -tree_path(struct line *line) +tree_path(const struct line *line) { return ((struct tree_entry *) line->data)->name; } - static int -tree_compare_entry(struct line *line1, struct line *line2) +tree_compare_entry(const struct line *line1, const struct line *line2) { if (line1->type != line2->type) return line1->type == LINE_TREE_DIR ? -1 : 1; return strcmp(tree_path(line1), tree_path(line2)); } +static const enum sort_field tree_sort_fields[] = { + ORDERBY_NAME, ORDERBY_DATE, ORDERBY_AUTHOR +}; +static struct sort_state tree_sort_state = SORT_STATE(tree_sort_fields); + +static int +tree_compare(const void *l1, const void *l2) +{ + const struct line *line1 = (const struct line *) l1; + const struct line *line2 = (const struct line *) l2; + const struct tree_entry *entry1 = ((const struct line *) l1)->data; + const struct tree_entry *entry2 = ((const struct line *) l2)->data; + + if (line1->type == LINE_TREE_HEAD) + return -1; + if (line2->type == LINE_TREE_HEAD) + return 1; + + switch (get_sort_field(tree_sort_state)) { + case ORDERBY_DATE: + return sort_order(tree_sort_state, timecmp(&entry1->time, &entry2->time)); + + case ORDERBY_AUTHOR: + return sort_order(tree_sort_state, strcmp(entry1->author, entry2->author)); + + case ORDERBY_NAME: + default: + return sort_order(tree_sort_state, tree_compare_entry(line1, line2)); + } +} + + static struct line * tree_entry(struct view *view, enum line_type type, const char *path, const char *mode, const char *id) @@ -3882,7 +4312,7 @@ static bool tree_read_date(struct view *view, char *text, bool *read_date) { static const char *author_name; - static time_t author_time; + static struct time author_time; if (!text && *read_date) { *read_date = FALSE; @@ -3903,7 +4333,7 @@ tree_read_date(struct view *view, char *text, bool *read_date) return TRUE; } - if (!run_io_rd(&io, log_file, FORMAT_NONE)) { + if (!run_io_rd(&io, log_file, opt_cdup, FORMAT_NONE)) { report("Failed to load tree data"); return TRUE; } @@ -3926,8 +4356,6 @@ tree_read_date(struct view *view, char *text, bool *read_date) 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, '/'); @@ -4031,7 +4459,7 @@ tree_draw(struct view *view, struct line *line, unsigned int lineno) if (opt_author && draw_author(view, entry->author)) return TRUE; - if (opt_date && draw_date(view, entry->author ? &entry->time : NULL)) + if (opt_date && draw_date(view, &entry->time)) return TRUE; } if (draw_text(view, line->type, entry->name, TRUE)) @@ -4050,7 +4478,7 @@ open_blob_editor() else if (!run_io_append(blob_ops.argv, FORMAT_ALL, fd)) report("Failed to save blob data to file"); else - open_editor(FALSE, file); + open_editor(file); if (fd != -1) unlink(file); } @@ -4076,10 +4504,15 @@ tree_request(struct view *view, enum request request, struct line *line) } else if (!is_head_commit(view->vid)) { open_blob_editor(); } else { - open_editor(TRUE, opt_file); + open_editor(opt_file); } return REQ_NONE; + case REQ_TOGGLE_SORT_FIELD: + case REQ_TOGGLE_SORT_ORDER: + sort_view(view, request, &tree_sort_state, tree_compare); + return REQ_NONE; + case REQ_PARENT: if (!*opt_path) { /* quit view if at top of tree */ @@ -4165,6 +4598,32 @@ tree_select(struct view *view, struct line *line) string_copy_rev(view->ref, entry->id); } +static bool +tree_prepare(struct view *view) +{ + if (view->lines == 0 && opt_prefix[0]) { + char *pos = opt_prefix; + + while (pos && *pos) { + char *end = strchr(pos, '/'); + + if (end) + *end = 0; + push_tree_stack_entry(pos, 0); + pos = end; + if (end) { + *end = '/'; + pos++; + } + } + + } else if (strcmp(view->vid, view->id)) { + opt_path[0] = 0; + } + + return init_io_rd(&view->io, view->ops->argv, opt_cdup, FORMAT_ALL); +} + static const char *tree_argv[SIZEOF_ARG] = { "git", "ls-tree", "%(commit)", "%(directory)", NULL }; @@ -4178,6 +4637,7 @@ static struct view_ops tree_ops = { tree_request, tree_grep, tree_select, + tree_prepare, }; static bool @@ -4242,7 +4702,7 @@ struct blame_commit { char id[SIZEOF_REV]; /* SHA1 ID. */ char title[128]; /* First line of the commit message. */ const char *author; /* Author of the commit. */ - time_t time; /* Date from the author ident. */ + struct time time; /* Date from the author ident. */ char filename[128]; /* Name of file. */ bool has_previous; /* Was a "previous" line detected. */ }; @@ -4256,8 +4716,16 @@ struct blame { static bool blame_open(struct view *view) { - if (*opt_ref || !io_open(&view->io, opt_file)) { - if (!run_io_rd(&view->io, blame_cat_file_argv, FORMAT_ALL)) + char path[SIZEOF_STR]; + + if (!view->parent && *opt_prefix) { + string_copy(path, opt_file); + if (!string_format(opt_file, "%s%s", opt_prefix, path)) + return FALSE; + } + + if (*opt_ref || !io_open(&view->io, "%s%s", opt_cdup, opt_file)) { + if (!run_io_rd(&view->io, blame_cat_file_argv, opt_cdup, FORMAT_ALL)) return FALSE; } @@ -4353,7 +4821,7 @@ blame_read_file(struct view *view, const char *line, bool *read_file) if (view->lines == 0 && !view->parent) die("No blame exist for %s", view->vid); - if (view->lines == 0 || !run_io_rd(&io, argv, FORMAT_ALL)) { + if (view->lines == 0 || !run_io_rd(&io, argv, opt_cdup, FORMAT_ALL)) { report("Failed to load blame data"); return TRUE; } @@ -4421,7 +4889,7 @@ blame_read(struct view *view, char *line) commit->author = get_author(line); } else if (match_blame_header("author-time ", &line)) { - commit->time = (time_t) atol(line); + parse_timesec(&commit->time, line); } else if (match_blame_header("author-tz ", &line)) { parse_timezone(&commit->time, line); @@ -4444,7 +4912,7 @@ static bool blame_draw(struct view *view, struct line *line, unsigned int lineno) { struct blame *blame = line->data; - time_t *time = NULL; + struct time *time = NULL; const char *id = NULL, *author = NULL; char text[SIZEOF_STR]; @@ -4636,10 +5104,36 @@ static struct view_ops blame_ops = { struct branch { const char *author; /* Author of the last commit. */ - time_t time; /* Date of the last activity. */ - struct ref *ref; /* Name and commit ID information. */ + struct time time; /* Date of the last activity. */ + const struct ref *ref; /* Name and commit ID information. */ }; +static const struct ref branch_all; + +static const enum sort_field branch_sort_fields[] = { + ORDERBY_NAME, ORDERBY_DATE, ORDERBY_AUTHOR +}; +static struct sort_state branch_sort_state = SORT_STATE(branch_sort_fields); + +static int +branch_compare(const void *l1, const void *l2) +{ + const struct branch *branch1 = ((const struct line *) l1)->data; + const struct branch *branch2 = ((const struct line *) l2)->data; + + switch (get_sort_field(branch_sort_state)) { + case ORDERBY_DATE: + return sort_order(branch_sort_state, timecmp(&branch1->time, &branch2->time)); + + case ORDERBY_AUTHOR: + return sort_order(branch_sort_state, strcmp(branch1->author, branch2->author)); + + case ORDERBY_NAME: + default: + return sort_order(branch_sort_state, strcmp(branch1->ref->name, branch2->ref->name)); + } +} + static bool branch_draw(struct view *view, struct line *line, unsigned int lineno) { @@ -4652,21 +5146,42 @@ branch_draw(struct view *view, struct line *line, unsigned int lineno) if (opt_author && draw_author(view, branch->author)) return TRUE; - draw_text(view, type, branch->ref->name, TRUE); + draw_text(view, type, branch->ref == &branch_all ? "All branches" : branch->ref->name, TRUE); return TRUE; } static enum request branch_request(struct view *view, enum request request, struct line *line) { + struct branch *branch = line->data; + switch (request) { case REQ_REFRESH: load_refs(); open_view(view, REQ_VIEW_BRANCH, OPEN_REFRESH); return REQ_NONE; + case REQ_TOGGLE_SORT_FIELD: + case REQ_TOGGLE_SORT_ORDER: + sort_view(view, request, &branch_sort_state, branch_compare); + return REQ_NONE; + case REQ_ENTER: - open_view(view, REQ_VIEW_MAIN, OPEN_SPLIT); + if (branch->ref == &branch_all) { + const char *all_branches_argv[] = { + "git", "log", "--no-color", "--pretty=raw", "--parents", + "--topo-order", "--all", NULL + }; + struct view *main_view = VIEW(REQ_VIEW_MAIN); + + if (!prepare_update(main_view, all_branches_argv, NULL, FORMAT_NONE)) { + report("Failed to load view of all branches"); + return REQ_NONE; + } + open_view(view, REQ_VIEW_MAIN, OPEN_PREPARED | OPEN_SPLIT); + } else { + open_view(view, REQ_VIEW_MAIN, OPEN_SPLIT); + } return REQ_NONE; default: @@ -4678,6 +5193,7 @@ static bool branch_read(struct view *view, char *line) { static char id[SIZEOF_REV]; + struct branch *reference; size_t i; if (!line) @@ -4689,15 +5205,22 @@ branch_read(struct view *view, char *line) return TRUE; case LINE_AUTHOR: - for (i = 0; i < view->lines; i++) { + for (i = 0, reference = NULL; i < view->lines; i++) { struct branch *branch = view->line[i].data; if (strcmp(branch->ref->id, id)) continue; + view->line[i].dirty = TRUE; + if (reference) { + branch->author = reference->author; + branch->time = reference->time; + continue; + } + parse_author_line(line + STRING_SIZE("author "), &branch->author, &branch->time); - view->line[i].dirty = TRUE; + reference = branch; } return TRUE; @@ -4708,7 +5231,7 @@ branch_read(struct view *view, char *line) } static bool -branch_open_visitor(void *data, struct ref *ref) +branch_open_visitor(void *data, const struct ref *ref) { struct view *view = data; struct branch *branch; @@ -4732,13 +5255,15 @@ branch_open(struct view *view) "--simplify-by-decoration", "--all", NULL }; - if (!run_io_rd(&view->io, branch_log, FORMAT_NONE)) { + if (!run_io_rd(&view->io, branch_log, NULL, FORMAT_NONE)) { report("Failed to load branch data"); return TRUE; } setup_update(view, view->id); + branch_open_visitor(view, &branch_all); foreach_ref(branch_open_visitor, view); + view->p_restore = TRUE; return TRUE; } @@ -4850,7 +5375,7 @@ status_run(struct view *view, const char *argv[], char status, enum line_type ty char *buf; struct io io = {}; - if (!run_io(&io, argv, NULL, IO_RD)) + if (!run_io(&io, argv, opt_cdup, IO_RD)) return FALSE; add_line_data(view, NULL, type); @@ -4931,7 +5456,7 @@ static const char *status_diff_files_argv[] = { }; static const char *status_list_other_argv[] = { - "git", "ls-files", "-z", "--others", "--exclude-standard", NULL + "git", "ls-files", "-z", "--others", "--exclude-standard", opt_prefix, NULL }; static const char *status_list_no_head_argv[] = { @@ -5000,8 +5525,7 @@ status_update_onbranch(void) if (!*opt_head) { struct io io = {}; - if (string_format(buf, "%s/rebase-merge/head-name", opt_git_dir) && - io_open(&io, buf) && + if (io_open(&io, "%s/rebase-merge/head-name", opt_git_dir) && io_read_buf(&io, buf, sizeof(buf))) { head = buf; if (!prefixcmp(head, "refs/heads/")) @@ -5249,10 +5773,8 @@ status_update_prepare(struct io *io, enum line_type type) return run_io(io, staged_argv, opt_cdup, IO_WR); case LINE_STAT_UNSTAGED: - return run_io(io, others_argv, opt_cdup, IO_WR); - case LINE_STAT_UNTRACKED: - return run_io(io, others_argv, NULL, IO_WR); + return run_io(io, others_argv, opt_cdup, IO_WR); default: die("line type %d not handled in switch", type); @@ -5310,7 +5832,7 @@ status_update_files(struct view *view, struct line *line) struct line *pos = view->line + view->lines; int files = 0; int file, done; - int cursor_y, cursor_x; + int cursor_y = -1, cursor_x = -1; if (!status_update_prepare(&io, line->type)) return FALSE; @@ -5378,9 +5900,8 @@ status_revert(struct status *status, enum line_type type, bool has_none) } else { report("Cannot revert changes to multiple files"); } - return FALSE; - } else { + } else if (prompt_yesno("Are you sure you want to revert changes?")) { char mode[10] = "100644"; const char *reset_argv[] = { "git", "update-index", "--cacheinfo", mode, @@ -5390,12 +5911,25 @@ status_revert(struct status *status, enum line_type type, bool has_none) "git", "checkout", "--", status->old.name, NULL }; - if (!prompt_yesno("Are you sure you want to overwrite any changes?")) - return FALSE; - 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); + if (status->status == 'U') { + string_format(mode, "%5o", status->old.mode); + + if (status->old.mode == 0 && status->new.mode == 0) { + reset_argv[2] = "--force-remove"; + reset_argv[3] = status->old.name; + reset_argv[4] = NULL; + } + + if (!run_io_fg(reset_argv, opt_cdup)) + return FALSE; + if (status->old.mode == 0 && status->new.mode == 0) + return TRUE; + } + + return run_io_fg(checkout_argv, opt_cdup); } + + return FALSE; } static enum request @@ -5430,14 +5964,12 @@ status_request(struct view *view, enum request request, struct line *line) return REQ_NONE; } - open_editor(status->status != '?', status->new.name); + open_editor(status->new.name); break; case REQ_VIEW_BLAME: - if (status) { - string_copy(opt_file, status->new.name); + if (status) opt_ref[0] = 0; - } return request; case REQ_ENTER: @@ -5497,13 +6029,15 @@ status_select(struct view *view, struct line *line) if (status && status->status == 'U') { text = "Press %s to resolve conflict in %s"; - key = get_key(REQ_STATUS_MERGE); + key = get_key(KEYMAP_STATUS, REQ_STATUS_MERGE); } else { - key = get_key(REQ_STATUS_UPDATE); + key = get_key(KEYMAP_STATUS, REQ_STATUS_UPDATE); } string_format(view->ref, text, key, file); + if (status) + string_copy(opt_file, status->new.name); } static bool @@ -5698,7 +6232,7 @@ stage_request(struct view *view, enum request request, struct line *line) case REQ_STAGE_NEXT: if (stage_line_type == LINE_STAT_UNTRACKED) { report("File is untracked; press %s to add", - get_key(REQ_STATUS_UPDATE)); + get_key(KEYMAP_STAGE, REQ_STATUS_UPDATE)); return REQ_NONE; } stage_next(view, line); @@ -5712,7 +6246,7 @@ stage_request(struct view *view, enum request request, struct line *line) return REQ_NONE; } - open_editor(stage_status.status != '?', stage_status.new.name); + open_editor(stage_status.new.name); break; case REQ_REFRESH: @@ -5779,8 +6313,8 @@ struct commit { char id[SIZEOF_REV]; /* SHA1 ID. */ char title[128]; /* First line of the commit message. */ const char *author; /* Author of the commit. */ - time_t time; /* Date from the author ident. */ - struct ref **refs; /* Repository references. */ + struct time time; /* Date from the author ident. */ + struct ref_list *refs; /* Repository references. */ chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */ size_t graph_size; /* The width of the graph array. */ bool has_parents; /* Rewritten --parents seen. */ @@ -6019,32 +6553,33 @@ main_draw(struct view *view, struct line *line, unsigned int lineno) return TRUE; if (opt_show_refs && commit->refs) { - size_t i = 0; + size_t i; - do { + for (i = 0; i < commit->refs->size; i++) { + struct ref *ref = commit->refs->refs[i]; enum line_type type; - if (commit->refs[i]->head) + if (ref->head) type = LINE_MAIN_HEAD; - else if (commit->refs[i]->ltag) + else if (ref->ltag) type = LINE_MAIN_LOCAL_TAG; - else if (commit->refs[i]->tag) + else if (ref->tag) type = LINE_MAIN_TAG; - else if (commit->refs[i]->tracked) + else if (ref->tracked) type = LINE_MAIN_TRACKED; - else if (commit->refs[i]->remote) + else if (ref->remote) type = LINE_MAIN_REMOTE; else type = LINE_MAIN_REF; if (draw_text(view, type, "[", TRUE) || - draw_text(view, type, commit->refs[i]->name, TRUE) || + draw_text(view, type, ref->name, TRUE) || draw_text(view, type, "]", TRUE)) return TRUE; if (draw_text(view, LINE_DEFAULT, " ", TRUE)) return TRUE; - } while (commit->refs[i++]->next); + } } draw_text(view, LINE_DEFAULT, commit->title, TRUE); @@ -6093,7 +6628,7 @@ main_read(struct view *view, char *line) } string_copy_rev(commit->id, line); - commit->refs = get_refs(commit->id); + commit->refs = get_ref_list(commit->id); graph->commit = commit; add_line_data(view, commit, LINE_MAIN_COMMIT); @@ -6170,17 +6705,18 @@ main_request(struct view *view, enum request request, struct line *line) } static bool -grep_refs(struct ref **refs, regex_t *regex) +grep_refs(struct ref_list *list, regex_t *regex) { regmatch_t pmatch; - size_t i = 0; + size_t i; - if (!opt_show_refs || !refs) + if (!opt_show_refs || !list) return FALSE; - do { - if (regexec(regex, refs[i]->name, 1, &pmatch, 0) != REG_NOMATCH) + + for (i = 0; i < list->size; i++) { + if (regexec(regex, list->refs[i]->name, 1, &pmatch, 0) != REG_NOMATCH) return TRUE; - } while (refs[i++]->next); + } return FALSE; } @@ -6265,6 +6801,14 @@ 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, }; +static inline unsigned char +utf8_char_length(const char *string, const char *end) +{ + int c = *(unsigned char *) string; + + return utf8_bytes[c]; +} + /* Decode UTF-8 multi-byte representation into a Unicode character. */ static inline unsigned long utf8_to_unicode(const char *string, size_t length) @@ -6332,8 +6876,7 @@ utf8_length(const char **start, size_t skip, int *width, size_t max_width, int * *trimmed = 0; while (string < end) { - int c = *(unsigned char *) string; - unsigned char bytes = utf8_bytes[c]; + unsigned char bytes = utf8_char_length(string, end); size_t ucwidth; unsigned long unicode; @@ -6662,20 +7205,80 @@ read_prompt(const char *prompt) return prompt_input(prompt, read_prompt_handler, NULL); } +static bool prompt_menu(const char *prompt, const struct menu_item *items, int *selected) +{ + enum input_status status = INPUT_OK; + int size = 0; + + while (items[size].text) + size++; + + while (status == INPUT_OK) { + const struct menu_item *item = &items[*selected]; + int key; + int i; + + mvwprintw(status_win, 0, 0, "%s (%d of %d) ", + prompt, *selected + 1, size); + if (item->hotkey) + wprintw(status_win, "[%c] ", (char) item->hotkey); + wprintw(status_win, "%s", item->text); + wclrtoeol(status_win); + + key = get_input(COLS - 1); + switch (key) { + case KEY_RETURN: + case KEY_ENTER: + case '\n': + status = INPUT_STOP; + break; + + case KEY_LEFT: + case KEY_UP: + *selected = *selected - 1; + if (*selected < 0) + *selected = size - 1; + break; + + case KEY_RIGHT: + case KEY_DOWN: + *selected = (*selected + 1) % size; + break; + + case KEY_ESC: + status = INPUT_CANCEL; + break; + + default: + for (i = 0; items[i].text; i++) + if (items[i].hotkey == key) { + *selected = i; + status = INPUT_STOP; + break; + } + } + } + + /* Clear the status window */ + status_empty = FALSE; + report(""); + + return status != INPUT_CANCEL; +} + /* * Repository properties */ -static struct ref *refs = NULL; +static struct ref **refs = NULL; static size_t refs_size = 0; -/* Id <-> ref store */ -static struct ref ***id_refs = NULL; -static size_t id_refs_size = 0; +static struct ref_list **ref_lists = NULL; +static size_t ref_lists_size = 0; -DEFINE_ALLOCATOR(realloc_refs, struct ref, 256) +DEFINE_ALLOCATOR(realloc_refs, struct ref *, 256) DEFINE_ALLOCATOR(realloc_refs_list, struct ref *, 8) -DEFINE_ALLOCATOR(realloc_refs_lists, struct ref **, 8) +DEFINE_ALLOCATOR(realloc_ref_lists, struct ref_list *, 8) static int compare_refs(const void *ref1_, const void *ref2_) @@ -6697,70 +7300,62 @@ compare_refs(const void *ref1_, const void *ref2_) } static void -foreach_ref(bool (*visitor)(void *data, struct ref *ref), void *data) +foreach_ref(bool (*visitor)(void *data, const struct ref *ref), void *data) { size_t i; for (i = 0; i < refs_size; i++) - if (!visitor(data, &refs[i])) + if (!visitor(data, refs[i])) break; } -static struct ref ** -get_refs(const char *id) +static struct ref_list * +get_ref_list(const char *id) { - struct ref **ref_list = NULL; - size_t ref_list_size = 0; + struct ref_list *list; size_t i; - for (i = 0; i < id_refs_size; i++) - if (!strcmp(id, id_refs[i][0]->id)) - return id_refs[i]; + for (i = 0; i < ref_lists_size; i++) + if (!strcmp(id, ref_lists[i]->id)) + return ref_lists[i]; - if (!realloc_refs_lists(&id_refs, id_refs_size, 1)) + if (!realloc_ref_lists(&ref_lists, ref_lists_size, 1)) + return NULL; + list = calloc(1, sizeof(*list)); + if (!list) return NULL; for (i = 0; i < refs_size; i++) { - if (strcmp(id, refs[i].id)) - continue; - - if (!realloc_refs_list(&ref_list, ref_list_size, 1)) - return ref_list; - - ref_list[ref_list_size] = &refs[i]; - /* XXX: The properties of the commit chains ensures that we can - * safely modify the shared ref. The repo references will - * always be similar for the same id. */ - ref_list[ref_list_size]->next = 1; - ref_list_size++; + if (!strcmp(id, refs[i]->id) && + realloc_refs_list(&list->refs, list->size, 1)) + list->refs[list->size++] = refs[i]; } - if (ref_list) { - qsort(ref_list, ref_list_size, sizeof(*ref_list), compare_refs); - ref_list[ref_list_size - 1]->next = 0; - id_refs[id_refs_size++] = ref_list; + if (!list->refs) { + free(list); + return NULL; } - return ref_list; + qsort(list->refs, list->size, sizeof(*list->refs), compare_refs); + ref_lists[ref_lists_size++] = list; + return list; } static int read_ref(char *id, size_t idlen, char *name, size_t namelen) { - struct ref *ref; + struct ref *ref = NULL; bool tag = FALSE; bool ltag = FALSE; bool remote = FALSE; bool tracked = FALSE; - bool check_replace = FALSE; bool head = FALSE; + int from = 0, to = refs_size - 1; if (!prefixcmp(name, "refs/tags/")) { if (!suffixcmp(name, namelen, "^{}")) { namelen -= 3; name[namelen] = 0; - if (refs_size > 0 && refs[refs_size - 1].ltag == TRUE) - check_replace = TRUE; } else { ltag = TRUE; } @@ -6785,27 +7380,38 @@ read_ref(char *id, size_t idlen, char *name, size_t namelen) return OK; } - if (check_replace && !strcmp(name, refs[refs_size - 1].name)) { - /* it's an annotated tag, replace the previous SHA1 with the - * resolved commit id; relies on the fact git-ls-remote lists - * the commit id of an annotated tag right before the commit id - * it points to. */ - refs[refs_size - 1].ltag = ltag; - string_copy_rev(refs[refs_size - 1].id, id); + /* If we are reloading or 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. */ + while (from <= to) { + size_t pos = (to + from) / 2; + int cmp = strcmp(name, refs[pos]->name); - return OK; - } + if (!cmp) { + ref = refs[pos]; + break; + } - if (!realloc_refs(&refs, refs_size, 1)) - return ERR; + if (cmp < 0) + to = pos - 1; + else + from = pos + 1; + } - ref = &refs[refs_size++]; - ref->name = malloc(namelen + 1); - if (!ref->name) - return ERR; + if (!ref) { + if (!realloc_refs(&refs, refs_size, 1)) + return ERR; + ref = calloc(1, sizeof(*ref) + namelen); + if (!ref) + return ERR; + memmove(refs + from + 1, refs + from, + (refs_size - from) * sizeof(*refs)); + refs[from] = ref; + strncpy(ref->name, name, namelen); + refs_size++; + } - strncpy(ref->name, name, namelen); - ref->name[namelen] = 0; ref->head = head; ref->tag = tag; ref->ltag = ltag; @@ -6826,6 +7432,7 @@ load_refs(void) "git", "ls-remote", opt_git_dir, NULL }; static bool init = FALSE; + size_t i; if (!init) { argv_from_env(ls_remote_argv, "TIG_LS_REMOTE"); @@ -6842,12 +7449,24 @@ load_refs(void) memmove(opt_head, offset, strlen(offset) + 1); } - while (refs_size > 0) - free(refs[--refs_size].name); - while (id_refs_size > 0) - free(id_refs[--id_refs_size]); + for (i = 0; i < refs_size; i++) + refs[i]->id[0] = 0; + + if (run_io_load(ls_remote_argv, "\t", read_ref) == ERR) + return ERR; - return run_io_load(ls_remote_argv, "\t", read_ref); + /* Update the ref lists to reflect changes. */ + for (i = 0; i < ref_lists_size; i++) { + struct ref_list *list = ref_lists[i]; + size_t old, new; + + for (old = new = 0; old < list->size; old++) + if (!strcmp(list->id, list->refs[old]->id)) + list->refs[new++] = list->refs[old]; + list->size = new; + } + + return OK; } static void @@ -6952,7 +7571,7 @@ read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen static int load_git_config(void) { - const char *config_list_argv[] = { "git", GIT_CONFIG, "--list", NULL }; + const char *config_list_argv[] = { "git", "config", "--list", NULL }; return run_io_load(config_list_argv, "=", read_repo_config_option); } @@ -7120,8 +7739,8 @@ parse_options(int argc, const char *argv[]) die("command too long"); } - if (!prepare_update(VIEW(request), custom_argv, NULL, FORMAT_NONE)) - die("Failed to format arguments"); + if (!prepare_update(VIEW(request), custom_argv, NULL, FORMAT_NONE)) + die("Failed to format arguments"); return request; } @@ -7155,12 +7774,15 @@ main(int argc, const char *argv[]) if (!opt_git_dir[0] && request != REQ_VIEW_PAGER) die("Not a git repository"); - if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8")) - opt_utf8 = FALSE; + if (*opt_encoding && strcmp(opt_codeset, "UTF-8")) { + opt_iconv_in = iconv_open("UTF-8", opt_encoding); + if (opt_iconv_in == ICONV_NONE) + die("Failed to initialize character set conversion"); + } - if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) { - opt_iconv = iconv_open(opt_codeset, opt_encoding); - if (opt_iconv == ICONV_NONE) + if (*opt_codeset && strcmp(opt_codeset, "UTF-8")) { + opt_iconv_out = iconv_open(opt_codeset, "UTF-8"); + if (opt_iconv_out == ICONV_NONE) die("Failed to initialize character set conversion"); }