1 /* Copyright (c) 2006-2008 Jonas Fonseca <fonseca@diku.dk>
3 * This program is free software; you can redistribute it and/or
4 * modify it under the terms of the GNU General Public License as
5 * published by the Free Software Foundation; either version 2 of
6 * the License, or (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
19 #define TIG_VERSION "unknown-version"
34 #include <sys/types.h>
45 /* ncurses(3): Must be defined to have extended wide-character functions. */
46 #define _XOPEN_SOURCE_EXTENDED
48 #ifdef HAVE_NCURSESW_NCURSES_H
49 #include <ncursesw/ncurses.h>
51 #ifdef HAVE_NCURSES_NCURSES_H
52 #include <ncurses/ncurses.h>
59 #define __NORETURN __attribute__((__noreturn__))
64 static void __NORETURN die(const char *err, ...);
65 static void warn(const char *msg, ...);
66 static void report(const char *msg, ...);
67 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, size_t, char *, size_t));
68 static void set_nonblocking_input(bool loading);
69 static size_t utf8_length(const char *string, int *width, size_t max_width, int *trimmed, bool reserve);
70 static bool prompt_yesno(const char *prompt);
72 #define ABS(x) ((x) >= 0 ? (x) : -(x))
73 #define MIN(x, y) ((x) < (y) ? (x) : (y))
75 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
76 #define STRING_SIZE(x) (sizeof(x) - 1)
78 #define SIZEOF_STR 1024 /* Default string size. */
79 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
80 #define SIZEOF_REV 41 /* Holds a SHA-1 and an ending NUL */
84 #define REVGRAPH_INIT 'I'
85 #define REVGRAPH_MERGE 'M'
86 #define REVGRAPH_BRANCH '+'
87 #define REVGRAPH_COMMIT '*'
88 #define REVGRAPH_BOUND '^'
90 #define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
92 /* This color name can be used to refer to the default term colors. */
93 #define COLOR_DEFAULT (-1)
95 #define ICONV_NONE ((iconv_t) -1)
97 #define ICONV_CONST /* nothing */
100 /* The format and size of the date column in the main view. */
101 #define DATE_FORMAT "%Y-%m-%d %H:%M"
102 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
104 #define AUTHOR_COLS 20
107 /* The default interval between line numbers. */
108 #define NUMBER_INTERVAL 5
112 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
114 #define NULL_ID "0000000000000000000000000000000000000000"
117 #define GIT_CONFIG "git config"
120 #define TIG_LS_REMOTE \
121 "git ls-remote . 2>/dev/null"
123 #define TIG_DIFF_CMD \
124 "git show --pretty=fuller --no-color --root --patch-with-stat --find-copies-harder -C %s 2>/dev/null"
126 #define TIG_LOG_CMD \
127 "git log --no-color --cc --stat -n100 %s 2>/dev/null"
129 #define TIG_MAIN_CMD \
130 "git log --no-color --topo-order --parents --pretty=raw %s 2>/dev/null"
132 #define TIG_TREE_CMD \
135 #define TIG_BLOB_CMD \
136 "git cat-file blob %s"
138 /* XXX: Needs to be defined to the empty string. */
139 #define TIG_HELP_CMD ""
140 #define TIG_PAGER_CMD ""
141 #define TIG_STATUS_CMD ""
142 #define TIG_STAGE_CMD ""
143 #define TIG_BLAME_CMD ""
145 /* Some ascii-shorthands fitted into the ncurses namespace. */
147 #define KEY_RETURN '\r'
152 char *name; /* Ref name; tag or head names are shortened. */
153 char id[SIZEOF_REV]; /* Commit SHA1 ID */
154 unsigned int head:1; /* Is it the current HEAD? */
155 unsigned int tag:1; /* Is it a tag? */
156 unsigned int ltag:1; /* If so, is the tag local? */
157 unsigned int remote:1; /* Is it a remote ref? */
158 unsigned int tracked:1; /* Is it the remote for the current HEAD? */
159 unsigned int next:1; /* For ref lists: are there more refs? */
162 static struct ref **get_refs(char *id);
171 set_from_int_map(struct int_map *map, size_t map_size,
172 int *value, const char *name, int namelen)
177 for (i = 0; i < map_size; i++)
178 if (namelen == map[i].namelen &&
179 !strncasecmp(name, map[i].name, namelen)) {
180 *value = map[i].value;
193 string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
195 if (srclen > dstlen - 1)
198 strncpy(dst, src, srclen);
202 /* Shorthands for safely copying into a fixed buffer. */
204 #define string_copy(dst, src) \
205 string_ncopy_do(dst, sizeof(dst), src, sizeof(src))
207 #define string_ncopy(dst, src, srclen) \
208 string_ncopy_do(dst, sizeof(dst), src, srclen)
210 #define string_copy_rev(dst, src) \
211 string_ncopy_do(dst, SIZEOF_REV, src, SIZEOF_REV - 1)
213 #define string_add(dst, from, src) \
214 string_ncopy_do(dst + (from), sizeof(dst) - (from), src, sizeof(src))
217 chomp_string(char *name)
221 while (isspace(*name))
224 namelen = strlen(name) - 1;
225 while (namelen > 0 && isspace(name[namelen]))
232 string_nformat(char *buf, size_t bufsize, size_t *bufpos, const char *fmt, ...)
235 size_t pos = bufpos ? *bufpos : 0;
238 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
244 return pos >= bufsize ? FALSE : TRUE;
247 #define string_format(buf, fmt, args...) \
248 string_nformat(buf, sizeof(buf), NULL, fmt, args)
250 #define string_format_from(buf, from, fmt, args...) \
251 string_nformat(buf, sizeof(buf), from, fmt, args)
254 string_enum_compare(const char *str1, const char *str2, int len)
258 #define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
260 /* Diff-Header == DIFF_HEADER */
261 for (i = 0; i < len; i++) {
262 if (toupper(str1[i]) == toupper(str2[i]))
265 if (string_enum_sep(str1[i]) &&
266 string_enum_sep(str2[i]))
269 return str1[i] - str2[i];
277 * NOTE: The following is a slightly modified copy of the git project's shell
278 * quoting routines found in the quote.c file.
280 * Help to copy the thing properly quoted for the shell safety. any single
281 * quote is replaced with '\'', any exclamation point is replaced with '\!',
282 * and the whole thing is enclosed in a
285 * original sq_quote result
286 * name ==> name ==> 'name'
287 * a b ==> a b ==> 'a b'
288 * a'b ==> a'\''b ==> 'a'\''b'
289 * a!b ==> a'\!'b ==> 'a'\!'b'
293 sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
297 #define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0)
300 while ((c = *src++)) {
301 if (c == '\'' || c == '!') {
312 if (bufsize < SIZEOF_STR)
324 /* XXX: Keep the view request first and in sync with views[]. */ \
325 REQ_GROUP("View switching") \
326 REQ_(VIEW_MAIN, "Show main view"), \
327 REQ_(VIEW_DIFF, "Show diff view"), \
328 REQ_(VIEW_LOG, "Show log view"), \
329 REQ_(VIEW_TREE, "Show tree view"), \
330 REQ_(VIEW_BLOB, "Show blob view"), \
331 REQ_(VIEW_BLAME, "Show blame view"), \
332 REQ_(VIEW_HELP, "Show help page"), \
333 REQ_(VIEW_PAGER, "Show pager view"), \
334 REQ_(VIEW_STATUS, "Show status view"), \
335 REQ_(VIEW_STAGE, "Show stage view"), \
337 REQ_GROUP("View manipulation") \
338 REQ_(ENTER, "Enter current line and scroll"), \
339 REQ_(NEXT, "Move to next"), \
340 REQ_(PREVIOUS, "Move to previous"), \
341 REQ_(VIEW_NEXT, "Move focus to next view"), \
342 REQ_(REFRESH, "Reload and refresh"), \
343 REQ_(MAXIMIZE, "Maximize the current view"), \
344 REQ_(VIEW_CLOSE, "Close the current view"), \
345 REQ_(QUIT, "Close all views and quit"), \
347 REQ_GROUP("Cursor navigation") \
348 REQ_(MOVE_UP, "Move cursor one line up"), \
349 REQ_(MOVE_DOWN, "Move cursor one line down"), \
350 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
351 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
352 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
353 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
355 REQ_GROUP("Scrolling") \
356 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
357 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
358 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
359 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
361 REQ_GROUP("Searching") \
362 REQ_(SEARCH, "Search the view"), \
363 REQ_(SEARCH_BACK, "Search backwards in the view"), \
364 REQ_(FIND_NEXT, "Find next search match"), \
365 REQ_(FIND_PREV, "Find previous search match"), \
368 REQ_(PROMPT, "Bring up the prompt"), \
369 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
370 REQ_(SCREEN_RESIZE, "Resize the screen"), \
371 REQ_(SHOW_VERSION, "Show version information"), \
372 REQ_(STOP_LOADING, "Stop all loading views"), \
373 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
374 REQ_(TOGGLE_DATE, "Toggle date display"), \
375 REQ_(TOGGLE_AUTHOR, "Toggle author display"), \
376 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"), \
377 REQ_(TOGGLE_REFS, "Toggle reference display (tags/branches)"), \
378 REQ_(STATUS_UPDATE, "Update file status"), \
379 REQ_(STATUS_CHECKOUT, "Checkout file"), \
380 REQ_(STATUS_MERGE, "Merge file using external tool"), \
381 REQ_(STAGE_NEXT, "Find next chunk to stage"), \
382 REQ_(TREE_PARENT, "Switch to parent directory in tree view"), \
383 REQ_(EDIT, "Open in editor"), \
384 REQ_(NONE, "Do nothing")
387 /* User action requests. */
389 #define REQ_GROUP(help)
390 #define REQ_(req, help) REQ_##req
392 /* Offset all requests to avoid conflicts with ncurses getch values. */
393 REQ_OFFSET = KEY_MAX + 1,
400 struct request_info {
401 enum request request;
407 static struct request_info req_info[] = {
408 #define REQ_GROUP(help) { 0, NULL, 0, (help) },
409 #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
416 get_request(const char *name)
418 int namelen = strlen(name);
421 for (i = 0; i < ARRAY_SIZE(req_info); i++)
422 if (req_info[i].namelen == namelen &&
423 !string_enum_compare(req_info[i].name, name, namelen))
424 return req_info[i].request;
434 static const char usage[] =
435 "tig " TIG_VERSION " (" __DATE__ ")\n"
437 "Usage: tig [options] [revs] [--] [paths]\n"
438 " or: tig show [options] [revs] [--] [paths]\n"
439 " or: tig blame [rev] path\n"
441 " or: tig < [git command output]\n"
444 " -v, --version Show version and exit\n"
445 " -h, --help Show help message and exit";
447 /* Option and state variables. */
448 static bool opt_date = TRUE;
449 static bool opt_author = TRUE;
450 static bool opt_line_number = FALSE;
451 static bool opt_line_graphics = TRUE;
452 static bool opt_rev_graph = FALSE;
453 static bool opt_show_refs = TRUE;
454 static int opt_num_interval = NUMBER_INTERVAL;
455 static int opt_tab_size = TAB_SIZE;
456 static int opt_author_cols = AUTHOR_COLS-1;
457 static char opt_cmd[SIZEOF_STR] = "";
458 static char opt_path[SIZEOF_STR] = "";
459 static char opt_file[SIZEOF_STR] = "";
460 static char opt_ref[SIZEOF_REF] = "";
461 static char opt_head[SIZEOF_REF] = "";
462 static char opt_remote[SIZEOF_REF] = "";
463 static bool opt_no_head = TRUE;
464 static FILE *opt_pipe = NULL;
465 static char opt_encoding[20] = "UTF-8";
466 static bool opt_utf8 = TRUE;
467 static char opt_codeset[20] = "UTF-8";
468 static iconv_t opt_iconv = ICONV_NONE;
469 static char opt_search[SIZEOF_STR] = "";
470 static char opt_cdup[SIZEOF_STR] = "";
471 static char opt_git_dir[SIZEOF_STR] = "";
472 static signed char opt_is_inside_work_tree = -1; /* set to TRUE or FALSE */
473 static char opt_editor[SIZEOF_STR] = "";
476 parse_options(int argc, char *argv[])
478 enum request request = REQ_VIEW_MAIN;
481 bool seen_dashdash = FALSE;
484 if (!isatty(STDIN_FILENO)) {
486 return REQ_VIEW_PAGER;
490 return REQ_VIEW_MAIN;
492 subcommand = argv[1];
493 if (!strcmp(subcommand, "status") || !strcmp(subcommand, "-S")) {
494 if (!strcmp(subcommand, "-S"))
495 warn("`-S' has been deprecated; use `tig status' instead");
497 warn("ignoring arguments after `%s'", subcommand);
498 return REQ_VIEW_STATUS;
500 } else if (!strcmp(subcommand, "blame")) {
501 if (argc <= 2 || argc > 4)
502 die("invalid number of options to blame\n\n%s", usage);
506 string_ncopy(opt_ref, argv[i], strlen(argv[i]));
510 string_ncopy(opt_file, argv[i], strlen(argv[i]));
511 return REQ_VIEW_BLAME;
513 } else if (!strcmp(subcommand, "show")) {
514 request = REQ_VIEW_DIFF;
516 } else if (!strcmp(subcommand, "log") || !strcmp(subcommand, "diff")) {
517 request = subcommand[0] == 'l' ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
518 warn("`tig %s' has been deprecated", subcommand);
525 /* XXX: This is vulnerable to the user overriding
526 * options required for the main view parser. */
527 string_copy(opt_cmd, "git log --no-color --pretty=raw --parents");
529 string_format(opt_cmd, "git %s", subcommand);
531 buf_size = strlen(opt_cmd);
533 for (i = 1 + !!subcommand; i < argc; i++) {
536 if (seen_dashdash || !strcmp(opt, "--")) {
537 seen_dashdash = TRUE;
539 } else if (!strcmp(opt, "-v") || !strcmp(opt, "--version")) {
540 printf("tig version %s\n", TIG_VERSION);
543 } else if (!strcmp(opt, "-h") || !strcmp(opt, "--help")) {
544 printf("%s\n", usage);
548 opt_cmd[buf_size++] = ' ';
549 buf_size = sq_quote(opt_cmd, buf_size, opt);
550 if (buf_size >= sizeof(opt_cmd))
551 die("command too long");
554 opt_cmd[buf_size] = 0;
561 * Line-oriented content detection.
565 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
566 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
567 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
568 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
569 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
570 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
571 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
572 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
573 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
574 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
575 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
576 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
577 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
578 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
579 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
580 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
581 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
582 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
583 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
584 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
585 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
586 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
587 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
588 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
589 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
590 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
591 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
592 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
593 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
594 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
595 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
596 LINE(DELIMITER, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
597 LINE(DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
598 LINE(LINE_NUMBER, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
599 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
600 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
601 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
602 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
603 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
604 LINE(MAIN_LOCAL_TAG,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
605 LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \
606 LINE(MAIN_TRACKED, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
607 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
608 LINE(MAIN_HEAD, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
609 LINE(MAIN_REVGRAPH,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
610 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
611 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
612 LINE(STAT_HEAD, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \
613 LINE(STAT_SECTION, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
614 LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
615 LINE(STAT_STAGED, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
616 LINE(STAT_UNSTAGED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
617 LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
618 LINE(BLAME_ID, "", COLOR_MAGENTA, COLOR_DEFAULT, 0)
621 #define LINE(type, line, fg, bg, attr) \
629 const char *name; /* Option name. */
630 int namelen; /* Size of option name. */
631 const char *line; /* The start of line to match. */
632 int linelen; /* Size of string to match. */
633 int fg, bg, attr; /* Color and text attributes for the lines. */
636 static struct line_info line_info[] = {
637 #define LINE(type, line, fg, bg, attr) \
638 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
643 static enum line_type
644 get_line_type(char *line)
646 int linelen = strlen(line);
649 for (type = 0; type < ARRAY_SIZE(line_info); type++)
650 /* Case insensitive search matches Signed-off-by lines better. */
651 if (linelen >= line_info[type].linelen &&
652 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
659 get_line_attr(enum line_type type)
661 assert(type < ARRAY_SIZE(line_info));
662 return COLOR_PAIR(type) | line_info[type].attr;
665 static struct line_info *
666 get_line_info(char *name)
668 size_t namelen = strlen(name);
671 for (type = 0; type < ARRAY_SIZE(line_info); type++)
672 if (namelen == line_info[type].namelen &&
673 !string_enum_compare(line_info[type].name, name, namelen))
674 return &line_info[type];
682 int default_bg = line_info[LINE_DEFAULT].bg;
683 int default_fg = line_info[LINE_DEFAULT].fg;
688 if (assume_default_colors(default_fg, default_bg) == ERR) {
689 default_bg = COLOR_BLACK;
690 default_fg = COLOR_WHITE;
693 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
694 struct line_info *info = &line_info[type];
695 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
696 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
698 init_pair(type, fg, bg);
706 unsigned int selected:1;
707 unsigned int dirty:1;
709 void *data; /* User data */
719 enum request request;
720 struct keybinding *next;
723 static struct keybinding default_keybindings[] = {
725 { 'm', REQ_VIEW_MAIN },
726 { 'd', REQ_VIEW_DIFF },
727 { 'l', REQ_VIEW_LOG },
728 { 't', REQ_VIEW_TREE },
729 { 'f', REQ_VIEW_BLOB },
730 { 'B', REQ_VIEW_BLAME },
731 { 'p', REQ_VIEW_PAGER },
732 { 'h', REQ_VIEW_HELP },
733 { 'S', REQ_VIEW_STATUS },
734 { 'c', REQ_VIEW_STAGE },
736 /* View manipulation */
737 { 'q', REQ_VIEW_CLOSE },
738 { KEY_TAB, REQ_VIEW_NEXT },
739 { KEY_RETURN, REQ_ENTER },
740 { KEY_UP, REQ_PREVIOUS },
741 { KEY_DOWN, REQ_NEXT },
742 { 'R', REQ_REFRESH },
743 { KEY_F(5), REQ_REFRESH },
744 { 'O', REQ_MAXIMIZE },
746 /* Cursor navigation */
747 { 'k', REQ_MOVE_UP },
748 { 'j', REQ_MOVE_DOWN },
749 { KEY_HOME, REQ_MOVE_FIRST_LINE },
750 { KEY_END, REQ_MOVE_LAST_LINE },
751 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
752 { ' ', REQ_MOVE_PAGE_DOWN },
753 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
754 { 'b', REQ_MOVE_PAGE_UP },
755 { '-', REQ_MOVE_PAGE_UP },
758 { KEY_IC, REQ_SCROLL_LINE_UP },
759 { KEY_DC, REQ_SCROLL_LINE_DOWN },
760 { 'w', REQ_SCROLL_PAGE_UP },
761 { 's', REQ_SCROLL_PAGE_DOWN },
765 { '?', REQ_SEARCH_BACK },
766 { 'n', REQ_FIND_NEXT },
767 { 'N', REQ_FIND_PREV },
771 { 'z', REQ_STOP_LOADING },
772 { 'v', REQ_SHOW_VERSION },
773 { 'r', REQ_SCREEN_REDRAW },
774 { '.', REQ_TOGGLE_LINENO },
775 { 'D', REQ_TOGGLE_DATE },
776 { 'A', REQ_TOGGLE_AUTHOR },
777 { 'g', REQ_TOGGLE_REV_GRAPH },
778 { 'F', REQ_TOGGLE_REFS },
780 { 'u', REQ_STATUS_UPDATE },
781 { '!', REQ_STATUS_CHECKOUT },
782 { 'M', REQ_STATUS_MERGE },
783 { '@', REQ_STAGE_NEXT },
784 { ',', REQ_TREE_PARENT },
787 /* Using the ncurses SIGWINCH handler. */
788 { KEY_RESIZE, REQ_SCREEN_RESIZE },
791 #define KEYMAP_INFO \
805 #define KEYMAP_(name) KEYMAP_##name
810 static struct int_map keymap_table[] = {
811 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
816 #define set_keymap(map, name) \
817 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
819 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
822 add_keybinding(enum keymap keymap, enum request request, int key)
824 struct keybinding *keybinding;
826 keybinding = calloc(1, sizeof(*keybinding));
828 die("Failed to allocate keybinding");
830 keybinding->alias = key;
831 keybinding->request = request;
832 keybinding->next = keybindings[keymap];
833 keybindings[keymap] = keybinding;
836 /* Looks for a key binding first in the given map, then in the generic map, and
837 * lastly in the default keybindings. */
839 get_keybinding(enum keymap keymap, int key)
841 struct keybinding *kbd;
844 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
845 if (kbd->alias == key)
848 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
849 if (kbd->alias == key)
852 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
853 if (default_keybindings[i].alias == key)
854 return default_keybindings[i].request;
856 return (enum request) key;
865 static struct key key_table[] = {
866 { "Enter", KEY_RETURN },
868 { "Backspace", KEY_BACKSPACE },
870 { "Escape", KEY_ESC },
871 { "Left", KEY_LEFT },
872 { "Right", KEY_RIGHT },
874 { "Down", KEY_DOWN },
875 { "Insert", KEY_IC },
876 { "Delete", KEY_DC },
878 { "Home", KEY_HOME },
880 { "PageUp", KEY_PPAGE },
881 { "PageDown", KEY_NPAGE },
891 { "F10", KEY_F(10) },
892 { "F11", KEY_F(11) },
893 { "F12", KEY_F(12) },
897 get_key_value(const char *name)
901 for (i = 0; i < ARRAY_SIZE(key_table); i++)
902 if (!strcasecmp(key_table[i].name, name))
903 return key_table[i].value;
905 if (strlen(name) == 1 && isprint(*name))
912 get_key_name(int key_value)
914 static char key_char[] = "'X'";
918 for (key = 0; key < ARRAY_SIZE(key_table); key++)
919 if (key_table[key].value == key_value)
920 seq = key_table[key].name;
924 isprint(key_value)) {
925 key_char[1] = (char) key_value;
929 return seq ? seq : "'?'";
933 get_key(enum request request)
935 static char buf[BUFSIZ];
942 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
943 struct keybinding *keybinding = &default_keybindings[i];
945 if (keybinding->request != request)
948 if (!string_format_from(buf, &pos, "%s%s", sep,
949 get_key_name(keybinding->alias)))
950 return "Too many keybindings!";
960 char cmd[SIZEOF_STR];
963 static struct run_request *run_request;
964 static size_t run_requests;
967 add_run_request(enum keymap keymap, int key, int argc, char **argv)
969 struct run_request *req;
970 char cmd[SIZEOF_STR];
973 for (bufpos = 0; argc > 0; argc--, argv++)
974 if (!string_format_from(cmd, &bufpos, "%s ", *argv))
977 req = realloc(run_request, (run_requests + 1) * sizeof(*run_request));
982 req = &run_request[run_requests++];
983 string_copy(req->cmd, cmd);
984 req->keymap = keymap;
987 return REQ_NONE + run_requests;
990 static struct run_request *
991 get_run_request(enum request request)
993 if (request <= REQ_NONE)
995 return &run_request[request - REQ_NONE - 1];
999 add_builtin_run_requests(void)
1006 { KEYMAP_MAIN, 'C', { "git cherry-pick %(commit)" } },
1007 { KEYMAP_GENERIC, 'G', { "git gc" } },
1011 for (i = 0; i < ARRAY_SIZE(reqs); i++) {
1014 req = add_run_request(reqs[i].keymap, reqs[i].key, 1, reqs[i].argv);
1015 if (req != REQ_NONE)
1016 add_keybinding(reqs[i].keymap, req, reqs[i].key);
1021 * User config file handling.
1024 static struct int_map color_map[] = {
1025 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
1037 #define set_color(color, name) \
1038 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
1040 static struct int_map attr_map[] = {
1041 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
1048 ATTR_MAP(UNDERLINE),
1051 #define set_attribute(attr, name) \
1052 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
1054 static int config_lineno;
1055 static bool config_errors;
1056 static char *config_msg;
1058 /* Wants: object fgcolor bgcolor [attr] */
1060 option_color_command(int argc, char *argv[])
1062 struct line_info *info;
1064 if (argc != 3 && argc != 4) {
1065 config_msg = "Wrong number of arguments given to color command";
1069 info = get_line_info(argv[0]);
1071 if (!string_enum_compare(argv[0], "main-delim", strlen("main-delim"))) {
1072 info = get_line_info("delimiter");
1074 } else if (!string_enum_compare(argv[0], "main-date", strlen("main-date"))) {
1075 info = get_line_info("date");
1078 config_msg = "Unknown color name";
1083 if (set_color(&info->fg, argv[1]) == ERR ||
1084 set_color(&info->bg, argv[2]) == ERR) {
1085 config_msg = "Unknown color";
1089 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
1090 config_msg = "Unknown attribute";
1097 static bool parse_bool(const char *s)
1099 return (!strcmp(s, "1") || !strcmp(s, "true") ||
1100 !strcmp(s, "yes")) ? TRUE : FALSE;
1104 parse_int(const char *s, int default_value, int min, int max)
1106 int value = atoi(s);
1108 return (value < min || value > max) ? default_value : value;
1111 /* Wants: name = value */
1113 option_set_command(int argc, char *argv[])
1116 config_msg = "Wrong number of arguments given to set command";
1120 if (strcmp(argv[1], "=")) {
1121 config_msg = "No value assigned";
1125 if (!strcmp(argv[0], "show-author")) {
1126 opt_author = parse_bool(argv[2]);
1130 if (!strcmp(argv[0], "show-date")) {
1131 opt_date = parse_bool(argv[2]);
1135 if (!strcmp(argv[0], "show-rev-graph")) {
1136 opt_rev_graph = parse_bool(argv[2]);
1140 if (!strcmp(argv[0], "show-refs")) {
1141 opt_show_refs = parse_bool(argv[2]);
1145 if (!strcmp(argv[0], "show-line-numbers")) {
1146 opt_line_number = parse_bool(argv[2]);
1150 if (!strcmp(argv[0], "line-graphics")) {
1151 opt_line_graphics = parse_bool(argv[2]);
1155 if (!strcmp(argv[0], "line-number-interval")) {
1156 opt_num_interval = parse_int(argv[2], opt_num_interval, 1, 1024);
1160 if (!strcmp(argv[0], "author-width")) {
1161 opt_author_cols = parse_int(argv[2], opt_author_cols, 0, 1024);
1165 if (!strcmp(argv[0], "tab-size")) {
1166 opt_tab_size = parse_int(argv[2], opt_tab_size, 1, 1024);
1170 if (!strcmp(argv[0], "commit-encoding")) {
1171 char *arg = argv[2];
1172 int delimiter = *arg;
1175 switch (delimiter) {
1178 for (arg++, i = 0; arg[i]; i++)
1179 if (arg[i] == delimiter) {
1184 string_ncopy(opt_encoding, arg, strlen(arg));
1189 config_msg = "Unknown variable name";
1193 /* Wants: mode request key */
1195 option_bind_command(int argc, char *argv[])
1197 enum request request;
1202 config_msg = "Wrong number of arguments given to bind command";
1206 if (set_keymap(&keymap, argv[0]) == ERR) {
1207 config_msg = "Unknown key map";
1211 key = get_key_value(argv[1]);
1213 config_msg = "Unknown key";
1217 request = get_request(argv[2]);
1218 if (request == REQ_NONE) {
1219 const char *obsolete[] = { "cherry-pick" };
1220 size_t namelen = strlen(argv[2]);
1223 for (i = 0; i < ARRAY_SIZE(obsolete); i++) {
1224 if (namelen == strlen(obsolete[i]) &&
1225 !string_enum_compare(obsolete[i], argv[2], namelen)) {
1226 config_msg = "Obsolete request name";
1231 if (request == REQ_NONE && *argv[2]++ == '!')
1232 request = add_run_request(keymap, key, argc - 2, argv + 2);
1233 if (request == REQ_NONE) {
1234 config_msg = "Unknown request name";
1238 add_keybinding(keymap, request, key);
1244 set_option(char *opt, char *value)
1251 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1252 argv[argc++] = value;
1255 /* Nothing more to tokenize or last available token. */
1256 if (!*value || argc >= ARRAY_SIZE(argv))
1260 while (isspace(*value))
1264 if (!strcmp(opt, "color"))
1265 return option_color_command(argc, argv);
1267 if (!strcmp(opt, "set"))
1268 return option_set_command(argc, argv);
1270 if (!strcmp(opt, "bind"))
1271 return option_bind_command(argc, argv);
1273 config_msg = "Unknown option command";
1278 read_option(char *opt, size_t optlen, char *value, size_t valuelen)
1283 config_msg = "Internal error";
1285 /* Check for comment markers, since read_properties() will
1286 * only ensure opt and value are split at first " \t". */
1287 optlen = strcspn(opt, "#");
1291 if (opt[optlen] != 0) {
1292 config_msg = "No option value";
1296 /* Look for comment endings in the value. */
1297 size_t len = strcspn(value, "#");
1299 if (len < valuelen) {
1301 value[valuelen] = 0;
1304 status = set_option(opt, value);
1307 if (status == ERR) {
1308 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1309 config_lineno, (int) optlen, opt, config_msg);
1310 config_errors = TRUE;
1313 /* Always keep going if errors are encountered. */
1318 load_option_file(const char *path)
1322 /* It's ok that the file doesn't exist. */
1323 file = fopen(path, "r");
1328 config_errors = FALSE;
1330 if (read_properties(file, " \t", read_option) == ERR ||
1331 config_errors == TRUE)
1332 fprintf(stderr, "Errors while loading %s.\n", path);
1338 char *home = getenv("HOME");
1339 char *tigrc_user = getenv("TIGRC_USER");
1340 char *tigrc_system = getenv("TIGRC_SYSTEM");
1341 char buf[SIZEOF_STR];
1343 add_builtin_run_requests();
1345 if (!tigrc_system) {
1346 if (!string_format(buf, "%s/tigrc", SYSCONFDIR))
1350 load_option_file(tigrc_system);
1353 if (!home || !string_format(buf, "%s/.tigrc", home))
1357 load_option_file(tigrc_user);
1370 /* The display array of active views and the index of the current view. */
1371 static struct view *display[2];
1372 static unsigned int current_view;
1374 /* Reading from the prompt? */
1375 static bool input_mode = FALSE;
1377 #define foreach_displayed_view(view, i) \
1378 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1380 #define displayed_views() (display[1] != NULL ? 2 : 1)
1382 /* Current head and commit ID */
1383 static char ref_blob[SIZEOF_REF] = "";
1384 static char ref_commit[SIZEOF_REF] = "HEAD";
1385 static char ref_head[SIZEOF_REF] = "HEAD";
1388 const char *name; /* View name */
1389 const char *cmd_fmt; /* Default command line format */
1390 const char *cmd_env; /* Command line set via environment */
1391 const char *id; /* Points to either of ref_{head,commit,blob} */
1393 struct view_ops *ops; /* View operations */
1395 enum keymap keymap; /* What keymap does this view have */
1396 bool git_dir; /* Whether the view requires a git directory. */
1398 char cmd[SIZEOF_STR]; /* Command buffer */
1399 char ref[SIZEOF_REF]; /* Hovered commit reference */
1400 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1402 int height, width; /* The width and height of the main window */
1403 WINDOW *win; /* The main window */
1404 WINDOW *title; /* The title window living below the main window */
1407 unsigned long offset; /* Offset of the window top */
1408 unsigned long lineno; /* Current line number */
1411 char grep[SIZEOF_STR]; /* Search string */
1412 regex_t *regex; /* Pre-compiled regex */
1414 /* If non-NULL, points to the view that opened this view. If this view
1415 * is closed tig will switch back to the parent view. */
1416 struct view *parent;
1419 size_t lines; /* Total number of lines */
1420 struct line *line; /* Line index */
1421 size_t line_alloc; /* Total number of allocated lines */
1422 size_t line_size; /* Total number of used lines */
1423 unsigned int digits; /* Number of digits in the lines member. */
1426 struct line *curline; /* Line currently being drawn. */
1427 enum line_type curtype; /* Attribute currently used for drawing. */
1428 unsigned long col; /* Column when drawing. */
1436 /* What type of content being displayed. Used in the title bar. */
1438 /* Open and reads in all view content. */
1439 bool (*open)(struct view *view);
1440 /* Read one line; updates view->line. */
1441 bool (*read)(struct view *view, char *data);
1442 /* Draw one line; @lineno must be < view->height. */
1443 bool (*draw)(struct view *view, struct line *line, unsigned int lineno);
1444 /* Depending on view handle a special requests. */
1445 enum request (*request)(struct view *view, enum request request, struct line *line);
1446 /* Search for regex in a line. */
1447 bool (*grep)(struct view *view, struct line *line);
1449 void (*select)(struct view *view, struct line *line);
1452 static struct view_ops pager_ops;
1453 static struct view_ops main_ops;
1454 static struct view_ops tree_ops;
1455 static struct view_ops blob_ops;
1456 static struct view_ops blame_ops;
1457 static struct view_ops help_ops;
1458 static struct view_ops status_ops;
1459 static struct view_ops stage_ops;
1461 #define VIEW_STR(name, cmd, env, ref, ops, map, git) \
1462 { name, cmd, #env, ref, ops, map, git }
1464 #define VIEW_(id, name, ops, git, ref) \
1465 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id, git)
1468 static struct view views[] = {
1469 VIEW_(MAIN, "main", &main_ops, TRUE, ref_head),
1470 VIEW_(DIFF, "diff", &pager_ops, TRUE, ref_commit),
1471 VIEW_(LOG, "log", &pager_ops, TRUE, ref_head),
1472 VIEW_(TREE, "tree", &tree_ops, TRUE, ref_commit),
1473 VIEW_(BLOB, "blob", &blob_ops, TRUE, ref_blob),
1474 VIEW_(BLAME, "blame", &blame_ops, TRUE, ref_commit),
1475 VIEW_(HELP, "help", &help_ops, FALSE, ""),
1476 VIEW_(PAGER, "pager", &pager_ops, FALSE, "stdin"),
1477 VIEW_(STATUS, "status", &status_ops, TRUE, ""),
1478 VIEW_(STAGE, "stage", &stage_ops, TRUE, ""),
1481 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1482 #define VIEW_REQ(view) ((view) - views + REQ_OFFSET + 1)
1484 #define foreach_view(view, i) \
1485 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1487 #define view_is_displayed(view) \
1488 (view == display[0] || view == display[1])
1495 static int line_graphics[] = {
1496 /* LINE_GRAPHIC_VLINE: */ '|'
1500 set_view_attr(struct view *view, enum line_type type)
1502 if (!view->curline->selected && view->curtype != type) {
1503 wattrset(view->win, get_line_attr(type));
1504 wchgat(view->win, -1, 0, type, NULL);
1505 view->curtype = type;
1510 draw_chars(struct view *view, enum line_type type, const char *string,
1511 int max_len, bool use_tilde)
1515 int trimmed = FALSE;
1521 len = utf8_length(string, &col, max_len, &trimmed, use_tilde);
1523 col = len = strlen(string);
1524 if (len > max_len) {
1528 col = len = max_len;
1533 set_view_attr(view, type);
1534 waddnstr(view->win, string, len);
1535 if (trimmed && use_tilde) {
1536 set_view_attr(view, LINE_DELIMITER);
1537 waddch(view->win, '~');
1545 draw_space(struct view *view, enum line_type type, int max, int spaces)
1547 static char space[] = " ";
1550 spaces = MIN(max, spaces);
1552 while (spaces > 0) {
1553 int len = MIN(spaces, sizeof(space) - 1);
1555 col += draw_chars(view, type, space, spaces, FALSE);
1563 draw_lineno(struct view *view, unsigned int lineno)
1566 int digits3 = view->digits < 3 ? 3 : view->digits;
1567 int max_number = MIN(digits3, STRING_SIZE(number));
1568 int max = view->width - view->col;
1571 if (max < max_number)
1574 lineno += view->offset + 1;
1575 if (lineno == 1 || (lineno % opt_num_interval) == 0) {
1576 static char fmt[] = "%1ld";
1578 if (view->digits <= 9)
1579 fmt[1] = '0' + digits3;
1581 if (!string_format(number, fmt, lineno))
1583 col = draw_chars(view, LINE_LINE_NUMBER, number, max_number, TRUE);
1585 col = draw_space(view, LINE_LINE_NUMBER, max_number, max_number);
1589 set_view_attr(view, LINE_DEFAULT);
1590 waddch(view->win, line_graphics[LINE_GRAPHIC_VLINE]);
1595 col += draw_space(view, LINE_DEFAULT, max - col, 1);
1598 return view->width - view->col <= 0;
1602 draw_text(struct view *view, enum line_type type, const char *string, bool trim)
1604 view->col += draw_chars(view, type, string, view->width - view->col, trim);
1605 return view->width - view->col <= 0;
1609 draw_graphic(struct view *view, enum line_type type, chtype graphic[], size_t size)
1611 int max = view->width - view->col;
1617 set_view_attr(view, type);
1618 /* Using waddch() instead of waddnstr() ensures that
1619 * they'll be rendered correctly for the cursor line. */
1620 for (i = 0; i < size; i++)
1621 waddch(view->win, graphic[i]);
1625 waddch(view->win, ' ');
1629 return view->width - view->col <= 0;
1633 draw_field(struct view *view, enum line_type type, char *text, int len, bool trim)
1635 int max = MIN(view->width - view->col, len);
1639 col = draw_chars(view, type, text, max - 1, trim);
1641 col = draw_space(view, type, max - 1, max - 1);
1643 view->col += col + draw_space(view, LINE_DEFAULT, max - col, max - col);
1644 return view->width - view->col <= 0;
1648 draw_date(struct view *view, struct tm *time)
1650 char buf[DATE_COLS];
1655 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, time);
1656 date = timelen ? buf : NULL;
1658 return draw_field(view, LINE_DATE, date, DATE_COLS, FALSE);
1662 draw_view_line(struct view *view, unsigned int lineno)
1665 bool selected = (view->offset + lineno == view->lineno);
1668 assert(view_is_displayed(view));
1670 if (view->offset + lineno >= view->lines)
1673 line = &view->line[view->offset + lineno];
1675 wmove(view->win, lineno, 0);
1677 view->curline = line;
1678 view->curtype = LINE_NONE;
1679 line->selected = FALSE;
1682 set_view_attr(view, LINE_CURSOR);
1683 line->selected = TRUE;
1684 view->ops->select(view, line);
1685 } else if (line->selected) {
1686 wclrtoeol(view->win);
1689 scrollok(view->win, FALSE);
1690 draw_ok = view->ops->draw(view, line, lineno);
1691 scrollok(view->win, TRUE);
1697 redraw_view_dirty(struct view *view)
1702 for (lineno = 0; lineno < view->height; lineno++) {
1703 struct line *line = &view->line[view->offset + lineno];
1709 if (!draw_view_line(view, lineno))
1715 redrawwin(view->win);
1717 wnoutrefresh(view->win);
1719 wrefresh(view->win);
1723 redraw_view_from(struct view *view, int lineno)
1725 assert(0 <= lineno && lineno < view->height);
1727 for (; lineno < view->height; lineno++) {
1728 if (!draw_view_line(view, lineno))
1732 redrawwin(view->win);
1734 wnoutrefresh(view->win);
1736 wrefresh(view->win);
1740 redraw_view(struct view *view)
1743 redraw_view_from(view, 0);
1748 update_view_title(struct view *view)
1750 char buf[SIZEOF_STR];
1751 char state[SIZEOF_STR];
1752 size_t bufpos = 0, statelen = 0;
1754 assert(view_is_displayed(view));
1756 if (view != VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) {
1757 unsigned int view_lines = view->offset + view->height;
1758 unsigned int lines = view->lines
1759 ? MIN(view_lines, view->lines) * 100 / view->lines
1762 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1769 time_t secs = time(NULL) - view->start_time;
1771 /* Three git seconds are a long time ... */
1773 string_format_from(state, &statelen, " %lds", secs);
1777 string_format_from(buf, &bufpos, "[%s]", view->name);
1778 if (*view->ref && bufpos < view->width) {
1779 size_t refsize = strlen(view->ref);
1780 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1782 if (minsize < view->width)
1783 refsize = view->width - minsize + 7;
1784 string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
1787 if (statelen && bufpos < view->width) {
1788 string_format_from(buf, &bufpos, " %s", state);
1791 if (view == display[current_view])
1792 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1794 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1796 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1797 wclrtoeol(view->title);
1798 wmove(view->title, 0, view->width - 1);
1801 wnoutrefresh(view->title);
1803 wrefresh(view->title);
1807 resize_display(void)
1810 struct view *base = display[0];
1811 struct view *view = display[1] ? display[1] : display[0];
1813 /* Setup window dimensions */
1815 getmaxyx(stdscr, base->height, base->width);
1817 /* Make room for the status window. */
1821 /* Horizontal split. */
1822 view->width = base->width;
1823 view->height = SCALE_SPLIT_VIEW(base->height);
1824 base->height -= view->height;
1826 /* Make room for the title bar. */
1830 /* Make room for the title bar. */
1835 foreach_displayed_view (view, i) {
1837 view->win = newwin(view->height, 0, offset, 0);
1839 die("Failed to create %s view", view->name);
1841 scrollok(view->win, TRUE);
1843 view->title = newwin(1, 0, offset + view->height, 0);
1845 die("Failed to create title window");
1848 wresize(view->win, view->height, view->width);
1849 mvwin(view->win, offset, 0);
1850 mvwin(view->title, offset + view->height, 0);
1853 offset += view->height + 1;
1858 redraw_display(void)
1863 foreach_displayed_view (view, i) {
1865 update_view_title(view);
1870 update_display_cursor(struct view *view)
1872 /* Move the cursor to the right-most column of the cursor line.
1874 * XXX: This could turn out to be a bit expensive, but it ensures that
1875 * the cursor does not jump around. */
1877 wmove(view->win, view->lineno - view->offset, view->width - 1);
1878 wrefresh(view->win);
1886 /* Scrolling backend */
1888 do_scroll_view(struct view *view, int lines)
1890 bool redraw_current_line = FALSE;
1892 /* The rendering expects the new offset. */
1893 view->offset += lines;
1895 assert(0 <= view->offset && view->offset < view->lines);
1898 /* Move current line into the view. */
1899 if (view->lineno < view->offset) {
1900 view->lineno = view->offset;
1901 redraw_current_line = TRUE;
1902 } else if (view->lineno >= view->offset + view->height) {
1903 view->lineno = view->offset + view->height - 1;
1904 redraw_current_line = TRUE;
1907 assert(view->offset <= view->lineno && view->lineno < view->lines);
1909 /* Redraw the whole screen if scrolling is pointless. */
1910 if (view->height < ABS(lines)) {
1914 int line = lines > 0 ? view->height - lines : 0;
1915 int end = line + ABS(lines);
1917 wscrl(view->win, lines);
1919 for (; line < end; line++) {
1920 if (!draw_view_line(view, line))
1924 if (redraw_current_line)
1925 draw_view_line(view, view->lineno - view->offset);
1928 redrawwin(view->win);
1929 wrefresh(view->win);
1933 /* Scroll frontend */
1935 scroll_view(struct view *view, enum request request)
1939 assert(view_is_displayed(view));
1942 case REQ_SCROLL_PAGE_DOWN:
1943 lines = view->height;
1944 case REQ_SCROLL_LINE_DOWN:
1945 if (view->offset + lines > view->lines)
1946 lines = view->lines - view->offset;
1948 if (lines == 0 || view->offset + view->height >= view->lines) {
1949 report("Cannot scroll beyond the last line");
1954 case REQ_SCROLL_PAGE_UP:
1955 lines = view->height;
1956 case REQ_SCROLL_LINE_UP:
1957 if (lines > view->offset)
1958 lines = view->offset;
1961 report("Cannot scroll beyond the first line");
1969 die("request %d not handled in switch", request);
1972 do_scroll_view(view, lines);
1977 move_view(struct view *view, enum request request)
1979 int scroll_steps = 0;
1983 case REQ_MOVE_FIRST_LINE:
1984 steps = -view->lineno;
1987 case REQ_MOVE_LAST_LINE:
1988 steps = view->lines - view->lineno - 1;
1991 case REQ_MOVE_PAGE_UP:
1992 steps = view->height > view->lineno
1993 ? -view->lineno : -view->height;
1996 case REQ_MOVE_PAGE_DOWN:
1997 steps = view->lineno + view->height >= view->lines
1998 ? view->lines - view->lineno - 1 : view->height;
2010 die("request %d not handled in switch", request);
2013 if (steps <= 0 && view->lineno == 0) {
2014 report("Cannot move beyond the first line");
2017 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
2018 report("Cannot move beyond the last line");
2022 /* Move the current line */
2023 view->lineno += steps;
2024 assert(0 <= view->lineno && view->lineno < view->lines);
2026 /* Check whether the view needs to be scrolled */
2027 if (view->lineno < view->offset ||
2028 view->lineno >= view->offset + view->height) {
2029 scroll_steps = steps;
2030 if (steps < 0 && -steps > view->offset) {
2031 scroll_steps = -view->offset;
2033 } else if (steps > 0) {
2034 if (view->lineno == view->lines - 1 &&
2035 view->lines > view->height) {
2036 scroll_steps = view->lines - view->offset - 1;
2037 if (scroll_steps >= view->height)
2038 scroll_steps -= view->height - 1;
2043 if (!view_is_displayed(view)) {
2044 view->offset += scroll_steps;
2045 assert(0 <= view->offset && view->offset < view->lines);
2046 view->ops->select(view, &view->line[view->lineno]);
2050 /* Repaint the old "current" line if we be scrolling */
2051 if (ABS(steps) < view->height)
2052 draw_view_line(view, view->lineno - steps - view->offset);
2055 do_scroll_view(view, scroll_steps);
2059 /* Draw the current line */
2060 draw_view_line(view, view->lineno - view->offset);
2062 redrawwin(view->win);
2063 wrefresh(view->win);
2072 static void search_view(struct view *view, enum request request);
2075 find_next_line(struct view *view, unsigned long lineno, struct line *line)
2077 assert(view_is_displayed(view));
2079 if (!view->ops->grep(view, line))
2082 if (lineno - view->offset >= view->height) {
2083 view->offset = lineno;
2084 view->lineno = lineno;
2088 unsigned long old_lineno = view->lineno - view->offset;
2090 view->lineno = lineno;
2091 draw_view_line(view, old_lineno);
2093 draw_view_line(view, view->lineno - view->offset);
2094 redrawwin(view->win);
2095 wrefresh(view->win);
2098 report("Line %ld matches '%s'", lineno + 1, view->grep);
2103 find_next(struct view *view, enum request request)
2105 unsigned long lineno = view->lineno;
2110 report("No previous search");
2112 search_view(view, request);
2122 case REQ_SEARCH_BACK:
2131 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
2132 lineno += direction;
2134 /* Note, lineno is unsigned long so will wrap around in which case it
2135 * will become bigger than view->lines. */
2136 for (; lineno < view->lines; lineno += direction) {
2137 struct line *line = &view->line[lineno];
2139 if (find_next_line(view, lineno, line))
2143 report("No match found for '%s'", view->grep);
2147 search_view(struct view *view, enum request request)
2152 regfree(view->regex);
2155 view->regex = calloc(1, sizeof(*view->regex));
2160 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
2161 if (regex_err != 0) {
2162 char buf[SIZEOF_STR] = "unknown error";
2164 regerror(regex_err, view->regex, buf, sizeof(buf));
2165 report("Search failed: %s", buf);
2169 string_copy(view->grep, opt_search);
2171 find_next(view, request);
2175 * Incremental updating
2179 end_update(struct view *view, bool force)
2183 while (!view->ops->read(view, NULL))
2186 set_nonblocking_input(FALSE);
2187 if (view->pipe == stdin)
2195 begin_update(struct view *view)
2198 string_copy(view->cmd, opt_cmd);
2200 /* When running random commands, initially show the
2201 * command in the title. However, it maybe later be
2202 * overwritten if a commit line is selected. */
2203 if (view == VIEW(REQ_VIEW_PAGER))
2204 string_copy(view->ref, view->cmd);
2208 } else if (view == VIEW(REQ_VIEW_TREE)) {
2209 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2210 char path[SIZEOF_STR];
2212 if (strcmp(view->vid, view->id))
2213 opt_path[0] = path[0] = 0;
2214 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
2217 if (!string_format(view->cmd, format, view->id, path))
2221 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2222 const char *id = view->id;
2224 if (!string_format(view->cmd, format, id, id, id, id, id))
2227 /* Put the current ref_* value to the view title ref
2228 * member. This is needed by the blob view. Most other
2229 * views sets it automatically after loading because the
2230 * first line is a commit line. */
2231 string_copy_rev(view->ref, view->id);
2234 /* Special case for the pager view. */
2236 view->pipe = opt_pipe;
2239 view->pipe = popen(view->cmd, "r");
2245 set_nonblocking_input(TRUE);
2250 string_copy_rev(view->vid, view->id);
2255 for (i = 0; i < view->lines; i++)
2256 if (view->line[i].data)
2257 free(view->line[i].data);
2263 view->start_time = time(NULL);
2268 #define ITEM_CHUNK_SIZE 256
2270 realloc_items(void *mem, size_t *size, size_t new_size, size_t item_size)
2272 size_t num_chunks = *size / ITEM_CHUNK_SIZE;
2273 size_t num_chunks_new = (new_size + ITEM_CHUNK_SIZE - 1) / ITEM_CHUNK_SIZE;
2275 if (mem == NULL || num_chunks != num_chunks_new) {
2276 *size = num_chunks_new * ITEM_CHUNK_SIZE;
2277 mem = realloc(mem, *size * item_size);
2283 static struct line *
2284 realloc_lines(struct view *view, size_t line_size)
2286 size_t alloc = view->line_alloc;
2287 struct line *tmp = realloc_items(view->line, &alloc, line_size,
2288 sizeof(*view->line));
2294 view->line_alloc = alloc;
2295 view->line_size = line_size;
2300 update_view(struct view *view)
2302 char in_buffer[BUFSIZ];
2303 char out_buffer[BUFSIZ * 2];
2305 /* The number of lines to read. If too low it will cause too much
2306 * redrawing (and possible flickering), if too high responsiveness
2308 unsigned long lines = view->height;
2309 int redraw_from = -1;
2314 /* Only redraw if lines are visible. */
2315 if (view->offset + view->height >= view->lines)
2316 redraw_from = view->lines - view->offset;
2318 /* FIXME: This is probably not perfect for backgrounded views. */
2319 if (!realloc_lines(view, view->lines + lines))
2322 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
2323 size_t linelen = strlen(line);
2326 line[linelen - 1] = 0;
2328 if (opt_iconv != ICONV_NONE) {
2329 ICONV_CONST char *inbuf = line;
2330 size_t inlen = linelen;
2332 char *outbuf = out_buffer;
2333 size_t outlen = sizeof(out_buffer);
2337 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
2338 if (ret != (size_t) -1) {
2340 linelen = strlen(out_buffer);
2344 if (!view->ops->read(view, line))
2354 lines = view->lines;
2355 for (digits = 0; lines; digits++)
2358 /* Keep the displayed view in sync with line number scaling. */
2359 if (digits != view->digits) {
2360 view->digits = digits;
2365 if (!view_is_displayed(view))
2368 if (view == VIEW(REQ_VIEW_TREE)) {
2369 /* Clear the view and redraw everything since the tree sorting
2370 * might have rearranged things. */
2373 } else if (redraw_from >= 0) {
2374 /* If this is an incremental update, redraw the previous line
2375 * since for commits some members could have changed when
2376 * loading the main view. */
2377 if (redraw_from > 0)
2380 /* Since revision graph visualization requires knowledge
2381 * about the parent commit, it causes a further one-off
2382 * needed to be redrawn for incremental updates. */
2383 if (redraw_from > 0 && opt_rev_graph)
2386 /* Incrementally draw avoids flickering. */
2387 redraw_view_from(view, redraw_from);
2390 if (view == VIEW(REQ_VIEW_BLAME))
2391 redraw_view_dirty(view);
2393 /* Update the title _after_ the redraw so that if the redraw picks up a
2394 * commit reference in view->ref it'll be available here. */
2395 update_view_title(view);
2398 if (ferror(view->pipe) && errno != 0) {
2399 report("Failed to read: %s", strerror(errno));
2400 end_update(view, TRUE);
2402 } else if (feof(view->pipe)) {
2404 end_update(view, FALSE);
2410 report("Allocation failure");
2411 end_update(view, TRUE);
2415 static struct line *
2416 add_line_data(struct view *view, void *data, enum line_type type)
2418 struct line *line = &view->line[view->lines++];
2420 memset(line, 0, sizeof(*line));
2427 static struct line *
2428 add_line_text(struct view *view, char *data, enum line_type type)
2431 data = strdup(data);
2433 return data ? add_line_data(view, data, type) : NULL;
2442 OPEN_DEFAULT = 0, /* Use default view switching. */
2443 OPEN_SPLIT = 1, /* Split current view. */
2444 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2445 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2446 OPEN_NOMAXIMIZE = 8 /* Do not maximize the current view. */
2450 open_view(struct view *prev, enum request request, enum open_flags flags)
2452 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2453 bool split = !!(flags & OPEN_SPLIT);
2454 bool reload = !!(flags & OPEN_RELOAD);
2455 bool nomaximize = !!(flags & OPEN_NOMAXIMIZE);
2456 struct view *view = VIEW(request);
2457 int nviews = displayed_views();
2458 struct view *base_view = display[0];
2460 if (view == prev && nviews == 1 && !reload) {
2461 report("Already in %s view", view->name);
2465 if (view->git_dir && !opt_git_dir[0]) {
2466 report("The %s view is disabled in pager view", view->name);
2474 } else if (!nomaximize) {
2475 /* Maximize the current view. */
2476 memset(display, 0, sizeof(display));
2478 display[current_view] = view;
2481 /* Resize the view when switching between split- and full-screen,
2482 * or when switching between two different full-screen views. */
2483 if (nviews != displayed_views() ||
2484 (nviews == 1 && base_view != display[0]))
2488 end_update(view, TRUE);
2490 if (view->ops->open) {
2491 if (!view->ops->open(view)) {
2492 report("Failed to load %s view", view->name);
2496 } else if ((reload || strcmp(view->vid, view->id)) &&
2497 !begin_update(view)) {
2498 report("Failed to load %s view", view->name);
2502 if (split && prev->lineno - prev->offset >= prev->height) {
2503 /* Take the title line into account. */
2504 int lines = prev->lineno - prev->offset - prev->height + 1;
2506 /* Scroll the view that was split if the current line is
2507 * outside the new limited view. */
2508 do_scroll_view(prev, lines);
2511 if (prev && view != prev) {
2512 if (split && !backgrounded) {
2513 /* "Blur" the previous view. */
2514 update_view_title(prev);
2517 view->parent = prev;
2520 if (view->pipe && view->lines == 0) {
2521 /* Clear the old view and let the incremental updating refill
2530 /* If the view is backgrounded the above calls to report()
2531 * won't redraw the view title. */
2533 update_view_title(view);
2537 run_confirm(const char *cmd, const char *prompt)
2539 if (prompt_yesno(prompt)) {
2545 open_external_viewer(const char *cmd)
2547 def_prog_mode(); /* save current tty modes */
2548 endwin(); /* restore original tty modes */
2550 fprintf(stderr, "Press Enter to continue");
2557 open_mergetool(const char *file)
2559 char cmd[SIZEOF_STR];
2560 char file_sq[SIZEOF_STR];
2562 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2563 string_format(cmd, "git mergetool %s", file_sq)) {
2564 open_external_viewer(cmd);
2569 open_editor(bool from_root, const char *file)
2571 char cmd[SIZEOF_STR];
2572 char file_sq[SIZEOF_STR];
2574 char *prefix = from_root ? opt_cdup : "";
2576 editor = getenv("GIT_EDITOR");
2577 if (!editor && *opt_editor)
2578 editor = opt_editor;
2580 editor = getenv("VISUAL");
2582 editor = getenv("EDITOR");
2586 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2587 string_format(cmd, "%s %s%s", editor, prefix, file_sq)) {
2588 open_external_viewer(cmd);
2593 open_run_request(enum request request)
2595 struct run_request *req = get_run_request(request);
2596 char buf[SIZEOF_STR * 2];
2601 report("Unknown run request");
2609 char *next = strstr(cmd, "%(");
2610 int len = next - cmd;
2617 } else if (!strncmp(next, "%(head)", 7)) {
2620 } else if (!strncmp(next, "%(commit)", 9)) {
2623 } else if (!strncmp(next, "%(blob)", 7)) {
2627 report("Unknown replacement in run request: `%s`", req->cmd);
2631 if (!string_format_from(buf, &bufpos, "%.*s%s", len, cmd, value))
2635 next = strchr(next, ')') + 1;
2639 open_external_viewer(buf);
2643 * User request switch noodle
2647 view_driver(struct view *view, enum request request)
2651 if (request == REQ_NONE) {
2656 if (request > REQ_NONE) {
2657 open_run_request(request);
2658 /* FIXME: When all views can refresh always do this. */
2659 if (view == VIEW(REQ_VIEW_STATUS) ||
2660 view == VIEW(REQ_VIEW_STAGE))
2661 request = REQ_REFRESH;
2666 if (view && view->lines) {
2667 request = view->ops->request(view, request, &view->line[view->lineno]);
2668 if (request == REQ_NONE)
2675 case REQ_MOVE_PAGE_UP:
2676 case REQ_MOVE_PAGE_DOWN:
2677 case REQ_MOVE_FIRST_LINE:
2678 case REQ_MOVE_LAST_LINE:
2679 move_view(view, request);
2682 case REQ_SCROLL_LINE_DOWN:
2683 case REQ_SCROLL_LINE_UP:
2684 case REQ_SCROLL_PAGE_DOWN:
2685 case REQ_SCROLL_PAGE_UP:
2686 scroll_view(view, request);
2689 case REQ_VIEW_BLAME:
2691 report("No file chosen, press %s to open tree view",
2692 get_key(REQ_VIEW_TREE));
2695 open_view(view, request, OPEN_DEFAULT);
2700 report("No file chosen, press %s to open tree view",
2701 get_key(REQ_VIEW_TREE));
2704 open_view(view, request, OPEN_DEFAULT);
2707 case REQ_VIEW_PAGER:
2708 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2709 report("No pager content, press %s to run command from prompt",
2710 get_key(REQ_PROMPT));
2713 open_view(view, request, OPEN_DEFAULT);
2716 case REQ_VIEW_STAGE:
2717 if (!VIEW(REQ_VIEW_STAGE)->lines) {
2718 report("No stage content, press %s to open the status view and choose file",
2719 get_key(REQ_VIEW_STATUS));
2722 open_view(view, request, OPEN_DEFAULT);
2725 case REQ_VIEW_STATUS:
2726 if (opt_is_inside_work_tree == FALSE) {
2727 report("The status view requires a working tree");
2730 open_view(view, request, OPEN_DEFAULT);
2738 open_view(view, request, OPEN_DEFAULT);
2743 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2745 if ((view == VIEW(REQ_VIEW_DIFF) &&
2746 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2747 (view == VIEW(REQ_VIEW_DIFF) &&
2748 view->parent == VIEW(REQ_VIEW_BLAME)) ||
2749 (view == VIEW(REQ_VIEW_STAGE) &&
2750 view->parent == VIEW(REQ_VIEW_STATUS)) ||
2751 (view == VIEW(REQ_VIEW_BLOB) &&
2752 view->parent == VIEW(REQ_VIEW_TREE))) {
2755 view = view->parent;
2756 line = view->lineno;
2757 move_view(view, request);
2758 if (view_is_displayed(view))
2759 update_view_title(view);
2760 if (line != view->lineno)
2761 view->ops->request(view, REQ_ENTER,
2762 &view->line[view->lineno]);
2765 move_view(view, request);
2771 int nviews = displayed_views();
2772 int next_view = (current_view + 1) % nviews;
2774 if (next_view == current_view) {
2775 report("Only one view is displayed");
2779 current_view = next_view;
2780 /* Blur out the title of the previous view. */
2781 update_view_title(view);
2786 report("Refreshing is not yet supported for the %s view", view->name);
2790 if (displayed_views() == 2)
2791 open_view(view, VIEW_REQ(view), OPEN_DEFAULT);
2794 case REQ_TOGGLE_LINENO:
2795 opt_line_number = !opt_line_number;
2799 case REQ_TOGGLE_DATE:
2800 opt_date = !opt_date;
2804 case REQ_TOGGLE_AUTHOR:
2805 opt_author = !opt_author;
2809 case REQ_TOGGLE_REV_GRAPH:
2810 opt_rev_graph = !opt_rev_graph;
2814 case REQ_TOGGLE_REFS:
2815 opt_show_refs = !opt_show_refs;
2820 case REQ_SEARCH_BACK:
2821 search_view(view, request);
2826 find_next(view, request);
2829 case REQ_STOP_LOADING:
2830 for (i = 0; i < ARRAY_SIZE(views); i++) {
2833 report("Stopped loading the %s view", view->name),
2834 end_update(view, TRUE);
2838 case REQ_SHOW_VERSION:
2839 report("tig-%s (built %s)", TIG_VERSION, __DATE__);
2842 case REQ_SCREEN_RESIZE:
2845 case REQ_SCREEN_REDRAW:
2850 report("Nothing to edit");
2854 report("Nothing to enter");
2857 case REQ_VIEW_CLOSE:
2858 /* XXX: Mark closed views by letting view->parent point to the
2859 * view itself. Parents to closed view should never be
2862 view->parent->parent != view->parent) {
2863 memset(display, 0, sizeof(display));
2865 display[current_view] = view->parent;
2866 view->parent = view;
2876 /* An unknown key will show most commonly used commands. */
2877 report("Unknown key, press 'h' for help");
2890 pager_draw(struct view *view, struct line *line, unsigned int lineno)
2892 char *text = line->data;
2894 if (opt_line_number && draw_lineno(view, lineno))
2897 draw_text(view, line->type, text, TRUE);
2902 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2904 char refbuf[SIZEOF_STR];
2908 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2911 pipe = popen(refbuf, "r");
2915 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2916 ref = chomp_string(ref);
2922 /* This is the only fatal call, since it can "corrupt" the buffer. */
2923 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2930 add_pager_refs(struct view *view, struct line *line)
2932 char buf[SIZEOF_STR];
2933 char *commit_id = (char *)line->data + STRING_SIZE("commit ");
2935 size_t bufpos = 0, refpos = 0;
2936 const char *sep = "Refs: ";
2937 bool is_tag = FALSE;
2939 assert(line->type == LINE_COMMIT);
2941 refs = get_refs(commit_id);
2943 if (view == VIEW(REQ_VIEW_DIFF))
2944 goto try_add_describe_ref;
2949 struct ref *ref = refs[refpos];
2950 char *fmt = ref->tag ? "%s[%s]" :
2951 ref->remote ? "%s<%s>" : "%s%s";
2953 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2958 } while (refs[refpos++]->next);
2960 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2961 try_add_describe_ref:
2962 /* Add <tag>-g<commit_id> "fake" reference. */
2963 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2970 if (!realloc_lines(view, view->line_size + 1))
2973 add_line_text(view, buf, LINE_PP_REFS);
2977 pager_read(struct view *view, char *data)
2984 line = add_line_text(view, data, get_line_type(data));
2988 if (line->type == LINE_COMMIT &&
2989 (view == VIEW(REQ_VIEW_DIFF) ||
2990 view == VIEW(REQ_VIEW_LOG)))
2991 add_pager_refs(view, line);
2997 pager_request(struct view *view, enum request request, struct line *line)
3001 if (request != REQ_ENTER)
3004 if (line->type == LINE_COMMIT &&
3005 (view == VIEW(REQ_VIEW_LOG) ||
3006 view == VIEW(REQ_VIEW_PAGER))) {
3007 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
3011 /* Always scroll the view even if it was split. That way
3012 * you can use Enter to scroll through the log view and
3013 * split open each commit diff. */
3014 scroll_view(view, REQ_SCROLL_LINE_DOWN);
3016 /* FIXME: A minor workaround. Scrolling the view will call report("")
3017 * but if we are scrolling a non-current view this won't properly
3018 * update the view title. */
3020 update_view_title(view);
3026 pager_grep(struct view *view, struct line *line)
3029 char *text = line->data;
3034 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
3041 pager_select(struct view *view, struct line *line)
3043 if (line->type == LINE_COMMIT) {
3044 char *text = (char *)line->data + STRING_SIZE("commit ");
3046 if (view != VIEW(REQ_VIEW_PAGER))
3047 string_copy_rev(view->ref, text);
3048 string_copy_rev(ref_commit, text);
3052 static struct view_ops pager_ops = {
3068 help_open(struct view *view)
3071 int lines = ARRAY_SIZE(req_info) + 2;
3074 if (view->lines > 0)
3077 for (i = 0; i < ARRAY_SIZE(req_info); i++)
3078 if (!req_info[i].request)
3081 lines += run_requests + 1;
3083 view->line = calloc(lines, sizeof(*view->line));
3087 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
3089 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
3092 if (req_info[i].request == REQ_NONE)
3095 if (!req_info[i].request) {
3096 add_line_text(view, "", LINE_DEFAULT);
3097 add_line_text(view, req_info[i].help, LINE_DEFAULT);
3101 key = get_key(req_info[i].request);
3103 key = "(no key defined)";
3105 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
3108 add_line_text(view, buf, LINE_DEFAULT);
3112 add_line_text(view, "", LINE_DEFAULT);
3113 add_line_text(view, "External commands:", LINE_DEFAULT);
3116 for (i = 0; i < run_requests; i++) {
3117 struct run_request *req = get_run_request(REQ_NONE + i + 1);
3123 key = get_key_name(req->key);
3125 key = "(no key defined)";
3127 if (!string_format(buf, " %-10s %-14s `%s`",
3128 keymap_table[req->keymap].name,
3132 add_line_text(view, buf, LINE_DEFAULT);
3138 static struct view_ops help_ops = {
3153 struct tree_stack_entry {
3154 struct tree_stack_entry *prev; /* Entry below this in the stack */
3155 unsigned long lineno; /* Line number to restore */
3156 char *name; /* Position of name in opt_path */
3159 /* The top of the path stack. */
3160 static struct tree_stack_entry *tree_stack = NULL;
3161 unsigned long tree_lineno = 0;
3164 pop_tree_stack_entry(void)
3166 struct tree_stack_entry *entry = tree_stack;
3168 tree_lineno = entry->lineno;
3170 tree_stack = entry->prev;
3175 push_tree_stack_entry(char *name, unsigned long lineno)
3177 struct tree_stack_entry *entry = calloc(1, sizeof(*entry));
3178 size_t pathlen = strlen(opt_path);
3183 entry->prev = tree_stack;
3184 entry->name = opt_path + pathlen;
3187 if (!string_format_from(opt_path, &pathlen, "%s/", name)) {
3188 pop_tree_stack_entry();
3192 /* Move the current line to the first tree entry. */
3194 entry->lineno = lineno;
3197 /* Parse output from git-ls-tree(1):
3199 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
3200 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
3201 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
3202 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
3205 #define SIZEOF_TREE_ATTR \
3206 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
3208 #define TREE_UP_FORMAT "040000 tree %s\t.."
3211 tree_compare_entry(enum line_type type1, char *name1,
3212 enum line_type type2, char *name2)
3214 if (type1 != type2) {
3215 if (type1 == LINE_TREE_DIR)
3220 return strcmp(name1, name2);
3224 tree_path(struct line *line)
3226 char *path = line->data;
3228 return path + SIZEOF_TREE_ATTR;
3232 tree_read(struct view *view, char *text)
3234 size_t textlen = text ? strlen(text) : 0;
3235 char buf[SIZEOF_STR];
3237 enum line_type type;
3238 bool first_read = view->lines == 0;
3242 if (textlen <= SIZEOF_TREE_ATTR)
3245 type = text[STRING_SIZE("100644 ")] == 't'
3246 ? LINE_TREE_DIR : LINE_TREE_FILE;
3249 /* Add path info line */
3250 if (!string_format(buf, "Directory path /%s", opt_path) ||
3251 !realloc_lines(view, view->line_size + 1) ||
3252 !add_line_text(view, buf, LINE_DEFAULT))
3255 /* Insert "link" to parent directory. */
3257 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
3258 !realloc_lines(view, view->line_size + 1) ||
3259 !add_line_text(view, buf, LINE_TREE_DIR))
3264 /* Strip the path part ... */
3266 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
3267 size_t striplen = strlen(opt_path);
3268 char *path = text + SIZEOF_TREE_ATTR;
3270 if (pathlen > striplen)
3271 memmove(path, path + striplen,
3272 pathlen - striplen + 1);
3275 /* Skip "Directory ..." and ".." line. */
3276 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
3277 struct line *line = &view->line[pos];
3278 char *path1 = tree_path(line);
3279 char *path2 = text + SIZEOF_TREE_ATTR;
3280 int cmp = tree_compare_entry(line->type, path1, type, path2);
3285 text = strdup(text);
3289 if (view->lines > pos)
3290 memmove(&view->line[pos + 1], &view->line[pos],
3291 (view->lines - pos) * sizeof(*line));
3293 line = &view->line[pos];
3300 if (!add_line_text(view, text, type))
3303 if (tree_lineno > view->lineno) {
3304 view->lineno = tree_lineno;
3312 tree_request(struct view *view, enum request request, struct line *line)
3314 enum open_flags flags;
3316 if (request == REQ_VIEW_BLAME) {
3317 char *filename = tree_path(line);
3319 if (line->type == LINE_TREE_DIR) {
3320 report("Cannot show blame for directory %s", opt_path);
3324 string_copy(opt_ref, view->vid);
3325 string_format(opt_file, "%s%s", opt_path, filename);
3328 if (request == REQ_TREE_PARENT) {
3331 request = REQ_ENTER;
3332 line = &view->line[1];
3334 /* quit view if at top of tree */
3335 return REQ_VIEW_CLOSE;
3338 if (request != REQ_ENTER)
3341 /* Cleanup the stack if the tree view is at a different tree. */
3342 while (!*opt_path && tree_stack)
3343 pop_tree_stack_entry();
3345 switch (line->type) {
3347 /* Depending on whether it is a subdir or parent (updir?) link
3348 * mangle the path buffer. */
3349 if (line == &view->line[1] && *opt_path) {
3350 pop_tree_stack_entry();
3353 char *basename = tree_path(line);
3355 push_tree_stack_entry(basename, view->lineno);
3358 /* Trees and subtrees share the same ID, so they are not not
3359 * unique like blobs. */
3360 flags = OPEN_RELOAD;
3361 request = REQ_VIEW_TREE;
3364 case LINE_TREE_FILE:
3365 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3366 request = REQ_VIEW_BLOB;
3373 open_view(view, request, flags);
3374 if (request == REQ_VIEW_TREE) {
3375 view->lineno = tree_lineno;
3382 tree_select(struct view *view, struct line *line)
3384 char *text = (char *)line->data + STRING_SIZE("100644 blob ");
3386 if (line->type == LINE_TREE_FILE) {
3387 string_copy_rev(ref_blob, text);
3389 } else if (line->type != LINE_TREE_DIR) {
3393 string_copy_rev(view->ref, text);
3396 static struct view_ops tree_ops = {
3407 blob_read(struct view *view, char *line)
3411 return add_line_text(view, line, LINE_DEFAULT) != NULL;
3414 static struct view_ops blob_ops = {
3427 * Loading the blame view is a two phase job:
3429 * 1. File content is read either using opt_file from the
3430 * filesystem or using git-cat-file.
3431 * 2. Then blame information is incrementally added by
3432 * reading output from git-blame.
3435 struct blame_commit {
3436 char id[SIZEOF_REV]; /* SHA1 ID. */
3437 char title[128]; /* First line of the commit message. */
3438 char author[75]; /* Author of the commit. */
3439 struct tm time; /* Date from the author ident. */
3440 char filename[128]; /* Name of file. */
3444 struct blame_commit *commit;
3445 unsigned int header:1;
3449 #define BLAME_CAT_FILE_CMD "git cat-file blob %s:%s"
3450 #define BLAME_INCREMENTAL_CMD "git blame --incremental %s %s"
3453 blame_open(struct view *view)
3455 char path[SIZEOF_STR];
3456 char ref[SIZEOF_STR] = "";
3458 if (sq_quote(path, 0, opt_file) >= sizeof(path))
3461 if (*opt_ref && sq_quote(ref, 0, opt_ref) >= sizeof(ref))
3465 if (!string_format(view->cmd, BLAME_CAT_FILE_CMD, ref, path))
3468 view->pipe = fopen(opt_file, "r");
3470 !string_format(view->cmd, BLAME_CAT_FILE_CMD, "HEAD", path))
3475 view->pipe = popen(view->cmd, "r");
3479 if (!string_format(view->cmd, BLAME_INCREMENTAL_CMD, ref, path))
3482 string_format(view->ref, "%s ...", opt_file);
3483 string_copy_rev(view->vid, opt_file);
3484 set_nonblocking_input(TRUE);
3489 for (i = 0; i < view->lines; i++)
3490 free(view->line[i].data);
3494 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3495 view->offset = view->lines = view->lineno = 0;
3497 view->start_time = time(NULL);
3502 static struct blame_commit *
3503 get_blame_commit(struct view *view, const char *id)
3507 for (i = 0; i < view->lines; i++) {
3508 struct blame *blame = view->line[i].data;
3513 if (!strncmp(blame->commit->id, id, SIZEOF_REV - 1))
3514 return blame->commit;
3518 struct blame_commit *commit = calloc(1, sizeof(*commit));
3521 string_ncopy(commit->id, id, SIZEOF_REV);
3527 parse_number(char **posref, size_t *number, size_t min, size_t max)
3529 char *pos = *posref;
3532 pos = strchr(pos + 1, ' ');
3533 if (!pos || !isdigit(pos[1]))
3535 *number = atoi(pos + 1);
3536 if (*number < min || *number > max)
3543 static struct blame_commit *
3544 parse_blame_commit(struct view *view, char *text, int *blamed)
3546 struct blame_commit *commit;
3547 struct blame *blame;
3548 char *pos = text + SIZEOF_REV - 1;
3552 if (strlen(text) <= SIZEOF_REV || *pos != ' ')
3555 if (!parse_number(&pos, &lineno, 1, view->lines) ||
3556 !parse_number(&pos, &group, 1, view->lines - lineno + 1))
3559 commit = get_blame_commit(view, text);
3565 struct line *line = &view->line[lineno + group - 1];
3568 blame->commit = commit;
3569 blame->header = !group;
3577 blame_read_file(struct view *view, char *line)
3582 if (view->lines > 0)
3583 pipe = popen(view->cmd, "r");
3584 else if (!view->parent)
3585 die("No blame exist for %s", view->vid);
3588 report("Failed to load blame data");
3597 size_t linelen = strlen(line);
3598 struct blame *blame = malloc(sizeof(*blame) + linelen);
3600 blame->commit = NULL;
3601 strncpy(blame->text, line, linelen);
3602 blame->text[linelen] = 0;
3603 return add_line_data(view, blame, LINE_BLAME_ID) != NULL;
3608 match_blame_header(const char *name, char **line)
3610 size_t namelen = strlen(name);
3611 bool matched = !strncmp(name, *line, namelen);
3620 blame_read(struct view *view, char *line)
3622 static struct blame_commit *commit = NULL;
3623 static int blamed = 0;
3624 static time_t author_time;
3627 return blame_read_file(view, line);
3633 string_format(view->ref, "%s", view->vid);
3634 if (view_is_displayed(view)) {
3635 update_view_title(view);
3636 redraw_view_from(view, 0);
3642 commit = parse_blame_commit(view, line, &blamed);
3643 string_format(view->ref, "%s %2d%%", view->vid,
3644 blamed * 100 / view->lines);
3646 } else if (match_blame_header("author ", &line)) {
3647 string_ncopy(commit->author, line, strlen(line));
3649 } else if (match_blame_header("author-time ", &line)) {
3650 author_time = (time_t) atol(line);
3652 } else if (match_blame_header("author-tz ", &line)) {
3655 tz = ('0' - line[1]) * 60 * 60 * 10;
3656 tz += ('0' - line[2]) * 60 * 60;
3657 tz += ('0' - line[3]) * 60;
3658 tz += ('0' - line[4]) * 60;
3664 gmtime_r(&author_time, &commit->time);
3666 } else if (match_blame_header("summary ", &line)) {
3667 string_ncopy(commit->title, line, strlen(line));
3669 } else if (match_blame_header("filename ", &line)) {
3670 string_ncopy(commit->filename, line, strlen(line));
3678 blame_draw(struct view *view, struct line *line, unsigned int lineno)
3680 struct blame *blame = line->data;
3681 struct tm *time = NULL;
3682 char *id = NULL, *author = NULL;
3684 if (blame->commit && *blame->commit->filename) {
3685 id = blame->commit->id;
3686 author = blame->commit->author;
3687 time = &blame->commit->time;
3690 if (opt_date && draw_date(view, time))
3694 draw_field(view, LINE_MAIN_AUTHOR, author, opt_author_cols, TRUE))
3697 if (draw_field(view, LINE_BLAME_ID, id, ID_COLS, FALSE))
3700 if (draw_lineno(view, lineno))
3703 draw_text(view, LINE_DEFAULT, blame->text, TRUE);
3708 blame_request(struct view *view, enum request request, struct line *line)
3710 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3711 struct blame *blame = line->data;
3715 if (!blame->commit) {
3716 report("No commit loaded yet");
3720 if (!strcmp(blame->commit->id, NULL_ID)) {
3721 char path[SIZEOF_STR];
3723 if (sq_quote(path, 0, view->vid) >= sizeof(path))
3725 string_format(opt_cmd, "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s 2>/dev/null", path);
3728 open_view(view, REQ_VIEW_DIFF, flags);
3739 blame_grep(struct view *view, struct line *line)
3741 struct blame *blame = line->data;
3742 struct blame_commit *commit = blame->commit;
3745 #define MATCH(text, on) \
3746 (on && *text && regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3749 char buf[DATE_COLS + 1];
3751 if (MATCH(commit->title, 1) ||
3752 MATCH(commit->author, opt_author) ||
3753 MATCH(commit->id, opt_date))
3756 if (strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time) &&
3761 return MATCH(blame->text, 1);
3767 blame_select(struct view *view, struct line *line)
3769 struct blame *blame = line->data;
3770 struct blame_commit *commit = blame->commit;
3775 if (!strcmp(commit->id, NULL_ID))
3776 string_ncopy(ref_commit, "HEAD", 4);
3778 string_copy_rev(ref_commit, commit->id);
3781 static struct view_ops blame_ops = {
3799 char rev[SIZEOF_REV];
3800 char name[SIZEOF_STR];
3804 char rev[SIZEOF_REV];
3805 char name[SIZEOF_STR];
3809 static char status_onbranch[SIZEOF_STR];
3810 static struct status stage_status;
3811 static enum line_type stage_line_type;
3812 static size_t stage_chunks;
3813 static int *stage_chunk;
3815 /* Get fields from the diff line:
3816 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
3819 status_get_diff(struct status *file, char *buf, size_t bufsize)
3821 char *old_mode = buf + 1;
3822 char *new_mode = buf + 8;
3823 char *old_rev = buf + 15;
3824 char *new_rev = buf + 56;
3825 char *status = buf + 97;
3828 old_mode[-1] != ':' ||
3829 new_mode[-1] != ' ' ||
3830 old_rev[-1] != ' ' ||
3831 new_rev[-1] != ' ' ||
3835 file->status = *status;
3837 string_copy_rev(file->old.rev, old_rev);
3838 string_copy_rev(file->new.rev, new_rev);
3840 file->old.mode = strtoul(old_mode, NULL, 8);
3841 file->new.mode = strtoul(new_mode, NULL, 8);
3843 file->old.name[0] = file->new.name[0] = 0;
3849 status_run(struct view *view, const char cmd[], char status, enum line_type type)
3851 struct status *file = NULL;
3852 struct status *unmerged = NULL;
3853 char buf[SIZEOF_STR * 4];
3857 pipe = popen(cmd, "r");
3861 add_line_data(view, NULL, type);
3863 while (!feof(pipe) && !ferror(pipe)) {
3867 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
3870 bufsize += readsize;
3872 /* Process while we have NUL chars. */
3873 while ((sep = memchr(buf, 0, bufsize))) {
3874 size_t sepsize = sep - buf + 1;
3877 if (!realloc_lines(view, view->line_size + 1))
3880 file = calloc(1, sizeof(*file));
3884 add_line_data(view, file, type);
3887 /* Parse diff info part. */
3889 file->status = status;
3891 string_copy(file->old.rev, NULL_ID);
3893 } else if (!file->status) {
3894 if (!status_get_diff(file, buf, sepsize))
3898 memmove(buf, sep + 1, bufsize);
3900 sep = memchr(buf, 0, bufsize);
3903 sepsize = sep - buf + 1;
3905 /* Collapse all 'M'odified entries that
3906 * follow a associated 'U'nmerged entry.
3908 if (file->status == 'U') {
3911 } else if (unmerged) {
3912 int collapse = !strcmp(buf, unmerged->new.name);
3923 /* Grab the old name for rename/copy. */
3924 if (!*file->old.name &&
3925 (file->status == 'R' || file->status == 'C')) {
3926 sepsize = sep - buf + 1;
3927 string_ncopy(file->old.name, buf, sepsize);
3929 memmove(buf, sep + 1, bufsize);
3931 sep = memchr(buf, 0, bufsize);
3934 sepsize = sep - buf + 1;
3937 /* git-ls-files just delivers a NUL separated
3938 * list of file names similar to the second half
3939 * of the git-diff-* output. */
3940 string_ncopy(file->new.name, buf, sepsize);
3941 if (!*file->old.name)
3942 string_copy(file->old.name, file->new.name);
3944 memmove(buf, sep + 1, bufsize);
3955 if (!view->line[view->lines - 1].data)
3956 add_line_data(view, NULL, LINE_STAT_NONE);
3962 /* Don't show unmerged entries in the staged section. */
3963 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached -M HEAD"
3964 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
3965 #define STATUS_LIST_OTHER_CMD \
3966 "git ls-files -z --others --exclude-per-directory=.gitignore"
3967 #define STATUS_LIST_NO_HEAD_CMD \
3968 "git ls-files -z --cached --exclude-per-directory=.gitignore"
3970 #define STATUS_DIFF_INDEX_SHOW_CMD \
3971 "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s %s 2>/dev/null"
3973 #define STATUS_DIFF_FILES_SHOW_CMD \
3974 "git diff-files --root --patch-with-stat -C -M -- %s %s 2>/dev/null"
3976 #define STATUS_DIFF_NO_HEAD_SHOW_CMD \
3977 "git diff --no-color --patch-with-stat /dev/null %s 2>/dev/null"
3979 /* First parse staged info using git-diff-index(1), then parse unstaged
3980 * info using git-diff-files(1), and finally untracked files using
3981 * git-ls-files(1). */
3983 status_open(struct view *view)
3985 struct stat statbuf;
3986 char exclude[SIZEOF_STR];
3987 char indexcmd[SIZEOF_STR] = STATUS_DIFF_INDEX_CMD;
3988 char othercmd[SIZEOF_STR] = STATUS_LIST_OTHER_CMD;
3989 unsigned long prev_lineno = view->lineno;
3990 char indexstatus = 0;
3993 for (i = 0; i < view->lines; i++)
3994 free(view->line[i].data);
3996 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3999 if (!realloc_lines(view, view->line_size + 7))
4002 add_line_data(view, NULL, LINE_STAT_HEAD);
4004 string_copy(status_onbranch, "Initial commit");
4005 else if (!*opt_head)
4006 string_copy(status_onbranch, "Not currently on any branch");
4007 else if (!string_format(status_onbranch, "On branch %s", opt_head))
4011 string_copy(indexcmd, STATUS_LIST_NO_HEAD_CMD);
4015 if (!string_format(exclude, "%s/info/exclude", opt_git_dir))
4018 if (stat(exclude, &statbuf) >= 0) {
4019 size_t cmdsize = strlen(othercmd);
4021 if (!string_format_from(othercmd, &cmdsize, " %s", "--exclude-from=") ||
4022 sq_quote(othercmd, cmdsize, exclude) >= sizeof(othercmd))
4025 cmdsize = strlen(indexcmd);
4027 (!string_format_from(indexcmd, &cmdsize, " %s", "--exclude-from=") ||
4028 sq_quote(indexcmd, cmdsize, exclude) >= sizeof(indexcmd)))
4032 system("git update-index -q --refresh >/dev/null 2>/dev/null");
4034 if (!status_run(view, indexcmd, indexstatus, LINE_STAT_STAGED) ||
4035 !status_run(view, STATUS_DIFF_FILES_CMD, 0, LINE_STAT_UNSTAGED) ||
4036 !status_run(view, othercmd, '?', LINE_STAT_UNTRACKED))
4039 /* If all went well restore the previous line number to stay in
4040 * the context or select a line with something that can be
4042 if (prev_lineno >= view->lines)
4043 prev_lineno = view->lines - 1;
4044 while (prev_lineno < view->lines && !view->line[prev_lineno].data)
4046 while (prev_lineno > 0 && !view->line[prev_lineno].data)
4049 /* If the above fails, always skip the "On branch" line. */
4050 if (prev_lineno < view->lines)
4051 view->lineno = prev_lineno;
4055 if (view->lineno < view->offset)
4056 view->offset = view->lineno;
4057 else if (view->offset + view->height <= view->lineno)
4058 view->offset = view->lineno - view->height + 1;
4064 status_draw(struct view *view, struct line *line, unsigned int lineno)
4066 struct status *status = line->data;
4067 enum line_type type;
4071 switch (line->type) {
4072 case LINE_STAT_STAGED:
4073 type = LINE_STAT_SECTION;
4074 text = "Changes to be committed:";
4077 case LINE_STAT_UNSTAGED:
4078 type = LINE_STAT_SECTION;
4079 text = "Changed but not updated:";
4082 case LINE_STAT_UNTRACKED:
4083 type = LINE_STAT_SECTION;
4084 text = "Untracked files:";
4087 case LINE_STAT_NONE:
4088 type = LINE_DEFAULT;
4089 text = " (no files)";
4092 case LINE_STAT_HEAD:
4093 type = LINE_STAT_HEAD;
4094 text = status_onbranch;
4101 static char buf[] = { '?', ' ', ' ', ' ', 0 };
4103 buf[0] = status->status;
4104 if (draw_text(view, line->type, buf, TRUE))
4106 type = LINE_DEFAULT;
4107 text = status->new.name;
4110 draw_text(view, type, text, TRUE);
4115 status_enter(struct view *view, struct line *line)
4117 struct status *status = line->data;
4118 char oldpath[SIZEOF_STR] = "";
4119 char newpath[SIZEOF_STR] = "";
4122 enum open_flags split;
4124 if (line->type == LINE_STAT_NONE ||
4125 (!status && line[1].type == LINE_STAT_NONE)) {
4126 report("No file to diff");
4131 if (sq_quote(oldpath, 0, status->old.name) >= sizeof(oldpath))
4133 /* Diffs for unmerged entries are empty when pasing the
4134 * new path, so leave it empty. */
4135 if (status->status != 'U' &&
4136 sq_quote(newpath, 0, status->new.name) >= sizeof(newpath))
4141 line->type != LINE_STAT_UNTRACKED &&
4142 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
4145 switch (line->type) {
4146 case LINE_STAT_STAGED:
4148 if (!string_format_from(opt_cmd, &cmdsize,
4149 STATUS_DIFF_NO_HEAD_SHOW_CMD,
4153 if (!string_format_from(opt_cmd, &cmdsize,
4154 STATUS_DIFF_INDEX_SHOW_CMD,
4160 info = "Staged changes to %s";
4162 info = "Staged changes";
4165 case LINE_STAT_UNSTAGED:
4166 if (!string_format_from(opt_cmd, &cmdsize,
4167 STATUS_DIFF_FILES_SHOW_CMD, oldpath, newpath))
4170 info = "Unstaged changes to %s";
4172 info = "Unstaged changes";
4175 case LINE_STAT_UNTRACKED:
4180 report("No file to show");
4184 opt_pipe = fopen(status->new.name, "r");
4185 info = "Untracked file %s";
4188 case LINE_STAT_HEAD:
4192 die("line type %d not handled in switch", line->type);
4195 split = view_is_displayed(view) ? OPEN_SPLIT : 0;
4196 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | split);
4197 if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) {
4199 stage_status = *status;
4201 memset(&stage_status, 0, sizeof(stage_status));
4204 stage_line_type = line->type;
4206 string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.new.name);
4213 status_exists(struct status *status, enum line_type type)
4215 struct view *view = VIEW(REQ_VIEW_STATUS);
4218 for (line = view->line; line < view->line + view->lines; line++) {
4219 struct status *pos = line->data;
4221 if (line->type == type && pos &&
4222 !strcmp(status->new.name, pos->new.name))
4231 status_update_prepare(enum line_type type)
4233 char cmd[SIZEOF_STR];
4237 type != LINE_STAT_UNTRACKED &&
4238 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4242 case LINE_STAT_STAGED:
4243 string_add(cmd, cmdsize, "git update-index -z --index-info");
4246 case LINE_STAT_UNSTAGED:
4247 case LINE_STAT_UNTRACKED:
4248 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
4252 die("line type %d not handled in switch", type);
4255 return popen(cmd, "w");
4259 status_update_write(FILE *pipe, struct status *status, enum line_type type)
4261 char buf[SIZEOF_STR];
4266 case LINE_STAT_STAGED:
4267 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
4270 status->old.name, 0))
4274 case LINE_STAT_UNSTAGED:
4275 case LINE_STAT_UNTRACKED:
4276 if (!string_format_from(buf, &bufsize, "%s%c", status->new.name, 0))
4281 die("line type %d not handled in switch", type);
4284 while (!ferror(pipe) && written < bufsize) {
4285 written += fwrite(buf + written, 1, bufsize - written, pipe);
4288 return written == bufsize;
4292 status_update_file(struct status *status, enum line_type type)
4294 FILE *pipe = status_update_prepare(type);
4300 result = status_update_write(pipe, status, type);
4306 status_update_files(struct view *view, struct line *line)
4308 FILE *pipe = status_update_prepare(line->type);
4310 struct line *pos = view->line + view->lines;
4317 for (pos = line; pos < view->line + view->lines && pos->data; pos++)
4320 for (file = 0, done = 0; result && file < files; line++, file++) {
4321 int almost_done = file * 100 / files;
4323 if (almost_done > done) {
4325 string_format(view->ref, "updating file %u of %u (%d%% done)",
4327 update_view_title(view);
4329 result = status_update_write(pipe, line->data, line->type);
4337 status_update(struct view *view)
4339 struct line *line = &view->line[view->lineno];
4341 assert(view->lines);
4344 /* This should work even for the "On branch" line. */
4345 if (line < view->line + view->lines && !line[1].data) {
4346 report("Nothing to update");
4350 if (!status_update_files(view, line + 1)) {
4351 report("Failed to update file status");
4355 } else if (!status_update_file(line->data, line->type)) {
4356 report("Failed to update file status");
4364 status_checkout(struct view *view)
4366 struct line *line = &view->line[view->lineno];
4368 assert(view->lines);
4370 if (!line->data || line->type != LINE_STAT_UNSTAGED) {
4371 /* This should work even for the "On branch" line. */
4372 if (line < view->line + view->lines && !line[1].data) {
4373 report("Nothing to checkout");
4374 } else if (line->type == LINE_STAT_UNTRACKED) {
4375 report("Cannot checkout untracked files");
4376 } else if (line->type == LINE_STAT_STAGED) {
4377 report("Cannot checkout staged files");
4379 report("Cannot checkout multiple files");
4384 struct status *status = line->data;
4385 char cmd[SIZEOF_STR];
4386 char file_sq[SIZEOF_STR];
4388 if (sq_quote(file_sq, 0, status->old.name) < sizeof(file_sq) &&
4389 string_format(cmd, "git checkout %s%s", opt_cdup, file_sq)) {
4390 run_confirm(cmd, "Are you sure you want to overwrite any changes?");
4398 status_request(struct view *view, enum request request, struct line *line)
4400 struct status *status = line->data;
4403 case REQ_STATUS_UPDATE:
4404 if (!status_update(view))
4408 case REQ_STATUS_CHECKOUT:
4409 if (!status_checkout(view))
4413 case REQ_STATUS_MERGE:
4414 if (!status || status->status != 'U') {
4415 report("Merging only possible for files with unmerged status ('U').");
4418 open_mergetool(status->new.name);
4425 open_editor(status->status != '?', status->new.name);
4428 case REQ_VIEW_BLAME:
4430 string_copy(opt_file, status->new.name);
4436 /* After returning the status view has been split to
4437 * show the stage view. No further reloading is
4439 status_enter(view, line);
4443 /* Simply reload the view. */
4450 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
4456 status_select(struct view *view, struct line *line)
4458 struct status *status = line->data;
4459 char file[SIZEOF_STR] = "all files";
4463 if (status && !string_format(file, "'%s'", status->new.name))
4466 if (!status && line[1].type == LINE_STAT_NONE)
4469 switch (line->type) {
4470 case LINE_STAT_STAGED:
4471 text = "Press %s to unstage %s for commit";
4474 case LINE_STAT_UNSTAGED:
4475 text = "Press %s to stage %s for commit";
4478 case LINE_STAT_UNTRACKED:
4479 text = "Press %s to stage %s for addition";
4482 case LINE_STAT_HEAD:
4483 case LINE_STAT_NONE:
4484 text = "Nothing to update";
4488 die("line type %d not handled in switch", line->type);
4491 if (status && status->status == 'U') {
4492 text = "Press %s to resolve conflict in %s";
4493 key = get_key(REQ_STATUS_MERGE);
4496 key = get_key(REQ_STATUS_UPDATE);
4499 string_format(view->ref, text, key, file);
4503 status_grep(struct view *view, struct line *line)
4505 struct status *status = line->data;
4506 enum { S_STATUS, S_NAME, S_END } state;
4513 for (state = S_STATUS; state < S_END; state++) {
4517 case S_NAME: text = status->new.name; break;
4519 buf[0] = status->status;
4527 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4534 static struct view_ops status_ops = {
4546 stage_diff_line(FILE *pipe, struct line *line)
4548 char *buf = line->data;
4549 size_t bufsize = strlen(buf);
4552 while (!ferror(pipe) && written < bufsize) {
4553 written += fwrite(buf + written, 1, bufsize - written, pipe);
4558 return written == bufsize;
4562 stage_diff_write(FILE *pipe, struct line *line, struct line *end)
4564 while (line < end) {
4565 if (!stage_diff_line(pipe, line++))
4567 if (line->type == LINE_DIFF_CHUNK ||
4568 line->type == LINE_DIFF_HEADER)
4575 static struct line *
4576 stage_diff_find(struct view *view, struct line *line, enum line_type type)
4578 for (; view->line < line; line--)
4579 if (line->type == type)
4586 stage_update_chunk(struct view *view, struct line *chunk)
4588 char cmd[SIZEOF_STR];
4590 struct line *diff_hdr;
4593 diff_hdr = stage_diff_find(view, chunk, LINE_DIFF_HEADER);
4598 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4601 if (!string_format_from(cmd, &cmdsize,
4602 "git apply --whitespace=nowarn --cached %s - && "
4603 "git update-index -q --unmerged --refresh 2>/dev/null",
4604 stage_line_type == LINE_STAT_STAGED ? "-R" : ""))
4607 pipe = popen(cmd, "w");
4611 if (!stage_diff_write(pipe, diff_hdr, chunk) ||
4612 !stage_diff_write(pipe, chunk, view->line + view->lines))
4617 return chunk ? TRUE : FALSE;
4621 stage_update(struct view *view, struct line *line)
4623 struct line *chunk = NULL;
4625 if (!opt_no_head && stage_line_type != LINE_STAT_UNTRACKED)
4626 chunk = stage_diff_find(view, line, LINE_DIFF_CHUNK);
4629 if (!stage_update_chunk(view, chunk)) {
4630 report("Failed to apply chunk");
4634 } else if (!stage_status.status) {
4635 view = VIEW(REQ_VIEW_STATUS);
4637 for (line = view->line; line < view->line + view->lines; line++)
4638 if (line->type == stage_line_type)
4641 if (!status_update_files(view, line + 1)) {
4642 report("Failed to update files");
4646 } else if (!status_update_file(&stage_status, stage_line_type)) {
4647 report("Failed to update file");
4655 stage_next(struct view *view, struct line *line)
4659 if (!stage_chunks) {
4660 static size_t alloc = 0;
4663 for (line = view->line; line < view->line + view->lines; line++) {
4664 if (line->type != LINE_DIFF_CHUNK)
4667 tmp = realloc_items(stage_chunk, &alloc,
4668 stage_chunks, sizeof(*tmp));
4670 report("Allocation failure");
4675 stage_chunk[stage_chunks++] = line - view->line;
4679 for (i = 0; i < stage_chunks; i++) {
4680 if (stage_chunk[i] > view->lineno) {
4681 do_scroll_view(view, stage_chunk[i] - view->lineno);
4682 report("Chunk %d of %d", i + 1, stage_chunks);
4687 report("No next chunk found");
4691 stage_request(struct view *view, enum request request, struct line *line)
4694 case REQ_STATUS_UPDATE:
4695 if (!stage_update(view, line))
4699 case REQ_STAGE_NEXT:
4700 if (stage_line_type == LINE_STAT_UNTRACKED) {
4701 report("File is untracked; press %s to add",
4702 get_key(REQ_STATUS_UPDATE));
4705 stage_next(view, line);
4709 if (!stage_status.new.name[0])
4712 open_editor(stage_status.status != '?', stage_status.new.name);
4716 /* Reload everything ... */
4719 case REQ_VIEW_BLAME:
4720 if (stage_status.new.name[0]) {
4721 string_copy(opt_file, stage_status.new.name);
4727 return pager_request(view, request, line);
4733 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD | OPEN_NOMAXIMIZE);
4735 /* Check whether the staged entry still exists, and close the
4736 * stage view if it doesn't. */
4737 if (!status_exists(&stage_status, stage_line_type))
4738 return REQ_VIEW_CLOSE;
4740 if (stage_line_type == LINE_STAT_UNTRACKED)
4741 opt_pipe = fopen(stage_status.new.name, "r");
4743 string_copy(opt_cmd, view->cmd);
4744 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | OPEN_NOMAXIMIZE);
4749 static struct view_ops stage_ops = {
4765 char id[SIZEOF_REV]; /* SHA1 ID. */
4766 char title[128]; /* First line of the commit message. */
4767 char author[75]; /* Author of the commit. */
4768 struct tm time; /* Date from the author ident. */
4769 struct ref **refs; /* Repository references. */
4770 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
4771 size_t graph_size; /* The width of the graph array. */
4772 bool has_parents; /* Rewritten --parents seen. */
4775 /* Size of rev graph with no "padding" columns */
4776 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
4779 struct rev_graph *prev, *next, *parents;
4780 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
4782 struct commit *commit;
4784 unsigned int boundary:1;
4787 /* Parents of the commit being visualized. */
4788 static struct rev_graph graph_parents[4];
4790 /* The current stack of revisions on the graph. */
4791 static struct rev_graph graph_stacks[4] = {
4792 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
4793 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
4794 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
4795 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
4799 graph_parent_is_merge(struct rev_graph *graph)
4801 return graph->parents->size > 1;
4805 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
4807 struct commit *commit = graph->commit;
4809 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
4810 commit->graph[commit->graph_size++] = symbol;
4814 clear_rev_graph(struct rev_graph *graph)
4816 graph->boundary = 0;
4817 graph->size = graph->pos = 0;
4818 graph->commit = NULL;
4819 memset(graph->parents, 0, sizeof(*graph->parents));
4823 done_rev_graph(struct rev_graph *graph)
4825 if (graph_parent_is_merge(graph) &&
4826 graph->pos < graph->size - 1 &&
4827 graph->next->size == graph->size + graph->parents->size - 1) {
4828 size_t i = graph->pos + graph->parents->size - 1;
4830 graph->commit->graph_size = i * 2;
4831 while (i < graph->next->size - 1) {
4832 append_to_rev_graph(graph, ' ');
4833 append_to_rev_graph(graph, '\\');
4838 clear_rev_graph(graph);
4842 push_rev_graph(struct rev_graph *graph, char *parent)
4846 /* "Collapse" duplicate parents lines.
4848 * FIXME: This needs to also update update the drawn graph but
4849 * for now it just serves as a method for pruning graph lines. */
4850 for (i = 0; i < graph->size; i++)
4851 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
4854 if (graph->size < SIZEOF_REVITEMS) {
4855 string_copy_rev(graph->rev[graph->size++], parent);
4860 get_rev_graph_symbol(struct rev_graph *graph)
4864 if (graph->boundary)
4865 symbol = REVGRAPH_BOUND;
4866 else if (graph->parents->size == 0)
4867 symbol = REVGRAPH_INIT;
4868 else if (graph_parent_is_merge(graph))
4869 symbol = REVGRAPH_MERGE;
4870 else if (graph->pos >= graph->size)
4871 symbol = REVGRAPH_BRANCH;
4873 symbol = REVGRAPH_COMMIT;
4879 draw_rev_graph(struct rev_graph *graph)
4882 chtype separator, line;
4884 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
4885 static struct rev_filler fillers[] = {
4891 chtype symbol = get_rev_graph_symbol(graph);
4892 struct rev_filler *filler;
4895 if (opt_line_graphics)
4896 fillers[DEFAULT].line = line_graphics[LINE_GRAPHIC_VLINE];
4898 filler = &fillers[DEFAULT];
4900 for (i = 0; i < graph->pos; i++) {
4901 append_to_rev_graph(graph, filler->line);
4902 if (graph_parent_is_merge(graph->prev) &&
4903 graph->prev->pos == i)
4904 filler = &fillers[RSHARP];
4906 append_to_rev_graph(graph, filler->separator);
4909 /* Place the symbol for this revision. */
4910 append_to_rev_graph(graph, symbol);
4912 if (graph->prev->size > graph->size)
4913 filler = &fillers[RDIAG];
4915 filler = &fillers[DEFAULT];
4919 for (; i < graph->size; i++) {
4920 append_to_rev_graph(graph, filler->separator);
4921 append_to_rev_graph(graph, filler->line);
4922 if (graph_parent_is_merge(graph->prev) &&
4923 i < graph->prev->pos + graph->parents->size)
4924 filler = &fillers[RSHARP];
4925 if (graph->prev->size > graph->size)
4926 filler = &fillers[LDIAG];
4929 if (graph->prev->size > graph->size) {
4930 append_to_rev_graph(graph, filler->separator);
4931 if (filler->line != ' ')
4932 append_to_rev_graph(graph, filler->line);
4936 /* Prepare the next rev graph */
4938 prepare_rev_graph(struct rev_graph *graph)
4942 /* First, traverse all lines of revisions up to the active one. */
4943 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
4944 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
4947 push_rev_graph(graph->next, graph->rev[graph->pos]);
4950 /* Interleave the new revision parent(s). */
4951 for (i = 0; !graph->boundary && i < graph->parents->size; i++)
4952 push_rev_graph(graph->next, graph->parents->rev[i]);
4954 /* Lastly, put any remaining revisions. */
4955 for (i = graph->pos + 1; i < graph->size; i++)
4956 push_rev_graph(graph->next, graph->rev[i]);
4960 update_rev_graph(struct rev_graph *graph)
4962 /* If this is the finalizing update ... */
4964 prepare_rev_graph(graph);
4966 /* Graph visualization needs a one rev look-ahead,
4967 * so the first update doesn't visualize anything. */
4968 if (!graph->prev->commit)
4971 draw_rev_graph(graph->prev);
4972 done_rev_graph(graph->prev->prev);
4981 main_draw(struct view *view, struct line *line, unsigned int lineno)
4983 struct commit *commit = line->data;
4985 if (!*commit->author)
4988 if (opt_date && draw_date(view, &commit->time))
4992 draw_field(view, LINE_MAIN_AUTHOR, commit->author, opt_author_cols, TRUE))
4995 if (opt_rev_graph && commit->graph_size &&
4996 draw_graphic(view, LINE_MAIN_REVGRAPH, commit->graph, commit->graph_size))
4999 if (opt_show_refs && commit->refs) {
5003 enum line_type type;
5005 if (commit->refs[i]->head)
5006 type = LINE_MAIN_HEAD;
5007 else if (commit->refs[i]->ltag)
5008 type = LINE_MAIN_LOCAL_TAG;
5009 else if (commit->refs[i]->tag)
5010 type = LINE_MAIN_TAG;
5011 else if (commit->refs[i]->tracked)
5012 type = LINE_MAIN_TRACKED;
5013 else if (commit->refs[i]->remote)
5014 type = LINE_MAIN_REMOTE;
5016 type = LINE_MAIN_REF;
5018 if (draw_text(view, type, "[", TRUE) ||
5019 draw_text(view, type, commit->refs[i]->name, TRUE) ||
5020 draw_text(view, type, "]", TRUE))
5023 if (draw_text(view, LINE_DEFAULT, " ", TRUE))
5025 } while (commit->refs[i++]->next);
5028 draw_text(view, LINE_DEFAULT, commit->title, TRUE);
5032 /* Reads git log --pretty=raw output and parses it into the commit struct. */
5034 main_read(struct view *view, char *line)
5036 static struct rev_graph *graph = graph_stacks;
5037 enum line_type type;
5038 struct commit *commit;
5043 if (!view->lines && !view->parent)
5044 die("No revisions match the given arguments.");
5045 if (view->lines > 0) {
5046 commit = view->line[view->lines - 1].data;
5047 if (!*commit->author) {
5050 graph->commit = NULL;
5053 update_rev_graph(graph);
5055 for (i = 0; i < ARRAY_SIZE(graph_stacks); i++)
5056 clear_rev_graph(&graph_stacks[i]);
5060 type = get_line_type(line);
5061 if (type == LINE_COMMIT) {
5062 commit = calloc(1, sizeof(struct commit));
5066 line += STRING_SIZE("commit ");
5068 graph->boundary = 1;
5072 string_copy_rev(commit->id, line);
5073 commit->refs = get_refs(commit->id);
5074 graph->commit = commit;
5075 add_line_data(view, commit, LINE_MAIN_COMMIT);
5077 while ((line = strchr(line, ' '))) {
5079 push_rev_graph(graph->parents, line);
5080 commit->has_parents = TRUE;
5087 commit = view->line[view->lines - 1].data;
5091 if (commit->has_parents)
5093 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
5098 /* Parse author lines where the name may be empty:
5099 * author <email@address.tld> 1138474660 +0100
5101 char *ident = line + STRING_SIZE("author ");
5102 char *nameend = strchr(ident, '<');
5103 char *emailend = strchr(ident, '>');
5105 if (!nameend || !emailend)
5108 update_rev_graph(graph);
5109 graph = graph->next;
5111 *nameend = *emailend = 0;
5112 ident = chomp_string(ident);
5114 ident = chomp_string(nameend + 1);
5119 string_ncopy(commit->author, ident, strlen(ident));
5121 /* Parse epoch and timezone */
5122 if (emailend[1] == ' ') {
5123 char *secs = emailend + 2;
5124 char *zone = strchr(secs, ' ');
5125 time_t time = (time_t) atol(secs);
5127 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
5131 tz = ('0' - zone[1]) * 60 * 60 * 10;
5132 tz += ('0' - zone[2]) * 60 * 60;
5133 tz += ('0' - zone[3]) * 60;
5134 tz += ('0' - zone[4]) * 60;
5142 gmtime_r(&time, &commit->time);
5147 /* Fill in the commit title if it has not already been set. */
5148 if (commit->title[0])
5151 /* Require titles to start with a non-space character at the
5152 * offset used by git log. */
5153 if (strncmp(line, " ", 4))
5156 /* Well, if the title starts with a whitespace character,
5157 * try to be forgiving. Otherwise we end up with no title. */
5158 while (isspace(*line))
5162 /* FIXME: More graceful handling of titles; append "..." to
5163 * shortened titles, etc. */
5165 string_ncopy(commit->title, line, strlen(line));
5172 main_request(struct view *view, enum request request, struct line *line)
5174 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
5178 open_view(view, REQ_VIEW_DIFF, flags);
5181 string_copy(opt_cmd, view->cmd);
5182 open_view(view, REQ_VIEW_MAIN, OPEN_RELOAD);
5192 grep_refs(struct ref **refs, regex_t *regex)
5200 if (regexec(regex, refs[i]->name, 1, &pmatch, 0) != REG_NOMATCH)
5202 } while (refs[i++]->next);
5208 main_grep(struct view *view, struct line *line)
5210 struct commit *commit = line->data;
5211 enum { S_TITLE, S_AUTHOR, S_DATE, S_REFS, S_END } state;
5212 char buf[DATE_COLS + 1];
5215 for (state = S_TITLE; state < S_END; state++) {
5219 case S_TITLE: text = commit->title; break;
5223 text = commit->author;
5228 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
5235 if (grep_refs(commit->refs, view->regex) == TRUE)
5242 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
5250 main_select(struct view *view, struct line *line)
5252 struct commit *commit = line->data;
5254 string_copy_rev(view->ref, commit->id);
5255 string_copy_rev(ref_commit, view->ref);
5258 static struct view_ops main_ops = {
5270 * Unicode / UTF-8 handling
5272 * NOTE: Much of the following code for dealing with unicode is derived from
5273 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
5274 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
5277 /* I've (over)annotated a lot of code snippets because I am not entirely
5278 * confident that the approach taken by this small UTF-8 interface is correct.
5282 unicode_width(unsigned long c)
5285 (c <= 0x115f /* Hangul Jamo */
5288 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
5290 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
5291 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
5292 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
5293 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
5294 || (c >= 0xffe0 && c <= 0xffe6)
5295 || (c >= 0x20000 && c <= 0x2fffd)
5296 || (c >= 0x30000 && c <= 0x3fffd)))
5300 return opt_tab_size;
5305 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
5306 * Illegal bytes are set one. */
5307 static const unsigned char utf8_bytes[256] = {
5308 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
5309 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
5310 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
5311 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
5312 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
5313 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
5314 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,
5315 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,
5318 /* Decode UTF-8 multi-byte representation into a unicode character. */
5319 static inline unsigned long
5320 utf8_to_unicode(const char *string, size_t length)
5322 unsigned long unicode;
5326 unicode = string[0];
5329 unicode = (string[0] & 0x1f) << 6;
5330 unicode += (string[1] & 0x3f);
5333 unicode = (string[0] & 0x0f) << 12;
5334 unicode += ((string[1] & 0x3f) << 6);
5335 unicode += (string[2] & 0x3f);
5338 unicode = (string[0] & 0x0f) << 18;
5339 unicode += ((string[1] & 0x3f) << 12);
5340 unicode += ((string[2] & 0x3f) << 6);
5341 unicode += (string[3] & 0x3f);
5344 unicode = (string[0] & 0x0f) << 24;
5345 unicode += ((string[1] & 0x3f) << 18);
5346 unicode += ((string[2] & 0x3f) << 12);
5347 unicode += ((string[3] & 0x3f) << 6);
5348 unicode += (string[4] & 0x3f);
5351 unicode = (string[0] & 0x01) << 30;
5352 unicode += ((string[1] & 0x3f) << 24);
5353 unicode += ((string[2] & 0x3f) << 18);
5354 unicode += ((string[3] & 0x3f) << 12);
5355 unicode += ((string[4] & 0x3f) << 6);
5356 unicode += (string[5] & 0x3f);
5359 die("Invalid unicode length");
5362 /* Invalid characters could return the special 0xfffd value but NUL
5363 * should be just as good. */
5364 return unicode > 0xffff ? 0 : unicode;
5367 /* Calculates how much of string can be shown within the given maximum width
5368 * and sets trimmed parameter to non-zero value if all of string could not be
5369 * shown. If the reserve flag is TRUE, it will reserve at least one
5370 * trailing character, which can be useful when drawing a delimiter.
5372 * Returns the number of bytes to output from string to satisfy max_width. */
5374 utf8_length(const char *string, int *width, size_t max_width, int *trimmed, bool reserve)
5376 const char *start = string;
5377 const char *end = strchr(string, '\0');
5378 unsigned char last_bytes = 0;
5379 size_t last_ucwidth = 0;
5384 while (string < end) {
5385 int c = *(unsigned char *) string;
5386 unsigned char bytes = utf8_bytes[c];
5388 unsigned long unicode;
5390 if (string + bytes > end)
5393 /* Change representation to figure out whether
5394 * it is a single- or double-width character. */
5396 unicode = utf8_to_unicode(string, bytes);
5397 /* FIXME: Graceful handling of invalid unicode character. */
5401 ucwidth = unicode_width(unicode);
5403 if (*width > max_width) {
5406 if (reserve && *width == max_width) {
5407 string -= last_bytes;
5408 *width -= last_ucwidth;
5415 last_ucwidth = ucwidth;
5418 return string - start;
5426 /* Whether or not the curses interface has been initialized. */
5427 static bool cursed = FALSE;
5429 /* The status window is used for polling keystrokes. */
5430 static WINDOW *status_win;
5432 static bool status_empty = TRUE;
5434 /* Update status and title window. */
5436 report(const char *msg, ...)
5438 struct view *view = display[current_view];
5444 char buf[SIZEOF_STR];
5447 va_start(args, msg);
5448 if (vsnprintf(buf, sizeof(buf), msg, args) >= sizeof(buf)) {
5449 buf[sizeof(buf) - 1] = 0;
5450 buf[sizeof(buf) - 2] = '.';
5451 buf[sizeof(buf) - 3] = '.';
5452 buf[sizeof(buf) - 4] = '.';
5458 if (!status_empty || *msg) {
5461 va_start(args, msg);
5463 wmove(status_win, 0, 0);
5465 vwprintw(status_win, msg, args);
5466 status_empty = FALSE;
5468 status_empty = TRUE;
5470 wclrtoeol(status_win);
5471 wrefresh(status_win);
5476 update_view_title(view);
5477 update_display_cursor(view);
5480 /* Controls when nodelay should be in effect when polling user input. */
5482 set_nonblocking_input(bool loading)
5484 static unsigned int loading_views;
5486 if ((loading == FALSE && loading_views-- == 1) ||
5487 (loading == TRUE && loading_views++ == 0))
5488 nodelay(status_win, loading);
5496 /* Initialize the curses library */
5497 if (isatty(STDIN_FILENO)) {
5498 cursed = !!initscr();
5500 /* Leave stdin and stdout alone when acting as a pager. */
5501 FILE *io = fopen("/dev/tty", "r+");
5504 die("Failed to open /dev/tty");
5505 cursed = !!newterm(NULL, io, io);
5509 die("Failed to initialize curses");
5511 nonl(); /* Tell curses not to do NL->CR/NL on output */
5512 cbreak(); /* Take input chars one at a time, no wait for \n */
5513 noecho(); /* Don't echo input */
5514 leaveok(stdscr, TRUE);
5519 getmaxyx(stdscr, y, x);
5520 status_win = newwin(1, 0, y - 1, 0);
5522 die("Failed to create status window");
5524 /* Enable keyboard mapping */
5525 keypad(status_win, TRUE);
5526 wbkgdset(status_win, get_line_attr(LINE_STATUS));
5528 TABSIZE = opt_tab_size;
5529 if (opt_line_graphics) {
5530 line_graphics[LINE_GRAPHIC_VLINE] = ACS_VLINE;
5535 prompt_yesno(const char *prompt)
5537 enum { WAIT, STOP, CANCEL } status = WAIT;
5538 bool answer = FALSE;
5540 while (status == WAIT) {
5546 foreach_view (view, i)
5551 mvwprintw(status_win, 0, 0, "%s [Yy]/[Nn]", prompt);
5552 wclrtoeol(status_win);
5554 /* Refresh, accept single keystroke of input */
5555 key = wgetch(status_win);
5579 /* Clear the status window */
5580 status_empty = FALSE;
5587 read_prompt(const char *prompt)
5589 enum { READING, STOP, CANCEL } status = READING;
5590 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
5593 while (status == READING) {
5599 foreach_view (view, i)
5604 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
5605 wclrtoeol(status_win);
5607 /* Refresh, accept single keystroke of input */
5608 key = wgetch(status_win);
5613 status = pos ? STOP : CANCEL;
5631 if (pos >= sizeof(buf)) {
5632 report("Input string too long");
5637 buf[pos++] = (char) key;
5641 /* Clear the status window */
5642 status_empty = FALSE;
5645 if (status == CANCEL)
5654 * Repository references
5657 static struct ref *refs = NULL;
5658 static size_t refs_alloc = 0;
5659 static size_t refs_size = 0;
5661 /* Id <-> ref store */
5662 static struct ref ***id_refs = NULL;
5663 static size_t id_refs_alloc = 0;
5664 static size_t id_refs_size = 0;
5666 static struct ref **
5669 struct ref ***tmp_id_refs;
5670 struct ref **ref_list = NULL;
5671 size_t ref_list_alloc = 0;
5672 size_t ref_list_size = 0;
5675 for (i = 0; i < id_refs_size; i++)
5676 if (!strcmp(id, id_refs[i][0]->id))
5679 tmp_id_refs = realloc_items(id_refs, &id_refs_alloc, id_refs_size + 1,
5684 id_refs = tmp_id_refs;
5686 for (i = 0; i < refs_size; i++) {
5689 if (strcmp(id, refs[i].id))
5692 tmp = realloc_items(ref_list, &ref_list_alloc,
5693 ref_list_size + 1, sizeof(*ref_list));
5701 if (ref_list_size > 0)
5702 ref_list[ref_list_size - 1]->next = 1;
5703 ref_list[ref_list_size] = &refs[i];
5705 /* XXX: The properties of the commit chains ensures that we can
5706 * safely modify the shared ref. The repo references will
5707 * always be similar for the same id. */
5708 ref_list[ref_list_size]->next = 0;
5713 id_refs[id_refs_size++] = ref_list;
5719 read_ref(char *id, size_t idlen, char *name, size_t namelen)
5724 bool remote = FALSE;
5725 bool tracked = FALSE;
5726 bool check_replace = FALSE;
5729 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
5730 if (!strcmp(name + namelen - 3, "^{}")) {
5733 if (refs_size > 0 && refs[refs_size - 1].ltag == TRUE)
5734 check_replace = TRUE;
5740 namelen -= STRING_SIZE("refs/tags/");
5741 name += STRING_SIZE("refs/tags/");
5743 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
5745 namelen -= STRING_SIZE("refs/remotes/");
5746 name += STRING_SIZE("refs/remotes/");
5747 tracked = !strcmp(opt_remote, name);
5749 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5750 namelen -= STRING_SIZE("refs/heads/");
5751 name += STRING_SIZE("refs/heads/");
5752 head = !strncmp(opt_head, name, namelen);
5754 } else if (!strcmp(name, "HEAD")) {
5755 opt_no_head = FALSE;
5759 if (check_replace && !strcmp(name, refs[refs_size - 1].name)) {
5760 /* it's an annotated tag, replace the previous sha1 with the
5761 * resolved commit id; relies on the fact git-ls-remote lists
5762 * the commit id of an annotated tag right beofre the commit id
5764 refs[refs_size - 1].ltag = ltag;
5765 string_copy_rev(refs[refs_size - 1].id, id);
5769 refs = realloc_items(refs, &refs_alloc, refs_size + 1, sizeof(*refs));
5773 ref = &refs[refs_size++];
5774 ref->name = malloc(namelen + 1);
5778 strncpy(ref->name, name, namelen);
5779 ref->name[namelen] = 0;
5783 ref->remote = remote;
5784 ref->tracked = tracked;
5785 string_copy_rev(ref->id, id);
5793 const char *cmd_env = getenv("TIG_LS_REMOTE");
5794 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
5796 return read_properties(popen(cmd, "r"), "\t", read_ref);
5800 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
5802 if (!strcmp(name, "i18n.commitencoding"))
5803 string_ncopy(opt_encoding, value, valuelen);
5805 if (!strcmp(name, "core.editor"))
5806 string_ncopy(opt_editor, value, valuelen);
5808 /* branch.<head>.remote */
5810 !strncmp(name, "branch.", 7) &&
5811 !strncmp(name + 7, opt_head, strlen(opt_head)) &&
5812 !strcmp(name + 7 + strlen(opt_head), ".remote"))
5813 string_ncopy(opt_remote, value, valuelen);
5815 if (*opt_head && *opt_remote &&
5816 !strncmp(name, "branch.", 7) &&
5817 !strncmp(name + 7, opt_head, strlen(opt_head)) &&
5818 !strcmp(name + 7 + strlen(opt_head), ".merge")) {
5819 size_t from = strlen(opt_remote);
5821 if (!strncmp(value, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5822 value += STRING_SIZE("refs/heads/");
5823 valuelen -= STRING_SIZE("refs/heads/");
5826 if (!string_format_from(opt_remote, &from, "/%s", value))
5834 load_git_config(void)
5836 return read_properties(popen(GIT_CONFIG " --list", "r"),
5837 "=", read_repo_config_option);
5841 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
5843 if (!opt_git_dir[0]) {
5844 string_ncopy(opt_git_dir, name, namelen);
5846 } else if (opt_is_inside_work_tree == -1) {
5847 /* This can be 3 different values depending on the
5848 * version of git being used. If git-rev-parse does not
5849 * understand --is-inside-work-tree it will simply echo
5850 * the option else either "true" or "false" is printed.
5851 * Default to true for the unknown case. */
5852 opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
5854 } else if (opt_cdup[0] == ' ') {
5855 string_ncopy(opt_cdup, name, namelen);
5857 if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5858 namelen -= STRING_SIZE("refs/heads/");
5859 name += STRING_SIZE("refs/heads/");
5860 string_ncopy(opt_head, name, namelen);
5868 load_repo_info(void)
5871 FILE *pipe = popen("(git rev-parse --git-dir --is-inside-work-tree "
5872 " --show-cdup; git symbolic-ref HEAD) 2>/dev/null", "r");
5874 /* XXX: The line outputted by "--show-cdup" can be empty so
5875 * initialize it to something invalid to make it possible to
5876 * detect whether it has been set or not. */
5879 result = read_properties(pipe, "=", read_repo_info);
5880 if (opt_cdup[0] == ' ')
5887 read_properties(FILE *pipe, const char *separators,
5888 int (*read_property)(char *, size_t, char *, size_t))
5890 char buffer[BUFSIZ];
5897 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
5902 name = chomp_string(name);
5903 namelen = strcspn(name, separators);
5905 if (name[namelen]) {
5907 value = chomp_string(name + namelen + 1);
5908 valuelen = strlen(value);
5915 state = read_property(name, namelen, value, valuelen);
5918 if (state != ERR && ferror(pipe))
5931 static void __NORETURN
5934 /* XXX: Restore tty modes and let the OS cleanup the rest! */
5940 static void __NORETURN
5941 die(const char *err, ...)
5947 va_start(args, err);
5948 fputs("tig: ", stderr);
5949 vfprintf(stderr, err, args);
5950 fputs("\n", stderr);
5957 warn(const char *msg, ...)
5961 va_start(args, msg);
5962 fputs("tig warning: ", stderr);
5963 vfprintf(stderr, msg, args);
5964 fputs("\n", stderr);
5969 main(int argc, char *argv[])
5972 enum request request;
5975 signal(SIGINT, quit);
5977 if (setlocale(LC_ALL, "")) {
5978 char *codeset = nl_langinfo(CODESET);
5980 string_ncopy(opt_codeset, codeset, strlen(codeset));
5983 if (load_repo_info() == ERR)
5984 die("Failed to load repo info.");
5986 if (load_options() == ERR)
5987 die("Failed to load user config.");
5989 if (load_git_config() == ERR)
5990 die("Failed to load repo config.");
5992 request = parse_options(argc, argv);
5993 if (request == REQ_NONE)
5996 /* Require a git repository unless when running in pager mode. */
5997 if (!opt_git_dir[0] && request != REQ_VIEW_PAGER)
5998 die("Not a git repository");
6000 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
6003 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
6004 opt_iconv = iconv_open(opt_codeset, opt_encoding);
6005 if (opt_iconv == ICONV_NONE)
6006 die("Failed to initialize character set conversion");
6009 if (*opt_git_dir && load_refs() == ERR)
6010 die("Failed to load refs.");
6012 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
6013 view->cmd_env = getenv(view->cmd_env);
6017 while (view_driver(display[current_view], request)) {
6021 foreach_view (view, i)
6024 /* Refresh, accept single keystroke of input */
6025 key = wgetch(status_win);
6027 /* wgetch() with nodelay() enabled returns ERR when there's no
6034 request = get_keybinding(display[current_view]->keymap, key);
6036 /* Some low-level request handling. This keeps access to
6037 * status_win restricted. */
6041 char *cmd = read_prompt(":");
6043 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
6044 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
6045 request = REQ_VIEW_DIFF;
6047 request = REQ_VIEW_PAGER;
6050 /* Always reload^Wrerun commands from the prompt. */
6051 open_view(view, request, OPEN_RELOAD);
6058 case REQ_SEARCH_BACK:
6060 const char *prompt = request == REQ_SEARCH
6062 char *search = read_prompt(prompt);
6065 string_ncopy(opt_search, search, strlen(search));
6070 case REQ_SCREEN_RESIZE:
6074 getmaxyx(stdscr, height, width);
6076 /* Resize the status view and let the view driver take
6077 * care of resizing the displayed views. */
6078 wresize(status_win, 1, width);
6079 mvwin(status_win, height - 1, 0);
6080 wrefresh(status_win);