X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=tig.c;h=e60b717c90d0d4f9be73f76aee418c214fce4731;hb=e4a8d2c0d050d3c53e1aa3846f2ffc2a981ffa65;hp=725ace02579782ab55d4b511cfd89c8c8fc2f19d;hpb=7a6f2dd185946eaaa38373c92667631edecd76ab;p=tig.git diff --git a/tig.c b/tig.c index 725ace0..e60b717 100644 --- a/tig.c +++ b/tig.c @@ -1,4 +1,4 @@ -/* Copyright (c) 2006-2009 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 @@ -11,143 +11,35 @@ * GNU General Public License for more details. */ -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#ifndef TIG_VERSION -#define TIG_VERSION "unknown-version" -#endif - -#ifndef DEBUG -#define NDEBUG -#endif - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include -#include -#include - -/* ncurses(3): Must be defined to have extended wide-character functions. */ -#define _XOPEN_SOURCE_EXTENDED - -#ifdef HAVE_NCURSESW_NCURSES_H -#include -#else -#ifdef HAVE_NCURSES_NCURSES_H -#include -#else -#include -#endif -#endif - -#if __GNUC__ >= 3 -#define __NORETURN __attribute__((__noreturn__)) -#else -#define __NORETURN -#endif +#include "tig.h" +#include "io.h" +#include "graph.h" static void __NORETURN die(const char *err, ...); static void warn(const char *msg, ...); static void report(const char *msg, ...); -static void set_nonblocking_input(bool loading); -static int load_refs(void); -static size_t utf8_length(const char **string, size_t col, int *width, size_t max_width, int *trimmed, bool reserve); - -#define ABS(x) ((x) >= 0 ? (x) : -(x)) -#define MIN(x, y) ((x) < (y) ? (x) : (y)) - -#define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0])) -#define STRING_SIZE(x) (sizeof(x) - 1) - -#define SIZEOF_STR 1024 /* Default string size. */ -#define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */ -#define SIZEOF_REV 41 /* Holds a SHA-1 and an ending NUL. */ -#define SIZEOF_ARG 32 /* Default argument array size. */ - -/* Revision graph */ - -#define REVGRAPH_INIT 'I' -#define REVGRAPH_MERGE 'M' -#define REVGRAPH_BRANCH '+' -#define REVGRAPH_COMMIT '*' -#define REVGRAPH_BOUND '^' - -#define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */ - -/* This color name can be used to refer to the default term colors. */ -#define COLOR_DEFAULT (-1) - -#define ICONV_NONE ((iconv_t) -1) -#ifndef ICONV_CONST -#define ICONV_CONST /* nothing */ -#endif - -/* 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 AUTHOR_COLS 20 -#define ID_COLS 8 - -/* The default interval between line numbers. */ -#define NUMBER_INTERVAL 5 - -#define TAB_SIZE 8 - -#define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3) - -#define NULL_ID "0000000000000000000000000000000000000000" - -#define S_ISGITLINK(mode) (((mode) & S_IFMT) == 0160000) - -#ifndef GIT_CONFIG -#define GIT_CONFIG "config" -#endif - -/* Some ASCII-shorthands fitted into the ncurses namespace. */ -#define KEY_TAB '\t' -#define KEY_RETURN '\r' -#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); - -enum format_flags { - FORMAT_ALL, /* Perform replacement in all arguments. */ - FORMAT_DASH, /* Perform replacement up until "--". */ - FORMAT_NONE /* No replacement should be performed. */ +struct ref_list { + char id[SIZEOF_REV]; /* Commit SHA1 ID */ + size_t size; /* Number of refs. */ + struct ref **refs; /* References for this ID. */ }; -static bool format_argv(const char *dst[], const char *src[], enum format_flags flags); +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, @@ -161,576 +53,190 @@ typedef enum input_status (*input_handler)(void *data, char *buf, int c); static char *prompt_input(const char *prompt, input_handler handler, void *data); static bool prompt_yesno(const char *prompt); -/* - * String helpers - */ - -static inline void -string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen) -{ - if (srclen > dstlen - 1) - srclen = dstlen - 1; - - strncpy(dst, src, srclen); - dst[srclen] = 0; -} - -/* Shorthands for safely copying into a fixed buffer. */ - -#define string_copy(dst, src) \ - string_ncopy_do(dst, sizeof(dst), src, sizeof(src)) - -#define string_ncopy(dst, src, srclen) \ - string_ncopy_do(dst, sizeof(dst), src, srclen) - -#define string_copy_rev(dst, src) \ - string_ncopy_do(dst, SIZEOF_REV, src, SIZEOF_REV - 1) - -#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) -{ - int namelen; - - while (isspace(*name)) - name++; - - namelen = strlen(name) - 1; - while (namelen > 0 && isspace(name[namelen])) - name[namelen--] = 0; - - return name; -} - -static bool -string_nformat(char *buf, size_t bufsize, size_t *bufpos, const char *fmt, ...) -{ - va_list args; - size_t pos = bufpos ? *bufpos : 0; - - va_start(args, fmt); - pos += vsnprintf(buf + pos, bufsize - pos, fmt, args); - va_end(args); - - if (bufpos) - *bufpos = pos; - - return pos >= bufsize ? FALSE : TRUE; -} - -#define string_format(buf, fmt, args...) \ - string_nformat(buf, sizeof(buf), NULL, fmt, args) - -#define string_format_from(buf, from, fmt, args...) \ - string_nformat(buf, sizeof(buf), from, fmt, args) - -static int -string_enum_compare(const char *str1, const char *str2, int len) -{ - size_t i; - -#define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.') - - /* Diff-Header == DIFF_HEADER */ - for (i = 0; i < len; i++) { - if (toupper(str1[i]) == toupper(str2[i])) - continue; - - if (string_enum_sep(str1[i]) && - string_enum_sep(str2[i])) - continue; - - return str1[i] - str2[i]; - } - - return 0; -} - -struct enum_map { - const char *name; - int namelen; - int value; +struct menu_item { + int hotkey; + const char *text; + void *data; }; -#define ENUM_MAP(name, value) { name, STRING_SIZE(name), value } +static bool prompt_menu(const char *prompt, const struct menu_item *items, int *selected); -static bool -map_enum_do(const struct enum_map *map, size_t map_size, int *value, const char *name) -{ - size_t namelen = strlen(name); - int i; - - for (i = 0; i < map_size; i++) - if (namelen == map[i].namelen && - !string_enum_compare(name, map[i].name, namelen)) { - *value = map[i].value; - return TRUE; - } - - return FALSE; -} - -#define map_enum(attr, map, name) \ - map_enum_do(map, ARRAY_SIZE(map), attr, name) - -#define prefixcmp(str1, str2) \ - strncmp(str1, str2, STRING_SIZE(str2)) - -static inline int -suffixcmp(const char *str, int slen, const char *suffix) -{ - size_t len = slen >= 0 ? slen : strlen(str); - size_t suffixlen = strlen(suffix); - - return suffixlen < len ? strcmp(str + len - suffixlen, suffix) : -1; -} - - -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 void -argv_from_env(const char **argv, const char *name) -{ - char *env = argv ? getenv(name) : NULL; - int argc = 0; - - if (env && *env) - env = strdup(env); - if (env && !argv_from_string(argv, &argc, env)) - die("Too many arguments in the `%s` environment variable", name); -} - - -/* - * Executing external commands. - */ - -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. */ +enum graphic { + GRAPHIC_ASCII = 0, + GRAPHIC_DEFAULT, + GRAPHIC_UTF8 }; -struct io { - enum io_type type; /* The requested type of pipe. */ - const char *dir; /* Directory from which to execute. */ - pid_t pid; /* Pipe for reading or writing. */ - int pipe; /* Pipe end for reading or writing. */ - int error; /* Error status. */ - const char *argv[SIZEOF_ARG]; /* Shell command arguments. */ - 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. */ +static const struct enum_map graphic_map[] = { +#define GRAPHIC_(name) ENUM_MAP(#name, GRAPHIC_##name) + GRAPHIC_(ASCII), + GRAPHIC_(DEFAULT), + GRAPHIC_(UTF8) +#undef GRAPHIC_ }; -static void -reset_io(struct io *io) -{ - io->pipe = -1; - io->pid = 0; - io->buf = io->bufpos = NULL; - io->bufalloc = io->bufsize = 0; - io->error = 0; - io->eof = 0; -} - -static void -init_io(struct io *io, const char *dir, enum io_type type) -{ - reset_io(io); - io->type = type; - io->dir = dir; -} +#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 bool -init_io_rd(struct io *io, const char *argv[], const char *dir, - enum format_flags flags) -{ - init_io(io, dir, IO_RD); - return format_argv(io->argv, argv, flags); -} +static const struct enum_map date_map[] = { +#define DATE_(name) ENUM_MAP(#name, DATE_##name) + DATE_INFO +#undef DATE_ +}; -static bool -io_open(struct io *io, const char *name) -{ - init_io(io, NULL, IO_FD); - io->pipe = *name ? open(name, O_RDONLY) : STDIN_FILENO; - if (io->pipe == -1) - io->error = errno; - return io->pipe != -1; -} +struct time { + time_t sec; + int tz; +}; -static bool -kill_io(struct io *io) +static inline int timecmp(const struct time *t1, const struct time *t2) { - return io->pid == 0 || kill(io->pid, SIGKILL) != -1; + return t1->sec - t2->sec; } -static bool -done_io(struct io *io) -{ - pid_t pid = io->pid; +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; - if (io->pipe != -1) - close(io->pipe); - free(io->buf); - reset_io(io); + if (!date || !time || !time->sec) + return ""; - while (pid > 0) { - int status; - pid_t waiting = waitpid(pid, &status, 0); + if (date == DATE_RELATIVE) { + struct timeval now; + time_t date = time->sec + time->tz; + time_t seconds; + int i; - if (waiting < 0) { - if (errno == EINTR) + 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; - report("waitpid failed (%s)", strerror(errno)); - return FALSE; - } - - return waiting == pid && - !WIFSIGNALED(status) && - WIFEXITED(status) && - !WEXITSTATUS(status); - } - - return TRUE; -} - -static bool -start_io(struct io *io) -{ - int pipefds[2] = { -1, -1 }; - - if (io->type == IO_FD) - return TRUE; - if ((io->type == IO_RD || io->type == IO_WR) && - pipe(pipefds) < 0) - return FALSE; - else if (io->type == IO_AP) - pipefds[1] = io->pipe; - - if ((io->pid = fork())) { - if (pipefds[!(io->type == IO_WR)] != -1) - close(pipefds[!(io->type == IO_WR)]); - if (io->pid != -1) { - io->pipe = pipefds[!!(io->type == IO_WR)]; - return TRUE; - } - - } else { - if (io->type != IO_FG) { - int devnull = open("/dev/null", O_RDWR); - int readfd = io->type == IO_WR ? pipefds[0] : devnull; - int writefd = (io->type == IO_RD || io->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]); + 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 (io->dir && *io->dir && chdir(io->dir) == -1) - die("Failed to change directory: %s", strerror(errno)); - - execvp(io->argv[0], (char *const*) io->argv); - die("Failed to execute program: %s", strerror(errno)); } - if (pipefds[!!(io->type == IO_WR)] != -1) - close(pipefds[!!(io->type == IO_WR)]); - return FALSE; -} - -static bool -run_io(struct io *io, const char **argv, const char *dir, enum io_type type) -{ - init_io(io, dir, type); - if (!format_argv(io->argv, argv, FORMAT_NONE)) - return FALSE; - return start_io(io); -} - -static int -run_io_do(struct io *io) -{ - return start_io(io) && done_io(io); -} - -static int -run_io_bg(const char **argv) -{ - struct io io = {}; - - init_io(&io, NULL, IO_BG); - if (!format_argv(io.argv, argv, FORMAT_NONE)) - return FALSE; - return run_io_do(&io); -} - -static bool -run_io_fg(const char **argv, const char *dir) -{ - struct io io = {}; - - init_io(&io, dir, IO_FG); - if (!format_argv(io.argv, argv, FORMAT_NONE)) - return FALSE; - return run_io_do(&io); -} - -static bool -run_io_append(const char **argv, enum format_flags flags, int fd) -{ - struct io io = {}; - - init_io(&io, NULL, IO_AP); - io.pipe = fd; - if (format_argv(io.argv, argv, flags)) - return run_io_do(&io); - close(fd); - return FALSE; -} - -static bool -run_io_rd(struct io *io, const char **argv, enum format_flags flags) -{ - return init_io_rd(io, argv, NULL, flags) && start_io(io); -} - -static bool -io_eof(struct io *io) -{ - return io->eof; -} - -static int -io_error(struct io *io) -{ - return io->error; + 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; } -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; +#define AUTHOR_VALUES \ + AUTHOR_(NO), \ + AUTHOR_(FULL), \ + AUTHOR_(ABBREVIATED) - FD_ZERO(&fds); - FD_SET(io->pipe, &fds); +enum author { +#define AUTHOR_(name) AUTHOR_##name + AUTHOR_VALUES, +#undef AUTHOR_ + AUTHOR_DEFAULT = AUTHOR_FULL +}; - return select(io->pipe + 1, &fds, NULL, NULL, &tv) > 0; -} +static const struct enum_map author_map[] = { +#define AUTHOR_(name) ENUM_MAP(#name, AUTHOR_##name) + AUTHOR_VALUES +#undef AUTHOR_ +}; -static ssize_t -io_read(struct io *io, void *buf, size_t bufsize) +static const char * +get_author_initials(const char *author) { - do { - ssize_t readsize = read(io->pipe, buf, bufsize); + static char initials[AUTHOR_COLS * 6 + 1]; + size_t pos = 0; + const char *end = strchr(author, '\0'); - 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 is_initial_sep(c) (isspace(c) || ispunct(c) || (c) == '@' || (c) == '-') -static char * -io_get(struct io *io, int c, bool can_read) -{ - char *eol; - ssize_t readsize; + memset(initials, 0, sizeof(initials)); + while (author < end) { + unsigned char bytes; + size_t i; - if (!io->buf) { - io->buf = io->bufpos = malloc(BUFSIZ); - if (!io->buf) - return NULL; - io->bufalloc = BUFSIZ; - io->bufsize = 0; - } + while (is_initial_sep(*author)) + author++; - 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; + bytes = utf8_char_length(author, end); + if (bytes < sizeof(initials) - 1 - pos) { + while (bytes--) { + initials[pos++] = *author++; } } - if (io_eof(io)) { - if (io->bufsize) { - io->bufpos[io->bufsize] = 0; - io->bufsize = 0; - return io->bufpos; - } - return NULL; + for (i = pos; author < end && !is_initial_sep(*author); author++) { + if (i < sizeof(initials) - 1) + initials[i++] = *author; } - if (!can_read) - return NULL; - - if (io->bufsize > 0 && io->bufpos > io->buf) - memmove(io->buf, io->bufpos, io->bufsize); - - 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; + initials[i++] = 0; } - return written == bufsize; + return initials; } -static bool -io_read_buf(struct io *io, char buf[], size_t bufsize) -{ - bool error; - - io->buf = io->bufpos = buf; - io->bufalloc = bufsize; - error = !io_get(io, '\n', TRUE) && io_error(io); - io->buf = NULL; - - return done_io(io) || error; -} +#define author_trim(cols) (cols == 0 || cols > 5) -static bool -run_io_buf(const char **argv, char buf[], size_t bufsize) -{ - struct io io = {}; - - return run_io_rd(&io, argv, FORMAT_NONE) && io_read_buf(&io, buf, bufsize); -} - -static int -io_load(struct io *io, const char *separators, - int (*read_property)(char *, size_t, char *, size_t)) +static const char * +mkauthor(const char *text, int cols, enum author author) { - char *name; - int state = OK; - - if (!start_io(io)) - return ERR; - - while (state == OK && (name = io_get(io, '\n', TRUE))) { - char *value; - size_t namelen; - size_t valuelen; - - name = chomp_string(name); - namelen = strcspn(name, separators); - - if (name[namelen]) { - name[namelen] = 0; - value = chomp_string(name + namelen + 1); - valuelen = strlen(value); - - } else { - value = ""; - valuelen = 0; - } - - state = read_property(name, namelen, value, valuelen); - } + bool trim = author_trim(cols); + bool abbreviate = author == AUTHOR_ABBREVIATED || !trim; - if (state != ERR && io_error(io)) - state = ERR; - done_io(io); - - return state; + if (!author) + return ""; + if (abbreviate && text) + return get_author_initials(text); + return text; } -static int -run_io_load(const char **argv, const char *separators, - int (*read_property)(char *, size_t, char *, size_t)) +static const char * +mkmode(mode_t mode) { - struct io io = {}; - - return init_io_rd(&io, argv, NULL, FORMAT_NONE) - ? io_load(&io, separators, read_property) : ERR; + if (S_ISDIR(mode)) + return "drwxr-xr-x"; + else if (S_ISLNK(mode)) + return "lrwxrwxrwx"; + else if (S_ISGITLINK(mode)) + return "m---------"; + else if (S_ISREG(mode) && mode & S_IXUSR) + return "-rwxr-xr-x"; + else if (S_ISREG(mode)) + return "-rw-r--r--"; + else + return "----------"; } @@ -747,6 +253,7 @@ run_io_load(const char **argv, const char *separators, REQ_(VIEW_TREE, "Show tree view"), \ REQ_(VIEW_BLOB, "Show blob view"), \ REQ_(VIEW_BLAME, "Show blame view"), \ + REQ_(VIEW_BRANCH, "Show branch view"), \ REQ_(VIEW_HELP, "Show help page"), \ REQ_(VIEW_PAGER, "Show pager view"), \ REQ_(VIEW_STATUS, "Show status view"), \ @@ -778,6 +285,7 @@ run_io_load(const char **argv, const char *separators, REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \ \ REQ_GROUP("Scrolling") \ + REQ_(SCROLL_FIRST_COL, "Scroll to the first line columns"), \ REQ_(SCROLL_LEFT, "Scroll two columns left"), \ REQ_(SCROLL_RIGHT, "Scroll two columns right"), \ REQ_(SCROLL_LINE_UP, "Scroll one line up"), \ @@ -792,11 +300,15 @@ run_io_load(const char **argv, const char *separators, REQ_(FIND_PREV, "Find previous search match"), \ \ REQ_GROUP("Option manipulation") \ + REQ_(OPTIONS, "Open option menu"), \ REQ_(TOGGLE_LINENO, "Toggle line numbers"), \ REQ_(TOGGLE_DATE, "Toggle date display"), \ REQ_(TOGGLE_AUTHOR, "Toggle author display"), \ REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"), \ + REQ_(TOGGLE_GRAPHIC, "Toggle (line) graphics mode"), \ 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"), \ @@ -813,7 +325,8 @@ enum request { #define REQ_(req, help) REQ_##req /* Offset all requests to avoid conflicts with ncurses getch values. */ - REQ_OFFSET = KEY_MAX + 1, + REQ_UNKNOWN = KEY_MAX + 1, + REQ_OFFSET, REQ_INFO #undef REQ_GROUP @@ -842,11 +355,10 @@ get_request(const char *name) int i; for (i = 0; i < ARRAY_SIZE(req_info); i++) - if (req_info[i].namelen == namelen && - !string_enum_compare(req_info[i].name, name, namelen)) + if (enum_equals(req_info[i], name, namelen)) return req_info[i].request; - return REQ_NONE; + return REQ_UNKNOWN; } @@ -855,26 +367,26 @@ get_request(const char *name) */ /* Option and state variables. */ -static bool opt_date = TRUE; -static bool opt_author = TRUE; +static enum graphic opt_line_graphics = GRAPHIC_DEFAULT; +static enum date opt_date = DATE_DEFAULT; +static enum author opt_author = AUTHOR_DEFAULT; +static bool opt_rev_graph = 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 bool opt_untracked_dirs_content = TRUE; +static int opt_num_interval = 5; static double opt_hscroll = 0.50; -static int opt_tab_size = TAB_SIZE; -static int opt_author_cols = AUTHOR_COLS-1; +static double opt_scale_split_view = 2.0 / 3.0; +static int opt_tab_size = 8; +static int opt_author_cols = AUTHOR_COLS; static char opt_path[SIZEOF_STR] = ""; static char opt_file[SIZEOF_STR] = ""; static char opt_ref[SIZEOF_REF] = ""; static char opt_head[SIZEOF_REF] = ""; -static char opt_head_rev[SIZEOF_REV] = ""; static char opt_remote[SIZEOF_REF] = ""; static char opt_encoding[20] = "UTF-8"; -static bool opt_utf8 = TRUE; -static char opt_codeset[20] = "UTF-8"; -static iconv_t opt_iconv = ICONV_NONE; +static iconv_t opt_iconv_in = ICONV_NONE; +static iconv_t opt_iconv_out = ICONV_NONE; static char opt_search[SIZEOF_STR] = ""; static char opt_cdup[SIZEOF_STR] = ""; static char opt_prefix[SIZEOF_STR] = ""; @@ -882,9 +394,13 @@ 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; +static const char **opt_diff_argv = NULL; +static const char **opt_rev_argv = NULL; +static const char **opt_file_argv = NULL; +static const char **opt_blame_argv = NULL; -#define is_initial_commit() (!*opt_head_rev) -#define is_head_commit(rev) (!strcmp((rev), "HEAD") || !strcmp(opt_head_rev, (rev))) +#define is_initial_commit() (!get_ref_head()) +#define is_head_commit(rev) (!strcmp((rev), "HEAD") || (get_ref_head() && !strcmp(rev, get_ref_head()->id))) /* @@ -920,6 +436,8 @@ 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), \ @@ -946,7 +464,17 @@ 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(BLAME_ID, "", 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), \ +LINE(GRAPH_LINE_0, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \ +LINE(GRAPH_LINE_1, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \ +LINE(GRAPH_LINE_2, "", COLOR_CYAN, COLOR_DEFAULT, 0), \ +LINE(GRAPH_LINE_3, "", COLOR_GREEN, COLOR_DEFAULT, 0), \ +LINE(GRAPH_LINE_4, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \ +LINE(GRAPH_LINE_5, "", COLOR_WHITE, COLOR_DEFAULT, 0), \ +LINE(GRAPH_LINE_6, "", COLOR_RED, COLOR_DEFAULT, 0), \ +LINE(GRAPH_COMMIT, "", COLOR_BLUE, COLOR_DEFAULT, 0) enum line_type { #define LINE(type, line, fg, bg, attr) \ @@ -1000,8 +528,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; @@ -1037,6 +564,7 @@ struct line { unsigned int selected:1; unsigned int dirty:1; unsigned int cleareol:1; + unsigned int other:16; void *data; /* User data */ }; @@ -1051,7 +579,7 @@ struct keybinding { enum request request; }; -static const struct keybinding default_keybindings[] = { +static struct keybinding default_keybindings[] = { /* View switching */ { 'm', REQ_VIEW_MAIN }, { 'd', REQ_VIEW_DIFF }, @@ -1059,6 +587,7 @@ static const 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 }, @@ -1069,7 +598,9 @@ static const struct keybinding default_keybindings[] = { { KEY_TAB, REQ_VIEW_NEXT }, { KEY_RETURN, REQ_ENTER }, { KEY_UP, REQ_PREVIOUS }, + { KEY_CTL('P'), REQ_PREVIOUS }, { KEY_DOWN, REQ_NEXT }, + { KEY_CTL('N'), REQ_NEXT }, { 'R', REQ_REFRESH }, { KEY_F(5), REQ_REFRESH }, { 'O', REQ_MAXIMIZE }, @@ -1080,16 +611,21 @@ static const struct keybinding default_keybindings[] = { { KEY_HOME, REQ_MOVE_FIRST_LINE }, { KEY_END, REQ_MOVE_LAST_LINE }, { KEY_NPAGE, REQ_MOVE_PAGE_DOWN }, + { KEY_CTL('D'), REQ_MOVE_PAGE_DOWN }, { ' ', REQ_MOVE_PAGE_DOWN }, { KEY_PPAGE, REQ_MOVE_PAGE_UP }, + { KEY_CTL('U'), REQ_MOVE_PAGE_UP }, { 'b', REQ_MOVE_PAGE_UP }, { '-', REQ_MOVE_PAGE_UP }, /* Scrolling */ + { '|', REQ_SCROLL_FIRST_COL }, { KEY_LEFT, REQ_SCROLL_LEFT }, { KEY_RIGHT, REQ_SCROLL_RIGHT }, { KEY_IC, REQ_SCROLL_LINE_UP }, + { KEY_CTL('Y'), REQ_SCROLL_LINE_UP }, { KEY_DC, REQ_SCROLL_LINE_DOWN }, + { KEY_CTL('E'), REQ_SCROLL_LINE_DOWN }, { 'w', REQ_SCROLL_PAGE_UP }, { 's', REQ_SCROLL_PAGE_DOWN }, @@ -1104,11 +640,16 @@ static const struct keybinding default_keybindings[] = { { 'z', REQ_STOP_LOADING }, { 'v', REQ_SHOW_VERSION }, { 'r', REQ_SCREEN_REDRAW }, + { KEY_CTL('L'), REQ_SCREEN_REDRAW }, + { 'o', REQ_OPTIONS }, { '.', REQ_TOGGLE_LINENO }, { 'D', REQ_TOGGLE_DATE }, { 'A', REQ_TOGGLE_AUTHOR }, { 'g', REQ_TOGGLE_REV_GRAPH }, + { '~', REQ_TOGGLE_GRAPHIC }, { 'F', REQ_TOGGLE_REFS }, + { 'I', REQ_TOGGLE_SORT_ORDER }, + { 'i', REQ_TOGGLE_SORT_FIELD }, { ':', REQ_PROMPT }, { 'u', REQ_STATUS_UPDATE }, { '!', REQ_STATUS_REVERT }, @@ -1126,6 +667,7 @@ static const struct keybinding default_keybindings[] = { KEYMAP_(TREE), \ KEYMAP_(BLOB), \ KEYMAP_(BLAME), \ + KEYMAP_(BRANCH), \ KEYMAP_(PAGER), \ KEYMAP_(HELP), \ KEYMAP_(STATUS), \ @@ -1156,12 +698,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 @@ -1232,16 +790,17 @@ get_key_value(const char *name) if (!strcasecmp(key_table[i].name, name)) return key_table[i].value; + if (strlen(name) == 2 && name[0] == '^' && isprint(*name)) + return (int)name[1] & 0x1f; if (strlen(name) == 1 && isprint(*name)) return (int) *name; - return ERR; } static const char * get_key_name(int key_value) { - static char key_char[] = "'X'"; + static char key_char[] = "'X'\0"; const char *seq = NULL; int key; @@ -1249,36 +808,85 @@ get_key_name(int key_value) if (key_table[key].value == key_value) seq = key_table[key].name; - if (seq == NULL && - key_value < 127 && - isprint(key_value)) { - key_char[1] = (char) key_value; + if (seq == NULL && key_value < 0x7f) { + char *s = key_char + 1; + + if (key_value >= 0x20) { + *s++ = key_value; + } else { + *s++ = '^'; + *s++ = 0x40 | (key_value & 0x1f); + } + *s++ = '\''; + *s++ = '\0'; seq = key_char; } return seq ? seq : "(no key)"; } +static bool +append_key(char *buf, size_t *pos, const struct keybinding *keybinding) +{ + const char *sep = *pos > 0 ? ", " : ""; + const char *keyname = get_key_name(keybinding->alias); + + return string_nformat(buf, BUFSIZ, pos, "%s%s", sep, keyname); +} + +static bool +append_keymap_request_keys(char *buf, size_t *pos, enum request request, + enum keymap keymap, bool all) +{ + int i; + + for (i = 0; i < keybindings[keymap].size; i++) { + if (keybindings[keymap].data[i].request == request) { + if (!append_key(buf, pos, &keybindings[keymap].data[i])) + return FALSE; + if (!all) + break; + } + } + + return TRUE; +} + +#define get_key(keymap, request) get_keys(keymap, request, FALSE) + static const char * -get_key(enum request request) +get_keys(enum keymap keymap, enum request request, bool all) { static char buf[BUFSIZ]; size_t pos = 0; - char *sep = ""; int i; buf[pos] = 0; - for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) { - const struct keybinding *keybinding = &default_keybindings[i]; + if (!append_keymap_request_keys(buf, &pos, request, keymap, all)) + return "Too many keybindings!"; + if (pos > 0 && !all) + return buf; - if (keybinding->request != request) - continue; + if (keymap != KEYMAP_GENERIC) { + /* Only the generic keymap includes the default keybindings when + * listing all keys. */ + if (all) + return buf; - if (!string_format_from(buf, &pos, "%s%s", sep, - get_key_name(keybinding->alias))) + if (!append_keymap_request_keys(buf, &pos, request, KEYMAP_GENERIC, all)) return "Too many keybindings!"; - sep = ", "; + if (pos) + return buf; + } + + for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) { + if (default_keybindings[i].request == request) { + if (!append_key(buf, &pos, &default_keybindings[i])) + return "Too many keybindings!"; + if (!all) + return buf; + } } return buf; @@ -1287,31 +895,28 @@ get_key(enum request request) struct run_request { enum keymap keymap; int key; - const char *argv[SIZEOF_ARG]; + const char **argv; }; 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) +add_run_request(enum keymap keymap, int key, const char **argv) { struct run_request *req; - if (argc >= ARRAY_SIZE(req->argv) - 1) - return REQ_NONE; - - req = realloc(run_request, (run_requests + 1) * sizeof(*run_request)); - if (!req) + if (!realloc_run_requests(&run_request, run_requests, 1)) return REQ_NONE; - run_request = req; req = &run_request[run_requests]; req->keymap = keymap; req->key = key; - req->argv[0] = NULL; + req->argv = NULL; - if (!format_argv(req->argv, argv, FORMAT_NONE)) + if (!argv_copy(&req->argv, argv)) return REQ_NONE; return REQ_NONE + ++run_requests; @@ -1329,22 +934,23 @@ 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; - int argc; - const char **argv; - } reqs[] = { - { KEYMAP_MAIN, 'C', ARRAY_SIZE(cherry_pick) - 1, cherry_pick }, - { KEYMAP_GENERIC, 'G', ARRAY_SIZE(gc) - 1, gc }, + struct run_request reqs[] = { + { KEYMAP_MAIN, 'C', cherry_pick }, + { KEYMAP_STATUS, 'C', commit }, + { KEYMAP_BRANCH, 'C', checkout }, + { KEYMAP_GENERIC, 'G', 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, reqs[i].argc, reqs[i].argv); + if (req != reqs[i].key) + continue; + req = add_run_request(reqs[i].keymap, reqs[i].key, reqs[i].argv); if (req != REQ_NONE) add_keybinding(reqs[i].keymap, req, reqs[i].key); } @@ -1354,9 +960,37 @@ add_builtin_run_requests(void) * User config file handling. */ -static int config_lineno; -static bool config_errors; -static const char *config_msg; +#define OPT_ERR_INFO \ + OPT_ERR_(INTEGER_VALUE_OUT_OF_BOUND, "Integer value out of bound"), \ + OPT_ERR_(INVALID_STEP_VALUE, "Invalid step value"), \ + OPT_ERR_(NO_OPTION_VALUE, "No option value"), \ + OPT_ERR_(NO_VALUE_ASSIGNED, "No value assigned"), \ + OPT_ERR_(OBSOLETE_REQUEST_NAME, "Obsolete request name"), \ + OPT_ERR_(OUT_OF_MEMORY, "Out of memory"), \ + OPT_ERR_(TOO_MANY_OPTION_ARGUMENTS, "Too many option arguments"), \ + OPT_ERR_(UNKNOWN_ATTRIBUTE, "Unknown attribute"), \ + OPT_ERR_(UNKNOWN_COLOR, "Unknown color"), \ + OPT_ERR_(UNKNOWN_COLOR_NAME, "Unknown color name"), \ + OPT_ERR_(UNKNOWN_KEY, "Unknown key"), \ + OPT_ERR_(UNKNOWN_KEY_MAP, "Unknown key map"), \ + OPT_ERR_(UNKNOWN_OPTION_COMMAND, "Unknown option command"), \ + OPT_ERR_(UNKNOWN_REQUEST_NAME, "Unknown request name"), \ + OPT_ERR_(UNKNOWN_VARIABLE_NAME, "Unknown variable name"), \ + OPT_ERR_(UNMATCHED_QUOTATION, "Unmatched quotation"), \ + OPT_ERR_(WRONG_NUMBER_OF_ARGUMENTS, "Wrong number of arguments"), + +enum option_code { +#define OPT_ERR_(name, msg) OPT_ERR_ ## name + OPT_ERR_INFO +#undef OPT_ERR_ + OPT_OK +}; + +static const char *option_errors[] = { +#define OPT_ERR_(name, msg) msg + OPT_ERR_INFO +#undef OPT_ERR_ +}; static const struct enum_map color_map[] = { #define COLOR_MAP(name) ENUM_MAP(#name, COLOR_##name) @@ -1384,39 +1018,37 @@ static const struct enum_map attr_map[] = { #define set_attribute(attr, name) map_enum(attr, attr_map, name) -static int parse_step(double *opt, const char *arg) +static enum option_code +parse_step(double *opt, const char *arg) { *opt = atoi(arg); if (!strchr(arg, '%')) - return OK; + return OPT_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; + return OPT_ERR_INVALID_STEP_VALUE; } if (*opt < 0.0) { *opt = 1; - config_msg = "Invalid step value"; - return ERR; + return OPT_ERR_INVALID_STEP_VALUE; } - return OK; + return OPT_OK; } -static int +static enum option_code parse_int(int *opt, const char *arg, int min, int max) { int value = atoi(arg); if (min <= value && value <= max) { *opt = value; - return OK; + return OPT_OK; } - config_msg = "Integer value out of bound"; - return ERR; + return OPT_ERR_INTEGER_VALUE_OUT_OF_BOUND; } static bool @@ -1430,15 +1062,13 @@ set_color(int *color, const char *name) } /* Wants: object fgcolor bgcolor [attribute] */ -static int +static enum option_code option_color_command(int argc, const char *argv[]) { struct line_info *info; - if (argc != 3 && argc != 4) { - config_msg = "Wrong number of arguments given to color command"; - return ERR; - } + if (argc < 3) + return OPT_ERR_WRONG_NUMBER_OF_ARGUMENTS; info = get_line_info(argv[0]); if (!info) { @@ -1449,35 +1079,55 @@ option_color_command(int argc, const char *argv[]) }; int index; - if (!map_enum(&index, obsolete, argv[0])) { - config_msg = "Unknown color name"; - return ERR; - } + if (!map_enum(&index, obsolete, argv[0])) + return OPT_ERR_UNKNOWN_COLOR_NAME; info = &line_info[index]; } if (!set_color(&info->fg, argv[1]) || - !set_color(&info->bg, argv[2])) { - config_msg = "Unknown color"; - return ERR; - } + !set_color(&info->bg, argv[2])) + return OPT_ERR_UNKNOWN_COLOR; - if (argc == 4 && !set_attribute(&info->attr, argv[3])) { - config_msg = "Unknown attribute"; - return ERR; + info->attr = 0; + while (argc-- > 3) { + int attr; + + if (!set_attribute(&attr, argv[argc])) + return OPT_ERR_UNKNOWN_ATTRIBUTE; + info->attr |= attr; } - return OK; + return OPT_OK; } -static int parse_bool(bool *opt, const char *arg) +static enum option_code +parse_bool(bool *opt, const char *arg) { *opt = (!strcmp(arg, "1") || !strcmp(arg, "true") || !strcmp(arg, "yes")) ? TRUE : FALSE; - return OK; + return OPT_OK; } -static int +static enum option_code +parse_enum_do(unsigned int *opt, const char *arg, + const struct enum_map *map, size_t map_size) +{ + bool is_true; + + assert(map_size > 1); + + if (map_enum_do(map, map_size, (int *) opt, arg)) + return OPT_OK; + + parse_bool(&is_true, arg); + *opt = is_true ? map[1].value : map[0].value; + return OPT_OK; +} + +#define parse_enum(opt, arg, map) \ + parse_enum_do(opt, arg, map, ARRAY_SIZE(map)) + +static enum option_code parse_string(char *opt, const char *arg, size_t optsize) { int arglen = strlen(arg); @@ -1485,36 +1135,44 @@ parse_string(char *opt, const char *arg, size_t optsize) switch (arg[0]) { case '\"': case '\'': - if (arglen == 1 || arg[arglen - 1] != arg[0]) { - config_msg = "Unmatched quotation"; - return ERR; - } + if (arglen == 1 || arg[arglen - 1] != arg[0]) + return OPT_ERR_UNMATCHED_QUOTATION; arg += 1; arglen -= 2; default: string_ncopy_do(opt, optsize, arg, arglen); - return OK; + return OPT_OK; } } +static enum option_code +parse_args(const char ***args, const char *argv[]) +{ + if (*args == NULL && !argv_copy(args, argv)) + return OPT_ERR_OUT_OF_MEMORY; + return OPT_OK; +} + /* Wants: name = value */ -static int +static enum option_code option_set_command(int argc, const char *argv[]) { - if (argc != 3) { - config_msg = "Wrong number of arguments given to set command"; - return ERR; - } + if (argc < 3) + return OPT_ERR_WRONG_NUMBER_OF_ARGUMENTS; - if (strcmp(argv[1], "=")) { - config_msg = "No value assigned"; - return ERR; - } + if (strcmp(argv[1], "=")) + return OPT_ERR_NO_VALUE_ASSIGNED; + + if (!strcmp(argv[0], "blame-options")) + return parse_args(&opt_blame_argv, argv + 2); + + if (argc != 3) + return OPT_ERR_WRONG_NUMBER_OF_ARGUMENTS; if (!strcmp(argv[0], "show-author")) - return parse_bool(&opt_author, argv[2]); + return parse_enum(&opt_author, argv[2], author_map); if (!strcmp(argv[0], "show-date")) - return parse_bool(&opt_date, argv[2]); + return parse_enum(&opt_date, argv[2], date_map); if (!strcmp(argv[0], "show-rev-graph")) return parse_bool(&opt_rev_graph, argv[2]); @@ -1526,7 +1184,7 @@ option_set_command(int argc, const char *argv[]) return parse_bool(&opt_line_number, argv[2]); if (!strcmp(argv[0], "line-graphics")) - return parse_bool(&opt_line_graphics, argv[2]); + return parse_enum(&opt_line_graphics, argv[2], graphic_map); if (!strcmp(argv[0], "line-number-interval")) return parse_int(&opt_num_interval, argv[2], 1, 1024); @@ -1537,42 +1195,41 @@ option_set_command(int argc, const char *argv[]) if (!strcmp(argv[0], "horizontal-scroll")) return parse_step(&opt_hscroll, argv[2]); + if (!strcmp(argv[0], "split-view-height")) + return parse_step(&opt_scale_split_view, argv[2]); + if (!strcmp(argv[0], "tab-size")) return parse_int(&opt_tab_size, argv[2], 1, 1024); if (!strcmp(argv[0], "commit-encoding")) return parse_string(opt_encoding, argv[2], sizeof(opt_encoding)); - config_msg = "Unknown variable name"; - return ERR; + if (!strcmp(argv[0], "status-untracked-dirs")) + return parse_bool(&opt_untracked_dirs_content, argv[2]); + + return OPT_ERR_UNKNOWN_VARIABLE_NAME; } /* Wants: mode request key */ -static int +static enum option_code option_bind_command(int argc, const char *argv[]) { enum request request; - int keymap; + int keymap = -1; int key; - if (argc < 3) { - config_msg = "Wrong number of arguments given to bind command"; - return ERR; - } + if (argc < 3) + return OPT_ERR_WRONG_NUMBER_OF_ARGUMENTS; - if (set_keymap(&keymap, argv[0]) == ERR) { - config_msg = "Unknown key map"; - return ERR; - } + if (!set_keymap(&keymap, argv[0])) + return OPT_ERR_UNKNOWN_KEY_MAP; key = get_key_value(argv[1]); - if (key == ERR) { - config_msg = "Unknown key"; - return ERR; - } + if (key == ERR) + return OPT_ERR_UNKNOWN_KEY; request = get_request(argv[2]); - if (request == REQ_NONE) { + if (request == REQ_UNKNOWN) { static const struct enum_map obsolete[] = { ENUM_MAP("cherry-pick", REQ_NONE), ENUM_MAP("screen-resize", REQ_NONE), @@ -1583,32 +1240,27 @@ option_bind_command(int argc, const char *argv[]) if (map_enum(&alias, obsolete, argv[2])) { if (alias != REQ_NONE) add_keybinding(keymap, alias, key); - config_msg = "Obsolete request name"; - return ERR; + return OPT_ERR_OBSOLETE_REQUEST_NAME; } } - if (request == REQ_NONE && *argv[2]++ == '!') - request = add_run_request(keymap, key, argc - 2, argv + 2); - if (request == REQ_NONE) { - config_msg = "Unknown request name"; - return ERR; - } + if (request == REQ_UNKNOWN && *argv[2]++ == '!') + request = add_run_request(keymap, key, argv + 2); + if (request == REQ_UNKNOWN) + return OPT_ERR_UNKNOWN_REQUEST_NAME; add_keybinding(keymap, request, key); - return OK; + return OPT_OK; } -static int +static enum option_code set_option(const char *opt, char *value) { const char *argv[SIZEOF_ARG]; int argc = 0; - if (!argv_from_string(argv, &argc, value)) { - config_msg = "Too many option arguments"; - return ERR; - } + if (!argv_from_string(argv, &argc, value)) + return OPT_ERR_TOO_MANY_OPTION_ARGUMENTS; if (!strcmp(opt, "color")) return option_color_command(argc, argv); @@ -1619,17 +1271,21 @@ set_option(const char *opt, char *value) if (!strcmp(opt, "bind")) return option_bind_command(argc, argv); - config_msg = "Unknown option command"; - return ERR; + return OPT_ERR_UNKNOWN_OPTION_COMMAND; } +struct config_state { + int lineno; + bool errors; +}; + static int -read_option(char *opt, size_t optlen, char *value, size_t valuelen) +read_option(char *opt, size_t optlen, char *value, size_t valuelen, void *data) { - int status = OK; + struct config_state *config = data; + enum option_code status = OPT_ERR_NO_OPTION_VALUE; - config_lineno++; - config_msg = "Internal error"; + config->lineno++; /* Check for comment markers, since read_properties() will * only ensure opt and value are split at first " \t". */ @@ -1637,11 +1293,7 @@ read_option(char *opt, size_t optlen, char *value, size_t valuelen) if (optlen == 0) return OK; - if (opt[optlen] != 0) { - config_msg = "No option value"; - status = ERR; - - } else { + if (opt[optlen] == 0) { /* Look for comment endings in the value. */ size_t len = strcspn(value, "#"); @@ -1653,10 +1305,10 @@ read_option(char *opt, size_t optlen, char *value, size_t valuelen) status = set_option(opt, value); } - if (status == ERR) { + if (status != OPT_OK) { warn("Error on line %d, near '%.*s': %s", - config_lineno, (int) optlen, opt, config_msg); - config_errors = TRUE; + config->lineno, (int) optlen, opt, option_errors[status]); + config->errors = TRUE; } /* Always keep going if errors are encountered. */ @@ -1666,17 +1318,15 @@ read_option(char *opt, size_t optlen, char *value, size_t valuelen) static void load_option_file(const char *path) { - struct io io = {}; + struct config_state config = { 0, FALSE }; + struct io io; /* It's OK that the file doesn't exist. */ - if (!io_open(&io, path)) + if (!io_open(&io, "%s", path)) return; - config_lineno = 0; - config_errors = FALSE; - - if (io_load(&io, " \t", read_option) == ERR || - config_errors == TRUE) + if (io_load(&io, " \t", read_option, &config) == ERR || + config.errors == TRUE) warn("Errors while loading %s.", path); } @@ -1686,10 +1336,9 @@ load_options(void) const char *home = getenv("HOME"); const char *tigrc_user = getenv("TIGRC_USER"); const char *tigrc_system = getenv("TIGRC_SYSTEM"); + const char *tig_diff_opts = getenv("TIG_DIFF_OPTS"); char buf[SIZEOF_STR]; - add_builtin_run_requests(); - if (!tigrc_system) tigrc_system = SYSCONFDIR "/tigrc"; load_option_file(tigrc_system); @@ -1701,6 +1350,21 @@ 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(); + + if (!opt_diff_argv && tig_diff_opts && *tig_diff_opts) { + static const char *diff_opts[SIZEOF_ARG] = { NULL }; + int argc = 0; + + if (!string_format(buf, "%s", tig_diff_opts) || + !argv_from_string(diff_opts, &argc, buf)) + die("TIG_DIFF_OPTS contains too many arguments"); + else if (!argv_copy(&opt_diff_argv, diff_opts)) + die("Failed to format TIG_DIFF_OPTS arguments"); + } + return OK; } @@ -1714,6 +1378,8 @@ struct view_ops; /* The display array of active views and the index of the current view. */ static struct view *display[2]; +static WINDOW *display_win[2]; +static WINDOW *display_title[2]; static unsigned int current_view; #define foreach_displayed_view(view, i) \ @@ -1725,10 +1391,25 @@ static unsigned int current_view; 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_env; /* Command line set via environment */ const char *id; /* Points to either of ref_{head,commit,blob} */ struct view_ops *ops; /* View operations */ @@ -1741,7 +1422,6 @@ struct view { int height, width; /* The width and height of the main window */ WINDOW *win; /* The main window */ - WINDOW *title; /* The title window living below the main window */ /* Navigation */ unsigned long offset; /* Offset of the window top */ @@ -1759,11 +1439,11 @@ struct view { /* 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 */ unsigned int digits; /* Number of digits in the lines member. */ /* Drawing */ @@ -1773,19 +1453,28 @@ struct view { bool has_scrolled; /* View was scrolled. */ /* Loading */ + const char **argv; /* 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; }; +enum open_flags { + OPEN_DEFAULT = 0, /* Use default view switching. */ + OPEN_SPLIT = 1, /* Split current view. */ + OPEN_RELOAD = 4, /* Reload view even if it is the current. */ + OPEN_REFRESH = 16, /* Refresh view using previous command. */ + OPEN_PREPARED = 32, /* Open already prepared command. */ + OPEN_EXTRA = 64, /* Open extra data from command. */ +}; + 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); + bool (*open)(struct view *view, enum open_flags flags); /* Read one line; updates view->line. */ bool (*read)(struct view *view, char *data); /* Draw one line; @lineno must be < view->height. */ @@ -1808,13 +1497,13 @@ static struct view_ops pager_ops; static struct view_ops stage_ops; static struct view_ops status_ops; static struct view_ops tree_ops; +static struct view_ops branch_ops; -#define VIEW_STR(name, env, ref, ops, map, git) \ - { name, #env, ref, ops, map, git } +#define VIEW_STR(type, name, ref, ops, map, git) \ + { type, name, ref, ops, map, git } #define VIEW_(id, name, ops, git, ref) \ - VIEW_STR(name, TIG_##id##_CMD, ref, ops, KEYMAP_##id, git) - + VIEW_STR(VIEW_##id, name, ref, ops, KEYMAP_##id, git) static struct view views[] = { VIEW_(MAIN, "main", &main_ops, TRUE, ref_head), @@ -1823,14 +1512,14 @@ static struct view views[] = { VIEW_(TREE, "tree", &tree_ops, TRUE, ref_commit), VIEW_(BLOB, "blob", &blob_ops, TRUE, ref_blob), VIEW_(BLAME, "blame", &blame_ops, TRUE, ref_commit), + VIEW_(BRANCH, "branch", &branch_ops, TRUE, ref_head), VIEW_(HELP, "help", &help_ops, FALSE, ""), - VIEW_(PAGER, "pager", &pager_ops, FALSE, "stdin"), + VIEW_(PAGER, "pager", &pager_ops, FALSE, ""), VIEW_(STATUS, "status", &status_ops, TRUE, ""), VIEW_(STAGE, "stage", &stage_ops, TRUE, ""), }; #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++) @@ -1838,92 +1527,116 @@ 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 chtype 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; } } -static int +#define VIEW_MAX_LEN(view) ((view)->width + (view)->yoffset - (view)->col) + +static bool 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; + return VIEW_MAX_LEN(view) <= 0; - if (opt_utf8) { - len = utf8_length(&string, skip, &col, max_len, &trimmed, use_tilde); - } else { - col = len = strlen(string); - if (len > max_len) { - if (use_tilde) { - max_len -= 1; + len = utf8_length(&string, skip, &col, max_len, &trimmed, use_tilde, 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); - if (len > 0) waddnstr(view->win, string, len); - if (trimmed && use_tilde) { - set_view_attr(view, LINE_DELIMITER); - waddch(view->win, '~'); - col++; + + if (trimmed && use_tilde) { + set_view_attr(view, LINE_DELIMITER); + waddch(view->win, '~'); + col++; + } } - return col; + view->col += col; + return VIEW_MAX_LEN(view) <= 0; } -static int +static bool draw_space(struct view *view, enum line_type type, int max, int spaces) { static char space[] = " "; - int col = 0; spaces = MIN(max, spaces); while (spaces > 0) { int len = MIN(spaces, sizeof(space) - 1); - col += draw_chars(view, type, space, len, FALSE); + if (draw_chars(view, type, space, len, FALSE)) + return TRUE; spaces -= len; } - return col; + return VIEW_MAX_LEN(view) <= 0; } static bool -draw_text(struct view *view, enum line_type type, const char *string, bool trim) +draw_text(struct view *view, enum line_type type, const char *string) { - view->col += draw_chars(view, type, string, view->width + view->yoffset - view->col, trim); - return view->width + view->yoffset <= view->col; + char text[SIZEOF_STR]; + + do { + size_t pos = string_expand(text, sizeof(text), string, opt_tab_size); + + if (draw_chars(view, type, text, VIEW_MAX_LEN(view), TRUE)) + return TRUE; + string += pos; + } while (*string); + + return VIEW_MAX_LEN(view) <= 0; } static bool -draw_graphic(struct view *view, enum line_type type, chtype graphic[], size_t size) +draw_graphic(struct view *view, enum line_type type, const chtype graphic[], size_t size, bool separator) { size_t skip = view->yoffset > view->col ? view->yoffset - view->col : 0; - int max = view->width + view->yoffset - view->col; + int max = VIEW_MAX_LEN(view); int i; if (max < size) @@ -1936,86 +1649,53 @@ draw_graphic(struct view *view, enum line_type type, chtype graphic[], size_t si waddch(view->win, graphic[i]); view->col += size; - if (size < max && skip <= size) - waddch(view->win, ' '); - view->col++; + if (separator) { + if (size < max && skip <= size) + waddch(view->win, ' '); + view->col++; + } - return view->width + view->yoffset <= view->col; + return VIEW_MAX_LEN(view) <= 0; } static bool draw_field(struct view *view, enum line_type type, const char *text, int len, bool trim) { - int max = MIN(view->width + view->yoffset - view->col, len); - int col; + int max = MIN(VIEW_MAX_LEN(view), len); + int col = view->col; - if (text) - col = draw_chars(view, type, text, max - 1, trim); - else - col = draw_space(view, type, max - 1, max - 1); + if (!text) + return draw_space(view, type, max, max); - view->col += col; - view->col += draw_space(view, LINE_DEFAULT, max - col, max - col); - return view->width + view->yoffset <= view->col; + return draw_chars(view, type, text, max - 1, trim) + || draw_space(view, LINE_DEFAULT, max - (view->col - col), max); } static bool -draw_date(struct view *view, struct tm *time) +draw_date(struct view *view, struct time *time) { - char buf[DATE_COLS]; - char *date; - int timelen = 0; + const char *date = mkdate(time, opt_date); + int cols = opt_date == DATE_SHORT ? DATE_SHORT_COLS : DATE_COLS; - if (time) - timelen = strftime(buf, sizeof(buf), DATE_FORMAT, time); - date = timelen ? buf : NULL; + if (opt_date == DATE_NO) + return FALSE; - return draw_field(view, LINE_DATE, date, DATE_COLS, FALSE); + return draw_field(view, LINE_DATE, date, cols, FALSE); } static bool draw_author(struct view *view, const char *author) { - bool trim = opt_author_cols == 0 || opt_author_cols > 5 || !author; - - if (!trim) { - static char initials[10]; - size_t pos; - -#define is_initial_sep(c) (isspace(c) || ispunct(c) || (c) == '@') - - memset(initials, 0, sizeof(initials)); - for (pos = 0; *author && pos < opt_author_cols - 1; author++, pos++) { - while (is_initial_sep(*author)) - author++; - strncpy(&initials[pos], author, sizeof(initials) - 1 - pos); - while (*author && !is_initial_sep(author[1])) - author++; - } - - author = initials; - } + bool trim = author_trim(opt_author_cols); + const char *text = mkauthor(author, opt_author_cols, opt_author); - return draw_field(view, LINE_AUTHOR, author, opt_author_cols, trim); + return draw_field(view, LINE_AUTHOR, text, 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 = "----------"; + const char *str = mkmode(mode); return draw_field(view, LINE_MODE, str, STRING_SIZE("-rw-r--r-- "), FALSE); } @@ -2025,24 +1705,60 @@ draw_lineno(struct view *view, unsigned int lineno) { char number[10]; int digits3 = view->digits < 3 ? 3 : view->digits; - int max = MIN(view->width + view->yoffset - view->col, digits3); + int max = MIN(VIEW_MAX_LEN(view), digits3); char *text = NULL; + chtype separator = opt_line_graphics ? ACS_VLINE : '|'; lineno += view->offset + 1; if (lineno == 1 || (lineno % opt_num_interval) == 0) { static char fmt[] = "%1ld"; - if (view->digits <= 9) - fmt[1] = '0' + digits3; - + 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); + draw_chars(view, LINE_LINE_NUMBER, text, max, TRUE); else - view->col += draw_space(view, LINE_LINE_NUMBER, max, digits3); - return draw_graphic(view, LINE_DEFAULT, &line_graphics[LINE_GRAPHIC_VLINE], 1); + draw_space(view, LINE_LINE_NUMBER, max, digits3); + return draw_graphic(view, LINE_DEFAULT, &separator, 1, TRUE); +} + +static bool +draw_refs(struct view *view, struct ref_list *refs) +{ + size_t i; + + if (!opt_show_refs || !refs) + return FALSE; + + for (i = 0; i < refs->size; i++) { + struct ref *ref = refs->refs[i]; + enum line_type type; + + if (ref->head) + type = LINE_MAIN_HEAD; + else if (ref->ltag) + type = LINE_MAIN_LOCAL_TAG; + else if (ref->tag) + type = LINE_MAIN_TAG; + else if (ref->tracked) + type = LINE_MAIN_TRACKED; + else if (ref->remote) + type = LINE_MAIN_REMOTE; + else + type = LINE_MAIN_REF; + + if (draw_text(view, type, "[") || + draw_text(view, type, ref->name) || + draw_text(view, type, "]")) + return TRUE; + + if (draw_text(view, LINE_DEFAULT, " ")) + return TRUE; + } + + return FALSE; } static bool @@ -2124,10 +1840,11 @@ update_view_title(struct view *view) char buf[SIZEOF_STR]; char state[SIZEOF_STR]; size_t bufpos = 0, statelen = 0; + WINDOW *window = display[0] == view ? display_title[0] : display_title[1]; assert(view_is_displayed(view)); - if (view != VIEW(REQ_VIEW_STATUS) && view->lines) { + 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 @@ -2164,13 +1881,22 @@ update_view_title(struct view *view) } if (view == display[current_view]) - wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS)); + wbkgdset(window, get_line_attr(LINE_TITLE_FOCUS)); else - wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR)); + wbkgdset(window, get_line_attr(LINE_TITLE_BLUR)); - mvwaddnstr(view->title, 0, 0, buf, bufpos); - wclrtoeol(view->title); - wnoutrefresh(view->title); + mvwaddnstr(window, 0, 0, buf, bufpos); + wclrtoeol(window); + wnoutrefresh(window); +} + +static int +apply_step(double step, int value) +{ + if (step >= 1) + return (int) step; + value *= step + 0.01; + return value ? value : 1; } static void @@ -2190,7 +1916,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. */ @@ -2203,23 +1931,25 @@ resize_display(void) offset = 0; foreach_displayed_view (view, i) { - if (!view->win) { - view->win = newwin(view->height, 0, offset, 0); - if (!view->win) + if (!display_win[i]) { + display_win[i] = newwin(view->height, view->width, offset, 0); + if (!display_win[i]) die("Failed to create %s view", view->name); - scrollok(view->win, FALSE); + scrollok(display_win[i], FALSE); - view->title = newwin(1, 0, offset + view->height, 0); - if (!view->title) + display_title[i] = newwin(1, view->width, offset + view->height, 0); + if (!display_title[i]) die("Failed to create title window"); } else { - wresize(view->win, view->height, view->width); - mvwin(view->win, offset, 0); - mvwin(view->title, offset + view->height, 0); + wresize(display_win[i], view->height, view->width); + mvwin(display_win[i], offset, 0); + mvwin(display_title[i], offset + view->height, 0); } + view->win = display_win[i]; + offset += view->height + 1; } } @@ -2238,23 +1968,76 @@ redraw_display(bool clear) } } + +/* + * Option management + */ + +#define TOGGLE_MENU \ + TOGGLE_(LINENO, '.', "line numbers", &opt_line_number, NULL) \ + TOGGLE_(DATE, 'D', "dates", &opt_date, date_map) \ + TOGGLE_(AUTHOR, 'A', "author names", &opt_author, author_map) \ + TOGGLE_(GRAPHIC, '~', "graphics", &opt_line_graphics, graphic_map) \ + TOGGLE_(REV_GRAPH, 'g', "revision graph", &opt_rev_graph, NULL) \ + TOGGLE_(REFS, 'F', "reference display", &opt_show_refs, NULL) + static void -toggle_view_option(bool *option, const char *help) -{ - *option = !*option; - redraw_display(FALSE); - report("%sabling %s", *option ? "En" : "Dis", help); +toggle_option(enum request request) +{ + const struct { + enum request request; + const struct enum_map *map; + size_t map_size; + } data[] = { +#define TOGGLE_(id, key, help, value, map) { REQ_TOGGLE_ ## id, map, ARRAY_SIZE(map) }, + TOGGLE_MENU +#undef TOGGLE_ + }; + const struct menu_item menu[] = { +#define TOGGLE_(id, key, help, value, map) { key, help, value }, + TOGGLE_MENU +#undef TOGGLE_ + { 0 } + }; + int i = 0; + + if (request == REQ_OPTIONS) { + if (!prompt_menu("Toggle option", menu, &i)) + return; + } else { + while (i < ARRAY_SIZE(data) && data[i].request != request) + i++; + if (i >= ARRAY_SIZE(data)) + die("Invalid request (%d)", request); + } + + if (data[i].map != NULL) { + unsigned int *opt = menu[i].data; + + *opt = (*opt + 1) % data[i].map_size; + redraw_display(FALSE); + report("Displaying %s %s", enum_name(data[i].map[*opt]), menu[i].text); + + } else { + bool *option = menu[i].data; + + *option = !*option; + redraw_display(FALSE); + report("%sabling %s", *option ? "En" : "Dis", menu[i].text); + } } static void -maximize_view(struct view *view) +maximize_view(struct view *view, bool redraw) { memset(display, 0, sizeof(display)); current_view = 0; display[current_view] = view; resize_display(); - redraw_display(FALSE); - report(""); + if (redraw) { + redraw_display(FALSE); + report(""); + } } @@ -2286,15 +2069,6 @@ goto_view_line(struct view *view, unsigned long offset, unsigned long lineno) return FALSE; } -static int -apply_step(double step, int value) -{ - if (step >= 1) - return (int) step; - value *= step + 0.01; - return value ? value : 1; -} - /* Scrolling backend */ static void do_scroll_view(struct view *view, int lines) @@ -2351,6 +2125,11 @@ scroll_view(struct view *view, enum request request) assert(view_is_displayed(view)); switch (request) { + case REQ_SCROLL_FIRST_COL: + view->yoffset = 0; + redraw_view_from(view, 0); + report(""); + return; case REQ_SCROLL_LEFT: if (view->yoffset == 0) { report("Cannot scroll beyond the first column"); @@ -2499,6 +2278,19 @@ move_view(struct view *view, enum request request) static void search_view(struct view *view, enum request request); +static bool +grep_text(struct view *view, const char *text[]) +{ + regmatch_t pmatch; + size_t i; + + for (i = 0; text[i]; i++) + if (*text[i] && + regexec(view->regex, text[i], 1, &pmatch, 0) != REG_NOMATCH) + return TRUE; + return FALSE; +} + static void select_view_line(struct view *view, unsigned long lineno) { @@ -2615,80 +2407,100 @@ reset_view(struct view *view) view->yoffset = 0; view->lines = 0; view->lineno = 0; - view->line_alloc = 0; view->vid[0] = 0; view->update_secs = 0; } -static void -free_argv(const char *argv[]) +static const char * +format_arg(const char *name) { - int argc; + 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; - for (argc = 0; argv[argc]; argc++) - free((void *) argv[argc]); + report("Unknown replacement: `%s`", name); + return NULL; } static bool -format_argv(const char *dst_argv[], const char *src_argv[], enum format_flags flags) +format_argv(const char ***dst_argv, const char *src_argv[], bool first) { char buf[SIZEOF_STR]; int argc; - bool noreplace = flags == FORMAT_NONE; - free_argv(dst_argv); + argv_free(*dst_argv); for (argc = 0; src_argv[argc]; argc++) { const char *arg = src_argv[argc]; size_t bufpos = 0; + if (!strcmp(arg, "%(fileargs)")) { + if (!argv_append_array(dst_argv, opt_file_argv)) + break; + continue; + + } else if (!strcmp(arg, "%(diffargs)")) { + if (!argv_append_array(dst_argv, opt_diff_argv)) + break; + continue; + + } else if (!strcmp(arg, "%(blameargs)")) { + if (!argv_append_array(dst_argv, opt_blame_argv)) + break; + continue; + + } else if (!strcmp(arg, "%(revargs)") || + (first && !strcmp(arg, "%(commit)"))) { + if (!argv_append_array(dst_argv, opt_rev_argv)) + break; + continue; + } + while (arg) { char *next = strstr(arg, "%("); int len = next - arg; const char *value; - if (!next || noreplace) { - if (flags == FORMAT_DASH && !strcmp(arg, "--")) - noreplace = TRUE; + if (!next) { len = strlen(arg); value = ""; - } else if (!prefixcmp(next, "%(directory)")) { - value = opt_path; - - } else if (!prefixcmp(next, "%(file)")) { - value = opt_file; - - } else if (!prefixcmp(next, "%(ref)")) { - value = *opt_ref ? opt_ref : "HEAD"; - - } else if (!prefixcmp(next, "%(head)")) { - value = ref_head; - - } else if (!prefixcmp(next, "%(commit)")) { - value = ref_commit; - - } else if (!prefixcmp(next, "%(blob)")) { - value = ref_blob; - } else { - report("Unknown replacement: `%s`", next); - return FALSE; + value = format_arg(next); + + if (!value) { + return FALSE; + } } if (!string_format_from(buf, &bufpos, "%.*s%s", len, arg, value)) return FALSE; - arg = next && !noreplace ? strchr(next, ')') + 1 : NULL; + arg = next ? strchr(next, ')') + 1 : NULL; } - dst_argv[argc] = strdup(buf); - if (!dst_argv[argc]) + if (!argv_append(dst_argv, buf)) break; } - dst_argv[argc] = NULL; - return src_argv[argc] == NULL; } @@ -2723,17 +2535,15 @@ end_update(struct view *view, bool force) while (!view->ops->read(view, NULL)) if (!force) return; - set_nonblocking_input(FALSE); if (force) - kill_io(view->pipe); - done_io(view->pipe); + io_kill(view->pipe); + io_done(view->pipe); view->pipe = NULL; } static void setup_update(struct view *view, const char *vid) { - set_nonblocking_input(TRUE); reset_view(view); string_copy_rev(view->vid, vid); view->pipe = &view->io; @@ -2741,37 +2551,25 @@ setup_update(struct view *view, const char *vid) } static bool -prepare_update(struct view *view, const char *argv[], const char *dir, - enum format_flags flags) -{ - if (view->pipe) - end_update(view, TRUE); - return init_io_rd(&view->io, argv, dir, flags); -} - -static bool -prepare_update_file(struct view *view, const char *name) -{ - if (view->pipe) - end_update(view, TRUE); - return io_open(&view->io, name); -} - -static bool -begin_update(struct view *view, bool refresh) +begin_update(struct view *view, const char *dir, const char **argv, enum open_flags flags) { - if (view->pipe) - end_update(view, TRUE); + bool extra = !!(flags & (OPEN_EXTRA)); + bool reload = !!(flags & (OPEN_RELOAD | OPEN_REFRESH | OPEN_PREPARED | OPEN_EXTRA)); + bool refresh = flags & (OPEN_REFRESH | OPEN_PREPARED); - if (refresh) { - if (!start_io(&view->io)) - return FALSE; + if (!reload && !strcmp(view->vid, view->id)) + return TRUE; - } else { - if (view == VIEW(REQ_VIEW_TREE) && strcmp(view->vid, view->id)) - opt_path[0] = 0; + if (view->pipe) { + if (extra) + io_done(view->pipe); + else + end_update(view, TRUE); + } - if (!run_io_rd(&view->io, view->ops->argv, FORMAT_ALL)) + if (!refresh) { + view->dir = dir; + if (!format_argv(&view->argv, argv, !view->prev)) return FALSE; /* Put the current ref_* value to the view title ref @@ -2781,39 +2579,20 @@ begin_update(struct view *view, bool refresh) string_copy_rev(view->ref, view->id); } - 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 (view->argv && view->argv[0] && + !io_run(&view->io, IO_RD, view->dir, view->argv)) + return FALSE; - if (mem == NULL || num_chunks != num_chunks_new) { - *size = num_chunks_new * ITEM_CHUNK_SIZE; - mem = realloc(mem, *size * item_size); - } + if (!extra) + setup_update(view, view->id); - return mem; + return TRUE; } -static struct line * -realloc_lines(struct view *view, size_t line_size) +static bool +view_open(struct view *view, enum open_flags flags) { - size_t alloc = view->line_alloc; - struct line *tmp = realloc_items(view->line, &alloc, line_size, - sizeof(*view->line)); - - if (!tmp) - return NULL; - - view->line = tmp; - view->line_alloc = alloc; - return view->line; + return begin_update(view, NULL, NULL, flags); } static bool @@ -2829,7 +2608,7 @@ update_view(struct view *view) if (!view->pipe) return TRUE; - if (!io_can_read(view->pipe)) { + if (!io_can_read(view->pipe, FALSE)) { if (view->lines == 0 && view_is_displayed(view)) { time_t secs = time(NULL) - view->start_time; @@ -2844,7 +2623,7 @@ update_view(struct view *view) } for (; (line = io_get(view->pipe, '\n', can_read)); can_read = FALSE) { - if (opt_iconv != ICONV_NONE) { + if (opt_iconv_in != ICONV_NONE) { ICONV_CONST char *inbuf = line; size_t inlen = strlen(line) + 1; @@ -2853,7 +2632,7 @@ update_view(struct view *view) size_t ret; - ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen); + ret = iconv(opt_iconv_in, &inbuf, &inlen, &outbuf, &outlen); if (ret != (size_t) -1) line = out_buffer; } @@ -2875,7 +2654,7 @@ update_view(struct view *view) /* Keep the displayed view in sync with line number scaling. */ if (digits != view->digits) { view->digits = digits; - if (opt_line_number || view == VIEW(REQ_VIEW_BLAME)) + if (opt_line_number || view->type == VIEW_BLAME) redraw = TRUE; } } @@ -2885,7 +2664,8 @@ update_view(struct view *view) end_update(view, TRUE); } else if (io_eof(view->pipe)) { - report(""); + if (view_is_displayed(view)) + report(""); end_update(view, FALSE); } @@ -2906,12 +2686,14 @@ update_view(struct view *view) return TRUE; } +DEFINE_ALLOCATOR(realloc_lines, struct line, 256) + static struct line * add_line_data(struct view *view, void *data, enum line_type type) { struct line *line; - if (!realloc_lines(view, view->lines + 1)) + if (!realloc_lines(&view->line, view->lines, 1)) return NULL; line = &view->line[view->lines++]; @@ -2949,23 +2731,63 @@ add_line_format(struct view *view, enum line_type type, const char *fmt, ...) * View opening */ -enum open_flags { - OPEN_DEFAULT = 0, /* Use default view switching. */ - OPEN_SPLIT = 1, /* Split current view. */ - OPEN_RELOAD = 4, /* Reload view even if it is the current. */ - OPEN_REFRESH = 16, /* Refresh view using previous command. */ - OPEN_PREPARED = 32, /* Open already prepared command. */ -}; +static void +load_view(struct view *view, enum open_flags flags) +{ + if (view->pipe) + end_update(view, TRUE); + if (!view->ops->open(view, flags)) { + report("Failed to load %s view", view->name); + return; + } + restore_view_position(view); + + 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(""); + } +} + +#define refresh_view(view) load_view(view, OPEN_REFRESH) + +static void +split_view(struct view *prev, struct view *view) +{ + display[1] = view; + current_view = 1; + view->parent = prev; + resize_display(); + + if (prev->lineno - prev->offset >= prev->height) { + /* Take the title line into account. */ + int lines = prev->lineno - prev->offset - prev->height + 1; + + /* Scroll the view that was split if the current line is + * outside the new limited view. */ + do_scroll_view(prev, lines); + } + + if (view != prev && view_is_displayed(prev)) { + /* "Blur" the previous view. */ + update_view_title(prev); + } +} static void open_view(struct view *prev, enum request request, enum open_flags flags) { bool split = !!(flags & OPEN_SPLIT); - bool reload = !!(flags & (OPEN_RELOAD | OPEN_REFRESH | OPEN_PREPARED)); - bool nomaximize = !!(flags & OPEN_REFRESH); + bool reload = !!(flags & (OPEN_RELOAD | OPEN_PREPARED)); struct view *view = VIEW(request); int nviews = displayed_views(); - struct view *base_view = display[0]; + + assert(flags ^ OPEN_REFRESH); if (view == prev && nviews == 1 && !reload) { report("Already in %s view", view->name); @@ -2978,64 +2800,41 @@ open_view(struct view *prev, enum request request, enum open_flags flags) } if (split) { - display[1] = view; - current_view = 1; - } else if (!nomaximize) { - /* Maximize the current view. */ - memset(display, 0, sizeof(display)); - current_view = 0; - display[current_view] = view; - } - - /* 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->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 | OPEN_PREPARED))) { - report("Failed to load %s view", view->name); - return; + split_view(prev, view); + } else { + maximize_view(view, FALSE); } - if (split && prev->lineno - prev->offset >= prev->height) { - /* Take the title line into account. */ - int lines = prev->lineno - prev->offset - prev->height + 1; - - /* Scroll the view that was split if the current line is - * outside the new limited view. */ - do_scroll_view(prev, lines); + /* No prev signals that this is the first loaded view. */ + if (prev && view != prev) { + view->prev = prev; } - if (prev && view != prev) { - if (split) { - /* "Blur" the previous view. */ - update_view_title(prev); - } + load_view(view, flags); +} - view->parent = prev; +static void +open_argv(struct view *prev, struct view *view, const char *argv[], const char *dir, enum open_flags flags) +{ + enum request request = view - views + REQ_OFFSET + 1; + + if (view->pipe) + end_update(view, TRUE); + view->dir = dir; + + if (!argv_copy(&view->argv, argv)) { + report("Failed to open %s view: %s", view->name, io_strerror(&view->io)); + } else { + open_view(prev, request, flags | OPEN_PREPARED); } +} - 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(""); - } +static void +open_file(struct view *prev, struct view *view, const char *file, enum open_flags flags) +{ + const char *file_argv[] = { opt_cdup, file , NULL }; + + open_argv(prev, view, file_argv, opt_cdup, flags); } static void @@ -3043,7 +2842,7 @@ open_external_viewer(const char *argv[], const char *dir) { def_prog_mode(); /* save current tty modes */ endwin(); /* restore original tty modes */ - run_io_fg(argv, dir); + io_run_fg(argv, dir); fprintf(stderr, "Press Enter to continue"); getc(opt_tty); reset_prog_mode(); @@ -3059,7 +2858,7 @@ open_mergetool(const char *file) } static void -open_editor(bool from_root, const char *file) +open_editor(const char *file) { const char *editor_argv[] = { "vi", file, NULL }; const char *editor; @@ -3075,23 +2874,25 @@ open_editor(bool from_root, const char *file) editor = "vi"; editor_argv[0] = editor; - open_external_viewer(editor_argv, from_root ? opt_cdup : NULL); + open_external_viewer(editor_argv, opt_cdup); } static void open_run_request(enum request request) { struct run_request *req = get_run_request(request); - const char *argv[ARRAY_SIZE(req->argv)] = { NULL }; + const char **argv = NULL; if (!req) { report("Unknown run request"); return; } - if (format_argv(argv, req->argv, FORMAT_ALL)) + if (format_argv(&argv, req->argv, FALSE)) open_external_viewer(argv, NULL); - free_argv(argv); + if (argv) + argv_free(argv); + free(argv); } /* @@ -3103,28 +2904,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: @@ -3136,6 +2927,7 @@ view_driver(struct view *view, enum request request) move_view(view, request); break; + case REQ_SCROLL_FIRST_COL: case REQ_SCROLL_LEFT: case REQ_SCROLL_RIGHT: case REQ_SCROLL_LINE_DOWN: @@ -3148,7 +2940,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); @@ -3157,16 +2949,23 @@ 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 (view == NULL) { + if (!io_open(&VIEW(REQ_VIEW_PAGER)->io, "")) + die("Failed to open stdin"); + open_view(view, request, OPEN_PREPARED); + break; + } + 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); @@ -3175,7 +2974,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); @@ -3194,6 +2993,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; @@ -3201,14 +3001,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; @@ -3217,9 +3010,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); } @@ -3247,27 +3038,22 @@ view_driver(struct view *view, enum request request) case REQ_MAXIMIZE: if (displayed_views() == 2) - maximize_view(view); + maximize_view(view, TRUE); break; + case REQ_OPTIONS: case REQ_TOGGLE_LINENO: - toggle_view_option(&opt_line_number, "line numbers"); - break; - case REQ_TOGGLE_DATE: - toggle_view_option(&opt_date, "date display"); - break; - case REQ_TOGGLE_AUTHOR: - toggle_view_option(&opt_author, "author display"); - break; - + case REQ_TOGGLE_GRAPHIC: case REQ_TOGGLE_REV_GRAPH: - toggle_view_option(&opt_rev_graph, "revision graph display"); + case REQ_TOGGLE_REFS: + toggle_option(request); break; - case REQ_TOGGLE_REFS: - toggle_view_option(&opt_show_refs, "reference display"); + 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: @@ -3281,8 +3067,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); @@ -3306,13 +3091,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) { - maximize_view(view->parent); - view->parent = view; + if (view->prev && view->prev != view) { + maximize_view(view->prev, TRUE); + view->prev = view; break; } /* Fall-through */ @@ -3320,7 +3104,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; } @@ -3332,27 +3117,108 @@ 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(time_t *time, const char *zone) +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; - tz += ('0' - zone[4]); + tz += ('0' - zone[3]) * 60 * 10; + tz += ('0' - zone[4]) * 60; if (zone[0] == '-') tz = -tz; - *time -= tz; + time->tz = tz; + time->sec -= tz; } /* Parse author lines where the name may be empty: * author 1138474660 +0100 */ static void -parse_author_line(char *ident, char *author, size_t authorsize, struct tm *tm) +parse_author_line(char *ident, const char **author, struct time *time) { char *nameend = strchr(ident, '<'); char *emailend = strchr(ident, '>'); @@ -3367,76 +3233,18 @@ parse_author_line(char *ident, char *author, size_t authorsize, struct tm *tm) ident = "Unknown"; } - string_ncopy_do(author, authorsize, ident, strlen(ident)); + *author = get_author(ident); /* Parse epoch and timezone */ if (emailend && emailend[1] == ' ') { char *secs = emailend + 2; char *zone = strchr(secs, ' '); - time_t time = (time_t) atol(secs); - - if (zone && strlen(zone) == STRING_SIZE(" +0700")) - parse_timezone(&time, zone + 1); - - gmtime_r(&time, tm); - } -} - -static enum input_status -select_commit_parent_handler(void *data, char *buf, int c) -{ - size_t parents = *(size_t *) data; - int parent = 0; - if (!isdigit(c)) - return INPUT_SKIP; + parse_timesec(time, secs); - if (*buf) - parent = atoi(buf) * 10; - parent += c - '0'; - - if (parent > parents) - return INPUT_SKIP; - return INPUT_OK; -} - -static bool -select_commit_parent(const char *id, char rev[SIZEOF_REV], const char *path) -{ - char buf[SIZEOF_STR * 4]; - const char *revlist_argv[] = { - "git", "rev-list", "-1", "--parents", id, "--", path, NULL - }; - int parents; - - if (!run_io_buf(revlist_argv, buf, sizeof(buf)) || - !*chomp_string(buf) || - (parents = (strlen(buf) / 40) - 1) < 0) { - report("Failed to get parent information"); - return FALSE; - - } else if (parents == 0) { - if (path) - report("Path '%s' does not exist in the parent", path); - else - report("The selected commit has no parents"); - return FALSE; - } - - if (parents > 1) { - char prompt[SIZEOF_STR]; - char *result; - - if (!string_format(prompt, "Which parent? [1..%d] ", parents)) - return FALSE; - result = prompt_input(prompt, select_commit_parent_handler, &parents); - if (!result) - return FALSE; - parents = atoi(result); + if (zone && strlen(zone) == STRING_SIZE(" +0700")) + parse_timezone(time, zone + 1); } - - string_copy_rev(rev, &buf[41 * parents]); - return TRUE; } /* @@ -3446,13 +3254,10 @@ select_commit_parent(const char *id, char rev[SIZEOF_REV], const char *path) static bool pager_draw(struct view *view, struct line *line, unsigned int lineno) { - 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); + draw_text(view, line->type, line->data); return TRUE; } @@ -3460,13 +3265,9 @@ static bool add_describe_ref(char *buf, size_t *bufpos, const char *commit_id, const char *sep) { const char *describe_argv[] = { "git", "describe", commit_id, NULL }; - char refbuf[SIZEOF_STR]; - char *ref = NULL; + char ref[SIZEOF_STR]; - if (run_io_buf(describe_argv, refbuf, sizeof(refbuf))) - ref = chomp_string(refbuf); - - 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. */ @@ -3481,22 +3282,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"; @@ -3505,9 +3306,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)) @@ -3533,8 +3334,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; @@ -3549,8 +3350,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; } @@ -3572,16 +3373,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; + const char *text[] = { line->data, NULL }; - if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH) - return FALSE; - - return TRUE; + return grep_text(view, text); } static void @@ -3590,7 +3384,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); } @@ -3598,8 +3392,7 @@ pager_select(struct view *view, struct line *line) static struct view_ops pager_ops = { "line", - NULL, - NULL, + view_open, pager_read, pager_draw, pager_request, @@ -3607,9 +3400,15 @@ 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 bool +log_open(struct view *view, enum open_flags flags) +{ + static const char *log_argv[] = { + "git", "log", "--no-color", "--cc", "--stat", "-n100", "%(head)", NULL + }; + + return begin_update(view, NULL, log_argv, flags); +} static enum request log_request(struct view *view, enum request request, struct line *line) @@ -3617,7 +3416,7 @@ log_request(struct view *view, enum request request, struct line *line) switch (request) { case REQ_REFRESH: load_refs(); - open_view(view, REQ_VIEW_LOG, OPEN_REFRESH); + refresh_view(view); return REQ_NONE; default: return pager_request(view, request, line); @@ -3626,8 +3425,7 @@ log_request(struct view *view, enum request request, struct line *line) static struct view_ops log_ops = { "line", - log_argv, - NULL, + log_open, pager_read, pager_draw, log_request, @@ -3635,16 +3433,49 @@ 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 bool +diff_open(struct view *view, enum open_flags flags) +{ + static const char *diff_argv[] = { + "git", "show", "--pretty=fuller", "--no-color", "--root", + "--patch-with-stat", "--find-copies-harder", "-C", + "%(diffargs)", "%(commit)", "--", "%(fileargs)", NULL + }; + + return begin_update(view, NULL, diff_argv, flags); +} + +static bool +diff_read(struct view *view, char *data) +{ + if (!data) { + /* Fall back to retry if no diff will be shown. */ + if (view->lines == 0 && opt_file_argv) { + int pos = argv_size(view->argv) + - argv_size(opt_file_argv) - 1; + + if (pos > 0 && !strcmp(view->argv[pos], "--")) { + for (; view->argv[pos]; pos++) { + free((void *) view->argv[pos]); + view->argv[pos] = NULL; + } + + if (view->pipe) + io_done(view->pipe); + if (io_run(&view->io, IO_RD, view->dir, view->argv)) + return FALSE; + } + } + return TRUE; + } + + return pager_read(view, data); +} static struct view_ops diff_ops = { "line", - diff_argv, - NULL, - pager_read, + diff_open, + diff_read, pager_draw, pager_request, pager_grep, @@ -3655,80 +3486,127 @@ static struct view_ops diff_ops = { * Help backend */ +static bool help_keymap_hidden[ARRAY_SIZE(keymap_table)]; + static bool -help_open(struct view *view) +help_open_keymap_title(struct view *view, enum keymap keymap) +{ + struct line *line; + + line = add_line_format(view, LINE_HELP_KEYMAP, "[%c] %s bindings", + help_keymap_hidden[keymap] ? '+' : '-', + enum_name(keymap_table[keymap])); + if (line) + line->other = keymap; + + return help_keymap_hidden[keymap]; +} + +static void +help_open_keymap(struct view *view, enum keymap keymap) { + const char *group = NULL; char buf[SIZEOF_STR]; size_t bufpos; + bool add_title = TRUE; int i; - if (view->lines > 0) - return TRUE; - - add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT); - for (i = 0; i < ARRAY_SIZE(req_info); i++) { - const char *key; + const char *key = NULL; if (req_info[i].request == REQ_NONE) continue; if (!req_info[i].request) { - add_line_text(view, "", LINE_DEFAULT); - add_line_text(view, req_info[i].help, LINE_DEFAULT); + group = req_info[i].help; continue; } - key = get_key(req_info[i].request); - if (!*key) - key = "(no key defined)"; + key = get_keys(keymap, req_info[i].request, TRUE); + if (!key || !*key) + continue; + + if (add_title && help_open_keymap_title(view, keymap)) + return; + add_title = FALSE; - for (bufpos = 0; bufpos <= req_info[i].namelen; bufpos++) { - buf[bufpos] = tolower(req_info[i].name[bufpos]); - if (buf[bufpos] == '_') - buf[bufpos] = '-'; + if (group) { + add_line_text(view, group, LINE_HELP_GROUP); + group = NULL; } - add_line_format(view, LINE_DEFAULT, " %-25s %-20s %s", - key, buf, req_info[i].help); + add_line_format(view, LINE_DEFAULT, " %-25s %-20s %s", key, + enum_name(req_info[i]), req_info[i].help); } - if (run_requests) { - add_line_text(view, "", LINE_DEFAULT); - add_line_text(view, "External commands:", LINE_DEFAULT); - } + group = "External commands:"; for (i = 0; i < run_requests; i++) { struct run_request *req = get_run_request(REQ_NONE + i + 1); const char *key; int argc; - if (!req) + if (!req || req->keymap != keymap) continue; key = get_key_name(req->key); if (!*key) key = "(no key defined)"; + if (add_title && help_open_keymap_title(view, keymap)) + return; + if (group) { + add_line_text(view, group, LINE_HELP_GROUP); + group = NULL; + } + for (bufpos = 0, argc = 0; req->argv[argc]; argc++) if (!string_format_from(buf, &bufpos, "%s%s", argc ? " " : "", req->argv[argc])) - return REQ_NONE; + return; - add_line_format(view, LINE_DEFAULT, " %-10s %-14s `%s`", - keymap_table[req->keymap].name, key, buf); + add_line_format(view, LINE_DEFAULT, " %-25s `%s`", key, buf); } +} + +static bool +help_open(struct view *view, enum open_flags flags) +{ + 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]; + refresh_view(view); + } + + 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, }; @@ -3799,26 +3677,57 @@ push_tree_stack_entry(const char *name, unsigned long lineno) struct tree_entry { char id[SIZEOF_REV]; mode_t mode; - struct tm time; /* Date from the author ident. */ - char author[75]; /* Author of the commit. */ + struct time time; /* Date from the author ident. */ + const char *author; /* Author of the commit. */ char name[1]; }; static const char * -tree_path(struct line *line) +tree_path(const struct line *line) { return ((struct tree_entry *) line->data)->name; } - static int -tree_compare_entry(struct line *line1, struct line *line2) +tree_compare_entry(const struct line *line1, const struct line *line2) { if (line1->type != line2->type) return line1->type == LINE_TREE_DIR ? -1 : 1; return strcmp(tree_path(line1), tree_path(line2)); } +static const enum sort_field tree_sort_fields[] = { + ORDERBY_NAME, ORDERBY_DATE, ORDERBY_AUTHOR +}; +static struct sort_state tree_sort_state = SORT_STATE(tree_sort_fields); + +static int +tree_compare(const void *l1, const void *l2) +{ + const struct line *line1 = (const struct line *) l1; + const struct line *line2 = (const struct line *) l2; + const struct tree_entry *entry1 = ((const struct line *) l1)->data; + const struct tree_entry *entry2 = ((const struct line *) l2)->data; + + if (line1->type == LINE_TREE_HEAD) + return -1; + if (line2->type == LINE_TREE_HEAD) + return 1; + + switch (get_sort_field(tree_sort_state)) { + case ORDERBY_DATE: + return sort_order(tree_sort_state, timecmp(&entry1->time, &entry2->time)); + + case ORDERBY_AUTHOR: + return sort_order(tree_sort_state, strcmp(entry1->author, entry2->author)); + + case ORDERBY_NAME: + default: + return sort_order(tree_sort_state, tree_compare_entry(line1, line2)); + } +} + + static struct line * tree_entry(struct view *view, enum line_type type, const char *path, const char *mode, const char *id) @@ -3843,21 +3752,19 @@ tree_entry(struct view *view, enum line_type type, const char *path, static bool tree_read_date(struct view *view, char *text, bool *read_date) { - static char author_name[SIZEOF_STR]; - static struct tm author_time; + static const char *author_name; + static 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 + "--cc", "--raw", view->id, "--", "%(directory)", NULL }; - struct io io = {}; if (!view->lines) { tree_entry(view, LINE_TREE_HEAD, opt_path, NULL, NULL); @@ -3865,19 +3772,17 @@ tree_read_date(struct view *view, char *text, bool *read_date) return TRUE; } - if (!run_io_rd(&io, log_file, FORMAT_NONE)) { + if (!begin_update(view, opt_cdup, log_file, OPEN_EXTRA)) { report("Failed to load tree data"); return TRUE; } - done_io(view->pipe); - view->io = io; *read_date = TRUE; return FALSE; } else if (*text == 'a' && get_line_type(text) == LINE_AUTHOR) { parse_author_line(text + STRING_SIZE("author "), - author_name, sizeof(author_name), &author_time); + &author_name, &author_time); } else if (*text == ':') { char *pos; @@ -3888,8 +3793,6 @@ tree_read_date(struct view *view, char *text, bool *read_date) if (!pos) return TRUE; text = pos + 1; - if (*opt_prefix && !strncmp(text, opt_prefix, strlen(opt_prefix))) - text += strlen(opt_prefix); if (*opt_path && !strncmp(text, opt_path, strlen(opt_path))) text += strlen(opt_path); pos = strchr(text, '/'); @@ -3900,18 +3803,18 @@ tree_read_date(struct view *view, char *text, bool *read_date) struct line *line = &view->line[i]; struct tree_entry *entry = line->data; - annotated += !!*entry->author; - if (*entry->author || strcmp(entry->name, text)) + annotated += !!entry->author; + if (entry->author || strcmp(entry->name, text)) continue; - string_copy(entry->author, author_name); - memcpy(&entry->time, &author_time, sizeof(entry->time)); + entry->author = author_name; + entry->time = author_time; line->dirty = 1; break; } if (annotated == view->lines) - kill_io(view->pipe); + io_kill(view->pipe); } return TRUE; } @@ -3984,35 +3887,36 @@ 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)) + if (draw_text(view, line->type, "Directory path /")) return TRUE; } else { if (draw_mode(view, entry->mode)) return TRUE; - if (opt_author && draw_author(view, entry->author)) + if (draw_author(view, entry->author)) return TRUE; - if (opt_date && draw_date(view, *entry->author ? &entry->time : NULL)) + if (draw_date(view, &entry->time)) return TRUE; } - if (draw_text(view, line->type, entry->name, TRUE)) - return TRUE; + + draw_text(view, line->type, entry->name); return TRUE; } static void -open_blob_editor() +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 (!run_io_append(blob_ops.argv, FORMAT_ALL, fd)) + else if (!io_run_append(blob_argv, fd)) report("Failed to save blob data to file"); else - open_editor(FALSE, file); + open_editor(file); if (fd != -1) unlink(file); } @@ -4021,6 +3925,7 @@ 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: @@ -4036,12 +3941,17 @@ 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)) { - open_blob_editor(); + open_blob_editor(entry->id); } else { - open_editor(TRUE, opt_file); + open_editor(opt_file); } return REQ_NONE; + case REQ_TOGGLE_SORT_FIELD: + case REQ_TOGGLE_SORT_ORDER: + sort_view(view, request, &tree_sort_state, tree_compare); + return REQ_NONE; + case REQ_PARENT: if (!*opt_path) { /* quit view if at top of tree */ @@ -4082,7 +3992,7 @@ 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; @@ -4097,6 +4007,20 @@ tree_request(struct view *view, enum request request, struct line *line) return REQ_NONE; } +static bool +tree_grep(struct view *view, struct line *line) +{ + struct tree_entry *entry = line->data; + const char *text[] = { + entry->name, + opt_author ? entry->author : "", + mkdate(&entry->time, opt_date), + NULL + }; + + return grep_text(view, text); +} + static void tree_select(struct view *view, struct line *line) { @@ -4113,21 +4037,56 @@ tree_select(struct view *view, struct line *line) string_copy_rev(view->ref, entry->id); } -static const char *tree_argv[SIZEOF_ARG] = { - "git", "ls-tree", "%(commit)", "%(directory)", NULL -}; +static bool +tree_open(struct view *view, enum open_flags flags) +{ + static const char *tree_argv[] = { + "git", "ls-tree", "%(commit)", "%(directory)", NULL + }; + + 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 begin_update(view, opt_cdup, tree_argv, flags); +} static struct view_ops tree_ops = { "file", - tree_argv, - NULL, + tree_open, tree_read, tree_draw, tree_request, - pager_grep, + tree_grep, tree_select, }; +static bool +blob_open(struct view *view, enum open_flags flags) +{ + static const char *blob_argv[] = { + "git", "cat-file", "blob", "%(blob)", NULL + }; + + return begin_update(view, NULL, blob_argv, flags); +} + static bool blob_read(struct view *view, char *line) { @@ -4141,21 +4100,16 @@ blob_request(struct view *view, enum request request, struct line *line) { switch (request) { case REQ_EDIT: - open_blob_editor(); + 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_open, blob_read, pager_draw, blob_request, @@ -4174,25 +4128,14 @@ static struct view_ops blob_ops = { * reading output from git-blame. */ -static const char *blame_head_argv[] = { - "git", "blame", "--incremental", "--", "%(file)", NULL -}; - -static const char *blame_ref_argv[] = { - "git", "blame", "--incremental", "%(ref)", "--", "%(file)", NULL -}; - -static const char *blame_cat_file_argv[] = { - "git", "cat-file", "blob", "%(ref):%(file)", NULL -}; - 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. */ + char parent_id[SIZEOF_REV]; /* Parent/previous SHA1 ID. */ + char parent_filename[128]; /* Parent/previous name of file. */ }; struct blame { @@ -4201,15 +4144,48 @@ struct blame { char text[1]; }; -static bool -blame_open(struct view *view) -{ - if (*opt_ref || !io_open(&view->io, opt_file)) { - if (!run_io_rd(&view->io, blame_cat_file_argv, FORMAT_ALL)) - return FALSE; +static bool +blame_open(struct view *view, enum open_flags flags) +{ + const char *file_argv[] = { opt_cdup, opt_file , NULL }; + char path[SIZEOF_STR]; + size_t i; + + 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 || !begin_update(view, opt_cdup, file_argv, flags)) { + const char *blame_cat_file_argv[] = { + "git", "cat-file", "blob", path, NULL + }; + + if (!string_format(path, "%s:%s", opt_ref, opt_file) || + !begin_update(view, opt_cdup, blame_cat_file_argv, flags)) + return FALSE; + } + + /* First pass: remove multiple references to the same commit. */ + for (i = 0; i < view->lines; i++) { + struct blame *blame = view->line[i].data; + + if (blame->commit && blame->commit->id[0]) + blame->commit->id[0] = 0; + else + blame->commit = NULL; + } + + /* Second pass: free existing references. */ + for (i = 0; i < view->lines; i++) { + struct blame *blame = view->line[i].data; + + if (blame->commit) + free(blame->commit); } - setup_update(view, opt_file); + string_format(view->vid, "%s:%s", opt_ref, opt_file); string_format(view->ref, "%s ...", opt_file); return TRUE; @@ -4295,19 +4271,19 @@ static bool blame_read_file(struct view *view, const char *line, bool *read_file) { if (!line) { - const char **argv = *opt_ref ? blame_ref_argv : blame_head_argv; - struct io io = {}; + const char *blame_argv[] = { + "git", "blame", "%(blameargs)", "--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 || !run_io_rd(&io, argv, FORMAT_ALL)) { + if (view->lines == 0 || !begin_update(view, opt_cdup, blame_argv, OPEN_EXTRA)) { report("Failed to load blame data"); return TRUE; } - done_io(view->pipe); - view->io = io; *read_file = FALSE; return FALSE; @@ -4342,7 +4318,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) @@ -4367,20 +4342,23 @@ blame_read(struct view *view, char *line) view->lines ? blamed * 100 / view->lines : 0); } else if (match_blame_header("author ", &line)) { - string_ncopy(commit->author, line, strlen(line)); + commit->author = get_author(line); } else if (match_blame_header("author-time ", &line)) { - author_time = (time_t) atol(line); + parse_timesec(&commit->time, line); } else if (match_blame_header("author-tz ", &line)) { - parse_timezone(&author_time, line); - 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; + if (strlen(line) <= SIZEOF_REV) + return FALSE; + string_copy_rev(commit->parent_id, line); + line += SIZEOF_REV; + string_ncopy(commit->parent_filename, line, strlen(line)); } else if (match_blame_header("filename ", &line)) { string_ncopy(commit->filename, line, strlen(line)); @@ -4394,9 +4372,8 @@ 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; @@ -4404,10 +4381,10 @@ blame_draw(struct view *view, struct line *line, unsigned int lineno) time = &blame->commit->time; } - if (opt_date && draw_date(view, time)) + if (draw_date(view, time)) return TRUE; - if (opt_author && draw_author(view, author)) + if (draw_author(view, author)) return TRUE; if (draw_field(view, LINE_BLAME_ID, id, ID_COLS, FALSE)) @@ -4416,8 +4393,7 @@ blame_draw(struct view *view, struct line *line, unsigned int lineno) if (draw_lineno(view, lineno)) return TRUE; - string_expand(text, sizeof(text), blame->text, opt_tab_size); - draw_text(view, LINE_DEFAULT, text, TRUE); + draw_text(view, LINE_DEFAULT, blame->text); return TRUE; } @@ -4436,16 +4412,20 @@ check_blame_commit(struct blame *blame, bool check_null_id) static void setup_blame_parent_line(struct view *view, struct blame *blame) { + char from[SIZEOF_REF + SIZEOF_STR]; + char to[SIZEOF_REF + SIZEOF_STR]; const char *diff_tree_argv[] = { - "git", "diff-tree", "-U0", blame->commit->id, - "--", blame->commit->filename, NULL + "git", "diff", "--no-textconv", "--no-extdiff", "--no-color", + "-U0", from, to, "--", NULL }; - struct io io = {}; + struct io io; int parent_lineno = -1; int blamed_lineno = -1; char *line; - if (!run_io(&io, diff_tree_argv, NULL, IO_RD)) + if (!string_format(from, "%s:%s", opt_ref, opt_file) || + !string_format(to, "%s:%s", blame->commit->id, blame->commit->filename) || + !io_run(&io, IO_RD, NULL, diff_tree_argv)) return; while ((line = io_get(&io, '\n', TRUE))) { @@ -4466,13 +4446,13 @@ setup_blame_parent_line(struct view *view, struct blame *blame) } } - done_io(&io); + io_done(&io); } 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; + enum open_flags flags = view_is_displayed(view) ? OPEN_SPLIT : OPEN_DEFAULT; struct blame *blame = line->data; switch (request) { @@ -4482,17 +4462,20 @@ blame_request(struct view *view, enum request request, struct line *line) string_copy(opt_file, blame->commit->filename); if (blame->lineno) view->lineno = blame->lineno; - open_view(view, REQ_VIEW_BLAME, OPEN_REFRESH); + refresh_view(view); } 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); + if (!check_blame_commit(blame, TRUE)) + break; + if (!*blame->commit->parent_id) { + report("The selected commit has no parents"); + } else { + string_copy_rev(opt_ref, blame->commit->parent_id); + string_copy(opt_file, blame->commit->parent_filename); setup_blame_parent_line(view, blame); - open_view(view, REQ_VIEW_BLAME, OPEN_REFRESH); + refresh_view(view); } break; @@ -4511,21 +4494,17 @@ blame_request(struct view *view, enum request request, struct line *line) "-C", "-M", "HEAD", "--", view->vid, NULL }; - if (!blame->commit->has_previous) { + if (!*blame->commit->parent_id) { 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, FORMAT_DASH)) { - report("Failed to allocate diff command"); - break; - } - flags |= OPEN_PREPARED; + open_argv(view, diff, diff_index_argv, NULL, flags); + } else { + open_view(view, REQ_VIEW_DIFF, flags); } - - 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; @@ -4542,27 +4521,16 @@ blame_grep(struct view *view, struct line *line) { struct blame *blame = line->data; struct blame_commit *commit = blame->commit; - regmatch_t pmatch; - -#define MATCH(text, on) \ - (on && *text && regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH) - - if (commit) { - char buf[DATE_COLS + 1]; - - if (MATCH(commit->title, 1) || - MATCH(commit->author, opt_author) || - MATCH(commit->id, opt_date)) - return TRUE; - - if (strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time) && - MATCH(buf, 1)) - return TRUE; - } - - return MATCH(blame->text, 1); + const char *text[] = { + blame->text, + commit ? commit->title : "", + commit ? commit->id : "", + commit && opt_author ? commit->author : "", + commit ? mkdate(&commit->time, opt_date) : "", + NULL + }; -#undef MATCH + return grep_text(view, text); } static void @@ -4582,7 +4550,6 @@ blame_select(struct view *view, struct line *line) static struct view_ops blame_ops = { "line", - NULL, blame_open, blame_read, blame_draw, @@ -4591,6 +4558,204 @@ static struct view_ops blame_ops = { 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 (draw_date(view, &branch->time)) + return TRUE; + + if (draw_author(view, branch->author)) + return TRUE; + + draw_text(view, type, branch->ref == &branch_all ? "All branches" : branch->ref->name); + 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(); + refresh_view(view); + 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: + { + const struct ref *ref = branch->ref; + const char *all_branches_argv[] = { + "git", "log", "--no-color", "--pretty=raw", "--parents", + "--topo-order", + ref == &branch_all ? "--all" : ref->name, NULL + }; + struct view *main_view = VIEW(REQ_VIEW_MAIN); + + open_argv(view, main_view, all_branches_argv, NULL, OPEN_SPLIT); + return REQ_NONE; + } + default: + return request; + } +} + +static bool +branch_read(struct view *view, char *line) +{ + static char id[SIZEOF_REV]; + struct branch *reference; + size_t i; + + if (!line) + return TRUE; + + switch (get_line_type(line)) { + case LINE_COMMIT: + string_copy_rev(id, line + STRING_SIZE("commit ")); + return TRUE; + + case LINE_AUTHOR: + for (i = 0, reference = NULL; i < view->lines; i++) { + struct branch *branch = view->line[i].data; + + if (strcmp(branch->ref->id, id)) + continue; + + view->line[i].dirty = TRUE; + if (reference) { + branch->author = reference->author; + branch->time = reference->time; + continue; + } + + parse_author_line(line + STRING_SIZE("author "), + &branch->author, &branch->time); + reference = branch; + } + return TRUE; + + default: + return TRUE; + } + +} + +static bool +branch_open_visitor(void *data, const struct ref *ref) +{ + struct view *view = data; + struct branch *branch; + + if (ref->tag || ref->ltag || ref->remote) + return TRUE; + + branch = calloc(1, sizeof(*branch)); + if (!branch) + return FALSE; + + branch->ref = ref; + return !!add_line_data(view, branch, LINE_DEFAULT); +} + +static bool +branch_open(struct view *view, enum open_flags flags) +{ + const char *branch_log[] = { + "git", "log", "--no-color", "--pretty=raw", + "--simplify-by-decoration", "--all", NULL + }; + + if (!begin_update(view, NULL, branch_log, flags)) { + report("Failed to load branch data"); + return TRUE; + } + + branch_open_visitor(view, &branch_all); + foreach_ref(branch_open_visitor, view); + view->p_restore = TRUE; + + return TRUE; +} + +static bool +branch_grep(struct view *view, struct line *line) +{ + struct branch *branch = line->data; + const char *text[] = { + branch->ref->name, + branch->author, + NULL + }; + + return grep_text(view, text); +} + +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 branch_ops = { + "branch", + branch_open, + branch_read, + branch_draw, + branch_request, + branch_grep, + branch_select, +}; + /* * Status backend */ @@ -4615,6 +4780,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) @@ -4660,9 +4827,9 @@ status_run(struct view *view, const char *argv[], char status, enum line_type ty { struct status *unmerged = NULL; char *buf; - struct io io = {}; + struct io io; - if (!run_io(&io, argv, NULL, IO_RD)) + if (!io_run(&io, IO_RD, opt_cdup, argv)) return FALSE; add_line_data(view, NULL, type); @@ -4721,14 +4888,14 @@ status_run(struct view *view, const char *argv[], char status, enum line_type ty if (io_error(&io)) { error_out: - done_io(&io); + io_done(&io); return FALSE; } if (!view->line[view->lines - 1].data) add_line_data(view, NULL, LINE_STAT_NONE); - done_io(&io); + io_done(&io); return TRUE; } @@ -4743,7 +4910,7 @@ static const char *status_diff_files_argv[] = { }; static const char *status_list_other_argv[] = { - "git", "ls-files", "-z", "--others", "--exclude-standard", NULL + "git", "ls-files", "-z", "--others", "--exclude-standard", opt_prefix, NULL, NULL, }; static const char *status_list_no_head_argv[] = { @@ -4810,12 +4977,11 @@ status_update_onbranch(void) continue; if (!*opt_head) { - struct io io = {}; + struct io io; - if (string_format(buf, "%s/rebase-merge/head-name", opt_git_dir) && - io_open(&io, buf) && + if (io_open(&io, "%s/rebase-merge/head-name", opt_git_dir) && io_read_buf(&io, buf, sizeof(buf))) { - head = chomp_string(buf); + head = buf; if (!prefixcmp(head, "refs/heads/")) head += STRING_SIZE("refs/heads/"); } @@ -4833,14 +4999,14 @@ status_update_onbranch(void) * info using git-diff-files(1), and finally untracked files using * git-ls-files(1). */ static bool -status_open(struct view *view) +status_open(struct view *view, enum open_flags flags) { reset_view(view); add_line_data(view, NULL, LINE_STAT_HEAD); status_update_onbranch(); - run_io_bg(update_index_argv); + io_run_bg(update_index_argv); if (is_initial_commit()) { if (!status_run(view, status_list_no_head_argv, 'A', LINE_STAT_STAGED)) @@ -4849,6 +5015,9 @@ status_open(struct view *view) return FALSE; } + if (!opt_untracked_dirs_content) + status_list_other_argv[ARRAY_SIZE(status_list_other_argv) - 2] = "--directory"; + if (!status_run(view, status_diff_files_argv, 0, LINE_STAT_UNSTAGED) || !status_run(view, status_list_other_argv, '?', LINE_STAT_UNTRACKED)) return FALSE; @@ -4901,25 +5070,16 @@ status_draw(struct view *view, struct line *line, unsigned int lineno) static char buf[] = { '?', ' ', ' ', ' ', 0 }; buf[0] = status->status; - if (draw_text(view, line->type, buf, TRUE)) + if (draw_text(view, line->type, buf)) return TRUE; type = LINE_DEFAULT; text = status->new.name; } - draw_text(view, type, text, TRUE); + draw_text(view, type, text); 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) { @@ -4929,7 +5089,7 @@ status_enter(struct view *view, struct line *line) * path, so leave it empty. */ const char *newpath = status && status->status != 'U' ? status->new.name : NULL; const char *info; - enum open_flags split; + enum open_flags flags = view_is_displayed(view) ? OPEN_SPLIT : OPEN_DEFAULT; struct view *stage = VIEW(REQ_VIEW_STAGE); if (line->type == LINE_STAT_NONE || @@ -4946,8 +5106,7 @@ status_enter(struct view *view, struct line *line) "--", "/dev/null", newpath, NULL }; - if (!prepare_update(stage, no_head_diff_argv, opt_cdup, FORMAT_DASH)) - return status_load_error(view, stage, newpath); + open_argv(view, stage, no_head_diff_argv, opt_cdup, flags); } else { const char *index_show_argv[] = { "git", "diff-index", "--root", "--patch-with-stat", @@ -4955,8 +5114,7 @@ status_enter(struct view *view, struct line *line) oldpath, newpath, NULL }; - if (!prepare_update(stage, index_show_argv, opt_cdup, FORMAT_DASH)) - return status_load_error(view, stage, newpath); + open_argv(view, stage, index_show_argv, opt_cdup, flags); } if (status) @@ -4972,8 +5130,7 @@ status_enter(struct view *view, struct line *line) "-C", "-M", "--", oldpath, newpath, NULL }; - if (!prepare_update(stage, files_show_argv, opt_cdup, FORMAT_DASH)) - return status_load_error(view, stage, newpath); + open_argv(view, stage, files_show_argv, opt_cdup, flags); if (status) info = "Unstaged changes to %s"; else @@ -4991,8 +5148,7 @@ status_enter(struct view *view, struct line *line) return REQ_NONE; } - if (!prepare_update_file(stage, newpath)) - return status_load_error(view, stage, newpath); + open_file(view, stage, newpath, flags); info = "Untracked file %s"; break; @@ -5003,8 +5159,6 @@ 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_PREPARED | split); if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) { if (status) { stage_status = *status; @@ -5058,13 +5212,11 @@ status_update_prepare(struct io *io, enum line_type type) switch (type) { case LINE_STAT_STAGED: - return run_io(io, staged_argv, opt_cdup, IO_WR); + return io_run(io, IO_WR, opt_cdup, staged_argv); case LINE_STAT_UNSTAGED: - return run_io(io, others_argv, opt_cdup, IO_WR); - case LINE_STAT_UNTRACKED: - return run_io(io, others_argv, NULL, IO_WR); + return io_run(io, IO_WR, opt_cdup, others_argv); default: die("line type %d not handled in switch", type); @@ -5103,25 +5255,26 @@ status_update_write(struct io *io, struct status *status, enum line_type type) static bool status_update_file(struct status *status, enum line_type type) { - struct io io = {}; + struct io io; bool result; if (!status_update_prepare(&io, type)) return FALSE; result = status_update_write(&io, status, type); - return done_io(&io) && result; + return io_done(&io) && result; } static bool status_update_files(struct view *view, struct line *line) { char buf[sizeof(view->ref)]; - struct io io = {}; + struct io io; bool result = TRUE; struct line *pos = view->line + view->lines; int files = 0; int file, done; + int cursor_y = -1, cursor_x = -1; if (!status_update_prepare(&io, line->type)) return FALSE; @@ -5130,6 +5283,7 @@ status_update_files(struct view *view, struct line *line) files++; 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; @@ -5138,13 +5292,14 @@ status_update_files(struct view *view, struct line *line) string_format(view->ref, "updating file %u of %u (%d%% done)", file, files, done); update_view_title(view); + setsyx(cursor_y, cursor_x); doupdate(); } result = status_update_write(&io, line->data, line->type); } string_copy(view->ref, buf); - return done_io(&io) && result; + return io_done(&io) && result; } static bool @@ -5187,9 +5342,8 @@ status_revert(struct status *status, enum line_type type, bool has_none) } else { report("Cannot revert changes to multiple files"); } - return FALSE; - } else { + } else if (prompt_yesno("Are you sure you want to revert changes?")) { char mode[10] = "100644"; const char *reset_argv[] = { "git", "update-index", "--cacheinfo", mode, @@ -5199,12 +5353,25 @@ status_revert(struct status *status, enum line_type type, bool has_none) "git", "checkout", "--", status->old.name, NULL }; - if (!prompt_yesno("Are you sure you want to overwrite any changes?")) - return FALSE; - string_format(mode, "%o", status->old.mode); - return (status->status != 'U' || run_io_fg(reset_argv, opt_cdup)) && - run_io_fg(checkout_argv, opt_cdup); + if (status->status == 'U') { + string_format(mode, "%5o", status->old.mode); + + if (status->old.mode == 0 && status->new.mode == 0) { + reset_argv[2] = "--force-remove"; + reset_argv[3] = status->old.name; + reset_argv[4] = NULL; + } + + if (!io_run_fg(reset_argv, opt_cdup)) + return FALSE; + if (status->old.mode == 0 && status->new.mode == 0) + return TRUE; + } + + return io_run_fg(checkout_argv, opt_cdup); } + + return FALSE; } static enum request @@ -5239,14 +5406,12 @@ status_request(struct view *view, enum request request, struct line *line) return REQ_NONE; } - open_editor(status->status != '?', status->new.name); + open_editor(status->new.name); break; case REQ_VIEW_BLAME: - if (status) { - string_copy(opt_file, status->new.name); + if (status) opt_ref[0] = 0; - } return request; case REQ_ENTER: @@ -5263,7 +5428,7 @@ status_request(struct view *view, enum request request, struct line *line) return request; } - open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD); + refresh_view(view); return REQ_NONE; } @@ -5306,42 +5471,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; @@ -5349,7 +5499,6 @@ status_grep(struct view *view, struct line *line) static struct view_ops status_ops = { "file", - NULL, status_open, NULL, status_draw, @@ -5392,7 +5541,7 @@ stage_apply_chunk(struct view *view, struct line *chunk, bool revert) "git", "apply", "--whitespace=nowarn", NULL }; struct line *diff_hdr; - struct io io = {}; + struct io io; int argc = 3; diff_hdr = stage_diff_find(view, chunk, LINE_DIFF_HEADER); @@ -5405,15 +5554,15 @@ stage_apply_chunk(struct view *view, struct line *chunk, bool revert) apply_argv[argc++] = "-R"; apply_argv[argc++] = "-"; apply_argv[argc++] = NULL; - if (!run_io(&io, apply_argv, opt_cdup, IO_WR)) + if (!io_run(&io, IO_WR, opt_cdup, apply_argv)) return FALSE; if (!stage_diff_write(&io, diff_hdr, chunk) || !stage_diff_write(&io, chunk, view->line + view->lines)) chunk = NULL; - done_io(&io); - run_io_bg(update_index_argv); + io_done(&io); + io_run_bg(update_index_argv); return chunk ? TRUE : FALSE; } @@ -5483,21 +5632,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; } } @@ -5530,7 +5673,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); @@ -5544,7 +5687,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: @@ -5565,8 +5708,7 @@ stage_request(struct view *view, enum request request, struct line *line) return request; } - VIEW(REQ_VIEW_STATUS)->p_restore = TRUE; - open_view(view, REQ_VIEW_STATUS, OPEN_REFRESH); + refresh_view(view->parent); /* Check whether the staged entry still exists, and close the * stage view if it doesn't. */ @@ -5575,311 +5717,137 @@ stage_request(struct view *view, enum request request, struct line *line) return REQ_VIEW_CLOSE; } - if (stage_line_type == LINE_STAT_UNTRACKED) { - if (!suffixcmp(stage_status.new.name, -1, "/")) { - report("Cannot display a directory"); - return REQ_NONE; - } - - 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); + refresh_view(view); return REQ_NONE; } static struct view_ops stage_ops = { "line", - NULL, - NULL, - pager_read, - pager_draw, - stage_request, - pager_grep, - pager_select, -}; - - -/* - * Revision graph - */ - -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. */ - chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */ - size_t graph_size; /* The width of the graph array. */ - bool has_parents; /* Rewritten --parents seen. */ -}; - -/* Size of rev graph with no "padding" columns */ -#define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2)) - -struct rev_graph { - struct rev_graph *prev, *next, *parents; - char rev[SIZEOF_REVITEMS][SIZEOF_REV]; - size_t size; - struct commit *commit; - size_t pos; - unsigned int boundary:1; -}; - -/* Parents of the commit being visualized. */ -static struct rev_graph graph_parents[4]; - -/* The current stack of revisions on the graph. */ -static struct rev_graph graph_stacks[4] = { - { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] }, - { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] }, - { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] }, - { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] }, -}; - -static inline bool -graph_parent_is_merge(struct rev_graph *graph) -{ - return graph->parents->size > 1; -} - -static inline void -append_to_rev_graph(struct rev_graph *graph, chtype symbol) -{ - struct commit *commit = graph->commit; - - if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1) - commit->graph[commit->graph_size++] = symbol; -} - -static void -clear_rev_graph(struct rev_graph *graph) -{ - graph->boundary = 0; - graph->size = graph->pos = 0; - graph->commit = NULL; - memset(graph->parents, 0, sizeof(*graph->parents)); -} - -static void -done_rev_graph(struct rev_graph *graph) -{ - if (graph_parent_is_merge(graph) && - graph->pos < graph->size - 1 && - graph->next->size == graph->size + graph->parents->size - 1) { - size_t i = graph->pos + graph->parents->size - 1; - - graph->commit->graph_size = i * 2; - while (i < graph->next->size - 1) { - append_to_rev_graph(graph, ' '); - append_to_rev_graph(graph, '\\'); - i++; - } - } - - clear_rev_graph(graph); -} - -static void -push_rev_graph(struct rev_graph *graph, const char *parent) -{ - int i; - - /* "Collapse" duplicate parents lines. - * - * FIXME: This needs to also update update the drawn graph but - * for now it just serves as a method for pruning graph lines. */ - for (i = 0; i < graph->size; i++) - if (!strncmp(graph->rev[i], parent, SIZEOF_REV)) - return; - - if (graph->size < SIZEOF_REVITEMS) { - string_copy_rev(graph->rev[graph->size++], parent); - } -} - -static chtype -get_rev_graph_symbol(struct rev_graph *graph) -{ - chtype symbol; - - if (graph->boundary) - symbol = REVGRAPH_BOUND; - else if (graph->parents->size == 0) - symbol = REVGRAPH_INIT; - else if (graph_parent_is_merge(graph)) - symbol = REVGRAPH_MERGE; - else if (graph->pos >= graph->size) - symbol = REVGRAPH_BRANCH; - else - symbol = REVGRAPH_COMMIT; - - return symbol; -} - -static void -draw_rev_graph(struct rev_graph *graph) -{ - struct rev_filler { - chtype separator, line; - }; - enum { DEFAULT, RSHARP, RDIAG, LDIAG }; - static struct rev_filler fillers[] = { - { ' ', '|' }, - { '`', '.' }, - { '\'', ' ' }, - { '/', ' ' }, - }; - chtype symbol = get_rev_graph_symbol(graph); - struct rev_filler *filler; - size_t i; - - if (opt_line_graphics) - fillers[DEFAULT].line = line_graphics[LINE_GRAPHIC_VLINE]; - - filler = &fillers[DEFAULT]; - - for (i = 0; i < graph->pos; i++) { - append_to_rev_graph(graph, filler->line); - if (graph_parent_is_merge(graph->prev) && - graph->prev->pos == i) - filler = &fillers[RSHARP]; + view_open, + pager_read, + pager_draw, + stage_request, + pager_grep, + pager_select, +}; - append_to_rev_graph(graph, filler->separator); - } - /* Place the symbol for this revision. */ - append_to_rev_graph(graph, symbol); +/* + * Revision graph + */ - if (graph->prev->size > graph->size) - filler = &fillers[RDIAG]; - else - filler = &fillers[DEFAULT]; +static const enum line_type graph_colors[] = { + LINE_GRAPH_LINE_0, + LINE_GRAPH_LINE_1, + LINE_GRAPH_LINE_2, + LINE_GRAPH_LINE_3, + LINE_GRAPH_LINE_4, + LINE_GRAPH_LINE_5, + LINE_GRAPH_LINE_6, +}; - i++; +static enum line_type get_graph_color(struct graph_symbol *symbol) +{ + if (symbol->commit) + return LINE_GRAPH_COMMIT; + assert(symbol->color < ARRAY_SIZE(graph_colors)); + return graph_colors[symbol->color]; +} - for (; i < graph->size; i++) { - append_to_rev_graph(graph, filler->separator); - append_to_rev_graph(graph, filler->line); - if (graph_parent_is_merge(graph->prev) && - i < graph->prev->pos + graph->parents->size) - filler = &fillers[RSHARP]; - if (graph->prev->size > graph->size) - filler = &fillers[LDIAG]; - } +static bool +draw_graph_utf8(struct view *view, struct graph_symbol *symbol, enum line_type color, bool first) +{ + const char *chars = graph_symbol_to_utf8(symbol); - if (graph->prev->size > graph->size) { - append_to_rev_graph(graph, filler->separator); - if (filler->line != ' ') - append_to_rev_graph(graph, filler->line); - } + return draw_text(view, color, chars + !!first); } -/* Prepare the next rev graph */ -static void -prepare_rev_graph(struct rev_graph *graph) +static bool +draw_graph_ascii(struct view *view, struct graph_symbol *symbol, enum line_type color, bool first) { - size_t i; - - /* First, traverse all lines of revisions up to the active one. */ - for (graph->pos = 0; graph->pos < graph->size; graph->pos++) { - if (!strcmp(graph->rev[graph->pos], graph->commit->id)) - break; + const char *chars = graph_symbol_to_ascii(symbol); - push_rev_graph(graph->next, graph->rev[graph->pos]); - } + return draw_text(view, color, chars + !!first); +} - /* Interleave the new revision parent(s). */ - for (i = 0; !graph->boundary && i < graph->parents->size; i++) - push_rev_graph(graph->next, graph->parents->rev[i]); +static bool +draw_graph_chtype(struct view *view, struct graph_symbol *symbol, enum line_type color, bool first) +{ + const chtype *chars = graph_symbol_to_chtype(symbol); - /* Lastly, put any remaining revisions. */ - for (i = graph->pos + 1; i < graph->size; i++) - push_rev_graph(graph->next, graph->rev[i]); + return draw_graphic(view, color, chars + !!first, 2 - !!first, FALSE); } -static void -update_rev_graph(struct view *view, struct rev_graph *graph) +typedef bool (*draw_graph_fn)(struct view *, struct graph_symbol *, enum line_type, bool); + +static bool draw_graph(struct view *view, struct graph_canvas *canvas) { - /* If this is the finalizing update ... */ - if (graph->commit) - prepare_rev_graph(graph); + static const draw_graph_fn fns[] = { + draw_graph_ascii, + draw_graph_chtype, + draw_graph_utf8 + }; + draw_graph_fn fn = fns[opt_line_graphics]; + int i; - /* Graph visualization needs a one rev look-ahead, - * so the first update doesn't visualize anything. */ - if (!graph->prev->commit) - return; + for (i = 0; i < canvas->size; i++) { + struct graph_symbol *symbol = &canvas->symbols[i]; + enum line_type color = get_graph_color(symbol); - 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); -} + if (fn(view, symbol, color, i == 0)) + return TRUE; + } + return draw_text(view, LINE_MAIN_REVGRAPH, " "); +} /* * Main view backend */ -static const char *main_argv[SIZEOF_ARG] = { - "git", "log", "--no-color", "--pretty=raw", "--parents", - "--topo-order", "%(head)", NULL +struct commit { + char id[SIZEOF_REV]; /* SHA1 ID. */ + char title[128]; /* First line of the commit message. */ + const char *author; /* Author of the commit. */ + struct time time; /* Date from the author ident. */ + struct ref_list *refs; /* Repository references. */ + struct graph_canvas graph; /* Ancestry chain graphics. */ }; +static bool +main_open(struct view *view, enum open_flags flags) +{ + static const char *main_argv[] = { + "git", "log", "--no-color", "--pretty=raw", "--parents", + "--topo-order", "%(diffargs)", "%(revargs)", + "--", "%(fileargs)", NULL + }; + + return begin_update(view, NULL, main_argv, flags); +} + 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)) + if (draw_date(view, &commit->time)) return TRUE; - if (opt_author && draw_author(view, commit->author)) + if (draw_author(view, commit->author)) return TRUE; - if (opt_rev_graph && commit->graph_size && - draw_graphic(view, LINE_MAIN_REVGRAPH, commit->graph, commit->graph_size)) + if (opt_rev_graph && draw_graph(view, &commit->graph)) return TRUE; - if (opt_show_refs && commit->refs) { - size_t i = 0; - - do { - enum line_type type; - - if (commit->refs[i]->head) - type = LINE_MAIN_HEAD; - else if (commit->refs[i]->ltag) - type = LINE_MAIN_LOCAL_TAG; - else if (commit->refs[i]->tag) - type = LINE_MAIN_TAG; - else if (commit->refs[i]->tracked) - type = LINE_MAIN_TRACKED; - else if (commit->refs[i]->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, "]", TRUE)) - return TRUE; - - if (draw_text(view, LINE_DEFAULT, " ", TRUE)) - return TRUE; - } while (commit->refs[i++]->next); - } + if (draw_refs(view, commit->refs)) + return TRUE; - draw_text(view, LINE_DEFAULT, commit->title, TRUE); + draw_text(view, LINE_DEFAULT, commit->title); return TRUE; } @@ -5887,53 +5855,43 @@ main_draw(struct view *view, struct line *line, unsigned int lineno) static bool main_read(struct view *view, char *line) { - static struct rev_graph *graph = graph_stacks; + static struct graph graph; enum line_type type; struct commit *commit; 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; view->line[view->lines - 1].dirty = 1; - if (!*commit->author) { + if (!commit->author) { view->lines--; free(commit); - graph->commit = NULL; } } - update_rev_graph(view, graph); - for (i = 0; i < ARRAY_SIZE(graph_stacks); i++) - clear_rev_graph(&graph_stacks[i]); + done_graph(&graph); return TRUE; } type = get_line_type(line); if (type == LINE_COMMIT) { + bool is_boundary; + commit = calloc(1, sizeof(struct commit)); if (!commit) return FALSE; line += STRING_SIZE("commit "); - if (*line == '-') { - graph->boundary = 1; + is_boundary = *line == '-'; + if (is_boundary) line++; - } string_copy_rev(commit->id, line); - commit->refs = get_refs(commit->id); - graph->commit = commit; + commit->refs = get_ref_list(commit->id); add_line_data(view, commit, LINE_MAIN_COMMIT); - - while ((line = strchr(line, ' '))) { - line++; - push_rev_graph(graph->parents, line); - commit->has_parents = TRUE; - } + graph_add_commit(&graph, &commit->graph, commit->id, line, is_boundary); return TRUE; } @@ -5943,17 +5901,14 @@ main_read(struct view *view, char *line) switch (type) { case LINE_PARENT: - if (commit->has_parents) - break; - push_rev_graph(graph->parents, line + STRING_SIZE("parent ")); + if (!graph.has_parents) + graph_add_parent(&graph, line + STRING_SIZE("parent ")); break; case LINE_AUTHOR: parse_author_line(line + STRING_SIZE("author "), - commit->author, sizeof(commit->author), - &commit->time); - update_rev_graph(view, graph); - graph = graph->next; + &commit->author, &commit->time); + graph_render_parents(&graph); break; default: @@ -5985,15 +5940,17 @@ 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: + if (view_is_displayed(view) && display[0] != view) + maximize_view(view, TRUE); open_view(view, REQ_VIEW_DIFF, flags); break; case REQ_REFRESH: load_refs(); - open_view(view, REQ_VIEW_MAIN, OPEN_REFRESH); + refresh_view(view); break; default: return request; @@ -6003,17 +5960,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; } @@ -6022,42 +5980,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 @@ -6071,8 +6001,7 @@ main_select(struct view *view, struct line *line) static struct view_ops main_ops = { "commit", - main_argv, - NULL, + main_open, main_read, main_draw, main_request, @@ -6081,159 +6010,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 UTF-8 branch commit elinks-0.11.0-g31f2c28. - */ - -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 **start, size_t skip, int *width, size_t max_width, int *trimmed, bool reserve) -{ - const char *string = *start; - 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); - 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; -} - - /* * Status management */ @@ -6300,17 +6076,6 @@ report(const char *msg, ...) update_view_title(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) { @@ -6341,7 +6106,7 @@ init_display(void) init_colors(); getmaxyx(stdscr, y, x); - status_win = newwin(1, 0, y - 1, 0); + status_win = newwin(1, x, y - 1, 0); if (!status_win) die("Failed to create status window"); @@ -6349,10 +6114,11 @@ init_display(void) keypad(status_win, TRUE); wbkgdset(status_win, get_line_attr(LINE_STATUS)); +#if defined(NCURSES_VERSION_PATCH) && (NCURSES_VERSION_PATCH >= 20080119) + set_tabsize(opt_tab_size); +#else TABSIZE = opt_tab_size; - if (opt_line_graphics) { - line_graphics[LINE_GRAPHIC_VLINE] = ACS_VLINE; - } +#endif term = getenv("XTERM_VERSION") ? NULL : getenv("COLORTERM"); if (term && !strcmp(term, "gnome-terminal")) { @@ -6386,12 +6152,16 @@ get_input(int prompt_position) input_mode = TRUE; while (TRUE) { + bool loading = FALSE; + 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; } /* Update the cursor position. */ @@ -6408,6 +6178,7 @@ get_input(int prompt_position) /* Refresh, accept single keystroke of input */ doupdate(); + nodelay(status_win, loading); key = wgetch(status_win); /* wgetch() with nodelay() enabled returns ERR when @@ -6523,18 +6294,81 @@ read_prompt(const char *prompt) return prompt_input(prompt, read_prompt_handler, NULL); } +static bool prompt_menu(const char *prompt, const struct menu_item *items, int *selected) +{ + enum input_status status = INPUT_OK; + int size = 0; + + while (items[size].text) + size++; + + while (status == INPUT_OK) { + const struct menu_item *item = &items[*selected]; + int key; + int i; + + mvwprintw(status_win, 0, 0, "%s (%d of %d) ", + prompt, *selected + 1, size); + if (item->hotkey) + wprintw(status_win, "[%c] ", (char) item->hotkey); + wprintw(status_win, "%s", item->text); + wclrtoeol(status_win); + + key = get_input(COLS - 1); + switch (key) { + case KEY_RETURN: + case KEY_ENTER: + case '\n': + status = INPUT_STOP; + break; + + case KEY_LEFT: + case KEY_UP: + *selected = *selected - 1; + if (*selected < 0) + *selected = size - 1; + break; + + case KEY_RIGHT: + case KEY_DOWN: + *selected = (*selected + 1) % size; + break; + + case KEY_ESC: + status = INPUT_CANCEL; + break; + + default: + for (i = 0; items[i].text; i++) + if (items[i].hotkey == key) { + *selected = i; + status = INPUT_STOP; + break; + } + } + } + + /* Clear the status window */ + status_empty = FALSE; + report(""); + + return status != INPUT_CANCEL; +} + /* * Repository properties */ -static struct ref *refs = NULL; -static size_t refs_alloc = 0; +static struct ref **refs = NULL; static size_t refs_size = 0; +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_) @@ -6555,76 +6389,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) +read_ref(char *id, size_t idlen, char *name, size_t namelen, void *data) { - 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; } @@ -6642,34 +6469,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; @@ -6677,31 +6519,58 @@ 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 *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) { - argv_from_env(ls_remote_argv, "TIG_LS_REMOTE"); + 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; - return run_io_load(ls_remote_argv, "\t", read_ref); + if (io_run_load(ls_remote_argv, "\t", read_ref, NULL) == 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 @@ -6722,19 +6591,19 @@ set_remote_branch(const char *name, const char *value, size_t valuelen) } static void -set_repo_config_option(char *name, char *value, int (*cmd)(int, const char **)) +set_repo_config_option(char *name, char *value, enum option_code (*cmd)(int, const char **)) { const char *argv[SIZEOF_ARG] = { name, "=" }; int argc = 1 + (cmd == option_set_command); - int error = ERR; + enum option_code error; if (!argv_from_string(argv, &argc, value)) - config_msg = "Too many option arguments"; + error = OPT_ERR_TOO_MANY_OPTION_ARGUMENTS; else error = cmd(argc, argv); - if (error == ERR) - warn("Option 'tig.%s': %s", name, config_msg); + if (error != OPT_OK) + warn("Option 'tig.%s': %s", name, option_errors[error]); } static bool @@ -6776,7 +6645,7 @@ set_work_tree(const char *value) } static int -read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen) +read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen, void *data) { if (!strcmp(name, "i18n.commitencoding")) string_ncopy(opt_encoding, value, valuelen); @@ -6806,13 +6675,13 @@ read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen static int load_git_config(void) { - const char *config_list_argv[] = { "git", GIT_CONFIG, "--list", NULL }; + const char *config_list_argv[] = { "git", "config", "--list", NULL }; - return run_io_load(config_list_argv, "=", read_repo_config_option); + return io_run_load(config_list_argv, "=", read_repo_config_option, NULL); } static int -read_repo_info(char *name, size_t namelen, char *value, size_t valuelen) +read_repo_info(char *name, size_t namelen, char *value, size_t valuelen, void *data) { if (!opt_git_dir[0]) { string_ncopy(opt_git_dir, name, namelen); @@ -6838,24 +6707,12 @@ read_repo_info(char *name, size_t namelen, char *value, size_t valuelen) static int load_repo_info(void) { - const char *head_argv[] = { - "git", "symbolic-ref", "HEAD", NULL - }; const char *rev_parse_argv[] = { "git", "rev-parse", "--git-dir", "--is-inside-work-tree", "--show-cdup", "--show-prefix", NULL }; - if (run_io_buf(head_argv, opt_head, sizeof(opt_head))) { - chomp_string(opt_head); - if (!prefixcmp(opt_head, "refs/heads/")) { - char *offset = opt_head + STRING_SIZE("refs/heads/"); - - memmove(opt_head, offset, strlen(offset) + 1); - } - } - - return run_io_load(rev_parse_argv, "=", read_repo_info); + return io_run_load(rev_parse_argv, "=", read_repo_info, NULL); } @@ -6868,7 +6725,7 @@ static const char usage[] = "\n" "Usage: tig [options] [revs] [--] [paths]\n" " or: tig show [options] [revs] [--] [paths]\n" -" or: tig blame [rev] path\n" +" or: tig blame [options] [rev] [--] path\n" " or: tig status\n" " or: tig < [git command output]\n" "\n" @@ -6913,27 +6770,50 @@ warn(const char *msg, ...) va_end(args); } +static int +read_filter_args(char *name, size_t namelen, char *value, size_t valuelen, void *data) +{ + const char ***filter_args = data; + + return argv_append(filter_args, name) ? OK : ERR; +} + +static void +filter_rev_parse(const char ***args, const char *arg1, const char *arg2, const char *argv[]) +{ + const char *rev_parse_argv[SIZEOF_ARG] = { "git", "rev-parse", arg1, arg2 }; + const char **all_argv = NULL; + + if (!argv_append_array(&all_argv, rev_parse_argv) || + !argv_append_array(&all_argv, argv) || + !io_run_load(all_argv, "\n", read_filter_args, args) == ERR) + die("Failed to split arguments"); + argv_free(all_argv); + free(all_argv); +} + +static void +filter_options(const char *argv[]) +{ + filter_rev_parse(&opt_file_argv, "--no-revs", "--no-flags", argv); + filter_rev_parse(&opt_diff_argv, "--no-revs", "--flags", argv); + filter_rev_parse(&opt_rev_argv, "--symbolic", "--revs-only", argv); +} + 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; + const char **filter_argv = NULL; + int i; - if (!isatty(STDIN_FILENO)) { - io_open(&VIEW(REQ_VIEW_PAGER)->io, ""); + if (!isatty(STDIN_FILENO)) return REQ_VIEW_PAGER; - } if (argc <= 1) - return REQ_NONE; + return REQ_VIEW_MAIN; subcommand = argv[1]; if (!strcmp(subcommand, "status")) { @@ -6942,16 +6822,18 @@ parse_options(int argc, const char *argv[]) return REQ_VIEW_STATUS; } else if (!strcmp(subcommand, "blame")) { - if (argc <= 2 || argc > 4) + filter_rev_parse(&opt_file_argv, "--no-revs", "--no-flags", argv + 2); + filter_rev_parse(&opt_blame_argv, "--no-revs", "--flags", argv + 2); + filter_rev_parse(&opt_rev_argv, "--symbolic", "--revs-only", argv + 2); + + if (!opt_file_argv || opt_file_argv[1] || (opt_rev_argv && opt_rev_argv[1])) 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++; + if (opt_rev_argv) { + string_ncopy(opt_ref, opt_rev_argv[0], strlen(opt_rev_argv[0])); } - string_ncopy(opt_file, argv[i], strlen(argv[i])); + string_ncopy(opt_file, opt_file_argv[0], strlen(opt_file_argv[0])); return REQ_VIEW_BLAME; } else if (!strcmp(subcommand, "show")) { @@ -6961,16 +6843,16 @@ parse_options(int argc, const char *argv[]) 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, "--")) { + if (seen_dashdash) { + argv_append(&opt_file_argv, opt); + continue; + + } else if (!strcmp(opt, "--")) { seen_dashdash = TRUE; + continue; } else if (!strcmp(opt, "-v") || !strcmp(opt, "--version")) { printf("tig version %s\n", TIG_VERSION); @@ -6979,15 +6861,18 @@ parse_options(int argc, const char *argv[]) } else if (!strcmp(opt, "-h") || !strcmp(opt, "--help")) { printf("%s\n", usage); quit(0); + + } else if (!strcmp(opt, "--all")) { + argv_append(&opt_rev_argv, opt); + continue; } - custom_argv[j++] = opt; - if (j >= ARRAY_SIZE(custom_argv)) + if (!argv_append(&filter_argv, opt)) die("command too long"); } - if (!prepare_update(VIEW(request), custom_argv, NULL, FORMAT_NONE)) - die("Failed to format arguments"); + if (filter_argv) + filter_options(filter_argv); return request; } @@ -6995,17 +6880,15 @@ parse_options(int argc, const char *argv[]) int main(int argc, const char *argv[]) { + const char *codeset = "UTF-8"; enum request request = parse_options(argc, argv); struct view *view; - 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) @@ -7021,27 +6904,23 @@ main(int argc, const char *argv[]) if (!opt_git_dir[0] && request != REQ_VIEW_PAGER) die("Not a git repository"); - if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8")) - opt_utf8 = FALSE; + if (*opt_encoding && strcmp(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"); } if (load_refs() == ERR) die("Failed to load refs."); - foreach_view (view, i) - argv_from_env(view->ops->argv, 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 = get_input(0); @@ -7051,6 +6930,10 @@ main(int argc, const char *argv[]) /* 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(":"); @@ -7077,10 +6960,8 @@ main(int argc, const char *argv[]) if (!argv_from_string(argv, &argc, cmd)) { report("Too many arguments"); - } else if (!prepare_update(next, argv, NULL, FORMAT_DASH)) { - report("Failed to format command"); } else { - open_view(view, REQ_VIEW_PAGER, OPEN_PREPARED); + open_argv(view, next, argv, NULL, OPEN_DEFAULT); } }