X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~mdw/git/tig/blobdiff_plain/cfc6fa71c81e66c653fe83763d262c948b87731c..8c11094f0b8ae9b82b1a3c44e6588e496214866b:/tig.c diff --git a/tig.c b/tig.c index cbfa854..db862bf 100644 --- a/tig.c +++ b/tig.c @@ -20,11 +20,19 @@ * * DESCRIPTION * ----------- - * Browse changes in a git repository. + * Browse changes in a git repository. Additionally, tig(1) can also act + * as a pager for output of various git commands. + * + * When browsing repositories, tig(1) uses the underlying git commands + * to present the user with various views, such as summarized commit log + * and showing the commit with the log message, diffstat, and the diff. + * + * Using tig(1) as a pager, it will display input from stdin and try + * to colorize it. **/ #ifndef VERSION -#define VERSION "tig-0.1" +#define VERSION "tig-0.3" #endif #ifndef DEBUG @@ -60,6 +68,8 @@ static void set_nonblocking_input(bool loading); /* This color name can be used to refer to the default term colors. */ #define COLOR_DEFAULT (-1) +#define TIG_HELP "(d)iff, (l)og, (m)ain, (q)uit, (h)elp, (Enter) show diff" + /* 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 ") @@ -67,6 +77,8 @@ static void set_nonblocking_input(bool loading); /* The default interval between line numbers. */ #define NUMBER_INTERVAL 1 +#define TABSIZE 8 + #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3) /* Some ascii-shorthands fitted into the ncurses namespace. */ @@ -74,6 +86,7 @@ static void set_nonblocking_input(bool loading); #define KEY_RETURN '\r' #define KEY_ESC 27 + /* User action requests. */ enum request { /* Offset all requests to avoid conflicts with ncurses getch values. */ @@ -98,7 +111,9 @@ enum request { REQ_VIEW_NEXT, REQ_MOVE_UP, + REQ_MOVE_UP_ENTER, REQ_MOVE_DOWN, + REQ_MOVE_DOWN_ENTER, REQ_MOVE_PAGE_UP, REQ_MOVE_PAGE_DOWN, REQ_MOVE_FIRST_LINE, @@ -110,13 +125,22 @@ enum request { REQ_SCROLL_PAGE_DOWN, }; +struct ref { + char *name; /* Ref name; tag or head names are shortened. */ + char id[41]; /* Commit SHA1 ID */ + unsigned int tag:1; /* Is it a tag? */ + unsigned int next:1; /* For ref lists: are there more refs? */ +}; + 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. */ }; + /* * String helpers */ @@ -133,6 +157,7 @@ string_ncopy(char *dst, char *src, int dstlen) #define string_copy(dst, src) \ string_ncopy(dst, src, sizeof(dst)) + /* Shell quoting * * NOTE: The following is a slightly modified copy of the git project's shell @@ -179,8 +204,10 @@ sq_quote(char buf[SIZEOF_CMD], size_t bufsize, const char *src) * ------- **/ -static int opt_line_number = FALSE; +/* Option and state variables. */ +static bool opt_line_number = FALSE; static int opt_num_interval = NUMBER_INTERVAL; +static int opt_tab_size = TABSIZE; static enum request opt_request = REQ_VIEW_MAIN; static char opt_cmd[SIZEOF_CMD] = ""; static FILE *opt_pipe = NULL; @@ -235,6 +262,26 @@ parse_options(int argc, char *argv[]) continue; } + /** + * -t[NSPACES], --tab-size[=NSPACES]:: + * Set the number of spaces tabs should be expanded to. + **/ + if (!strncmp(opt, "-t", 2) || + !strncmp(opt, "--tab-size", 10)) { + char *num = opt; + + if (opt[1] == 't') { + num = opt + 2; + + } else if (opt[STRING_SIZE("--tab-size")] == '=') { + num = opt + STRING_SIZE("--tab-size="); + } + + if (isdigit(*num)) + opt_tab_size = MIN(atoi(num), TABSIZE); + continue; + } + /** * -v, --version:: * Show version and exit. @@ -247,8 +294,8 @@ parse_options(int argc, char *argv[]) /** * \--:: - * End of tig(1) options. Useful when specifying commands - * for the main view. Example: + * End of tig(1) options. Useful when specifying command + * options for the main view. Example: * * $ tig -- --since=1.month **/ @@ -258,13 +305,13 @@ parse_options(int argc, char *argv[]) } /** - * log [options]:: + * log [git log options]:: * Open log view using the given git log options. * - * diff [options]:: + * diff [git diff options]:: * Open diff view using the given git diff options. * - * show [options]:: + * show [git show options]:: * Open diff view using the given git show options. **/ if (!strcmp(opt, "log") || @@ -275,11 +322,16 @@ parse_options(int argc, char *argv[]) break; } - /* Make stuff like: + /** + * [git log options]:: + * tig(1) will stop the option parsing when the first + * command line parameter not starting with "-" is + * encountered. All options including this one will be + * passed to git log when loading the main view. + * This makes it possible to say: * * $ tig tag-1.0..HEAD - * - * work. */ + **/ if (opt[0] && opt[0] != '-') break; @@ -309,16 +361,21 @@ parse_options(int argc, char *argv[]) * ~~~~~~~~~~~~~~~~~~~ * All git command options specified on the command line will * be passed to the given command and all will be shell quoted - * before used. + * before they are passed to the shell. * - * NOTE: It is possible to specify options even for the main - * view. If doing this you should not touch the `--pretty` - * option. + * NOTE: If you specify options for the main view, you should + * not use the `--pretty` option as this option will be set + * automatically to the format expected by the main view. * * Example on how to open the log view and show both author and * committer information: * * $ tig log --pretty=fuller + * + * See the <> section below + * for an introduction to revision options supported by the git + * commands. For details on specific git command options, refer + * to the man page of the command in question. **/ if (opt_request == REQ_VIEW_MAIN) @@ -345,137 +402,6 @@ parse_options(int argc, char *argv[]) } -/** - * KEYS - * ---- - **/ - -#define HELP "(d)iff, (l)og, (m)ain, (q)uit, (v)ersion, (h)elp" - -struct keymap { - int alias; - int request; -}; - -struct keymap keymap[] = { - /** - * View switching - * ~~~~~~~~~~~~~~ - * d:: - * Switch to diff view. - * l:: - * Switch to log view. - * m:: - * Switch to main view. - * p:: - * Switch to pager view. - * h:: - * Show man page. - * Return:: - * If in main view split the view - * and show the diff in the bottom view. - * Tab:: - * Switch to next view. - **/ - { '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 }, - { KEY_RETURN, REQ_ENTER }, - - /** - * Cursor navigation - * ~~~~~~~~~~~~~~~~~ - * Up, k:: - * Move curser one line up. - * Down, j:: - * Move cursor one line down. - * Page Up:: - * Move curser one page up. - * Page Down:: - * Move cursor one page down. - * Home:: - * Jump to first line. - * End:: - * Jump to last line. - **/ - { KEY_UP, REQ_MOVE_UP }, - { 'k', REQ_MOVE_UP }, - { KEY_DOWN, REQ_MOVE_DOWN }, - { 'j', REQ_MOVE_DOWN }, - { KEY_HOME, REQ_MOVE_FIRST_LINE }, - { KEY_END, REQ_MOVE_LAST_LINE }, - { KEY_NPAGE, REQ_MOVE_PAGE_DOWN }, - { KEY_PPAGE, REQ_MOVE_PAGE_UP }, - - /** - * Scrolling - * ~~~~~~~~~ - * Insert:: - * Scroll view one line up. - * Delete:: - * Scroll view one line down. - * w:: - * Scroll view one page up. - * s:: - * Scroll view one page down. - **/ - { KEY_IC, REQ_SCROLL_LINE_UP }, - { KEY_DC, REQ_SCROLL_LINE_DOWN }, - { 'w', REQ_SCROLL_PAGE_UP }, - { 's', REQ_SCROLL_PAGE_DOWN }, - - /** - * Misc - * ~~~~ - * q, Escape:: - * Quit - * r:: - * Redraw screen. - * z:: - * Stop all background loading. - * v:: - * Show version. - * n:: - * Toggle line numbers on/off. - * ':':: - * Open prompt. This allows you to specify what git command to run. - * Example: - * - * :log -p - * - **/ - { KEY_ESC, REQ_QUIT }, - { 'q', REQ_QUIT }, - { 'z', REQ_STOP_LOADING }, - { 'v', REQ_SHOW_VERSION }, - { 'r', REQ_SCREEN_REDRAW }, - { 'n', REQ_TOGGLE_LINE_NUMBERS }, - { ':', REQ_PROMPT }, - - /* 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 -get_request(int key) -{ - int i; - - for (i = 0; i < ARRAY_SIZE(keymap); i++) - if (keymap[i].alias == key) - return keymap[i].request; - - return (enum request) key; -} - - /* * Line-oriented content detection. */ @@ -489,8 +415,8 @@ 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), \ -LINE(DIFF_OLDMODE, "old mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ -LINE(DIFF_NEWMODE, "new mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ +LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ +LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ LINE(DIFF_COPY, "copy ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ LINE(DIFF_RENAME, "rename ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ LINE(DIFF_SIM, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \ @@ -520,7 +446,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) \ @@ -591,9 +519,38 @@ 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-03-26 19:42 Petr Baudis | [cogito-0.17.1] Cogito 0.17.1 + * + * 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" tig + * + * Or set the variable permanently in your environment. + * + * TIG_LS_REMOTE:: + * Set command for retrieving all repository references. The command + * should output data in the same format as git-ls-remote(1). + **/ + +#define TIG_LS_REMOTE \ + "git ls-remote . 2>/dev/null" + +/** + * [[view-commands]] + * 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: + * If for example you prefer commits in the main view to be sorted by date + * and only show 500 commits, use: * * $ TIG_MAIN_CMD="git log --date-order -n500 --pretty=raw %s" tig * @@ -607,7 +564,9 @@ init_colors(void) * as a backend. * * TIG_LOG_CMD:: - * The command used for the log view. + * The command used for the log view. If you prefer to have both + * author and committer shown in the log view be sure to pass + * `--pretty=fuller` to git log. * * TIG_MAIN_CMD:: * The command used for the main view. Note, you must always specify @@ -624,18 +583,44 @@ init_colors(void) #define TIG_MAIN_CMD \ "git log --topo-order --stat --pretty=raw %s" -/* We silently ignore that the following are also exported. */ +/* ... silently ignore that the following are also exported. */ #define TIG_HELP_CMD \ - "man tig 2> /dev/null" + "man tig 2>/dev/null" #define TIG_PAGER_CMD \ "" - -/* - * Viewer - */ +/** + * The viewer + * ---------- + * + * tig(1) presents various 'views' of a repository. Each view is based on output + * from an external command, most often 'git log', 'git diff', or 'git show'. + * + * The main view:: + * Is the default view, and it shows a one line summary of each commit + * in the chosen list of revision. The summary includes commit date, + * author, and the first line of the log message. Additionally, any + * repository references, such as tags, will be shown. + * + * The log view:: + * Presents a more rich view of the revision log showing the whole log + * message and the diffstat. + * + * The diff view:: + * Shows either the diff of the current working tree, that is, what + * has changed since the last commit, or the commit diff complete + * with log message, diffstat and diff. + * + * The pager view:: + * Is used for displaying both input from stdin and output from git + * commands entered in the internal prompt. + * + * The help view:: + * Displays the information from the tig(1) man page. For the help view + * to work you need to have the tig(1) man page installed. + **/ struct view { const char *name; /* View name */ @@ -645,6 +630,9 @@ struct view { size_t objsize; /* Size of objects in the line index */ struct view_ops { + /* What type of content being displayed. Used in the + * title bar. */ + char *type; /* Draw one line; @lineno must be < view->height. */ bool (*draw)(struct view *view, unsigned int lineno); /* Read one line; updates view->line. */ @@ -678,8 +666,8 @@ struct view { static struct view_ops pager_ops; static struct view_ops main_ops; -char ref_head[SIZEOF_REF] = "HEAD"; -char ref_commit[SIZEOF_REF] = "HEAD"; +static char ref_head[SIZEOF_REF] = "HEAD"; +static char ref_commit[SIZEOF_REF] = "HEAD"; #define VIEW_STR(name, cmd, env, ref, objsize, ops) \ { name, cmd, #env, ref, objsize, ops } @@ -756,8 +744,11 @@ resize_display(void) offset = 0; foreach_view (view, i) { + /* Keep the size of the all view windows one lager than is + * required. This makes current line management easier when the + * cursor will go outside the window. */ if (!view->win) { - view->win = newwin(view->height, 0, offset, 0); + view->win = newwin(view->height + 1, 0, offset, 0); if (!view->win) die("Failed to create %s view", view->name); @@ -768,7 +759,7 @@ resize_display(void) die("Failed to create title window"); } else { - wresize(view->win, view->height, view->width); + wresize(view->win, view->height + 1, view->width); mvwin(view->win, offset, 0); mvwin(view->title, offset + view->height, 0); wrefresh(view->win); @@ -792,15 +783,13 @@ update_view_title(struct view *view) /* [main] ref: 334b506... - commit 6 of 4383 (0%) */ if (*view->ref) - wprintw(view->title, "[%s] ref: %s", view->name, view->ref); + wprintw(view->title, "[%s] %s", view->name, view->ref); else wprintw(view->title, "[%s]", view->name); if (view->lines) { - char *type = view == VIEW(REQ_VIEW_MAIN) ? "commit" : "line"; - wprintw(view->title, " - %s %d of %d (%d%%)", - type, + view->ops->type, view->lineno + 1, view->lines, (view->lineno + 1) * 100 / view->lines); @@ -845,6 +834,12 @@ do_scroll_view(struct view *view, int lines) view->ops->draw(view, 0); } else if (view->lineno >= view->offset + view->height) { + if (view->lineno == view->offset + view->height) { + /* Clear the hidden line so it doesn't show if the view + * is scrolled up. */ + wmove(view->win, view->height, 0); + wclrtoeol(view->win); + } view->lineno = view->offset + view->height - 1; view->ops->draw(view, view->lineno - view->offset); } @@ -922,10 +917,12 @@ move_view(struct view *view, enum request request) break; case REQ_MOVE_UP: + case REQ_MOVE_UP_ENTER: steps = -1; break; case REQ_MOVE_DOWN: + case REQ_MOVE_DOWN_ENTER: steps = 1; break; @@ -1079,9 +1076,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; @@ -1129,7 +1125,16 @@ update_view(struct view *view) time_t secs = time(NULL) - view->start_time; if (view == VIEW(REQ_VIEW_HELP)) { - report("%s", HELP); + char *msg = TIG_HELP; + + if (view->lines == 0) { + /* Slightly ugly, but abusing view->ref keeps + * the error message. */ + string_copy(view->ref, "No help available"); + msg = "The tig(1) manpage is not installed"; + } + + report("%s", msg); goto end; } @@ -1173,7 +1178,7 @@ open_view(struct view *prev, enum request request, enum open_flags flags) current_view = nviews; /* Blur out the title of the previous view. */ update_view_title(prev); - report("Switching to %s view", view->name); + report(""); return; } } @@ -1213,7 +1218,8 @@ open_view(struct view *prev, enum request request, enum open_flags flags) if (prev && view != prev) { /* "Blur" the previous view. */ - update_view_title(prev); + if (!backgrounded) + update_view_title(prev); /* Continue loading split views in the background. */ if (!split) @@ -1227,8 +1233,16 @@ open_view(struct view *prev, enum request request, enum open_flags flags) report("Loading..."); } else { redraw_view(view); - report(""); + if (view == VIEW(REQ_VIEW_HELP)) + report("%s", TIG_HELP); + else + report(""); } + + /* If the view is backgrounded the above calls to report() + * won't redraw the view title. */ + if (backgrounded) + update_view_title(view); } @@ -1266,6 +1280,11 @@ view_driver(struct view *view, enum request request) open_view(view, request, OPEN_DEFAULT); break; + case REQ_MOVE_UP_ENTER: + case REQ_MOVE_DOWN_ENTER: + move_view(view, request); + /* Fall-through */ + case REQ_ENTER: if (!view->lines) { report("Nothing to enter"); @@ -1286,12 +1305,13 @@ view_driver(struct view *view, enum request request) current_view = next_view; /* Blur out the title of the previous view. */ update_view_title(view); - report("Switching to %s view", display[current_view]->name); + report(""); break; } case REQ_TOGGLE_LINE_NUMBERS: opt_line_number = !opt_line_number; redraw_view(view); + update_view_title(view); break; case REQ_PROMPT: @@ -1302,7 +1322,7 @@ view_driver(struct view *view, enum request request) case REQ_STOP_LOADING: foreach_view (view, i) { if (view->pipe) - report("Stopped loaded of %s view", view->name), + report("Stopped loaded the %s view", view->name), end_update(view); } break; @@ -1330,7 +1350,7 @@ view_driver(struct view *view, enum request request) default: /* An unknown key will show most commonly used commands. */ - report("%s", HELP); + report("Unknown key, press 'h' for help"); return TRUE; } @@ -1356,6 +1376,8 @@ pager_draw(struct view *view, unsigned int lineno) line = view->line[view->offset + lineno]; type = get_line_type(line); + wmove(view->win, lineno, 0); + if (view->offset + lineno == view->lineno) { if (type == LINE_COMMIT) { string_copy(view->ref, line + 7); @@ -1363,61 +1385,62 @@ pager_draw(struct view *view, unsigned int lineno) } type = LINE_CURSOR; + wchgat(view->win, -1, 0, type, NULL); } attr = get_line_attr(type); wattrset(view->win, attr); linelen = strlen(line); - linelen = MIN(linelen, view->width); - if (opt_line_number) { - static char indent[] = " "; - unsigned long real_lineno = view->offset + lineno + 1; - int col = 0; + if (opt_line_number || opt_tab_size < TABSIZE) { + static char spaces[] = " "; + int col_offset = 0, col = 0; - if (real_lineno == 1 || (real_lineno % opt_num_interval) == 0) - mvwprintw(view->win, lineno, 0, "%.*d", view->digits, real_lineno); + if (opt_line_number) { + unsigned long real_lineno = view->offset + lineno + 1; - else if (view->digits < sizeof(indent)) - mvwaddnstr(view->win, lineno, 0, indent, view->digits); + if (real_lineno == 1 || + (real_lineno % opt_num_interval) == 0) { + wprintw(view->win, "%.*d", view->digits, real_lineno); - waddstr(view->win, ": "); + } else { + waddnstr(view->win, spaces, + MIN(view->digits, STRING_SIZE(spaces))); + } + waddstr(view->win, ": "); + col_offset = view->digits + 2; + } + + while (line && col_offset + col < view->width) { + int cols_max = view->width - col_offset - col; + char *text = line; + int cols; - while (line) { if (*line == '\t') { - waddnstr(view->win, " ", 8 - (col % 8)); - col += 8 - (col % 8); + assert(sizeof(spaces) > TABSIZE); line++; + text = spaces; + cols = opt_tab_size - (col % opt_tab_size); } else { - char *tab = strchr(line, '\t'); - - if (tab) - waddnstr(view->win, line, tab - line); - else - waddstr(view->win, line); - col += tab - line; - line = tab; + line = strchr(line, '\t'); + cols = line ? line - text : strlen(text); } + + waddnstr(view->win, text, MIN(cols, cols_max)); + col += cols; } - waddstr(view->win, line); } else { -#if 0 - /* NOTE: Code for only highlighting the text on the cursor line. - * Kept since I've not yet decided whether to highlight the - * entire line or not. --fonseca */ - /* No empty lines makes cursor drawing and clearing implicit. */ - if (!*line) - line = " ", linelen = 1; -#endif - mvwaddnstr(view->win, lineno, 0, line, linelen); - } + int col = 0, pos = 0; - /* Paint the rest of the line if it's the cursor line. */ - if (type == LINE_CURSOR) - wchgat(view->win, -1, 0, type, NULL); + for (; pos < linelen && col < view->width; pos++, col++) + if (line[pos] == '\t') + col += TABSIZE - (col % TABSIZE) - 1; + + waddnstr(view->win, line, pos); + } return TRUE; } @@ -1425,6 +1448,13 @@ pager_draw(struct view *view, unsigned int lineno) static bool pager_read(struct view *view, char *line) { + /* Compress empty lines in the help view. */ + if (view == VIEW(REQ_VIEW_HELP) && + !*line && + view->lines && + !*((char *) view->line[view->lines - 1])) + return TRUE; + view->line[view->lines] = strdup(line); if (!view->line[view->lines]) return FALSE; @@ -1439,26 +1469,32 @@ pager_enter(struct view *view) char *line = view->line[view->lineno]; if (get_line_type(line) == LINE_COMMIT) { - open_view(view, REQ_VIEW_DIFF, OPEN_DEFAULT); + if (view == VIEW(REQ_VIEW_LOG)) + open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT | OPEN_BACKGROUNDED); + else + open_view(view, REQ_VIEW_DIFF, OPEN_DEFAULT); } return TRUE; } static struct view_ops pager_ops = { + "line", pager_draw, pager_read, pager_enter, }; +static struct ref **get_refs(char *id); + static bool main_draw(struct view *view, unsigned int lineno) { char buf[DATE_COLS + 1]; struct commit *commit; enum line_type type; - int cols = 0; + int col = 0; size_t timelen; if (view->offset + lineno >= view->lines) @@ -1468,39 +1504,77 @@ main_draw(struct view *view, unsigned int lineno) if (!*commit->author) return FALSE; + wmove(view->win, lineno, col); + if (view->offset + lineno == view->lineno) { string_copy(view->ref, commit->id); string_copy(ref_commit, view->ref); type = LINE_CURSOR; + wattrset(view->win, get_line_attr(type)); + wchgat(view->win, -1, 0, type, NULL); + } else { type = LINE_MAIN_COMMIT; + wattrset(view->win, get_line_attr(LINE_MAIN_DATE)); } - wmove(view->win, lineno, cols); - wattrset(view->win, get_line_attr(LINE_MAIN_DATE)); - timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time); waddnstr(view->win, buf, timelen); waddstr(view->win, " "); - cols += DATE_COLS; - wmove(view->win, lineno, cols); - wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR)); + col += DATE_COLS; + wmove(view->win, lineno, col); + if (type != LINE_CURSOR) + wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR)); if (strlen(commit->author) > 19) { waddnstr(view->win, commit->author, 18); - wattrset(view->win, get_line_attr(LINE_MAIN_DELIM)); + if (type != LINE_CURSOR) + wattrset(view->win, get_line_attr(LINE_MAIN_DELIM)); waddch(view->win, '~'); } else { waddstr(view->win, commit->author); } - cols += 20; - wattrset(view->win, A_NORMAL); - mvwaddch(view->win, lineno, cols, ACS_LTEE); - wattrset(view->win, get_line_attr(type)); - mvwaddstr(view->win, lineno, cols + 2, commit->title); - wattrset(view->win, A_NORMAL); + col += 20; + if (type != LINE_CURSOR) + wattrset(view->win, A_NORMAL); + + mvwaddch(view->win, lineno, col, ACS_LTEE); + wmove(view->win, lineno, col + 2); + col += 2; + + if (commit->refs) { + size_t i = 0; + + do { + if (type == LINE_CURSOR) + ; + else 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, "]"); + if (type != LINE_CURSOR) + wattrset(view->win, A_NORMAL); + waddstr(view->win, " "); + col += strlen(commit->refs[i]->name) + STRING_SIZE("[] "); + } while (commit->refs[i++]->next); + } + + if (type != LINE_CURSOR) + wattrset(view->win, get_line_attr(type)); + + { + int titlelen = strlen(commit->title); + + if (col + titlelen > view->width) + titlelen = view->width - col; + + waddnstr(view->win, commit->title, titlelen); + } return TRUE; } @@ -1522,6 +1596,7 @@ 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: @@ -1600,11 +1675,150 @@ main_enter(struct view *view) } static struct view_ops main_ops = { + "commit", main_draw, main_read, main_enter, }; + +/** + * KEYS + * ---- + * Below the default key bindings are shown. + **/ + +struct keymap { + int alias; + int request; +}; + +static struct keymap keymap[] = { + /** + * View switching + * ~~~~~~~~~~~~~~ + * m:: + * Switch to main view. + * d:: + * Switch to diff view. + * l:: + * Switch to log view. + * p:: + * Switch to pager view. + * h:: + * Show man page. + * Return:: + * If on a commit line show the commit diff. Additionally, if in + * main or log view this will split the view. To open the commit + * diff in full size view either use 'd' or press Return twice. + * Tab:: + * Switch to next view. + **/ + { '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 }, + { KEY_RETURN, REQ_ENTER }, + + /** + * Cursor navigation + * ~~~~~~~~~~~~~~~~~ + * Up:: + * Move cursor one line up. + * Down:: + * Move cursor one line down. + * k:: + * Move cursor one line up and enter. When used in the main view + * this will always show the diff of the current commit in the + * split diff view. + * j:: + * Move cursor one line down and enter. + * PgUp:: + * Move cursor one page up. + * PgDown:: + * Move cursor one page down. + * Home:: + * Jump to first line. + * End:: + * Jump to last line. + **/ + { KEY_UP, REQ_MOVE_UP }, + { KEY_DOWN, REQ_MOVE_DOWN }, + { 'k', REQ_MOVE_UP_ENTER }, + { 'j', REQ_MOVE_DOWN_ENTER }, + { KEY_HOME, REQ_MOVE_FIRST_LINE }, + { KEY_END, REQ_MOVE_LAST_LINE }, + { KEY_NPAGE, REQ_MOVE_PAGE_DOWN }, + { KEY_PPAGE, REQ_MOVE_PAGE_UP }, + + /** + * Scrolling + * ~~~~~~~~~ + * Insert:: + * Scroll view one line up. + * Delete:: + * Scroll view one line down. + * w:: + * Scroll view one page up. + * s:: + * Scroll view one page down. + **/ + { KEY_IC, REQ_SCROLL_LINE_UP }, + { KEY_DC, REQ_SCROLL_LINE_DOWN }, + { 'w', REQ_SCROLL_PAGE_UP }, + { 's', REQ_SCROLL_PAGE_DOWN }, + + /** + * Misc + * ~~~~ + * q:: + * Quit + * r:: + * Redraw screen. + * z:: + * Stop all background loading. This can be useful if you use + * tig(1) in a repository with a long history without limiting + * the revision log. + * v:: + * Show version. + * n:: + * Toggle line numbers on/off. + * ':':: + * Open prompt. This allows you to specify what git command + * to run. Example: + * + * :log -p + **/ + { 'q', REQ_QUIT }, + { 'z', REQ_STOP_LOADING }, + { 'v', REQ_SHOW_VERSION }, + { 'r', REQ_SCREEN_REDRAW }, + { 'n', REQ_TOGGLE_LINE_NUMBERS }, + { ':', REQ_PROMPT }, + + /* 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 +get_request(int key) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(keymap); i++) + if (keymap[i].alias == key) + return keymap[i].request; + + return (enum request) key; +} + + /* * Status management */ @@ -1619,32 +1833,47 @@ static WINDOW *status_win; static void report(const char *msg, ...) { - va_list args; + static bool empty = TRUE; + struct view *view = display[current_view]; + + if (!empty || *msg) { + va_list args; - /* Update the title window first, so the cursor ends up in the status - * window. */ - update_view_title(display[current_view]); + va_start(args, msg); - va_start(args, msg); + werase(status_win); + wmove(status_win, 0, 0); + if (*msg) { + vwprintw(status_win, msg, args); + empty = FALSE; + } else { + empty = TRUE; + } + wrefresh(status_win); + + va_end(args); + } - werase(status_win); - wmove(status_win, 0, 0); - if (*msg) - vwprintw(status_win, msg, args); - wrefresh(status_win); + update_view_title(view); - va_end(args); + /* Move the cursor to the right-most column of the cursor line. + * + * XXX: This could turn out to be a bit expensive, but it ensures that + * the cursor does not jump around. */ + if (view->lines) { + wmove(view->win, view->lineno - view->offset, view->width - 1); + wrefresh(view->win); + } } /* Controls when nodelay should be in effect when polling user input. */ static void set_nonblocking_input(bool loading) { - /* The number of loading views. */ - static unsigned int nloading; + static unsigned int loading_views; - if ((loading == FALSE && nloading-- == 1) || - (loading == TRUE && nloading++ == 0)) + if ((loading == FALSE && loading_views-- == 1) || + (loading == TRUE && loading_views++ == 0)) nodelay(status_win, loading); } @@ -1684,6 +1913,120 @@ init_display(void) wbkgdset(status_win, get_line_attr(LINE_STATUS)); } + +/* + * Repository references + */ + +static struct ref *refs; +static 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; + if (id_refs_size > 0) + id_refs[id_refs_size - 1]->next = 1; + id_refs[id_refs_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. */ + id_refs[id_refs_size]->next = 0; + id_refs_size++; + } + + return id_refs; +} + +static int +load_refs(void) +{ + char *cmd_env = getenv("TIG_LS_REMOTE"); + char *cmd = cmd_env && *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; + bool tag_commit = FALSE; + + if (!name) + continue; + + *name++ = 0; + namelen = strlen(name) - 1; + + /* Commits referenced by tags has "^{}" appended. */ + if (name[namelen - 1] == '}') { + while (namelen > 0 && name[namelen] != '^') + namelen--; + if (namelen > 0) + tag_commit = TRUE; + } + name[namelen] = 0; + + if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) { + if (!tag_commit) + continue; + name += STRING_SIZE("refs/tags/"); + tag = TRUE; + + } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) { + name += STRING_SIZE("refs/heads/"); + + } else if (!strcmp(name, "HEAD")) { + continue; + } + + 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); + + if (refs_size == 0) + die("Not a git repository"); + + return OK; +} + /* * Main */ @@ -1724,6 +2067,9 @@ main(int argc, char *argv[]) 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); @@ -1742,7 +2088,7 @@ main(int argc, char *argv[]) key = wgetch(status_win); request = get_request(key); - /* Some low-level request handling. This keeps handling of + /* Some low-level request handling. This keeps access to * status_win restricted. */ switch (request) { case REQ_PROMPT: @@ -1787,6 +2133,109 @@ main(int argc, char *argv[]) } /** + * [[refspec]] + * Revision specification + * ---------------------- + * This section describes various ways to specify what revisions to display + * or otherwise limit the view to. tig(1) does not itself parse the described + * revision options so refer to the relevant git man pages for futher + * information. Relevant man pages besides git-log(1) are git-diff(1) and + * git-rev-list(1). + * + * You can tune the interaction with git by making use of the options + * explained in this section. For example, by configuring the environment + * variables described in the <> section. + * + * Limit by path name + * ~~~~~~~~~~~~~~~~~~ + * If you are interested only in those revisions that made changes to a + * specific file (or even several files) list the files like this: + * + * $ tig log Makefile + * + * To avoid ambiguity with repository references such as tag name, be sure + * to separate file names from other git options using "\--". So if you + * have a file named 'master' it will clash with the reference named + * 'master', and thus you will have to use: + * + * $ tig log -- master + * + * NOTE: For the main view, avoiding ambiguity will in some cases require + * you to specify two "\--" options. The first will make tig(1) stop + * option processing and the latter will be passed to git log. + * + * Limit by date or number + * ~~~~~~~~~~~~~~~~~~~~~~~ + * To speed up interaction with git, you can limit the amount of commits + * to show both for the log and main view. Either limit by date using + * e.g. `--since=1.month` or limit by the number of commits using `-n400`. + * + * If you are only interested in changed that happened between two dates + * you can use: + * + * $ tig -- --after=May.5th --before=2006-05-16.15:44 + * + * NOTE: The dot (".") is used as a separator instead of a space to avoid + * having to quote the option value. If you prefer use `--after="May 5th"` + * instead of `--after="May 5th"`. + * + * Limiting by commit ranges + * ~~~~~~~~~~~~~~~~~~~~~~~~~ + * Alternatively, commits can be limited to a specific range, such as + * "all commits between 'tag-1.0' and 'tag-2.0'". For example: + * + * $ tig log tag-1.0..tag-2.0 + * + * This way of commit limiting makes it trivial to only browse the commits + * which haven't been pushed to a remote branch. Assuming 'origin' is your + * upstream remote branch, using: + * + * $ tig log origin..HEAD + * + * will list what will be pushed to the remote branch. Optionally, the ending + * 'HEAD' can be left out since it is implied. + * + * Limiting by reachability + * ~~~~~~~~~~~~~~~~~~~~~~~~ + * Git interprets the range specifier "tag-1.0..tag-2.0" as + * "all commits reachable from 'tag-2.0' but not from 'tag-1.0'". + * Where reachability refers to what commits are ancestors (or part of the + * history) of the branch or tagged revision in question. + * + * If you prefer to specify which commit to preview in this way use the + * following: + * + * $ tig log tag-2.0 ^tag-1.0 + * + * You can think of '^' as a negation operator. Using this alternate syntax, + * it is possible to further prune commits by specifying multiple branch + * cut offs. + * + * Combining revisions specification + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Revisions options can to some degree be combined, which makes it possible + * to say "show at most 20 commits from within the last month that changed + * files under the Documentation/ directory." + * + * $ tig -- --since=1.month -n20 -- Documentation/ + * + * Examining all repository references + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * In some cases, it can be useful to query changes across all references + * in a repository. An example is to ask "did any line of development in + * this repository change a particular file within the last week". This + * can be accomplished using: + * + * $ tig -- --all --since=1.week -- Makefile + * + * BUGS + * ---- + * Known bugs and problems: + * + * - If the screen width is very small the main view can draw + * outside the current view causing bad wrapping. Same goes + * for title and status windows. + * * TODO * ---- * Features that should be explored.