1 /* Copyright (c) 2006-2007 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
51 #define __NORETURN __attribute__((__noreturn__))
56 static void __NORETURN die(const char *err, ...);
57 static void warn(const char *msg, ...);
58 static void report(const char *msg, ...);
59 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, size_t, char *, size_t));
60 static void set_nonblocking_input(bool loading);
61 static size_t utf8_length(const char *string, size_t max_width, int *trimmed, bool reserve);
63 #define ABS(x) ((x) >= 0 ? (x) : -(x))
64 #define MIN(x, y) ((x) < (y) ? (x) : (y))
66 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
67 #define STRING_SIZE(x) (sizeof(x) - 1)
69 #define SIZEOF_STR 1024 /* Default string size. */
70 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
71 #define SIZEOF_REV 41 /* Holds a SHA-1 and an ending NUL */
75 #define REVGRAPH_INIT 'I'
76 #define REVGRAPH_MERGE 'M'
77 #define REVGRAPH_BRANCH '+'
78 #define REVGRAPH_COMMIT '*'
79 #define REVGRAPH_BOUND '^'
80 #define REVGRAPH_LINE '|'
82 #define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
84 /* This color name can be used to refer to the default term colors. */
85 #define COLOR_DEFAULT (-1)
87 #define ICONV_NONE ((iconv_t) -1)
89 #define ICONV_CONST /* nothing */
92 /* The format and size of the date column in the main view. */
93 #define DATE_FORMAT "%Y-%m-%d %H:%M"
94 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
96 #define AUTHOR_COLS 20
98 /* The default interval between line numbers. */
99 #define NUMBER_INTERVAL 1
103 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
106 #define GIT_CONFIG "git config"
109 #define TIG_LS_REMOTE \
110 "git ls-remote $(git rev-parse --git-dir) 2>/dev/null"
112 #define TIG_DIFF_CMD \
113 "git show --pretty=fuller --no-color --root --patch-with-stat --find-copies-harder -C %s 2>/dev/null"
115 #define TIG_LOG_CMD \
116 "git log --no-color --cc --stat -n100 %s 2>/dev/null"
118 #define TIG_MAIN_CMD \
119 "git log --no-color --topo-order --boundary --pretty=raw %s 2>/dev/null"
121 #define TIG_TREE_CMD \
124 #define TIG_BLOB_CMD \
125 "git cat-file blob %s"
127 /* XXX: Needs to be defined to the empty string. */
128 #define TIG_HELP_CMD ""
129 #define TIG_PAGER_CMD ""
130 #define TIG_STATUS_CMD ""
131 #define TIG_STAGE_CMD ""
133 /* Some ascii-shorthands fitted into the ncurses namespace. */
135 #define KEY_RETURN '\r'
140 char *name; /* Ref name; tag or head names are shortened. */
141 char id[SIZEOF_REV]; /* Commit SHA1 ID */
142 unsigned int tag:1; /* Is it a tag? */
143 unsigned int remote:1; /* Is it a remote ref? */
144 unsigned int next:1; /* For ref lists: are there more refs? */
147 static struct ref **get_refs(char *id);
156 set_from_int_map(struct int_map *map, size_t map_size,
157 int *value, const char *name, int namelen)
162 for (i = 0; i < map_size; i++)
163 if (namelen == map[i].namelen &&
164 !strncasecmp(name, map[i].name, namelen)) {
165 *value = map[i].value;
178 string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
180 if (srclen > dstlen - 1)
183 strncpy(dst, src, srclen);
187 /* Shorthands for safely copying into a fixed buffer. */
189 #define string_copy(dst, src) \
190 string_ncopy_do(dst, sizeof(dst), src, sizeof(src))
192 #define string_ncopy(dst, src, srclen) \
193 string_ncopy_do(dst, sizeof(dst), src, srclen)
195 #define string_copy_rev(dst, src) \
196 string_ncopy_do(dst, SIZEOF_REV, src, SIZEOF_REV - 1)
198 #define string_add(dst, from, src) \
199 string_ncopy_do(dst + (from), sizeof(dst) - (from), src, sizeof(src))
202 chomp_string(char *name)
206 while (isspace(*name))
209 namelen = strlen(name) - 1;
210 while (namelen > 0 && isspace(name[namelen]))
217 string_nformat(char *buf, size_t bufsize, size_t *bufpos, const char *fmt, ...)
220 size_t pos = bufpos ? *bufpos : 0;
223 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
229 return pos >= bufsize ? FALSE : TRUE;
232 #define string_format(buf, fmt, args...) \
233 string_nformat(buf, sizeof(buf), NULL, fmt, args)
235 #define string_format_from(buf, from, fmt, args...) \
236 string_nformat(buf, sizeof(buf), from, fmt, args)
239 string_enum_compare(const char *str1, const char *str2, int len)
243 #define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
245 /* Diff-Header == DIFF_HEADER */
246 for (i = 0; i < len; i++) {
247 if (toupper(str1[i]) == toupper(str2[i]))
250 if (string_enum_sep(str1[i]) &&
251 string_enum_sep(str2[i]))
254 return str1[i] - str2[i];
262 * NOTE: The following is a slightly modified copy of the git project's shell
263 * quoting routines found in the quote.c file.
265 * Help to copy the thing properly quoted for the shell safety. any single
266 * quote is replaced with '\'', any exclamation point is replaced with '\!',
267 * and the whole thing is enclosed in a
270 * original sq_quote result
271 * name ==> name ==> 'name'
272 * a b ==> a b ==> 'a b'
273 * a'b ==> a'\''b ==> 'a'\''b'
274 * a!b ==> a'\!'b ==> 'a'\!'b'
278 sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
282 #define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0)
285 while ((c = *src++)) {
286 if (c == '\'' || c == '!') {
297 if (bufsize < SIZEOF_STR)
309 /* XXX: Keep the view request first and in sync with views[]. */ \
310 REQ_GROUP("View switching") \
311 REQ_(VIEW_MAIN, "Show main view"), \
312 REQ_(VIEW_DIFF, "Show diff view"), \
313 REQ_(VIEW_LOG, "Show log view"), \
314 REQ_(VIEW_TREE, "Show tree view"), \
315 REQ_(VIEW_BLOB, "Show blob view"), \
316 REQ_(VIEW_HELP, "Show help page"), \
317 REQ_(VIEW_PAGER, "Show pager view"), \
318 REQ_(VIEW_STATUS, "Show status view"), \
319 REQ_(VIEW_STAGE, "Show stage view"), \
321 REQ_GROUP("View manipulation") \
322 REQ_(ENTER, "Enter current line and scroll"), \
323 REQ_(NEXT, "Move to next"), \
324 REQ_(PREVIOUS, "Move to previous"), \
325 REQ_(VIEW_NEXT, "Move focus to next view"), \
326 REQ_(REFRESH, "Reload and refresh"), \
327 REQ_(VIEW_CLOSE, "Close the current view"), \
328 REQ_(QUIT, "Close all views and quit"), \
330 REQ_GROUP("Cursor navigation") \
331 REQ_(MOVE_UP, "Move cursor one line up"), \
332 REQ_(MOVE_DOWN, "Move cursor one line down"), \
333 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
334 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
335 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
336 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
338 REQ_GROUP("Scrolling") \
339 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
340 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
341 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
342 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
344 REQ_GROUP("Searching") \
345 REQ_(SEARCH, "Search the view"), \
346 REQ_(SEARCH_BACK, "Search backwards in the view"), \
347 REQ_(FIND_NEXT, "Find next search match"), \
348 REQ_(FIND_PREV, "Find previous search match"), \
351 REQ_(PROMPT, "Bring up the prompt"), \
352 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
353 REQ_(SCREEN_RESIZE, "Resize the screen"), \
354 REQ_(SHOW_VERSION, "Show version information"), \
355 REQ_(STOP_LOADING, "Stop all loading views"), \
356 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
357 REQ_(TOGGLE_DATE, "Toggle date display"), \
358 REQ_(TOGGLE_AUTHOR, "Toggle author display"), \
359 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"), \
360 REQ_(TOGGLE_REFS, "Toggle reference display (tags/branches)"), \
361 REQ_(STATUS_UPDATE, "Update file status"), \
362 REQ_(STATUS_MERGE, "Merge file using external tool"), \
363 REQ_(TREE_PARENT, "Switch to parent directory in tree view"), \
364 REQ_(EDIT, "Open in editor"), \
365 REQ_(NONE, "Do nothing")
368 /* User action requests. */
370 #define REQ_GROUP(help)
371 #define REQ_(req, help) REQ_##req
373 /* Offset all requests to avoid conflicts with ncurses getch values. */
374 REQ_OFFSET = KEY_MAX + 1,
381 struct request_info {
382 enum request request;
388 static struct request_info req_info[] = {
389 #define REQ_GROUP(help) { 0, NULL, 0, (help) },
390 #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
397 get_request(const char *name)
399 int namelen = strlen(name);
402 for (i = 0; i < ARRAY_SIZE(req_info); i++)
403 if (req_info[i].namelen == namelen &&
404 !string_enum_compare(req_info[i].name, name, namelen))
405 return req_info[i].request;
415 static const char usage[] =
416 "tig " TIG_VERSION " (" __DATE__ ")\n"
418 "Usage: tig [options] [revs] [--] [paths]\n"
419 " or: tig show [options] [revs] [--] [paths]\n"
421 " or: tig < [git command output]\n"
424 " -v, --version Show version and exit\n"
425 " -h, --help Show help message and exit\n";
427 /* Option and state variables. */
428 static bool opt_date = TRUE;
429 static bool opt_author = TRUE;
430 static bool opt_line_number = FALSE;
431 static bool opt_rev_graph = FALSE;
432 static bool opt_show_refs = TRUE;
433 static int opt_num_interval = NUMBER_INTERVAL;
434 static int opt_tab_size = TABSIZE;
435 static enum request opt_request = REQ_VIEW_MAIN;
436 static char opt_cmd[SIZEOF_STR] = "";
437 static char opt_path[SIZEOF_STR] = "";
438 static FILE *opt_pipe = NULL;
439 static char opt_encoding[20] = "UTF-8";
440 static bool opt_utf8 = TRUE;
441 static char opt_codeset[20] = "UTF-8";
442 static iconv_t opt_iconv = ICONV_NONE;
443 static char opt_search[SIZEOF_STR] = "";
444 static char opt_cdup[SIZEOF_STR] = "";
445 static char opt_git_dir[SIZEOF_STR] = "";
446 static signed char opt_is_inside_work_tree = -1; /* set to TRUE or FALSE */
447 static char opt_editor[SIZEOF_STR] = "";
455 check_option(char *opt, char short_name, char *name, enum option_type type, ...)
465 int namelen = strlen(name);
469 if (strncmp(opt, name, namelen))
472 if (opt[namelen] == '=')
473 value = opt + namelen + 1;
476 if (!short_name || opt[1] != short_name)
481 va_start(args, type);
482 if (type == OPT_INT) {
483 number = va_arg(args, int *);
485 *number = atoi(value);
492 /* Returns the index of log or diff command or -1 to exit. */
494 parse_options(int argc, char *argv[])
498 char *subcommand = NULL;
501 for (i = 1; i < argc; i++) {
504 if (!strcmp(opt, "log") ||
505 !strcmp(opt, "diff")) {
507 opt_request = opt[0] == 'l'
508 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
509 warn("`tig %s' has been deprecated", opt);
513 if (!strcmp(opt, "show")) {
515 opt_request = REQ_VIEW_DIFF;
519 if (!strcmp(opt, "status")) {
521 opt_request = REQ_VIEW_STATUS;
525 if (opt[0] && opt[0] != '-')
528 if (!strcmp(opt, "--")) {
533 if (check_option(opt, 'v', "version", OPT_NONE)) {
534 printf("tig version %s\n", TIG_VERSION);
538 if (check_option(opt, 'h', "help", OPT_NONE)) {
543 if (!strcmp(opt, "-S")) {
544 warn("`%s' has been deprecated; use `tig status' instead", opt);
545 opt_request = REQ_VIEW_STATUS;
549 if (!strcmp(opt, "-l")) {
550 opt_request = REQ_VIEW_LOG;
551 } else if (!strcmp(opt, "-d")) {
552 opt_request = REQ_VIEW_DIFF;
553 } else if (check_option(opt, 'n', "line-number", OPT_INT, &opt_num_interval)) {
554 opt_line_number = TRUE;
555 } else if (check_option(opt, 'b', "tab-size", OPT_INT, &opt_tab_size)) {
556 opt_tab_size = MIN(opt_tab_size, TABSIZE);
558 if (altargc >= ARRAY_SIZE(altargv))
559 die("maximum number of arguments exceeded");
560 altargv[altargc++] = opt;
564 warn("`%s' has been deprecated", opt);
567 /* Check that no 'alt' arguments occured before a subcommand. */
568 if (subcommand && i < argc && altargc > 0)
569 die("unknown arguments before `%s'", argv[i]);
571 if (!isatty(STDIN_FILENO)) {
572 opt_request = REQ_VIEW_PAGER;
575 } else if (opt_request == REQ_VIEW_STATUS) {
577 warn("ignoring arguments after `%s'", argv[i]);
579 } else if (i < argc || altargc > 0) {
583 if (opt_request == REQ_VIEW_MAIN)
584 /* XXX: This is vulnerable to the user overriding
585 * options required for the main view parser. */
586 string_copy(opt_cmd, "git log --no-color --pretty=raw --boundary");
588 string_copy(opt_cmd, "git");
589 buf_size = strlen(opt_cmd);
591 while (buf_size < sizeof(opt_cmd) && alti < altargc) {
592 opt_cmd[buf_size++] = ' ';
593 buf_size = sq_quote(opt_cmd, buf_size, altargv[alti++]);
596 while (buf_size < sizeof(opt_cmd) && i < argc) {
597 opt_cmd[buf_size++] = ' ';
598 buf_size = sq_quote(opt_cmd, buf_size, argv[i++]);
601 if (buf_size >= sizeof(opt_cmd))
602 die("command too long");
604 opt_cmd[buf_size] = 0;
607 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
615 * Line-oriented content detection.
619 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
620 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
621 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
622 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
623 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
624 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
625 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
626 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
627 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
628 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
629 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
630 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
631 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
632 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
633 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
634 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
635 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
636 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
637 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
638 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
639 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
640 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
641 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
642 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
643 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
644 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
645 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
646 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
647 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
648 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
649 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
650 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
651 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
652 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
653 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
654 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
655 LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
656 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
657 LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
658 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
659 LINE(MAIN_REVGRAPH,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
660 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
661 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
662 LINE(STAT_SECTION, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
663 LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
664 LINE(STAT_STAGED, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
665 LINE(STAT_UNSTAGED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
666 LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0)
669 #define LINE(type, line, fg, bg, attr) \
676 const char *name; /* Option name. */
677 int namelen; /* Size of option name. */
678 const char *line; /* The start of line to match. */
679 int linelen; /* Size of string to match. */
680 int fg, bg, attr; /* Color and text attributes for the lines. */
683 static struct line_info line_info[] = {
684 #define LINE(type, line, fg, bg, attr) \
685 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
690 static enum line_type
691 get_line_type(char *line)
693 int linelen = strlen(line);
696 for (type = 0; type < ARRAY_SIZE(line_info); type++)
697 /* Case insensitive search matches Signed-off-by lines better. */
698 if (linelen >= line_info[type].linelen &&
699 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
706 get_line_attr(enum line_type type)
708 assert(type < ARRAY_SIZE(line_info));
709 return COLOR_PAIR(type) | line_info[type].attr;
712 static struct line_info *
713 get_line_info(char *name, int namelen)
717 for (type = 0; type < ARRAY_SIZE(line_info); type++)
718 if (namelen == line_info[type].namelen &&
719 !string_enum_compare(line_info[type].name, name, namelen))
720 return &line_info[type];
728 int default_bg = line_info[LINE_DEFAULT].bg;
729 int default_fg = line_info[LINE_DEFAULT].fg;
734 if (assume_default_colors(default_fg, default_bg) == ERR) {
735 default_bg = COLOR_BLACK;
736 default_fg = COLOR_WHITE;
739 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
740 struct line_info *info = &line_info[type];
741 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
742 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
744 init_pair(type, fg, bg);
752 unsigned int selected:1;
754 void *data; /* User data */
764 enum request request;
765 struct keybinding *next;
768 static struct keybinding default_keybindings[] = {
770 { 'm', REQ_VIEW_MAIN },
771 { 'd', REQ_VIEW_DIFF },
772 { 'l', REQ_VIEW_LOG },
773 { 't', REQ_VIEW_TREE },
774 { 'f', REQ_VIEW_BLOB },
775 { 'p', REQ_VIEW_PAGER },
776 { 'h', REQ_VIEW_HELP },
777 { 'S', REQ_VIEW_STATUS },
778 { 'c', REQ_VIEW_STAGE },
780 /* View manipulation */
781 { 'q', REQ_VIEW_CLOSE },
782 { KEY_TAB, REQ_VIEW_NEXT },
783 { KEY_RETURN, REQ_ENTER },
784 { KEY_UP, REQ_PREVIOUS },
785 { KEY_DOWN, REQ_NEXT },
786 { 'R', REQ_REFRESH },
788 /* Cursor navigation */
789 { 'k', REQ_MOVE_UP },
790 { 'j', REQ_MOVE_DOWN },
791 { KEY_HOME, REQ_MOVE_FIRST_LINE },
792 { KEY_END, REQ_MOVE_LAST_LINE },
793 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
794 { ' ', REQ_MOVE_PAGE_DOWN },
795 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
796 { 'b', REQ_MOVE_PAGE_UP },
797 { '-', REQ_MOVE_PAGE_UP },
800 { KEY_IC, REQ_SCROLL_LINE_UP },
801 { KEY_DC, REQ_SCROLL_LINE_DOWN },
802 { 'w', REQ_SCROLL_PAGE_UP },
803 { 's', REQ_SCROLL_PAGE_DOWN },
807 { '?', REQ_SEARCH_BACK },
808 { 'n', REQ_FIND_NEXT },
809 { 'N', REQ_FIND_PREV },
813 { 'z', REQ_STOP_LOADING },
814 { 'v', REQ_SHOW_VERSION },
815 { 'r', REQ_SCREEN_REDRAW },
816 { '.', REQ_TOGGLE_LINENO },
817 { 'D', REQ_TOGGLE_DATE },
818 { 'A', REQ_TOGGLE_AUTHOR },
819 { 'g', REQ_TOGGLE_REV_GRAPH },
820 { 'F', REQ_TOGGLE_REFS },
822 { 'u', REQ_STATUS_UPDATE },
823 { 'M', REQ_STATUS_MERGE },
824 { ',', REQ_TREE_PARENT },
827 /* Using the ncurses SIGWINCH handler. */
828 { KEY_RESIZE, REQ_SCREEN_RESIZE },
831 #define KEYMAP_INFO \
844 #define KEYMAP_(name) KEYMAP_##name
849 static struct int_map keymap_table[] = {
850 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
855 #define set_keymap(map, name) \
856 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
858 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
861 add_keybinding(enum keymap keymap, enum request request, int key)
863 struct keybinding *keybinding;
865 keybinding = calloc(1, sizeof(*keybinding));
867 die("Failed to allocate keybinding");
869 keybinding->alias = key;
870 keybinding->request = request;
871 keybinding->next = keybindings[keymap];
872 keybindings[keymap] = keybinding;
875 /* Looks for a key binding first in the given map, then in the generic map, and
876 * lastly in the default keybindings. */
878 get_keybinding(enum keymap keymap, int key)
880 struct keybinding *kbd;
883 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
884 if (kbd->alias == key)
887 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
888 if (kbd->alias == key)
891 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
892 if (default_keybindings[i].alias == key)
893 return default_keybindings[i].request;
895 return (enum request) key;
904 static struct key key_table[] = {
905 { "Enter", KEY_RETURN },
907 { "Backspace", KEY_BACKSPACE },
909 { "Escape", KEY_ESC },
910 { "Left", KEY_LEFT },
911 { "Right", KEY_RIGHT },
913 { "Down", KEY_DOWN },
914 { "Insert", KEY_IC },
915 { "Delete", KEY_DC },
917 { "Home", KEY_HOME },
919 { "PageUp", KEY_PPAGE },
920 { "PageDown", KEY_NPAGE },
930 { "F10", KEY_F(10) },
931 { "F11", KEY_F(11) },
932 { "F12", KEY_F(12) },
936 get_key_value(const char *name)
940 for (i = 0; i < ARRAY_SIZE(key_table); i++)
941 if (!strcasecmp(key_table[i].name, name))
942 return key_table[i].value;
944 if (strlen(name) == 1 && isprint(*name))
951 get_key_name(int key_value)
953 static char key_char[] = "'X'";
957 for (key = 0; key < ARRAY_SIZE(key_table); key++)
958 if (key_table[key].value == key_value)
959 seq = key_table[key].name;
963 isprint(key_value)) {
964 key_char[1] = (char) key_value;
968 return seq ? seq : "'?'";
972 get_key(enum request request)
974 static char buf[BUFSIZ];
981 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
982 struct keybinding *keybinding = &default_keybindings[i];
984 if (keybinding->request != request)
987 if (!string_format_from(buf, &pos, "%s%s", sep,
988 get_key_name(keybinding->alias)))
989 return "Too many keybindings!";
999 char cmd[SIZEOF_STR];
1002 static struct run_request *run_request;
1003 static size_t run_requests;
1006 add_run_request(enum keymap keymap, int key, int argc, char **argv)
1008 struct run_request *tmp;
1009 struct run_request req = { keymap, key };
1012 for (bufpos = 0; argc > 0; argc--, argv++)
1013 if (!string_format_from(req.cmd, &bufpos, "%s ", *argv))
1016 req.cmd[bufpos - 1] = 0;
1018 tmp = realloc(run_request, (run_requests + 1) * sizeof(*run_request));
1023 run_request[run_requests++] = req;
1025 return REQ_NONE + run_requests;
1028 static struct run_request *
1029 get_run_request(enum request request)
1031 if (request <= REQ_NONE)
1033 return &run_request[request - REQ_NONE - 1];
1037 add_builtin_run_requests(void)
1044 { KEYMAP_MAIN, 'C', { "git cherry-pick %(commit)" } },
1045 { KEYMAP_GENERIC, 'G', { "git gc" } },
1049 for (i = 0; i < ARRAY_SIZE(reqs); i++) {
1052 req = add_run_request(reqs[i].keymap, reqs[i].key, 1, reqs[i].argv);
1053 if (req != REQ_NONE)
1054 add_keybinding(reqs[i].keymap, req, reqs[i].key);
1059 * User config file handling.
1062 static struct int_map color_map[] = {
1063 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
1075 #define set_color(color, name) \
1076 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
1078 static struct int_map attr_map[] = {
1079 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
1086 ATTR_MAP(UNDERLINE),
1089 #define set_attribute(attr, name) \
1090 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
1092 static int config_lineno;
1093 static bool config_errors;
1094 static char *config_msg;
1096 /* Wants: object fgcolor bgcolor [attr] */
1098 option_color_command(int argc, char *argv[])
1100 struct line_info *info;
1102 if (argc != 3 && argc != 4) {
1103 config_msg = "Wrong number of arguments given to color command";
1107 info = get_line_info(argv[0], strlen(argv[0]));
1109 config_msg = "Unknown color name";
1113 if (set_color(&info->fg, argv[1]) == ERR ||
1114 set_color(&info->bg, argv[2]) == ERR) {
1115 config_msg = "Unknown color";
1119 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
1120 config_msg = "Unknown attribute";
1127 /* Wants: name = value */
1129 option_set_command(int argc, char *argv[])
1132 config_msg = "Wrong number of arguments given to set command";
1136 if (strcmp(argv[1], "=")) {
1137 config_msg = "No value assigned";
1141 if (!strcmp(argv[0], "show-rev-graph")) {
1142 opt_rev_graph = (!strcmp(argv[2], "1") ||
1143 !strcmp(argv[2], "true") ||
1144 !strcmp(argv[2], "yes"));
1148 if (!strcmp(argv[0], "line-number-interval")) {
1149 opt_num_interval = atoi(argv[2]);
1153 if (!strcmp(argv[0], "tab-size")) {
1154 opt_tab_size = atoi(argv[2]);
1158 if (!strcmp(argv[0], "commit-encoding")) {
1159 char *arg = argv[2];
1160 int delimiter = *arg;
1163 switch (delimiter) {
1166 for (arg++, i = 0; arg[i]; i++)
1167 if (arg[i] == delimiter) {
1172 string_ncopy(opt_encoding, arg, strlen(arg));
1177 config_msg = "Unknown variable name";
1181 /* Wants: mode request key */
1183 option_bind_command(int argc, char *argv[])
1185 enum request request;
1190 config_msg = "Wrong number of arguments given to bind command";
1194 if (set_keymap(&keymap, argv[0]) == ERR) {
1195 config_msg = "Unknown key map";
1199 key = get_key_value(argv[1]);
1201 config_msg = "Unknown key";
1205 request = get_request(argv[2]);
1206 if (request == REQ_NONE) {
1207 const char *obsolete[] = { "cherry-pick" };
1208 size_t namelen = strlen(argv[2]);
1211 for (i = 0; i < ARRAY_SIZE(obsolete); i++) {
1212 if (namelen == strlen(obsolete[i]) &&
1213 !string_enum_compare(obsolete[i], argv[2], namelen)) {
1214 config_msg = "Obsolete request name";
1219 if (request == REQ_NONE && *argv[2]++ == '!')
1220 request = add_run_request(keymap, key, argc - 2, argv + 2);
1221 if (request == REQ_NONE) {
1222 config_msg = "Unknown request name";
1226 add_keybinding(keymap, request, key);
1232 set_option(char *opt, char *value)
1239 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1240 argv[argc++] = value;
1243 /* Nothing more to tokenize or last available token. */
1244 if (!*value || argc >= ARRAY_SIZE(argv))
1248 while (isspace(*value))
1252 if (!strcmp(opt, "color"))
1253 return option_color_command(argc, argv);
1255 if (!strcmp(opt, "set"))
1256 return option_set_command(argc, argv);
1258 if (!strcmp(opt, "bind"))
1259 return option_bind_command(argc, argv);
1261 config_msg = "Unknown option command";
1266 read_option(char *opt, size_t optlen, char *value, size_t valuelen)
1271 config_msg = "Internal error";
1273 /* Check for comment markers, since read_properties() will
1274 * only ensure opt and value are split at first " \t". */
1275 optlen = strcspn(opt, "#");
1279 if (opt[optlen] != 0) {
1280 config_msg = "No option value";
1284 /* Look for comment endings in the value. */
1285 size_t len = strcspn(value, "#");
1287 if (len < valuelen) {
1289 value[valuelen] = 0;
1292 status = set_option(opt, value);
1295 if (status == ERR) {
1296 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1297 config_lineno, (int) optlen, opt, config_msg);
1298 config_errors = TRUE;
1301 /* Always keep going if errors are encountered. */
1306 load_option_file(const char *path)
1310 /* It's ok that the file doesn't exist. */
1311 file = fopen(path, "r");
1316 config_errors = FALSE;
1318 if (read_properties(file, " \t", read_option) == ERR ||
1319 config_errors == TRUE)
1320 fprintf(stderr, "Errors while loading %s.\n", path);
1326 char *home = getenv("HOME");
1327 char *tigrc_user = getenv("TIGRC_USER");
1328 char *tigrc_system = getenv("TIGRC_SYSTEM");
1329 char buf[SIZEOF_STR];
1331 add_builtin_run_requests();
1333 if (!tigrc_system) {
1334 if (!string_format(buf, "%s/tigrc", SYSCONFDIR))
1338 load_option_file(tigrc_system);
1341 if (!home || !string_format(buf, "%s/.tigrc", home))
1345 load_option_file(tigrc_user);
1358 /* The display array of active views and the index of the current view. */
1359 static struct view *display[2];
1360 static unsigned int current_view;
1362 /* Reading from the prompt? */
1363 static bool input_mode = FALSE;
1365 #define foreach_displayed_view(view, i) \
1366 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1368 #define displayed_views() (display[1] != NULL ? 2 : 1)
1370 /* Current head and commit ID */
1371 static char ref_blob[SIZEOF_REF] = "";
1372 static char ref_commit[SIZEOF_REF] = "HEAD";
1373 static char ref_head[SIZEOF_REF] = "HEAD";
1376 const char *name; /* View name */
1377 const char *cmd_fmt; /* Default command line format */
1378 const char *cmd_env; /* Command line set via environment */
1379 const char *id; /* Points to either of ref_{head,commit,blob} */
1381 struct view_ops *ops; /* View operations */
1383 enum keymap keymap; /* What keymap does this view have */
1385 char cmd[SIZEOF_STR]; /* Command buffer */
1386 char ref[SIZEOF_REF]; /* Hovered commit reference */
1387 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1389 int height, width; /* The width and height of the main window */
1390 WINDOW *win; /* The main window */
1391 WINDOW *title; /* The title window living below the main window */
1394 unsigned long offset; /* Offset of the window top */
1395 unsigned long lineno; /* Current line number */
1398 char grep[SIZEOF_STR]; /* Search string */
1399 regex_t *regex; /* Pre-compiled regex */
1401 /* If non-NULL, points to the view that opened this view. If this view
1402 * is closed tig will switch back to the parent view. */
1403 struct view *parent;
1406 unsigned long lines; /* Total number of lines */
1407 struct line *line; /* Line index */
1408 unsigned long line_size;/* Total number of allocated lines */
1409 unsigned int digits; /* Number of digits in the lines member. */
1417 /* What type of content being displayed. Used in the title bar. */
1419 /* Open and reads in all view content. */
1420 bool (*open)(struct view *view);
1421 /* Read one line; updates view->line. */
1422 bool (*read)(struct view *view, char *data);
1423 /* Draw one line; @lineno must be < view->height. */
1424 bool (*draw)(struct view *view, struct line *line, unsigned int lineno, bool selected);
1425 /* Depending on view handle a special requests. */
1426 enum request (*request)(struct view *view, enum request request, struct line *line);
1427 /* Search for regex in a line. */
1428 bool (*grep)(struct view *view, struct line *line);
1430 void (*select)(struct view *view, struct line *line);
1433 static struct view_ops pager_ops;
1434 static struct view_ops main_ops;
1435 static struct view_ops tree_ops;
1436 static struct view_ops blob_ops;
1437 static struct view_ops help_ops;
1438 static struct view_ops status_ops;
1439 static struct view_ops stage_ops;
1441 #define VIEW_STR(name, cmd, env, ref, ops, map) \
1442 { name, cmd, #env, ref, ops, map}
1444 #define VIEW_(id, name, ops, ref) \
1445 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id)
1448 static struct view views[] = {
1449 VIEW_(MAIN, "main", &main_ops, ref_head),
1450 VIEW_(DIFF, "diff", &pager_ops, ref_commit),
1451 VIEW_(LOG, "log", &pager_ops, ref_head),
1452 VIEW_(TREE, "tree", &tree_ops, ref_commit),
1453 VIEW_(BLOB, "blob", &blob_ops, ref_blob),
1454 VIEW_(HELP, "help", &help_ops, ""),
1455 VIEW_(PAGER, "pager", &pager_ops, "stdin"),
1456 VIEW_(STATUS, "status", &status_ops, ""),
1457 VIEW_(STAGE, "stage", &stage_ops, ""),
1460 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1462 #define foreach_view(view, i) \
1463 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1465 #define view_is_displayed(view) \
1466 (view == display[0] || view == display[1])
1469 draw_text(struct view *view, const char *string, int max_len, int col,
1470 bool use_tilde, int tilde_attr)
1473 int trimmed = FALSE;
1479 len = utf8_length(string, max_len, &trimmed, use_tilde);
1481 len = strlen(string);
1482 if (len > max_len) {
1491 waddnstr(view->win, string, len);
1492 if (trimmed && use_tilde) {
1493 if (tilde_attr != -1)
1494 wattrset(view->win, tilde_attr);
1495 waddch(view->win, '~');
1503 draw_view_line(struct view *view, unsigned int lineno)
1506 bool selected = (view->offset + lineno == view->lineno);
1509 assert(view_is_displayed(view));
1511 if (view->offset + lineno >= view->lines)
1514 line = &view->line[view->offset + lineno];
1517 line->selected = TRUE;
1518 view->ops->select(view, line);
1519 } else if (line->selected) {
1520 line->selected = FALSE;
1521 wmove(view->win, lineno, 0);
1522 wclrtoeol(view->win);
1525 scrollok(view->win, FALSE);
1526 draw_ok = view->ops->draw(view, line, lineno, selected);
1527 scrollok(view->win, TRUE);
1533 redraw_view_from(struct view *view, int lineno)
1535 assert(0 <= lineno && lineno < view->height);
1537 for (; lineno < view->height; lineno++) {
1538 if (!draw_view_line(view, lineno))
1542 redrawwin(view->win);
1544 wnoutrefresh(view->win);
1546 wrefresh(view->win);
1550 redraw_view(struct view *view)
1553 redraw_view_from(view, 0);
1558 update_view_title(struct view *view)
1560 char buf[SIZEOF_STR];
1561 char state[SIZEOF_STR];
1562 size_t bufpos = 0, statelen = 0;
1564 assert(view_is_displayed(view));
1566 if (view != VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) {
1567 unsigned int view_lines = view->offset + view->height;
1568 unsigned int lines = view->lines
1569 ? MIN(view_lines, view->lines) * 100 / view->lines
1572 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1579 time_t secs = time(NULL) - view->start_time;
1581 /* Three git seconds are a long time ... */
1583 string_format_from(state, &statelen, " %lds", secs);
1587 string_format_from(buf, &bufpos, "[%s]", view->name);
1588 if (*view->ref && bufpos < view->width) {
1589 size_t refsize = strlen(view->ref);
1590 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1592 if (minsize < view->width)
1593 refsize = view->width - minsize + 7;
1594 string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
1597 if (statelen && bufpos < view->width) {
1598 string_format_from(buf, &bufpos, " %s", state);
1601 if (view == display[current_view])
1602 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1604 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1606 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1607 wclrtoeol(view->title);
1608 wmove(view->title, 0, view->width - 1);
1611 wnoutrefresh(view->title);
1613 wrefresh(view->title);
1617 resize_display(void)
1620 struct view *base = display[0];
1621 struct view *view = display[1] ? display[1] : display[0];
1623 /* Setup window dimensions */
1625 getmaxyx(stdscr, base->height, base->width);
1627 /* Make room for the status window. */
1631 /* Horizontal split. */
1632 view->width = base->width;
1633 view->height = SCALE_SPLIT_VIEW(base->height);
1634 base->height -= view->height;
1636 /* Make room for the title bar. */
1640 /* Make room for the title bar. */
1645 foreach_displayed_view (view, i) {
1647 view->win = newwin(view->height, 0, offset, 0);
1649 die("Failed to create %s view", view->name);
1651 scrollok(view->win, TRUE);
1653 view->title = newwin(1, 0, offset + view->height, 0);
1655 die("Failed to create title window");
1658 wresize(view->win, view->height, view->width);
1659 mvwin(view->win, offset, 0);
1660 mvwin(view->title, offset + view->height, 0);
1663 offset += view->height + 1;
1668 redraw_display(void)
1673 foreach_displayed_view (view, i) {
1675 update_view_title(view);
1680 update_display_cursor(struct view *view)
1682 /* Move the cursor to the right-most column of the cursor line.
1684 * XXX: This could turn out to be a bit expensive, but it ensures that
1685 * the cursor does not jump around. */
1687 wmove(view->win, view->lineno - view->offset, view->width - 1);
1688 wrefresh(view->win);
1696 /* Scrolling backend */
1698 do_scroll_view(struct view *view, int lines)
1700 bool redraw_current_line = FALSE;
1702 /* The rendering expects the new offset. */
1703 view->offset += lines;
1705 assert(0 <= view->offset && view->offset < view->lines);
1708 /* Move current line into the view. */
1709 if (view->lineno < view->offset) {
1710 view->lineno = view->offset;
1711 redraw_current_line = TRUE;
1712 } else if (view->lineno >= view->offset + view->height) {
1713 view->lineno = view->offset + view->height - 1;
1714 redraw_current_line = TRUE;
1717 assert(view->offset <= view->lineno && view->lineno < view->lines);
1719 /* Redraw the whole screen if scrolling is pointless. */
1720 if (view->height < ABS(lines)) {
1724 int line = lines > 0 ? view->height - lines : 0;
1725 int end = line + ABS(lines);
1727 wscrl(view->win, lines);
1729 for (; line < end; line++) {
1730 if (!draw_view_line(view, line))
1734 if (redraw_current_line)
1735 draw_view_line(view, view->lineno - view->offset);
1738 redrawwin(view->win);
1739 wrefresh(view->win);
1743 /* Scroll frontend */
1745 scroll_view(struct view *view, enum request request)
1749 assert(view_is_displayed(view));
1752 case REQ_SCROLL_PAGE_DOWN:
1753 lines = view->height;
1754 case REQ_SCROLL_LINE_DOWN:
1755 if (view->offset + lines > view->lines)
1756 lines = view->lines - view->offset;
1758 if (lines == 0 || view->offset + view->height >= view->lines) {
1759 report("Cannot scroll beyond the last line");
1764 case REQ_SCROLL_PAGE_UP:
1765 lines = view->height;
1766 case REQ_SCROLL_LINE_UP:
1767 if (lines > view->offset)
1768 lines = view->offset;
1771 report("Cannot scroll beyond the first line");
1779 die("request %d not handled in switch", request);
1782 do_scroll_view(view, lines);
1787 move_view(struct view *view, enum request request)
1789 int scroll_steps = 0;
1793 case REQ_MOVE_FIRST_LINE:
1794 steps = -view->lineno;
1797 case REQ_MOVE_LAST_LINE:
1798 steps = view->lines - view->lineno - 1;
1801 case REQ_MOVE_PAGE_UP:
1802 steps = view->height > view->lineno
1803 ? -view->lineno : -view->height;
1806 case REQ_MOVE_PAGE_DOWN:
1807 steps = view->lineno + view->height >= view->lines
1808 ? view->lines - view->lineno - 1 : view->height;
1820 die("request %d not handled in switch", request);
1823 if (steps <= 0 && view->lineno == 0) {
1824 report("Cannot move beyond the first line");
1827 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1828 report("Cannot move beyond the last line");
1832 /* Move the current line */
1833 view->lineno += steps;
1834 assert(0 <= view->lineno && view->lineno < view->lines);
1836 /* Check whether the view needs to be scrolled */
1837 if (view->lineno < view->offset ||
1838 view->lineno >= view->offset + view->height) {
1839 scroll_steps = steps;
1840 if (steps < 0 && -steps > view->offset) {
1841 scroll_steps = -view->offset;
1843 } else if (steps > 0) {
1844 if (view->lineno == view->lines - 1 &&
1845 view->lines > view->height) {
1846 scroll_steps = view->lines - view->offset - 1;
1847 if (scroll_steps >= view->height)
1848 scroll_steps -= view->height - 1;
1853 if (!view_is_displayed(view)) {
1854 view->offset += scroll_steps;
1855 assert(0 <= view->offset && view->offset < view->lines);
1856 view->ops->select(view, &view->line[view->lineno]);
1860 /* Repaint the old "current" line if we be scrolling */
1861 if (ABS(steps) < view->height)
1862 draw_view_line(view, view->lineno - steps - view->offset);
1865 do_scroll_view(view, scroll_steps);
1869 /* Draw the current line */
1870 draw_view_line(view, view->lineno - view->offset);
1872 redrawwin(view->win);
1873 wrefresh(view->win);
1882 static void search_view(struct view *view, enum request request);
1885 find_next_line(struct view *view, unsigned long lineno, struct line *line)
1887 assert(view_is_displayed(view));
1889 if (!view->ops->grep(view, line))
1892 if (lineno - view->offset >= view->height) {
1893 view->offset = lineno;
1894 view->lineno = lineno;
1898 unsigned long old_lineno = view->lineno - view->offset;
1900 view->lineno = lineno;
1901 draw_view_line(view, old_lineno);
1903 draw_view_line(view, view->lineno - view->offset);
1904 redrawwin(view->win);
1905 wrefresh(view->win);
1908 report("Line %ld matches '%s'", lineno + 1, view->grep);
1913 find_next(struct view *view, enum request request)
1915 unsigned long lineno = view->lineno;
1920 report("No previous search");
1922 search_view(view, request);
1932 case REQ_SEARCH_BACK:
1941 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
1942 lineno += direction;
1944 /* Note, lineno is unsigned long so will wrap around in which case it
1945 * will become bigger than view->lines. */
1946 for (; lineno < view->lines; lineno += direction) {
1947 struct line *line = &view->line[lineno];
1949 if (find_next_line(view, lineno, line))
1953 report("No match found for '%s'", view->grep);
1957 search_view(struct view *view, enum request request)
1962 regfree(view->regex);
1965 view->regex = calloc(1, sizeof(*view->regex));
1970 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
1971 if (regex_err != 0) {
1972 char buf[SIZEOF_STR] = "unknown error";
1974 regerror(regex_err, view->regex, buf, sizeof(buf));
1975 report("Search failed: %s", buf);
1979 string_copy(view->grep, opt_search);
1981 find_next(view, request);
1985 * Incremental updating
1989 end_update(struct view *view)
1993 set_nonblocking_input(FALSE);
1994 if (view->pipe == stdin)
2002 begin_update(struct view *view)
2008 string_copy(view->cmd, opt_cmd);
2010 /* When running random commands, initially show the
2011 * command in the title. However, it maybe later be
2012 * overwritten if a commit line is selected. */
2013 if (view == VIEW(REQ_VIEW_PAGER))
2014 string_copy(view->ref, view->cmd);
2018 } else if (view == VIEW(REQ_VIEW_TREE)) {
2019 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2020 char path[SIZEOF_STR];
2022 if (strcmp(view->vid, view->id))
2023 opt_path[0] = path[0] = 0;
2024 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
2027 if (!string_format(view->cmd, format, view->id, path))
2031 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2032 const char *id = view->id;
2034 if (!string_format(view->cmd, format, id, id, id, id, id))
2037 /* Put the current ref_* value to the view title ref
2038 * member. This is needed by the blob view. Most other
2039 * views sets it automatically after loading because the
2040 * first line is a commit line. */
2041 string_copy_rev(view->ref, view->id);
2044 /* Special case for the pager view. */
2046 view->pipe = opt_pipe;
2049 view->pipe = popen(view->cmd, "r");
2055 set_nonblocking_input(TRUE);
2060 string_copy_rev(view->vid, view->id);
2065 for (i = 0; i < view->lines; i++)
2066 if (view->line[i].data)
2067 free(view->line[i].data);
2073 view->start_time = time(NULL);
2078 static struct line *
2079 realloc_lines(struct view *view, size_t line_size)
2081 struct line *tmp = realloc(view->line, sizeof(*view->line) * line_size);
2087 view->line_size = line_size;
2092 update_view(struct view *view)
2094 char in_buffer[BUFSIZ];
2095 char out_buffer[BUFSIZ * 2];
2097 /* The number of lines to read. If too low it will cause too much
2098 * redrawing (and possible flickering), if too high responsiveness
2100 unsigned long lines = view->height;
2101 int redraw_from = -1;
2106 /* Only redraw if lines are visible. */
2107 if (view->offset + view->height >= view->lines)
2108 redraw_from = view->lines - view->offset;
2110 /* FIXME: This is probably not perfect for backgrounded views. */
2111 if (!realloc_lines(view, view->lines + lines))
2114 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
2115 size_t linelen = strlen(line);
2118 line[linelen - 1] = 0;
2120 if (opt_iconv != ICONV_NONE) {
2121 ICONV_CONST char *inbuf = line;
2122 size_t inlen = linelen;
2124 char *outbuf = out_buffer;
2125 size_t outlen = sizeof(out_buffer);
2129 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
2130 if (ret != (size_t) -1) {
2132 linelen = strlen(out_buffer);
2136 if (!view->ops->read(view, line))
2146 lines = view->lines;
2147 for (digits = 0; lines; digits++)
2150 /* Keep the displayed view in sync with line number scaling. */
2151 if (digits != view->digits) {
2152 view->digits = digits;
2157 if (!view_is_displayed(view))
2160 if (view == VIEW(REQ_VIEW_TREE)) {
2161 /* Clear the view and redraw everything since the tree sorting
2162 * might have rearranged things. */
2165 } else if (redraw_from >= 0) {
2166 /* If this is an incremental update, redraw the previous line
2167 * since for commits some members could have changed when
2168 * loading the main view. */
2169 if (redraw_from > 0)
2172 /* Since revision graph visualization requires knowledge
2173 * about the parent commit, it causes a further one-off
2174 * needed to be redrawn for incremental updates. */
2175 if (redraw_from > 0 && opt_rev_graph)
2178 /* Incrementally draw avoids flickering. */
2179 redraw_view_from(view, redraw_from);
2182 /* Update the title _after_ the redraw so that if the redraw picks up a
2183 * commit reference in view->ref it'll be available here. */
2184 update_view_title(view);
2187 if (ferror(view->pipe)) {
2188 report("Failed to read: %s", strerror(errno));
2191 } else if (feof(view->pipe)) {
2199 report("Allocation failure");
2202 view->ops->read(view, NULL);
2207 static struct line *
2208 add_line_data(struct view *view, void *data, enum line_type type)
2210 struct line *line = &view->line[view->lines++];
2212 memset(line, 0, sizeof(*line));
2219 static struct line *
2220 add_line_text(struct view *view, char *data, enum line_type type)
2223 data = strdup(data);
2225 return data ? add_line_data(view, data, type) : NULL;
2234 OPEN_DEFAULT = 0, /* Use default view switching. */
2235 OPEN_SPLIT = 1, /* Split current view. */
2236 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2237 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2241 open_view(struct view *prev, enum request request, enum open_flags flags)
2243 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2244 bool split = !!(flags & OPEN_SPLIT);
2245 bool reload = !!(flags & OPEN_RELOAD);
2246 struct view *view = VIEW(request);
2247 int nviews = displayed_views();
2248 struct view *base_view = display[0];
2250 if (view == prev && nviews == 1 && !reload) {
2251 report("Already in %s view", view->name);
2255 if (view->ops->open) {
2256 if (!view->ops->open(view)) {
2257 report("Failed to load %s view", view->name);
2261 } else if ((reload || strcmp(view->vid, view->id)) &&
2262 !begin_update(view)) {
2263 report("Failed to load %s view", view->name);
2272 /* Maximize the current view. */
2273 memset(display, 0, sizeof(display));
2275 display[current_view] = view;
2278 /* Resize the view when switching between split- and full-screen,
2279 * or when switching between two different full-screen views. */
2280 if (nviews != displayed_views() ||
2281 (nviews == 1 && base_view != display[0]))
2284 if (split && prev->lineno - prev->offset >= prev->height) {
2285 /* Take the title line into account. */
2286 int lines = prev->lineno - prev->offset - prev->height + 1;
2288 /* Scroll the view that was split if the current line is
2289 * outside the new limited view. */
2290 do_scroll_view(prev, lines);
2293 if (prev && view != prev) {
2294 if (split && !backgrounded) {
2295 /* "Blur" the previous view. */
2296 update_view_title(prev);
2299 view->parent = prev;
2302 if (view->pipe && view->lines == 0) {
2303 /* Clear the old view and let the incremental updating refill
2312 /* If the view is backgrounded the above calls to report()
2313 * won't redraw the view title. */
2315 update_view_title(view);
2319 open_external_viewer(const char *cmd)
2321 def_prog_mode(); /* save current tty modes */
2322 endwin(); /* restore original tty modes */
2324 fprintf(stderr, "Press Enter to continue");
2331 open_mergetool(const char *file)
2333 char cmd[SIZEOF_STR];
2334 char file_sq[SIZEOF_STR];
2336 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2337 string_format(cmd, "git mergetool %s", file_sq)) {
2338 open_external_viewer(cmd);
2343 open_editor(bool from_root, const char *file)
2345 char cmd[SIZEOF_STR];
2346 char file_sq[SIZEOF_STR];
2348 char *prefix = from_root ? opt_cdup : "";
2350 editor = getenv("GIT_EDITOR");
2351 if (!editor && *opt_editor)
2352 editor = opt_editor;
2354 editor = getenv("VISUAL");
2356 editor = getenv("EDITOR");
2360 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2361 string_format(cmd, "%s %s%s", editor, prefix, file_sq)) {
2362 open_external_viewer(cmd);
2367 open_run_request(enum request request)
2369 struct run_request *req = get_run_request(request);
2370 char buf[SIZEOF_STR * 2];
2375 report("Unknown run request");
2383 char *next = strstr(cmd, "%(");
2384 int len = next - cmd;
2391 } else if (!strncmp(next, "%(head)", 7)) {
2394 } else if (!strncmp(next, "%(commit)", 9)) {
2397 } else if (!strncmp(next, "%(blob)", 7)) {
2401 report("Unknown replacement in run request: `%s`", req->cmd);
2405 if (!string_format_from(buf, &bufpos, "%.*s%s", len, cmd, value))
2409 next = strchr(next, ')') + 1;
2413 open_external_viewer(buf);
2417 * User request switch noodle
2421 view_driver(struct view *view, enum request request)
2425 if (request == REQ_NONE) {
2430 if (request > REQ_NONE) {
2431 open_run_request(request);
2435 if (view && view->lines) {
2436 request = view->ops->request(view, request, &view->line[view->lineno]);
2437 if (request == REQ_NONE)
2444 case REQ_MOVE_PAGE_UP:
2445 case REQ_MOVE_PAGE_DOWN:
2446 case REQ_MOVE_FIRST_LINE:
2447 case REQ_MOVE_LAST_LINE:
2448 move_view(view, request);
2451 case REQ_SCROLL_LINE_DOWN:
2452 case REQ_SCROLL_LINE_UP:
2453 case REQ_SCROLL_PAGE_DOWN:
2454 case REQ_SCROLL_PAGE_UP:
2455 scroll_view(view, request);
2460 report("No file chosen, press %s to open tree view",
2461 get_key(REQ_VIEW_TREE));
2464 open_view(view, request, OPEN_DEFAULT);
2467 case REQ_VIEW_PAGER:
2468 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2469 report("No pager content, press %s to run command from prompt",
2470 get_key(REQ_PROMPT));
2473 open_view(view, request, OPEN_DEFAULT);
2476 case REQ_VIEW_STAGE:
2477 if (!VIEW(REQ_VIEW_STAGE)->lines) {
2478 report("No stage content, press %s to open the status view and choose file",
2479 get_key(REQ_VIEW_STATUS));
2482 open_view(view, request, OPEN_DEFAULT);
2485 case REQ_VIEW_STATUS:
2486 if (opt_is_inside_work_tree == FALSE) {
2487 report("The status view requires a working tree");
2490 open_view(view, request, OPEN_DEFAULT);
2498 open_view(view, request, OPEN_DEFAULT);
2503 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2505 if ((view == VIEW(REQ_VIEW_DIFF) &&
2506 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2507 (view == VIEW(REQ_VIEW_STAGE) &&
2508 view->parent == VIEW(REQ_VIEW_STATUS)) ||
2509 (view == VIEW(REQ_VIEW_BLOB) &&
2510 view->parent == VIEW(REQ_VIEW_TREE))) {
2513 view = view->parent;
2514 line = view->lineno;
2515 move_view(view, request);
2516 if (view_is_displayed(view))
2517 update_view_title(view);
2518 if (line != view->lineno)
2519 view->ops->request(view, REQ_ENTER,
2520 &view->line[view->lineno]);
2523 move_view(view, request);
2529 int nviews = displayed_views();
2530 int next_view = (current_view + 1) % nviews;
2532 if (next_view == current_view) {
2533 report("Only one view is displayed");
2537 current_view = next_view;
2538 /* Blur out the title of the previous view. */
2539 update_view_title(view);
2544 report("Refreshing is not yet supported for the %s view", view->name);
2547 case REQ_TOGGLE_LINENO:
2548 opt_line_number = !opt_line_number;
2552 case REQ_TOGGLE_DATE:
2553 opt_date = !opt_date;
2557 case REQ_TOGGLE_AUTHOR:
2558 opt_author = !opt_author;
2562 case REQ_TOGGLE_REV_GRAPH:
2563 opt_rev_graph = !opt_rev_graph;
2567 case REQ_TOGGLE_REFS:
2568 opt_show_refs = !opt_show_refs;
2573 /* Always reload^Wrerun commands from the prompt. */
2574 open_view(view, opt_request, OPEN_RELOAD);
2578 case REQ_SEARCH_BACK:
2579 search_view(view, request);
2584 find_next(view, request);
2587 case REQ_STOP_LOADING:
2588 for (i = 0; i < ARRAY_SIZE(views); i++) {
2591 report("Stopped loading the %s view", view->name),
2596 case REQ_SHOW_VERSION:
2597 report("tig-%s (built %s)", TIG_VERSION, __DATE__);
2600 case REQ_SCREEN_RESIZE:
2603 case REQ_SCREEN_REDRAW:
2608 report("Nothing to edit");
2613 report("Nothing to enter");
2617 case REQ_VIEW_CLOSE:
2618 /* XXX: Mark closed views by letting view->parent point to the
2619 * view itself. Parents to closed view should never be
2622 view->parent->parent != view->parent) {
2623 memset(display, 0, sizeof(display));
2625 display[current_view] = view->parent;
2626 view->parent = view;
2636 /* An unknown key will show most commonly used commands. */
2637 report("Unknown key, press 'h' for help");
2650 pager_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2652 char *text = line->data;
2653 enum line_type type = line->type;
2656 wmove(view->win, lineno, 0);
2660 wchgat(view->win, -1, 0, type, NULL);
2663 attr = get_line_attr(type);
2664 wattrset(view->win, attr);
2666 if (opt_line_number || opt_tab_size < TABSIZE) {
2667 static char spaces[] = " ";
2668 int col_offset = 0, col = 0;
2670 if (opt_line_number) {
2671 unsigned long real_lineno = view->offset + lineno + 1;
2673 if (real_lineno == 1 ||
2674 (real_lineno % opt_num_interval) == 0) {
2675 wprintw(view->win, "%.*d", view->digits, real_lineno);
2678 waddnstr(view->win, spaces,
2679 MIN(view->digits, STRING_SIZE(spaces)));
2681 waddstr(view->win, ": ");
2682 col_offset = view->digits + 2;
2685 while (text && col_offset + col < view->width) {
2686 int cols_max = view->width - col_offset - col;
2690 if (*text == '\t') {
2692 assert(sizeof(spaces) > TABSIZE);
2694 cols = opt_tab_size - (col % opt_tab_size);
2697 text = strchr(text, '\t');
2698 cols = line ? text - pos : strlen(pos);
2701 waddnstr(view->win, pos, MIN(cols, cols_max));
2706 int tilde_attr = get_line_attr(LINE_MAIN_DELIM);
2708 draw_text(view, text, view->width, 0, TRUE, tilde_attr);
2715 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2717 char refbuf[SIZEOF_STR];
2721 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2724 pipe = popen(refbuf, "r");
2728 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2729 ref = chomp_string(ref);
2735 /* This is the only fatal call, since it can "corrupt" the buffer. */
2736 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2743 add_pager_refs(struct view *view, struct line *line)
2745 char buf[SIZEOF_STR];
2746 char *commit_id = (char *)line->data + STRING_SIZE("commit ");
2748 size_t bufpos = 0, refpos = 0;
2749 const char *sep = "Refs: ";
2750 bool is_tag = FALSE;
2752 assert(line->type == LINE_COMMIT);
2754 refs = get_refs(commit_id);
2756 if (view == VIEW(REQ_VIEW_DIFF))
2757 goto try_add_describe_ref;
2762 struct ref *ref = refs[refpos];
2763 char *fmt = ref->tag ? "%s[%s]" :
2764 ref->remote ? "%s<%s>" : "%s%s";
2766 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2771 } while (refs[refpos++]->next);
2773 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2774 try_add_describe_ref:
2775 /* Add <tag>-g<commit_id> "fake" reference. */
2776 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2783 if (!realloc_lines(view, view->line_size + 1))
2786 add_line_text(view, buf, LINE_PP_REFS);
2790 pager_read(struct view *view, char *data)
2797 line = add_line_text(view, data, get_line_type(data));
2801 if (line->type == LINE_COMMIT &&
2802 (view == VIEW(REQ_VIEW_DIFF) ||
2803 view == VIEW(REQ_VIEW_LOG)))
2804 add_pager_refs(view, line);
2810 pager_request(struct view *view, enum request request, struct line *line)
2814 if (request != REQ_ENTER)
2817 if (line->type == LINE_COMMIT &&
2818 (view == VIEW(REQ_VIEW_LOG) ||
2819 view == VIEW(REQ_VIEW_PAGER))) {
2820 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2824 /* Always scroll the view even if it was split. That way
2825 * you can use Enter to scroll through the log view and
2826 * split open each commit diff. */
2827 scroll_view(view, REQ_SCROLL_LINE_DOWN);
2829 /* FIXME: A minor workaround. Scrolling the view will call report("")
2830 * but if we are scrolling a non-current view this won't properly
2831 * update the view title. */
2833 update_view_title(view);
2839 pager_grep(struct view *view, struct line *line)
2842 char *text = line->data;
2847 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
2854 pager_select(struct view *view, struct line *line)
2856 if (line->type == LINE_COMMIT) {
2857 char *text = (char *)line->data + STRING_SIZE("commit ");
2859 if (view != VIEW(REQ_VIEW_PAGER))
2860 string_copy_rev(view->ref, text);
2861 string_copy_rev(ref_commit, text);
2865 static struct view_ops pager_ops = {
2881 help_open(struct view *view)
2884 int lines = ARRAY_SIZE(req_info) + 2;
2887 if (view->lines > 0)
2890 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2891 if (!req_info[i].request)
2894 lines += run_requests + 1;
2896 view->line = calloc(lines, sizeof(*view->line));
2900 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
2902 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2905 if (req_info[i].request == REQ_NONE)
2908 if (!req_info[i].request) {
2909 add_line_text(view, "", LINE_DEFAULT);
2910 add_line_text(view, req_info[i].help, LINE_DEFAULT);
2914 key = get_key(req_info[i].request);
2916 key = "(no key defined)";
2918 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
2921 add_line_text(view, buf, LINE_DEFAULT);
2925 add_line_text(view, "", LINE_DEFAULT);
2926 add_line_text(view, "External commands:", LINE_DEFAULT);
2929 for (i = 0; i < run_requests; i++) {
2930 struct run_request *req = get_run_request(REQ_NONE + i + 1);
2936 key = get_key_name(req->key);
2938 key = "(no key defined)";
2940 if (!string_format(buf, " %-10s %-14s `%s`",
2941 keymap_table[req->keymap].name,
2945 add_line_text(view, buf, LINE_DEFAULT);
2951 static struct view_ops help_ops = {
2966 struct tree_stack_entry {
2967 struct tree_stack_entry *prev; /* Entry below this in the stack */
2968 unsigned long lineno; /* Line number to restore */
2969 char *name; /* Position of name in opt_path */
2972 /* The top of the path stack. */
2973 static struct tree_stack_entry *tree_stack = NULL;
2974 unsigned long tree_lineno = 0;
2977 pop_tree_stack_entry(void)
2979 struct tree_stack_entry *entry = tree_stack;
2981 tree_lineno = entry->lineno;
2983 tree_stack = entry->prev;
2988 push_tree_stack_entry(char *name, unsigned long lineno)
2990 struct tree_stack_entry *entry = calloc(1, sizeof(*entry));
2991 size_t pathlen = strlen(opt_path);
2996 entry->prev = tree_stack;
2997 entry->name = opt_path + pathlen;
3000 if (!string_format_from(opt_path, &pathlen, "%s/", name)) {
3001 pop_tree_stack_entry();
3005 /* Move the current line to the first tree entry. */
3007 entry->lineno = lineno;
3010 /* Parse output from git-ls-tree(1):
3012 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
3013 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
3014 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
3015 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
3018 #define SIZEOF_TREE_ATTR \
3019 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
3021 #define TREE_UP_FORMAT "040000 tree %s\t.."
3024 tree_compare_entry(enum line_type type1, char *name1,
3025 enum line_type type2, char *name2)
3027 if (type1 != type2) {
3028 if (type1 == LINE_TREE_DIR)
3033 return strcmp(name1, name2);
3037 tree_read(struct view *view, char *text)
3039 size_t textlen = text ? strlen(text) : 0;
3040 char buf[SIZEOF_STR];
3042 enum line_type type;
3043 bool first_read = view->lines == 0;
3045 if (textlen <= SIZEOF_TREE_ATTR)
3048 type = text[STRING_SIZE("100644 ")] == 't'
3049 ? LINE_TREE_DIR : LINE_TREE_FILE;
3052 /* Add path info line */
3053 if (!string_format(buf, "Directory path /%s", opt_path) ||
3054 !realloc_lines(view, view->line_size + 1) ||
3055 !add_line_text(view, buf, LINE_DEFAULT))
3058 /* Insert "link" to parent directory. */
3060 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
3061 !realloc_lines(view, view->line_size + 1) ||
3062 !add_line_text(view, buf, LINE_TREE_DIR))
3067 /* Strip the path part ... */
3069 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
3070 size_t striplen = strlen(opt_path);
3071 char *path = text + SIZEOF_TREE_ATTR;
3073 if (pathlen > striplen)
3074 memmove(path, path + striplen,
3075 pathlen - striplen + 1);
3078 /* Skip "Directory ..." and ".." line. */
3079 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
3080 struct line *line = &view->line[pos];
3081 char *path1 = ((char *) line->data) + SIZEOF_TREE_ATTR;
3082 char *path2 = text + SIZEOF_TREE_ATTR;
3083 int cmp = tree_compare_entry(line->type, path1, type, path2);
3088 text = strdup(text);
3092 if (view->lines > pos)
3093 memmove(&view->line[pos + 1], &view->line[pos],
3094 (view->lines - pos) * sizeof(*line));
3096 line = &view->line[pos];
3103 if (!add_line_text(view, text, type))
3106 if (tree_lineno > view->lineno) {
3107 view->lineno = tree_lineno;
3115 tree_request(struct view *view, enum request request, struct line *line)
3117 enum open_flags flags;
3119 if (request == REQ_TREE_PARENT) {
3122 request = REQ_ENTER;
3123 line = &view->line[1];
3125 /* quit view if at top of tree */
3126 return REQ_VIEW_CLOSE;
3129 if (request != REQ_ENTER)
3132 /* Cleanup the stack if the tree view is at a different tree. */
3133 while (!*opt_path && tree_stack)
3134 pop_tree_stack_entry();
3136 switch (line->type) {
3138 /* Depending on whether it is a subdir or parent (updir?) link
3139 * mangle the path buffer. */
3140 if (line == &view->line[1] && *opt_path) {
3141 pop_tree_stack_entry();
3144 char *data = line->data;
3145 char *basename = data + SIZEOF_TREE_ATTR;
3147 push_tree_stack_entry(basename, view->lineno);
3150 /* Trees and subtrees share the same ID, so they are not not
3151 * unique like blobs. */
3152 flags = OPEN_RELOAD;
3153 request = REQ_VIEW_TREE;
3156 case LINE_TREE_FILE:
3157 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3158 request = REQ_VIEW_BLOB;
3165 open_view(view, request, flags);
3166 if (request == REQ_VIEW_TREE) {
3167 view->lineno = tree_lineno;
3174 tree_select(struct view *view, struct line *line)
3176 char *text = (char *)line->data + STRING_SIZE("100644 blob ");
3178 if (line->type == LINE_TREE_FILE) {
3179 string_copy_rev(ref_blob, text);
3181 } else if (line->type != LINE_TREE_DIR) {
3185 string_copy_rev(view->ref, text);
3188 static struct view_ops tree_ops = {
3199 blob_read(struct view *view, char *line)
3201 return add_line_text(view, line, LINE_DEFAULT) != NULL;
3204 static struct view_ops blob_ops = {
3223 char rev[SIZEOF_REV];
3224 char name[SIZEOF_STR];
3228 char rev[SIZEOF_REV];
3229 char name[SIZEOF_STR];
3233 static struct status stage_status;
3234 static enum line_type stage_line_type;
3236 /* Get fields from the diff line:
3237 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
3240 status_get_diff(struct status *file, char *buf, size_t bufsize)
3242 char *old_mode = buf + 1;
3243 char *new_mode = buf + 8;
3244 char *old_rev = buf + 15;
3245 char *new_rev = buf + 56;
3246 char *status = buf + 97;
3249 old_mode[-1] != ':' ||
3250 new_mode[-1] != ' ' ||
3251 old_rev[-1] != ' ' ||
3252 new_rev[-1] != ' ' ||
3256 file->status = *status;
3258 string_copy_rev(file->old.rev, old_rev);
3259 string_copy_rev(file->new.rev, new_rev);
3261 file->old.mode = strtoul(old_mode, NULL, 8);
3262 file->new.mode = strtoul(new_mode, NULL, 8);
3264 file->old.name[0] = file->new.name[0] = 0;
3270 status_run(struct view *view, const char cmd[], bool diff, enum line_type type)
3272 struct status *file = NULL;
3273 struct status *unmerged = NULL;
3274 char buf[SIZEOF_STR * 4];
3278 pipe = popen(cmd, "r");
3282 add_line_data(view, NULL, type);
3284 while (!feof(pipe) && !ferror(pipe)) {
3288 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
3291 bufsize += readsize;
3293 /* Process while we have NUL chars. */
3294 while ((sep = memchr(buf, 0, bufsize))) {
3295 size_t sepsize = sep - buf + 1;
3298 if (!realloc_lines(view, view->line_size + 1))
3301 file = calloc(1, sizeof(*file));
3305 add_line_data(view, file, type);
3308 /* Parse diff info part. */
3312 } else if (!file->status) {
3313 if (!status_get_diff(file, buf, sepsize))
3317 memmove(buf, sep + 1, bufsize);
3319 sep = memchr(buf, 0, bufsize);
3322 sepsize = sep - buf + 1;
3324 /* Collapse all 'M'odified entries that
3325 * follow a associated 'U'nmerged entry.
3327 if (file->status == 'U') {
3330 } else if (unmerged) {
3331 int collapse = !strcmp(buf, unmerged->new.name);
3342 /* Grab the old name for rename/copy. */
3343 if (!*file->old.name &&
3344 (file->status == 'R' || file->status == 'C')) {
3345 sepsize = sep - buf + 1;
3346 string_ncopy(file->old.name, buf, sepsize);
3348 memmove(buf, sep + 1, bufsize);
3350 sep = memchr(buf, 0, bufsize);
3353 sepsize = sep - buf + 1;
3356 /* git-ls-files just delivers a NUL separated
3357 * list of file names similar to the second half
3358 * of the git-diff-* output. */
3359 string_ncopy(file->new.name, buf, sepsize);
3360 if (!*file->old.name)
3361 string_copy(file->old.name, file->new.name);
3363 memmove(buf, sep + 1, bufsize);
3374 if (!view->line[view->lines - 1].data)
3375 add_line_data(view, NULL, LINE_STAT_NONE);
3381 /* Don't show unmerged entries in the staged section. */
3382 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached -M HEAD"
3383 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
3384 #define STATUS_LIST_OTHER_CMD \
3385 "git ls-files -z --others --exclude-per-directory=.gitignore"
3387 #define STATUS_DIFF_INDEX_SHOW_CMD \
3388 "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s %s 2>/dev/null"
3390 #define STATUS_DIFF_FILES_SHOW_CMD \
3391 "git diff-files --root --patch-with-stat -C -M -- %s %s 2>/dev/null"
3393 /* First parse staged info using git-diff-index(1), then parse unstaged
3394 * info using git-diff-files(1), and finally untracked files using
3395 * git-ls-files(1). */
3397 status_open(struct view *view)
3399 struct stat statbuf;
3400 char exclude[SIZEOF_STR];
3401 char cmd[SIZEOF_STR];
3402 unsigned long prev_lineno = view->lineno;
3405 for (i = 0; i < view->lines; i++)
3406 free(view->line[i].data);
3408 view->lines = view->line_size = view->lineno = 0;
3411 if (!realloc_lines(view, view->line_size + 6))
3414 if (!string_format(exclude, "%s/info/exclude", opt_git_dir))
3417 string_copy(cmd, STATUS_LIST_OTHER_CMD);
3419 if (stat(exclude, &statbuf) >= 0) {
3420 size_t cmdsize = strlen(cmd);
3422 if (!string_format_from(cmd, &cmdsize, " %s", "--exclude-from=") ||
3423 sq_quote(cmd, cmdsize, exclude) >= sizeof(cmd))
3427 system("git update-index -q --refresh");
3429 if (!status_run(view, STATUS_DIFF_INDEX_CMD, TRUE, LINE_STAT_STAGED) ||
3430 !status_run(view, STATUS_DIFF_FILES_CMD, TRUE, LINE_STAT_UNSTAGED) ||
3431 !status_run(view, cmd, FALSE, LINE_STAT_UNTRACKED))
3434 /* If all went well restore the previous line number to stay in
3436 if (prev_lineno < view->lines)
3437 view->lineno = prev_lineno;
3439 view->lineno = view->lines - 1;
3445 status_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
3447 struct status *status = line->data;
3448 int tilde_attr = get_line_attr(LINE_MAIN_DELIM);
3450 wmove(view->win, lineno, 0);
3453 wattrset(view->win, get_line_attr(LINE_CURSOR));
3454 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
3457 } else if (!status && line->type != LINE_STAT_NONE) {
3458 wattrset(view->win, get_line_attr(LINE_STAT_SECTION));
3459 wchgat(view->win, -1, 0, LINE_STAT_SECTION, NULL);
3462 wattrset(view->win, get_line_attr(line->type));
3468 switch (line->type) {
3469 case LINE_STAT_STAGED:
3470 text = "Changes to be committed:";
3473 case LINE_STAT_UNSTAGED:
3474 text = "Changed but not updated:";
3477 case LINE_STAT_UNTRACKED:
3478 text = "Untracked files:";
3481 case LINE_STAT_NONE:
3482 text = " (no files)";
3489 draw_text(view, text, view->width, 0, TRUE, tilde_attr);
3493 waddch(view->win, status->status);
3495 wattrset(view->win, A_NORMAL);
3496 wmove(view->win, lineno, 4);
3497 if (view->width < 5)
3500 draw_text(view, status->new.name, view->width - 5, 5, TRUE, tilde_attr);
3505 status_enter(struct view *view, struct line *line)
3507 struct status *status = line->data;
3508 char oldpath[SIZEOF_STR] = "";
3509 char newpath[SIZEOF_STR] = "";
3513 if (line->type == LINE_STAT_NONE ||
3514 (!status && line[1].type == LINE_STAT_NONE)) {
3515 report("No file to diff");
3520 if (sq_quote(oldpath, 0, status->old.name) >= sizeof(oldpath))
3522 /* Diffs for unmerged entries are empty when pasing the
3523 * new path, so leave it empty. */
3524 if (status->status != 'U' &&
3525 sq_quote(newpath, 0, status->new.name) >= sizeof(newpath))
3530 line->type != LINE_STAT_UNTRACKED &&
3531 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
3534 switch (line->type) {
3535 case LINE_STAT_STAGED:
3536 if (!string_format_from(opt_cmd, &cmdsize,
3537 STATUS_DIFF_INDEX_SHOW_CMD, oldpath, newpath))
3540 info = "Staged changes to %s";
3542 info = "Staged changes";
3545 case LINE_STAT_UNSTAGED:
3546 if (!string_format_from(opt_cmd, &cmdsize,
3547 STATUS_DIFF_FILES_SHOW_CMD, oldpath, newpath))
3550 info = "Unstaged changes to %s";
3552 info = "Unstaged changes";
3555 case LINE_STAT_UNTRACKED:
3561 report("No file to show");
3565 opt_pipe = fopen(status->new.name, "r");
3566 info = "Untracked file %s";
3570 die("line type %d not handled in switch", line->type);
3573 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | OPEN_SPLIT);
3574 if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) {
3576 stage_status = *status;
3578 memset(&stage_status, 0, sizeof(stage_status));
3581 stage_line_type = line->type;
3582 string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.new.name);
3590 status_update_file(struct view *view, struct status *status, enum line_type type)
3592 char cmd[SIZEOF_STR];
3593 char buf[SIZEOF_STR];
3600 type != LINE_STAT_UNTRACKED &&
3601 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
3605 case LINE_STAT_STAGED:
3606 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
3609 status->old.name, 0))
3612 string_add(cmd, cmdsize, "git update-index -z --index-info");
3615 case LINE_STAT_UNSTAGED:
3616 case LINE_STAT_UNTRACKED:
3617 if (!string_format_from(buf, &bufsize, "%s%c", status->new.name, 0))
3620 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
3624 die("line type %d not handled in switch", type);
3627 pipe = popen(cmd, "w");
3631 while (!ferror(pipe) && written < bufsize) {
3632 written += fwrite(buf + written, 1, bufsize - written, pipe);
3637 if (written != bufsize)
3644 status_update(struct view *view)
3646 struct line *line = &view->line[view->lineno];
3648 assert(view->lines);
3651 while (++line < view->line + view->lines && line->data) {
3652 if (!status_update_file(view, line->data, line->type))
3653 report("Failed to update file status");
3656 if (!line[-1].data) {
3657 report("Nothing to update");
3661 } else if (!status_update_file(view, line->data, line->type)) {
3662 report("Failed to update file status");
3667 status_request(struct view *view, enum request request, struct line *line)
3669 struct status *status = line->data;
3672 case REQ_STATUS_UPDATE:
3673 status_update(view);
3676 case REQ_STATUS_MERGE:
3677 if (!status || status->status != 'U') {
3678 report("Merging only possible for files with unmerged status ('U').");
3681 open_mergetool(status->new.name);
3688 open_editor(status->status != '?', status->new.name);
3692 /* After returning the status view has been split to
3693 * show the stage view. No further reloading is
3695 status_enter(view, line);
3699 /* Simply reload the view. */
3706 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
3712 status_select(struct view *view, struct line *line)
3714 struct status *status = line->data;
3715 char file[SIZEOF_STR] = "all files";
3719 if (status && !string_format(file, "'%s'", status->new.name))
3722 if (!status && line[1].type == LINE_STAT_NONE)
3725 switch (line->type) {
3726 case LINE_STAT_STAGED:
3727 text = "Press %s to unstage %s for commit";
3730 case LINE_STAT_UNSTAGED:
3731 text = "Press %s to stage %s for commit";
3734 case LINE_STAT_UNTRACKED:
3735 text = "Press %s to stage %s for addition";
3738 case LINE_STAT_NONE:
3739 text = "Nothing to update";
3743 die("line type %d not handled in switch", line->type);
3746 if (status && status->status == 'U') {
3747 text = "Press %s to resolve conflict in %s";
3748 key = get_key(REQ_STATUS_MERGE);
3751 key = get_key(REQ_STATUS_UPDATE);
3754 string_format(view->ref, text, key, file);
3758 status_grep(struct view *view, struct line *line)
3760 struct status *status = line->data;
3761 enum { S_STATUS, S_NAME, S_END } state;
3768 for (state = S_STATUS; state < S_END; state++) {
3772 case S_NAME: text = status->new.name; break;
3774 buf[0] = status->status;
3782 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3789 static struct view_ops status_ops = {
3801 stage_diff_line(FILE *pipe, struct line *line)
3803 char *buf = line->data;
3804 size_t bufsize = strlen(buf);
3807 while (!ferror(pipe) && written < bufsize) {
3808 written += fwrite(buf + written, 1, bufsize - written, pipe);
3813 return written == bufsize;
3816 static struct line *
3817 stage_diff_hdr(struct view *view, struct line *line)
3819 int diff_hdr_dir = line->type == LINE_DIFF_CHUNK ? -1 : 1;
3820 struct line *diff_hdr;
3822 if (line->type == LINE_DIFF_CHUNK)
3823 diff_hdr = line - 1;
3825 diff_hdr = view->line + 1;
3827 while (diff_hdr > view->line && diff_hdr < view->line + view->lines) {
3828 if (diff_hdr->type == LINE_DIFF_HEADER)
3831 diff_hdr += diff_hdr_dir;
3838 stage_update_chunk(struct view *view, struct line *line)
3840 char cmd[SIZEOF_STR];
3842 struct line *diff_hdr, *diff_chunk, *diff_end;
3845 diff_hdr = stage_diff_hdr(view, line);
3850 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
3853 if (!string_format_from(cmd, &cmdsize,
3854 "git apply --cached %s - && "
3855 "git update-index -q --unmerged --refresh 2>/dev/null",
3856 stage_line_type == LINE_STAT_STAGED ? "-R" : ""))
3859 pipe = popen(cmd, "w");
3863 diff_end = view->line + view->lines;
3864 if (line->type != LINE_DIFF_CHUNK) {
3865 diff_chunk = diff_hdr;
3868 for (diff_chunk = line + 1; diff_chunk < diff_end; diff_chunk++)
3869 if (diff_chunk->type == LINE_DIFF_CHUNK ||
3870 diff_chunk->type == LINE_DIFF_HEADER)
3871 diff_end = diff_chunk;
3875 while (diff_hdr->type != LINE_DIFF_CHUNK) {
3876 switch (diff_hdr->type) {
3877 case LINE_DIFF_HEADER:
3878 case LINE_DIFF_INDEX:
3888 if (!stage_diff_line(pipe, diff_hdr++)) {
3895 while (diff_chunk < diff_end && stage_diff_line(pipe, diff_chunk))
3900 if (diff_chunk != diff_end)
3907 stage_update(struct view *view, struct line *line)
3909 if (stage_line_type != LINE_STAT_UNTRACKED &&
3910 (line->type == LINE_DIFF_CHUNK || !stage_status.status)) {
3911 if (!stage_update_chunk(view, line)) {
3912 report("Failed to apply chunk");
3916 } else if (!status_update_file(view, &stage_status, stage_line_type)) {
3917 report("Failed to update file");
3921 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
3923 view = VIEW(REQ_VIEW_STATUS);
3924 if (view_is_displayed(view))
3925 status_enter(view, &view->line[view->lineno]);
3929 stage_request(struct view *view, enum request request, struct line *line)
3932 case REQ_STATUS_UPDATE:
3933 stage_update(view, line);
3937 if (!stage_status.new.name[0])
3940 open_editor(stage_status.status != '?', stage_status.new.name);
3944 pager_request(view, request, line);
3954 static struct view_ops stage_ops = {
3970 char id[SIZEOF_REV]; /* SHA1 ID. */
3971 char title[128]; /* First line of the commit message. */
3972 char author[75]; /* Author of the commit. */
3973 struct tm time; /* Date from the author ident. */
3974 struct ref **refs; /* Repository references. */
3975 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
3976 size_t graph_size; /* The width of the graph array. */
3979 /* Size of rev graph with no "padding" columns */
3980 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
3983 struct rev_graph *prev, *next, *parents;
3984 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
3986 struct commit *commit;
3988 unsigned int boundary:1;
3991 /* Parents of the commit being visualized. */
3992 static struct rev_graph graph_parents[4];
3994 /* The current stack of revisions on the graph. */
3995 static struct rev_graph graph_stacks[4] = {
3996 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
3997 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
3998 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
3999 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
4003 graph_parent_is_merge(struct rev_graph *graph)
4005 return graph->parents->size > 1;
4009 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
4011 struct commit *commit = graph->commit;
4013 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
4014 commit->graph[commit->graph_size++] = symbol;
4018 done_rev_graph(struct rev_graph *graph)
4020 if (graph_parent_is_merge(graph) &&
4021 graph->pos < graph->size - 1 &&
4022 graph->next->size == graph->size + graph->parents->size - 1) {
4023 size_t i = graph->pos + graph->parents->size - 1;
4025 graph->commit->graph_size = i * 2;
4026 while (i < graph->next->size - 1) {
4027 append_to_rev_graph(graph, ' ');
4028 append_to_rev_graph(graph, '\\');
4033 graph->size = graph->pos = 0;
4034 graph->commit = NULL;
4035 memset(graph->parents, 0, sizeof(*graph->parents));
4039 push_rev_graph(struct rev_graph *graph, char *parent)
4043 /* "Collapse" duplicate parents lines.
4045 * FIXME: This needs to also update update the drawn graph but
4046 * for now it just serves as a method for pruning graph lines. */
4047 for (i = 0; i < graph->size; i++)
4048 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
4051 if (graph->size < SIZEOF_REVITEMS) {
4052 string_copy_rev(graph->rev[graph->size++], parent);
4057 get_rev_graph_symbol(struct rev_graph *graph)
4061 if (graph->boundary)
4062 symbol = REVGRAPH_BOUND;
4063 else if (graph->parents->size == 0)
4064 symbol = REVGRAPH_INIT;
4065 else if (graph_parent_is_merge(graph))
4066 symbol = REVGRAPH_MERGE;
4067 else if (graph->pos >= graph->size)
4068 symbol = REVGRAPH_BRANCH;
4070 symbol = REVGRAPH_COMMIT;
4076 draw_rev_graph(struct rev_graph *graph)
4079 chtype separator, line;
4081 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
4082 static struct rev_filler fillers[] = {
4083 { ' ', REVGRAPH_LINE },
4088 chtype symbol = get_rev_graph_symbol(graph);
4089 struct rev_filler *filler;
4092 filler = &fillers[DEFAULT];
4094 for (i = 0; i < graph->pos; i++) {
4095 append_to_rev_graph(graph, filler->line);
4096 if (graph_parent_is_merge(graph->prev) &&
4097 graph->prev->pos == i)
4098 filler = &fillers[RSHARP];
4100 append_to_rev_graph(graph, filler->separator);
4103 /* Place the symbol for this revision. */
4104 append_to_rev_graph(graph, symbol);
4106 if (graph->prev->size > graph->size)
4107 filler = &fillers[RDIAG];
4109 filler = &fillers[DEFAULT];
4113 for (; i < graph->size; i++) {
4114 append_to_rev_graph(graph, filler->separator);
4115 append_to_rev_graph(graph, filler->line);
4116 if (graph_parent_is_merge(graph->prev) &&
4117 i < graph->prev->pos + graph->parents->size)
4118 filler = &fillers[RSHARP];
4119 if (graph->prev->size > graph->size)
4120 filler = &fillers[LDIAG];
4123 if (graph->prev->size > graph->size) {
4124 append_to_rev_graph(graph, filler->separator);
4125 if (filler->line != ' ')
4126 append_to_rev_graph(graph, filler->line);
4130 /* Prepare the next rev graph */
4132 prepare_rev_graph(struct rev_graph *graph)
4136 /* First, traverse all lines of revisions up to the active one. */
4137 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
4138 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
4141 push_rev_graph(graph->next, graph->rev[graph->pos]);
4144 /* Interleave the new revision parent(s). */
4145 for (i = 0; !graph->boundary && i < graph->parents->size; i++)
4146 push_rev_graph(graph->next, graph->parents->rev[i]);
4148 /* Lastly, put any remaining revisions. */
4149 for (i = graph->pos + 1; i < graph->size; i++)
4150 push_rev_graph(graph->next, graph->rev[i]);
4154 update_rev_graph(struct rev_graph *graph)
4156 /* If this is the finalizing update ... */
4158 prepare_rev_graph(graph);
4160 /* Graph visualization needs a one rev look-ahead,
4161 * so the first update doesn't visualize anything. */
4162 if (!graph->prev->commit)
4165 draw_rev_graph(graph->prev);
4166 done_rev_graph(graph->prev->prev);
4175 main_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
4177 char buf[DATE_COLS + 1];
4178 struct commit *commit = line->data;
4179 enum line_type type;
4185 if (!*commit->author)
4188 space = view->width;
4189 wmove(view->win, lineno, col);
4193 wattrset(view->win, get_line_attr(type));
4194 wchgat(view->win, -1, 0, type, NULL);
4197 type = LINE_MAIN_COMMIT;
4198 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
4199 tilde_attr = get_line_attr(LINE_MAIN_DELIM);
4205 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
4207 view, buf, view->width - col, col, FALSE, tilde_attr);
4209 view, " ", view->width - col - n, col + n, FALSE,
4213 wmove(view->win, lineno, col);
4214 if (col >= view->width)
4217 if (type != LINE_CURSOR)
4218 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
4223 max_len = view->width - col;
4224 if (max_len > AUTHOR_COLS - 1)
4225 max_len = AUTHOR_COLS - 1;
4227 view, commit->author, max_len, col, TRUE, tilde_attr);
4229 if (col >= view->width)
4233 if (opt_rev_graph && commit->graph_size) {
4234 size_t graph_size = view->width - col;
4237 if (type != LINE_CURSOR)
4238 wattrset(view->win, get_line_attr(LINE_MAIN_REVGRAPH));
4239 wmove(view->win, lineno, col);
4240 if (graph_size > commit->graph_size)
4241 graph_size = commit->graph_size;
4242 /* Using waddch() instead of waddnstr() ensures that
4243 * they'll be rendered correctly for the cursor line. */
4244 for (i = 0; i < graph_size; i++)
4245 waddch(view->win, commit->graph[i]);
4247 col += commit->graph_size + 1;
4248 if (col >= view->width)
4250 waddch(view->win, ' ');
4252 if (type != LINE_CURSOR)
4253 wattrset(view->win, A_NORMAL);
4255 wmove(view->win, lineno, col);
4257 if (opt_show_refs && commit->refs) {
4261 if (type == LINE_CURSOR)
4263 else if (commit->refs[i]->tag)
4264 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
4265 else if (commit->refs[i]->remote)
4266 wattrset(view->win, get_line_attr(LINE_MAIN_REMOTE));
4268 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
4271 view, "[", view->width - col, col, TRUE,
4274 view, commit->refs[i]->name, view->width - col,
4275 col, TRUE, tilde_attr);
4277 view, "]", view->width - col, col, TRUE,
4279 if (type != LINE_CURSOR)
4280 wattrset(view->win, A_NORMAL);
4282 view, " ", view->width - col, col, TRUE,
4284 if (col >= view->width)
4286 } while (commit->refs[i++]->next);
4289 if (type != LINE_CURSOR)
4290 wattrset(view->win, get_line_attr(type));
4293 view, commit->title, view->width - col, col, TRUE, tilde_attr);
4298 /* Reads git log --pretty=raw output and parses it into the commit struct. */
4300 main_read(struct view *view, char *line)
4302 static struct rev_graph *graph = graph_stacks;
4303 enum line_type type;
4304 struct commit *commit;
4307 update_rev_graph(graph);
4311 type = get_line_type(line);
4312 if (type == LINE_COMMIT) {
4313 commit = calloc(1, sizeof(struct commit));
4317 line += STRING_SIZE("commit ");
4319 graph->boundary = 1;
4323 string_copy_rev(commit->id, line);
4324 commit->refs = get_refs(commit->id);
4325 graph->commit = commit;
4326 add_line_data(view, commit, LINE_MAIN_COMMIT);
4332 commit = view->line[view->lines - 1].data;
4336 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
4341 /* Parse author lines where the name may be empty:
4342 * author <email@address.tld> 1138474660 +0100
4344 char *ident = line + STRING_SIZE("author ");
4345 char *nameend = strchr(ident, '<');
4346 char *emailend = strchr(ident, '>');
4348 if (!nameend || !emailend)
4351 update_rev_graph(graph);
4352 graph = graph->next;
4354 *nameend = *emailend = 0;
4355 ident = chomp_string(ident);
4357 ident = chomp_string(nameend + 1);
4362 string_ncopy(commit->author, ident, strlen(ident));
4364 /* Parse epoch and timezone */
4365 if (emailend[1] == ' ') {
4366 char *secs = emailend + 2;
4367 char *zone = strchr(secs, ' ');
4368 time_t time = (time_t) atol(secs);
4370 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
4374 tz = ('0' - zone[1]) * 60 * 60 * 10;
4375 tz += ('0' - zone[2]) * 60 * 60;
4376 tz += ('0' - zone[3]) * 60;
4377 tz += ('0' - zone[4]) * 60;
4385 gmtime_r(&time, &commit->time);
4390 /* Fill in the commit title if it has not already been set. */
4391 if (commit->title[0])
4394 /* Require titles to start with a non-space character at the
4395 * offset used by git log. */
4396 if (strncmp(line, " ", 4))
4399 /* Well, if the title starts with a whitespace character,
4400 * try to be forgiving. Otherwise we end up with no title. */
4401 while (isspace(*line))
4405 /* FIXME: More graceful handling of titles; append "..." to
4406 * shortened titles, etc. */
4408 string_ncopy(commit->title, line, strlen(line));
4415 main_request(struct view *view, enum request request, struct line *line)
4417 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
4419 if (request == REQ_ENTER)
4420 open_view(view, REQ_VIEW_DIFF, flags);
4428 main_grep(struct view *view, struct line *line)
4430 struct commit *commit = line->data;
4431 enum { S_TITLE, S_AUTHOR, S_DATE, S_END } state;
4432 char buf[DATE_COLS + 1];
4435 for (state = S_TITLE; state < S_END; state++) {
4439 case S_TITLE: text = commit->title; break;
4440 case S_AUTHOR: text = commit->author; break;
4442 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
4451 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4459 main_select(struct view *view, struct line *line)
4461 struct commit *commit = line->data;
4463 string_copy_rev(view->ref, commit->id);
4464 string_copy_rev(ref_commit, view->ref);
4467 static struct view_ops main_ops = {
4479 * Unicode / UTF-8 handling
4481 * NOTE: Much of the following code for dealing with unicode is derived from
4482 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
4483 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
4486 /* I've (over)annotated a lot of code snippets because I am not entirely
4487 * confident that the approach taken by this small UTF-8 interface is correct.
4491 unicode_width(unsigned long c)
4494 (c <= 0x115f /* Hangul Jamo */
4497 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
4499 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
4500 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
4501 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
4502 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
4503 || (c >= 0xffe0 && c <= 0xffe6)
4504 || (c >= 0x20000 && c <= 0x2fffd)
4505 || (c >= 0x30000 && c <= 0x3fffd)))
4509 return opt_tab_size;
4514 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
4515 * Illegal bytes are set one. */
4516 static const unsigned char utf8_bytes[256] = {
4517 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,
4518 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,
4519 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,
4520 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,
4521 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,
4522 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,
4523 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,
4524 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,
4527 /* Decode UTF-8 multi-byte representation into a unicode character. */
4528 static inline unsigned long
4529 utf8_to_unicode(const char *string, size_t length)
4531 unsigned long unicode;
4535 unicode = string[0];
4538 unicode = (string[0] & 0x1f) << 6;
4539 unicode += (string[1] & 0x3f);
4542 unicode = (string[0] & 0x0f) << 12;
4543 unicode += ((string[1] & 0x3f) << 6);
4544 unicode += (string[2] & 0x3f);
4547 unicode = (string[0] & 0x0f) << 18;
4548 unicode += ((string[1] & 0x3f) << 12);
4549 unicode += ((string[2] & 0x3f) << 6);
4550 unicode += (string[3] & 0x3f);
4553 unicode = (string[0] & 0x0f) << 24;
4554 unicode += ((string[1] & 0x3f) << 18);
4555 unicode += ((string[2] & 0x3f) << 12);
4556 unicode += ((string[3] & 0x3f) << 6);
4557 unicode += (string[4] & 0x3f);
4560 unicode = (string[0] & 0x01) << 30;
4561 unicode += ((string[1] & 0x3f) << 24);
4562 unicode += ((string[2] & 0x3f) << 18);
4563 unicode += ((string[3] & 0x3f) << 12);
4564 unicode += ((string[4] & 0x3f) << 6);
4565 unicode += (string[5] & 0x3f);
4568 die("Invalid unicode length");
4571 /* Invalid characters could return the special 0xfffd value but NUL
4572 * should be just as good. */
4573 return unicode > 0xffff ? 0 : unicode;
4576 /* Calculates how much of string can be shown within the given maximum width
4577 * and sets trimmed parameter to non-zero value if all of string could not be
4578 * shown. If the reserve flag is TRUE, it will reserve at least one
4579 * trailing character, which can be useful when drawing a delimiter.
4581 * Returns the number of bytes to output from string to satisfy max_width. */
4583 utf8_length(const char *string, size_t max_width, int *trimmed, bool reserve)
4585 const char *start = string;
4586 const char *end = strchr(string, '\0');
4587 unsigned char last_bytes = 0;
4592 while (string < end) {
4593 int c = *(unsigned char *) string;
4594 unsigned char bytes = utf8_bytes[c];
4596 unsigned long unicode;
4598 if (string + bytes > end)
4601 /* Change representation to figure out whether
4602 * it is a single- or double-width character. */
4604 unicode = utf8_to_unicode(string, bytes);
4605 /* FIXME: Graceful handling of invalid unicode character. */
4609 ucwidth = unicode_width(unicode);
4611 if (width > max_width) {
4613 if (reserve && width - ucwidth == max_width) {
4614 string -= last_bytes;
4623 return string - start;
4631 /* Whether or not the curses interface has been initialized. */
4632 static bool cursed = FALSE;
4634 /* The status window is used for polling keystrokes. */
4635 static WINDOW *status_win;
4637 static bool status_empty = TRUE;
4639 /* Update status and title window. */
4641 report(const char *msg, ...)
4643 struct view *view = display[current_view];
4649 char buf[SIZEOF_STR];
4652 va_start(args, msg);
4653 if (vsnprintf(buf, sizeof(buf), msg, args) >= sizeof(buf)) {
4654 buf[sizeof(buf) - 1] = 0;
4655 buf[sizeof(buf) - 2] = '.';
4656 buf[sizeof(buf) - 3] = '.';
4657 buf[sizeof(buf) - 4] = '.';
4663 if (!status_empty || *msg) {
4666 va_start(args, msg);
4668 wmove(status_win, 0, 0);
4670 vwprintw(status_win, msg, args);
4671 status_empty = FALSE;
4673 status_empty = TRUE;
4675 wclrtoeol(status_win);
4676 wrefresh(status_win);
4681 update_view_title(view);
4682 update_display_cursor(view);
4685 /* Controls when nodelay should be in effect when polling user input. */
4687 set_nonblocking_input(bool loading)
4689 static unsigned int loading_views;
4691 if ((loading == FALSE && loading_views-- == 1) ||
4692 (loading == TRUE && loading_views++ == 0))
4693 nodelay(status_win, loading);
4701 /* Initialize the curses library */
4702 if (isatty(STDIN_FILENO)) {
4703 cursed = !!initscr();
4705 /* Leave stdin and stdout alone when acting as a pager. */
4706 FILE *io = fopen("/dev/tty", "r+");
4709 die("Failed to open /dev/tty");
4710 cursed = !!newterm(NULL, io, io);
4714 die("Failed to initialize curses");
4716 nonl(); /* Tell curses not to do NL->CR/NL on output */
4717 cbreak(); /* Take input chars one at a time, no wait for \n */
4718 noecho(); /* Don't echo input */
4719 leaveok(stdscr, TRUE);
4724 getmaxyx(stdscr, y, x);
4725 status_win = newwin(1, 0, y - 1, 0);
4727 die("Failed to create status window");
4729 /* Enable keyboard mapping */
4730 keypad(status_win, TRUE);
4731 wbkgdset(status_win, get_line_attr(LINE_STATUS));
4735 read_prompt(const char *prompt)
4737 enum { READING, STOP, CANCEL } status = READING;
4738 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
4741 while (status == READING) {
4747 foreach_view (view, i)
4752 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
4753 wclrtoeol(status_win);
4755 /* Refresh, accept single keystroke of input */
4756 key = wgetch(status_win);
4761 status = pos ? STOP : CANCEL;
4779 if (pos >= sizeof(buf)) {
4780 report("Input string too long");
4785 buf[pos++] = (char) key;
4789 /* Clear the status window */
4790 status_empty = FALSE;
4793 if (status == CANCEL)
4802 * Repository references
4805 static struct ref *refs;
4806 static size_t refs_size;
4808 /* Id <-> ref store */
4809 static struct ref ***id_refs;
4810 static size_t id_refs_size;
4812 static struct ref **
4815 struct ref ***tmp_id_refs;
4816 struct ref **ref_list = NULL;
4817 size_t ref_list_size = 0;
4820 for (i = 0; i < id_refs_size; i++)
4821 if (!strcmp(id, id_refs[i][0]->id))
4824 tmp_id_refs = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs));
4828 id_refs = tmp_id_refs;
4830 for (i = 0; i < refs_size; i++) {
4833 if (strcmp(id, refs[i].id))
4836 tmp = realloc(ref_list, (ref_list_size + 1) * sizeof(*ref_list));
4844 if (ref_list_size > 0)
4845 ref_list[ref_list_size - 1]->next = 1;
4846 ref_list[ref_list_size] = &refs[i];
4848 /* XXX: The properties of the commit chains ensures that we can
4849 * safely modify the shared ref. The repo references will
4850 * always be similar for the same id. */
4851 ref_list[ref_list_size]->next = 0;
4856 id_refs[id_refs_size++] = ref_list;
4862 read_ref(char *id, size_t idlen, char *name, size_t namelen)
4866 bool remote = FALSE;
4868 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
4869 /* Commits referenced by tags has "^{}" appended. */
4870 if (name[namelen - 1] != '}')
4873 while (namelen > 0 && name[namelen] != '^')
4877 namelen -= STRING_SIZE("refs/tags/");
4878 name += STRING_SIZE("refs/tags/");
4880 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
4882 namelen -= STRING_SIZE("refs/remotes/");
4883 name += STRING_SIZE("refs/remotes/");
4885 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
4886 namelen -= STRING_SIZE("refs/heads/");
4887 name += STRING_SIZE("refs/heads/");
4889 } else if (!strcmp(name, "HEAD")) {
4893 refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
4897 ref = &refs[refs_size++];
4898 ref->name = malloc(namelen + 1);
4902 strncpy(ref->name, name, namelen);
4903 ref->name[namelen] = 0;
4905 ref->remote = remote;
4906 string_copy_rev(ref->id, id);
4914 const char *cmd_env = getenv("TIG_LS_REMOTE");
4915 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
4917 return read_properties(popen(cmd, "r"), "\t", read_ref);
4921 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
4923 if (!strcmp(name, "i18n.commitencoding"))
4924 string_ncopy(opt_encoding, value, valuelen);
4926 if (!strcmp(name, "core.editor"))
4927 string_ncopy(opt_editor, value, valuelen);
4933 load_repo_config(void)
4935 return read_properties(popen(GIT_CONFIG " --list", "r"),
4936 "=", read_repo_config_option);
4940 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
4942 if (!opt_git_dir[0]) {
4943 string_ncopy(opt_git_dir, name, namelen);
4945 } else if (opt_is_inside_work_tree == -1) {
4946 /* This can be 3 different values depending on the
4947 * version of git being used. If git-rev-parse does not
4948 * understand --is-inside-work-tree it will simply echo
4949 * the option else either "true" or "false" is printed.
4950 * Default to true for the unknown case. */
4951 opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
4954 string_ncopy(opt_cdup, name, namelen);
4960 /* XXX: The line outputted by "--show-cdup" can be empty so the option
4961 * must be the last one! */
4963 load_repo_info(void)
4965 return read_properties(popen("git rev-parse --git-dir --is-inside-work-tree --show-cdup 2>/dev/null", "r"),
4966 "=", read_repo_info);
4970 read_properties(FILE *pipe, const char *separators,
4971 int (*read_property)(char *, size_t, char *, size_t))
4973 char buffer[BUFSIZ];
4980 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
4985 name = chomp_string(name);
4986 namelen = strcspn(name, separators);
4988 if (name[namelen]) {
4990 value = chomp_string(name + namelen + 1);
4991 valuelen = strlen(value);
4998 state = read_property(name, namelen, value, valuelen);
5001 if (state != ERR && ferror(pipe))
5014 static void __NORETURN
5017 /* XXX: Restore tty modes and let the OS cleanup the rest! */
5023 static void __NORETURN
5024 die(const char *err, ...)
5030 va_start(args, err);
5031 fputs("tig: ", stderr);
5032 vfprintf(stderr, err, args);
5033 fputs("\n", stderr);
5040 warn(const char *msg, ...)
5044 va_start(args, msg);
5045 fputs("tig warning: ", stderr);
5046 vfprintf(stderr, msg, args);
5047 fputs("\n", stderr);
5052 main(int argc, char *argv[])
5055 enum request request;
5058 signal(SIGINT, quit);
5060 if (setlocale(LC_ALL, "")) {
5061 char *codeset = nl_langinfo(CODESET);
5063 string_ncopy(opt_codeset, codeset, strlen(codeset));
5066 if (load_repo_info() == ERR)
5067 die("Failed to load repo info.");
5069 if (load_options() == ERR)
5070 die("Failed to load user config.");
5072 /* Load the repo config file so options can be overwritten from
5073 * the command line. */
5074 if (load_repo_config() == ERR)
5075 die("Failed to load repo config.");
5077 if (!parse_options(argc, argv))
5080 /* Require a git repository unless when running in pager mode. */
5081 if (!opt_git_dir[0])
5082 die("Not a git repository");
5084 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
5085 opt_iconv = iconv_open(opt_codeset, opt_encoding);
5086 if (opt_iconv == ICONV_NONE)
5087 die("Failed to initialize character set conversion");
5090 if (load_refs() == ERR)
5091 die("Failed to load refs.");
5093 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
5094 view->cmd_env = getenv(view->cmd_env);
5096 request = opt_request;
5100 while (view_driver(display[current_view], request)) {
5104 foreach_view (view, i)
5107 /* Refresh, accept single keystroke of input */
5108 key = wgetch(status_win);
5110 /* wgetch() with nodelay() enabled returns ERR when there's no
5117 request = get_keybinding(display[current_view]->keymap, key);
5119 /* Some low-level request handling. This keeps access to
5120 * status_win restricted. */
5124 char *cmd = read_prompt(":");
5126 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
5127 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
5128 opt_request = REQ_VIEW_DIFF;
5130 opt_request = REQ_VIEW_PAGER;
5139 case REQ_SEARCH_BACK:
5141 const char *prompt = request == REQ_SEARCH
5143 char *search = read_prompt(prompt);
5146 string_ncopy(opt_search, search, strlen(search));
5151 case REQ_SCREEN_RESIZE:
5155 getmaxyx(stdscr, height, width);
5157 /* Resize the status view and let the view driver take
5158 * care of resizing the displayed views. */
5159 wresize(status_win, 1, width);
5160 mvwin(status_win, height - 1, 0);
5161 wrefresh(status_win);