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;
1224 /* Nothing more to tokenize or last available token. */
1225 if (!*value || argc >= ARRAY_SIZE(argv))
1229 while (isspace(*value))
1233 if (!strcmp(opt, "color"))
1234 return option_color_command(argc, argv);
1236 if (!strcmp(opt, "set"))
1237 return option_set_command(argc, argv);
1239 if (!strcmp(opt, "bind"))
1240 return option_bind_command(argc, argv);
1242 config_msg = "Unknown option command";
1247 read_option(char *opt, size_t optlen, char *value, size_t valuelen)
1252 config_msg = "Internal error";
1254 /* Check for comment markers, since read_properties() will
1255 * only ensure opt and value are split at first " \t". */
1256 optlen = strcspn(opt, "#");
1260 if (opt[optlen] != 0) {
1261 config_msg = "No option value";
1265 /* Look for comment endings in the value. */
1266 size_t len = strcspn(value, "#");
1268 if (len < valuelen) {
1270 value[valuelen] = 0;
1273 status = set_option(opt, value);
1276 if (status == ERR) {
1277 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1278 config_lineno, (int) optlen, opt, config_msg);
1279 config_errors = TRUE;
1282 /* Always keep going if errors are encountered. */
1289 char *home = getenv("HOME");
1290 char buf[SIZEOF_STR];
1294 config_errors = FALSE;
1296 add_builtin_run_requests();
1298 if (!home || !string_format(buf, "%s/.tigrc", home))
1301 /* It's ok that the file doesn't exist. */
1302 file = fopen(buf, "r");
1306 if (read_properties(file, " \t", read_option) == ERR ||
1307 config_errors == TRUE)
1308 fprintf(stderr, "Errors while loading %s.\n", buf);
1321 /* The display array of active views and the index of the current view. */
1322 static struct view *display[2];
1323 static unsigned int current_view;
1325 /* Reading from the prompt? */
1326 static bool input_mode = FALSE;
1328 #define foreach_displayed_view(view, i) \
1329 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1331 #define displayed_views() (display[1] != NULL ? 2 : 1)
1333 /* Current head and commit ID */
1334 static char ref_blob[SIZEOF_REF] = "";
1335 static char ref_commit[SIZEOF_REF] = "HEAD";
1336 static char ref_head[SIZEOF_REF] = "HEAD";
1339 const char *name; /* View name */
1340 const char *cmd_fmt; /* Default command line format */
1341 const char *cmd_env; /* Command line set via environment */
1342 const char *id; /* Points to either of ref_{head,commit,blob} */
1344 struct view_ops *ops; /* View operations */
1346 enum keymap keymap; /* What keymap does this view have */
1348 char cmd[SIZEOF_STR]; /* Command buffer */
1349 char ref[SIZEOF_REF]; /* Hovered commit reference */
1350 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1352 int height, width; /* The width and height of the main window */
1353 WINDOW *win; /* The main window */
1354 WINDOW *title; /* The title window living below the main window */
1357 unsigned long offset; /* Offset of the window top */
1358 unsigned long lineno; /* Current line number */
1361 char grep[SIZEOF_STR]; /* Search string */
1362 regex_t *regex; /* Pre-compiled regex */
1364 /* If non-NULL, points to the view that opened this view. If this view
1365 * is closed tig will switch back to the parent view. */
1366 struct view *parent;
1369 unsigned long lines; /* Total number of lines */
1370 struct line *line; /* Line index */
1371 unsigned long line_size;/* Total number of allocated lines */
1372 unsigned int digits; /* Number of digits in the lines member. */
1380 /* What type of content being displayed. Used in the title bar. */
1382 /* Open and reads in all view content. */
1383 bool (*open)(struct view *view);
1384 /* Read one line; updates view->line. */
1385 bool (*read)(struct view *view, char *data);
1386 /* Draw one line; @lineno must be < view->height. */
1387 bool (*draw)(struct view *view, struct line *line, unsigned int lineno, bool selected);
1388 /* Depending on view handle a special requests. */
1389 enum request (*request)(struct view *view, enum request request, struct line *line);
1390 /* Search for regex in a line. */
1391 bool (*grep)(struct view *view, struct line *line);
1393 void (*select)(struct view *view, struct line *line);
1396 static struct view_ops pager_ops;
1397 static struct view_ops main_ops;
1398 static struct view_ops tree_ops;
1399 static struct view_ops blob_ops;
1400 static struct view_ops help_ops;
1401 static struct view_ops status_ops;
1402 static struct view_ops stage_ops;
1404 #define VIEW_STR(name, cmd, env, ref, ops, map) \
1405 { name, cmd, #env, ref, ops, map}
1407 #define VIEW_(id, name, ops, ref) \
1408 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id)
1411 static struct view views[] = {
1412 VIEW_(MAIN, "main", &main_ops, ref_head),
1413 VIEW_(DIFF, "diff", &pager_ops, ref_commit),
1414 VIEW_(LOG, "log", &pager_ops, ref_head),
1415 VIEW_(TREE, "tree", &tree_ops, ref_commit),
1416 VIEW_(BLOB, "blob", &blob_ops, ref_blob),
1417 VIEW_(HELP, "help", &help_ops, ""),
1418 VIEW_(PAGER, "pager", &pager_ops, "stdin"),
1419 VIEW_(STATUS, "status", &status_ops, ""),
1420 VIEW_(STAGE, "stage", &stage_ops, ""),
1423 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1425 #define foreach_view(view, i) \
1426 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1428 #define view_is_displayed(view) \
1429 (view == display[0] || view == display[1])
1432 draw_view_line(struct view *view, unsigned int lineno)
1435 bool selected = (view->offset + lineno == view->lineno);
1438 assert(view_is_displayed(view));
1440 if (view->offset + lineno >= view->lines)
1443 line = &view->line[view->offset + lineno];
1446 line->selected = TRUE;
1447 view->ops->select(view, line);
1448 } else if (line->selected) {
1449 line->selected = FALSE;
1450 wmove(view->win, lineno, 0);
1451 wclrtoeol(view->win);
1454 scrollok(view->win, FALSE);
1455 draw_ok = view->ops->draw(view, line, lineno, selected);
1456 scrollok(view->win, TRUE);
1462 redraw_view_from(struct view *view, int lineno)
1464 assert(0 <= lineno && lineno < view->height);
1466 for (; lineno < view->height; lineno++) {
1467 if (!draw_view_line(view, lineno))
1471 redrawwin(view->win);
1473 wnoutrefresh(view->win);
1475 wrefresh(view->win);
1479 redraw_view(struct view *view)
1482 redraw_view_from(view, 0);
1487 update_view_title(struct view *view)
1489 char buf[SIZEOF_STR];
1490 char state[SIZEOF_STR];
1491 size_t bufpos = 0, statelen = 0;
1493 assert(view_is_displayed(view));
1495 if (view != VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) {
1496 unsigned int view_lines = view->offset + view->height;
1497 unsigned int lines = view->lines
1498 ? MIN(view_lines, view->lines) * 100 / view->lines
1501 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1508 time_t secs = time(NULL) - view->start_time;
1510 /* Three git seconds are a long time ... */
1512 string_format_from(state, &statelen, " %lds", secs);
1516 string_format_from(buf, &bufpos, "[%s]", view->name);
1517 if (*view->ref && bufpos < view->width) {
1518 size_t refsize = strlen(view->ref);
1519 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1521 if (minsize < view->width)
1522 refsize = view->width - minsize + 7;
1523 string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
1526 if (statelen && bufpos < view->width) {
1527 string_format_from(buf, &bufpos, " %s", state);
1530 if (view == display[current_view])
1531 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1533 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1535 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1536 wclrtoeol(view->title);
1537 wmove(view->title, 0, view->width - 1);
1540 wnoutrefresh(view->title);
1542 wrefresh(view->title);
1546 resize_display(void)
1549 struct view *base = display[0];
1550 struct view *view = display[1] ? display[1] : display[0];
1552 /* Setup window dimensions */
1554 getmaxyx(stdscr, base->height, base->width);
1556 /* Make room for the status window. */
1560 /* Horizontal split. */
1561 view->width = base->width;
1562 view->height = SCALE_SPLIT_VIEW(base->height);
1563 base->height -= view->height;
1565 /* Make room for the title bar. */
1569 /* Make room for the title bar. */
1574 foreach_displayed_view (view, i) {
1576 view->win = newwin(view->height, 0, offset, 0);
1578 die("Failed to create %s view", view->name);
1580 scrollok(view->win, TRUE);
1582 view->title = newwin(1, 0, offset + view->height, 0);
1584 die("Failed to create title window");
1587 wresize(view->win, view->height, view->width);
1588 mvwin(view->win, offset, 0);
1589 mvwin(view->title, offset + view->height, 0);
1592 offset += view->height + 1;
1597 redraw_display(void)
1602 foreach_displayed_view (view, i) {
1604 update_view_title(view);
1609 update_display_cursor(struct view *view)
1611 /* Move the cursor to the right-most column of the cursor line.
1613 * XXX: This could turn out to be a bit expensive, but it ensures that
1614 * the cursor does not jump around. */
1616 wmove(view->win, view->lineno - view->offset, view->width - 1);
1617 wrefresh(view->win);
1625 /* Scrolling backend */
1627 do_scroll_view(struct view *view, int lines)
1629 bool redraw_current_line = FALSE;
1631 /* The rendering expects the new offset. */
1632 view->offset += lines;
1634 assert(0 <= view->offset && view->offset < view->lines);
1637 /* Move current line into the view. */
1638 if (view->lineno < view->offset) {
1639 view->lineno = view->offset;
1640 redraw_current_line = TRUE;
1641 } else if (view->lineno >= view->offset + view->height) {
1642 view->lineno = view->offset + view->height - 1;
1643 redraw_current_line = TRUE;
1646 assert(view->offset <= view->lineno && view->lineno < view->lines);
1648 /* Redraw the whole screen if scrolling is pointless. */
1649 if (view->height < ABS(lines)) {
1653 int line = lines > 0 ? view->height - lines : 0;
1654 int end = line + ABS(lines);
1656 wscrl(view->win, lines);
1658 for (; line < end; line++) {
1659 if (!draw_view_line(view, line))
1663 if (redraw_current_line)
1664 draw_view_line(view, view->lineno - view->offset);
1667 redrawwin(view->win);
1668 wrefresh(view->win);
1672 /* Scroll frontend */
1674 scroll_view(struct view *view, enum request request)
1678 assert(view_is_displayed(view));
1681 case REQ_SCROLL_PAGE_DOWN:
1682 lines = view->height;
1683 case REQ_SCROLL_LINE_DOWN:
1684 if (view->offset + lines > view->lines)
1685 lines = view->lines - view->offset;
1687 if (lines == 0 || view->offset + view->height >= view->lines) {
1688 report("Cannot scroll beyond the last line");
1693 case REQ_SCROLL_PAGE_UP:
1694 lines = view->height;
1695 case REQ_SCROLL_LINE_UP:
1696 if (lines > view->offset)
1697 lines = view->offset;
1700 report("Cannot scroll beyond the first line");
1708 die("request %d not handled in switch", request);
1711 do_scroll_view(view, lines);
1716 move_view(struct view *view, enum request request)
1718 int scroll_steps = 0;
1722 case REQ_MOVE_FIRST_LINE:
1723 steps = -view->lineno;
1726 case REQ_MOVE_LAST_LINE:
1727 steps = view->lines - view->lineno - 1;
1730 case REQ_MOVE_PAGE_UP:
1731 steps = view->height > view->lineno
1732 ? -view->lineno : -view->height;
1735 case REQ_MOVE_PAGE_DOWN:
1736 steps = view->lineno + view->height >= view->lines
1737 ? view->lines - view->lineno - 1 : view->height;
1749 die("request %d not handled in switch", request);
1752 if (steps <= 0 && view->lineno == 0) {
1753 report("Cannot move beyond the first line");
1756 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1757 report("Cannot move beyond the last line");
1761 /* Move the current line */
1762 view->lineno += steps;
1763 assert(0 <= view->lineno && view->lineno < view->lines);
1765 /* Check whether the view needs to be scrolled */
1766 if (view->lineno < view->offset ||
1767 view->lineno >= view->offset + view->height) {
1768 scroll_steps = steps;
1769 if (steps < 0 && -steps > view->offset) {
1770 scroll_steps = -view->offset;
1772 } else if (steps > 0) {
1773 if (view->lineno == view->lines - 1 &&
1774 view->lines > view->height) {
1775 scroll_steps = view->lines - view->offset - 1;
1776 if (scroll_steps >= view->height)
1777 scroll_steps -= view->height - 1;
1782 if (!view_is_displayed(view)) {
1783 view->offset += scroll_steps;
1784 assert(0 <= view->offset && view->offset < view->lines);
1785 view->ops->select(view, &view->line[view->lineno]);
1789 /* Repaint the old "current" line if we be scrolling */
1790 if (ABS(steps) < view->height)
1791 draw_view_line(view, view->lineno - steps - view->offset);
1794 do_scroll_view(view, scroll_steps);
1798 /* Draw the current line */
1799 draw_view_line(view, view->lineno - view->offset);
1801 redrawwin(view->win);
1802 wrefresh(view->win);
1811 static void search_view(struct view *view, enum request request);
1814 find_next_line(struct view *view, unsigned long lineno, struct line *line)
1816 assert(view_is_displayed(view));
1818 if (!view->ops->grep(view, line))
1821 if (lineno - view->offset >= view->height) {
1822 view->offset = lineno;
1823 view->lineno = lineno;
1827 unsigned long old_lineno = view->lineno - view->offset;
1829 view->lineno = lineno;
1830 draw_view_line(view, old_lineno);
1832 draw_view_line(view, view->lineno - view->offset);
1833 redrawwin(view->win);
1834 wrefresh(view->win);
1837 report("Line %ld matches '%s'", lineno + 1, view->grep);
1842 find_next(struct view *view, enum request request)
1844 unsigned long lineno = view->lineno;
1849 report("No previous search");
1851 search_view(view, request);
1861 case REQ_SEARCH_BACK:
1870 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
1871 lineno += direction;
1873 /* Note, lineno is unsigned long so will wrap around in which case it
1874 * will become bigger than view->lines. */
1875 for (; lineno < view->lines; lineno += direction) {
1876 struct line *line = &view->line[lineno];
1878 if (find_next_line(view, lineno, line))
1882 report("No match found for '%s'", view->grep);
1886 search_view(struct view *view, enum request request)
1891 regfree(view->regex);
1894 view->regex = calloc(1, sizeof(*view->regex));
1899 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
1900 if (regex_err != 0) {
1901 char buf[SIZEOF_STR] = "unknown error";
1903 regerror(regex_err, view->regex, buf, sizeof(buf));
1904 report("Search failed: %s", buf);
1908 string_copy(view->grep, opt_search);
1910 find_next(view, request);
1914 * Incremental updating
1918 end_update(struct view *view)
1922 set_nonblocking_input(FALSE);
1923 if (view->pipe == stdin)
1931 begin_update(struct view *view)
1937 string_copy(view->cmd, opt_cmd);
1939 /* When running random commands, initially show the
1940 * command in the title. However, it maybe later be
1941 * overwritten if a commit line is selected. */
1942 if (view == VIEW(REQ_VIEW_PAGER))
1943 string_copy(view->ref, view->cmd);
1947 } else if (view == VIEW(REQ_VIEW_TREE)) {
1948 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1949 char path[SIZEOF_STR];
1951 if (strcmp(view->vid, view->id))
1952 opt_path[0] = path[0] = 0;
1953 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
1956 if (!string_format(view->cmd, format, view->id, path))
1960 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1961 const char *id = view->id;
1963 if (!string_format(view->cmd, format, id, id, id, id, id))
1966 /* Put the current ref_* value to the view title ref
1967 * member. This is needed by the blob view. Most other
1968 * views sets it automatically after loading because the
1969 * first line is a commit line. */
1970 string_copy_rev(view->ref, view->id);
1973 /* Special case for the pager view. */
1975 view->pipe = opt_pipe;
1978 view->pipe = popen(view->cmd, "r");
1984 set_nonblocking_input(TRUE);
1989 string_copy_rev(view->vid, view->id);
1994 for (i = 0; i < view->lines; i++)
1995 if (view->line[i].data)
1996 free(view->line[i].data);
2002 view->start_time = time(NULL);
2007 static struct line *
2008 realloc_lines(struct view *view, size_t line_size)
2010 struct line *tmp = realloc(view->line, sizeof(*view->line) * line_size);
2016 view->line_size = line_size;
2021 update_view(struct view *view)
2023 char in_buffer[BUFSIZ];
2024 char out_buffer[BUFSIZ * 2];
2026 /* The number of lines to read. If too low it will cause too much
2027 * redrawing (and possible flickering), if too high responsiveness
2029 unsigned long lines = view->height;
2030 int redraw_from = -1;
2035 /* Only redraw if lines are visible. */
2036 if (view->offset + view->height >= view->lines)
2037 redraw_from = view->lines - view->offset;
2039 /* FIXME: This is probably not perfect for backgrounded views. */
2040 if (!realloc_lines(view, view->lines + lines))
2043 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
2044 size_t linelen = strlen(line);
2047 line[linelen - 1] = 0;
2049 if (opt_iconv != ICONV_NONE) {
2050 ICONV_CONST char *inbuf = line;
2051 size_t inlen = linelen;
2053 char *outbuf = out_buffer;
2054 size_t outlen = sizeof(out_buffer);
2058 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
2059 if (ret != (size_t) -1) {
2061 linelen = strlen(out_buffer);
2065 if (!view->ops->read(view, line))
2075 lines = view->lines;
2076 for (digits = 0; lines; digits++)
2079 /* Keep the displayed view in sync with line number scaling. */
2080 if (digits != view->digits) {
2081 view->digits = digits;
2086 if (!view_is_displayed(view))
2089 if (view == VIEW(REQ_VIEW_TREE)) {
2090 /* Clear the view and redraw everything since the tree sorting
2091 * might have rearranged things. */
2094 } else if (redraw_from >= 0) {
2095 /* If this is an incremental update, redraw the previous line
2096 * since for commits some members could have changed when
2097 * loading the main view. */
2098 if (redraw_from > 0)
2101 /* Since revision graph visualization requires knowledge
2102 * about the parent commit, it causes a further one-off
2103 * needed to be redrawn for incremental updates. */
2104 if (redraw_from > 0 && opt_rev_graph)
2107 /* Incrementally draw avoids flickering. */
2108 redraw_view_from(view, redraw_from);
2111 /* Update the title _after_ the redraw so that if the redraw picks up a
2112 * commit reference in view->ref it'll be available here. */
2113 update_view_title(view);
2116 if (ferror(view->pipe)) {
2117 report("Failed to read: %s", strerror(errno));
2120 } else if (feof(view->pipe)) {
2128 report("Allocation failure");
2131 view->ops->read(view, NULL);
2136 static struct line *
2137 add_line_data(struct view *view, void *data, enum line_type type)
2139 struct line *line = &view->line[view->lines++];
2141 memset(line, 0, sizeof(*line));
2148 static struct line *
2149 add_line_text(struct view *view, char *data, enum line_type type)
2152 data = strdup(data);
2154 return data ? add_line_data(view, data, type) : NULL;
2163 OPEN_DEFAULT = 0, /* Use default view switching. */
2164 OPEN_SPLIT = 1, /* Split current view. */
2165 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2166 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2170 open_view(struct view *prev, enum request request, enum open_flags flags)
2172 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2173 bool split = !!(flags & OPEN_SPLIT);
2174 bool reload = !!(flags & OPEN_RELOAD);
2175 struct view *view = VIEW(request);
2176 int nviews = displayed_views();
2177 struct view *base_view = display[0];
2179 if (view == prev && nviews == 1 && !reload) {
2180 report("Already in %s view", view->name);
2184 if (view->ops->open) {
2185 if (!view->ops->open(view)) {
2186 report("Failed to load %s view", view->name);
2190 } else if ((reload || strcmp(view->vid, view->id)) &&
2191 !begin_update(view)) {
2192 report("Failed to load %s view", view->name);
2201 /* Maximize the current view. */
2202 memset(display, 0, sizeof(display));
2204 display[current_view] = view;
2207 /* Resize the view when switching between split- and full-screen,
2208 * or when switching between two different full-screen views. */
2209 if (nviews != displayed_views() ||
2210 (nviews == 1 && base_view != display[0]))
2213 if (split && prev->lineno - prev->offset >= prev->height) {
2214 /* Take the title line into account. */
2215 int lines = prev->lineno - prev->offset - prev->height + 1;
2217 /* Scroll the view that was split if the current line is
2218 * outside the new limited view. */
2219 do_scroll_view(prev, lines);
2222 if (prev && view != prev) {
2223 if (split && !backgrounded) {
2224 /* "Blur" the previous view. */
2225 update_view_title(prev);
2228 view->parent = prev;
2231 if (view->pipe && view->lines == 0) {
2232 /* Clear the old view and let the incremental updating refill
2241 /* If the view is backgrounded the above calls to report()
2242 * won't redraw the view title. */
2244 update_view_title(view);
2248 open_external_viewer(const char *cmd)
2250 def_prog_mode(); /* save current tty modes */
2251 endwin(); /* restore original tty modes */
2253 fprintf(stderr, "Press Enter to continue");
2260 open_mergetool(const char *file)
2262 char cmd[SIZEOF_STR];
2263 char file_sq[SIZEOF_STR];
2265 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2266 string_format(cmd, "git mergetool %s", file_sq)) {
2267 open_external_viewer(cmd);
2272 open_editor(bool from_root, const char *file)
2274 char cmd[SIZEOF_STR];
2275 char file_sq[SIZEOF_STR];
2277 char *prefix = from_root ? opt_cdup : "";
2279 editor = getenv("GIT_EDITOR");
2280 if (!editor && *opt_editor)
2281 editor = opt_editor;
2283 editor = getenv("VISUAL");
2285 editor = getenv("EDITOR");
2289 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2290 string_format(cmd, "%s %s%s", editor, prefix, file_sq)) {
2291 open_external_viewer(cmd);
2296 open_run_request(enum request request)
2298 struct run_request *req = get_run_request(request);
2299 char buf[SIZEOF_STR * 2];
2304 report("Unknown run request");
2312 char *next = strstr(cmd, "%(");
2313 int len = next - cmd;
2320 } else if (!strncmp(next, "%(head)", 7)) {
2323 } else if (!strncmp(next, "%(commit)", 9)) {
2326 } else if (!strncmp(next, "%(blob)", 7)) {
2330 report("Unknown replacement in run request: `%s`", req->cmd);
2334 if (!string_format_from(buf, &bufpos, "%.*s%s", len, cmd, value))
2338 next = strchr(next, ')') + 1;
2342 open_external_viewer(buf);
2346 * User request switch noodle
2350 view_driver(struct view *view, enum request request)
2354 if (request == REQ_NONE) {
2359 if (request > REQ_NONE) {
2360 open_run_request(request);
2364 if (view && view->lines) {
2365 request = view->ops->request(view, request, &view->line[view->lineno]);
2366 if (request == REQ_NONE)
2373 case REQ_MOVE_PAGE_UP:
2374 case REQ_MOVE_PAGE_DOWN:
2375 case REQ_MOVE_FIRST_LINE:
2376 case REQ_MOVE_LAST_LINE:
2377 move_view(view, request);
2380 case REQ_SCROLL_LINE_DOWN:
2381 case REQ_SCROLL_LINE_UP:
2382 case REQ_SCROLL_PAGE_DOWN:
2383 case REQ_SCROLL_PAGE_UP:
2384 scroll_view(view, request);
2389 report("No file chosen, press %s to open tree view",
2390 get_key(REQ_VIEW_TREE));
2393 open_view(view, request, OPEN_DEFAULT);
2396 case REQ_VIEW_PAGER:
2397 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2398 report("No pager content, press %s to run command from prompt",
2399 get_key(REQ_PROMPT));
2402 open_view(view, request, OPEN_DEFAULT);
2405 case REQ_VIEW_STAGE:
2406 if (!VIEW(REQ_VIEW_STAGE)->lines) {
2407 report("No stage content, press %s to open the status view and choose file",
2408 get_key(REQ_VIEW_STATUS));
2411 open_view(view, request, OPEN_DEFAULT);
2414 case REQ_VIEW_STATUS:
2415 if (opt_is_inside_work_tree == FALSE) {
2416 report("The status view requires a working tree");
2419 open_view(view, request, OPEN_DEFAULT);
2427 open_view(view, request, OPEN_DEFAULT);
2432 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2434 if ((view == VIEW(REQ_VIEW_DIFF) &&
2435 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2436 (view == VIEW(REQ_VIEW_STAGE) &&
2437 view->parent == VIEW(REQ_VIEW_STATUS)) ||
2438 (view == VIEW(REQ_VIEW_BLOB) &&
2439 view->parent == VIEW(REQ_VIEW_TREE))) {
2442 view = view->parent;
2443 line = view->lineno;
2444 move_view(view, request);
2445 if (view_is_displayed(view))
2446 update_view_title(view);
2447 if (line != view->lineno)
2448 view->ops->request(view, REQ_ENTER,
2449 &view->line[view->lineno]);
2452 move_view(view, request);
2458 int nviews = displayed_views();
2459 int next_view = (current_view + 1) % nviews;
2461 if (next_view == current_view) {
2462 report("Only one view is displayed");
2466 current_view = next_view;
2467 /* Blur out the title of the previous view. */
2468 update_view_title(view);
2473 report("Refreshing is not yet supported for the %s view", view->name);
2476 case REQ_TOGGLE_LINENO:
2477 opt_line_number = !opt_line_number;
2481 case REQ_TOGGLE_REV_GRAPH:
2482 opt_rev_graph = !opt_rev_graph;
2487 /* Always reload^Wrerun commands from the prompt. */
2488 open_view(view, opt_request, OPEN_RELOAD);
2492 case REQ_SEARCH_BACK:
2493 search_view(view, request);
2498 find_next(view, request);
2501 case REQ_STOP_LOADING:
2502 for (i = 0; i < ARRAY_SIZE(views); i++) {
2505 report("Stopped loading the %s view", view->name),
2510 case REQ_SHOW_VERSION:
2511 report("tig-%s (built %s)", TIG_VERSION, __DATE__);
2514 case REQ_SCREEN_RESIZE:
2517 case REQ_SCREEN_REDRAW:
2522 report("Nothing to edit");
2527 report("Nothing to enter");
2531 case REQ_VIEW_CLOSE:
2532 /* XXX: Mark closed views by letting view->parent point to the
2533 * view itself. Parents to closed view should never be
2536 view->parent->parent != view->parent) {
2537 memset(display, 0, sizeof(display));
2539 display[current_view] = view->parent;
2540 view->parent = view;
2550 /* An unknown key will show most commonly used commands. */
2551 report("Unknown key, press 'h' for help");
2564 pager_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2566 char *text = line->data;
2567 enum line_type type = line->type;
2568 int textlen = strlen(text);
2571 wmove(view->win, lineno, 0);
2575 wchgat(view->win, -1, 0, type, NULL);
2578 attr = get_line_attr(type);
2579 wattrset(view->win, attr);
2581 if (opt_line_number || opt_tab_size < TABSIZE) {
2582 static char spaces[] = " ";
2583 int col_offset = 0, col = 0;
2585 if (opt_line_number) {
2586 unsigned long real_lineno = view->offset + lineno + 1;
2588 if (real_lineno == 1 ||
2589 (real_lineno % opt_num_interval) == 0) {
2590 wprintw(view->win, "%.*d", view->digits, real_lineno);
2593 waddnstr(view->win, spaces,
2594 MIN(view->digits, STRING_SIZE(spaces)));
2596 waddstr(view->win, ": ");
2597 col_offset = view->digits + 2;
2600 while (text && col_offset + col < view->width) {
2601 int cols_max = view->width - col_offset - col;
2605 if (*text == '\t') {
2607 assert(sizeof(spaces) > TABSIZE);
2609 cols = opt_tab_size - (col % opt_tab_size);
2612 text = strchr(text, '\t');
2613 cols = line ? text - pos : strlen(pos);
2616 waddnstr(view->win, pos, MIN(cols, cols_max));
2621 int col = 0, pos = 0;
2623 for (; pos < textlen && col < view->width; pos++, col++)
2624 if (text[pos] == '\t')
2625 col += TABSIZE - (col % TABSIZE) - 1;
2627 waddnstr(view->win, text, pos);
2634 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2636 char refbuf[SIZEOF_STR];
2640 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2643 pipe = popen(refbuf, "r");
2647 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2648 ref = chomp_string(ref);
2654 /* This is the only fatal call, since it can "corrupt" the buffer. */
2655 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2662 add_pager_refs(struct view *view, struct line *line)
2664 char buf[SIZEOF_STR];
2665 char *commit_id = line->data + STRING_SIZE("commit ");
2667 size_t bufpos = 0, refpos = 0;
2668 const char *sep = "Refs: ";
2669 bool is_tag = FALSE;
2671 assert(line->type == LINE_COMMIT);
2673 refs = get_refs(commit_id);
2675 if (view == VIEW(REQ_VIEW_DIFF))
2676 goto try_add_describe_ref;
2681 struct ref *ref = refs[refpos];
2682 char *fmt = ref->tag ? "%s[%s]" :
2683 ref->remote ? "%s<%s>" : "%s%s";
2685 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2690 } while (refs[refpos++]->next);
2692 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2693 try_add_describe_ref:
2694 /* Add <tag>-g<commit_id> "fake" reference. */
2695 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2702 if (!realloc_lines(view, view->line_size + 1))
2705 add_line_text(view, buf, LINE_PP_REFS);
2709 pager_read(struct view *view, char *data)
2716 line = add_line_text(view, data, get_line_type(data));
2720 if (line->type == LINE_COMMIT &&
2721 (view == VIEW(REQ_VIEW_DIFF) ||
2722 view == VIEW(REQ_VIEW_LOG)))
2723 add_pager_refs(view, line);
2729 pager_request(struct view *view, enum request request, struct line *line)
2733 if (request != REQ_ENTER)
2736 if (line->type == LINE_COMMIT &&
2737 (view == VIEW(REQ_VIEW_LOG) ||
2738 view == VIEW(REQ_VIEW_PAGER))) {
2739 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2743 /* Always scroll the view even if it was split. That way
2744 * you can use Enter to scroll through the log view and
2745 * split open each commit diff. */
2746 scroll_view(view, REQ_SCROLL_LINE_DOWN);
2748 /* FIXME: A minor workaround. Scrolling the view will call report("")
2749 * but if we are scrolling a non-current view this won't properly
2750 * update the view title. */
2752 update_view_title(view);
2758 pager_grep(struct view *view, struct line *line)
2761 char *text = line->data;
2766 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
2773 pager_select(struct view *view, struct line *line)
2775 if (line->type == LINE_COMMIT) {
2776 char *text = line->data + STRING_SIZE("commit ");
2778 if (view != VIEW(REQ_VIEW_PAGER))
2779 string_copy_rev(view->ref, text);
2780 string_copy_rev(ref_commit, text);
2784 static struct view_ops pager_ops = {
2800 help_open(struct view *view)
2803 int lines = ARRAY_SIZE(req_info) + 2;
2806 if (view->lines > 0)
2809 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2810 if (!req_info[i].request)
2813 lines += run_requests + 1;
2815 view->line = calloc(lines, sizeof(*view->line));
2819 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
2821 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2824 if (req_info[i].request == REQ_NONE)
2827 if (!req_info[i].request) {
2828 add_line_text(view, "", LINE_DEFAULT);
2829 add_line_text(view, req_info[i].help, LINE_DEFAULT);
2833 key = get_key(req_info[i].request);
2835 key = "(no key defined)";
2837 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
2840 add_line_text(view, buf, LINE_DEFAULT);
2844 add_line_text(view, "", LINE_DEFAULT);
2845 add_line_text(view, "External commands:", LINE_DEFAULT);
2848 for (i = 0; i < run_requests; i++) {
2849 struct run_request *req = get_run_request(REQ_NONE + i + 1);
2855 key = get_key_name(req->key);
2857 key = "(no key defined)";
2859 if (!string_format(buf, " %-10s %-14s `%s`",
2860 keymap_table[req->keymap].name,
2864 add_line_text(view, buf, LINE_DEFAULT);
2870 static struct view_ops help_ops = {
2885 struct tree_stack_entry {
2886 struct tree_stack_entry *prev; /* Entry below this in the stack */
2887 unsigned long lineno; /* Line number to restore */
2888 char *name; /* Position of name in opt_path */
2891 /* The top of the path stack. */
2892 static struct tree_stack_entry *tree_stack = NULL;
2893 unsigned long tree_lineno = 0;
2896 pop_tree_stack_entry(void)
2898 struct tree_stack_entry *entry = tree_stack;
2900 tree_lineno = entry->lineno;
2902 tree_stack = entry->prev;
2907 push_tree_stack_entry(char *name, unsigned long lineno)
2909 struct tree_stack_entry *entry = calloc(1, sizeof(*entry));
2910 size_t pathlen = strlen(opt_path);
2915 entry->prev = tree_stack;
2916 entry->name = opt_path + pathlen;
2919 if (!string_format_from(opt_path, &pathlen, "%s/", name)) {
2920 pop_tree_stack_entry();
2924 /* Move the current line to the first tree entry. */
2926 entry->lineno = lineno;
2929 /* Parse output from git-ls-tree(1):
2931 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
2932 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
2933 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
2934 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
2937 #define SIZEOF_TREE_ATTR \
2938 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
2940 #define TREE_UP_FORMAT "040000 tree %s\t.."
2943 tree_compare_entry(enum line_type type1, char *name1,
2944 enum line_type type2, char *name2)
2946 if (type1 != type2) {
2947 if (type1 == LINE_TREE_DIR)
2952 return strcmp(name1, name2);
2956 tree_read(struct view *view, char *text)
2958 size_t textlen = text ? strlen(text) : 0;
2959 char buf[SIZEOF_STR];
2961 enum line_type type;
2962 bool first_read = view->lines == 0;
2964 if (textlen <= SIZEOF_TREE_ATTR)
2967 type = text[STRING_SIZE("100644 ")] == 't'
2968 ? LINE_TREE_DIR : LINE_TREE_FILE;
2971 /* Add path info line */
2972 if (!string_format(buf, "Directory path /%s", opt_path) ||
2973 !realloc_lines(view, view->line_size + 1) ||
2974 !add_line_text(view, buf, LINE_DEFAULT))
2977 /* Insert "link" to parent directory. */
2979 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
2980 !realloc_lines(view, view->line_size + 1) ||
2981 !add_line_text(view, buf, LINE_TREE_DIR))
2986 /* Strip the path part ... */
2988 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
2989 size_t striplen = strlen(opt_path);
2990 char *path = text + SIZEOF_TREE_ATTR;
2992 if (pathlen > striplen)
2993 memmove(path, path + striplen,
2994 pathlen - striplen + 1);
2997 /* Skip "Directory ..." and ".." line. */
2998 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
2999 struct line *line = &view->line[pos];
3000 char *path1 = ((char *) line->data) + SIZEOF_TREE_ATTR;
3001 char *path2 = text + SIZEOF_TREE_ATTR;
3002 int cmp = tree_compare_entry(line->type, path1, type, path2);
3007 text = strdup(text);
3011 if (view->lines > pos)
3012 memmove(&view->line[pos + 1], &view->line[pos],
3013 (view->lines - pos) * sizeof(*line));
3015 line = &view->line[pos];
3022 if (!add_line_text(view, text, type))
3025 if (tree_lineno > view->lineno) {
3026 view->lineno = tree_lineno;
3034 tree_request(struct view *view, enum request request, struct line *line)
3036 enum open_flags flags;
3038 if (request != REQ_ENTER)
3041 /* Cleanup the stack if the tree view is at a different tree. */
3042 while (!*opt_path && tree_stack)
3043 pop_tree_stack_entry();
3045 switch (line->type) {
3047 /* Depending on whether it is a subdir or parent (updir?) link
3048 * mangle the path buffer. */
3049 if (line == &view->line[1] && *opt_path) {
3050 pop_tree_stack_entry();
3053 char *data = line->data;
3054 char *basename = data + SIZEOF_TREE_ATTR;
3056 push_tree_stack_entry(basename, view->lineno);
3059 /* Trees and subtrees share the same ID, so they are not not
3060 * unique like blobs. */
3061 flags = OPEN_RELOAD;
3062 request = REQ_VIEW_TREE;
3065 case LINE_TREE_FILE:
3066 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3067 request = REQ_VIEW_BLOB;
3074 open_view(view, request, flags);
3075 if (request == REQ_VIEW_TREE) {
3076 view->lineno = tree_lineno;
3083 tree_select(struct view *view, struct line *line)
3085 char *text = line->data + STRING_SIZE("100644 blob ");
3087 if (line->type == LINE_TREE_FILE) {
3088 string_copy_rev(ref_blob, text);
3090 } else if (line->type != LINE_TREE_DIR) {
3094 string_copy_rev(view->ref, text);
3097 static struct view_ops tree_ops = {
3108 blob_read(struct view *view, char *line)
3110 return add_line_text(view, line, LINE_DEFAULT) != NULL;
3113 static struct view_ops blob_ops = {
3132 char rev[SIZEOF_REV];
3136 char rev[SIZEOF_REV];
3138 char name[SIZEOF_STR];
3141 static struct status stage_status;
3142 static enum line_type stage_line_type;
3144 /* Get fields from the diff line:
3145 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
3148 status_get_diff(struct status *file, char *buf, size_t bufsize)
3150 char *old_mode = buf + 1;
3151 char *new_mode = buf + 8;
3152 char *old_rev = buf + 15;
3153 char *new_rev = buf + 56;
3154 char *status = buf + 97;
3156 if (bufsize != 99 ||
3157 old_mode[-1] != ':' ||
3158 new_mode[-1] != ' ' ||
3159 old_rev[-1] != ' ' ||
3160 new_rev[-1] != ' ' ||
3164 file->status = *status;
3166 string_copy_rev(file->old.rev, old_rev);
3167 string_copy_rev(file->new.rev, new_rev);
3169 file->old.mode = strtoul(old_mode, NULL, 8);
3170 file->new.mode = strtoul(new_mode, NULL, 8);
3178 status_run(struct view *view, const char cmd[], bool diff, enum line_type type)
3180 struct status *file = NULL;
3181 struct status *unmerged = NULL;
3182 char buf[SIZEOF_STR * 4];
3186 pipe = popen(cmd, "r");
3190 add_line_data(view, NULL, type);
3192 while (!feof(pipe) && !ferror(pipe)) {
3196 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
3199 bufsize += readsize;
3201 /* Process while we have NUL chars. */
3202 while ((sep = memchr(buf, 0, bufsize))) {
3203 size_t sepsize = sep - buf + 1;
3206 if (!realloc_lines(view, view->line_size + 1))
3209 file = calloc(1, sizeof(*file));
3213 add_line_data(view, file, type);
3216 /* Parse diff info part. */
3220 } else if (!file->status) {
3221 if (!status_get_diff(file, buf, sepsize))
3225 memmove(buf, sep + 1, bufsize);
3227 sep = memchr(buf, 0, bufsize);
3230 sepsize = sep - buf + 1;
3232 /* Collapse all 'M'odified entries that
3233 * follow a associated 'U'nmerged entry.
3235 if (file->status == 'U') {
3238 } else if (unmerged) {
3239 int collapse = !strcmp(buf, unmerged->name);
3250 /* git-ls-files just delivers a NUL separated
3251 * list of file names similar to the second half
3252 * of the git-diff-* output. */
3253 string_ncopy(file->name, buf, sepsize);
3255 memmove(buf, sep + 1, bufsize);
3266 if (!view->line[view->lines - 1].data)
3267 add_line_data(view, NULL, LINE_STAT_NONE);
3273 /* Don't show unmerged entries in the staged section. */
3274 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached HEAD"
3275 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
3276 #define STATUS_LIST_OTHER_CMD \
3277 "git ls-files -z --others --exclude-per-directory=.gitignore"
3279 #define STATUS_DIFF_SHOW_CMD \
3280 "git diff --root --patch-with-stat --find-copies-harder -B -C %s -- %s 2>/dev/null"
3282 /* First parse staged info using git-diff-index(1), then parse unstaged
3283 * info using git-diff-files(1), and finally untracked files using
3284 * git-ls-files(1). */
3286 status_open(struct view *view)
3288 struct stat statbuf;
3289 char exclude[SIZEOF_STR];
3290 char cmd[SIZEOF_STR];
3291 unsigned long prev_lineno = view->lineno;
3294 for (i = 0; i < view->lines; i++)
3295 free(view->line[i].data);
3297 view->lines = view->line_size = view->lineno = 0;
3300 if (!realloc_lines(view, view->line_size + 6))
3303 if (!string_format(exclude, "%s/info/exclude", opt_git_dir))
3306 string_copy(cmd, STATUS_LIST_OTHER_CMD);
3308 if (stat(exclude, &statbuf) >= 0) {
3309 size_t cmdsize = strlen(cmd);
3311 if (!string_format_from(cmd, &cmdsize, " %s", "--exclude-from=") ||
3312 sq_quote(cmd, cmdsize, exclude) >= sizeof(cmd))
3316 if (!status_run(view, STATUS_DIFF_INDEX_CMD, TRUE, LINE_STAT_STAGED) ||
3317 !status_run(view, STATUS_DIFF_FILES_CMD, TRUE, LINE_STAT_UNSTAGED) ||
3318 !status_run(view, cmd, FALSE, LINE_STAT_UNTRACKED))
3321 /* If all went well restore the previous line number to stay in
3323 if (prev_lineno < view->lines)
3324 view->lineno = prev_lineno;
3326 view->lineno = view->lines - 1;
3332 status_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
3334 struct status *status = line->data;
3336 wmove(view->win, lineno, 0);
3339 wattrset(view->win, get_line_attr(LINE_CURSOR));
3340 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
3342 } else if (!status && line->type != LINE_STAT_NONE) {
3343 wattrset(view->win, get_line_attr(LINE_STAT_SECTION));
3344 wchgat(view->win, -1, 0, LINE_STAT_SECTION, NULL);
3347 wattrset(view->win, get_line_attr(line->type));
3353 switch (line->type) {
3354 case LINE_STAT_STAGED:
3355 text = "Changes to be committed:";
3358 case LINE_STAT_UNSTAGED:
3359 text = "Changed but not updated:";
3362 case LINE_STAT_UNTRACKED:
3363 text = "Untracked files:";
3366 case LINE_STAT_NONE:
3367 text = " (no files)";
3374 waddstr(view->win, text);
3378 waddch(view->win, status->status);
3380 wattrset(view->win, A_NORMAL);
3381 wmove(view->win, lineno, 4);
3382 waddstr(view->win, status->name);
3388 status_enter(struct view *view, struct line *line)
3390 struct status *status = line->data;
3391 char path[SIZEOF_STR] = "";
3395 if (line->type == LINE_STAT_NONE ||
3396 (!status && line[1].type == LINE_STAT_NONE)) {
3397 report("No file to diff");
3401 if (status && sq_quote(path, 0, status->name) >= sizeof(path))
3405 line->type != LINE_STAT_UNTRACKED &&
3406 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
3409 switch (line->type) {
3410 case LINE_STAT_STAGED:
3411 if (!string_format_from(opt_cmd, &cmdsize, STATUS_DIFF_SHOW_CMD,
3415 info = "Staged changes to %s";
3417 info = "Staged changes";
3420 case LINE_STAT_UNSTAGED:
3421 if (!string_format_from(opt_cmd, &cmdsize, STATUS_DIFF_SHOW_CMD,
3425 info = "Unstaged changes to %s";
3427 info = "Unstaged changes";
3430 case LINE_STAT_UNTRACKED:
3436 report("No file to show");
3440 opt_pipe = fopen(status->name, "r");
3441 info = "Untracked file %s";
3445 die("line type %d not handled in switch", line->type);
3448 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | OPEN_SPLIT);
3449 if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) {
3451 stage_status = *status;
3453 memset(&stage_status, 0, sizeof(stage_status));
3456 stage_line_type = line->type;
3457 string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.name);
3465 status_update_file(struct view *view, struct status *status, enum line_type type)
3467 char cmd[SIZEOF_STR];
3468 char buf[SIZEOF_STR];
3475 type != LINE_STAT_UNTRACKED &&
3476 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
3480 case LINE_STAT_STAGED:
3481 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
3487 string_add(cmd, cmdsize, "git update-index -z --index-info");
3490 case LINE_STAT_UNSTAGED:
3491 case LINE_STAT_UNTRACKED:
3492 if (!string_format_from(buf, &bufsize, "%s%c", status->name, 0))
3495 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
3499 die("line type %d not handled in switch", type);
3502 pipe = popen(cmd, "w");
3506 while (!ferror(pipe) && written < bufsize) {
3507 written += fwrite(buf + written, 1, bufsize - written, pipe);
3512 if (written != bufsize)
3519 status_update(struct view *view)
3521 struct line *line = &view->line[view->lineno];
3523 assert(view->lines);
3526 while (++line < view->line + view->lines && line->data) {
3527 if (!status_update_file(view, line->data, line->type))
3528 report("Failed to update file status");
3531 if (!line[-1].data) {
3532 report("Nothing to update");
3536 } else if (!status_update_file(view, line->data, line->type)) {
3537 report("Failed to update file status");
3542 status_request(struct view *view, enum request request, struct line *line)
3544 struct status *status = line->data;
3547 case REQ_STATUS_UPDATE:
3548 status_update(view);
3551 case REQ_STATUS_MERGE:
3552 open_mergetool(status->name);
3559 open_editor(status->status != '?', status->name);
3563 /* After returning the status view has been split to
3564 * show the stage view. No further reloading is
3566 status_enter(view, line);
3570 /* Simply reload the view. */
3577 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
3583 status_select(struct view *view, struct line *line)
3585 struct status *status = line->data;
3586 char file[SIZEOF_STR] = "all files";
3590 if (status && !string_format(file, "'%s'", status->name))
3593 if (!status && line[1].type == LINE_STAT_NONE)
3596 switch (line->type) {
3597 case LINE_STAT_STAGED:
3598 text = "Press %s to unstage %s for commit";
3601 case LINE_STAT_UNSTAGED:
3602 text = "Press %s to stage %s for commit";
3605 case LINE_STAT_UNTRACKED:
3606 text = "Press %s to stage %s for addition";
3609 case LINE_STAT_NONE:
3610 text = "Nothing to update";
3614 die("line type %d not handled in switch", line->type);
3617 if (status && status->status == 'U') {
3618 text = "Press %s to resolve conflict in %s";
3619 key = get_key(REQ_STATUS_MERGE);
3622 key = get_key(REQ_STATUS_UPDATE);
3625 string_format(view->ref, text, key, file);
3629 status_grep(struct view *view, struct line *line)
3631 struct status *status = line->data;
3632 enum { S_STATUS, S_NAME, S_END } state;
3639 for (state = S_STATUS; state < S_END; state++) {
3643 case S_NAME: text = status->name; break;
3645 buf[0] = status->status;
3653 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3660 static struct view_ops status_ops = {
3672 stage_diff_line(FILE *pipe, struct line *line)
3674 char *buf = line->data;
3675 size_t bufsize = strlen(buf);
3678 while (!ferror(pipe) && written < bufsize) {
3679 written += fwrite(buf + written, 1, bufsize - written, pipe);
3684 return written == bufsize;
3687 static struct line *
3688 stage_diff_hdr(struct view *view, struct line *line)
3690 int diff_hdr_dir = line->type == LINE_DIFF_CHUNK ? -1 : 1;
3691 struct line *diff_hdr;
3693 if (line->type == LINE_DIFF_CHUNK)
3694 diff_hdr = line - 1;
3696 diff_hdr = view->line + 1;
3698 while (diff_hdr > view->line && diff_hdr < view->line + view->lines) {
3699 if (diff_hdr->type == LINE_DIFF_HEADER)
3702 diff_hdr += diff_hdr_dir;
3709 stage_update_chunk(struct view *view, struct line *line)
3711 char cmd[SIZEOF_STR];
3713 struct line *diff_hdr, *diff_chunk, *diff_end;
3716 diff_hdr = stage_diff_hdr(view, line);
3721 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
3724 if (!string_format_from(cmd, &cmdsize,
3725 "git apply --cached %s - && "
3726 "git update-index -q --unmerged --refresh 2>/dev/null",
3727 stage_line_type == LINE_STAT_STAGED ? "-R" : ""))
3730 pipe = popen(cmd, "w");
3734 diff_end = view->line + view->lines;
3735 if (line->type != LINE_DIFF_CHUNK) {
3736 diff_chunk = diff_hdr;
3739 for (diff_chunk = line + 1; diff_chunk < diff_end; diff_chunk++)
3740 if (diff_chunk->type == LINE_DIFF_CHUNK ||
3741 diff_chunk->type == LINE_DIFF_HEADER)
3742 diff_end = diff_chunk;
3746 while (diff_hdr->type != LINE_DIFF_CHUNK) {
3747 switch (diff_hdr->type) {
3748 case LINE_DIFF_HEADER:
3749 case LINE_DIFF_INDEX:
3759 if (!stage_diff_line(pipe, diff_hdr++)) {
3766 while (diff_chunk < diff_end && stage_diff_line(pipe, diff_chunk))
3771 if (diff_chunk != diff_end)
3778 stage_update(struct view *view, struct line *line)
3780 if (stage_line_type != LINE_STAT_UNTRACKED &&
3781 (line->type == LINE_DIFF_CHUNK || !stage_status.status)) {
3782 if (!stage_update_chunk(view, line)) {
3783 report("Failed to apply chunk");
3787 } else if (!status_update_file(view, &stage_status, stage_line_type)) {
3788 report("Failed to update file");
3792 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
3794 view = VIEW(REQ_VIEW_STATUS);
3795 if (view_is_displayed(view))
3796 status_enter(view, &view->line[view->lineno]);
3800 stage_request(struct view *view, enum request request, struct line *line)
3803 case REQ_STATUS_UPDATE:
3804 stage_update(view, line);
3808 if (!stage_status.name[0])
3811 open_editor(stage_status.status != '?', stage_status.name);
3815 pager_request(view, request, line);
3825 static struct view_ops stage_ops = {
3841 char id[SIZEOF_REV]; /* SHA1 ID. */
3842 char title[128]; /* First line of the commit message. */
3843 char author[75]; /* Author of the commit. */
3844 struct tm time; /* Date from the author ident. */
3845 struct ref **refs; /* Repository references. */
3846 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
3847 size_t graph_size; /* The width of the graph array. */
3850 /* Size of rev graph with no "padding" columns */
3851 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
3854 struct rev_graph *prev, *next, *parents;
3855 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
3857 struct commit *commit;
3861 /* Parents of the commit being visualized. */
3862 static struct rev_graph graph_parents[4];
3864 /* The current stack of revisions on the graph. */
3865 static struct rev_graph graph_stacks[4] = {
3866 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
3867 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
3868 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
3869 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
3873 graph_parent_is_merge(struct rev_graph *graph)
3875 return graph->parents->size > 1;
3879 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
3881 struct commit *commit = graph->commit;
3883 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
3884 commit->graph[commit->graph_size++] = symbol;
3888 done_rev_graph(struct rev_graph *graph)
3890 if (graph_parent_is_merge(graph) &&
3891 graph->pos < graph->size - 1 &&
3892 graph->next->size == graph->size + graph->parents->size - 1) {
3893 size_t i = graph->pos + graph->parents->size - 1;
3895 graph->commit->graph_size = i * 2;
3896 while (i < graph->next->size - 1) {
3897 append_to_rev_graph(graph, ' ');
3898 append_to_rev_graph(graph, '\\');
3903 graph->size = graph->pos = 0;
3904 graph->commit = NULL;
3905 memset(graph->parents, 0, sizeof(*graph->parents));
3909 push_rev_graph(struct rev_graph *graph, char *parent)
3913 /* "Collapse" duplicate parents lines.
3915 * FIXME: This needs to also update update the drawn graph but
3916 * for now it just serves as a method for pruning graph lines. */
3917 for (i = 0; i < graph->size; i++)
3918 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
3921 if (graph->size < SIZEOF_REVITEMS) {
3922 string_copy_rev(graph->rev[graph->size++], parent);
3927 get_rev_graph_symbol(struct rev_graph *graph)
3931 if (graph->parents->size == 0)
3932 symbol = REVGRAPH_INIT;
3933 else if (graph_parent_is_merge(graph))
3934 symbol = REVGRAPH_MERGE;
3935 else if (graph->pos >= graph->size)
3936 symbol = REVGRAPH_BRANCH;
3938 symbol = REVGRAPH_COMMIT;
3944 draw_rev_graph(struct rev_graph *graph)
3947 chtype separator, line;
3949 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
3950 static struct rev_filler fillers[] = {
3951 { ' ', REVGRAPH_LINE },
3956 chtype symbol = get_rev_graph_symbol(graph);
3957 struct rev_filler *filler;
3960 filler = &fillers[DEFAULT];
3962 for (i = 0; i < graph->pos; i++) {
3963 append_to_rev_graph(graph, filler->line);
3964 if (graph_parent_is_merge(graph->prev) &&
3965 graph->prev->pos == i)
3966 filler = &fillers[RSHARP];
3968 append_to_rev_graph(graph, filler->separator);
3971 /* Place the symbol for this revision. */
3972 append_to_rev_graph(graph, symbol);
3974 if (graph->prev->size > graph->size)
3975 filler = &fillers[RDIAG];
3977 filler = &fillers[DEFAULT];
3981 for (; i < graph->size; i++) {
3982 append_to_rev_graph(graph, filler->separator);
3983 append_to_rev_graph(graph, filler->line);
3984 if (graph_parent_is_merge(graph->prev) &&
3985 i < graph->prev->pos + graph->parents->size)
3986 filler = &fillers[RSHARP];
3987 if (graph->prev->size > graph->size)
3988 filler = &fillers[LDIAG];
3991 if (graph->prev->size > graph->size) {
3992 append_to_rev_graph(graph, filler->separator);
3993 if (filler->line != ' ')
3994 append_to_rev_graph(graph, filler->line);
3998 /* Prepare the next rev graph */
4000 prepare_rev_graph(struct rev_graph *graph)
4004 /* First, traverse all lines of revisions up to the active one. */
4005 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
4006 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
4009 push_rev_graph(graph->next, graph->rev[graph->pos]);
4012 /* Interleave the new revision parent(s). */
4013 for (i = 0; i < graph->parents->size; i++)
4014 push_rev_graph(graph->next, graph->parents->rev[i]);
4016 /* Lastly, put any remaining revisions. */
4017 for (i = graph->pos + 1; i < graph->size; i++)
4018 push_rev_graph(graph->next, graph->rev[i]);
4022 update_rev_graph(struct rev_graph *graph)
4024 /* If this is the finalizing update ... */
4026 prepare_rev_graph(graph);
4028 /* Graph visualization needs a one rev look-ahead,
4029 * so the first update doesn't visualize anything. */
4030 if (!graph->prev->commit)
4033 draw_rev_graph(graph->prev);
4034 done_rev_graph(graph->prev->prev);
4043 main_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
4045 char buf[DATE_COLS + 1];
4046 struct commit *commit = line->data;
4047 enum line_type type;
4053 if (!*commit->author)
4056 wmove(view->win, lineno, col);
4060 wattrset(view->win, get_line_attr(type));
4061 wchgat(view->win, -1, 0, type, NULL);
4064 type = LINE_MAIN_COMMIT;
4065 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
4068 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
4069 waddnstr(view->win, buf, timelen);
4070 waddstr(view->win, " ");
4073 wmove(view->win, lineno, col);
4074 if (type != LINE_CURSOR)
4075 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
4078 authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
4080 authorlen = strlen(commit->author);
4081 if (authorlen > AUTHOR_COLS - 2) {
4082 authorlen = AUTHOR_COLS - 2;
4088 waddnstr(view->win, commit->author, authorlen);
4089 if (type != LINE_CURSOR)
4090 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
4091 waddch(view->win, '~');
4093 waddstr(view->win, commit->author);
4097 if (type != LINE_CURSOR)
4098 wattrset(view->win, A_NORMAL);
4100 if (opt_rev_graph && commit->graph_size) {
4103 wmove(view->win, lineno, col);
4104 /* Using waddch() instead of waddnstr() ensures that
4105 * they'll be rendered correctly for the cursor line. */
4106 for (i = 0; i < commit->graph_size; i++)
4107 waddch(view->win, commit->graph[i]);
4109 waddch(view->win, ' ');
4110 col += commit->graph_size + 1;
4113 wmove(view->win, lineno, col);
4119 if (type == LINE_CURSOR)
4121 else if (commit->refs[i]->tag)
4122 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
4123 else if (commit->refs[i]->remote)
4124 wattrset(view->win, get_line_attr(LINE_MAIN_REMOTE));
4126 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
4127 waddstr(view->win, "[");
4128 waddstr(view->win, commit->refs[i]->name);
4129 waddstr(view->win, "]");
4130 if (type != LINE_CURSOR)
4131 wattrset(view->win, A_NORMAL);
4132 waddstr(view->win, " ");
4133 col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
4134 } while (commit->refs[i++]->next);
4137 if (type != LINE_CURSOR)
4138 wattrset(view->win, get_line_attr(type));
4141 int titlelen = strlen(commit->title);
4143 if (col + titlelen > view->width)
4144 titlelen = view->width - col;
4146 waddnstr(view->win, commit->title, titlelen);
4152 /* Reads git log --pretty=raw output and parses it into the commit struct. */
4154 main_read(struct view *view, char *line)
4156 static struct rev_graph *graph = graph_stacks;
4157 enum line_type type;
4158 struct commit *commit;
4161 update_rev_graph(graph);
4165 type = get_line_type(line);
4166 if (type == LINE_COMMIT) {
4167 commit = calloc(1, sizeof(struct commit));
4171 string_copy_rev(commit->id, line + STRING_SIZE("commit "));
4172 commit->refs = get_refs(commit->id);
4173 graph->commit = commit;
4174 add_line_data(view, commit, LINE_MAIN_COMMIT);
4180 commit = view->line[view->lines - 1].data;
4184 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
4189 /* Parse author lines where the name may be empty:
4190 * author <email@address.tld> 1138474660 +0100
4192 char *ident = line + STRING_SIZE("author ");
4193 char *nameend = strchr(ident, '<');
4194 char *emailend = strchr(ident, '>');
4196 if (!nameend || !emailend)
4199 update_rev_graph(graph);
4200 graph = graph->next;
4202 *nameend = *emailend = 0;
4203 ident = chomp_string(ident);
4205 ident = chomp_string(nameend + 1);
4210 string_ncopy(commit->author, ident, strlen(ident));
4212 /* Parse epoch and timezone */
4213 if (emailend[1] == ' ') {
4214 char *secs = emailend + 2;
4215 char *zone = strchr(secs, ' ');
4216 time_t time = (time_t) atol(secs);
4218 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
4222 tz = ('0' - zone[1]) * 60 * 60 * 10;
4223 tz += ('0' - zone[2]) * 60 * 60;
4224 tz += ('0' - zone[3]) * 60;
4225 tz += ('0' - zone[4]) * 60;
4233 gmtime_r(&time, &commit->time);
4238 /* Fill in the commit title if it has not already been set. */
4239 if (commit->title[0])
4242 /* Require titles to start with a non-space character at the
4243 * offset used by git log. */
4244 if (strncmp(line, " ", 4))
4247 /* Well, if the title starts with a whitespace character,
4248 * try to be forgiving. Otherwise we end up with no title. */
4249 while (isspace(*line))
4253 /* FIXME: More graceful handling of titles; append "..." to
4254 * shortened titles, etc. */
4256 string_ncopy(commit->title, line, strlen(line));
4263 main_request(struct view *view, enum request request, struct line *line)
4265 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
4267 if (request == REQ_ENTER)
4268 open_view(view, REQ_VIEW_DIFF, flags);
4276 main_grep(struct view *view, struct line *line)
4278 struct commit *commit = line->data;
4279 enum { S_TITLE, S_AUTHOR, S_DATE, S_END } state;
4280 char buf[DATE_COLS + 1];
4283 for (state = S_TITLE; state < S_END; state++) {
4287 case S_TITLE: text = commit->title; break;
4288 case S_AUTHOR: text = commit->author; break;
4290 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
4299 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4307 main_select(struct view *view, struct line *line)
4309 struct commit *commit = line->data;
4311 string_copy_rev(view->ref, commit->id);
4312 string_copy_rev(ref_commit, view->ref);
4315 static struct view_ops main_ops = {
4327 * Unicode / UTF-8 handling
4329 * NOTE: Much of the following code for dealing with unicode is derived from
4330 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
4331 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
4334 /* I've (over)annotated a lot of code snippets because I am not entirely
4335 * confident that the approach taken by this small UTF-8 interface is correct.
4339 unicode_width(unsigned long c)
4342 (c <= 0x115f /* Hangul Jamo */
4345 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
4347 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
4348 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
4349 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
4350 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
4351 || (c >= 0xffe0 && c <= 0xffe6)
4352 || (c >= 0x20000 && c <= 0x2fffd)
4353 || (c >= 0x30000 && c <= 0x3fffd)))
4359 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
4360 * Illegal bytes are set one. */
4361 static const unsigned char utf8_bytes[256] = {
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 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,
4368 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,
4369 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,
4372 /* Decode UTF-8 multi-byte representation into a unicode character. */
4373 static inline unsigned long
4374 utf8_to_unicode(const char *string, size_t length)
4376 unsigned long unicode;
4380 unicode = string[0];
4383 unicode = (string[0] & 0x1f) << 6;
4384 unicode += (string[1] & 0x3f);
4387 unicode = (string[0] & 0x0f) << 12;
4388 unicode += ((string[1] & 0x3f) << 6);
4389 unicode += (string[2] & 0x3f);
4392 unicode = (string[0] & 0x0f) << 18;
4393 unicode += ((string[1] & 0x3f) << 12);
4394 unicode += ((string[2] & 0x3f) << 6);
4395 unicode += (string[3] & 0x3f);
4398 unicode = (string[0] & 0x0f) << 24;
4399 unicode += ((string[1] & 0x3f) << 18);
4400 unicode += ((string[2] & 0x3f) << 12);
4401 unicode += ((string[3] & 0x3f) << 6);
4402 unicode += (string[4] & 0x3f);
4405 unicode = (string[0] & 0x01) << 30;
4406 unicode += ((string[1] & 0x3f) << 24);
4407 unicode += ((string[2] & 0x3f) << 18);
4408 unicode += ((string[3] & 0x3f) << 12);
4409 unicode += ((string[4] & 0x3f) << 6);
4410 unicode += (string[5] & 0x3f);
4413 die("Invalid unicode length");
4416 /* Invalid characters could return the special 0xfffd value but NUL
4417 * should be just as good. */
4418 return unicode > 0xffff ? 0 : unicode;
4421 /* Calculates how much of string can be shown within the given maximum width
4422 * and sets trimmed parameter to non-zero value if all of string could not be
4425 * Additionally, adds to coloffset how many many columns to move to align with
4426 * the expected position. Takes into account how multi-byte and double-width
4427 * characters will effect the cursor position.
4429 * Returns the number of bytes to output from string to satisfy max_width. */
4431 utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
4433 const char *start = string;
4434 const char *end = strchr(string, '\0');
4440 while (string < end) {
4441 int c = *(unsigned char *) string;
4442 unsigned char bytes = utf8_bytes[c];
4444 unsigned long unicode;
4446 if (string + bytes > end)
4449 /* Change representation to figure out whether
4450 * it is a single- or double-width character. */
4452 unicode = utf8_to_unicode(string, bytes);
4453 /* FIXME: Graceful handling of invalid unicode character. */
4457 ucwidth = unicode_width(unicode);
4459 if (width > max_width) {
4464 /* The column offset collects the differences between the
4465 * number of bytes encoding a character and the number of
4466 * columns will be used for rendering said character.
4468 * So if some character A is encoded in 2 bytes, but will be
4469 * represented on the screen using only 1 byte this will and up
4470 * adding 1 to the multi-byte column offset.
4472 * Assumes that no double-width character can be encoding in
4473 * less than two bytes. */
4474 if (bytes > ucwidth)
4475 mbwidth += bytes - ucwidth;
4480 *coloffset += mbwidth;
4482 return string - start;
4490 /* Whether or not the curses interface has been initialized. */
4491 static bool cursed = FALSE;
4493 /* The status window is used for polling keystrokes. */
4494 static WINDOW *status_win;
4496 static bool status_empty = TRUE;
4498 /* Update status and title window. */
4500 report(const char *msg, ...)
4502 struct view *view = display[current_view];
4508 char buf[SIZEOF_STR];
4511 va_start(args, msg);
4512 if (vsnprintf(buf, sizeof(buf), msg, args) >= sizeof(buf)) {
4513 buf[sizeof(buf) - 1] = 0;
4514 buf[sizeof(buf) - 2] = '.';
4515 buf[sizeof(buf) - 3] = '.';
4516 buf[sizeof(buf) - 4] = '.';
4522 if (!status_empty || *msg) {
4525 va_start(args, msg);
4527 wmove(status_win, 0, 0);
4529 vwprintw(status_win, msg, args);
4530 status_empty = FALSE;
4532 status_empty = TRUE;
4534 wclrtoeol(status_win);
4535 wrefresh(status_win);
4540 update_view_title(view);
4541 update_display_cursor(view);
4544 /* Controls when nodelay should be in effect when polling user input. */
4546 set_nonblocking_input(bool loading)
4548 static unsigned int loading_views;
4550 if ((loading == FALSE && loading_views-- == 1) ||
4551 (loading == TRUE && loading_views++ == 0))
4552 nodelay(status_win, loading);
4560 /* Initialize the curses library */
4561 if (isatty(STDIN_FILENO)) {
4562 cursed = !!initscr();
4564 /* Leave stdin and stdout alone when acting as a pager. */
4565 FILE *io = fopen("/dev/tty", "r+");
4568 die("Failed to open /dev/tty");
4569 cursed = !!newterm(NULL, io, io);
4573 die("Failed to initialize curses");
4575 nonl(); /* Tell curses not to do NL->CR/NL on output */
4576 cbreak(); /* Take input chars one at a time, no wait for \n */
4577 noecho(); /* Don't echo input */
4578 leaveok(stdscr, TRUE);
4583 getmaxyx(stdscr, y, x);
4584 status_win = newwin(1, 0, y - 1, 0);
4586 die("Failed to create status window");
4588 /* Enable keyboard mapping */
4589 keypad(status_win, TRUE);
4590 wbkgdset(status_win, get_line_attr(LINE_STATUS));
4594 read_prompt(const char *prompt)
4596 enum { READING, STOP, CANCEL } status = READING;
4597 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
4600 while (status == READING) {
4606 foreach_view (view, i)
4611 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
4612 wclrtoeol(status_win);
4614 /* Refresh, accept single keystroke of input */
4615 key = wgetch(status_win);
4620 status = pos ? STOP : CANCEL;
4638 if (pos >= sizeof(buf)) {
4639 report("Input string too long");
4644 buf[pos++] = (char) key;
4648 /* Clear the status window */
4649 status_empty = FALSE;
4652 if (status == CANCEL)
4661 * Repository references
4664 static struct ref *refs;
4665 static size_t refs_size;
4667 /* Id <-> ref store */
4668 static struct ref ***id_refs;
4669 static size_t id_refs_size;
4671 static struct ref **
4674 struct ref ***tmp_id_refs;
4675 struct ref **ref_list = NULL;
4676 size_t ref_list_size = 0;
4679 for (i = 0; i < id_refs_size; i++)
4680 if (!strcmp(id, id_refs[i][0]->id))
4683 tmp_id_refs = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs));
4687 id_refs = tmp_id_refs;
4689 for (i = 0; i < refs_size; i++) {
4692 if (strcmp(id, refs[i].id))
4695 tmp = realloc(ref_list, (ref_list_size + 1) * sizeof(*ref_list));
4703 if (ref_list_size > 0)
4704 ref_list[ref_list_size - 1]->next = 1;
4705 ref_list[ref_list_size] = &refs[i];
4707 /* XXX: The properties of the commit chains ensures that we can
4708 * safely modify the shared ref. The repo references will
4709 * always be similar for the same id. */
4710 ref_list[ref_list_size]->next = 0;
4715 id_refs[id_refs_size++] = ref_list;
4721 read_ref(char *id, size_t idlen, char *name, size_t namelen)
4725 bool remote = FALSE;
4727 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
4728 /* Commits referenced by tags has "^{}" appended. */
4729 if (name[namelen - 1] != '}')
4732 while (namelen > 0 && name[namelen] != '^')
4736 namelen -= STRING_SIZE("refs/tags/");
4737 name += STRING_SIZE("refs/tags/");
4739 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
4741 namelen -= STRING_SIZE("refs/remotes/");
4742 name += STRING_SIZE("refs/remotes/");
4744 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
4745 namelen -= STRING_SIZE("refs/heads/");
4746 name += STRING_SIZE("refs/heads/");
4748 } else if (!strcmp(name, "HEAD")) {
4752 refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
4756 ref = &refs[refs_size++];
4757 ref->name = malloc(namelen + 1);
4761 strncpy(ref->name, name, namelen);
4762 ref->name[namelen] = 0;
4764 ref->remote = remote;
4765 string_copy_rev(ref->id, id);
4773 const char *cmd_env = getenv("TIG_LS_REMOTE");
4774 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
4776 return read_properties(popen(cmd, "r"), "\t", read_ref);
4780 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
4782 if (!strcmp(name, "i18n.commitencoding"))
4783 string_ncopy(opt_encoding, value, valuelen);
4785 if (!strcmp(name, "core.editor"))
4786 string_ncopy(opt_editor, value, valuelen);
4792 load_repo_config(void)
4794 return read_properties(popen(GIT_CONFIG " --list", "r"),
4795 "=", read_repo_config_option);
4799 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
4801 if (!opt_git_dir[0]) {
4802 string_ncopy(opt_git_dir, name, namelen);
4804 } else if (opt_is_inside_work_tree == -1) {
4805 /* This can be 3 different values depending on the
4806 * version of git being used. If git-rev-parse does not
4807 * understand --is-inside-work-tree it will simply echo
4808 * the option else either "true" or "false" is printed.
4809 * Default to true for the unknown case. */
4810 opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
4813 string_ncopy(opt_cdup, name, namelen);
4819 /* XXX: The line outputted by "--show-cdup" can be empty so the option
4820 * must be the last one! */
4822 load_repo_info(void)
4824 return read_properties(popen("git rev-parse --git-dir --is-inside-work-tree --show-cdup 2>/dev/null", "r"),
4825 "=", read_repo_info);
4829 read_properties(FILE *pipe, const char *separators,
4830 int (*read_property)(char *, size_t, char *, size_t))
4832 char buffer[BUFSIZ];
4839 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
4844 name = chomp_string(name);
4845 namelen = strcspn(name, separators);
4847 if (name[namelen]) {
4849 value = chomp_string(name + namelen + 1);
4850 valuelen = strlen(value);
4857 state = read_property(name, namelen, value, valuelen);
4860 if (state != ERR && ferror(pipe))
4873 static void __NORETURN
4876 /* XXX: Restore tty modes and let the OS cleanup the rest! */
4882 static void __NORETURN
4883 die(const char *err, ...)
4889 va_start(args, err);
4890 fputs("tig: ", stderr);
4891 vfprintf(stderr, err, args);
4892 fputs("\n", stderr);
4899 main(int argc, char *argv[])
4902 enum request request;
4905 signal(SIGINT, quit);
4907 if (setlocale(LC_ALL, "")) {
4908 char *codeset = nl_langinfo(CODESET);
4910 string_ncopy(opt_codeset, codeset, strlen(codeset));
4913 if (load_repo_info() == ERR)
4914 die("Failed to load repo info.");
4916 if (load_options() == ERR)
4917 die("Failed to load user config.");
4919 /* Load the repo config file so options can be overwritten from
4920 * the command line. */
4921 if (load_repo_config() == ERR)
4922 die("Failed to load repo config.");
4924 if (!parse_options(argc, argv))
4927 /* Require a git repository unless when running in pager mode. */
4928 if (!opt_git_dir[0])
4929 die("Not a git repository");
4931 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
4932 opt_iconv = iconv_open(opt_codeset, opt_encoding);
4933 if (opt_iconv == ICONV_NONE)
4934 die("Failed to initialize character set conversion");
4937 if (load_refs() == ERR)
4938 die("Failed to load refs.");
4940 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
4941 view->cmd_env = getenv(view->cmd_env);
4943 request = opt_request;
4947 while (view_driver(display[current_view], request)) {
4951 foreach_view (view, i)
4954 /* Refresh, accept single keystroke of input */
4955 key = wgetch(status_win);
4957 /* wgetch() with nodelay() enabled returns ERR when there's no
4964 request = get_keybinding(display[current_view]->keymap, key);
4966 /* Some low-level request handling. This keeps access to
4967 * status_win restricted. */
4971 char *cmd = read_prompt(":");
4973 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
4974 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
4975 opt_request = REQ_VIEW_DIFF;
4977 opt_request = REQ_VIEW_PAGER;
4986 case REQ_SEARCH_BACK:
4988 const char *prompt = request == REQ_SEARCH
4990 char *search = read_prompt(prompt);
4993 string_ncopy(opt_search, search, strlen(search));
4998 case REQ_SCREEN_RESIZE:
5002 getmaxyx(stdscr, height, width);
5004 /* Resize the status view and let the view driver take
5005 * care of resizing the displayed views. */
5006 wresize(status_win, 1, width);
5007 mvwin(status_win, height - 1, 0);
5008 wrefresh(status_win);