1 /* Copyright (c) 2006-2008 Jonas Fonseca <fonseca@diku.dk>
3 * This program is free software; you can redistribute it and/or
4 * modify it under the terms of the GNU General Public License as
5 * published by the Free Software Foundation; either version 2 of
6 * the License, or (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
19 #define TIG_VERSION "unknown-version"
34 #include <sys/types.h>
45 /* ncurses(3): Must be defined to have extended wide-character functions. */
46 #define _XOPEN_SOURCE_EXTENDED
48 #ifdef HAVE_NCURSESW_NCURSES_H
49 #include <ncursesw/ncurses.h>
51 #ifdef HAVE_NCURSES_NCURSES_H
52 #include <ncurses/ncurses.h>
59 #define __NORETURN __attribute__((__noreturn__))
64 static void __NORETURN die(const char *err, ...);
65 static void warn(const char *msg, ...);
66 static void report(const char *msg, ...);
67 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, size_t, char *, size_t));
68 static void set_nonblocking_input(bool loading);
69 static size_t utf8_length(const char *string, int *width, size_t max_width, int *trimmed, bool reserve);
71 #define ABS(x) ((x) >= 0 ? (x) : -(x))
72 #define MIN(x, y) ((x) < (y) ? (x) : (y))
74 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
75 #define STRING_SIZE(x) (sizeof(x) - 1)
77 #define SIZEOF_STR 1024 /* Default string size. */
78 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
79 #define SIZEOF_REV 41 /* Holds a SHA-1 and an ending NUL */
83 #define REVGRAPH_INIT 'I'
84 #define REVGRAPH_MERGE 'M'
85 #define REVGRAPH_BRANCH '+'
86 #define REVGRAPH_COMMIT '*'
87 #define REVGRAPH_BOUND '^'
89 #define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
91 /* This color name can be used to refer to the default term colors. */
92 #define COLOR_DEFAULT (-1)
94 #define ICONV_NONE ((iconv_t) -1)
96 #define ICONV_CONST /* nothing */
99 /* The format and size of the date column in the main view. */
100 #define DATE_FORMAT "%Y-%m-%d %H:%M"
101 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
103 #define AUTHOR_COLS 20
106 /* The default interval between line numbers. */
107 #define NUMBER_INTERVAL 5
111 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
113 #define NULL_ID "0000000000000000000000000000000000000000"
116 #define GIT_CONFIG "git config"
119 #define TIG_LS_REMOTE \
120 "git ls-remote . 2>/dev/null"
122 #define TIG_DIFF_CMD \
123 "git show --pretty=fuller --no-color --root --patch-with-stat --find-copies-harder -C %s 2>/dev/null"
125 #define TIG_LOG_CMD \
126 "git log --no-color --cc --stat -n100 %s 2>/dev/null"
128 #define TIG_MAIN_CMD \
129 "git log --no-color --topo-order --parents --boundary --pretty=raw %s 2>/dev/null"
131 #define TIG_TREE_CMD \
134 #define TIG_BLOB_CMD \
135 "git cat-file blob %s"
137 /* XXX: Needs to be defined to the empty string. */
138 #define TIG_HELP_CMD ""
139 #define TIG_PAGER_CMD ""
140 #define TIG_STATUS_CMD ""
141 #define TIG_STAGE_CMD ""
142 #define TIG_BLAME_CMD ""
144 /* Some ascii-shorthands fitted into the ncurses namespace. */
146 #define KEY_RETURN '\r'
151 char *name; /* Ref name; tag or head names are shortened. */
152 char id[SIZEOF_REV]; /* Commit SHA1 ID */
153 unsigned int head:1; /* Is it the current HEAD? */
154 unsigned int tag:1; /* Is it a tag? */
155 unsigned int ltag:1; /* If so, is the tag local? */
156 unsigned int remote:1; /* Is it a remote ref? */
157 unsigned int tracked:1; /* Is it the remote for the current HEAD? */
158 unsigned int next:1; /* For ref lists: are there more refs? */
161 static struct ref **get_refs(char *id);
170 set_from_int_map(struct int_map *map, size_t map_size,
171 int *value, const char *name, int namelen)
176 for (i = 0; i < map_size; i++)
177 if (namelen == map[i].namelen &&
178 !strncasecmp(name, map[i].name, namelen)) {
179 *value = map[i].value;
192 string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
194 if (srclen > dstlen - 1)
197 strncpy(dst, src, srclen);
201 /* Shorthands for safely copying into a fixed buffer. */
203 #define string_copy(dst, src) \
204 string_ncopy_do(dst, sizeof(dst), src, sizeof(src))
206 #define string_ncopy(dst, src, srclen) \
207 string_ncopy_do(dst, sizeof(dst), src, srclen)
209 #define string_copy_rev(dst, src) \
210 string_ncopy_do(dst, SIZEOF_REV, src, SIZEOF_REV - 1)
212 #define string_add(dst, from, src) \
213 string_ncopy_do(dst + (from), sizeof(dst) - (from), src, sizeof(src))
216 chomp_string(char *name)
220 while (isspace(*name))
223 namelen = strlen(name) - 1;
224 while (namelen > 0 && isspace(name[namelen]))
231 string_nformat(char *buf, size_t bufsize, size_t *bufpos, const char *fmt, ...)
234 size_t pos = bufpos ? *bufpos : 0;
237 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
243 return pos >= bufsize ? FALSE : TRUE;
246 #define string_format(buf, fmt, args...) \
247 string_nformat(buf, sizeof(buf), NULL, fmt, args)
249 #define string_format_from(buf, from, fmt, args...) \
250 string_nformat(buf, sizeof(buf), from, fmt, args)
253 string_enum_compare(const char *str1, const char *str2, int len)
257 #define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
259 /* Diff-Header == DIFF_HEADER */
260 for (i = 0; i < len; i++) {
261 if (toupper(str1[i]) == toupper(str2[i]))
264 if (string_enum_sep(str1[i]) &&
265 string_enum_sep(str2[i]))
268 return str1[i] - str2[i];
276 * NOTE: The following is a slightly modified copy of the git project's shell
277 * quoting routines found in the quote.c file.
279 * Help to copy the thing properly quoted for the shell safety. any single
280 * quote is replaced with '\'', any exclamation point is replaced with '\!',
281 * and the whole thing is enclosed in a
284 * original sq_quote result
285 * name ==> name ==> 'name'
286 * a b ==> a b ==> 'a b'
287 * a'b ==> a'\''b ==> 'a'\''b'
288 * a!b ==> a'\!'b ==> 'a'\!'b'
292 sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
296 #define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0)
299 while ((c = *src++)) {
300 if (c == '\'' || c == '!') {
311 if (bufsize < SIZEOF_STR)
323 /* XXX: Keep the view request first and in sync with views[]. */ \
324 REQ_GROUP("View switching") \
325 REQ_(VIEW_MAIN, "Show main view"), \
326 REQ_(VIEW_DIFF, "Show diff view"), \
327 REQ_(VIEW_LOG, "Show log view"), \
328 REQ_(VIEW_TREE, "Show tree view"), \
329 REQ_(VIEW_BLOB, "Show blob view"), \
330 REQ_(VIEW_BLAME, "Show blame view"), \
331 REQ_(VIEW_HELP, "Show help page"), \
332 REQ_(VIEW_PAGER, "Show pager view"), \
333 REQ_(VIEW_STATUS, "Show status view"), \
334 REQ_(VIEW_STAGE, "Show stage view"), \
336 REQ_GROUP("View manipulation") \
337 REQ_(ENTER, "Enter current line and scroll"), \
338 REQ_(NEXT, "Move to next"), \
339 REQ_(PREVIOUS, "Move to previous"), \
340 REQ_(VIEW_NEXT, "Move focus to next view"), \
341 REQ_(REFRESH, "Reload and refresh"), \
342 REQ_(MAXIMIZE, "Maximize the current view"), \
343 REQ_(VIEW_CLOSE, "Close the current view"), \
344 REQ_(QUIT, "Close all views and quit"), \
346 REQ_GROUP("Cursor navigation") \
347 REQ_(MOVE_UP, "Move cursor one line up"), \
348 REQ_(MOVE_DOWN, "Move cursor one line down"), \
349 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
350 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
351 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
352 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
354 REQ_GROUP("Scrolling") \
355 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
356 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
357 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
358 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
360 REQ_GROUP("Searching") \
361 REQ_(SEARCH, "Search the view"), \
362 REQ_(SEARCH_BACK, "Search backwards in the view"), \
363 REQ_(FIND_NEXT, "Find next search match"), \
364 REQ_(FIND_PREV, "Find previous search match"), \
367 REQ_(PROMPT, "Bring up the prompt"), \
368 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
369 REQ_(SCREEN_RESIZE, "Resize the screen"), \
370 REQ_(SHOW_VERSION, "Show version information"), \
371 REQ_(STOP_LOADING, "Stop all loading views"), \
372 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
373 REQ_(TOGGLE_DATE, "Toggle date display"), \
374 REQ_(TOGGLE_AUTHOR, "Toggle author display"), \
375 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"), \
376 REQ_(TOGGLE_REFS, "Toggle reference display (tags/branches)"), \
377 REQ_(STATUS_UPDATE, "Update file status"), \
378 REQ_(STATUS_MERGE, "Merge file using external tool"), \
379 REQ_(STAGE_NEXT, "Find next chunk to stage"), \
380 REQ_(TREE_PARENT, "Switch to parent directory in tree view"), \
381 REQ_(EDIT, "Open in editor"), \
382 REQ_(NONE, "Do nothing")
385 /* User action requests. */
387 #define REQ_GROUP(help)
388 #define REQ_(req, help) REQ_##req
390 /* Offset all requests to avoid conflicts with ncurses getch values. */
391 REQ_OFFSET = KEY_MAX + 1,
398 struct request_info {
399 enum request request;
405 static struct request_info req_info[] = {
406 #define REQ_GROUP(help) { 0, NULL, 0, (help) },
407 #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
414 get_request(const char *name)
416 int namelen = strlen(name);
419 for (i = 0; i < ARRAY_SIZE(req_info); i++)
420 if (req_info[i].namelen == namelen &&
421 !string_enum_compare(req_info[i].name, name, namelen))
422 return req_info[i].request;
432 static const char usage[] =
433 "tig " TIG_VERSION " (" __DATE__ ")\n"
435 "Usage: tig [options] [revs] [--] [paths]\n"
436 " or: tig show [options] [revs] [--] [paths]\n"
437 " or: tig blame [rev] path\n"
439 " or: tig < [git command output]\n"
442 " -v, --version Show version and exit\n"
443 " -h, --help Show help message and exit";
445 /* Option and state variables. */
446 static bool opt_date = TRUE;
447 static bool opt_author = TRUE;
448 static bool opt_line_number = FALSE;
449 static bool opt_line_graphics = TRUE;
450 static bool opt_rev_graph = FALSE;
451 static bool opt_show_refs = TRUE;
452 static int opt_num_interval = NUMBER_INTERVAL;
453 static int opt_tab_size = TAB_SIZE;
454 static enum request opt_request = REQ_VIEW_MAIN;
455 static char opt_cmd[SIZEOF_STR] = "";
456 static char opt_path[SIZEOF_STR] = "";
457 static char opt_file[SIZEOF_STR] = "";
458 static char opt_ref[SIZEOF_REF] = "";
459 static char opt_head[SIZEOF_REF] = "";
460 static char opt_remote[SIZEOF_REF] = "";
461 static bool opt_no_head = TRUE;
462 static FILE *opt_pipe = NULL;
463 static char opt_encoding[20] = "UTF-8";
464 static bool opt_utf8 = TRUE;
465 static char opt_codeset[20] = "UTF-8";
466 static iconv_t opt_iconv = ICONV_NONE;
467 static char opt_search[SIZEOF_STR] = "";
468 static char opt_cdup[SIZEOF_STR] = "";
469 static char opt_git_dir[SIZEOF_STR] = "";
470 static signed char opt_is_inside_work_tree = -1; /* set to TRUE or FALSE */
471 static char opt_editor[SIZEOF_STR] = "";
474 parse_options(int argc, char *argv[])
478 bool seen_dashdash = FALSE;
481 if (!isatty(STDIN_FILENO)) {
482 opt_request = REQ_VIEW_PAGER;
490 subcommand = argv[1];
491 if (!strcmp(subcommand, "status") || !strcmp(subcommand, "-S")) {
492 opt_request = REQ_VIEW_STATUS;
493 if (!strcmp(subcommand, "-S"))
494 warn("`-S' has been deprecated; use `tig status' instead");
496 warn("ignoring arguments after `%s'", subcommand);
499 } else if (!strcmp(subcommand, "blame")) {
500 opt_request = REQ_VIEW_BLAME;
501 if (argc <= 2 || argc > 4)
502 die("invalid number of options to blame\n\n%s", usage);
506 string_ncopy(opt_ref, argv[i], strlen(argv[i]));
510 string_ncopy(opt_file, argv[i], strlen(argv[i]));
513 } else if (!strcmp(subcommand, "show")) {
514 opt_request = REQ_VIEW_DIFF;
516 } else if (!strcmp(subcommand, "log") || !strcmp(subcommand, "diff")) {
517 opt_request = subcommand[0] == 'l'
518 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
519 warn("`tig %s' has been deprecated", subcommand);
526 /* XXX: This is vulnerable to the user overriding
527 * options required for the main view parser. */
528 string_copy(opt_cmd, "git log --no-color --pretty=raw --boundary --parents");
530 string_format(opt_cmd, "git %s", subcommand);
532 buf_size = strlen(opt_cmd);
534 for (i = 1 + !!subcommand; i < argc; i++) {
537 if (seen_dashdash || !strcmp(opt, "--")) {
538 seen_dashdash = TRUE;
540 } else if (!strcmp(opt, "-v") || !strcmp(opt, "--version")) {
541 printf("tig version %s\n", TIG_VERSION);
544 } else if (!strcmp(opt, "-h") || !strcmp(opt, "--help")) {
545 printf("%s\n", usage);
549 opt_cmd[buf_size++] = ' ';
550 buf_size = sq_quote(opt_cmd, buf_size, opt);
551 if (buf_size >= sizeof(opt_cmd))
552 die("command too long");
555 opt_cmd[buf_size] = 0;
562 * Line-oriented content detection.
566 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
567 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
568 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
569 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
570 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
571 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
572 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
573 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
574 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
575 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
576 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
577 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
578 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
579 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
580 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
581 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
582 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
583 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
584 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
585 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
586 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
587 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
588 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
589 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
590 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
591 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
592 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
593 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
594 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
595 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
596 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
597 LINE(DELIMITER, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
598 LINE(DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
599 LINE(LINE_NUMBER, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
600 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
601 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
602 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
603 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
604 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
605 LINE(MAIN_LOCAL_TAG,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
606 LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \
607 LINE(MAIN_TRACKED, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
608 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
609 LINE(MAIN_HEAD, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
610 LINE(MAIN_REVGRAPH,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
611 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
612 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
613 LINE(STAT_HEAD, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \
614 LINE(STAT_SECTION, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
615 LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
616 LINE(STAT_STAGED, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
617 LINE(STAT_UNSTAGED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
618 LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
619 LINE(BLAME_ID, "", COLOR_MAGENTA, COLOR_DEFAULT, 0)
622 #define LINE(type, line, fg, bg, attr) \
630 const char *name; /* Option name. */
631 int namelen; /* Size of option name. */
632 const char *line; /* The start of line to match. */
633 int linelen; /* Size of string to match. */
634 int fg, bg, attr; /* Color and text attributes for the lines. */
637 static struct line_info line_info[] = {
638 #define LINE(type, line, fg, bg, attr) \
639 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
644 static enum line_type
645 get_line_type(char *line)
647 int linelen = strlen(line);
650 for (type = 0; type < ARRAY_SIZE(line_info); type++)
651 /* Case insensitive search matches Signed-off-by lines better. */
652 if (linelen >= line_info[type].linelen &&
653 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
660 get_line_attr(enum line_type type)
662 assert(type < ARRAY_SIZE(line_info));
663 return COLOR_PAIR(type) | line_info[type].attr;
666 static struct line_info *
667 get_line_info(char *name)
669 size_t namelen = strlen(name);
672 for (type = 0; type < ARRAY_SIZE(line_info); type++)
673 if (namelen == line_info[type].namelen &&
674 !string_enum_compare(line_info[type].name, name, namelen))
675 return &line_info[type];
683 int default_bg = line_info[LINE_DEFAULT].bg;
684 int default_fg = line_info[LINE_DEFAULT].fg;
689 if (assume_default_colors(default_fg, default_bg) == ERR) {
690 default_bg = COLOR_BLACK;
691 default_fg = COLOR_WHITE;
694 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
695 struct line_info *info = &line_info[type];
696 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
697 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
699 init_pair(type, fg, bg);
707 unsigned int selected:1;
708 unsigned int dirty:1;
710 void *data; /* User data */
720 enum request request;
721 struct keybinding *next;
724 static struct keybinding default_keybindings[] = {
726 { 'm', REQ_VIEW_MAIN },
727 { 'd', REQ_VIEW_DIFF },
728 { 'l', REQ_VIEW_LOG },
729 { 't', REQ_VIEW_TREE },
730 { 'f', REQ_VIEW_BLOB },
731 { 'B', REQ_VIEW_BLAME },
732 { 'p', REQ_VIEW_PAGER },
733 { 'h', REQ_VIEW_HELP },
734 { 'S', REQ_VIEW_STATUS },
735 { 'c', REQ_VIEW_STAGE },
737 /* View manipulation */
738 { 'q', REQ_VIEW_CLOSE },
739 { KEY_TAB, REQ_VIEW_NEXT },
740 { KEY_RETURN, REQ_ENTER },
741 { KEY_UP, REQ_PREVIOUS },
742 { KEY_DOWN, REQ_NEXT },
743 { 'R', REQ_REFRESH },
744 { KEY_F(5), REQ_REFRESH },
745 { 'O', REQ_MAXIMIZE },
747 /* Cursor navigation */
748 { 'k', REQ_MOVE_UP },
749 { 'j', REQ_MOVE_DOWN },
750 { KEY_HOME, REQ_MOVE_FIRST_LINE },
751 { KEY_END, REQ_MOVE_LAST_LINE },
752 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
753 { ' ', REQ_MOVE_PAGE_DOWN },
754 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
755 { 'b', REQ_MOVE_PAGE_UP },
756 { '-', REQ_MOVE_PAGE_UP },
759 { KEY_IC, REQ_SCROLL_LINE_UP },
760 { KEY_DC, REQ_SCROLL_LINE_DOWN },
761 { 'w', REQ_SCROLL_PAGE_UP },
762 { 's', REQ_SCROLL_PAGE_DOWN },
766 { '?', REQ_SEARCH_BACK },
767 { 'n', REQ_FIND_NEXT },
768 { 'N', REQ_FIND_PREV },
772 { 'z', REQ_STOP_LOADING },
773 { 'v', REQ_SHOW_VERSION },
774 { 'r', REQ_SCREEN_REDRAW },
775 { '.', REQ_TOGGLE_LINENO },
776 { 'D', REQ_TOGGLE_DATE },
777 { 'A', REQ_TOGGLE_AUTHOR },
778 { 'g', REQ_TOGGLE_REV_GRAPH },
779 { 'F', REQ_TOGGLE_REFS },
781 { 'u', REQ_STATUS_UPDATE },
782 { 'M', REQ_STATUS_MERGE },
783 { '@', REQ_STAGE_NEXT },
784 { ',', REQ_TREE_PARENT },
787 /* Using the ncurses SIGWINCH handler. */
788 { KEY_RESIZE, REQ_SCREEN_RESIZE },
791 #define KEYMAP_INFO \
805 #define KEYMAP_(name) KEYMAP_##name
810 static struct int_map keymap_table[] = {
811 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
816 #define set_keymap(map, name) \
817 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
819 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
822 add_keybinding(enum keymap keymap, enum request request, int key)
824 struct keybinding *keybinding;
826 keybinding = calloc(1, sizeof(*keybinding));
828 die("Failed to allocate keybinding");
830 keybinding->alias = key;
831 keybinding->request = request;
832 keybinding->next = keybindings[keymap];
833 keybindings[keymap] = keybinding;
836 /* Looks for a key binding first in the given map, then in the generic map, and
837 * lastly in the default keybindings. */
839 get_keybinding(enum keymap keymap, int key)
841 struct keybinding *kbd;
844 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
845 if (kbd->alias == key)
848 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
849 if (kbd->alias == key)
852 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
853 if (default_keybindings[i].alias == key)
854 return default_keybindings[i].request;
856 return (enum request) key;
865 static struct key key_table[] = {
866 { "Enter", KEY_RETURN },
868 { "Backspace", KEY_BACKSPACE },
870 { "Escape", KEY_ESC },
871 { "Left", KEY_LEFT },
872 { "Right", KEY_RIGHT },
874 { "Down", KEY_DOWN },
875 { "Insert", KEY_IC },
876 { "Delete", KEY_DC },
878 { "Home", KEY_HOME },
880 { "PageUp", KEY_PPAGE },
881 { "PageDown", KEY_NPAGE },
891 { "F10", KEY_F(10) },
892 { "F11", KEY_F(11) },
893 { "F12", KEY_F(12) },
897 get_key_value(const char *name)
901 for (i = 0; i < ARRAY_SIZE(key_table); i++)
902 if (!strcasecmp(key_table[i].name, name))
903 return key_table[i].value;
905 if (strlen(name) == 1 && isprint(*name))
912 get_key_name(int key_value)
914 static char key_char[] = "'X'";
918 for (key = 0; key < ARRAY_SIZE(key_table); key++)
919 if (key_table[key].value == key_value)
920 seq = key_table[key].name;
924 isprint(key_value)) {
925 key_char[1] = (char) key_value;
929 return seq ? seq : "'?'";
933 get_key(enum request request)
935 static char buf[BUFSIZ];
942 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
943 struct keybinding *keybinding = &default_keybindings[i];
945 if (keybinding->request != request)
948 if (!string_format_from(buf, &pos, "%s%s", sep,
949 get_key_name(keybinding->alias)))
950 return "Too many keybindings!";
960 char cmd[SIZEOF_STR];
963 static struct run_request *run_request;
964 static size_t run_requests;
967 add_run_request(enum keymap keymap, int key, int argc, char **argv)
969 struct run_request *req;
970 char cmd[SIZEOF_STR];
973 for (bufpos = 0; argc > 0; argc--, argv++)
974 if (!string_format_from(cmd, &bufpos, "%s ", *argv))
977 req = realloc(run_request, (run_requests + 1) * sizeof(*run_request));
982 req = &run_request[run_requests++];
983 string_copy(req->cmd, cmd);
984 req->keymap = keymap;
987 return REQ_NONE + run_requests;
990 static struct run_request *
991 get_run_request(enum request request)
993 if (request <= REQ_NONE)
995 return &run_request[request - REQ_NONE - 1];
999 add_builtin_run_requests(void)
1006 { KEYMAP_MAIN, 'C', { "git cherry-pick %(commit)" } },
1007 { KEYMAP_GENERIC, 'G', { "git gc" } },
1011 for (i = 0; i < ARRAY_SIZE(reqs); i++) {
1014 req = add_run_request(reqs[i].keymap, reqs[i].key, 1, reqs[i].argv);
1015 if (req != REQ_NONE)
1016 add_keybinding(reqs[i].keymap, req, reqs[i].key);
1021 * User config file handling.
1024 static struct int_map color_map[] = {
1025 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
1037 #define set_color(color, name) \
1038 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
1040 static struct int_map attr_map[] = {
1041 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
1048 ATTR_MAP(UNDERLINE),
1051 #define set_attribute(attr, name) \
1052 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
1054 static int config_lineno;
1055 static bool config_errors;
1056 static char *config_msg;
1058 /* Wants: object fgcolor bgcolor [attr] */
1060 option_color_command(int argc, char *argv[])
1062 struct line_info *info;
1064 if (argc != 3 && argc != 4) {
1065 config_msg = "Wrong number of arguments given to color command";
1069 info = get_line_info(argv[0]);
1071 if (!string_enum_compare(argv[0], "main-delim", strlen("main-delim"))) {
1072 info = get_line_info("delimiter");
1074 } else if (!string_enum_compare(argv[0], "main-date", strlen("main-date"))) {
1075 info = get_line_info("date");
1078 config_msg = "Unknown color name";
1083 if (set_color(&info->fg, argv[1]) == ERR ||
1084 set_color(&info->bg, argv[2]) == ERR) {
1085 config_msg = "Unknown color";
1089 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
1090 config_msg = "Unknown attribute";
1097 static bool parse_bool(const char *s)
1099 return (!strcmp(s, "1") || !strcmp(s, "true") ||
1100 !strcmp(s, "yes")) ? TRUE : FALSE;
1103 /* Wants: name = value */
1105 option_set_command(int argc, char *argv[])
1108 config_msg = "Wrong number of arguments given to set command";
1112 if (strcmp(argv[1], "=")) {
1113 config_msg = "No value assigned";
1117 if (!strcmp(argv[0], "show-author")) {
1118 opt_author = parse_bool(argv[2]);
1122 if (!strcmp(argv[0], "show-date")) {
1123 opt_date = parse_bool(argv[2]);
1127 if (!strcmp(argv[0], "show-rev-graph")) {
1128 opt_rev_graph = parse_bool(argv[2]);
1132 if (!strcmp(argv[0], "show-refs")) {
1133 opt_show_refs = parse_bool(argv[2]);
1137 if (!strcmp(argv[0], "show-line-numbers")) {
1138 opt_line_number = parse_bool(argv[2]);
1142 if (!strcmp(argv[0], "line-graphics")) {
1143 opt_line_graphics = parse_bool(argv[2]);
1147 if (!strcmp(argv[0], "line-number-interval")) {
1148 opt_num_interval = atoi(argv[2]);
1152 if (!strcmp(argv[0], "tab-size")) {
1153 opt_tab_size = atoi(argv[2]);
1157 if (!strcmp(argv[0], "commit-encoding")) {
1158 char *arg = argv[2];
1159 int delimiter = *arg;
1162 switch (delimiter) {
1165 for (arg++, i = 0; arg[i]; i++)
1166 if (arg[i] == delimiter) {
1171 string_ncopy(opt_encoding, arg, strlen(arg));
1176 config_msg = "Unknown variable name";
1180 /* Wants: mode request key */
1182 option_bind_command(int argc, char *argv[])
1184 enum request request;
1189 config_msg = "Wrong number of arguments given to bind command";
1193 if (set_keymap(&keymap, argv[0]) == ERR) {
1194 config_msg = "Unknown key map";
1198 key = get_key_value(argv[1]);
1200 config_msg = "Unknown key";
1204 request = get_request(argv[2]);
1205 if (request == REQ_NONE) {
1206 const char *obsolete[] = { "cherry-pick" };
1207 size_t namelen = strlen(argv[2]);
1210 for (i = 0; i < ARRAY_SIZE(obsolete); i++) {
1211 if (namelen == strlen(obsolete[i]) &&
1212 !string_enum_compare(obsolete[i], argv[2], namelen)) {
1213 config_msg = "Obsolete request name";
1218 if (request == REQ_NONE && *argv[2]++ == '!')
1219 request = add_run_request(keymap, key, argc - 2, argv + 2);
1220 if (request == REQ_NONE) {
1221 config_msg = "Unknown request name";
1225 add_keybinding(keymap, request, key);
1231 set_option(char *opt, char *value)
1238 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1239 argv[argc++] = value;
1242 /* Nothing more to tokenize or last available token. */
1243 if (!*value || argc >= ARRAY_SIZE(argv))
1247 while (isspace(*value))
1251 if (!strcmp(opt, "color"))
1252 return option_color_command(argc, argv);
1254 if (!strcmp(opt, "set"))
1255 return option_set_command(argc, argv);
1257 if (!strcmp(opt, "bind"))
1258 return option_bind_command(argc, argv);
1260 config_msg = "Unknown option command";
1265 read_option(char *opt, size_t optlen, char *value, size_t valuelen)
1270 config_msg = "Internal error";
1272 /* Check for comment markers, since read_properties() will
1273 * only ensure opt and value are split at first " \t". */
1274 optlen = strcspn(opt, "#");
1278 if (opt[optlen] != 0) {
1279 config_msg = "No option value";
1283 /* Look for comment endings in the value. */
1284 size_t len = strcspn(value, "#");
1286 if (len < valuelen) {
1288 value[valuelen] = 0;
1291 status = set_option(opt, value);
1294 if (status == ERR) {
1295 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1296 config_lineno, (int) optlen, opt, config_msg);
1297 config_errors = TRUE;
1300 /* Always keep going if errors are encountered. */
1305 load_option_file(const char *path)
1309 /* It's ok that the file doesn't exist. */
1310 file = fopen(path, "r");
1315 config_errors = FALSE;
1317 if (read_properties(file, " \t", read_option) == ERR ||
1318 config_errors == TRUE)
1319 fprintf(stderr, "Errors while loading %s.\n", path);
1325 char *home = getenv("HOME");
1326 char *tigrc_user = getenv("TIGRC_USER");
1327 char *tigrc_system = getenv("TIGRC_SYSTEM");
1328 char buf[SIZEOF_STR];
1330 add_builtin_run_requests();
1332 if (!tigrc_system) {
1333 if (!string_format(buf, "%s/tigrc", SYSCONFDIR))
1337 load_option_file(tigrc_system);
1340 if (!home || !string_format(buf, "%s/.tigrc", home))
1344 load_option_file(tigrc_user);
1357 /* The display array of active views and the index of the current view. */
1358 static struct view *display[2];
1359 static unsigned int current_view;
1361 /* Reading from the prompt? */
1362 static bool input_mode = FALSE;
1364 #define foreach_displayed_view(view, i) \
1365 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1367 #define displayed_views() (display[1] != NULL ? 2 : 1)
1369 /* Current head and commit ID */
1370 static char ref_blob[SIZEOF_REF] = "";
1371 static char ref_commit[SIZEOF_REF] = "HEAD";
1372 static char ref_head[SIZEOF_REF] = "HEAD";
1375 const char *name; /* View name */
1376 const char *cmd_fmt; /* Default command line format */
1377 const char *cmd_env; /* Command line set via environment */
1378 const char *id; /* Points to either of ref_{head,commit,blob} */
1380 struct view_ops *ops; /* View operations */
1382 enum keymap keymap; /* What keymap does this view have */
1383 bool git_dir; /* Whether the view requires a git directory. */
1385 char cmd[SIZEOF_STR]; /* Command buffer */
1386 char ref[SIZEOF_REF]; /* Hovered commit reference */
1387 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1389 int height, width; /* The width and height of the main window */
1390 WINDOW *win; /* The main window */
1391 WINDOW *title; /* The title window living below the main window */
1394 unsigned long offset; /* Offset of the window top */
1395 unsigned long lineno; /* Current line number */
1398 char grep[SIZEOF_STR]; /* Search string */
1399 regex_t *regex; /* Pre-compiled regex */
1401 /* If non-NULL, points to the view that opened this view. If this view
1402 * is closed tig will switch back to the parent view. */
1403 struct view *parent;
1406 size_t lines; /* Total number of lines */
1407 struct line *line; /* Line index */
1408 size_t line_alloc; /* Total number of allocated lines */
1409 size_t line_size; /* Total number of used lines */
1410 unsigned int digits; /* Number of digits in the lines member. */
1413 struct line *curline; /* Line currently being drawn. */
1414 enum line_type curtype; /* Attribute currently used for drawing. */
1415 unsigned long col; /* Column when drawing. */
1423 /* What type of content being displayed. Used in the title bar. */
1425 /* Open and reads in all view content. */
1426 bool (*open)(struct view *view);
1427 /* Read one line; updates view->line. */
1428 bool (*read)(struct view *view, char *data);
1429 /* Draw one line; @lineno must be < view->height. */
1430 bool (*draw)(struct view *view, struct line *line, unsigned int lineno);
1431 /* Depending on view handle a special requests. */
1432 enum request (*request)(struct view *view, enum request request, struct line *line);
1433 /* Search for regex in a line. */
1434 bool (*grep)(struct view *view, struct line *line);
1436 void (*select)(struct view *view, struct line *line);
1439 static struct view_ops pager_ops;
1440 static struct view_ops main_ops;
1441 static struct view_ops tree_ops;
1442 static struct view_ops blob_ops;
1443 static struct view_ops blame_ops;
1444 static struct view_ops help_ops;
1445 static struct view_ops status_ops;
1446 static struct view_ops stage_ops;
1448 #define VIEW_STR(name, cmd, env, ref, ops, map, git) \
1449 { name, cmd, #env, ref, ops, map, git }
1451 #define VIEW_(id, name, ops, git, ref) \
1452 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id, git)
1455 static struct view views[] = {
1456 VIEW_(MAIN, "main", &main_ops, TRUE, ref_head),
1457 VIEW_(DIFF, "diff", &pager_ops, TRUE, ref_commit),
1458 VIEW_(LOG, "log", &pager_ops, TRUE, ref_head),
1459 VIEW_(TREE, "tree", &tree_ops, TRUE, ref_commit),
1460 VIEW_(BLOB, "blob", &blob_ops, TRUE, ref_blob),
1461 VIEW_(BLAME, "blame", &blame_ops, TRUE, ref_commit),
1462 VIEW_(HELP, "help", &help_ops, FALSE, ""),
1463 VIEW_(PAGER, "pager", &pager_ops, FALSE, "stdin"),
1464 VIEW_(STATUS, "status", &status_ops, TRUE, ""),
1465 VIEW_(STAGE, "stage", &stage_ops, TRUE, ""),
1468 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1469 #define VIEW_REQ(view) ((view) - views + REQ_OFFSET + 1)
1471 #define foreach_view(view, i) \
1472 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1474 #define view_is_displayed(view) \
1475 (view == display[0] || view == display[1])
1482 static int line_graphics[] = {
1483 /* LINE_GRAPHIC_VLINE: */ '|'
1487 set_view_attr(struct view *view, enum line_type type)
1489 if (!view->curline->selected && view->curtype != type) {
1490 wattrset(view->win, get_line_attr(type));
1491 wchgat(view->win, -1, 0, type, NULL);
1492 view->curtype = type;
1497 draw_chars(struct view *view, enum line_type type, const char *string,
1498 int max_len, bool use_tilde)
1502 int trimmed = FALSE;
1508 len = utf8_length(string, &col, max_len, &trimmed, use_tilde);
1510 col = len = strlen(string);
1511 if (len > max_len) {
1515 col = len = max_len;
1520 set_view_attr(view, type);
1521 waddnstr(view->win, string, len);
1522 if (trimmed && use_tilde) {
1523 set_view_attr(view, LINE_DELIMITER);
1524 waddch(view->win, '~');
1532 draw_space(struct view *view, enum line_type type, int max, int spaces)
1534 static char space[] = " ";
1537 spaces = MIN(max, spaces);
1539 while (spaces > 0) {
1540 int len = MIN(spaces, sizeof(space) - 1);
1542 col += draw_chars(view, type, space, spaces, FALSE);
1550 draw_lineno(struct view *view, unsigned int lineno)
1553 int digits3 = view->digits < 3 ? 3 : view->digits;
1554 int max_number = MIN(digits3, STRING_SIZE(number));
1555 int max = view->width - view->col;
1558 if (max < max_number)
1561 lineno += view->offset + 1;
1562 if (lineno == 1 || (lineno % opt_num_interval) == 0) {
1563 static char fmt[] = "%1ld";
1565 if (view->digits <= 9)
1566 fmt[1] = '0' + digits3;
1568 if (!string_format(number, fmt, lineno))
1570 col = draw_chars(view, LINE_LINE_NUMBER, number, max_number, TRUE);
1572 col = draw_space(view, LINE_LINE_NUMBER, max_number, max_number);
1576 set_view_attr(view, LINE_DEFAULT);
1577 waddch(view->win, line_graphics[LINE_GRAPHIC_VLINE]);
1582 col += draw_space(view, LINE_DEFAULT, max - col, 1);
1585 return view->width - view->col <= 0;
1589 draw_text(struct view *view, enum line_type type, const char *string, bool trim)
1591 view->col += draw_chars(view, type, string, view->width - view->col, trim);
1592 return view->width - view->col <= 0;
1596 draw_graphic(struct view *view, enum line_type type, chtype graphic[], size_t size)
1598 int max = view->width - view->col;
1604 set_view_attr(view, type);
1605 /* Using waddch() instead of waddnstr() ensures that
1606 * they'll be rendered correctly for the cursor line. */
1607 for (i = 0; i < size; i++)
1608 waddch(view->win, graphic[i]);
1612 waddch(view->win, ' ');
1616 return view->width - view->col <= 0;
1620 draw_field(struct view *view, enum line_type type, char *text, int len, bool trim)
1622 int max = MIN(view->width - view->col, len);
1626 col = draw_chars(view, type, text, max - 1, trim);
1628 col = draw_space(view, type, max - 1, max - 1);
1630 view->col += col + draw_space(view, LINE_DEFAULT, max - col, max - col);
1631 return view->width - view->col <= 0;
1635 draw_date(struct view *view, struct tm *time)
1637 char buf[DATE_COLS];
1642 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, time);
1643 date = timelen ? buf : NULL;
1645 return draw_field(view, LINE_DATE, date, DATE_COLS, FALSE);
1649 draw_view_line(struct view *view, unsigned int lineno)
1652 bool selected = (view->offset + lineno == view->lineno);
1655 assert(view_is_displayed(view));
1657 if (view->offset + lineno >= view->lines)
1660 line = &view->line[view->offset + lineno];
1662 wmove(view->win, lineno, 0);
1664 view->curline = line;
1665 view->curtype = LINE_NONE;
1666 line->selected = FALSE;
1669 set_view_attr(view, LINE_CURSOR);
1670 line->selected = TRUE;
1671 view->ops->select(view, line);
1672 } else if (line->selected) {
1673 wclrtoeol(view->win);
1676 scrollok(view->win, FALSE);
1677 draw_ok = view->ops->draw(view, line, lineno);
1678 scrollok(view->win, TRUE);
1684 redraw_view_dirty(struct view *view)
1689 for (lineno = 0; lineno < view->height; lineno++) {
1690 struct line *line = &view->line[view->offset + lineno];
1696 if (!draw_view_line(view, lineno))
1702 redrawwin(view->win);
1704 wnoutrefresh(view->win);
1706 wrefresh(view->win);
1710 redraw_view_from(struct view *view, int lineno)
1712 assert(0 <= lineno && lineno < view->height);
1714 for (; lineno < view->height; lineno++) {
1715 if (!draw_view_line(view, lineno))
1719 redrawwin(view->win);
1721 wnoutrefresh(view->win);
1723 wrefresh(view->win);
1727 redraw_view(struct view *view)
1730 redraw_view_from(view, 0);
1735 update_view_title(struct view *view)
1737 char buf[SIZEOF_STR];
1738 char state[SIZEOF_STR];
1739 size_t bufpos = 0, statelen = 0;
1741 assert(view_is_displayed(view));
1743 if (view != VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) {
1744 unsigned int view_lines = view->offset + view->height;
1745 unsigned int lines = view->lines
1746 ? MIN(view_lines, view->lines) * 100 / view->lines
1749 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1756 time_t secs = time(NULL) - view->start_time;
1758 /* Three git seconds are a long time ... */
1760 string_format_from(state, &statelen, " %lds", secs);
1764 string_format_from(buf, &bufpos, "[%s]", view->name);
1765 if (*view->ref && bufpos < view->width) {
1766 size_t refsize = strlen(view->ref);
1767 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1769 if (minsize < view->width)
1770 refsize = view->width - minsize + 7;
1771 string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
1774 if (statelen && bufpos < view->width) {
1775 string_format_from(buf, &bufpos, " %s", state);
1778 if (view == display[current_view])
1779 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1781 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1783 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1784 wclrtoeol(view->title);
1785 wmove(view->title, 0, view->width - 1);
1788 wnoutrefresh(view->title);
1790 wrefresh(view->title);
1794 resize_display(void)
1797 struct view *base = display[0];
1798 struct view *view = display[1] ? display[1] : display[0];
1800 /* Setup window dimensions */
1802 getmaxyx(stdscr, base->height, base->width);
1804 /* Make room for the status window. */
1808 /* Horizontal split. */
1809 view->width = base->width;
1810 view->height = SCALE_SPLIT_VIEW(base->height);
1811 base->height -= view->height;
1813 /* Make room for the title bar. */
1817 /* Make room for the title bar. */
1822 foreach_displayed_view (view, i) {
1824 view->win = newwin(view->height, 0, offset, 0);
1826 die("Failed to create %s view", view->name);
1828 scrollok(view->win, TRUE);
1830 view->title = newwin(1, 0, offset + view->height, 0);
1832 die("Failed to create title window");
1835 wresize(view->win, view->height, view->width);
1836 mvwin(view->win, offset, 0);
1837 mvwin(view->title, offset + view->height, 0);
1840 offset += view->height + 1;
1845 redraw_display(void)
1850 foreach_displayed_view (view, i) {
1852 update_view_title(view);
1857 update_display_cursor(struct view *view)
1859 /* Move the cursor to the right-most column of the cursor line.
1861 * XXX: This could turn out to be a bit expensive, but it ensures that
1862 * the cursor does not jump around. */
1864 wmove(view->win, view->lineno - view->offset, view->width - 1);
1865 wrefresh(view->win);
1873 /* Scrolling backend */
1875 do_scroll_view(struct view *view, int lines)
1877 bool redraw_current_line = FALSE;
1879 /* The rendering expects the new offset. */
1880 view->offset += lines;
1882 assert(0 <= view->offset && view->offset < view->lines);
1885 /* Move current line into the view. */
1886 if (view->lineno < view->offset) {
1887 view->lineno = view->offset;
1888 redraw_current_line = TRUE;
1889 } else if (view->lineno >= view->offset + view->height) {
1890 view->lineno = view->offset + view->height - 1;
1891 redraw_current_line = TRUE;
1894 assert(view->offset <= view->lineno && view->lineno < view->lines);
1896 /* Redraw the whole screen if scrolling is pointless. */
1897 if (view->height < ABS(lines)) {
1901 int line = lines > 0 ? view->height - lines : 0;
1902 int end = line + ABS(lines);
1904 wscrl(view->win, lines);
1906 for (; line < end; line++) {
1907 if (!draw_view_line(view, line))
1911 if (redraw_current_line)
1912 draw_view_line(view, view->lineno - view->offset);
1915 redrawwin(view->win);
1916 wrefresh(view->win);
1920 /* Scroll frontend */
1922 scroll_view(struct view *view, enum request request)
1926 assert(view_is_displayed(view));
1929 case REQ_SCROLL_PAGE_DOWN:
1930 lines = view->height;
1931 case REQ_SCROLL_LINE_DOWN:
1932 if (view->offset + lines > view->lines)
1933 lines = view->lines - view->offset;
1935 if (lines == 0 || view->offset + view->height >= view->lines) {
1936 report("Cannot scroll beyond the last line");
1941 case REQ_SCROLL_PAGE_UP:
1942 lines = view->height;
1943 case REQ_SCROLL_LINE_UP:
1944 if (lines > view->offset)
1945 lines = view->offset;
1948 report("Cannot scroll beyond the first line");
1956 die("request %d not handled in switch", request);
1959 do_scroll_view(view, lines);
1964 move_view(struct view *view, enum request request)
1966 int scroll_steps = 0;
1970 case REQ_MOVE_FIRST_LINE:
1971 steps = -view->lineno;
1974 case REQ_MOVE_LAST_LINE:
1975 steps = view->lines - view->lineno - 1;
1978 case REQ_MOVE_PAGE_UP:
1979 steps = view->height > view->lineno
1980 ? -view->lineno : -view->height;
1983 case REQ_MOVE_PAGE_DOWN:
1984 steps = view->lineno + view->height >= view->lines
1985 ? view->lines - view->lineno - 1 : view->height;
1997 die("request %d not handled in switch", request);
2000 if (steps <= 0 && view->lineno == 0) {
2001 report("Cannot move beyond the first line");
2004 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
2005 report("Cannot move beyond the last line");
2009 /* Move the current line */
2010 view->lineno += steps;
2011 assert(0 <= view->lineno && view->lineno < view->lines);
2013 /* Check whether the view needs to be scrolled */
2014 if (view->lineno < view->offset ||
2015 view->lineno >= view->offset + view->height) {
2016 scroll_steps = steps;
2017 if (steps < 0 && -steps > view->offset) {
2018 scroll_steps = -view->offset;
2020 } else if (steps > 0) {
2021 if (view->lineno == view->lines - 1 &&
2022 view->lines > view->height) {
2023 scroll_steps = view->lines - view->offset - 1;
2024 if (scroll_steps >= view->height)
2025 scroll_steps -= view->height - 1;
2030 if (!view_is_displayed(view)) {
2031 view->offset += scroll_steps;
2032 assert(0 <= view->offset && view->offset < view->lines);
2033 view->ops->select(view, &view->line[view->lineno]);
2037 /* Repaint the old "current" line if we be scrolling */
2038 if (ABS(steps) < view->height)
2039 draw_view_line(view, view->lineno - steps - view->offset);
2042 do_scroll_view(view, scroll_steps);
2046 /* Draw the current line */
2047 draw_view_line(view, view->lineno - view->offset);
2049 redrawwin(view->win);
2050 wrefresh(view->win);
2059 static void search_view(struct view *view, enum request request);
2062 find_next_line(struct view *view, unsigned long lineno, struct line *line)
2064 assert(view_is_displayed(view));
2066 if (!view->ops->grep(view, line))
2069 if (lineno - view->offset >= view->height) {
2070 view->offset = lineno;
2071 view->lineno = lineno;
2075 unsigned long old_lineno = view->lineno - view->offset;
2077 view->lineno = lineno;
2078 draw_view_line(view, old_lineno);
2080 draw_view_line(view, view->lineno - view->offset);
2081 redrawwin(view->win);
2082 wrefresh(view->win);
2085 report("Line %ld matches '%s'", lineno + 1, view->grep);
2090 find_next(struct view *view, enum request request)
2092 unsigned long lineno = view->lineno;
2097 report("No previous search");
2099 search_view(view, request);
2109 case REQ_SEARCH_BACK:
2118 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
2119 lineno += direction;
2121 /* Note, lineno is unsigned long so will wrap around in which case it
2122 * will become bigger than view->lines. */
2123 for (; lineno < view->lines; lineno += direction) {
2124 struct line *line = &view->line[lineno];
2126 if (find_next_line(view, lineno, line))
2130 report("No match found for '%s'", view->grep);
2134 search_view(struct view *view, enum request request)
2139 regfree(view->regex);
2142 view->regex = calloc(1, sizeof(*view->regex));
2147 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
2148 if (regex_err != 0) {
2149 char buf[SIZEOF_STR] = "unknown error";
2151 regerror(regex_err, view->regex, buf, sizeof(buf));
2152 report("Search failed: %s", buf);
2156 string_copy(view->grep, opt_search);
2158 find_next(view, request);
2162 * Incremental updating
2166 end_update(struct view *view, bool force)
2170 while (!view->ops->read(view, NULL))
2173 set_nonblocking_input(FALSE);
2174 if (view->pipe == stdin)
2182 begin_update(struct view *view)
2185 string_copy(view->cmd, opt_cmd);
2187 /* When running random commands, initially show the
2188 * command in the title. However, it maybe later be
2189 * overwritten if a commit line is selected. */
2190 if (view == VIEW(REQ_VIEW_PAGER))
2191 string_copy(view->ref, view->cmd);
2195 } else if (view == VIEW(REQ_VIEW_TREE)) {
2196 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2197 char path[SIZEOF_STR];
2199 if (strcmp(view->vid, view->id))
2200 opt_path[0] = path[0] = 0;
2201 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
2204 if (!string_format(view->cmd, format, view->id, path))
2208 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2209 const char *id = view->id;
2211 if (!string_format(view->cmd, format, id, id, id, id, id))
2214 /* Put the current ref_* value to the view title ref
2215 * member. This is needed by the blob view. Most other
2216 * views sets it automatically after loading because the
2217 * first line is a commit line. */
2218 string_copy_rev(view->ref, view->id);
2221 /* Special case for the pager view. */
2223 view->pipe = opt_pipe;
2226 view->pipe = popen(view->cmd, "r");
2232 set_nonblocking_input(TRUE);
2237 string_copy_rev(view->vid, view->id);
2242 for (i = 0; i < view->lines; i++)
2243 if (view->line[i].data)
2244 free(view->line[i].data);
2250 view->start_time = time(NULL);
2255 #define ITEM_CHUNK_SIZE 256
2257 realloc_items(void *mem, size_t *size, size_t new_size, size_t item_size)
2259 size_t num_chunks = *size / ITEM_CHUNK_SIZE;
2260 size_t num_chunks_new = (new_size + ITEM_CHUNK_SIZE - 1) / ITEM_CHUNK_SIZE;
2262 if (mem == NULL || num_chunks != num_chunks_new) {
2263 *size = num_chunks_new * ITEM_CHUNK_SIZE;
2264 mem = realloc(mem, *size * item_size);
2270 static struct line *
2271 realloc_lines(struct view *view, size_t line_size)
2273 size_t alloc = view->line_alloc;
2274 struct line *tmp = realloc_items(view->line, &alloc, line_size,
2275 sizeof(*view->line));
2281 view->line_alloc = alloc;
2282 view->line_size = line_size;
2287 update_view(struct view *view)
2289 char in_buffer[BUFSIZ];
2290 char out_buffer[BUFSIZ * 2];
2292 /* The number of lines to read. If too low it will cause too much
2293 * redrawing (and possible flickering), if too high responsiveness
2295 unsigned long lines = view->height;
2296 int redraw_from = -1;
2301 /* Only redraw if lines are visible. */
2302 if (view->offset + view->height >= view->lines)
2303 redraw_from = view->lines - view->offset;
2305 /* FIXME: This is probably not perfect for backgrounded views. */
2306 if (!realloc_lines(view, view->lines + lines))
2309 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
2310 size_t linelen = strlen(line);
2313 line[linelen - 1] = 0;
2315 if (opt_iconv != ICONV_NONE) {
2316 ICONV_CONST char *inbuf = line;
2317 size_t inlen = linelen;
2319 char *outbuf = out_buffer;
2320 size_t outlen = sizeof(out_buffer);
2324 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
2325 if (ret != (size_t) -1) {
2327 linelen = strlen(out_buffer);
2331 if (!view->ops->read(view, line))
2341 lines = view->lines;
2342 for (digits = 0; lines; digits++)
2345 /* Keep the displayed view in sync with line number scaling. */
2346 if (digits != view->digits) {
2347 view->digits = digits;
2352 if (!view_is_displayed(view))
2355 if (view == VIEW(REQ_VIEW_TREE)) {
2356 /* Clear the view and redraw everything since the tree sorting
2357 * might have rearranged things. */
2360 } else if (redraw_from >= 0) {
2361 /* If this is an incremental update, redraw the previous line
2362 * since for commits some members could have changed when
2363 * loading the main view. */
2364 if (redraw_from > 0)
2367 /* Since revision graph visualization requires knowledge
2368 * about the parent commit, it causes a further one-off
2369 * needed to be redrawn for incremental updates. */
2370 if (redraw_from > 0 && opt_rev_graph)
2373 /* Incrementally draw avoids flickering. */
2374 redraw_view_from(view, redraw_from);
2377 if (view == VIEW(REQ_VIEW_BLAME))
2378 redraw_view_dirty(view);
2380 /* Update the title _after_ the redraw so that if the redraw picks up a
2381 * commit reference in view->ref it'll be available here. */
2382 update_view_title(view);
2385 if (ferror(view->pipe)) {
2386 report("Failed to read: %s", strerror(errno));
2387 end_update(view, TRUE);
2389 } else if (feof(view->pipe)) {
2391 end_update(view, FALSE);
2397 report("Allocation failure");
2398 end_update(view, TRUE);
2402 static struct line *
2403 add_line_data(struct view *view, void *data, enum line_type type)
2405 struct line *line = &view->line[view->lines++];
2407 memset(line, 0, sizeof(*line));
2414 static struct line *
2415 add_line_text(struct view *view, char *data, enum line_type type)
2418 data = strdup(data);
2420 return data ? add_line_data(view, data, type) : NULL;
2429 OPEN_DEFAULT = 0, /* Use default view switching. */
2430 OPEN_SPLIT = 1, /* Split current view. */
2431 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2432 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2433 OPEN_NOMAXIMIZE = 8 /* Do not maximize the current view. */
2437 open_view(struct view *prev, enum request request, enum open_flags flags)
2439 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2440 bool split = !!(flags & OPEN_SPLIT);
2441 bool reload = !!(flags & OPEN_RELOAD);
2442 bool nomaximize = !!(flags & OPEN_NOMAXIMIZE);
2443 struct view *view = VIEW(request);
2444 int nviews = displayed_views();
2445 struct view *base_view = display[0];
2447 if (view == prev && nviews == 1 && !reload) {
2448 report("Already in %s view", view->name);
2452 if (view->git_dir && !opt_git_dir[0]) {
2453 report("The %s view is disabled in pager view", view->name);
2461 } else if (!nomaximize) {
2462 /* Maximize the current view. */
2463 memset(display, 0, sizeof(display));
2465 display[current_view] = view;
2468 /* Resize the view when switching between split- and full-screen,
2469 * or when switching between two different full-screen views. */
2470 if (nviews != displayed_views() ||
2471 (nviews == 1 && base_view != display[0]))
2475 end_update(view, TRUE);
2477 if (view->ops->open) {
2478 if (!view->ops->open(view)) {
2479 report("Failed to load %s view", view->name);
2483 } else if ((reload || strcmp(view->vid, view->id)) &&
2484 !begin_update(view)) {
2485 report("Failed to load %s view", view->name);
2489 if (split && prev->lineno - prev->offset >= prev->height) {
2490 /* Take the title line into account. */
2491 int lines = prev->lineno - prev->offset - prev->height + 1;
2493 /* Scroll the view that was split if the current line is
2494 * outside the new limited view. */
2495 do_scroll_view(prev, lines);
2498 if (prev && view != prev) {
2499 if (split && !backgrounded) {
2500 /* "Blur" the previous view. */
2501 update_view_title(prev);
2504 view->parent = prev;
2507 if (view->pipe && view->lines == 0) {
2508 /* Clear the old view and let the incremental updating refill
2517 /* If the view is backgrounded the above calls to report()
2518 * won't redraw the view title. */
2520 update_view_title(view);
2524 open_external_viewer(const char *cmd)
2526 def_prog_mode(); /* save current tty modes */
2527 endwin(); /* restore original tty modes */
2529 fprintf(stderr, "Press Enter to continue");
2536 open_mergetool(const char *file)
2538 char cmd[SIZEOF_STR];
2539 char file_sq[SIZEOF_STR];
2541 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2542 string_format(cmd, "git mergetool %s", file_sq)) {
2543 open_external_viewer(cmd);
2548 open_editor(bool from_root, const char *file)
2550 char cmd[SIZEOF_STR];
2551 char file_sq[SIZEOF_STR];
2553 char *prefix = from_root ? opt_cdup : "";
2555 editor = getenv("GIT_EDITOR");
2556 if (!editor && *opt_editor)
2557 editor = opt_editor;
2559 editor = getenv("VISUAL");
2561 editor = getenv("EDITOR");
2565 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2566 string_format(cmd, "%s %s%s", editor, prefix, file_sq)) {
2567 open_external_viewer(cmd);
2572 open_run_request(enum request request)
2574 struct run_request *req = get_run_request(request);
2575 char buf[SIZEOF_STR * 2];
2580 report("Unknown run request");
2588 char *next = strstr(cmd, "%(");
2589 int len = next - cmd;
2596 } else if (!strncmp(next, "%(head)", 7)) {
2599 } else if (!strncmp(next, "%(commit)", 9)) {
2602 } else if (!strncmp(next, "%(blob)", 7)) {
2606 report("Unknown replacement in run request: `%s`", req->cmd);
2610 if (!string_format_from(buf, &bufpos, "%.*s%s", len, cmd, value))
2614 next = strchr(next, ')') + 1;
2618 open_external_viewer(buf);
2622 * User request switch noodle
2626 view_driver(struct view *view, enum request request)
2630 if (request == REQ_NONE) {
2635 if (request > REQ_NONE) {
2636 open_run_request(request);
2637 /* FIXME: When all views can refresh always do this. */
2638 if (view == VIEW(REQ_VIEW_STATUS) ||
2639 view == VIEW(REQ_VIEW_STAGE))
2640 request = REQ_REFRESH;
2645 if (view && view->lines) {
2646 request = view->ops->request(view, request, &view->line[view->lineno]);
2647 if (request == REQ_NONE)
2654 case REQ_MOVE_PAGE_UP:
2655 case REQ_MOVE_PAGE_DOWN:
2656 case REQ_MOVE_FIRST_LINE:
2657 case REQ_MOVE_LAST_LINE:
2658 move_view(view, request);
2661 case REQ_SCROLL_LINE_DOWN:
2662 case REQ_SCROLL_LINE_UP:
2663 case REQ_SCROLL_PAGE_DOWN:
2664 case REQ_SCROLL_PAGE_UP:
2665 scroll_view(view, request);
2668 case REQ_VIEW_BLAME:
2670 report("No file chosen, press %s to open tree view",
2671 get_key(REQ_VIEW_TREE));
2674 open_view(view, request, OPEN_DEFAULT);
2679 report("No file chosen, press %s to open tree view",
2680 get_key(REQ_VIEW_TREE));
2683 open_view(view, request, OPEN_DEFAULT);
2686 case REQ_VIEW_PAGER:
2687 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2688 report("No pager content, press %s to run command from prompt",
2689 get_key(REQ_PROMPT));
2692 open_view(view, request, OPEN_DEFAULT);
2695 case REQ_VIEW_STAGE:
2696 if (!VIEW(REQ_VIEW_STAGE)->lines) {
2697 report("No stage content, press %s to open the status view and choose file",
2698 get_key(REQ_VIEW_STATUS));
2701 open_view(view, request, OPEN_DEFAULT);
2704 case REQ_VIEW_STATUS:
2705 if (opt_is_inside_work_tree == FALSE) {
2706 report("The status view requires a working tree");
2709 open_view(view, request, OPEN_DEFAULT);
2717 open_view(view, request, OPEN_DEFAULT);
2722 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2724 if ((view == VIEW(REQ_VIEW_DIFF) &&
2725 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2726 (view == VIEW(REQ_VIEW_DIFF) &&
2727 view->parent == VIEW(REQ_VIEW_BLAME)) ||
2728 (view == VIEW(REQ_VIEW_STAGE) &&
2729 view->parent == VIEW(REQ_VIEW_STATUS)) ||
2730 (view == VIEW(REQ_VIEW_BLOB) &&
2731 view->parent == VIEW(REQ_VIEW_TREE))) {
2734 view = view->parent;
2735 line = view->lineno;
2736 move_view(view, request);
2737 if (view_is_displayed(view))
2738 update_view_title(view);
2739 if (line != view->lineno)
2740 view->ops->request(view, REQ_ENTER,
2741 &view->line[view->lineno]);
2744 move_view(view, request);
2750 int nviews = displayed_views();
2751 int next_view = (current_view + 1) % nviews;
2753 if (next_view == current_view) {
2754 report("Only one view is displayed");
2758 current_view = next_view;
2759 /* Blur out the title of the previous view. */
2760 update_view_title(view);
2765 report("Refreshing is not yet supported for the %s view", view->name);
2769 if (displayed_views() == 2)
2770 open_view(view, VIEW_REQ(view), OPEN_DEFAULT);
2773 case REQ_TOGGLE_LINENO:
2774 opt_line_number = !opt_line_number;
2778 case REQ_TOGGLE_DATE:
2779 opt_date = !opt_date;
2783 case REQ_TOGGLE_AUTHOR:
2784 opt_author = !opt_author;
2788 case REQ_TOGGLE_REV_GRAPH:
2789 opt_rev_graph = !opt_rev_graph;
2793 case REQ_TOGGLE_REFS:
2794 opt_show_refs = !opt_show_refs;
2799 /* Always reload^Wrerun commands from the prompt. */
2800 open_view(view, opt_request, OPEN_RELOAD);
2804 case REQ_SEARCH_BACK:
2805 search_view(view, request);
2810 find_next(view, request);
2813 case REQ_STOP_LOADING:
2814 for (i = 0; i < ARRAY_SIZE(views); i++) {
2817 report("Stopped loading the %s view", view->name),
2818 end_update(view, TRUE);
2822 case REQ_SHOW_VERSION:
2823 report("tig-%s (built %s)", TIG_VERSION, __DATE__);
2826 case REQ_SCREEN_RESIZE:
2829 case REQ_SCREEN_REDRAW:
2834 report("Nothing to edit");
2839 report("Nothing to enter");
2843 case REQ_VIEW_CLOSE:
2844 /* XXX: Mark closed views by letting view->parent point to the
2845 * view itself. Parents to closed view should never be
2848 view->parent->parent != view->parent) {
2849 memset(display, 0, sizeof(display));
2851 display[current_view] = view->parent;
2852 view->parent = view;
2862 /* An unknown key will show most commonly used commands. */
2863 report("Unknown key, press 'h' for help");
2876 pager_draw(struct view *view, struct line *line, unsigned int lineno)
2878 char *text = line->data;
2880 if (opt_line_number && draw_lineno(view, lineno))
2883 draw_text(view, line->type, text, TRUE);
2888 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2890 char refbuf[SIZEOF_STR];
2894 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2897 pipe = popen(refbuf, "r");
2901 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2902 ref = chomp_string(ref);
2908 /* This is the only fatal call, since it can "corrupt" the buffer. */
2909 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2916 add_pager_refs(struct view *view, struct line *line)
2918 char buf[SIZEOF_STR];
2919 char *commit_id = (char *)line->data + STRING_SIZE("commit ");
2921 size_t bufpos = 0, refpos = 0;
2922 const char *sep = "Refs: ";
2923 bool is_tag = FALSE;
2925 assert(line->type == LINE_COMMIT);
2927 refs = get_refs(commit_id);
2929 if (view == VIEW(REQ_VIEW_DIFF))
2930 goto try_add_describe_ref;
2935 struct ref *ref = refs[refpos];
2936 char *fmt = ref->tag ? "%s[%s]" :
2937 ref->remote ? "%s<%s>" : "%s%s";
2939 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2944 } while (refs[refpos++]->next);
2946 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2947 try_add_describe_ref:
2948 /* Add <tag>-g<commit_id> "fake" reference. */
2949 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2956 if (!realloc_lines(view, view->line_size + 1))
2959 add_line_text(view, buf, LINE_PP_REFS);
2963 pager_read(struct view *view, char *data)
2970 line = add_line_text(view, data, get_line_type(data));
2974 if (line->type == LINE_COMMIT &&
2975 (view == VIEW(REQ_VIEW_DIFF) ||
2976 view == VIEW(REQ_VIEW_LOG)))
2977 add_pager_refs(view, line);
2983 pager_request(struct view *view, enum request request, struct line *line)
2987 if (request != REQ_ENTER)
2990 if (line->type == LINE_COMMIT &&
2991 (view == VIEW(REQ_VIEW_LOG) ||
2992 view == VIEW(REQ_VIEW_PAGER))) {
2993 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2997 /* Always scroll the view even if it was split. That way
2998 * you can use Enter to scroll through the log view and
2999 * split open each commit diff. */
3000 scroll_view(view, REQ_SCROLL_LINE_DOWN);
3002 /* FIXME: A minor workaround. Scrolling the view will call report("")
3003 * but if we are scrolling a non-current view this won't properly
3004 * update the view title. */
3006 update_view_title(view);
3012 pager_grep(struct view *view, struct line *line)
3015 char *text = line->data;
3020 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
3027 pager_select(struct view *view, struct line *line)
3029 if (line->type == LINE_COMMIT) {
3030 char *text = (char *)line->data + STRING_SIZE("commit ");
3032 if (view != VIEW(REQ_VIEW_PAGER))
3033 string_copy_rev(view->ref, text);
3034 string_copy_rev(ref_commit, text);
3038 static struct view_ops pager_ops = {
3054 help_open(struct view *view)
3057 int lines = ARRAY_SIZE(req_info) + 2;
3060 if (view->lines > 0)
3063 for (i = 0; i < ARRAY_SIZE(req_info); i++)
3064 if (!req_info[i].request)
3067 lines += run_requests + 1;
3069 view->line = calloc(lines, sizeof(*view->line));
3073 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
3075 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
3078 if (req_info[i].request == REQ_NONE)
3081 if (!req_info[i].request) {
3082 add_line_text(view, "", LINE_DEFAULT);
3083 add_line_text(view, req_info[i].help, LINE_DEFAULT);
3087 key = get_key(req_info[i].request);
3089 key = "(no key defined)";
3091 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
3094 add_line_text(view, buf, LINE_DEFAULT);
3098 add_line_text(view, "", LINE_DEFAULT);
3099 add_line_text(view, "External commands:", LINE_DEFAULT);
3102 for (i = 0; i < run_requests; i++) {
3103 struct run_request *req = get_run_request(REQ_NONE + i + 1);
3109 key = get_key_name(req->key);
3111 key = "(no key defined)";
3113 if (!string_format(buf, " %-10s %-14s `%s`",
3114 keymap_table[req->keymap].name,
3118 add_line_text(view, buf, LINE_DEFAULT);
3124 static struct view_ops help_ops = {
3139 struct tree_stack_entry {
3140 struct tree_stack_entry *prev; /* Entry below this in the stack */
3141 unsigned long lineno; /* Line number to restore */
3142 char *name; /* Position of name in opt_path */
3145 /* The top of the path stack. */
3146 static struct tree_stack_entry *tree_stack = NULL;
3147 unsigned long tree_lineno = 0;
3150 pop_tree_stack_entry(void)
3152 struct tree_stack_entry *entry = tree_stack;
3154 tree_lineno = entry->lineno;
3156 tree_stack = entry->prev;
3161 push_tree_stack_entry(char *name, unsigned long lineno)
3163 struct tree_stack_entry *entry = calloc(1, sizeof(*entry));
3164 size_t pathlen = strlen(opt_path);
3169 entry->prev = tree_stack;
3170 entry->name = opt_path + pathlen;
3173 if (!string_format_from(opt_path, &pathlen, "%s/", name)) {
3174 pop_tree_stack_entry();
3178 /* Move the current line to the first tree entry. */
3180 entry->lineno = lineno;
3183 /* Parse output from git-ls-tree(1):
3185 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
3186 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
3187 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
3188 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
3191 #define SIZEOF_TREE_ATTR \
3192 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
3194 #define TREE_UP_FORMAT "040000 tree %s\t.."
3197 tree_compare_entry(enum line_type type1, char *name1,
3198 enum line_type type2, char *name2)
3200 if (type1 != type2) {
3201 if (type1 == LINE_TREE_DIR)
3206 return strcmp(name1, name2);
3210 tree_path(struct line *line)
3212 char *path = line->data;
3214 return path + SIZEOF_TREE_ATTR;
3218 tree_read(struct view *view, char *text)
3220 size_t textlen = text ? strlen(text) : 0;
3221 char buf[SIZEOF_STR];
3223 enum line_type type;
3224 bool first_read = view->lines == 0;
3228 if (textlen <= SIZEOF_TREE_ATTR)
3231 type = text[STRING_SIZE("100644 ")] == 't'
3232 ? LINE_TREE_DIR : LINE_TREE_FILE;
3235 /* Add path info line */
3236 if (!string_format(buf, "Directory path /%s", opt_path) ||
3237 !realloc_lines(view, view->line_size + 1) ||
3238 !add_line_text(view, buf, LINE_DEFAULT))
3241 /* Insert "link" to parent directory. */
3243 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
3244 !realloc_lines(view, view->line_size + 1) ||
3245 !add_line_text(view, buf, LINE_TREE_DIR))
3250 /* Strip the path part ... */
3252 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
3253 size_t striplen = strlen(opt_path);
3254 char *path = text + SIZEOF_TREE_ATTR;
3256 if (pathlen > striplen)
3257 memmove(path, path + striplen,
3258 pathlen - striplen + 1);
3261 /* Skip "Directory ..." and ".." line. */
3262 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
3263 struct line *line = &view->line[pos];
3264 char *path1 = tree_path(line);
3265 char *path2 = text + SIZEOF_TREE_ATTR;
3266 int cmp = tree_compare_entry(line->type, path1, type, path2);
3271 text = strdup(text);
3275 if (view->lines > pos)
3276 memmove(&view->line[pos + 1], &view->line[pos],
3277 (view->lines - pos) * sizeof(*line));
3279 line = &view->line[pos];
3286 if (!add_line_text(view, text, type))
3289 if (tree_lineno > view->lineno) {
3290 view->lineno = tree_lineno;
3298 tree_request(struct view *view, enum request request, struct line *line)
3300 enum open_flags flags;
3302 if (request == REQ_VIEW_BLAME) {
3303 char *filename = tree_path(line);
3305 if (line->type == LINE_TREE_DIR) {
3306 report("Cannot show blame for directory %s", opt_path);
3310 string_copy(opt_ref, view->vid);
3311 string_format(opt_file, "%s%s", opt_path, filename);
3314 if (request == REQ_TREE_PARENT) {
3317 request = REQ_ENTER;
3318 line = &view->line[1];
3320 /* quit view if at top of tree */
3321 return REQ_VIEW_CLOSE;
3324 if (request != REQ_ENTER)
3327 /* Cleanup the stack if the tree view is at a different tree. */
3328 while (!*opt_path && tree_stack)
3329 pop_tree_stack_entry();
3331 switch (line->type) {
3333 /* Depending on whether it is a subdir or parent (updir?) link
3334 * mangle the path buffer. */
3335 if (line == &view->line[1] && *opt_path) {
3336 pop_tree_stack_entry();
3339 char *basename = tree_path(line);
3341 push_tree_stack_entry(basename, view->lineno);
3344 /* Trees and subtrees share the same ID, so they are not not
3345 * unique like blobs. */
3346 flags = OPEN_RELOAD;
3347 request = REQ_VIEW_TREE;
3350 case LINE_TREE_FILE:
3351 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3352 request = REQ_VIEW_BLOB;
3359 open_view(view, request, flags);
3360 if (request == REQ_VIEW_TREE) {
3361 view->lineno = tree_lineno;
3368 tree_select(struct view *view, struct line *line)
3370 char *text = (char *)line->data + STRING_SIZE("100644 blob ");
3372 if (line->type == LINE_TREE_FILE) {
3373 string_copy_rev(ref_blob, text);
3375 } else if (line->type != LINE_TREE_DIR) {
3379 string_copy_rev(view->ref, text);
3382 static struct view_ops tree_ops = {
3393 blob_read(struct view *view, char *line)
3397 return add_line_text(view, line, LINE_DEFAULT) != NULL;
3400 static struct view_ops blob_ops = {
3413 * Loading the blame view is a two phase job:
3415 * 1. File content is read either using opt_file from the
3416 * filesystem or using git-cat-file.
3417 * 2. Then blame information is incrementally added by
3418 * reading output from git-blame.
3421 struct blame_commit {
3422 char id[SIZEOF_REV]; /* SHA1 ID. */
3423 char title[128]; /* First line of the commit message. */
3424 char author[75]; /* Author of the commit. */
3425 struct tm time; /* Date from the author ident. */
3426 char filename[128]; /* Name of file. */
3430 struct blame_commit *commit;
3431 unsigned int header:1;
3435 #define BLAME_CAT_FILE_CMD "git cat-file blob %s:%s"
3436 #define BLAME_INCREMENTAL_CMD "git blame --incremental %s %s"
3439 blame_open(struct view *view)
3441 char path[SIZEOF_STR];
3442 char ref[SIZEOF_STR] = "";
3444 if (sq_quote(path, 0, opt_file) >= sizeof(path))
3447 if (*opt_ref && sq_quote(ref, 0, opt_ref) >= sizeof(ref))
3451 if (!string_format(view->cmd, BLAME_CAT_FILE_CMD, ref, path))
3454 view->pipe = fopen(opt_file, "r");
3456 !string_format(view->cmd, BLAME_CAT_FILE_CMD, "HEAD", path))
3461 view->pipe = popen(view->cmd, "r");
3465 if (!string_format(view->cmd, BLAME_INCREMENTAL_CMD, ref, path))
3468 string_format(view->ref, "%s ...", opt_file);
3469 string_copy_rev(view->vid, opt_file);
3470 set_nonblocking_input(TRUE);
3475 for (i = 0; i < view->lines; i++)
3476 free(view->line[i].data);
3480 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3481 view->offset = view->lines = view->lineno = 0;
3483 view->start_time = time(NULL);
3488 static struct blame_commit *
3489 get_blame_commit(struct view *view, const char *id)
3493 for (i = 0; i < view->lines; i++) {
3494 struct blame *blame = view->line[i].data;
3499 if (!strncmp(blame->commit->id, id, SIZEOF_REV - 1))
3500 return blame->commit;
3504 struct blame_commit *commit = calloc(1, sizeof(*commit));
3507 string_ncopy(commit->id, id, SIZEOF_REV);
3513 parse_number(char **posref, size_t *number, size_t min, size_t max)
3515 char *pos = *posref;
3518 pos = strchr(pos + 1, ' ');
3519 if (!pos || !isdigit(pos[1]))
3521 *number = atoi(pos + 1);
3522 if (*number < min || *number > max)
3529 static struct blame_commit *
3530 parse_blame_commit(struct view *view, char *text, int *blamed)
3532 struct blame_commit *commit;
3533 struct blame *blame;
3534 char *pos = text + SIZEOF_REV - 1;
3538 if (strlen(text) <= SIZEOF_REV || *pos != ' ')
3541 if (!parse_number(&pos, &lineno, 1, view->lines) ||
3542 !parse_number(&pos, &group, 1, view->lines - lineno + 1))
3545 commit = get_blame_commit(view, text);
3551 struct line *line = &view->line[lineno + group - 1];
3554 blame->commit = commit;
3555 blame->header = !group;
3563 blame_read_file(struct view *view, char *line)
3568 if (view->lines > 0)
3569 pipe = popen(view->cmd, "r");
3570 else if (!view->parent)
3571 die("No blame exist for %s", view->vid);
3574 report("Failed to load blame data");
3583 size_t linelen = strlen(line);
3584 struct blame *blame = malloc(sizeof(*blame) + linelen);
3586 blame->commit = NULL;
3587 strncpy(blame->text, line, linelen);
3588 blame->text[linelen] = 0;
3589 return add_line_data(view, blame, LINE_BLAME_ID) != NULL;
3594 match_blame_header(const char *name, char **line)
3596 size_t namelen = strlen(name);
3597 bool matched = !strncmp(name, *line, namelen);
3606 blame_read(struct view *view, char *line)
3608 static struct blame_commit *commit = NULL;
3609 static int blamed = 0;
3610 static time_t author_time;
3613 return blame_read_file(view, line);
3619 string_format(view->ref, "%s", view->vid);
3620 if (view_is_displayed(view)) {
3621 update_view_title(view);
3622 redraw_view_from(view, 0);
3628 commit = parse_blame_commit(view, line, &blamed);
3629 string_format(view->ref, "%s %2d%%", view->vid,
3630 blamed * 100 / view->lines);
3632 } else if (match_blame_header("author ", &line)) {
3633 string_ncopy(commit->author, line, strlen(line));
3635 } else if (match_blame_header("author-time ", &line)) {
3636 author_time = (time_t) atol(line);
3638 } else if (match_blame_header("author-tz ", &line)) {
3641 tz = ('0' - line[1]) * 60 * 60 * 10;
3642 tz += ('0' - line[2]) * 60 * 60;
3643 tz += ('0' - line[3]) * 60;
3644 tz += ('0' - line[4]) * 60;
3650 gmtime_r(&author_time, &commit->time);
3652 } else if (match_blame_header("summary ", &line)) {
3653 string_ncopy(commit->title, line, strlen(line));
3655 } else if (match_blame_header("filename ", &line)) {
3656 string_ncopy(commit->filename, line, strlen(line));
3664 blame_draw(struct view *view, struct line *line, unsigned int lineno)
3666 struct blame *blame = line->data;
3667 struct tm *time = NULL;
3668 char *id = NULL, *author = NULL;
3670 if (blame->commit && *blame->commit->filename) {
3671 id = blame->commit->id;
3672 author = blame->commit->author;
3673 time = &blame->commit->time;
3676 if (opt_date && draw_date(view, time))
3680 draw_field(view, LINE_MAIN_AUTHOR, author, AUTHOR_COLS, TRUE))
3683 if (draw_field(view, LINE_BLAME_ID, id, ID_COLS, FALSE))
3686 if (draw_lineno(view, lineno))
3689 draw_text(view, LINE_DEFAULT, blame->text, TRUE);
3694 blame_request(struct view *view, enum request request, struct line *line)
3696 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3697 struct blame *blame = line->data;
3701 if (!blame->commit) {
3702 report("No commit loaded yet");
3706 if (!strcmp(blame->commit->id, NULL_ID)) {
3707 char path[SIZEOF_STR];
3709 if (sq_quote(path, 0, view->vid) >= sizeof(path))
3711 string_format(opt_cmd, "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s 2>/dev/null", path);
3714 open_view(view, REQ_VIEW_DIFF, flags);
3725 blame_grep(struct view *view, struct line *line)
3727 struct blame *blame = line->data;
3728 struct blame_commit *commit = blame->commit;
3731 #define MATCH(text, on) \
3732 (on && *text && regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3735 char buf[DATE_COLS + 1];
3737 if (MATCH(commit->title, 1) ||
3738 MATCH(commit->author, opt_author) ||
3739 MATCH(commit->id, opt_date))
3742 if (strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time) &&
3747 return MATCH(blame->text, 1);
3753 blame_select(struct view *view, struct line *line)
3755 struct blame *blame = line->data;
3756 struct blame_commit *commit = blame->commit;
3761 if (!strcmp(commit->id, NULL_ID))
3762 string_ncopy(ref_commit, "HEAD", 4);
3764 string_copy_rev(ref_commit, commit->id);
3767 static struct view_ops blame_ops = {
3785 char rev[SIZEOF_REV];
3786 char name[SIZEOF_STR];
3790 char rev[SIZEOF_REV];
3791 char name[SIZEOF_STR];
3795 static char status_onbranch[SIZEOF_STR];
3796 static struct status stage_status;
3797 static enum line_type stage_line_type;
3798 static size_t stage_chunks;
3799 static int *stage_chunk;
3801 /* Get fields from the diff line:
3802 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
3805 status_get_diff(struct status *file, char *buf, size_t bufsize)
3807 char *old_mode = buf + 1;
3808 char *new_mode = buf + 8;
3809 char *old_rev = buf + 15;
3810 char *new_rev = buf + 56;
3811 char *status = buf + 97;
3814 old_mode[-1] != ':' ||
3815 new_mode[-1] != ' ' ||
3816 old_rev[-1] != ' ' ||
3817 new_rev[-1] != ' ' ||
3821 file->status = *status;
3823 string_copy_rev(file->old.rev, old_rev);
3824 string_copy_rev(file->new.rev, new_rev);
3826 file->old.mode = strtoul(old_mode, NULL, 8);
3827 file->new.mode = strtoul(new_mode, NULL, 8);
3829 file->old.name[0] = file->new.name[0] = 0;
3835 status_run(struct view *view, const char cmd[], char status, enum line_type type)
3837 struct status *file = NULL;
3838 struct status *unmerged = NULL;
3839 char buf[SIZEOF_STR * 4];
3843 pipe = popen(cmd, "r");
3847 add_line_data(view, NULL, type);
3849 while (!feof(pipe) && !ferror(pipe)) {
3853 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
3856 bufsize += readsize;
3858 /* Process while we have NUL chars. */
3859 while ((sep = memchr(buf, 0, bufsize))) {
3860 size_t sepsize = sep - buf + 1;
3863 if (!realloc_lines(view, view->line_size + 1))
3866 file = calloc(1, sizeof(*file));
3870 add_line_data(view, file, type);
3873 /* Parse diff info part. */
3875 file->status = status;
3877 string_copy(file->old.rev, NULL_ID);
3879 } else if (!file->status) {
3880 if (!status_get_diff(file, buf, sepsize))
3884 memmove(buf, sep + 1, bufsize);
3886 sep = memchr(buf, 0, bufsize);
3889 sepsize = sep - buf + 1;
3891 /* Collapse all 'M'odified entries that
3892 * follow a associated 'U'nmerged entry.
3894 if (file->status == 'U') {
3897 } else if (unmerged) {
3898 int collapse = !strcmp(buf, unmerged->new.name);
3909 /* Grab the old name for rename/copy. */
3910 if (!*file->old.name &&
3911 (file->status == 'R' || file->status == 'C')) {
3912 sepsize = sep - buf + 1;
3913 string_ncopy(file->old.name, buf, sepsize);
3915 memmove(buf, sep + 1, bufsize);
3917 sep = memchr(buf, 0, bufsize);
3920 sepsize = sep - buf + 1;
3923 /* git-ls-files just delivers a NUL separated
3924 * list of file names similar to the second half
3925 * of the git-diff-* output. */
3926 string_ncopy(file->new.name, buf, sepsize);
3927 if (!*file->old.name)
3928 string_copy(file->old.name, file->new.name);
3930 memmove(buf, sep + 1, bufsize);
3941 if (!view->line[view->lines - 1].data)
3942 add_line_data(view, NULL, LINE_STAT_NONE);
3948 /* Don't show unmerged entries in the staged section. */
3949 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached -M HEAD"
3950 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
3951 #define STATUS_LIST_OTHER_CMD \
3952 "git ls-files -z --others --exclude-per-directory=.gitignore"
3953 #define STATUS_LIST_NO_HEAD_CMD \
3954 "git ls-files -z --cached --exclude-per-directory=.gitignore"
3956 #define STATUS_DIFF_INDEX_SHOW_CMD \
3957 "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s %s 2>/dev/null"
3959 #define STATUS_DIFF_FILES_SHOW_CMD \
3960 "git diff-files --root --patch-with-stat -C -M -- %s %s 2>/dev/null"
3962 #define STATUS_DIFF_NO_HEAD_SHOW_CMD \
3963 "git diff --no-color --patch-with-stat /dev/null %s 2>/dev/null"
3965 /* First parse staged info using git-diff-index(1), then parse unstaged
3966 * info using git-diff-files(1), and finally untracked files using
3967 * git-ls-files(1). */
3969 status_open(struct view *view)
3971 struct stat statbuf;
3972 char exclude[SIZEOF_STR];
3973 char indexcmd[SIZEOF_STR] = STATUS_DIFF_INDEX_CMD;
3974 char othercmd[SIZEOF_STR] = STATUS_LIST_OTHER_CMD;
3975 unsigned long prev_lineno = view->lineno;
3976 char indexstatus = 0;
3979 for (i = 0; i < view->lines; i++)
3980 free(view->line[i].data);
3982 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3985 if (!realloc_lines(view, view->line_size + 7))
3988 add_line_data(view, NULL, LINE_STAT_HEAD);
3990 string_copy(status_onbranch, "Initial commit");
3991 else if (!*opt_head)
3992 string_copy(status_onbranch, "Not currently on any branch");
3993 else if (!string_format(status_onbranch, "On branch %s", opt_head))
3997 string_copy(indexcmd, STATUS_LIST_NO_HEAD_CMD);
4001 if (!string_format(exclude, "%s/info/exclude", opt_git_dir))
4004 if (stat(exclude, &statbuf) >= 0) {
4005 size_t cmdsize = strlen(othercmd);
4007 if (!string_format_from(othercmd, &cmdsize, " %s", "--exclude-from=") ||
4008 sq_quote(othercmd, cmdsize, exclude) >= sizeof(othercmd))
4011 cmdsize = strlen(indexcmd);
4013 (!string_format_from(indexcmd, &cmdsize, " %s", "--exclude-from=") ||
4014 sq_quote(indexcmd, cmdsize, exclude) >= sizeof(indexcmd)))
4018 system("git update-index -q --refresh >/dev/null 2>/dev/null");
4020 if (!status_run(view, indexcmd, indexstatus, LINE_STAT_STAGED) ||
4021 !status_run(view, STATUS_DIFF_FILES_CMD, 0, LINE_STAT_UNSTAGED) ||
4022 !status_run(view, othercmd, '?', LINE_STAT_UNTRACKED))
4025 /* If all went well restore the previous line number to stay in
4026 * the context or select a line with something that can be
4028 if (prev_lineno >= view->lines)
4029 prev_lineno = view->lines - 1;
4030 while (prev_lineno < view->lines && !view->line[prev_lineno].data)
4032 while (prev_lineno > 0 && !view->line[prev_lineno].data)
4035 /* If the above fails, always skip the "On branch" line. */
4036 if (prev_lineno < view->lines)
4037 view->lineno = prev_lineno;
4041 if (view->lineno < view->offset)
4042 view->offset = view->lineno;
4043 else if (view->offset + view->height <= view->lineno)
4044 view->offset = view->lineno - view->height + 1;
4050 status_draw(struct view *view, struct line *line, unsigned int lineno)
4052 struct status *status = line->data;
4053 enum line_type type;
4057 switch (line->type) {
4058 case LINE_STAT_STAGED:
4059 type = LINE_STAT_SECTION;
4060 text = "Changes to be committed:";
4063 case LINE_STAT_UNSTAGED:
4064 type = LINE_STAT_SECTION;
4065 text = "Changed but not updated:";
4068 case LINE_STAT_UNTRACKED:
4069 type = LINE_STAT_SECTION;
4070 text = "Untracked files:";
4073 case LINE_STAT_NONE:
4074 type = LINE_DEFAULT;
4075 text = " (no files)";
4078 case LINE_STAT_HEAD:
4079 type = LINE_STAT_HEAD;
4080 text = status_onbranch;
4087 static char buf[] = { '?', ' ', ' ', ' ', 0 };
4089 buf[0] = status->status;
4090 if (draw_text(view, line->type, buf, TRUE))
4092 type = LINE_DEFAULT;
4093 text = status->new.name;
4096 draw_text(view, type, text, TRUE);
4101 status_enter(struct view *view, struct line *line)
4103 struct status *status = line->data;
4104 char oldpath[SIZEOF_STR] = "";
4105 char newpath[SIZEOF_STR] = "";
4108 enum open_flags split;
4110 if (line->type == LINE_STAT_NONE ||
4111 (!status && line[1].type == LINE_STAT_NONE)) {
4112 report("No file to diff");
4117 if (sq_quote(oldpath, 0, status->old.name) >= sizeof(oldpath))
4119 /* Diffs for unmerged entries are empty when pasing the
4120 * new path, so leave it empty. */
4121 if (status->status != 'U' &&
4122 sq_quote(newpath, 0, status->new.name) >= sizeof(newpath))
4127 line->type != LINE_STAT_UNTRACKED &&
4128 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
4131 switch (line->type) {
4132 case LINE_STAT_STAGED:
4134 if (!string_format_from(opt_cmd, &cmdsize,
4135 STATUS_DIFF_NO_HEAD_SHOW_CMD,
4139 if (!string_format_from(opt_cmd, &cmdsize,
4140 STATUS_DIFF_INDEX_SHOW_CMD,
4146 info = "Staged changes to %s";
4148 info = "Staged changes";
4151 case LINE_STAT_UNSTAGED:
4152 if (!string_format_from(opt_cmd, &cmdsize,
4153 STATUS_DIFF_FILES_SHOW_CMD, oldpath, newpath))
4156 info = "Unstaged changes to %s";
4158 info = "Unstaged changes";
4161 case LINE_STAT_UNTRACKED:
4166 report("No file to show");
4170 opt_pipe = fopen(status->new.name, "r");
4171 info = "Untracked file %s";
4174 case LINE_STAT_HEAD:
4178 die("line type %d not handled in switch", line->type);
4181 split = view_is_displayed(view) ? OPEN_SPLIT : 0;
4182 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | split);
4183 if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) {
4185 stage_status = *status;
4187 memset(&stage_status, 0, sizeof(stage_status));
4190 stage_line_type = line->type;
4192 string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.new.name);
4199 status_exists(struct status *status, enum line_type type)
4201 struct view *view = VIEW(REQ_VIEW_STATUS);
4204 for (line = view->line; line < view->line + view->lines; line++) {
4205 struct status *pos = line->data;
4207 if (line->type == type && pos &&
4208 !strcmp(status->new.name, pos->new.name))
4217 status_update_prepare(enum line_type type)
4219 char cmd[SIZEOF_STR];
4223 type != LINE_STAT_UNTRACKED &&
4224 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4228 case LINE_STAT_STAGED:
4229 string_add(cmd, cmdsize, "git update-index -z --index-info");
4232 case LINE_STAT_UNSTAGED:
4233 case LINE_STAT_UNTRACKED:
4234 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
4238 die("line type %d not handled in switch", type);
4241 return popen(cmd, "w");
4245 status_update_write(FILE *pipe, struct status *status, enum line_type type)
4247 char buf[SIZEOF_STR];
4252 case LINE_STAT_STAGED:
4253 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
4256 status->old.name, 0))
4260 case LINE_STAT_UNSTAGED:
4261 case LINE_STAT_UNTRACKED:
4262 if (!string_format_from(buf, &bufsize, "%s%c", status->new.name, 0))
4267 die("line type %d not handled in switch", type);
4270 while (!ferror(pipe) && written < bufsize) {
4271 written += fwrite(buf + written, 1, bufsize - written, pipe);
4274 return written == bufsize;
4278 status_update_file(struct status *status, enum line_type type)
4280 FILE *pipe = status_update_prepare(type);
4286 result = status_update_write(pipe, status, type);
4292 status_update_files(struct view *view, struct line *line)
4294 FILE *pipe = status_update_prepare(line->type);
4296 struct line *pos = view->line + view->lines;
4303 for (pos = line; pos < view->line + view->lines && pos->data; pos++)
4306 for (file = 0, done = 0; result && file < files; line++, file++) {
4307 int almost_done = file * 100 / files;
4309 if (almost_done > done) {
4311 string_format(view->ref, "updating file %u of %u (%d%% done)",
4313 update_view_title(view);
4315 result = status_update_write(pipe, line->data, line->type);
4323 status_update(struct view *view)
4325 struct line *line = &view->line[view->lineno];
4327 assert(view->lines);
4330 /* This should work even for the "On branch" line. */
4331 if (line < view->line + view->lines && !line[1].data) {
4332 report("Nothing to update");
4336 if (!status_update_files(view, line + 1)) {
4337 report("Failed to update file status");
4341 } else if (!status_update_file(line->data, line->type)) {
4342 report("Failed to update file status");
4350 status_request(struct view *view, enum request request, struct line *line)
4352 struct status *status = line->data;
4355 case REQ_STATUS_UPDATE:
4356 if (!status_update(view))
4360 case REQ_STATUS_MERGE:
4361 if (!status || status->status != 'U') {
4362 report("Merging only possible for files with unmerged status ('U').");
4365 open_mergetool(status->new.name);
4372 open_editor(status->status != '?', status->new.name);
4375 case REQ_VIEW_BLAME:
4377 string_copy(opt_file, status->new.name);
4383 /* After returning the status view has been split to
4384 * show the stage view. No further reloading is
4386 status_enter(view, line);
4390 /* Simply reload the view. */
4397 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
4403 status_select(struct view *view, struct line *line)
4405 struct status *status = line->data;
4406 char file[SIZEOF_STR] = "all files";
4410 if (status && !string_format(file, "'%s'", status->new.name))
4413 if (!status && line[1].type == LINE_STAT_NONE)
4416 switch (line->type) {
4417 case LINE_STAT_STAGED:
4418 text = "Press %s to unstage %s for commit";
4421 case LINE_STAT_UNSTAGED:
4422 text = "Press %s to stage %s for commit";
4425 case LINE_STAT_UNTRACKED:
4426 text = "Press %s to stage %s for addition";
4429 case LINE_STAT_HEAD:
4430 case LINE_STAT_NONE:
4431 text = "Nothing to update";
4435 die("line type %d not handled in switch", line->type);
4438 if (status && status->status == 'U') {
4439 text = "Press %s to resolve conflict in %s";
4440 key = get_key(REQ_STATUS_MERGE);
4443 key = get_key(REQ_STATUS_UPDATE);
4446 string_format(view->ref, text, key, file);
4450 status_grep(struct view *view, struct line *line)
4452 struct status *status = line->data;
4453 enum { S_STATUS, S_NAME, S_END } state;
4460 for (state = S_STATUS; state < S_END; state++) {
4464 case S_NAME: text = status->new.name; break;
4466 buf[0] = status->status;
4474 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4481 static struct view_ops status_ops = {
4493 stage_diff_line(FILE *pipe, struct line *line)
4495 char *buf = line->data;
4496 size_t bufsize = strlen(buf);
4499 while (!ferror(pipe) && written < bufsize) {
4500 written += fwrite(buf + written, 1, bufsize - written, pipe);
4505 return written == bufsize;
4509 stage_diff_write(FILE *pipe, struct line *line, struct line *end)
4511 while (line < end) {
4512 if (!stage_diff_line(pipe, line++))
4514 if (line->type == LINE_DIFF_CHUNK ||
4515 line->type == LINE_DIFF_HEADER)
4522 static struct line *
4523 stage_diff_find(struct view *view, struct line *line, enum line_type type)
4525 for (; view->line < line; line--)
4526 if (line->type == type)
4533 stage_update_chunk(struct view *view, struct line *chunk)
4535 char cmd[SIZEOF_STR];
4537 struct line *diff_hdr;
4540 diff_hdr = stage_diff_find(view, chunk, LINE_DIFF_HEADER);
4545 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4548 if (!string_format_from(cmd, &cmdsize,
4549 "git apply --whitespace=nowarn --cached %s - && "
4550 "git update-index -q --unmerged --refresh 2>/dev/null",
4551 stage_line_type == LINE_STAT_STAGED ? "-R" : ""))
4554 pipe = popen(cmd, "w");
4558 if (!stage_diff_write(pipe, diff_hdr, chunk) ||
4559 !stage_diff_write(pipe, chunk, view->line + view->lines))
4564 return chunk ? TRUE : FALSE;
4568 stage_update(struct view *view, struct line *line)
4570 struct line *chunk = NULL;
4572 if (!opt_no_head && stage_line_type != LINE_STAT_UNTRACKED)
4573 chunk = stage_diff_find(view, line, LINE_DIFF_CHUNK);
4576 if (!stage_update_chunk(view, chunk)) {
4577 report("Failed to apply chunk");
4581 } else if (!stage_status.status) {
4582 view = VIEW(REQ_VIEW_STATUS);
4584 for (line = view->line; line < view->line + view->lines; line++)
4585 if (line->type == stage_line_type)
4588 if (!status_update_files(view, line + 1)) {
4589 report("Failed to update files");
4593 } else if (!status_update_file(&stage_status, stage_line_type)) {
4594 report("Failed to update file");
4602 stage_next(struct view *view, struct line *line)
4606 if (!stage_chunks) {
4607 static size_t alloc = 0;
4610 for (line = view->line; line < view->line + view->lines; line++) {
4611 if (line->type != LINE_DIFF_CHUNK)
4614 tmp = realloc_items(stage_chunk, &alloc,
4615 stage_chunks, sizeof(*tmp));
4617 report("Allocation failure");
4622 stage_chunk[stage_chunks++] = line - view->line;
4626 for (i = 0; i < stage_chunks; i++) {
4627 if (stage_chunk[i] > view->lineno) {
4628 do_scroll_view(view, stage_chunk[i] - view->lineno);
4629 report("Chunk %d of %d", i + 1, stage_chunks);
4634 report("No next chunk found");
4638 stage_request(struct view *view, enum request request, struct line *line)
4641 case REQ_STATUS_UPDATE:
4642 if (!stage_update(view, line))
4646 case REQ_STAGE_NEXT:
4647 if (stage_line_type == LINE_STAT_UNTRACKED) {
4648 report("File is untracked; press %s to add",
4649 get_key(REQ_STATUS_UPDATE));
4652 stage_next(view, line);
4656 if (!stage_status.new.name[0])
4659 open_editor(stage_status.status != '?', stage_status.new.name);
4663 /* Reload everything ... */
4666 case REQ_VIEW_BLAME:
4667 if (stage_status.new.name[0]) {
4668 string_copy(opt_file, stage_status.new.name);
4674 return pager_request(view, request, line);
4680 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD | OPEN_NOMAXIMIZE);
4682 /* Check whether the staged entry still exists, and close the
4683 * stage view if it doesn't. */
4684 if (!status_exists(&stage_status, stage_line_type))
4685 return REQ_VIEW_CLOSE;
4687 if (stage_line_type == LINE_STAT_UNTRACKED)
4688 opt_pipe = fopen(stage_status.new.name, "r");
4690 string_copy(opt_cmd, view->cmd);
4691 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | OPEN_NOMAXIMIZE);
4696 static struct view_ops stage_ops = {
4712 char id[SIZEOF_REV]; /* SHA1 ID. */
4713 char title[128]; /* First line of the commit message. */
4714 char author[75]; /* Author of the commit. */
4715 struct tm time; /* Date from the author ident. */
4716 struct ref **refs; /* Repository references. */
4717 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
4718 size_t graph_size; /* The width of the graph array. */
4719 bool has_parents; /* Rewritten --parents seen. */
4722 /* Size of rev graph with no "padding" columns */
4723 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
4726 struct rev_graph *prev, *next, *parents;
4727 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
4729 struct commit *commit;
4731 unsigned int boundary:1;
4734 /* Parents of the commit being visualized. */
4735 static struct rev_graph graph_parents[4];
4737 /* The current stack of revisions on the graph. */
4738 static struct rev_graph graph_stacks[4] = {
4739 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
4740 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
4741 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
4742 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
4746 graph_parent_is_merge(struct rev_graph *graph)
4748 return graph->parents->size > 1;
4752 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
4754 struct commit *commit = graph->commit;
4756 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
4757 commit->graph[commit->graph_size++] = symbol;
4761 done_rev_graph(struct rev_graph *graph)
4763 if (graph_parent_is_merge(graph) &&
4764 graph->pos < graph->size - 1 &&
4765 graph->next->size == graph->size + graph->parents->size - 1) {
4766 size_t i = graph->pos + graph->parents->size - 1;
4768 graph->commit->graph_size = i * 2;
4769 while (i < graph->next->size - 1) {
4770 append_to_rev_graph(graph, ' ');
4771 append_to_rev_graph(graph, '\\');
4776 graph->size = graph->pos = 0;
4777 graph->commit = NULL;
4778 memset(graph->parents, 0, sizeof(*graph->parents));
4782 push_rev_graph(struct rev_graph *graph, char *parent)
4786 /* "Collapse" duplicate parents lines.
4788 * FIXME: This needs to also update update the drawn graph but
4789 * for now it just serves as a method for pruning graph lines. */
4790 for (i = 0; i < graph->size; i++)
4791 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
4794 if (graph->size < SIZEOF_REVITEMS) {
4795 string_copy_rev(graph->rev[graph->size++], parent);
4800 get_rev_graph_symbol(struct rev_graph *graph)
4804 if (graph->boundary)
4805 symbol = REVGRAPH_BOUND;
4806 else if (graph->parents->size == 0)
4807 symbol = REVGRAPH_INIT;
4808 else if (graph_parent_is_merge(graph))
4809 symbol = REVGRAPH_MERGE;
4810 else if (graph->pos >= graph->size)
4811 symbol = REVGRAPH_BRANCH;
4813 symbol = REVGRAPH_COMMIT;
4819 draw_rev_graph(struct rev_graph *graph)
4822 chtype separator, line;
4824 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
4825 static struct rev_filler fillers[] = {
4831 chtype symbol = get_rev_graph_symbol(graph);
4832 struct rev_filler *filler;
4835 if (opt_line_graphics)
4836 fillers[DEFAULT].line = line_graphics[LINE_GRAPHIC_VLINE];
4838 filler = &fillers[DEFAULT];
4840 for (i = 0; i < graph->pos; i++) {
4841 append_to_rev_graph(graph, filler->line);
4842 if (graph_parent_is_merge(graph->prev) &&
4843 graph->prev->pos == i)
4844 filler = &fillers[RSHARP];
4846 append_to_rev_graph(graph, filler->separator);
4849 /* Place the symbol for this revision. */
4850 append_to_rev_graph(graph, symbol);
4852 if (graph->prev->size > graph->size)
4853 filler = &fillers[RDIAG];
4855 filler = &fillers[DEFAULT];
4859 for (; i < graph->size; i++) {
4860 append_to_rev_graph(graph, filler->separator);
4861 append_to_rev_graph(graph, filler->line);
4862 if (graph_parent_is_merge(graph->prev) &&
4863 i < graph->prev->pos + graph->parents->size)
4864 filler = &fillers[RSHARP];
4865 if (graph->prev->size > graph->size)
4866 filler = &fillers[LDIAG];
4869 if (graph->prev->size > graph->size) {
4870 append_to_rev_graph(graph, filler->separator);
4871 if (filler->line != ' ')
4872 append_to_rev_graph(graph, filler->line);
4876 /* Prepare the next rev graph */
4878 prepare_rev_graph(struct rev_graph *graph)
4882 /* First, traverse all lines of revisions up to the active one. */
4883 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
4884 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
4887 push_rev_graph(graph->next, graph->rev[graph->pos]);
4890 /* Interleave the new revision parent(s). */
4891 for (i = 0; !graph->boundary && i < graph->parents->size; i++)
4892 push_rev_graph(graph->next, graph->parents->rev[i]);
4894 /* Lastly, put any remaining revisions. */
4895 for (i = graph->pos + 1; i < graph->size; i++)
4896 push_rev_graph(graph->next, graph->rev[i]);
4900 update_rev_graph(struct rev_graph *graph)
4902 /* If this is the finalizing update ... */
4904 prepare_rev_graph(graph);
4906 /* Graph visualization needs a one rev look-ahead,
4907 * so the first update doesn't visualize anything. */
4908 if (!graph->prev->commit)
4911 draw_rev_graph(graph->prev);
4912 done_rev_graph(graph->prev->prev);
4921 main_draw(struct view *view, struct line *line, unsigned int lineno)
4923 struct commit *commit = line->data;
4925 if (!*commit->author)
4928 if (opt_date && draw_date(view, &commit->time))
4932 draw_field(view, LINE_MAIN_AUTHOR, commit->author, AUTHOR_COLS, TRUE))
4935 if (opt_rev_graph && commit->graph_size &&
4936 draw_graphic(view, LINE_MAIN_REVGRAPH, commit->graph, commit->graph_size))
4939 if (opt_show_refs && commit->refs) {
4943 enum line_type type;
4945 if (commit->refs[i]->head)
4946 type = LINE_MAIN_HEAD;
4947 else if (commit->refs[i]->ltag)
4948 type = LINE_MAIN_LOCAL_TAG;
4949 else if (commit->refs[i]->tag)
4950 type = LINE_MAIN_TAG;
4951 else if (commit->refs[i]->tracked)
4952 type = LINE_MAIN_TRACKED;
4953 else if (commit->refs[i]->remote)
4954 type = LINE_MAIN_REMOTE;
4956 type = LINE_MAIN_REF;
4958 if (draw_text(view, type, "[", TRUE) ||
4959 draw_text(view, type, commit->refs[i]->name, TRUE) ||
4960 draw_text(view, type, "]", TRUE))
4963 if (draw_text(view, LINE_DEFAULT, " ", TRUE))
4965 } while (commit->refs[i++]->next);
4968 draw_text(view, LINE_DEFAULT, commit->title, TRUE);
4972 /* Reads git log --pretty=raw output and parses it into the commit struct. */
4974 main_read(struct view *view, char *line)
4976 static struct rev_graph *graph = graph_stacks;
4977 enum line_type type;
4978 struct commit *commit;
4981 if (!view->lines && !view->parent)
4982 die("No revisions match the given arguments.");
4983 update_rev_graph(graph);
4987 type = get_line_type(line);
4988 if (type == LINE_COMMIT) {
4989 commit = calloc(1, sizeof(struct commit));
4993 line += STRING_SIZE("commit ");
4995 graph->boundary = 1;
4999 string_copy_rev(commit->id, line);
5000 commit->refs = get_refs(commit->id);
5001 graph->commit = commit;
5002 add_line_data(view, commit, LINE_MAIN_COMMIT);
5004 while ((line = strchr(line, ' '))) {
5006 push_rev_graph(graph->parents, line);
5007 commit->has_parents = TRUE;
5014 commit = view->line[view->lines - 1].data;
5018 if (commit->has_parents)
5020 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
5025 /* Parse author lines where the name may be empty:
5026 * author <email@address.tld> 1138474660 +0100
5028 char *ident = line + STRING_SIZE("author ");
5029 char *nameend = strchr(ident, '<');
5030 char *emailend = strchr(ident, '>');
5032 if (!nameend || !emailend)
5035 update_rev_graph(graph);
5036 graph = graph->next;
5038 *nameend = *emailend = 0;
5039 ident = chomp_string(ident);
5041 ident = chomp_string(nameend + 1);
5046 string_ncopy(commit->author, ident, strlen(ident));
5048 /* Parse epoch and timezone */
5049 if (emailend[1] == ' ') {
5050 char *secs = emailend + 2;
5051 char *zone = strchr(secs, ' ');
5052 time_t time = (time_t) atol(secs);
5054 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
5058 tz = ('0' - zone[1]) * 60 * 60 * 10;
5059 tz += ('0' - zone[2]) * 60 * 60;
5060 tz += ('0' - zone[3]) * 60;
5061 tz += ('0' - zone[4]) * 60;
5069 gmtime_r(&time, &commit->time);
5074 /* Fill in the commit title if it has not already been set. */
5075 if (commit->title[0])
5078 /* Require titles to start with a non-space character at the
5079 * offset used by git log. */
5080 if (strncmp(line, " ", 4))
5083 /* Well, if the title starts with a whitespace character,
5084 * try to be forgiving. Otherwise we end up with no title. */
5085 while (isspace(*line))
5089 /* FIXME: More graceful handling of titles; append "..." to
5090 * shortened titles, etc. */
5092 string_ncopy(commit->title, line, strlen(line));
5099 main_request(struct view *view, enum request request, struct line *line)
5101 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
5103 if (request == REQ_ENTER)
5104 open_view(view, REQ_VIEW_DIFF, flags);
5112 grep_refs(struct ref **refs, regex_t *regex)
5120 if (regexec(regex, refs[i]->name, 1, &pmatch, 0) != REG_NOMATCH)
5122 } while (refs[i++]->next);
5128 main_grep(struct view *view, struct line *line)
5130 struct commit *commit = line->data;
5131 enum { S_TITLE, S_AUTHOR, S_DATE, S_REFS, S_END } state;
5132 char buf[DATE_COLS + 1];
5135 for (state = S_TITLE; state < S_END; state++) {
5139 case S_TITLE: text = commit->title; break;
5143 text = commit->author;
5148 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
5155 if (grep_refs(commit->refs, view->regex) == TRUE)
5162 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
5170 main_select(struct view *view, struct line *line)
5172 struct commit *commit = line->data;
5174 string_copy_rev(view->ref, commit->id);
5175 string_copy_rev(ref_commit, view->ref);
5178 static struct view_ops main_ops = {
5190 * Unicode / UTF-8 handling
5192 * NOTE: Much of the following code for dealing with unicode is derived from
5193 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
5194 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
5197 /* I've (over)annotated a lot of code snippets because I am not entirely
5198 * confident that the approach taken by this small UTF-8 interface is correct.
5202 unicode_width(unsigned long c)
5205 (c <= 0x115f /* Hangul Jamo */
5208 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
5210 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
5211 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
5212 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
5213 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
5214 || (c >= 0xffe0 && c <= 0xffe6)
5215 || (c >= 0x20000 && c <= 0x2fffd)
5216 || (c >= 0x30000 && c <= 0x3fffd)))
5220 return opt_tab_size;
5225 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
5226 * Illegal bytes are set one. */
5227 static const unsigned char utf8_bytes[256] = {
5228 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,
5229 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,
5230 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,
5231 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,
5232 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,
5233 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,
5234 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,
5235 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,
5238 /* Decode UTF-8 multi-byte representation into a unicode character. */
5239 static inline unsigned long
5240 utf8_to_unicode(const char *string, size_t length)
5242 unsigned long unicode;
5246 unicode = string[0];
5249 unicode = (string[0] & 0x1f) << 6;
5250 unicode += (string[1] & 0x3f);
5253 unicode = (string[0] & 0x0f) << 12;
5254 unicode += ((string[1] & 0x3f) << 6);
5255 unicode += (string[2] & 0x3f);
5258 unicode = (string[0] & 0x0f) << 18;
5259 unicode += ((string[1] & 0x3f) << 12);
5260 unicode += ((string[2] & 0x3f) << 6);
5261 unicode += (string[3] & 0x3f);
5264 unicode = (string[0] & 0x0f) << 24;
5265 unicode += ((string[1] & 0x3f) << 18);
5266 unicode += ((string[2] & 0x3f) << 12);
5267 unicode += ((string[3] & 0x3f) << 6);
5268 unicode += (string[4] & 0x3f);
5271 unicode = (string[0] & 0x01) << 30;
5272 unicode += ((string[1] & 0x3f) << 24);
5273 unicode += ((string[2] & 0x3f) << 18);
5274 unicode += ((string[3] & 0x3f) << 12);
5275 unicode += ((string[4] & 0x3f) << 6);
5276 unicode += (string[5] & 0x3f);
5279 die("Invalid unicode length");
5282 /* Invalid characters could return the special 0xfffd value but NUL
5283 * should be just as good. */
5284 return unicode > 0xffff ? 0 : unicode;
5287 /* Calculates how much of string can be shown within the given maximum width
5288 * and sets trimmed parameter to non-zero value if all of string could not be
5289 * shown. If the reserve flag is TRUE, it will reserve at least one
5290 * trailing character, which can be useful when drawing a delimiter.
5292 * Returns the number of bytes to output from string to satisfy max_width. */
5294 utf8_length(const char *string, int *width, size_t max_width, int *trimmed, bool reserve)
5296 const char *start = string;
5297 const char *end = strchr(string, '\0');
5298 unsigned char last_bytes = 0;
5299 size_t last_ucwidth = 0;
5304 while (string < end) {
5305 int c = *(unsigned char *) string;
5306 unsigned char bytes = utf8_bytes[c];
5308 unsigned long unicode;
5310 if (string + bytes > end)
5313 /* Change representation to figure out whether
5314 * it is a single- or double-width character. */
5316 unicode = utf8_to_unicode(string, bytes);
5317 /* FIXME: Graceful handling of invalid unicode character. */
5321 ucwidth = unicode_width(unicode);
5323 if (*width > max_width) {
5326 if (reserve && *width == max_width) {
5327 string -= last_bytes;
5328 *width -= last_ucwidth;
5335 last_ucwidth = ucwidth;
5338 return string - start;
5346 /* Whether or not the curses interface has been initialized. */
5347 static bool cursed = FALSE;
5349 /* The status window is used for polling keystrokes. */
5350 static WINDOW *status_win;
5352 static bool status_empty = TRUE;
5354 /* Update status and title window. */
5356 report(const char *msg, ...)
5358 struct view *view = display[current_view];
5364 char buf[SIZEOF_STR];
5367 va_start(args, msg);
5368 if (vsnprintf(buf, sizeof(buf), msg, args) >= sizeof(buf)) {
5369 buf[sizeof(buf) - 1] = 0;
5370 buf[sizeof(buf) - 2] = '.';
5371 buf[sizeof(buf) - 3] = '.';
5372 buf[sizeof(buf) - 4] = '.';
5378 if (!status_empty || *msg) {
5381 va_start(args, msg);
5383 wmove(status_win, 0, 0);
5385 vwprintw(status_win, msg, args);
5386 status_empty = FALSE;
5388 status_empty = TRUE;
5390 wclrtoeol(status_win);
5391 wrefresh(status_win);
5396 update_view_title(view);
5397 update_display_cursor(view);
5400 /* Controls when nodelay should be in effect when polling user input. */
5402 set_nonblocking_input(bool loading)
5404 static unsigned int loading_views;
5406 if ((loading == FALSE && loading_views-- == 1) ||
5407 (loading == TRUE && loading_views++ == 0))
5408 nodelay(status_win, loading);
5416 /* Initialize the curses library */
5417 if (isatty(STDIN_FILENO)) {
5418 cursed = !!initscr();
5420 /* Leave stdin and stdout alone when acting as a pager. */
5421 FILE *io = fopen("/dev/tty", "r+");
5424 die("Failed to open /dev/tty");
5425 cursed = !!newterm(NULL, io, io);
5429 die("Failed to initialize curses");
5431 nonl(); /* Tell curses not to do NL->CR/NL on output */
5432 cbreak(); /* Take input chars one at a time, no wait for \n */
5433 noecho(); /* Don't echo input */
5434 leaveok(stdscr, TRUE);
5439 getmaxyx(stdscr, y, x);
5440 status_win = newwin(1, 0, y - 1, 0);
5442 die("Failed to create status window");
5444 /* Enable keyboard mapping */
5445 keypad(status_win, TRUE);
5446 wbkgdset(status_win, get_line_attr(LINE_STATUS));
5448 TABSIZE = opt_tab_size;
5449 if (opt_line_graphics) {
5450 line_graphics[LINE_GRAPHIC_VLINE] = ACS_VLINE;
5455 read_prompt(const char *prompt)
5457 enum { READING, STOP, CANCEL } status = READING;
5458 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
5461 while (status == READING) {
5467 foreach_view (view, i)
5472 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
5473 wclrtoeol(status_win);
5475 /* Refresh, accept single keystroke of input */
5476 key = wgetch(status_win);
5481 status = pos ? STOP : CANCEL;
5499 if (pos >= sizeof(buf)) {
5500 report("Input string too long");
5505 buf[pos++] = (char) key;
5509 /* Clear the status window */
5510 status_empty = FALSE;
5513 if (status == CANCEL)
5522 * Repository references
5525 static struct ref *refs = NULL;
5526 static size_t refs_alloc = 0;
5527 static size_t refs_size = 0;
5529 /* Id <-> ref store */
5530 static struct ref ***id_refs = NULL;
5531 static size_t id_refs_alloc = 0;
5532 static size_t id_refs_size = 0;
5534 static struct ref **
5537 struct ref ***tmp_id_refs;
5538 struct ref **ref_list = NULL;
5539 size_t ref_list_alloc = 0;
5540 size_t ref_list_size = 0;
5543 for (i = 0; i < id_refs_size; i++)
5544 if (!strcmp(id, id_refs[i][0]->id))
5547 tmp_id_refs = realloc_items(id_refs, &id_refs_alloc, id_refs_size + 1,
5552 id_refs = tmp_id_refs;
5554 for (i = 0; i < refs_size; i++) {
5557 if (strcmp(id, refs[i].id))
5560 tmp = realloc_items(ref_list, &ref_list_alloc,
5561 ref_list_size + 1, sizeof(*ref_list));
5569 if (ref_list_size > 0)
5570 ref_list[ref_list_size - 1]->next = 1;
5571 ref_list[ref_list_size] = &refs[i];
5573 /* XXX: The properties of the commit chains ensures that we can
5574 * safely modify the shared ref. The repo references will
5575 * always be similar for the same id. */
5576 ref_list[ref_list_size]->next = 0;
5581 id_refs[id_refs_size++] = ref_list;
5587 read_ref(char *id, size_t idlen, char *name, size_t namelen)
5592 bool remote = FALSE;
5593 bool tracked = FALSE;
5594 bool check_replace = FALSE;
5597 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
5598 if (!strcmp(name + namelen - 3, "^{}")) {
5601 if (refs_size > 0 && refs[refs_size - 1].ltag == TRUE)
5602 check_replace = TRUE;
5608 namelen -= STRING_SIZE("refs/tags/");
5609 name += STRING_SIZE("refs/tags/");
5611 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
5613 namelen -= STRING_SIZE("refs/remotes/");
5614 name += STRING_SIZE("refs/remotes/");
5615 tracked = !strcmp(opt_remote, name);
5617 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5618 namelen -= STRING_SIZE("refs/heads/");
5619 name += STRING_SIZE("refs/heads/");
5620 head = !strncmp(opt_head, name, namelen);
5622 } else if (!strcmp(name, "HEAD")) {
5623 opt_no_head = FALSE;
5627 if (check_replace && !strcmp(name, refs[refs_size - 1].name)) {
5628 /* it's an annotated tag, replace the previous sha1 with the
5629 * resolved commit id; relies on the fact git-ls-remote lists
5630 * the commit id of an annotated tag right beofre the commit id
5632 refs[refs_size - 1].ltag = ltag;
5633 string_copy_rev(refs[refs_size - 1].id, id);
5637 refs = realloc_items(refs, &refs_alloc, refs_size + 1, sizeof(*refs));
5641 ref = &refs[refs_size++];
5642 ref->name = malloc(namelen + 1);
5646 strncpy(ref->name, name, namelen);
5647 ref->name[namelen] = 0;
5651 ref->remote = remote;
5652 ref->tracked = tracked;
5653 string_copy_rev(ref->id, id);
5661 const char *cmd_env = getenv("TIG_LS_REMOTE");
5662 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
5664 return read_properties(popen(cmd, "r"), "\t", read_ref);
5668 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
5670 if (!strcmp(name, "i18n.commitencoding"))
5671 string_ncopy(opt_encoding, value, valuelen);
5673 if (!strcmp(name, "core.editor"))
5674 string_ncopy(opt_editor, value, valuelen);
5676 /* branch.<head>.remote */
5678 !strncmp(name, "branch.", 7) &&
5679 !strncmp(name + 7, opt_head, strlen(opt_head)) &&
5680 !strcmp(name + 7 + strlen(opt_head), ".remote"))
5681 string_ncopy(opt_remote, value, valuelen);
5683 if (*opt_head && *opt_remote &&
5684 !strncmp(name, "branch.", 7) &&
5685 !strncmp(name + 7, opt_head, strlen(opt_head)) &&
5686 !strcmp(name + 7 + strlen(opt_head), ".merge")) {
5687 size_t from = strlen(opt_remote);
5689 if (!strncmp(value, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5690 value += STRING_SIZE("refs/heads/");
5691 valuelen -= STRING_SIZE("refs/heads/");
5694 if (!string_format_from(opt_remote, &from, "/%s", value))
5702 load_git_config(void)
5704 return read_properties(popen(GIT_CONFIG " --list", "r"),
5705 "=", read_repo_config_option);
5709 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
5711 if (!opt_git_dir[0]) {
5712 string_ncopy(opt_git_dir, name, namelen);
5714 } else if (opt_is_inside_work_tree == -1) {
5715 /* This can be 3 different values depending on the
5716 * version of git being used. If git-rev-parse does not
5717 * understand --is-inside-work-tree it will simply echo
5718 * the option else either "true" or "false" is printed.
5719 * Default to true for the unknown case. */
5720 opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
5722 } else if (opt_cdup[0] == ' ') {
5723 string_ncopy(opt_cdup, name, namelen);
5725 if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5726 namelen -= STRING_SIZE("refs/heads/");
5727 name += STRING_SIZE("refs/heads/");
5728 string_ncopy(opt_head, name, namelen);
5736 load_repo_info(void)
5739 FILE *pipe = popen("(git rev-parse --git-dir --is-inside-work-tree "
5740 " --show-cdup; git symbolic-ref HEAD) 2>/dev/null", "r");
5742 /* XXX: The line outputted by "--show-cdup" can be empty so
5743 * initialize it to something invalid to make it possible to
5744 * detect whether it has been set or not. */
5747 result = read_properties(pipe, "=", read_repo_info);
5748 if (opt_cdup[0] == ' ')
5755 read_properties(FILE *pipe, const char *separators,
5756 int (*read_property)(char *, size_t, char *, size_t))
5758 char buffer[BUFSIZ];
5765 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
5770 name = chomp_string(name);
5771 namelen = strcspn(name, separators);
5773 if (name[namelen]) {
5775 value = chomp_string(name + namelen + 1);
5776 valuelen = strlen(value);
5783 state = read_property(name, namelen, value, valuelen);
5786 if (state != ERR && ferror(pipe))
5799 static void __NORETURN
5802 /* XXX: Restore tty modes and let the OS cleanup the rest! */
5808 static void __NORETURN
5809 die(const char *err, ...)
5815 va_start(args, err);
5816 fputs("tig: ", stderr);
5817 vfprintf(stderr, err, args);
5818 fputs("\n", stderr);
5825 warn(const char *msg, ...)
5829 va_start(args, msg);
5830 fputs("tig warning: ", stderr);
5831 vfprintf(stderr, msg, args);
5832 fputs("\n", stderr);
5837 main(int argc, char *argv[])
5840 enum request request;
5843 signal(SIGINT, quit);
5845 if (setlocale(LC_ALL, "")) {
5846 char *codeset = nl_langinfo(CODESET);
5848 string_ncopy(opt_codeset, codeset, strlen(codeset));
5851 if (load_repo_info() == ERR)
5852 die("Failed to load repo info.");
5854 if (load_options() == ERR)
5855 die("Failed to load user config.");
5857 if (load_git_config() == ERR)
5858 die("Failed to load repo config.");
5860 if (!parse_options(argc, argv))
5863 /* Require a git repository unless when running in pager mode. */
5864 if (!opt_git_dir[0] && opt_request != REQ_VIEW_PAGER)
5865 die("Not a git repository");
5867 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
5870 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
5871 opt_iconv = iconv_open(opt_codeset, opt_encoding);
5872 if (opt_iconv == ICONV_NONE)
5873 die("Failed to initialize character set conversion");
5876 if (*opt_git_dir && load_refs() == ERR)
5877 die("Failed to load refs.");
5879 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
5880 view->cmd_env = getenv(view->cmd_env);
5882 request = opt_request;
5886 while (view_driver(display[current_view], request)) {
5890 foreach_view (view, i)
5893 /* Refresh, accept single keystroke of input */
5894 key = wgetch(status_win);
5896 /* wgetch() with nodelay() enabled returns ERR when there's no
5903 request = get_keybinding(display[current_view]->keymap, key);
5905 /* Some low-level request handling. This keeps access to
5906 * status_win restricted. */
5910 char *cmd = read_prompt(":");
5912 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
5913 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
5914 opt_request = REQ_VIEW_DIFF;
5916 opt_request = REQ_VIEW_PAGER;
5925 case REQ_SEARCH_BACK:
5927 const char *prompt = request == REQ_SEARCH
5929 char *search = read_prompt(prompt);
5932 string_ncopy(opt_search, search, strlen(search));
5937 case REQ_SCREEN_RESIZE:
5941 getmaxyx(stdscr, height, width);
5943 /* Resize the status view and let the view driver take
5944 * care of resizing the displayed views. */
5945 wresize(status_win, 1, width);
5946 mvwin(status_win, height - 1, 0);
5947 wrefresh(status_win);