X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=tig.c;h=55e022468ee15d7add89caa417d6b5b811a66caa;hb=f2d95af83227cea265bcce5e8604eccc6d3fd849;hp=ffeaa857dc39d70797018027d17ebd4a2c02c048;hpb=c147a4d920aa68dd292b7d7d78a4b341910851e2;p=tig.git diff --git a/tig.c b/tig.c index ffeaa85..55e0224 100644 --- a/tig.c +++ b/tig.c @@ -68,11 +68,11 @@ 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 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)) +#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,41 +103,45 @@ static int load_refs(void); /* 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 -/* 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. */ +/* Some ASCII-shorthands fitted into the ncurses namespace. */ #define KEY_TAB '\t' #define KEY_RETURN '\r' #define KEY_ESC 27 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. */ }; -static struct ref **get_refs(const char *id); +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_list *get_ref_list(const char *id); +static void foreach_ref(bool (*visitor)(void *data, struct ref *ref), void *data); +static int load_refs(void); enum format_flags { FORMAT_ALL, /* Perform replacement in all arguments. */ @@ -147,29 +151,6 @@ 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; -}; - -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; -} - enum input_status { INPUT_OK, INPUT_SKIP, @@ -182,6 +163,35 @@ 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. + */ + +#define DEFINE_ALLOCATOR(name, type, chunk_size) \ +static type * \ +name(type **mem, size_t size, size_t increase) \ +{ \ + size_t num_chunks = (size + chunk_size - 1) / chunk_size; \ + size_t num_chunks_new = (size + increase + chunk_size - 1) / chunk_size;\ + type *tmp = *mem; \ + \ + if (mem == NULL || num_chunks != num_chunks_new) { \ + tmp = realloc(tmp, num_chunks_new * chunk_size * sizeof(type)); \ + if (tmp) \ + *mem = tmp; \ + } \ + \ + return tmp; \ +} + /* * String helpers */ @@ -210,20 +220,6 @@ 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 size_t -string_expand_length(const char *line, int tabsize) -{ - size_t size, pos; - - for (pos = 0; line[pos]; pos++) { - if (line[pos] == '\t' && tabsize > 0) - size += tabsize - (size % tabsize); - else - size++; - } - return size; -} - static void string_expand(char *dst, size_t dstlen, const char *src, int tabsize) { @@ -304,6 +300,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(const 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)) @@ -317,6 +340,17 @@ suffixcmp(const char *str, int slen, const char *suffix) } +static const char * +mkdate(const time_t *time) +{ + static char buf[DATE_COLS + 1]; + struct tm tm; + + gmtime_r(time, &tm); + return strftime(buf, sizeof(buf), DATE_FORMAT, &tm) ? buf : NULL; +} + + static bool argv_from_string(const char *argv[SIZEOF_ARG], int *argc, char *cmd) { @@ -407,6 +441,8 @@ io_open(struct io *io, const char *name) { init_io(io, NULL, IO_FD); io->pipe = *name ? open(name, O_RDONLY) : STDIN_FILENO; + if (io->pipe == -1) + io->error = errno; return io->pipe != -1; } @@ -566,7 +602,7 @@ io_error(struct io *io) return io->error; } -static bool +static char * io_strerror(struct io *io) { return strerror(io->error); @@ -600,20 +636,14 @@ io_read(struct io *io, void *buf, size_t bufsize) } while (1); } +DEFINE_ALLOCATOR(realloc_io_buf, char, BUFSIZ) + static char * io_get(struct io *io, int c, bool can_read) { char *eol; ssize_t readsize; - if (!io->buf) { - io->buf = io->bufpos = malloc(BUFSIZ); - if (!io->buf) - return NULL; - io->bufalloc = BUFSIZ; - io->bufsize = 0; - } - while (TRUE) { if (io->bufsize > 0) { eol = memchr(io->bufpos, c, io->bufsize); @@ -642,6 +672,12 @@ io_get(struct io *io, int c, bool can_read) if (io->bufsize > 0 && io->bufpos > io->buf) memmove(io->buf, io->bufpos, io->bufsize); + if (io->bufalloc == io->bufsize) { + if (!realloc_io_buf(&io->buf, io->bufalloc, BUFSIZ)) + return NULL; + io->bufalloc += BUFSIZ; + } + io->bufpos = io->buf; readsize = io_read(io, io->buf + io->bufsize, io->bufalloc - io->bufsize); if (io_error(io)) @@ -671,20 +707,24 @@ io_write(struct io *io, const void *buf, size_t bufsize) } static bool -run_io_buf(const char **argv, char buf[], size_t bufsize) +io_read_buf(struct io *io, char buf[], size_t bufsize) { - struct io io = {}; - bool error; + char *result = io_get(io, '\n', TRUE); - if (!run_io_rd(&io, argv, FORMAT_NONE)) - return FALSE; + if (result) { + result = chomp_string(result); + string_ncopy_do(buf, bufsize, result, strlen(result)); + } + + return done_io(io) && result; +} - io.buf = io.bufpos = buf; - io.bufalloc = bufsize; - error = !io_get(&io, '\n', TRUE) && io_error(&io); - io.buf = NULL; +static bool +run_io_buf(const char **argv, char buf[], size_t bufsize) +{ + struct io io = {}; - return done_io(&io) || error; + return run_io_rd(&io, argv, FORMAT_NONE) && io_read_buf(&io, buf, bufsize); } static int @@ -749,6 +789,7 @@ run_io_load(const char **argv, const char *separators, REQ_(VIEW_TREE, "Show tree view"), \ REQ_(VIEW_BLOB, "Show blob view"), \ REQ_(VIEW_BLAME, "Show blame view"), \ + REQ_(VIEW_BRANCH, "Show branch view"), \ REQ_(VIEW_HELP, "Show help page"), \ REQ_(VIEW_PAGER, "Show pager view"), \ REQ_(VIEW_STATUS, "Show status view"), \ @@ -780,6 +821,8 @@ run_io_load(const char **argv, const char *separators, 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"), \ @@ -792,11 +835,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"), \ @@ -827,7 +874,7 @@ struct request_info { const char *help; }; -static struct request_info req_info[] = { +static const struct request_info req_info[] = { #define REQ_GROUP(help) { 0, NULL, 0, (help) }, #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) } REQ_INFO @@ -854,29 +901,19 @@ 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_date_short = FALSE; static bool opt_author = TRUE; 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_tab_size = TAB_SIZE; -static int opt_author_cols = AUTHOR_COLS-1; +static int opt_num_interval = 5; +static double opt_hscroll = 0.50; +static double opt_scale_split_view = 2.0 / 3.0; +static int opt_tab_size = 8; +static int opt_author_cols = 19; static char opt_path[SIZEOF_STR] = ""; static char opt_file[SIZEOF_STR] = ""; static char opt_ref[SIZEOF_REF] = ""; @@ -928,7 +965,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), \ @@ -937,10 +974,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), \ @@ -949,8 +986,7 @@ 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_PARENT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_BOLD), \ -LINE(TREE_MODE, "", COLOR_CYAN, COLOR_DEFAULT, 0), \ +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), \ @@ -1064,7 +1100,7 @@ struct keybinding { enum request request; }; -static struct keybinding default_keybindings[] = { +static const struct keybinding default_keybindings[] = { /* View switching */ { 'm', REQ_VIEW_MAIN }, { 'd', REQ_VIEW_DIFF }, @@ -1072,6 +1108,7 @@ static struct keybinding default_keybindings[] = { { 't', REQ_VIEW_TREE }, { 'f', REQ_VIEW_BLOB }, { 'B', REQ_VIEW_BLAME }, + { 'H', REQ_VIEW_BRANCH }, { 'p', REQ_VIEW_PAGER }, { 'h', REQ_VIEW_HELP }, { 'S', REQ_VIEW_STATUS }, @@ -1099,6 +1136,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 }, @@ -1115,11 +1154,15 @@ static 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 }, + { 'T', REQ_TOGGLE_DATE_SHORT }, { '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 }, @@ -1137,6 +1180,7 @@ static struct keybinding default_keybindings[] = { KEYMAP_(TREE), \ KEYMAP_(BLOB), \ KEYMAP_(BLAME), \ + KEYMAP_(BRANCH), \ KEYMAP_(PAGER), \ KEYMAP_(HELP), \ KEYMAP_(STATUS), \ @@ -1148,14 +1192,13 @@ enum keymap { #undef KEYMAP_ }; -static struct int_map keymap_table[] = { -#define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name } +static const 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; @@ -1204,7 +1247,7 @@ struct key { int value; }; -static struct key key_table[] = { +static const struct key key_table[] = { { "Enter", KEY_RETURN }, { "Space", ' ' }, { "Backspace", KEY_BACKSPACE }, @@ -1282,7 +1325,7 @@ get_key(enum request request) buf[pos] = 0; for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) { - struct keybinding *keybinding = &default_keybindings[i]; + const struct keybinding *keybinding = &default_keybindings[i]; if (keybinding->request != request) continue; @@ -1305,6 +1348,8 @@ struct run_request { static struct run_request *run_request; static size_t run_requests; +DEFINE_ALLOCATOR(realloc_run_requests, struct run_request, 8) + static enum request add_run_request(enum keymap keymap, int key, int argc, const char **argv) { @@ -1313,11 +1358,9 @@ add_run_request(enum keymap keymap, int key, int argc, const char **argv) if (argc >= ARRAY_SIZE(req->argv) - 1) return REQ_NONE; - req = realloc(run_request, (run_requests + 1) * sizeof(*run_request)); - if (!req) + if (!realloc_run_requests(&run_request, run_requests, 1)) return REQ_NONE; - run_request = req; req = &run_request[run_requests]; req->keymap = keymap; req->key = key; @@ -1341,6 +1384,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; @@ -1349,6 +1393,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; @@ -1366,8 +1411,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 const struct enum_map color_map[] = { +#define COLOR_MAP(name) ENUM_MAP(#name, COLOR_##name) COLOR_MAP(DEFAULT), COLOR_MAP(BLACK), COLOR_MAP(BLUE), @@ -1379,11 +1428,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 const struct enum_map attr_map[] = { +#define ATTR_MAP(name) ENUM_MAP(#name, A_##name) ATTR_MAP(NORMAL), ATTR_MAP(BLINK), ATTR_MAP(BOLD), @@ -1393,64 +1439,124 @@ 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_step(double *opt, const char *arg) +{ + *opt = atoi(arg); + if (!strchr(arg, '%')) + return OK; + + /* "Shift down" so 100% and 1 does not conflict. */ + *opt = (*opt - 1) / 100; + if (*opt >= 1.0) { + *opt = 0.99; + config_msg = "Step value larger than 100%"; + return ERR; + } + if (*opt < 0.0) { + *opt = 1; + config_msg = "Invalid step value"; + return ERR; + } + return OK; +} + +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[]) { struct line_info *info; - if (argc != 3 && argc != 4) { + if (argc < 3) { config_msg = "Wrong number of arguments given to color command"; return ERR; } 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 const 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) { - 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; } -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, arglen); + return OK; + } } /* Wants: name = value */ @@ -1467,68 +1573,44 @@ 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-date")) + return parse_bool(&opt_date, argv[2]); - if (!strcmp(argv[0], "show-rev-graph")) { - opt_rev_graph = parse_bool(argv[2]); - return OK; - } + if (!strcmp(argv[0], "date-short")) + return parse_bool(&opt_date_short, 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], "horizontal-scroll")) + return parse_step(&opt_hscroll, argv[2]); - 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], "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); + + if (!strcmp(argv[0], "commit-encoding")) + return parse_string(opt_encoding, argv[2], sizeof(opt_encoding)); config_msg = "Unknown variable name"; return ERR; @@ -1539,7 +1621,7 @@ static int option_bind_command(int argc, const char *argv[]) { enum request request; - int keymap; + int keymap = -1; int key; if (argc < 3) { @@ -1560,23 +1642,16 @@ option_bind_command(int argc, const char *argv[]) request = get_request(argv[2]); if (request == REQ_NONE) { - struct { - const char *name; - enum request request; - } obsolete[] = { - { "cherry-pick", REQ_NONE }, - { "screen-resize", REQ_NONE }, - { "tree-parent", REQ_PARENT }, + static const struct enum_map obsolete[] = { + ENUM_MAP("cherry-pick", REQ_NONE), + ENUM_MAP("screen-resize", REQ_NONE), + ENUM_MAP("tree-parent", REQ_PARENT), }; - size_t namelen = strlen(argv[2]); - int i; + int alias; - for (i = 0; i < ARRAY_SIZE(obsolete); i++) { - if (namelen != strlen(obsolete[i].name) || - string_enum_compare(obsolete[i].name, argv[2], namelen)) - continue; - if (obsolete[i].request != REQ_NONE) - add_keybinding(keymap, obsolete[i].request, key); + if (map_enum(&alias, obsolete, argv[2])) { + if (alias != REQ_NONE) + add_keybinding(keymap, alias, key); config_msg = "Obsolete request name"; return ERR; } @@ -1662,7 +1737,7 @@ 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; @@ -1684,11 +1759,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) { @@ -1742,14 +1814,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. */ @@ -1758,7 +1832,6 @@ struct view { /* Buffering */ size_t lines; /* Total number of lines */ struct line *line; /* Line index */ - size_t line_alloc; /* Total number of allocated lines */ unsigned int digits; /* Number of digits in the lines member. */ /* Drawing */ @@ -1787,7 +1860,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); @@ -1803,6 +1876,7 @@ static struct view_ops pager_ops; static struct view_ops stage_ops; static struct view_ops status_ops; static struct view_ops tree_ops; +static struct view_ops branch_ops; #define VIEW_STR(name, env, ref, ops, map, git) \ { name, #env, ref, ops, map, git } @@ -1818,6 +1892,7 @@ static struct view views[] = { VIEW_(TREE, "tree", &tree_ops, TRUE, ref_commit), VIEW_(BLOB, "blob", &blob_ops, TRUE, ref_blob), VIEW_(BLAME, "blame", &blame_ops, TRUE, ref_commit), + VIEW_(BRANCH, "branch", &branch_ops, TRUE, ref_head), VIEW_(HELP, "help", &help_ops, FALSE, ""), VIEW_(PAGER, "pager", &pager_ops, FALSE, "stdin"), VIEW_(STATUS, "status", &status_ops, TRUE, ""), @@ -1838,7 +1913,7 @@ enum line_graphic { LINE_GRAPHIC_VLINE }; -static int line_graphics[] = { +static chtype line_graphics[] = { /* LINE_GRAPHIC_VLINE: */ '|' }; @@ -1859,12 +1934,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) { @@ -1877,7 +1953,8 @@ 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, '~'); @@ -1898,63 +1975,25 @@ draw_space(struct view *view, enum line_type type, int max, int spaces) while (spaces > 0) { int len = MIN(spaces, sizeof(space) - 1); - col += draw_chars(view, type, space, spaces, FALSE); + col += draw_chars(view, type, space, len, FALSE); spaces -= len; } return col; } -static bool -draw_lineno(struct view *view, unsigned int lineno) -{ - char number[10]; - int digits3 = view->digits < 3 ? 3 : view->digits; - int max_number = MIN(digits3, STRING_SIZE(number)); - int max = view->width - view->col; - int col; - - if (max < max_number) - max_number = max; - - lineno += view->offset + 1; - if (lineno == 1 || (lineno % opt_num_interval) == 0) { - static char fmt[] = "%1ld"; - - if (view->digits <= 9) - fmt[1] = '0' + digits3; - - if (!string_format(number, fmt, lineno)) - number[0] = 0; - col = draw_chars(view, LINE_LINE_NUMBER, number, max_number, TRUE); - } else { - col = draw_space(view, LINE_LINE_NUMBER, max_number, max_number); - } - - if (col < max) { - set_view_attr(view, LINE_DEFAULT); - waddch(view->win, line_graphics[LINE_GRAPHIC_VLINE]); - col++; - } - - if (col < max) - col += draw_space(view, LINE_DEFAULT, max - col, 1); - view->col += col; - - return view->width - view->col <= 0; -} - 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); - return view->width - view->col <= 0; + view->col += draw_chars(view, type, string, view->width + view->yoffset - view->col, trim); + return view->width + view->yoffset <= view->col; } static bool draw_graphic(struct view *view, enum line_type type, chtype graphic[], size_t size) { - int max = view->width - view->col; + size_t skip = view->yoffset > view->col ? view->yoffset - view->col : 0; + int max = view->width + view->yoffset - view->col; int i; if (max < size) @@ -1963,22 +2002,21 @@ 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; + return view->width + view->yoffset <= view->col; } static bool draw_field(struct view *view, enum line_type type, const char *text, int len, bool trim) { - int max = MIN(view->width - view->col, len); + int max = MIN(view->width + view->yoffset - view->col, len); int col; if (text) @@ -1986,22 +2024,18 @@ 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 -draw_date(struct view *view, struct tm *time) +draw_date(struct view *view, time_t *time) { - char buf[DATE_COLS]; - char *date; - int timelen = 0; + const char *date = mkdate(time); + int cols = opt_date_short ? DATE_SHORT_COLS : DATE_COLS; - if (time) - timelen = strftime(buf, sizeof(buf), DATE_FORMAT, time); - date = timelen ? buf : NULL; - - return draw_field(view, LINE_DATE, date, DATE_COLS, FALSE); + return draw_field(view, LINE_DATE, date, cols, FALSE); } static bool @@ -2027,7 +2061,51 @@ draw_author(struct view *view, const char *author) author = initials; } - return draw_field(view, LINE_MAIN_AUTHOR, author, opt_author_cols, trim); + return draw_field(view, LINE_AUTHOR, author, opt_author_cols, trim); +} + +static bool +draw_mode(struct view *view, mode_t mode) +{ + const char *str; + + if (S_ISDIR(mode)) + str = "drwxr-xr-x"; + else if (S_ISLNK(mode)) + str = "lrwxrwxrwx"; + else if (S_ISGITLINK(mode)) + str = "m---------"; + else if (S_ISREG(mode) && mode & S_IXUSR) + str = "-rwxr-xr-x"; + else if (S_ISREG(mode)) + str = "-rw-r--r--"; + else + str = "----------"; + + return draw_field(view, LINE_MODE, str, STRING_SIZE("-rw-r--r-- "), FALSE); +} + +static bool +draw_lineno(struct view *view, unsigned int lineno) +{ + char number[10]; + int digits3 = view->digits < 3 ? 3 : view->digits; + int max = MIN(view->width + view->yoffset - view->col, digits3); + char *text = NULL; + + lineno += view->offset + 1; + if (lineno == 1 || (lineno % opt_num_interval) == 0) { + static char fmt[] = "%1ld"; + + fmt[1] = '0' + (view->digits <= 9 ? digits3 : 1); + if (string_format(number, fmt, lineno)) + text = number; + } + if (text) + view->col += draw_chars(view, LINE_LINE_NUMBER, text, max, TRUE); + else + view->col += draw_space(view, LINE_LINE_NUMBER, max, digits3); + return draw_graphic(view, LINE_DEFAULT, &line_graphics[LINE_GRAPHIC_VLINE], 1); } static bool @@ -2158,6 +2236,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) { @@ -2175,7 +2262,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. */ @@ -2231,10 +2320,63 @@ 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)) + toggle_view_option(menu[selected].data, menu[selected].text); +} + +static void +maximize_view(struct view *view) +{ + memset(display, 0, sizeof(display)); + current_view = 0; + display[current_view] = view; + resize_display(); + redraw_display(FALSE); + report(""); +} + + /* * 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) @@ -2291,9 +2433,26 @@ scroll_view(struct view *view, enum request request) assert(view_is_displayed(view)); switch (request) { - case REQ_SCROLL_PAGE_DOWN: - lines = view->height; - case REQ_SCROLL_LINE_DOWN: + case REQ_SCROLL_LEFT: + if (view->yoffset == 0) { + report("Cannot scroll beyond the first column"); + return; + } + if (view->yoffset <= apply_step(opt_hscroll, view->width)) + view->yoffset = 0; + else + view->yoffset -= apply_step(opt_hscroll, view->width); + redraw_view_from(view, 0); + report(""); + return; + case REQ_SCROLL_RIGHT: + view->yoffset += apply_step(opt_hscroll, view->width); + redraw_view(view); + report(""); + return; + case REQ_SCROLL_PAGE_DOWN: + lines = view->height; + case REQ_SCROLL_LINE_DOWN: if (view->offset + lines > view->lines) lines = view->lines - view->offset; @@ -2422,23 +2581,34 @@ move_view(struct view *view, enum request request) static void search_view(struct view *view, enum request request); +static bool +grep_text(struct view *view, const char *text[]) +{ + regmatch_t pmatch; + size_t i; + + for (i = 0; text[i]; i++) + if (*text[i] && + regexec(view->regex, text[i], 1, &pmatch, 0) != REG_NOMATCH) + return TRUE; + return FALSE; +} + 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); - wnoutrefresh(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]); } @@ -2532,13 +2702,14 @@ 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; view->vid[0] = 0; view->update_secs = 0; } @@ -2628,25 +2799,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; @@ -2723,36 +2880,6 @@ begin_update(struct view *view, bool refresh) return TRUE; } -#define ITEM_CHUNK_SIZE 256 -static void * -realloc_items(void *mem, size_t *size, size_t new_size, size_t item_size) -{ - size_t num_chunks = *size / ITEM_CHUNK_SIZE; - size_t num_chunks_new = (new_size + ITEM_CHUNK_SIZE - 1) / ITEM_CHUNK_SIZE; - - if (mem == NULL || num_chunks != num_chunks_new) { - *size = num_chunks_new * ITEM_CHUNK_SIZE; - mem = realloc(mem, *size * item_size); - } - - return mem; -} - -static struct line * -realloc_lines(struct view *view, size_t line_size) -{ - size_t alloc = view->line_alloc; - struct line *tmp = realloc_items(view->line, &alloc, line_size, - sizeof(*view->line)); - - if (!tmp) - return NULL; - - view->line = tmp; - view->line_alloc = alloc; - return view->line; -} - static bool update_view(struct view *view) { @@ -2767,7 +2894,7 @@ update_view(struct view *view) return TRUE; if (!io_can_read(view->pipe)) { - if (view->lines == 0) { + if (view->lines == 0 && view_is_displayed(view)) { time_t secs = time(NULL) - view->start_time; if (secs > 1 && secs > view->update_secs) { @@ -2843,12 +2970,14 @@ update_view(struct view *view) return TRUE; } +DEFINE_ALLOCATOR(realloc_lines, struct line, 256) + static struct line * add_line_data(struct view *view, void *data, enum line_type type) { struct line *line; - if (!realloc_lines(view, view->lines + 1)) + if (!realloc_lines(&view->line, view->lines, 1)) return NULL; line = &view->line[view->lines++]; @@ -2889,9 +3018,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. */ }; @@ -2899,10 +3026,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]; @@ -2919,8 +3045,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)); @@ -2959,7 +3084,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); } @@ -2977,11 +3102,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 @@ -3049,10 +3169,8 @@ view_driver(struct view *view, enum request request) { int i; - if (request == REQ_NONE) { - doupdate(); + if (request == REQ_NONE) return TRUE; - } if (request > REQ_NONE) { open_run_request(request); @@ -3060,6 +3178,7 @@ view_driver(struct view *view, enum request request) if (view == VIEW(REQ_VIEW_STATUS) || view == VIEW(REQ_VIEW_MAIN) || view == VIEW(REQ_VIEW_LOG) || + view == VIEW(REQ_VIEW_BRANCH) || view == VIEW(REQ_VIEW_STAGE)) request = REQ_REFRESH; else @@ -3082,6 +3201,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: @@ -3138,6 +3259,7 @@ view_driver(struct view *view, enum request request) case REQ_VIEW_LOG: case REQ_VIEW_TREE: case REQ_VIEW_HELP: + case REQ_VIEW_BRANCH: open_view(view, request, OPEN_DEFAULT); break; @@ -3152,7 +3274,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; @@ -3191,7 +3315,11 @@ view_driver(struct view *view, enum request request) case REQ_MAXIMIZE: if (displayed_views() == 2) - open_view(view, VIEW_REQ(view), OPEN_DEFAULT); + maximize_view(view); + break; + + case REQ_OPTIONS: + open_option_menu(); break; case REQ_TOGGLE_LINENO: @@ -3202,6 +3330,10 @@ view_driver(struct view *view, enum request request) toggle_view_option(&opt_date, "date display"); break; + case REQ_TOGGLE_DATE_SHORT: + toggle_view_option(&opt_date_short, "date shortening"); + break; + case REQ_TOGGLE_AUTHOR: toggle_view_option(&opt_author, "author display"); break; @@ -3214,6 +3346,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); @@ -3255,13 +3392,8 @@ view_driver(struct view *view, enum request request) * followed. */ if (view->parent && view->parent->parent != view->parent) { - memset(display, 0, sizeof(display)); - current_view = 0; - display[current_view] = view->parent; + maximize_view(view->parent); view->parent = view; - resize_display(); - redraw_display(FALSE); - report(""); break; } /* Fall-through */ @@ -3281,11 +3413,101 @@ 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 + * search to lookup or find place to position new entries. No entries + * are ever freed. */ +static const char * +get_author(const char *name) +{ + static const char **authors; + static size_t authors_size; + int from = 0, to = authors_size - 1; + + while (from <= to) { + size_t pos = (to + from) / 2; + int cmp = strcmp(name, authors[pos]); + + if (!cmp) + return authors[pos]; + + if (cmp < 0) + to = pos - 1; + else + from = pos + 1; + } + + if (!realloc_authors(&authors, authors_size, 1)) + return NULL; + name = strdup(name); + if (!name) + return NULL; + + memmove(authors + from + 1, authors + from, (authors_size - from) * sizeof(*authors)); + authors[from] = name; + authors_size++; + + return name; +} + +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) +parse_author_line(char *ident, const char **author, time_t *time) { char *nameend = strchr(ident, '<'); char *emailend = strchr(ident, '>'); @@ -3300,82 +3522,80 @@ parse_author_line(char *ident, char *author, size_t authorsize, struct tm *tm) ident = "Unknown"; } - string_ncopy_do(author, authorsize, ident, strlen(ident)); + *author = get_author(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")) { - 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; + *time = (time_t) atol(secs); - if (zone[0] == '-') - tz = -tz; - - time -= tz; - } - - gmtime_r(&time, tm); + 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 -select_commit_parent(const char *id, char rev[SIZEOF_REV]) +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, NULL + "git", "log", "--no-color", "-1", + "--pretty=format:%P", id, "--", path, NULL }; int parents; if (!run_io_buf(revlist_argv, buf, sizeof(buf)) || - !*chomp_string(buf) || - (parents = (strlen(buf) / 40) - 1) < 0) { + (parents = strlen(buf) / 40) < 0) { report("Failed to get parent information"); return FALSE; } else if (parents == 0) { - report("The selected commit has no parents"); + if (path) + report("Path '%s' does not exist in the parent", path); + else + 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); - } + if (parents > 1 && !open_commit_parent_menu(buf, &parents)) + return FALSE; string_copy_rev(rev, &buf[41 * parents]); return TRUE; @@ -3402,13 +3622,9 @@ static bool add_describe_ref(char *buf, size_t *bufpos, const char *commit_id, const char *sep) { const char *describe_argv[] = { "git", "describe", commit_id, NULL }; - char refbuf[SIZEOF_STR]; - char *ref = NULL; - - if (run_io_buf(describe_argv, refbuf, sizeof(refbuf))) - ref = chomp_string(refbuf); + char ref[SIZEOF_STR]; - if (!ref || !*ref) + if (!run_io_buf(describe_argv, ref, sizeof(ref)) || !*ref) return TRUE; /* This is the only fatal call, since it can "corrupt" the buffer. */ @@ -3423,22 +3639,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"; @@ -3447,7 +3663,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: @@ -3514,16 +3730,9 @@ pager_request(struct view *view, enum request request, struct line *line) static bool pager_grep(struct view *view, struct line *line) { - regmatch_t pmatch; - char *text = line->data; - - if (!*text) - return FALSE; - - if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH) - return FALSE; + const char *text[] = { line->data, NULL }; - return TRUE; + return grep_text(view, text); } static void @@ -3726,14 +3935,11 @@ 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 SIZEOF_TREE_MODE \ STRING_SIZE("100644 ") @@ -3744,26 +3950,57 @@ push_tree_stack_entry(const char *name, unsigned long lineno) 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. */ + time_t 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, 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) @@ -3788,8 +4025,8 @@ tree_entry(struct view *view, enum line_type type, const char *path, static bool tree_read_date(struct view *view, char *text, bool *read_date) { - static char author_name[SIZEOF_STR]; - static struct tm author_time; + static const char *author_name; + static time_t author_time; if (!text && *read_date) { *read_date = FALSE; @@ -3804,6 +4041,12 @@ tree_read_date(struct view *view, char *text, bool *read_date) }; 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; @@ -3816,7 +4059,7 @@ tree_read_date(struct view *view, char *text, bool *read_date) } else if (*text == 'a' && get_line_type(text) == LINE_AUTHOR) { parse_author_line(text + STRING_SIZE("author "), - author_name, sizeof(author_name), &author_time); + &author_name, &author_time); } else if (*text == ':') { char *pos; @@ -3839,12 +4082,12 @@ tree_read_date(struct view *view, char *text, bool *read_date) struct line *line = &view->line[i]; struct tree_entry *entry = line->data; - annotated += !!*entry->author; - if (*entry->author || strcmp(entry->name, text)) + 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)); + entry->author = author_name; + entry->time = author_time; line->dirty = 1; break; } @@ -3871,7 +4114,7 @@ tree_read(struct view *view, char *text) if (textlen <= SIZEOF_TREE_ATTR) return FALSE; if (view->lines == 0 && - !tree_entry(view, LINE_TREE_PARENT, opt_path, NULL, NULL)) + !tree_entry(view, LINE_TREE_HEAD, opt_path, NULL, NULL)) return FALSE; /* Strip the path part ... */ @@ -3922,33 +4165,17 @@ tree_draw(struct view *view, struct line *line, unsigned int lineno) { struct tree_entry *entry = line->data; - if (line->type == LINE_TREE_PARENT) { + if (line->type == LINE_TREE_HEAD) { if (draw_text(view, line->type, "Directory path /", TRUE)) return TRUE; } else { - char mode[11] = "-r--r--r--"; - - if (S_ISDIR(entry->mode)) { - mode[3] = mode[6] = mode[9] = 'x'; - mode[0] = 'd'; - } - if (S_ISLNK(entry->mode)) - mode[0] = 'l'; - if (entry->mode & S_IWUSR) - mode[2] = 'w'; - if (entry->mode & S_IXUSR) - mode[3] = 'x'; - if (entry->mode & S_IXGRP) - mode[6] = 'x'; - if (entry->mode & S_IXOTH) - mode[9] = 'x'; - if (draw_field(view, LINE_TREE_MODE, mode, 11, TRUE)) + 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)) + if (opt_date && draw_date(view, entry->author ? &entry->time : NULL)) return TRUE; } if (draw_text(view, line->type, entry->name, TRUE)) @@ -3997,6 +4224,11 @@ tree_request(struct view *view, enum request request, struct line *line) } 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 */ @@ -4019,7 +4251,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(); @@ -4052,6 +4284,20 @@ tree_request(struct view *view, enum request request, struct line *line) return REQ_NONE; } +static bool +tree_grep(struct view *view, struct line *line) +{ + struct tree_entry *entry = line->data; + const char *text[] = { + entry->name, + opt_author ? entry->author : "", + opt_date ? mkdate(&entry->time) : "", + NULL + }; + + return grep_text(view, text); +} + static void tree_select(struct view *view, struct line *line) { @@ -4079,7 +4325,7 @@ static struct view_ops tree_ops = { tree_read, tree_draw, tree_request, - pager_grep, + tree_grep, tree_select, }; @@ -4144,14 +4390,15 @@ static const char *blame_cat_file_argv[] = { struct blame_commit { char id[SIZEOF_REV]; /* SHA1 ID. */ char title[128]; /* First line of the commit message. */ - char author[75]; /* Author of the commit. */ - struct tm time; /* Date from the author ident. */ + const char *author; /* Author of the commit. */ + time_t time; /* Date from the author ident. */ char filename[128]; /* Name of file. */ bool has_previous; /* Was a "previous" line detected. */ }; struct blame { struct blame_commit *commit; + unsigned long lineno; char text[1]; }; @@ -4215,14 +4462,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; @@ -4236,6 +4485,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; } @@ -4263,14 +4513,15 @@ blame_read_file(struct view *view, const char *line, bool *read_file) return FALSE; } else { - size_t linelen = string_expand_length(line, opt_tab_size); + size_t linelen = strlen(line); struct blame *blame = malloc(sizeof(*blame) + linelen); if (!blame) return FALSE; blame->commit = NULL; - string_expand(blame->text, linelen + 1, line, opt_tab_size); + strncpy(blame->text, line, linelen); + blame->text[linelen] = 0; return add_line_data(view, blame, LINE_BLAME_ID) != NULL; } } @@ -4292,7 +4543,6 @@ blame_read(struct view *view, char *line) { static struct blame_commit *commit = NULL; static int blamed = 0; - static time_t author_time; static bool read_file = TRUE; if (read_file) @@ -4317,24 +4567,13 @@ blame_read(struct view *view, char *line) view->lines ? blamed * 100 / view->lines : 0); } else if (match_blame_header("author ", &line)) { - string_ncopy(commit->author, line, strlen(line)); + commit->author = get_author(line); } else if (match_blame_header("author-time ", &line)) { - author_time = (time_t) atol(line); + commit->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; - gmtime_r(&author_time, &commit->time); + parse_timezone(&commit->time, line); } else if (match_blame_header("summary ", &line)) { string_ncopy(commit->title, line, strlen(line)); @@ -4354,8 +4593,9 @@ static bool blame_draw(struct view *view, struct line *line, unsigned int lineno) { struct blame *blame = line->data; - struct tm *time = NULL; + time_t *time = NULL; const char *id = NULL, *author = NULL; + char text[SIZEOF_STR]; if (blame->commit && *blame->commit->filename) { id = blame->commit->id; @@ -4375,22 +4615,59 @@ 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; } static bool -check_blame_commit(struct blame *blame) +check_blame_commit(struct blame *blame, bool check_null_id) { if (!blame->commit) report("Commit data not loaded yet"); - else if (!strcmp(blame->commit->id, NULL_ID)) + else if (check_null_id && !strcmp(blame->commit->id, NULL_ID)) report("No commit exist for the selected line"); else return TRUE; return FALSE; } +static void +setup_blame_parent_line(struct view *view, struct blame *blame) +{ + const char *diff_tree_argv[] = { + "git", "diff-tree", "-U0", blame->commit->id, + "--", blame->commit->filename, NULL + }; + struct io io = {}; + int parent_lineno = -1; + int blamed_lineno = -1; + char *line; + + if (!run_io(&io, diff_tree_argv, NULL, IO_RD)) + return; + + while ((line = io_get(&io, '\n', TRUE))) { + if (*line == '@') { + char *pos = strchr(line, '+'); + + parent_lineno = atoi(line + 4); + if (pos) + blamed_lineno = atoi(pos + 1); + + } else if (*line == '+' && parent_lineno != -1) { + if (blame->lineno == blamed_lineno - 1 && + !strcmp(blame->text, line + 1)) { + view->lineno = parent_lineno ? parent_lineno - 1 : 0; + break; + } + blamed_lineno++; + } + } + + done_io(&io); +} + static enum request blame_request(struct view *view, enum request request, struct line *line) { @@ -4399,23 +4676,28 @@ blame_request(struct view *view, enum request request, struct line *line) switch (request) { case REQ_VIEW_BLAME: - if (check_blame_commit(blame)) { + if (check_blame_commit(blame, TRUE)) { 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)) + if (check_blame_commit(blame, TRUE) && + select_commit_parent(blame->commit->id, opt_ref, + blame->commit->filename)) { + string_copy(opt_file, blame->commit->filename); + setup_blame_parent_line(view, blame); open_view(view, REQ_VIEW_BLAME, OPEN_REFRESH); + } break; case REQ_ENTER: - if (!blame->commit) { - report("No commit loaded yet"); + if (!check_blame_commit(blame, FALSE)) break; - } if (view_is_displayed(VIEW(REQ_VIEW_DIFF)) && !strcmp(blame->commit->id, VIEW(REQ_VIEW_DIFF)->ref)) @@ -4455,57 +4737,231 @@ blame_request(struct view *view, enum request request, struct line *line) } static bool -blame_grep(struct view *view, struct line *line) +blame_grep(struct view *view, struct line *line) +{ + struct blame *blame = line->data; + struct blame_commit *commit = blame->commit; + const char *text[] = { + blame->text, + commit ? commit->title : "", + commit ? commit->id : "", + commit && opt_author ? commit->author : "", + commit && opt_date ? mkdate(&commit->time) : "", + NULL + }; + + return grep_text(view, text); +} + +static void +blame_select(struct view *view, struct line *line) +{ + struct blame *blame = line->data; + struct blame_commit *commit = blame->commit; + + if (!commit) + return; + + if (!strcmp(commit->id, NULL_ID)) + string_ncopy(ref_commit, "HEAD", 4); + else + string_copy_rev(ref_commit, commit->id); +} + +static struct view_ops blame_ops = { + "line", + NULL, + blame_open, + blame_read, + blame_draw, + blame_request, + blame_grep, + blame_select, +}; + +/* + * Branch backend + */ + +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. */ +}; + +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, 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) +{ + struct branch *branch = line->data; + enum line_type type = branch->ref->head ? LINE_MAIN_HEAD : LINE_DEFAULT; + + if (opt_date && draw_date(view, branch->author ? &branch->time : NULL)) + return TRUE; + + if (opt_author && draw_author(view, branch->author)) + return TRUE; + + draw_text(view, type, branch->ref->name, TRUE); + return TRUE; +} + +static enum request +branch_request(struct view *view, enum request request, struct line *line) +{ + 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); + return REQ_NONE; + + default: + return request; + } +} + +static bool +branch_read(struct view *view, char *line) +{ + static char id[SIZEOF_REV]; + struct branch *reference; + size_t i; + + if (!line) + return TRUE; + + switch (get_line_type(line)) { + case LINE_COMMIT: + string_copy_rev(id, line + STRING_SIZE("commit ")); + return TRUE; + + case LINE_AUTHOR: + 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); + reference = branch; + } + return TRUE; + + default: + return TRUE; + } + +} + +static bool +branch_open_visitor(void *data, struct ref *ref) { - struct blame *blame = line->data; - struct blame_commit *commit = blame->commit; - regmatch_t pmatch; + struct view *view = data; + struct branch *branch; -#define MATCH(text, on) \ - (on && *text && regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH) + if (ref->tag || ref->ltag || ref->remote) + return TRUE; - if (commit) { - char buf[DATE_COLS + 1]; + branch = calloc(1, sizeof(*branch)); + if (!branch) + return FALSE; - if (MATCH(commit->title, 1) || - MATCH(commit->author, opt_author) || - MATCH(commit->id, opt_date)) - return TRUE; + branch->ref = ref; + return !!add_line_data(view, branch, LINE_DEFAULT); +} - if (strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time) && - MATCH(buf, 1)) - return TRUE; +static bool +branch_open(struct view *view) +{ + const char *branch_log[] = { + "git", "log", "--no-color", "--pretty=raw", + "--simplify-by-decoration", "--all", NULL + }; + + if (!run_io_rd(&view->io, branch_log, FORMAT_NONE)) { + report("Failed to load branch data"); + return TRUE; } - return MATCH(blame->text, 1); + setup_update(view, view->id); + foreach_ref(branch_open_visitor, view); + view->p_restore = TRUE; + + return TRUE; +} + +static bool +branch_grep(struct view *view, struct line *line) +{ + struct branch *branch = line->data; + const char *text[] = { + branch->ref->name, + branch->author, + NULL + }; -#undef MATCH + return grep_text(view, text); } static void -blame_select(struct view *view, struct line *line) +branch_select(struct view *view, struct line *line) { - struct blame *blame = line->data; - struct blame_commit *commit = blame->commit; - - if (!commit) - return; + struct branch *branch = line->data; - if (!strcmp(commit->id, NULL_ID)) - string_ncopy(ref_commit, "HEAD", 4); - else - string_copy_rev(ref_commit, commit->id); + string_copy_rev(view->ref, branch->ref->id); + string_copy_rev(ref_commit, branch->ref->id); + string_copy_rev(ref_head, branch->ref->id); } -static struct view_ops blame_ops = { - "line", +static struct view_ops branch_ops = { + "branch", NULL, - blame_open, - blame_read, - blame_draw, - blame_request, - blame_grep, - blame_select, + branch_open, + branch_read, + branch_draw, + branch_request, + branch_grep, + branch_select, }; /* @@ -4532,6 +4988,8 @@ static enum line_type stage_line_type; static size_t stage_chunks; static int *stage_chunk; +DEFINE_ALLOCATOR(realloc_ints, int, 32) + /* This should work even for the "On branch" line. */ static inline bool status_has_none(struct view *view, struct line *line) @@ -4575,7 +5033,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 = {}; @@ -4586,6 +5043,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)) @@ -4598,7 +5057,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; @@ -4606,21 +5065,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; } } @@ -4704,6 +5155,55 @@ status_restore(struct view *view) 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 (string_format(buf, "%s/rebase-merge/head-name", opt_git_dir) && + io_open(&io, buf) && + io_read_buf(&io, buf, sizeof(buf))) { + head = buf; + if (!prefixcmp(head, "refs/heads/")) + head += STRING_SIZE("refs/heads/"); + } + } + + 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 * info using git-diff-files(1), and finally untracked files using * git-ls-files(1). */ @@ -4713,12 +5213,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); @@ -4766,7 +5261,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: @@ -4791,6 +5286,15 @@ status_draw(struct view *view, struct line *line, unsigned int lineno) return TRUE; } +static enum request +status_load_error(struct view *view, struct view *stage, const char *path) +{ + if (displayed_views() == 2 || display[current_view] != view) + maximize_view(view); + report("Failed to load '%s': %s", path, io_strerror(&stage->io)); + return REQ_NONE; +} + static enum request status_enter(struct view *view, struct line *line) { @@ -4818,7 +5322,7 @@ status_enter(struct view *view, struct line *line) }; if (!prepare_update(stage, no_head_diff_argv, opt_cdup, FORMAT_DASH)) - return REQ_QUIT; + return status_load_error(view, stage, newpath); } else { const char *index_show_argv[] = { "git", "diff-index", "--root", "--patch-with-stat", @@ -4827,7 +5331,7 @@ status_enter(struct view *view, struct line *line) }; if (!prepare_update(stage, index_show_argv, opt_cdup, FORMAT_DASH)) - return REQ_QUIT; + return status_load_error(view, stage, newpath); } if (status) @@ -4844,7 +5348,7 @@ status_enter(struct view *view, struct line *line) }; if (!prepare_update(stage, files_show_argv, opt_cdup, FORMAT_DASH)) - return REQ_QUIT; + return status_load_error(view, stage, newpath); if (status) info = "Unstaged changes to %s"; else @@ -4863,7 +5367,7 @@ status_enter(struct view *view, struct line *line) } if (!prepare_update_file(stage, newpath)) - return REQ_QUIT; + return status_load_error(view, stage, newpath); info = "Untracked file %s"; break; @@ -4981,18 +5485,19 @@ status_update_file(struct status *status, enum line_type type) return FALSE; result = status_update_write(&io, status, type); - done_io(&io); - return result; + return done_io(&io) && result; } static bool status_update_files(struct view *view, struct line *line) { + char buf[sizeof(view->ref)]; struct io io = {}; bool result = TRUE; struct line *pos = view->line + view->lines; int files = 0; int file, done; + int cursor_y = -1, cursor_x = -1; if (!status_update_prepare(&io, line->type)) return FALSE; @@ -5000,7 +5505,9 @@ status_update_files(struct view *view, struct line *line) for (pos = line; pos < view->line + view->lines && pos->data; pos++) files++; - for (file = 0, done = 0; result && file < files; line++, file++) { + string_copy(buf, view->ref); + getsyx(cursor_y, cursor_x); + for (file = 0, done = 5; result && file < files; line++, file++) { int almost_done = file * 100 / files; if (almost_done > done) { @@ -5008,12 +5515,14 @@ status_update_files(struct view *view, struct line *line) string_format(view->ref, "updating file %u of %u (%d%% done)", file, files, done); update_view_title(view); + setsyx(cursor_y, cursor_x); + doupdate(); } result = status_update_write(&io, line->data, line->type); } + string_copy(view->ref, buf); - done_io(&io); - return result; + return done_io(&io) && result; } static bool @@ -5059,13 +5568,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); } } @@ -5115,8 +5631,7 @@ status_request(struct view *view, enum request request, struct line *line) /* After returning the status view has been split to * show the stage view. No further reloading is * necessary. */ - status_enter(view, line); - return REQ_NONE; + return status_enter(view, line); case REQ_REFRESH: /* Simply reload the view. */ @@ -5182,29 +5697,12 @@ static bool status_grep(struct view *view, struct line *line) { struct status *status = line->data; - enum { S_STATUS, S_NAME, S_END } state; - char buf[2] = "?"; - regmatch_t pmatch; - - if (!status) - return FALSE; - - for (state = S_STATUS; state < S_END; state++) { - const char *text; - switch (state) { - case S_NAME: text = status->new.name; break; - case S_STATUS: - buf[0] = status->status; - text = buf; - break; - - default: - return FALSE; - } + if (status) { + const char buf[2] = { status->status, 0 }; + const char *text[] = { status->new.name, buf, NULL }; - if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH) - return TRUE; + return grep_text(view, text); } return FALSE; @@ -5346,21 +5844,15 @@ stage_next(struct view *view, struct line *line) int i; if (!stage_chunks) { - static size_t alloc = 0; - int *tmp; - for (line = view->line; line < view->line + view->lines; line++) { if (line->type != LINE_DIFF_CHUNK) continue; - tmp = realloc_items(stage_chunk, &alloc, - stage_chunks, sizeof(*tmp)); - if (!tmp) { + if (!realloc_ints(&stage_chunk, stage_chunks, 1)) { report("Allocation failure"); return; } - stage_chunk = tmp; stage_chunk[stage_chunks++] = line - view->line; } } @@ -5429,7 +5921,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. */ @@ -5473,9 +5965,9 @@ static struct view_ops stage_ops = { struct commit { char id[SIZEOF_REV]; /* SHA1 ID. */ char title[128]; /* First line of the commit message. */ - char author[75]; /* Author of the commit. */ - struct tm time; /* Date from the author ident. */ - struct ref **refs; /* Repository references. */ + const char *author; /* Author of the commit. */ + time_t 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. */ @@ -5700,7 +6192,7 @@ main_draw(struct view *view, struct line *line, unsigned int lineno) { struct commit *commit = line->data; - if (!*commit->author) + if (!commit->author) return FALSE; if (opt_date && draw_date(view, &commit->time)) @@ -5714,32 +6206,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); @@ -5762,7 +6255,7 @@ main_read(struct view *view, char *line) if (view->lines > 0) { commit = view->line[view->lines - 1].data; view->line[view->lines - 1].dirty = 1; - if (!*commit->author) { + if (!commit->author) { view->lines--; free(commit); graph->commit = NULL; @@ -5788,7 +6281,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); @@ -5813,8 +6306,7 @@ main_read(struct view *view, char *line) case LINE_AUTHOR: parse_author_line(line + STRING_SIZE("author "), - commit->author, sizeof(commit->author), - &commit->time); + &commit->author, &commit->time); update_rev_graph(view, graph); graph = graph->next; break; @@ -5866,17 +6358,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 (!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; } @@ -5885,42 +6378,14 @@ static bool main_grep(struct view *view, struct line *line) { struct commit *commit = line->data; - enum { S_TITLE, S_AUTHOR, S_DATE, S_REFS, S_END } state; - char buf[DATE_COLS + 1]; - regmatch_t pmatch; - - for (state = S_TITLE; state < S_END; state++) { - char *text; - - switch (state) { - case S_TITLE: text = commit->title; break; - case S_AUTHOR: - if (!opt_author) - continue; - text = commit->author; - break; - case S_DATE: - if (!opt_date) - continue; - if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time)) - continue; - text = buf; - break; - case S_REFS: - if (!opt_show_refs) - continue; - if (grep_refs(commit->refs, view->regex) == TRUE) - return TRUE; - continue; - default: - return FALSE; - } - - if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH) - return TRUE; - } + const char *text[] = { + commit->title, + opt_author ? commit->author : "", + opt_date ? mkdate(&commit->time) : "", + NULL + }; - return FALSE; + return grep_text(view, text) || grep_refs(commit->refs, view->regex); } static void @@ -5947,15 +6412,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) { @@ -5993,7 +6454,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) { @@ -6034,7 +6495,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 @@ -6049,9 +6510,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; @@ -6072,11 +6533,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; @@ -6089,11 +6554,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; } @@ -6195,7 +6660,7 @@ 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, FALSE); @@ -6386,18 +6851,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 size_t refs_alloc = 0; +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_alloc = 0; -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_list, struct ref *, 8) +DEFINE_ALLOCATOR(realloc_ref_lists, struct ref_list *, 8) static int compare_refs(const void *ref1_, const void *ref2_) @@ -6418,76 +6945,63 @@ compare_refs(const void *ref1_, const void *ref2_) return strcmp(ref1->name, ref2->name); } -static struct ref ** -get_refs(const char *id) +static void +foreach_ref(bool (*visitor)(void *data, struct ref *ref), void *data) { - struct ref ***tmp_id_refs; - struct ref **ref_list = NULL; - size_t ref_list_alloc = 0; - size_t ref_list_size = 0; size_t i; - for (i = 0; i < id_refs_size; i++) - if (!strcmp(id, id_refs[i][0]->id)) - return id_refs[i]; - - tmp_id_refs = realloc_items(id_refs, &id_refs_alloc, id_refs_size + 1, - sizeof(*id_refs)); - if (!tmp_id_refs) - return NULL; - - id_refs = tmp_id_refs; - - for (i = 0; i < refs_size; i++) { - struct ref **tmp; + for (i = 0; i < refs_size; i++) + if (!visitor(data, refs[i])) + break; +} - if (strcmp(id, refs[i].id)) - continue; +static struct ref_list * +get_ref_list(const char *id) +{ + struct ref_list *list; + size_t i; - tmp = realloc_items(ref_list, &ref_list_alloc, - ref_list_size + 1, sizeof(*ref_list)); - if (!tmp) { - if (ref_list) - free(ref_list); - return NULL; - } + for (i = 0; i < ref_lists_size; i++) + if (!strcmp(id, ref_lists[i]->id)) + return ref_lists[i]; - ref_list = tmp; - 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; + if (!realloc_ref_lists(&ref_lists, ref_lists_size, 1)) + return NULL; + list = calloc(1, sizeof(*list)); + if (!list) + return NULL; - ref_list_size++; + for (i = 0; i < refs_size; i++) { + 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; } @@ -6512,27 +7026,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 (cmp < 0) + to = pos - 1; + else + from = pos + 1; } - refs = realloc_items(refs, &refs_alloc, refs_size + 1, sizeof(*refs)); - if (!refs) - return ERR; - 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; @@ -6546,10 +7071,14 @@ read_ref(char *id, size_t idlen, char *name, size_t namelen) static int load_refs(void) { + const char *head_argv[] = { + "git", "symbolic-ref", "HEAD", NULL + }; static const char *ls_remote_argv[SIZEOF_ARG] = { - "git", "ls-remote", ".", NULL + "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"); @@ -6559,12 +7088,102 @@ load_refs(void) if (!*opt_git_dir) return OK; - while (refs_size > 0) - free(refs[--refs_size].name); - while (id_refs_size > 0) - free(id_refs[--id_refs_size]); + if (run_io_buf(head_argv, opt_head, sizeof(opt_head)) && + !prefixcmp(opt_head, "refs/heads/")) { + char *offset = opt_head + STRING_SIZE("refs/heads/"); + + memmove(opt_head, offset, strlen(offset) + 1); + } + + 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; + + /* 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 +set_remote_branch(const char *name, const char *value, size_t valuelen) +{ + if (!strcmp(name, ".remote")) { + string_ncopy(opt_remote, value, valuelen); + + } else if (*opt_remote && !strcmp(name, ".merge")) { + size_t from = strlen(opt_remote); + + if (!prefixcmp(value, "refs/heads/")) + value += STRING_SIZE("refs/heads/"); + + if (!string_format_from(opt_remote, &from, "/%s", value)) + opt_remote[0] = 0; + } +} + +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 bool +set_environment_variable(const char *name, const char *value) +{ + size_t len = strlen(name) + 1 + strlen(value) + 1; + char *env = malloc(len); + + if (env && + string_nformat(env, len, NULL, "%s=%s", name, value) && + putenv(env) == 0) + return TRUE; + free(env); + return FALSE; +} - return run_io_load(ls_remote_argv, "\t", read_ref); +static void +set_work_tree(const char *value) +{ + char cwd[SIZEOF_STR]; + + if (!getcwd(cwd, sizeof(cwd))) + die("Failed to get cwd path: %s", strerror(errno)); + if (chdir(opt_git_dir) < 0) + die("Failed to chdir(%s): %s", strerror(errno)); + if (!getcwd(opt_git_dir, sizeof(opt_git_dir))) + die("Failed to get git path: %s", strerror(errno)); + if (chdir(cwd) < 0) + die("Failed to chdir(%s): %s", cwd, strerror(errno)); + if (chdir(value) < 0) + die("Failed to chdir(%s): %s", value, strerror(errno)); + if (!getcwd(cwd, sizeof(cwd))) + die("Failed to get cwd path: %s", strerror(errno)); + if (!set_environment_variable("GIT_WORK_TREE", cwd)) + die("Failed to set GIT_WORK_TREE to '%s'", cwd); + if (!set_environment_variable("GIT_DIR", opt_git_dir)) + die("Failed to set GIT_DIR to '%s'", opt_git_dir); + opt_is_inside_work_tree = TRUE; } static int @@ -6573,30 +7192,24 @@ read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen if (!strcmp(name, "i18n.commitencoding")) string_ncopy(opt_encoding, value, valuelen); - if (!strcmp(name, "core.editor")) + else if (!strcmp(name, "core.editor")) string_ncopy(opt_editor, value, valuelen); - /* branch..remote */ - if (*opt_head && - !strncmp(name, "branch.", 7) && - !strncmp(name + 7, opt_head, strlen(opt_head)) && - !strcmp(name + 7 + strlen(opt_head), ".remote")) - string_ncopy(opt_remote, value, valuelen); + else if (!strcmp(name, "core.worktree")) + set_work_tree(value); - if (*opt_head && *opt_remote && - !strncmp(name, "branch.", 7) && - !strncmp(name + 7, opt_head, strlen(opt_head)) && - !strcmp(name + 7 + strlen(opt_head), ".merge")) { - size_t from = strlen(opt_remote); + else if (!prefixcmp(name, "tig.color.")) + set_repo_config_option(name + 10, value, option_color_command); - if (!prefixcmp(value, "refs/heads/")) { - value += STRING_SIZE("refs/heads/"); - valuelen -= STRING_SIZE("refs/heads/"); - } + else if (!prefixcmp(name, "tig.bind.")) + set_repo_config_option(name + 9, value, option_bind_command); - if (!string_format_from(opt_remote, &from, "/%s", value)) - opt_remote[0] = 0; - } + else if (!prefixcmp(name, "tig.")) + set_repo_config_option(name + 4, value, option_set_command); + + else if (*opt_head && !prefixcmp(name, "branch.") && + !strncmp(name + 7, opt_head, strlen(opt_head))) + set_remote_branch(name + 7 + strlen(opt_head), value, valuelen); return OK; } @@ -6636,23 +7249,11 @@ read_repo_info(char *name, size_t namelen, char *value, size_t valuelen) static int load_repo_info(void) { - const char *head_argv[] = { - "git", "symbolic-ref", "HEAD", NULL - }; const char *rev_parse_argv[] = { "git", "rev-parse", "--git-dir", "--is-inside-work-tree", "--show-cdup", "--show-prefix", NULL }; - if (run_io_buf(head_argv, opt_head, sizeof(opt_head))) { - chomp_string(opt_head); - if (!prefixcmp(opt_head, "refs/heads/")) { - char *offset = opt_head + STRING_SIZE("refs/heads/"); - - memmove(opt_head, offset, strlen(offset) + 1); - } - } - return run_io_load(rev_parse_argv, "=", read_repo_info); } @@ -6661,6 +7262,19 @@ load_repo_info(void) * 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) { @@ -6785,6 +7399,7 @@ main(int argc, const char *argv[]) size_t i; signal(SIGINT, quit); + signal(SIGPIPE, SIG_IGN); if (setlocale(LC_ALL, "")) { char *codeset = nl_langinfo(CODESET); @@ -6839,7 +7454,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;