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_(NONE, "Do nothing"), \
347 REQ_(PROMPT, "Bring up the prompt"), \
348 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
349 REQ_(SCREEN_RESIZE, "Resize the screen"), \
350 REQ_(SHOW_VERSION, "Show version information"), \
351 REQ_(STOP_LOADING, "Stop all loading views"), \
352 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
353 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"), \
354 REQ_(STATUS_UPDATE, "Update file status"), \
355 REQ_(STATUS_MERGE, "Merge file using external tool"), \
356 REQ_(EDIT, "Open in editor"), \
357 REQ_(CHERRY_PICK, "Cherry-pick commit to current branch")
360 /* User action requests. */
362 #define REQ_GROUP(help)
363 #define REQ_(req, help) REQ_##req
365 /* Offset all requests to avoid conflicts with ncurses getch values. */
366 REQ_OFFSET = KEY_MAX + 1,
374 struct request_info {
375 enum request request;
381 static struct request_info req_info[] = {
382 #define REQ_GROUP(help) { 0, NULL, 0, (help) },
383 #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
390 get_request(const char *name)
392 int namelen = strlen(name);
395 for (i = 0; i < ARRAY_SIZE(req_info); i++)
396 if (req_info[i].namelen == namelen &&
397 !string_enum_compare(req_info[i].name, name, namelen))
398 return req_info[i].request;
408 static const char usage[] =
409 "tig " TIG_VERSION " (" __DATE__ ")\n"
411 "Usage: tig [options]\n"
412 " or: tig [options] [--] [git log options]\n"
413 " or: tig [options] log [git log options]\n"
414 " or: tig [options] diff [git diff options]\n"
415 " or: tig [options] show [git show options]\n"
416 " or: tig [options] < [git command output]\n"
419 " -l Start up in log view\n"
420 " -d Start up in diff view\n"
421 " -S Start up in status view\n"
422 " -n[I], --line-number[=I] Show line numbers with given interval\n"
423 " -b[N], --tab-size[=N] Set number of spaces for tab expansion\n"
424 " -- Mark end of tig options\n"
425 " -v, --version Show version and exit\n"
426 " -h, --help Show help message and exit\n";
428 /* Option and state variables. */
429 static bool opt_line_number = FALSE;
430 static bool opt_rev_graph = FALSE;
431 static int opt_num_interval = NUMBER_INTERVAL;
432 static int opt_tab_size = TABSIZE;
433 static enum request opt_request = REQ_VIEW_MAIN;
434 static char opt_cmd[SIZEOF_STR] = "";
435 static char opt_path[SIZEOF_STR] = "";
436 static FILE *opt_pipe = NULL;
437 static char opt_encoding[20] = "UTF-8";
438 static bool opt_utf8 = TRUE;
439 static char opt_codeset[20] = "UTF-8";
440 static iconv_t opt_iconv = ICONV_NONE;
441 static char opt_search[SIZEOF_STR] = "";
442 static char opt_cdup[SIZEOF_STR] = "";
443 static char opt_git_dir[SIZEOF_STR] = "";
444 static char opt_is_inside_work_tree = -1; /* set to TRUE or FALSE */
445 static char opt_editor[SIZEOF_STR] = "";
453 check_option(char *opt, char short_name, char *name, enum option_type type, ...)
463 int namelen = strlen(name);
467 if (strncmp(opt, name, namelen))
470 if (opt[namelen] == '=')
471 value = opt + namelen + 1;
474 if (!short_name || opt[1] != short_name)
479 va_start(args, type);
480 if (type == OPT_INT) {
481 number = va_arg(args, int *);
483 *number = atoi(value);
490 /* Returns the index of log or diff command or -1 to exit. */
492 parse_options(int argc, char *argv[])
496 for (i = 1; i < argc; i++) {
499 if (!strcmp(opt, "log") ||
500 !strcmp(opt, "diff") ||
501 !strcmp(opt, "show")) {
502 opt_request = opt[0] == 'l'
503 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
507 if (opt[0] && opt[0] != '-')
510 if (!strcmp(opt, "-l")) {
511 opt_request = REQ_VIEW_LOG;
515 if (!strcmp(opt, "-d")) {
516 opt_request = REQ_VIEW_DIFF;
520 if (!strcmp(opt, "-S")) {
521 opt_request = REQ_VIEW_STATUS;
525 if (check_option(opt, 'n', "line-number", OPT_INT, &opt_num_interval)) {
526 opt_line_number = TRUE;
530 if (check_option(opt, 'b', "tab-size", OPT_INT, &opt_tab_size)) {
531 opt_tab_size = MIN(opt_tab_size, TABSIZE);
535 if (check_option(opt, 'v', "version", OPT_NONE)) {
536 printf("tig version %s\n", TIG_VERSION);
540 if (check_option(opt, 'h', "help", OPT_NONE)) {
545 if (!strcmp(opt, "--")) {
550 die("unknown option '%s'\n\n%s", opt, usage);
553 if (!isatty(STDIN_FILENO)) {
554 opt_request = REQ_VIEW_PAGER;
557 } else if (i < argc) {
560 if (opt_request == REQ_VIEW_MAIN)
561 /* XXX: This is vulnerable to the user overriding
562 * options required for the main view parser. */
563 string_copy(opt_cmd, "git log --pretty=raw");
565 string_copy(opt_cmd, "git");
566 buf_size = strlen(opt_cmd);
568 while (buf_size < sizeof(opt_cmd) && i < argc) {
569 opt_cmd[buf_size++] = ' ';
570 buf_size = sq_quote(opt_cmd, buf_size, argv[i++]);
573 if (buf_size >= sizeof(opt_cmd))
574 die("command too long");
576 opt_cmd[buf_size] = 0;
579 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
587 * Line-oriented content detection.
591 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
592 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
593 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
594 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
595 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
596 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
597 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
598 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
599 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
600 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
601 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
602 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
603 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
604 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
605 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
606 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
607 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
608 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
609 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
610 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
611 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
612 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
613 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
614 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
615 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
616 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
617 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
618 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
619 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
620 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
621 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
622 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
623 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
624 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
625 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
626 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
627 LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
628 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
629 LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
630 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
631 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
632 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
633 LINE(STAT_SECTION, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
634 LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
635 LINE(STAT_STAGED, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
636 LINE(STAT_UNSTAGED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
637 LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0)
640 #define LINE(type, line, fg, bg, attr) \
647 const char *name; /* Option name. */
648 int namelen; /* Size of option name. */
649 const char *line; /* The start of line to match. */
650 int linelen; /* Size of string to match. */
651 int fg, bg, attr; /* Color and text attributes for the lines. */
654 static struct line_info line_info[] = {
655 #define LINE(type, line, fg, bg, attr) \
656 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
661 static enum line_type
662 get_line_type(char *line)
664 int linelen = strlen(line);
667 for (type = 0; type < ARRAY_SIZE(line_info); type++)
668 /* Case insensitive search matches Signed-off-by lines better. */
669 if (linelen >= line_info[type].linelen &&
670 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
677 get_line_attr(enum line_type type)
679 assert(type < ARRAY_SIZE(line_info));
680 return COLOR_PAIR(type) | line_info[type].attr;
683 static struct line_info *
684 get_line_info(char *name, int namelen)
688 for (type = 0; type < ARRAY_SIZE(line_info); type++)
689 if (namelen == line_info[type].namelen &&
690 !string_enum_compare(line_info[type].name, name, namelen))
691 return &line_info[type];
699 int default_bg = COLOR_BLACK;
700 int default_fg = COLOR_WHITE;
705 if (use_default_colors() != ERR) {
710 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
711 struct line_info *info = &line_info[type];
712 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
713 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
715 init_pair(type, fg, bg);
723 unsigned int selected:1;
725 void *data; /* User data */
735 enum request request;
736 struct keybinding *next;
739 static struct keybinding default_keybindings[] = {
741 { 'm', REQ_VIEW_MAIN },
742 { 'd', REQ_VIEW_DIFF },
743 { 'l', REQ_VIEW_LOG },
744 { 't', REQ_VIEW_TREE },
745 { 'f', REQ_VIEW_BLOB },
746 { 'p', REQ_VIEW_PAGER },
747 { 'h', REQ_VIEW_HELP },
748 { 'S', REQ_VIEW_STATUS },
749 { 'c', REQ_VIEW_STAGE },
751 /* View manipulation */
752 { 'q', REQ_VIEW_CLOSE },
753 { KEY_TAB, REQ_VIEW_NEXT },
754 { KEY_RETURN, REQ_ENTER },
755 { KEY_UP, REQ_PREVIOUS },
756 { KEY_DOWN, REQ_NEXT },
757 { 'R', REQ_REFRESH },
759 /* Cursor navigation */
760 { 'k', REQ_MOVE_UP },
761 { 'j', REQ_MOVE_DOWN },
762 { KEY_HOME, REQ_MOVE_FIRST_LINE },
763 { KEY_END, REQ_MOVE_LAST_LINE },
764 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
765 { ' ', REQ_MOVE_PAGE_DOWN },
766 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
767 { 'b', REQ_MOVE_PAGE_UP },
768 { '-', REQ_MOVE_PAGE_UP },
771 { KEY_IC, REQ_SCROLL_LINE_UP },
772 { KEY_DC, REQ_SCROLL_LINE_DOWN },
773 { 'w', REQ_SCROLL_PAGE_UP },
774 { 's', REQ_SCROLL_PAGE_DOWN },
778 { '?', REQ_SEARCH_BACK },
779 { 'n', REQ_FIND_NEXT },
780 { 'N', REQ_FIND_PREV },
784 { 'z', REQ_STOP_LOADING },
785 { 'v', REQ_SHOW_VERSION },
786 { 'r', REQ_SCREEN_REDRAW },
787 { '.', REQ_TOGGLE_LINENO },
788 { 'g', REQ_TOGGLE_REV_GRAPH },
790 { 'u', REQ_STATUS_UPDATE },
791 { 'M', REQ_STATUS_MERGE },
793 { 'C', REQ_CHERRY_PICK },
795 /* Using the ncurses SIGWINCH handler. */
796 { KEY_RESIZE, REQ_SCREEN_RESIZE },
799 #define KEYMAP_INFO \
812 #define KEYMAP_(name) KEYMAP_##name
817 static struct int_map keymap_table[] = {
818 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
823 #define set_keymap(map, name) \
824 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
826 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
829 add_keybinding(enum keymap keymap, enum request request, int key)
831 struct keybinding *keybinding;
833 keybinding = calloc(1, sizeof(*keybinding));
835 die("Failed to allocate keybinding");
837 keybinding->alias = key;
838 keybinding->request = request;
839 keybinding->next = keybindings[keymap];
840 keybindings[keymap] = keybinding;
843 /* Looks for a key binding first in the given map, then in the generic map, and
844 * lastly in the default keybindings. */
846 get_keybinding(enum keymap keymap, int key)
848 struct keybinding *kbd;
851 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
852 if (kbd->alias == key)
855 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
856 if (kbd->alias == key)
859 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
860 if (default_keybindings[i].alias == key)
861 return default_keybindings[i].request;
863 return (enum request) key;
872 static struct key key_table[] = {
873 { "Enter", KEY_RETURN },
875 { "Backspace", KEY_BACKSPACE },
877 { "Escape", KEY_ESC },
878 { "Left", KEY_LEFT },
879 { "Right", KEY_RIGHT },
881 { "Down", KEY_DOWN },
882 { "Insert", KEY_IC },
883 { "Delete", KEY_DC },
885 { "Home", KEY_HOME },
887 { "PageUp", KEY_PPAGE },
888 { "PageDown", KEY_NPAGE },
898 { "F10", KEY_F(10) },
899 { "F11", KEY_F(11) },
900 { "F12", KEY_F(12) },
904 get_key_value(const char *name)
908 for (i = 0; i < ARRAY_SIZE(key_table); i++)
909 if (!strcasecmp(key_table[i].name, name))
910 return key_table[i].value;
912 if (strlen(name) == 1 && isprint(*name))
919 get_key(enum request request)
921 static char buf[BUFSIZ];
922 static char key_char[] = "'X'";
929 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
930 struct keybinding *keybinding = &default_keybindings[i];
934 if (keybinding->request != request)
937 for (key = 0; key < ARRAY_SIZE(key_table); key++)
938 if (key_table[key].value == keybinding->alias)
939 seq = key_table[key].name;
942 keybinding->alias < 127 &&
943 isprint(keybinding->alias)) {
944 key_char[1] = (char) keybinding->alias;
951 if (!string_format_from(buf, &pos, "%s%s", sep, seq))
952 return "Too many keybindings!";
961 * User config file handling.
964 static struct int_map color_map[] = {
965 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
977 #define set_color(color, name) \
978 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
980 static struct int_map attr_map[] = {
981 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
991 #define set_attribute(attr, name) \
992 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
994 static int config_lineno;
995 static bool config_errors;
996 static char *config_msg;
998 /* Wants: object fgcolor bgcolor [attr] */
1000 option_color_command(int argc, char *argv[])
1002 struct line_info *info;
1004 if (argc != 3 && argc != 4) {
1005 config_msg = "Wrong number of arguments given to color command";
1009 info = get_line_info(argv[0], strlen(argv[0]));
1011 config_msg = "Unknown color name";
1015 if (set_color(&info->fg, argv[1]) == ERR ||
1016 set_color(&info->bg, argv[2]) == ERR) {
1017 config_msg = "Unknown color";
1021 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
1022 config_msg = "Unknown attribute";
1029 /* Wants: name = value */
1031 option_set_command(int argc, char *argv[])
1034 config_msg = "Wrong number of arguments given to set command";
1038 if (strcmp(argv[1], "=")) {
1039 config_msg = "No value assigned";
1043 if (!strcmp(argv[0], "show-rev-graph")) {
1044 opt_rev_graph = (!strcmp(argv[2], "1") ||
1045 !strcmp(argv[2], "true") ||
1046 !strcmp(argv[2], "yes"));
1050 if (!strcmp(argv[0], "line-number-interval")) {
1051 opt_num_interval = atoi(argv[2]);
1055 if (!strcmp(argv[0], "tab-size")) {
1056 opt_tab_size = atoi(argv[2]);
1060 if (!strcmp(argv[0], "commit-encoding")) {
1061 char *arg = argv[2];
1062 int delimiter = *arg;
1065 switch (delimiter) {
1068 for (arg++, i = 0; arg[i]; i++)
1069 if (arg[i] == delimiter) {
1074 string_ncopy(opt_encoding, arg, strlen(arg));
1079 config_msg = "Unknown variable name";
1083 /* Wants: mode request key */
1085 option_bind_command(int argc, char *argv[])
1087 enum request request;
1092 config_msg = "Wrong number of arguments given to bind command";
1096 if (set_keymap(&keymap, argv[0]) == ERR) {
1097 config_msg = "Unknown key map";
1101 key = get_key_value(argv[1]);
1103 config_msg = "Unknown key";
1107 request = get_request(argv[2]);
1108 if (request == REQ_UNKNOWN) {
1109 config_msg = "Unknown request name";
1113 add_keybinding(keymap, request, key);
1119 set_option(char *opt, char *value)
1126 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1127 argv[argc++] = value;
1134 while (isspace(*value))
1138 if (!strcmp(opt, "color"))
1139 return option_color_command(argc, argv);
1141 if (!strcmp(opt, "set"))
1142 return option_set_command(argc, argv);
1144 if (!strcmp(opt, "bind"))
1145 return option_bind_command(argc, argv);
1147 config_msg = "Unknown option command";
1152 read_option(char *opt, size_t optlen, char *value, size_t valuelen)
1157 config_msg = "Internal error";
1159 /* Check for comment markers, since read_properties() will
1160 * only ensure opt and value are split at first " \t". */
1161 optlen = strcspn(opt, "#");
1165 if (opt[optlen] != 0) {
1166 config_msg = "No option value";
1170 /* Look for comment endings in the value. */
1171 size_t len = strcspn(value, "#");
1173 if (len < valuelen) {
1175 value[valuelen] = 0;
1178 status = set_option(opt, value);
1181 if (status == ERR) {
1182 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1183 config_lineno, (int) optlen, opt, config_msg);
1184 config_errors = TRUE;
1187 /* Always keep going if errors are encountered. */
1194 char *home = getenv("HOME");
1195 char buf[SIZEOF_STR];
1199 config_errors = FALSE;
1201 if (!home || !string_format(buf, "%s/.tigrc", home))
1204 /* It's ok that the file doesn't exist. */
1205 file = fopen(buf, "r");
1209 if (read_properties(file, " \t", read_option) == ERR ||
1210 config_errors == TRUE)
1211 fprintf(stderr, "Errors while loading %s.\n", buf);
1224 /* The display array of active views and the index of the current view. */
1225 static struct view *display[2];
1226 static unsigned int current_view;
1228 /* Reading from the prompt? */
1229 static bool input_mode = FALSE;
1231 #define foreach_displayed_view(view, i) \
1232 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1234 #define displayed_views() (display[1] != NULL ? 2 : 1)
1236 /* Current head and commit ID */
1237 static char ref_blob[SIZEOF_REF] = "";
1238 static char ref_commit[SIZEOF_REF] = "HEAD";
1239 static char ref_head[SIZEOF_REF] = "HEAD";
1242 const char *name; /* View name */
1243 const char *cmd_fmt; /* Default command line format */
1244 const char *cmd_env; /* Command line set via environment */
1245 const char *id; /* Points to either of ref_{head,commit,blob} */
1247 struct view_ops *ops; /* View operations */
1249 enum keymap keymap; /* What keymap does this view have */
1251 char cmd[SIZEOF_STR]; /* Command buffer */
1252 char ref[SIZEOF_REF]; /* Hovered commit reference */
1253 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1255 int height, width; /* The width and height of the main window */
1256 WINDOW *win; /* The main window */
1257 WINDOW *title; /* The title window living below the main window */
1260 unsigned long offset; /* Offset of the window top */
1261 unsigned long lineno; /* Current line number */
1264 char grep[SIZEOF_STR]; /* Search string */
1265 regex_t *regex; /* Pre-compiled regex */
1267 /* If non-NULL, points to the view that opened this view. If this view
1268 * is closed tig will switch back to the parent view. */
1269 struct view *parent;
1272 unsigned long lines; /* Total number of lines */
1273 struct line *line; /* Line index */
1274 unsigned long line_size;/* Total number of allocated lines */
1275 unsigned int digits; /* Number of digits in the lines member. */
1283 /* What type of content being displayed. Used in the title bar. */
1285 /* Open and reads in all view content. */
1286 bool (*open)(struct view *view);
1287 /* Read one line; updates view->line. */
1288 bool (*read)(struct view *view, char *data);
1289 /* Draw one line; @lineno must be < view->height. */
1290 bool (*draw)(struct view *view, struct line *line, unsigned int lineno, bool selected);
1291 /* Depending on view handle a special requests. */
1292 enum request (*request)(struct view *view, enum request request, struct line *line);
1293 /* Search for regex in a line. */
1294 bool (*grep)(struct view *view, struct line *line);
1296 void (*select)(struct view *view, struct line *line);
1299 static struct view_ops pager_ops;
1300 static struct view_ops main_ops;
1301 static struct view_ops tree_ops;
1302 static struct view_ops blob_ops;
1303 static struct view_ops help_ops;
1304 static struct view_ops status_ops;
1305 static struct view_ops stage_ops;
1307 #define VIEW_STR(name, cmd, env, ref, ops, map) \
1308 { name, cmd, #env, ref, ops, map}
1310 #define VIEW_(id, name, ops, ref) \
1311 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id)
1314 static struct view views[] = {
1315 VIEW_(MAIN, "main", &main_ops, ref_head),
1316 VIEW_(DIFF, "diff", &pager_ops, ref_commit),
1317 VIEW_(LOG, "log", &pager_ops, ref_head),
1318 VIEW_(TREE, "tree", &tree_ops, ref_commit),
1319 VIEW_(BLOB, "blob", &blob_ops, ref_blob),
1320 VIEW_(HELP, "help", &help_ops, ""),
1321 VIEW_(PAGER, "pager", &pager_ops, "stdin"),
1322 VIEW_(STATUS, "status", &status_ops, ""),
1323 VIEW_(STAGE, "stage", &stage_ops, ""),
1326 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1328 #define foreach_view(view, i) \
1329 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1331 #define view_is_displayed(view) \
1332 (view == display[0] || view == display[1])
1335 draw_view_line(struct view *view, unsigned int lineno)
1338 bool selected = (view->offset + lineno == view->lineno);
1341 assert(view_is_displayed(view));
1343 if (view->offset + lineno >= view->lines)
1346 line = &view->line[view->offset + lineno];
1349 line->selected = TRUE;
1350 view->ops->select(view, line);
1351 } else if (line->selected) {
1352 line->selected = FALSE;
1353 wmove(view->win, lineno, 0);
1354 wclrtoeol(view->win);
1357 scrollok(view->win, FALSE);
1358 draw_ok = view->ops->draw(view, line, lineno, selected);
1359 scrollok(view->win, TRUE);
1365 redraw_view_from(struct view *view, int lineno)
1367 assert(0 <= lineno && lineno < view->height);
1369 for (; lineno < view->height; lineno++) {
1370 if (!draw_view_line(view, lineno))
1374 redrawwin(view->win);
1376 wnoutrefresh(view->win);
1378 wrefresh(view->win);
1382 redraw_view(struct view *view)
1385 redraw_view_from(view, 0);
1390 update_view_title(struct view *view)
1392 char buf[SIZEOF_STR];
1393 char state[SIZEOF_STR];
1394 size_t bufpos = 0, statelen = 0;
1396 assert(view_is_displayed(view));
1398 if (view != VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) {
1399 unsigned int view_lines = view->offset + view->height;
1400 unsigned int lines = view->lines
1401 ? MIN(view_lines, view->lines) * 100 / view->lines
1404 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1411 time_t secs = time(NULL) - view->start_time;
1413 /* Three git seconds are a long time ... */
1415 string_format_from(state, &statelen, " %lds", secs);
1419 string_format_from(buf, &bufpos, "[%s]", view->name);
1420 if (*view->ref && bufpos < view->width) {
1421 size_t refsize = strlen(view->ref);
1422 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1424 if (minsize < view->width)
1425 refsize = view->width - minsize + 7;
1426 string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
1429 if (statelen && bufpos < view->width) {
1430 string_format_from(buf, &bufpos, " %s", state);
1433 if (view == display[current_view])
1434 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1436 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1438 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1439 wclrtoeol(view->title);
1440 wmove(view->title, 0, view->width - 1);
1443 wnoutrefresh(view->title);
1445 wrefresh(view->title);
1449 resize_display(void)
1452 struct view *base = display[0];
1453 struct view *view = display[1] ? display[1] : display[0];
1455 /* Setup window dimensions */
1457 getmaxyx(stdscr, base->height, base->width);
1459 /* Make room for the status window. */
1463 /* Horizontal split. */
1464 view->width = base->width;
1465 view->height = SCALE_SPLIT_VIEW(base->height);
1466 base->height -= view->height;
1468 /* Make room for the title bar. */
1472 /* Make room for the title bar. */
1477 foreach_displayed_view (view, i) {
1479 view->win = newwin(view->height, 0, offset, 0);
1481 die("Failed to create %s view", view->name);
1483 scrollok(view->win, TRUE);
1485 view->title = newwin(1, 0, offset + view->height, 0);
1487 die("Failed to create title window");
1490 wresize(view->win, view->height, view->width);
1491 mvwin(view->win, offset, 0);
1492 mvwin(view->title, offset + view->height, 0);
1495 offset += view->height + 1;
1500 redraw_display(void)
1505 foreach_displayed_view (view, i) {
1507 update_view_title(view);
1512 update_display_cursor(struct view *view)
1514 /* Move the cursor to the right-most column of the cursor line.
1516 * XXX: This could turn out to be a bit expensive, but it ensures that
1517 * the cursor does not jump around. */
1519 wmove(view->win, view->lineno - view->offset, view->width - 1);
1520 wrefresh(view->win);
1528 /* Scrolling backend */
1530 do_scroll_view(struct view *view, int lines)
1532 bool redraw_current_line = FALSE;
1534 /* The rendering expects the new offset. */
1535 view->offset += lines;
1537 assert(0 <= view->offset && view->offset < view->lines);
1540 /* Move current line into the view. */
1541 if (view->lineno < view->offset) {
1542 view->lineno = view->offset;
1543 redraw_current_line = TRUE;
1544 } else if (view->lineno >= view->offset + view->height) {
1545 view->lineno = view->offset + view->height - 1;
1546 redraw_current_line = TRUE;
1549 assert(view->offset <= view->lineno && view->lineno < view->lines);
1551 /* Redraw the whole screen if scrolling is pointless. */
1552 if (view->height < ABS(lines)) {
1556 int line = lines > 0 ? view->height - lines : 0;
1557 int end = line + ABS(lines);
1559 wscrl(view->win, lines);
1561 for (; line < end; line++) {
1562 if (!draw_view_line(view, line))
1566 if (redraw_current_line)
1567 draw_view_line(view, view->lineno - view->offset);
1570 redrawwin(view->win);
1571 wrefresh(view->win);
1575 /* Scroll frontend */
1577 scroll_view(struct view *view, enum request request)
1581 assert(view_is_displayed(view));
1584 case REQ_SCROLL_PAGE_DOWN:
1585 lines = view->height;
1586 case REQ_SCROLL_LINE_DOWN:
1587 if (view->offset + lines > view->lines)
1588 lines = view->lines - view->offset;
1590 if (lines == 0 || view->offset + view->height >= view->lines) {
1591 report("Cannot scroll beyond the last line");
1596 case REQ_SCROLL_PAGE_UP:
1597 lines = view->height;
1598 case REQ_SCROLL_LINE_UP:
1599 if (lines > view->offset)
1600 lines = view->offset;
1603 report("Cannot scroll beyond the first line");
1611 die("request %d not handled in switch", request);
1614 do_scroll_view(view, lines);
1619 move_view(struct view *view, enum request request)
1621 int scroll_steps = 0;
1625 case REQ_MOVE_FIRST_LINE:
1626 steps = -view->lineno;
1629 case REQ_MOVE_LAST_LINE:
1630 steps = view->lines - view->lineno - 1;
1633 case REQ_MOVE_PAGE_UP:
1634 steps = view->height > view->lineno
1635 ? -view->lineno : -view->height;
1638 case REQ_MOVE_PAGE_DOWN:
1639 steps = view->lineno + view->height >= view->lines
1640 ? view->lines - view->lineno - 1 : view->height;
1652 die("request %d not handled in switch", request);
1655 if (steps <= 0 && view->lineno == 0) {
1656 report("Cannot move beyond the first line");
1659 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1660 report("Cannot move beyond the last line");
1664 /* Move the current line */
1665 view->lineno += steps;
1666 assert(0 <= view->lineno && view->lineno < view->lines);
1668 /* Check whether the view needs to be scrolled */
1669 if (view->lineno < view->offset ||
1670 view->lineno >= view->offset + view->height) {
1671 scroll_steps = steps;
1672 if (steps < 0 && -steps > view->offset) {
1673 scroll_steps = -view->offset;
1675 } else if (steps > 0) {
1676 if (view->lineno == view->lines - 1 &&
1677 view->lines > view->height) {
1678 scroll_steps = view->lines - view->offset - 1;
1679 if (scroll_steps >= view->height)
1680 scroll_steps -= view->height - 1;
1685 if (!view_is_displayed(view)) {
1686 view->offset += scroll_steps;
1687 assert(0 <= view->offset && view->offset < view->lines);
1688 view->ops->select(view, &view->line[view->lineno]);
1692 /* Repaint the old "current" line if we be scrolling */
1693 if (ABS(steps) < view->height)
1694 draw_view_line(view, view->lineno - steps - view->offset);
1697 do_scroll_view(view, scroll_steps);
1701 /* Draw the current line */
1702 draw_view_line(view, view->lineno - view->offset);
1704 redrawwin(view->win);
1705 wrefresh(view->win);
1714 static void search_view(struct view *view, enum request request);
1717 find_next_line(struct view *view, unsigned long lineno, struct line *line)
1719 assert(view_is_displayed(view));
1721 if (!view->ops->grep(view, line))
1724 if (lineno - view->offset >= view->height) {
1725 view->offset = lineno;
1726 view->lineno = lineno;
1730 unsigned long old_lineno = view->lineno - view->offset;
1732 view->lineno = lineno;
1733 draw_view_line(view, old_lineno);
1735 draw_view_line(view, view->lineno - view->offset);
1736 redrawwin(view->win);
1737 wrefresh(view->win);
1740 report("Line %ld matches '%s'", lineno + 1, view->grep);
1745 find_next(struct view *view, enum request request)
1747 unsigned long lineno = view->lineno;
1752 report("No previous search");
1754 search_view(view, request);
1764 case REQ_SEARCH_BACK:
1773 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
1774 lineno += direction;
1776 /* Note, lineno is unsigned long so will wrap around in which case it
1777 * will become bigger than view->lines. */
1778 for (; lineno < view->lines; lineno += direction) {
1779 struct line *line = &view->line[lineno];
1781 if (find_next_line(view, lineno, line))
1785 report("No match found for '%s'", view->grep);
1789 search_view(struct view *view, enum request request)
1794 regfree(view->regex);
1797 view->regex = calloc(1, sizeof(*view->regex));
1802 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
1803 if (regex_err != 0) {
1804 char buf[SIZEOF_STR] = "unknown error";
1806 regerror(regex_err, view->regex, buf, sizeof(buf));
1807 report("Search failed: %s", buf);
1811 string_copy(view->grep, opt_search);
1813 find_next(view, request);
1817 * Incremental updating
1821 end_update(struct view *view)
1825 set_nonblocking_input(FALSE);
1826 if (view->pipe == stdin)
1834 begin_update(struct view *view)
1840 string_copy(view->cmd, opt_cmd);
1842 /* When running random commands, initially show the
1843 * command in the title. However, it maybe later be
1844 * overwritten if a commit line is selected. */
1845 if (view == VIEW(REQ_VIEW_PAGER))
1846 string_copy(view->ref, view->cmd);
1850 } else if (view == VIEW(REQ_VIEW_TREE)) {
1851 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1852 char path[SIZEOF_STR];
1854 if (strcmp(view->vid, view->id))
1855 opt_path[0] = path[0] = 0;
1856 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
1859 if (!string_format(view->cmd, format, view->id, path))
1863 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1864 const char *id = view->id;
1866 if (!string_format(view->cmd, format, id, id, id, id, id))
1869 /* Put the current ref_* value to the view title ref
1870 * member. This is needed by the blob view. Most other
1871 * views sets it automatically after loading because the
1872 * first line is a commit line. */
1873 string_copy_rev(view->ref, view->id);
1876 /* Special case for the pager view. */
1878 view->pipe = opt_pipe;
1881 view->pipe = popen(view->cmd, "r");
1887 set_nonblocking_input(TRUE);
1892 string_copy_rev(view->vid, view->id);
1897 for (i = 0; i < view->lines; i++)
1898 if (view->line[i].data)
1899 free(view->line[i].data);
1905 view->start_time = time(NULL);
1910 static struct line *
1911 realloc_lines(struct view *view, size_t line_size)
1913 struct line *tmp = realloc(view->line, sizeof(*view->line) * line_size);
1919 view->line_size = line_size;
1924 update_view(struct view *view)
1926 char in_buffer[BUFSIZ];
1927 char out_buffer[BUFSIZ * 2];
1929 /* The number of lines to read. If too low it will cause too much
1930 * redrawing (and possible flickering), if too high responsiveness
1932 unsigned long lines = view->height;
1933 int redraw_from = -1;
1938 /* Only redraw if lines are visible. */
1939 if (view->offset + view->height >= view->lines)
1940 redraw_from = view->lines - view->offset;
1942 /* FIXME: This is probably not perfect for backgrounded views. */
1943 if (!realloc_lines(view, view->lines + lines))
1946 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
1947 size_t linelen = strlen(line);
1950 line[linelen - 1] = 0;
1952 if (opt_iconv != ICONV_NONE) {
1953 ICONV_CONST char *inbuf = line;
1954 size_t inlen = linelen;
1956 char *outbuf = out_buffer;
1957 size_t outlen = sizeof(out_buffer);
1961 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
1962 if (ret != (size_t) -1) {
1964 linelen = strlen(out_buffer);
1968 if (!view->ops->read(view, line))
1978 lines = view->lines;
1979 for (digits = 0; lines; digits++)
1982 /* Keep the displayed view in sync with line number scaling. */
1983 if (digits != view->digits) {
1984 view->digits = digits;
1989 if (!view_is_displayed(view))
1992 if (view == VIEW(REQ_VIEW_TREE)) {
1993 /* Clear the view and redraw everything since the tree sorting
1994 * might have rearranged things. */
1997 } else if (redraw_from >= 0) {
1998 /* If this is an incremental update, redraw the previous line
1999 * since for commits some members could have changed when
2000 * loading the main view. */
2001 if (redraw_from > 0)
2004 /* Since revision graph visualization requires knowledge
2005 * about the parent commit, it causes a further one-off
2006 * needed to be redrawn for incremental updates. */
2007 if (redraw_from > 0 && opt_rev_graph)
2010 /* Incrementally draw avoids flickering. */
2011 redraw_view_from(view, redraw_from);
2014 /* Update the title _after_ the redraw so that if the redraw picks up a
2015 * commit reference in view->ref it'll be available here. */
2016 update_view_title(view);
2019 if (ferror(view->pipe)) {
2020 report("Failed to read: %s", strerror(errno));
2023 } else if (feof(view->pipe)) {
2031 report("Allocation failure");
2034 view->ops->read(view, NULL);
2039 static struct line *
2040 add_line_data(struct view *view, void *data, enum line_type type)
2042 struct line *line = &view->line[view->lines++];
2044 memset(line, 0, sizeof(*line));
2051 static struct line *
2052 add_line_text(struct view *view, char *data, enum line_type type)
2055 data = strdup(data);
2057 return data ? add_line_data(view, data, type) : NULL;
2066 OPEN_DEFAULT = 0, /* Use default view switching. */
2067 OPEN_SPLIT = 1, /* Split current view. */
2068 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2069 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2073 open_view(struct view *prev, enum request request, enum open_flags flags)
2075 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2076 bool split = !!(flags & OPEN_SPLIT);
2077 bool reload = !!(flags & OPEN_RELOAD);
2078 struct view *view = VIEW(request);
2079 int nviews = displayed_views();
2080 struct view *base_view = display[0];
2082 if (view == prev && nviews == 1 && !reload) {
2083 report("Already in %s view", view->name);
2087 if (view->ops->open) {
2088 if (!view->ops->open(view)) {
2089 report("Failed to load %s view", view->name);
2093 } else if ((reload || strcmp(view->vid, view->id)) &&
2094 !begin_update(view)) {
2095 report("Failed to load %s view", view->name);
2104 /* Maximize the current view. */
2105 memset(display, 0, sizeof(display));
2107 display[current_view] = view;
2110 /* Resize the view when switching between split- and full-screen,
2111 * or when switching between two different full-screen views. */
2112 if (nviews != displayed_views() ||
2113 (nviews == 1 && base_view != display[0]))
2116 if (split && prev->lineno - prev->offset >= prev->height) {
2117 /* Take the title line into account. */
2118 int lines = prev->lineno - prev->offset - prev->height + 1;
2120 /* Scroll the view that was split if the current line is
2121 * outside the new limited view. */
2122 do_scroll_view(prev, lines);
2125 if (prev && view != prev) {
2126 if (split && !backgrounded) {
2127 /* "Blur" the previous view. */
2128 update_view_title(prev);
2131 view->parent = prev;
2134 if (view->pipe && view->lines == 0) {
2135 /* Clear the old view and let the incremental updating refill
2144 /* If the view is backgrounded the above calls to report()
2145 * won't redraw the view title. */
2147 update_view_title(view);
2151 open_external_viewer(const char *cmd)
2153 def_prog_mode(); /* save current tty modes */
2154 endwin(); /* restore original tty modes */
2156 fprintf(stderr, "Press Enter to continue");
2163 open_mergetool(const char *file)
2165 char cmd[SIZEOF_STR];
2166 char file_sq[SIZEOF_STR];
2168 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2169 string_format(cmd, "git mergetool %s", file_sq)) {
2170 open_external_viewer(cmd);
2175 open_editor(bool from_root, const char *file)
2177 char cmd[SIZEOF_STR];
2178 char file_sq[SIZEOF_STR];
2180 char *prefix = from_root ? opt_cdup : "";
2182 editor = getenv("GIT_EDITOR");
2183 if (!editor && *opt_editor)
2184 editor = opt_editor;
2186 editor = getenv("VISUAL");
2188 editor = getenv("EDITOR");
2192 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2193 string_format(cmd, "%s %s%s", editor, prefix, file_sq)) {
2194 open_external_viewer(cmd);
2199 * User request switch noodle
2203 view_driver(struct view *view, enum request request)
2207 if (request == REQ_NONE) {
2212 if (view && view->lines) {
2213 request = view->ops->request(view, request, &view->line[view->lineno]);
2214 if (request == REQ_NONE)
2221 case REQ_MOVE_PAGE_UP:
2222 case REQ_MOVE_PAGE_DOWN:
2223 case REQ_MOVE_FIRST_LINE:
2224 case REQ_MOVE_LAST_LINE:
2225 move_view(view, request);
2228 case REQ_SCROLL_LINE_DOWN:
2229 case REQ_SCROLL_LINE_UP:
2230 case REQ_SCROLL_PAGE_DOWN:
2231 case REQ_SCROLL_PAGE_UP:
2232 scroll_view(view, request);
2237 report("No file chosen, press %s to open tree view",
2238 get_key(REQ_VIEW_TREE));
2241 open_view(view, request, OPEN_DEFAULT);
2244 case REQ_VIEW_PAGER:
2245 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2246 report("No pager content, press %s to run command from prompt",
2247 get_key(REQ_PROMPT));
2250 open_view(view, request, OPEN_DEFAULT);
2253 case REQ_VIEW_STAGE:
2254 if (!VIEW(REQ_VIEW_STAGE)->lines) {
2255 report("No stage content, press %s to open the status view and choose file",
2256 get_key(REQ_VIEW_STATUS));
2259 open_view(view, request, OPEN_DEFAULT);
2262 case REQ_VIEW_STATUS:
2263 if (opt_is_inside_work_tree == FALSE) {
2264 report("The status view requires a working tree");
2267 open_view(view, request, OPEN_DEFAULT);
2275 open_view(view, request, OPEN_DEFAULT);
2280 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2282 if ((view == VIEW(REQ_VIEW_DIFF) &&
2283 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2284 (view == VIEW(REQ_VIEW_STAGE) &&
2285 view->parent == VIEW(REQ_VIEW_STATUS)) ||
2286 (view == VIEW(REQ_VIEW_BLOB) &&
2287 view->parent == VIEW(REQ_VIEW_TREE))) {
2290 view = view->parent;
2291 line = view->lineno;
2292 move_view(view, request);
2293 if (view_is_displayed(view))
2294 update_view_title(view);
2295 if (line != view->lineno)
2296 view->ops->request(view, REQ_ENTER,
2297 &view->line[view->lineno]);
2300 move_view(view, request);
2306 int nviews = displayed_views();
2307 int next_view = (current_view + 1) % nviews;
2309 if (next_view == current_view) {
2310 report("Only one view is displayed");
2314 current_view = next_view;
2315 /* Blur out the title of the previous view. */
2316 update_view_title(view);
2321 report("Refreshing is not yet supported for the %s view", view->name);
2324 case REQ_TOGGLE_LINENO:
2325 opt_line_number = !opt_line_number;
2329 case REQ_TOGGLE_REV_GRAPH:
2330 opt_rev_graph = !opt_rev_graph;
2335 /* Always reload^Wrerun commands from the prompt. */
2336 open_view(view, opt_request, OPEN_RELOAD);
2340 case REQ_SEARCH_BACK:
2341 search_view(view, request);
2346 find_next(view, request);
2349 case REQ_STOP_LOADING:
2350 for (i = 0; i < ARRAY_SIZE(views); i++) {
2353 report("Stopped loading the %s view", view->name),
2358 case REQ_SHOW_VERSION:
2359 report("tig-%s (built %s)", TIG_VERSION, __DATE__);
2362 case REQ_SCREEN_RESIZE:
2365 case REQ_SCREEN_REDRAW:
2370 report("Nothing to edit");
2373 case REQ_CHERRY_PICK:
2374 report("Nothing to cherry-pick");
2378 report("Nothing to enter");
2382 case REQ_VIEW_CLOSE:
2383 /* XXX: Mark closed views by letting view->parent point to the
2384 * view itself. Parents to closed view should never be
2387 view->parent->parent != view->parent) {
2388 memset(display, 0, sizeof(display));
2390 display[current_view] = view->parent;
2391 view->parent = view;
2401 /* An unknown key will show most commonly used commands. */
2402 report("Unknown key, press 'h' for help");
2415 pager_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2417 char *text = line->data;
2418 enum line_type type = line->type;
2419 int textlen = strlen(text);
2422 wmove(view->win, lineno, 0);
2426 wchgat(view->win, -1, 0, type, NULL);
2429 attr = get_line_attr(type);
2430 wattrset(view->win, attr);
2432 if (opt_line_number || opt_tab_size < TABSIZE) {
2433 static char spaces[] = " ";
2434 int col_offset = 0, col = 0;
2436 if (opt_line_number) {
2437 unsigned long real_lineno = view->offset + lineno + 1;
2439 if (real_lineno == 1 ||
2440 (real_lineno % opt_num_interval) == 0) {
2441 wprintw(view->win, "%.*d", view->digits, real_lineno);
2444 waddnstr(view->win, spaces,
2445 MIN(view->digits, STRING_SIZE(spaces)));
2447 waddstr(view->win, ": ");
2448 col_offset = view->digits + 2;
2451 while (text && col_offset + col < view->width) {
2452 int cols_max = view->width - col_offset - col;
2456 if (*text == '\t') {
2458 assert(sizeof(spaces) > TABSIZE);
2460 cols = opt_tab_size - (col % opt_tab_size);
2463 text = strchr(text, '\t');
2464 cols = line ? text - pos : strlen(pos);
2467 waddnstr(view->win, pos, MIN(cols, cols_max));
2472 int col = 0, pos = 0;
2474 for (; pos < textlen && col < view->width; pos++, col++)
2475 if (text[pos] == '\t')
2476 col += TABSIZE - (col % TABSIZE) - 1;
2478 waddnstr(view->win, text, pos);
2485 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2487 char refbuf[SIZEOF_STR];
2491 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2494 pipe = popen(refbuf, "r");
2498 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2499 ref = chomp_string(ref);
2505 /* This is the only fatal call, since it can "corrupt" the buffer. */
2506 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2513 add_pager_refs(struct view *view, struct line *line)
2515 char buf[SIZEOF_STR];
2516 char *commit_id = line->data + STRING_SIZE("commit ");
2518 size_t bufpos = 0, refpos = 0;
2519 const char *sep = "Refs: ";
2520 bool is_tag = FALSE;
2522 assert(line->type == LINE_COMMIT);
2524 refs = get_refs(commit_id);
2526 if (view == VIEW(REQ_VIEW_DIFF))
2527 goto try_add_describe_ref;
2532 struct ref *ref = refs[refpos];
2533 char *fmt = ref->tag ? "%s[%s]" :
2534 ref->remote ? "%s<%s>" : "%s%s";
2536 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2541 } while (refs[refpos++]->next);
2543 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2544 try_add_describe_ref:
2545 /* Add <tag>-g<commit_id> "fake" reference. */
2546 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2553 if (!realloc_lines(view, view->line_size + 1))
2556 add_line_text(view, buf, LINE_PP_REFS);
2560 pager_read(struct view *view, char *data)
2567 line = add_line_text(view, data, get_line_type(data));
2571 if (line->type == LINE_COMMIT &&
2572 (view == VIEW(REQ_VIEW_DIFF) ||
2573 view == VIEW(REQ_VIEW_LOG)))
2574 add_pager_refs(view, line);
2580 pager_request(struct view *view, enum request request, struct line *line)
2584 if (request != REQ_ENTER)
2587 if (line->type == LINE_COMMIT &&
2588 (view == VIEW(REQ_VIEW_LOG) ||
2589 view == VIEW(REQ_VIEW_PAGER))) {
2590 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2594 /* Always scroll the view even if it was split. That way
2595 * you can use Enter to scroll through the log view and
2596 * split open each commit diff. */
2597 scroll_view(view, REQ_SCROLL_LINE_DOWN);
2599 /* FIXME: A minor workaround. Scrolling the view will call report("")
2600 * but if we are scrolling a non-current view this won't properly
2601 * update the view title. */
2603 update_view_title(view);
2609 pager_grep(struct view *view, struct line *line)
2612 char *text = line->data;
2617 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
2624 pager_select(struct view *view, struct line *line)
2626 if (line->type == LINE_COMMIT) {
2627 char *text = line->data + STRING_SIZE("commit ");
2629 if (view != VIEW(REQ_VIEW_PAGER))
2630 string_copy_rev(view->ref, text);
2631 string_copy_rev(ref_commit, text);
2635 static struct view_ops pager_ops = {
2651 help_open(struct view *view)
2654 int lines = ARRAY_SIZE(req_info) + 2;
2657 if (view->lines > 0)
2660 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2661 if (!req_info[i].request)
2664 view->line = calloc(lines, sizeof(*view->line));
2668 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
2670 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2673 if (req_info[i].request == REQ_NONE)
2676 if (!req_info[i].request) {
2677 add_line_text(view, "", LINE_DEFAULT);
2678 add_line_text(view, req_info[i].help, LINE_DEFAULT);
2682 key = get_key(req_info[i].request);
2684 key = "(no key defined)";
2686 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
2689 add_line_text(view, buf, LINE_DEFAULT);
2695 static struct view_ops help_ops = {
2710 struct tree_stack_entry {
2711 struct tree_stack_entry *prev; /* Entry below this in the stack */
2712 unsigned long lineno; /* Line number to restore */
2713 char *name; /* Position of name in opt_path */
2716 /* The top of the path stack. */
2717 static struct tree_stack_entry *tree_stack = NULL;
2718 unsigned long tree_lineno = 0;
2721 pop_tree_stack_entry(void)
2723 struct tree_stack_entry *entry = tree_stack;
2725 tree_lineno = entry->lineno;
2727 tree_stack = entry->prev;
2732 push_tree_stack_entry(char *name, unsigned long lineno)
2734 struct tree_stack_entry *entry = calloc(1, sizeof(*entry));
2735 size_t pathlen = strlen(opt_path);
2740 entry->prev = tree_stack;
2741 entry->name = opt_path + pathlen;
2744 if (!string_format_from(opt_path, &pathlen, "%s/", name)) {
2745 pop_tree_stack_entry();
2749 /* Move the current line to the first tree entry. */
2751 entry->lineno = lineno;
2754 /* Parse output from git-ls-tree(1):
2756 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
2757 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
2758 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
2759 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
2762 #define SIZEOF_TREE_ATTR \
2763 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
2765 #define TREE_UP_FORMAT "040000 tree %s\t.."
2768 tree_compare_entry(enum line_type type1, char *name1,
2769 enum line_type type2, char *name2)
2771 if (type1 != type2) {
2772 if (type1 == LINE_TREE_DIR)
2777 return strcmp(name1, name2);
2781 tree_read(struct view *view, char *text)
2783 size_t textlen = text ? strlen(text) : 0;
2784 char buf[SIZEOF_STR];
2786 enum line_type type;
2787 bool first_read = view->lines == 0;
2789 if (textlen <= SIZEOF_TREE_ATTR)
2792 type = text[STRING_SIZE("100644 ")] == 't'
2793 ? LINE_TREE_DIR : LINE_TREE_FILE;
2796 /* Add path info line */
2797 if (!string_format(buf, "Directory path /%s", opt_path) ||
2798 !realloc_lines(view, view->line_size + 1) ||
2799 !add_line_text(view, buf, LINE_DEFAULT))
2802 /* Insert "link" to parent directory. */
2804 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
2805 !realloc_lines(view, view->line_size + 1) ||
2806 !add_line_text(view, buf, LINE_TREE_DIR))
2811 /* Strip the path part ... */
2813 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
2814 size_t striplen = strlen(opt_path);
2815 char *path = text + SIZEOF_TREE_ATTR;
2817 if (pathlen > striplen)
2818 memmove(path, path + striplen,
2819 pathlen - striplen + 1);
2822 /* Skip "Directory ..." and ".." line. */
2823 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
2824 struct line *line = &view->line[pos];
2825 char *path1 = ((char *) line->data) + SIZEOF_TREE_ATTR;
2826 char *path2 = text + SIZEOF_TREE_ATTR;
2827 int cmp = tree_compare_entry(line->type, path1, type, path2);
2832 text = strdup(text);
2836 if (view->lines > pos)
2837 memmove(&view->line[pos + 1], &view->line[pos],
2838 (view->lines - pos) * sizeof(*line));
2840 line = &view->line[pos];
2847 if (!add_line_text(view, text, type))
2850 if (tree_lineno > view->lineno) {
2851 view->lineno = tree_lineno;
2859 tree_request(struct view *view, enum request request, struct line *line)
2861 enum open_flags flags;
2863 if (request != REQ_ENTER)
2866 /* Cleanup the stack if the tree view is at a different tree. */
2867 while (!*opt_path && tree_stack)
2868 pop_tree_stack_entry();
2870 switch (line->type) {
2872 /* Depending on whether it is a subdir or parent (updir?) link
2873 * mangle the path buffer. */
2874 if (line == &view->line[1] && *opt_path) {
2875 pop_tree_stack_entry();
2878 char *data = line->data;
2879 char *basename = data + SIZEOF_TREE_ATTR;
2881 push_tree_stack_entry(basename, view->lineno);
2884 /* Trees and subtrees share the same ID, so they are not not
2885 * unique like blobs. */
2886 flags = OPEN_RELOAD;
2887 request = REQ_VIEW_TREE;
2890 case LINE_TREE_FILE:
2891 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
2892 request = REQ_VIEW_BLOB;
2899 open_view(view, request, flags);
2900 if (request == REQ_VIEW_TREE) {
2901 view->lineno = tree_lineno;
2908 tree_select(struct view *view, struct line *line)
2910 char *text = line->data + STRING_SIZE("100644 blob ");
2912 if (line->type == LINE_TREE_FILE) {
2913 string_copy_rev(ref_blob, text);
2915 } else if (line->type != LINE_TREE_DIR) {
2919 string_copy_rev(view->ref, text);
2922 static struct view_ops tree_ops = {
2933 blob_read(struct view *view, char *line)
2935 return add_line_text(view, line, LINE_DEFAULT) != NULL;
2938 static struct view_ops blob_ops = {
2957 char rev[SIZEOF_REV];
2961 char rev[SIZEOF_REV];
2963 char name[SIZEOF_STR];
2966 static struct status stage_status;
2967 static enum line_type stage_line_type;
2969 /* Get fields from the diff line:
2970 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
2973 status_get_diff(struct status *file, char *buf, size_t bufsize)
2975 char *old_mode = buf + 1;
2976 char *new_mode = buf + 8;
2977 char *old_rev = buf + 15;
2978 char *new_rev = buf + 56;
2979 char *status = buf + 97;
2981 if (bufsize != 99 ||
2982 old_mode[-1] != ':' ||
2983 new_mode[-1] != ' ' ||
2984 old_rev[-1] != ' ' ||
2985 new_rev[-1] != ' ' ||
2989 file->status = *status;
2991 string_copy_rev(file->old.rev, old_rev);
2992 string_copy_rev(file->new.rev, new_rev);
2994 file->old.mode = strtoul(old_mode, NULL, 8);
2995 file->new.mode = strtoul(new_mode, NULL, 8);
3003 status_run(struct view *view, const char cmd[], bool diff, enum line_type type)
3005 struct status *file = NULL;
3006 struct status *unmerged = NULL;
3007 char buf[SIZEOF_STR * 4];
3011 pipe = popen(cmd, "r");
3015 add_line_data(view, NULL, type);
3017 while (!feof(pipe) && !ferror(pipe)) {
3021 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
3024 bufsize += readsize;
3026 /* Process while we have NUL chars. */
3027 while ((sep = memchr(buf, 0, bufsize))) {
3028 size_t sepsize = sep - buf + 1;
3031 if (!realloc_lines(view, view->line_size + 1))
3034 file = calloc(1, sizeof(*file));
3038 add_line_data(view, file, type);
3041 /* Parse diff info part. */
3045 } else if (!file->status) {
3046 if (!status_get_diff(file, buf, sepsize))
3050 memmove(buf, sep + 1, bufsize);
3052 sep = memchr(buf, 0, bufsize);
3055 sepsize = sep - buf + 1;
3057 /* Collapse all 'M'odified entries that
3058 * follow a associated 'U'nmerged entry.
3060 if (file->status == 'U') {
3063 } else if (unmerged) {
3064 int collapse = !strcmp(buf, unmerged->name);
3075 /* git-ls-files just delivers a NUL separated
3076 * list of file names similar to the second half
3077 * of the git-diff-* output. */
3078 string_ncopy(file->name, buf, sepsize);
3080 memmove(buf, sep + 1, bufsize);
3091 if (!view->line[view->lines - 1].data)
3092 add_line_data(view, NULL, LINE_STAT_NONE);
3098 /* Don't show unmerged entries in the staged section. */
3099 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached HEAD"
3100 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
3101 #define STATUS_LIST_OTHER_CMD \
3102 "git ls-files -z --others --exclude-per-directory=.gitignore"
3104 #define STATUS_DIFF_SHOW_CMD \
3105 "git diff --root --patch-with-stat --find-copies-harder -B -C %s -- %s 2>/dev/null"
3107 /* First parse staged info using git-diff-index(1), then parse unstaged
3108 * info using git-diff-files(1), and finally untracked files using
3109 * git-ls-files(1). */
3111 status_open(struct view *view)
3113 struct stat statbuf;
3114 char exclude[SIZEOF_STR];
3115 char cmd[SIZEOF_STR];
3116 unsigned long prev_lineno = view->lineno;
3119 for (i = 0; i < view->lines; i++)
3120 free(view->line[i].data);
3122 view->lines = view->line_size = view->lineno = 0;
3125 if (!realloc_lines(view, view->line_size + 6))
3128 if (!string_format(exclude, "%s/info/exclude", opt_git_dir))
3131 string_copy(cmd, STATUS_LIST_OTHER_CMD);
3133 if (stat(exclude, &statbuf) >= 0) {
3134 size_t cmdsize = strlen(cmd);
3136 if (!string_format_from(cmd, &cmdsize, " %s", "--exclude-from=") ||
3137 sq_quote(cmd, cmdsize, exclude) >= sizeof(cmd))
3141 if (!status_run(view, STATUS_DIFF_INDEX_CMD, TRUE, LINE_STAT_STAGED) ||
3142 !status_run(view, STATUS_DIFF_FILES_CMD, TRUE, LINE_STAT_UNSTAGED) ||
3143 !status_run(view, cmd, FALSE, LINE_STAT_UNTRACKED))
3146 /* If all went well restore the previous line number to stay in
3148 if (prev_lineno < view->lines)
3149 view->lineno = prev_lineno;
3151 view->lineno = view->lines - 1;
3157 status_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
3159 struct status *status = line->data;
3161 wmove(view->win, lineno, 0);
3164 wattrset(view->win, get_line_attr(LINE_CURSOR));
3165 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
3167 } else if (!status && line->type != LINE_STAT_NONE) {
3168 wattrset(view->win, get_line_attr(LINE_STAT_SECTION));
3169 wchgat(view->win, -1, 0, LINE_STAT_SECTION, NULL);
3172 wattrset(view->win, get_line_attr(line->type));
3178 switch (line->type) {
3179 case LINE_STAT_STAGED:
3180 text = "Changes to be committed:";
3183 case LINE_STAT_UNSTAGED:
3184 text = "Changed but not updated:";
3187 case LINE_STAT_UNTRACKED:
3188 text = "Untracked files:";
3191 case LINE_STAT_NONE:
3192 text = " (no files)";
3199 waddstr(view->win, text);
3203 waddch(view->win, status->status);
3205 wattrset(view->win, A_NORMAL);
3206 wmove(view->win, lineno, 4);
3207 waddstr(view->win, status->name);
3213 status_enter(struct view *view, struct line *line)
3215 struct status *status = line->data;
3216 char path[SIZEOF_STR] = "";
3220 if (line->type == LINE_STAT_NONE ||
3221 (!status && line[1].type == LINE_STAT_NONE)) {
3222 report("No file to diff");
3226 if (status && sq_quote(path, 0, status->name) >= sizeof(path))
3230 line->type != LINE_STAT_UNTRACKED &&
3231 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
3234 switch (line->type) {
3235 case LINE_STAT_STAGED:
3236 if (!string_format_from(opt_cmd, &cmdsize, STATUS_DIFF_SHOW_CMD,
3240 info = "Staged changes to %s";
3242 info = "Staged changes";
3245 case LINE_STAT_UNSTAGED:
3246 if (!string_format_from(opt_cmd, &cmdsize, STATUS_DIFF_SHOW_CMD,
3250 info = "Unstaged changes to %s";
3252 info = "Unstaged changes";
3255 case LINE_STAT_UNTRACKED:
3261 report("No file to show");
3265 opt_pipe = fopen(status->name, "r");
3266 info = "Untracked file %s";
3273 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | OPEN_SPLIT);
3274 if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) {
3276 stage_status = *status;
3278 memset(&stage_status, 0, sizeof(stage_status));
3281 stage_line_type = line->type;
3282 string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.name);
3290 status_update_file(struct view *view, struct status *status, enum line_type type)
3292 char cmd[SIZEOF_STR];
3293 char buf[SIZEOF_STR];
3300 type != LINE_STAT_UNTRACKED &&
3301 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
3305 case LINE_STAT_STAGED:
3306 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
3312 string_add(cmd, cmdsize, "git update-index -z --index-info");
3315 case LINE_STAT_UNSTAGED:
3316 case LINE_STAT_UNTRACKED:
3317 if (!string_format_from(buf, &bufsize, "%s%c", status->name, 0))
3320 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
3327 pipe = popen(cmd, "w");
3331 while (!ferror(pipe) && written < bufsize) {
3332 written += fwrite(buf + written, 1, bufsize - written, pipe);
3337 if (written != bufsize)
3344 status_update(struct view *view)
3346 struct line *line = &view->line[view->lineno];
3348 assert(view->lines);
3351 while (++line < view->line + view->lines && line->data) {
3352 if (!status_update_file(view, line->data, line->type))
3353 report("Failed to update file status");
3356 if (!line[-1].data) {
3357 report("Nothing to update");
3361 } else if (!status_update_file(view, line->data, line->type)) {
3362 report("Failed to update file status");
3367 status_request(struct view *view, enum request request, struct line *line)
3369 struct status *status = line->data;
3372 case REQ_STATUS_UPDATE:
3373 status_update(view);
3376 case REQ_STATUS_MERGE:
3377 open_mergetool(status->name);
3384 open_editor(status->status != '?', status->name);
3388 /* After returning the status view has been split to
3389 * show the stage view. No further reloading is
3391 status_enter(view, line);
3395 /* Simply reload the view. */
3402 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
3408 status_select(struct view *view, struct line *line)
3410 struct status *status = line->data;
3411 char file[SIZEOF_STR] = "all files";
3415 if (status && !string_format(file, "'%s'", status->name))
3418 if (!status && line[1].type == LINE_STAT_NONE)
3421 switch (line->type) {
3422 case LINE_STAT_STAGED:
3423 text = "Press %s to unstage %s for commit";
3426 case LINE_STAT_UNSTAGED:
3427 text = "Press %s to stage %s for commit";
3430 case LINE_STAT_UNTRACKED:
3431 text = "Press %s to stage %s for addition";
3434 case LINE_STAT_NONE:
3435 text = "Nothing to update";
3442 if (status && status->status == 'U') {
3443 text = "Press %s to resolve conflict in %s";
3444 key = get_key(REQ_STATUS_MERGE);
3447 key = get_key(REQ_STATUS_UPDATE);
3450 string_format(view->ref, text, key, file);
3454 status_grep(struct view *view, struct line *line)
3456 struct status *status = line->data;
3457 enum { S_STATUS, S_NAME, S_END } state;
3464 for (state = S_STATUS; state < S_END; state++) {
3468 case S_NAME: text = status->name; break;
3470 buf[0] = status->status;
3478 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3485 static struct view_ops status_ops = {
3497 stage_diff_line(FILE *pipe, struct line *line)
3499 char *buf = line->data;
3500 size_t bufsize = strlen(buf);
3503 while (!ferror(pipe) && written < bufsize) {
3504 written += fwrite(buf + written, 1, bufsize - written, pipe);
3509 return written == bufsize;
3512 static struct line *
3513 stage_diff_hdr(struct view *view, struct line *line)
3515 int diff_hdr_dir = line->type == LINE_DIFF_CHUNK ? -1 : 1;
3516 struct line *diff_hdr;
3518 if (line->type == LINE_DIFF_CHUNK)
3519 diff_hdr = line - 1;
3521 diff_hdr = view->line + 1;
3523 while (diff_hdr > view->line && diff_hdr < view->line + view->lines) {
3524 if (diff_hdr->type == LINE_DIFF_HEADER)
3527 diff_hdr += diff_hdr_dir;
3534 stage_update_chunk(struct view *view, struct line *line)
3536 char cmd[SIZEOF_STR];
3538 struct line *diff_hdr, *diff_chunk, *diff_end;
3541 diff_hdr = stage_diff_hdr(view, line);
3546 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
3549 if (!string_format_from(cmd, &cmdsize,
3550 "git apply --cached %s - && "
3551 "git update-index -q --unmerged --refresh 2>/dev/null",
3552 stage_line_type == LINE_STAT_STAGED ? "-R" : ""))
3555 pipe = popen(cmd, "w");
3559 diff_end = view->line + view->lines;
3560 if (line->type != LINE_DIFF_CHUNK) {
3561 diff_chunk = diff_hdr;
3564 for (diff_chunk = line + 1; diff_chunk < diff_end; diff_chunk++)
3565 if (diff_chunk->type == LINE_DIFF_CHUNK ||
3566 diff_chunk->type == LINE_DIFF_HEADER)
3567 diff_end = diff_chunk;
3571 while (diff_hdr->type != LINE_DIFF_CHUNK) {
3572 switch (diff_hdr->type) {
3573 case LINE_DIFF_HEADER:
3574 case LINE_DIFF_INDEX:
3584 if (!stage_diff_line(pipe, diff_hdr++)) {
3591 while (diff_chunk < diff_end && stage_diff_line(pipe, diff_chunk))
3596 if (diff_chunk != diff_end)
3603 stage_update(struct view *view, struct line *line)
3605 if (stage_line_type != LINE_STAT_UNTRACKED &&
3606 (line->type == LINE_DIFF_CHUNK || !stage_status.status)) {
3607 if (!stage_update_chunk(view, line)) {
3608 report("Failed to apply chunk");
3612 } else if (!status_update_file(view, &stage_status, stage_line_type)) {
3613 report("Failed to update file");
3617 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
3619 view = VIEW(REQ_VIEW_STATUS);
3620 if (view_is_displayed(view))
3621 status_enter(view, &view->line[view->lineno]);
3625 stage_request(struct view *view, enum request request, struct line *line)
3628 case REQ_STATUS_UPDATE:
3629 stage_update(view, line);
3633 if (!stage_status.name[0])
3636 open_editor(stage_status.status != '?', stage_status.name);
3640 pager_request(view, request, line);
3650 static struct view_ops stage_ops = {
3666 char id[SIZEOF_REV]; /* SHA1 ID. */
3667 char title[128]; /* First line of the commit message. */
3668 char author[75]; /* Author of the commit. */
3669 struct tm time; /* Date from the author ident. */
3670 struct ref **refs; /* Repository references. */
3671 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
3672 size_t graph_size; /* The width of the graph array. */
3675 /* Size of rev graph with no "padding" columns */
3676 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
3679 struct rev_graph *prev, *next, *parents;
3680 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
3682 struct commit *commit;
3686 /* Parents of the commit being visualized. */
3687 static struct rev_graph graph_parents[4];
3689 /* The current stack of revisions on the graph. */
3690 static struct rev_graph graph_stacks[4] = {
3691 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
3692 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
3693 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
3694 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
3698 graph_parent_is_merge(struct rev_graph *graph)
3700 return graph->parents->size > 1;
3704 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
3706 struct commit *commit = graph->commit;
3708 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
3709 commit->graph[commit->graph_size++] = symbol;
3713 done_rev_graph(struct rev_graph *graph)
3715 if (graph_parent_is_merge(graph) &&
3716 graph->pos < graph->size - 1 &&
3717 graph->next->size == graph->size + graph->parents->size - 1) {
3718 size_t i = graph->pos + graph->parents->size - 1;
3720 graph->commit->graph_size = i * 2;
3721 while (i < graph->next->size - 1) {
3722 append_to_rev_graph(graph, ' ');
3723 append_to_rev_graph(graph, '\\');
3728 graph->size = graph->pos = 0;
3729 graph->commit = NULL;
3730 memset(graph->parents, 0, sizeof(*graph->parents));
3734 push_rev_graph(struct rev_graph *graph, char *parent)
3738 /* "Collapse" duplicate parents lines.
3740 * FIXME: This needs to also update update the drawn graph but
3741 * for now it just serves as a method for pruning graph lines. */
3742 for (i = 0; i < graph->size; i++)
3743 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
3746 if (graph->size < SIZEOF_REVITEMS) {
3747 string_copy_rev(graph->rev[graph->size++], parent);
3752 get_rev_graph_symbol(struct rev_graph *graph)
3756 if (graph->parents->size == 0)
3757 symbol = REVGRAPH_INIT;
3758 else if (graph_parent_is_merge(graph))
3759 symbol = REVGRAPH_MERGE;
3760 else if (graph->pos >= graph->size)
3761 symbol = REVGRAPH_BRANCH;
3763 symbol = REVGRAPH_COMMIT;
3769 draw_rev_graph(struct rev_graph *graph)
3772 chtype separator, line;
3774 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
3775 static struct rev_filler fillers[] = {
3776 { ' ', REVGRAPH_LINE },
3781 chtype symbol = get_rev_graph_symbol(graph);
3782 struct rev_filler *filler;
3785 filler = &fillers[DEFAULT];
3787 for (i = 0; i < graph->pos; i++) {
3788 append_to_rev_graph(graph, filler->line);
3789 if (graph_parent_is_merge(graph->prev) &&
3790 graph->prev->pos == i)
3791 filler = &fillers[RSHARP];
3793 append_to_rev_graph(graph, filler->separator);
3796 /* Place the symbol for this revision. */
3797 append_to_rev_graph(graph, symbol);
3799 if (graph->prev->size > graph->size)
3800 filler = &fillers[RDIAG];
3802 filler = &fillers[DEFAULT];
3806 for (; i < graph->size; i++) {
3807 append_to_rev_graph(graph, filler->separator);
3808 append_to_rev_graph(graph, filler->line);
3809 if (graph_parent_is_merge(graph->prev) &&
3810 i < graph->prev->pos + graph->parents->size)
3811 filler = &fillers[RSHARP];
3812 if (graph->prev->size > graph->size)
3813 filler = &fillers[LDIAG];
3816 if (graph->prev->size > graph->size) {
3817 append_to_rev_graph(graph, filler->separator);
3818 if (filler->line != ' ')
3819 append_to_rev_graph(graph, filler->line);
3823 /* Prepare the next rev graph */
3825 prepare_rev_graph(struct rev_graph *graph)
3829 /* First, traverse all lines of revisions up to the active one. */
3830 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
3831 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
3834 push_rev_graph(graph->next, graph->rev[graph->pos]);
3837 /* Interleave the new revision parent(s). */
3838 for (i = 0; i < graph->parents->size; i++)
3839 push_rev_graph(graph->next, graph->parents->rev[i]);
3841 /* Lastly, put any remaining revisions. */
3842 for (i = graph->pos + 1; i < graph->size; i++)
3843 push_rev_graph(graph->next, graph->rev[i]);
3847 update_rev_graph(struct rev_graph *graph)
3849 /* If this is the finalizing update ... */
3851 prepare_rev_graph(graph);
3853 /* Graph visualization needs a one rev look-ahead,
3854 * so the first update doesn't visualize anything. */
3855 if (!graph->prev->commit)
3858 draw_rev_graph(graph->prev);
3859 done_rev_graph(graph->prev->prev);
3868 main_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
3870 char buf[DATE_COLS + 1];
3871 struct commit *commit = line->data;
3872 enum line_type type;
3878 if (!*commit->author)
3881 wmove(view->win, lineno, col);
3885 wattrset(view->win, get_line_attr(type));
3886 wchgat(view->win, -1, 0, type, NULL);
3889 type = LINE_MAIN_COMMIT;
3890 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
3893 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
3894 waddnstr(view->win, buf, timelen);
3895 waddstr(view->win, " ");
3898 wmove(view->win, lineno, col);
3899 if (type != LINE_CURSOR)
3900 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
3903 authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
3905 authorlen = strlen(commit->author);
3906 if (authorlen > AUTHOR_COLS - 2) {
3907 authorlen = AUTHOR_COLS - 2;
3913 waddnstr(view->win, commit->author, authorlen);
3914 if (type != LINE_CURSOR)
3915 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
3916 waddch(view->win, '~');
3918 waddstr(view->win, commit->author);
3922 if (type != LINE_CURSOR)
3923 wattrset(view->win, A_NORMAL);
3925 if (opt_rev_graph && commit->graph_size) {
3928 wmove(view->win, lineno, col);
3929 /* Using waddch() instead of waddnstr() ensures that
3930 * they'll be rendered correctly for the cursor line. */
3931 for (i = 0; i < commit->graph_size; i++)
3932 waddch(view->win, commit->graph[i]);
3934 waddch(view->win, ' ');
3935 col += commit->graph_size + 1;
3938 wmove(view->win, lineno, col);
3944 if (type == LINE_CURSOR)
3946 else if (commit->refs[i]->tag)
3947 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
3948 else if (commit->refs[i]->remote)
3949 wattrset(view->win, get_line_attr(LINE_MAIN_REMOTE));
3951 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
3952 waddstr(view->win, "[");
3953 waddstr(view->win, commit->refs[i]->name);
3954 waddstr(view->win, "]");
3955 if (type != LINE_CURSOR)
3956 wattrset(view->win, A_NORMAL);
3957 waddstr(view->win, " ");
3958 col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
3959 } while (commit->refs[i++]->next);
3962 if (type != LINE_CURSOR)
3963 wattrset(view->win, get_line_attr(type));
3966 int titlelen = strlen(commit->title);
3968 if (col + titlelen > view->width)
3969 titlelen = view->width - col;
3971 waddnstr(view->win, commit->title, titlelen);
3977 /* Reads git log --pretty=raw output and parses it into the commit struct. */
3979 main_read(struct view *view, char *line)
3981 static struct rev_graph *graph = graph_stacks;
3982 enum line_type type;
3983 struct commit *commit;
3986 update_rev_graph(graph);
3990 type = get_line_type(line);
3991 if (type == LINE_COMMIT) {
3992 commit = calloc(1, sizeof(struct commit));
3996 string_copy_rev(commit->id, line + STRING_SIZE("commit "));
3997 commit->refs = get_refs(commit->id);
3998 graph->commit = commit;
3999 add_line_data(view, commit, LINE_MAIN_COMMIT);
4005 commit = view->line[view->lines - 1].data;
4009 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
4014 /* Parse author lines where the name may be empty:
4015 * author <email@address.tld> 1138474660 +0100
4017 char *ident = line + STRING_SIZE("author ");
4018 char *nameend = strchr(ident, '<');
4019 char *emailend = strchr(ident, '>');
4021 if (!nameend || !emailend)
4024 update_rev_graph(graph);
4025 graph = graph->next;
4027 *nameend = *emailend = 0;
4028 ident = chomp_string(ident);
4030 ident = chomp_string(nameend + 1);
4035 string_ncopy(commit->author, ident, strlen(ident));
4037 /* Parse epoch and timezone */
4038 if (emailend[1] == ' ') {
4039 char *secs = emailend + 2;
4040 char *zone = strchr(secs, ' ');
4041 time_t time = (time_t) atol(secs);
4043 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
4047 tz = ('0' - zone[1]) * 60 * 60 * 10;
4048 tz += ('0' - zone[2]) * 60 * 60;
4049 tz += ('0' - zone[3]) * 60;
4050 tz += ('0' - zone[4]) * 60;
4058 gmtime_r(&time, &commit->time);
4063 /* Fill in the commit title if it has not already been set. */
4064 if (commit->title[0])
4067 /* Require titles to start with a non-space character at the
4068 * offset used by git log. */
4069 if (strncmp(line, " ", 4))
4072 /* Well, if the title starts with a whitespace character,
4073 * try to be forgiving. Otherwise we end up with no title. */
4074 while (isspace(*line))
4078 /* FIXME: More graceful handling of titles; append "..." to
4079 * shortened titles, etc. */
4081 string_ncopy(commit->title, line, strlen(line));
4088 cherry_pick_commit(struct commit *commit)
4090 char cmd[SIZEOF_STR];
4091 char *cherry_pick = getenv("TIG_CHERRY_PICK");
4094 cherry_pick = "git cherry-pick";
4096 if (string_format(cmd, "%s %s", cherry_pick, commit->id)) {
4097 open_external_viewer(cmd);
4102 main_request(struct view *view, enum request request, struct line *line)
4104 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
4106 if (request == REQ_ENTER)
4107 open_view(view, REQ_VIEW_DIFF, flags);
4108 else if (request == REQ_CHERRY_PICK)
4109 cherry_pick_commit(line->data);
4117 main_grep(struct view *view, struct line *line)
4119 struct commit *commit = line->data;
4120 enum { S_TITLE, S_AUTHOR, S_DATE, S_END } state;
4121 char buf[DATE_COLS + 1];
4124 for (state = S_TITLE; state < S_END; state++) {
4128 case S_TITLE: text = commit->title; break;
4129 case S_AUTHOR: text = commit->author; break;
4131 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
4140 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4148 main_select(struct view *view, struct line *line)
4150 struct commit *commit = line->data;
4152 string_copy_rev(view->ref, commit->id);
4153 string_copy_rev(ref_commit, view->ref);
4156 static struct view_ops main_ops = {
4168 * Unicode / UTF-8 handling
4170 * NOTE: Much of the following code for dealing with unicode is derived from
4171 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
4172 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
4175 /* I've (over)annotated a lot of code snippets because I am not entirely
4176 * confident that the approach taken by this small UTF-8 interface is correct.
4180 unicode_width(unsigned long c)
4183 (c <= 0x115f /* Hangul Jamo */
4186 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
4188 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
4189 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
4190 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
4191 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
4192 || (c >= 0xffe0 && c <= 0xffe6)
4193 || (c >= 0x20000 && c <= 0x2fffd)
4194 || (c >= 0x30000 && c <= 0x3fffd)))
4200 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
4201 * Illegal bytes are set one. */
4202 static const unsigned char utf8_bytes[256] = {
4203 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,
4204 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,
4205 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,
4206 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,
4207 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,
4208 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,
4209 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,
4210 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,
4213 /* Decode UTF-8 multi-byte representation into a unicode character. */
4214 static inline unsigned long
4215 utf8_to_unicode(const char *string, size_t length)
4217 unsigned long unicode;
4221 unicode = string[0];
4224 unicode = (string[0] & 0x1f) << 6;
4225 unicode += (string[1] & 0x3f);
4228 unicode = (string[0] & 0x0f) << 12;
4229 unicode += ((string[1] & 0x3f) << 6);
4230 unicode += (string[2] & 0x3f);
4233 unicode = (string[0] & 0x0f) << 18;
4234 unicode += ((string[1] & 0x3f) << 12);
4235 unicode += ((string[2] & 0x3f) << 6);
4236 unicode += (string[3] & 0x3f);
4239 unicode = (string[0] & 0x0f) << 24;
4240 unicode += ((string[1] & 0x3f) << 18);
4241 unicode += ((string[2] & 0x3f) << 12);
4242 unicode += ((string[3] & 0x3f) << 6);
4243 unicode += (string[4] & 0x3f);
4246 unicode = (string[0] & 0x01) << 30;
4247 unicode += ((string[1] & 0x3f) << 24);
4248 unicode += ((string[2] & 0x3f) << 18);
4249 unicode += ((string[3] & 0x3f) << 12);
4250 unicode += ((string[4] & 0x3f) << 6);
4251 unicode += (string[5] & 0x3f);
4254 die("Invalid unicode length");
4257 /* Invalid characters could return the special 0xfffd value but NUL
4258 * should be just as good. */
4259 return unicode > 0xffff ? 0 : unicode;
4262 /* Calculates how much of string can be shown within the given maximum width
4263 * and sets trimmed parameter to non-zero value if all of string could not be
4266 * Additionally, adds to coloffset how many many columns to move to align with
4267 * the expected position. Takes into account how multi-byte and double-width
4268 * characters will effect the cursor position.
4270 * Returns the number of bytes to output from string to satisfy max_width. */
4272 utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
4274 const char *start = string;
4275 const char *end = strchr(string, '\0');
4281 while (string < end) {
4282 int c = *(unsigned char *) string;
4283 unsigned char bytes = utf8_bytes[c];
4285 unsigned long unicode;
4287 if (string + bytes > end)
4290 /* Change representation to figure out whether
4291 * it is a single- or double-width character. */
4293 unicode = utf8_to_unicode(string, bytes);
4294 /* FIXME: Graceful handling of invalid unicode character. */
4298 ucwidth = unicode_width(unicode);
4300 if (width > max_width) {
4305 /* The column offset collects the differences between the
4306 * number of bytes encoding a character and the number of
4307 * columns will be used for rendering said character.
4309 * So if some character A is encoded in 2 bytes, but will be
4310 * represented on the screen using only 1 byte this will and up
4311 * adding 1 to the multi-byte column offset.
4313 * Assumes that no double-width character can be encoding in
4314 * less than two bytes. */
4315 if (bytes > ucwidth)
4316 mbwidth += bytes - ucwidth;
4321 *coloffset += mbwidth;
4323 return string - start;
4331 /* Whether or not the curses interface has been initialized. */
4332 static bool cursed = FALSE;
4334 /* The status window is used for polling keystrokes. */
4335 static WINDOW *status_win;
4337 static bool status_empty = TRUE;
4339 /* Update status and title window. */
4341 report(const char *msg, ...)
4343 struct view *view = display[current_view];
4349 char buf[SIZEOF_STR];
4352 va_start(args, msg);
4353 if (vsnprintf(buf, sizeof(buf), msg, args) >= sizeof(buf)) {
4354 buf[sizeof(buf) - 1] = 0;
4355 buf[sizeof(buf) - 2] = '.';
4356 buf[sizeof(buf) - 3] = '.';
4357 buf[sizeof(buf) - 4] = '.';
4363 if (!status_empty || *msg) {
4366 va_start(args, msg);
4368 wmove(status_win, 0, 0);
4370 vwprintw(status_win, msg, args);
4371 status_empty = FALSE;
4373 status_empty = TRUE;
4375 wclrtoeol(status_win);
4376 wrefresh(status_win);
4381 update_view_title(view);
4382 update_display_cursor(view);
4385 /* Controls when nodelay should be in effect when polling user input. */
4387 set_nonblocking_input(bool loading)
4389 static unsigned int loading_views;
4391 if ((loading == FALSE && loading_views-- == 1) ||
4392 (loading == TRUE && loading_views++ == 0))
4393 nodelay(status_win, loading);
4401 /* Initialize the curses library */
4402 if (isatty(STDIN_FILENO)) {
4403 cursed = !!initscr();
4405 /* Leave stdin and stdout alone when acting as a pager. */
4406 FILE *io = fopen("/dev/tty", "r+");
4409 die("Failed to open /dev/tty");
4410 cursed = !!newterm(NULL, io, io);
4414 die("Failed to initialize curses");
4416 nonl(); /* Tell curses not to do NL->CR/NL on output */
4417 cbreak(); /* Take input chars one at a time, no wait for \n */
4418 noecho(); /* Don't echo input */
4419 leaveok(stdscr, TRUE);
4424 getmaxyx(stdscr, y, x);
4425 status_win = newwin(1, 0, y - 1, 0);
4427 die("Failed to create status window");
4429 /* Enable keyboard mapping */
4430 keypad(status_win, TRUE);
4431 wbkgdset(status_win, get_line_attr(LINE_STATUS));
4435 read_prompt(const char *prompt)
4437 enum { READING, STOP, CANCEL } status = READING;
4438 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
4441 while (status == READING) {
4447 foreach_view (view, i)
4452 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
4453 wclrtoeol(status_win);
4455 /* Refresh, accept single keystroke of input */
4456 key = wgetch(status_win);
4461 status = pos ? STOP : CANCEL;
4479 if (pos >= sizeof(buf)) {
4480 report("Input string too long");
4485 buf[pos++] = (char) key;
4489 /* Clear the status window */
4490 status_empty = FALSE;
4493 if (status == CANCEL)
4502 * Repository references
4505 static struct ref *refs;
4506 static size_t refs_size;
4508 /* Id <-> ref store */
4509 static struct ref ***id_refs;
4510 static size_t id_refs_size;
4512 static struct ref **
4515 struct ref ***tmp_id_refs;
4516 struct ref **ref_list = NULL;
4517 size_t ref_list_size = 0;
4520 for (i = 0; i < id_refs_size; i++)
4521 if (!strcmp(id, id_refs[i][0]->id))
4524 tmp_id_refs = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs));
4528 id_refs = tmp_id_refs;
4530 for (i = 0; i < refs_size; i++) {
4533 if (strcmp(id, refs[i].id))
4536 tmp = realloc(ref_list, (ref_list_size + 1) * sizeof(*ref_list));
4544 if (ref_list_size > 0)
4545 ref_list[ref_list_size - 1]->next = 1;
4546 ref_list[ref_list_size] = &refs[i];
4548 /* XXX: The properties of the commit chains ensures that we can
4549 * safely modify the shared ref. The repo references will
4550 * always be similar for the same id. */
4551 ref_list[ref_list_size]->next = 0;
4556 id_refs[id_refs_size++] = ref_list;
4562 read_ref(char *id, size_t idlen, char *name, size_t namelen)
4566 bool remote = FALSE;
4568 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
4569 /* Commits referenced by tags has "^{}" appended. */
4570 if (name[namelen - 1] != '}')
4573 while (namelen > 0 && name[namelen] != '^')
4577 namelen -= STRING_SIZE("refs/tags/");
4578 name += STRING_SIZE("refs/tags/");
4580 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
4582 namelen -= STRING_SIZE("refs/remotes/");
4583 name += STRING_SIZE("refs/remotes/");
4585 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
4586 namelen -= STRING_SIZE("refs/heads/");
4587 name += STRING_SIZE("refs/heads/");
4589 } else if (!strcmp(name, "HEAD")) {
4593 refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
4597 ref = &refs[refs_size++];
4598 ref->name = malloc(namelen + 1);
4602 strncpy(ref->name, name, namelen);
4603 ref->name[namelen] = 0;
4605 ref->remote = remote;
4606 string_copy_rev(ref->id, id);
4614 const char *cmd_env = getenv("TIG_LS_REMOTE");
4615 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
4617 return read_properties(popen(cmd, "r"), "\t", read_ref);
4621 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
4623 if (!strcmp(name, "i18n.commitencoding"))
4624 string_ncopy(opt_encoding, value, valuelen);
4626 if (!strcmp(name, "core.editor"))
4627 string_ncopy(opt_editor, value, valuelen);
4633 load_repo_config(void)
4635 return read_properties(popen(GIT_CONFIG " --list", "r"),
4636 "=", read_repo_config_option);
4640 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
4642 if (!opt_git_dir[0]) {
4643 string_ncopy(opt_git_dir, name, namelen);
4645 } else if (opt_is_inside_work_tree == -1) {
4646 /* This can be 3 different values depending on the
4647 * version of git being used. If git-rev-parse does not
4648 * understand --is-inside-work-tree it will simply echo
4649 * the option else either "true" or "false" is printed.
4650 * Default to true for the unknown case. */
4651 opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
4654 string_ncopy(opt_cdup, name, namelen);
4660 /* XXX: The line outputted by "--show-cdup" can be empty so the option
4661 * must be the last one! */
4663 load_repo_info(void)
4665 return read_properties(popen("git rev-parse --git-dir --is-inside-work-tree --show-cdup 2>/dev/null", "r"),
4666 "=", read_repo_info);
4670 read_properties(FILE *pipe, const char *separators,
4671 int (*read_property)(char *, size_t, char *, size_t))
4673 char buffer[BUFSIZ];
4680 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
4685 name = chomp_string(name);
4686 namelen = strcspn(name, separators);
4688 if (name[namelen]) {
4690 value = chomp_string(name + namelen + 1);
4691 valuelen = strlen(value);
4698 state = read_property(name, namelen, value, valuelen);
4701 if (state != ERR && ferror(pipe))
4714 static void __NORETURN
4717 /* XXX: Restore tty modes and let the OS cleanup the rest! */
4723 static void __NORETURN
4724 die(const char *err, ...)
4730 va_start(args, err);
4731 fputs("tig: ", stderr);
4732 vfprintf(stderr, err, args);
4733 fputs("\n", stderr);
4740 main(int argc, char *argv[])
4743 enum request request;
4746 signal(SIGINT, quit);
4748 if (setlocale(LC_ALL, "")) {
4749 char *codeset = nl_langinfo(CODESET);
4751 string_ncopy(opt_codeset, codeset, strlen(codeset));
4754 if (load_repo_info() == ERR)
4755 die("Failed to load repo info.");
4757 if (load_options() == ERR)
4758 die("Failed to load user config.");
4760 /* Load the repo config file so options can be overwritten from
4761 * the command line. */
4762 if (load_repo_config() == ERR)
4763 die("Failed to load repo config.");
4765 if (!parse_options(argc, argv))
4768 /* Require a git repository unless when running in pager mode. */
4769 if (!opt_git_dir[0])
4770 die("Not a git repository");
4772 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
4773 opt_iconv = iconv_open(opt_codeset, opt_encoding);
4774 if (opt_iconv == ICONV_NONE)
4775 die("Failed to initialize character set conversion");
4778 if (load_refs() == ERR)
4779 die("Failed to load refs.");
4781 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
4782 view->cmd_env = getenv(view->cmd_env);
4784 request = opt_request;
4788 while (view_driver(display[current_view], request)) {
4792 foreach_view (view, i)
4795 /* Refresh, accept single keystroke of input */
4796 key = wgetch(status_win);
4798 /* wgetch() with nodelay() enabled returns ERR when there's no
4805 request = get_keybinding(display[current_view]->keymap, key);
4807 /* Some low-level request handling. This keeps access to
4808 * status_win restricted. */
4812 char *cmd = read_prompt(":");
4814 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
4815 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
4816 opt_request = REQ_VIEW_DIFF;
4818 opt_request = REQ_VIEW_PAGER;
4827 case REQ_SEARCH_BACK:
4829 const char *prompt = request == REQ_SEARCH
4831 char *search = read_prompt(prompt);
4834 string_ncopy(opt_search, search, strlen(search));
4839 case REQ_SCREEN_RESIZE:
4843 getmaxyx(stdscr, height, width);
4845 /* Resize the status view and let the view driver take
4846 * care of resizing the displayed views. */
4847 wresize(status_win, 1, width);
4848 mvwin(status_win, height - 1, 0);
4849 wrefresh(status_win);