X-Git-Url: https://git.distorted.org.uk/~mdw/tig/blobdiff_plain/6908bdbdb5996f30f7dcb5e2dd71f8c8bbbab8a3..c34d9c9f0e09a3ec6e990294a7baceaf4d940259:/tig.c diff --git a/tig.c b/tig.c index 882b2fd..20abf80 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, @@ -108,11 +110,19 @@ enum request { REQ_SCROLL_PAGE_DOWN, }; +struct ref { + char *name; + char id[41]; + unsigned int tag:1; + unsigned int next:1; +}; + struct commit { char id[41]; /* SHA1 ID. */ char title[75]; /* The first line of the commit message. */ char author[75]; /* The author of the commit. */ struct tm time; /* Date from the author ident. */ + struct ref **refs; /* Repository references; tags & branch heads. */ }; /* @@ -183,11 +193,8 @@ static enum request opt_request = REQ_VIEW_MAIN; static char opt_cmd[SIZEOF_CMD] = ""; static FILE *opt_pipe = NULL; -char ref_head[SIZEOF_REF] = "HEAD"; -char ref_commit[SIZEOF_REF] = "HEAD"; - /* Returns the index of log or diff command or -1 to exit. */ -static int +static bool parse_options(int argc, char *argv[]) { int i; @@ -196,22 +203,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; @@ -220,7 +213,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; @@ -233,7 +226,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') { @@ -255,29 +248,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; @@ -285,17 +295,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"); @@ -313,7 +349,7 @@ parse_options(int argc, char *argv[]) } - return i; + return TRUE; } @@ -339,6 +375,8 @@ struct keymap keymap[] = { * Switch to log view. * m:: * Switch to main view. + * p:: + * Switch to pager view. * h:: * Show man page. * Return:: @@ -350,6 +388,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 }, @@ -411,7 +450,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 }, @@ -423,6 +466,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 @@ -447,7 +493,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), \ @@ -459,14 +505,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), \ @@ -480,7 +528,9 @@ LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \ LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \ LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \ LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \ -LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), +LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \ +LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \ +LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), enum line_type { #define LINE(type, line, fg, bg, attr) \ @@ -548,30 +598,105 @@ init_colors(void) } +/** + * ENVIRONMENT VARIABLES + * --------------------- + * Several options related to the interface with git can be configured + * via environment options. + * + * Repository references + * ~~~~~~~~~~~~~~~~~~~~~ + * Commits that are referenced by tags and branch heads will be marked + * by the reference name surrounded by '[' and ']': + * + * 2006-04-18 23:12 Jonas Fonseca | [tig-0.2] tig version 0.2 + * + * If you want to filter out certain directories under `.git/refs/`, say + * `tmp` you can do it by setting the following variable: + * + * $ TIG_LS_REMOTE="git ls-remote | sed '/\/tmp\//d'" + * + * Or set the variable permanently in your environment. + * + * TIG_LS_REMOTE:: + * Set command for retrieving all repository references.private + **/ + +#define TIG_LS_REMOTE \ + "git ls-remote ." + +/** + * View commands + * ~~~~~~~~~~~~~ + * 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 *defcmd; /* Default command line */ + 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; char cmd[SIZEOF_CMD]; /* Command buffer */ - char ref[SIZEOF_REF]; /* Hovered Commit reference */ - /* The view reference that describes the content of this view. */ - char vref[SIZEOF_REF]; + 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 */ @@ -579,7 +704,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; @@ -589,25 +715,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" +char ref_head[SIZEOF_REF] = "HEAD"; +char ref_commit[SIZEOF_REF] = "HEAD"; -#define MAIN_CMD \ - "git log --stat --pretty=raw %s" +#define VIEW_STR(name, cmd, env, ref, objsize, ops) \ + { name, cmd, #env, ref, objsize, ops } -#define HELP_CMD \ - "man tig 2> /dev/null" +#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]) @@ -785,7 +907,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; @@ -797,7 +919,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; } @@ -849,11 +971,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; } @@ -910,9 +1032,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->defcmd, - 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; } @@ -932,6 +1059,7 @@ begin_update(struct view *view) view->offset = 0; view->lines = 0; view->lineno = 0; + string_copy(view->vid, id); if (view->line) { int i; @@ -955,7 +1083,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 +1099,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) @@ -985,9 +1116,8 @@ update_view(struct view *view) view->line = tmp; while ((line = fgets(buffer, sizeof(buffer), view->pipe))) { - int linelen; + int linelen = strlen(line); - linelen = strlen(line); if (linelen) line[linelen - 1] = 0; @@ -998,8 +1128,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 +1153,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; @@ -1039,10 +1184,19 @@ end: return FALSE; } +enum open_flags { + OPEN_DEFAULT = 0, /* Use default view switching. */ + OPEN_SPLIT = 1, /* Split current view. */ + OPEN_BACKGROUNDED = 2, /* Backgrounded. */ + OPEN_RELOAD = 4, /* Reload view even if it is the current. */ +}; + static void -switch_view(struct view *prev, enum request request, - bool backgrounded, bool split) +open_view(struct view *prev, enum request request, enum open_flags flags) { + bool backgrounded = !!(flags & OPEN_BACKGROUNDED); + bool split = !!(flags & OPEN_SPLIT); + bool reload = !!(flags & OPEN_RELOAD); struct view *view = VIEW(request); struct view *displayed; int nviews; @@ -1051,7 +1205,7 @@ switch_view(struct view *prev, enum request request, 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); @@ -1060,12 +1214,12 @@ switch_view(struct view *prev, enum request request, } } - if (view == prev && nviews == 1) { + if (view == prev && nviews == 1 && !reload) { report("Already in %s view", view->name); return; } - if (strcmp(view->vref, view->id) && + if ((reload || strcmp(view->vid, view->id)) && !begin_update(view)) { report("Failed to load %s view", view->name); return; @@ -1084,9 +1238,9 @@ switch_view(struct view *prev, enum request request, 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. */ @@ -1145,7 +1299,7 @@ view_driver(struct view *view, enum request request) case REQ_VIEW_LOG: case REQ_VIEW_HELP: case REQ_VIEW_PAGER: - switch_view(view, request, FALSE, FALSE); + open_view(view, request, OPEN_DEFAULT); break; case REQ_ENTER: @@ -1177,7 +1331,8 @@ view_driver(struct view *view, enum request request) break; case REQ_PROMPT: - switch_view(view, opt_request, FALSE, FALSE); + /* Always reload^Wrerun commands from the prompt. */ + open_view(view, opt_request, OPEN_RELOAD); break; case REQ_STOP_LOADING: @@ -1192,8 +1347,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: @@ -1232,17 +1393,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; } @@ -1253,13 +1408,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') { @@ -1316,19 +1475,21 @@ pager_enter(struct view *view) char *line = view->line[view->lineno]; if (get_line_type(line) == LINE_COMMIT) { - switch_view(view, REQ_VIEW_DIFF, FALSE, FALSE); + open_view(view, REQ_VIEW_DIFF, OPEN_DEFAULT); } return TRUE; } - static struct view_ops pager_ops = { pager_draw, pager_read, pager_enter, }; + +static struct ref **get_refs(char *id); + static bool main_draw(struct view *view, unsigned int lineno) { @@ -1347,6 +1508,7 @@ main_draw(struct view *view, unsigned int lineno) if (view->offset + lineno == view->lineno) { string_copy(view->ref, commit->id); + string_copy(ref_commit, view->ref); type = LINE_CURSOR; } else { type = LINE_MAIN_COMMIT; @@ -1374,8 +1536,26 @@ main_draw(struct view *view, unsigned int lineno) cols += 20; wattrset(view->win, A_NORMAL); mvwaddch(view->win, lineno, cols, ACS_LTEE); + wmove(view->win, lineno, cols + 2); + + if (commit->refs) { + size_t i = 0; + + do { + if (commit->refs[i]->tag) + wattrset(view->win, get_line_attr(LINE_MAIN_TAG)); + else + wattrset(view->win, get_line_attr(LINE_MAIN_REF)); + waddstr(view->win, "["); + waddstr(view->win, commit->refs[i]->name); + waddstr(view->win, "]"); + wattrset(view->win, A_NORMAL); + waddstr(view->win, " "); + } while (commit->refs[i++]->next); + } + wattrset(view->win, get_line_attr(type)); - mvwaddstr(view->win, lineno, cols + 2, commit->title); + waddstr(view->win, commit->title); wattrset(view->win, A_NORMAL); return TRUE; @@ -1398,9 +1578,10 @@ main_read(struct view *view, char *line) view->line[view->lines++] = commit; string_copy(commit->id, line); + commit->refs = get_refs(commit->id); break; - case LINE_AUTHOR_IDENT: + case LINE_AUTHOR: { char *ident = line + STRING_SIZE("author "); char *end = strchr(ident, '<'); @@ -1456,8 +1637,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); @@ -1469,7 +1652,7 @@ main_read(struct view *view, char *line) static bool main_enter(struct view *view) { - switch_view(view, REQ_VIEW_DIFF, TRUE, TRUE); + open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT | OPEN_BACKGROUNDED); return TRUE; } @@ -1479,10 +1662,14 @@ static struct view_ops main_ops = { main_enter, }; + /* * 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; @@ -1492,15 +1679,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); @@ -1508,19 +1696,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 @@ -1530,14 +1713,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 */ @@ -1556,6 +1742,98 @@ init_display(void) wbkgdset(status_win, get_line_attr(LINE_STATUS)); } + +/* + * Repository references + */ + +static struct ref *refs; +size_t refs_size; + +static struct ref ** +get_refs(char *id) +{ + struct ref **id_refs = NULL; + size_t id_refs_size = 0; + size_t i; + + for (i = 0; i < refs_size; i++) { + struct ref **tmp; + + if (strcmp(id, refs[i].id)) + continue; + + tmp = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs)); + if (!tmp) { + if (id_refs) + free(id_refs); + return NULL; + } + + id_refs = tmp; + id_refs[id_refs_size++] = &refs[i]; + if (id_refs_size > 1) + id_refs[id_refs_size - 1]->next = 1; + } + + return id_refs; +} + +static int +load_refs(void) +{ + char *cmd_env = getenv("TIG_LS_REMOTE"); + char *cmd = cmd_env ? cmd_env : TIG_LS_REMOTE; + FILE *pipe = popen(cmd, "r"); + char buffer[BUFSIZ]; + char *line; + + if (!pipe) + return ERR; + + while ((line = fgets(buffer, sizeof(buffer), pipe))) { + char *name = strchr(line, '\t'); + struct ref *ref; + int namelen; + bool tag = FALSE; + + if (!name) + continue; + + *name++ = 0; + namelen = strlen(name) - 1; + if (name[namelen - 1] == '}') { + while (namelen > 0 && name[namelen] != '^') + namelen--; + } + name[namelen] = 0; + + if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) { + name += STRING_SIZE("refs/tags/"); + tag = TRUE; + } + + refs = realloc(refs, sizeof(*refs) * (refs_size + 1)); + if (!refs) + return ERR; + + ref = &refs[refs_size++]; + ref->tag = tag; + ref->name = strdup(name); + if (!ref->name) + return ERR; + + string_copy(ref->id, line); + } + + if (ferror(pipe)) + return ERR; + + pclose(pipe); + + return OK; +} + /* * Main */ @@ -1563,12 +1841,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); } @@ -1590,21 +1865,26 @@ 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; + if (load_refs() == ERR) + die("Failed to load refs."); + + 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; @@ -1615,16 +1895,42 @@ 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. */ nocbreak(); echo(); - if (wgetnstr(status_win, opt_cmd, sizeof(opt_cmd)) == OK) - die("%s", opt_cmd); + + if (wgetnstr(status_win, opt_cmd + 4, sizeof(opt_cmd) - 4) == OK) { + memcpy(opt_cmd, "git ", 4); + opt_request = REQ_VIEW_PAGER; + } else { + request = ERR; + } + 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; } } @@ -1638,15 +1944,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. *