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>
48 #define __NORETURN __attribute__((__noreturn__))
53 static void __NORETURN die(const char *err, ...);
54 static void report(const char *msg, ...);
55 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, size_t, char *, size_t));
56 static void set_nonblocking_input(bool loading);
57 static size_t utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed);
59 #define ABS(x) ((x) >= 0 ? (x) : -(x))
60 #define MIN(x, y) ((x) < (y) ? (x) : (y))
62 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
63 #define STRING_SIZE(x) (sizeof(x) - 1)
65 #define SIZEOF_STR 1024 /* Default string size. */
66 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
67 #define SIZEOF_REV 41 /* Holds a SHA-1 and an ending NUL */
71 #define REVGRAPH_INIT 'I'
72 #define REVGRAPH_MERGE 'M'
73 #define REVGRAPH_BRANCH '+'
74 #define REVGRAPH_COMMIT '*'
75 #define REVGRAPH_LINE '|'
77 #define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
79 /* This color name can be used to refer to the default term colors. */
80 #define COLOR_DEFAULT (-1)
82 #define ICONV_NONE ((iconv_t) -1)
84 #define ICONV_CONST /* nothing */
87 /* The format and size of the date column in the main view. */
88 #define DATE_FORMAT "%Y-%m-%d %H:%M"
89 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
91 #define AUTHOR_COLS 20
93 /* The default interval between line numbers. */
94 #define NUMBER_INTERVAL 1
98 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
101 #define GIT_CONFIG "git config"
104 #define TIG_LS_REMOTE \
105 "git ls-remote $(git rev-parse --git-dir) 2>/dev/null"
107 #define TIG_DIFF_CMD \
108 "git show --root --patch-with-stat --find-copies-harder -B -C %s 2>/dev/null"
110 #define TIG_LOG_CMD \
111 "git log --cc --stat -n100 %s 2>/dev/null"
113 #define TIG_MAIN_CMD \
114 "git log --topo-order --pretty=raw %s 2>/dev/null"
116 #define TIG_TREE_CMD \
119 #define TIG_BLOB_CMD \
120 "git cat-file blob %s"
122 /* XXX: Needs to be defined to the empty string. */
123 #define TIG_HELP_CMD ""
124 #define TIG_PAGER_CMD ""
125 #define TIG_STATUS_CMD ""
126 #define TIG_STAGE_CMD ""
128 /* Some ascii-shorthands fitted into the ncurses namespace. */
130 #define KEY_RETURN '\r'
135 char *name; /* Ref name; tag or head names are shortened. */
136 char id[SIZEOF_REV]; /* Commit SHA1 ID */
137 unsigned int tag:1; /* Is it a tag? */
138 unsigned int remote:1; /* Is it a remote ref? */
139 unsigned int next:1; /* For ref lists: are there more refs? */
142 static struct ref **get_refs(char *id);
151 set_from_int_map(struct int_map *map, size_t map_size,
152 int *value, const char *name, int namelen)
157 for (i = 0; i < map_size; i++)
158 if (namelen == map[i].namelen &&
159 !strncasecmp(name, map[i].name, namelen)) {
160 *value = map[i].value;
173 string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
175 if (srclen > dstlen - 1)
178 strncpy(dst, src, srclen);
182 /* Shorthands for safely copying into a fixed buffer. */
184 #define string_copy(dst, src) \
185 string_ncopy_do(dst, sizeof(dst), src, sizeof(src))
187 #define string_ncopy(dst, src, srclen) \
188 string_ncopy_do(dst, sizeof(dst), src, srclen)
190 #define string_copy_rev(dst, src) \
191 string_ncopy_do(dst, SIZEOF_REV, src, SIZEOF_REV - 1)
193 #define string_add(dst, from, src) \
194 string_ncopy_do(dst + (from), sizeof(dst) - (from), src, sizeof(src))
197 chomp_string(char *name)
201 while (isspace(*name))
204 namelen = strlen(name) - 1;
205 while (namelen > 0 && isspace(name[namelen]))
212 string_nformat(char *buf, size_t bufsize, size_t *bufpos, const char *fmt, ...)
215 size_t pos = bufpos ? *bufpos : 0;
218 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
224 return pos >= bufsize ? FALSE : TRUE;
227 #define string_format(buf, fmt, args...) \
228 string_nformat(buf, sizeof(buf), NULL, fmt, args)
230 #define string_format_from(buf, from, fmt, args...) \
231 string_nformat(buf, sizeof(buf), from, fmt, args)
234 string_enum_compare(const char *str1, const char *str2, int len)
238 #define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
240 /* Diff-Header == DIFF_HEADER */
241 for (i = 0; i < len; i++) {
242 if (toupper(str1[i]) == toupper(str2[i]))
245 if (string_enum_sep(str1[i]) &&
246 string_enum_sep(str2[i]))
249 return str1[i] - str2[i];
257 * NOTE: The following is a slightly modified copy of the git project's shell
258 * quoting routines found in the quote.c file.
260 * Help to copy the thing properly quoted for the shell safety. any single
261 * quote is replaced with '\'', any exclamation point is replaced with '\!',
262 * and the whole thing is enclosed in a
265 * original sq_quote result
266 * name ==> name ==> 'name'
267 * a b ==> a b ==> 'a b'
268 * a'b ==> a'\''b ==> 'a'\''b'
269 * a!b ==> a'\!'b ==> 'a'\!'b'
273 sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
277 #define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0)
280 while ((c = *src++)) {
281 if (c == '\'' || c == '!') {
292 if (bufsize < SIZEOF_STR)
304 /* XXX: Keep the view request first and in sync with views[]. */ \
305 REQ_GROUP("View switching") \
306 REQ_(VIEW_MAIN, "Show main view"), \
307 REQ_(VIEW_DIFF, "Show diff view"), \
308 REQ_(VIEW_LOG, "Show log view"), \
309 REQ_(VIEW_TREE, "Show tree view"), \
310 REQ_(VIEW_BLOB, "Show blob view"), \
311 REQ_(VIEW_HELP, "Show help page"), \
312 REQ_(VIEW_PAGER, "Show pager view"), \
313 REQ_(VIEW_STATUS, "Show status view"), \
314 REQ_(VIEW_STAGE, "Show stage view"), \
316 REQ_GROUP("View manipulation") \
317 REQ_(ENTER, "Enter current line and scroll"), \
318 REQ_(NEXT, "Move to next"), \
319 REQ_(PREVIOUS, "Move to previous"), \
320 REQ_(VIEW_NEXT, "Move focus to next view"), \
321 REQ_(REFRESH, "Reload and refresh"), \
322 REQ_(VIEW_CLOSE, "Close the current view"), \
323 REQ_(QUIT, "Close all views and quit"), \
325 REQ_GROUP("Cursor navigation") \
326 REQ_(MOVE_UP, "Move cursor one line up"), \
327 REQ_(MOVE_DOWN, "Move cursor one line down"), \
328 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
329 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
330 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
331 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
333 REQ_GROUP("Scrolling") \
334 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
335 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
336 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
337 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
339 REQ_GROUP("Searching") \
340 REQ_(SEARCH, "Search the view"), \
341 REQ_(SEARCH_BACK, "Search backwards in the view"), \
342 REQ_(FIND_NEXT, "Find next search match"), \
343 REQ_(FIND_PREV, "Find previous search match"), \
346 REQ_(PROMPT, "Bring up the prompt"), \
347 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
348 REQ_(SCREEN_RESIZE, "Resize the screen"), \
349 REQ_(SHOW_VERSION, "Show version information"), \
350 REQ_(STOP_LOADING, "Stop all loading views"), \
351 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
352 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"), \
353 REQ_(STATUS_UPDATE, "Update file status"), \
354 REQ_(STATUS_MERGE, "Merge file using external tool"), \
355 REQ_(EDIT, "Open in editor"), \
356 REQ_(NONE, "Do nothing")
359 /* User action requests. */
361 #define REQ_GROUP(help)
362 #define REQ_(req, help) REQ_##req
364 /* Offset all requests to avoid conflicts with ncurses getch values. */
365 REQ_OFFSET = KEY_MAX + 1,
372 struct request_info {
373 enum request request;
379 static struct request_info req_info[] = {
380 #define REQ_GROUP(help) { 0, NULL, 0, (help) },
381 #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
388 get_request(const char *name)
390 int namelen = strlen(name);
393 for (i = 0; i < ARRAY_SIZE(req_info); i++)
394 if (req_info[i].namelen == namelen &&
395 !string_enum_compare(req_info[i].name, name, namelen))
396 return req_info[i].request;
406 static const char usage[] =
407 "tig " TIG_VERSION " (" __DATE__ ")\n"
409 "Usage: tig [options]\n"
410 " or: tig [options] [--] [git log options]\n"
411 " or: tig [options] log [git log options]\n"
412 " or: tig [options] diff [git diff options]\n"
413 " or: tig [options] show [git show options]\n"
414 " or: tig [options] < [git command output]\n"
417 " -l Start up in log view\n"
418 " -d Start up in diff view\n"
419 " -S Start up in status view\n"
420 " -n[I], --line-number[=I] Show line numbers with given interval\n"
421 " -b[N], --tab-size[=N] Set number of spaces for tab expansion\n"
422 " -- Mark end of tig options\n"
423 " -v, --version Show version and exit\n"
424 " -h, --help Show help message and exit\n";
426 /* Option and state variables. */
427 static bool opt_line_number = FALSE;
428 static bool opt_rev_graph = FALSE;
429 static int opt_num_interval = NUMBER_INTERVAL;
430 static int opt_tab_size = TABSIZE;
431 static enum request opt_request = REQ_VIEW_MAIN;
432 static char opt_cmd[SIZEOF_STR] = "";
433 static char opt_path[SIZEOF_STR] = "";
434 static FILE *opt_pipe = NULL;
435 static char opt_encoding[20] = "UTF-8";
436 static bool opt_utf8 = TRUE;
437 static char opt_codeset[20] = "UTF-8";
438 static iconv_t opt_iconv = ICONV_NONE;
439 static char opt_search[SIZEOF_STR] = "";
440 static char opt_cdup[SIZEOF_STR] = "";
441 static char opt_git_dir[SIZEOF_STR] = "";
442 static char opt_is_inside_work_tree = -1; /* set to TRUE or FALSE */
443 static char opt_editor[SIZEOF_STR] = "";
451 check_option(char *opt, char short_name, char *name, enum option_type type, ...)
461 int namelen = strlen(name);
465 if (strncmp(opt, name, namelen))
468 if (opt[namelen] == '=')
469 value = opt + namelen + 1;
472 if (!short_name || opt[1] != short_name)
477 va_start(args, type);
478 if (type == OPT_INT) {
479 number = va_arg(args, int *);
481 *number = atoi(value);
488 /* Returns the index of log or diff command or -1 to exit. */
490 parse_options(int argc, char *argv[])
494 for (i = 1; i < argc; i++) {
497 if (!strcmp(opt, "log") ||
498 !strcmp(opt, "diff") ||
499 !strcmp(opt, "show")) {
500 opt_request = opt[0] == 'l'
501 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
505 if (opt[0] && opt[0] != '-')
508 if (!strcmp(opt, "-l")) {
509 opt_request = REQ_VIEW_LOG;
513 if (!strcmp(opt, "-d")) {
514 opt_request = REQ_VIEW_DIFF;
518 if (!strcmp(opt, "-S")) {
519 opt_request = REQ_VIEW_STATUS;
523 if (check_option(opt, 'n', "line-number", OPT_INT, &opt_num_interval)) {
524 opt_line_number = TRUE;
528 if (check_option(opt, 'b', "tab-size", OPT_INT, &opt_tab_size)) {
529 opt_tab_size = MIN(opt_tab_size, TABSIZE);
533 if (check_option(opt, 'v', "version", OPT_NONE)) {
534 printf("tig version %s\n", TIG_VERSION);
538 if (check_option(opt, 'h', "help", OPT_NONE)) {
543 if (!strcmp(opt, "--")) {
548 die("unknown option '%s'\n\n%s", opt, usage);
551 if (!isatty(STDIN_FILENO)) {
552 opt_request = REQ_VIEW_PAGER;
555 } else if (i < argc) {
558 if (opt_request == REQ_VIEW_MAIN)
559 /* XXX: This is vulnerable to the user overriding
560 * options required for the main view parser. */
561 string_copy(opt_cmd, "git log --pretty=raw");
563 string_copy(opt_cmd, "git");
564 buf_size = strlen(opt_cmd);
566 while (buf_size < sizeof(opt_cmd) && i < argc) {
567 opt_cmd[buf_size++] = ' ';
568 buf_size = sq_quote(opt_cmd, buf_size, argv[i++]);
571 if (buf_size >= sizeof(opt_cmd))
572 die("command too long");
574 opt_cmd[buf_size] = 0;
577 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
585 * Line-oriented content detection.
589 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
590 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
591 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
592 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
593 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
594 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
595 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
596 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
597 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
598 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
599 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
600 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
601 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
602 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
603 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
604 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
605 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
606 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
607 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
608 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
609 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
610 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
611 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
612 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
613 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
614 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
615 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
616 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
617 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
618 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
619 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
620 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
621 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
622 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
623 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
624 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
625 LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
626 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
627 LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
628 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
629 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
630 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
631 LINE(STAT_SECTION, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
632 LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
633 LINE(STAT_STAGED, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
634 LINE(STAT_UNSTAGED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
635 LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0)
638 #define LINE(type, line, fg, bg, attr) \
645 const char *name; /* Option name. */
646 int namelen; /* Size of option name. */
647 const char *line; /* The start of line to match. */
648 int linelen; /* Size of string to match. */
649 int fg, bg, attr; /* Color and text attributes for the lines. */
652 static struct line_info line_info[] = {
653 #define LINE(type, line, fg, bg, attr) \
654 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
659 static enum line_type
660 get_line_type(char *line)
662 int linelen = strlen(line);
665 for (type = 0; type < ARRAY_SIZE(line_info); type++)
666 /* Case insensitive search matches Signed-off-by lines better. */
667 if (linelen >= line_info[type].linelen &&
668 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
675 get_line_attr(enum line_type type)
677 assert(type < ARRAY_SIZE(line_info));
678 return COLOR_PAIR(type) | line_info[type].attr;
681 static struct line_info *
682 get_line_info(char *name, int namelen)
686 for (type = 0; type < ARRAY_SIZE(line_info); type++)
687 if (namelen == line_info[type].namelen &&
688 !string_enum_compare(line_info[type].name, name, namelen))
689 return &line_info[type];
697 int default_bg = COLOR_BLACK;
698 int default_fg = COLOR_WHITE;
703 if (use_default_colors() != ERR) {
708 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
709 struct line_info *info = &line_info[type];
710 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
711 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
713 init_pair(type, fg, bg);
721 unsigned int selected:1;
723 void *data; /* User data */
733 enum request request;
734 struct keybinding *next;
737 static struct keybinding default_keybindings[] = {
739 { 'm', REQ_VIEW_MAIN },
740 { 'd', REQ_VIEW_DIFF },
741 { 'l', REQ_VIEW_LOG },
742 { 't', REQ_VIEW_TREE },
743 { 'f', REQ_VIEW_BLOB },
744 { 'p', REQ_VIEW_PAGER },
745 { 'h', REQ_VIEW_HELP },
746 { 'S', REQ_VIEW_STATUS },
747 { 'c', REQ_VIEW_STAGE },
749 /* View manipulation */
750 { 'q', REQ_VIEW_CLOSE },
751 { KEY_TAB, REQ_VIEW_NEXT },
752 { KEY_RETURN, REQ_ENTER },
753 { KEY_UP, REQ_PREVIOUS },
754 { KEY_DOWN, REQ_NEXT },
755 { 'R', REQ_REFRESH },
757 /* Cursor navigation */
758 { 'k', REQ_MOVE_UP },
759 { 'j', REQ_MOVE_DOWN },
760 { KEY_HOME, REQ_MOVE_FIRST_LINE },
761 { KEY_END, REQ_MOVE_LAST_LINE },
762 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
763 { ' ', REQ_MOVE_PAGE_DOWN },
764 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
765 { 'b', REQ_MOVE_PAGE_UP },
766 { '-', REQ_MOVE_PAGE_UP },
769 { KEY_IC, REQ_SCROLL_LINE_UP },
770 { KEY_DC, REQ_SCROLL_LINE_DOWN },
771 { 'w', REQ_SCROLL_PAGE_UP },
772 { 's', REQ_SCROLL_PAGE_DOWN },
776 { '?', REQ_SEARCH_BACK },
777 { 'n', REQ_FIND_NEXT },
778 { 'N', REQ_FIND_PREV },
782 { 'z', REQ_STOP_LOADING },
783 { 'v', REQ_SHOW_VERSION },
784 { 'r', REQ_SCREEN_REDRAW },
785 { '.', REQ_TOGGLE_LINENO },
786 { 'g', REQ_TOGGLE_REV_GRAPH },
788 { 'u', REQ_STATUS_UPDATE },
789 { 'M', REQ_STATUS_MERGE },
792 /* Using the ncurses SIGWINCH handler. */
793 { KEY_RESIZE, REQ_SCREEN_RESIZE },
796 #define KEYMAP_INFO \
809 #define KEYMAP_(name) KEYMAP_##name
814 static struct int_map keymap_table[] = {
815 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
820 #define set_keymap(map, name) \
821 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
823 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
826 add_keybinding(enum keymap keymap, enum request request, int key)
828 struct keybinding *keybinding;
830 keybinding = calloc(1, sizeof(*keybinding));
832 die("Failed to allocate keybinding");
834 keybinding->alias = key;
835 keybinding->request = request;
836 keybinding->next = keybindings[keymap];
837 keybindings[keymap] = keybinding;
840 /* Looks for a key binding first in the given map, then in the generic map, and
841 * lastly in the default keybindings. */
843 get_keybinding(enum keymap keymap, int key)
845 struct keybinding *kbd;
848 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
849 if (kbd->alias == key)
852 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
853 if (kbd->alias == key)
856 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
857 if (default_keybindings[i].alias == key)
858 return default_keybindings[i].request;
860 return (enum request) key;
869 static struct key key_table[] = {
870 { "Enter", KEY_RETURN },
872 { "Backspace", KEY_BACKSPACE },
874 { "Escape", KEY_ESC },
875 { "Left", KEY_LEFT },
876 { "Right", KEY_RIGHT },
878 { "Down", KEY_DOWN },
879 { "Insert", KEY_IC },
880 { "Delete", KEY_DC },
882 { "Home", KEY_HOME },
884 { "PageUp", KEY_PPAGE },
885 { "PageDown", KEY_NPAGE },
895 { "F10", KEY_F(10) },
896 { "F11", KEY_F(11) },
897 { "F12", KEY_F(12) },
901 get_key_value(const char *name)
905 for (i = 0; i < ARRAY_SIZE(key_table); i++)
906 if (!strcasecmp(key_table[i].name, name))
907 return key_table[i].value;
909 if (strlen(name) == 1 && isprint(*name))
916 get_key_name(int key_value)
918 static char key_char[] = "'X'";
922 for (key = 0; key < ARRAY_SIZE(key_table); key++)
923 if (key_table[key].value == key_value)
924 seq = key_table[key].name;
928 isprint(key_value)) {
929 key_char[1] = (char) key_value;
933 return seq ? seq : "'?'";
937 get_key(enum request request)
939 static char buf[BUFSIZ];
940 static char key_char[] = "'X'";
947 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
948 struct keybinding *keybinding = &default_keybindings[i];
952 if (keybinding->request != request)
955 for (key = 0; key < ARRAY_SIZE(key_table); key++)
956 if (key_table[key].value == keybinding->alias)
957 seq = key_table[key].name;
960 keybinding->alias < 127 &&
961 isprint(keybinding->alias)) {
962 key_char[1] = (char) keybinding->alias;
969 if (!string_format_from(buf, &pos, "%s%s", sep, seq))
970 return "Too many keybindings!";
980 char cmd[SIZEOF_STR];
983 static struct run_request *run_request;
984 static size_t run_requests;
987 add_run_request(enum keymap keymap, int key, int argc, char **argv)
989 struct run_request *tmp;
990 struct run_request req = { keymap, key };
993 for (bufpos = 0; argc > 0; argc--, argv++)
994 if (!string_format_from(req.cmd, &bufpos, "%s ", *argv))
997 req.cmd[bufpos - 1] = 0;
999 tmp = realloc(run_request, (run_requests + 1) * sizeof(*run_request));
1004 run_request[run_requests++] = req;
1006 return REQ_NONE + run_requests;
1009 static struct run_request *
1010 get_run_request(enum request request)
1012 if (request <= REQ_NONE)
1014 return &run_request[request - REQ_NONE - 1];
1018 add_builtin_run_requests(void)
1025 { KEYMAP_MAIN, 'C', { "git cherry-pick %(commit)" } },
1026 { KEYMAP_GENERIC, 'G', { "git gc" } },
1030 for (i = 0; i < ARRAY_SIZE(reqs); i++) {
1033 req = add_run_request(reqs[i].keymap, reqs[i].key, 1, reqs[i].argv);
1034 if (req != REQ_NONE)
1035 add_keybinding(reqs[i].keymap, req, reqs[i].key);
1040 * User config file handling.
1043 static struct int_map color_map[] = {
1044 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
1056 #define set_color(color, name) \
1057 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
1059 static struct int_map attr_map[] = {
1060 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
1067 ATTR_MAP(UNDERLINE),
1070 #define set_attribute(attr, name) \
1071 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
1073 static int config_lineno;
1074 static bool config_errors;
1075 static char *config_msg;
1077 /* Wants: object fgcolor bgcolor [attr] */
1079 option_color_command(int argc, char *argv[])
1081 struct line_info *info;
1083 if (argc != 3 && argc != 4) {
1084 config_msg = "Wrong number of arguments given to color command";
1088 info = get_line_info(argv[0], strlen(argv[0]));
1090 config_msg = "Unknown color name";
1094 if (set_color(&info->fg, argv[1]) == ERR ||
1095 set_color(&info->bg, argv[2]) == ERR) {
1096 config_msg = "Unknown color";
1100 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
1101 config_msg = "Unknown attribute";
1108 /* Wants: name = value */
1110 option_set_command(int argc, char *argv[])
1113 config_msg = "Wrong number of arguments given to set command";
1117 if (strcmp(argv[1], "=")) {
1118 config_msg = "No value assigned";
1122 if (!strcmp(argv[0], "show-rev-graph")) {
1123 opt_rev_graph = (!strcmp(argv[2], "1") ||
1124 !strcmp(argv[2], "true") ||
1125 !strcmp(argv[2], "yes"));
1129 if (!strcmp(argv[0], "line-number-interval")) {
1130 opt_num_interval = atoi(argv[2]);
1134 if (!strcmp(argv[0], "tab-size")) {
1135 opt_tab_size = atoi(argv[2]);
1139 if (!strcmp(argv[0], "commit-encoding")) {
1140 char *arg = argv[2];
1141 int delimiter = *arg;
1144 switch (delimiter) {
1147 for (arg++, i = 0; arg[i]; i++)
1148 if (arg[i] == delimiter) {
1153 string_ncopy(opt_encoding, arg, strlen(arg));
1158 config_msg = "Unknown variable name";
1162 /* Wants: mode request key */
1164 option_bind_command(int argc, char *argv[])
1166 enum request request;
1171 config_msg = "Wrong number of arguments given to bind command";
1175 if (set_keymap(&keymap, argv[0]) == ERR) {
1176 config_msg = "Unknown key map";
1180 key = get_key_value(argv[1]);
1182 config_msg = "Unknown key";
1186 request = get_request(argv[2]);
1187 if (request = REQ_NONE) {
1188 const char *obsolete[] = { "cherry-pick" };
1189 size_t namelen = strlen(argv[2]);
1192 for (i = 0; i < ARRAY_SIZE(obsolete); i++) {
1193 if (namelen == strlen(obsolete[i]) &&
1194 !string_enum_compare(obsolete[i], argv[2], namelen)) {
1195 config_msg = "Obsolete request name";
1200 if (request == REQ_NONE && *argv[2]++ == '!')
1201 request = add_run_request(keymap, key, argc - 2, argv + 2);
1202 if (request == REQ_NONE) {
1203 config_msg = "Unknown request name";
1207 add_keybinding(keymap, request, key);
1213 set_option(char *opt, char *value)
1220 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1221 argv[argc++] = value;
1228 while (isspace(*value))
1232 if (!strcmp(opt, "color"))
1233 return option_color_command(argc, argv);
1235 if (!strcmp(opt, "set"))
1236 return option_set_command(argc, argv);
1238 if (!strcmp(opt, "bind"))
1239 return option_bind_command(argc, argv);
1241 config_msg = "Unknown option command";
1246 read_option(char *opt, size_t optlen, char *value, size_t valuelen)
1251 config_msg = "Internal error";
1253 /* Check for comment markers, since read_properties() will
1254 * only ensure opt and value are split at first " \t". */
1255 optlen = strcspn(opt, "#");
1259 if (opt[optlen] != 0) {
1260 config_msg = "No option value";
1264 /* Look for comment endings in the value. */
1265 size_t len = strcspn(value, "#");
1267 if (len < valuelen) {
1269 value[valuelen] = 0;
1272 status = set_option(opt, value);
1275 if (status == ERR) {
1276 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1277 config_lineno, (int) optlen, opt, config_msg);
1278 config_errors = TRUE;
1281 /* Always keep going if errors are encountered. */
1288 char *home = getenv("HOME");
1289 char buf[SIZEOF_STR];
1293 config_errors = FALSE;
1295 add_builtin_run_requests();
1297 if (!home || !string_format(buf, "%s/.tigrc", home))
1300 /* It's ok that the file doesn't exist. */
1301 file = fopen(buf, "r");
1305 if (read_properties(file, " \t", read_option) == ERR ||
1306 config_errors == TRUE)
1307 fprintf(stderr, "Errors while loading %s.\n", buf);
1320 /* The display array of active views and the index of the current view. */
1321 static struct view *display[2];
1322 static unsigned int current_view;
1324 /* Reading from the prompt? */
1325 static bool input_mode = FALSE;
1327 #define foreach_displayed_view(view, i) \
1328 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1330 #define displayed_views() (display[1] != NULL ? 2 : 1)
1332 /* Current head and commit ID */
1333 static char ref_blob[SIZEOF_REF] = "";
1334 static char ref_commit[SIZEOF_REF] = "HEAD";
1335 static char ref_head[SIZEOF_REF] = "HEAD";
1338 const char *name; /* View name */
1339 const char *cmd_fmt; /* Default command line format */
1340 const char *cmd_env; /* Command line set via environment */
1341 const char *id; /* Points to either of ref_{head,commit,blob} */
1343 struct view_ops *ops; /* View operations */
1345 enum keymap keymap; /* What keymap does this view have */
1347 char cmd[SIZEOF_STR]; /* Command buffer */
1348 char ref[SIZEOF_REF]; /* Hovered commit reference */
1349 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1351 int height, width; /* The width and height of the main window */
1352 WINDOW *win; /* The main window */
1353 WINDOW *title; /* The title window living below the main window */
1356 unsigned long offset; /* Offset of the window top */
1357 unsigned long lineno; /* Current line number */
1360 char grep[SIZEOF_STR]; /* Search string */
1361 regex_t *regex; /* Pre-compiled regex */
1363 /* If non-NULL, points to the view that opened this view. If this view
1364 * is closed tig will switch back to the parent view. */
1365 struct view *parent;
1368 unsigned long lines; /* Total number of lines */
1369 struct line *line; /* Line index */
1370 unsigned long line_size;/* Total number of allocated lines */
1371 unsigned int digits; /* Number of digits in the lines member. */
1379 /* What type of content being displayed. Used in the title bar. */
1381 /* Open and reads in all view content. */
1382 bool (*open)(struct view *view);
1383 /* Read one line; updates view->line. */
1384 bool (*read)(struct view *view, char *data);
1385 /* Draw one line; @lineno must be < view->height. */
1386 bool (*draw)(struct view *view, struct line *line, unsigned int lineno, bool selected);
1387 /* Depending on view handle a special requests. */
1388 enum request (*request)(struct view *view, enum request request, struct line *line);
1389 /* Search for regex in a line. */
1390 bool (*grep)(struct view *view, struct line *line);
1392 void (*select)(struct view *view, struct line *line);
1395 static struct view_ops pager_ops;
1396 static struct view_ops main_ops;
1397 static struct view_ops tree_ops;
1398 static struct view_ops blob_ops;
1399 static struct view_ops help_ops;
1400 static struct view_ops status_ops;
1401 static struct view_ops stage_ops;
1403 #define VIEW_STR(name, cmd, env, ref, ops, map) \
1404 { name, cmd, #env, ref, ops, map}
1406 #define VIEW_(id, name, ops, ref) \
1407 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id)
1410 static struct view views[] = {
1411 VIEW_(MAIN, "main", &main_ops, ref_head),
1412 VIEW_(DIFF, "diff", &pager_ops, ref_commit),
1413 VIEW_(LOG, "log", &pager_ops, ref_head),
1414 VIEW_(TREE, "tree", &tree_ops, ref_commit),
1415 VIEW_(BLOB, "blob", &blob_ops, ref_blob),
1416 VIEW_(HELP, "help", &help_ops, ""),
1417 VIEW_(PAGER, "pager", &pager_ops, "stdin"),
1418 VIEW_(STATUS, "status", &status_ops, ""),
1419 VIEW_(STAGE, "stage", &stage_ops, ""),
1422 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1424 #define foreach_view(view, i) \
1425 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1427 #define view_is_displayed(view) \
1428 (view == display[0] || view == display[1])
1431 draw_view_line(struct view *view, unsigned int lineno)
1434 bool selected = (view->offset + lineno == view->lineno);
1437 assert(view_is_displayed(view));
1439 if (view->offset + lineno >= view->lines)
1442 line = &view->line[view->offset + lineno];
1445 line->selected = TRUE;
1446 view->ops->select(view, line);
1447 } else if (line->selected) {
1448 line->selected = FALSE;
1449 wmove(view->win, lineno, 0);
1450 wclrtoeol(view->win);
1453 scrollok(view->win, FALSE);
1454 draw_ok = view->ops->draw(view, line, lineno, selected);
1455 scrollok(view->win, TRUE);
1461 redraw_view_from(struct view *view, int lineno)
1463 assert(0 <= lineno && lineno < view->height);
1465 for (; lineno < view->height; lineno++) {
1466 if (!draw_view_line(view, lineno))
1470 redrawwin(view->win);
1472 wnoutrefresh(view->win);
1474 wrefresh(view->win);
1478 redraw_view(struct view *view)
1481 redraw_view_from(view, 0);
1486 update_view_title(struct view *view)
1488 char buf[SIZEOF_STR];
1489 char state[SIZEOF_STR];
1490 size_t bufpos = 0, statelen = 0;
1492 assert(view_is_displayed(view));
1494 if (view != VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) {
1495 unsigned int view_lines = view->offset + view->height;
1496 unsigned int lines = view->lines
1497 ? MIN(view_lines, view->lines) * 100 / view->lines
1500 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1507 time_t secs = time(NULL) - view->start_time;
1509 /* Three git seconds are a long time ... */
1511 string_format_from(state, &statelen, " %lds", secs);
1515 string_format_from(buf, &bufpos, "[%s]", view->name);
1516 if (*view->ref && bufpos < view->width) {
1517 size_t refsize = strlen(view->ref);
1518 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1520 if (minsize < view->width)
1521 refsize = view->width - minsize + 7;
1522 string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
1525 if (statelen && bufpos < view->width) {
1526 string_format_from(buf, &bufpos, " %s", state);
1529 if (view == display[current_view])
1530 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1532 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1534 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1535 wclrtoeol(view->title);
1536 wmove(view->title, 0, view->width - 1);
1539 wnoutrefresh(view->title);
1541 wrefresh(view->title);
1545 resize_display(void)
1548 struct view *base = display[0];
1549 struct view *view = display[1] ? display[1] : display[0];
1551 /* Setup window dimensions */
1553 getmaxyx(stdscr, base->height, base->width);
1555 /* Make room for the status window. */
1559 /* Horizontal split. */
1560 view->width = base->width;
1561 view->height = SCALE_SPLIT_VIEW(base->height);
1562 base->height -= view->height;
1564 /* Make room for the title bar. */
1568 /* Make room for the title bar. */
1573 foreach_displayed_view (view, i) {
1575 view->win = newwin(view->height, 0, offset, 0);
1577 die("Failed to create %s view", view->name);
1579 scrollok(view->win, TRUE);
1581 view->title = newwin(1, 0, offset + view->height, 0);
1583 die("Failed to create title window");
1586 wresize(view->win, view->height, view->width);
1587 mvwin(view->win, offset, 0);
1588 mvwin(view->title, offset + view->height, 0);
1591 offset += view->height + 1;
1596 redraw_display(void)
1601 foreach_displayed_view (view, i) {
1603 update_view_title(view);
1608 update_display_cursor(struct view *view)
1610 /* Move the cursor to the right-most column of the cursor line.
1612 * XXX: This could turn out to be a bit expensive, but it ensures that
1613 * the cursor does not jump around. */
1615 wmove(view->win, view->lineno - view->offset, view->width - 1);
1616 wrefresh(view->win);
1624 /* Scrolling backend */
1626 do_scroll_view(struct view *view, int lines)
1628 bool redraw_current_line = FALSE;
1630 /* The rendering expects the new offset. */
1631 view->offset += lines;
1633 assert(0 <= view->offset && view->offset < view->lines);
1636 /* Move current line into the view. */
1637 if (view->lineno < view->offset) {
1638 view->lineno = view->offset;
1639 redraw_current_line = TRUE;
1640 } else if (view->lineno >= view->offset + view->height) {
1641 view->lineno = view->offset + view->height - 1;
1642 redraw_current_line = TRUE;
1645 assert(view->offset <= view->lineno && view->lineno < view->lines);
1647 /* Redraw the whole screen if scrolling is pointless. */
1648 if (view->height < ABS(lines)) {
1652 int line = lines > 0 ? view->height - lines : 0;
1653 int end = line + ABS(lines);
1655 wscrl(view->win, lines);
1657 for (; line < end; line++) {
1658 if (!draw_view_line(view, line))
1662 if (redraw_current_line)
1663 draw_view_line(view, view->lineno - view->offset);
1666 redrawwin(view->win);
1667 wrefresh(view->win);
1671 /* Scroll frontend */
1673 scroll_view(struct view *view, enum request request)
1677 assert(view_is_displayed(view));
1680 case REQ_SCROLL_PAGE_DOWN:
1681 lines = view->height;
1682 case REQ_SCROLL_LINE_DOWN:
1683 if (view->offset + lines > view->lines)
1684 lines = view->lines - view->offset;
1686 if (lines == 0 || view->offset + view->height >= view->lines) {
1687 report("Cannot scroll beyond the last line");
1692 case REQ_SCROLL_PAGE_UP:
1693 lines = view->height;
1694 case REQ_SCROLL_LINE_UP:
1695 if (lines > view->offset)
1696 lines = view->offset;
1699 report("Cannot scroll beyond the first line");
1707 die("request %d not handled in switch", request);
1710 do_scroll_view(view, lines);
1715 move_view(struct view *view, enum request request)
1717 int scroll_steps = 0;
1721 case REQ_MOVE_FIRST_LINE:
1722 steps = -view->lineno;
1725 case REQ_MOVE_LAST_LINE:
1726 steps = view->lines - view->lineno - 1;
1729 case REQ_MOVE_PAGE_UP:
1730 steps = view->height > view->lineno
1731 ? -view->lineno : -view->height;
1734 case REQ_MOVE_PAGE_DOWN:
1735 steps = view->lineno + view->height >= view->lines
1736 ? view->lines - view->lineno - 1 : view->height;
1748 die("request %d not handled in switch", request);
1751 if (steps <= 0 && view->lineno == 0) {
1752 report("Cannot move beyond the first line");
1755 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1756 report("Cannot move beyond the last line");
1760 /* Move the current line */
1761 view->lineno += steps;
1762 assert(0 <= view->lineno && view->lineno < view->lines);
1764 /* Check whether the view needs to be scrolled */
1765 if (view->lineno < view->offset ||
1766 view->lineno >= view->offset + view->height) {
1767 scroll_steps = steps;
1768 if (steps < 0 && -steps > view->offset) {
1769 scroll_steps = -view->offset;
1771 } else if (steps > 0) {
1772 if (view->lineno == view->lines - 1 &&
1773 view->lines > view->height) {
1774 scroll_steps = view->lines - view->offset - 1;
1775 if (scroll_steps >= view->height)
1776 scroll_steps -= view->height - 1;
1781 if (!view_is_displayed(view)) {
1782 view->offset += scroll_steps;
1783 assert(0 <= view->offset && view->offset < view->lines);
1784 view->ops->select(view, &view->line[view->lineno]);
1788 /* Repaint the old "current" line if we be scrolling */
1789 if (ABS(steps) < view->height)
1790 draw_view_line(view, view->lineno - steps - view->offset);
1793 do_scroll_view(view, scroll_steps);
1797 /* Draw the current line */
1798 draw_view_line(view, view->lineno - view->offset);
1800 redrawwin(view->win);
1801 wrefresh(view->win);
1810 static void search_view(struct view *view, enum request request);
1813 find_next_line(struct view *view, unsigned long lineno, struct line *line)
1815 assert(view_is_displayed(view));
1817 if (!view->ops->grep(view, line))
1820 if (lineno - view->offset >= view->height) {
1821 view->offset = lineno;
1822 view->lineno = lineno;
1826 unsigned long old_lineno = view->lineno - view->offset;
1828 view->lineno = lineno;
1829 draw_view_line(view, old_lineno);
1831 draw_view_line(view, view->lineno - view->offset);
1832 redrawwin(view->win);
1833 wrefresh(view->win);
1836 report("Line %ld matches '%s'", lineno + 1, view->grep);
1841 find_next(struct view *view, enum request request)
1843 unsigned long lineno = view->lineno;
1848 report("No previous search");
1850 search_view(view, request);
1860 case REQ_SEARCH_BACK:
1869 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
1870 lineno += direction;
1872 /* Note, lineno is unsigned long so will wrap around in which case it
1873 * will become bigger than view->lines. */
1874 for (; lineno < view->lines; lineno += direction) {
1875 struct line *line = &view->line[lineno];
1877 if (find_next_line(view, lineno, line))
1881 report("No match found for '%s'", view->grep);
1885 search_view(struct view *view, enum request request)
1890 regfree(view->regex);
1893 view->regex = calloc(1, sizeof(*view->regex));
1898 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
1899 if (regex_err != 0) {
1900 char buf[SIZEOF_STR] = "unknown error";
1902 regerror(regex_err, view->regex, buf, sizeof(buf));
1903 report("Search failed: %s", buf);
1907 string_copy(view->grep, opt_search);
1909 find_next(view, request);
1913 * Incremental updating
1917 end_update(struct view *view)
1921 set_nonblocking_input(FALSE);
1922 if (view->pipe == stdin)
1930 begin_update(struct view *view)
1936 string_copy(view->cmd, opt_cmd);
1938 /* When running random commands, initially show the
1939 * command in the title. However, it maybe later be
1940 * overwritten if a commit line is selected. */
1941 if (view == VIEW(REQ_VIEW_PAGER))
1942 string_copy(view->ref, view->cmd);
1946 } else if (view == VIEW(REQ_VIEW_TREE)) {
1947 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1948 char path[SIZEOF_STR];
1950 if (strcmp(view->vid, view->id))
1951 opt_path[0] = path[0] = 0;
1952 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
1955 if (!string_format(view->cmd, format, view->id, path))
1959 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1960 const char *id = view->id;
1962 if (!string_format(view->cmd, format, id, id, id, id, id))
1965 /* Put the current ref_* value to the view title ref
1966 * member. This is needed by the blob view. Most other
1967 * views sets it automatically after loading because the
1968 * first line is a commit line. */
1969 string_copy_rev(view->ref, view->id);
1972 /* Special case for the pager view. */
1974 view->pipe = opt_pipe;
1977 view->pipe = popen(view->cmd, "r");
1983 set_nonblocking_input(TRUE);
1988 string_copy_rev(view->vid, view->id);
1993 for (i = 0; i < view->lines; i++)
1994 if (view->line[i].data)
1995 free(view->line[i].data);
2001 view->start_time = time(NULL);
2006 static struct line *
2007 realloc_lines(struct view *view, size_t line_size)
2009 struct line *tmp = realloc(view->line, sizeof(*view->line) * line_size);
2015 view->line_size = line_size;
2020 update_view(struct view *view)
2022 char in_buffer[BUFSIZ];
2023 char out_buffer[BUFSIZ * 2];
2025 /* The number of lines to read. If too low it will cause too much
2026 * redrawing (and possible flickering), if too high responsiveness
2028 unsigned long lines = view->height;
2029 int redraw_from = -1;
2034 /* Only redraw if lines are visible. */
2035 if (view->offset + view->height >= view->lines)
2036 redraw_from = view->lines - view->offset;
2038 /* FIXME: This is probably not perfect for backgrounded views. */
2039 if (!realloc_lines(view, view->lines + lines))
2042 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
2043 size_t linelen = strlen(line);
2046 line[linelen - 1] = 0;
2048 if (opt_iconv != ICONV_NONE) {
2049 ICONV_CONST char *inbuf = line;
2050 size_t inlen = linelen;
2052 char *outbuf = out_buffer;
2053 size_t outlen = sizeof(out_buffer);
2057 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
2058 if (ret != (size_t) -1) {
2060 linelen = strlen(out_buffer);
2064 if (!view->ops->read(view, line))
2074 lines = view->lines;
2075 for (digits = 0; lines; digits++)
2078 /* Keep the displayed view in sync with line number scaling. */
2079 if (digits != view->digits) {
2080 view->digits = digits;
2085 if (!view_is_displayed(view))
2088 if (view == VIEW(REQ_VIEW_TREE)) {
2089 /* Clear the view and redraw everything since the tree sorting
2090 * might have rearranged things. */
2093 } else if (redraw_from >= 0) {
2094 /* If this is an incremental update, redraw the previous line
2095 * since for commits some members could have changed when
2096 * loading the main view. */
2097 if (redraw_from > 0)
2100 /* Since revision graph visualization requires knowledge
2101 * about the parent commit, it causes a further one-off
2102 * needed to be redrawn for incremental updates. */
2103 if (redraw_from > 0 && opt_rev_graph)
2106 /* Incrementally draw avoids flickering. */
2107 redraw_view_from(view, redraw_from);
2110 /* Update the title _after_ the redraw so that if the redraw picks up a
2111 * commit reference in view->ref it'll be available here. */
2112 update_view_title(view);
2115 if (ferror(view->pipe)) {
2116 report("Failed to read: %s", strerror(errno));
2119 } else if (feof(view->pipe)) {
2127 report("Allocation failure");
2130 view->ops->read(view, NULL);
2135 static struct line *
2136 add_line_data(struct view *view, void *data, enum line_type type)
2138 struct line *line = &view->line[view->lines++];
2140 memset(line, 0, sizeof(*line));
2147 static struct line *
2148 add_line_text(struct view *view, char *data, enum line_type type)
2151 data = strdup(data);
2153 return data ? add_line_data(view, data, type) : NULL;
2162 OPEN_DEFAULT = 0, /* Use default view switching. */
2163 OPEN_SPLIT = 1, /* Split current view. */
2164 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2165 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2169 open_view(struct view *prev, enum request request, enum open_flags flags)
2171 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2172 bool split = !!(flags & OPEN_SPLIT);
2173 bool reload = !!(flags & OPEN_RELOAD);
2174 struct view *view = VIEW(request);
2175 int nviews = displayed_views();
2176 struct view *base_view = display[0];
2178 if (view == prev && nviews == 1 && !reload) {
2179 report("Already in %s view", view->name);
2183 if (view->ops->open) {
2184 if (!view->ops->open(view)) {
2185 report("Failed to load %s view", view->name);
2189 } else if ((reload || strcmp(view->vid, view->id)) &&
2190 !begin_update(view)) {
2191 report("Failed to load %s view", view->name);
2200 /* Maximize the current view. */
2201 memset(display, 0, sizeof(display));
2203 display[current_view] = view;
2206 /* Resize the view when switching between split- and full-screen,
2207 * or when switching between two different full-screen views. */
2208 if (nviews != displayed_views() ||
2209 (nviews == 1 && base_view != display[0]))
2212 if (split && prev->lineno - prev->offset >= prev->height) {
2213 /* Take the title line into account. */
2214 int lines = prev->lineno - prev->offset - prev->height + 1;
2216 /* Scroll the view that was split if the current line is
2217 * outside the new limited view. */
2218 do_scroll_view(prev, lines);
2221 if (prev && view != prev) {
2222 if (split && !backgrounded) {
2223 /* "Blur" the previous view. */
2224 update_view_title(prev);
2227 view->parent = prev;
2230 if (view->pipe && view->lines == 0) {
2231 /* Clear the old view and let the incremental updating refill
2240 /* If the view is backgrounded the above calls to report()
2241 * won't redraw the view title. */
2243 update_view_title(view);
2247 open_external_viewer(const char *cmd)
2249 def_prog_mode(); /* save current tty modes */
2250 endwin(); /* restore original tty modes */
2252 fprintf(stderr, "Press Enter to continue");
2259 open_mergetool(const char *file)
2261 char cmd[SIZEOF_STR];
2262 char file_sq[SIZEOF_STR];
2264 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2265 string_format(cmd, "git mergetool %s", file_sq)) {
2266 open_external_viewer(cmd);
2271 open_editor(bool from_root, const char *file)
2273 char cmd[SIZEOF_STR];
2274 char file_sq[SIZEOF_STR];
2276 char *prefix = from_root ? opt_cdup : "";
2278 editor = getenv("GIT_EDITOR");
2279 if (!editor && *opt_editor)
2280 editor = opt_editor;
2282 editor = getenv("VISUAL");
2284 editor = getenv("EDITOR");
2288 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2289 string_format(cmd, "%s %s%s", editor, prefix, file_sq)) {
2290 open_external_viewer(cmd);
2295 open_run_request(enum request request)
2297 struct run_request *req = get_run_request(request);
2298 char buf[SIZEOF_STR * 2];
2303 report("Unknown run request");
2311 char *next = strstr(cmd, "%(");
2312 int len = next - cmd;
2319 } else if (!strncmp(next, "%(head)", 7)) {
2322 } else if (!strncmp(next, "%(commit)", 9)) {
2325 } else if (!strncmp(next, "%(blob)", 7)) {
2329 report("Unknown replacement in run request: `%s`", req->cmd);
2333 if (!string_format_from(buf, &bufpos, "%.*s%s", len, cmd, value))
2337 next = strchr(next, ')') + 1;
2341 open_external_viewer(buf);
2345 * User request switch noodle
2349 view_driver(struct view *view, enum request request)
2353 if (request == REQ_NONE) {
2358 if (request > REQ_NONE) {
2359 open_run_request(request);
2363 if (view && view->lines) {
2364 request = view->ops->request(view, request, &view->line[view->lineno]);
2365 if (request == REQ_NONE)
2372 case REQ_MOVE_PAGE_UP:
2373 case REQ_MOVE_PAGE_DOWN:
2374 case REQ_MOVE_FIRST_LINE:
2375 case REQ_MOVE_LAST_LINE:
2376 move_view(view, request);
2379 case REQ_SCROLL_LINE_DOWN:
2380 case REQ_SCROLL_LINE_UP:
2381 case REQ_SCROLL_PAGE_DOWN:
2382 case REQ_SCROLL_PAGE_UP:
2383 scroll_view(view, request);
2388 report("No file chosen, press %s to open tree view",
2389 get_key(REQ_VIEW_TREE));
2392 open_view(view, request, OPEN_DEFAULT);
2395 case REQ_VIEW_PAGER:
2396 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2397 report("No pager content, press %s to run command from prompt",
2398 get_key(REQ_PROMPT));
2401 open_view(view, request, OPEN_DEFAULT);
2404 case REQ_VIEW_STAGE:
2405 if (!VIEW(REQ_VIEW_STAGE)->lines) {
2406 report("No stage content, press %s to open the status view and choose file",
2407 get_key(REQ_VIEW_STATUS));
2410 open_view(view, request, OPEN_DEFAULT);
2413 case REQ_VIEW_STATUS:
2414 if (opt_is_inside_work_tree == FALSE) {
2415 report("The status view requires a working tree");
2418 open_view(view, request, OPEN_DEFAULT);
2426 open_view(view, request, OPEN_DEFAULT);
2431 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2433 if ((view == VIEW(REQ_VIEW_DIFF) &&
2434 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2435 (view == VIEW(REQ_VIEW_STAGE) &&
2436 view->parent == VIEW(REQ_VIEW_STATUS)) ||
2437 (view == VIEW(REQ_VIEW_BLOB) &&
2438 view->parent == VIEW(REQ_VIEW_TREE))) {
2441 view = view->parent;
2442 line = view->lineno;
2443 move_view(view, request);
2444 if (view_is_displayed(view))
2445 update_view_title(view);
2446 if (line != view->lineno)
2447 view->ops->request(view, REQ_ENTER,
2448 &view->line[view->lineno]);
2451 move_view(view, request);
2457 int nviews = displayed_views();
2458 int next_view = (current_view + 1) % nviews;
2460 if (next_view == current_view) {
2461 report("Only one view is displayed");
2465 current_view = next_view;
2466 /* Blur out the title of the previous view. */
2467 update_view_title(view);
2472 report("Refreshing is not yet supported for the %s view", view->name);
2475 case REQ_TOGGLE_LINENO:
2476 opt_line_number = !opt_line_number;
2480 case REQ_TOGGLE_REV_GRAPH:
2481 opt_rev_graph = !opt_rev_graph;
2486 /* Always reload^Wrerun commands from the prompt. */
2487 open_view(view, opt_request, OPEN_RELOAD);
2491 case REQ_SEARCH_BACK:
2492 search_view(view, request);
2497 find_next(view, request);
2500 case REQ_STOP_LOADING:
2501 for (i = 0; i < ARRAY_SIZE(views); i++) {
2504 report("Stopped loading the %s view", view->name),
2509 case REQ_SHOW_VERSION:
2510 report("tig-%s (built %s)", TIG_VERSION, __DATE__);
2513 case REQ_SCREEN_RESIZE:
2516 case REQ_SCREEN_REDRAW:
2521 report("Nothing to edit");
2526 report("Nothing to enter");
2530 case REQ_VIEW_CLOSE:
2531 /* XXX: Mark closed views by letting view->parent point to the
2532 * view itself. Parents to closed view should never be
2535 view->parent->parent != view->parent) {
2536 memset(display, 0, sizeof(display));
2538 display[current_view] = view->parent;
2539 view->parent = view;
2549 /* An unknown key will show most commonly used commands. */
2550 report("Unknown key, press 'h' for help");
2563 pager_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2565 char *text = line->data;
2566 enum line_type type = line->type;
2567 int textlen = strlen(text);
2570 wmove(view->win, lineno, 0);
2574 wchgat(view->win, -1, 0, type, NULL);
2577 attr = get_line_attr(type);
2578 wattrset(view->win, attr);
2580 if (opt_line_number || opt_tab_size < TABSIZE) {
2581 static char spaces[] = " ";
2582 int col_offset = 0, col = 0;
2584 if (opt_line_number) {
2585 unsigned long real_lineno = view->offset + lineno + 1;
2587 if (real_lineno == 1 ||
2588 (real_lineno % opt_num_interval) == 0) {
2589 wprintw(view->win, "%.*d", view->digits, real_lineno);
2592 waddnstr(view->win, spaces,
2593 MIN(view->digits, STRING_SIZE(spaces)));
2595 waddstr(view->win, ": ");
2596 col_offset = view->digits + 2;
2599 while (text && col_offset + col < view->width) {
2600 int cols_max = view->width - col_offset - col;
2604 if (*text == '\t') {
2606 assert(sizeof(spaces) > TABSIZE);
2608 cols = opt_tab_size - (col % opt_tab_size);
2611 text = strchr(text, '\t');
2612 cols = line ? text - pos : strlen(pos);
2615 waddnstr(view->win, pos, MIN(cols, cols_max));
2620 int col = 0, pos = 0;
2622 for (; pos < textlen && col < view->width; pos++, col++)
2623 if (text[pos] == '\t')
2624 col += TABSIZE - (col % TABSIZE) - 1;
2626 waddnstr(view->win, text, pos);
2633 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2635 char refbuf[SIZEOF_STR];
2639 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2642 pipe = popen(refbuf, "r");
2646 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2647 ref = chomp_string(ref);
2653 /* This is the only fatal call, since it can "corrupt" the buffer. */
2654 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2661 add_pager_refs(struct view *view, struct line *line)
2663 char buf[SIZEOF_STR];
2664 char *commit_id = line->data + STRING_SIZE("commit ");
2666 size_t bufpos = 0, refpos = 0;
2667 const char *sep = "Refs: ";
2668 bool is_tag = FALSE;
2670 assert(line->type == LINE_COMMIT);
2672 refs = get_refs(commit_id);
2674 if (view == VIEW(REQ_VIEW_DIFF))
2675 goto try_add_describe_ref;
2680 struct ref *ref = refs[refpos];
2681 char *fmt = ref->tag ? "%s[%s]" :
2682 ref->remote ? "%s<%s>" : "%s%s";
2684 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2689 } while (refs[refpos++]->next);
2691 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2692 try_add_describe_ref:
2693 /* Add <tag>-g<commit_id> "fake" reference. */
2694 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2701 if (!realloc_lines(view, view->line_size + 1))
2704 add_line_text(view, buf, LINE_PP_REFS);
2708 pager_read(struct view *view, char *data)
2715 line = add_line_text(view, data, get_line_type(data));
2719 if (line->type == LINE_COMMIT &&
2720 (view == VIEW(REQ_VIEW_DIFF) ||
2721 view == VIEW(REQ_VIEW_LOG)))
2722 add_pager_refs(view, line);
2728 pager_request(struct view *view, enum request request, struct line *line)
2732 if (request != REQ_ENTER)
2735 if (line->type == LINE_COMMIT &&
2736 (view == VIEW(REQ_VIEW_LOG) ||
2737 view == VIEW(REQ_VIEW_PAGER))) {
2738 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2742 /* Always scroll the view even if it was split. That way
2743 * you can use Enter to scroll through the log view and
2744 * split open each commit diff. */
2745 scroll_view(view, REQ_SCROLL_LINE_DOWN);
2747 /* FIXME: A minor workaround. Scrolling the view will call report("")
2748 * but if we are scrolling a non-current view this won't properly
2749 * update the view title. */
2751 update_view_title(view);
2757 pager_grep(struct view *view, struct line *line)
2760 char *text = line->data;
2765 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
2772 pager_select(struct view *view, struct line *line)
2774 if (line->type == LINE_COMMIT) {
2775 char *text = line->data + STRING_SIZE("commit ");
2777 if (view != VIEW(REQ_VIEW_PAGER))
2778 string_copy_rev(view->ref, text);
2779 string_copy_rev(ref_commit, text);
2783 static struct view_ops pager_ops = {
2799 help_open(struct view *view)
2802 int lines = ARRAY_SIZE(req_info) + 2;
2805 if (view->lines > 0)
2808 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2809 if (!req_info[i].request)
2812 lines += run_requests + 1;
2814 view->line = calloc(lines, sizeof(*view->line));
2818 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
2820 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2823 if (req_info[i].request == REQ_NONE)
2826 if (!req_info[i].request) {
2827 add_line_text(view, "", LINE_DEFAULT);
2828 add_line_text(view, req_info[i].help, LINE_DEFAULT);
2832 key = get_key(req_info[i].request);
2834 key = "(no key defined)";
2836 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
2839 add_line_text(view, buf, LINE_DEFAULT);
2843 add_line_text(view, "", LINE_DEFAULT);
2844 add_line_text(view, "External commands:", LINE_DEFAULT);
2847 for (i = 0; i < run_requests; i++) {
2848 struct run_request *req = get_run_request(REQ_NONE + i + 1);
2854 key = get_key_name(req->key);
2856 key = "(no key defined)";
2858 if (!string_format(buf, " %-10s %-14s `%s`",
2859 keymap_table[req->keymap].name,
2863 add_line_text(view, buf, LINE_DEFAULT);
2869 static struct view_ops help_ops = {
2884 struct tree_stack_entry {
2885 struct tree_stack_entry *prev; /* Entry below this in the stack */
2886 unsigned long lineno; /* Line number to restore */
2887 char *name; /* Position of name in opt_path */
2890 /* The top of the path stack. */
2891 static struct tree_stack_entry *tree_stack = NULL;
2892 unsigned long tree_lineno = 0;
2895 pop_tree_stack_entry(void)
2897 struct tree_stack_entry *entry = tree_stack;
2899 tree_lineno = entry->lineno;
2901 tree_stack = entry->prev;
2906 push_tree_stack_entry(char *name, unsigned long lineno)
2908 struct tree_stack_entry *entry = calloc(1, sizeof(*entry));
2909 size_t pathlen = strlen(opt_path);
2914 entry->prev = tree_stack;
2915 entry->name = opt_path + pathlen;
2918 if (!string_format_from(opt_path, &pathlen, "%s/", name)) {
2919 pop_tree_stack_entry();
2923 /* Move the current line to the first tree entry. */
2925 entry->lineno = lineno;
2928 /* Parse output from git-ls-tree(1):
2930 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
2931 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
2932 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
2933 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
2936 #define SIZEOF_TREE_ATTR \
2937 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
2939 #define TREE_UP_FORMAT "040000 tree %s\t.."
2942 tree_compare_entry(enum line_type type1, char *name1,
2943 enum line_type type2, char *name2)
2945 if (type1 != type2) {
2946 if (type1 == LINE_TREE_DIR)
2951 return strcmp(name1, name2);
2955 tree_read(struct view *view, char *text)
2957 size_t textlen = text ? strlen(text) : 0;
2958 char buf[SIZEOF_STR];
2960 enum line_type type;
2961 bool first_read = view->lines == 0;
2963 if (textlen <= SIZEOF_TREE_ATTR)
2966 type = text[STRING_SIZE("100644 ")] == 't'
2967 ? LINE_TREE_DIR : LINE_TREE_FILE;
2970 /* Add path info line */
2971 if (!string_format(buf, "Directory path /%s", opt_path) ||
2972 !realloc_lines(view, view->line_size + 1) ||
2973 !add_line_text(view, buf, LINE_DEFAULT))
2976 /* Insert "link" to parent directory. */
2978 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
2979 !realloc_lines(view, view->line_size + 1) ||
2980 !add_line_text(view, buf, LINE_TREE_DIR))
2985 /* Strip the path part ... */
2987 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
2988 size_t striplen = strlen(opt_path);
2989 char *path = text + SIZEOF_TREE_ATTR;
2991 if (pathlen > striplen)
2992 memmove(path, path + striplen,
2993 pathlen - striplen + 1);
2996 /* Skip "Directory ..." and ".." line. */
2997 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
2998 struct line *line = &view->line[pos];
2999 char *path1 = ((char *) line->data) + SIZEOF_TREE_ATTR;
3000 char *path2 = text + SIZEOF_TREE_ATTR;
3001 int cmp = tree_compare_entry(line->type, path1, type, path2);
3006 text = strdup(text);
3010 if (view->lines > pos)
3011 memmove(&view->line[pos + 1], &view->line[pos],
3012 (view->lines - pos) * sizeof(*line));
3014 line = &view->line[pos];
3021 if (!add_line_text(view, text, type))
3024 if (tree_lineno > view->lineno) {
3025 view->lineno = tree_lineno;
3033 tree_request(struct view *view, enum request request, struct line *line)
3035 enum open_flags flags;
3037 if (request != REQ_ENTER)
3040 /* Cleanup the stack if the tree view is at a different tree. */
3041 while (!*opt_path && tree_stack)
3042 pop_tree_stack_entry();
3044 switch (line->type) {
3046 /* Depending on whether it is a subdir or parent (updir?) link
3047 * mangle the path buffer. */
3048 if (line == &view->line[1] && *opt_path) {
3049 pop_tree_stack_entry();
3052 char *data = line->data;
3053 char *basename = data + SIZEOF_TREE_ATTR;
3055 push_tree_stack_entry(basename, view->lineno);
3058 /* Trees and subtrees share the same ID, so they are not not
3059 * unique like blobs. */
3060 flags = OPEN_RELOAD;
3061 request = REQ_VIEW_TREE;
3064 case LINE_TREE_FILE:
3065 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3066 request = REQ_VIEW_BLOB;
3073 open_view(view, request, flags);
3074 if (request == REQ_VIEW_TREE) {
3075 view->lineno = tree_lineno;
3082 tree_select(struct view *view, struct line *line)
3084 char *text = line->data + STRING_SIZE("100644 blob ");
3086 if (line->type == LINE_TREE_FILE) {
3087 string_copy_rev(ref_blob, text);
3089 } else if (line->type != LINE_TREE_DIR) {
3093 string_copy_rev(view->ref, text);
3096 static struct view_ops tree_ops = {
3107 blob_read(struct view *view, char *line)
3109 return add_line_text(view, line, LINE_DEFAULT) != NULL;
3112 static struct view_ops blob_ops = {
3131 char rev[SIZEOF_REV];
3135 char rev[SIZEOF_REV];
3137 char name[SIZEOF_STR];
3140 static struct status stage_status;
3141 static enum line_type stage_line_type;
3143 /* Get fields from the diff line:
3144 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
3147 status_get_diff(struct status *file, char *buf, size_t bufsize)
3149 char *old_mode = buf + 1;
3150 char *new_mode = buf + 8;
3151 char *old_rev = buf + 15;
3152 char *new_rev = buf + 56;
3153 char *status = buf + 97;
3155 if (bufsize != 99 ||
3156 old_mode[-1] != ':' ||
3157 new_mode[-1] != ' ' ||
3158 old_rev[-1] != ' ' ||
3159 new_rev[-1] != ' ' ||
3163 file->status = *status;
3165 string_copy_rev(file->old.rev, old_rev);
3166 string_copy_rev(file->new.rev, new_rev);
3168 file->old.mode = strtoul(old_mode, NULL, 8);
3169 file->new.mode = strtoul(new_mode, NULL, 8);
3177 status_run(struct view *view, const char cmd[], bool diff, enum line_type type)
3179 struct status *file = NULL;
3180 struct status *unmerged = NULL;
3181 char buf[SIZEOF_STR * 4];
3185 pipe = popen(cmd, "r");
3189 add_line_data(view, NULL, type);
3191 while (!feof(pipe) && !ferror(pipe)) {
3195 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
3198 bufsize += readsize;
3200 /* Process while we have NUL chars. */
3201 while ((sep = memchr(buf, 0, bufsize))) {
3202 size_t sepsize = sep - buf + 1;
3205 if (!realloc_lines(view, view->line_size + 1))
3208 file = calloc(1, sizeof(*file));
3212 add_line_data(view, file, type);
3215 /* Parse diff info part. */
3219 } else if (!file->status) {
3220 if (!status_get_diff(file, buf, sepsize))
3224 memmove(buf, sep + 1, bufsize);
3226 sep = memchr(buf, 0, bufsize);
3229 sepsize = sep - buf + 1;
3231 /* Collapse all 'M'odified entries that
3232 * follow a associated 'U'nmerged entry.
3234 if (file->status == 'U') {
3237 } else if (unmerged) {
3238 int collapse = !strcmp(buf, unmerged->name);
3249 /* git-ls-files just delivers a NUL separated
3250 * list of file names similar to the second half
3251 * of the git-diff-* output. */
3252 string_ncopy(file->name, buf, sepsize);
3254 memmove(buf, sep + 1, bufsize);
3265 if (!view->line[view->lines - 1].data)
3266 add_line_data(view, NULL, LINE_STAT_NONE);
3272 /* Don't show unmerged entries in the staged section. */
3273 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached HEAD"
3274 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
3275 #define STATUS_LIST_OTHER_CMD \
3276 "git ls-files -z --others --exclude-per-directory=.gitignore"
3278 #define STATUS_DIFF_SHOW_CMD \
3279 "git diff --root --patch-with-stat --find-copies-harder -B -C %s -- %s 2>/dev/null"
3281 /* First parse staged info using git-diff-index(1), then parse unstaged
3282 * info using git-diff-files(1), and finally untracked files using
3283 * git-ls-files(1). */
3285 status_open(struct view *view)
3287 struct stat statbuf;
3288 char exclude[SIZEOF_STR];
3289 char cmd[SIZEOF_STR];
3290 unsigned long prev_lineno = view->lineno;
3293 for (i = 0; i < view->lines; i++)
3294 free(view->line[i].data);
3296 view->lines = view->line_size = view->lineno = 0;
3299 if (!realloc_lines(view, view->line_size + 6))
3302 if (!string_format(exclude, "%s/info/exclude", opt_git_dir))
3305 string_copy(cmd, STATUS_LIST_OTHER_CMD);
3307 if (stat(exclude, &statbuf) >= 0) {
3308 size_t cmdsize = strlen(cmd);
3310 if (!string_format_from(cmd, &cmdsize, " %s", "--exclude-from=") ||
3311 sq_quote(cmd, cmdsize, exclude) >= sizeof(cmd))
3315 if (!status_run(view, STATUS_DIFF_INDEX_CMD, TRUE, LINE_STAT_STAGED) ||
3316 !status_run(view, STATUS_DIFF_FILES_CMD, TRUE, LINE_STAT_UNSTAGED) ||
3317 !status_run(view, cmd, FALSE, LINE_STAT_UNTRACKED))
3320 /* If all went well restore the previous line number to stay in
3322 if (prev_lineno < view->lines)
3323 view->lineno = prev_lineno;
3325 view->lineno = view->lines - 1;
3331 status_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
3333 struct status *status = line->data;
3335 wmove(view->win, lineno, 0);
3338 wattrset(view->win, get_line_attr(LINE_CURSOR));
3339 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
3341 } else if (!status && line->type != LINE_STAT_NONE) {
3342 wattrset(view->win, get_line_attr(LINE_STAT_SECTION));
3343 wchgat(view->win, -1, 0, LINE_STAT_SECTION, NULL);
3346 wattrset(view->win, get_line_attr(line->type));
3352 switch (line->type) {
3353 case LINE_STAT_STAGED:
3354 text = "Changes to be committed:";
3357 case LINE_STAT_UNSTAGED:
3358 text = "Changed but not updated:";
3361 case LINE_STAT_UNTRACKED:
3362 text = "Untracked files:";
3365 case LINE_STAT_NONE:
3366 text = " (no files)";
3373 waddstr(view->win, text);
3377 waddch(view->win, status->status);
3379 wattrset(view->win, A_NORMAL);
3380 wmove(view->win, lineno, 4);
3381 waddstr(view->win, status->name);
3387 status_enter(struct view *view, struct line *line)
3389 struct status *status = line->data;
3390 char path[SIZEOF_STR] = "";
3394 if (line->type == LINE_STAT_NONE ||
3395 (!status && line[1].type == LINE_STAT_NONE)) {
3396 report("No file to diff");
3400 if (status && sq_quote(path, 0, status->name) >= sizeof(path))
3404 line->type != LINE_STAT_UNTRACKED &&
3405 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
3408 switch (line->type) {
3409 case LINE_STAT_STAGED:
3410 if (!string_format_from(opt_cmd, &cmdsize, STATUS_DIFF_SHOW_CMD,
3414 info = "Staged changes to %s";
3416 info = "Staged changes";
3419 case LINE_STAT_UNSTAGED:
3420 if (!string_format_from(opt_cmd, &cmdsize, STATUS_DIFF_SHOW_CMD,
3424 info = "Unstaged changes to %s";
3426 info = "Unstaged changes";
3429 case LINE_STAT_UNTRACKED:
3435 report("No file to show");
3439 opt_pipe = fopen(status->name, "r");
3440 info = "Untracked file %s";
3444 die("line type %d not handled in switch", line->type);
3447 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | OPEN_SPLIT);
3448 if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) {
3450 stage_status = *status;
3452 memset(&stage_status, 0, sizeof(stage_status));
3455 stage_line_type = line->type;
3456 string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.name);
3464 status_update_file(struct view *view, struct status *status, enum line_type type)
3466 char cmd[SIZEOF_STR];
3467 char buf[SIZEOF_STR];
3474 type != LINE_STAT_UNTRACKED &&
3475 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
3479 case LINE_STAT_STAGED:
3480 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
3486 string_add(cmd, cmdsize, "git update-index -z --index-info");
3489 case LINE_STAT_UNSTAGED:
3490 case LINE_STAT_UNTRACKED:
3491 if (!string_format_from(buf, &bufsize, "%s%c", status->name, 0))
3494 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
3498 die("line type %d not handled in switch", type);
3501 pipe = popen(cmd, "w");
3505 while (!ferror(pipe) && written < bufsize) {
3506 written += fwrite(buf + written, 1, bufsize - written, pipe);
3511 if (written != bufsize)
3518 status_update(struct view *view)
3520 struct line *line = &view->line[view->lineno];
3522 assert(view->lines);
3525 while (++line < view->line + view->lines && line->data) {
3526 if (!status_update_file(view, line->data, line->type))
3527 report("Failed to update file status");
3530 if (!line[-1].data) {
3531 report("Nothing to update");
3535 } else if (!status_update_file(view, line->data, line->type)) {
3536 report("Failed to update file status");
3541 status_request(struct view *view, enum request request, struct line *line)
3543 struct status *status = line->data;
3546 case REQ_STATUS_UPDATE:
3547 status_update(view);
3550 case REQ_STATUS_MERGE:
3551 open_mergetool(status->name);
3558 open_editor(status->status != '?', status->name);
3562 /* After returning the status view has been split to
3563 * show the stage view. No further reloading is
3565 status_enter(view, line);
3569 /* Simply reload the view. */
3576 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
3582 status_select(struct view *view, struct line *line)
3584 struct status *status = line->data;
3585 char file[SIZEOF_STR] = "all files";
3589 if (status && !string_format(file, "'%s'", status->name))
3592 if (!status && line[1].type == LINE_STAT_NONE)
3595 switch (line->type) {
3596 case LINE_STAT_STAGED:
3597 text = "Press %s to unstage %s for commit";
3600 case LINE_STAT_UNSTAGED:
3601 text = "Press %s to stage %s for commit";
3604 case LINE_STAT_UNTRACKED:
3605 text = "Press %s to stage %s for addition";
3608 case LINE_STAT_NONE:
3609 text = "Nothing to update";
3613 die("line type %d not handled in switch", line->type);
3616 if (status && status->status == 'U') {
3617 text = "Press %s to resolve conflict in %s";
3618 key = get_key(REQ_STATUS_MERGE);
3621 key = get_key(REQ_STATUS_UPDATE);
3624 string_format(view->ref, text, key, file);
3628 status_grep(struct view *view, struct line *line)
3630 struct status *status = line->data;
3631 enum { S_STATUS, S_NAME, S_END } state;
3638 for (state = S_STATUS; state < S_END; state++) {
3642 case S_NAME: text = status->name; break;
3644 buf[0] = status->status;
3652 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3659 static struct view_ops status_ops = {
3671 stage_diff_line(FILE *pipe, struct line *line)
3673 char *buf = line->data;
3674 size_t bufsize = strlen(buf);
3677 while (!ferror(pipe) && written < bufsize) {
3678 written += fwrite(buf + written, 1, bufsize - written, pipe);
3683 return written == bufsize;
3686 static struct line *
3687 stage_diff_hdr(struct view *view, struct line *line)
3689 int diff_hdr_dir = line->type == LINE_DIFF_CHUNK ? -1 : 1;
3690 struct line *diff_hdr;
3692 if (line->type == LINE_DIFF_CHUNK)
3693 diff_hdr = line - 1;
3695 diff_hdr = view->line + 1;
3697 while (diff_hdr > view->line && diff_hdr < view->line + view->lines) {
3698 if (diff_hdr->type == LINE_DIFF_HEADER)
3701 diff_hdr += diff_hdr_dir;
3708 stage_update_chunk(struct view *view, struct line *line)
3710 char cmd[SIZEOF_STR];
3712 struct line *diff_hdr, *diff_chunk, *diff_end;
3715 diff_hdr = stage_diff_hdr(view, line);
3720 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
3723 if (!string_format_from(cmd, &cmdsize,
3724 "git apply --cached %s - && "
3725 "git update-index -q --unmerged --refresh 2>/dev/null",
3726 stage_line_type == LINE_STAT_STAGED ? "-R" : ""))
3729 pipe = popen(cmd, "w");
3733 diff_end = view->line + view->lines;
3734 if (line->type != LINE_DIFF_CHUNK) {
3735 diff_chunk = diff_hdr;
3738 for (diff_chunk = line + 1; diff_chunk < diff_end; diff_chunk++)
3739 if (diff_chunk->type == LINE_DIFF_CHUNK ||
3740 diff_chunk->type == LINE_DIFF_HEADER)
3741 diff_end = diff_chunk;
3745 while (diff_hdr->type != LINE_DIFF_CHUNK) {
3746 switch (diff_hdr->type) {
3747 case LINE_DIFF_HEADER:
3748 case LINE_DIFF_INDEX:
3758 if (!stage_diff_line(pipe, diff_hdr++)) {
3765 while (diff_chunk < diff_end && stage_diff_line(pipe, diff_chunk))
3770 if (diff_chunk != diff_end)
3777 stage_update(struct view *view, struct line *line)
3779 if (stage_line_type != LINE_STAT_UNTRACKED &&
3780 (line->type == LINE_DIFF_CHUNK || !stage_status.status)) {
3781 if (!stage_update_chunk(view, line)) {
3782 report("Failed to apply chunk");
3786 } else if (!status_update_file(view, &stage_status, stage_line_type)) {
3787 report("Failed to update file");
3791 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
3793 view = VIEW(REQ_VIEW_STATUS);
3794 if (view_is_displayed(view))
3795 status_enter(view, &view->line[view->lineno]);
3799 stage_request(struct view *view, enum request request, struct line *line)
3802 case REQ_STATUS_UPDATE:
3803 stage_update(view, line);
3807 if (!stage_status.name[0])
3810 open_editor(stage_status.status != '?', stage_status.name);
3814 pager_request(view, request, line);
3824 static struct view_ops stage_ops = {
3840 char id[SIZEOF_REV]; /* SHA1 ID. */
3841 char title[128]; /* First line of the commit message. */
3842 char author[75]; /* Author of the commit. */
3843 struct tm time; /* Date from the author ident. */
3844 struct ref **refs; /* Repository references. */
3845 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
3846 size_t graph_size; /* The width of the graph array. */
3849 /* Size of rev graph with no "padding" columns */
3850 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
3853 struct rev_graph *prev, *next, *parents;
3854 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
3856 struct commit *commit;
3860 /* Parents of the commit being visualized. */
3861 static struct rev_graph graph_parents[4];
3863 /* The current stack of revisions on the graph. */
3864 static struct rev_graph graph_stacks[4] = {
3865 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
3866 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
3867 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
3868 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
3872 graph_parent_is_merge(struct rev_graph *graph)
3874 return graph->parents->size > 1;
3878 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
3880 struct commit *commit = graph->commit;
3882 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
3883 commit->graph[commit->graph_size++] = symbol;
3887 done_rev_graph(struct rev_graph *graph)
3889 if (graph_parent_is_merge(graph) &&
3890 graph->pos < graph->size - 1 &&
3891 graph->next->size == graph->size + graph->parents->size - 1) {
3892 size_t i = graph->pos + graph->parents->size - 1;
3894 graph->commit->graph_size = i * 2;
3895 while (i < graph->next->size - 1) {
3896 append_to_rev_graph(graph, ' ');
3897 append_to_rev_graph(graph, '\\');
3902 graph->size = graph->pos = 0;
3903 graph->commit = NULL;
3904 memset(graph->parents, 0, sizeof(*graph->parents));
3908 push_rev_graph(struct rev_graph *graph, char *parent)
3912 /* "Collapse" duplicate parents lines.
3914 * FIXME: This needs to also update update the drawn graph but
3915 * for now it just serves as a method for pruning graph lines. */
3916 for (i = 0; i < graph->size; i++)
3917 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
3920 if (graph->size < SIZEOF_REVITEMS) {
3921 string_copy_rev(graph->rev[graph->size++], parent);
3926 get_rev_graph_symbol(struct rev_graph *graph)
3930 if (graph->parents->size == 0)
3931 symbol = REVGRAPH_INIT;
3932 else if (graph_parent_is_merge(graph))
3933 symbol = REVGRAPH_MERGE;
3934 else if (graph->pos >= graph->size)
3935 symbol = REVGRAPH_BRANCH;
3937 symbol = REVGRAPH_COMMIT;
3943 draw_rev_graph(struct rev_graph *graph)
3946 chtype separator, line;
3948 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
3949 static struct rev_filler fillers[] = {
3950 { ' ', REVGRAPH_LINE },
3955 chtype symbol = get_rev_graph_symbol(graph);
3956 struct rev_filler *filler;
3959 filler = &fillers[DEFAULT];
3961 for (i = 0; i < graph->pos; i++) {
3962 append_to_rev_graph(graph, filler->line);
3963 if (graph_parent_is_merge(graph->prev) &&
3964 graph->prev->pos == i)
3965 filler = &fillers[RSHARP];
3967 append_to_rev_graph(graph, filler->separator);
3970 /* Place the symbol for this revision. */
3971 append_to_rev_graph(graph, symbol);
3973 if (graph->prev->size > graph->size)
3974 filler = &fillers[RDIAG];
3976 filler = &fillers[DEFAULT];
3980 for (; i < graph->size; i++) {
3981 append_to_rev_graph(graph, filler->separator);
3982 append_to_rev_graph(graph, filler->line);
3983 if (graph_parent_is_merge(graph->prev) &&
3984 i < graph->prev->pos + graph->parents->size)
3985 filler = &fillers[RSHARP];
3986 if (graph->prev->size > graph->size)
3987 filler = &fillers[LDIAG];
3990 if (graph->prev->size > graph->size) {
3991 append_to_rev_graph(graph, filler->separator);
3992 if (filler->line != ' ')
3993 append_to_rev_graph(graph, filler->line);
3997 /* Prepare the next rev graph */
3999 prepare_rev_graph(struct rev_graph *graph)
4003 /* First, traverse all lines of revisions up to the active one. */
4004 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
4005 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
4008 push_rev_graph(graph->next, graph->rev[graph->pos]);
4011 /* Interleave the new revision parent(s). */
4012 for (i = 0; i < graph->parents->size; i++)
4013 push_rev_graph(graph->next, graph->parents->rev[i]);
4015 /* Lastly, put any remaining revisions. */
4016 for (i = graph->pos + 1; i < graph->size; i++)
4017 push_rev_graph(graph->next, graph->rev[i]);
4021 update_rev_graph(struct rev_graph *graph)
4023 /* If this is the finalizing update ... */
4025 prepare_rev_graph(graph);
4027 /* Graph visualization needs a one rev look-ahead,
4028 * so the first update doesn't visualize anything. */
4029 if (!graph->prev->commit)
4032 draw_rev_graph(graph->prev);
4033 done_rev_graph(graph->prev->prev);
4042 main_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
4044 char buf[DATE_COLS + 1];
4045 struct commit *commit = line->data;
4046 enum line_type type;
4052 if (!*commit->author)
4055 wmove(view->win, lineno, col);
4059 wattrset(view->win, get_line_attr(type));
4060 wchgat(view->win, -1, 0, type, NULL);
4063 type = LINE_MAIN_COMMIT;
4064 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
4067 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
4068 waddnstr(view->win, buf, timelen);
4069 waddstr(view->win, " ");
4072 wmove(view->win, lineno, col);
4073 if (type != LINE_CURSOR)
4074 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
4077 authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
4079 authorlen = strlen(commit->author);
4080 if (authorlen > AUTHOR_COLS - 2) {
4081 authorlen = AUTHOR_COLS - 2;
4087 waddnstr(view->win, commit->author, authorlen);
4088 if (type != LINE_CURSOR)
4089 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
4090 waddch(view->win, '~');
4092 waddstr(view->win, commit->author);
4096 if (type != LINE_CURSOR)
4097 wattrset(view->win, A_NORMAL);
4099 if (opt_rev_graph && commit->graph_size) {
4102 wmove(view->win, lineno, col);
4103 /* Using waddch() instead of waddnstr() ensures that
4104 * they'll be rendered correctly for the cursor line. */
4105 for (i = 0; i < commit->graph_size; i++)
4106 waddch(view->win, commit->graph[i]);
4108 waddch(view->win, ' ');
4109 col += commit->graph_size + 1;
4112 wmove(view->win, lineno, col);
4118 if (type == LINE_CURSOR)
4120 else if (commit->refs[i]->tag)
4121 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
4122 else if (commit->refs[i]->remote)
4123 wattrset(view->win, get_line_attr(LINE_MAIN_REMOTE));
4125 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
4126 waddstr(view->win, "[");
4127 waddstr(view->win, commit->refs[i]->name);
4128 waddstr(view->win, "]");
4129 if (type != LINE_CURSOR)
4130 wattrset(view->win, A_NORMAL);
4131 waddstr(view->win, " ");
4132 col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
4133 } while (commit->refs[i++]->next);
4136 if (type != LINE_CURSOR)
4137 wattrset(view->win, get_line_attr(type));
4140 int titlelen = strlen(commit->title);
4142 if (col + titlelen > view->width)
4143 titlelen = view->width - col;
4145 waddnstr(view->win, commit->title, titlelen);
4151 /* Reads git log --pretty=raw output and parses it into the commit struct. */
4153 main_read(struct view *view, char *line)
4155 static struct rev_graph *graph = graph_stacks;
4156 enum line_type type;
4157 struct commit *commit;
4160 update_rev_graph(graph);
4164 type = get_line_type(line);
4165 if (type == LINE_COMMIT) {
4166 commit = calloc(1, sizeof(struct commit));
4170 string_copy_rev(commit->id, line + STRING_SIZE("commit "));
4171 commit->refs = get_refs(commit->id);
4172 graph->commit = commit;
4173 add_line_data(view, commit, LINE_MAIN_COMMIT);
4179 commit = view->line[view->lines - 1].data;
4183 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
4188 /* Parse author lines where the name may be empty:
4189 * author <email@address.tld> 1138474660 +0100
4191 char *ident = line + STRING_SIZE("author ");
4192 char *nameend = strchr(ident, '<');
4193 char *emailend = strchr(ident, '>');
4195 if (!nameend || !emailend)
4198 update_rev_graph(graph);
4199 graph = graph->next;
4201 *nameend = *emailend = 0;
4202 ident = chomp_string(ident);
4204 ident = chomp_string(nameend + 1);
4209 string_ncopy(commit->author, ident, strlen(ident));
4211 /* Parse epoch and timezone */
4212 if (emailend[1] == ' ') {
4213 char *secs = emailend + 2;
4214 char *zone = strchr(secs, ' ');
4215 time_t time = (time_t) atol(secs);
4217 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
4221 tz = ('0' - zone[1]) * 60 * 60 * 10;
4222 tz += ('0' - zone[2]) * 60 * 60;
4223 tz += ('0' - zone[3]) * 60;
4224 tz += ('0' - zone[4]) * 60;
4232 gmtime_r(&time, &commit->time);
4237 /* Fill in the commit title if it has not already been set. */
4238 if (commit->title[0])
4241 /* Require titles to start with a non-space character at the
4242 * offset used by git log. */
4243 if (strncmp(line, " ", 4))
4246 /* Well, if the title starts with a whitespace character,
4247 * try to be forgiving. Otherwise we end up with no title. */
4248 while (isspace(*line))
4252 /* FIXME: More graceful handling of titles; append "..." to
4253 * shortened titles, etc. */
4255 string_ncopy(commit->title, line, strlen(line));
4262 main_request(struct view *view, enum request request, struct line *line)
4264 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
4266 if (request == REQ_ENTER)
4267 open_view(view, REQ_VIEW_DIFF, flags);
4275 main_grep(struct view *view, struct line *line)
4277 struct commit *commit = line->data;
4278 enum { S_TITLE, S_AUTHOR, S_DATE, S_END } state;
4279 char buf[DATE_COLS + 1];
4282 for (state = S_TITLE; state < S_END; state++) {
4286 case S_TITLE: text = commit->title; break;
4287 case S_AUTHOR: text = commit->author; break;
4289 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
4298 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4306 main_select(struct view *view, struct line *line)
4308 struct commit *commit = line->data;
4310 string_copy_rev(view->ref, commit->id);
4311 string_copy_rev(ref_commit, view->ref);
4314 static struct view_ops main_ops = {
4326 * Unicode / UTF-8 handling
4328 * NOTE: Much of the following code for dealing with unicode is derived from
4329 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
4330 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
4333 /* I've (over)annotated a lot of code snippets because I am not entirely
4334 * confident that the approach taken by this small UTF-8 interface is correct.
4338 unicode_width(unsigned long c)
4341 (c <= 0x115f /* Hangul Jamo */
4344 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
4346 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
4347 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
4348 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
4349 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
4350 || (c >= 0xffe0 && c <= 0xffe6)
4351 || (c >= 0x20000 && c <= 0x2fffd)
4352 || (c >= 0x30000 && c <= 0x3fffd)))
4358 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
4359 * Illegal bytes are set one. */
4360 static const unsigned char utf8_bytes[256] = {
4361 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,
4362 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,
4363 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,
4364 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,
4365 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,
4366 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,
4367 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,
4368 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,
4371 /* Decode UTF-8 multi-byte representation into a unicode character. */
4372 static inline unsigned long
4373 utf8_to_unicode(const char *string, size_t length)
4375 unsigned long unicode;
4379 unicode = string[0];
4382 unicode = (string[0] & 0x1f) << 6;
4383 unicode += (string[1] & 0x3f);
4386 unicode = (string[0] & 0x0f) << 12;
4387 unicode += ((string[1] & 0x3f) << 6);
4388 unicode += (string[2] & 0x3f);
4391 unicode = (string[0] & 0x0f) << 18;
4392 unicode += ((string[1] & 0x3f) << 12);
4393 unicode += ((string[2] & 0x3f) << 6);
4394 unicode += (string[3] & 0x3f);
4397 unicode = (string[0] & 0x0f) << 24;
4398 unicode += ((string[1] & 0x3f) << 18);
4399 unicode += ((string[2] & 0x3f) << 12);
4400 unicode += ((string[3] & 0x3f) << 6);
4401 unicode += (string[4] & 0x3f);
4404 unicode = (string[0] & 0x01) << 30;
4405 unicode += ((string[1] & 0x3f) << 24);
4406 unicode += ((string[2] & 0x3f) << 18);
4407 unicode += ((string[3] & 0x3f) << 12);
4408 unicode += ((string[4] & 0x3f) << 6);
4409 unicode += (string[5] & 0x3f);
4412 die("Invalid unicode length");
4415 /* Invalid characters could return the special 0xfffd value but NUL
4416 * should be just as good. */
4417 return unicode > 0xffff ? 0 : unicode;
4420 /* Calculates how much of string can be shown within the given maximum width
4421 * and sets trimmed parameter to non-zero value if all of string could not be
4424 * Additionally, adds to coloffset how many many columns to move to align with
4425 * the expected position. Takes into account how multi-byte and double-width
4426 * characters will effect the cursor position.
4428 * Returns the number of bytes to output from string to satisfy max_width. */
4430 utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
4432 const char *start = string;
4433 const char *end = strchr(string, '\0');
4439 while (string < end) {
4440 int c = *(unsigned char *) string;
4441 unsigned char bytes = utf8_bytes[c];
4443 unsigned long unicode;
4445 if (string + bytes > end)
4448 /* Change representation to figure out whether
4449 * it is a single- or double-width character. */
4451 unicode = utf8_to_unicode(string, bytes);
4452 /* FIXME: Graceful handling of invalid unicode character. */
4456 ucwidth = unicode_width(unicode);
4458 if (width > max_width) {
4463 /* The column offset collects the differences between the
4464 * number of bytes encoding a character and the number of
4465 * columns will be used for rendering said character.
4467 * So if some character A is encoded in 2 bytes, but will be
4468 * represented on the screen using only 1 byte this will and up
4469 * adding 1 to the multi-byte column offset.
4471 * Assumes that no double-width character can be encoding in
4472 * less than two bytes. */
4473 if (bytes > ucwidth)
4474 mbwidth += bytes - ucwidth;
4479 *coloffset += mbwidth;
4481 return string - start;
4489 /* Whether or not the curses interface has been initialized. */
4490 static bool cursed = FALSE;
4492 /* The status window is used for polling keystrokes. */
4493 static WINDOW *status_win;
4495 static bool status_empty = TRUE;
4497 /* Update status and title window. */
4499 report(const char *msg, ...)
4501 struct view *view = display[current_view];
4507 char buf[SIZEOF_STR];
4510 va_start(args, msg);
4511 if (vsnprintf(buf, sizeof(buf), msg, args) >= sizeof(buf)) {
4512 buf[sizeof(buf) - 1] = 0;
4513 buf[sizeof(buf) - 2] = '.';
4514 buf[sizeof(buf) - 3] = '.';
4515 buf[sizeof(buf) - 4] = '.';
4521 if (!status_empty || *msg) {
4524 va_start(args, msg);
4526 wmove(status_win, 0, 0);
4528 vwprintw(status_win, msg, args);
4529 status_empty = FALSE;
4531 status_empty = TRUE;
4533 wclrtoeol(status_win);
4534 wrefresh(status_win);
4539 update_view_title(view);
4540 update_display_cursor(view);
4543 /* Controls when nodelay should be in effect when polling user input. */
4545 set_nonblocking_input(bool loading)
4547 static unsigned int loading_views;
4549 if ((loading == FALSE && loading_views-- == 1) ||
4550 (loading == TRUE && loading_views++ == 0))
4551 nodelay(status_win, loading);
4559 /* Initialize the curses library */
4560 if (isatty(STDIN_FILENO)) {
4561 cursed = !!initscr();
4563 /* Leave stdin and stdout alone when acting as a pager. */
4564 FILE *io = fopen("/dev/tty", "r+");
4567 die("Failed to open /dev/tty");
4568 cursed = !!newterm(NULL, io, io);
4572 die("Failed to initialize curses");
4574 nonl(); /* Tell curses not to do NL->CR/NL on output */
4575 cbreak(); /* Take input chars one at a time, no wait for \n */
4576 noecho(); /* Don't echo input */
4577 leaveok(stdscr, TRUE);
4582 getmaxyx(stdscr, y, x);
4583 status_win = newwin(1, 0, y - 1, 0);
4585 die("Failed to create status window");
4587 /* Enable keyboard mapping */
4588 keypad(status_win, TRUE);
4589 wbkgdset(status_win, get_line_attr(LINE_STATUS));
4593 read_prompt(const char *prompt)
4595 enum { READING, STOP, CANCEL } status = READING;
4596 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
4599 while (status == READING) {
4605 foreach_view (view, i)
4610 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
4611 wclrtoeol(status_win);
4613 /* Refresh, accept single keystroke of input */
4614 key = wgetch(status_win);
4619 status = pos ? STOP : CANCEL;
4637 if (pos >= sizeof(buf)) {
4638 report("Input string too long");
4643 buf[pos++] = (char) key;
4647 /* Clear the status window */
4648 status_empty = FALSE;
4651 if (status == CANCEL)
4660 * Repository references
4663 static struct ref *refs;
4664 static size_t refs_size;
4666 /* Id <-> ref store */
4667 static struct ref ***id_refs;
4668 static size_t id_refs_size;
4670 static struct ref **
4673 struct ref ***tmp_id_refs;
4674 struct ref **ref_list = NULL;
4675 size_t ref_list_size = 0;
4678 for (i = 0; i < id_refs_size; i++)
4679 if (!strcmp(id, id_refs[i][0]->id))
4682 tmp_id_refs = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs));
4686 id_refs = tmp_id_refs;
4688 for (i = 0; i < refs_size; i++) {
4691 if (strcmp(id, refs[i].id))
4694 tmp = realloc(ref_list, (ref_list_size + 1) * sizeof(*ref_list));
4702 if (ref_list_size > 0)
4703 ref_list[ref_list_size - 1]->next = 1;
4704 ref_list[ref_list_size] = &refs[i];
4706 /* XXX: The properties of the commit chains ensures that we can
4707 * safely modify the shared ref. The repo references will
4708 * always be similar for the same id. */
4709 ref_list[ref_list_size]->next = 0;
4714 id_refs[id_refs_size++] = ref_list;
4720 read_ref(char *id, size_t idlen, char *name, size_t namelen)
4724 bool remote = FALSE;
4726 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
4727 /* Commits referenced by tags has "^{}" appended. */
4728 if (name[namelen - 1] != '}')
4731 while (namelen > 0 && name[namelen] != '^')
4735 namelen -= STRING_SIZE("refs/tags/");
4736 name += STRING_SIZE("refs/tags/");
4738 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
4740 namelen -= STRING_SIZE("refs/remotes/");
4741 name += STRING_SIZE("refs/remotes/");
4743 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
4744 namelen -= STRING_SIZE("refs/heads/");
4745 name += STRING_SIZE("refs/heads/");
4747 } else if (!strcmp(name, "HEAD")) {
4751 refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
4755 ref = &refs[refs_size++];
4756 ref->name = malloc(namelen + 1);
4760 strncpy(ref->name, name, namelen);
4761 ref->name[namelen] = 0;
4763 ref->remote = remote;
4764 string_copy_rev(ref->id, id);
4772 const char *cmd_env = getenv("TIG_LS_REMOTE");
4773 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
4775 return read_properties(popen(cmd, "r"), "\t", read_ref);
4779 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
4781 if (!strcmp(name, "i18n.commitencoding"))
4782 string_ncopy(opt_encoding, value, valuelen);
4784 if (!strcmp(name, "core.editor"))
4785 string_ncopy(opt_editor, value, valuelen);
4791 load_repo_config(void)
4793 return read_properties(popen(GIT_CONFIG " --list", "r"),
4794 "=", read_repo_config_option);
4798 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
4800 if (!opt_git_dir[0]) {
4801 string_ncopy(opt_git_dir, name, namelen);
4803 } else if (opt_is_inside_work_tree == -1) {
4804 /* This can be 3 different values depending on the
4805 * version of git being used. If git-rev-parse does not
4806 * understand --is-inside-work-tree it will simply echo
4807 * the option else either "true" or "false" is printed.
4808 * Default to true for the unknown case. */
4809 opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
4812 string_ncopy(opt_cdup, name, namelen);
4818 /* XXX: The line outputted by "--show-cdup" can be empty so the option
4819 * must be the last one! */
4821 load_repo_info(void)
4823 return read_properties(popen("git rev-parse --git-dir --is-inside-work-tree --show-cdup 2>/dev/null", "r"),
4824 "=", read_repo_info);
4828 read_properties(FILE *pipe, const char *separators,
4829 int (*read_property)(char *, size_t, char *, size_t))
4831 char buffer[BUFSIZ];
4838 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
4843 name = chomp_string(name);
4844 namelen = strcspn(name, separators);
4846 if (name[namelen]) {
4848 value = chomp_string(name + namelen + 1);
4849 valuelen = strlen(value);
4856 state = read_property(name, namelen, value, valuelen);
4859 if (state != ERR && ferror(pipe))
4872 static void __NORETURN
4875 /* XXX: Restore tty modes and let the OS cleanup the rest! */
4881 static void __NORETURN
4882 die(const char *err, ...)
4888 va_start(args, err);
4889 fputs("tig: ", stderr);
4890 vfprintf(stderr, err, args);
4891 fputs("\n", stderr);
4898 main(int argc, char *argv[])
4901 enum request request;
4904 signal(SIGINT, quit);
4906 if (setlocale(LC_ALL, "")) {
4907 char *codeset = nl_langinfo(CODESET);
4909 string_ncopy(opt_codeset, codeset, strlen(codeset));
4912 if (load_repo_info() == ERR)
4913 die("Failed to load repo info.");
4915 if (load_options() == ERR)
4916 die("Failed to load user config.");
4918 /* Load the repo config file so options can be overwritten from
4919 * the command line. */
4920 if (load_repo_config() == ERR)
4921 die("Failed to load repo config.");
4923 if (!parse_options(argc, argv))
4926 /* Require a git repository unless when running in pager mode. */
4927 if (!opt_git_dir[0])
4928 die("Not a git repository");
4930 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
4931 opt_iconv = iconv_open(opt_codeset, opt_encoding);
4932 if (opt_iconv == ICONV_NONE)
4933 die("Failed to initialize character set conversion");
4936 if (load_refs() == ERR)
4937 die("Failed to load refs.");
4939 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
4940 view->cmd_env = getenv(view->cmd_env);
4942 request = opt_request;
4946 while (view_driver(display[current_view], request)) {
4950 foreach_view (view, i)
4953 /* Refresh, accept single keystroke of input */
4954 key = wgetch(status_win);
4956 /* wgetch() with nodelay() enabled returns ERR when there's no
4963 request = get_keybinding(display[current_view]->keymap, key);
4965 /* Some low-level request handling. This keeps access to
4966 * status_win restricted. */
4970 char *cmd = read_prompt(":");
4972 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
4973 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
4974 opt_request = REQ_VIEW_DIFF;
4976 opt_request = REQ_VIEW_PAGER;
4985 case REQ_SEARCH_BACK:
4987 const char *prompt = request == REQ_SEARCH
4989 char *search = read_prompt(prompt);
4992 string_ncopy(opt_search, search, strlen(search));
4997 case REQ_SCREEN_RESIZE:
5001 getmaxyx(stdscr, height, width);
5003 /* Resize the status view and let the view driver take
5004 * care of resizing the displayed views. */
5005 wresize(status_win, 1, width);
5006 mvwin(status_win, height - 1, 0);
5007 wrefresh(status_win);