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)
106 #define NULL_ID "0000000000000000000000000000000000000000"
109 #define GIT_CONFIG "git config"
112 #define TIG_LS_REMOTE \
113 "git ls-remote $(git rev-parse --git-dir) 2>/dev/null"
115 #define TIG_DIFF_CMD \
116 "git show --pretty=fuller --no-color --root --patch-with-stat --find-copies-harder -C %s 2>/dev/null"
118 #define TIG_LOG_CMD \
119 "git log --no-color --cc --stat -n100 %s 2>/dev/null"
121 #define TIG_MAIN_CMD \
122 "git log --no-color --topo-order --parents --boundary --pretty=raw %s 2>/dev/null"
124 #define TIG_TREE_CMD \
127 #define TIG_BLOB_CMD \
128 "git cat-file blob %s"
130 /* XXX: Needs to be defined to the empty string. */
131 #define TIG_HELP_CMD ""
132 #define TIG_PAGER_CMD ""
133 #define TIG_STATUS_CMD ""
134 #define TIG_STAGE_CMD ""
135 #define TIG_BLAME_CMD ""
137 /* Some ascii-shorthands fitted into the ncurses namespace. */
139 #define KEY_RETURN '\r'
144 char *name; /* Ref name; tag or head names are shortened. */
145 char id[SIZEOF_REV]; /* Commit SHA1 ID */
146 unsigned int head:1; /* Is it the current HEAD? */
147 unsigned int tag:1; /* Is it a tag? */
148 unsigned int ltag:1; /* If so, is the tag local? */
149 unsigned int remote:1; /* Is it a remote ref? */
150 unsigned int tracked:1; /* Is it the remote for the current HEAD? */
151 unsigned int next:1; /* For ref lists: are there more refs? */
154 static struct ref **get_refs(char *id);
163 set_from_int_map(struct int_map *map, size_t map_size,
164 int *value, const char *name, int namelen)
169 for (i = 0; i < map_size; i++)
170 if (namelen == map[i].namelen &&
171 !strncasecmp(name, map[i].name, namelen)) {
172 *value = map[i].value;
185 string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
187 if (srclen > dstlen - 1)
190 strncpy(dst, src, srclen);
194 /* Shorthands for safely copying into a fixed buffer. */
196 #define string_copy(dst, src) \
197 string_ncopy_do(dst, sizeof(dst), src, sizeof(src))
199 #define string_ncopy(dst, src, srclen) \
200 string_ncopy_do(dst, sizeof(dst), src, srclen)
202 #define string_copy_rev(dst, src) \
203 string_ncopy_do(dst, SIZEOF_REV, src, SIZEOF_REV - 1)
205 #define string_add(dst, from, src) \
206 string_ncopy_do(dst + (from), sizeof(dst) - (from), src, sizeof(src))
209 chomp_string(char *name)
213 while (isspace(*name))
216 namelen = strlen(name) - 1;
217 while (namelen > 0 && isspace(name[namelen]))
224 string_nformat(char *buf, size_t bufsize, size_t *bufpos, const char *fmt, ...)
227 size_t pos = bufpos ? *bufpos : 0;
230 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
236 return pos >= bufsize ? FALSE : TRUE;
239 #define string_format(buf, fmt, args...) \
240 string_nformat(buf, sizeof(buf), NULL, fmt, args)
242 #define string_format_from(buf, from, fmt, args...) \
243 string_nformat(buf, sizeof(buf), from, fmt, args)
246 string_enum_compare(const char *str1, const char *str2, int len)
250 #define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
252 /* Diff-Header == DIFF_HEADER */
253 for (i = 0; i < len; i++) {
254 if (toupper(str1[i]) == toupper(str2[i]))
257 if (string_enum_sep(str1[i]) &&
258 string_enum_sep(str2[i]))
261 return str1[i] - str2[i];
269 * NOTE: The following is a slightly modified copy of the git project's shell
270 * quoting routines found in the quote.c file.
272 * Help to copy the thing properly quoted for the shell safety. any single
273 * quote is replaced with '\'', any exclamation point is replaced with '\!',
274 * and the whole thing is enclosed in a
277 * original sq_quote result
278 * name ==> name ==> 'name'
279 * a b ==> a b ==> 'a b'
280 * a'b ==> a'\''b ==> 'a'\''b'
281 * a!b ==> a'\!'b ==> 'a'\!'b'
285 sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
289 #define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0)
292 while ((c = *src++)) {
293 if (c == '\'' || c == '!') {
304 if (bufsize < SIZEOF_STR)
316 /* XXX: Keep the view request first and in sync with views[]. */ \
317 REQ_GROUP("View switching") \
318 REQ_(VIEW_MAIN, "Show main view"), \
319 REQ_(VIEW_DIFF, "Show diff view"), \
320 REQ_(VIEW_LOG, "Show log view"), \
321 REQ_(VIEW_TREE, "Show tree view"), \
322 REQ_(VIEW_BLOB, "Show blob view"), \
323 REQ_(VIEW_BLAME, "Show blame view"), \
324 REQ_(VIEW_HELP, "Show help page"), \
325 REQ_(VIEW_PAGER, "Show pager view"), \
326 REQ_(VIEW_STATUS, "Show status view"), \
327 REQ_(VIEW_STAGE, "Show stage view"), \
329 REQ_GROUP("View manipulation") \
330 REQ_(ENTER, "Enter current line and scroll"), \
331 REQ_(NEXT, "Move to next"), \
332 REQ_(PREVIOUS, "Move to previous"), \
333 REQ_(VIEW_NEXT, "Move focus to next view"), \
334 REQ_(REFRESH, "Reload and refresh"), \
335 REQ_(MAXIMIZE, "Maximize the current view"), \
336 REQ_(VIEW_CLOSE, "Close the current view"), \
337 REQ_(QUIT, "Close all views and quit"), \
339 REQ_GROUP("Cursor navigation") \
340 REQ_(MOVE_UP, "Move cursor one line up"), \
341 REQ_(MOVE_DOWN, "Move cursor one line down"), \
342 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
343 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
344 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
345 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
347 REQ_GROUP("Scrolling") \
348 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
349 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
350 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
351 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
353 REQ_GROUP("Searching") \
354 REQ_(SEARCH, "Search the view"), \
355 REQ_(SEARCH_BACK, "Search backwards in the view"), \
356 REQ_(FIND_NEXT, "Find next search match"), \
357 REQ_(FIND_PREV, "Find previous search match"), \
360 REQ_(PROMPT, "Bring up the prompt"), \
361 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
362 REQ_(SCREEN_RESIZE, "Resize the screen"), \
363 REQ_(SHOW_VERSION, "Show version information"), \
364 REQ_(STOP_LOADING, "Stop all loading views"), \
365 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
366 REQ_(TOGGLE_DATE, "Toggle date display"), \
367 REQ_(TOGGLE_AUTHOR, "Toggle author display"), \
368 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"), \
369 REQ_(TOGGLE_REFS, "Toggle reference display (tags/branches)"), \
370 REQ_(STATUS_UPDATE, "Update file status"), \
371 REQ_(STATUS_MERGE, "Merge file using external tool"), \
372 REQ_(TREE_PARENT, "Switch to parent directory in tree view"), \
373 REQ_(EDIT, "Open in editor"), \
374 REQ_(NONE, "Do nothing")
377 /* User action requests. */
379 #define REQ_GROUP(help)
380 #define REQ_(req, help) REQ_##req
382 /* Offset all requests to avoid conflicts with ncurses getch values. */
383 REQ_OFFSET = KEY_MAX + 1,
390 struct request_info {
391 enum request request;
397 static struct request_info req_info[] = {
398 #define REQ_GROUP(help) { 0, NULL, 0, (help) },
399 #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
406 get_request(const char *name)
408 int namelen = strlen(name);
411 for (i = 0; i < ARRAY_SIZE(req_info); i++)
412 if (req_info[i].namelen == namelen &&
413 !string_enum_compare(req_info[i].name, name, namelen))
414 return req_info[i].request;
424 static const char usage[] =
425 "tig " TIG_VERSION " (" __DATE__ ")\n"
427 "Usage: tig [options] [revs] [--] [paths]\n"
428 " or: tig show [options] [revs] [--] [paths]\n"
429 " or: tig blame [rev] path\n"
431 " or: tig < [git command output]\n"
434 " -v, --version Show version and exit\n"
435 " -h, --help Show help message and exit";
437 /* Option and state variables. */
438 static bool opt_date = TRUE;
439 static bool opt_author = TRUE;
440 static bool opt_line_number = FALSE;
441 static bool opt_rev_graph = FALSE;
442 static bool opt_show_refs = TRUE;
443 static int opt_num_interval = NUMBER_INTERVAL;
444 static int opt_tab_size = TABSIZE;
445 static enum request opt_request = REQ_VIEW_MAIN;
446 static char opt_cmd[SIZEOF_STR] = "";
447 static char opt_path[SIZEOF_STR] = "";
448 static char opt_file[SIZEOF_STR] = "";
449 static char opt_ref[SIZEOF_REF] = "";
450 static char opt_head[SIZEOF_REF] = "";
451 static char opt_remote[SIZEOF_REF] = "";
452 static bool opt_no_head = TRUE;
453 static FILE *opt_pipe = NULL;
454 static char opt_encoding[20] = "UTF-8";
455 static bool opt_utf8 = TRUE;
456 static char opt_codeset[20] = "UTF-8";
457 static iconv_t opt_iconv = ICONV_NONE;
458 static char opt_search[SIZEOF_STR] = "";
459 static char opt_cdup[SIZEOF_STR] = "";
460 static char opt_git_dir[SIZEOF_STR] = "";
461 static signed char opt_is_inside_work_tree = -1; /* set to TRUE or FALSE */
462 static char opt_editor[SIZEOF_STR] = "";
465 parse_options(int argc, char *argv[])
469 bool seen_dashdash = FALSE;
472 if (!isatty(STDIN_FILENO)) {
473 opt_request = REQ_VIEW_PAGER;
481 subcommand = argv[1];
482 if (!strcmp(subcommand, "status") || !strcmp(subcommand, "-S")) {
483 opt_request = REQ_VIEW_STATUS;
484 if (!strcmp(subcommand, "-S"))
485 warn("`-S' has been deprecated; use `tig status' instead");
487 warn("ignoring arguments after `%s'", subcommand);
490 } else if (!strcmp(subcommand, "blame")) {
491 opt_request = REQ_VIEW_BLAME;
492 if (argc <= 2 || argc > 4)
493 die("invalid number of options to blame\n\n%s", usage);
497 string_ncopy(opt_ref, argv[i], strlen(argv[i]));
501 string_ncopy(opt_file, argv[i], strlen(argv[i]));
504 } else if (!strcmp(subcommand, "show")) {
505 opt_request = REQ_VIEW_DIFF;
507 } else if (!strcmp(subcommand, "log") || !strcmp(subcommand, "diff")) {
508 opt_request = subcommand[0] == 'l'
509 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
510 warn("`tig %s' has been deprecated", subcommand);
517 /* XXX: This is vulnerable to the user overriding
518 * options required for the main view parser. */
519 string_copy(opt_cmd, "git log --no-color --pretty=raw --boundary --parents");
521 string_format(opt_cmd, "git %s", subcommand);
523 buf_size = strlen(opt_cmd);
525 for (i = 1 + !!subcommand; i < argc; i++) {
528 if (seen_dashdash || !strcmp(opt, "--")) {
529 seen_dashdash = TRUE;
531 } else if (!strcmp(opt, "-v") || !strcmp(opt, "--version")) {
532 printf("tig version %s\n", TIG_VERSION);
535 } else if (!strcmp(opt, "-h") || !strcmp(opt, "--help")) {
536 printf("%s\n", usage);
540 opt_cmd[buf_size++] = ' ';
541 buf_size = sq_quote(opt_cmd, buf_size, opt);
542 if (buf_size >= sizeof(opt_cmd))
543 die("command too long");
546 opt_cmd[buf_size] = 0;
553 * Line-oriented content detection.
557 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
558 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
559 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
560 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
561 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
562 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
563 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
564 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
565 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
566 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
567 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
568 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
569 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
570 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
571 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
572 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
573 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
574 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
575 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
576 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
577 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
578 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
579 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
580 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
581 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
582 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
583 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
584 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
585 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
586 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
587 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
588 LINE(DELIMITER, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
589 LINE(LINE_NUMBER, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
590 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
591 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
592 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
593 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
594 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
595 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
596 LINE(MAIN_LOCAL_TAG,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
597 LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \
598 LINE(MAIN_TRACKED, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
599 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
600 LINE(MAIN_HEAD, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
601 LINE(MAIN_REVGRAPH,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
602 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
603 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
604 LINE(STAT_HEAD, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \
605 LINE(STAT_SECTION, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
606 LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
607 LINE(STAT_STAGED, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
608 LINE(STAT_UNSTAGED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
609 LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
610 LINE(BLAME_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
611 LINE(BLAME_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
612 LINE(BLAME_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
613 LINE(BLAME_ID, "", COLOR_MAGENTA, COLOR_DEFAULT, 0)
616 #define LINE(type, line, fg, bg, attr) \
623 const char *name; /* Option name. */
624 int namelen; /* Size of option name. */
625 const char *line; /* The start of line to match. */
626 int linelen; /* Size of string to match. */
627 int fg, bg, attr; /* Color and text attributes for the lines. */
630 static struct line_info line_info[] = {
631 #define LINE(type, line, fg, bg, attr) \
632 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
637 static enum line_type
638 get_line_type(char *line)
640 int linelen = strlen(line);
643 for (type = 0; type < ARRAY_SIZE(line_info); type++)
644 /* Case insensitive search matches Signed-off-by lines better. */
645 if (linelen >= line_info[type].linelen &&
646 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
653 get_line_attr(enum line_type type)
655 assert(type < ARRAY_SIZE(line_info));
656 return COLOR_PAIR(type) | line_info[type].attr;
659 static struct line_info *
660 get_line_info(char *name)
662 size_t namelen = strlen(name);
665 for (type = 0; type < ARRAY_SIZE(line_info); type++)
666 if (namelen == line_info[type].namelen &&
667 !string_enum_compare(line_info[type].name, name, namelen))
668 return &line_info[type];
676 int default_bg = line_info[LINE_DEFAULT].bg;
677 int default_fg = line_info[LINE_DEFAULT].fg;
682 if (assume_default_colors(default_fg, default_bg) == ERR) {
683 default_bg = COLOR_BLACK;
684 default_fg = COLOR_WHITE;
687 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
688 struct line_info *info = &line_info[type];
689 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
690 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
692 init_pair(type, fg, bg);
700 unsigned int selected:1;
701 unsigned int dirty:1;
703 void *data; /* User data */
713 enum request request;
714 struct keybinding *next;
717 static struct keybinding default_keybindings[] = {
719 { 'm', REQ_VIEW_MAIN },
720 { 'd', REQ_VIEW_DIFF },
721 { 'l', REQ_VIEW_LOG },
722 { 't', REQ_VIEW_TREE },
723 { 'f', REQ_VIEW_BLOB },
724 { 'B', REQ_VIEW_BLAME },
725 { 'p', REQ_VIEW_PAGER },
726 { 'h', REQ_VIEW_HELP },
727 { 'S', REQ_VIEW_STATUS },
728 { 'c', REQ_VIEW_STAGE },
730 /* View manipulation */
731 { 'q', REQ_VIEW_CLOSE },
732 { KEY_TAB, REQ_VIEW_NEXT },
733 { KEY_RETURN, REQ_ENTER },
734 { KEY_UP, REQ_PREVIOUS },
735 { KEY_DOWN, REQ_NEXT },
736 { 'R', REQ_REFRESH },
737 { 'M', REQ_MAXIMIZE },
739 /* Cursor navigation */
740 { 'k', REQ_MOVE_UP },
741 { 'j', REQ_MOVE_DOWN },
742 { KEY_HOME, REQ_MOVE_FIRST_LINE },
743 { KEY_END, REQ_MOVE_LAST_LINE },
744 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
745 { ' ', REQ_MOVE_PAGE_DOWN },
746 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
747 { 'b', REQ_MOVE_PAGE_UP },
748 { '-', REQ_MOVE_PAGE_UP },
751 { KEY_IC, REQ_SCROLL_LINE_UP },
752 { KEY_DC, REQ_SCROLL_LINE_DOWN },
753 { 'w', REQ_SCROLL_PAGE_UP },
754 { 's', REQ_SCROLL_PAGE_DOWN },
758 { '?', REQ_SEARCH_BACK },
759 { 'n', REQ_FIND_NEXT },
760 { 'N', REQ_FIND_PREV },
764 { 'z', REQ_STOP_LOADING },
765 { 'v', REQ_SHOW_VERSION },
766 { 'r', REQ_SCREEN_REDRAW },
767 { '.', REQ_TOGGLE_LINENO },
768 { 'D', REQ_TOGGLE_DATE },
769 { 'A', REQ_TOGGLE_AUTHOR },
770 { 'g', REQ_TOGGLE_REV_GRAPH },
771 { 'F', REQ_TOGGLE_REFS },
773 { 'u', REQ_STATUS_UPDATE },
774 { 'M', REQ_STATUS_MERGE },
775 { ',', REQ_TREE_PARENT },
778 /* Using the ncurses SIGWINCH handler. */
779 { KEY_RESIZE, REQ_SCREEN_RESIZE },
782 #define KEYMAP_INFO \
796 #define KEYMAP_(name) KEYMAP_##name
801 static struct int_map keymap_table[] = {
802 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
807 #define set_keymap(map, name) \
808 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
810 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
813 add_keybinding(enum keymap keymap, enum request request, int key)
815 struct keybinding *keybinding;
817 keybinding = calloc(1, sizeof(*keybinding));
819 die("Failed to allocate keybinding");
821 keybinding->alias = key;
822 keybinding->request = request;
823 keybinding->next = keybindings[keymap];
824 keybindings[keymap] = keybinding;
827 /* Looks for a key binding first in the given map, then in the generic map, and
828 * lastly in the default keybindings. */
830 get_keybinding(enum keymap keymap, int key)
832 struct keybinding *kbd;
835 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
836 if (kbd->alias == key)
839 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
840 if (kbd->alias == key)
843 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
844 if (default_keybindings[i].alias == key)
845 return default_keybindings[i].request;
847 return (enum request) key;
856 static struct key key_table[] = {
857 { "Enter", KEY_RETURN },
859 { "Backspace", KEY_BACKSPACE },
861 { "Escape", KEY_ESC },
862 { "Left", KEY_LEFT },
863 { "Right", KEY_RIGHT },
865 { "Down", KEY_DOWN },
866 { "Insert", KEY_IC },
867 { "Delete", KEY_DC },
869 { "Home", KEY_HOME },
871 { "PageUp", KEY_PPAGE },
872 { "PageDown", KEY_NPAGE },
882 { "F10", KEY_F(10) },
883 { "F11", KEY_F(11) },
884 { "F12", KEY_F(12) },
888 get_key_value(const char *name)
892 for (i = 0; i < ARRAY_SIZE(key_table); i++)
893 if (!strcasecmp(key_table[i].name, name))
894 return key_table[i].value;
896 if (strlen(name) == 1 && isprint(*name))
903 get_key_name(int key_value)
905 static char key_char[] = "'X'";
909 for (key = 0; key < ARRAY_SIZE(key_table); key++)
910 if (key_table[key].value == key_value)
911 seq = key_table[key].name;
915 isprint(key_value)) {
916 key_char[1] = (char) key_value;
920 return seq ? seq : "'?'";
924 get_key(enum request request)
926 static char buf[BUFSIZ];
933 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
934 struct keybinding *keybinding = &default_keybindings[i];
936 if (keybinding->request != request)
939 if (!string_format_from(buf, &pos, "%s%s", sep,
940 get_key_name(keybinding->alias)))
941 return "Too many keybindings!";
951 char cmd[SIZEOF_STR];
954 static struct run_request *run_request;
955 static size_t run_requests;
958 add_run_request(enum keymap keymap, int key, int argc, char **argv)
960 struct run_request *tmp;
961 struct run_request req = { keymap, key };
964 for (bufpos = 0; argc > 0; argc--, argv++)
965 if (!string_format_from(req.cmd, &bufpos, "%s ", *argv))
968 req.cmd[bufpos - 1] = 0;
970 tmp = realloc(run_request, (run_requests + 1) * sizeof(*run_request));
975 run_request[run_requests++] = req;
977 return REQ_NONE + run_requests;
980 static struct run_request *
981 get_run_request(enum request request)
983 if (request <= REQ_NONE)
985 return &run_request[request - REQ_NONE - 1];
989 add_builtin_run_requests(void)
996 { KEYMAP_MAIN, 'C', { "git cherry-pick %(commit)" } },
997 { KEYMAP_GENERIC, 'G', { "git gc" } },
1001 for (i = 0; i < ARRAY_SIZE(reqs); i++) {
1004 req = add_run_request(reqs[i].keymap, reqs[i].key, 1, reqs[i].argv);
1005 if (req != REQ_NONE)
1006 add_keybinding(reqs[i].keymap, req, reqs[i].key);
1011 * User config file handling.
1014 static struct int_map color_map[] = {
1015 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
1027 #define set_color(color, name) \
1028 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
1030 static struct int_map attr_map[] = {
1031 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
1038 ATTR_MAP(UNDERLINE),
1041 #define set_attribute(attr, name) \
1042 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
1044 static int config_lineno;
1045 static bool config_errors;
1046 static char *config_msg;
1048 /* Wants: object fgcolor bgcolor [attr] */
1050 option_color_command(int argc, char *argv[])
1052 struct line_info *info;
1054 if (argc != 3 && argc != 4) {
1055 config_msg = "Wrong number of arguments given to color command";
1059 info = get_line_info(argv[0]);
1061 if (!string_enum_compare(argv[0], "main-delim", strlen("main-delim"))) {
1062 info = get_line_info("delimiter");
1065 config_msg = "Unknown color name";
1070 if (set_color(&info->fg, argv[1]) == ERR ||
1071 set_color(&info->bg, argv[2]) == ERR) {
1072 config_msg = "Unknown color";
1076 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
1077 config_msg = "Unknown attribute";
1084 static bool parse_bool(const char *s)
1086 return (!strcmp(s, "1") || !strcmp(s, "true") ||
1087 !strcmp(s, "yes")) ? TRUE : FALSE;
1090 /* Wants: name = value */
1092 option_set_command(int argc, char *argv[])
1095 config_msg = "Wrong number of arguments given to set command";
1099 if (strcmp(argv[1], "=")) {
1100 config_msg = "No value assigned";
1104 if (!strcmp(argv[0], "show-author")) {
1105 opt_author = parse_bool(argv[2]);
1109 if (!strcmp(argv[0], "show-date")) {
1110 opt_date = parse_bool(argv[2]);
1114 if (!strcmp(argv[0], "show-rev-graph")) {
1115 opt_rev_graph = parse_bool(argv[2]);
1119 if (!strcmp(argv[0], "show-refs")) {
1120 opt_show_refs = parse_bool(argv[2]);
1124 if (!strcmp(argv[0], "show-line-numbers")) {
1125 opt_line_number = parse_bool(argv[2]);
1129 if (!strcmp(argv[0], "line-number-interval")) {
1130 opt_num_interval = atoi(argv[2]);
1134 if (!strcmp(argv[0], "tab-size")) {
1135 opt_tab_size = atoi(argv[2]);
1139 if (!strcmp(argv[0], "commit-encoding")) {
1140 char *arg = argv[2];
1141 int delimiter = *arg;
1144 switch (delimiter) {
1147 for (arg++, i = 0; arg[i]; i++)
1148 if (arg[i] == delimiter) {
1153 string_ncopy(opt_encoding, arg, strlen(arg));
1158 config_msg = "Unknown variable name";
1162 /* Wants: mode request key */
1164 option_bind_command(int argc, char *argv[])
1166 enum request request;
1171 config_msg = "Wrong number of arguments given to bind command";
1175 if (set_keymap(&keymap, argv[0]) == ERR) {
1176 config_msg = "Unknown key map";
1180 key = get_key_value(argv[1]);
1182 config_msg = "Unknown key";
1186 request = get_request(argv[2]);
1187 if (request == REQ_NONE) {
1188 const char *obsolete[] = { "cherry-pick" };
1189 size_t namelen = strlen(argv[2]);
1192 for (i = 0; i < ARRAY_SIZE(obsolete); i++) {
1193 if (namelen == strlen(obsolete[i]) &&
1194 !string_enum_compare(obsolete[i], argv[2], namelen)) {
1195 config_msg = "Obsolete request name";
1200 if (request == REQ_NONE && *argv[2]++ == '!')
1201 request = add_run_request(keymap, key, argc - 2, argv + 2);
1202 if (request == REQ_NONE) {
1203 config_msg = "Unknown request name";
1207 add_keybinding(keymap, request, key);
1213 set_option(char *opt, char *value)
1220 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1221 argv[argc++] = value;
1224 /* Nothing more to tokenize or last available token. */
1225 if (!*value || argc >= ARRAY_SIZE(argv))
1229 while (isspace(*value))
1233 if (!strcmp(opt, "color"))
1234 return option_color_command(argc, argv);
1236 if (!strcmp(opt, "set"))
1237 return option_set_command(argc, argv);
1239 if (!strcmp(opt, "bind"))
1240 return option_bind_command(argc, argv);
1242 config_msg = "Unknown option command";
1247 read_option(char *opt, size_t optlen, char *value, size_t valuelen)
1252 config_msg = "Internal error";
1254 /* Check for comment markers, since read_properties() will
1255 * only ensure opt and value are split at first " \t". */
1256 optlen = strcspn(opt, "#");
1260 if (opt[optlen] != 0) {
1261 config_msg = "No option value";
1265 /* Look for comment endings in the value. */
1266 size_t len = strcspn(value, "#");
1268 if (len < valuelen) {
1270 value[valuelen] = 0;
1273 status = set_option(opt, value);
1276 if (status == ERR) {
1277 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1278 config_lineno, (int) optlen, opt, config_msg);
1279 config_errors = TRUE;
1282 /* Always keep going if errors are encountered. */
1287 load_option_file(const char *path)
1291 /* It's ok that the file doesn't exist. */
1292 file = fopen(path, "r");
1297 config_errors = FALSE;
1299 if (read_properties(file, " \t", read_option) == ERR ||
1300 config_errors == TRUE)
1301 fprintf(stderr, "Errors while loading %s.\n", path);
1307 char *home = getenv("HOME");
1308 char *tigrc_user = getenv("TIGRC_USER");
1309 char *tigrc_system = getenv("TIGRC_SYSTEM");
1310 char buf[SIZEOF_STR];
1312 add_builtin_run_requests();
1314 if (!tigrc_system) {
1315 if (!string_format(buf, "%s/tigrc", SYSCONFDIR))
1319 load_option_file(tigrc_system);
1322 if (!home || !string_format(buf, "%s/.tigrc", home))
1326 load_option_file(tigrc_user);
1339 /* The display array of active views and the index of the current view. */
1340 static struct view *display[2];
1341 static unsigned int current_view;
1343 /* Reading from the prompt? */
1344 static bool input_mode = FALSE;
1346 #define foreach_displayed_view(view, i) \
1347 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1349 #define displayed_views() (display[1] != NULL ? 2 : 1)
1351 /* Current head and commit ID */
1352 static char ref_blob[SIZEOF_REF] = "";
1353 static char ref_commit[SIZEOF_REF] = "HEAD";
1354 static char ref_head[SIZEOF_REF] = "HEAD";
1357 const char *name; /* View name */
1358 const char *cmd_fmt; /* Default command line format */
1359 const char *cmd_env; /* Command line set via environment */
1360 const char *id; /* Points to either of ref_{head,commit,blob} */
1362 struct view_ops *ops; /* View operations */
1364 enum keymap keymap; /* What keymap does this view have */
1365 bool git_dir; /* Whether the view requires a git directory. */
1367 char cmd[SIZEOF_STR]; /* Command buffer */
1368 char ref[SIZEOF_REF]; /* Hovered commit reference */
1369 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1371 int height, width; /* The width and height of the main window */
1372 WINDOW *win; /* The main window */
1373 WINDOW *title; /* The title window living below the main window */
1376 unsigned long offset; /* Offset of the window top */
1377 unsigned long lineno; /* Current line number */
1380 char grep[SIZEOF_STR]; /* Search string */
1381 regex_t *regex; /* Pre-compiled regex */
1383 /* If non-NULL, points to the view that opened this view. If this view
1384 * is closed tig will switch back to the parent view. */
1385 struct view *parent;
1388 size_t lines; /* Total number of lines */
1389 struct line *line; /* Line index */
1390 size_t line_alloc; /* Total number of allocated lines */
1391 size_t line_size; /* Total number of used lines */
1392 unsigned int digits; /* Number of digits in the lines member. */
1400 /* What type of content being displayed. Used in the title bar. */
1402 /* Open and reads in all view content. */
1403 bool (*open)(struct view *view);
1404 /* Read one line; updates view->line. */
1405 bool (*read)(struct view *view, char *data);
1406 /* Draw one line; @lineno must be < view->height. */
1407 bool (*draw)(struct view *view, struct line *line, unsigned int lineno, bool selected);
1408 /* Depending on view handle a special requests. */
1409 enum request (*request)(struct view *view, enum request request, struct line *line);
1410 /* Search for regex in a line. */
1411 bool (*grep)(struct view *view, struct line *line);
1413 void (*select)(struct view *view, struct line *line);
1416 static struct view_ops pager_ops;
1417 static struct view_ops main_ops;
1418 static struct view_ops tree_ops;
1419 static struct view_ops blob_ops;
1420 static struct view_ops blame_ops;
1421 static struct view_ops help_ops;
1422 static struct view_ops status_ops;
1423 static struct view_ops stage_ops;
1425 #define VIEW_STR(name, cmd, env, ref, ops, map, git) \
1426 { name, cmd, #env, ref, ops, map, git }
1428 #define VIEW_(id, name, ops, git, ref) \
1429 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id, git)
1432 static struct view views[] = {
1433 VIEW_(MAIN, "main", &main_ops, TRUE, ref_head),
1434 VIEW_(DIFF, "diff", &pager_ops, TRUE, ref_commit),
1435 VIEW_(LOG, "log", &pager_ops, TRUE, ref_head),
1436 VIEW_(TREE, "tree", &tree_ops, TRUE, ref_commit),
1437 VIEW_(BLOB, "blob", &blob_ops, TRUE, ref_blob),
1438 VIEW_(BLAME, "blame", &blame_ops, TRUE, ref_commit),
1439 VIEW_(HELP, "help", &help_ops, FALSE, ""),
1440 VIEW_(PAGER, "pager", &pager_ops, FALSE, "stdin"),
1441 VIEW_(STATUS, "status", &status_ops, TRUE, ""),
1442 VIEW_(STAGE, "stage", &stage_ops, TRUE, ""),
1445 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1446 #define VIEW_REQ(view) ((view) - views + REQ_OFFSET + 1)
1448 #define foreach_view(view, i) \
1449 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1451 #define view_is_displayed(view) \
1452 (view == display[0] || view == display[1])
1455 draw_text(struct view *view, const char *string, int max_len,
1456 bool use_tilde, bool selected)
1459 int trimmed = FALSE;
1465 len = utf8_length(string, max_len, &trimmed, use_tilde);
1467 len = strlen(string);
1468 if (len > max_len) {
1477 waddnstr(view->win, string, len);
1478 if (trimmed && use_tilde) {
1480 wattrset(view->win, get_line_attr(LINE_DELIMITER));
1481 waddch(view->win, '~');
1489 draw_lineno(struct view *view, unsigned int lineno, int max, bool selected)
1491 static char fmt[] = "%1ld";
1492 char number[10] = " ";
1493 int max_number = MIN(view->digits, STRING_SIZE(number));
1494 bool showtrimmed = FALSE;
1497 lineno += view->offset + 1;
1498 if (lineno == 1 || (lineno % opt_num_interval) == 0) {
1499 if (view->digits <= 9)
1500 fmt[1] = '0' + view->digits;
1502 if (!string_format(number, fmt, lineno))
1507 if (max < max_number)
1511 wattrset(view->win, get_line_attr(LINE_LINE_NUMBER));
1512 col = draw_text(view, number, max_number, showtrimmed, selected);
1515 wattrset(view->win, A_NORMAL);
1516 waddch(view->win, ACS_VLINE);
1520 waddch(view->win, ' ');
1528 draw_view_line(struct view *view, unsigned int lineno)
1531 bool selected = (view->offset + lineno == view->lineno);
1534 assert(view_is_displayed(view));
1536 if (view->offset + lineno >= view->lines)
1539 line = &view->line[view->offset + lineno];
1542 line->selected = TRUE;
1543 view->ops->select(view, line);
1544 } else if (line->selected) {
1545 line->selected = FALSE;
1546 wmove(view->win, lineno, 0);
1547 wclrtoeol(view->win);
1550 scrollok(view->win, FALSE);
1551 draw_ok = view->ops->draw(view, line, lineno, selected);
1552 scrollok(view->win, TRUE);
1558 redraw_view_dirty(struct view *view)
1563 for (lineno = 0; lineno < view->height; lineno++) {
1564 struct line *line = &view->line[view->offset + lineno];
1570 if (!draw_view_line(view, lineno))
1576 redrawwin(view->win);
1578 wnoutrefresh(view->win);
1580 wrefresh(view->win);
1584 redraw_view_from(struct view *view, int lineno)
1586 assert(0 <= lineno && lineno < view->height);
1588 for (; lineno < view->height; lineno++) {
1589 if (!draw_view_line(view, lineno))
1593 redrawwin(view->win);
1595 wnoutrefresh(view->win);
1597 wrefresh(view->win);
1601 redraw_view(struct view *view)
1604 redraw_view_from(view, 0);
1609 update_view_title(struct view *view)
1611 char buf[SIZEOF_STR];
1612 char state[SIZEOF_STR];
1613 size_t bufpos = 0, statelen = 0;
1615 assert(view_is_displayed(view));
1617 if (view != VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) {
1618 unsigned int view_lines = view->offset + view->height;
1619 unsigned int lines = view->lines
1620 ? MIN(view_lines, view->lines) * 100 / view->lines
1623 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1630 time_t secs = time(NULL) - view->start_time;
1632 /* Three git seconds are a long time ... */
1634 string_format_from(state, &statelen, " %lds", secs);
1638 string_format_from(buf, &bufpos, "[%s]", view->name);
1639 if (*view->ref && bufpos < view->width) {
1640 size_t refsize = strlen(view->ref);
1641 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1643 if (minsize < view->width)
1644 refsize = view->width - minsize + 7;
1645 string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
1648 if (statelen && bufpos < view->width) {
1649 string_format_from(buf, &bufpos, " %s", state);
1652 if (view == display[current_view])
1653 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1655 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1657 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1658 wclrtoeol(view->title);
1659 wmove(view->title, 0, view->width - 1);
1662 wnoutrefresh(view->title);
1664 wrefresh(view->title);
1668 resize_display(void)
1671 struct view *base = display[0];
1672 struct view *view = display[1] ? display[1] : display[0];
1674 /* Setup window dimensions */
1676 getmaxyx(stdscr, base->height, base->width);
1678 /* Make room for the status window. */
1682 /* Horizontal split. */
1683 view->width = base->width;
1684 view->height = SCALE_SPLIT_VIEW(base->height);
1685 base->height -= view->height;
1687 /* Make room for the title bar. */
1691 /* Make room for the title bar. */
1696 foreach_displayed_view (view, i) {
1698 view->win = newwin(view->height, 0, offset, 0);
1700 die("Failed to create %s view", view->name);
1702 scrollok(view->win, TRUE);
1704 view->title = newwin(1, 0, offset + view->height, 0);
1706 die("Failed to create title window");
1709 wresize(view->win, view->height, view->width);
1710 mvwin(view->win, offset, 0);
1711 mvwin(view->title, offset + view->height, 0);
1714 offset += view->height + 1;
1719 redraw_display(void)
1724 foreach_displayed_view (view, i) {
1726 update_view_title(view);
1731 update_display_cursor(struct view *view)
1733 /* Move the cursor to the right-most column of the cursor line.
1735 * XXX: This could turn out to be a bit expensive, but it ensures that
1736 * the cursor does not jump around. */
1738 wmove(view->win, view->lineno - view->offset, view->width - 1);
1739 wrefresh(view->win);
1747 /* Scrolling backend */
1749 do_scroll_view(struct view *view, int lines)
1751 bool redraw_current_line = FALSE;
1753 /* The rendering expects the new offset. */
1754 view->offset += lines;
1756 assert(0 <= view->offset && view->offset < view->lines);
1759 /* Move current line into the view. */
1760 if (view->lineno < view->offset) {
1761 view->lineno = view->offset;
1762 redraw_current_line = TRUE;
1763 } else if (view->lineno >= view->offset + view->height) {
1764 view->lineno = view->offset + view->height - 1;
1765 redraw_current_line = TRUE;
1768 assert(view->offset <= view->lineno && view->lineno < view->lines);
1770 /* Redraw the whole screen if scrolling is pointless. */
1771 if (view->height < ABS(lines)) {
1775 int line = lines > 0 ? view->height - lines : 0;
1776 int end = line + ABS(lines);
1778 wscrl(view->win, lines);
1780 for (; line < end; line++) {
1781 if (!draw_view_line(view, line))
1785 if (redraw_current_line)
1786 draw_view_line(view, view->lineno - view->offset);
1789 redrawwin(view->win);
1790 wrefresh(view->win);
1794 /* Scroll frontend */
1796 scroll_view(struct view *view, enum request request)
1800 assert(view_is_displayed(view));
1803 case REQ_SCROLL_PAGE_DOWN:
1804 lines = view->height;
1805 case REQ_SCROLL_LINE_DOWN:
1806 if (view->offset + lines > view->lines)
1807 lines = view->lines - view->offset;
1809 if (lines == 0 || view->offset + view->height >= view->lines) {
1810 report("Cannot scroll beyond the last line");
1815 case REQ_SCROLL_PAGE_UP:
1816 lines = view->height;
1817 case REQ_SCROLL_LINE_UP:
1818 if (lines > view->offset)
1819 lines = view->offset;
1822 report("Cannot scroll beyond the first line");
1830 die("request %d not handled in switch", request);
1833 do_scroll_view(view, lines);
1838 move_view(struct view *view, enum request request)
1840 int scroll_steps = 0;
1844 case REQ_MOVE_FIRST_LINE:
1845 steps = -view->lineno;
1848 case REQ_MOVE_LAST_LINE:
1849 steps = view->lines - view->lineno - 1;
1852 case REQ_MOVE_PAGE_UP:
1853 steps = view->height > view->lineno
1854 ? -view->lineno : -view->height;
1857 case REQ_MOVE_PAGE_DOWN:
1858 steps = view->lineno + view->height >= view->lines
1859 ? view->lines - view->lineno - 1 : view->height;
1871 die("request %d not handled in switch", request);
1874 if (steps <= 0 && view->lineno == 0) {
1875 report("Cannot move beyond the first line");
1878 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1879 report("Cannot move beyond the last line");
1883 /* Move the current line */
1884 view->lineno += steps;
1885 assert(0 <= view->lineno && view->lineno < view->lines);
1887 /* Check whether the view needs to be scrolled */
1888 if (view->lineno < view->offset ||
1889 view->lineno >= view->offset + view->height) {
1890 scroll_steps = steps;
1891 if (steps < 0 && -steps > view->offset) {
1892 scroll_steps = -view->offset;
1894 } else if (steps > 0) {
1895 if (view->lineno == view->lines - 1 &&
1896 view->lines > view->height) {
1897 scroll_steps = view->lines - view->offset - 1;
1898 if (scroll_steps >= view->height)
1899 scroll_steps -= view->height - 1;
1904 if (!view_is_displayed(view)) {
1905 view->offset += scroll_steps;
1906 assert(0 <= view->offset && view->offset < view->lines);
1907 view->ops->select(view, &view->line[view->lineno]);
1911 /* Repaint the old "current" line if we be scrolling */
1912 if (ABS(steps) < view->height)
1913 draw_view_line(view, view->lineno - steps - view->offset);
1916 do_scroll_view(view, scroll_steps);
1920 /* Draw the current line */
1921 draw_view_line(view, view->lineno - view->offset);
1923 redrawwin(view->win);
1924 wrefresh(view->win);
1933 static void search_view(struct view *view, enum request request);
1936 find_next_line(struct view *view, unsigned long lineno, struct line *line)
1938 assert(view_is_displayed(view));
1940 if (!view->ops->grep(view, line))
1943 if (lineno - view->offset >= view->height) {
1944 view->offset = lineno;
1945 view->lineno = lineno;
1949 unsigned long old_lineno = view->lineno - view->offset;
1951 view->lineno = lineno;
1952 draw_view_line(view, old_lineno);
1954 draw_view_line(view, view->lineno - view->offset);
1955 redrawwin(view->win);
1956 wrefresh(view->win);
1959 report("Line %ld matches '%s'", lineno + 1, view->grep);
1964 find_next(struct view *view, enum request request)
1966 unsigned long lineno = view->lineno;
1971 report("No previous search");
1973 search_view(view, request);
1983 case REQ_SEARCH_BACK:
1992 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
1993 lineno += direction;
1995 /* Note, lineno is unsigned long so will wrap around in which case it
1996 * will become bigger than view->lines. */
1997 for (; lineno < view->lines; lineno += direction) {
1998 struct line *line = &view->line[lineno];
2000 if (find_next_line(view, lineno, line))
2004 report("No match found for '%s'", view->grep);
2008 search_view(struct view *view, enum request request)
2013 regfree(view->regex);
2016 view->regex = calloc(1, sizeof(*view->regex));
2021 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
2022 if (regex_err != 0) {
2023 char buf[SIZEOF_STR] = "unknown error";
2025 regerror(regex_err, view->regex, buf, sizeof(buf));
2026 report("Search failed: %s", buf);
2030 string_copy(view->grep, opt_search);
2032 find_next(view, request);
2036 * Incremental updating
2040 end_update(struct view *view)
2044 set_nonblocking_input(FALSE);
2045 if (view->pipe == stdin)
2053 begin_update(struct view *view)
2059 string_copy(view->cmd, opt_cmd);
2061 /* When running random commands, initially show the
2062 * command in the title. However, it maybe later be
2063 * overwritten if a commit line is selected. */
2064 if (view == VIEW(REQ_VIEW_PAGER))
2065 string_copy(view->ref, view->cmd);
2069 } else if (view == VIEW(REQ_VIEW_TREE)) {
2070 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2071 char path[SIZEOF_STR];
2073 if (strcmp(view->vid, view->id))
2074 opt_path[0] = path[0] = 0;
2075 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
2078 if (!string_format(view->cmd, format, view->id, path))
2082 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2083 const char *id = view->id;
2085 if (!string_format(view->cmd, format, id, id, id, id, id))
2088 /* Put the current ref_* value to the view title ref
2089 * member. This is needed by the blob view. Most other
2090 * views sets it automatically after loading because the
2091 * first line is a commit line. */
2092 string_copy_rev(view->ref, view->id);
2095 /* Special case for the pager view. */
2097 view->pipe = opt_pipe;
2100 view->pipe = popen(view->cmd, "r");
2106 set_nonblocking_input(TRUE);
2111 string_copy_rev(view->vid, view->id);
2116 for (i = 0; i < view->lines; i++)
2117 if (view->line[i].data)
2118 free(view->line[i].data);
2124 view->start_time = time(NULL);
2129 #define ITEM_CHUNK_SIZE 256
2131 realloc_items(void *mem, size_t *size, size_t new_size, size_t item_size)
2133 size_t num_chunks = *size / ITEM_CHUNK_SIZE;
2134 size_t num_chunks_new = (new_size + ITEM_CHUNK_SIZE - 1) / ITEM_CHUNK_SIZE;
2136 if (mem == NULL || num_chunks != num_chunks_new) {
2137 *size = num_chunks_new * ITEM_CHUNK_SIZE;
2138 mem = realloc(mem, *size * item_size);
2144 static struct line *
2145 realloc_lines(struct view *view, size_t line_size)
2147 size_t alloc = view->line_alloc;
2148 struct line *tmp = realloc_items(view->line, &alloc, line_size,
2149 sizeof(*view->line));
2155 view->line_alloc = alloc;
2156 view->line_size = line_size;
2161 update_view(struct view *view)
2163 char in_buffer[BUFSIZ];
2164 char out_buffer[BUFSIZ * 2];
2166 /* The number of lines to read. If too low it will cause too much
2167 * redrawing (and possible flickering), if too high responsiveness
2169 unsigned long lines = view->height;
2170 int redraw_from = -1;
2175 /* Only redraw if lines are visible. */
2176 if (view->offset + view->height >= view->lines)
2177 redraw_from = view->lines - view->offset;
2179 /* FIXME: This is probably not perfect for backgrounded views. */
2180 if (!realloc_lines(view, view->lines + lines))
2183 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
2184 size_t linelen = strlen(line);
2187 line[linelen - 1] = 0;
2189 if (opt_iconv != ICONV_NONE) {
2190 ICONV_CONST char *inbuf = line;
2191 size_t inlen = linelen;
2193 char *outbuf = out_buffer;
2194 size_t outlen = sizeof(out_buffer);
2198 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
2199 if (ret != (size_t) -1) {
2201 linelen = strlen(out_buffer);
2205 if (!view->ops->read(view, line))
2215 lines = view->lines;
2216 for (digits = 0; lines; digits++)
2219 /* Keep the displayed view in sync with line number scaling. */
2220 if (digits != view->digits) {
2221 view->digits = digits;
2226 if (!view_is_displayed(view))
2229 if (view == VIEW(REQ_VIEW_TREE)) {
2230 /* Clear the view and redraw everything since the tree sorting
2231 * might have rearranged things. */
2234 } else if (redraw_from >= 0) {
2235 /* If this is an incremental update, redraw the previous line
2236 * since for commits some members could have changed when
2237 * loading the main view. */
2238 if (redraw_from > 0)
2241 /* Since revision graph visualization requires knowledge
2242 * about the parent commit, it causes a further one-off
2243 * needed to be redrawn for incremental updates. */
2244 if (redraw_from > 0 && opt_rev_graph)
2247 /* Incrementally draw avoids flickering. */
2248 redraw_view_from(view, redraw_from);
2251 if (view == VIEW(REQ_VIEW_BLAME))
2252 redraw_view_dirty(view);
2254 /* Update the title _after_ the redraw so that if the redraw picks up a
2255 * commit reference in view->ref it'll be available here. */
2256 update_view_title(view);
2259 if (ferror(view->pipe)) {
2260 report("Failed to read: %s", strerror(errno));
2263 } else if (feof(view->pipe)) {
2271 report("Allocation failure");
2274 if (view->ops->read(view, NULL))
2279 static struct line *
2280 add_line_data(struct view *view, void *data, enum line_type type)
2282 struct line *line = &view->line[view->lines++];
2284 memset(line, 0, sizeof(*line));
2291 static struct line *
2292 add_line_text(struct view *view, char *data, enum line_type type)
2295 data = strdup(data);
2297 return data ? add_line_data(view, data, type) : NULL;
2306 OPEN_DEFAULT = 0, /* Use default view switching. */
2307 OPEN_SPLIT = 1, /* Split current view. */
2308 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2309 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2310 OPEN_NOMAXIMIZE = 8, /* Do not maximize the current view. */
2314 open_view(struct view *prev, enum request request, enum open_flags flags)
2316 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2317 bool split = !!(flags & OPEN_SPLIT);
2318 bool reload = !!(flags & OPEN_RELOAD);
2319 bool nomaximize = !!(flags & OPEN_NOMAXIMIZE);
2320 struct view *view = VIEW(request);
2321 int nviews = displayed_views();
2322 struct view *base_view = display[0];
2324 if (view == prev && nviews == 1 && !reload) {
2325 report("Already in %s view", view->name);
2329 if (view->git_dir && !opt_git_dir[0]) {
2330 report("The %s view is disabled in pager view", view->name);
2338 } else if (!nomaximize) {
2339 /* Maximize the current view. */
2340 memset(display, 0, sizeof(display));
2342 display[current_view] = view;
2345 /* Resize the view when switching between split- and full-screen,
2346 * or when switching between two different full-screen views. */
2347 if (nviews != displayed_views() ||
2348 (nviews == 1 && base_view != display[0]))
2351 if (view->ops->open) {
2352 if (!view->ops->open(view)) {
2353 report("Failed to load %s view", view->name);
2357 } else if ((reload || strcmp(view->vid, view->id)) &&
2358 !begin_update(view)) {
2359 report("Failed to load %s view", view->name);
2363 if (split && prev->lineno - prev->offset >= prev->height) {
2364 /* Take the title line into account. */
2365 int lines = prev->lineno - prev->offset - prev->height + 1;
2367 /* Scroll the view that was split if the current line is
2368 * outside the new limited view. */
2369 do_scroll_view(prev, lines);
2372 if (prev && view != prev) {
2373 if (split && !backgrounded) {
2374 /* "Blur" the previous view. */
2375 update_view_title(prev);
2378 view->parent = prev;
2381 if (view->pipe && view->lines == 0) {
2382 /* Clear the old view and let the incremental updating refill
2391 /* If the view is backgrounded the above calls to report()
2392 * won't redraw the view title. */
2394 update_view_title(view);
2398 open_external_viewer(const char *cmd)
2400 def_prog_mode(); /* save current tty modes */
2401 endwin(); /* restore original tty modes */
2403 fprintf(stderr, "Press Enter to continue");
2410 open_mergetool(const char *file)
2412 char cmd[SIZEOF_STR];
2413 char file_sq[SIZEOF_STR];
2415 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2416 string_format(cmd, "git mergetool %s", file_sq)) {
2417 open_external_viewer(cmd);
2422 open_editor(bool from_root, const char *file)
2424 char cmd[SIZEOF_STR];
2425 char file_sq[SIZEOF_STR];
2427 char *prefix = from_root ? opt_cdup : "";
2429 editor = getenv("GIT_EDITOR");
2430 if (!editor && *opt_editor)
2431 editor = opt_editor;
2433 editor = getenv("VISUAL");
2435 editor = getenv("EDITOR");
2439 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2440 string_format(cmd, "%s %s%s", editor, prefix, file_sq)) {
2441 open_external_viewer(cmd);
2446 open_run_request(enum request request)
2448 struct run_request *req = get_run_request(request);
2449 char buf[SIZEOF_STR * 2];
2454 report("Unknown run request");
2462 char *next = strstr(cmd, "%(");
2463 int len = next - cmd;
2470 } else if (!strncmp(next, "%(head)", 7)) {
2473 } else if (!strncmp(next, "%(commit)", 9)) {
2476 } else if (!strncmp(next, "%(blob)", 7)) {
2480 report("Unknown replacement in run request: `%s`", req->cmd);
2484 if (!string_format_from(buf, &bufpos, "%.*s%s", len, cmd, value))
2488 next = strchr(next, ')') + 1;
2492 open_external_viewer(buf);
2496 * User request switch noodle
2500 view_driver(struct view *view, enum request request)
2504 if (request == REQ_NONE) {
2509 if (request > REQ_NONE) {
2510 open_run_request(request);
2511 /* FIXME: When all views can refresh always do this. */
2512 if (view == VIEW(REQ_VIEW_STATUS) ||
2513 view == VIEW(REQ_VIEW_STAGE))
2514 request = REQ_REFRESH;
2519 if (view && view->lines) {
2520 request = view->ops->request(view, request, &view->line[view->lineno]);
2521 if (request == REQ_NONE)
2528 case REQ_MOVE_PAGE_UP:
2529 case REQ_MOVE_PAGE_DOWN:
2530 case REQ_MOVE_FIRST_LINE:
2531 case REQ_MOVE_LAST_LINE:
2532 move_view(view, request);
2535 case REQ_SCROLL_LINE_DOWN:
2536 case REQ_SCROLL_LINE_UP:
2537 case REQ_SCROLL_PAGE_DOWN:
2538 case REQ_SCROLL_PAGE_UP:
2539 scroll_view(view, request);
2542 case REQ_VIEW_BLAME:
2544 report("No file chosen, press %s to open tree view",
2545 get_key(REQ_VIEW_TREE));
2548 open_view(view, request, OPEN_DEFAULT);
2553 report("No file chosen, press %s to open tree view",
2554 get_key(REQ_VIEW_TREE));
2557 open_view(view, request, OPEN_DEFAULT);
2560 case REQ_VIEW_PAGER:
2561 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2562 report("No pager content, press %s to run command from prompt",
2563 get_key(REQ_PROMPT));
2566 open_view(view, request, OPEN_DEFAULT);
2569 case REQ_VIEW_STAGE:
2570 if (!VIEW(REQ_VIEW_STAGE)->lines) {
2571 report("No stage content, press %s to open the status view and choose file",
2572 get_key(REQ_VIEW_STATUS));
2575 open_view(view, request, OPEN_DEFAULT);
2578 case REQ_VIEW_STATUS:
2579 if (opt_is_inside_work_tree == FALSE) {
2580 report("The status view requires a working tree");
2583 open_view(view, request, OPEN_DEFAULT);
2591 open_view(view, request, OPEN_DEFAULT);
2596 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2598 if ((view == VIEW(REQ_VIEW_DIFF) &&
2599 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2600 (view == VIEW(REQ_VIEW_DIFF) &&
2601 view->parent == VIEW(REQ_VIEW_BLAME)) ||
2602 (view == VIEW(REQ_VIEW_STAGE) &&
2603 view->parent == VIEW(REQ_VIEW_STATUS)) ||
2604 (view == VIEW(REQ_VIEW_BLOB) &&
2605 view->parent == VIEW(REQ_VIEW_TREE))) {
2608 view = view->parent;
2609 line = view->lineno;
2610 move_view(view, request);
2611 if (view_is_displayed(view))
2612 update_view_title(view);
2613 if (line != view->lineno)
2614 view->ops->request(view, REQ_ENTER,
2615 &view->line[view->lineno]);
2618 move_view(view, request);
2624 int nviews = displayed_views();
2625 int next_view = (current_view + 1) % nviews;
2627 if (next_view == current_view) {
2628 report("Only one view is displayed");
2632 current_view = next_view;
2633 /* Blur out the title of the previous view. */
2634 update_view_title(view);
2639 report("Refreshing is not yet supported for the %s view", view->name);
2643 if (displayed_views() == 2)
2644 open_view(view, VIEW_REQ(view), OPEN_DEFAULT);
2647 case REQ_TOGGLE_LINENO:
2648 opt_line_number = !opt_line_number;
2652 case REQ_TOGGLE_DATE:
2653 opt_date = !opt_date;
2657 case REQ_TOGGLE_AUTHOR:
2658 opt_author = !opt_author;
2662 case REQ_TOGGLE_REV_GRAPH:
2663 opt_rev_graph = !opt_rev_graph;
2667 case REQ_TOGGLE_REFS:
2668 opt_show_refs = !opt_show_refs;
2673 /* Always reload^Wrerun commands from the prompt. */
2674 open_view(view, opt_request, OPEN_RELOAD);
2678 case REQ_SEARCH_BACK:
2679 search_view(view, request);
2684 find_next(view, request);
2687 case REQ_STOP_LOADING:
2688 for (i = 0; i < ARRAY_SIZE(views); i++) {
2691 report("Stopped loading the %s view", view->name),
2696 case REQ_SHOW_VERSION:
2697 report("tig-%s (built %s)", TIG_VERSION, __DATE__);
2700 case REQ_SCREEN_RESIZE:
2703 case REQ_SCREEN_REDRAW:
2708 report("Nothing to edit");
2713 report("Nothing to enter");
2717 case REQ_VIEW_CLOSE:
2718 /* XXX: Mark closed views by letting view->parent point to the
2719 * view itself. Parents to closed view should never be
2722 view->parent->parent != view->parent) {
2723 memset(display, 0, sizeof(display));
2725 display[current_view] = view->parent;
2726 view->parent = view;
2736 /* An unknown key will show most commonly used commands. */
2737 report("Unknown key, press 'h' for help");
2750 pager_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2752 static char spaces[] = " ";
2753 char *text = line->data;
2754 enum line_type type = line->type;
2755 int attr = A_NORMAL;
2758 wmove(view->win, lineno, 0);
2762 wchgat(view->win, -1, 0, type, NULL);
2763 attr = get_line_attr(type);
2765 wattrset(view->win, attr);
2767 if (opt_line_number) {
2768 col += draw_lineno(view, lineno, view->width, selected);
2769 if (col >= view->width)
2774 attr = get_line_attr(type);
2775 wattrset(view->win, attr);
2777 if (opt_tab_size < TABSIZE) {
2778 int col_offset = col;
2781 while (text && col_offset + col < view->width) {
2782 int cols_max = view->width - col_offset - col;
2786 if (*text == '\t') {
2788 assert(sizeof(spaces) > TABSIZE);
2790 cols = opt_tab_size - (col % opt_tab_size);
2793 text = strchr(text, '\t');
2794 cols = line ? text - pos : strlen(pos);
2797 waddnstr(view->win, pos, MIN(cols, cols_max));
2802 draw_text(view, text, view->width - col, TRUE, selected);
2809 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2811 char refbuf[SIZEOF_STR];
2815 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2818 pipe = popen(refbuf, "r");
2822 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2823 ref = chomp_string(ref);
2829 /* This is the only fatal call, since it can "corrupt" the buffer. */
2830 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2837 add_pager_refs(struct view *view, struct line *line)
2839 char buf[SIZEOF_STR];
2840 char *commit_id = (char *)line->data + STRING_SIZE("commit ");
2842 size_t bufpos = 0, refpos = 0;
2843 const char *sep = "Refs: ";
2844 bool is_tag = FALSE;
2846 assert(line->type == LINE_COMMIT);
2848 refs = get_refs(commit_id);
2850 if (view == VIEW(REQ_VIEW_DIFF))
2851 goto try_add_describe_ref;
2856 struct ref *ref = refs[refpos];
2857 char *fmt = ref->tag ? "%s[%s]" :
2858 ref->remote ? "%s<%s>" : "%s%s";
2860 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2865 } while (refs[refpos++]->next);
2867 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2868 try_add_describe_ref:
2869 /* Add <tag>-g<commit_id> "fake" reference. */
2870 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2877 if (!realloc_lines(view, view->line_size + 1))
2880 add_line_text(view, buf, LINE_PP_REFS);
2884 pager_read(struct view *view, char *data)
2891 line = add_line_text(view, data, get_line_type(data));
2895 if (line->type == LINE_COMMIT &&
2896 (view == VIEW(REQ_VIEW_DIFF) ||
2897 view == VIEW(REQ_VIEW_LOG)))
2898 add_pager_refs(view, line);
2904 pager_request(struct view *view, enum request request, struct line *line)
2908 if (request != REQ_ENTER)
2911 if (line->type == LINE_COMMIT &&
2912 (view == VIEW(REQ_VIEW_LOG) ||
2913 view == VIEW(REQ_VIEW_PAGER))) {
2914 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2918 /* Always scroll the view even if it was split. That way
2919 * you can use Enter to scroll through the log view and
2920 * split open each commit diff. */
2921 scroll_view(view, REQ_SCROLL_LINE_DOWN);
2923 /* FIXME: A minor workaround. Scrolling the view will call report("")
2924 * but if we are scrolling a non-current view this won't properly
2925 * update the view title. */
2927 update_view_title(view);
2933 pager_grep(struct view *view, struct line *line)
2936 char *text = line->data;
2941 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
2948 pager_select(struct view *view, struct line *line)
2950 if (line->type == LINE_COMMIT) {
2951 char *text = (char *)line->data + STRING_SIZE("commit ");
2953 if (view != VIEW(REQ_VIEW_PAGER))
2954 string_copy_rev(view->ref, text);
2955 string_copy_rev(ref_commit, text);
2959 static struct view_ops pager_ops = {
2975 help_open(struct view *view)
2978 int lines = ARRAY_SIZE(req_info) + 2;
2981 if (view->lines > 0)
2984 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2985 if (!req_info[i].request)
2988 lines += run_requests + 1;
2990 view->line = calloc(lines, sizeof(*view->line));
2994 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
2996 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2999 if (req_info[i].request == REQ_NONE)
3002 if (!req_info[i].request) {
3003 add_line_text(view, "", LINE_DEFAULT);
3004 add_line_text(view, req_info[i].help, LINE_DEFAULT);
3008 key = get_key(req_info[i].request);
3010 key = "(no key defined)";
3012 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
3015 add_line_text(view, buf, LINE_DEFAULT);
3019 add_line_text(view, "", LINE_DEFAULT);
3020 add_line_text(view, "External commands:", LINE_DEFAULT);
3023 for (i = 0; i < run_requests; i++) {
3024 struct run_request *req = get_run_request(REQ_NONE + i + 1);
3030 key = get_key_name(req->key);
3032 key = "(no key defined)";
3034 if (!string_format(buf, " %-10s %-14s `%s`",
3035 keymap_table[req->keymap].name,
3039 add_line_text(view, buf, LINE_DEFAULT);
3045 static struct view_ops help_ops = {
3060 struct tree_stack_entry {
3061 struct tree_stack_entry *prev; /* Entry below this in the stack */
3062 unsigned long lineno; /* Line number to restore */
3063 char *name; /* Position of name in opt_path */
3066 /* The top of the path stack. */
3067 static struct tree_stack_entry *tree_stack = NULL;
3068 unsigned long tree_lineno = 0;
3071 pop_tree_stack_entry(void)
3073 struct tree_stack_entry *entry = tree_stack;
3075 tree_lineno = entry->lineno;
3077 tree_stack = entry->prev;
3082 push_tree_stack_entry(char *name, unsigned long lineno)
3084 struct tree_stack_entry *entry = calloc(1, sizeof(*entry));
3085 size_t pathlen = strlen(opt_path);
3090 entry->prev = tree_stack;
3091 entry->name = opt_path + pathlen;
3094 if (!string_format_from(opt_path, &pathlen, "%s/", name)) {
3095 pop_tree_stack_entry();
3099 /* Move the current line to the first tree entry. */
3101 entry->lineno = lineno;
3104 /* Parse output from git-ls-tree(1):
3106 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
3107 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
3108 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
3109 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
3112 #define SIZEOF_TREE_ATTR \
3113 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
3115 #define TREE_UP_FORMAT "040000 tree %s\t.."
3118 tree_compare_entry(enum line_type type1, char *name1,
3119 enum line_type type2, char *name2)
3121 if (type1 != type2) {
3122 if (type1 == LINE_TREE_DIR)
3127 return strcmp(name1, name2);
3131 tree_path(struct line *line)
3133 char *path = line->data;
3135 return path + SIZEOF_TREE_ATTR;
3139 tree_read(struct view *view, char *text)
3141 size_t textlen = text ? strlen(text) : 0;
3142 char buf[SIZEOF_STR];
3144 enum line_type type;
3145 bool first_read = view->lines == 0;
3149 if (textlen <= SIZEOF_TREE_ATTR)
3152 type = text[STRING_SIZE("100644 ")] == 't'
3153 ? LINE_TREE_DIR : LINE_TREE_FILE;
3156 /* Add path info line */
3157 if (!string_format(buf, "Directory path /%s", opt_path) ||
3158 !realloc_lines(view, view->line_size + 1) ||
3159 !add_line_text(view, buf, LINE_DEFAULT))
3162 /* Insert "link" to parent directory. */
3164 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
3165 !realloc_lines(view, view->line_size + 1) ||
3166 !add_line_text(view, buf, LINE_TREE_DIR))
3171 /* Strip the path part ... */
3173 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
3174 size_t striplen = strlen(opt_path);
3175 char *path = text + SIZEOF_TREE_ATTR;
3177 if (pathlen > striplen)
3178 memmove(path, path + striplen,
3179 pathlen - striplen + 1);
3182 /* Skip "Directory ..." and ".." line. */
3183 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
3184 struct line *line = &view->line[pos];
3185 char *path1 = tree_path(line);
3186 char *path2 = text + SIZEOF_TREE_ATTR;
3187 int cmp = tree_compare_entry(line->type, path1, type, path2);
3192 text = strdup(text);
3196 if (view->lines > pos)
3197 memmove(&view->line[pos + 1], &view->line[pos],
3198 (view->lines - pos) * sizeof(*line));
3200 line = &view->line[pos];
3207 if (!add_line_text(view, text, type))
3210 if (tree_lineno > view->lineno) {
3211 view->lineno = tree_lineno;
3219 tree_request(struct view *view, enum request request, struct line *line)
3221 enum open_flags flags;
3223 if (request == REQ_VIEW_BLAME) {
3224 char *filename = tree_path(line);
3226 if (line->type == LINE_TREE_DIR) {
3227 report("Cannot show blame for directory %s", opt_path);
3231 string_copy(opt_ref, view->vid);
3232 string_format(opt_file, "%s%s", opt_path, filename);
3235 if (request == REQ_TREE_PARENT) {
3238 request = REQ_ENTER;
3239 line = &view->line[1];
3241 /* quit view if at top of tree */
3242 return REQ_VIEW_CLOSE;
3245 if (request != REQ_ENTER)
3248 /* Cleanup the stack if the tree view is at a different tree. */
3249 while (!*opt_path && tree_stack)
3250 pop_tree_stack_entry();
3252 switch (line->type) {
3254 /* Depending on whether it is a subdir or parent (updir?) link
3255 * mangle the path buffer. */
3256 if (line == &view->line[1] && *opt_path) {
3257 pop_tree_stack_entry();
3260 char *basename = tree_path(line);
3262 push_tree_stack_entry(basename, view->lineno);
3265 /* Trees and subtrees share the same ID, so they are not not
3266 * unique like blobs. */
3267 flags = OPEN_RELOAD;
3268 request = REQ_VIEW_TREE;
3271 case LINE_TREE_FILE:
3272 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3273 request = REQ_VIEW_BLOB;
3280 open_view(view, request, flags);
3281 if (request == REQ_VIEW_TREE) {
3282 view->lineno = tree_lineno;
3289 tree_select(struct view *view, struct line *line)
3291 char *text = (char *)line->data + STRING_SIZE("100644 blob ");
3293 if (line->type == LINE_TREE_FILE) {
3294 string_copy_rev(ref_blob, text);
3296 } else if (line->type != LINE_TREE_DIR) {
3300 string_copy_rev(view->ref, text);
3303 static struct view_ops tree_ops = {
3314 blob_read(struct view *view, char *line)
3318 return add_line_text(view, line, LINE_DEFAULT) != NULL;
3321 static struct view_ops blob_ops = {
3334 * Loading the blame view is a two phase job:
3336 * 1. File content is read either using opt_file from the
3337 * filesystem or using git-cat-file.
3338 * 2. Then blame information is incrementally added by
3339 * reading output from git-blame.
3342 struct blame_commit {
3343 char id[SIZEOF_REV]; /* SHA1 ID. */
3344 char title[128]; /* First line of the commit message. */
3345 char author[75]; /* Author of the commit. */
3346 struct tm time; /* Date from the author ident. */
3347 char filename[128]; /* Name of file. */
3351 struct blame_commit *commit;
3352 unsigned int header:1;
3356 #define BLAME_CAT_FILE_CMD "git cat-file blob %s:%s"
3357 #define BLAME_INCREMENTAL_CMD "git blame --incremental %s %s"
3360 blame_open(struct view *view)
3362 char path[SIZEOF_STR];
3363 char ref[SIZEOF_STR] = "";
3365 if (sq_quote(path, 0, opt_file) >= sizeof(path))
3368 if (*opt_ref && sq_quote(ref, 0, opt_ref) >= sizeof(ref))
3372 if (!string_format(view->cmd, BLAME_CAT_FILE_CMD, ref, path))
3375 view->pipe = fopen(opt_file, "r");
3377 !string_format(view->cmd, BLAME_CAT_FILE_CMD, "HEAD", path))
3382 view->pipe = popen(view->cmd, "r");
3386 if (!string_format(view->cmd, BLAME_INCREMENTAL_CMD, ref, path))
3389 string_format(view->ref, "%s ...", opt_file);
3390 string_copy_rev(view->vid, opt_file);
3391 set_nonblocking_input(TRUE);
3396 for (i = 0; i < view->lines; i++)
3397 free(view->line[i].data);
3401 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3402 view->offset = view->lines = view->lineno = 0;
3404 view->start_time = time(NULL);
3409 static struct blame_commit *
3410 get_blame_commit(struct view *view, const char *id)
3414 for (i = 0; i < view->lines; i++) {
3415 struct blame *blame = view->line[i].data;
3420 if (!strncmp(blame->commit->id, id, SIZEOF_REV - 1))
3421 return blame->commit;
3425 struct blame_commit *commit = calloc(1, sizeof(*commit));
3428 string_ncopy(commit->id, id, SIZEOF_REV);
3434 parse_number(char **posref, size_t *number, size_t min, size_t max)
3436 char *pos = *posref;
3439 pos = strchr(pos + 1, ' ');
3440 if (!pos || !isdigit(pos[1]))
3442 *number = atoi(pos + 1);
3443 if (*number < min || *number > max)
3450 static struct blame_commit *
3451 parse_blame_commit(struct view *view, char *text, int *blamed)
3453 struct blame_commit *commit;
3454 struct blame *blame;
3455 char *pos = text + SIZEOF_REV - 1;
3459 if (strlen(text) <= SIZEOF_REV || *pos != ' ')
3462 if (!parse_number(&pos, &lineno, 1, view->lines) ||
3463 !parse_number(&pos, &group, 1, view->lines - lineno + 1))
3466 commit = get_blame_commit(view, text);
3472 struct line *line = &view->line[lineno + group - 1];
3475 blame->commit = commit;
3476 blame->header = !group;
3484 blame_read_file(struct view *view, char *line)
3489 if (view->lines > 0)
3490 pipe = popen(view->cmd, "r");
3491 else if (!view->parent)
3492 die("No blame exist for %s", view->vid);
3495 report("Failed to load blame data");
3504 size_t linelen = strlen(line);
3505 struct blame *blame = malloc(sizeof(*blame) + linelen);
3510 blame->commit = NULL;
3511 strncpy(blame->text, line, linelen);
3512 blame->text[linelen] = 0;
3513 return add_line_data(view, blame, LINE_BLAME_COMMIT) != NULL;
3518 match_blame_header(const char *name, char **line)
3520 size_t namelen = strlen(name);
3521 bool matched = !strncmp(name, *line, namelen);
3530 blame_read(struct view *view, char *line)
3532 static struct blame_commit *commit = NULL;
3533 static int blamed = 0;
3534 static time_t author_time;
3537 return blame_read_file(view, line);
3543 string_format(view->ref, "%s", view->vid);
3544 if (view_is_displayed(view)) {
3545 update_view_title(view);
3546 redraw_view_from(view, 0);
3552 commit = parse_blame_commit(view, line, &blamed);
3553 string_format(view->ref, "%s %2d%%", view->vid,
3554 blamed * 100 / view->lines);
3556 } else if (match_blame_header("author ", &line)) {
3557 string_ncopy(commit->author, line, strlen(line));
3559 } else if (match_blame_header("author-time ", &line)) {
3560 author_time = (time_t) atol(line);
3562 } else if (match_blame_header("author-tz ", &line)) {
3565 tz = ('0' - line[1]) * 60 * 60 * 10;
3566 tz += ('0' - line[2]) * 60 * 60;
3567 tz += ('0' - line[3]) * 60;
3568 tz += ('0' - line[4]) * 60;
3574 gmtime_r(&author_time, &commit->time);
3576 } else if (match_blame_header("summary ", &line)) {
3577 string_ncopy(commit->title, line, strlen(line));
3579 } else if (match_blame_header("filename ", &line)) {
3580 string_ncopy(commit->filename, line, strlen(line));
3588 blame_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
3590 struct blame *blame = line->data;
3593 wmove(view->win, lineno, 0);
3596 wattrset(view->win, get_line_attr(LINE_CURSOR));
3597 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
3599 wattrset(view->win, A_NORMAL);
3606 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
3607 if (blame->commit) {
3608 char buf[DATE_COLS + 1];
3611 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &blame->commit->time);
3612 n = draw_text(view, buf, view->width - col, FALSE, selected);
3613 draw_text(view, " ", view->width - col - n, FALSE, selected);
3617 wmove(view->win, lineno, col);
3618 if (col >= view->width)
3623 int max = MIN(AUTHOR_COLS - 1, view->width - col);
3626 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
3628 draw_text(view, blame->commit->author, max, TRUE, selected);
3630 if (col >= view->width)
3632 wmove(view->win, lineno, col);
3636 int max = MIN(ID_COLS - 1, view->width - col);
3639 wattrset(view->win, get_line_attr(LINE_BLAME_ID));
3641 draw_text(view, blame->commit->id, max, FALSE, -1);
3643 if (col >= view->width)
3645 wmove(view->win, lineno, col);
3649 col += draw_lineno(view, lineno, view->width - col, selected);
3650 if (col >= view->width)
3654 col += draw_text(view, blame->text, view->width - col, TRUE, selected);
3660 blame_request(struct view *view, enum request request, struct line *line)
3662 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3663 struct blame *blame = line->data;
3667 if (!blame->commit) {
3668 report("No commit loaded yet");
3672 if (!strcmp(blame->commit->id, NULL_ID)) {
3673 char path[SIZEOF_STR];
3675 if (sq_quote(path, 0, view->vid) >= sizeof(path))
3677 string_format(opt_cmd, "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s 2>/dev/null", path);
3680 open_view(view, REQ_VIEW_DIFF, flags);
3691 blame_grep(struct view *view, struct line *line)
3693 struct blame *blame = line->data;
3694 struct blame_commit *commit = blame->commit;
3697 #define MATCH(text) \
3698 (*text && regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3701 char buf[DATE_COLS + 1];
3703 if (MATCH(commit->title) ||
3704 MATCH(commit->author) ||
3708 if (strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time) &&
3713 return MATCH(blame->text);
3719 blame_select(struct view *view, struct line *line)
3721 struct blame *blame = line->data;
3722 struct blame_commit *commit = blame->commit;
3727 if (!strcmp(commit->id, NULL_ID))
3728 string_ncopy(ref_commit, "HEAD", 4);
3730 string_copy_rev(ref_commit, commit->id);
3733 static struct view_ops blame_ops = {
3751 char rev[SIZEOF_REV];
3752 char name[SIZEOF_STR];
3756 char rev[SIZEOF_REV];
3757 char name[SIZEOF_STR];
3761 static char status_onbranch[SIZEOF_STR];
3762 static struct status stage_status;
3763 static enum line_type stage_line_type;
3765 /* Get fields from the diff line:
3766 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
3769 status_get_diff(struct status *file, char *buf, size_t bufsize)
3771 char *old_mode = buf + 1;
3772 char *new_mode = buf + 8;
3773 char *old_rev = buf + 15;
3774 char *new_rev = buf + 56;
3775 char *status = buf + 97;
3778 old_mode[-1] != ':' ||
3779 new_mode[-1] != ' ' ||
3780 old_rev[-1] != ' ' ||
3781 new_rev[-1] != ' ' ||
3785 file->status = *status;
3787 string_copy_rev(file->old.rev, old_rev);
3788 string_copy_rev(file->new.rev, new_rev);
3790 file->old.mode = strtoul(old_mode, NULL, 8);
3791 file->new.mode = strtoul(new_mode, NULL, 8);
3793 file->old.name[0] = file->new.name[0] = 0;
3799 status_run(struct view *view, const char cmd[], char status, enum line_type type)
3801 struct status *file = NULL;
3802 struct status *unmerged = NULL;
3803 char buf[SIZEOF_STR * 4];
3807 pipe = popen(cmd, "r");
3811 add_line_data(view, NULL, type);
3813 while (!feof(pipe) && !ferror(pipe)) {
3817 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
3820 bufsize += readsize;
3822 /* Process while we have NUL chars. */
3823 while ((sep = memchr(buf, 0, bufsize))) {
3824 size_t sepsize = sep - buf + 1;
3827 if (!realloc_lines(view, view->line_size + 1))
3830 file = calloc(1, sizeof(*file));
3834 add_line_data(view, file, type);
3837 /* Parse diff info part. */
3839 file->status = status;
3841 string_copy(file->old.rev, NULL_ID);
3843 } else if (!file->status) {
3844 if (!status_get_diff(file, buf, sepsize))
3848 memmove(buf, sep + 1, bufsize);
3850 sep = memchr(buf, 0, bufsize);
3853 sepsize = sep - buf + 1;
3855 /* Collapse all 'M'odified entries that
3856 * follow a associated 'U'nmerged entry.
3858 if (file->status == 'U') {
3861 } else if (unmerged) {
3862 int collapse = !strcmp(buf, unmerged->new.name);
3873 /* Grab the old name for rename/copy. */
3874 if (!*file->old.name &&
3875 (file->status == 'R' || file->status == 'C')) {
3876 sepsize = sep - buf + 1;
3877 string_ncopy(file->old.name, buf, sepsize);
3879 memmove(buf, sep + 1, bufsize);
3881 sep = memchr(buf, 0, bufsize);
3884 sepsize = sep - buf + 1;
3887 /* git-ls-files just delivers a NUL separated
3888 * list of file names similar to the second half
3889 * of the git-diff-* output. */
3890 string_ncopy(file->new.name, buf, sepsize);
3891 if (!*file->old.name)
3892 string_copy(file->old.name, file->new.name);
3894 memmove(buf, sep + 1, bufsize);
3905 if (!view->line[view->lines - 1].data)
3906 add_line_data(view, NULL, LINE_STAT_NONE);
3912 /* Don't show unmerged entries in the staged section. */
3913 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached -M HEAD"
3914 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
3915 #define STATUS_LIST_OTHER_CMD \
3916 "git ls-files -z --others --exclude-per-directory=.gitignore"
3917 #define STATUS_LIST_NO_HEAD_CMD \
3918 "git ls-files -z --cached --exclude-per-directory=.gitignore"
3920 #define STATUS_DIFF_INDEX_SHOW_CMD \
3921 "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s %s 2>/dev/null"
3923 #define STATUS_DIFF_FILES_SHOW_CMD \
3924 "git diff-files --root --patch-with-stat -C -M -- %s %s 2>/dev/null"
3926 #define STATUS_DIFF_NO_HEAD_SHOW_CMD \
3927 "git diff --no-color --patch-with-stat /dev/null %s 2>/dev/null"
3929 /* First parse staged info using git-diff-index(1), then parse unstaged
3930 * info using git-diff-files(1), and finally untracked files using
3931 * git-ls-files(1). */
3933 status_open(struct view *view)
3935 struct stat statbuf;
3936 char exclude[SIZEOF_STR];
3937 char indexcmd[SIZEOF_STR] = STATUS_DIFF_INDEX_CMD;
3938 char othercmd[SIZEOF_STR] = STATUS_LIST_OTHER_CMD;
3939 unsigned long prev_lineno = view->lineno;
3940 char indexstatus = 0;
3943 for (i = 0; i < view->lines; i++)
3944 free(view->line[i].data);
3946 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3949 if (!realloc_lines(view, view->line_size + 7))
3952 add_line_data(view, NULL, LINE_STAT_HEAD);
3954 string_copy(status_onbranch, "Initial commit");
3955 else if (!*opt_head)
3956 string_copy(status_onbranch, "Not currently on any branch");
3957 else if (!string_format(status_onbranch, "On branch %s", opt_head))
3961 string_copy(indexcmd, STATUS_LIST_NO_HEAD_CMD);
3965 if (!string_format(exclude, "%s/info/exclude", opt_git_dir))
3968 if (stat(exclude, &statbuf) >= 0) {
3969 size_t cmdsize = strlen(othercmd);
3971 if (!string_format_from(othercmd, &cmdsize, " %s", "--exclude-from=") ||
3972 sq_quote(othercmd, cmdsize, exclude) >= sizeof(othercmd))
3975 cmdsize = strlen(indexcmd);
3977 (!string_format_from(indexcmd, &cmdsize, " %s", "--exclude-from=") ||
3978 sq_quote(indexcmd, cmdsize, exclude) >= sizeof(indexcmd)))
3982 system("git update-index -q --refresh");
3984 if (!status_run(view, indexcmd, indexstatus, LINE_STAT_STAGED) ||
3985 !status_run(view, STATUS_DIFF_FILES_CMD, 0, LINE_STAT_UNSTAGED) ||
3986 !status_run(view, othercmd, '?', LINE_STAT_UNTRACKED))
3989 /* If all went well restore the previous line number to stay in
3990 * the context or select a line with something that can be
3992 if (prev_lineno >= view->lines)
3993 prev_lineno = view->lines - 1;
3994 while (prev_lineno < view->lines && !view->line[prev_lineno].data)
3996 while (prev_lineno > 0 && !view->line[prev_lineno].data)
3999 /* If the above fails, always skip the "On branch" line. */
4000 if (prev_lineno < view->lines)
4001 view->lineno = prev_lineno;
4005 if (view->lineno < view->offset)
4006 view->offset = view->lineno;
4007 else if (view->offset + view->height <= view->lineno)
4008 view->offset = view->lineno - view->height + 1;
4014 status_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
4016 struct status *status = line->data;
4020 wmove(view->win, lineno, 0);
4023 wattrset(view->win, get_line_attr(LINE_CURSOR));
4024 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
4026 } else if (line->type == LINE_STAT_HEAD) {
4027 wattrset(view->win, get_line_attr(LINE_STAT_HEAD));
4028 wchgat(view->win, -1, 0, LINE_STAT_HEAD, NULL);
4030 } else if (!status && line->type != LINE_STAT_NONE) {
4031 wattrset(view->win, get_line_attr(LINE_STAT_SECTION));
4032 wchgat(view->win, -1, 0, LINE_STAT_SECTION, NULL);
4035 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)";
4056 case LINE_STAT_HEAD:
4057 text = status_onbranch;
4064 char buf[] = { status->status, ' ', ' ', ' ', 0 };
4066 col += draw_text(view, buf, view->width, TRUE, selected);
4068 wattrset(view->win, A_NORMAL);
4069 text = status->new.name;
4072 draw_text(view, text, view->width - col, TRUE, selected);
4077 status_enter(struct view *view, struct line *line)
4079 struct status *status = line->data;
4080 char oldpath[SIZEOF_STR] = "";
4081 char newpath[SIZEOF_STR] = "";
4085 if (line->type == LINE_STAT_NONE ||
4086 (!status && line[1].type == LINE_STAT_NONE)) {
4087 report("No file to diff");
4092 if (sq_quote(oldpath, 0, status->old.name) >= sizeof(oldpath))
4094 /* Diffs for unmerged entries are empty when pasing the
4095 * new path, so leave it empty. */
4096 if (status->status != 'U' &&
4097 sq_quote(newpath, 0, status->new.name) >= sizeof(newpath))
4102 line->type != LINE_STAT_UNTRACKED &&
4103 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
4106 switch (line->type) {
4107 case LINE_STAT_STAGED:
4109 if (!string_format_from(opt_cmd, &cmdsize,
4110 STATUS_DIFF_NO_HEAD_SHOW_CMD,
4114 if (!string_format_from(opt_cmd, &cmdsize,
4115 STATUS_DIFF_INDEX_SHOW_CMD,
4121 info = "Staged changes to %s";
4123 info = "Staged changes";
4126 case LINE_STAT_UNSTAGED:
4127 if (!string_format_from(opt_cmd, &cmdsize,
4128 STATUS_DIFF_FILES_SHOW_CMD, oldpath, newpath))
4131 info = "Unstaged changes to %s";
4133 info = "Unstaged changes";
4136 case LINE_STAT_UNTRACKED:
4141 report("No file to show");
4145 opt_pipe = fopen(status->new.name, "r");
4146 info = "Untracked file %s";
4149 case LINE_STAT_HEAD:
4153 die("line type %d not handled in switch", line->type);
4156 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | OPEN_SPLIT);
4157 if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) {
4159 stage_status = *status;
4161 memset(&stage_status, 0, sizeof(stage_status));
4164 stage_line_type = line->type;
4165 string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.new.name);
4172 status_exists(struct status *status, enum line_type type)
4174 struct view *view = VIEW(REQ_VIEW_STATUS);
4177 for (line = view->line; line < view->line + view->lines; line++) {
4178 struct status *pos = line->data;
4180 if (line->type == type && pos &&
4181 !strcmp(status->new.name, pos->new.name))
4190 status_update_prepare(enum line_type type)
4192 char cmd[SIZEOF_STR];
4196 type != LINE_STAT_UNTRACKED &&
4197 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4201 case LINE_STAT_STAGED:
4202 string_add(cmd, cmdsize, "git update-index -z --index-info");
4205 case LINE_STAT_UNSTAGED:
4206 case LINE_STAT_UNTRACKED:
4207 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
4211 die("line type %d not handled in switch", type);
4214 return popen(cmd, "w");
4218 status_update_write(FILE *pipe, struct status *status, enum line_type type)
4220 char buf[SIZEOF_STR];
4225 case LINE_STAT_STAGED:
4226 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
4229 status->old.name, 0))
4233 case LINE_STAT_UNSTAGED:
4234 case LINE_STAT_UNTRACKED:
4235 if (!string_format_from(buf, &bufsize, "%s%c", status->new.name, 0))
4240 die("line type %d not handled in switch", type);
4243 while (!ferror(pipe) && written < bufsize) {
4244 written += fwrite(buf + written, 1, bufsize - written, pipe);
4247 return written == bufsize;
4251 status_update_file(struct status *status, enum line_type type)
4253 FILE *pipe = status_update_prepare(type);
4259 result = status_update_write(pipe, status, type);
4265 status_update_files(struct view *view, struct line *line)
4267 FILE *pipe = status_update_prepare(line->type);
4269 struct line *pos = view->line + view->lines;
4276 for (pos = line; pos < view->line + view->lines && pos->data; pos++)
4279 for (file = 0, done = 0; result && file < files; line++, file++) {
4280 int almost_done = file * 100 / files;
4282 if (almost_done > done) {
4284 string_format(view->ref, "updating file %u of %u (%d%% done)",
4286 update_view_title(view);
4288 result = status_update_write(pipe, line->data, line->type);
4296 status_update(struct view *view)
4298 struct line *line = &view->line[view->lineno];
4300 assert(view->lines);
4303 /* This should work even for the "On branch" line. */
4304 if (line < view->line + view->lines && !line[1].data) {
4305 report("Nothing to update");
4309 if (!status_update_files(view, line + 1))
4310 report("Failed to update file status");
4312 } else if (!status_update_file(line->data, line->type)) {
4313 report("Failed to update file status");
4320 status_request(struct view *view, enum request request, struct line *line)
4322 struct status *status = line->data;
4325 case REQ_STATUS_UPDATE:
4326 if (!status_update(view))
4330 case REQ_STATUS_MERGE:
4331 if (!status || status->status != 'U') {
4332 report("Merging only possible for files with unmerged status ('U').");
4335 open_mergetool(status->new.name);
4342 open_editor(status->status != '?', status->new.name);
4345 case REQ_VIEW_BLAME:
4347 string_copy(opt_file, status->new.name);
4353 /* After returning the status view has been split to
4354 * show the stage view. No further reloading is
4356 status_enter(view, line);
4360 /* Simply reload the view. */
4367 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
4373 status_select(struct view *view, struct line *line)
4375 struct status *status = line->data;
4376 char file[SIZEOF_STR] = "all files";
4380 if (status && !string_format(file, "'%s'", status->new.name))
4383 if (!status && line[1].type == LINE_STAT_NONE)
4386 switch (line->type) {
4387 case LINE_STAT_STAGED:
4388 text = "Press %s to unstage %s for commit";
4391 case LINE_STAT_UNSTAGED:
4392 text = "Press %s to stage %s for commit";
4395 case LINE_STAT_UNTRACKED:
4396 text = "Press %s to stage %s for addition";
4399 case LINE_STAT_HEAD:
4400 case LINE_STAT_NONE:
4401 text = "Nothing to update";
4405 die("line type %d not handled in switch", line->type);
4408 if (status && status->status == 'U') {
4409 text = "Press %s to resolve conflict in %s";
4410 key = get_key(REQ_STATUS_MERGE);
4413 key = get_key(REQ_STATUS_UPDATE);
4416 string_format(view->ref, text, key, file);
4420 status_grep(struct view *view, struct line *line)
4422 struct status *status = line->data;
4423 enum { S_STATUS, S_NAME, S_END } state;
4430 for (state = S_STATUS; state < S_END; state++) {
4434 case S_NAME: text = status->new.name; break;
4436 buf[0] = status->status;
4444 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4451 static struct view_ops status_ops = {
4463 stage_diff_line(FILE *pipe, struct line *line)
4465 char *buf = line->data;
4466 size_t bufsize = strlen(buf);
4469 while (!ferror(pipe) && written < bufsize) {
4470 written += fwrite(buf + written, 1, bufsize - written, pipe);
4475 return written == bufsize;
4479 stage_diff_write(FILE *pipe, struct line *line, struct line *end)
4481 while (line < end) {
4482 if (!stage_diff_line(pipe, line++))
4484 if (line->type == LINE_DIFF_CHUNK ||
4485 line->type == LINE_DIFF_HEADER)
4492 static struct line *
4493 stage_diff_find(struct view *view, struct line *line, enum line_type type)
4495 for (; view->line < line; line--)
4496 if (line->type == type)
4503 stage_update_chunk(struct view *view, struct line *chunk)
4505 char cmd[SIZEOF_STR];
4507 struct line *diff_hdr;
4510 diff_hdr = stage_diff_find(view, chunk, LINE_DIFF_HEADER);
4515 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4518 if (!string_format_from(cmd, &cmdsize,
4519 "git apply --whitespace=nowarn --cached %s - && "
4520 "git update-index -q --unmerged --refresh 2>/dev/null",
4521 stage_line_type == LINE_STAT_STAGED ? "-R" : ""))
4524 pipe = popen(cmd, "w");
4528 if (!stage_diff_write(pipe, diff_hdr, chunk) ||
4529 !stage_diff_write(pipe, chunk, view->line + view->lines))
4534 return chunk ? TRUE : FALSE;
4538 stage_update(struct view *view, struct line *line)
4540 struct line *chunk = NULL;
4542 if (!opt_no_head && stage_line_type != LINE_STAT_UNTRACKED)
4543 chunk = stage_diff_find(view, line, LINE_DIFF_CHUNK);
4546 if (!stage_update_chunk(view, chunk)) {
4547 report("Failed to apply chunk");
4551 } else if (!status_update_file(&stage_status, stage_line_type)) {
4552 report("Failed to update file");
4560 stage_request(struct view *view, enum request request, struct line *line)
4563 case REQ_STATUS_UPDATE:
4564 stage_update(view, line);
4568 if (!stage_status.new.name[0])
4571 open_editor(stage_status.status != '?', stage_status.new.name);
4575 /* Reload everything ... */
4578 case REQ_VIEW_BLAME:
4579 if (stage_status.new.name[0]) {
4580 string_copy(opt_file, stage_status.new.name);
4586 return pager_request(view, request, line);
4592 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD | OPEN_NOMAXIMIZE);
4594 /* Check whether the staged entry still exists, and close the
4595 * stage view if it doesn't. */
4596 if (!status_exists(&stage_status, stage_line_type))
4597 return REQ_VIEW_CLOSE;
4599 if (stage_line_type == LINE_STAT_UNTRACKED)
4600 opt_pipe = fopen(stage_status.new.name, "r");
4602 string_copy(opt_cmd, view->cmd);
4603 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | OPEN_NOMAXIMIZE);
4608 static struct view_ops stage_ops = {
4624 char id[SIZEOF_REV]; /* SHA1 ID. */
4625 char title[128]; /* First line of the commit message. */
4626 char author[75]; /* Author of the commit. */
4627 struct tm time; /* Date from the author ident. */
4628 struct ref **refs; /* Repository references. */
4629 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
4630 size_t graph_size; /* The width of the graph array. */
4631 bool has_parents; /* Rewritten --parents seen. */
4634 /* Size of rev graph with no "padding" columns */
4635 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
4638 struct rev_graph *prev, *next, *parents;
4639 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
4641 struct commit *commit;
4643 unsigned int boundary:1;
4646 /* Parents of the commit being visualized. */
4647 static struct rev_graph graph_parents[4];
4649 /* The current stack of revisions on the graph. */
4650 static struct rev_graph graph_stacks[4] = {
4651 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
4652 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
4653 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
4654 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
4658 graph_parent_is_merge(struct rev_graph *graph)
4660 return graph->parents->size > 1;
4664 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
4666 struct commit *commit = graph->commit;
4668 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
4669 commit->graph[commit->graph_size++] = symbol;
4673 done_rev_graph(struct rev_graph *graph)
4675 if (graph_parent_is_merge(graph) &&
4676 graph->pos < graph->size - 1 &&
4677 graph->next->size == graph->size + graph->parents->size - 1) {
4678 size_t i = graph->pos + graph->parents->size - 1;
4680 graph->commit->graph_size = i * 2;
4681 while (i < graph->next->size - 1) {
4682 append_to_rev_graph(graph, ' ');
4683 append_to_rev_graph(graph, '\\');
4688 graph->size = graph->pos = 0;
4689 graph->commit = NULL;
4690 memset(graph->parents, 0, sizeof(*graph->parents));
4694 push_rev_graph(struct rev_graph *graph, char *parent)
4698 /* "Collapse" duplicate parents lines.
4700 * FIXME: This needs to also update update the drawn graph but
4701 * for now it just serves as a method for pruning graph lines. */
4702 for (i = 0; i < graph->size; i++)
4703 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
4706 if (graph->size < SIZEOF_REVITEMS) {
4707 string_copy_rev(graph->rev[graph->size++], parent);
4712 get_rev_graph_symbol(struct rev_graph *graph)
4716 if (graph->boundary)
4717 symbol = REVGRAPH_BOUND;
4718 else if (graph->parents->size == 0)
4719 symbol = REVGRAPH_INIT;
4720 else if (graph_parent_is_merge(graph))
4721 symbol = REVGRAPH_MERGE;
4722 else if (graph->pos >= graph->size)
4723 symbol = REVGRAPH_BRANCH;
4725 symbol = REVGRAPH_COMMIT;
4731 draw_rev_graph(struct rev_graph *graph)
4734 chtype separator, line;
4736 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
4737 static struct rev_filler fillers[] = {
4738 { ' ', REVGRAPH_LINE },
4743 chtype symbol = get_rev_graph_symbol(graph);
4744 struct rev_filler *filler;
4747 filler = &fillers[DEFAULT];
4749 for (i = 0; i < graph->pos; i++) {
4750 append_to_rev_graph(graph, filler->line);
4751 if (graph_parent_is_merge(graph->prev) &&
4752 graph->prev->pos == i)
4753 filler = &fillers[RSHARP];
4755 append_to_rev_graph(graph, filler->separator);
4758 /* Place the symbol for this revision. */
4759 append_to_rev_graph(graph, symbol);
4761 if (graph->prev->size > graph->size)
4762 filler = &fillers[RDIAG];
4764 filler = &fillers[DEFAULT];
4768 for (; i < graph->size; i++) {
4769 append_to_rev_graph(graph, filler->separator);
4770 append_to_rev_graph(graph, filler->line);
4771 if (graph_parent_is_merge(graph->prev) &&
4772 i < graph->prev->pos + graph->parents->size)
4773 filler = &fillers[RSHARP];
4774 if (graph->prev->size > graph->size)
4775 filler = &fillers[LDIAG];
4778 if (graph->prev->size > graph->size) {
4779 append_to_rev_graph(graph, filler->separator);
4780 if (filler->line != ' ')
4781 append_to_rev_graph(graph, filler->line);
4785 /* Prepare the next rev graph */
4787 prepare_rev_graph(struct rev_graph *graph)
4791 /* First, traverse all lines of revisions up to the active one. */
4792 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
4793 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
4796 push_rev_graph(graph->next, graph->rev[graph->pos]);
4799 /* Interleave the new revision parent(s). */
4800 for (i = 0; !graph->boundary && i < graph->parents->size; i++)
4801 push_rev_graph(graph->next, graph->parents->rev[i]);
4803 /* Lastly, put any remaining revisions. */
4804 for (i = graph->pos + 1; i < graph->size; i++)
4805 push_rev_graph(graph->next, graph->rev[i]);
4809 update_rev_graph(struct rev_graph *graph)
4811 /* If this is the finalizing update ... */
4813 prepare_rev_graph(graph);
4815 /* Graph visualization needs a one rev look-ahead,
4816 * so the first update doesn't visualize anything. */
4817 if (!graph->prev->commit)
4820 draw_rev_graph(graph->prev);
4821 done_rev_graph(graph->prev->prev);
4830 main_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
4832 char buf[DATE_COLS + 1];
4833 struct commit *commit = line->data;
4834 enum line_type type;
4839 if (!*commit->author)
4842 space = view->width;
4843 wmove(view->win, lineno, col);
4847 wattrset(view->win, get_line_attr(type));
4848 wchgat(view->win, -1, 0, type, NULL);
4850 type = LINE_MAIN_COMMIT;
4851 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
4857 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
4858 n = draw_text(view, buf, view->width - col, FALSE, selected);
4859 draw_text(view, " ", view->width - col - n, FALSE, selected);
4862 wmove(view->win, lineno, col);
4863 if (col >= view->width)
4866 if (type != LINE_CURSOR)
4867 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
4872 max_len = view->width - col;
4873 if (max_len > AUTHOR_COLS - 1)
4874 max_len = AUTHOR_COLS - 1;
4875 draw_text(view, commit->author, max_len, TRUE, selected);
4877 if (col >= view->width)
4881 if (opt_rev_graph && commit->graph_size) {
4882 size_t graph_size = view->width - col;
4885 if (type != LINE_CURSOR)
4886 wattrset(view->win, get_line_attr(LINE_MAIN_REVGRAPH));
4887 wmove(view->win, lineno, col);
4888 if (graph_size > commit->graph_size)
4889 graph_size = commit->graph_size;
4890 /* Using waddch() instead of waddnstr() ensures that
4891 * they'll be rendered correctly for the cursor line. */
4892 for (i = 0; i < graph_size; i++)
4893 waddch(view->win, commit->graph[i]);
4895 col += commit->graph_size + 1;
4896 if (col >= view->width)
4898 waddch(view->win, ' ');
4900 if (type != LINE_CURSOR)
4901 wattrset(view->win, A_NORMAL);
4903 wmove(view->win, lineno, col);
4905 if (opt_show_refs && commit->refs) {
4909 if (type == LINE_CURSOR)
4911 else if (commit->refs[i]->head)
4912 wattrset(view->win, get_line_attr(LINE_MAIN_HEAD));
4913 else if (commit->refs[i]->ltag)
4914 wattrset(view->win, get_line_attr(LINE_MAIN_LOCAL_TAG));
4915 else if (commit->refs[i]->tag)
4916 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
4917 else if (commit->refs[i]->tracked)
4918 wattrset(view->win, get_line_attr(LINE_MAIN_TRACKED));
4919 else if (commit->refs[i]->remote)
4920 wattrset(view->win, get_line_attr(LINE_MAIN_REMOTE));
4922 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
4924 col += draw_text(view, "[", view->width - col, TRUE, selected);
4925 col += draw_text(view, commit->refs[i]->name, view->width - col,
4927 col += draw_text(view, "]", view->width - col, TRUE, selected);
4928 if (type != LINE_CURSOR)
4929 wattrset(view->win, A_NORMAL);
4930 col += draw_text(view, " ", view->width - col, TRUE, selected);
4931 if (col >= view->width)
4933 } while (commit->refs[i++]->next);
4936 if (type != LINE_CURSOR)
4937 wattrset(view->win, get_line_attr(type));
4939 draw_text(view, commit->title, view->width - col, TRUE, selected);
4943 /* Reads git log --pretty=raw output and parses it into the commit struct. */
4945 main_read(struct view *view, char *line)
4947 static struct rev_graph *graph = graph_stacks;
4948 enum line_type type;
4949 struct commit *commit;
4952 if (!view->lines && !view->parent)
4953 die("No revisions match the given arguments.");
4954 update_rev_graph(graph);
4958 type = get_line_type(line);
4959 if (type == LINE_COMMIT) {
4960 commit = calloc(1, sizeof(struct commit));
4964 line += STRING_SIZE("commit ");
4966 graph->boundary = 1;
4970 string_copy_rev(commit->id, line);
4971 commit->refs = get_refs(commit->id);
4972 graph->commit = commit;
4973 add_line_data(view, commit, LINE_MAIN_COMMIT);
4975 while ((line = strchr(line, ' '))) {
4977 push_rev_graph(graph->parents, line);
4978 commit->has_parents = TRUE;
4985 commit = view->line[view->lines - 1].data;
4989 if (commit->has_parents)
4991 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
4996 /* Parse author lines where the name may be empty:
4997 * author <email@address.tld> 1138474660 +0100
4999 char *ident = line + STRING_SIZE("author ");
5000 char *nameend = strchr(ident, '<');
5001 char *emailend = strchr(ident, '>');
5003 if (!nameend || !emailend)
5006 update_rev_graph(graph);
5007 graph = graph->next;
5009 *nameend = *emailend = 0;
5010 ident = chomp_string(ident);
5012 ident = chomp_string(nameend + 1);
5017 string_ncopy(commit->author, ident, strlen(ident));
5019 /* Parse epoch and timezone */
5020 if (emailend[1] == ' ') {
5021 char *secs = emailend + 2;
5022 char *zone = strchr(secs, ' ');
5023 time_t time = (time_t) atol(secs);
5025 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
5029 tz = ('0' - zone[1]) * 60 * 60 * 10;
5030 tz += ('0' - zone[2]) * 60 * 60;
5031 tz += ('0' - zone[3]) * 60;
5032 tz += ('0' - zone[4]) * 60;
5040 gmtime_r(&time, &commit->time);
5045 /* Fill in the commit title if it has not already been set. */
5046 if (commit->title[0])
5049 /* Require titles to start with a non-space character at the
5050 * offset used by git log. */
5051 if (strncmp(line, " ", 4))
5054 /* Well, if the title starts with a whitespace character,
5055 * try to be forgiving. Otherwise we end up with no title. */
5056 while (isspace(*line))
5060 /* FIXME: More graceful handling of titles; append "..." to
5061 * shortened titles, etc. */
5063 string_ncopy(commit->title, line, strlen(line));
5070 main_request(struct view *view, enum request request, struct line *line)
5072 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
5074 if (request == REQ_ENTER)
5075 open_view(view, REQ_VIEW_DIFF, flags);
5083 main_grep(struct view *view, struct line *line)
5085 struct commit *commit = line->data;
5086 enum { S_TITLE, S_AUTHOR, S_DATE, S_END } state;
5087 char buf[DATE_COLS + 1];
5090 for (state = S_TITLE; state < S_END; state++) {
5094 case S_TITLE: text = commit->title; break;
5095 case S_AUTHOR: text = commit->author; break;
5097 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
5106 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
5114 main_select(struct view *view, struct line *line)
5116 struct commit *commit = line->data;
5118 string_copy_rev(view->ref, commit->id);
5119 string_copy_rev(ref_commit, view->ref);
5122 static struct view_ops main_ops = {
5134 * Unicode / UTF-8 handling
5136 * NOTE: Much of the following code for dealing with unicode is derived from
5137 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
5138 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
5141 /* I've (over)annotated a lot of code snippets because I am not entirely
5142 * confident that the approach taken by this small UTF-8 interface is correct.
5146 unicode_width(unsigned long c)
5149 (c <= 0x115f /* Hangul Jamo */
5152 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
5154 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
5155 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
5156 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
5157 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
5158 || (c >= 0xffe0 && c <= 0xffe6)
5159 || (c >= 0x20000 && c <= 0x2fffd)
5160 || (c >= 0x30000 && c <= 0x3fffd)))
5164 return opt_tab_size;
5169 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
5170 * Illegal bytes are set one. */
5171 static const unsigned char utf8_bytes[256] = {
5172 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,
5173 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,
5174 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,
5175 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,
5176 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,
5177 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,
5178 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,
5179 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,
5182 /* Decode UTF-8 multi-byte representation into a unicode character. */
5183 static inline unsigned long
5184 utf8_to_unicode(const char *string, size_t length)
5186 unsigned long unicode;
5190 unicode = string[0];
5193 unicode = (string[0] & 0x1f) << 6;
5194 unicode += (string[1] & 0x3f);
5197 unicode = (string[0] & 0x0f) << 12;
5198 unicode += ((string[1] & 0x3f) << 6);
5199 unicode += (string[2] & 0x3f);
5202 unicode = (string[0] & 0x0f) << 18;
5203 unicode += ((string[1] & 0x3f) << 12);
5204 unicode += ((string[2] & 0x3f) << 6);
5205 unicode += (string[3] & 0x3f);
5208 unicode = (string[0] & 0x0f) << 24;
5209 unicode += ((string[1] & 0x3f) << 18);
5210 unicode += ((string[2] & 0x3f) << 12);
5211 unicode += ((string[3] & 0x3f) << 6);
5212 unicode += (string[4] & 0x3f);
5215 unicode = (string[0] & 0x01) << 30;
5216 unicode += ((string[1] & 0x3f) << 24);
5217 unicode += ((string[2] & 0x3f) << 18);
5218 unicode += ((string[3] & 0x3f) << 12);
5219 unicode += ((string[4] & 0x3f) << 6);
5220 unicode += (string[5] & 0x3f);
5223 die("Invalid unicode length");
5226 /* Invalid characters could return the special 0xfffd value but NUL
5227 * should be just as good. */
5228 return unicode > 0xffff ? 0 : unicode;
5231 /* Calculates how much of string can be shown within the given maximum width
5232 * and sets trimmed parameter to non-zero value if all of string could not be
5233 * shown. If the reserve flag is TRUE, it will reserve at least one
5234 * trailing character, which can be useful when drawing a delimiter.
5236 * Returns the number of bytes to output from string to satisfy max_width. */
5238 utf8_length(const char *string, size_t max_width, int *trimmed, bool reserve)
5240 const char *start = string;
5241 const char *end = strchr(string, '\0');
5242 unsigned char last_bytes = 0;
5247 while (string < end) {
5248 int c = *(unsigned char *) string;
5249 unsigned char bytes = utf8_bytes[c];
5251 unsigned long unicode;
5253 if (string + bytes > end)
5256 /* Change representation to figure out whether
5257 * it is a single- or double-width character. */
5259 unicode = utf8_to_unicode(string, bytes);
5260 /* FIXME: Graceful handling of invalid unicode character. */
5264 ucwidth = unicode_width(unicode);
5266 if (width > max_width) {
5268 if (reserve && width - ucwidth == max_width) {
5269 string -= last_bytes;
5278 return string - start;
5286 /* Whether or not the curses interface has been initialized. */
5287 static bool cursed = FALSE;
5289 /* The status window is used for polling keystrokes. */
5290 static WINDOW *status_win;
5292 static bool status_empty = TRUE;
5294 /* Update status and title window. */
5296 report(const char *msg, ...)
5298 struct view *view = display[current_view];
5304 char buf[SIZEOF_STR];
5307 va_start(args, msg);
5308 if (vsnprintf(buf, sizeof(buf), msg, args) >= sizeof(buf)) {
5309 buf[sizeof(buf) - 1] = 0;
5310 buf[sizeof(buf) - 2] = '.';
5311 buf[sizeof(buf) - 3] = '.';
5312 buf[sizeof(buf) - 4] = '.';
5318 if (!status_empty || *msg) {
5321 va_start(args, msg);
5323 wmove(status_win, 0, 0);
5325 vwprintw(status_win, msg, args);
5326 status_empty = FALSE;
5328 status_empty = TRUE;
5330 wclrtoeol(status_win);
5331 wrefresh(status_win);
5336 update_view_title(view);
5337 update_display_cursor(view);
5340 /* Controls when nodelay should be in effect when polling user input. */
5342 set_nonblocking_input(bool loading)
5344 static unsigned int loading_views;
5346 if ((loading == FALSE && loading_views-- == 1) ||
5347 (loading == TRUE && loading_views++ == 0))
5348 nodelay(status_win, loading);
5356 /* Initialize the curses library */
5357 if (isatty(STDIN_FILENO)) {
5358 cursed = !!initscr();
5360 /* Leave stdin and stdout alone when acting as a pager. */
5361 FILE *io = fopen("/dev/tty", "r+");
5364 die("Failed to open /dev/tty");
5365 cursed = !!newterm(NULL, io, io);
5369 die("Failed to initialize curses");
5371 nonl(); /* Tell curses not to do NL->CR/NL on output */
5372 cbreak(); /* Take input chars one at a time, no wait for \n */
5373 noecho(); /* Don't echo input */
5374 leaveok(stdscr, TRUE);
5379 getmaxyx(stdscr, y, x);
5380 status_win = newwin(1, 0, y - 1, 0);
5382 die("Failed to create status window");
5384 /* Enable keyboard mapping */
5385 keypad(status_win, TRUE);
5386 wbkgdset(status_win, get_line_attr(LINE_STATUS));
5390 read_prompt(const char *prompt)
5392 enum { READING, STOP, CANCEL } status = READING;
5393 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
5396 while (status == READING) {
5402 foreach_view (view, i)
5407 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
5408 wclrtoeol(status_win);
5410 /* Refresh, accept single keystroke of input */
5411 key = wgetch(status_win);
5416 status = pos ? STOP : CANCEL;
5434 if (pos >= sizeof(buf)) {
5435 report("Input string too long");
5440 buf[pos++] = (char) key;
5444 /* Clear the status window */
5445 status_empty = FALSE;
5448 if (status == CANCEL)
5457 * Repository references
5460 static struct ref *refs = NULL;
5461 static size_t refs_alloc = 0;
5462 static size_t refs_size = 0;
5464 /* Id <-> ref store */
5465 static struct ref ***id_refs = NULL;
5466 static size_t id_refs_alloc = 0;
5467 static size_t id_refs_size = 0;
5469 static struct ref **
5472 struct ref ***tmp_id_refs;
5473 struct ref **ref_list = NULL;
5474 size_t ref_list_alloc = 0;
5475 size_t ref_list_size = 0;
5478 for (i = 0; i < id_refs_size; i++)
5479 if (!strcmp(id, id_refs[i][0]->id))
5482 tmp_id_refs = realloc_items(id_refs, &id_refs_alloc, id_refs_size + 1,
5487 id_refs = tmp_id_refs;
5489 for (i = 0; i < refs_size; i++) {
5492 if (strcmp(id, refs[i].id))
5495 tmp = realloc_items(ref_list, &ref_list_alloc,
5496 ref_list_size + 1, sizeof(*ref_list));
5504 if (ref_list_size > 0)
5505 ref_list[ref_list_size - 1]->next = 1;
5506 ref_list[ref_list_size] = &refs[i];
5508 /* XXX: The properties of the commit chains ensures that we can
5509 * safely modify the shared ref. The repo references will
5510 * always be similar for the same id. */
5511 ref_list[ref_list_size]->next = 0;
5516 id_refs[id_refs_size++] = ref_list;
5522 read_ref(char *id, size_t idlen, char *name, size_t namelen)
5527 bool remote = FALSE;
5528 bool tracked = FALSE;
5529 bool check_replace = FALSE;
5532 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
5533 if (!strcmp(name + namelen - 3, "^{}")) {
5536 if (refs_size > 0 && refs[refs_size - 1].ltag == TRUE)
5537 check_replace = TRUE;
5543 namelen -= STRING_SIZE("refs/tags/");
5544 name += STRING_SIZE("refs/tags/");
5546 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
5548 namelen -= STRING_SIZE("refs/remotes/");
5549 name += STRING_SIZE("refs/remotes/");
5550 tracked = !strcmp(opt_remote, name);
5552 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5553 namelen -= STRING_SIZE("refs/heads/");
5554 name += STRING_SIZE("refs/heads/");
5555 head = !strncmp(opt_head, name, namelen);
5557 } else if (!strcmp(name, "HEAD")) {
5558 opt_no_head = FALSE;
5562 if (check_replace && !strcmp(name, refs[refs_size - 1].name)) {
5563 /* it's an annotated tag, replace the previous sha1 with the
5564 * resolved commit id; relies on the fact git-ls-remote lists
5565 * the commit id of an annotated tag right beofre the commit id
5567 refs[refs_size - 1].ltag = ltag;
5568 string_copy_rev(refs[refs_size - 1].id, id);
5572 refs = realloc_items(refs, &refs_alloc, refs_size + 1, sizeof(*refs));
5576 ref = &refs[refs_size++];
5577 ref->name = malloc(namelen + 1);
5581 strncpy(ref->name, name, namelen);
5582 ref->name[namelen] = 0;
5586 ref->remote = remote;
5587 ref->tracked = tracked;
5588 string_copy_rev(ref->id, id);
5596 const char *cmd_env = getenv("TIG_LS_REMOTE");
5597 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
5599 return read_properties(popen(cmd, "r"), "\t", read_ref);
5603 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
5605 if (!strcmp(name, "i18n.commitencoding"))
5606 string_ncopy(opt_encoding, value, valuelen);
5608 if (!strcmp(name, "core.editor"))
5609 string_ncopy(opt_editor, value, valuelen);
5611 /* branch.<head>.remote */
5613 !strncmp(name, "branch.", 7) &&
5614 !strncmp(name + 7, opt_head, strlen(opt_head)) &&
5615 !strcmp(name + 7 + strlen(opt_head), ".remote"))
5616 string_ncopy(opt_remote, value, valuelen);
5618 if (*opt_head && *opt_remote &&
5619 !strncmp(name, "branch.", 7) &&
5620 !strncmp(name + 7, opt_head, strlen(opt_head)) &&
5621 !strcmp(name + 7 + strlen(opt_head), ".merge")) {
5622 size_t from = strlen(opt_remote);
5624 if (!strncmp(value, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5625 value += STRING_SIZE("refs/heads/");
5626 valuelen -= STRING_SIZE("refs/heads/");
5629 if (!string_format_from(opt_remote, &from, "/%s", value))
5637 load_git_config(void)
5639 return read_properties(popen(GIT_CONFIG " --list", "r"),
5640 "=", read_repo_config_option);
5644 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
5646 if (!opt_git_dir[0]) {
5647 string_ncopy(opt_git_dir, name, namelen);
5649 } else if (opt_is_inside_work_tree == -1) {
5650 /* This can be 3 different values depending on the
5651 * version of git being used. If git-rev-parse does not
5652 * understand --is-inside-work-tree it will simply echo
5653 * the option else either "true" or "false" is printed.
5654 * Default to true for the unknown case. */
5655 opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
5657 } else if (opt_cdup[0] == ' ') {
5658 string_ncopy(opt_cdup, name, namelen);
5660 if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5661 namelen -= STRING_SIZE("refs/heads/");
5662 name += STRING_SIZE("refs/heads/");
5663 string_ncopy(opt_head, name, namelen);
5671 load_repo_info(void)
5674 FILE *pipe = popen("git rev-parse --git-dir --is-inside-work-tree "
5675 " --show-cdup --symbolic-full-name HEAD 2>/dev/null", "r");
5677 /* XXX: The line outputted by "--show-cdup" can be empty so
5678 * initialize it to something invalid to make it possible to
5679 * detect whether it has been set or not. */
5682 result = read_properties(pipe, "=", read_repo_info);
5683 if (opt_cdup[0] == ' ')
5690 read_properties(FILE *pipe, const char *separators,
5691 int (*read_property)(char *, size_t, char *, size_t))
5693 char buffer[BUFSIZ];
5700 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
5705 name = chomp_string(name);
5706 namelen = strcspn(name, separators);
5708 if (name[namelen]) {
5710 value = chomp_string(name + namelen + 1);
5711 valuelen = strlen(value);
5718 state = read_property(name, namelen, value, valuelen);
5721 if (state != ERR && ferror(pipe))
5734 static void __NORETURN
5737 /* XXX: Restore tty modes and let the OS cleanup the rest! */
5743 static void __NORETURN
5744 die(const char *err, ...)
5750 va_start(args, err);
5751 fputs("tig: ", stderr);
5752 vfprintf(stderr, err, args);
5753 fputs("\n", stderr);
5760 warn(const char *msg, ...)
5764 va_start(args, msg);
5765 fputs("tig warning: ", stderr);
5766 vfprintf(stderr, msg, args);
5767 fputs("\n", stderr);
5772 main(int argc, char *argv[])
5775 enum request request;
5778 signal(SIGINT, quit);
5780 if (setlocale(LC_ALL, "")) {
5781 char *codeset = nl_langinfo(CODESET);
5783 string_ncopy(opt_codeset, codeset, strlen(codeset));
5786 if (load_repo_info() == ERR)
5787 die("Failed to load repo info.");
5789 if (load_options() == ERR)
5790 die("Failed to load user config.");
5792 if (load_git_config() == ERR)
5793 die("Failed to load repo config.");
5795 if (!parse_options(argc, argv))
5798 /* Require a git repository unless when running in pager mode. */
5799 if (!opt_git_dir[0] && opt_request != REQ_VIEW_PAGER)
5800 die("Not a git repository");
5802 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
5805 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
5806 opt_iconv = iconv_open(opt_codeset, opt_encoding);
5807 if (opt_iconv == ICONV_NONE)
5808 die("Failed to initialize character set conversion");
5811 if (*opt_git_dir && load_refs() == ERR)
5812 die("Failed to load refs.");
5814 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
5815 view->cmd_env = getenv(view->cmd_env);
5817 request = opt_request;
5821 while (view_driver(display[current_view], request)) {
5825 foreach_view (view, i)
5828 /* Refresh, accept single keystroke of input */
5829 key = wgetch(status_win);
5831 /* wgetch() with nodelay() enabled returns ERR when there's no
5838 request = get_keybinding(display[current_view]->keymap, key);
5840 /* Some low-level request handling. This keeps access to
5841 * status_win restricted. */
5845 char *cmd = read_prompt(":");
5847 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
5848 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
5849 opt_request = REQ_VIEW_DIFF;
5851 opt_request = REQ_VIEW_PAGER;
5860 case REQ_SEARCH_BACK:
5862 const char *prompt = request == REQ_SEARCH
5864 char *search = read_prompt(prompt);
5867 string_ncopy(opt_search, search, strlen(search));
5872 case REQ_SCREEN_RESIZE:
5876 getmaxyx(stdscr, height, width);
5878 /* Resize the status view and let the view driver take
5879 * care of resizing the displayed views. */
5880 wresize(status_win, 1, width);
5881 mvwin(status_win, height - 1, 0);
5882 wrefresh(status_win);