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
51 #define __NORETURN __attribute__((__noreturn__))
56 static void __NORETURN die(const char *err, ...);
57 static void warn(const char *msg, ...);
58 static void report(const char *msg, ...);
59 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, size_t, char *, size_t));
60 static void set_nonblocking_input(bool loading);
61 static size_t utf8_length(const char *string, size_t max_width, int *trimmed, bool reserve);
63 #define ABS(x) ((x) >= 0 ? (x) : -(x))
64 #define MIN(x, y) ((x) < (y) ? (x) : (y))
66 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
67 #define STRING_SIZE(x) (sizeof(x) - 1)
69 #define SIZEOF_STR 1024 /* Default string size. */
70 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
71 #define SIZEOF_REV 41 /* Holds a SHA-1 and an ending NUL */
75 #define REVGRAPH_INIT 'I'
76 #define REVGRAPH_MERGE 'M'
77 #define REVGRAPH_BRANCH '+'
78 #define REVGRAPH_COMMIT '*'
79 #define REVGRAPH_BOUND '^'
80 #define REVGRAPH_LINE '|'
82 #define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
84 /* This color name can be used to refer to the default term colors. */
85 #define COLOR_DEFAULT (-1)
87 #define ICONV_NONE ((iconv_t) -1)
89 #define ICONV_CONST /* nothing */
92 /* The format and size of the date column in the main view. */
93 #define DATE_FORMAT "%Y-%m-%d %H:%M"
94 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
96 #define AUTHOR_COLS 20
99 /* The default interval between line numbers. */
100 #define NUMBER_INTERVAL 5
104 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
107 #define GIT_CONFIG "git config"
110 #define TIG_LS_REMOTE \
111 "git ls-remote $(git rev-parse --git-dir) 2>/dev/null"
113 #define TIG_DIFF_CMD \
114 "git show --pretty=fuller --no-color --root --patch-with-stat --find-copies-harder -C %s 2>/dev/null"
116 #define TIG_LOG_CMD \
117 "git log --no-color --cc --stat -n100 %s 2>/dev/null"
119 #define TIG_MAIN_CMD \
120 "git log --no-color --topo-order --parents --boundary --pretty=raw %s 2>/dev/null"
122 #define TIG_TREE_CMD \
125 #define TIG_BLOB_CMD \
126 "git cat-file blob %s"
128 /* XXX: Needs to be defined to the empty string. */
129 #define TIG_HELP_CMD ""
130 #define TIG_PAGER_CMD ""
131 #define TIG_STATUS_CMD ""
132 #define TIG_STAGE_CMD ""
133 #define TIG_BLAME_CMD ""
135 /* Some ascii-shorthands fitted into the ncurses namespace. */
137 #define KEY_RETURN '\r'
142 char *name; /* Ref name; tag or head names are shortened. */
143 char id[SIZEOF_REV]; /* Commit SHA1 ID */
144 unsigned int tag:1; /* Is it a tag? */
145 unsigned int ltag:1; /* If so, is the tag local? */
146 unsigned int remote:1; /* Is it a remote ref? */
147 unsigned int next:1; /* For ref lists: are there more refs? */
150 static struct ref **get_refs(char *id);
159 set_from_int_map(struct int_map *map, size_t map_size,
160 int *value, const char *name, int namelen)
165 for (i = 0; i < map_size; i++)
166 if (namelen == map[i].namelen &&
167 !strncasecmp(name, map[i].name, namelen)) {
168 *value = map[i].value;
181 string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
183 if (srclen > dstlen - 1)
186 strncpy(dst, src, srclen);
190 /* Shorthands for safely copying into a fixed buffer. */
192 #define string_copy(dst, src) \
193 string_ncopy_do(dst, sizeof(dst), src, sizeof(src))
195 #define string_ncopy(dst, src, srclen) \
196 string_ncopy_do(dst, sizeof(dst), src, srclen)
198 #define string_copy_rev(dst, src) \
199 string_ncopy_do(dst, SIZEOF_REV, src, SIZEOF_REV - 1)
201 #define string_add(dst, from, src) \
202 string_ncopy_do(dst + (from), sizeof(dst) - (from), src, sizeof(src))
205 chomp_string(char *name)
209 while (isspace(*name))
212 namelen = strlen(name) - 1;
213 while (namelen > 0 && isspace(name[namelen]))
220 string_nformat(char *buf, size_t bufsize, size_t *bufpos, const char *fmt, ...)
223 size_t pos = bufpos ? *bufpos : 0;
226 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
232 return pos >= bufsize ? FALSE : TRUE;
235 #define string_format(buf, fmt, args...) \
236 string_nformat(buf, sizeof(buf), NULL, fmt, args)
238 #define string_format_from(buf, from, fmt, args...) \
239 string_nformat(buf, sizeof(buf), from, fmt, args)
242 string_enum_compare(const char *str1, const char *str2, int len)
246 #define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
248 /* Diff-Header == DIFF_HEADER */
249 for (i = 0; i < len; i++) {
250 if (toupper(str1[i]) == toupper(str2[i]))
253 if (string_enum_sep(str1[i]) &&
254 string_enum_sep(str2[i]))
257 return str1[i] - str2[i];
265 * NOTE: The following is a slightly modified copy of the git project's shell
266 * quoting routines found in the quote.c file.
268 * Help to copy the thing properly quoted for the shell safety. any single
269 * quote is replaced with '\'', any exclamation point is replaced with '\!',
270 * and the whole thing is enclosed in a
273 * original sq_quote result
274 * name ==> name ==> 'name'
275 * a b ==> a b ==> 'a b'
276 * a'b ==> a'\''b ==> 'a'\''b'
277 * a!b ==> a'\!'b ==> 'a'\!'b'
281 sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
285 #define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0)
288 while ((c = *src++)) {
289 if (c == '\'' || c == '!') {
300 if (bufsize < SIZEOF_STR)
312 /* XXX: Keep the view request first and in sync with views[]. */ \
313 REQ_GROUP("View switching") \
314 REQ_(VIEW_MAIN, "Show main view"), \
315 REQ_(VIEW_DIFF, "Show diff view"), \
316 REQ_(VIEW_LOG, "Show log view"), \
317 REQ_(VIEW_TREE, "Show tree view"), \
318 REQ_(VIEW_BLOB, "Show blob view"), \
319 REQ_(VIEW_BLAME, "Show blame view"), \
320 REQ_(VIEW_HELP, "Show help page"), \
321 REQ_(VIEW_PAGER, "Show pager view"), \
322 REQ_(VIEW_STATUS, "Show status view"), \
323 REQ_(VIEW_STAGE, "Show stage view"), \
325 REQ_GROUP("View manipulation") \
326 REQ_(ENTER, "Enter current line and scroll"), \
327 REQ_(NEXT, "Move to next"), \
328 REQ_(PREVIOUS, "Move to previous"), \
329 REQ_(VIEW_NEXT, "Move focus to next view"), \
330 REQ_(REFRESH, "Reload and refresh"), \
331 REQ_(VIEW_CLOSE, "Close the current view"), \
332 REQ_(QUIT, "Close all views and quit"), \
334 REQ_GROUP("Cursor navigation") \
335 REQ_(MOVE_UP, "Move cursor one line up"), \
336 REQ_(MOVE_DOWN, "Move cursor one line down"), \
337 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
338 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
339 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
340 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
342 REQ_GROUP("Scrolling") \
343 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
344 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
345 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
346 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
348 REQ_GROUP("Searching") \
349 REQ_(SEARCH, "Search the view"), \
350 REQ_(SEARCH_BACK, "Search backwards in the view"), \
351 REQ_(FIND_NEXT, "Find next search match"), \
352 REQ_(FIND_PREV, "Find previous search match"), \
355 REQ_(PROMPT, "Bring up the prompt"), \
356 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
357 REQ_(SCREEN_RESIZE, "Resize the screen"), \
358 REQ_(SHOW_VERSION, "Show version information"), \
359 REQ_(STOP_LOADING, "Stop all loading views"), \
360 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
361 REQ_(TOGGLE_DATE, "Toggle date display"), \
362 REQ_(TOGGLE_AUTHOR, "Toggle author display"), \
363 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"), \
364 REQ_(TOGGLE_REFS, "Toggle reference display (tags/branches)"), \
365 REQ_(STATUS_UPDATE, "Update file status"), \
366 REQ_(STATUS_MERGE, "Merge file using external tool"), \
367 REQ_(TREE_PARENT, "Switch to parent directory in tree view"), \
368 REQ_(EDIT, "Open in editor"), \
369 REQ_(NONE, "Do nothing")
372 /* User action requests. */
374 #define REQ_GROUP(help)
375 #define REQ_(req, help) REQ_##req
377 /* Offset all requests to avoid conflicts with ncurses getch values. */
378 REQ_OFFSET = KEY_MAX + 1,
385 struct request_info {
386 enum request request;
392 static struct request_info req_info[] = {
393 #define REQ_GROUP(help) { 0, NULL, 0, (help) },
394 #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
401 get_request(const char *name)
403 int namelen = strlen(name);
406 for (i = 0; i < ARRAY_SIZE(req_info); i++)
407 if (req_info[i].namelen == namelen &&
408 !string_enum_compare(req_info[i].name, name, namelen))
409 return req_info[i].request;
419 static const char usage[] =
420 "tig " TIG_VERSION " (" __DATE__ ")\n"
422 "Usage: tig [options] [revs] [--] [paths]\n"
423 " or: tig show [options] [revs] [--] [paths]\n"
424 " or: tig blame [rev] path\n"
426 " or: tig < [git command output]\n"
429 " -v, --version Show version and exit\n"
430 " -h, --help Show help message and exit";
432 /* Option and state variables. */
433 static bool opt_date = TRUE;
434 static bool opt_author = TRUE;
435 static bool opt_line_number = FALSE;
436 static bool opt_rev_graph = FALSE;
437 static bool opt_show_refs = TRUE;
438 static int opt_num_interval = NUMBER_INTERVAL;
439 static int opt_tab_size = TABSIZE;
440 static enum request opt_request = REQ_VIEW_MAIN;
441 static char opt_cmd[SIZEOF_STR] = "";
442 static char opt_path[SIZEOF_STR] = "";
443 static char opt_file[SIZEOF_STR] = "";
444 static char opt_ref[SIZEOF_REF] = "";
445 static FILE *opt_pipe = NULL;
446 static char opt_encoding[20] = "UTF-8";
447 static bool opt_utf8 = TRUE;
448 static char opt_codeset[20] = "UTF-8";
449 static iconv_t opt_iconv = ICONV_NONE;
450 static char opt_search[SIZEOF_STR] = "";
451 static char opt_cdup[SIZEOF_STR] = "";
452 static char opt_git_dir[SIZEOF_STR] = "";
453 static signed char opt_is_inside_work_tree = -1; /* set to TRUE or FALSE */
454 static char opt_editor[SIZEOF_STR] = "";
462 check_option(char *opt, char short_name, char *name, enum option_type type, ...)
472 int namelen = strlen(name);
476 if (strncmp(opt, name, namelen))
479 if (opt[namelen] == '=')
480 value = opt + namelen + 1;
483 if (!short_name || opt[1] != short_name)
488 va_start(args, type);
489 if (type == OPT_INT) {
490 number = va_arg(args, int *);
492 *number = atoi(value);
499 /* Returns the index of log or diff command or -1 to exit. */
501 parse_options(int argc, char *argv[])
511 subcommand = argv[1];
512 if (!strcmp(subcommand, "status")) {
513 opt_request = REQ_VIEW_STATUS;
515 warn("ignoring arguments after `%s'", subcommand);
518 } else if (!strcmp(subcommand, "blame")) {
519 opt_request = REQ_VIEW_BLAME;
520 if (argc <= 2 || argc > 4)
521 die("invalid number of options to blame\n\n%s", usage);
525 string_ncopy(opt_ref, argv[i], strlen(argv[i]));
529 string_ncopy(opt_file, argv[i], strlen(argv[i]));
532 } else if (!strcmp(subcommand, "show")) {
533 opt_request = REQ_VIEW_DIFF;
535 } else if (!strcmp(subcommand, "log") || !strcmp(subcommand, "diff")) {
536 opt_request = subcommand[0] == 'l'
537 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
538 warn("`tig %s' has been deprecated", subcommand);
544 for (i = 1 + !!subcommand; i < argc; i++) {
547 if (opt[0] && opt[0] != '-')
550 if (!strcmp(opt, "--")) {
555 if (check_option(opt, 'v', "version", OPT_NONE)) {
556 printf("tig version %s\n", TIG_VERSION);
560 if (check_option(opt, 'h', "help", OPT_NONE)) {
565 if (!strcmp(opt, "-S")) {
566 warn("`%s' has been deprecated; use `tig status' instead", opt);
567 opt_request = REQ_VIEW_STATUS;
571 if (!strcmp(opt, "-l")) {
572 opt_request = REQ_VIEW_LOG;
573 } else if (!strcmp(opt, "-d")) {
574 opt_request = REQ_VIEW_DIFF;
575 } else if (check_option(opt, 'n', "line-number", OPT_INT, &opt_num_interval)) {
576 opt_line_number = TRUE;
577 } else if (check_option(opt, 'b', "tab-size", OPT_INT, &opt_tab_size)) {
578 opt_tab_size = MIN(opt_tab_size, TABSIZE);
580 if (altargc >= ARRAY_SIZE(altargv))
581 die("maximum number of arguments exceeded");
582 altargv[altargc++] = opt;
586 warn("`%s' has been deprecated", opt);
589 if (!isatty(STDIN_FILENO)) {
590 opt_request = REQ_VIEW_PAGER;
593 } else if (i < argc || altargc > 0) {
597 if (opt_request == REQ_VIEW_MAIN)
598 /* XXX: This is vulnerable to the user overriding
599 * options required for the main view parser. */
600 string_copy(opt_cmd, "git log --no-color --pretty=raw --boundary --parents");
602 string_format(opt_cmd, "git %s", subcommand);
603 buf_size = strlen(opt_cmd);
605 while (buf_size < sizeof(opt_cmd) && alti < altargc) {
606 opt_cmd[buf_size++] = ' ';
607 buf_size = sq_quote(opt_cmd, buf_size, altargv[alti++]);
610 while (buf_size < sizeof(opt_cmd) && i < argc) {
611 opt_cmd[buf_size++] = ' ';
612 buf_size = sq_quote(opt_cmd, buf_size, argv[i++]);
615 if (buf_size >= sizeof(opt_cmd))
616 die("command too long");
618 opt_cmd[buf_size] = 0;
626 * Line-oriented content detection.
630 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
631 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
632 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
633 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
634 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
635 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
636 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
637 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
638 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
639 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
640 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
641 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
642 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
643 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
644 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
645 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
646 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
647 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
648 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
649 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
650 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
651 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
652 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
653 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
654 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
655 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
656 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
657 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
658 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
659 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
660 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
661 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
662 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
663 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
664 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
665 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
666 LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
667 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
668 LINE(MAIN_LOCAL_TAG,"", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
669 LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
670 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
671 LINE(MAIN_REVGRAPH,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
672 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
673 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
674 LINE(STAT_SECTION, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
675 LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
676 LINE(STAT_STAGED, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
677 LINE(STAT_UNSTAGED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
678 LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
679 LINE(BLAME_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
680 LINE(BLAME_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
681 LINE(BLAME_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
682 LINE(BLAME_ID, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
683 LINE(BLAME_LINENO, "", COLOR_CYAN, COLOR_DEFAULT, 0)
686 #define LINE(type, line, fg, bg, attr) \
693 const char *name; /* Option name. */
694 int namelen; /* Size of option name. */
695 const char *line; /* The start of line to match. */
696 int linelen; /* Size of string to match. */
697 int fg, bg, attr; /* Color and text attributes for the lines. */
700 static struct line_info line_info[] = {
701 #define LINE(type, line, fg, bg, attr) \
702 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
707 static enum line_type
708 get_line_type(char *line)
710 int linelen = strlen(line);
713 for (type = 0; type < ARRAY_SIZE(line_info); type++)
714 /* Case insensitive search matches Signed-off-by lines better. */
715 if (linelen >= line_info[type].linelen &&
716 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
723 get_line_attr(enum line_type type)
725 assert(type < ARRAY_SIZE(line_info));
726 return COLOR_PAIR(type) | line_info[type].attr;
729 static struct line_info *
730 get_line_info(char *name, int namelen)
734 for (type = 0; type < ARRAY_SIZE(line_info); type++)
735 if (namelen == line_info[type].namelen &&
736 !string_enum_compare(line_info[type].name, name, namelen))
737 return &line_info[type];
745 int default_bg = line_info[LINE_DEFAULT].bg;
746 int default_fg = line_info[LINE_DEFAULT].fg;
751 if (assume_default_colors(default_fg, default_bg) == ERR) {
752 default_bg = COLOR_BLACK;
753 default_fg = COLOR_WHITE;
756 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
757 struct line_info *info = &line_info[type];
758 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
759 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
761 init_pair(type, fg, bg);
769 unsigned int selected:1;
770 unsigned int dirty:1;
772 void *data; /* User data */
782 enum request request;
783 struct keybinding *next;
786 static struct keybinding default_keybindings[] = {
788 { 'm', REQ_VIEW_MAIN },
789 { 'd', REQ_VIEW_DIFF },
790 { 'l', REQ_VIEW_LOG },
791 { 't', REQ_VIEW_TREE },
792 { 'f', REQ_VIEW_BLOB },
793 { 'B', REQ_VIEW_BLAME },
794 { 'p', REQ_VIEW_PAGER },
795 { 'h', REQ_VIEW_HELP },
796 { 'S', REQ_VIEW_STATUS },
797 { 'c', REQ_VIEW_STAGE },
799 /* View manipulation */
800 { 'q', REQ_VIEW_CLOSE },
801 { KEY_TAB, REQ_VIEW_NEXT },
802 { KEY_RETURN, REQ_ENTER },
803 { KEY_UP, REQ_PREVIOUS },
804 { KEY_DOWN, REQ_NEXT },
805 { 'R', REQ_REFRESH },
807 /* Cursor navigation */
808 { 'k', REQ_MOVE_UP },
809 { 'j', REQ_MOVE_DOWN },
810 { KEY_HOME, REQ_MOVE_FIRST_LINE },
811 { KEY_END, REQ_MOVE_LAST_LINE },
812 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
813 { ' ', REQ_MOVE_PAGE_DOWN },
814 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
815 { 'b', REQ_MOVE_PAGE_UP },
816 { '-', REQ_MOVE_PAGE_UP },
819 { KEY_IC, REQ_SCROLL_LINE_UP },
820 { KEY_DC, REQ_SCROLL_LINE_DOWN },
821 { 'w', REQ_SCROLL_PAGE_UP },
822 { 's', REQ_SCROLL_PAGE_DOWN },
826 { '?', REQ_SEARCH_BACK },
827 { 'n', REQ_FIND_NEXT },
828 { 'N', REQ_FIND_PREV },
832 { 'z', REQ_STOP_LOADING },
833 { 'v', REQ_SHOW_VERSION },
834 { 'r', REQ_SCREEN_REDRAW },
835 { '.', REQ_TOGGLE_LINENO },
836 { 'D', REQ_TOGGLE_DATE },
837 { 'A', REQ_TOGGLE_AUTHOR },
838 { 'g', REQ_TOGGLE_REV_GRAPH },
839 { 'F', REQ_TOGGLE_REFS },
841 { 'u', REQ_STATUS_UPDATE },
842 { 'M', REQ_STATUS_MERGE },
843 { ',', REQ_TREE_PARENT },
846 /* Using the ncurses SIGWINCH handler. */
847 { KEY_RESIZE, REQ_SCREEN_RESIZE },
850 #define KEYMAP_INFO \
864 #define KEYMAP_(name) KEYMAP_##name
869 static struct int_map keymap_table[] = {
870 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
875 #define set_keymap(map, name) \
876 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
878 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
881 add_keybinding(enum keymap keymap, enum request request, int key)
883 struct keybinding *keybinding;
885 keybinding = calloc(1, sizeof(*keybinding));
887 die("Failed to allocate keybinding");
889 keybinding->alias = key;
890 keybinding->request = request;
891 keybinding->next = keybindings[keymap];
892 keybindings[keymap] = keybinding;
895 /* Looks for a key binding first in the given map, then in the generic map, and
896 * lastly in the default keybindings. */
898 get_keybinding(enum keymap keymap, int key)
900 struct keybinding *kbd;
903 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
904 if (kbd->alias == key)
907 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
908 if (kbd->alias == key)
911 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
912 if (default_keybindings[i].alias == key)
913 return default_keybindings[i].request;
915 return (enum request) key;
924 static struct key key_table[] = {
925 { "Enter", KEY_RETURN },
927 { "Backspace", KEY_BACKSPACE },
929 { "Escape", KEY_ESC },
930 { "Left", KEY_LEFT },
931 { "Right", KEY_RIGHT },
933 { "Down", KEY_DOWN },
934 { "Insert", KEY_IC },
935 { "Delete", KEY_DC },
937 { "Home", KEY_HOME },
939 { "PageUp", KEY_PPAGE },
940 { "PageDown", KEY_NPAGE },
950 { "F10", KEY_F(10) },
951 { "F11", KEY_F(11) },
952 { "F12", KEY_F(12) },
956 get_key_value(const char *name)
960 for (i = 0; i < ARRAY_SIZE(key_table); i++)
961 if (!strcasecmp(key_table[i].name, name))
962 return key_table[i].value;
964 if (strlen(name) == 1 && isprint(*name))
971 get_key_name(int key_value)
973 static char key_char[] = "'X'";
977 for (key = 0; key < ARRAY_SIZE(key_table); key++)
978 if (key_table[key].value == key_value)
979 seq = key_table[key].name;
983 isprint(key_value)) {
984 key_char[1] = (char) key_value;
988 return seq ? seq : "'?'";
992 get_key(enum request request)
994 static char buf[BUFSIZ];
1001 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
1002 struct keybinding *keybinding = &default_keybindings[i];
1004 if (keybinding->request != request)
1007 if (!string_format_from(buf, &pos, "%s%s", sep,
1008 get_key_name(keybinding->alias)))
1009 return "Too many keybindings!";
1016 struct run_request {
1019 char cmd[SIZEOF_STR];
1022 static struct run_request *run_request;
1023 static size_t run_requests;
1026 add_run_request(enum keymap keymap, int key, int argc, char **argv)
1028 struct run_request *tmp;
1029 struct run_request req = { keymap, key };
1032 for (bufpos = 0; argc > 0; argc--, argv++)
1033 if (!string_format_from(req.cmd, &bufpos, "%s ", *argv))
1036 req.cmd[bufpos - 1] = 0;
1038 tmp = realloc(run_request, (run_requests + 1) * sizeof(*run_request));
1043 run_request[run_requests++] = req;
1045 return REQ_NONE + run_requests;
1048 static struct run_request *
1049 get_run_request(enum request request)
1051 if (request <= REQ_NONE)
1053 return &run_request[request - REQ_NONE - 1];
1057 add_builtin_run_requests(void)
1064 { KEYMAP_MAIN, 'C', { "git cherry-pick %(commit)" } },
1065 { KEYMAP_GENERIC, 'G', { "git gc" } },
1069 for (i = 0; i < ARRAY_SIZE(reqs); i++) {
1072 req = add_run_request(reqs[i].keymap, reqs[i].key, 1, reqs[i].argv);
1073 if (req != REQ_NONE)
1074 add_keybinding(reqs[i].keymap, req, reqs[i].key);
1079 * User config file handling.
1082 static struct int_map color_map[] = {
1083 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
1095 #define set_color(color, name) \
1096 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
1098 static struct int_map attr_map[] = {
1099 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
1106 ATTR_MAP(UNDERLINE),
1109 #define set_attribute(attr, name) \
1110 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
1112 static int config_lineno;
1113 static bool config_errors;
1114 static char *config_msg;
1116 /* Wants: object fgcolor bgcolor [attr] */
1118 option_color_command(int argc, char *argv[])
1120 struct line_info *info;
1122 if (argc != 3 && argc != 4) {
1123 config_msg = "Wrong number of arguments given to color command";
1127 info = get_line_info(argv[0], strlen(argv[0]));
1129 config_msg = "Unknown color name";
1133 if (set_color(&info->fg, argv[1]) == ERR ||
1134 set_color(&info->bg, argv[2]) == ERR) {
1135 config_msg = "Unknown color";
1139 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
1140 config_msg = "Unknown attribute";
1147 static bool parse_bool(const char *s)
1149 return (!strcmp(s, "1") || !strcmp(s, "true") ||
1150 !strcmp(s, "yes")) ? TRUE : FALSE;
1153 /* Wants: name = value */
1155 option_set_command(int argc, char *argv[])
1158 config_msg = "Wrong number of arguments given to set command";
1162 if (strcmp(argv[1], "=")) {
1163 config_msg = "No value assigned";
1167 if (!strcmp(argv[0], "show-author")) {
1168 opt_author = parse_bool(argv[2]);
1172 if (!strcmp(argv[0], "show-date")) {
1173 opt_date = parse_bool(argv[2]);
1177 if (!strcmp(argv[0], "show-rev-graph")) {
1178 opt_rev_graph = parse_bool(argv[2]);
1182 if (!strcmp(argv[0], "show-refs")) {
1183 opt_show_refs = parse_bool(argv[2]);
1187 if (!strcmp(argv[0], "show-line-numbers")) {
1188 opt_line_number = parse_bool(argv[2]);
1192 if (!strcmp(argv[0], "line-number-interval")) {
1193 opt_num_interval = atoi(argv[2]);
1197 if (!strcmp(argv[0], "tab-size")) {
1198 opt_tab_size = atoi(argv[2]);
1202 if (!strcmp(argv[0], "commit-encoding")) {
1203 char *arg = argv[2];
1204 int delimiter = *arg;
1207 switch (delimiter) {
1210 for (arg++, i = 0; arg[i]; i++)
1211 if (arg[i] == delimiter) {
1216 string_ncopy(opt_encoding, arg, strlen(arg));
1221 config_msg = "Unknown variable name";
1225 /* Wants: mode request key */
1227 option_bind_command(int argc, char *argv[])
1229 enum request request;
1234 config_msg = "Wrong number of arguments given to bind command";
1238 if (set_keymap(&keymap, argv[0]) == ERR) {
1239 config_msg = "Unknown key map";
1243 key = get_key_value(argv[1]);
1245 config_msg = "Unknown key";
1249 request = get_request(argv[2]);
1250 if (request == REQ_NONE) {
1251 const char *obsolete[] = { "cherry-pick" };
1252 size_t namelen = strlen(argv[2]);
1255 for (i = 0; i < ARRAY_SIZE(obsolete); i++) {
1256 if (namelen == strlen(obsolete[i]) &&
1257 !string_enum_compare(obsolete[i], argv[2], namelen)) {
1258 config_msg = "Obsolete request name";
1263 if (request == REQ_NONE && *argv[2]++ == '!')
1264 request = add_run_request(keymap, key, argc - 2, argv + 2);
1265 if (request == REQ_NONE) {
1266 config_msg = "Unknown request name";
1270 add_keybinding(keymap, request, key);
1276 set_option(char *opt, char *value)
1283 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1284 argv[argc++] = value;
1287 /* Nothing more to tokenize or last available token. */
1288 if (!*value || argc >= ARRAY_SIZE(argv))
1292 while (isspace(*value))
1296 if (!strcmp(opt, "color"))
1297 return option_color_command(argc, argv);
1299 if (!strcmp(opt, "set"))
1300 return option_set_command(argc, argv);
1302 if (!strcmp(opt, "bind"))
1303 return option_bind_command(argc, argv);
1305 config_msg = "Unknown option command";
1310 read_option(char *opt, size_t optlen, char *value, size_t valuelen)
1315 config_msg = "Internal error";
1317 /* Check for comment markers, since read_properties() will
1318 * only ensure opt and value are split at first " \t". */
1319 optlen = strcspn(opt, "#");
1323 if (opt[optlen] != 0) {
1324 config_msg = "No option value";
1328 /* Look for comment endings in the value. */
1329 size_t len = strcspn(value, "#");
1331 if (len < valuelen) {
1333 value[valuelen] = 0;
1336 status = set_option(opt, value);
1339 if (status == ERR) {
1340 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1341 config_lineno, (int) optlen, opt, config_msg);
1342 config_errors = TRUE;
1345 /* Always keep going if errors are encountered. */
1350 load_option_file(const char *path)
1354 /* It's ok that the file doesn't exist. */
1355 file = fopen(path, "r");
1360 config_errors = FALSE;
1362 if (read_properties(file, " \t", read_option) == ERR ||
1363 config_errors == TRUE)
1364 fprintf(stderr, "Errors while loading %s.\n", path);
1370 char *home = getenv("HOME");
1371 char *tigrc_user = getenv("TIGRC_USER");
1372 char *tigrc_system = getenv("TIGRC_SYSTEM");
1373 char buf[SIZEOF_STR];
1375 add_builtin_run_requests();
1377 if (!tigrc_system) {
1378 if (!string_format(buf, "%s/tigrc", SYSCONFDIR))
1382 load_option_file(tigrc_system);
1385 if (!home || !string_format(buf, "%s/.tigrc", home))
1389 load_option_file(tigrc_user);
1402 /* The display array of active views and the index of the current view. */
1403 static struct view *display[2];
1404 static unsigned int current_view;
1406 /* Reading from the prompt? */
1407 static bool input_mode = FALSE;
1409 #define foreach_displayed_view(view, i) \
1410 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1412 #define displayed_views() (display[1] != NULL ? 2 : 1)
1414 /* Current head and commit ID */
1415 static char ref_blob[SIZEOF_REF] = "";
1416 static char ref_commit[SIZEOF_REF] = "HEAD";
1417 static char ref_head[SIZEOF_REF] = "HEAD";
1420 const char *name; /* View name */
1421 const char *cmd_fmt; /* Default command line format */
1422 const char *cmd_env; /* Command line set via environment */
1423 const char *id; /* Points to either of ref_{head,commit,blob} */
1425 struct view_ops *ops; /* View operations */
1427 enum keymap keymap; /* What keymap does this view have */
1429 char cmd[SIZEOF_STR]; /* Command buffer */
1430 char ref[SIZEOF_REF]; /* Hovered commit reference */
1431 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1433 int height, width; /* The width and height of the main window */
1434 WINDOW *win; /* The main window */
1435 WINDOW *title; /* The title window living below the main window */
1438 unsigned long offset; /* Offset of the window top */
1439 unsigned long lineno; /* Current line number */
1442 char grep[SIZEOF_STR]; /* Search string */
1443 regex_t *regex; /* Pre-compiled regex */
1445 /* If non-NULL, points to the view that opened this view. If this view
1446 * is closed tig will switch back to the parent view. */
1447 struct view *parent;
1450 size_t lines; /* Total number of lines */
1451 struct line *line; /* Line index */
1452 size_t line_alloc; /* Total number of allocated lines */
1453 size_t line_size; /* Total number of used lines */
1454 unsigned int digits; /* Number of digits in the lines member. */
1462 /* What type of content being displayed. Used in the title bar. */
1464 /* Open and reads in all view content. */
1465 bool (*open)(struct view *view);
1466 /* Read one line; updates view->line. */
1467 bool (*read)(struct view *view, char *data);
1468 /* Draw one line; @lineno must be < view->height. */
1469 bool (*draw)(struct view *view, struct line *line, unsigned int lineno, bool selected);
1470 /* Depending on view handle a special requests. */
1471 enum request (*request)(struct view *view, enum request request, struct line *line);
1472 /* Search for regex in a line. */
1473 bool (*grep)(struct view *view, struct line *line);
1475 void (*select)(struct view *view, struct line *line);
1478 static struct view_ops pager_ops;
1479 static struct view_ops main_ops;
1480 static struct view_ops tree_ops;
1481 static struct view_ops blob_ops;
1482 static struct view_ops blame_ops;
1483 static struct view_ops help_ops;
1484 static struct view_ops status_ops;
1485 static struct view_ops stage_ops;
1487 #define VIEW_STR(name, cmd, env, ref, ops, map) \
1488 { name, cmd, #env, ref, ops, map}
1490 #define VIEW_(id, name, ops, ref) \
1491 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id)
1494 static struct view views[] = {
1495 VIEW_(MAIN, "main", &main_ops, ref_head),
1496 VIEW_(DIFF, "diff", &pager_ops, ref_commit),
1497 VIEW_(LOG, "log", &pager_ops, ref_head),
1498 VIEW_(TREE, "tree", &tree_ops, ref_commit),
1499 VIEW_(BLOB, "blob", &blob_ops, ref_blob),
1500 VIEW_(BLAME, "blame", &blame_ops, ref_commit),
1501 VIEW_(HELP, "help", &help_ops, ""),
1502 VIEW_(PAGER, "pager", &pager_ops, "stdin"),
1503 VIEW_(STATUS, "status", &status_ops, ""),
1504 VIEW_(STAGE, "stage", &stage_ops, ""),
1507 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1509 #define foreach_view(view, i) \
1510 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1512 #define view_is_displayed(view) \
1513 (view == display[0] || view == display[1])
1516 draw_text(struct view *view, const char *string, int max_len,
1517 bool use_tilde, int tilde_attr)
1520 int trimmed = FALSE;
1526 len = utf8_length(string, max_len, &trimmed, use_tilde);
1528 len = strlen(string);
1529 if (len > max_len) {
1538 waddnstr(view->win, string, len);
1539 if (trimmed && use_tilde) {
1540 if (tilde_attr != -1)
1541 wattrset(view->win, tilde_attr);
1542 waddch(view->win, '~');
1550 draw_view_line(struct view *view, unsigned int lineno)
1553 bool selected = (view->offset + lineno == view->lineno);
1556 assert(view_is_displayed(view));
1558 if (view->offset + lineno >= view->lines)
1561 line = &view->line[view->offset + lineno];
1564 line->selected = TRUE;
1565 view->ops->select(view, line);
1566 } else if (line->selected) {
1567 line->selected = FALSE;
1568 wmove(view->win, lineno, 0);
1569 wclrtoeol(view->win);
1572 scrollok(view->win, FALSE);
1573 draw_ok = view->ops->draw(view, line, lineno, selected);
1574 scrollok(view->win, TRUE);
1580 redraw_view_dirty(struct view *view)
1585 for (lineno = 0; lineno < view->height; lineno++) {
1586 struct line *line = &view->line[view->offset + lineno];
1592 if (!draw_view_line(view, lineno))
1598 redrawwin(view->win);
1600 wnoutrefresh(view->win);
1602 wrefresh(view->win);
1606 redraw_view_from(struct view *view, int lineno)
1608 assert(0 <= lineno && lineno < view->height);
1610 for (; lineno < view->height; lineno++) {
1611 if (!draw_view_line(view, lineno))
1615 redrawwin(view->win);
1617 wnoutrefresh(view->win);
1619 wrefresh(view->win);
1623 redraw_view(struct view *view)
1626 redraw_view_from(view, 0);
1631 update_view_title(struct view *view)
1633 char buf[SIZEOF_STR];
1634 char state[SIZEOF_STR];
1635 size_t bufpos = 0, statelen = 0;
1637 assert(view_is_displayed(view));
1639 if (view != VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) {
1640 unsigned int view_lines = view->offset + view->height;
1641 unsigned int lines = view->lines
1642 ? MIN(view_lines, view->lines) * 100 / view->lines
1645 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1652 time_t secs = time(NULL) - view->start_time;
1654 /* Three git seconds are a long time ... */
1656 string_format_from(state, &statelen, " %lds", secs);
1660 string_format_from(buf, &bufpos, "[%s]", view->name);
1661 if (*view->ref && bufpos < view->width) {
1662 size_t refsize = strlen(view->ref);
1663 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1665 if (minsize < view->width)
1666 refsize = view->width - minsize + 7;
1667 string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
1670 if (statelen && bufpos < view->width) {
1671 string_format_from(buf, &bufpos, " %s", state);
1674 if (view == display[current_view])
1675 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1677 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1679 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1680 wclrtoeol(view->title);
1681 wmove(view->title, 0, view->width - 1);
1684 wnoutrefresh(view->title);
1686 wrefresh(view->title);
1690 resize_display(void)
1693 struct view *base = display[0];
1694 struct view *view = display[1] ? display[1] : display[0];
1696 /* Setup window dimensions */
1698 getmaxyx(stdscr, base->height, base->width);
1700 /* Make room for the status window. */
1704 /* Horizontal split. */
1705 view->width = base->width;
1706 view->height = SCALE_SPLIT_VIEW(base->height);
1707 base->height -= view->height;
1709 /* Make room for the title bar. */
1713 /* Make room for the title bar. */
1718 foreach_displayed_view (view, i) {
1720 view->win = newwin(view->height, 0, offset, 0);
1722 die("Failed to create %s view", view->name);
1724 scrollok(view->win, TRUE);
1726 view->title = newwin(1, 0, offset + view->height, 0);
1728 die("Failed to create title window");
1731 wresize(view->win, view->height, view->width);
1732 mvwin(view->win, offset, 0);
1733 mvwin(view->title, offset + view->height, 0);
1736 offset += view->height + 1;
1741 redraw_display(void)
1746 foreach_displayed_view (view, i) {
1748 update_view_title(view);
1753 update_display_cursor(struct view *view)
1755 /* Move the cursor to the right-most column of the cursor line.
1757 * XXX: This could turn out to be a bit expensive, but it ensures that
1758 * the cursor does not jump around. */
1760 wmove(view->win, view->lineno - view->offset, view->width - 1);
1761 wrefresh(view->win);
1769 /* Scrolling backend */
1771 do_scroll_view(struct view *view, int lines)
1773 bool redraw_current_line = FALSE;
1775 /* The rendering expects the new offset. */
1776 view->offset += lines;
1778 assert(0 <= view->offset && view->offset < view->lines);
1781 /* Move current line into the view. */
1782 if (view->lineno < view->offset) {
1783 view->lineno = view->offset;
1784 redraw_current_line = TRUE;
1785 } else if (view->lineno >= view->offset + view->height) {
1786 view->lineno = view->offset + view->height - 1;
1787 redraw_current_line = TRUE;
1790 assert(view->offset <= view->lineno && view->lineno < view->lines);
1792 /* Redraw the whole screen if scrolling is pointless. */
1793 if (view->height < ABS(lines)) {
1797 int line = lines > 0 ? view->height - lines : 0;
1798 int end = line + ABS(lines);
1800 wscrl(view->win, lines);
1802 for (; line < end; line++) {
1803 if (!draw_view_line(view, line))
1807 if (redraw_current_line)
1808 draw_view_line(view, view->lineno - view->offset);
1811 redrawwin(view->win);
1812 wrefresh(view->win);
1816 /* Scroll frontend */
1818 scroll_view(struct view *view, enum request request)
1822 assert(view_is_displayed(view));
1825 case REQ_SCROLL_PAGE_DOWN:
1826 lines = view->height;
1827 case REQ_SCROLL_LINE_DOWN:
1828 if (view->offset + lines > view->lines)
1829 lines = view->lines - view->offset;
1831 if (lines == 0 || view->offset + view->height >= view->lines) {
1832 report("Cannot scroll beyond the last line");
1837 case REQ_SCROLL_PAGE_UP:
1838 lines = view->height;
1839 case REQ_SCROLL_LINE_UP:
1840 if (lines > view->offset)
1841 lines = view->offset;
1844 report("Cannot scroll beyond the first line");
1852 die("request %d not handled in switch", request);
1855 do_scroll_view(view, lines);
1860 move_view(struct view *view, enum request request)
1862 int scroll_steps = 0;
1866 case REQ_MOVE_FIRST_LINE:
1867 steps = -view->lineno;
1870 case REQ_MOVE_LAST_LINE:
1871 steps = view->lines - view->lineno - 1;
1874 case REQ_MOVE_PAGE_UP:
1875 steps = view->height > view->lineno
1876 ? -view->lineno : -view->height;
1879 case REQ_MOVE_PAGE_DOWN:
1880 steps = view->lineno + view->height >= view->lines
1881 ? view->lines - view->lineno - 1 : view->height;
1893 die("request %d not handled in switch", request);
1896 if (steps <= 0 && view->lineno == 0) {
1897 report("Cannot move beyond the first line");
1900 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1901 report("Cannot move beyond the last line");
1905 /* Move the current line */
1906 view->lineno += steps;
1907 assert(0 <= view->lineno && view->lineno < view->lines);
1909 /* Check whether the view needs to be scrolled */
1910 if (view->lineno < view->offset ||
1911 view->lineno >= view->offset + view->height) {
1912 scroll_steps = steps;
1913 if (steps < 0 && -steps > view->offset) {
1914 scroll_steps = -view->offset;
1916 } else if (steps > 0) {
1917 if (view->lineno == view->lines - 1 &&
1918 view->lines > view->height) {
1919 scroll_steps = view->lines - view->offset - 1;
1920 if (scroll_steps >= view->height)
1921 scroll_steps -= view->height - 1;
1926 if (!view_is_displayed(view)) {
1927 view->offset += scroll_steps;
1928 assert(0 <= view->offset && view->offset < view->lines);
1929 view->ops->select(view, &view->line[view->lineno]);
1933 /* Repaint the old "current" line if we be scrolling */
1934 if (ABS(steps) < view->height)
1935 draw_view_line(view, view->lineno - steps - view->offset);
1938 do_scroll_view(view, scroll_steps);
1942 /* Draw the current line */
1943 draw_view_line(view, view->lineno - view->offset);
1945 redrawwin(view->win);
1946 wrefresh(view->win);
1955 static void search_view(struct view *view, enum request request);
1958 find_next_line(struct view *view, unsigned long lineno, struct line *line)
1960 assert(view_is_displayed(view));
1962 if (!view->ops->grep(view, line))
1965 if (lineno - view->offset >= view->height) {
1966 view->offset = lineno;
1967 view->lineno = lineno;
1971 unsigned long old_lineno = view->lineno - view->offset;
1973 view->lineno = lineno;
1974 draw_view_line(view, old_lineno);
1976 draw_view_line(view, view->lineno - view->offset);
1977 redrawwin(view->win);
1978 wrefresh(view->win);
1981 report("Line %ld matches '%s'", lineno + 1, view->grep);
1986 find_next(struct view *view, enum request request)
1988 unsigned long lineno = view->lineno;
1993 report("No previous search");
1995 search_view(view, request);
2005 case REQ_SEARCH_BACK:
2014 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
2015 lineno += direction;
2017 /* Note, lineno is unsigned long so will wrap around in which case it
2018 * will become bigger than view->lines. */
2019 for (; lineno < view->lines; lineno += direction) {
2020 struct line *line = &view->line[lineno];
2022 if (find_next_line(view, lineno, line))
2026 report("No match found for '%s'", view->grep);
2030 search_view(struct view *view, enum request request)
2035 regfree(view->regex);
2038 view->regex = calloc(1, sizeof(*view->regex));
2043 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
2044 if (regex_err != 0) {
2045 char buf[SIZEOF_STR] = "unknown error";
2047 regerror(regex_err, view->regex, buf, sizeof(buf));
2048 report("Search failed: %s", buf);
2052 string_copy(view->grep, opt_search);
2054 find_next(view, request);
2058 * Incremental updating
2062 end_update(struct view *view)
2066 set_nonblocking_input(FALSE);
2067 if (view->pipe == stdin)
2075 begin_update(struct view *view)
2081 string_copy(view->cmd, opt_cmd);
2083 /* When running random commands, initially show the
2084 * command in the title. However, it maybe later be
2085 * overwritten if a commit line is selected. */
2086 if (view == VIEW(REQ_VIEW_PAGER))
2087 string_copy(view->ref, view->cmd);
2091 } else if (view == VIEW(REQ_VIEW_TREE)) {
2092 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2093 char path[SIZEOF_STR];
2095 if (strcmp(view->vid, view->id))
2096 opt_path[0] = path[0] = 0;
2097 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
2100 if (!string_format(view->cmd, format, view->id, path))
2104 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2105 const char *id = view->id;
2107 if (!string_format(view->cmd, format, id, id, id, id, id))
2110 /* Put the current ref_* value to the view title ref
2111 * member. This is needed by the blob view. Most other
2112 * views sets it automatically after loading because the
2113 * first line is a commit line. */
2114 string_copy_rev(view->ref, view->id);
2117 /* Special case for the pager view. */
2119 view->pipe = opt_pipe;
2122 view->pipe = popen(view->cmd, "r");
2128 set_nonblocking_input(TRUE);
2133 string_copy_rev(view->vid, view->id);
2138 for (i = 0; i < view->lines; i++)
2139 if (view->line[i].data)
2140 free(view->line[i].data);
2146 view->start_time = time(NULL);
2151 #define ITEM_CHUNK_SIZE 256
2153 realloc_items(void *mem, size_t *size, size_t new_size, size_t item_size)
2155 size_t num_chunks = *size / ITEM_CHUNK_SIZE;
2156 size_t num_chunks_new = (new_size + ITEM_CHUNK_SIZE - 1) / ITEM_CHUNK_SIZE;
2158 if (mem == NULL || num_chunks != num_chunks_new) {
2159 *size = num_chunks_new * ITEM_CHUNK_SIZE;
2160 mem = realloc(mem, *size * item_size);
2166 static struct line *
2167 realloc_lines(struct view *view, size_t line_size)
2169 size_t alloc = view->line_alloc;
2170 struct line *tmp = realloc_items(view->line, &alloc, line_size,
2171 sizeof(*view->line));
2177 view->line_alloc = alloc;
2178 view->line_size = line_size;
2183 update_view(struct view *view)
2185 char in_buffer[BUFSIZ];
2186 char out_buffer[BUFSIZ * 2];
2188 /* The number of lines to read. If too low it will cause too much
2189 * redrawing (and possible flickering), if too high responsiveness
2191 unsigned long lines = view->height;
2192 int redraw_from = -1;
2197 /* Only redraw if lines are visible. */
2198 if (view->offset + view->height >= view->lines)
2199 redraw_from = view->lines - view->offset;
2201 /* FIXME: This is probably not perfect for backgrounded views. */
2202 if (!realloc_lines(view, view->lines + lines))
2205 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
2206 size_t linelen = strlen(line);
2209 line[linelen - 1] = 0;
2211 if (opt_iconv != ICONV_NONE) {
2212 ICONV_CONST char *inbuf = line;
2213 size_t inlen = linelen;
2215 char *outbuf = out_buffer;
2216 size_t outlen = sizeof(out_buffer);
2220 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
2221 if (ret != (size_t) -1) {
2223 linelen = strlen(out_buffer);
2227 if (!view->ops->read(view, line))
2237 lines = view->lines;
2238 for (digits = 0; lines; digits++)
2241 /* Keep the displayed view in sync with line number scaling. */
2242 if (digits != view->digits) {
2243 view->digits = digits;
2248 if (!view_is_displayed(view))
2251 if (view == VIEW(REQ_VIEW_TREE)) {
2252 /* Clear the view and redraw everything since the tree sorting
2253 * might have rearranged things. */
2257 } else if (redraw_from >= 0) {
2258 /* If this is an incremental update, redraw the previous line
2259 * since for commits some members could have changed when
2260 * loading the main view. */
2261 if (redraw_from > 0)
2264 /* Since revision graph visualization requires knowledge
2265 * about the parent commit, it causes a further one-off
2266 * needed to be redrawn for incremental updates. */
2267 if (redraw_from > 0 && opt_rev_graph)
2270 /* Incrementally draw avoids flickering. */
2271 redraw_view_from(view, redraw_from);
2274 if (view == VIEW(REQ_VIEW_BLAME))
2275 redraw_view_dirty(view);
2277 /* Update the title _after_ the redraw so that if the redraw picks up a
2278 * commit reference in view->ref it'll be available here. */
2279 update_view_title(view);
2282 if (ferror(view->pipe)) {
2283 report("Failed to read: %s", strerror(errno));
2286 } else if (feof(view->pipe)) {
2294 report("Allocation failure");
2297 if (view->ops->read(view, NULL))
2302 static struct line *
2303 add_line_data(struct view *view, void *data, enum line_type type)
2305 struct line *line = &view->line[view->lines++];
2307 memset(line, 0, sizeof(*line));
2314 static struct line *
2315 add_line_text(struct view *view, char *data, enum line_type type)
2318 data = strdup(data);
2320 return data ? add_line_data(view, data, type) : NULL;
2329 OPEN_DEFAULT = 0, /* Use default view switching. */
2330 OPEN_SPLIT = 1, /* Split current view. */
2331 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2332 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2336 open_view(struct view *prev, enum request request, enum open_flags flags)
2338 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2339 bool split = !!(flags & OPEN_SPLIT);
2340 bool reload = !!(flags & OPEN_RELOAD);
2341 struct view *view = VIEW(request);
2342 int nviews = displayed_views();
2343 struct view *base_view = display[0];
2345 if (view == prev && nviews == 1 && !reload) {
2346 report("Already in %s view", view->name);
2350 if (view->ops->open) {
2351 if (!view->ops->open(view)) {
2352 report("Failed to load %s view", view->name);
2356 } else if ((reload || strcmp(view->vid, view->id)) &&
2357 !begin_update(view)) {
2358 report("Failed to load %s view", view->name);
2367 /* Maximize the current view. */
2368 memset(display, 0, sizeof(display));
2370 display[current_view] = view;
2373 /* Resize the view when switching between split- and full-screen,
2374 * or when switching between two different full-screen views. */
2375 if (nviews != displayed_views() ||
2376 (nviews == 1 && base_view != display[0]))
2379 if (split && prev->lineno - prev->offset >= prev->height) {
2380 /* Take the title line into account. */
2381 int lines = prev->lineno - prev->offset - prev->height + 1;
2383 /* Scroll the view that was split if the current line is
2384 * outside the new limited view. */
2385 do_scroll_view(prev, lines);
2388 if (prev && view != prev) {
2389 if (split && !backgrounded) {
2390 /* "Blur" the previous view. */
2391 update_view_title(prev);
2394 view->parent = prev;
2397 if (view->pipe && view->lines == 0) {
2398 /* Clear the old view and let the incremental updating refill
2407 /* If the view is backgrounded the above calls to report()
2408 * won't redraw the view title. */
2410 update_view_title(view);
2414 open_external_viewer(const char *cmd)
2416 def_prog_mode(); /* save current tty modes */
2417 endwin(); /* restore original tty modes */
2419 fprintf(stderr, "Press Enter to continue");
2426 open_mergetool(const char *file)
2428 char cmd[SIZEOF_STR];
2429 char file_sq[SIZEOF_STR];
2431 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2432 string_format(cmd, "git mergetool %s", file_sq)) {
2433 open_external_viewer(cmd);
2438 open_editor(bool from_root, const char *file)
2440 char cmd[SIZEOF_STR];
2441 char file_sq[SIZEOF_STR];
2443 char *prefix = from_root ? opt_cdup : "";
2445 editor = getenv("GIT_EDITOR");
2446 if (!editor && *opt_editor)
2447 editor = opt_editor;
2449 editor = getenv("VISUAL");
2451 editor = getenv("EDITOR");
2455 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2456 string_format(cmd, "%s %s%s", editor, prefix, file_sq)) {
2457 open_external_viewer(cmd);
2462 open_run_request(enum request request)
2464 struct run_request *req = get_run_request(request);
2465 char buf[SIZEOF_STR * 2];
2470 report("Unknown run request");
2478 char *next = strstr(cmd, "%(");
2479 int len = next - cmd;
2486 } else if (!strncmp(next, "%(head)", 7)) {
2489 } else if (!strncmp(next, "%(commit)", 9)) {
2492 } else if (!strncmp(next, "%(blob)", 7)) {
2496 report("Unknown replacement in run request: `%s`", req->cmd);
2500 if (!string_format_from(buf, &bufpos, "%.*s%s", len, cmd, value))
2504 next = strchr(next, ')') + 1;
2508 open_external_viewer(buf);
2512 * User request switch noodle
2516 view_driver(struct view *view, enum request request)
2520 if (request == REQ_NONE) {
2525 if (request > REQ_NONE) {
2526 open_run_request(request);
2530 if (view && view->lines) {
2531 request = view->ops->request(view, request, &view->line[view->lineno]);
2532 if (request == REQ_NONE)
2539 case REQ_MOVE_PAGE_UP:
2540 case REQ_MOVE_PAGE_DOWN:
2541 case REQ_MOVE_FIRST_LINE:
2542 case REQ_MOVE_LAST_LINE:
2543 move_view(view, request);
2546 case REQ_SCROLL_LINE_DOWN:
2547 case REQ_SCROLL_LINE_UP:
2548 case REQ_SCROLL_PAGE_DOWN:
2549 case REQ_SCROLL_PAGE_UP:
2550 scroll_view(view, request);
2553 case REQ_VIEW_BLAME:
2555 report("No file chosen, press %s to open tree view",
2556 get_key(REQ_VIEW_TREE));
2559 open_view(view, request, OPEN_DEFAULT);
2564 report("No file chosen, press %s to open tree view",
2565 get_key(REQ_VIEW_TREE));
2568 open_view(view, request, OPEN_DEFAULT);
2571 case REQ_VIEW_PAGER:
2572 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2573 report("No pager content, press %s to run command from prompt",
2574 get_key(REQ_PROMPT));
2577 open_view(view, request, OPEN_DEFAULT);
2580 case REQ_VIEW_STAGE:
2581 if (!VIEW(REQ_VIEW_STAGE)->lines) {
2582 report("No stage content, press %s to open the status view and choose file",
2583 get_key(REQ_VIEW_STATUS));
2586 open_view(view, request, OPEN_DEFAULT);
2589 case REQ_VIEW_STATUS:
2590 if (opt_is_inside_work_tree == FALSE) {
2591 report("The status view requires a working tree");
2594 open_view(view, request, OPEN_DEFAULT);
2602 open_view(view, request, OPEN_DEFAULT);
2607 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2609 if ((view == VIEW(REQ_VIEW_DIFF) &&
2610 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2611 (view == VIEW(REQ_VIEW_DIFF) &&
2612 view->parent == VIEW(REQ_VIEW_BLAME)) ||
2613 (view == VIEW(REQ_VIEW_STAGE) &&
2614 view->parent == VIEW(REQ_VIEW_STATUS)) ||
2615 (view == VIEW(REQ_VIEW_BLOB) &&
2616 view->parent == VIEW(REQ_VIEW_TREE))) {
2619 view = view->parent;
2620 line = view->lineno;
2621 move_view(view, request);
2622 if (view_is_displayed(view))
2623 update_view_title(view);
2624 if (line != view->lineno)
2625 view->ops->request(view, REQ_ENTER,
2626 &view->line[view->lineno]);
2629 move_view(view, request);
2635 int nviews = displayed_views();
2636 int next_view = (current_view + 1) % nviews;
2638 if (next_view == current_view) {
2639 report("Only one view is displayed");
2643 current_view = next_view;
2644 /* Blur out the title of the previous view. */
2645 update_view_title(view);
2650 report("Refreshing is not yet supported for the %s view", view->name);
2653 case REQ_TOGGLE_LINENO:
2654 opt_line_number = !opt_line_number;
2658 case REQ_TOGGLE_DATE:
2659 opt_date = !opt_date;
2663 case REQ_TOGGLE_AUTHOR:
2664 opt_author = !opt_author;
2668 case REQ_TOGGLE_REV_GRAPH:
2669 opt_rev_graph = !opt_rev_graph;
2673 case REQ_TOGGLE_REFS:
2674 opt_show_refs = !opt_show_refs;
2679 /* Always reload^Wrerun commands from the prompt. */
2680 open_view(view, opt_request, OPEN_RELOAD);
2684 case REQ_SEARCH_BACK:
2685 search_view(view, request);
2690 find_next(view, request);
2693 case REQ_STOP_LOADING:
2694 for (i = 0; i < ARRAY_SIZE(views); i++) {
2697 report("Stopped loading the %s view", view->name),
2702 case REQ_SHOW_VERSION:
2703 report("tig-%s (built %s)", TIG_VERSION, __DATE__);
2706 case REQ_SCREEN_RESIZE:
2709 case REQ_SCREEN_REDRAW:
2714 report("Nothing to edit");
2719 report("Nothing to enter");
2723 case REQ_VIEW_CLOSE:
2724 /* XXX: Mark closed views by letting view->parent point to the
2725 * view itself. Parents to closed view should never be
2728 view->parent->parent != view->parent) {
2729 memset(display, 0, sizeof(display));
2731 display[current_view] = view->parent;
2732 view->parent = view;
2742 /* An unknown key will show most commonly used commands. */
2743 report("Unknown key, press 'h' for help");
2756 pager_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2758 char *text = line->data;
2759 enum line_type type = line->type;
2762 wmove(view->win, lineno, 0);
2766 wchgat(view->win, -1, 0, type, NULL);
2769 attr = get_line_attr(type);
2770 wattrset(view->win, attr);
2772 if (opt_line_number || opt_tab_size < TABSIZE) {
2773 static char spaces[] = " ";
2774 int col_offset = 0, col = 0;
2776 if (opt_line_number) {
2777 unsigned long real_lineno = view->offset + lineno + 1;
2779 if (real_lineno == 1 ||
2780 (real_lineno % opt_num_interval) == 0) {
2781 wprintw(view->win, "%.*d", view->digits, real_lineno);
2784 waddnstr(view->win, spaces,
2785 MIN(view->digits, STRING_SIZE(spaces)));
2787 waddstr(view->win, ": ");
2788 col_offset = view->digits + 2;
2791 while (text && col_offset + col < view->width) {
2792 int cols_max = view->width - col_offset - col;
2796 if (*text == '\t') {
2798 assert(sizeof(spaces) > TABSIZE);
2800 cols = opt_tab_size - (col % opt_tab_size);
2803 text = strchr(text, '\t');
2804 cols = line ? text - pos : strlen(pos);
2807 waddnstr(view->win, pos, MIN(cols, cols_max));
2812 int tilde_attr = get_line_attr(LINE_MAIN_DELIM);
2814 draw_text(view, text, view->width, TRUE, tilde_attr);
2821 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2823 char refbuf[SIZEOF_STR];
2827 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2830 pipe = popen(refbuf, "r");
2834 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2835 ref = chomp_string(ref);
2841 /* This is the only fatal call, since it can "corrupt" the buffer. */
2842 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2849 add_pager_refs(struct view *view, struct line *line)
2851 char buf[SIZEOF_STR];
2852 char *commit_id = (char *)line->data + STRING_SIZE("commit ");
2854 size_t bufpos = 0, refpos = 0;
2855 const char *sep = "Refs: ";
2856 bool is_tag = FALSE;
2858 assert(line->type == LINE_COMMIT);
2860 refs = get_refs(commit_id);
2862 if (view == VIEW(REQ_VIEW_DIFF))
2863 goto try_add_describe_ref;
2868 struct ref *ref = refs[refpos];
2869 char *fmt = ref->tag ? "%s[%s]" :
2870 ref->remote ? "%s<%s>" : "%s%s";
2872 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2877 } while (refs[refpos++]->next);
2879 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2880 try_add_describe_ref:
2881 /* Add <tag>-g<commit_id> "fake" reference. */
2882 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2889 if (!realloc_lines(view, view->line_size + 1))
2892 add_line_text(view, buf, LINE_PP_REFS);
2896 pager_read(struct view *view, char *data)
2903 line = add_line_text(view, data, get_line_type(data));
2907 if (line->type == LINE_COMMIT &&
2908 (view == VIEW(REQ_VIEW_DIFF) ||
2909 view == VIEW(REQ_VIEW_LOG)))
2910 add_pager_refs(view, line);
2916 pager_request(struct view *view, enum request request, struct line *line)
2920 if (request != REQ_ENTER)
2923 if (line->type == LINE_COMMIT &&
2924 (view == VIEW(REQ_VIEW_LOG) ||
2925 view == VIEW(REQ_VIEW_PAGER))) {
2926 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2930 /* Always scroll the view even if it was split. That way
2931 * you can use Enter to scroll through the log view and
2932 * split open each commit diff. */
2933 scroll_view(view, REQ_SCROLL_LINE_DOWN);
2935 /* FIXME: A minor workaround. Scrolling the view will call report("")
2936 * but if we are scrolling a non-current view this won't properly
2937 * update the view title. */
2939 update_view_title(view);
2945 pager_grep(struct view *view, struct line *line)
2948 char *text = line->data;
2953 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
2960 pager_select(struct view *view, struct line *line)
2962 if (line->type == LINE_COMMIT) {
2963 char *text = (char *)line->data + STRING_SIZE("commit ");
2965 if (view != VIEW(REQ_VIEW_PAGER))
2966 string_copy_rev(view->ref, text);
2967 string_copy_rev(ref_commit, text);
2971 static struct view_ops pager_ops = {
2987 help_open(struct view *view)
2990 int lines = ARRAY_SIZE(req_info) + 2;
2993 if (view->lines > 0)
2996 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2997 if (!req_info[i].request)
3000 lines += run_requests + 1;
3002 view->line = calloc(lines, sizeof(*view->line));
3006 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
3008 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
3011 if (req_info[i].request == REQ_NONE)
3014 if (!req_info[i].request) {
3015 add_line_text(view, "", LINE_DEFAULT);
3016 add_line_text(view, req_info[i].help, LINE_DEFAULT);
3020 key = get_key(req_info[i].request);
3022 key = "(no key defined)";
3024 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
3027 add_line_text(view, buf, LINE_DEFAULT);
3031 add_line_text(view, "", LINE_DEFAULT);
3032 add_line_text(view, "External commands:", LINE_DEFAULT);
3035 for (i = 0; i < run_requests; i++) {
3036 struct run_request *req = get_run_request(REQ_NONE + i + 1);
3042 key = get_key_name(req->key);
3044 key = "(no key defined)";
3046 if (!string_format(buf, " %-10s %-14s `%s`",
3047 keymap_table[req->keymap].name,
3051 add_line_text(view, buf, LINE_DEFAULT);
3057 static struct view_ops help_ops = {
3072 struct tree_stack_entry {
3073 struct tree_stack_entry *prev; /* Entry below this in the stack */
3074 unsigned long lineno; /* Line number to restore */
3075 char *name; /* Position of name in opt_path */
3078 /* The top of the path stack. */
3079 static struct tree_stack_entry *tree_stack = NULL;
3080 unsigned long tree_lineno = 0;
3083 pop_tree_stack_entry(void)
3085 struct tree_stack_entry *entry = tree_stack;
3087 tree_lineno = entry->lineno;
3089 tree_stack = entry->prev;
3094 push_tree_stack_entry(char *name, unsigned long lineno)
3096 struct tree_stack_entry *entry = calloc(1, sizeof(*entry));
3097 size_t pathlen = strlen(opt_path);
3102 entry->prev = tree_stack;
3103 entry->name = opt_path + pathlen;
3106 if (!string_format_from(opt_path, &pathlen, "%s/", name)) {
3107 pop_tree_stack_entry();
3111 /* Move the current line to the first tree entry. */
3113 entry->lineno = lineno;
3116 /* Parse output from git-ls-tree(1):
3118 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
3119 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
3120 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
3121 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
3124 #define SIZEOF_TREE_ATTR \
3125 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
3127 #define TREE_UP_FORMAT "040000 tree %s\t.."
3130 tree_compare_entry(enum line_type type1, char *name1,
3131 enum line_type type2, char *name2)
3133 if (type1 != type2) {
3134 if (type1 == LINE_TREE_DIR)
3139 return strcmp(name1, name2);
3143 tree_path(struct line *line)
3145 char *path = line->data;
3147 return path + SIZEOF_TREE_ATTR;
3151 tree_read(struct view *view, char *text)
3153 size_t textlen = text ? strlen(text) : 0;
3154 char buf[SIZEOF_STR];
3156 enum line_type type;
3157 bool first_read = view->lines == 0;
3161 if (textlen <= SIZEOF_TREE_ATTR)
3164 type = text[STRING_SIZE("100644 ")] == 't'
3165 ? LINE_TREE_DIR : LINE_TREE_FILE;
3168 /* Add path info line */
3169 if (!string_format(buf, "Directory path /%s", opt_path) ||
3170 !realloc_lines(view, view->line_size + 1) ||
3171 !add_line_text(view, buf, LINE_DEFAULT))
3174 /* Insert "link" to parent directory. */
3176 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
3177 !realloc_lines(view, view->line_size + 1) ||
3178 !add_line_text(view, buf, LINE_TREE_DIR))
3183 /* Strip the path part ... */
3185 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
3186 size_t striplen = strlen(opt_path);
3187 char *path = text + SIZEOF_TREE_ATTR;
3189 if (pathlen > striplen)
3190 memmove(path, path + striplen,
3191 pathlen - striplen + 1);
3194 /* Skip "Directory ..." and ".." line. */
3195 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
3196 struct line *line = &view->line[pos];
3197 char *path1 = tree_path(line);
3198 char *path2 = text + SIZEOF_TREE_ATTR;
3199 int cmp = tree_compare_entry(line->type, path1, type, path2);
3204 text = strdup(text);
3208 if (view->lines > pos)
3209 memmove(&view->line[pos + 1], &view->line[pos],
3210 (view->lines - pos) * sizeof(*line));
3212 line = &view->line[pos];
3219 if (!add_line_text(view, text, type))
3222 if (tree_lineno > view->lineno) {
3223 view->lineno = tree_lineno;
3231 tree_request(struct view *view, enum request request, struct line *line)
3233 enum open_flags flags;
3235 if (request == REQ_VIEW_BLAME) {
3236 char *filename = tree_path(line);
3238 if (line->type == LINE_TREE_DIR) {
3239 report("Cannot show blame for directory %s", opt_path);
3243 string_copy(opt_ref, ref_commit);
3244 string_ncopy(opt_file, filename, strlen(filename));
3247 if (request == REQ_TREE_PARENT) {
3250 request = REQ_ENTER;
3251 line = &view->line[1];
3253 /* quit view if at top of tree */
3254 return REQ_VIEW_CLOSE;
3257 if (request != REQ_ENTER)
3260 /* Cleanup the stack if the tree view is at a different tree. */
3261 while (!*opt_path && tree_stack)
3262 pop_tree_stack_entry();
3264 switch (line->type) {
3266 /* Depending on whether it is a subdir or parent (updir?) link
3267 * mangle the path buffer. */
3268 if (line == &view->line[1] && *opt_path) {
3269 pop_tree_stack_entry();
3272 char *basename = tree_path(line);
3274 push_tree_stack_entry(basename, view->lineno);
3277 /* Trees and subtrees share the same ID, so they are not not
3278 * unique like blobs. */
3279 flags = OPEN_RELOAD;
3280 request = REQ_VIEW_TREE;
3283 case LINE_TREE_FILE:
3284 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3285 request = REQ_VIEW_BLOB;
3292 open_view(view, request, flags);
3293 if (request == REQ_VIEW_TREE) {
3294 view->lineno = tree_lineno;
3301 tree_select(struct view *view, struct line *line)
3303 char *text = (char *)line->data + STRING_SIZE("100644 blob ");
3305 if (line->type == LINE_TREE_FILE) {
3306 string_copy_rev(ref_blob, text);
3308 } else if (line->type != LINE_TREE_DIR) {
3312 string_copy_rev(view->ref, text);
3315 static struct view_ops tree_ops = {
3326 blob_read(struct view *view, char *line)
3330 return add_line_text(view, line, LINE_DEFAULT) != NULL;
3333 static struct view_ops blob_ops = {
3346 * Loading the blame view is a two phase job:
3348 * 1. File content is read either using opt_file from the
3349 * filesystem or using git-cat-file.
3350 * 2. Then blame information is incrementally added by
3351 * reading output from git-blame.
3354 struct blame_commit {
3355 char id[SIZEOF_REV]; /* SHA1 ID. */
3356 char title[128]; /* First line of the commit message. */
3357 char author[75]; /* Author of the commit. */
3358 struct tm time; /* Date from the author ident. */
3359 char filename[128]; /* Name of file. */
3363 struct blame_commit *commit;
3364 unsigned int header:1;
3368 #define BLAME_CAT_FILE_CMD "git cat-file blob %s:%s"
3369 #define BLAME_INCREMENTAL_CMD "git blame --incremental %s %s"
3372 blame_open(struct view *view)
3374 char path[SIZEOF_STR];
3375 char ref[SIZEOF_STR] = "";
3377 if (sq_quote(path, 0, opt_file) >= sizeof(path))
3380 if (*opt_ref && sq_quote(ref, 0, opt_ref) >= sizeof(ref))
3384 if (!string_format(view->cmd, BLAME_CAT_FILE_CMD, ref, path))
3387 view->pipe = fopen(opt_file, "r");
3389 !string_format(view->cmd, BLAME_CAT_FILE_CMD, "HEAD", path))
3394 view->pipe = popen(view->cmd, "r");
3398 if (!string_format(view->cmd, BLAME_INCREMENTAL_CMD, ref, path))
3401 string_format(view->ref, "%s ...", opt_file);
3402 string_copy_rev(view->vid, opt_file);
3403 set_nonblocking_input(TRUE);
3408 for (i = 0; i < view->lines; i++)
3409 free(view->line[i].data);
3413 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3414 view->offset = view->lines = view->lineno = 0;
3416 view->start_time = time(NULL);
3419 static struct blame_commit *
3420 get_blame_commit(struct view *view, const char *id)
3424 for (i = 0; i < view->lines; i++) {
3425 struct blame *blame = view->line[i].data;
3430 if (!strncmp(blame->commit->id, id, SIZEOF_REV - 1))
3431 return blame->commit;
3435 struct blame_commit *commit = calloc(1, sizeof(*commit));
3438 string_ncopy(commit->id, id, SIZEOF_REV);
3444 parse_number(char **posref, size_t *number, size_t min, size_t max)
3446 char *pos = *posref;
3449 pos = strchr(pos + 1, ' ');
3450 if (!pos || !isdigit(pos[1]))
3452 *number = atoi(pos + 1);
3453 if (*number < min || *number > max)
3460 static struct blame_commit *
3461 parse_blame_commit(struct view *view, char *text, int *blamed)
3463 struct blame_commit *commit;
3464 struct blame *blame;
3465 char *pos = text + SIZEOF_REV - 1;
3470 if (strlen(text) <= SIZEOF_REV || *pos != ' ')
3473 if (!parse_number(&pos, &lineno, 1, view->lines) ||
3474 !parse_number(&pos, &group, 1, view->lines - lineno + 1))
3477 commit = get_blame_commit(view, text);
3483 struct line *line = &view->line[lineno + group - 1];
3486 blame->commit = commit;
3495 blame_read_file(struct view *view, char *line)
3500 if (view->lines > 0)
3501 pipe = popen(view->cmd, "r");
3504 report("Failed to load blame data");
3513 size_t linelen = strlen(line);
3514 struct blame *blame = malloc(sizeof(*blame) + linelen);
3519 blame->commit = NULL;
3520 strncpy(blame->text, line, linelen);
3521 blame->text[linelen] = 0;
3522 return add_line_data(view, blame, LINE_BLAME_COMMIT) != NULL;
3527 match_blame_header(const char *name, char **line)
3529 size_t namelen = strlen(name);
3530 bool matched = !strncmp(name, *line, namelen);
3539 blame_read(struct view *view, char *line)
3541 static struct blame_commit *commit = NULL;
3542 static int blamed = 0;
3543 static time_t author_time;
3546 return blame_read_file(view, line);
3552 string_format(view->ref, "%s", view->vid);
3553 if (view_is_displayed(view)) {
3554 update_view_title(view);
3555 redraw_view_from(view, 0);
3561 commit = parse_blame_commit(view, line, &blamed);
3562 string_format(view->ref, "%s %2d%%", view->vid,
3563 blamed * 100 / view->lines);
3565 } else if (match_blame_header("author ", &line)) {
3566 string_ncopy(commit->author, line, strlen(line));
3568 } else if (match_blame_header("author-time ", &line)) {
3569 author_time = (time_t) atol(line);
3571 } else if (match_blame_header("author-tz ", &line)) {
3574 tz = ('0' - line[1]) * 60 * 60 * 10;
3575 tz += ('0' - line[2]) * 60 * 60;
3576 tz += ('0' - line[3]) * 60;
3577 tz += ('0' - line[4]) * 60;
3583 gmtime_r(&author_time, &commit->time);
3585 } else if (match_blame_header("summary ", &line)) {
3586 string_ncopy(commit->title, line, strlen(line));
3588 } else if (match_blame_header("filename ", &line)) {
3589 string_ncopy(commit->filename, line, strlen(line));
3597 blame_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
3599 int tilde_attr = -1;
3600 struct blame *blame = line->data;
3603 wmove(view->win, lineno, 0);
3606 wattrset(view->win, get_line_attr(LINE_CURSOR));
3607 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
3609 wattrset(view->win, A_NORMAL);
3610 tilde_attr = get_line_attr(LINE_MAIN_DELIM);
3617 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
3618 if (blame->commit) {
3619 char buf[DATE_COLS + 1];
3622 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &blame->commit->time);
3623 n = draw_text(view, buf, view->width - col, FALSE, tilde_attr);
3624 draw_text(view, " ", view->width - col - n, FALSE, tilde_attr);
3628 wmove(view->win, lineno, col);
3629 if (col >= view->width)
3634 int max = MIN(AUTHOR_COLS - 1, view->width - col);
3637 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
3639 draw_text(view, blame->commit->author, max, TRUE, tilde_attr);
3641 if (col >= view->width)
3643 wmove(view->win, lineno, col);
3647 int max = MIN(ID_COLS - 1, view->width - col);
3650 wattrset(view->win, get_line_attr(LINE_BLAME_ID));
3652 draw_text(view, blame->commit->id, max, FALSE, -1);
3654 if (col >= view->width)
3656 wmove(view->win, lineno, col);
3660 unsigned long real_lineno = view->offset + lineno + 1;
3661 char number[10] = " ";
3662 int max = MIN(view->digits, STRING_SIZE(number));
3663 bool showtrimmed = FALSE;
3665 if (real_lineno == 1 ||
3666 (real_lineno % opt_num_interval) == 0) {
3667 char fmt[] = "%1ld";
3669 if (view->digits <= 9)
3670 fmt[1] = '0' + view->digits;
3672 if (!string_format(number, fmt, real_lineno))
3677 if (max > view->width - col)
3678 max = view->width - col;
3680 wattrset(view->win, get_line_attr(LINE_BLAME_LINENO));
3681 col += draw_text(view, number, max, showtrimmed, tilde_attr);
3682 if (col >= view->width)
3687 wattrset(view->win, A_NORMAL);
3689 if (col >= view->width)
3691 waddch(view->win, ACS_VLINE);
3693 if (col >= view->width)
3695 waddch(view->win, ' ');
3697 col += draw_text(view, blame->text, view->width - col, TRUE, tilde_attr);
3703 blame_request(struct view *view, enum request request, struct line *line)
3705 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3706 struct blame *blame = line->data;
3710 if (!blame->commit) {
3711 report("No commit loaded yet");
3715 if (!strcmp(blame->commit->id, "0000000000000000000000000000000000000000")) {
3716 char path[SIZEOF_STR];
3718 if (sq_quote(path, 0, view->vid) >= sizeof(path))
3720 string_format(opt_cmd, "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s 2>/dev/null", path);
3723 open_view(view, REQ_VIEW_DIFF, flags);
3734 blame_grep(struct view *view, struct line *line)
3736 struct blame *blame = line->data;
3737 struct blame_commit *commit = blame->commit;
3740 #define MATCH(text) \
3741 (*text && regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3744 char buf[DATE_COLS + 1];
3746 if (MATCH(commit->title) ||
3747 MATCH(commit->author) ||
3751 if (strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time) &&
3756 return MATCH(blame->text);
3762 blame_select(struct view *view, struct line *line)
3764 struct blame *blame = line->data;
3765 struct blame_commit *commit = blame->commit;
3770 if (!strcmp(commit->id, "0000000000000000000000000000000000000000"))
3771 string_ncopy(ref_commit, "HEAD", 4);
3773 string_copy_rev(ref_commit, commit->id);
3776 static struct view_ops blame_ops = {
3794 char rev[SIZEOF_REV];
3795 char name[SIZEOF_STR];
3799 char rev[SIZEOF_REV];
3800 char name[SIZEOF_STR];
3804 static struct status stage_status;
3805 static enum line_type stage_line_type;
3807 /* Get fields from the diff line:
3808 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
3811 status_get_diff(struct status *file, char *buf, size_t bufsize)
3813 char *old_mode = buf + 1;
3814 char *new_mode = buf + 8;
3815 char *old_rev = buf + 15;
3816 char *new_rev = buf + 56;
3817 char *status = buf + 97;
3820 old_mode[-1] != ':' ||
3821 new_mode[-1] != ' ' ||
3822 old_rev[-1] != ' ' ||
3823 new_rev[-1] != ' ' ||
3827 file->status = *status;
3829 string_copy_rev(file->old.rev, old_rev);
3830 string_copy_rev(file->new.rev, new_rev);
3832 file->old.mode = strtoul(old_mode, NULL, 8);
3833 file->new.mode = strtoul(new_mode, NULL, 8);
3835 file->old.name[0] = file->new.name[0] = 0;
3841 status_run(struct view *view, const char cmd[], bool diff, enum line_type type)
3843 struct status *file = NULL;
3844 struct status *unmerged = NULL;
3845 char buf[SIZEOF_STR * 4];
3849 pipe = popen(cmd, "r");
3853 add_line_data(view, NULL, type);
3855 while (!feof(pipe) && !ferror(pipe)) {
3859 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
3862 bufsize += readsize;
3864 /* Process while we have NUL chars. */
3865 while ((sep = memchr(buf, 0, bufsize))) {
3866 size_t sepsize = sep - buf + 1;
3869 if (!realloc_lines(view, view->line_size + 1))
3872 file = calloc(1, sizeof(*file));
3876 add_line_data(view, file, type);
3879 /* Parse diff info part. */
3883 } else if (!file->status) {
3884 if (!status_get_diff(file, buf, sepsize))
3888 memmove(buf, sep + 1, bufsize);
3890 sep = memchr(buf, 0, bufsize);
3893 sepsize = sep - buf + 1;
3895 /* Collapse all 'M'odified entries that
3896 * follow a associated 'U'nmerged entry.
3898 if (file->status == 'U') {
3901 } else if (unmerged) {
3902 int collapse = !strcmp(buf, unmerged->new.name);
3913 /* Grab the old name for rename/copy. */
3914 if (!*file->old.name &&
3915 (file->status == 'R' || file->status == 'C')) {
3916 sepsize = sep - buf + 1;
3917 string_ncopy(file->old.name, buf, sepsize);
3919 memmove(buf, sep + 1, bufsize);
3921 sep = memchr(buf, 0, bufsize);
3924 sepsize = sep - buf + 1;
3927 /* git-ls-files just delivers a NUL separated
3928 * list of file names similar to the second half
3929 * of the git-diff-* output. */
3930 string_ncopy(file->new.name, buf, sepsize);
3931 if (!*file->old.name)
3932 string_copy(file->old.name, file->new.name);
3934 memmove(buf, sep + 1, bufsize);
3945 if (!view->line[view->lines - 1].data)
3946 add_line_data(view, NULL, LINE_STAT_NONE);
3952 /* Don't show unmerged entries in the staged section. */
3953 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached -M HEAD"
3954 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
3955 #define STATUS_LIST_OTHER_CMD \
3956 "git ls-files -z --others --exclude-per-directory=.gitignore"
3958 #define STATUS_DIFF_INDEX_SHOW_CMD \
3959 "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s %s 2>/dev/null"
3961 #define STATUS_DIFF_FILES_SHOW_CMD \
3962 "git diff-files --root --patch-with-stat -C -M -- %s %s 2>/dev/null"
3964 /* First parse staged info using git-diff-index(1), then parse unstaged
3965 * info using git-diff-files(1), and finally untracked files using
3966 * git-ls-files(1). */
3968 status_open(struct view *view)
3970 struct stat statbuf;
3971 char exclude[SIZEOF_STR];
3972 char cmd[SIZEOF_STR];
3973 unsigned long prev_lineno = view->lineno;
3976 for (i = 0; i < view->lines; i++)
3977 free(view->line[i].data);
3979 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3982 if (!realloc_lines(view, view->line_size + 6))
3985 if (!string_format(exclude, "%s/info/exclude", opt_git_dir))
3988 string_copy(cmd, STATUS_LIST_OTHER_CMD);
3990 if (stat(exclude, &statbuf) >= 0) {
3991 size_t cmdsize = strlen(cmd);
3993 if (!string_format_from(cmd, &cmdsize, " %s", "--exclude-from=") ||
3994 sq_quote(cmd, cmdsize, exclude) >= sizeof(cmd))
3998 system("git update-index -q --refresh");
4000 if (!status_run(view, STATUS_DIFF_INDEX_CMD, TRUE, LINE_STAT_STAGED) ||
4001 !status_run(view, STATUS_DIFF_FILES_CMD, TRUE, LINE_STAT_UNSTAGED) ||
4002 !status_run(view, cmd, FALSE, LINE_STAT_UNTRACKED))
4005 /* If all went well restore the previous line number to stay in
4007 if (prev_lineno < view->lines)
4008 view->lineno = prev_lineno;
4010 view->lineno = view->lines - 1;
4016 status_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
4018 struct status *status = line->data;
4019 int tilde_attr = get_line_attr(LINE_MAIN_DELIM);
4021 wmove(view->win, lineno, 0);
4024 wattrset(view->win, get_line_attr(LINE_CURSOR));
4025 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
4028 } else if (!status && line->type != LINE_STAT_NONE) {
4029 wattrset(view->win, get_line_attr(LINE_STAT_SECTION));
4030 wchgat(view->win, -1, 0, LINE_STAT_SECTION, NULL);
4033 wattrset(view->win, get_line_attr(line->type));
4039 switch (line->type) {
4040 case LINE_STAT_STAGED:
4041 text = "Changes to be committed:";
4044 case LINE_STAT_UNSTAGED:
4045 text = "Changed but not updated:";
4048 case LINE_STAT_UNTRACKED:
4049 text = "Untracked files:";
4052 case LINE_STAT_NONE:
4053 text = " (no files)";
4060 draw_text(view, text, view->width, TRUE, tilde_attr);
4064 waddch(view->win, status->status);
4066 wattrset(view->win, A_NORMAL);
4067 wmove(view->win, lineno, 4);
4068 if (view->width < 5)
4071 draw_text(view, status->new.name, view->width - 5, TRUE, tilde_attr);
4076 status_enter(struct view *view, struct line *line)
4078 struct status *status = line->data;
4079 char oldpath[SIZEOF_STR] = "";
4080 char newpath[SIZEOF_STR] = "";
4084 if (line->type == LINE_STAT_NONE ||
4085 (!status && line[1].type == LINE_STAT_NONE)) {
4086 report("No file to diff");
4091 if (sq_quote(oldpath, 0, status->old.name) >= sizeof(oldpath))
4093 /* Diffs for unmerged entries are empty when pasing the
4094 * new path, so leave it empty. */
4095 if (status->status != 'U' &&
4096 sq_quote(newpath, 0, status->new.name) >= sizeof(newpath))
4101 line->type != LINE_STAT_UNTRACKED &&
4102 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
4105 switch (line->type) {
4106 case LINE_STAT_STAGED:
4107 if (!string_format_from(opt_cmd, &cmdsize,
4108 STATUS_DIFF_INDEX_SHOW_CMD, oldpath, newpath))
4111 info = "Staged changes to %s";
4113 info = "Staged changes";
4116 case LINE_STAT_UNSTAGED:
4117 if (!string_format_from(opt_cmd, &cmdsize,
4118 STATUS_DIFF_FILES_SHOW_CMD, oldpath, newpath))
4121 info = "Unstaged changes to %s";
4123 info = "Unstaged changes";
4126 case LINE_STAT_UNTRACKED:
4132 report("No file to show");
4136 opt_pipe = fopen(status->new.name, "r");
4137 info = "Untracked file %s";
4141 die("line type %d not handled in switch", line->type);
4144 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | OPEN_SPLIT);
4145 if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) {
4147 stage_status = *status;
4149 memset(&stage_status, 0, sizeof(stage_status));
4152 stage_line_type = line->type;
4153 string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.new.name);
4161 status_update_file(struct view *view, struct status *status, enum line_type type)
4163 char cmd[SIZEOF_STR];
4164 char buf[SIZEOF_STR];
4171 type != LINE_STAT_UNTRACKED &&
4172 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4176 case LINE_STAT_STAGED:
4177 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
4180 status->old.name, 0))
4183 string_add(cmd, cmdsize, "git update-index -z --index-info");
4186 case LINE_STAT_UNSTAGED:
4187 case LINE_STAT_UNTRACKED:
4188 if (!string_format_from(buf, &bufsize, "%s%c", status->new.name, 0))
4191 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
4195 die("line type %d not handled in switch", type);
4198 pipe = popen(cmd, "w");
4202 while (!ferror(pipe) && written < bufsize) {
4203 written += fwrite(buf + written, 1, bufsize - written, pipe);
4208 if (written != bufsize)
4215 status_update(struct view *view)
4217 struct line *line = &view->line[view->lineno];
4219 assert(view->lines);
4222 while (++line < view->line + view->lines && line->data) {
4223 if (!status_update_file(view, line->data, line->type))
4224 report("Failed to update file status");
4227 if (!line[-1].data) {
4228 report("Nothing to update");
4232 } else if (!status_update_file(view, line->data, line->type)) {
4233 report("Failed to update file status");
4238 status_request(struct view *view, enum request request, struct line *line)
4240 struct status *status = line->data;
4243 case REQ_STATUS_UPDATE:
4244 status_update(view);
4247 case REQ_STATUS_MERGE:
4248 if (!status || status->status != 'U') {
4249 report("Merging only possible for files with unmerged status ('U').");
4252 open_mergetool(status->new.name);
4259 open_editor(status->status != '?', status->new.name);
4262 case REQ_VIEW_BLAME:
4264 string_copy(opt_file, status->new.name);
4270 /* After returning the status view has been split to
4271 * show the stage view. No further reloading is
4273 status_enter(view, line);
4277 /* Simply reload the view. */
4284 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
4290 status_select(struct view *view, struct line *line)
4292 struct status *status = line->data;
4293 char file[SIZEOF_STR] = "all files";
4297 if (status && !string_format(file, "'%s'", status->new.name))
4300 if (!status && line[1].type == LINE_STAT_NONE)
4303 switch (line->type) {
4304 case LINE_STAT_STAGED:
4305 text = "Press %s to unstage %s for commit";
4308 case LINE_STAT_UNSTAGED:
4309 text = "Press %s to stage %s for commit";
4312 case LINE_STAT_UNTRACKED:
4313 text = "Press %s to stage %s for addition";
4316 case LINE_STAT_NONE:
4317 text = "Nothing to update";
4321 die("line type %d not handled in switch", line->type);
4324 if (status && status->status == 'U') {
4325 text = "Press %s to resolve conflict in %s";
4326 key = get_key(REQ_STATUS_MERGE);
4329 key = get_key(REQ_STATUS_UPDATE);
4332 string_format(view->ref, text, key, file);
4336 status_grep(struct view *view, struct line *line)
4338 struct status *status = line->data;
4339 enum { S_STATUS, S_NAME, S_END } state;
4346 for (state = S_STATUS; state < S_END; state++) {
4350 case S_NAME: text = status->new.name; break;
4352 buf[0] = status->status;
4360 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4367 static struct view_ops status_ops = {
4379 stage_diff_line(FILE *pipe, struct line *line)
4381 char *buf = line->data;
4382 size_t bufsize = strlen(buf);
4385 while (!ferror(pipe) && written < bufsize) {
4386 written += fwrite(buf + written, 1, bufsize - written, pipe);
4391 return written == bufsize;
4394 static struct line *
4395 stage_diff_hdr(struct view *view, struct line *line)
4397 int diff_hdr_dir = line->type == LINE_DIFF_CHUNK ? -1 : 1;
4398 struct line *diff_hdr;
4400 if (line->type == LINE_DIFF_CHUNK)
4401 diff_hdr = line - 1;
4403 diff_hdr = view->line + 1;
4405 while (diff_hdr > view->line && diff_hdr < view->line + view->lines) {
4406 if (diff_hdr->type == LINE_DIFF_HEADER)
4409 diff_hdr += diff_hdr_dir;
4416 stage_update_chunk(struct view *view, struct line *line)
4418 char cmd[SIZEOF_STR];
4420 struct line *diff_hdr, *diff_chunk, *diff_end;
4423 diff_hdr = stage_diff_hdr(view, line);
4428 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4431 if (!string_format_from(cmd, &cmdsize,
4432 "git apply --cached %s - && "
4433 "git update-index -q --unmerged --refresh 2>/dev/null",
4434 stage_line_type == LINE_STAT_STAGED ? "-R" : ""))
4437 pipe = popen(cmd, "w");
4441 diff_end = view->line + view->lines;
4442 if (line->type != LINE_DIFF_CHUNK) {
4443 diff_chunk = diff_hdr;
4446 for (diff_chunk = line + 1; diff_chunk < diff_end; diff_chunk++)
4447 if (diff_chunk->type == LINE_DIFF_CHUNK ||
4448 diff_chunk->type == LINE_DIFF_HEADER)
4449 diff_end = diff_chunk;
4453 while (diff_hdr->type != LINE_DIFF_CHUNK) {
4454 switch (diff_hdr->type) {
4455 case LINE_DIFF_HEADER:
4456 case LINE_DIFF_INDEX:
4466 if (!stage_diff_line(pipe, diff_hdr++)) {
4473 while (diff_chunk < diff_end && stage_diff_line(pipe, diff_chunk))
4478 if (diff_chunk != diff_end)
4485 stage_update(struct view *view, struct line *line)
4487 if (stage_line_type != LINE_STAT_UNTRACKED &&
4488 (line->type == LINE_DIFF_CHUNK || !stage_status.status)) {
4489 if (!stage_update_chunk(view, line)) {
4490 report("Failed to apply chunk");
4494 } else if (!status_update_file(view, &stage_status, stage_line_type)) {
4495 report("Failed to update file");
4499 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
4501 view = VIEW(REQ_VIEW_STATUS);
4502 if (view_is_displayed(view))
4503 status_enter(view, &view->line[view->lineno]);
4507 stage_request(struct view *view, enum request request, struct line *line)
4510 case REQ_STATUS_UPDATE:
4511 stage_update(view, line);
4515 if (!stage_status.new.name[0])
4518 open_editor(stage_status.status != '?', stage_status.new.name);
4521 case REQ_VIEW_BLAME:
4522 if (stage_status.new.name[0]) {
4523 string_copy(opt_file, stage_status.new.name);
4529 pager_request(view, request, line);
4539 static struct view_ops stage_ops = {
4555 char id[SIZEOF_REV]; /* SHA1 ID. */
4556 char title[128]; /* First line of the commit message. */
4557 char author[75]; /* Author of the commit. */
4558 struct tm time; /* Date from the author ident. */
4559 struct ref **refs; /* Repository references. */
4560 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
4561 size_t graph_size; /* The width of the graph array. */
4562 bool has_parents; /* Rewritten --parents seen. */
4565 /* Size of rev graph with no "padding" columns */
4566 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
4569 struct rev_graph *prev, *next, *parents;
4570 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
4572 struct commit *commit;
4574 unsigned int boundary:1;
4577 /* Parents of the commit being visualized. */
4578 static struct rev_graph graph_parents[4];
4580 /* The current stack of revisions on the graph. */
4581 static struct rev_graph graph_stacks[4] = {
4582 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
4583 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
4584 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
4585 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
4589 graph_parent_is_merge(struct rev_graph *graph)
4591 return graph->parents->size > 1;
4595 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
4597 struct commit *commit = graph->commit;
4599 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
4600 commit->graph[commit->graph_size++] = symbol;
4604 done_rev_graph(struct rev_graph *graph)
4606 if (graph_parent_is_merge(graph) &&
4607 graph->pos < graph->size - 1 &&
4608 graph->next->size == graph->size + graph->parents->size - 1) {
4609 size_t i = graph->pos + graph->parents->size - 1;
4611 graph->commit->graph_size = i * 2;
4612 while (i < graph->next->size - 1) {
4613 append_to_rev_graph(graph, ' ');
4614 append_to_rev_graph(graph, '\\');
4619 graph->size = graph->pos = 0;
4620 graph->commit = NULL;
4621 memset(graph->parents, 0, sizeof(*graph->parents));
4625 push_rev_graph(struct rev_graph *graph, char *parent)
4629 /* "Collapse" duplicate parents lines.
4631 * FIXME: This needs to also update update the drawn graph but
4632 * for now it just serves as a method for pruning graph lines. */
4633 for (i = 0; i < graph->size; i++)
4634 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
4637 if (graph->size < SIZEOF_REVITEMS) {
4638 string_copy_rev(graph->rev[graph->size++], parent);
4643 get_rev_graph_symbol(struct rev_graph *graph)
4647 if (graph->boundary)
4648 symbol = REVGRAPH_BOUND;
4649 else if (graph->parents->size == 0)
4650 symbol = REVGRAPH_INIT;
4651 else if (graph_parent_is_merge(graph))
4652 symbol = REVGRAPH_MERGE;
4653 else if (graph->pos >= graph->size)
4654 symbol = REVGRAPH_BRANCH;
4656 symbol = REVGRAPH_COMMIT;
4662 draw_rev_graph(struct rev_graph *graph)
4665 chtype separator, line;
4667 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
4668 static struct rev_filler fillers[] = {
4669 { ' ', REVGRAPH_LINE },
4674 chtype symbol = get_rev_graph_symbol(graph);
4675 struct rev_filler *filler;
4678 filler = &fillers[DEFAULT];
4680 for (i = 0; i < graph->pos; i++) {
4681 append_to_rev_graph(graph, filler->line);
4682 if (graph_parent_is_merge(graph->prev) &&
4683 graph->prev->pos == i)
4684 filler = &fillers[RSHARP];
4686 append_to_rev_graph(graph, filler->separator);
4689 /* Place the symbol for this revision. */
4690 append_to_rev_graph(graph, symbol);
4692 if (graph->prev->size > graph->size)
4693 filler = &fillers[RDIAG];
4695 filler = &fillers[DEFAULT];
4699 for (; i < graph->size; i++) {
4700 append_to_rev_graph(graph, filler->separator);
4701 append_to_rev_graph(graph, filler->line);
4702 if (graph_parent_is_merge(graph->prev) &&
4703 i < graph->prev->pos + graph->parents->size)
4704 filler = &fillers[RSHARP];
4705 if (graph->prev->size > graph->size)
4706 filler = &fillers[LDIAG];
4709 if (graph->prev->size > graph->size) {
4710 append_to_rev_graph(graph, filler->separator);
4711 if (filler->line != ' ')
4712 append_to_rev_graph(graph, filler->line);
4716 /* Prepare the next rev graph */
4718 prepare_rev_graph(struct rev_graph *graph)
4722 /* First, traverse all lines of revisions up to the active one. */
4723 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
4724 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
4727 push_rev_graph(graph->next, graph->rev[graph->pos]);
4730 /* Interleave the new revision parent(s). */
4731 for (i = 0; !graph->boundary && i < graph->parents->size; i++)
4732 push_rev_graph(graph->next, graph->parents->rev[i]);
4734 /* Lastly, put any remaining revisions. */
4735 for (i = graph->pos + 1; i < graph->size; i++)
4736 push_rev_graph(graph->next, graph->rev[i]);
4740 update_rev_graph(struct rev_graph *graph)
4742 /* If this is the finalizing update ... */
4744 prepare_rev_graph(graph);
4746 /* Graph visualization needs a one rev look-ahead,
4747 * so the first update doesn't visualize anything. */
4748 if (!graph->prev->commit)
4751 draw_rev_graph(graph->prev);
4752 done_rev_graph(graph->prev->prev);
4761 main_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
4763 char buf[DATE_COLS + 1];
4764 struct commit *commit = line->data;
4765 enum line_type type;
4771 if (!*commit->author)
4774 space = view->width;
4775 wmove(view->win, lineno, col);
4779 wattrset(view->win, get_line_attr(type));
4780 wchgat(view->win, -1, 0, type, NULL);
4783 type = LINE_MAIN_COMMIT;
4784 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
4785 tilde_attr = get_line_attr(LINE_MAIN_DELIM);
4791 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
4792 n = draw_text(view, buf, view->width - col, FALSE, tilde_attr);
4793 draw_text(view, " ", view->width - col - n, FALSE, tilde_attr);
4796 wmove(view->win, lineno, col);
4797 if (col >= view->width)
4800 if (type != LINE_CURSOR)
4801 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
4806 max_len = view->width - col;
4807 if (max_len > AUTHOR_COLS - 1)
4808 max_len = AUTHOR_COLS - 1;
4809 draw_text(view, commit->author, max_len, TRUE, tilde_attr);
4811 if (col >= view->width)
4815 if (opt_rev_graph && commit->graph_size) {
4816 size_t graph_size = view->width - col;
4819 if (type != LINE_CURSOR)
4820 wattrset(view->win, get_line_attr(LINE_MAIN_REVGRAPH));
4821 wmove(view->win, lineno, col);
4822 if (graph_size > commit->graph_size)
4823 graph_size = commit->graph_size;
4824 /* Using waddch() instead of waddnstr() ensures that
4825 * they'll be rendered correctly for the cursor line. */
4826 for (i = 0; i < graph_size; i++)
4827 waddch(view->win, commit->graph[i]);
4829 col += commit->graph_size + 1;
4830 if (col >= view->width)
4832 waddch(view->win, ' ');
4834 if (type != LINE_CURSOR)
4835 wattrset(view->win, A_NORMAL);
4837 wmove(view->win, lineno, col);
4839 if (opt_show_refs && commit->refs) {
4843 if (type == LINE_CURSOR)
4845 else if (commit->refs[i]->ltag)
4846 wattrset(view->win, get_line_attr(LINE_MAIN_LOCAL_TAG));
4847 else if (commit->refs[i]->tag)
4848 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
4849 else if (commit->refs[i]->remote)
4850 wattrset(view->win, get_line_attr(LINE_MAIN_REMOTE));
4852 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
4854 col += draw_text(view, "[", view->width - col, TRUE, tilde_attr);
4855 col += draw_text(view, commit->refs[i]->name, view->width - col,
4857 col += draw_text(view, "]", view->width - col, TRUE, tilde_attr);
4858 if (type != LINE_CURSOR)
4859 wattrset(view->win, A_NORMAL);
4860 col += draw_text(view, " ", view->width - col, TRUE, tilde_attr);
4861 if (col >= view->width)
4863 } while (commit->refs[i++]->next);
4866 if (type != LINE_CURSOR)
4867 wattrset(view->win, get_line_attr(type));
4869 draw_text(view, commit->title, view->width - col, TRUE, tilde_attr);
4873 /* Reads git log --pretty=raw output and parses it into the commit struct. */
4875 main_read(struct view *view, char *line)
4877 static struct rev_graph *graph = graph_stacks;
4878 enum line_type type;
4879 struct commit *commit;
4882 update_rev_graph(graph);
4886 type = get_line_type(line);
4887 if (type == LINE_COMMIT) {
4888 commit = calloc(1, sizeof(struct commit));
4892 line += STRING_SIZE("commit ");
4894 graph->boundary = 1;
4898 string_copy_rev(commit->id, line);
4899 commit->refs = get_refs(commit->id);
4900 graph->commit = commit;
4901 add_line_data(view, commit, LINE_MAIN_COMMIT);
4903 while ((line = strchr(line, ' '))) {
4905 push_rev_graph(graph->parents, line);
4906 commit->has_parents = TRUE;
4913 commit = view->line[view->lines - 1].data;
4917 if (commit->has_parents)
4919 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
4924 /* Parse author lines where the name may be empty:
4925 * author <email@address.tld> 1138474660 +0100
4927 char *ident = line + STRING_SIZE("author ");
4928 char *nameend = strchr(ident, '<');
4929 char *emailend = strchr(ident, '>');
4931 if (!nameend || !emailend)
4934 update_rev_graph(graph);
4935 graph = graph->next;
4937 *nameend = *emailend = 0;
4938 ident = chomp_string(ident);
4940 ident = chomp_string(nameend + 1);
4945 string_ncopy(commit->author, ident, strlen(ident));
4947 /* Parse epoch and timezone */
4948 if (emailend[1] == ' ') {
4949 char *secs = emailend + 2;
4950 char *zone = strchr(secs, ' ');
4951 time_t time = (time_t) atol(secs);
4953 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
4957 tz = ('0' - zone[1]) * 60 * 60 * 10;
4958 tz += ('0' - zone[2]) * 60 * 60;
4959 tz += ('0' - zone[3]) * 60;
4960 tz += ('0' - zone[4]) * 60;
4968 gmtime_r(&time, &commit->time);
4973 /* Fill in the commit title if it has not already been set. */
4974 if (commit->title[0])
4977 /* Require titles to start with a non-space character at the
4978 * offset used by git log. */
4979 if (strncmp(line, " ", 4))
4982 /* Well, if the title starts with a whitespace character,
4983 * try to be forgiving. Otherwise we end up with no title. */
4984 while (isspace(*line))
4988 /* FIXME: More graceful handling of titles; append "..." to
4989 * shortened titles, etc. */
4991 string_ncopy(commit->title, line, strlen(line));
4998 main_request(struct view *view, enum request request, struct line *line)
5000 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
5002 if (request == REQ_ENTER)
5003 open_view(view, REQ_VIEW_DIFF, flags);
5011 main_grep(struct view *view, struct line *line)
5013 struct commit *commit = line->data;
5014 enum { S_TITLE, S_AUTHOR, S_DATE, S_END } state;
5015 char buf[DATE_COLS + 1];
5018 for (state = S_TITLE; state < S_END; state++) {
5022 case S_TITLE: text = commit->title; break;
5023 case S_AUTHOR: text = commit->author; break;
5025 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
5034 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
5042 main_select(struct view *view, struct line *line)
5044 struct commit *commit = line->data;
5046 string_copy_rev(view->ref, commit->id);
5047 string_copy_rev(ref_commit, view->ref);
5050 static struct view_ops main_ops = {
5062 * Unicode / UTF-8 handling
5064 * NOTE: Much of the following code for dealing with unicode is derived from
5065 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
5066 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
5069 /* I've (over)annotated a lot of code snippets because I am not entirely
5070 * confident that the approach taken by this small UTF-8 interface is correct.
5074 unicode_width(unsigned long c)
5077 (c <= 0x115f /* Hangul Jamo */
5080 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
5082 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
5083 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
5084 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
5085 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
5086 || (c >= 0xffe0 && c <= 0xffe6)
5087 || (c >= 0x20000 && c <= 0x2fffd)
5088 || (c >= 0x30000 && c <= 0x3fffd)))
5092 return opt_tab_size;
5097 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
5098 * Illegal bytes are set one. */
5099 static const unsigned char utf8_bytes[256] = {
5100 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,
5101 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,
5102 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,
5103 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,
5104 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,
5105 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,
5106 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,
5107 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,
5110 /* Decode UTF-8 multi-byte representation into a unicode character. */
5111 static inline unsigned long
5112 utf8_to_unicode(const char *string, size_t length)
5114 unsigned long unicode;
5118 unicode = string[0];
5121 unicode = (string[0] & 0x1f) << 6;
5122 unicode += (string[1] & 0x3f);
5125 unicode = (string[0] & 0x0f) << 12;
5126 unicode += ((string[1] & 0x3f) << 6);
5127 unicode += (string[2] & 0x3f);
5130 unicode = (string[0] & 0x0f) << 18;
5131 unicode += ((string[1] & 0x3f) << 12);
5132 unicode += ((string[2] & 0x3f) << 6);
5133 unicode += (string[3] & 0x3f);
5136 unicode = (string[0] & 0x0f) << 24;
5137 unicode += ((string[1] & 0x3f) << 18);
5138 unicode += ((string[2] & 0x3f) << 12);
5139 unicode += ((string[3] & 0x3f) << 6);
5140 unicode += (string[4] & 0x3f);
5143 unicode = (string[0] & 0x01) << 30;
5144 unicode += ((string[1] & 0x3f) << 24);
5145 unicode += ((string[2] & 0x3f) << 18);
5146 unicode += ((string[3] & 0x3f) << 12);
5147 unicode += ((string[4] & 0x3f) << 6);
5148 unicode += (string[5] & 0x3f);
5151 die("Invalid unicode length");
5154 /* Invalid characters could return the special 0xfffd value but NUL
5155 * should be just as good. */
5156 return unicode > 0xffff ? 0 : unicode;
5159 /* Calculates how much of string can be shown within the given maximum width
5160 * and sets trimmed parameter to non-zero value if all of string could not be
5161 * shown. If the reserve flag is TRUE, it will reserve at least one
5162 * trailing character, which can be useful when drawing a delimiter.
5164 * Returns the number of bytes to output from string to satisfy max_width. */
5166 utf8_length(const char *string, size_t max_width, int *trimmed, bool reserve)
5168 const char *start = string;
5169 const char *end = strchr(string, '\0');
5170 unsigned char last_bytes = 0;
5175 while (string < end) {
5176 int c = *(unsigned char *) string;
5177 unsigned char bytes = utf8_bytes[c];
5179 unsigned long unicode;
5181 if (string + bytes > end)
5184 /* Change representation to figure out whether
5185 * it is a single- or double-width character. */
5187 unicode = utf8_to_unicode(string, bytes);
5188 /* FIXME: Graceful handling of invalid unicode character. */
5192 ucwidth = unicode_width(unicode);
5194 if (width > max_width) {
5196 if (reserve && width - ucwidth == max_width) {
5197 string -= last_bytes;
5206 return string - start;
5214 /* Whether or not the curses interface has been initialized. */
5215 static bool cursed = FALSE;
5217 /* The status window is used for polling keystrokes. */
5218 static WINDOW *status_win;
5220 static bool status_empty = TRUE;
5222 /* Update status and title window. */
5224 report(const char *msg, ...)
5226 struct view *view = display[current_view];
5232 char buf[SIZEOF_STR];
5235 va_start(args, msg);
5236 if (vsnprintf(buf, sizeof(buf), msg, args) >= sizeof(buf)) {
5237 buf[sizeof(buf) - 1] = 0;
5238 buf[sizeof(buf) - 2] = '.';
5239 buf[sizeof(buf) - 3] = '.';
5240 buf[sizeof(buf) - 4] = '.';
5246 if (!status_empty || *msg) {
5249 va_start(args, msg);
5251 wmove(status_win, 0, 0);
5253 vwprintw(status_win, msg, args);
5254 status_empty = FALSE;
5256 status_empty = TRUE;
5258 wclrtoeol(status_win);
5259 wrefresh(status_win);
5264 update_view_title(view);
5265 update_display_cursor(view);
5268 /* Controls when nodelay should be in effect when polling user input. */
5270 set_nonblocking_input(bool loading)
5272 static unsigned int loading_views;
5274 if ((loading == FALSE && loading_views-- == 1) ||
5275 (loading == TRUE && loading_views++ == 0))
5276 nodelay(status_win, loading);
5284 /* Initialize the curses library */
5285 if (isatty(STDIN_FILENO)) {
5286 cursed = !!initscr();
5288 /* Leave stdin and stdout alone when acting as a pager. */
5289 FILE *io = fopen("/dev/tty", "r+");
5292 die("Failed to open /dev/tty");
5293 cursed = !!newterm(NULL, io, io);
5297 die("Failed to initialize curses");
5299 nonl(); /* Tell curses not to do NL->CR/NL on output */
5300 cbreak(); /* Take input chars one at a time, no wait for \n */
5301 noecho(); /* Don't echo input */
5302 leaveok(stdscr, TRUE);
5307 getmaxyx(stdscr, y, x);
5308 status_win = newwin(1, 0, y - 1, 0);
5310 die("Failed to create status window");
5312 /* Enable keyboard mapping */
5313 keypad(status_win, TRUE);
5314 wbkgdset(status_win, get_line_attr(LINE_STATUS));
5318 read_prompt(const char *prompt)
5320 enum { READING, STOP, CANCEL } status = READING;
5321 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
5324 while (status == READING) {
5330 foreach_view (view, i)
5335 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
5336 wclrtoeol(status_win);
5338 /* Refresh, accept single keystroke of input */
5339 key = wgetch(status_win);
5344 status = pos ? STOP : CANCEL;
5362 if (pos >= sizeof(buf)) {
5363 report("Input string too long");
5368 buf[pos++] = (char) key;
5372 /* Clear the status window */
5373 status_empty = FALSE;
5376 if (status == CANCEL)
5385 * Repository references
5388 static struct ref *refs = NULL;
5389 static size_t refs_alloc = 0;
5390 static size_t refs_size = 0;
5392 /* Id <-> ref store */
5393 static struct ref ***id_refs = NULL;
5394 static size_t id_refs_alloc = 0;
5395 static size_t id_refs_size = 0;
5397 static struct ref **
5400 struct ref ***tmp_id_refs;
5401 struct ref **ref_list = NULL;
5402 size_t ref_list_alloc = 0;
5403 size_t ref_list_size = 0;
5406 for (i = 0; i < id_refs_size; i++)
5407 if (!strcmp(id, id_refs[i][0]->id))
5410 tmp_id_refs = realloc_items(id_refs, &id_refs_alloc, id_refs_size + 1,
5415 id_refs = tmp_id_refs;
5417 for (i = 0; i < refs_size; i++) {
5420 if (strcmp(id, refs[i].id))
5423 tmp = realloc_items(ref_list, &ref_list_alloc,
5424 ref_list_size + 1, sizeof(*ref_list));
5432 if (ref_list_size > 0)
5433 ref_list[ref_list_size - 1]->next = 1;
5434 ref_list[ref_list_size] = &refs[i];
5436 /* XXX: The properties of the commit chains ensures that we can
5437 * safely modify the shared ref. The repo references will
5438 * always be similar for the same id. */
5439 ref_list[ref_list_size]->next = 0;
5444 id_refs[id_refs_size++] = ref_list;
5450 read_ref(char *id, size_t idlen, char *name, size_t namelen)
5455 bool remote = FALSE;
5456 bool check_replace = FALSE;
5458 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
5459 if (!strcmp(name + namelen - 3, "^{}")) {
5462 if (refs_size > 0 && refs[refs_size - 1].ltag == TRUE)
5463 check_replace = TRUE;
5469 namelen -= STRING_SIZE("refs/tags/");
5470 name += STRING_SIZE("refs/tags/");
5472 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
5474 namelen -= STRING_SIZE("refs/remotes/");
5475 name += STRING_SIZE("refs/remotes/");
5477 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5478 namelen -= STRING_SIZE("refs/heads/");
5479 name += STRING_SIZE("refs/heads/");
5481 } else if (!strcmp(name, "HEAD")) {
5485 if (check_replace && !strcmp(name, refs[refs_size - 1].name)) {
5486 /* it's an annotated tag, replace the previous sha1 with the
5487 * resolved commit id; relies on the fact git-ls-remote lists
5488 * the commit id of an annotated tag right beofre the commit id
5490 refs[refs_size - 1].ltag = ltag;
5491 string_copy_rev(refs[refs_size - 1].id, id);
5495 refs = realloc_items(refs, &refs_alloc, refs_size + 1, sizeof(*refs));
5499 ref = &refs[refs_size++];
5500 ref->name = malloc(namelen + 1);
5504 strncpy(ref->name, name, namelen);
5505 ref->name[namelen] = 0;
5508 ref->remote = remote;
5509 string_copy_rev(ref->id, id);
5517 const char *cmd_env = getenv("TIG_LS_REMOTE");
5518 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
5520 return read_properties(popen(cmd, "r"), "\t", read_ref);
5524 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
5526 if (!strcmp(name, "i18n.commitencoding"))
5527 string_ncopy(opt_encoding, value, valuelen);
5529 if (!strcmp(name, "core.editor"))
5530 string_ncopy(opt_editor, value, valuelen);
5536 load_repo_config(void)
5538 return read_properties(popen(GIT_CONFIG " --list", "r"),
5539 "=", read_repo_config_option);
5543 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
5545 if (!opt_git_dir[0]) {
5546 string_ncopy(opt_git_dir, name, namelen);
5548 } else if (opt_is_inside_work_tree == -1) {
5549 /* This can be 3 different values depending on the
5550 * version of git being used. If git-rev-parse does not
5551 * understand --is-inside-work-tree it will simply echo
5552 * the option else either "true" or "false" is printed.
5553 * Default to true for the unknown case. */
5554 opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
5557 string_ncopy(opt_cdup, name, namelen);
5563 /* XXX: The line outputted by "--show-cdup" can be empty so the option
5564 * must be the last one! */
5566 load_repo_info(void)
5568 return read_properties(popen("git rev-parse --git-dir --is-inside-work-tree --show-cdup 2>/dev/null", "r"),
5569 "=", read_repo_info);
5573 read_properties(FILE *pipe, const char *separators,
5574 int (*read_property)(char *, size_t, char *, size_t))
5576 char buffer[BUFSIZ];
5583 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
5588 name = chomp_string(name);
5589 namelen = strcspn(name, separators);
5591 if (name[namelen]) {
5593 value = chomp_string(name + namelen + 1);
5594 valuelen = strlen(value);
5601 state = read_property(name, namelen, value, valuelen);
5604 if (state != ERR && ferror(pipe))
5617 static void __NORETURN
5620 /* XXX: Restore tty modes and let the OS cleanup the rest! */
5626 static void __NORETURN
5627 die(const char *err, ...)
5633 va_start(args, err);
5634 fputs("tig: ", stderr);
5635 vfprintf(stderr, err, args);
5636 fputs("\n", stderr);
5643 warn(const char *msg, ...)
5647 va_start(args, msg);
5648 fputs("tig warning: ", stderr);
5649 vfprintf(stderr, msg, args);
5650 fputs("\n", stderr);
5655 main(int argc, char *argv[])
5658 enum request request;
5661 signal(SIGINT, quit);
5663 if (setlocale(LC_ALL, "")) {
5664 char *codeset = nl_langinfo(CODESET);
5666 string_ncopy(opt_codeset, codeset, strlen(codeset));
5669 if (load_repo_info() == ERR)
5670 die("Failed to load repo info.");
5672 if (load_options() == ERR)
5673 die("Failed to load user config.");
5675 /* Load the repo config file so options can be overwritten from
5676 * the command line. */
5677 if (load_repo_config() == ERR)
5678 die("Failed to load repo config.");
5680 if (!parse_options(argc, argv))
5683 /* Require a git repository unless when running in pager mode. */
5684 if (!opt_git_dir[0])
5685 die("Not a git repository");
5687 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
5690 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
5691 opt_iconv = iconv_open(opt_codeset, opt_encoding);
5692 if (opt_iconv == ICONV_NONE)
5693 die("Failed to initialize character set conversion");
5696 if (load_refs() == ERR)
5697 die("Failed to load refs.");
5699 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
5700 view->cmd_env = getenv(view->cmd_env);
5702 request = opt_request;
5706 while (view_driver(display[current_view], request)) {
5710 foreach_view (view, i)
5713 /* Refresh, accept single keystroke of input */
5714 key = wgetch(status_win);
5716 /* wgetch() with nodelay() enabled returns ERR when there's no
5723 request = get_keybinding(display[current_view]->keymap, key);
5725 /* Some low-level request handling. This keeps access to
5726 * status_win restricted. */
5730 char *cmd = read_prompt(":");
5732 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
5733 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
5734 opt_request = REQ_VIEW_DIFF;
5736 opt_request = REQ_VIEW_PAGER;
5745 case REQ_SEARCH_BACK:
5747 const char *prompt = request == REQ_SEARCH
5749 char *search = read_prompt(prompt);
5752 string_ncopy(opt_search, search, strlen(search));
5757 case REQ_SCREEN_RESIZE:
5761 getmaxyx(stdscr, height, width);
5763 /* Resize the status view and let the view driver take
5764 * care of resizing the displayed views. */
5765 wresize(status_win, 1, width);
5766 mvwin(status_win, height - 1, 0);
5767 wrefresh(status_win);