X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=tig.c;h=77e1d8183372669bc1fed7fda0cbb97ada649417;hb=bb46d935db3f3548bbe2d411f58588943058148c;hp=ba7af0960788322ea513a61e5bad3ab56c0701aa;hpb=a4da0896a29f20a7831a2195de46ade8a682bc4e;p=tig.git diff --git a/tig.c b/tig.c index ba7af09..77e1d81 100644 --- a/tig.c +++ b/tig.c @@ -1,4 +1,4 @@ -/* Copyright (c) 2006-2008 Jonas Fonseca +/* Copyright (c) 2006-2010 Jonas Fonseca * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -32,9 +32,13 @@ #include #include #include +#include #include +#include #include +#include #include +#include #include @@ -64,14 +68,10 @@ static void __NORETURN die(const char *err, ...); static void warn(const char *msg, ...); static void report(const char *msg, ...); -static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, size_t, char *, size_t)); -static void set_nonblocking_input(bool loading); -static size_t utf8_length(const char *string, int *width, size_t max_width, int *trimmed, bool reserve); -static bool prompt_yesno(const char *prompt); -static int load_refs(void); #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) @@ -102,93 +102,84 @@ 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 +#define AUTHOR_COLS 19 -/* The default interval between line numbers. */ -#define NUMBER_INTERVAL 5 - -#define TAB_SIZE 8 - -#define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3) +#define MIN_VIEW_HEIGHT 4 #define NULL_ID "0000000000000000000000000000000000000000" -#ifndef GIT_CONFIG -#define GIT_CONFIG "config" -#endif - -#define TIG_LS_REMOTE \ - "git ls-remote . 2>/dev/null" - -#define TIG_DIFF_CMD \ - "git show --pretty=fuller --no-color --root --patch-with-stat --find-copies-harder -C %s 2>/dev/null" - -#define TIG_LOG_CMD \ - "git log --no-color --cc --stat -n100 %s 2>/dev/null" +#define S_ISGITLINK(mode) (((mode) & S_IFMT) == 0160000) -#define TIG_MAIN_BASE \ - "git log --no-color --pretty=raw --parents --topo-order" - -#define TIG_MAIN_CMD \ - TIG_MAIN_BASE " %s 2>/dev/null" - -#define TIG_TREE_CMD \ - "git ls-tree %s %s" - -#define TIG_BLOB_CMD \ - "git cat-file blob %s" - -/* XXX: Needs to be defined to the empty string. */ -#define TIG_HELP_CMD "" -#define TIG_PAGER_CMD "" -#define TIG_STATUS_CMD "" -#define TIG_STAGE_CMD "" -#define TIG_BLAME_CMD "" - -/* 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. */ +}; -struct int_map { - const char *name; - int namelen; - int value; +static struct ref *get_ref_head(); +static struct ref_list *get_ref_list(const char *id); +static void foreach_ref(bool (*visitor)(void *data, const struct ref *ref), void *data); +static int load_refs(void); + +enum input_status { + INPUT_OK, + INPUT_SKIP, + INPUT_STOP, + INPUT_CANCEL }; -static int -set_from_int_map(struct int_map *map, size_t map_size, - int *value, const char *name, int namelen) -{ +typedef enum input_status (*input_handler)(void *data, char *buf, int c); - int i; +static char *prompt_input(const char *prompt, input_handler handler, void *data); +static bool prompt_yesno(const char *prompt); - for (i = 0; i < map_size; i++) - if (namelen == map[i].namelen && - !strncasecmp(name, map[i].name, namelen)) { - *value = map[i].value; - return OK; - } +struct menu_item { + int hotkey; + const char *text; + void *data; +}; - return ERR; -} +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 @@ -218,6 +209,27 @@ string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen) #define string_add(dst, from, src) \ string_ncopy_do(dst + (from), sizeof(dst) - (from), src, sizeof(src)) +static void +string_expand(char *dst, size_t dstlen, const char *src, int tabsize) +{ + size_t size, pos; + + for (size = pos = 0; size < dstlen - 1 && src[pos]; pos++) { + if (src[pos] == '\t') { + size_t expanded = tabsize - (size % tabsize); + + if (expanded + size >= dstlen - 1) + expanded = dstlen - size - 1; + memcpy(dst + size, " ", expanded); + size += expanded; + } else { + dst[size++] = src[pos]; + } + } + + dst[size] = 0; +} + static char * chomp_string(char *name) { @@ -277,6 +289,53 @@ string_enum_compare(const char *str1, const char *str2, int len) return 0; } +#define enum_equals(entry, str, len) \ + ((entry).namelen == (len) && !string_enum_compare((entry).name, str, len)) + +struct enum_map { + const char *name; + int namelen; + int value; +}; + +#define ENUM_MAP(name, value) { name, STRING_SIZE(name), value } + +static char * +enum_map_name(const char *name, size_t namelen) +{ + static char buf[SIZEOF_STR]; + int bufpos; + + for (bufpos = 0; bufpos <= namelen; bufpos++) { + buf[bufpos] = tolower(name[bufpos]); + if (buf[bufpos] == '_') + buf[bufpos] = '-'; + } + + buf[bufpos] = 0; + return buf; +} + +#define enum_name(entry) enum_map_name((entry).name, (entry).namelen) + +static bool +map_enum_do(const struct enum_map *map, size_t map_size, int *value, const char *name) +{ + size_t namelen = strlen(name); + int i; + + for (i = 0; i < map_size; i++) + if (enum_equals(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)) @@ -289,297 +348,863 @@ suffixcmp(const char *str, int slen, const char *suffix) return suffixlen < len ? strcmp(str + len - suffixlen, suffix) : -1; } -/* Shell quoting - * - * NOTE: The following is a slightly modified copy of the git project's shell - * quoting routines found in the quote.c file. - * - * Help to copy the thing properly quoted for the shell safety. any single - * quote is replaced with '\'', any exclamation point is replaced with '\!', - * and the whole thing is enclosed in a + +/* + * Unicode / UTF-8 handling * - * E.g. - * original sq_quote result - * name ==> name ==> 'name' - * a b ==> a b ==> 'a b' - * a'b ==> a'\''b ==> 'a'\''b' - * a!b ==> a'\!'b ==> 'a'\!'b' + * 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 UTF-8 branch commit elinks-0.11.0-g31f2c28. */ -static size_t -sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src) +static inline int +unicode_width(unsigned long c, int tab_size) { - char c; + if (c >= 0x1100 && + (c <= 0x115f /* Hangul Jamo */ + || c == 0x2329 + || c == 0x232a + || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f) + /* CJK ... Yi */ + || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */ + || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */ + || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */ + || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */ + || (c >= 0xffe0 && c <= 0xffe6) + || (c >= 0x20000 && c <= 0x2fffd) + || (c >= 0x30000 && c <= 0x3fffd))) + return 2; -#define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0) + if (c == '\t') + return tab_size; - BUFPUT('\''); - while ((c = *src++)) { - if (c == '\'' || c == '!') { - BUFPUT('\''); - BUFPUT('\\'); - BUFPUT(c); - BUFPUT('\''); - } else { - BUFPUT(c); - } - } - BUFPUT('\''); + return 1; +} + +/* Number of bytes used for encoding a UTF-8 character indexed by first byte. + * Illegal bytes are set one. */ +static const unsigned char utf8_bytes[256] = { + 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, + 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, + 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, + 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, + 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, + 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, + 2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2, + 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, +}; - if (bufsize < SIZEOF_STR) - buf[bufsize] = 0; +static inline unsigned char +utf8_char_length(const char *string, const char *end) +{ + int c = *(unsigned char *) string; - return bufsize; + return utf8_bytes[c]; } +/* Decode UTF-8 multi-byte representation into a Unicode character. */ +static inline unsigned long +utf8_to_unicode(const char *string, size_t length) +{ + unsigned long unicode; -/* - * User requests - */ - -#define REQ_INFO \ - /* XXX: Keep the view request first and in sync with views[]. */ \ - REQ_GROUP("View switching") \ - REQ_(VIEW_MAIN, "Show main view"), \ - REQ_(VIEW_DIFF, "Show diff view"), \ - REQ_(VIEW_LOG, "Show log view"), \ - REQ_(VIEW_TREE, "Show tree view"), \ - REQ_(VIEW_BLOB, "Show blob view"), \ - REQ_(VIEW_BLAME, "Show blame view"), \ - REQ_(VIEW_HELP, "Show help page"), \ - REQ_(VIEW_PAGER, "Show pager view"), \ - REQ_(VIEW_STATUS, "Show status view"), \ - REQ_(VIEW_STAGE, "Show stage view"), \ - \ - REQ_GROUP("View manipulation") \ - REQ_(ENTER, "Enter current line and scroll"), \ - REQ_(NEXT, "Move to next"), \ - REQ_(PREVIOUS, "Move to previous"), \ - REQ_(VIEW_NEXT, "Move focus to next view"), \ - REQ_(REFRESH, "Reload and refresh"), \ - REQ_(MAXIMIZE, "Maximize the current view"), \ - REQ_(VIEW_CLOSE, "Close the current view"), \ - REQ_(QUIT, "Close all views and quit"), \ - \ - REQ_GROUP("View specific requests") \ - REQ_(STATUS_UPDATE, "Update file status"), \ - REQ_(STATUS_REVERT, "Revert file changes"), \ - REQ_(STATUS_MERGE, "Merge file using external tool"), \ - REQ_(STAGE_NEXT, "Find next chunk to stage"), \ - REQ_(TREE_PARENT, "Switch to parent directory in tree view"), \ - \ - REQ_GROUP("Cursor navigation") \ - REQ_(MOVE_UP, "Move cursor one line up"), \ - REQ_(MOVE_DOWN, "Move cursor one line down"), \ - REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \ - REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \ - REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \ - REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \ - \ - REQ_GROUP("Scrolling") \ - REQ_(SCROLL_LINE_UP, "Scroll one line up"), \ - REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \ - REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \ - REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \ - \ - REQ_GROUP("Searching") \ - REQ_(SEARCH, "Search the view"), \ - REQ_(SEARCH_BACK, "Search backwards in the view"), \ - REQ_(FIND_NEXT, "Find next search match"), \ - REQ_(FIND_PREV, "Find previous search match"), \ - \ - REQ_GROUP("Option manipulation") \ - REQ_(TOGGLE_LINENO, "Toggle line numbers"), \ - REQ_(TOGGLE_DATE, "Toggle date display"), \ - REQ_(TOGGLE_AUTHOR, "Toggle author display"), \ - REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"), \ - REQ_(TOGGLE_REFS, "Toggle reference display (tags/branches)"), \ - \ - REQ_GROUP("Misc") \ - REQ_(PROMPT, "Bring up the prompt"), \ - REQ_(SCREEN_REDRAW, "Redraw the screen"), \ - REQ_(SCREEN_RESIZE, "Resize the screen"), \ - REQ_(SHOW_VERSION, "Show version information"), \ - REQ_(STOP_LOADING, "Stop all loading views"), \ - REQ_(EDIT, "Open in editor"), \ - REQ_(NONE, "Do nothing") + switch (length) { + case 1: + unicode = string[0]; + break; + case 2: + unicode = (string[0] & 0x1f) << 6; + unicode += (string[1] & 0x3f); + break; + case 3: + unicode = (string[0] & 0x0f) << 12; + unicode += ((string[1] & 0x3f) << 6); + unicode += (string[2] & 0x3f); + break; + case 4: + unicode = (string[0] & 0x0f) << 18; + unicode += ((string[1] & 0x3f) << 12); + unicode += ((string[2] & 0x3f) << 6); + unicode += (string[3] & 0x3f); + break; + case 5: + unicode = (string[0] & 0x0f) << 24; + unicode += ((string[1] & 0x3f) << 18); + unicode += ((string[2] & 0x3f) << 12); + unicode += ((string[3] & 0x3f) << 6); + unicode += (string[4] & 0x3f); + break; + case 6: + unicode = (string[0] & 0x01) << 30; + unicode += ((string[1] & 0x3f) << 24); + unicode += ((string[2] & 0x3f) << 18); + unicode += ((string[3] & 0x3f) << 12); + unicode += ((string[4] & 0x3f) << 6); + unicode += (string[5] & 0x3f); + break; + default: + return 0; + } + /* Invalid characters could return the special 0xfffd value but NUL + * should be just as good. */ + return unicode > 0xffff ? 0 : unicode; +} -/* User action requests. */ -enum request { -#define REQ_GROUP(help) -#define REQ_(req, help) REQ_##req +/* Calculates how much of string can be shown within the given maximum width + * and sets trimmed parameter to non-zero value if all of string could not be + * shown. If the reserve flag is TRUE, it will reserve at least one + * trailing character, which can be useful when drawing a delimiter. + * + * Returns the number of bytes to output from string to satisfy max_width. */ +static size_t +utf8_length(const char **start, size_t skip, int *width, size_t max_width, int *trimmed, bool reserve, int tab_size) +{ + const char *string = *start; + const char *end = strchr(string, '\0'); + unsigned char last_bytes = 0; + size_t last_ucwidth = 0; - /* Offset all requests to avoid conflicts with ncurses getch values. */ - REQ_OFFSET = KEY_MAX + 1, - REQ_INFO + *width = 0; + *trimmed = 0; -#undef REQ_GROUP -#undef REQ_ -}; + while (string < end) { + unsigned char bytes = utf8_char_length(string, end); + size_t ucwidth; + unsigned long unicode; -struct request_info { - enum request request; - const char *name; - int namelen; - const char *help; + if (string + bytes > end) + break; + + /* Change representation to figure out whether + * it is a single- or double-width character. */ + + unicode = utf8_to_unicode(string, bytes); + /* FIXME: Graceful handling of invalid Unicode character. */ + if (!unicode) + break; + + ucwidth = unicode_width(unicode, tab_size); + if (skip > 0) { + skip -= ucwidth <= skip ? ucwidth : skip; + *start += bytes; + } + *width += ucwidth; + if (*width > max_width) { + *trimmed = 1; + *width -= ucwidth; + if (reserve && *width == max_width) { + string -= last_bytes; + *width -= last_ucwidth; + } + break; + } + + string += bytes; + last_bytes = ucwidth ? bytes : 0; + last_ucwidth = ucwidth; + } + + return string - *start; +} + + +#define DATE_INFO \ + DATE_(NO), \ + DATE_(DEFAULT), \ + DATE_(LOCAL), \ + DATE_(RELATIVE), \ + DATE_(SHORT) + +enum date { +#define DATE_(name) DATE_##name + DATE_INFO +#undef DATE_ }; -static 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 -#undef REQ_GROUP -#undef REQ_ +static const struct enum_map date_map[] = { +#define DATE_(name) ENUM_MAP(#name, DATE_##name) + DATE_INFO +#undef DATE_ }; -static enum request -get_request(const char *name) +struct time { + time_t sec; + int tz; +}; + +static inline int timecmp(const struct time *t1, const struct time *t2) { - int namelen = strlen(name); - int i; + return t1->sec - t2->sec; +} - for (i = 0; i < ARRAY_SIZE(req_info); i++) - if (req_info[i].namelen == namelen && - !string_enum_compare(req_info[i].name, name, namelen)) - return req_info[i].request; +static const char * +mkdate(const struct time *time, enum date date) +{ + static char buf[DATE_COLS + 1]; + static const struct enum_map reldate[] = { + { "second", 1, 60 * 2 }, + { "minute", 60, 60 * 60 * 2 }, + { "hour", 60 * 60, 60 * 60 * 24 * 2 }, + { "day", 60 * 60 * 24, 60 * 60 * 24 * 7 * 2 }, + { "week", 60 * 60 * 24 * 7, 60 * 60 * 24 * 7 * 5 }, + { "month", 60 * 60 * 24 * 30, 60 * 60 * 24 * 30 * 12 }, + }; + struct tm tm; - return REQ_NONE; + if (!date || !time || !time->sec) + return ""; + + if (date == DATE_RELATIVE) { + struct timeval now; + time_t date = time->sec + time->tz; + time_t seconds; + int i; + + gettimeofday(&now, NULL); + seconds = now.tv_sec < date ? date - now.tv_sec : now.tv_sec - date; + for (i = 0; i < ARRAY_SIZE(reldate); i++) { + if (seconds >= reldate[i].value) + continue; + + seconds /= reldate[i].namelen; + if (!string_format(buf, "%ld %s%s %s", + seconds, reldate[i].name, + seconds > 1 ? "s" : "", + now.tv_sec >= date ? "ago" : "ahead")) + break; + return buf; + } + } + + if (date == DATE_LOCAL) { + time_t date = time->sec + time->tz; + localtime_r(&date, &tm); + } + else { + gmtime_r(&time->sec, &tm); + } + return strftime(buf, sizeof(buf), DATE_FORMAT, &tm) ? buf : NULL; +} + + +#define AUTHOR_VALUES \ + AUTHOR_(NO), \ + AUTHOR_(FULL), \ + AUTHOR_(ABBREVIATED) + +enum author { +#define AUTHOR_(name) AUTHOR_##name + AUTHOR_VALUES, +#undef AUTHOR_ + AUTHOR_DEFAULT = AUTHOR_FULL +}; + +static const struct enum_map author_map[] = { +#define AUTHOR_(name) ENUM_MAP(#name, AUTHOR_##name) + AUTHOR_VALUES +#undef AUTHOR_ +}; + +static const char * +get_author_initials(const char *author) +{ + static char initials[AUTHOR_COLS * 6 + 1]; + size_t pos = 0; + const char *end = strchr(author, '\0'); + +#define is_initial_sep(c) (isspace(c) || ispunct(c) || (c) == '@' || (c) == '-') + + memset(initials, 0, sizeof(initials)); + while (author < end) { + unsigned char bytes; + size_t i; + + while (is_initial_sep(*author)) + author++; + + bytes = utf8_char_length(author, end); + if (bytes < sizeof(initials) - 1 - pos) { + while (bytes--) { + initials[pos++] = *author++; + } + } + + for (i = pos; author < end && !is_initial_sep(*author); author++) { + if (i < sizeof(initials) - 1) + initials[i++] = *author; + } + + initials[i++] = 0; + } + + return initials; +} + + +static bool +argv_from_string(const char *argv[SIZEOF_ARG], int *argc, char *cmd) +{ + int valuelen; + + while (*cmd && *argc < SIZEOF_ARG && (valuelen = strcspn(cmd, " \t"))) { + bool advance = cmd[valuelen] != 0; + + cmd[valuelen] = 0; + argv[(*argc)++] = chomp_string(cmd); + cmd = chomp_string(cmd + valuelen + advance); + } + + if (*argc < SIZEOF_ARG) + argv[*argc] = NULL; + return *argc < SIZEOF_ARG; +} + +static bool +argv_from_env(const char **argv, const char *name) +{ + char *env = argv ? getenv(name) : NULL; + int argc = 0; + + if (env && *env) + env = strdup(env); + return !env || argv_from_string(argv, &argc, env); +} + +static void +argv_free(const char *argv[]) +{ + int argc; + + for (argc = 0; argv[argc]; argc++) + free((void *) argv[argc]); + argv[0] = NULL; +} + +static bool +argv_copy(const char *dst[], const char *src[]) +{ + int argc; + + for (argc = 0; src[argc]; argc++) + if (!(dst[argc] = strdup(src[argc]))) + return FALSE; + return TRUE; } /* - * Options + * Executing external commands. */ -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"; +enum io_type { + IO_FD, /* File descriptor based IO. */ + IO_BG, /* Execute command in the background. */ + IO_FG, /* Execute command with same std{in,out,err}. */ + IO_RD, /* Read only fork+exec IO. */ + IO_WR, /* Write only fork+exec IO. */ + IO_AP, /* Append fork+exec output to file. */ +}; -/* Option and state variables. */ -static bool opt_date = TRUE; -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 char opt_cmd[SIZEOF_STR] = ""; -static char opt_path[SIZEOF_STR] = ""; -static char opt_file[SIZEOF_STR] = ""; -static char opt_ref[SIZEOF_REF] = ""; -static char opt_head[SIZEOF_REF] = ""; -static char opt_head_rev[SIZEOF_REV] = ""; -static char opt_remote[SIZEOF_REF] = ""; -static FILE *opt_pipe = NULL; -static char opt_encoding[20] = "UTF-8"; -static bool opt_utf8 = TRUE; -static char opt_codeset[20] = "UTF-8"; -static iconv_t opt_iconv = ICONV_NONE; -static char opt_search[SIZEOF_STR] = ""; -static char opt_cdup[SIZEOF_STR] = ""; -static char opt_git_dir[SIZEOF_STR] = ""; -static signed char opt_is_inside_work_tree = -1; /* set to TRUE or FALSE */ -static char opt_editor[SIZEOF_STR] = ""; -static FILE *opt_tty = NULL; +struct io { + int pipe; /* Pipe end for reading or writing. */ + pid_t pid; /* PID of spawned process. */ + int error; /* Error status. */ + char *buf; /* Read buffer. */ + size_t bufalloc; /* Allocated buffer size. */ + size_t bufsize; /* Buffer content size. */ + char *bufpos; /* Current buffer position. */ + unsigned int eof:1; /* Has end of file been reached. */ +}; -#define is_initial_commit() (!*opt_head_rev) -#define is_head_commit(rev) (!strcmp((rev), "HEAD") || !strcmp(opt_head_rev, (rev))) +static void +io_init(struct io *io) +{ + memset(io, 0, sizeof(*io)); + io->pipe = -1; +} -static enum request -parse_options(int argc, const char *argv[]) +static bool +io_open(struct io *io, const char *fmt, ...) { - enum request request = REQ_VIEW_MAIN; - size_t buf_size; - const char *subcommand; - bool seen_dashdash = FALSE; - int i; + char name[SIZEOF_STR] = ""; + bool fits; + va_list args; - if (!isatty(STDIN_FILENO)) { - opt_pipe = stdin; - return REQ_VIEW_PAGER; + io_init(io); + + va_start(args, fmt); + fits = vsnprintf(name, sizeof(name), fmt, args) < sizeof(name); + va_end(args); + + if (!fits) { + io->error = ENAMETOOLONG; + return FALSE; } + io->pipe = *name ? open(name, O_RDONLY) : STDIN_FILENO; + if (io->pipe == -1) + io->error = errno; + return io->pipe != -1; +} - if (argc <= 1) - return REQ_VIEW_MAIN; +static bool +io_kill(struct io *io) +{ + return io->pid == 0 || kill(io->pid, SIGKILL) != -1; +} - subcommand = argv[1]; - if (!strcmp(subcommand, "status") || !strcmp(subcommand, "-S")) { - if (!strcmp(subcommand, "-S")) - warn("`-S' has been deprecated; use `tig status' instead"); - if (argc > 2) - warn("ignoring arguments after `%s'", subcommand); - return REQ_VIEW_STATUS; +static bool +io_done(struct io *io) +{ + pid_t pid = io->pid; - } else if (!strcmp(subcommand, "blame")) { - if (argc <= 2 || argc > 4) - die("invalid number of options to blame\n\n%s", usage); + if (io->pipe != -1) + close(io->pipe); + free(io->buf); + io_init(io); - i = 2; - if (argc == 4) { - string_ncopy(opt_ref, argv[i], strlen(argv[i])); - i++; + while (pid > 0) { + int status; + pid_t waiting = waitpid(pid, &status, 0); + + if (waiting < 0) { + if (errno == EINTR) + continue; + io->error = errno; + return FALSE; } - string_ncopy(opt_file, argv[i], strlen(argv[i])); - return REQ_VIEW_BLAME; + return waiting == pid && + !WIFSIGNALED(status) && + WIFEXITED(status) && + !WEXITSTATUS(status); + } - } else if (!strcmp(subcommand, "show")) { - request = REQ_VIEW_DIFF; + return TRUE; +} + +static bool +io_run(struct io *io, enum io_type type, const char *dir, const char *argv[], ...) +{ + int pipefds[2] = { -1, -1 }; + va_list args; + + io_init(io); - } else if (!strcmp(subcommand, "log") || !strcmp(subcommand, "diff")) { - request = subcommand[0] == 'l' ? REQ_VIEW_LOG : REQ_VIEW_DIFF; - warn("`tig %s' has been deprecated", subcommand); + if ((type == IO_RD || type == IO_WR) && pipe(pipefds) < 0) { + io->error = errno; + return FALSE; + } else if (type == IO_AP) { + va_start(args, argv); + pipefds[1] = va_arg(args, int); + va_end(args); + } + + if ((io->pid = fork())) { + if (io->pid == -1) + io->error = errno; + if (pipefds[!(type == IO_WR)] != -1) + close(pipefds[!(type == IO_WR)]); + if (io->pid != -1) { + io->pipe = pipefds[!!(type == IO_WR)]; + return TRUE; + } } else { - subcommand = NULL; + if (type != IO_FG) { + int devnull = open("/dev/null", O_RDWR); + int readfd = type == IO_WR ? pipefds[0] : devnull; + int writefd = (type == IO_RD || type == IO_AP) + ? pipefds[1] : devnull; + + dup2(readfd, STDIN_FILENO); + dup2(writefd, STDOUT_FILENO); + dup2(devnull, STDERR_FILENO); + + close(devnull); + if (pipefds[0] != -1) + close(pipefds[0]); + if (pipefds[1] != -1) + close(pipefds[1]); + } + + if (dir && *dir && chdir(dir) == -1) + exit(errno); + + execvp(argv[0], (char *const*) argv); + exit(errno); } - if (!subcommand) - /* XXX: This is vulnerable to the user overriding - * options required for the main view parser. */ - string_copy(opt_cmd, TIG_MAIN_BASE); - else - string_format(opt_cmd, "git %s", subcommand); + if (pipefds[!!(type == IO_WR)] != -1) + close(pipefds[!!(type == IO_WR)]); + return FALSE; +} - buf_size = strlen(opt_cmd); +static bool +io_complete(enum io_type type, const char **argv, const char *dir, int fd) +{ + struct io io; - for (i = 1 + !!subcommand; i < argc; i++) { - const char *opt = argv[i]; + return io_run(&io, type, dir, argv, fd) && io_done(&io); +} + +static bool +io_run_bg(const char **argv) +{ + return io_complete(IO_BG, argv, NULL, -1); +} + +static bool +io_run_fg(const char **argv, const char *dir) +{ + return io_complete(IO_FG, argv, dir, -1); +} + +static bool +io_run_append(const char **argv, int fd) +{ + return io_complete(IO_AP, argv, NULL, fd); +} + +static bool +io_eof(struct io *io) +{ + return io->eof; +} + +static int +io_error(struct io *io) +{ + return io->error; +} + +static char * +io_strerror(struct io *io) +{ + return strerror(io->error); +} + +static bool +io_can_read(struct io *io) +{ + struct timeval tv = { 0, 500 }; + fd_set fds; + + FD_ZERO(&fds); + FD_SET(io->pipe, &fds); + + return select(io->pipe + 1, &fds, NULL, NULL, &tv) > 0; +} + +static ssize_t +io_read(struct io *io, void *buf, size_t bufsize) +{ + do { + ssize_t readsize = read(io->pipe, buf, bufsize); + + if (readsize < 0 && (errno == EAGAIN || errno == EINTR)) + continue; + else if (readsize == -1) + io->error = errno; + else if (readsize == 0) + io->eof = 1; + return readsize; + } while (1); +} + +DEFINE_ALLOCATOR(io_realloc_buf, char, BUFSIZ) + +static char * +io_get(struct io *io, int c, bool can_read) +{ + char *eol; + ssize_t readsize; + + while (TRUE) { + if (io->bufsize > 0) { + eol = memchr(io->bufpos, c, io->bufsize); + if (eol) { + char *line = io->bufpos; + + *eol = 0; + io->bufpos = eol + 1; + io->bufsize -= io->bufpos - line; + return line; + } + } + + if (io_eof(io)) { + if (io->bufsize) { + io->bufpos[io->bufsize] = 0; + io->bufsize = 0; + return io->bufpos; + } + return NULL; + } + + if (!can_read) + return NULL; + + if (io->bufsize > 0 && io->bufpos > io->buf) + memmove(io->buf, io->bufpos, io->bufsize); + + if (io->bufalloc == io->bufsize) { + if (!io_realloc_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)) + return NULL; + io->bufsize += readsize; + } +} + +static bool +io_write(struct io *io, const void *buf, size_t bufsize) +{ + size_t written = 0; + + while (!io_error(io) && written < bufsize) { + ssize_t size; + + size = write(io->pipe, buf + written, bufsize - written); + if (size < 0 && (errno == EAGAIN || errno == EINTR)) + continue; + else if (size == -1) + io->error = errno; + else + written += size; + } + + return written == bufsize; +} + +static bool +io_read_buf(struct io *io, char buf[], size_t bufsize) +{ + char *result = io_get(io, '\n', TRUE); + + if (result) { + result = chomp_string(result); + string_ncopy_do(buf, bufsize, result, strlen(result)); + } + + return io_done(io) && result; +} + +static bool +io_run_buf(const char **argv, char buf[], size_t bufsize) +{ + struct io io; + + return io_run(&io, IO_RD, NULL, argv) && io_read_buf(&io, buf, bufsize); +} + +static int +io_load(struct io *io, const char *separators, + int (*read_property)(char *, size_t, char *, size_t)) +{ + char *name; + int state = OK; + + while (state == OK && (name = io_get(io, '\n', TRUE))) { + char *value; + size_t namelen; + size_t valuelen; + + name = chomp_string(name); + namelen = strcspn(name, separators); + + if (name[namelen]) { + name[namelen] = 0; + value = chomp_string(name + namelen + 1); + valuelen = strlen(value); + + } else { + value = ""; + valuelen = 0; + } + + state = read_property(name, namelen, value, valuelen); + } + + if (state != ERR && io_error(io)) + state = ERR; + io_done(io); + + return state; +} + +static int +io_run_load(const char **argv, const char *separators, + int (*read_property)(char *, size_t, char *, size_t)) +{ + struct io io; + + if (!io_run(&io, IO_RD, NULL, argv)) + return ERR; + return io_load(&io, separators, read_property); +} + + +/* + * User requests + */ + +#define REQ_INFO \ + /* XXX: Keep the view request first and in sync with views[]. */ \ + REQ_GROUP("View switching") \ + REQ_(VIEW_MAIN, "Show main view"), \ + REQ_(VIEW_DIFF, "Show diff view"), \ + REQ_(VIEW_LOG, "Show log view"), \ + 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"), \ + REQ_(VIEW_STAGE, "Show stage view"), \ + \ + REQ_GROUP("View manipulation") \ + REQ_(ENTER, "Enter current line and scroll"), \ + REQ_(NEXT, "Move to next"), \ + REQ_(PREVIOUS, "Move to previous"), \ + REQ_(PARENT, "Move to parent"), \ + REQ_(VIEW_NEXT, "Move focus to next view"), \ + REQ_(REFRESH, "Reload and refresh"), \ + REQ_(MAXIMIZE, "Maximize the current view"), \ + REQ_(VIEW_CLOSE, "Close the current view"), \ + REQ_(QUIT, "Close all views and quit"), \ + \ + REQ_GROUP("View specific requests") \ + REQ_(STATUS_UPDATE, "Update file status"), \ + REQ_(STATUS_REVERT, "Revert file changes"), \ + REQ_(STATUS_MERGE, "Merge file using external tool"), \ + REQ_(STAGE_NEXT, "Find next chunk to stage"), \ + \ + REQ_GROUP("Cursor navigation") \ + REQ_(MOVE_UP, "Move cursor one line up"), \ + REQ_(MOVE_DOWN, "Move cursor one line down"), \ + REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \ + REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \ + REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \ + 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"), \ + REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \ + \ + REQ_GROUP("Searching") \ + REQ_(SEARCH, "Search the view"), \ + REQ_(SEARCH_BACK, "Search backwards in the view"), \ + REQ_(FIND_NEXT, "Find next search match"), \ + 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"), \ + REQ_(SCREEN_REDRAW, "Redraw the screen"), \ + REQ_(SHOW_VERSION, "Show version information"), \ + REQ_(STOP_LOADING, "Stop all loading views"), \ + REQ_(EDIT, "Open in editor"), \ + REQ_(NONE, "Do nothing") + + +/* User action requests. */ +enum request { +#define REQ_GROUP(help) +#define REQ_(req, help) REQ_##req + + /* Offset all requests to avoid conflicts with ncurses getch values. */ + REQ_UNKNOWN = KEY_MAX + 1, + REQ_OFFSET, + REQ_INFO + +#undef REQ_GROUP +#undef REQ_ +}; + +struct request_info { + enum request request; + const char *name; + int namelen; + const char *help; +}; + +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 +#undef REQ_GROUP +#undef REQ_ +}; - if (seen_dashdash || !strcmp(opt, "--")) { - seen_dashdash = TRUE; +static enum request +get_request(const char *name) +{ + int namelen = strlen(name); + int i; - } else if (!strcmp(opt, "-v") || !strcmp(opt, "--version")) { - printf("tig version %s\n", TIG_VERSION); - return REQ_NONE; + for (i = 0; i < ARRAY_SIZE(req_info); i++) + if (enum_equals(req_info[i], name, namelen)) + return req_info[i].request; - } else if (!strcmp(opt, "-h") || !strcmp(opt, "--help")) { - printf("%s\n", usage); - return REQ_NONE; - } + return REQ_UNKNOWN; +} - opt_cmd[buf_size++] = ' '; - buf_size = sq_quote(opt_cmd, buf_size, opt); - if (buf_size >= sizeof(opt_cmd)) - die("command too long"); - } - opt_cmd[buf_size] = 0; +/* + * Options + */ - return request; -} +/* Option and state variables. */ +static enum date opt_date = DATE_DEFAULT; +static enum author opt_author = AUTHOR_DEFAULT; +static bool opt_line_number = FALSE; +static bool opt_line_graphics = TRUE; +static bool opt_rev_graph = FALSE; +static bool opt_show_refs = TRUE; +static int opt_num_interval = 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 = AUTHOR_COLS; +static char opt_path[SIZEOF_STR] = ""; +static char opt_file[SIZEOF_STR] = ""; +static char opt_ref[SIZEOF_REF] = ""; +static char opt_head[SIZEOF_REF] = ""; +static char opt_remote[SIZEOF_REF] = ""; +static char opt_encoding[20] = "UTF-8"; +static iconv_t opt_iconv_in = ICONV_NONE; +static iconv_t opt_iconv_out = ICONV_NONE; +static char opt_search[SIZEOF_STR] = ""; +static char opt_cdup[SIZEOF_STR] = ""; +static char opt_prefix[SIZEOF_STR] = ""; +static char opt_git_dir[SIZEOF_STR] = ""; +static signed char opt_is_inside_work_tree = -1; /* set to TRUE or FALSE */ +static char opt_editor[SIZEOF_STR] = ""; +static FILE *opt_tty = NULL; + +#define is_initial_commit() (!get_ref_head()) +#define is_head_commit(rev) (!strcmp((rev), "HEAD") || (get_ref_head() && !strcmp(rev, get_ref_head()->id))) /* @@ -611,19 +1236,21 @@ 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), \ +LINE(TESTED, " Tested-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \ +LINE(REVIEWED, " Reviewed-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \ LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \ 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), \ @@ -632,7 +1259,8 @@ LINE(MAIN_TRACKED, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \ LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, 0), \ LINE(MAIN_HEAD, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \ LINE(MAIN_REVGRAPH,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \ -LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \ +LINE(TREE_HEAD, "", COLOR_DEFAULT, COLOR_DEFAULT, A_BOLD), \ +LINE(TREE_DIR, "", COLOR_YELLOW, COLOR_DEFAULT, A_NORMAL), \ LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \ LINE(STAT_HEAD, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \ LINE(STAT_SECTION, "", COLOR_CYAN, COLOR_DEFAULT, 0), \ @@ -640,6 +1268,8 @@ LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \ LINE(STAT_STAGED, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \ LINE(STAT_UNSTAGED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \ LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \ +LINE(HELP_KEYMAP, "", COLOR_CYAN, COLOR_DEFAULT, 0), \ +LINE(HELP_GROUP, "", COLOR_BLUE, COLOR_DEFAULT, 0), \ LINE(BLAME_ID, "", COLOR_MAGENTA, COLOR_DEFAULT, 0) enum line_type { @@ -694,8 +1324,7 @@ get_line_info(const char *name) enum line_type type; for (type = 0; type < ARRAY_SIZE(line_info); type++) - if (namelen == line_info[type].namelen && - !string_enum_compare(line_info[type].name, name, namelen)) + if (enum_equals(line_info[type], name, namelen)) return &line_info[type]; return NULL; @@ -730,6 +1359,8 @@ struct line { /* State flags */ unsigned int selected:1; unsigned int dirty:1; + unsigned int cleareol:1; + unsigned int other:16; void *data; /* User data */ }; @@ -752,6 +1383,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 }, @@ -779,6 +1411,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 }, @@ -795,21 +1429,21 @@ 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 }, { '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 }, { 'M', REQ_STATUS_MERGE }, { '@', REQ_STAGE_NEXT }, - { ',', REQ_TREE_PARENT }, + { ',', REQ_PARENT }, { 'e', REQ_EDIT }, - - /* Using the ncurses SIGWINCH handler. */ - { KEY_RESIZE, REQ_SCREEN_RESIZE }, }; #define KEYMAP_INFO \ @@ -820,6 +1454,7 @@ static struct keybinding default_keybindings[] = { KEYMAP_(TREE), \ KEYMAP_(BLOB), \ KEYMAP_(BLAME), \ + KEYMAP_(BRANCH), \ KEYMAP_(PAGER), \ KEYMAP_(HELP), \ KEYMAP_(STATUS), \ @@ -831,14 +1466,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; @@ -851,12 +1485,28 @@ static void add_keybinding(enum keymap keymap, enum request request, int key) { struct keybinding_table *table = &keybindings[keymap]; + size_t i; + + for (i = 0; i < keybindings[keymap].size; i++) { + if (keybindings[keymap].data[i].alias == key) { + keybindings[keymap].data[i].request = request; + return; + } + } table->data = realloc(table->data, (table->size + 1) * sizeof(*table->data)); if (!table->data) die("Failed to allocate keybinding"); table->data[table->size].alias = key; table->data[table->size++].request = request; + + if (request == REQ_NONE && keymap == KEYMAP_GENERIC) { + int i; + + for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) + if (default_keybindings[i].alias == key) + default_keybindings[i].request = REQ_NONE; + } } /* Looks for a key binding first in the given map, then in the generic map, and @@ -887,7 +1537,7 @@ struct key { int value; }; -static struct key key_table[] = { +static const struct key key_table[] = { { "Enter", KEY_RETURN }, { "Space", ' ' }, { "Backspace", KEY_BACKSPACE }, @@ -954,26 +1604,68 @@ get_key_name(int key_value) return seq ? seq : "(no key)"; } +static bool +append_key(char *buf, size_t *pos, const struct keybinding *keybinding) +{ + const char *sep = *pos > 0 ? ", " : ""; + const char *keyname = get_key_name(keybinding->alias); + + return string_nformat(buf, BUFSIZ, pos, "%s%s", sep, keyname); +} + +static bool +append_keymap_request_keys(char *buf, size_t *pos, enum request request, + enum keymap keymap, bool all) +{ + int i; + + for (i = 0; i < keybindings[keymap].size; i++) { + if (keybindings[keymap].data[i].request == request) { + if (!append_key(buf, pos, &keybindings[keymap].data[i])) + return FALSE; + if (!all) + break; + } + } + + return TRUE; +} + +#define get_key(keymap, request) get_keys(keymap, request, FALSE) + static const char * -get_key(enum request request) +get_keys(enum keymap keymap, enum request request, bool all) { static char buf[BUFSIZ]; size_t pos = 0; - char *sep = ""; int i; buf[pos] = 0; - for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) { - struct keybinding *keybinding = &default_keybindings[i]; + if (!append_keymap_request_keys(buf, &pos, request, keymap, all)) + return "Too many keybindings!"; + if (pos > 0 && !all) + return buf; - if (keybinding->request != request) - continue; + if (keymap != KEYMAP_GENERIC) { + /* Only the generic keymap includes the default keybindings when + * listing all keys. */ + if (all) + return buf; - if (!string_format_from(buf, &pos, "%s%s", sep, - get_key_name(keybinding->alias))) + if (!append_keymap_request_keys(buf, &pos, request, KEYMAP_GENERIC, all)) return "Too many keybindings!"; - sep = ", "; + if (pos) + return buf; + } + + for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) { + if (default_keybindings[i].request == request) { + if (!append_key(buf, &pos, &default_keybindings[i])) + return "Too many keybindings!"; + if (!all) + return buf; + } } return buf; @@ -982,34 +1674,34 @@ get_key(enum request request) struct run_request { enum keymap keymap; int key; - char cmd[SIZEOF_STR]; + const char *argv[SIZEOF_ARG]; }; 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) { struct run_request *req; - char cmd[SIZEOF_STR]; - size_t bufpos; - for (bufpos = 0; argc > 0; argc--, argv++) - if (!string_format_from(cmd, &bufpos, "%s ", *argv)) - return REQ_NONE; + 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++]; - string_copy(req->cmd, cmd); + req = &run_request[run_requests]; req->keymap = keymap; req->key = key; + req->argv[0] = NULL; + + if (!argv_copy(req->argv, argv)) + return REQ_NONE; - return REQ_NONE + run_requests; + return REQ_NONE + ++run_requests; } static struct run_request * @@ -1023,20 +1715,29 @@ get_run_request(enum request request) static void add_builtin_run_requests(void) { + const char *cherry_pick[] = { "git", "cherry-pick", "%(commit)", NULL }; + const char *checkout[] = { "git", "checkout", "%(branch)", NULL }; + const char *commit[] = { "git", "commit", NULL }; + const char *gc[] = { "git", "gc", NULL }; struct { enum keymap keymap; int key; - const char *argv[1]; + int argc; + const char **argv; } reqs[] = { - { KEYMAP_MAIN, 'C', { "git cherry-pick %(commit)" } }, - { KEYMAP_GENERIC, 'G', { "git gc" } }, + { KEYMAP_MAIN, 'C', ARRAY_SIZE(cherry_pick) - 1, cherry_pick }, + { KEYMAP_STATUS, 'C', ARRAY_SIZE(commit) - 1, commit }, + { KEYMAP_BRANCH, 'C', ARRAY_SIZE(checkout) - 1, checkout }, + { KEYMAP_GENERIC, 'G', ARRAY_SIZE(gc) - 1, gc }, }; int i; for (i = 0; i < ARRAY_SIZE(reqs); i++) { - enum request req; + enum request req = get_keybinding(reqs[i].keymap, reqs[i].key); - req = add_run_request(reqs[i].keymap, reqs[i].key, 1, reqs[i].argv); + if (req != reqs[i].key) + continue; + req = add_run_request(reqs[i].keymap, reqs[i].key, reqs[i].argc, reqs[i].argv); if (req != REQ_NONE) add_keybinding(reqs[i].keymap, req, reqs[i].key); } @@ -1046,8 +1747,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), @@ -1059,11 +1764,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), @@ -1073,64 +1775,144 @@ 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"); - - } else { + 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; + + 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) +{ + *opt = (!strcmp(arg, "1") || !strcmp(arg, "true") || !strcmp(arg, "yes")) + ? TRUE : FALSE; + return OK; +} + +static int parse_enum_do(unsigned int *opt, const char *arg, + const struct enum_map *map, size_t map_size) { - return (!strcmp(s, "1") || !strcmp(s, "true") || - !strcmp(s, "yes")) ? TRUE : FALSE; + bool is_true; + + assert(map_size > 1); + + if (map_enum_do(map, map_size, (int *) opt, arg)) + return OK; + + if (parse_bool(&is_true, arg) != OK) + return ERR; + + *opt = is_true ? map[1].value : map[0].value; + return OK; } +#define parse_enum(opt, arg, map) \ + parse_enum_do(opt, arg, map, ARRAY_SIZE(map)) + static int -parse_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 */ @@ -1147,68 +1929,41 @@ 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_enum(&opt_author, argv[2], author_map); - if (!strcmp(argv[0], "show-date")) { - opt_date = parse_bool(argv[2]); - return OK; - } + if (!strcmp(argv[0], "show-date")) + return parse_enum(&opt_date, argv[2], date_map); - if (!strcmp(argv[0], "show-rev-graph")) { - opt_rev_graph = 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-refs")) { - opt_show_refs = parse_bool(argv[2]); - return OK; - } + if (!strcmp(argv[0], "show-refs")) + return parse_bool(&opt_show_refs, argv[2]); - if (!strcmp(argv[0], "show-line-numbers")) { - opt_line_number = 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-graphics")) { - opt_line_graphics = parse_bool(argv[2]); - return OK; - } + if (!strcmp(argv[0], "line-graphics")) + return parse_bool(&opt_line_graphics, 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-number-interval")) + return parse_int(&opt_num_interval, argv[2], 1, 1024); - if (!strcmp(argv[0], "author-width")) { - opt_author_cols = parse_int(argv[2], opt_author_cols, 0, 1024); - return OK; - } + if (!strcmp(argv[0], "author-width")) + return parse_int(&opt_author_cols, argv[2], 0, 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], "horizontal-scroll")) + return parse_step(&opt_hscroll, argv[2]); - if (!strcmp(argv[0], "commit-encoding")) { - const char *arg = argv[2]; - int arglen = strlen(arg); + if (!strcmp(argv[0], "split-view-height")) + return parse_step(&opt_scale_split_view, 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], "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; @@ -1219,7 +1974,7 @@ static int option_bind_command(int argc, const char *argv[]) { enum request request; - int keymap; + int keymap = -1; int key; if (argc < 3) { @@ -1227,7 +1982,7 @@ option_bind_command(int argc, const char *argv[]) return ERR; } - if (set_keymap(&keymap, argv[0]) == ERR) { + if (!set_keymap(&keymap, argv[0])) { config_msg = "Unknown key map"; return ERR; } @@ -1239,22 +1994,24 @@ option_bind_command(int argc, const char *argv[]) } request = get_request(argv[2]); - if (request == REQ_NONE) { - const char *obsolete[] = { "cherry-pick" }; - size_t namelen = strlen(argv[2]); - int i; - - for (i = 0; i < ARRAY_SIZE(obsolete); i++) { - if (namelen == strlen(obsolete[i]) && - !string_enum_compare(obsolete[i], argv[2], namelen)) { - config_msg = "Obsolete request name"; - return ERR; - } + if (request == REQ_UNKNOWN) { + static const struct enum_map obsolete[] = { + ENUM_MAP("cherry-pick", REQ_NONE), + ENUM_MAP("screen-resize", REQ_NONE), + ENUM_MAP("tree-parent", REQ_PARENT), + }; + int alias; + + if (map_enum(&alias, obsolete, argv[2])) { + if (alias != REQ_NONE) + add_keybinding(keymap, alias, key); + config_msg = "Obsolete request name"; + return ERR; } } - if (request == REQ_NONE && *argv[2]++ == '!') + if (request == REQ_UNKNOWN && *argv[2]++ == '!') request = add_run_request(keymap, key, argc - 2, argv + 2); - if (request == REQ_NONE) { + if (request == REQ_UNKNOWN) { config_msg = "Unknown request name"; return ERR; } @@ -1268,21 +2025,11 @@ static int set_option(const char *opt, char *value) { const char *argv[SIZEOF_ARG]; - int valuelen; int argc = 0; - /* Tokenize */ - while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) { - argv[argc++] = value; - value += valuelen; - - /* Nothing more to tokenize or last available token. */ - if (!*value || argc >= ARRAY_SIZE(argv)) - break; - - *value++ = 0; - while (isspace(*value)) - value++; + if (!argv_from_string(argv, &argc, value)) { + config_msg = "Too many option arguments"; + return ERR; } if (!strcmp(opt, "color")) @@ -1329,8 +2076,8 @@ read_option(char *opt, size_t optlen, char *value, size_t valuelen) } if (status == ERR) { - fprintf(stderr, "Error on line %d, near '%.*s': %s\n", - config_lineno, (int) optlen, opt, config_msg); + warn("Error on line %d, near '%.*s': %s", + config_lineno, (int) optlen, opt, config_msg); config_errors = TRUE; } @@ -1341,19 +2088,18 @@ read_option(char *opt, size_t optlen, char *value, size_t valuelen) static void load_option_file(const char *path) { - FILE *file; + struct io io; - /* It's ok that the file doesn't exist. */ - file = fopen(path, "r"); - if (!file) + /* It's OK that the file doesn't exist. */ + if (!io_open(&io, "%s", path)) return; config_lineno = 0; config_errors = FALSE; - if (read_properties(file, " \t", read_option) == ERR || + if (io_load(&io, " \t", read_option) == ERR || config_errors == TRUE) - fprintf(stderr, "Errors while loading %s.\n", path); + warn("Errors while loading %s.", path); } static int @@ -1364,13 +2110,8 @@ load_options(void) const char *tigrc_system = getenv("TIGRC_SYSTEM"); char buf[SIZEOF_STR]; - 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) { @@ -1380,6 +2121,10 @@ load_options(void) } load_option_file(tigrc_user); + /* Add _after_ loading config files to avoid adding run requests + * that conflict with keybindings. */ + add_builtin_run_requests(); + return OK; } @@ -1395,9 +2140,6 @@ struct view_ops; static struct view *display[2]; static unsigned int current_view; -/* Reading from the prompt? */ -static bool input_mode = FALSE; - #define foreach_displayed_view(view, i) \ for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++) @@ -1407,10 +2149,25 @@ static bool input_mode = FALSE; static char ref_blob[SIZEOF_REF] = ""; static char ref_commit[SIZEOF_REF] = "HEAD"; static char ref_head[SIZEOF_REF] = "HEAD"; +static char ref_branch[SIZEOF_REF] = ""; + +enum view_type { + VIEW_MAIN, + VIEW_DIFF, + VIEW_LOG, + VIEW_TREE, + VIEW_BLOB, + VIEW_BLAME, + VIEW_BRANCH, + VIEW_HELP, + VIEW_PAGER, + VIEW_STATUS, + VIEW_STAGE, +}; struct view { + enum view_type type; /* View type */ const char *name; /* View name */ - const char *cmd_fmt; /* Default command line format */ const char *cmd_env; /* Command line set via environment */ const char *id; /* Points to either of ref_{head,commit,blob} */ @@ -1419,7 +2176,6 @@ struct view { enum keymap keymap; /* What keymap does this view have */ bool git_dir; /* Whether the view requires a git directory. */ - char cmd[SIZEOF_STR]; /* Command buffer */ char ref[SIZEOF_REF]; /* Hovered commit reference */ char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */ @@ -1429,36 +2185,47 @@ 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. */ struct view *parent; + struct view *prev; /* Buffering */ size_t lines; /* Total number of lines */ struct line *line; /* Line index */ - size_t line_alloc; /* Total number of allocated lines */ - size_t line_size; /* Total number of used lines */ unsigned int digits; /* Number of digits in the lines member. */ /* Drawing */ struct line *curline; /* Line currently being drawn. */ enum line_type curtype; /* Attribute currently used for drawing. */ unsigned long col; /* Column when drawing. */ + bool has_scrolled; /* View was scrolled. */ /* Loading */ - FILE *pipe; + const char *argv[SIZEOF_ARG]; /* Shell command arguments. */ + const char *dir; /* Directory from which to execute. */ + struct io io; + struct io *pipe; time_t start_time; + time_t update_secs; }; struct view_ops { /* What type of content being displayed. Used in the title bar. */ const char *type; + /* Default command arguments. */ + const char **argv; /* Open and reads in all view content. */ bool (*open)(struct view *view); /* Read one line; updates view->line. */ @@ -1467,14 +2234,17 @@ 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); + /* Prepare view for loading */ + bool (*prepare)(struct view *view); }; static struct view_ops blame_ops; static struct view_ops blob_ops; +static struct view_ops diff_ops; static struct view_ops help_ops; static struct view_ops log_ops; static struct view_ops main_ops; @@ -1482,21 +2252,22 @@ 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, cmd, env, ref, ops, map, git) \ - { name, cmd, #env, ref, ops, map, git } +#define VIEW_STR(type, name, env, ref, ops, map, git) \ + { type, name, #env, ref, ops, map, git } #define VIEW_(id, name, ops, git, ref) \ - VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id, git) - + VIEW_STR(VIEW_##id, name, TIG_##id##_CMD, ref, ops, KEYMAP_##id, git) static struct view views[] = { VIEW_(MAIN, "main", &main_ops, TRUE, ref_head), - VIEW_(DIFF, "diff", &pager_ops, TRUE, ref_commit), + VIEW_(DIFF, "diff", &diff_ops, TRUE, ref_commit), VIEW_(LOG, "log", &log_ops, TRUE, ref_head), 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, ""), @@ -1504,7 +2275,6 @@ static struct view views[] = { }; #define VIEW(req) (&views[(req) - REQ_OFFSET - 1]) -#define VIEW_REQ(view) ((view) - views + REQ_OFFSET + 1) #define foreach_view(view, i) \ for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++) @@ -1512,20 +2282,24 @@ static struct view views[] = { #define view_is_displayed(view) \ (view == display[0] || view == display[1]) +static enum request +view_request(struct view *view, enum request request) +{ + if (!view || !view->lines) + return request; + return view->ops->request(view, request, &view->line[view->lineno]); +} -enum line_graphic { - LINE_GRAPHIC_VLINE -}; -static int line_graphics[] = { - /* LINE_GRAPHIC_VLINE: */ '|' -}; +/* + * View drawing. + */ static inline void set_view_attr(struct view *view, enum line_type type) { if (!view->curline->selected && view->curtype != type) { - wattrset(view->win, get_line_attr(type)); + (void) wattrset(view->win, get_line_attr(type)); wchgat(view->win, -1, 0, type, NULL); view->curtype = type; } @@ -1535,28 +2309,37 @@ static int draw_chars(struct view *view, enum line_type type, const char *string, int max_len, bool use_tilde) { + static char out_buffer[BUFSIZ * 2]; int len = 0; int col = 0; int trimmed = FALSE; + 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); - } else { - col = len = strlen(string); - if (len > max_len) { - if (use_tilde) { - max_len -= 1; + len = utf8_length(&string, skip, &col, max_len, &trimmed, use_tilde, opt_tab_size); + + set_view_attr(view, type); + if (len > 0) { + if (opt_iconv_out != ICONV_NONE) { + ICONV_CONST char *inbuf = (ICONV_CONST char *) string; + size_t inlen = len + 1; + + char *outbuf = out_buffer; + size_t outlen = sizeof(out_buffer); + + size_t ret; + + ret = iconv(opt_iconv_out, &inbuf, &inlen, &outbuf, &outlen); + if (ret != (size_t) -1) { + string = out_buffer; + len = sizeof(out_buffer) - outlen; } - col = len = max_len; - trimmed = TRUE; } - } - set_view_attr(view, type); - waddnstr(view->win, string, len); + waddnstr(view->win, string, len); + } if (trimmed && use_tilde) { set_view_attr(view, LINE_DELIMITER); waddch(view->win, '~'); @@ -1577,63 +2360,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) @@ -1642,22 +2387,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) @@ -1665,22 +2409,75 @@ 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 time *time) +{ + const char *date = mkdate(time, opt_date); + int cols = opt_date == DATE_SHORT ? DATE_SHORT_COLS : DATE_COLS; + + return draw_field(view, LINE_DATE, date, cols, FALSE); +} + +static bool +draw_author(struct view *view, const char *author) +{ + bool trim = opt_author_cols == 0 || opt_author_cols > 5; + bool abbreviate = opt_author == AUTHOR_ABBREVIATED || !trim; + + if (abbreviate && author) + author = get_author_initials(author); + + 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_date(struct view *view, struct tm *time) +draw_lineno(struct view *view, unsigned int lineno) { - char buf[DATE_COLS]; - char *date; - int timelen = 0; + char number[10]; + int digits3 = view->digits < 3 ? 3 : view->digits; + int max = MIN(view->width + view->yoffset - view->col, digits3); + char *text = NULL; + chtype separator = opt_line_graphics ? ACS_VLINE : '|'; - if (time) - timelen = strftime(buf, sizeof(buf), DATE_FORMAT, time); - date = timelen ? buf : NULL; + lineno += view->offset + 1; + if (lineno == 1 || (lineno % opt_num_interval) == 0) { + static char fmt[] = "%1ld"; - return draw_field(view, LINE_DATE, date, DATE_COLS, FALSE); + 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, &separator, 1); } static bool @@ -1688,7 +2485,6 @@ draw_view_line(struct view *view, unsigned int lineno) { struct line *line; bool selected = (view->offset + lineno == view->lineno); - bool draw_ok; assert(view_is_displayed(view)); @@ -1698,24 +2494,21 @@ draw_view_line(struct view *view, unsigned int lineno) line = &view->line[view->offset + lineno]; wmove(view->win, lineno, 0); + if (line->cleareol) + wclrtoeol(view->win); view->col = 0; view->curline = line; view->curtype = LINE_NONE; line->selected = FALSE; + line->dirty = line->cleareol = 0; if (selected) { set_view_attr(view, LINE_CURSOR); line->selected = TRUE; view->ops->select(view, line); - } else if (line->selected) { - wclrtoeol(view->win); } - scrollok(view->win, FALSE); - draw_ok = view->ops->draw(view, line, lineno); - scrollok(view->win, TRUE); - - return draw_ok; + return view->ops->draw(view, line, lineno); } static void @@ -1725,11 +2518,10 @@ redraw_view_dirty(struct view *view) int lineno; for (lineno = 0; lineno < view->height; lineno++) { - struct line *line = &view->line[view->offset + lineno]; - - if (!line->dirty) + if (view->offset + lineno >= view->lines) + break; + if (!view->line[view->offset + lineno].dirty) continue; - line->dirty = 0; dirty = TRUE; if (!draw_view_line(view, lineno)) break; @@ -1737,11 +2529,7 @@ redraw_view_dirty(struct view *view) if (!dirty) return; - redrawwin(view->win); - if (input_mode) - wnoutrefresh(view->win); - else - wrefresh(view->win); + wnoutrefresh(view->win); } static void @@ -1754,17 +2542,13 @@ redraw_view_from(struct view *view, int lineno) break; } - redrawwin(view->win); - if (input_mode) - wnoutrefresh(view->win); - else - wrefresh(view->win); + wnoutrefresh(view->win); } static void redraw_view(struct view *view) { - wclear(view->win); + werase(view->win); redraw_view_from(view, 0); } @@ -1778,25 +2562,26 @@ update_view_title(struct view *view) assert(view_is_displayed(view)); - if (view != VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) { + if (view->type != VIEW_STATUS && view->lines) { unsigned int view_lines = view->offset + view->height; unsigned int lines = view->lines ? MIN(view_lines, view->lines) * 100 / view->lines : 0; - string_format_from(state, &statelen, "- %s %d of %d (%d%%)", + string_format_from(state, &statelen, " - %s %d of %d (%d%%)", view->ops->type, view->lineno + 1, view->lines, lines); - if (view->pipe) { - time_t secs = time(NULL) - view->start_time; + } - /* Three git seconds are a long time ... */ - if (secs > 2) - string_format_from(state, &statelen, " %lds", secs); - } + if (view->pipe) { + time_t secs = time(NULL) - view->start_time; + + /* Three git seconds are a long time ... */ + if (secs > 2) + string_format_from(state, &statelen, " loading %lds", secs); } string_format_from(buf, &bufpos, "[%s]", view->name); @@ -1810,7 +2595,7 @@ update_view_title(struct view *view) } if (statelen && bufpos < view->width) { - string_format_from(buf, &bufpos, " %s", state); + string_format_from(buf, &bufpos, "%s", state); } if (view == display[current_view]) @@ -1820,12 +2605,16 @@ update_view_title(struct view *view) mvwaddnstr(view->title, 0, 0, buf, bufpos); wclrtoeol(view->title); - wmove(view->title, 0, view->width - 1); + wnoutrefresh(view->title); +} - if (input_mode) - wnoutrefresh(view->title); - else - wrefresh(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 @@ -1845,7 +2634,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. */ @@ -1863,7 +2654,7 @@ resize_display(void) if (!view->win) die("Failed to create %s view", view->name); - scrollok(view->win, TRUE); + scrollok(view->win, FALSE); view->title = newwin(1, 0, offset + view->height, 0); if (!view->title) @@ -1880,34 +2671,110 @@ resize_display(void) } static void -redraw_display(void) +redraw_display(bool clear) { struct view *view; int i; foreach_displayed_view (view, i) { + if (clear) + wclear(view->win); redraw_view(view); update_view_title(view); } } + +/* + * Option management + */ + +static void +toggle_enum_option_do(unsigned int *opt, const char *help, + const struct enum_map *map, size_t size) +{ + *opt = (*opt + 1) % size; + redraw_display(FALSE); + report("Displaying %s %s", enum_name(map[*opt]), help); +} + +#define toggle_enum_option(opt, help, map) \ + toggle_enum_option_do(opt, help, map, ARRAY_SIZE(map)) + +#define toggle_date() toggle_enum_option(&opt_date, "dates", date_map) +#define toggle_author() toggle_enum_option(&opt_author, "author names", author_map) + static void -update_display_cursor(struct view *view) +toggle_view_option(bool *option, const char *help) { - /* Move the cursor to the right-most column of the cursor line. - * - * XXX: This could turn out to be a bit expensive, but it ensures that - * the cursor does not jump around. */ - if (view->lines) { - wmove(view->win, view->lineno - view->offset, view->width - 1); - wrefresh(view->win); + *option = !*option; + redraw_display(FALSE); + report("%sabling %s", *option ? "En" : "Dis", help); +} + +static void +open_option_menu(void) +{ + const struct menu_item menu[] = { + { '.', "line numbers", &opt_line_number }, + { 'D', "date display", &opt_date }, + { 'A', "author display", &opt_author }, + { 'g', "revision graph display", &opt_rev_graph }, + { 'F', "reference display", &opt_show_refs }, + { 0 } + }; + int selected = 0; + + if (prompt_menu("Toggle option", menu, &selected)) { + if (menu[selected].data == &opt_date) + toggle_date(); + else if (menu[selected].data == &opt_author) + toggle_author(); + else + toggle_view_option(menu[selected].data, menu[selected].text); } } +static void +maximize_view(struct view *view) +{ + 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) @@ -1939,19 +2806,19 @@ do_scroll_view(struct view *view, int lines) int line = lines > 0 ? view->height - lines : 0; int end = line + ABS(lines); + scrollok(view->win, TRUE); wscrl(view->win, lines); + scrollok(view->win, FALSE); - for (; line < end; line++) { - if (!draw_view_line(view, line)) - break; - } + while (line < end && draw_view_line(view, line)) + line++; if (redraw_current_line) draw_view_line(view, view->lineno - view->offset); + wnoutrefresh(view->win); } - redrawwin(view->win); - wrefresh(view->win); + view->has_scrolled = TRUE; report(""); } @@ -1964,6 +2831,23 @@ scroll_view(struct view *view, enum request request) assert(view_is_displayed(view)); switch (request) { + case REQ_SCROLL_LEFT: + if (view->yoffset == 0) { + report("Cannot scroll beyond the first column"); + return; + } + if (view->yoffset <= 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: @@ -2084,8 +2968,7 @@ move_view(struct view *view, enum request request) /* Draw the current line */ draw_view_line(view, view->lineno - view->offset); - redrawwin(view->win); - wrefresh(view->win); + wnoutrefresh(view->win); report(""); } @@ -2097,31 +2980,37 @@ move_view(struct view *view, enum request request) static void search_view(struct view *view, enum request request); static bool -find_next_line(struct view *view, unsigned long lineno, struct line *line) +grep_text(struct view *view, const char *text[]) { - assert(view_is_displayed(view)); - - if (!view->ops->grep(view, line)) - return FALSE; - - if (lineno - view->offset >= view->height) { - view->offset = lineno; - view->lineno = lineno; - redraw_view(view); + regmatch_t pmatch; + size_t i; - } else { - unsigned long old_lineno = view->lineno - view->offset; + for (i = 0; text[i]; i++) + if (*text[i] && + regexec(view->regex, text[i], 1, &pmatch, 0) != REG_NOMATCH) + return TRUE; + return FALSE; +} - view->lineno = lineno; - draw_view_line(view, old_lineno); +static void +select_view_line(struct view *view, unsigned long lineno) +{ + unsigned long old_lineno = view->lineno; + unsigned long old_offset = view->offset; - draw_view_line(view, view->lineno - view->offset); - redrawwin(view->win); - wrefresh(view->win); + if (goto_view_line(view, view->offset, lineno)) { + if (view_is_displayed(view)) { + 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]); + } } - - report("Line %ld matches '%s'", lineno + 1, view->grep); - return TRUE; } static void @@ -2159,10 +3048,11 @@ find_next(struct view *view, enum request request) /* Note, lineno is unsigned long so will wrap around in which case it * will become bigger than view->lines. */ for (; lineno < view->lines; lineno += direction) { - struct line *line = &view->line[lineno]; - - if (find_next_line(view, lineno, line)) + if (view->ops->grep(view, &view->line[lineno])) { + select_view_line(view, lineno); + report("Line %ld matches '%s'", lineno + 1, view->grep); return; + } } report("No match found for '%s'", view->grep); @@ -2209,13 +3099,114 @@ reset_view(struct view *view) free(view->line[i].data); 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_size = 0; - view->line_alloc = 0; view->vid[0] = 0; + view->update_secs = 0; +} + +static const char * +format_arg(const char *name) +{ + static struct { + const char *name; + size_t namelen; + const char *value; + const char *value_if_empty; + } vars[] = { +#define FORMAT_VAR(name, value, value_if_empty) \ + { name, STRING_SIZE(name), value, value_if_empty } + FORMAT_VAR("%(directory)", opt_path, ""), + FORMAT_VAR("%(file)", opt_file, ""), + FORMAT_VAR("%(ref)", opt_ref, "HEAD"), + FORMAT_VAR("%(head)", ref_head, ""), + FORMAT_VAR("%(commit)", ref_commit, ""), + FORMAT_VAR("%(blob)", ref_blob, ""), + FORMAT_VAR("%(branch)", ref_branch, ""), + }; + int i; + + for (i = 0; i < ARRAY_SIZE(vars); i++) + if (!strncmp(name, vars[i].name, vars[i].namelen)) + return *vars[i].value ? vars[i].value : vars[i].value_if_empty; + + report("Unknown replacement: `%s`", name); + return NULL; +} + +static bool +format_argv(const char *dst_argv[], const char *src_argv[], bool replace) +{ + char buf[SIZEOF_STR]; + int argc; + + argv_free(dst_argv); + + for (argc = 0; src_argv[argc]; argc++) { + const char *arg = src_argv[argc]; + size_t bufpos = 0; + + while (arg) { + char *next = strstr(arg, "%("); + int len = next - arg; + const char *value; + + if (!next || !replace) { + len = strlen(arg); + value = ""; + + } else { + value = format_arg(next); + + if (!value) { + return FALSE; + } + } + + if (!string_format_from(buf, &bufpos, "%.*s%s", len, arg, value)) + return FALSE; + + arg = next && replace ? strchr(next, ')') + 1 : NULL; + } + + dst_argv[argc] = strdup(buf); + if (!dst_argv[argc]) + break; + } + + dst_argv[argc] = NULL; + + return src_argv[argc] == NULL; +} + +static bool +restore_view_position(struct view *view) +{ + if (!view->p_restore || (view->pipe && view->lines <= view->p_lineno)) + return FALSE; + + /* Changing the view position cancels the restoring. */ + /* FIXME: Changing back to the first line is not detected. */ + if (view->offset != 0 || view->lineno != 0) { + view->p_restore = FALSE; + return FALSE; + } + + if (goto_view_line(view, view->p_offset, view->p_lineno) && + view_is_displayed(view)) + werase(view->win); + + view->yoffset = view->p_yoffset; + view->p_restore = FALSE; + + return TRUE; } static void @@ -2226,46 +3217,67 @@ end_update(struct view *view, bool force) while (!view->ops->read(view, NULL)) if (!force) return; - set_nonblocking_input(FALSE); - if (view->pipe == stdin) - fclose(view->pipe); - else - pclose(view->pipe); + if (force) + io_kill(view->pipe); + io_done(view->pipe); view->pipe = NULL; } +static void +setup_update(struct view *view, const char *vid) +{ + reset_view(view); + string_copy_rev(view->vid, vid); + view->pipe = &view->io; + view->start_time = time(NULL); +} + static bool -begin_update(struct view *view, bool refresh) +prepare_io(struct view *view, const char *dir, const char *argv[], bool replace) { - if (opt_cmd[0]) { - string_copy(view->cmd, opt_cmd); - opt_cmd[0] = 0; - /* When running random commands, initially show the - * command in the title. However, it maybe later be - * overwritten if a commit line is selected. */ - if (view == VIEW(REQ_VIEW_PAGER)) - string_copy(view->ref, view->cmd); - else - view->ref[0] = 0; + view->dir = dir; + return format_argv(view->argv, argv, replace); +} - } else if (view == VIEW(REQ_VIEW_TREE)) { - const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt; - char path[SIZEOF_STR]; +static bool +prepare_update(struct view *view, const char *argv[], const char *dir) +{ + if (view->pipe) + end_update(view, TRUE); + return prepare_io(view, dir, argv, FALSE); +} - if (strcmp(view->vid, view->id)) - opt_path[0] = path[0] = 0; - else if (sq_quote(path, 0, opt_path) >= sizeof(path)) - return FALSE; +static bool +start_update(struct view *view, const char **argv, const char *dir) +{ + if (view->pipe) + io_done(view->pipe); + return prepare_io(view, dir, argv, FALSE) && + io_run(&view->io, IO_RD, dir, view->argv); +} - if (!string_format(view->cmd, format, view->id, path)) - return FALSE; +static bool +prepare_update_file(struct view *view, const char *name) +{ + if (view->pipe) + end_update(view, TRUE); + argv_free(view->argv); + return io_open(&view->io, "%s/%s", opt_cdup[0] ? opt_cdup : ".", name); +} - } else if (!refresh) { - const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt; - const char *id = view->id; +static bool +begin_update(struct view *view, bool refresh) +{ + if (view->pipe) + end_update(view, TRUE); - if (!string_format(view->cmd, format, id, id, id, id, id)) + if (!refresh) { + if (view->ops->prepare) { + if (!view->ops->prepare(view)) + return FALSE; + } else if (!prepare_io(view, NULL, view->ops->argv, TRUE)) { return FALSE; + } /* Put the current ref_* value to the view title ref * member. This is needed by the blob view. Most other @@ -2274,179 +3286,120 @@ begin_update(struct view *view, bool refresh) string_copy_rev(view->ref, view->id); } - /* Special case for the pager view. */ - if (opt_pipe) { - view->pipe = opt_pipe; - opt_pipe = NULL; - } else { - view->pipe = popen(view->cmd, "r"); - } - - if (!view->pipe) + if (view->argv[0] && !io_run(&view->io, IO_RD, view->dir, view->argv)) return FALSE; - set_nonblocking_input(TRUE); - reset_view(view); - string_copy_rev(view->vid, view->id); - - view->start_time = time(NULL); + setup_update(view, view->id); 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; - view->line_size = line_size; - return view->line; -} - static bool update_view(struct view *view) { - char in_buffer[BUFSIZ]; char out_buffer[BUFSIZ * 2]; char *line; - /* The number of lines to read. If too low it will cause too much - * redrawing (and possible flickering), if too high responsiveness - * will suffer. */ - unsigned long lines = view->height; - int redraw_from = -1; + /* Clear the view and redraw everything since the tree sorting + * might have rearranged things. */ + bool redraw = view->lines == 0; + bool can_read = TRUE; if (!view->pipe) return TRUE; - /* Only redraw if lines are visible. */ - if (view->offset + view->height >= view->lines) - redraw_from = view->lines - view->offset; - - /* FIXME: This is probably not perfect for backgrounded views. */ - if (!realloc_lines(view, view->lines + lines)) - goto alloc_error; - - while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) { - size_t linelen = strlen(line); + if (!io_can_read(view->pipe)) { + if (view->lines == 0 && view_is_displayed(view)) { + time_t secs = time(NULL) - view->start_time; - if (linelen) - line[linelen - 1] = 0; + if (secs > 1 && secs > view->update_secs) { + if (view->update_secs == 0) + redraw_view(view); + update_view_title(view); + view->update_secs = secs; + } + } + return TRUE; + } - if (opt_iconv != ICONV_NONE) { + for (; (line = io_get(view->pipe, '\n', can_read)); can_read = FALSE) { + if (opt_iconv_in != ICONV_NONE) { ICONV_CONST char *inbuf = line; - size_t inlen = linelen; + size_t inlen = strlen(line) + 1; char *outbuf = out_buffer; size_t outlen = sizeof(out_buffer); size_t ret; - ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen); - if (ret != (size_t) -1) { + ret = iconv(opt_iconv_in, &inbuf, &inlen, &outbuf, &outlen); + if (ret != (size_t) -1) line = out_buffer; - linelen = strlen(out_buffer); - } } - if (!view->ops->read(view, line)) - goto alloc_error; - - if (lines-- == 1) - break; + if (!view->ops->read(view, line)) { + report("Allocation failure"); + end_update(view, TRUE); + return FALSE; + } } { + unsigned long lines = view->lines; int digits; - lines = view->lines; for (digits = 0; lines; digits++) lines /= 10; /* Keep the displayed view in sync with line number scaling. */ if (digits != view->digits) { view->digits = digits; - redraw_from = 0; + if (opt_line_number || view->type == VIEW_BLAME) + redraw = TRUE; } } - if (ferror(view->pipe) && errno != 0) { - report("Failed to read: %s", strerror(errno)); + if (io_error(view->pipe)) { + report("Failed to read: %s", io_strerror(view->pipe)); end_update(view, TRUE); - } else if (feof(view->pipe)) { - report(""); + } else if (io_eof(view->pipe)) { + if (view_is_displayed(view)) + report(""); end_update(view, FALSE); } + if (restore_view_position(view)) + redraw = TRUE; + if (!view_is_displayed(view)) return TRUE; - if (view == VIEW(REQ_VIEW_TREE)) { - /* Clear the view and redraw everything since the tree sorting - * might have rearranged things. */ - redraw_view(view); - - } else if (redraw_from >= 0) { - /* If this is an incremental update, redraw the previous line - * since for commits some members could have changed when - * loading the main view. */ - if (redraw_from > 0) - redraw_from--; - - /* Since revision graph visualization requires knowledge - * about the parent commit, it causes a further one-off - * needed to be redrawn for incremental updates. */ - if (redraw_from > 0 && opt_rev_graph) - redraw_from--; - - /* Incrementally draw avoids flickering. */ - redraw_view_from(view, redraw_from); - } - - if (view == VIEW(REQ_VIEW_BLAME)) + if (redraw) + redraw_view_from(view, 0); + else redraw_view_dirty(view); /* Update the title _after_ the redraw so that if the redraw picks up a * commit reference in view->ref it'll be available here. */ update_view_title(view); return TRUE; - -alloc_error: - report("Allocation failure"); - end_update(view, TRUE); - return FALSE; } +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 = &view->line[view->lines++]; + struct line *line; + + if (!realloc_lines(&view->line, view->lines, 1)) + return NULL; + line = &view->line[view->lines++]; memset(line, 0, sizeof(*line)); line->type = type; line->data = data; + line->dirty = 1; return line; } @@ -2459,6 +3412,19 @@ add_line_text(struct view *view, const char *text, enum line_type type) return data ? add_line_data(view, data, type) : NULL; } +static struct line * +add_line_format(struct view *view, enum line_type type, const char *fmt, ...) +{ + char buf[SIZEOF_STR]; + va_list args; + + va_start(args, fmt); + if (vsnprintf(buf, sizeof(buf), fmt, args) >= sizeof(buf)) + buf[0] = 0; + va_end(args); + + return buf[0] ? add_line_text(view, buf, type) : NULL; +} /* * View opening @@ -2467,19 +3433,17 @@ add_line_text(struct view *view, const char *text, enum line_type type) 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. */ }; 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)); - bool nomaximize = !!(flags & (OPEN_NOMAXIMIZE | OPEN_REFRESH)); + bool reload = !!(flags & (OPEN_RELOAD | OPEN_REFRESH | OPEN_PREPARED)); + bool nomaximize = !!(flags & OPEN_REFRESH); struct view *view = VIEW(request); int nviews = displayed_views(); struct view *base_view = display[0]; @@ -2496,8 +3460,8 @@ 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; + view->parent = prev; } else if (!nomaximize) { /* Maximize the current view. */ memset(display, 0, sizeof(display)); @@ -2505,23 +3469,28 @@ open_view(struct view *prev, enum request request, enum open_flags flags) display[current_view] = view; } + /* No prev signals that this is the first loaded view. */ + if (prev && view != prev) { + view->prev = prev; + } + /* Resize the view when switching between split- and full-screen, * or when switching between two different full-screen views. */ if (nviews != displayed_views() || (nviews == 1 && base_view != display[0])) resize_display(); - if (view->pipe) - end_update(view, TRUE); - if (view->ops->open) { + if (view->pipe) + end_update(view, TRUE); if (!view->ops->open(view)) { report("Failed to load %s view", view->name); return; } + restore_view_position(view); } else if ((reload || strcmp(view->vid, view->id)) && - !begin_update(view, flags & OPEN_REFRESH)) { + !begin_update(view, flags & (OPEN_REFRESH | OPEN_PREPARED))) { report("Failed to load %s view", view->name); return; } @@ -2535,73 +3504,48 @@ open_view(struct view *prev, enum request request, enum open_flags flags) do_scroll_view(prev, lines); } - if (prev && view != prev) { - if (split && !backgrounded) { - /* "Blur" the previous view. */ - update_view_title(prev); - } - - view->parent = prev; + if (prev && view != prev && split && view_is_displayed(prev)) { + /* "Blur" the previous view. */ + update_view_title(prev); } if (view->pipe && view->lines == 0) { /* Clear the old view and let the incremental updating refill * the screen. */ werase(view->win); + view->p_restore = flags & (OPEN_RELOAD | OPEN_REFRESH); report(""); } else if (view_is_displayed(view)) { 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 bool -run_confirm(const char *cmd, const char *prompt) -{ - bool confirmation = prompt_yesno(prompt); - - if (confirmation) - system(cmd); - - return confirmation; + report(""); + } } static void -open_external_viewer(const char *cmd) +open_external_viewer(const char *argv[], const char *dir) { def_prog_mode(); /* save current tty modes */ endwin(); /* restore original tty modes */ - system(cmd); + io_run_fg(argv, dir); fprintf(stderr, "Press Enter to continue"); getc(opt_tty); reset_prog_mode(); - redraw_display(); + redraw_display(TRUE); } static void open_mergetool(const char *file) { - char cmd[SIZEOF_STR]; - char file_sq[SIZEOF_STR]; + const char *mergetool_argv[] = { "git", "mergetool", file, NULL }; - if (sq_quote(file_sq, 0, file) < sizeof(file_sq) && - string_format(cmd, "git mergetool %s", file_sq)) { - open_external_viewer(cmd); - } + open_external_viewer(mergetool_argv, opt_cdup); } static void -open_editor(bool from_root, const char *file) +open_editor(const char *file) { - char cmd[SIZEOF_STR]; - char file_sq[SIZEOF_STR]; + const char *editor_argv[] = { "vi", file, NULL }; const char *editor; - char *prefix = from_root ? opt_cdup : ""; editor = getenv("GIT_EDITOR"); if (!editor && *opt_editor) @@ -2613,60 +3557,24 @@ open_editor(bool from_root, const char *file) if (!editor) editor = "vi"; - if (sq_quote(file_sq, 0, file) < sizeof(file_sq) && - string_format(cmd, "%s %s%s", editor, prefix, file_sq)) { - open_external_viewer(cmd); - } + editor_argv[0] = editor; + open_external_viewer(editor_argv, opt_cdup); } static void open_run_request(enum request request) { struct run_request *req = get_run_request(request); - char buf[SIZEOF_STR * 2]; - size_t bufpos; - char *cmd; + const char *argv[ARRAY_SIZE(req->argv)] = { NULL }; if (!req) { report("Unknown run request"); return; } - bufpos = 0; - cmd = req->cmd; - - while (cmd) { - char *next = strstr(cmd, "%("); - int len = next - cmd; - char *value; - - if (!next) { - len = strlen(cmd); - value = ""; - - } else if (!strncmp(next, "%(head)", 7)) { - value = ref_head; - - } else if (!strncmp(next, "%(commit)", 9)) { - value = ref_commit; - - } else if (!strncmp(next, "%(blob)", 7)) { - value = ref_blob; - - } else { - report("Unknown replacement in run request: `%s`", req->cmd); - return; - } - - if (!string_format_from(buf, &bufpos, "%.*s%s", len, cmd, value)) - return; - - if (next) - next = strchr(next, ')') + 1; - cmd = next; - } - - open_external_viewer(buf); + if (format_argv(argv, req->argv, TRUE)) + open_external_viewer(argv, NULL); + argv_free(argv); } /* @@ -2678,28 +3586,18 @@ 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); - /* FIXME: When all views can refresh always do this. */ - if (view == VIEW(REQ_VIEW_STATUS) || - view == VIEW(REQ_VIEW_MAIN) || - view == VIEW(REQ_VIEW_LOG) || - view == VIEW(REQ_VIEW_STAGE)) - request = REQ_REFRESH; - else - return TRUE; + view_request(view, REQ_REFRESH); + return TRUE; } - if (view && view->lines) { - request = view->ops->request(view, request, &view->line[view->lineno]); - if (request == REQ_NONE) - return TRUE; - } + request = view_request(view, request); + if (request == REQ_NONE) + return TRUE; switch (request) { case REQ_MOVE_UP: @@ -2711,6 +3609,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: @@ -2721,7 +3621,7 @@ view_driver(struct view *view, enum request request) case REQ_VIEW_BLAME: if (!opt_file[0]) { report("No file chosen, press %s to open tree view", - get_key(REQ_VIEW_TREE)); + get_key(view->keymap, REQ_VIEW_TREE)); break; } open_view(view, request, OPEN_DEFAULT); @@ -2730,16 +3630,16 @@ view_driver(struct view *view, enum request request) case REQ_VIEW_BLOB: if (!ref_blob[0]) { report("No file chosen, press %s to open tree view", - get_key(REQ_VIEW_TREE)); + get_key(view->keymap, REQ_VIEW_TREE)); break; } open_view(view, request, OPEN_DEFAULT); break; case REQ_VIEW_PAGER: - if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) { + if (!VIEW(REQ_VIEW_PAGER)->pipe && !VIEW(REQ_VIEW_PAGER)->lines) { report("No pager content, press %s to run command from prompt", - get_key(REQ_PROMPT)); + get_key(view->keymap, REQ_PROMPT)); break; } open_view(view, request, OPEN_DEFAULT); @@ -2748,7 +3648,7 @@ view_driver(struct view *view, enum request request) case REQ_VIEW_STAGE: if (!VIEW(REQ_VIEW_STAGE)->lines) { report("No stage content, press %s to open the status view and choose file", - get_key(REQ_VIEW_STATUS)); + get_key(view->keymap, REQ_VIEW_STATUS)); break; } open_view(view, request, OPEN_DEFAULT); @@ -2767,6 +3667,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; @@ -2774,14 +3675,7 @@ view_driver(struct view *view, enum request request) case REQ_PREVIOUS: request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP; - if ((view == VIEW(REQ_VIEW_DIFF) && - view->parent == VIEW(REQ_VIEW_MAIN)) || - (view == VIEW(REQ_VIEW_DIFF) && - view->parent == VIEW(REQ_VIEW_BLAME)) || - (view == VIEW(REQ_VIEW_STAGE) && - view->parent == VIEW(REQ_VIEW_STATUS)) || - (view == VIEW(REQ_VIEW_BLOB) && - view->parent == VIEW(REQ_VIEW_TREE))) { + if (view->parent) { int line; view = view->parent; @@ -2790,9 +3684,7 @@ view_driver(struct view *view, enum request request) if (view_is_displayed(view)) update_view_title(view); if (line != view->lineno) - view->ops->request(view, REQ_ENTER, - &view->line[view->lineno]); - + view_request(view, REQ_ENTER); } else { move_view(view, request); } @@ -2820,32 +3712,36 @@ 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: - opt_line_number = !opt_line_number; - redraw_display(); + toggle_view_option(&opt_line_number, "line numbers"); break; case REQ_TOGGLE_DATE: - opt_date = !opt_date; - redraw_display(); + toggle_date(); break; case REQ_TOGGLE_AUTHOR: - opt_author = !opt_author; - redraw_display(); + toggle_author(); break; case REQ_TOGGLE_REV_GRAPH: - opt_rev_graph = !opt_rev_graph; - redraw_display(); + toggle_view_option(&opt_rev_graph, "revision graph display"); break; case REQ_TOGGLE_REFS: - opt_show_refs = !opt_show_refs; - redraw_display(); + 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: @@ -2859,8 +3755,7 @@ view_driver(struct view *view, enum request request) break; case REQ_STOP_LOADING: - for (i = 0; i < ARRAY_SIZE(views); i++) { - view = &views[i]; + foreach_view(view, i) { if (view->pipe) report("Stopped loading the %s view", view->name), end_update(view, TRUE); @@ -2871,11 +3766,8 @@ view_driver(struct view *view, enum request request) report("tig-%s (built %s)", TIG_VERSION, __DATE__); return TRUE; - case REQ_SCREEN_RESIZE: - resize_display(); - /* Fall-through */ case REQ_SCREEN_REDRAW: - redraw_display(); + redraw_display(TRUE); break; case REQ_EDIT: @@ -2887,18 +3779,12 @@ view_driver(struct view *view, enum request request) break; case REQ_VIEW_CLOSE: - /* XXX: Mark closed views by letting view->parent point to the + /* XXX: Mark closed views by letting view->prev point to the * view itself. Parents to closed view should never be * followed. */ - if (view->parent && - view->parent->parent != view->parent) { - memset(display, 0, sizeof(display)); - current_view = 0; - display[current_view] = view->parent; - view->parent = view; - resize_display(); - redraw_display(); - report(""); + if (view->prev && view->prev != view) { + maximize_view(view->prev); + view->prev = view; break; } /* Fall-through */ @@ -2906,7 +3792,8 @@ view_driver(struct view *view, enum request request) return FALSE; default: - report("Unknown key, press 'h' for help"); + report("Unknown key, press %s for help", + get_key(view->keymap, REQ_VIEW_HELP)); return TRUE; } @@ -2914,6 +3801,207 @@ 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_timesec(struct time *time, const char *sec) +{ + time->sec = (time_t) atol(sec); +} + +static void +parse_timezone(struct time *time, const char *zone) +{ + long tz; + + tz = ('0' - zone[1]) * 60 * 60 * 10; + tz += ('0' - zone[2]) * 60 * 60; + tz += ('0' - zone[3]) * 60 * 10; + tz += ('0' - zone[4]) * 60; + + if (zone[0] == '-') + tz = -tz; + + time->tz = tz; + time->sec -= tz; +} + +/* Parse author lines where the name may be empty: + * author 1138474660 +0100 + */ +static void +parse_author_line(char *ident, const char **author, struct time *time) +{ + char *nameend = strchr(ident, '<'); + char *emailend = strchr(ident, '>'); + + if (nameend && emailend) + *nameend = *emailend = 0; + ident = chomp_string(ident); + if (!*ident) { + if (nameend) + ident = chomp_string(nameend + 1); + if (!*ident) + ident = "Unknown"; + } + + *author = get_author(ident); + + /* Parse epoch and timezone */ + if (emailend && emailend[1] == ' ') { + char *secs = emailend + 2; + char *zone = strchr(secs, ' '); + + parse_timesec(time, secs); + + if (zone && strlen(zone) == STRING_SIZE(" +0700")) + parse_timezone(time, zone + 1); + } +} + +static bool +open_commit_parent_menu(char buf[SIZEOF_STR], int *parents) +{ + 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; + + items = calloc(*parents + 1, sizeof(*items)); + if (!items) + return FALSE; + + for (i = 0; i < *parents; i++) { + string_copy_rev(rev, &buf[SIZEOF_REV * i]); + if (!io_run_buf(revlist_argv, text, sizeof(text)) || + !(items[i].text = strdup(text))) { + ok = FALSE; + break; + } + } + + 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], const char *path) +{ + char buf[SIZEOF_STR * 4]; + const char *revlist_argv[] = { + "git", "log", "--no-color", "-1", + "--pretty=format:%P", id, "--", path, NULL + }; + int parents; + + if (!io_run_buf(revlist_argv, buf, sizeof(buf)) || + (parents = strlen(buf) / 40) < 0) { + report("Failed to get parent information"); + return FALSE; + + } else if (parents == 0) { + 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) + parents = 0; + else if (!open_commit_parent_menu(buf, &parents)) + return FALSE; + + string_copy_rev(rev, &buf[41 * parents]); + return TRUE; +} + /* * Pager backend */ @@ -2921,11 +4009,12 @@ view_driver(struct view *view, enum request request) static bool pager_draw(struct view *view, struct line *line, unsigned int lineno) { - char *text = line->data; + char text[SIZEOF_STR]; if (opt_line_number && draw_lineno(view, lineno)) return TRUE; + string_expand(text, sizeof(text), line->data, opt_tab_size); draw_text(view, line->type, text, TRUE); return TRUE; } @@ -2933,22 +4022,10 @@ pager_draw(struct view *view, struct line *line, unsigned int lineno) static bool add_describe_ref(char *buf, size_t *bufpos, const char *commit_id, const char *sep) { - char refbuf[SIZEOF_STR]; - char *ref = NULL; - FILE *pipe; - - if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id)) - return TRUE; + const char *describe_argv[] = { "git", "describe", commit_id, NULL }; + char ref[SIZEOF_STR]; - pipe = popen(refbuf, "r"); - if (!pipe) - return TRUE; - - if ((ref = fgets(refbuf, sizeof(refbuf), pipe))) - ref = chomp_string(ref); - pclose(pipe); - - if (!ref || !*ref) + if (!io_run_buf(describe_argv, ref, sizeof(ref)) || !*ref) return TRUE; /* This is the only fatal call, since it can "corrupt" the buffer. */ @@ -2963,22 +4040,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) { - if (view == VIEW(REQ_VIEW_DIFF)) + list = get_ref_list(commit_id); + if (!list) { + if (view->type == 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"; @@ -2987,9 +4064,9 @@ 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)) { + if (!is_tag && view->type == VIEW_DIFF) { try_add_describe_ref: /* Add -g "fake" reference. */ if (!add_describe_ref(buf, &bufpos, commit_id, sep)) @@ -2999,9 +4076,6 @@ try_add_describe_ref: if (bufpos == 0) return; - if (!realloc_lines(view, view->line_size + 1)) - return; - add_line_text(view, buf, LINE_PP_REFS); } @@ -3018,8 +4092,8 @@ pager_read(struct view *view, char *data) return FALSE; if (line->type == LINE_COMMIT && - (view == VIEW(REQ_VIEW_DIFF) || - view == VIEW(REQ_VIEW_LOG))) + (view->type == VIEW_DIFF || + view->type == VIEW_LOG)) add_pager_refs(view, line); return TRUE; @@ -3034,8 +4108,8 @@ pager_request(struct view *view, enum request request, struct line *line) return request; if (line->type == LINE_COMMIT && - (view == VIEW(REQ_VIEW_LOG) || - view == VIEW(REQ_VIEW_PAGER))) { + (view->type == VIEW_LOG || + view->type == VIEW_PAGER)) { open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT); split = 1; } @@ -3057,16 +4131,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 @@ -3075,7 +4142,7 @@ pager_select(struct view *view, struct line *line) if (line->type == LINE_COMMIT) { char *text = (char *)line->data + STRING_SIZE("commit "); - if (view != VIEW(REQ_VIEW_PAGER)) + if (view->type != VIEW_PAGER) string_copy_rev(view->ref, text); string_copy_rev(ref_commit, text); } @@ -3084,6 +4151,7 @@ pager_select(struct view *view, struct line *line) static struct view_ops pager_ops = { "line", NULL, + NULL, pager_read, pager_draw, pager_request, @@ -3091,6 +4159,10 @@ static struct view_ops pager_ops = { pager_select, }; +static const char *log_argv[SIZEOF_ARG] = { + "git", "log", "--no-color", "--cc", "--stat", "-n100", "%(head)", NULL +}; + static enum request log_request(struct view *view, enum request request, struct line *line) { @@ -3106,6 +4178,7 @@ log_request(struct view *view, enum request request, struct line *line) static struct view_ops log_ops = { "line", + log_argv, NULL, pager_read, pager_draw, @@ -3114,88 +4187,149 @@ static struct view_ops log_ops = { pager_select, }; +static const char *diff_argv[SIZEOF_ARG] = { + "git", "show", "--pretty=fuller", "--no-color", "--root", + "--patch-with-stat", "--find-copies-harder", "-C", "%(commit)", NULL +}; + +static struct view_ops diff_ops = { + "line", + diff_argv, + NULL, + pager_read, + pager_draw, + pager_request, + pager_grep, + pager_select, +}; /* * Help backend */ +static bool help_keymap_hidden[ARRAY_SIZE(keymap_table)]; + static bool -help_open(struct view *view) +help_open_keymap_title(struct view *view, enum keymap keymap) { - char buf[BUFSIZ]; - int lines = ARRAY_SIZE(req_info) + 2; - int i; - - if (view->lines > 0) - return TRUE; - - for (i = 0; i < ARRAY_SIZE(req_info); i++) - if (!req_info[i].request) - lines++; + struct line *line; - lines += run_requests + 1; + line = add_line_format(view, LINE_HELP_KEYMAP, "[%c] %s bindings", + help_keymap_hidden[keymap] ? '+' : '-', + enum_name(keymap_table[keymap])); + if (line) + line->other = keymap; - view->line = calloc(lines, sizeof(*view->line)); - if (!view->line) - return FALSE; + return help_keymap_hidden[keymap]; +} - add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT); +static void +help_open_keymap(struct view *view, enum keymap keymap) +{ + const char *group = NULL; + char buf[SIZEOF_STR]; + size_t bufpos; + bool add_title = TRUE; + int i; for (i = 0; i < ARRAY_SIZE(req_info); i++) { - const char *key; + const char *key = NULL; if (req_info[i].request == REQ_NONE) continue; if (!req_info[i].request) { - add_line_text(view, "", LINE_DEFAULT); - add_line_text(view, req_info[i].help, LINE_DEFAULT); + group = req_info[i].help; continue; } - key = get_key(req_info[i].request); - if (!*key) - key = "(no key defined)"; - - if (!string_format(buf, " %-25s %s", key, req_info[i].help)) + key = get_keys(keymap, req_info[i].request, TRUE); + if (!key || !*key) continue; - add_line_text(view, buf, LINE_DEFAULT); - } + if (add_title && help_open_keymap_title(view, keymap)) + return; + add_title = FALSE; - if (run_requests) { - add_line_text(view, "", LINE_DEFAULT); - add_line_text(view, "External commands:", LINE_DEFAULT); + if (group) { + add_line_text(view, group, LINE_HELP_GROUP); + group = NULL; + } + + add_line_format(view, LINE_DEFAULT, " %-25s %-20s %s", key, + enum_name(req_info[i]), req_info[i].help); } + group = "External commands:"; + for (i = 0; i < run_requests; i++) { struct run_request *req = get_run_request(REQ_NONE + i + 1); const char *key; + int argc; - if (!req) + if (!req || req->keymap != keymap) continue; key = get_key_name(req->key); if (!*key) key = "(no key defined)"; - if (!string_format(buf, " %-10s %-14s `%s`", - keymap_table[req->keymap].name, - key, req->cmd)) - continue; + if (add_title && help_open_keymap_title(view, keymap)) + return; + if (group) { + add_line_text(view, group, LINE_HELP_GROUP); + group = NULL; + } + + for (bufpos = 0, argc = 0; req->argv[argc]; argc++) + if (!string_format_from(buf, &bufpos, "%s%s", + argc ? " " : "", req->argv[argc])) + return; - add_line_text(view, buf, LINE_DEFAULT); + add_line_format(view, LINE_DEFAULT, " %-25s `%s`", key, buf); } +} + +static bool +help_open(struct view *view) +{ + enum keymap keymap; + + reset_view(view); + add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT); + add_line_text(view, "", LINE_DEFAULT); + + for (keymap = 0; keymap < ARRAY_SIZE(keymap_table); keymap++) + help_open_keymap(view, keymap); return TRUE; } +static enum request +help_request(struct view *view, enum request request, struct line *line) +{ + switch (request) { + case REQ_ENTER: + if (line->type == LINE_HELP_KEYMAP) { + help_keymap_hidden[line->other] = + !help_keymap_hidden[line->other]; + view->p_restore = TRUE; + open_view(view, REQ_VIEW_HELP, OPEN_REFRESH); + } + + return REQ_NONE; + default: + return pager_request(view, request, line); + } +} + static struct view_ops help_ops = { "line", + NULL, help_open, NULL, pager_draw, - pager_request, + help_request, pager_grep, pager_select, }; @@ -3251,110 +4385,218 @@ 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 ") + +#define TREE_ID_OFFSET \ + STRING_SIZE("100644 blob ") + +struct tree_entry { + char id[SIZEOF_REV]; + mode_t mode; + struct time time; /* Date from the author ident. */ + const char *author; /* Author of the commit. */ + char name[1]; +}; + +static const char * +tree_path(const struct line *line) +{ + return ((struct tree_entry *) line->data)->name; +} + +static int +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)); +} -#define TREE_UP_FORMAT "040000 tree %s\t.." +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_entry(enum line_type type1, const char *name1, - enum line_type type2, const char *name2) +tree_compare(const void *l1, const void *l2) { - if (type1 != type2) { - if (type1 == LINE_TREE_DIR) - return -1; + const struct line *line1 = (const struct line *) l1; + const struct line *line2 = (const struct line *) l2; + const struct tree_entry *entry1 = ((const struct line *) l1)->data; + const struct tree_entry *entry2 = ((const struct line *) l2)->data; + + if (line1->type == LINE_TREE_HEAD) + return -1; + if (line2->type == LINE_TREE_HEAD) return 1; + + switch (get_sort_field(tree_sort_state)) { + case ORDERBY_DATE: + return sort_order(tree_sort_state, timecmp(&entry1->time, &entry2->time)); + + case ORDERBY_AUTHOR: + return sort_order(tree_sort_state, strcmp(entry1->author, entry2->author)); + + case ORDERBY_NAME: + default: + return sort_order(tree_sort_state, tree_compare_entry(line1, line2)); + } +} + + +static struct line * +tree_entry(struct view *view, enum line_type type, const char *path, + const char *mode, const char *id) +{ + struct tree_entry *entry = calloc(1, sizeof(*entry) + strlen(path)); + struct line *line = entry ? add_line_data(view, entry, type) : NULL; + + if (!entry || !line) { + free(entry); + return NULL; } - return strcmp(name1, name2); + strncpy(entry->name, path, strlen(path)); + if (mode) + entry->mode = strtoul(mode, NULL, 8); + if (id) + string_copy_rev(entry->id, id); + + return line; } -static const char * -tree_path(struct line *line) -{ - const char *path = line->data; +static bool +tree_read_date(struct view *view, char *text, bool *read_date) +{ + static const char *author_name; + static struct time author_time; + + if (!text && *read_date) { + *read_date = FALSE; + return TRUE; + + } else if (!text) { + char *path = *opt_path ? opt_path : "."; + /* Find next entry to process */ + const char *log_file[] = { + "git", "log", "--no-color", "--pretty=raw", + "--cc", "--raw", view->id, "--", path, NULL + }; + + if (!view->lines) { + tree_entry(view, LINE_TREE_HEAD, opt_path, NULL, NULL); + report("Tree is empty"); + return TRUE; + } + + if (!start_update(view, log_file, opt_cdup)) { + report("Failed to load tree data"); + return TRUE; + } + + *read_date = TRUE; + return FALSE; + + } else if (*text == 'a' && get_line_type(text) == LINE_AUTHOR) { + parse_author_line(text + STRING_SIZE("author "), + &author_name, &author_time); + + } else if (*text == ':') { + char *pos; + size_t annotated = 1; + size_t i; + + pos = strchr(text, '\t'); + if (!pos) + return TRUE; + text = pos + 1; + if (*opt_path && !strncmp(text, opt_path, strlen(opt_path))) + text += strlen(opt_path); + pos = strchr(text, '/'); + if (pos) + *pos = 0; + + for (i = 1; i < view->lines; i++) { + struct line *line = &view->line[i]; + struct tree_entry *entry = line->data; + + annotated += !!entry->author; + if (entry->author || strcmp(entry->name, text)) + continue; + + entry->author = author_name; + entry->time = author_time; + line->dirty = 1; + break; + } - return path + SIZEOF_TREE_ATTR; + if (annotated == view->lines) + io_kill(view->pipe); + } + return TRUE; } static bool tree_read(struct view *view, char *text) { - size_t textlen = text ? strlen(text) : 0; - char buf[SIZEOF_STR]; - unsigned long pos; + static bool read_date = FALSE; + struct tree_entry *data; + struct line *entry, *line; enum line_type type; - bool first_read = view->lines == 0; + size_t textlen = text ? strlen(text) : 0; + char *path = text + SIZEOF_TREE_ATTR; + + if (read_date || !text) + return tree_read_date(view, text, &read_date); - if (!text) - return TRUE; if (textlen <= SIZEOF_TREE_ATTR) return FALSE; - - type = text[STRING_SIZE("100644 ")] == 't' - ? LINE_TREE_DIR : LINE_TREE_FILE; - - if (first_read) { - /* Add path info line */ - if (!string_format(buf, "Directory path /%s", opt_path) || - !realloc_lines(view, view->line_size + 1) || - !add_line_text(view, buf, LINE_DEFAULT)) - return FALSE; - - /* Insert "link" to parent directory. */ - if (*opt_path) { - if (!string_format(buf, TREE_UP_FORMAT, view->ref) || - !realloc_lines(view, view->line_size + 1) || - !add_line_text(view, buf, LINE_TREE_DIR)) - return FALSE; - } - } + if (view->lines == 0 && + !tree_entry(view, LINE_TREE_HEAD, opt_path, NULL, NULL)) + return FALSE; /* Strip the path part ... */ if (*opt_path) { size_t pathlen = textlen - SIZEOF_TREE_ATTR; size_t striplen = strlen(opt_path); - char *path = text + SIZEOF_TREE_ATTR; if (pathlen > striplen) memmove(path, path + striplen, pathlen - striplen + 1); + + /* Insert "link" to parent directory. */ + if (view->lines == 1 && + !tree_entry(view, LINE_TREE_DIR, "..", "040000", view->ref)) + return FALSE; } - /* Skip "Directory ..." and ".." line. */ - for (pos = 1 + !!*opt_path; pos < view->lines; pos++) { - struct line *line = &view->line[pos]; - const char *path1 = tree_path(line); - char *path2 = text + SIZEOF_TREE_ATTR; - int cmp = tree_compare_entry(line->type, path1, type, path2); + type = text[SIZEOF_TREE_MODE] == 't' ? LINE_TREE_DIR : LINE_TREE_FILE; + entry = tree_entry(view, type, path, text, text + TREE_ID_OFFSET); + if (!entry) + return FALSE; + data = entry->data; - if (cmp <= 0) + /* Skip "Directory ..." and ".." line. */ + for (line = &view->line[1 + !!*opt_path]; line < entry; line++) { + if (tree_compare_entry(line, entry) <= 0) continue; - text = strdup(text); - if (!text) - return FALSE; - - if (view->lines > pos) - memmove(&view->line[pos + 1], &view->line[pos], - (view->lines - pos) * sizeof(*line)); + memmove(line + 1, line, (entry - line) * sizeof(*entry)); - line = &view->line[pos]; - line->data = text; + line->data = data; line->type = type; - view->lines++; + for (; line <= entry; line++) + line->dirty = line->cleareol = 1; return TRUE; } - if (!add_line_text(view, text, type)) - return FALSE; - if (tree_lineno > view->lineno) { view->lineno = tree_lineno; tree_lineno = 0; @@ -3363,10 +4605,51 @@ tree_read(struct view *view, char *text) return TRUE; } +static bool +tree_draw(struct view *view, struct line *line, unsigned int lineno) +{ + struct tree_entry *entry = line->data; + + if (line->type == LINE_TREE_HEAD) { + if (draw_text(view, line->type, "Directory path /", TRUE)) + return TRUE; + } else { + if (draw_mode(view, entry->mode)) + return TRUE; + + if (opt_author && draw_author(view, entry->author)) + return TRUE; + + if (opt_date && draw_date(view, &entry->time)) + return TRUE; + } + if (draw_text(view, line->type, entry->name, TRUE)) + return TRUE; + return TRUE; +} + +static void +open_blob_editor(const char *id) +{ + const char *blob_argv[] = { "git", "cat-file", "blob", id, NULL }; + char file[SIZEOF_STR] = "/tmp/tigblob.XXXXXX"; + int fd = mkstemp(file); + + if (fd == -1) + report("Failed to create temporary file"); + else if (!io_run_append(blob_argv, fd)) + report("Failed to save blob data to file"); + else + open_editor(file); + if (fd != -1) + unlink(file); +} + static enum request tree_request(struct view *view, enum request request, struct line *line) { enum open_flags flags; + struct tree_entry *entry = line->data; switch (request) { case REQ_VIEW_BLAME: @@ -3382,13 +4665,18 @@ tree_request(struct view *view, enum request request, struct line *line) if (line->type != LINE_TREE_FILE) { report("Edit only supported for files"); } else if (!is_head_commit(view->vid)) { - report("Edit only supported for files in the current work tree"); + open_blob_editor(entry->id); } else { - open_editor(TRUE, opt_file); + open_editor(opt_file); } return REQ_NONE; - case REQ_TREE_PARENT: + 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 */ return REQ_VIEW_CLOSE; @@ -3410,7 +4698,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(); @@ -3428,46 +4716,91 @@ tree_request(struct view *view, enum request request, struct line *line) break; case LINE_TREE_FILE: - flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT; + flags = view_is_displayed(view) ? OPEN_SPLIT : OPEN_DEFAULT; request = REQ_VIEW_BLOB; break; default: - return TRUE; + return REQ_NONE; } open_view(view, request, flags); - if (request == REQ_VIEW_TREE) { + if (request == REQ_VIEW_TREE) view->lineno = tree_lineno; - } return REQ_NONE; } +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 : "", + mkdate(&entry->time, opt_date), + NULL + }; + + return grep_text(view, text); +} + static void tree_select(struct view *view, struct line *line) { - char *text = (char *)line->data + STRING_SIZE("100644 blob "); + struct tree_entry *entry = line->data; if (line->type == LINE_TREE_FILE) { - string_copy_rev(ref_blob, text); + string_copy_rev(ref_blob, entry->id); string_format(opt_file, "%s%s", opt_path, tree_path(line)); } else if (line->type != LINE_TREE_DIR) { return; } - string_copy_rev(view->ref, text); + string_copy_rev(view->ref, entry->id); } +static bool +tree_prepare(struct view *view) +{ + if (view->lines == 0 && opt_prefix[0]) { + char *pos = opt_prefix; + + while (pos && *pos) { + char *end = strchr(pos, '/'); + + if (end) + *end = 0; + push_tree_stack_entry(pos, 0); + pos = end; + if (end) { + *end = '/'; + pos++; + } + } + + } else if (strcmp(view->vid, view->id)) { + opt_path[0] = 0; + } + + return prepare_io(view, opt_cdup, view->ops->argv, TRUE); +} + +static const char *tree_argv[SIZEOF_ARG] = { + "git", "ls-tree", "%(commit)", "%(directory)", NULL +}; + static struct view_ops tree_ops = { "file", + tree_argv, NULL, tree_read, - pager_draw, + tree_draw, tree_request, - pager_grep, + tree_grep, tree_select, + tree_prepare, }; static bool @@ -3478,12 +4811,29 @@ blob_read(struct view *view, char *line) return add_line_text(view, line, LINE_DEFAULT) != NULL; } +static enum request +blob_request(struct view *view, enum request request, struct line *line) +{ + switch (request) { + case REQ_EDIT: + open_blob_editor(view->vid); + return REQ_NONE; + default: + return pager_request(view, request, line); + } +} + +static const char *blob_argv[SIZEOF_ARG] = { + "git", "cat-file", "blob", "%(blob)", NULL +}; + static struct view_ops blob_ops = { "line", + blob_argv, NULL, blob_read, pager_draw, - pager_request, + blob_request, pager_grep, pager_select, }; @@ -3502,45 +4852,41 @@ static struct view_ops blob_ops = { 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. */ + struct time 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 int header:1; + unsigned long lineno; char text[1]; }; -#define BLAME_CAT_FILE_CMD "git cat-file blob %s:%s" -#define BLAME_INCREMENTAL_CMD "git blame --incremental %s -- %s" - static bool blame_open(struct view *view) { char path[SIZEOF_STR]; - char ref[SIZEOF_STR] = ""; - - if (sq_quote(path, 0, opt_file) >= sizeof(path)) - return FALSE; - if (*opt_ref && sq_quote(ref, 0, opt_ref) >= sizeof(ref)) - return FALSE; + if (!view->prev && *opt_prefix) { + string_copy(path, opt_file); + if (!string_format(opt_file, "%s%s", opt_prefix, path)) + return FALSE; + } - if (*opt_ref || !(view->pipe = fopen(opt_file, "r"))) { - const char *id = *opt_ref ? ref : "HEAD"; + if (*opt_ref || !io_open(&view->io, "%s%s", opt_cdup, opt_file)) { + const char *blame_cat_file_argv[] = { + "git", "cat-file", "blob", path, NULL + }; - if (!string_format(view->cmd, BLAME_CAT_FILE_CMD, id, path) || - !(view->pipe = popen(view->cmd, "r"))) + if (!string_format(path, "%s:%s", opt_ref, opt_file) || + !start_update(view, blame_cat_file_argv, opt_cdup)) return FALSE; } - reset_view(view); + setup_update(view, opt_file); string_format(view->ref, "%s ...", opt_file); - string_copy_rev(view->vid, opt_file); - set_nonblocking_input(TRUE); - view->start_time = time(NULL); return TRUE; } @@ -3591,14 +4937,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; @@ -3612,7 +4960,7 @@ parse_blame_commit(struct view *view, const char *text, int *blamed) blame = line->data; blame->commit = commit; - blame->header = !group; + blame->lineno = orig_lineno + group - 1; line->dirty = 1; } @@ -3623,24 +4971,19 @@ static bool blame_read_file(struct view *view, const char *line, bool *read_file) { if (!line) { - char ref[SIZEOF_STR] = ""; - char path[SIZEOF_STR]; - FILE *pipe = NULL; + const char *blame_argv[] = { + "git", "blame", "--incremental", + *opt_ref ? opt_ref : "--incremental", "--", opt_file, NULL + }; - if (view->lines == 0 && !view->parent) + if (view->lines == 0 && !view->prev) die("No blame exist for %s", view->vid); - if (view->lines == 0 || - sq_quote(path, 0, opt_file) >= sizeof(path) || - (*opt_ref && sq_quote(ref, 0, opt_ref) >= sizeof(ref)) || - !string_format(view->cmd, BLAME_INCREMENTAL_CMD, ref, path) || - !(pipe = popen(view->cmd, "r"))) { + if (view->lines == 0 || !start_update(view, blame_argv, opt_cdup)) { report("Failed to load blame data"); return TRUE; } - fclose(view->pipe); - view->pipe = pipe; *read_file = FALSE; return FALSE; @@ -3648,6 +4991,9 @@ blame_read_file(struct view *view, const char *line, bool *read_file) size_t linelen = strlen(line); struct blame *blame = malloc(sizeof(*blame) + linelen); + if (!blame) + return FALSE; + blame->commit = NULL; strncpy(blame->text, line, linelen); blame->text[linelen] = 0; @@ -3672,7 +5018,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) @@ -3694,31 +5039,23 @@ blame_read(struct view *view, char *line) if (!commit) { commit = parse_blame_commit(view, line, &blamed); string_format(view->ref, "%s %2d%%", view->vid, - blamed * 100 / view->lines); + 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); + parse_timesec(&commit->time, 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)); + } else if (match_blame_header("previous ", &line)) { + commit->has_previous = TRUE; + } else if (match_blame_header("filename ", &line)) { string_ncopy(commit->filename, line, strlen(line)); commit = NULL; @@ -3731,8 +5068,9 @@ static bool blame_draw(struct view *view, struct line *line, unsigned int lineno) { struct blame *blame = line->data; - struct tm *time = NULL; + struct time *time = NULL; const char *id = NULL, *author = NULL; + char text[SIZEOF_STR]; if (blame->commit && *blame->commit->filename) { id = blame->commit->id; @@ -3740,105 +5078,385 @@ blame_draw(struct view *view, struct line *line, unsigned int lineno) time = &blame->commit->time; } - if (opt_date && draw_date(view, time)) - return TRUE; + if (opt_date && draw_date(view, time)) + return TRUE; + + if (opt_author && draw_author(view, author)) + return TRUE; + + if (draw_field(view, LINE_BLAME_ID, id, ID_COLS, FALSE)) + return TRUE; + + if (draw_lineno(view, lineno)) + return 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, bool check_null_id) +{ + if (!blame->commit) + report("Commit data not loaded yet"); + 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 (!io_run(&io, IO_RD, NULL, diff_tree_argv)) + 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++; + } + } + + io_done(&io); +} + +static enum request +blame_request(struct view *view, enum request request, struct line *line) +{ + enum open_flags flags = view_is_displayed(view) ? OPEN_SPLIT : OPEN_DEFAULT; + struct blame *blame = line->data; + + switch (request) { + case REQ_VIEW_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, 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 (!check_blame_commit(blame, FALSE)) + break; + + if (view_is_displayed(VIEW(REQ_VIEW_DIFF)) && + !strcmp(blame->commit->id, VIEW(REQ_VIEW_DIFF)->ref)) + break; + + if (!strcmp(blame->commit->id, NULL_ID)) { + struct view *diff = VIEW(REQ_VIEW_DIFF); + const char *diff_index_argv[] = { + "git", "diff-index", "--root", "--patch-with-stat", + "-C", "-M", "HEAD", "--", view->vid, NULL + }; + + if (!blame->commit->has_previous) { + diff_index_argv[1] = "diff"; + diff_index_argv[2] = "--no-color"; + diff_index_argv[6] = "--"; + diff_index_argv[7] = "/dev/null"; + } + + if (!prepare_update(diff, diff_index_argv, NULL)) { + report("Failed to allocate diff command"); + break; + } + flags |= OPEN_PREPARED; + } + + open_view(view, REQ_VIEW_DIFF, flags); + if (VIEW(REQ_VIEW_DIFF)->pipe && !strcmp(blame->commit->id, NULL_ID)) + string_copy_rev(VIEW(REQ_VIEW_DIFF)->ref, NULL_ID); + break; + + default: + return request; + } + + return REQ_NONE; +} + +static bool +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 ? mkdate(&commit->time, opt_date) : "", + 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. */ + struct time time; /* Date of the last activity. */ + const struct ref *ref; /* Name and commit ID information. */ +}; + +static const struct ref branch_all; + +static const enum sort_field branch_sort_fields[] = { + ORDERBY_NAME, ORDERBY_DATE, ORDERBY_AUTHOR +}; +static struct sort_state branch_sort_state = SORT_STATE(branch_sort_fields); + +static int +branch_compare(const void *l1, const void *l2) +{ + const struct branch *branch1 = ((const struct line *) l1)->data; + const struct branch *branch2 = ((const struct line *) l2)->data; + + switch (get_sort_field(branch_sort_state)) { + case ORDERBY_DATE: + return sort_order(branch_sort_state, timecmp(&branch1->time, &branch2->time)); + + case ORDERBY_AUTHOR: + return sort_order(branch_sort_state, strcmp(branch1->author, branch2->author)); + + case ORDERBY_NAME: + default: + return sort_order(branch_sort_state, strcmp(branch1->ref->name, branch2->ref->name)); + } +} + +static bool +branch_draw(struct view *view, struct line *line, unsigned int lineno) +{ + struct branch *branch = line->data; + enum line_type type = branch->ref->head ? LINE_MAIN_HEAD : LINE_DEFAULT; + + if (opt_date && draw_date(view, &branch->time)) + return TRUE; + + if (opt_author && draw_author(view, branch->author)) + return TRUE; + + draw_text(view, type, branch->ref == &branch_all ? "All branches" : branch->ref->name, TRUE); + return TRUE; +} + +static enum request +branch_request(struct view *view, enum request request, struct line *line) +{ + struct branch *branch = line->data; + + switch (request) { + case REQ_REFRESH: + load_refs(); + open_view(view, REQ_VIEW_BRANCH, OPEN_REFRESH); + return REQ_NONE; + + case REQ_TOGGLE_SORT_FIELD: + case REQ_TOGGLE_SORT_ORDER: + sort_view(view, request, &branch_sort_state, branch_compare); + return REQ_NONE; + + case REQ_ENTER: + if (branch->ref == &branch_all) { + const char *all_branches_argv[] = { + "git", "log", "--no-color", "--pretty=raw", "--parents", + "--topo-order", "--all", NULL + }; + struct view *main_view = VIEW(REQ_VIEW_MAIN); + + if (!prepare_update(main_view, all_branches_argv, NULL)) { + report("Failed to load view of all branches"); + return REQ_NONE; + } + open_view(view, REQ_VIEW_MAIN, OPEN_PREPARED | OPEN_SPLIT); + } else { + open_view(view, REQ_VIEW_MAIN, OPEN_SPLIT); + } + return REQ_NONE; + + default: + return request; + } +} - if (opt_author && - draw_field(view, LINE_MAIN_AUTHOR, author, opt_author_cols, TRUE)) - return TRUE; +static bool +branch_read(struct view *view, char *line) +{ + static char id[SIZEOF_REV]; + struct branch *reference; + size_t i; - if (draw_field(view, LINE_BLAME_ID, id, ID_COLS, FALSE)) + if (!line) return TRUE; - if (draw_lineno(view, lineno)) + switch (get_line_type(line)) { + case LINE_COMMIT: + string_copy_rev(id, line + STRING_SIZE("commit ")); return TRUE; - draw_text(view, LINE_DEFAULT, blame->text, TRUE); - return TRUE; -} - -static enum request -blame_request(struct view *view, enum request request, struct line *line) -{ - enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT; - struct blame *blame = line->data; + case LINE_AUTHOR: + for (i = 0, reference = NULL; i < view->lines; i++) { + struct branch *branch = view->line[i].data; - switch (request) { - case REQ_ENTER: - if (!blame->commit) { - report("No commit loaded yet"); - break; - } + if (strcmp(branch->ref->id, id)) + continue; - if (!strcmp(blame->commit->id, NULL_ID)) { - char path[SIZEOF_STR]; + view->line[i].dirty = TRUE; + if (reference) { + branch->author = reference->author; + branch->time = reference->time; + continue; + } - if (sq_quote(path, 0, view->vid) >= sizeof(path)) - break; - string_format(opt_cmd, "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s 2>/dev/null", path); + parse_author_line(line + STRING_SIZE("author "), + &branch->author, &branch->time); + reference = branch; } - - open_view(view, REQ_VIEW_DIFF, flags); - break; + return TRUE; default: - return request; + return TRUE; } - return REQ_NONE; } static bool -blame_grep(struct view *view, struct line *line) +branch_open_visitor(void *data, const 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 (!start_update(view, branch_log, NULL)) { + report("Failed to load branch data"); + return TRUE; } - return MATCH(blame->text, 1); + setup_update(view, view->id); + branch_open_visitor(view, &branch_all); + foreach_ref(branch_open_visitor, view); + view->p_restore = TRUE; -#undef MATCH + return TRUE; } -static void -blame_select(struct view *view, struct line *line) +static bool +branch_grep(struct view *view, struct line *line) { - struct blame *blame = line->data; - struct blame_commit *commit = blame->commit; + struct branch *branch = line->data; + const char *text[] = { + branch->ref->name, + branch->author, + NULL + }; - if (!commit) - return; + return grep_text(view, text); +} - if (!strcmp(commit->id, NULL_ID)) - string_ncopy(ref_commit, "HEAD", 4); - else - string_copy_rev(ref_commit, commit->id); +static void +branch_select(struct view *view, struct line *line) +{ + struct branch *branch = line->data; + + string_copy_rev(view->ref, branch->ref->id); + string_copy_rev(ref_commit, branch->ref->id); + string_copy_rev(ref_head, branch->ref->id); + string_copy_rev(ref_branch, branch->ref->name); } -static struct view_ops blame_ops = { - "line", - blame_open, - blame_read, - blame_draw, - blame_request, - blame_grep, - blame_select, +static struct view_ops branch_ops = { + "branch", + NULL, + branch_open, + branch_read, + branch_draw, + branch_request, + branch_grep, + branch_select, }; /* @@ -3865,6 +5483,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) @@ -3884,7 +5504,7 @@ status_get_diff(struct status *file, const char *buf, size_t bufsize) const char *new_rev = buf + 56; const char *status = buf + 97; - if (bufsize < 99 || + if (bufsize < 98 || old_mode[-1] != ':' || new_mode[-1] != ' ' || old_rev[-1] != ' ' || @@ -3906,135 +5526,177 @@ status_get_diff(struct status *file, const char *buf, size_t bufsize) } static bool -status_run(struct view *view, const char cmd[], char status, enum line_type type) +status_run(struct view *view, const char *argv[], char status, enum line_type type) { - struct status *file = NULL; struct status *unmerged = NULL; - char buf[SIZEOF_STR * 4]; - size_t bufsize = 0; - FILE *pipe; + char *buf; + struct io io; - pipe = popen(cmd, "r"); - if (!pipe) + if (!io_run(&io, IO_RD, opt_cdup, argv)) return FALSE; add_line_data(view, NULL, type); - while (!feof(pipe) && !ferror(pipe)) { - char *sep; - size_t readsize; + while ((buf = io_get(&io, 0, TRUE))) { + struct status *file = unmerged; - readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe); - if (!readsize) - break; - bufsize += readsize; - - /* Process while we have NUL chars. */ - while ((sep = memchr(buf, 0, bufsize))) { - size_t sepsize = sep - buf + 1; - - if (!file) { - if (!realloc_lines(view, view->line_size + 1)) - goto error_out; - - file = calloc(1, sizeof(*file)); - if (!file) - goto error_out; - - add_line_data(view, file, type); - } + if (!file) { + file = calloc(1, sizeof(*file)); + if (!file || !add_line_data(view, file, type)) + goto error_out; + } - /* Parse diff info part. */ - if (status) { - file->status = status; - if (status == 'A') - string_copy(file->old.rev, NULL_ID); + /* Parse diff info part. */ + if (status) { + file->status = status; + if (status == 'A') + string_copy(file->old.rev, NULL_ID); - } else if (!file->status) { - if (!status_get_diff(file, buf, sepsize)) - goto error_out; + } else if (!file->status || file == unmerged) { + if (!status_get_diff(file, buf, strlen(buf))) + goto error_out; - bufsize -= sepsize; - memmove(buf, sep + 1, bufsize); + buf = io_get(&io, 0, TRUE); + if (!buf) + break; - sep = memchr(buf, 0, bufsize); - if (!sep) - break; - sepsize = sep - buf + 1; - - /* 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); - - unmerged = NULL; - if (collapse) { - free(file); - view->lines--; - continue; - } - } + /* Collapse all modified entries that follow an + * associated unmerged entry. */ + if (unmerged == file) { + unmerged->status = 'U'; + unmerged = NULL; + } else if (file->status == 'U') { + unmerged = file; } + } - /* Grab the old name for rename/copy. */ - if (!*file->old.name && - (file->status == 'R' || file->status == 'C')) { - sepsize = sep - buf + 1; - string_ncopy(file->old.name, buf, sepsize); - bufsize -= sepsize; - memmove(buf, sep + 1, bufsize); - - sep = memchr(buf, 0, bufsize); - if (!sep) - break; - sepsize = sep - buf + 1; - } + /* Grab the old name for rename/copy. */ + if (!*file->old.name && + (file->status == 'R' || file->status == 'C')) { + string_ncopy(file->old.name, buf, strlen(buf)); - /* git-ls-files just delivers a NUL separated - * list of file names similar to the second half - * of the git-diff-* output. */ - string_ncopy(file->new.name, buf, sepsize); - if (!*file->old.name) - string_copy(file->old.name, file->new.name); - bufsize -= sepsize; - memmove(buf, sep + 1, bufsize); - file = NULL; + buf = io_get(&io, 0, TRUE); + if (!buf) + break; } + + /* git-ls-files just delivers a NUL separated list of + * file names similar to the second half of the + * git-diff-* output. */ + string_ncopy(file->new.name, buf, strlen(buf)); + if (!*file->old.name) + string_copy(file->old.name, file->new.name); + file = NULL; } - if (ferror(pipe)) { + if (io_error(&io)) { error_out: - pclose(pipe); + io_done(&io); return FALSE; } if (!view->line[view->lines - 1].data) add_line_data(view, NULL, LINE_STAT_NONE); - pclose(pipe); + io_done(&io); return TRUE; } /* Don't show unmerged entries in the staged section. */ -#define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached -M HEAD" -#define STATUS_DIFF_FILES_CMD "git diff-files -z" -#define STATUS_LIST_OTHER_CMD \ - "git ls-files -z --others --exclude-standard" -#define STATUS_LIST_NO_HEAD_CMD \ - "git ls-files -z --cached --exclude-standard" +static const char *status_diff_index_argv[] = { + "git", "diff-index", "-z", "--diff-filter=ACDMRTXB", + "--cached", "-M", "HEAD", NULL +}; + +static const char *status_diff_files_argv[] = { + "git", "diff-files", "-z", NULL +}; + +static const char *status_list_other_argv[] = { + "git", "ls-files", "-z", "--others", "--exclude-standard", opt_prefix, NULL +}; + +static const char *status_list_no_head_argv[] = { + "git", "ls-files", "-z", "--cached", "--exclude-standard", NULL +}; + +static const char *update_index_argv[] = { + "git", "update-index", "-q", "--unmerged", "--refresh", NULL +}; + +/* Restore the previous line number to stay in the context or select a + * line with something that can be updated. */ +static void +status_restore(struct view *view) +{ + if (view->p_lineno >= view->lines) + view->p_lineno = view->lines - 1; + while (view->p_lineno < view->lines && !view->line[view->p_lineno].data) + view->p_lineno++; + while (view->p_lineno > 0 && !view->line[view->p_lineno].data) + view->p_lineno--; + + /* If the above fails, always skip the "On branch" line. */ + if (view->p_lineno < view->lines) + view->lineno = view->p_lineno; + else + view->lineno = 1; + + if (view->lineno < view->offset) + view->offset = view->lineno; + else if (view->offset + view->height <= view->lineno) + view->offset = view->lineno - view->height + 1; + + view->p_restore = FALSE; +} + +static void +status_update_onbranch(void) +{ + static const char *paths[][2] = { + { "rebase-apply/rebasing", "Rebasing" }, + { "rebase-apply/applying", "Applying mailbox" }, + { "rebase-apply/", "Rebasing mailbox" }, + { "rebase-merge/interactive", "Interactive rebase" }, + { "rebase-merge/", "Rebase merge" }, + { "MERGE_HEAD", "Merging" }, + { "BISECT_LOG", "Bisecting" }, + { "HEAD", "On branch" }, + }; + char buf[SIZEOF_STR]; + struct stat stat; + int i; + + if (is_initial_commit()) { + string_copy(status_onbranch, "Initial commit"); + return; + } + + for (i = 0; i < ARRAY_SIZE(paths); i++) { + char *head = opt_head; + + if (!string_format(buf, "%s/%s", opt_git_dir, paths[i][0]) || + lstat(buf, &stat) < 0) + continue; + + if (!*opt_head) { + struct io io; -#define STATUS_DIFF_INDEX_SHOW_CMD \ - "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s %s 2>/dev/null" + if (io_open(&io, "%s/rebase-merge/head-name", opt_git_dir) && + io_read_buf(&io, buf, sizeof(buf))) { + head = buf; + if (!prefixcmp(head, "refs/heads/")) + head += STRING_SIZE("refs/heads/"); + } + } -#define STATUS_DIFF_FILES_SHOW_CMD \ - "git diff-files --root --patch-with-stat -C -M -- %s %s 2>/dev/null" + if (!string_format(status_onbranch, "%s %s", paths[i][1], head)) + string_copy(status_onbranch, opt_head); + return; + } -#define STATUS_DIFF_NO_HEAD_SHOW_CMD \ - "git diff --no-color --patch-with-stat /dev/null %s 2>/dev/null" + 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 @@ -4042,55 +5704,28 @@ error_out: static bool status_open(struct view *view) { - unsigned long prev_lineno = view->lineno; - reset_view(view); - if (!realloc_lines(view, view->line_size + 7)) - return FALSE; - 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(); - system("git update-index -q --refresh >/dev/null 2>/dev/null"); + io_run_bg(update_index_argv); if (is_initial_commit()) { - if (!status_run(view, STATUS_LIST_NO_HEAD_CMD, 'A', LINE_STAT_STAGED)) + if (!status_run(view, status_list_no_head_argv, 'A', LINE_STAT_STAGED)) return FALSE; - } else if (!status_run(view, STATUS_DIFF_INDEX_CMD, 0, LINE_STAT_STAGED)) { + } else if (!status_run(view, status_diff_index_argv, 0, LINE_STAT_STAGED)) { return FALSE; } - if (!status_run(view, STATUS_DIFF_FILES_CMD, 0, LINE_STAT_UNSTAGED) || - !status_run(view, STATUS_LIST_OTHER_CMD, '?', LINE_STAT_UNTRACKED)) + if (!status_run(view, status_diff_files_argv, 0, LINE_STAT_UNSTAGED) || + !status_run(view, status_list_other_argv, '?', LINE_STAT_UNTRACKED)) return FALSE; - /* If all went well restore the previous line number to stay in - * the context or select a line with something that can be - * updated. */ - if (prev_lineno >= view->lines) - prev_lineno = view->lines - 1; - while (prev_lineno < view->lines && !view->line[prev_lineno].data) - prev_lineno++; - while (prev_lineno > 0 && !view->line[prev_lineno].data) - prev_lineno--; - - /* If the above fails, always skip the "On branch" line. */ - if (prev_lineno < view->lines) - view->lineno = prev_lineno; - else - view->lineno = 1; - - if (view->lineno < view->offset) - view->offset = view->lineno; - else if (view->offset + view->height <= view->lineno) - view->offset = view->lineno - view->height + 1; - + /* Restore the exact position or use the specialized restore + * mode? */ + if (!view->p_restore) + status_restore(view); return TRUE; } @@ -4120,7 +5755,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: @@ -4145,15 +5780,26 @@ 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) { struct status *status = line->data; - char oldpath[SIZEOF_STR] = ""; - char newpath[SIZEOF_STR] = ""; + const char *oldpath = status ? status->old.name : NULL; + /* Diffs for unmerged entries are empty when passing the new + * path, so leave it empty. */ + const char *newpath = status && status->status != 'U' ? status->new.name : NULL; const char *info; - size_t cmdsize = 0; enum open_flags split; + struct view *stage = VIEW(REQ_VIEW_STAGE); if (line->type == LINE_STAT_NONE || (!status && line[1].type == LINE_STAT_NONE)) { @@ -4161,33 +5807,25 @@ status_enter(struct view *view, struct line *line) return REQ_NONE; } - if (status) { - if (sq_quote(oldpath, 0, status->old.name) >= sizeof(oldpath)) - return REQ_QUIT; - /* Diffs for unmerged entries are empty when pasing the - * new path, so leave it empty. */ - if (status->status != 'U' && - sq_quote(newpath, 0, status->new.name) >= sizeof(newpath)) - return REQ_QUIT; - } - - if (opt_cdup[0] && - line->type != LINE_STAT_UNTRACKED && - !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup)) - return REQ_QUIT; - switch (line->type) { case LINE_STAT_STAGED: if (is_initial_commit()) { - if (!string_format_from(opt_cmd, &cmdsize, - STATUS_DIFF_NO_HEAD_SHOW_CMD, - newpath)) - return REQ_QUIT; + const char *no_head_diff_argv[] = { + "git", "diff", "--no-color", "--patch-with-stat", + "--", "/dev/null", newpath, NULL + }; + + if (!prepare_update(stage, no_head_diff_argv, opt_cdup)) + return status_load_error(view, stage, newpath); } else { - if (!string_format_from(opt_cmd, &cmdsize, - STATUS_DIFF_INDEX_SHOW_CMD, - oldpath, newpath)) - return REQ_QUIT; + const char *index_show_argv[] = { + "git", "diff-index", "--root", "--patch-with-stat", + "-C", "-M", "--cached", "HEAD", "--", + oldpath, newpath, NULL + }; + + if (!prepare_update(stage, index_show_argv, opt_cdup)) + return status_load_error(view, stage, newpath); } if (status) @@ -4197,20 +5835,22 @@ status_enter(struct view *view, struct line *line) break; case LINE_STAT_UNSTAGED: - if (!string_format_from(opt_cmd, &cmdsize, - STATUS_DIFF_FILES_SHOW_CMD, oldpath, newpath)) - return REQ_QUIT; + { + const char *files_show_argv[] = { + "git", "diff-files", "--root", "--patch-with-stat", + "-C", "-M", "--", oldpath, newpath, NULL + }; + + if (!prepare_update(stage, files_show_argv, opt_cdup)) + return status_load_error(view, stage, newpath); if (status) info = "Unstaged changes to %s"; else info = "Unstaged changes"; break; - + } case LINE_STAT_UNTRACKED: - if (opt_pipe) - return REQ_QUIT; - - if (!status) { + if (!newpath) { report("No file to show"); return REQ_NONE; } @@ -4220,7 +5860,8 @@ status_enter(struct view *view, struct line *line) return REQ_NONE; } - opt_pipe = fopen(status->new.name, "r"); + if (!prepare_update_file(stage, newpath)) + return status_load_error(view, stage, newpath); info = "Untracked file %s"; break; @@ -4231,8 +5872,8 @@ status_enter(struct view *view, struct line *line) die("line type %d not handled in switch", line->type); } - split = view_is_displayed(view) ? OPEN_SPLIT : 0; - open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | split); + split = view_is_displayed(view) ? OPEN_SPLIT : OPEN_DEFAULT; + open_view(view, REQ_VIEW_STAGE, OPEN_PREPARED | split); if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) { if (status) { stage_status = *status; @@ -4252,54 +5893,57 @@ static bool status_exists(struct status *status, enum line_type type) { struct view *view = VIEW(REQ_VIEW_STATUS); - struct line *line; + unsigned long lineno; - for (line = view->line; line < view->line + view->lines; line++) { + for (lineno = 0; lineno < view->lines; lineno++) { + struct line *line = &view->line[lineno]; struct status *pos = line->data; - if (line->type == type && pos && - !strcmp(status->new.name, pos->new.name)) + if (line->type != type) + continue; + if (!pos && (!status || !status->status) && line[1].data) { + select_view_line(view, lineno); return TRUE; + } + if (pos && !strcmp(status->new.name, pos->new.name)) { + select_view_line(view, lineno); + return TRUE; + } } return FALSE; } -static FILE * -status_update_prepare(enum line_type type) +static bool +status_update_prepare(struct io *io, enum line_type type) { - char cmd[SIZEOF_STR]; - size_t cmdsize = 0; - - if (opt_cdup[0] && - type != LINE_STAT_UNTRACKED && - !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup)) - return NULL; + const char *staged_argv[] = { + "git", "update-index", "-z", "--index-info", NULL + }; + const char *others_argv[] = { + "git", "update-index", "-z", "--add", "--remove", "--stdin", NULL + }; switch (type) { case LINE_STAT_STAGED: - string_add(cmd, cmdsize, "git update-index -z --index-info"); - break; + return io_run(io, IO_WR, opt_cdup, staged_argv); case LINE_STAT_UNSTAGED: case LINE_STAT_UNTRACKED: - string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin"); - break; + return io_run(io, IO_WR, opt_cdup, others_argv); default: die("line type %d not handled in switch", type); + return FALSE; } - - return popen(cmd, "w"); } static bool -status_update_write(FILE *pipe, struct status *status, enum line_type type) +status_update_write(struct io *io, struct status *status, enum line_type type) { char buf[SIZEOF_STR]; size_t bufsize = 0; - size_t written = 0; switch (type) { case LINE_STAT_STAGED: @@ -4320,43 +5964,42 @@ status_update_write(FILE *pipe, struct status *status, enum line_type type) die("line type %d not handled in switch", type); } - while (!ferror(pipe) && written < bufsize) { - written += fwrite(buf + written, 1, bufsize - written, pipe); - } - - return written == bufsize; + return io_write(io, buf, bufsize); } static bool status_update_file(struct status *status, enum line_type type) { - FILE *pipe = status_update_prepare(type); + struct io io; bool result; - if (!pipe) + if (!status_update_prepare(&io, type)) return FALSE; - result = status_update_write(pipe, status, type); - pclose(pipe); - return result; + result = status_update_write(&io, status, type); + return io_done(&io) && result; } static bool status_update_files(struct view *view, struct line *line) { - FILE *pipe = status_update_prepare(line->type); + 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 (!pipe) + if (!status_update_prepare(&io, line->type)) return FALSE; 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) { @@ -4364,12 +6007,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(pipe, line->data, line->type); + result = status_update_write(&io, line->data, line->type); } + string_copy(view->ref, buf); - pclose(pipe); - return result; + return io_done(&io) && result; } static bool @@ -4412,18 +6057,36 @@ status_revert(struct status *status, enum line_type type, bool has_none) } else { report("Cannot revert changes to multiple files"); } - return FALSE; - } else { - char cmd[SIZEOF_STR]; - char file_sq[SIZEOF_STR]; + } else if (prompt_yesno("Are you sure you want to revert changes?")) { + char mode[10] = "100644"; + const char *reset_argv[] = { + "git", "update-index", "--cacheinfo", mode, + status->old.rev, status->old.name, NULL + }; + const char *checkout_argv[] = { + "git", "checkout", "--", status->old.name, NULL + }; + + if (status->status == 'U') { + string_format(mode, "%5o", status->old.mode); + + if (status->old.mode == 0 && status->new.mode == 0) { + reset_argv[2] = "--force-remove"; + reset_argv[3] = status->old.name; + reset_argv[4] = NULL; + } - if (sq_quote(file_sq, 0, status->old.name) >= sizeof(file_sq) || - !string_format(cmd, "git checkout -- %s%s", opt_cdup, file_sq)) - return FALSE; + if (!io_run_fg(reset_argv, opt_cdup)) + return FALSE; + if (status->old.mode == 0 && status->new.mode == 0) + return TRUE; + } - return run_confirm(cmd, "Are you sure you want to overwrite any changes?"); + return io_run_fg(checkout_argv, opt_cdup); } + + return FALSE; } static enum request @@ -4458,22 +6121,19 @@ status_request(struct view *view, enum request request, struct line *line) return REQ_NONE; } - open_editor(status->status != '?', status->new.name); + open_editor(status->new.name); break; case REQ_VIEW_BLAME: - if (status) { - string_copy(opt_file, status->new.name); + if (status) opt_ref[0] = 0; - } return request; case REQ_ENTER: /* 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. */ @@ -4526,42 +6186,27 @@ status_select(struct view *view, struct line *line) if (status && status->status == 'U') { text = "Press %s to resolve conflict in %s"; - key = get_key(REQ_STATUS_MERGE); + key = get_key(KEYMAP_STATUS, REQ_STATUS_MERGE); } else { - key = get_key(REQ_STATUS_UPDATE); + key = get_key(KEYMAP_STATUS, REQ_STATUS_UPDATE); } string_format(view->ref, text, key, file); + if (status) + string_copy(opt_file, status->new.name); } static bool 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; @@ -4569,6 +6214,7 @@ status_grep(struct view *view, struct line *line) static struct view_ops status_ops = { "file", + NULL, status_open, NULL, status_draw, @@ -4579,27 +6225,13 @@ static struct view_ops status_ops = { static bool -stage_diff_line(FILE *pipe, struct line *line) -{ - const char *buf = line->data; - size_t bufsize = strlen(buf); - size_t written = 0; - - while (!ferror(pipe) && written < bufsize) { - written += fwrite(buf + written, 1, bufsize - written, pipe); - } - - fputc('\n', pipe); - - return written == bufsize; -} - -static bool -stage_diff_write(FILE *pipe, struct line *line, struct line *end) +stage_diff_write(struct io *io, struct line *line, struct line *end) { while (line < end) { - if (!stage_diff_line(pipe, line++)) + if (!io_write(io, line->data, strlen(line->data)) || + !io_write(io, "\n", 1)) return FALSE; + line++; if (line->type == LINE_DIFF_CHUNK || line->type == LINE_DIFF_HEADER) break; @@ -4621,35 +6253,32 @@ stage_diff_find(struct view *view, struct line *line, enum line_type type) static bool stage_apply_chunk(struct view *view, struct line *chunk, bool revert) { - char cmd[SIZEOF_STR]; - size_t cmdsize = 0; + const char *apply_argv[SIZEOF_ARG] = { + "git", "apply", "--whitespace=nowarn", NULL + }; struct line *diff_hdr; - FILE *pipe; + struct io io; + int argc = 3; diff_hdr = stage_diff_find(view, chunk, LINE_DIFF_HEADER); if (!diff_hdr) return FALSE; - if (opt_cdup[0] && - !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup)) - return FALSE; - - if (!string_format_from(cmd, &cmdsize, - "git apply --whitespace=nowarn %s %s - && " - "git update-index -q --unmerged --refresh 2>/dev/null", - revert ? "" : "--cached", - revert || stage_line_type == LINE_STAT_STAGED ? "-R" : "")) - return FALSE; - - pipe = popen(cmd, "w"); - if (!pipe) + if (!revert) + apply_argv[argc++] = "--cached"; + if (revert || stage_line_type == LINE_STAT_STAGED) + apply_argv[argc++] = "-R"; + apply_argv[argc++] = "-"; + apply_argv[argc++] = NULL; + if (!io_run(&io, IO_WR, opt_cdup, apply_argv)) return FALSE; - if (!stage_diff_write(pipe, diff_hdr, chunk) || - !stage_diff_write(pipe, chunk, view->line + view->lines)) + if (!stage_diff_write(&io, diff_hdr, chunk) || + !stage_diff_write(&io, chunk, view->line + view->lines)) chunk = NULL; - pclose(pipe); + io_done(&io); + io_run_bg(update_index_argv); return chunk ? TRUE : FALSE; } @@ -4719,21 +6348,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; } } @@ -4766,7 +6389,7 @@ stage_request(struct view *view, enum request request, struct line *line) case REQ_STAGE_NEXT: if (stage_line_type == LINE_STAT_UNTRACKED) { report("File is untracked; press %s to add", - get_key(REQ_STATUS_UPDATE)); + get_key(KEYMAP_STAGE, REQ_STATUS_UPDATE)); return REQ_NONE; } stage_next(view, line); @@ -4780,7 +6403,7 @@ stage_request(struct view *view, enum request request, struct line *line) return REQ_NONE; } - open_editor(stage_status.status != '?', stage_status.new.name); + open_editor(stage_status.new.name); break; case REQ_REFRESH: @@ -4801,12 +6424,15 @@ stage_request(struct view *view, enum request request, struct line *line) return request; } - open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD | OPEN_NOMAXIMIZE); + VIEW(REQ_VIEW_STATUS)->p_restore = TRUE; + open_view(view, REQ_VIEW_STATUS, OPEN_REFRESH); /* Check whether the staged entry still exists, and close the * stage view if it doesn't. */ - if (!status_exists(&stage_status, stage_line_type)) + if (!status_exists(&stage_status, stage_line_type)) { + status_restore(VIEW(REQ_VIEW_STATUS)); return REQ_VIEW_CLOSE; + } if (stage_line_type == LINE_STAT_UNTRACKED) { if (!suffixcmp(stage_status.new.name, -1, "/")) { @@ -4814,7 +6440,10 @@ stage_request(struct view *view, enum request request, struct line *line) return REQ_NONE; } - opt_pipe = fopen(stage_status.new.name, "r"); + if (!prepare_update_file(view, stage_status.new.name)) { + report("Failed to open file: %s", strerror(errno)); + return REQ_NONE; + } } open_view(view, REQ_VIEW_STAGE, OPEN_REFRESH); @@ -4824,6 +6453,7 @@ stage_request(struct view *view, enum request request, struct line *line) static struct view_ops stage_ops = { "line", NULL, + NULL, pager_read, pager_draw, stage_request, @@ -4839,9 +6469,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. */ + struct time time; /* Date from the author ident. */ + struct ref_list *refs; /* Repository references. */ chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */ size_t graph_size; /* The width of the graph array. */ bool has_parents; /* Rewritten --parents seen. */ @@ -4967,9 +6597,7 @@ draw_rev_graph(struct rev_graph *graph) struct rev_filler *filler; size_t i; - if (opt_line_graphics) - fillers[DEFAULT].line = line_graphics[LINE_GRAPHIC_VLINE]; - + fillers[DEFAULT].line = opt_line_graphics ? ACS_VLINE : '|'; filler = &fillers[DEFAULT]; for (i = 0; i < graph->pos; i++) { @@ -5032,7 +6660,7 @@ prepare_rev_graph(struct rev_graph *graph) } static void -update_rev_graph(struct rev_graph *graph) +update_rev_graph(struct view *view, struct rev_graph *graph) { /* If this is the finalizing update ... */ if (graph->commit) @@ -5043,6 +6671,10 @@ update_rev_graph(struct rev_graph *graph) if (!graph->prev->commit) return; + if (view->lines > 2) + view->line[view->lines - 3].dirty = 1; + if (view->lines > 1) + view->line[view->lines - 2].dirty = 1; draw_rev_graph(graph->prev); done_rev_graph(graph->prev->prev); } @@ -5052,19 +6684,23 @@ update_rev_graph(struct rev_graph *graph) * Main view backend */ +static const char *main_argv[SIZEOF_ARG] = { + "git", "log", "--no-color", "--pretty=raw", "--parents", + "--topo-order", "%(head)", NULL +}; + static bool 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)) return TRUE; - if (opt_author && - draw_field(view, LINE_MAIN_AUTHOR, commit->author, opt_author_cols, TRUE)) + if (opt_author && draw_author(view, commit->author)) return TRUE; if (opt_rev_graph && commit->graph_size && @@ -5072,32 +6708,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); @@ -5115,17 +6752,18 @@ main_read(struct view *view, char *line) if (!line) { int i; - if (!view->lines && !view->parent) + if (!view->lines && !view->prev) die("No revisions match the given arguments."); if (view->lines > 0) { commit = view->line[view->lines - 1].data; - if (!*commit->author) { + view->line[view->lines - 1].dirty = 1; + if (!commit->author) { view->lines--; free(commit); graph->commit = NULL; } } - update_rev_graph(graph); + update_rev_graph(view, graph); for (i = 0; i < ARRAY_SIZE(graph_stacks); i++) clear_rev_graph(&graph_stacks[i]); @@ -5145,7 +6783,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); @@ -5169,55 +6807,12 @@ main_read(struct view *view, char *line) break; case LINE_AUTHOR: - { - /* Parse author lines where the name may be empty: - * author 1138474660 +0100 - */ - char *ident = line + STRING_SIZE("author "); - char *nameend = strchr(ident, '<'); - char *emailend = strchr(ident, '>'); - - if (!nameend || !emailend) - break; - - update_rev_graph(graph); + parse_author_line(line + STRING_SIZE("author "), + &commit->author, &commit->time); + update_rev_graph(view, graph); graph = graph->next; - - *nameend = *emailend = 0; - ident = chomp_string(ident); - if (!*ident) { - ident = chomp_string(nameend + 1); - if (!*ident) - ident = "Unknown"; - } - - string_ncopy(commit->author, ident, strlen(ident)); - - /* Parse epoch and timezone */ - if (emailend[1] == ' ') { - char *secs = emailend + 2; - char *zone = strchr(secs, ' '); - time_t time = (time_t) atol(secs); - - if (zone && strlen(zone) == STRING_SIZE(" +0700")) { - long tz; - - zone++; - tz = ('0' - zone[1]) * 60 * 60 * 10; - tz += ('0' - zone[2]) * 60 * 60; - tz += ('0' - zone[3]) * 60; - tz += ('0' - zone[4]) * 60; - - if (zone[0] == '-') - tz = -tz; - - time -= tz; - } - - gmtime_r(&time, &commit->time); - } break; - } + default: /* Fill in the commit title if it has not already been set. */ if (commit->title[0]) @@ -5237,7 +6832,8 @@ main_read(struct view *view, char *line) /* FIXME: More graceful handling of titles; append "..." to * shortened titles, etc. */ - string_ncopy(commit->title, line, strlen(line)); + string_expand(commit->title, sizeof(commit->title), line, 1); + view->line[view->lines - 1].dirty = 1; } return TRUE; @@ -5246,7 +6842,7 @@ main_read(struct view *view, char *line) static enum request main_request(struct view *view, enum request request, struct line *line) { - enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT; + enum open_flags flags = view_is_displayed(view) ? OPEN_SPLIT : OPEN_DEFAULT; switch (request) { case REQ_ENTER: @@ -5264,17 +6860,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; } @@ -5283,42 +6880,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 : "", + mkdate(&commit->time, opt_date), + NULL + }; - return FALSE; + return grep_text(view, text) || grep_refs(commit->refs, view->regex); } static void @@ -5332,6 +6901,7 @@ main_select(struct view *view, struct line *line) static struct view_ops main_ops = { "commit", + main_argv, NULL, main_read, main_draw, @@ -5341,159 +6911,6 @@ static struct view_ops main_ops = { }; -/* - * Unicode / UTF-8 handling - * - * 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. - */ - -/* 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) -{ - if (c >= 0x1100 && - (c <= 0x115f /* Hangul Jamo */ - || c == 0x2329 - || c == 0x232a - || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f) - /* CJK ... Yi */ - || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */ - || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */ - || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */ - || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */ - || (c >= 0xffe0 && c <= 0xffe6) - || (c >= 0x20000 && c <= 0x2fffd) - || (c >= 0x30000 && c <= 0x3fffd))) - return 2; - - if (c == '\t') - return opt_tab_size; - - return 1; -} - -/* Number of bytes used for encoding a UTF-8 character indexed by first byte. - * Illegal bytes are set one. */ -static const unsigned char utf8_bytes[256] = { - 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, - 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, - 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, - 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, - 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, - 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, - 2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2, - 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. */ -static inline unsigned long -utf8_to_unicode(const char *string, size_t length) -{ - unsigned long unicode; - - switch (length) { - case 1: - unicode = string[0]; - break; - case 2: - unicode = (string[0] & 0x1f) << 6; - unicode += (string[1] & 0x3f); - break; - case 3: - unicode = (string[0] & 0x0f) << 12; - unicode += ((string[1] & 0x3f) << 6); - unicode += (string[2] & 0x3f); - break; - case 4: - unicode = (string[0] & 0x0f) << 18; - unicode += ((string[1] & 0x3f) << 12); - unicode += ((string[2] & 0x3f) << 6); - unicode += (string[3] & 0x3f); - break; - case 5: - unicode = (string[0] & 0x0f) << 24; - unicode += ((string[1] & 0x3f) << 18); - unicode += ((string[2] & 0x3f) << 12); - unicode += ((string[3] & 0x3f) << 6); - unicode += (string[4] & 0x3f); - break; - case 6: - unicode = (string[0] & 0x01) << 30; - unicode += ((string[1] & 0x3f) << 24); - unicode += ((string[2] & 0x3f) << 18); - unicode += ((string[3] & 0x3f) << 12); - unicode += ((string[4] & 0x3f) << 6); - unicode += (string[5] & 0x3f); - break; - default: - die("Invalid unicode length"); - } - - /* Invalid characters could return the special 0xfffd value but NUL - * should be just as good. */ - return unicode > 0xffff ? 0 : unicode; -} - -/* Calculates how much of string can be shown within the given maximum width - * and sets trimmed parameter to non-zero value if all of string could not be - * shown. If the reserve flag is TRUE, it will reserve at least one - * trailing character, which can be useful when drawing a delimiter. - * - * 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) -{ - const char *start = string; - const char *end = strchr(string, '\0'); - unsigned char last_bytes = 0; - size_t last_ucwidth = 0; - - *width = 0; - *trimmed = 0; - - while (string < end) { - int c = *(unsigned char *) string; - unsigned char bytes = utf8_bytes[c]; - size_t ucwidth; - unsigned long unicode; - - if (string + bytes > end) - break; - - /* Change representation to figure out whether - * it is a single- or double-width character. */ - - unicode = utf8_to_unicode(string, bytes); - /* FIXME: Graceful handling of invalid unicode character. */ - if (!unicode) - break; - - ucwidth = unicode_width(unicode); - *width += ucwidth; - if (*width > max_width) { - *trimmed = 1; - *width -= ucwidth; - if (reserve && *width == max_width) { - string -= last_bytes; - *width -= last_ucwidth; - } - break; - } - - string += bytes; - last_bytes = bytes; - last_ucwidth = ucwidth; - } - - return string - start; -} - - /* * Status management */ @@ -5501,10 +6918,17 @@ utf8_length(const char *string, int *width, size_t max_width, int *trimmed, bool /* Whether or not the curses interface has been initialized. */ static bool cursed = FALSE; +/* Terminal hacks and workarounds. */ +static bool use_scroll_redrawwin; +static bool use_scroll_status_wclear; + /* The status window is used for polling keystrokes. */ static WINDOW *status_win; -static bool status_empty = TRUE; +/* Reading from the prompt? */ +static bool input_mode = FALSE; + +static bool status_empty = FALSE; /* Update status and title window. */ static void @@ -5536,6 +6960,8 @@ report(const char *msg, ...) va_start(args, msg); wmove(status_win, 0, 0); + if (view->has_scrolled && use_scroll_status_wclear) + wclear(status_win); if (*msg) { vwprintw(status_win, msg, args); status_empty = FALSE; @@ -5543,29 +6969,18 @@ report(const char *msg, ...) status_empty = TRUE; } wclrtoeol(status_win); - wrefresh(status_win); + wnoutrefresh(status_win); va_end(args); } update_view_title(view); - update_display_cursor(view); -} - -/* Controls when nodelay should be in effect when polling user input. */ -static void -set_nonblocking_input(bool loading) -{ - static unsigned int loading_views; - - if ((loading == FALSE && loading_views-- == 1) || - (loading == TRUE && loading_views++ == 0)) - nodelay(status_win, loading); } static void init_display(void) { + const char *term; int x, y; /* Initialize the curses library */ @@ -5583,10 +6998,10 @@ init_display(void) if (!cursed) die("Failed to initialize curses"); - nonl(); /* Tell curses not to do NL->CR/NL on output */ + nonl(); /* Disable conversion and detect newlines from input. */ cbreak(); /* Take input chars one at a time, no wait for \n */ noecho(); /* Don't echo input */ - leaveok(stdscr, TRUE); + leaveok(stdscr, FALSE); if (has_colors()) init_colors(); @@ -5601,53 +7016,132 @@ init_display(void) wbkgdset(status_win, get_line_attr(LINE_STATUS)); TABSIZE = opt_tab_size; - if (opt_line_graphics) { - line_graphics[LINE_GRAPHIC_VLINE] = ACS_VLINE; + + term = getenv("XTERM_VERSION") ? NULL : getenv("COLORTERM"); + if (term && !strcmp(term, "gnome-terminal")) { + /* In the gnome-terminal-emulator, the message from + * scrolling up one line when impossible followed by + * scrolling down one line causes corruption of the + * status line. This is fixed by calling wclear. */ + use_scroll_status_wclear = TRUE; + use_scroll_redrawwin = FALSE; + + } else if (term && !strcmp(term, "xrvt-xpm")) { + /* No problems with full optimizations in xrvt-(unicode) + * and aterm. */ + use_scroll_status_wclear = use_scroll_redrawwin = FALSE; + + } else { + /* When scrolling in (u)xterm the last line in the + * scrolling direction will update slowly. */ + use_scroll_redrawwin = TRUE; + use_scroll_status_wclear = FALSE; } } -static bool -prompt_yesno(const char *prompt) +static int +get_input(int prompt_position) { - enum { WAIT, STOP, CANCEL } status = WAIT; - bool answer = FALSE; - - while (status == WAIT) { - struct view *view; - int i, key; + struct view *view; + int i, key, cursor_y, cursor_x; + bool loading = FALSE; + if (prompt_position) input_mode = TRUE; - foreach_view (view, i) + while (TRUE) { + foreach_view (view, i) { update_view(view); + if (view_is_displayed(view) && view->has_scrolled && + use_scroll_redrawwin) + redrawwin(view->win); + view->has_scrolled = FALSE; + if (view->pipe) + loading = TRUE; + } - input_mode = FALSE; - - mvwprintw(status_win, 0, 0, "%s [Yy]/[Nn]", prompt); - wclrtoeol(status_win); + /* Update the cursor position. */ + if (prompt_position) { + getbegyx(status_win, cursor_y, cursor_x); + cursor_x = prompt_position; + } else { + view = display[current_view]; + getbegyx(view->win, cursor_y, cursor_x); + cursor_x = view->width - 1; + cursor_y += view->lineno - view->offset; + } + setsyx(cursor_y, cursor_x); /* Refresh, accept single keystroke of input */ + doupdate(); + nodelay(status_win, loading); key = wgetch(status_win); + + /* wgetch() with nodelay() enabled returns ERR when + * there's no input. */ + if (key == ERR) { + + } else if (key == KEY_RESIZE) { + int height, width; + + getmaxyx(stdscr, height, width); + + wresize(status_win, 1, width); + mvwin(status_win, height - 1, 0); + wnoutrefresh(status_win); + resize_display(); + redraw_display(TRUE); + + } else { + input_mode = FALSE; + return key; + } + } +} + +static char * +prompt_input(const char *prompt, input_handler handler, void *data) +{ + enum input_status status = INPUT_OK; + static char buf[SIZEOF_STR]; + size_t pos = 0; + + buf[pos] = 0; + + while (status == INPUT_OK || status == INPUT_SKIP) { + int key; + + mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf); + wclrtoeol(status_win); + + key = get_input(pos + 1); switch (key) { - case ERR: + case KEY_RETURN: + case KEY_ENTER: + case '\n': + status = pos ? INPUT_STOP : INPUT_CANCEL; break; - case 'y': - case 'Y': - answer = TRUE; - status = STOP; + case KEY_BACKSPACE: + if (pos > 0) + buf[--pos] = 0; + else + status = INPUT_CANCEL; break; case KEY_ESC: - case KEY_RETURN: - case KEY_ENTER: - case KEY_BACKSPACE: - case 'n': - case 'N': - case '\n': + status = INPUT_CANCEL; + break; + default: - answer = FALSE; - status = CANCEL; + if (pos >= sizeof(buf)) { + report("Input string too long"); + return NULL; + } + + status = handler(data, buf, key); + if (status == INPUT_OK) + buf[pos++] = (char) key; } } @@ -5655,61 +7149,98 @@ prompt_yesno(const char *prompt) status_empty = FALSE; report(""); - return answer; + if (status == INPUT_CANCEL) + return NULL; + + buf[pos++] = 0; + + return buf; +} + +static enum input_status +prompt_yesno_handler(void *data, char *buf, int c) +{ + if (c == 'y' || c == 'Y') + return INPUT_STOP; + if (c == 'n' || c == 'N') + return INPUT_CANCEL; + return INPUT_SKIP; +} + +static bool +prompt_yesno(const char *prompt) +{ + char prompt2[SIZEOF_STR]; + + if (!string_format(prompt2, "%s [Yy/Nn]", prompt)) + return FALSE; + + return !!prompt_input(prompt2, prompt_yesno_handler, NULL); +} + +static enum input_status +read_prompt_handler(void *data, char *buf, int c) +{ + return isprint(c) ? INPUT_OK : INPUT_SKIP; } static char * read_prompt(const char *prompt) { - enum { READING, STOP, CANCEL } status = READING; - static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")]; - int pos = 0; - - while (status == READING) { - struct view *view; - int i, key; + return prompt_input(prompt, read_prompt_handler, NULL); +} - input_mode = TRUE; +static bool prompt_menu(const char *prompt, const struct menu_item *items, int *selected) +{ + enum input_status status = INPUT_OK; + int size = 0; - foreach_view (view, i) - update_view(view); + while (items[size].text) + size++; - input_mode = FALSE; + while (status == INPUT_OK) { + const struct menu_item *item = &items[*selected]; + int key; + int i; - mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf); + 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); - /* Refresh, accept single keystroke of input */ - key = wgetch(status_win); + key = get_input(COLS - 1); switch (key) { case KEY_RETURN: case KEY_ENTER: case '\n': - status = pos ? STOP : CANCEL; + status = INPUT_STOP; break; - case KEY_BACKSPACE: - if (pos > 0) - pos--; - else - status = CANCEL; + case KEY_LEFT: + case KEY_UP: + *selected = *selected - 1; + if (*selected < 0) + *selected = size - 1; break; - case KEY_ESC: - status = CANCEL; + case KEY_RIGHT: + case KEY_DOWN: + *selected = (*selected + 1) % size; break; - case ERR: + case KEY_ESC: + status = INPUT_CANCEL; break; default: - if (pos >= sizeof(buf)) { - report("Input string too long"); - return NULL; - } - - if (isprint(key)) - buf[pos++] = (char) key; + for (i = 0; items[i].text; i++) + if (items[i].hotkey == key) { + *selected = i; + status = INPUT_STOP; + break; + } } } @@ -5717,26 +7248,23 @@ read_prompt(const char *prompt) status_empty = FALSE; report(""); - if (status == CANCEL) - return NULL; - - buf[pos++] = 0; - - return buf; + return status != INPUT_CANCEL; } /* - * Repository references + * Repository properties */ -static struct ref *refs = NULL; -static size_t refs_alloc = 0; +static struct ref **refs = NULL; static size_t refs_size = 0; +static struct ref *refs_head = NULL; + +static struct ref_list **ref_lists = NULL; +static size_t ref_lists_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; +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_) @@ -5757,76 +7285,69 @@ 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, const 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++) + if (!visitor(data, refs[i])) + break; +} - for (i = 0; i < refs_size; i++) { - struct ref **tmp; +static struct ref * +get_ref_head() +{ + return refs_head; +} - 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; } @@ -5844,34 +7365,49 @@ read_ref(char *id, size_t idlen, char *name, size_t namelen) } else if (!prefixcmp(name, "refs/heads/")) { namelen -= STRING_SIZE("refs/heads/"); name += STRING_SIZE("refs/heads/"); - head = !strncmp(opt_head, name, namelen); + if (!strncmp(opt_head, name, namelen)) + return OK; } else if (!strcmp(name, "HEAD")) { - string_ncopy(opt_head_rev, id, idlen); - return OK; + head = TRUE; + if (*opt_head) { + namelen = strlen(opt_head); + name = opt_head; + } } - 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; @@ -5879,24 +7415,129 @@ read_ref(char *id, size_t idlen, char *name, size_t namelen) ref->tracked = tracked; string_copy_rev(ref->id, id); + if (head) + refs_head = ref; return OK; } static int load_refs(void) { - const char *cmd_env = getenv("TIG_LS_REMOTE"); - const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE; + const char *head_argv[] = { + "git", "symbolic-ref", "HEAD", NULL + }; + static const char *ls_remote_argv[SIZEOF_ARG] = { + "git", "ls-remote", opt_git_dir, NULL + }; + static bool init = FALSE; + size_t i; + + if (!init) { + if (!argv_from_env(ls_remote_argv, "TIG_LS_REMOTE")) + die("TIG_LS_REMOTE contains too many arguments"); + init = TRUE; + } 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 (io_run_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); + } + + refs_head = NULL; + for (i = 0; i < refs_size; i++) + refs[i]->id[0] = 0; + + if (io_run_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 read_properties(popen(cmd, "r"), "\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 @@ -5905,30 +7546,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; } @@ -5936,8 +7571,9 @@ read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen static int load_git_config(void) { - return read_properties(popen("git " GIT_CONFIG " --list", "r"), - "=", read_repo_config_option); + const char *config_list_argv[] = { "git", "config", "--list", NULL }; + + return io_run_load(config_list_argv, "=", read_repo_config_option); } static int @@ -5954,14 +7590,11 @@ read_repo_info(char *name, size_t namelen, char *value, size_t valuelen) * Default to true for the unknown case. */ opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE; - } else if (opt_cdup[0] == ' ') { + } else if (*name == '.') { string_ncopy(opt_cdup, name, namelen); + } else { - if (!prefixcmp(name, "refs/heads/")) { - namelen -= STRING_SIZE("refs/heads/"); - name += STRING_SIZE("refs/heads/"); - string_ncopy(opt_head, name, namelen); - } + string_ncopy(opt_prefix, name, namelen); } return OK; @@ -5970,60 +7603,12 @@ read_repo_info(char *name, size_t namelen, char *value, size_t valuelen) static int load_repo_info(void) { - int result; - FILE *pipe = popen("(git rev-parse --git-dir --is-inside-work-tree " - " --show-cdup; git symbolic-ref HEAD) 2>/dev/null", "r"); - - /* XXX: The line outputted by "--show-cdup" can be empty so - * initialize it to something invalid to make it possible to - * detect whether it has been set or not. */ - opt_cdup[0] = ' '; - - result = read_properties(pipe, "=", read_repo_info); - if (opt_cdup[0] == ' ') - opt_cdup[0] = 0; - - return result; -} - -static int -read_properties(FILE *pipe, const char *separators, - int (*read_property)(char *, size_t, char *, size_t)) -{ - char buffer[BUFSIZ]; - char *name; - int state = OK; - - if (!pipe) - return ERR; - - while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) { - char *value; - size_t namelen; - size_t valuelen; - - name = chomp_string(name); - namelen = strcspn(name, separators); - - if (name[namelen]) { - name[namelen] = 0; - value = chomp_string(name + namelen + 1); - valuelen = strlen(value); - - } else { - value = ""; - valuelen = 0; - } - - state = read_property(name, namelen, value, valuelen); - } - - if (state != ERR && ferror(pipe)) - state = ERR; - - pclose(pipe); + const char *rev_parse_argv[] = { + "git", "rev-parse", "--git-dir", "--is-inside-work-tree", + "--show-cdup", "--show-prefix", NULL + }; - return state; + return io_run_load(rev_parse_argv, "=", read_repo_info); } @@ -6031,6 +7616,19 @@ read_properties(FILE *pipe, const char *separators, * Main */ +static const char usage[] = +"tig " TIG_VERSION " (" __DATE__ ")\n" +"\n" +"Usage: tig [options] [revs] [--] [paths]\n" +" or: tig show [options] [revs] [--] [paths]\n" +" or: tig blame [rev] path\n" +" or: tig status\n" +" or: tig < [git command output]\n" +"\n" +"Options:\n" +" -v, --version Show version and exit\n" +" -h, --help Show help message and exit"; + static void __NORETURN quit(int sig) { @@ -6068,19 +7666,98 @@ warn(const char *msg, ...) va_end(args); } +static enum request +parse_options(int argc, const char *argv[]) +{ + enum request request = REQ_VIEW_MAIN; + const char *subcommand; + bool seen_dashdash = FALSE; + /* XXX: This is vulnerable to the user overriding options + * required for the main view parser. */ + const char *custom_argv[SIZEOF_ARG] = { + "git", "log", "--no-color", "--pretty=raw", "--parents", + "--topo-order", NULL + }; + int i, j = 6; + + if (!isatty(STDIN_FILENO)) { + io_open(&VIEW(REQ_VIEW_PAGER)->io, ""); + return REQ_VIEW_PAGER; + } + + if (argc <= 1) + return REQ_NONE; + + subcommand = argv[1]; + if (!strcmp(subcommand, "status")) { + if (argc > 2) + warn("ignoring arguments after `%s'", subcommand); + return REQ_VIEW_STATUS; + + } else if (!strcmp(subcommand, "blame")) { + if (argc <= 2 || argc > 4) + die("invalid number of options to blame\n\n%s", usage); + + i = 2; + if (argc == 4) { + string_ncopy(opt_ref, argv[i], strlen(argv[i])); + i++; + } + + string_ncopy(opt_file, argv[i], strlen(argv[i])); + return REQ_VIEW_BLAME; + + } else if (!strcmp(subcommand, "show")) { + request = REQ_VIEW_DIFF; + + } else { + subcommand = NULL; + } + + if (subcommand) { + custom_argv[1] = subcommand; + j = 2; + } + + for (i = 1 + !!subcommand; i < argc; i++) { + const char *opt = argv[i]; + + if (seen_dashdash || !strcmp(opt, "--")) { + seen_dashdash = TRUE; + + } else if (!strcmp(opt, "-v") || !strcmp(opt, "--version")) { + printf("tig version %s\n", TIG_VERSION); + quit(0); + + } else if (!strcmp(opt, "-h") || !strcmp(opt, "--help")) { + printf("%s\n", usage); + quit(0); + } + + custom_argv[j++] = opt; + if (j >= ARRAY_SIZE(custom_argv)) + die("command too long"); + } + + if (!prepare_update(VIEW(request), custom_argv, NULL)) + die("Failed to format arguments"); + + return request; +} + int main(int argc, const char *argv[]) { + const char *codeset = "UTF-8"; + enum request request = parse_options(argc, argv); struct view *view; - enum request request; size_t i; signal(SIGINT, quit); + signal(SIGPIPE, SIG_IGN); if (setlocale(LC_ALL, "")) { - char *codeset = nl_langinfo(CODESET); - - string_ncopy(opt_codeset, codeset, strlen(codeset)); + codeset = nl_langinfo(CODESET); } if (load_repo_info() == ERR) @@ -6092,20 +7769,19 @@ main(int argc, const char *argv[]) if (load_git_config() == ERR) die("Failed to load repo config."); - request = parse_options(argc, argv); - if (request == REQ_NONE) - return 0; - /* Require a git repository unless when running in pager mode. */ if (!opt_git_dir[0] && request != REQ_VIEW_PAGER) die("Not a git repository"); - if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8")) - opt_utf8 = FALSE; + if (*opt_encoding && strcmp(codeset, "UTF-8")) { + opt_iconv_in = iconv_open("UTF-8", opt_encoding); + if (opt_iconv_in == ICONV_NONE) + die("Failed to initialize character set conversion"); + } - if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) { - opt_iconv = iconv_open(opt_codeset, opt_encoding); - if (opt_iconv == ICONV_NONE) + if (codeset && strcmp(codeset, "UTF-8")) { + opt_iconv_out = iconv_open(codeset, "UTF-8"); + if (opt_iconv_out == ICONV_NONE) die("Failed to initialize character set conversion"); } @@ -6113,46 +7789,60 @@ main(int argc, const char *argv[]) die("Failed to load refs."); foreach_view (view, i) - view->cmd_env = getenv(view->cmd_env); + if (!argv_from_env(view->ops->argv, view->cmd_env)) + die("Too many arguments in the `%s` environment variable", + view->cmd_env); init_display(); + if (request != REQ_NONE) + open_view(NULL, request, OPEN_PREPARED); + request = request == REQ_NONE ? REQ_VIEW_MAIN : REQ_NONE; + while (view_driver(display[current_view], request)) { - int key; - int i; + int key = get_input(0); - foreach_view (view, i) - update_view(view); view = display[current_view]; - - /* Refresh, accept single keystroke of input */ - key = wgetch(status_win); - - /* wgetch() with nodelay() enabled returns ERR when there's no - * input. */ - if (key == ERR) { - request = REQ_NONE; - continue; - } - request = get_keybinding(view->keymap, key); /* Some low-level request handling. This keeps access to * status_win restricted. */ switch (request) { + case REQ_NONE: + report("Unknown key, press %s for help", + get_key(view->keymap, REQ_VIEW_HELP)); + break; case REQ_PROMPT: { char *cmd = read_prompt(":"); - if (cmd && string_format(opt_cmd, "git %s", cmd)) { - if (strncmp(cmd, "show", 4) && isspace(cmd[4])) { - request = REQ_VIEW_DIFF; + 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 { - request = REQ_VIEW_PAGER; + report("Unable to parse '%s' as a line number", cmd); } - /* Always reload^Wrerun commands from the prompt. */ - open_view(view, request, OPEN_RELOAD); + } else if (cmd) { + struct view *next = VIEW(REQ_VIEW_PAGER); + const char *argv[SIZEOF_ARG] = { "git" }; + int argc = 1; + + /* When running random commands, initially show the + * command in the title. However, it maybe later be + * overwritten if a commit line is selected. */ + string_ncopy(next->ref, cmd, strlen(cmd)); + + if (!argv_from_string(argv, &argc, cmd)) { + report("Too many arguments"); + } else if (!prepare_update(next, argv, NULL)) { + report("Failed to format command"); + } else { + open_view(view, REQ_VIEW_PAGER, OPEN_PREPARED); + } } request = REQ_NONE; @@ -6166,23 +7856,14 @@ main(int argc, const char *argv[]) if (search) string_ncopy(opt_search, search, strlen(search)); + else if (*opt_search) + request = request == REQ_SEARCH ? + REQ_FIND_NEXT : + REQ_FIND_PREV; else request = REQ_NONE; break; } - case REQ_SCREEN_RESIZE: - { - int height, width; - - getmaxyx(stdscr, height, width); - - /* Resize the status view and let the view driver take - * care of resizing the displayed views. */ - wresize(status_win, 1, width); - mvwin(status_win, height - 1, 0); - wrefresh(status_win); - break; - } default: break; }