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 ltag:1; /* If so, is the tag local? */
144 unsigned int remote:1; /* Is it a remote ref? */
145 unsigned int next:1; /* For ref lists: are there more refs? */
148 static struct ref **get_refs(char *id);
157 set_from_int_map(struct int_map *map, size_t map_size,
158 int *value, const char *name, int namelen)
163 for (i = 0; i < map_size; i++)
164 if (namelen == map[i].namelen &&
165 !strncasecmp(name, map[i].name, namelen)) {
166 *value = map[i].value;
179 string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
181 if (srclen > dstlen - 1)
184 strncpy(dst, src, srclen);
188 /* Shorthands for safely copying into a fixed buffer. */
190 #define string_copy(dst, src) \
191 string_ncopy_do(dst, sizeof(dst), src, sizeof(src))
193 #define string_ncopy(dst, src, srclen) \
194 string_ncopy_do(dst, sizeof(dst), src, srclen)
196 #define string_copy_rev(dst, src) \
197 string_ncopy_do(dst, SIZEOF_REV, src, SIZEOF_REV - 1)
199 #define string_add(dst, from, src) \
200 string_ncopy_do(dst + (from), sizeof(dst) - (from), src, sizeof(src))
203 chomp_string(char *name)
207 while (isspace(*name))
210 namelen = strlen(name) - 1;
211 while (namelen > 0 && isspace(name[namelen]))
218 string_nformat(char *buf, size_t bufsize, size_t *bufpos, const char *fmt, ...)
221 size_t pos = bufpos ? *bufpos : 0;
224 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
230 return pos >= bufsize ? FALSE : TRUE;
233 #define string_format(buf, fmt, args...) \
234 string_nformat(buf, sizeof(buf), NULL, fmt, args)
236 #define string_format_from(buf, from, fmt, args...) \
237 string_nformat(buf, sizeof(buf), from, fmt, args)
240 string_enum_compare(const char *str1, const char *str2, int len)
244 #define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
246 /* Diff-Header == DIFF_HEADER */
247 for (i = 0; i < len; i++) {
248 if (toupper(str1[i]) == toupper(str2[i]))
251 if (string_enum_sep(str1[i]) &&
252 string_enum_sep(str2[i]))
255 return str1[i] - str2[i];
263 * NOTE: The following is a slightly modified copy of the git project's shell
264 * quoting routines found in the quote.c file.
266 * Help to copy the thing properly quoted for the shell safety. any single
267 * quote is replaced with '\'', any exclamation point is replaced with '\!',
268 * and the whole thing is enclosed in a
271 * original sq_quote result
272 * name ==> name ==> 'name'
273 * a b ==> a b ==> 'a b'
274 * a'b ==> a'\''b ==> 'a'\''b'
275 * a!b ==> a'\!'b ==> 'a'\!'b'
279 sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
283 #define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0)
286 while ((c = *src++)) {
287 if (c == '\'' || c == '!') {
298 if (bufsize < SIZEOF_STR)
310 /* XXX: Keep the view request first and in sync with views[]. */ \
311 REQ_GROUP("View switching") \
312 REQ_(VIEW_MAIN, "Show main view"), \
313 REQ_(VIEW_DIFF, "Show diff view"), \
314 REQ_(VIEW_LOG, "Show log view"), \
315 REQ_(VIEW_TREE, "Show tree view"), \
316 REQ_(VIEW_BLOB, "Show blob view"), \
317 REQ_(VIEW_HELP, "Show help page"), \
318 REQ_(VIEW_PAGER, "Show pager view"), \
319 REQ_(VIEW_STATUS, "Show status view"), \
320 REQ_(VIEW_STAGE, "Show stage view"), \
322 REQ_GROUP("View manipulation") \
323 REQ_(ENTER, "Enter current line and scroll"), \
324 REQ_(NEXT, "Move to next"), \
325 REQ_(PREVIOUS, "Move to previous"), \
326 REQ_(VIEW_NEXT, "Move focus to next view"), \
327 REQ_(REFRESH, "Reload and refresh"), \
328 REQ_(VIEW_CLOSE, "Close the current view"), \
329 REQ_(QUIT, "Close all views and quit"), \
331 REQ_GROUP("Cursor navigation") \
332 REQ_(MOVE_UP, "Move cursor one line up"), \
333 REQ_(MOVE_DOWN, "Move cursor one line down"), \
334 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
335 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
336 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
337 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
339 REQ_GROUP("Scrolling") \
340 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
341 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
342 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
343 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
345 REQ_GROUP("Searching") \
346 REQ_(SEARCH, "Search the view"), \
347 REQ_(SEARCH_BACK, "Search backwards in the view"), \
348 REQ_(FIND_NEXT, "Find next search match"), \
349 REQ_(FIND_PREV, "Find previous search match"), \
352 REQ_(PROMPT, "Bring up the prompt"), \
353 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
354 REQ_(SCREEN_RESIZE, "Resize the screen"), \
355 REQ_(SHOW_VERSION, "Show version information"), \
356 REQ_(STOP_LOADING, "Stop all loading views"), \
357 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
358 REQ_(TOGGLE_DATE, "Toggle date display"), \
359 REQ_(TOGGLE_AUTHOR, "Toggle author display"), \
360 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"), \
361 REQ_(TOGGLE_REFS, "Toggle reference display (tags/branches)"), \
362 REQ_(STATUS_UPDATE, "Update file status"), \
363 REQ_(STATUS_MERGE, "Merge file using external tool"), \
364 REQ_(TREE_PARENT, "Switch to parent directory in tree view"), \
365 REQ_(EDIT, "Open in editor"), \
366 REQ_(NONE, "Do nothing")
369 /* User action requests. */
371 #define REQ_GROUP(help)
372 #define REQ_(req, help) REQ_##req
374 /* Offset all requests to avoid conflicts with ncurses getch values. */
375 REQ_OFFSET = KEY_MAX + 1,
382 struct request_info {
383 enum request request;
389 static struct request_info req_info[] = {
390 #define REQ_GROUP(help) { 0, NULL, 0, (help) },
391 #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
398 get_request(const char *name)
400 int namelen = strlen(name);
403 for (i = 0; i < ARRAY_SIZE(req_info); i++)
404 if (req_info[i].namelen == namelen &&
405 !string_enum_compare(req_info[i].name, name, namelen))
406 return req_info[i].request;
416 static const char usage[] =
417 "tig " TIG_VERSION " (" __DATE__ ")\n"
419 "Usage: tig [options] [revs] [--] [paths]\n"
420 " or: tig show [options] [revs] [--] [paths]\n"
422 " or: tig < [git command output]\n"
425 " -v, --version Show version and exit\n"
426 " -h, --help Show help message and exit\n";
428 /* Option and state variables. */
429 static bool opt_date = TRUE;
430 static bool opt_author = TRUE;
431 static bool opt_line_number = FALSE;
432 static bool opt_rev_graph = FALSE;
433 static bool opt_show_refs = TRUE;
434 static int opt_num_interval = NUMBER_INTERVAL;
435 static int opt_tab_size = TABSIZE;
436 static enum request opt_request = REQ_VIEW_MAIN;
437 static char opt_cmd[SIZEOF_STR] = "";
438 static char opt_path[SIZEOF_STR] = "";
439 static FILE *opt_pipe = NULL;
440 static char opt_encoding[20] = "UTF-8";
441 static bool opt_utf8 = TRUE;
442 static char opt_codeset[20] = "UTF-8";
443 static iconv_t opt_iconv = ICONV_NONE;
444 static char opt_search[SIZEOF_STR] = "";
445 static char opt_cdup[SIZEOF_STR] = "";
446 static char opt_git_dir[SIZEOF_STR] = "";
447 static signed char opt_is_inside_work_tree = -1; /* set to TRUE or FALSE */
448 static char opt_editor[SIZEOF_STR] = "";
456 check_option(char *opt, char short_name, char *name, enum option_type type, ...)
466 int namelen = strlen(name);
470 if (strncmp(opt, name, namelen))
473 if (opt[namelen] == '=')
474 value = opt + namelen + 1;
477 if (!short_name || opt[1] != short_name)
482 va_start(args, type);
483 if (type == OPT_INT) {
484 number = va_arg(args, int *);
486 *number = atoi(value);
493 /* Returns the index of log or diff command or -1 to exit. */
495 parse_options(int argc, char *argv[])
499 char *subcommand = NULL;
502 for (i = 1; i < argc; i++) {
505 if (!strcmp(opt, "log") ||
506 !strcmp(opt, "diff")) {
508 opt_request = opt[0] == 'l'
509 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
510 warn("`tig %s' has been deprecated", opt);
514 if (!strcmp(opt, "show")) {
516 opt_request = REQ_VIEW_DIFF;
520 if (!strcmp(opt, "status")) {
522 opt_request = REQ_VIEW_STATUS;
526 if (opt[0] && opt[0] != '-')
529 if (!strcmp(opt, "--")) {
534 if (check_option(opt, 'v', "version", OPT_NONE)) {
535 printf("tig version %s\n", TIG_VERSION);
539 if (check_option(opt, 'h', "help", OPT_NONE)) {
544 if (!strcmp(opt, "-S")) {
545 warn("`%s' has been deprecated; use `tig status' instead", opt);
546 opt_request = REQ_VIEW_STATUS;
550 if (!strcmp(opt, "-l")) {
551 opt_request = REQ_VIEW_LOG;
552 } else if (!strcmp(opt, "-d")) {
553 opt_request = REQ_VIEW_DIFF;
554 } else if (check_option(opt, 'n', "line-number", OPT_INT, &opt_num_interval)) {
555 opt_line_number = TRUE;
556 } else if (check_option(opt, 'b', "tab-size", OPT_INT, &opt_tab_size)) {
557 opt_tab_size = MIN(opt_tab_size, TABSIZE);
559 if (altargc >= ARRAY_SIZE(altargv))
560 die("maximum number of arguments exceeded");
561 altargv[altargc++] = opt;
565 warn("`%s' has been deprecated", opt);
568 /* Check that no 'alt' arguments occured before a subcommand. */
569 if (subcommand && i < argc && altargc > 0)
570 die("unknown arguments before `%s'", argv[i]);
572 if (!isatty(STDIN_FILENO)) {
573 opt_request = REQ_VIEW_PAGER;
576 } else if (opt_request == REQ_VIEW_STATUS) {
578 warn("ignoring arguments after `%s'", argv[i]);
580 } else if (i < argc || altargc > 0) {
584 if (opt_request == REQ_VIEW_MAIN)
585 /* XXX: This is vulnerable to the user overriding
586 * options required for the main view parser. */
587 string_copy(opt_cmd, "git log --no-color --pretty=raw --boundary");
589 string_copy(opt_cmd, "git");
590 buf_size = strlen(opt_cmd);
592 while (buf_size < sizeof(opt_cmd) && alti < altargc) {
593 opt_cmd[buf_size++] = ' ';
594 buf_size = sq_quote(opt_cmd, buf_size, altargv[alti++]);
597 while (buf_size < sizeof(opt_cmd) && i < argc) {
598 opt_cmd[buf_size++] = ' ';
599 buf_size = sq_quote(opt_cmd, buf_size, argv[i++]);
602 if (buf_size >= sizeof(opt_cmd))
603 die("command too long");
605 opt_cmd[buf_size] = 0;
608 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
616 * Line-oriented content detection.
620 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
621 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
622 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
623 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
624 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
625 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
626 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
627 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
628 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
629 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
630 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
631 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
632 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
633 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
634 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
635 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
636 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
637 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
638 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
639 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
640 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
641 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
642 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
643 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
644 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
645 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
646 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
647 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
648 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
649 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
650 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
651 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
652 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
653 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
654 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
655 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
656 LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
657 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
658 LINE(MAIN_LOCAL_TAG,"", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
659 LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
660 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
661 LINE(MAIN_REVGRAPH,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
662 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
663 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
664 LINE(STAT_SECTION, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
665 LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
666 LINE(STAT_STAGED, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
667 LINE(STAT_UNSTAGED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
668 LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0)
671 #define LINE(type, line, fg, bg, attr) \
678 const char *name; /* Option name. */
679 int namelen; /* Size of option name. */
680 const char *line; /* The start of line to match. */
681 int linelen; /* Size of string to match. */
682 int fg, bg, attr; /* Color and text attributes for the lines. */
685 static struct line_info line_info[] = {
686 #define LINE(type, line, fg, bg, attr) \
687 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
692 static enum line_type
693 get_line_type(char *line)
695 int linelen = strlen(line);
698 for (type = 0; type < ARRAY_SIZE(line_info); type++)
699 /* Case insensitive search matches Signed-off-by lines better. */
700 if (linelen >= line_info[type].linelen &&
701 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
708 get_line_attr(enum line_type type)
710 assert(type < ARRAY_SIZE(line_info));
711 return COLOR_PAIR(type) | line_info[type].attr;
714 static struct line_info *
715 get_line_info(char *name, int namelen)
719 for (type = 0; type < ARRAY_SIZE(line_info); type++)
720 if (namelen == line_info[type].namelen &&
721 !string_enum_compare(line_info[type].name, name, namelen))
722 return &line_info[type];
730 int default_bg = line_info[LINE_DEFAULT].bg;
731 int default_fg = line_info[LINE_DEFAULT].fg;
736 if (assume_default_colors(default_fg, default_bg) == ERR) {
737 default_bg = COLOR_BLACK;
738 default_fg = COLOR_WHITE;
741 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
742 struct line_info *info = &line_info[type];
743 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
744 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
746 init_pair(type, fg, bg);
754 unsigned int selected:1;
756 void *data; /* User data */
766 enum request request;
767 struct keybinding *next;
770 static struct keybinding default_keybindings[] = {
772 { 'm', REQ_VIEW_MAIN },
773 { 'd', REQ_VIEW_DIFF },
774 { 'l', REQ_VIEW_LOG },
775 { 't', REQ_VIEW_TREE },
776 { 'f', REQ_VIEW_BLOB },
777 { 'p', REQ_VIEW_PAGER },
778 { 'h', REQ_VIEW_HELP },
779 { 'S', REQ_VIEW_STATUS },
780 { 'c', REQ_VIEW_STAGE },
782 /* View manipulation */
783 { 'q', REQ_VIEW_CLOSE },
784 { KEY_TAB, REQ_VIEW_NEXT },
785 { KEY_RETURN, REQ_ENTER },
786 { KEY_UP, REQ_PREVIOUS },
787 { KEY_DOWN, REQ_NEXT },
788 { 'R', REQ_REFRESH },
790 /* Cursor navigation */
791 { 'k', REQ_MOVE_UP },
792 { 'j', REQ_MOVE_DOWN },
793 { KEY_HOME, REQ_MOVE_FIRST_LINE },
794 { KEY_END, REQ_MOVE_LAST_LINE },
795 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
796 { ' ', REQ_MOVE_PAGE_DOWN },
797 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
798 { 'b', REQ_MOVE_PAGE_UP },
799 { '-', REQ_MOVE_PAGE_UP },
802 { KEY_IC, REQ_SCROLL_LINE_UP },
803 { KEY_DC, REQ_SCROLL_LINE_DOWN },
804 { 'w', REQ_SCROLL_PAGE_UP },
805 { 's', REQ_SCROLL_PAGE_DOWN },
809 { '?', REQ_SEARCH_BACK },
810 { 'n', REQ_FIND_NEXT },
811 { 'N', REQ_FIND_PREV },
815 { 'z', REQ_STOP_LOADING },
816 { 'v', REQ_SHOW_VERSION },
817 { 'r', REQ_SCREEN_REDRAW },
818 { '.', REQ_TOGGLE_LINENO },
819 { 'D', REQ_TOGGLE_DATE },
820 { 'A', REQ_TOGGLE_AUTHOR },
821 { 'g', REQ_TOGGLE_REV_GRAPH },
822 { 'F', REQ_TOGGLE_REFS },
824 { 'u', REQ_STATUS_UPDATE },
825 { 'M', REQ_STATUS_MERGE },
826 { ',', REQ_TREE_PARENT },
829 /* Using the ncurses SIGWINCH handler. */
830 { KEY_RESIZE, REQ_SCREEN_RESIZE },
833 #define KEYMAP_INFO \
846 #define KEYMAP_(name) KEYMAP_##name
851 static struct int_map keymap_table[] = {
852 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
857 #define set_keymap(map, name) \
858 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
860 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
863 add_keybinding(enum keymap keymap, enum request request, int key)
865 struct keybinding *keybinding;
867 keybinding = calloc(1, sizeof(*keybinding));
869 die("Failed to allocate keybinding");
871 keybinding->alias = key;
872 keybinding->request = request;
873 keybinding->next = keybindings[keymap];
874 keybindings[keymap] = keybinding;
877 /* Looks for a key binding first in the given map, then in the generic map, and
878 * lastly in the default keybindings. */
880 get_keybinding(enum keymap keymap, int key)
882 struct keybinding *kbd;
885 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
886 if (kbd->alias == key)
889 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
890 if (kbd->alias == key)
893 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
894 if (default_keybindings[i].alias == key)
895 return default_keybindings[i].request;
897 return (enum request) key;
906 static struct key key_table[] = {
907 { "Enter", KEY_RETURN },
909 { "Backspace", KEY_BACKSPACE },
911 { "Escape", KEY_ESC },
912 { "Left", KEY_LEFT },
913 { "Right", KEY_RIGHT },
915 { "Down", KEY_DOWN },
916 { "Insert", KEY_IC },
917 { "Delete", KEY_DC },
919 { "Home", KEY_HOME },
921 { "PageUp", KEY_PPAGE },
922 { "PageDown", KEY_NPAGE },
932 { "F10", KEY_F(10) },
933 { "F11", KEY_F(11) },
934 { "F12", KEY_F(12) },
938 get_key_value(const char *name)
942 for (i = 0; i < ARRAY_SIZE(key_table); i++)
943 if (!strcasecmp(key_table[i].name, name))
944 return key_table[i].value;
946 if (strlen(name) == 1 && isprint(*name))
953 get_key_name(int key_value)
955 static char key_char[] = "'X'";
959 for (key = 0; key < ARRAY_SIZE(key_table); key++)
960 if (key_table[key].value == key_value)
961 seq = key_table[key].name;
965 isprint(key_value)) {
966 key_char[1] = (char) key_value;
970 return seq ? seq : "'?'";
974 get_key(enum request request)
976 static char buf[BUFSIZ];
983 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
984 struct keybinding *keybinding = &default_keybindings[i];
986 if (keybinding->request != request)
989 if (!string_format_from(buf, &pos, "%s%s", sep,
990 get_key_name(keybinding->alias)))
991 return "Too many keybindings!";
1001 char cmd[SIZEOF_STR];
1004 static struct run_request *run_request;
1005 static size_t run_requests;
1008 add_run_request(enum keymap keymap, int key, int argc, char **argv)
1010 struct run_request *tmp;
1011 struct run_request req = { keymap, key };
1014 for (bufpos = 0; argc > 0; argc--, argv++)
1015 if (!string_format_from(req.cmd, &bufpos, "%s ", *argv))
1018 req.cmd[bufpos - 1] = 0;
1020 tmp = realloc(run_request, (run_requests + 1) * sizeof(*run_request));
1025 run_request[run_requests++] = req;
1027 return REQ_NONE + run_requests;
1030 static struct run_request *
1031 get_run_request(enum request request)
1033 if (request <= REQ_NONE)
1035 return &run_request[request - REQ_NONE - 1];
1039 add_builtin_run_requests(void)
1046 { KEYMAP_MAIN, 'C', { "git cherry-pick %(commit)" } },
1047 { KEYMAP_GENERIC, 'G', { "git gc" } },
1051 for (i = 0; i < ARRAY_SIZE(reqs); i++) {
1054 req = add_run_request(reqs[i].keymap, reqs[i].key, 1, reqs[i].argv);
1055 if (req != REQ_NONE)
1056 add_keybinding(reqs[i].keymap, req, reqs[i].key);
1061 * User config file handling.
1064 static struct int_map color_map[] = {
1065 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
1077 #define set_color(color, name) \
1078 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
1080 static struct int_map attr_map[] = {
1081 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
1088 ATTR_MAP(UNDERLINE),
1091 #define set_attribute(attr, name) \
1092 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
1094 static int config_lineno;
1095 static bool config_errors;
1096 static char *config_msg;
1098 /* Wants: object fgcolor bgcolor [attr] */
1100 option_color_command(int argc, char *argv[])
1102 struct line_info *info;
1104 if (argc != 3 && argc != 4) {
1105 config_msg = "Wrong number of arguments given to color command";
1109 info = get_line_info(argv[0], strlen(argv[0]));
1111 config_msg = "Unknown color name";
1115 if (set_color(&info->fg, argv[1]) == ERR ||
1116 set_color(&info->bg, argv[2]) == ERR) {
1117 config_msg = "Unknown color";
1121 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
1122 config_msg = "Unknown attribute";
1129 /* Wants: name = value */
1131 option_set_command(int argc, char *argv[])
1134 config_msg = "Wrong number of arguments given to set command";
1138 if (strcmp(argv[1], "=")) {
1139 config_msg = "No value assigned";
1143 if (!strcmp(argv[0], "show-rev-graph")) {
1144 opt_rev_graph = (!strcmp(argv[2], "1") ||
1145 !strcmp(argv[2], "true") ||
1146 !strcmp(argv[2], "yes"));
1150 if (!strcmp(argv[0], "line-number-interval")) {
1151 opt_num_interval = atoi(argv[2]);
1155 if (!strcmp(argv[0], "tab-size")) {
1156 opt_tab_size = atoi(argv[2]);
1160 if (!strcmp(argv[0], "commit-encoding")) {
1161 char *arg = argv[2];
1162 int delimiter = *arg;
1165 switch (delimiter) {
1168 for (arg++, i = 0; arg[i]; i++)
1169 if (arg[i] == delimiter) {
1174 string_ncopy(opt_encoding, arg, strlen(arg));
1179 config_msg = "Unknown variable name";
1183 /* Wants: mode request key */
1185 option_bind_command(int argc, char *argv[])
1187 enum request request;
1192 config_msg = "Wrong number of arguments given to bind command";
1196 if (set_keymap(&keymap, argv[0]) == ERR) {
1197 config_msg = "Unknown key map";
1201 key = get_key_value(argv[1]);
1203 config_msg = "Unknown key";
1207 request = get_request(argv[2]);
1208 if (request == REQ_NONE) {
1209 const char *obsolete[] = { "cherry-pick" };
1210 size_t namelen = strlen(argv[2]);
1213 for (i = 0; i < ARRAY_SIZE(obsolete); i++) {
1214 if (namelen == strlen(obsolete[i]) &&
1215 !string_enum_compare(obsolete[i], argv[2], namelen)) {
1216 config_msg = "Obsolete request name";
1221 if (request == REQ_NONE && *argv[2]++ == '!')
1222 request = add_run_request(keymap, key, argc - 2, argv + 2);
1223 if (request == REQ_NONE) {
1224 config_msg = "Unknown request name";
1228 add_keybinding(keymap, request, key);
1234 set_option(char *opt, char *value)
1241 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1242 argv[argc++] = value;
1245 /* Nothing more to tokenize or last available token. */
1246 if (!*value || argc >= ARRAY_SIZE(argv))
1250 while (isspace(*value))
1254 if (!strcmp(opt, "color"))
1255 return option_color_command(argc, argv);
1257 if (!strcmp(opt, "set"))
1258 return option_set_command(argc, argv);
1260 if (!strcmp(opt, "bind"))
1261 return option_bind_command(argc, argv);
1263 config_msg = "Unknown option command";
1268 read_option(char *opt, size_t optlen, char *value, size_t valuelen)
1273 config_msg = "Internal error";
1275 /* Check for comment markers, since read_properties() will
1276 * only ensure opt and value are split at first " \t". */
1277 optlen = strcspn(opt, "#");
1281 if (opt[optlen] != 0) {
1282 config_msg = "No option value";
1286 /* Look for comment endings in the value. */
1287 size_t len = strcspn(value, "#");
1289 if (len < valuelen) {
1291 value[valuelen] = 0;
1294 status = set_option(opt, value);
1297 if (status == ERR) {
1298 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1299 config_lineno, (int) optlen, opt, config_msg);
1300 config_errors = TRUE;
1303 /* Always keep going if errors are encountered. */
1308 load_option_file(const char *path)
1312 /* It's ok that the file doesn't exist. */
1313 file = fopen(path, "r");
1318 config_errors = FALSE;
1320 if (read_properties(file, " \t", read_option) == ERR ||
1321 config_errors == TRUE)
1322 fprintf(stderr, "Errors while loading %s.\n", path);
1328 char *home = getenv("HOME");
1329 char *tigrc_user = getenv("TIGRC_USER");
1330 char *tigrc_system = getenv("TIGRC_SYSTEM");
1331 char buf[SIZEOF_STR];
1333 add_builtin_run_requests();
1335 if (!tigrc_system) {
1336 if (!string_format(buf, "%s/tigrc", SYSCONFDIR))
1340 load_option_file(tigrc_system);
1343 if (!home || !string_format(buf, "%s/.tigrc", home))
1347 load_option_file(tigrc_user);
1360 /* The display array of active views and the index of the current view. */
1361 static struct view *display[2];
1362 static unsigned int current_view;
1364 /* Reading from the prompt? */
1365 static bool input_mode = FALSE;
1367 #define foreach_displayed_view(view, i) \
1368 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1370 #define displayed_views() (display[1] != NULL ? 2 : 1)
1372 /* Current head and commit ID */
1373 static char ref_blob[SIZEOF_REF] = "";
1374 static char ref_commit[SIZEOF_REF] = "HEAD";
1375 static char ref_head[SIZEOF_REF] = "HEAD";
1378 const char *name; /* View name */
1379 const char *cmd_fmt; /* Default command line format */
1380 const char *cmd_env; /* Command line set via environment */
1381 const char *id; /* Points to either of ref_{head,commit,blob} */
1383 struct view_ops *ops; /* View operations */
1385 enum keymap keymap; /* What keymap does this view have */
1387 char cmd[SIZEOF_STR]; /* Command buffer */
1388 char ref[SIZEOF_REF]; /* Hovered commit reference */
1389 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1391 int height, width; /* The width and height of the main window */
1392 WINDOW *win; /* The main window */
1393 WINDOW *title; /* The title window living below the main window */
1396 unsigned long offset; /* Offset of the window top */
1397 unsigned long lineno; /* Current line number */
1400 char grep[SIZEOF_STR]; /* Search string */
1401 regex_t *regex; /* Pre-compiled regex */
1403 /* If non-NULL, points to the view that opened this view. If this view
1404 * is closed tig will switch back to the parent view. */
1405 struct view *parent;
1408 unsigned long lines; /* Total number of lines */
1409 struct line *line; /* Line index */
1410 unsigned long line_size;/* Total number of allocated lines */
1411 unsigned int digits; /* Number of digits in the lines member. */
1419 /* What type of content being displayed. Used in the title bar. */
1421 /* Open and reads in all view content. */
1422 bool (*open)(struct view *view);
1423 /* Read one line; updates view->line. */
1424 bool (*read)(struct view *view, char *data);
1425 /* Draw one line; @lineno must be < view->height. */
1426 bool (*draw)(struct view *view, struct line *line, unsigned int lineno, bool selected);
1427 /* Depending on view handle a special requests. */
1428 enum request (*request)(struct view *view, enum request request, struct line *line);
1429 /* Search for regex in a line. */
1430 bool (*grep)(struct view *view, struct line *line);
1432 void (*select)(struct view *view, struct line *line);
1435 static struct view_ops pager_ops;
1436 static struct view_ops main_ops;
1437 static struct view_ops tree_ops;
1438 static struct view_ops blob_ops;
1439 static struct view_ops help_ops;
1440 static struct view_ops status_ops;
1441 static struct view_ops stage_ops;
1443 #define VIEW_STR(name, cmd, env, ref, ops, map) \
1444 { name, cmd, #env, ref, ops, map}
1446 #define VIEW_(id, name, ops, ref) \
1447 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id)
1450 static struct view views[] = {
1451 VIEW_(MAIN, "main", &main_ops, ref_head),
1452 VIEW_(DIFF, "diff", &pager_ops, ref_commit),
1453 VIEW_(LOG, "log", &pager_ops, ref_head),
1454 VIEW_(TREE, "tree", &tree_ops, ref_commit),
1455 VIEW_(BLOB, "blob", &blob_ops, ref_blob),
1456 VIEW_(HELP, "help", &help_ops, ""),
1457 VIEW_(PAGER, "pager", &pager_ops, "stdin"),
1458 VIEW_(STATUS, "status", &status_ops, ""),
1459 VIEW_(STAGE, "stage", &stage_ops, ""),
1462 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1464 #define foreach_view(view, i) \
1465 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1467 #define view_is_displayed(view) \
1468 (view == display[0] || view == display[1])
1471 draw_text(struct view *view, const char *string, int max_len, int col,
1472 bool use_tilde, int tilde_attr)
1475 int trimmed = FALSE;
1481 len = utf8_length(string, max_len, &trimmed, use_tilde);
1483 len = strlen(string);
1484 if (len > max_len) {
1493 waddnstr(view->win, string, len);
1494 if (trimmed && use_tilde) {
1495 if (tilde_attr != -1)
1496 wattrset(view->win, tilde_attr);
1497 waddch(view->win, '~');
1505 draw_view_line(struct view *view, unsigned int lineno)
1508 bool selected = (view->offset + lineno == view->lineno);
1511 assert(view_is_displayed(view));
1513 if (view->offset + lineno >= view->lines)
1516 line = &view->line[view->offset + lineno];
1519 line->selected = TRUE;
1520 view->ops->select(view, line);
1521 } else if (line->selected) {
1522 line->selected = FALSE;
1523 wmove(view->win, lineno, 0);
1524 wclrtoeol(view->win);
1527 scrollok(view->win, FALSE);
1528 draw_ok = view->ops->draw(view, line, lineno, selected);
1529 scrollok(view->win, TRUE);
1535 redraw_view_from(struct view *view, int lineno)
1537 assert(0 <= lineno && lineno < view->height);
1539 for (; lineno < view->height; lineno++) {
1540 if (!draw_view_line(view, lineno))
1544 redrawwin(view->win);
1546 wnoutrefresh(view->win);
1548 wrefresh(view->win);
1552 redraw_view(struct view *view)
1555 redraw_view_from(view, 0);
1560 update_view_title(struct view *view)
1562 char buf[SIZEOF_STR];
1563 char state[SIZEOF_STR];
1564 size_t bufpos = 0, statelen = 0;
1566 assert(view_is_displayed(view));
1568 if (view != VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) {
1569 unsigned int view_lines = view->offset + view->height;
1570 unsigned int lines = view->lines
1571 ? MIN(view_lines, view->lines) * 100 / view->lines
1574 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1581 time_t secs = time(NULL) - view->start_time;
1583 /* Three git seconds are a long time ... */
1585 string_format_from(state, &statelen, " %lds", secs);
1589 string_format_from(buf, &bufpos, "[%s]", view->name);
1590 if (*view->ref && bufpos < view->width) {
1591 size_t refsize = strlen(view->ref);
1592 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1594 if (minsize < view->width)
1595 refsize = view->width - minsize + 7;
1596 string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
1599 if (statelen && bufpos < view->width) {
1600 string_format_from(buf, &bufpos, " %s", state);
1603 if (view == display[current_view])
1604 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1606 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1608 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1609 wclrtoeol(view->title);
1610 wmove(view->title, 0, view->width - 1);
1613 wnoutrefresh(view->title);
1615 wrefresh(view->title);
1619 resize_display(void)
1622 struct view *base = display[0];
1623 struct view *view = display[1] ? display[1] : display[0];
1625 /* Setup window dimensions */
1627 getmaxyx(stdscr, base->height, base->width);
1629 /* Make room for the status window. */
1633 /* Horizontal split. */
1634 view->width = base->width;
1635 view->height = SCALE_SPLIT_VIEW(base->height);
1636 base->height -= view->height;
1638 /* Make room for the title bar. */
1642 /* Make room for the title bar. */
1647 foreach_displayed_view (view, i) {
1649 view->win = newwin(view->height, 0, offset, 0);
1651 die("Failed to create %s view", view->name);
1653 scrollok(view->win, TRUE);
1655 view->title = newwin(1, 0, offset + view->height, 0);
1657 die("Failed to create title window");
1660 wresize(view->win, view->height, view->width);
1661 mvwin(view->win, offset, 0);
1662 mvwin(view->title, offset + view->height, 0);
1665 offset += view->height + 1;
1670 redraw_display(void)
1675 foreach_displayed_view (view, i) {
1677 update_view_title(view);
1682 update_display_cursor(struct view *view)
1684 /* Move the cursor to the right-most column of the cursor line.
1686 * XXX: This could turn out to be a bit expensive, but it ensures that
1687 * the cursor does not jump around. */
1689 wmove(view->win, view->lineno - view->offset, view->width - 1);
1690 wrefresh(view->win);
1698 /* Scrolling backend */
1700 do_scroll_view(struct view *view, int lines)
1702 bool redraw_current_line = FALSE;
1704 /* The rendering expects the new offset. */
1705 view->offset += lines;
1707 assert(0 <= view->offset && view->offset < view->lines);
1710 /* Move current line into the view. */
1711 if (view->lineno < view->offset) {
1712 view->lineno = view->offset;
1713 redraw_current_line = TRUE;
1714 } else if (view->lineno >= view->offset + view->height) {
1715 view->lineno = view->offset + view->height - 1;
1716 redraw_current_line = TRUE;
1719 assert(view->offset <= view->lineno && view->lineno < view->lines);
1721 /* Redraw the whole screen if scrolling is pointless. */
1722 if (view->height < ABS(lines)) {
1726 int line = lines > 0 ? view->height - lines : 0;
1727 int end = line + ABS(lines);
1729 wscrl(view->win, lines);
1731 for (; line < end; line++) {
1732 if (!draw_view_line(view, line))
1736 if (redraw_current_line)
1737 draw_view_line(view, view->lineno - view->offset);
1740 redrawwin(view->win);
1741 wrefresh(view->win);
1745 /* Scroll frontend */
1747 scroll_view(struct view *view, enum request request)
1751 assert(view_is_displayed(view));
1754 case REQ_SCROLL_PAGE_DOWN:
1755 lines = view->height;
1756 case REQ_SCROLL_LINE_DOWN:
1757 if (view->offset + lines > view->lines)
1758 lines = view->lines - view->offset;
1760 if (lines == 0 || view->offset + view->height >= view->lines) {
1761 report("Cannot scroll beyond the last line");
1766 case REQ_SCROLL_PAGE_UP:
1767 lines = view->height;
1768 case REQ_SCROLL_LINE_UP:
1769 if (lines > view->offset)
1770 lines = view->offset;
1773 report("Cannot scroll beyond the first line");
1781 die("request %d not handled in switch", request);
1784 do_scroll_view(view, lines);
1789 move_view(struct view *view, enum request request)
1791 int scroll_steps = 0;
1795 case REQ_MOVE_FIRST_LINE:
1796 steps = -view->lineno;
1799 case REQ_MOVE_LAST_LINE:
1800 steps = view->lines - view->lineno - 1;
1803 case REQ_MOVE_PAGE_UP:
1804 steps = view->height > view->lineno
1805 ? -view->lineno : -view->height;
1808 case REQ_MOVE_PAGE_DOWN:
1809 steps = view->lineno + view->height >= view->lines
1810 ? view->lines - view->lineno - 1 : view->height;
1822 die("request %d not handled in switch", request);
1825 if (steps <= 0 && view->lineno == 0) {
1826 report("Cannot move beyond the first line");
1829 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1830 report("Cannot move beyond the last line");
1834 /* Move the current line */
1835 view->lineno += steps;
1836 assert(0 <= view->lineno && view->lineno < view->lines);
1838 /* Check whether the view needs to be scrolled */
1839 if (view->lineno < view->offset ||
1840 view->lineno >= view->offset + view->height) {
1841 scroll_steps = steps;
1842 if (steps < 0 && -steps > view->offset) {
1843 scroll_steps = -view->offset;
1845 } else if (steps > 0) {
1846 if (view->lineno == view->lines - 1 &&
1847 view->lines > view->height) {
1848 scroll_steps = view->lines - view->offset - 1;
1849 if (scroll_steps >= view->height)
1850 scroll_steps -= view->height - 1;
1855 if (!view_is_displayed(view)) {
1856 view->offset += scroll_steps;
1857 assert(0 <= view->offset && view->offset < view->lines);
1858 view->ops->select(view, &view->line[view->lineno]);
1862 /* Repaint the old "current" line if we be scrolling */
1863 if (ABS(steps) < view->height)
1864 draw_view_line(view, view->lineno - steps - view->offset);
1867 do_scroll_view(view, scroll_steps);
1871 /* Draw the current line */
1872 draw_view_line(view, view->lineno - view->offset);
1874 redrawwin(view->win);
1875 wrefresh(view->win);
1884 static void search_view(struct view *view, enum request request);
1887 find_next_line(struct view *view, unsigned long lineno, struct line *line)
1889 assert(view_is_displayed(view));
1891 if (!view->ops->grep(view, line))
1894 if (lineno - view->offset >= view->height) {
1895 view->offset = lineno;
1896 view->lineno = lineno;
1900 unsigned long old_lineno = view->lineno - view->offset;
1902 view->lineno = lineno;
1903 draw_view_line(view, old_lineno);
1905 draw_view_line(view, view->lineno - view->offset);
1906 redrawwin(view->win);
1907 wrefresh(view->win);
1910 report("Line %ld matches '%s'", lineno + 1, view->grep);
1915 find_next(struct view *view, enum request request)
1917 unsigned long lineno = view->lineno;
1922 report("No previous search");
1924 search_view(view, request);
1934 case REQ_SEARCH_BACK:
1943 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
1944 lineno += direction;
1946 /* Note, lineno is unsigned long so will wrap around in which case it
1947 * will become bigger than view->lines. */
1948 for (; lineno < view->lines; lineno += direction) {
1949 struct line *line = &view->line[lineno];
1951 if (find_next_line(view, lineno, line))
1955 report("No match found for '%s'", view->grep);
1959 search_view(struct view *view, enum request request)
1964 regfree(view->regex);
1967 view->regex = calloc(1, sizeof(*view->regex));
1972 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
1973 if (regex_err != 0) {
1974 char buf[SIZEOF_STR] = "unknown error";
1976 regerror(regex_err, view->regex, buf, sizeof(buf));
1977 report("Search failed: %s", buf);
1981 string_copy(view->grep, opt_search);
1983 find_next(view, request);
1987 * Incremental updating
1991 end_update(struct view *view)
1995 set_nonblocking_input(FALSE);
1996 if (view->pipe == stdin)
2004 begin_update(struct view *view)
2010 string_copy(view->cmd, opt_cmd);
2012 /* When running random commands, initially show the
2013 * command in the title. However, it maybe later be
2014 * overwritten if a commit line is selected. */
2015 if (view == VIEW(REQ_VIEW_PAGER))
2016 string_copy(view->ref, view->cmd);
2020 } else if (view == VIEW(REQ_VIEW_TREE)) {
2021 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2022 char path[SIZEOF_STR];
2024 if (strcmp(view->vid, view->id))
2025 opt_path[0] = path[0] = 0;
2026 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
2029 if (!string_format(view->cmd, format, view->id, path))
2033 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2034 const char *id = view->id;
2036 if (!string_format(view->cmd, format, id, id, id, id, id))
2039 /* Put the current ref_* value to the view title ref
2040 * member. This is needed by the blob view. Most other
2041 * views sets it automatically after loading because the
2042 * first line is a commit line. */
2043 string_copy_rev(view->ref, view->id);
2046 /* Special case for the pager view. */
2048 view->pipe = opt_pipe;
2051 view->pipe = popen(view->cmd, "r");
2057 set_nonblocking_input(TRUE);
2062 string_copy_rev(view->vid, view->id);
2067 for (i = 0; i < view->lines; i++)
2068 if (view->line[i].data)
2069 free(view->line[i].data);
2075 view->start_time = time(NULL);
2080 static struct line *
2081 realloc_lines(struct view *view, size_t line_size)
2083 struct line *tmp = realloc(view->line, sizeof(*view->line) * line_size);
2089 view->line_size = line_size;
2094 update_view(struct view *view)
2096 char in_buffer[BUFSIZ];
2097 char out_buffer[BUFSIZ * 2];
2099 /* The number of lines to read. If too low it will cause too much
2100 * redrawing (and possible flickering), if too high responsiveness
2102 unsigned long lines = view->height;
2103 int redraw_from = -1;
2108 /* Only redraw if lines are visible. */
2109 if (view->offset + view->height >= view->lines)
2110 redraw_from = view->lines - view->offset;
2112 /* FIXME: This is probably not perfect for backgrounded views. */
2113 if (!realloc_lines(view, view->lines + lines))
2116 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
2117 size_t linelen = strlen(line);
2120 line[linelen - 1] = 0;
2122 if (opt_iconv != ICONV_NONE) {
2123 ICONV_CONST char *inbuf = line;
2124 size_t inlen = linelen;
2126 char *outbuf = out_buffer;
2127 size_t outlen = sizeof(out_buffer);
2131 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
2132 if (ret != (size_t) -1) {
2134 linelen = strlen(out_buffer);
2138 if (!view->ops->read(view, line))
2148 lines = view->lines;
2149 for (digits = 0; lines; digits++)
2152 /* Keep the displayed view in sync with line number scaling. */
2153 if (digits != view->digits) {
2154 view->digits = digits;
2159 if (!view_is_displayed(view))
2162 if (view == VIEW(REQ_VIEW_TREE)) {
2163 /* Clear the view and redraw everything since the tree sorting
2164 * might have rearranged things. */
2167 } else if (redraw_from >= 0) {
2168 /* If this is an incremental update, redraw the previous line
2169 * since for commits some members could have changed when
2170 * loading the main view. */
2171 if (redraw_from > 0)
2174 /* Since revision graph visualization requires knowledge
2175 * about the parent commit, it causes a further one-off
2176 * needed to be redrawn for incremental updates. */
2177 if (redraw_from > 0 && opt_rev_graph)
2180 /* Incrementally draw avoids flickering. */
2181 redraw_view_from(view, redraw_from);
2184 /* Update the title _after_ the redraw so that if the redraw picks up a
2185 * commit reference in view->ref it'll be available here. */
2186 update_view_title(view);
2189 if (ferror(view->pipe)) {
2190 report("Failed to read: %s", strerror(errno));
2193 } else if (feof(view->pipe)) {
2201 report("Allocation failure");
2204 view->ops->read(view, NULL);
2209 static struct line *
2210 add_line_data(struct view *view, void *data, enum line_type type)
2212 struct line *line = &view->line[view->lines++];
2214 memset(line, 0, sizeof(*line));
2221 static struct line *
2222 add_line_text(struct view *view, char *data, enum line_type type)
2225 data = strdup(data);
2227 return data ? add_line_data(view, data, type) : NULL;
2236 OPEN_DEFAULT = 0, /* Use default view switching. */
2237 OPEN_SPLIT = 1, /* Split current view. */
2238 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2239 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2243 open_view(struct view *prev, enum request request, enum open_flags flags)
2245 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2246 bool split = !!(flags & OPEN_SPLIT);
2247 bool reload = !!(flags & OPEN_RELOAD);
2248 struct view *view = VIEW(request);
2249 int nviews = displayed_views();
2250 struct view *base_view = display[0];
2252 if (view == prev && nviews == 1 && !reload) {
2253 report("Already in %s view", view->name);
2257 if (view->ops->open) {
2258 if (!view->ops->open(view)) {
2259 report("Failed to load %s view", view->name);
2263 } else if ((reload || strcmp(view->vid, view->id)) &&
2264 !begin_update(view)) {
2265 report("Failed to load %s view", view->name);
2274 /* Maximize the current view. */
2275 memset(display, 0, sizeof(display));
2277 display[current_view] = view;
2280 /* Resize the view when switching between split- and full-screen,
2281 * or when switching between two different full-screen views. */
2282 if (nviews != displayed_views() ||
2283 (nviews == 1 && base_view != display[0]))
2286 if (split && prev->lineno - prev->offset >= prev->height) {
2287 /* Take the title line into account. */
2288 int lines = prev->lineno - prev->offset - prev->height + 1;
2290 /* Scroll the view that was split if the current line is
2291 * outside the new limited view. */
2292 do_scroll_view(prev, lines);
2295 if (prev && view != prev) {
2296 if (split && !backgrounded) {
2297 /* "Blur" the previous view. */
2298 update_view_title(prev);
2301 view->parent = prev;
2304 if (view->pipe && view->lines == 0) {
2305 /* Clear the old view and let the incremental updating refill
2314 /* If the view is backgrounded the above calls to report()
2315 * won't redraw the view title. */
2317 update_view_title(view);
2321 open_external_viewer(const char *cmd)
2323 def_prog_mode(); /* save current tty modes */
2324 endwin(); /* restore original tty modes */
2326 fprintf(stderr, "Press Enter to continue");
2333 open_mergetool(const char *file)
2335 char cmd[SIZEOF_STR];
2336 char file_sq[SIZEOF_STR];
2338 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2339 string_format(cmd, "git mergetool %s", file_sq)) {
2340 open_external_viewer(cmd);
2345 open_editor(bool from_root, const char *file)
2347 char cmd[SIZEOF_STR];
2348 char file_sq[SIZEOF_STR];
2350 char *prefix = from_root ? opt_cdup : "";
2352 editor = getenv("GIT_EDITOR");
2353 if (!editor && *opt_editor)
2354 editor = opt_editor;
2356 editor = getenv("VISUAL");
2358 editor = getenv("EDITOR");
2362 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2363 string_format(cmd, "%s %s%s", editor, prefix, file_sq)) {
2364 open_external_viewer(cmd);
2369 open_run_request(enum request request)
2371 struct run_request *req = get_run_request(request);
2372 char buf[SIZEOF_STR * 2];
2377 report("Unknown run request");
2385 char *next = strstr(cmd, "%(");
2386 int len = next - cmd;
2393 } else if (!strncmp(next, "%(head)", 7)) {
2396 } else if (!strncmp(next, "%(commit)", 9)) {
2399 } else if (!strncmp(next, "%(blob)", 7)) {
2403 report("Unknown replacement in run request: `%s`", req->cmd);
2407 if (!string_format_from(buf, &bufpos, "%.*s%s", len, cmd, value))
2411 next = strchr(next, ')') + 1;
2415 open_external_viewer(buf);
2419 * User request switch noodle
2423 view_driver(struct view *view, enum request request)
2427 if (request == REQ_NONE) {
2432 if (request > REQ_NONE) {
2433 open_run_request(request);
2437 if (view && view->lines) {
2438 request = view->ops->request(view, request, &view->line[view->lineno]);
2439 if (request == REQ_NONE)
2446 case REQ_MOVE_PAGE_UP:
2447 case REQ_MOVE_PAGE_DOWN:
2448 case REQ_MOVE_FIRST_LINE:
2449 case REQ_MOVE_LAST_LINE:
2450 move_view(view, request);
2453 case REQ_SCROLL_LINE_DOWN:
2454 case REQ_SCROLL_LINE_UP:
2455 case REQ_SCROLL_PAGE_DOWN:
2456 case REQ_SCROLL_PAGE_UP:
2457 scroll_view(view, request);
2462 report("No file chosen, press %s to open tree view",
2463 get_key(REQ_VIEW_TREE));
2466 open_view(view, request, OPEN_DEFAULT);
2469 case REQ_VIEW_PAGER:
2470 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2471 report("No pager content, press %s to run command from prompt",
2472 get_key(REQ_PROMPT));
2475 open_view(view, request, OPEN_DEFAULT);
2478 case REQ_VIEW_STAGE:
2479 if (!VIEW(REQ_VIEW_STAGE)->lines) {
2480 report("No stage content, press %s to open the status view and choose file",
2481 get_key(REQ_VIEW_STATUS));
2484 open_view(view, request, OPEN_DEFAULT);
2487 case REQ_VIEW_STATUS:
2488 if (opt_is_inside_work_tree == FALSE) {
2489 report("The status view requires a working tree");
2492 open_view(view, request, OPEN_DEFAULT);
2500 open_view(view, request, OPEN_DEFAULT);
2505 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2507 if ((view == VIEW(REQ_VIEW_DIFF) &&
2508 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2509 (view == VIEW(REQ_VIEW_STAGE) &&
2510 view->parent == VIEW(REQ_VIEW_STATUS)) ||
2511 (view == VIEW(REQ_VIEW_BLOB) &&
2512 view->parent == VIEW(REQ_VIEW_TREE))) {
2515 view = view->parent;
2516 line = view->lineno;
2517 move_view(view, request);
2518 if (view_is_displayed(view))
2519 update_view_title(view);
2520 if (line != view->lineno)
2521 view->ops->request(view, REQ_ENTER,
2522 &view->line[view->lineno]);
2525 move_view(view, request);
2531 int nviews = displayed_views();
2532 int next_view = (current_view + 1) % nviews;
2534 if (next_view == current_view) {
2535 report("Only one view is displayed");
2539 current_view = next_view;
2540 /* Blur out the title of the previous view. */
2541 update_view_title(view);
2546 report("Refreshing is not yet supported for the %s view", view->name);
2549 case REQ_TOGGLE_LINENO:
2550 opt_line_number = !opt_line_number;
2554 case REQ_TOGGLE_DATE:
2555 opt_date = !opt_date;
2559 case REQ_TOGGLE_AUTHOR:
2560 opt_author = !opt_author;
2564 case REQ_TOGGLE_REV_GRAPH:
2565 opt_rev_graph = !opt_rev_graph;
2569 case REQ_TOGGLE_REFS:
2570 opt_show_refs = !opt_show_refs;
2575 /* Always reload^Wrerun commands from the prompt. */
2576 open_view(view, opt_request, OPEN_RELOAD);
2580 case REQ_SEARCH_BACK:
2581 search_view(view, request);
2586 find_next(view, request);
2589 case REQ_STOP_LOADING:
2590 for (i = 0; i < ARRAY_SIZE(views); i++) {
2593 report("Stopped loading the %s view", view->name),
2598 case REQ_SHOW_VERSION:
2599 report("tig-%s (built %s)", TIG_VERSION, __DATE__);
2602 case REQ_SCREEN_RESIZE:
2605 case REQ_SCREEN_REDRAW:
2610 report("Nothing to edit");
2615 report("Nothing to enter");
2619 case REQ_VIEW_CLOSE:
2620 /* XXX: Mark closed views by letting view->parent point to the
2621 * view itself. Parents to closed view should never be
2624 view->parent->parent != view->parent) {
2625 memset(display, 0, sizeof(display));
2627 display[current_view] = view->parent;
2628 view->parent = view;
2638 /* An unknown key will show most commonly used commands. */
2639 report("Unknown key, press 'h' for help");
2652 pager_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2654 char *text = line->data;
2655 enum line_type type = line->type;
2658 wmove(view->win, lineno, 0);
2662 wchgat(view->win, -1, 0, type, NULL);
2665 attr = get_line_attr(type);
2666 wattrset(view->win, attr);
2668 if (opt_line_number || opt_tab_size < TABSIZE) {
2669 static char spaces[] = " ";
2670 int col_offset = 0, col = 0;
2672 if (opt_line_number) {
2673 unsigned long real_lineno = view->offset + lineno + 1;
2675 if (real_lineno == 1 ||
2676 (real_lineno % opt_num_interval) == 0) {
2677 wprintw(view->win, "%.*d", view->digits, real_lineno);
2680 waddnstr(view->win, spaces,
2681 MIN(view->digits, STRING_SIZE(spaces)));
2683 waddstr(view->win, ": ");
2684 col_offset = view->digits + 2;
2687 while (text && col_offset + col < view->width) {
2688 int cols_max = view->width - col_offset - col;
2692 if (*text == '\t') {
2694 assert(sizeof(spaces) > TABSIZE);
2696 cols = opt_tab_size - (col % opt_tab_size);
2699 text = strchr(text, '\t');
2700 cols = line ? text - pos : strlen(pos);
2703 waddnstr(view->win, pos, MIN(cols, cols_max));
2708 int tilde_attr = get_line_attr(LINE_MAIN_DELIM);
2710 draw_text(view, text, view->width, 0, TRUE, tilde_attr);
2717 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2719 char refbuf[SIZEOF_STR];
2723 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2726 pipe = popen(refbuf, "r");
2730 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2731 ref = chomp_string(ref);
2737 /* This is the only fatal call, since it can "corrupt" the buffer. */
2738 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2745 add_pager_refs(struct view *view, struct line *line)
2747 char buf[SIZEOF_STR];
2748 char *commit_id = (char *)line->data + STRING_SIZE("commit ");
2750 size_t bufpos = 0, refpos = 0;
2751 const char *sep = "Refs: ";
2752 bool is_tag = FALSE;
2754 assert(line->type == LINE_COMMIT);
2756 refs = get_refs(commit_id);
2758 if (view == VIEW(REQ_VIEW_DIFF))
2759 goto try_add_describe_ref;
2764 struct ref *ref = refs[refpos];
2765 char *fmt = ref->tag ? "%s[%s]" :
2766 ref->remote ? "%s<%s>" : "%s%s";
2768 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2773 } while (refs[refpos++]->next);
2775 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2776 try_add_describe_ref:
2777 /* Add <tag>-g<commit_id> "fake" reference. */
2778 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2785 if (!realloc_lines(view, view->line_size + 1))
2788 add_line_text(view, buf, LINE_PP_REFS);
2792 pager_read(struct view *view, char *data)
2799 line = add_line_text(view, data, get_line_type(data));
2803 if (line->type == LINE_COMMIT &&
2804 (view == VIEW(REQ_VIEW_DIFF) ||
2805 view == VIEW(REQ_VIEW_LOG)))
2806 add_pager_refs(view, line);
2812 pager_request(struct view *view, enum request request, struct line *line)
2816 if (request != REQ_ENTER)
2819 if (line->type == LINE_COMMIT &&
2820 (view == VIEW(REQ_VIEW_LOG) ||
2821 view == VIEW(REQ_VIEW_PAGER))) {
2822 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2826 /* Always scroll the view even if it was split. That way
2827 * you can use Enter to scroll through the log view and
2828 * split open each commit diff. */
2829 scroll_view(view, REQ_SCROLL_LINE_DOWN);
2831 /* FIXME: A minor workaround. Scrolling the view will call report("")
2832 * but if we are scrolling a non-current view this won't properly
2833 * update the view title. */
2835 update_view_title(view);
2841 pager_grep(struct view *view, struct line *line)
2844 char *text = line->data;
2849 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
2856 pager_select(struct view *view, struct line *line)
2858 if (line->type == LINE_COMMIT) {
2859 char *text = (char *)line->data + STRING_SIZE("commit ");
2861 if (view != VIEW(REQ_VIEW_PAGER))
2862 string_copy_rev(view->ref, text);
2863 string_copy_rev(ref_commit, text);
2867 static struct view_ops pager_ops = {
2883 help_open(struct view *view)
2886 int lines = ARRAY_SIZE(req_info) + 2;
2889 if (view->lines > 0)
2892 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2893 if (!req_info[i].request)
2896 lines += run_requests + 1;
2898 view->line = calloc(lines, sizeof(*view->line));
2902 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
2904 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2907 if (req_info[i].request == REQ_NONE)
2910 if (!req_info[i].request) {
2911 add_line_text(view, "", LINE_DEFAULT);
2912 add_line_text(view, req_info[i].help, LINE_DEFAULT);
2916 key = get_key(req_info[i].request);
2918 key = "(no key defined)";
2920 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
2923 add_line_text(view, buf, LINE_DEFAULT);
2927 add_line_text(view, "", LINE_DEFAULT);
2928 add_line_text(view, "External commands:", LINE_DEFAULT);
2931 for (i = 0; i < run_requests; i++) {
2932 struct run_request *req = get_run_request(REQ_NONE + i + 1);
2938 key = get_key_name(req->key);
2940 key = "(no key defined)";
2942 if (!string_format(buf, " %-10s %-14s `%s`",
2943 keymap_table[req->keymap].name,
2947 add_line_text(view, buf, LINE_DEFAULT);
2953 static struct view_ops help_ops = {
2968 struct tree_stack_entry {
2969 struct tree_stack_entry *prev; /* Entry below this in the stack */
2970 unsigned long lineno; /* Line number to restore */
2971 char *name; /* Position of name in opt_path */
2974 /* The top of the path stack. */
2975 static struct tree_stack_entry *tree_stack = NULL;
2976 unsigned long tree_lineno = 0;
2979 pop_tree_stack_entry(void)
2981 struct tree_stack_entry *entry = tree_stack;
2983 tree_lineno = entry->lineno;
2985 tree_stack = entry->prev;
2990 push_tree_stack_entry(char *name, unsigned long lineno)
2992 struct tree_stack_entry *entry = calloc(1, sizeof(*entry));
2993 size_t pathlen = strlen(opt_path);
2998 entry->prev = tree_stack;
2999 entry->name = opt_path + pathlen;
3002 if (!string_format_from(opt_path, &pathlen, "%s/", name)) {
3003 pop_tree_stack_entry();
3007 /* Move the current line to the first tree entry. */
3009 entry->lineno = lineno;
3012 /* Parse output from git-ls-tree(1):
3014 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
3015 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
3016 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
3017 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
3020 #define SIZEOF_TREE_ATTR \
3021 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
3023 #define TREE_UP_FORMAT "040000 tree %s\t.."
3026 tree_compare_entry(enum line_type type1, char *name1,
3027 enum line_type type2, char *name2)
3029 if (type1 != type2) {
3030 if (type1 == LINE_TREE_DIR)
3035 return strcmp(name1, name2);
3039 tree_read(struct view *view, char *text)
3041 size_t textlen = text ? strlen(text) : 0;
3042 char buf[SIZEOF_STR];
3044 enum line_type type;
3045 bool first_read = view->lines == 0;
3047 if (textlen <= SIZEOF_TREE_ATTR)
3050 type = text[STRING_SIZE("100644 ")] == 't'
3051 ? LINE_TREE_DIR : LINE_TREE_FILE;
3054 /* Add path info line */
3055 if (!string_format(buf, "Directory path /%s", opt_path) ||
3056 !realloc_lines(view, view->line_size + 1) ||
3057 !add_line_text(view, buf, LINE_DEFAULT))
3060 /* Insert "link" to parent directory. */
3062 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
3063 !realloc_lines(view, view->line_size + 1) ||
3064 !add_line_text(view, buf, LINE_TREE_DIR))
3069 /* Strip the path part ... */
3071 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
3072 size_t striplen = strlen(opt_path);
3073 char *path = text + SIZEOF_TREE_ATTR;
3075 if (pathlen > striplen)
3076 memmove(path, path + striplen,
3077 pathlen - striplen + 1);
3080 /* Skip "Directory ..." and ".." line. */
3081 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
3082 struct line *line = &view->line[pos];
3083 char *path1 = ((char *) line->data) + SIZEOF_TREE_ATTR;
3084 char *path2 = text + SIZEOF_TREE_ATTR;
3085 int cmp = tree_compare_entry(line->type, path1, type, path2);
3090 text = strdup(text);
3094 if (view->lines > pos)
3095 memmove(&view->line[pos + 1], &view->line[pos],
3096 (view->lines - pos) * sizeof(*line));
3098 line = &view->line[pos];
3105 if (!add_line_text(view, text, type))
3108 if (tree_lineno > view->lineno) {
3109 view->lineno = tree_lineno;
3117 tree_request(struct view *view, enum request request, struct line *line)
3119 enum open_flags flags;
3121 if (request == REQ_TREE_PARENT) {
3124 request = REQ_ENTER;
3125 line = &view->line[1];
3127 /* quit view if at top of tree */
3128 return REQ_VIEW_CLOSE;
3131 if (request != REQ_ENTER)
3134 /* Cleanup the stack if the tree view is at a different tree. */
3135 while (!*opt_path && tree_stack)
3136 pop_tree_stack_entry();
3138 switch (line->type) {
3140 /* Depending on whether it is a subdir or parent (updir?) link
3141 * mangle the path buffer. */
3142 if (line == &view->line[1] && *opt_path) {
3143 pop_tree_stack_entry();
3146 char *data = line->data;
3147 char *basename = data + SIZEOF_TREE_ATTR;
3149 push_tree_stack_entry(basename, view->lineno);
3152 /* Trees and subtrees share the same ID, so they are not not
3153 * unique like blobs. */
3154 flags = OPEN_RELOAD;
3155 request = REQ_VIEW_TREE;
3158 case LINE_TREE_FILE:
3159 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3160 request = REQ_VIEW_BLOB;
3167 open_view(view, request, flags);
3168 if (request == REQ_VIEW_TREE) {
3169 view->lineno = tree_lineno;
3176 tree_select(struct view *view, struct line *line)
3178 char *text = (char *)line->data + STRING_SIZE("100644 blob ");
3180 if (line->type == LINE_TREE_FILE) {
3181 string_copy_rev(ref_blob, text);
3183 } else if (line->type != LINE_TREE_DIR) {
3187 string_copy_rev(view->ref, text);
3190 static struct view_ops tree_ops = {
3201 blob_read(struct view *view, char *line)
3203 return add_line_text(view, line, LINE_DEFAULT) != NULL;
3206 static struct view_ops blob_ops = {
3225 char rev[SIZEOF_REV];
3226 char name[SIZEOF_STR];
3230 char rev[SIZEOF_REV];
3231 char name[SIZEOF_STR];
3235 static struct status stage_status;
3236 static enum line_type stage_line_type;
3238 /* Get fields from the diff line:
3239 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
3242 status_get_diff(struct status *file, char *buf, size_t bufsize)
3244 char *old_mode = buf + 1;
3245 char *new_mode = buf + 8;
3246 char *old_rev = buf + 15;
3247 char *new_rev = buf + 56;
3248 char *status = buf + 97;
3251 old_mode[-1] != ':' ||
3252 new_mode[-1] != ' ' ||
3253 old_rev[-1] != ' ' ||
3254 new_rev[-1] != ' ' ||
3258 file->status = *status;
3260 string_copy_rev(file->old.rev, old_rev);
3261 string_copy_rev(file->new.rev, new_rev);
3263 file->old.mode = strtoul(old_mode, NULL, 8);
3264 file->new.mode = strtoul(new_mode, NULL, 8);
3266 file->old.name[0] = file->new.name[0] = 0;
3272 status_run(struct view *view, const char cmd[], bool diff, enum line_type type)
3274 struct status *file = NULL;
3275 struct status *unmerged = NULL;
3276 char buf[SIZEOF_STR * 4];
3280 pipe = popen(cmd, "r");
3284 add_line_data(view, NULL, type);
3286 while (!feof(pipe) && !ferror(pipe)) {
3290 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
3293 bufsize += readsize;
3295 /* Process while we have NUL chars. */
3296 while ((sep = memchr(buf, 0, bufsize))) {
3297 size_t sepsize = sep - buf + 1;
3300 if (!realloc_lines(view, view->line_size + 1))
3303 file = calloc(1, sizeof(*file));
3307 add_line_data(view, file, type);
3310 /* Parse diff info part. */
3314 } else if (!file->status) {
3315 if (!status_get_diff(file, buf, sepsize))
3319 memmove(buf, sep + 1, bufsize);
3321 sep = memchr(buf, 0, bufsize);
3324 sepsize = sep - buf + 1;
3326 /* Collapse all 'M'odified entries that
3327 * follow a associated 'U'nmerged entry.
3329 if (file->status == 'U') {
3332 } else if (unmerged) {
3333 int collapse = !strcmp(buf, unmerged->new.name);
3344 /* Grab the old name for rename/copy. */
3345 if (!*file->old.name &&
3346 (file->status == 'R' || file->status == 'C')) {
3347 sepsize = sep - buf + 1;
3348 string_ncopy(file->old.name, buf, sepsize);
3350 memmove(buf, sep + 1, bufsize);
3352 sep = memchr(buf, 0, bufsize);
3355 sepsize = sep - buf + 1;
3358 /* git-ls-files just delivers a NUL separated
3359 * list of file names similar to the second half
3360 * of the git-diff-* output. */
3361 string_ncopy(file->new.name, buf, sepsize);
3362 if (!*file->old.name)
3363 string_copy(file->old.name, file->new.name);
3365 memmove(buf, sep + 1, bufsize);
3376 if (!view->line[view->lines - 1].data)
3377 add_line_data(view, NULL, LINE_STAT_NONE);
3383 /* Don't show unmerged entries in the staged section. */
3384 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached -M HEAD"
3385 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
3386 #define STATUS_LIST_OTHER_CMD \
3387 "git ls-files -z --others --exclude-per-directory=.gitignore"
3389 #define STATUS_DIFF_INDEX_SHOW_CMD \
3390 "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s %s 2>/dev/null"
3392 #define STATUS_DIFF_FILES_SHOW_CMD \
3393 "git diff-files --root --patch-with-stat -C -M -- %s %s 2>/dev/null"
3395 /* First parse staged info using git-diff-index(1), then parse unstaged
3396 * info using git-diff-files(1), and finally untracked files using
3397 * git-ls-files(1). */
3399 status_open(struct view *view)
3401 struct stat statbuf;
3402 char exclude[SIZEOF_STR];
3403 char cmd[SIZEOF_STR];
3404 unsigned long prev_lineno = view->lineno;
3407 for (i = 0; i < view->lines; i++)
3408 free(view->line[i].data);
3410 view->lines = view->line_size = view->lineno = 0;
3413 if (!realloc_lines(view, view->line_size + 6))
3416 if (!string_format(exclude, "%s/info/exclude", opt_git_dir))
3419 string_copy(cmd, STATUS_LIST_OTHER_CMD);
3421 if (stat(exclude, &statbuf) >= 0) {
3422 size_t cmdsize = strlen(cmd);
3424 if (!string_format_from(cmd, &cmdsize, " %s", "--exclude-from=") ||
3425 sq_quote(cmd, cmdsize, exclude) >= sizeof(cmd))
3429 system("git update-index -q --refresh");
3431 if (!status_run(view, STATUS_DIFF_INDEX_CMD, TRUE, LINE_STAT_STAGED) ||
3432 !status_run(view, STATUS_DIFF_FILES_CMD, TRUE, LINE_STAT_UNSTAGED) ||
3433 !status_run(view, cmd, FALSE, LINE_STAT_UNTRACKED))
3436 /* If all went well restore the previous line number to stay in
3438 if (prev_lineno < view->lines)
3439 view->lineno = prev_lineno;
3441 view->lineno = view->lines - 1;
3447 status_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
3449 struct status *status = line->data;
3450 int tilde_attr = get_line_attr(LINE_MAIN_DELIM);
3452 wmove(view->win, lineno, 0);
3455 wattrset(view->win, get_line_attr(LINE_CURSOR));
3456 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
3459 } else if (!status && line->type != LINE_STAT_NONE) {
3460 wattrset(view->win, get_line_attr(LINE_STAT_SECTION));
3461 wchgat(view->win, -1, 0, LINE_STAT_SECTION, NULL);
3464 wattrset(view->win, get_line_attr(line->type));
3470 switch (line->type) {
3471 case LINE_STAT_STAGED:
3472 text = "Changes to be committed:";
3475 case LINE_STAT_UNSTAGED:
3476 text = "Changed but not updated:";
3479 case LINE_STAT_UNTRACKED:
3480 text = "Untracked files:";
3483 case LINE_STAT_NONE:
3484 text = " (no files)";
3491 draw_text(view, text, view->width, 0, TRUE, tilde_attr);
3495 waddch(view->win, status->status);
3497 wattrset(view->win, A_NORMAL);
3498 wmove(view->win, lineno, 4);
3499 if (view->width < 5)
3502 draw_text(view, status->new.name, view->width - 5, 5, TRUE, tilde_attr);
3507 status_enter(struct view *view, struct line *line)
3509 struct status *status = line->data;
3510 char oldpath[SIZEOF_STR] = "";
3511 char newpath[SIZEOF_STR] = "";
3515 if (line->type == LINE_STAT_NONE ||
3516 (!status && line[1].type == LINE_STAT_NONE)) {
3517 report("No file to diff");
3522 if (sq_quote(oldpath, 0, status->old.name) >= sizeof(oldpath))
3524 /* Diffs for unmerged entries are empty when pasing the
3525 * new path, so leave it empty. */
3526 if (status->status != 'U' &&
3527 sq_quote(newpath, 0, status->new.name) >= sizeof(newpath))
3532 line->type != LINE_STAT_UNTRACKED &&
3533 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
3536 switch (line->type) {
3537 case LINE_STAT_STAGED:
3538 if (!string_format_from(opt_cmd, &cmdsize,
3539 STATUS_DIFF_INDEX_SHOW_CMD, oldpath, newpath))
3542 info = "Staged changes to %s";
3544 info = "Staged changes";
3547 case LINE_STAT_UNSTAGED:
3548 if (!string_format_from(opt_cmd, &cmdsize,
3549 STATUS_DIFF_FILES_SHOW_CMD, oldpath, newpath))
3552 info = "Unstaged changes to %s";
3554 info = "Unstaged changes";
3557 case LINE_STAT_UNTRACKED:
3563 report("No file to show");
3567 opt_pipe = fopen(status->new.name, "r");
3568 info = "Untracked file %s";
3572 die("line type %d not handled in switch", line->type);
3575 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | OPEN_SPLIT);
3576 if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) {
3578 stage_status = *status;
3580 memset(&stage_status, 0, sizeof(stage_status));
3583 stage_line_type = line->type;
3584 string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.new.name);
3592 status_update_file(struct view *view, struct status *status, enum line_type type)
3594 char cmd[SIZEOF_STR];
3595 char buf[SIZEOF_STR];
3602 type != LINE_STAT_UNTRACKED &&
3603 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
3607 case LINE_STAT_STAGED:
3608 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
3611 status->old.name, 0))
3614 string_add(cmd, cmdsize, "git update-index -z --index-info");
3617 case LINE_STAT_UNSTAGED:
3618 case LINE_STAT_UNTRACKED:
3619 if (!string_format_from(buf, &bufsize, "%s%c", status->new.name, 0))
3622 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
3626 die("line type %d not handled in switch", type);
3629 pipe = popen(cmd, "w");
3633 while (!ferror(pipe) && written < bufsize) {
3634 written += fwrite(buf + written, 1, bufsize - written, pipe);
3639 if (written != bufsize)
3646 status_update(struct view *view)
3648 struct line *line = &view->line[view->lineno];
3650 assert(view->lines);
3653 while (++line < view->line + view->lines && line->data) {
3654 if (!status_update_file(view, line->data, line->type))
3655 report("Failed to update file status");
3658 if (!line[-1].data) {
3659 report("Nothing to update");
3663 } else if (!status_update_file(view, line->data, line->type)) {
3664 report("Failed to update file status");
3669 status_request(struct view *view, enum request request, struct line *line)
3671 struct status *status = line->data;
3674 case REQ_STATUS_UPDATE:
3675 status_update(view);
3678 case REQ_STATUS_MERGE:
3679 if (!status || status->status != 'U') {
3680 report("Merging only possible for files with unmerged status ('U').");
3683 open_mergetool(status->new.name);
3690 open_editor(status->status != '?', status->new.name);
3694 /* After returning the status view has been split to
3695 * show the stage view. No further reloading is
3697 status_enter(view, line);
3701 /* Simply reload the view. */
3708 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
3714 status_select(struct view *view, struct line *line)
3716 struct status *status = line->data;
3717 char file[SIZEOF_STR] = "all files";
3721 if (status && !string_format(file, "'%s'", status->new.name))
3724 if (!status && line[1].type == LINE_STAT_NONE)
3727 switch (line->type) {
3728 case LINE_STAT_STAGED:
3729 text = "Press %s to unstage %s for commit";
3732 case LINE_STAT_UNSTAGED:
3733 text = "Press %s to stage %s for commit";
3736 case LINE_STAT_UNTRACKED:
3737 text = "Press %s to stage %s for addition";
3740 case LINE_STAT_NONE:
3741 text = "Nothing to update";
3745 die("line type %d not handled in switch", line->type);
3748 if (status && status->status == 'U') {
3749 text = "Press %s to resolve conflict in %s";
3750 key = get_key(REQ_STATUS_MERGE);
3753 key = get_key(REQ_STATUS_UPDATE);
3756 string_format(view->ref, text, key, file);
3760 status_grep(struct view *view, struct line *line)
3762 struct status *status = line->data;
3763 enum { S_STATUS, S_NAME, S_END } state;
3770 for (state = S_STATUS; state < S_END; state++) {
3774 case S_NAME: text = status->new.name; break;
3776 buf[0] = status->status;
3784 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3791 static struct view_ops status_ops = {
3803 stage_diff_line(FILE *pipe, struct line *line)
3805 char *buf = line->data;
3806 size_t bufsize = strlen(buf);
3809 while (!ferror(pipe) && written < bufsize) {
3810 written += fwrite(buf + written, 1, bufsize - written, pipe);
3815 return written == bufsize;
3818 static struct line *
3819 stage_diff_hdr(struct view *view, struct line *line)
3821 int diff_hdr_dir = line->type == LINE_DIFF_CHUNK ? -1 : 1;
3822 struct line *diff_hdr;
3824 if (line->type == LINE_DIFF_CHUNK)
3825 diff_hdr = line - 1;
3827 diff_hdr = view->line + 1;
3829 while (diff_hdr > view->line && diff_hdr < view->line + view->lines) {
3830 if (diff_hdr->type == LINE_DIFF_HEADER)
3833 diff_hdr += diff_hdr_dir;
3840 stage_update_chunk(struct view *view, struct line *line)
3842 char cmd[SIZEOF_STR];
3844 struct line *diff_hdr, *diff_chunk, *diff_end;
3847 diff_hdr = stage_diff_hdr(view, line);
3852 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
3855 if (!string_format_from(cmd, &cmdsize,
3856 "git apply --cached %s - && "
3857 "git update-index -q --unmerged --refresh 2>/dev/null",
3858 stage_line_type == LINE_STAT_STAGED ? "-R" : ""))
3861 pipe = popen(cmd, "w");
3865 diff_end = view->line + view->lines;
3866 if (line->type != LINE_DIFF_CHUNK) {
3867 diff_chunk = diff_hdr;
3870 for (diff_chunk = line + 1; diff_chunk < diff_end; diff_chunk++)
3871 if (diff_chunk->type == LINE_DIFF_CHUNK ||
3872 diff_chunk->type == LINE_DIFF_HEADER)
3873 diff_end = diff_chunk;
3877 while (diff_hdr->type != LINE_DIFF_CHUNK) {
3878 switch (diff_hdr->type) {
3879 case LINE_DIFF_HEADER:
3880 case LINE_DIFF_INDEX:
3890 if (!stage_diff_line(pipe, diff_hdr++)) {
3897 while (diff_chunk < diff_end && stage_diff_line(pipe, diff_chunk))
3902 if (diff_chunk != diff_end)
3909 stage_update(struct view *view, struct line *line)
3911 if (stage_line_type != LINE_STAT_UNTRACKED &&
3912 (line->type == LINE_DIFF_CHUNK || !stage_status.status)) {
3913 if (!stage_update_chunk(view, line)) {
3914 report("Failed to apply chunk");
3918 } else if (!status_update_file(view, &stage_status, stage_line_type)) {
3919 report("Failed to update file");
3923 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
3925 view = VIEW(REQ_VIEW_STATUS);
3926 if (view_is_displayed(view))
3927 status_enter(view, &view->line[view->lineno]);
3931 stage_request(struct view *view, enum request request, struct line *line)
3934 case REQ_STATUS_UPDATE:
3935 stage_update(view, line);
3939 if (!stage_status.new.name[0])
3942 open_editor(stage_status.status != '?', stage_status.new.name);
3946 pager_request(view, request, line);
3956 static struct view_ops stage_ops = {
3972 char id[SIZEOF_REV]; /* SHA1 ID. */
3973 char title[128]; /* First line of the commit message. */
3974 char author[75]; /* Author of the commit. */
3975 struct tm time; /* Date from the author ident. */
3976 struct ref **refs; /* Repository references. */
3977 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
3978 size_t graph_size; /* The width of the graph array. */
3981 /* Size of rev graph with no "padding" columns */
3982 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
3985 struct rev_graph *prev, *next, *parents;
3986 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
3988 struct commit *commit;
3990 unsigned int boundary:1;
3993 /* Parents of the commit being visualized. */
3994 static struct rev_graph graph_parents[4];
3996 /* The current stack of revisions on the graph. */
3997 static struct rev_graph graph_stacks[4] = {
3998 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
3999 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
4000 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
4001 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
4005 graph_parent_is_merge(struct rev_graph *graph)
4007 return graph->parents->size > 1;
4011 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
4013 struct commit *commit = graph->commit;
4015 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
4016 commit->graph[commit->graph_size++] = symbol;
4020 done_rev_graph(struct rev_graph *graph)
4022 if (graph_parent_is_merge(graph) &&
4023 graph->pos < graph->size - 1 &&
4024 graph->next->size == graph->size + graph->parents->size - 1) {
4025 size_t i = graph->pos + graph->parents->size - 1;
4027 graph->commit->graph_size = i * 2;
4028 while (i < graph->next->size - 1) {
4029 append_to_rev_graph(graph, ' ');
4030 append_to_rev_graph(graph, '\\');
4035 graph->size = graph->pos = 0;
4036 graph->commit = NULL;
4037 memset(graph->parents, 0, sizeof(*graph->parents));
4041 push_rev_graph(struct rev_graph *graph, char *parent)
4045 /* "Collapse" duplicate parents lines.
4047 * FIXME: This needs to also update update the drawn graph but
4048 * for now it just serves as a method for pruning graph lines. */
4049 for (i = 0; i < graph->size; i++)
4050 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
4053 if (graph->size < SIZEOF_REVITEMS) {
4054 string_copy_rev(graph->rev[graph->size++], parent);
4059 get_rev_graph_symbol(struct rev_graph *graph)
4063 if (graph->boundary)
4064 symbol = REVGRAPH_BOUND;
4065 else if (graph->parents->size == 0)
4066 symbol = REVGRAPH_INIT;
4067 else if (graph_parent_is_merge(graph))
4068 symbol = REVGRAPH_MERGE;
4069 else if (graph->pos >= graph->size)
4070 symbol = REVGRAPH_BRANCH;
4072 symbol = REVGRAPH_COMMIT;
4078 draw_rev_graph(struct rev_graph *graph)
4081 chtype separator, line;
4083 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
4084 static struct rev_filler fillers[] = {
4085 { ' ', REVGRAPH_LINE },
4090 chtype symbol = get_rev_graph_symbol(graph);
4091 struct rev_filler *filler;
4094 filler = &fillers[DEFAULT];
4096 for (i = 0; i < graph->pos; i++) {
4097 append_to_rev_graph(graph, filler->line);
4098 if (graph_parent_is_merge(graph->prev) &&
4099 graph->prev->pos == i)
4100 filler = &fillers[RSHARP];
4102 append_to_rev_graph(graph, filler->separator);
4105 /* Place the symbol for this revision. */
4106 append_to_rev_graph(graph, symbol);
4108 if (graph->prev->size > graph->size)
4109 filler = &fillers[RDIAG];
4111 filler = &fillers[DEFAULT];
4115 for (; i < graph->size; i++) {
4116 append_to_rev_graph(graph, filler->separator);
4117 append_to_rev_graph(graph, filler->line);
4118 if (graph_parent_is_merge(graph->prev) &&
4119 i < graph->prev->pos + graph->parents->size)
4120 filler = &fillers[RSHARP];
4121 if (graph->prev->size > graph->size)
4122 filler = &fillers[LDIAG];
4125 if (graph->prev->size > graph->size) {
4126 append_to_rev_graph(graph, filler->separator);
4127 if (filler->line != ' ')
4128 append_to_rev_graph(graph, filler->line);
4132 /* Prepare the next rev graph */
4134 prepare_rev_graph(struct rev_graph *graph)
4138 /* First, traverse all lines of revisions up to the active one. */
4139 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
4140 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
4143 push_rev_graph(graph->next, graph->rev[graph->pos]);
4146 /* Interleave the new revision parent(s). */
4147 for (i = 0; !graph->boundary && i < graph->parents->size; i++)
4148 push_rev_graph(graph->next, graph->parents->rev[i]);
4150 /* Lastly, put any remaining revisions. */
4151 for (i = graph->pos + 1; i < graph->size; i++)
4152 push_rev_graph(graph->next, graph->rev[i]);
4156 update_rev_graph(struct rev_graph *graph)
4158 /* If this is the finalizing update ... */
4160 prepare_rev_graph(graph);
4162 /* Graph visualization needs a one rev look-ahead,
4163 * so the first update doesn't visualize anything. */
4164 if (!graph->prev->commit)
4167 draw_rev_graph(graph->prev);
4168 done_rev_graph(graph->prev->prev);
4177 main_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
4179 char buf[DATE_COLS + 1];
4180 struct commit *commit = line->data;
4181 enum line_type type;
4187 if (!*commit->author)
4190 space = view->width;
4191 wmove(view->win, lineno, col);
4195 wattrset(view->win, get_line_attr(type));
4196 wchgat(view->win, -1, 0, type, NULL);
4199 type = LINE_MAIN_COMMIT;
4200 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
4201 tilde_attr = get_line_attr(LINE_MAIN_DELIM);
4207 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
4209 view, buf, view->width - col, col, FALSE, tilde_attr);
4211 view, " ", view->width - col - n, col + n, FALSE,
4215 wmove(view->win, lineno, col);
4216 if (col >= view->width)
4219 if (type != LINE_CURSOR)
4220 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
4225 max_len = view->width - col;
4226 if (max_len > AUTHOR_COLS - 1)
4227 max_len = AUTHOR_COLS - 1;
4229 view, commit->author, max_len, col, TRUE, tilde_attr);
4231 if (col >= view->width)
4235 if (opt_rev_graph && commit->graph_size) {
4236 size_t graph_size = view->width - col;
4239 if (type != LINE_CURSOR)
4240 wattrset(view->win, get_line_attr(LINE_MAIN_REVGRAPH));
4241 wmove(view->win, lineno, col);
4242 if (graph_size > commit->graph_size)
4243 graph_size = commit->graph_size;
4244 /* Using waddch() instead of waddnstr() ensures that
4245 * they'll be rendered correctly for the cursor line. */
4246 for (i = 0; i < graph_size; i++)
4247 waddch(view->win, commit->graph[i]);
4249 col += commit->graph_size + 1;
4250 if (col >= view->width)
4252 waddch(view->win, ' ');
4254 if (type != LINE_CURSOR)
4255 wattrset(view->win, A_NORMAL);
4257 wmove(view->win, lineno, col);
4259 if (opt_show_refs && commit->refs) {
4263 if (type == LINE_CURSOR)
4265 else if (commit->refs[i]->ltag)
4266 wattrset(view->win, get_line_attr(LINE_MAIN_LOCAL_TAG));
4267 else if (commit->refs[i]->tag)
4268 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
4269 else if (commit->refs[i]->remote)
4270 wattrset(view->win, get_line_attr(LINE_MAIN_REMOTE));
4272 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
4275 view, "[", view->width - col, col, TRUE,
4278 view, commit->refs[i]->name, view->width - col,
4279 col, TRUE, tilde_attr);
4281 view, "]", view->width - col, col, TRUE,
4283 if (type != LINE_CURSOR)
4284 wattrset(view->win, A_NORMAL);
4286 view, " ", view->width - col, col, TRUE,
4288 if (col >= view->width)
4290 } while (commit->refs[i++]->next);
4293 if (type != LINE_CURSOR)
4294 wattrset(view->win, get_line_attr(type));
4297 view, commit->title, view->width - col, col, TRUE, tilde_attr);
4302 /* Reads git log --pretty=raw output and parses it into the commit struct. */
4304 main_read(struct view *view, char *line)
4306 static struct rev_graph *graph = graph_stacks;
4307 enum line_type type;
4308 struct commit *commit;
4311 update_rev_graph(graph);
4315 type = get_line_type(line);
4316 if (type == LINE_COMMIT) {
4317 commit = calloc(1, sizeof(struct commit));
4321 line += STRING_SIZE("commit ");
4323 graph->boundary = 1;
4327 string_copy_rev(commit->id, line);
4328 commit->refs = get_refs(commit->id);
4329 graph->commit = commit;
4330 add_line_data(view, commit, LINE_MAIN_COMMIT);
4336 commit = view->line[view->lines - 1].data;
4340 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
4345 /* Parse author lines where the name may be empty:
4346 * author <email@address.tld> 1138474660 +0100
4348 char *ident = line + STRING_SIZE("author ");
4349 char *nameend = strchr(ident, '<');
4350 char *emailend = strchr(ident, '>');
4352 if (!nameend || !emailend)
4355 update_rev_graph(graph);
4356 graph = graph->next;
4358 *nameend = *emailend = 0;
4359 ident = chomp_string(ident);
4361 ident = chomp_string(nameend + 1);
4366 string_ncopy(commit->author, ident, strlen(ident));
4368 /* Parse epoch and timezone */
4369 if (emailend[1] == ' ') {
4370 char *secs = emailend + 2;
4371 char *zone = strchr(secs, ' ');
4372 time_t time = (time_t) atol(secs);
4374 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
4378 tz = ('0' - zone[1]) * 60 * 60 * 10;
4379 tz += ('0' - zone[2]) * 60 * 60;
4380 tz += ('0' - zone[3]) * 60;
4381 tz += ('0' - zone[4]) * 60;
4389 gmtime_r(&time, &commit->time);
4394 /* Fill in the commit title if it has not already been set. */
4395 if (commit->title[0])
4398 /* Require titles to start with a non-space character at the
4399 * offset used by git log. */
4400 if (strncmp(line, " ", 4))
4403 /* Well, if the title starts with a whitespace character,
4404 * try to be forgiving. Otherwise we end up with no title. */
4405 while (isspace(*line))
4409 /* FIXME: More graceful handling of titles; append "..." to
4410 * shortened titles, etc. */
4412 string_ncopy(commit->title, line, strlen(line));
4419 main_request(struct view *view, enum request request, struct line *line)
4421 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
4423 if (request == REQ_ENTER)
4424 open_view(view, REQ_VIEW_DIFF, flags);
4432 main_grep(struct view *view, struct line *line)
4434 struct commit *commit = line->data;
4435 enum { S_TITLE, S_AUTHOR, S_DATE, S_END } state;
4436 char buf[DATE_COLS + 1];
4439 for (state = S_TITLE; state < S_END; state++) {
4443 case S_TITLE: text = commit->title; break;
4444 case S_AUTHOR: text = commit->author; break;
4446 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
4455 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4463 main_select(struct view *view, struct line *line)
4465 struct commit *commit = line->data;
4467 string_copy_rev(view->ref, commit->id);
4468 string_copy_rev(ref_commit, view->ref);
4471 static struct view_ops main_ops = {
4483 * Unicode / UTF-8 handling
4485 * NOTE: Much of the following code for dealing with unicode is derived from
4486 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
4487 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
4490 /* I've (over)annotated a lot of code snippets because I am not entirely
4491 * confident that the approach taken by this small UTF-8 interface is correct.
4495 unicode_width(unsigned long c)
4498 (c <= 0x115f /* Hangul Jamo */
4501 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
4503 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
4504 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
4505 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
4506 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
4507 || (c >= 0xffe0 && c <= 0xffe6)
4508 || (c >= 0x20000 && c <= 0x2fffd)
4509 || (c >= 0x30000 && c <= 0x3fffd)))
4513 return opt_tab_size;
4518 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
4519 * Illegal bytes are set one. */
4520 static const unsigned char utf8_bytes[256] = {
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 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,
4524 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,
4525 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,
4526 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,
4527 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,
4528 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,
4531 /* Decode UTF-8 multi-byte representation into a unicode character. */
4532 static inline unsigned long
4533 utf8_to_unicode(const char *string, size_t length)
4535 unsigned long unicode;
4539 unicode = string[0];
4542 unicode = (string[0] & 0x1f) << 6;
4543 unicode += (string[1] & 0x3f);
4546 unicode = (string[0] & 0x0f) << 12;
4547 unicode += ((string[1] & 0x3f) << 6);
4548 unicode += (string[2] & 0x3f);
4551 unicode = (string[0] & 0x0f) << 18;
4552 unicode += ((string[1] & 0x3f) << 12);
4553 unicode += ((string[2] & 0x3f) << 6);
4554 unicode += (string[3] & 0x3f);
4557 unicode = (string[0] & 0x0f) << 24;
4558 unicode += ((string[1] & 0x3f) << 18);
4559 unicode += ((string[2] & 0x3f) << 12);
4560 unicode += ((string[3] & 0x3f) << 6);
4561 unicode += (string[4] & 0x3f);
4564 unicode = (string[0] & 0x01) << 30;
4565 unicode += ((string[1] & 0x3f) << 24);
4566 unicode += ((string[2] & 0x3f) << 18);
4567 unicode += ((string[3] & 0x3f) << 12);
4568 unicode += ((string[4] & 0x3f) << 6);
4569 unicode += (string[5] & 0x3f);
4572 die("Invalid unicode length");
4575 /* Invalid characters could return the special 0xfffd value but NUL
4576 * should be just as good. */
4577 return unicode > 0xffff ? 0 : unicode;
4580 /* Calculates how much of string can be shown within the given maximum width
4581 * and sets trimmed parameter to non-zero value if all of string could not be
4582 * shown. If the reserve flag is TRUE, it will reserve at least one
4583 * trailing character, which can be useful when drawing a delimiter.
4585 * Returns the number of bytes to output from string to satisfy max_width. */
4587 utf8_length(const char *string, size_t max_width, int *trimmed, bool reserve)
4589 const char *start = string;
4590 const char *end = strchr(string, '\0');
4591 unsigned char last_bytes = 0;
4596 while (string < end) {
4597 int c = *(unsigned char *) string;
4598 unsigned char bytes = utf8_bytes[c];
4600 unsigned long unicode;
4602 if (string + bytes > end)
4605 /* Change representation to figure out whether
4606 * it is a single- or double-width character. */
4608 unicode = utf8_to_unicode(string, bytes);
4609 /* FIXME: Graceful handling of invalid unicode character. */
4613 ucwidth = unicode_width(unicode);
4615 if (width > max_width) {
4617 if (reserve && width - ucwidth == max_width) {
4618 string -= last_bytes;
4627 return string - start;
4635 /* Whether or not the curses interface has been initialized. */
4636 static bool cursed = FALSE;
4638 /* The status window is used for polling keystrokes. */
4639 static WINDOW *status_win;
4641 static bool status_empty = TRUE;
4643 /* Update status and title window. */
4645 report(const char *msg, ...)
4647 struct view *view = display[current_view];
4653 char buf[SIZEOF_STR];
4656 va_start(args, msg);
4657 if (vsnprintf(buf, sizeof(buf), msg, args) >= sizeof(buf)) {
4658 buf[sizeof(buf) - 1] = 0;
4659 buf[sizeof(buf) - 2] = '.';
4660 buf[sizeof(buf) - 3] = '.';
4661 buf[sizeof(buf) - 4] = '.';
4667 if (!status_empty || *msg) {
4670 va_start(args, msg);
4672 wmove(status_win, 0, 0);
4674 vwprintw(status_win, msg, args);
4675 status_empty = FALSE;
4677 status_empty = TRUE;
4679 wclrtoeol(status_win);
4680 wrefresh(status_win);
4685 update_view_title(view);
4686 update_display_cursor(view);
4689 /* Controls when nodelay should be in effect when polling user input. */
4691 set_nonblocking_input(bool loading)
4693 static unsigned int loading_views;
4695 if ((loading == FALSE && loading_views-- == 1) ||
4696 (loading == TRUE && loading_views++ == 0))
4697 nodelay(status_win, loading);
4705 /* Initialize the curses library */
4706 if (isatty(STDIN_FILENO)) {
4707 cursed = !!initscr();
4709 /* Leave stdin and stdout alone when acting as a pager. */
4710 FILE *io = fopen("/dev/tty", "r+");
4713 die("Failed to open /dev/tty");
4714 cursed = !!newterm(NULL, io, io);
4718 die("Failed to initialize curses");
4720 nonl(); /* Tell curses not to do NL->CR/NL on output */
4721 cbreak(); /* Take input chars one at a time, no wait for \n */
4722 noecho(); /* Don't echo input */
4723 leaveok(stdscr, TRUE);
4728 getmaxyx(stdscr, y, x);
4729 status_win = newwin(1, 0, y - 1, 0);
4731 die("Failed to create status window");
4733 /* Enable keyboard mapping */
4734 keypad(status_win, TRUE);
4735 wbkgdset(status_win, get_line_attr(LINE_STATUS));
4739 read_prompt(const char *prompt)
4741 enum { READING, STOP, CANCEL } status = READING;
4742 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
4745 while (status == READING) {
4751 foreach_view (view, i)
4756 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
4757 wclrtoeol(status_win);
4759 /* Refresh, accept single keystroke of input */
4760 key = wgetch(status_win);
4765 status = pos ? STOP : CANCEL;
4783 if (pos >= sizeof(buf)) {
4784 report("Input string too long");
4789 buf[pos++] = (char) key;
4793 /* Clear the status window */
4794 status_empty = FALSE;
4797 if (status == CANCEL)
4806 * Repository references
4809 static struct ref *refs;
4810 static size_t refs_size;
4812 /* Id <-> ref store */
4813 static struct ref ***id_refs;
4814 static size_t id_refs_size;
4816 static struct ref **
4819 struct ref ***tmp_id_refs;
4820 struct ref **ref_list = NULL;
4821 size_t ref_list_size = 0;
4824 for (i = 0; i < id_refs_size; i++)
4825 if (!strcmp(id, id_refs[i][0]->id))
4828 tmp_id_refs = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs));
4832 id_refs = tmp_id_refs;
4834 for (i = 0; i < refs_size; i++) {
4837 if (strcmp(id, refs[i].id))
4840 tmp = realloc(ref_list, (ref_list_size + 1) * sizeof(*ref_list));
4848 if (ref_list_size > 0)
4849 ref_list[ref_list_size - 1]->next = 1;
4850 ref_list[ref_list_size] = &refs[i];
4852 /* XXX: The properties of the commit chains ensures that we can
4853 * safely modify the shared ref. The repo references will
4854 * always be similar for the same id. */
4855 ref_list[ref_list_size]->next = 0;
4860 id_refs[id_refs_size++] = ref_list;
4866 read_ref(char *id, size_t idlen, char *name, size_t namelen)
4871 bool remote = FALSE;
4872 bool check_replace = FALSE;
4874 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
4875 if (!strcmp(name + namelen - 3, "^{}")) {
4878 if (refs_size > 0 && refs[refs_size - 1].ltag == TRUE)
4879 check_replace = TRUE;
4885 namelen -= STRING_SIZE("refs/tags/");
4886 name += STRING_SIZE("refs/tags/");
4888 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
4890 namelen -= STRING_SIZE("refs/remotes/");
4891 name += STRING_SIZE("refs/remotes/");
4893 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
4894 namelen -= STRING_SIZE("refs/heads/");
4895 name += STRING_SIZE("refs/heads/");
4897 } else if (!strcmp(name, "HEAD")) {
4901 if (check_replace && !strcmp(name, refs[refs_size - 1].name)) {
4902 /* it's an annotated tag, replace the previous sha1 with the
4903 * resolved commit id; relies on the fact git-ls-remote lists
4904 * the commit id of an annotated tag right beofre the commit id
4906 refs[refs_size - 1].ltag = ltag;
4907 string_copy_rev(refs[refs_size - 1].id, id);
4911 refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
4915 ref = &refs[refs_size++];
4916 ref->name = malloc(namelen + 1);
4920 strncpy(ref->name, name, namelen);
4921 ref->name[namelen] = 0;
4924 ref->remote = remote;
4925 string_copy_rev(ref->id, id);
4933 const char *cmd_env = getenv("TIG_LS_REMOTE");
4934 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
4936 return read_properties(popen(cmd, "r"), "\t", read_ref);
4940 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
4942 if (!strcmp(name, "i18n.commitencoding"))
4943 string_ncopy(opt_encoding, value, valuelen);
4945 if (!strcmp(name, "core.editor"))
4946 string_ncopy(opt_editor, value, valuelen);
4952 load_repo_config(void)
4954 return read_properties(popen(GIT_CONFIG " --list", "r"),
4955 "=", read_repo_config_option);
4959 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
4961 if (!opt_git_dir[0]) {
4962 string_ncopy(opt_git_dir, name, namelen);
4964 } else if (opt_is_inside_work_tree == -1) {
4965 /* This can be 3 different values depending on the
4966 * version of git being used. If git-rev-parse does not
4967 * understand --is-inside-work-tree it will simply echo
4968 * the option else either "true" or "false" is printed.
4969 * Default to true for the unknown case. */
4970 opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
4973 string_ncopy(opt_cdup, name, namelen);
4979 /* XXX: The line outputted by "--show-cdup" can be empty so the option
4980 * must be the last one! */
4982 load_repo_info(void)
4984 return read_properties(popen("git rev-parse --git-dir --is-inside-work-tree --show-cdup 2>/dev/null", "r"),
4985 "=", read_repo_info);
4989 read_properties(FILE *pipe, const char *separators,
4990 int (*read_property)(char *, size_t, char *, size_t))
4992 char buffer[BUFSIZ];
4999 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
5004 name = chomp_string(name);
5005 namelen = strcspn(name, separators);
5007 if (name[namelen]) {
5009 value = chomp_string(name + namelen + 1);
5010 valuelen = strlen(value);
5017 state = read_property(name, namelen, value, valuelen);
5020 if (state != ERR && ferror(pipe))
5033 static void __NORETURN
5036 /* XXX: Restore tty modes and let the OS cleanup the rest! */
5042 static void __NORETURN
5043 die(const char *err, ...)
5049 va_start(args, err);
5050 fputs("tig: ", stderr);
5051 vfprintf(stderr, err, args);
5052 fputs("\n", stderr);
5059 warn(const char *msg, ...)
5063 va_start(args, msg);
5064 fputs("tig warning: ", stderr);
5065 vfprintf(stderr, msg, args);
5066 fputs("\n", stderr);
5071 main(int argc, char *argv[])
5074 enum request request;
5077 signal(SIGINT, quit);
5079 if (setlocale(LC_ALL, "")) {
5080 char *codeset = nl_langinfo(CODESET);
5082 string_ncopy(opt_codeset, codeset, strlen(codeset));
5085 if (load_repo_info() == ERR)
5086 die("Failed to load repo info.");
5088 if (load_options() == ERR)
5089 die("Failed to load user config.");
5091 /* Load the repo config file so options can be overwritten from
5092 * the command line. */
5093 if (load_repo_config() == ERR)
5094 die("Failed to load repo config.");
5096 if (!parse_options(argc, argv))
5099 /* Require a git repository unless when running in pager mode. */
5100 if (!opt_git_dir[0])
5101 die("Not a git repository");
5103 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
5104 opt_iconv = iconv_open(opt_codeset, opt_encoding);
5105 if (opt_iconv == ICONV_NONE)
5106 die("Failed to initialize character set conversion");
5109 if (load_refs() == ERR)
5110 die("Failed to load refs.");
5112 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
5113 view->cmd_env = getenv(view->cmd_env);
5115 request = opt_request;
5119 while (view_driver(display[current_view], request)) {
5123 foreach_view (view, i)
5126 /* Refresh, accept single keystroke of input */
5127 key = wgetch(status_win);
5129 /* wgetch() with nodelay() enabled returns ERR when there's no
5136 request = get_keybinding(display[current_view]->keymap, key);
5138 /* Some low-level request handling. This keeps access to
5139 * status_win restricted. */
5143 char *cmd = read_prompt(":");
5145 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
5146 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
5147 opt_request = REQ_VIEW_DIFF;
5149 opt_request = REQ_VIEW_PAGER;
5158 case REQ_SEARCH_BACK:
5160 const char *prompt = request == REQ_SEARCH
5162 char *search = read_prompt(prompt);
5165 string_ncopy(opt_search, search, strlen(search));
5170 case REQ_SCREEN_RESIZE:
5174 getmaxyx(stdscr, height, width);
5176 /* Resize the status view and let the view driver take
5177 * care of resizing the displayed views. */
5178 wresize(status_win, 1, width);
5179 mvwin(status_win, height - 1, 0);
5180 wrefresh(status_win);