X-Git-Url: https://git.distorted.org.uk/~mdw/tig/blobdiff_plain/49f2b43f1ad2b29dea90f8478120a0fafcb587e0..cfc6fa71c81e66c653fe83763d262c948b87731c:/tig.c diff --git a/tig.c b/tig.c index 90e9e5f..cbfa854 100644 --- a/tig.c +++ b/tig.c @@ -15,21 +15,22 @@ * tig [options] [--] [git log options] * tig [options] log [git log options] * tig [options] diff [git diff options] - * tig [options] < [git log or git diff output] + * tig [options] show [git show options] + * tig [options] < [git command output] * * DESCRIPTION * ----------- * Browse changes in a git repository. **/ -#ifndef DEBUG -#define NDEBUG -#endif - #ifndef VERSION #define VERSION "tig-0.1" #endif +#ifndef DEBUG +#define NDEBUG +#endif + #include #include #include @@ -45,7 +46,7 @@ static void die(const char *err, ...); static void report(const char *msg, ...); -static void set_nonblocking_input(int boolean); +static void set_nonblocking_input(bool loading); #define ABS(x) ((x) >= 0 ? (x) : -(x)) #define MIN(x, y) ((x) < (y) ? (x) : (y)) @@ -68,7 +69,7 @@ static void set_nonblocking_input(int boolean); #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3) -/* Some ascii-shorthands that fit into the ncurses namespace. */ +/* Some ascii-shorthands fitted into the ncurses namespace. */ #define KEY_TAB '\t' #define KEY_RETURN '\r' #define KEY_ESC 27 @@ -89,6 +90,7 @@ enum request { REQ_QUIT, REQ_PROMPT, REQ_SCREEN_REDRAW, + REQ_SCREEN_RESIZE, REQ_SCREEN_UPDATE, REQ_SHOW_VERSION, REQ_STOP_LOADING, @@ -184,7 +186,7 @@ static char opt_cmd[SIZEOF_CMD] = ""; static FILE *opt_pipe = NULL; /* Returns the index of log or diff command or -1 to exit. */ -static int +static bool parse_options(int argc, char *argv[]) { int i; @@ -193,22 +195,8 @@ parse_options(int argc, char *argv[]) char *opt = argv[i]; /** - * log [options]:: - * git log options. - * - * diff [options]:: - * git diff options. - **/ - if (!strcmp(opt, "log") || - !strcmp(opt, "diff")) { - opt_request = opt[0] == 'l' - ? REQ_VIEW_LOG : REQ_VIEW_DIFF; - break; - } - - /** * -l:: - * Start up in log view. + * Start up in log view using the internal log command. **/ if (!strcmp(opt, "-l")) { opt_request = REQ_VIEW_LOG; @@ -217,7 +205,7 @@ parse_options(int argc, char *argv[]) /** * -d:: - * Start up in diff view. + * Start up in diff view using the internal diff command. **/ if (!strcmp(opt, "-d")) { opt_request = REQ_VIEW_DIFF; @@ -230,7 +218,7 @@ parse_options(int argc, char *argv[]) * Optionally, with interval different than each line. **/ if (!strncmp(opt, "-n", 2) || - !strncmp(opt, "--line-number", 13)) { + !strncmp(opt, "--line-number", 13)) { char *num = opt; if (opt[1] == 'n') { @@ -252,29 +240,46 @@ parse_options(int argc, char *argv[]) * Show version and exit. **/ if (!strcmp(opt, "-v") || - !strcmp(opt, "--version")) { + !strcmp(opt, "--version")) { printf("tig version %s\n", VERSION); - return -1; + return FALSE; } /** * \--:: - * End of tig options. Useful when specifying commands + * End of tig(1) options. Useful when specifying commands * for the main view. Example: * - * $ tig -- --pretty=raw tag-1.0..HEAD + * $ tig -- --since=1.month **/ if (!strcmp(opt, "--")) { i++; break; } - /* Make stuff like: - * + /** + * log [options]:: + * Open log view using the given git log options. + * + * diff [options]:: + * Open diff view using the given git diff options. + * + * show [options]:: + * Open diff view using the given git show options. + **/ + if (!strcmp(opt, "log") || + !strcmp(opt, "diff") || + !strcmp(opt, "show")) { + opt_request = opt[0] == 'l' + ? REQ_VIEW_LOG : REQ_VIEW_DIFF; + break; + } + + /* Make stuff like: + * * $ tig tag-1.0..HEAD * - * work. - */ + * work. */ if (opt[0] && opt[0] != '-') break; @@ -282,17 +287,43 @@ parse_options(int argc, char *argv[]) } if (!isatty(STDIN_FILENO)) { - /* XXX: When pager mode has been requested, silently override - * view startup options. */ + /** + * Pager mode + * ~~~~~~~~~~ + * If stdin is a pipe, any log or diff options will be ignored and the + * pager view will be opened loading data from stdin. The pager mode + * can be used for colorizing output from various git commands. + * + * Example on how to colorize the output of git-show(1): + * + * $ git show | tig + **/ opt_request = REQ_VIEW_PAGER; opt_pipe = stdin; } else if (i < argc) { size_t buf_size; - /* XXX: This is vulnerable to the user overriding options - * required for the main view parser. */ + /** + * Git command options + * ~~~~~~~~~~~~~~~~~~~ + * All git command options specified on the command line will + * be passed to the given command and all will be shell quoted + * before used. + * + * NOTE: It is possible to specify options even for the main + * view. If doing this you should not touch the `--pretty` + * option. + * + * Example on how to open the log view and show both author and + * committer information: + * + * $ tig log --pretty=fuller + **/ + if (opt_request == REQ_VIEW_MAIN) + /* XXX: This is vulnerable to the user overriding + * options required for the main view parser. */ string_copy(opt_cmd, "git log --stat --pretty=raw"); else string_copy(opt_cmd, "git"); @@ -310,7 +341,7 @@ parse_options(int argc, char *argv[]) } - return i; + return TRUE; } @@ -336,6 +367,8 @@ struct keymap keymap[] = { * Switch to log view. * m:: * Switch to main view. + * p:: + * Switch to pager view. * h:: * Show man page. * Return:: @@ -347,6 +380,7 @@ struct keymap keymap[] = { { 'm', REQ_VIEW_MAIN }, { 'd', REQ_VIEW_DIFF }, { 'l', REQ_VIEW_LOG }, + { 'p', REQ_VIEW_PAGER }, { 'h', REQ_VIEW_HELP }, { KEY_TAB, REQ_VIEW_NEXT }, @@ -408,7 +442,11 @@ struct keymap keymap[] = { * n:: * Toggle line numbers on/off. * ':':: - * Open prompt. + * Open prompt. This allows you to specify what git command to run. + * Example: + * + * :log -p + * **/ { KEY_ESC, REQ_QUIT }, { 'q', REQ_QUIT }, @@ -420,6 +458,9 @@ struct keymap keymap[] = { /* wgetch() with nodelay() enabled returns ERR when there's no input. */ { ERR, REQ_SCREEN_UPDATE }, + + /* Use the ncurses SIGWINCH handler. */ + { KEY_RESIZE, REQ_SCREEN_RESIZE }, }; static enum request @@ -444,7 +485,7 @@ get_request(int key) * --------- --------------- ---------- ---------- ---------- */ \ /* Diff markup */ \ LINE(DIFF, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ -LINE(INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \ +LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \ LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \ LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \ LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \ @@ -456,14 +497,16 @@ LINE(DIFF_SIM, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ LINE(DIFF_DISSIM, "dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ /* Pretty print commit header */ \ LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \ +LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \ LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \ LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ -LINE(PP_COMMIT, "Commit: ", COLOR_GREEN, COLOR_DEFAULT, 0), \ +LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ +LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ /* Raw commit header */ \ LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \ LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \ LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \ -LINE(AUTHOR_IDENT, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \ +LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \ LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \ /* Misc */ \ LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \ @@ -545,19 +588,68 @@ init_colors(void) } +/** + * ENVIRONMENT VARIABLES + * --------------------- + * It is possible to alter which commands are used for the different views. + * If for example you prefer commits in the main to be sorted by date and + * only show 500 commits, use: + * + * $ TIG_MAIN_CMD="git log --date-order -n500 --pretty=raw %s" tig + * + * Or set the variable permanently in your environment. + * + * Notice, how `%s` is used to specify the commit reference. There can + * be a maximum of 5 `%s` ref specifications. + * + * TIG_DIFF_CMD:: + * The command used for the diff view. By default, git show is used + * as a backend. + * + * TIG_LOG_CMD:: + * The command used for the log view. + * + * TIG_MAIN_CMD:: + * The command used for the main view. Note, you must always specify + * the option: `--pretty=raw` since the main view parser expects to + * read that format. + **/ + +#define TIG_DIFF_CMD \ + "git show --patch-with-stat --find-copies-harder -B -C %s" + +#define TIG_LOG_CMD \ + "git log --cc --stat -n100 %s" + +#define TIG_MAIN_CMD \ + "git log --topo-order --stat --pretty=raw %s" + +/* We silently ignore that the following are also exported. */ + +#define TIG_HELP_CMD \ + "man tig 2> /dev/null" + +#define TIG_PAGER_CMD \ + "" + + /* * Viewer */ struct view { const char *name; /* View name */ - const char *cmdfmt; /* Default command line format */ + char *cmd_fmt; /* Default command line format */ + char *cmd_env; /* Command line set via environment */ char *id; /* Points to either of ref_{head,commit} */ size_t objsize; /* Size of objects in the line index */ struct view_ops { + /* Draw one line; @lineno must be < view->height. */ bool (*draw)(struct view *view, unsigned int lineno); + /* Read one line; updates view->line. */ bool (*read)(struct view *view, char *line); + /* Depending on view, change display based on current line. */ bool (*enter)(struct view *view); } *ops; @@ -565,9 +657,9 @@ struct view { char ref[SIZEOF_REF]; /* Hovered commit reference */ char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */ - WINDOW *win; - WINDOW *title; - int height, width; + 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 */ @@ -575,7 +667,8 @@ struct view { /* Buffering */ unsigned long lines; /* Total number of lines */ - void **line; /* Line index */ + void **line; /* Line index; each line contains user data */ + unsigned int digits; /* Number of digits in the lines member. */ /* Loading */ FILE *pipe; @@ -585,28 +678,21 @@ struct view { static struct view_ops pager_ops; static struct view_ops main_ops; -#define DIFF_CMD \ - "git log --stat -n1 %s ; echo; " \ - "git diff --find-copies-harder -B -C %s^ %s" - -#define LOG_CMD \ - "git log --cc --stat -n100 %s" - -#define MAIN_CMD \ - "git log --stat --pretty=raw %s" - -#define HELP_CMD \ - "man tig 2> /dev/null" - char ref_head[SIZEOF_REF] = "HEAD"; char ref_commit[SIZEOF_REF] = "HEAD"; +#define VIEW_STR(name, cmd, env, ref, objsize, ops) \ + { name, cmd, #env, ref, objsize, ops } + +#define VIEW_(id, name, ops, ref, objsize) \ + VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, objsize, ops) + static struct view views[] = { - { "main", MAIN_CMD, ref_head, sizeof(struct commit), &main_ops }, - { "diff", DIFF_CMD, ref_commit, sizeof(char), &pager_ops }, - { "log", LOG_CMD, ref_head, sizeof(char), &pager_ops }, - { "help", HELP_CMD, ref_head, sizeof(char), &pager_ops }, - { "pager", "cat", ref_head, sizeof(char), &pager_ops }, + VIEW_(MAIN, "main", &main_ops, ref_head, sizeof(struct commit)), + VIEW_(DIFF, "diff", &pager_ops, ref_commit, sizeof(char)), + VIEW_(LOG, "log", &pager_ops, ref_head, sizeof(char)), + VIEW_(HELP, "help", &pager_ops, ref_head, sizeof(char)), + VIEW_(PAGER, "pager", &pager_ops, "static", sizeof(char)), }; #define VIEW(req) (&views[(req) - REQ_OFFSET - 1]) @@ -784,7 +870,7 @@ scroll_view(struct view *view, enum request request) lines = view->lines - view->offset; if (lines == 0 || view->offset + view->height >= view->lines) { - report("Already on last line"); + report("Cannot scroll beyond the last line"); return; } break; @@ -796,7 +882,7 @@ scroll_view(struct view *view, enum request request) lines = view->offset; if (lines == 0) { - report("Already on first line"); + report("Cannot scroll beyond the first line"); return; } @@ -848,11 +934,11 @@ move_view(struct view *view, enum request request) } if (steps <= 0 && view->lineno == 0) { - report("Already on first line"); + report("Cannot move beyond the first line"); return; } else if (steps >= 0 && view->lineno + 1 >= view->lines) { - report("Already on last line"); + report("Cannot move beyond the last line"); return; } @@ -909,9 +995,14 @@ begin_update(struct view *view) if (opt_cmd[0]) { string_copy(view->cmd, opt_cmd); opt_cmd[0] = 0; + /* When running random commands, the view ref could have become + * invalid so clear it. */ + view->ref[0] = 0; } else { - if (snprintf(view->cmd, sizeof(view->cmd), view->cmdfmt, - id, id, id) >= sizeof(view->cmd)) + char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt; + + if (snprintf(view->cmd, sizeof(view->cmd), format, + id, id, id, id, id) >= sizeof(view->cmd)) return FALSE; } @@ -955,7 +1046,10 @@ end_update(struct view *view) if (!view->pipe) return; set_nonblocking_input(FALSE); - pclose(view->pipe); + if (view->pipe == stdin) + fclose(view->pipe); + else + pclose(view->pipe); view->pipe = NULL; } @@ -968,7 +1062,7 @@ update_view(struct view *view) /* The number of lines to read. If too low it will cause too much * redrawing (and possible flickering), if too high responsiveness * will suffer. */ - int lines = view->height; + unsigned long lines = view->height; int redraw_from = -1; if (!view->pipe) @@ -998,8 +1092,19 @@ update_view(struct view *view) break; } - /* CPU hogilicious! */ - update_view_title(view); + { + int digits; + + lines = view->lines; + for (digits = 0; lines; digits++) + lines /= 10; + + /* Keep the displayed view in sync with line number scaling. */ + if (digits != view->digits) { + view->digits = digits; + redraw_from = 0; + } + } if (redraw_from >= 0) { /* If this is an incremental update, redraw the previous line @@ -1012,6 +1117,10 @@ update_view(struct view *view) redraw_view_from(view, redraw_from); } + /* Update the title _after_ the redraw so that if the redraw picks up a + * commit reference in view->ref it'll be available here. */ + update_view_title(view); + if (ferror(view->pipe)) { report("Failed to read: %s", strerror(errno)); goto end; @@ -1060,7 +1169,7 @@ open_view(struct view *prev, enum request request, enum open_flags flags) foreach_view (displayed, nviews) { if (prev != view && view == displayed && - !strcmp(view->id, prev->id)) { + !strcmp(view->vid, prev->vid)) { current_view = nviews; /* Blur out the title of the previous view. */ update_view_title(prev); @@ -1074,7 +1183,7 @@ open_view(struct view *prev, enum request request, enum open_flags flags) return; } - if (strcmp(view->vid, view->id) && + if ((reload || strcmp(view->vid, view->id)) && !begin_update(view)) { report("Failed to load %s view", view->name); return; @@ -1093,9 +1202,9 @@ open_view(struct view *prev, enum request request, enum open_flags flags) resize_display(); - if (split && prev->lineno - prev->offset > prev->height) { + if (split && prev->lineno - prev->offset >= prev->height) { /* Take the title line into account. */ - int lines = prev->lineno - prev->height + 1; + 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. */ @@ -1186,6 +1295,7 @@ view_driver(struct view *view, enum request request) break; case REQ_PROMPT: + /* Always reload^Wrerun commands from the prompt. */ open_view(view, opt_request, OPEN_RELOAD); break; @@ -1201,8 +1311,14 @@ view_driver(struct view *view, enum request request) report("Version: %s", VERSION); return TRUE; + case REQ_SCREEN_RESIZE: + resize_display(); + /* Fall-through */ case REQ_SCREEN_REDRAW: - redraw_view(view); + foreach_view (view, i) { + redraw_view(view); + update_view_title(view); + } break; case REQ_SCREEN_UPDATE: @@ -1241,17 +1357,11 @@ pager_draw(struct view *view, unsigned int lineno) type = get_line_type(line); if (view->offset + lineno == view->lineno) { - switch (type) { - case LINE_COMMIT: + if (type == LINE_COMMIT) { string_copy(view->ref, line + 7); string_copy(ref_commit, view->ref); - break; - case LINE_PP_COMMIT: - string_copy(view->ref, line + 8); - string_copy(ref_commit, view->ref); - default: - break; } + type = LINE_CURSOR; } @@ -1262,13 +1372,17 @@ pager_draw(struct view *view, unsigned int lineno) linelen = MIN(linelen, view->width); if (opt_line_number) { - unsigned int real_lineno = view->offset + lineno + 1; + static char indent[] = " "; + unsigned long real_lineno = view->offset + lineno + 1; int col = 0; if (real_lineno == 1 || (real_lineno % opt_num_interval) == 0) - mvwprintw(view->win, lineno, 0, "%4d: ", real_lineno); - else - mvwaddstr(view->win, lineno, 0, " : "); + mvwprintw(view->win, lineno, 0, "%.*d", view->digits, real_lineno); + + else if (view->digits < sizeof(indent)) + mvwaddnstr(view->win, lineno, 0, indent, view->digits); + + waddstr(view->win, ": "); while (line) { if (*line == '\t') { @@ -1331,13 +1445,13 @@ pager_enter(struct view *view) return TRUE; } - static struct view_ops pager_ops = { pager_draw, pager_read, pager_enter, }; + static bool main_draw(struct view *view, unsigned int lineno) { @@ -1410,7 +1524,7 @@ main_read(struct view *view, char *line) string_copy(commit->id, line); break; - case LINE_AUTHOR_IDENT: + case LINE_AUTHOR: { char *ident = line + STRING_SIZE("author "); char *end = strchr(ident, '<'); @@ -1466,8 +1580,10 @@ main_read(struct view *view, char *line) /* Require titles to start with a non-space character at the * offset used by git log. */ + /* FIXME: More gracefull handling of titles; append "..." to + * shortened titles, etc. */ if (strncmp(line, " ", 4) || - isspace(line[5])) + isspace(line[4])) break; string_copy(commit->title, line + 4); @@ -1493,6 +1609,9 @@ static struct view_ops main_ops = { * Status management */ +/* Whether or not the curses interface has been initialized. */ +bool cursed = FALSE; + /* The status window is used for polling keystrokes. */ static WINDOW *status_win; @@ -1502,15 +1621,16 @@ report(const char *msg, ...) { va_list args; - va_start(args, msg); - /* Update the title window first, so the cursor ends up in the status * window. */ update_view_title(display[current_view]); + va_start(args, msg); + werase(status_win); wmove(status_win, 0, 0); - vwprintw(status_win, msg, args); + if (*msg) + vwprintw(status_win, msg, args); wrefresh(status_win); va_end(args); @@ -1518,19 +1638,14 @@ report(const char *msg, ...) /* Controls when nodelay should be in effect when polling user input. */ static void -set_nonblocking_input(int loading) +set_nonblocking_input(bool loading) { /* The number of loading views. */ static unsigned int nloading; - if (loading == TRUE) { - if (nloading++ == 0) - nodelay(status_win, TRUE); - return; - } - - if (nloading-- == 1) - nodelay(status_win, FALSE); + if ((loading == FALSE && nloading-- == 1) || + (loading == TRUE && nloading++ == 0)) + nodelay(status_win, loading); } static void @@ -1540,14 +1655,17 @@ init_display(void) /* Initialize the curses library */ if (isatty(STDIN_FILENO)) { - initscr(); + cursed = !!initscr(); } else { /* Leave stdin and stdout alone when acting as a pager. */ FILE *io = fopen("/dev/tty", "r+"); - newterm(NULL, io, io); + cursed = !!newterm(NULL, io, io); } + if (!cursed) + die("Failed to initialize curses"); + nonl(); /* Tell curses not to do NL->CR/NL on output */ cbreak(); /* Take input chars one at a time, no wait for \n */ noecho(); /* Don't echo input */ @@ -1573,12 +1691,9 @@ init_display(void) static void quit(int sig) { - if (status_win) - delwin(status_win); - endwin(); - - /* FIXME: Shutdown gracefully. */ - + /* XXX: Restore tty modes and let the OS cleanup the rest! */ + if (cursed) + endwin(); exit(0); } @@ -1600,21 +1715,23 @@ static void die(const char *err, ...) int main(int argc, char *argv[]) { + struct view *view; enum request request; - int git_arg; + size_t i; signal(SIGINT, quit); - git_arg = parse_options(argc, argv); - if (git_arg < 0) + if (!parse_options(argc, argv)) return 0; + for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++) + view->cmd_env = getenv(view->cmd_env); + request = opt_request; init_display(); while (view_driver(display[current_view], request)) { - struct view *view; int key; int i; @@ -1625,7 +1742,10 @@ main(int argc, char *argv[]) key = wgetch(status_win); request = get_request(key); - if (request == REQ_PROMPT) { + /* Some low-level request handling. This keeps handling of + * status_win restricted. */ + switch (request) { + case REQ_PROMPT: report(":"); /* Temporarily switch to line-oriented and echoed * input. */ @@ -1641,6 +1761,23 @@ main(int argc, char *argv[]) noecho(); cbreak(); + break; + + case REQ_SCREEN_RESIZE: + { + int height, width; + + getmaxyx(stdscr, height, width); + + /* Resize the status view and let the view driver take + * care of resizing the displayed views. */ + wresize(status_win, 1, width); + mvwin(status_win, height - 1, 0); + wrefresh(status_win); + break; + } + default: + break; } } @@ -1654,15 +1791,7 @@ main(int argc, char *argv[]) * ---- * Features that should be explored. * - * - Dynamic scaling of line number indentation. - * - * - Internal command line (exmode-inspired) which allows to specify what git - * log or git diff command to run. Example: - * - * :log -p - * - * - Terminal resizing support. I am yet to figure out whether catching - * SIGWINCH is preferred over using ncurses' built-in support for resizing. + * - Searching. * * - Locale support. *