1 /* Copyright (c) 2006-2008 Jonas Fonseca <fonseca@diku.dk>
3 * This program is free software; you can redistribute it and/or
4 * modify it under the terms of the GNU General Public License as
5 * published by the Free Software Foundation; either version 2 of
6 * the License, or (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
19 #define TIG_VERSION "unknown-version"
34 #include <sys/types.h>
45 /* ncurses(3): Must be defined to have extended wide-character functions. */
46 #define _XOPEN_SOURCE_EXTENDED
48 #ifdef HAVE_NCURSESW_NCURSES_H
49 #include <ncursesw/ncurses.h>
51 #ifdef HAVE_NCURSES_NCURSES_H
52 #include <ncurses/ncurses.h>
59 #define __NORETURN __attribute__((__noreturn__))
64 static void __NORETURN die(const char *err, ...);
65 static void warn(const char *msg, ...);
66 static void report(const char *msg, ...);
67 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, size_t, char *, size_t));
68 static void set_nonblocking_input(bool loading);
69 static size_t utf8_length(const char *string, int *width, size_t max_width, int *trimmed, bool reserve);
71 #define ABS(x) ((x) >= 0 ? (x) : -(x))
72 #define MIN(x, y) ((x) < (y) ? (x) : (y))
74 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
75 #define STRING_SIZE(x) (sizeof(x) - 1)
77 #define SIZEOF_STR 1024 /* Default string size. */
78 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
79 #define SIZEOF_REV 41 /* Holds a SHA-1 and an ending NUL */
83 #define REVGRAPH_INIT 'I'
84 #define REVGRAPH_MERGE 'M'
85 #define REVGRAPH_BRANCH '+'
86 #define REVGRAPH_COMMIT '*'
87 #define REVGRAPH_BOUND '^'
89 #define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
91 /* This color name can be used to refer to the default term colors. */
92 #define COLOR_DEFAULT (-1)
94 #define ICONV_NONE ((iconv_t) -1)
96 #define ICONV_CONST /* nothing */
99 /* The format and size of the date column in the main view. */
100 #define DATE_FORMAT "%Y-%m-%d %H:%M"
101 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
103 #define AUTHOR_COLS 20
106 /* The default interval between line numbers. */
107 #define NUMBER_INTERVAL 5
111 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
113 #define NULL_ID "0000000000000000000000000000000000000000"
116 #define GIT_CONFIG "git config"
119 #define TIG_LS_REMOTE \
120 "git ls-remote . 2>/dev/null"
122 #define TIG_DIFF_CMD \
123 "git show --pretty=fuller --no-color --root --patch-with-stat --find-copies-harder -C %s 2>/dev/null"
125 #define TIG_LOG_CMD \
126 "git log --no-color --cc --stat -n100 %s 2>/dev/null"
128 #define TIG_MAIN_CMD \
129 "git log --no-color --topo-order --parents --boundary --pretty=raw %s 2>/dev/null"
131 #define TIG_TREE_CMD \
134 #define TIG_BLOB_CMD \
135 "git cat-file blob %s"
137 /* XXX: Needs to be defined to the empty string. */
138 #define TIG_HELP_CMD ""
139 #define TIG_PAGER_CMD ""
140 #define TIG_STATUS_CMD ""
141 #define TIG_STAGE_CMD ""
142 #define TIG_BLAME_CMD ""
144 /* Some ascii-shorthands fitted into the ncurses namespace. */
146 #define KEY_RETURN '\r'
151 char *name; /* Ref name; tag or head names are shortened. */
152 char id[SIZEOF_REV]; /* Commit SHA1 ID */
153 unsigned int head:1; /* Is it the current HEAD? */
154 unsigned int tag:1; /* Is it a tag? */
155 unsigned int ltag:1; /* If so, is the tag local? */
156 unsigned int remote:1; /* Is it a remote ref? */
157 unsigned int tracked:1; /* Is it the remote for the current HEAD? */
158 unsigned int next:1; /* For ref lists: are there more refs? */
161 static struct ref **get_refs(char *id);
170 set_from_int_map(struct int_map *map, size_t map_size,
171 int *value, const char *name, int namelen)
176 for (i = 0; i < map_size; i++)
177 if (namelen == map[i].namelen &&
178 !strncasecmp(name, map[i].name, namelen)) {
179 *value = map[i].value;
192 string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
194 if (srclen > dstlen - 1)
197 strncpy(dst, src, srclen);
201 /* Shorthands for safely copying into a fixed buffer. */
203 #define string_copy(dst, src) \
204 string_ncopy_do(dst, sizeof(dst), src, sizeof(src))
206 #define string_ncopy(dst, src, srclen) \
207 string_ncopy_do(dst, sizeof(dst), src, srclen)
209 #define string_copy_rev(dst, src) \
210 string_ncopy_do(dst, SIZEOF_REV, src, SIZEOF_REV - 1)
212 #define string_add(dst, from, src) \
213 string_ncopy_do(dst + (from), sizeof(dst) - (from), src, sizeof(src))
216 chomp_string(char *name)
220 while (isspace(*name))
223 namelen = strlen(name) - 1;
224 while (namelen > 0 && isspace(name[namelen]))
231 string_nformat(char *buf, size_t bufsize, size_t *bufpos, const char *fmt, ...)
234 size_t pos = bufpos ? *bufpos : 0;
237 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
243 return pos >= bufsize ? FALSE : TRUE;
246 #define string_format(buf, fmt, args...) \
247 string_nformat(buf, sizeof(buf), NULL, fmt, args)
249 #define string_format_from(buf, from, fmt, args...) \
250 string_nformat(buf, sizeof(buf), from, fmt, args)
253 string_enum_compare(const char *str1, const char *str2, int len)
257 #define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
259 /* Diff-Header == DIFF_HEADER */
260 for (i = 0; i < len; i++) {
261 if (toupper(str1[i]) == toupper(str2[i]))
264 if (string_enum_sep(str1[i]) &&
265 string_enum_sep(str2[i]))
268 return str1[i] - str2[i];
276 * NOTE: The following is a slightly modified copy of the git project's shell
277 * quoting routines found in the quote.c file.
279 * Help to copy the thing properly quoted for the shell safety. any single
280 * quote is replaced with '\'', any exclamation point is replaced with '\!',
281 * and the whole thing is enclosed in a
284 * original sq_quote result
285 * name ==> name ==> 'name'
286 * a b ==> a b ==> 'a b'
287 * a'b ==> a'\''b ==> 'a'\''b'
288 * a!b ==> a'\!'b ==> 'a'\!'b'
292 sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
296 #define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0)
299 while ((c = *src++)) {
300 if (c == '\'' || c == '!') {
311 if (bufsize < SIZEOF_STR)
323 /* XXX: Keep the view request first and in sync with views[]. */ \
324 REQ_GROUP("View switching") \
325 REQ_(VIEW_MAIN, "Show main view"), \
326 REQ_(VIEW_DIFF, "Show diff view"), \
327 REQ_(VIEW_LOG, "Show log view"), \
328 REQ_(VIEW_TREE, "Show tree view"), \
329 REQ_(VIEW_BLOB, "Show blob view"), \
330 REQ_(VIEW_BLAME, "Show blame view"), \
331 REQ_(VIEW_HELP, "Show help page"), \
332 REQ_(VIEW_PAGER, "Show pager view"), \
333 REQ_(VIEW_STATUS, "Show status view"), \
334 REQ_(VIEW_STAGE, "Show stage view"), \
336 REQ_GROUP("View manipulation") \
337 REQ_(ENTER, "Enter current line and scroll"), \
338 REQ_(NEXT, "Move to next"), \
339 REQ_(PREVIOUS, "Move to previous"), \
340 REQ_(VIEW_NEXT, "Move focus to next view"), \
341 REQ_(REFRESH, "Reload and refresh"), \
342 REQ_(MAXIMIZE, "Maximize the current view"), \
343 REQ_(VIEW_CLOSE, "Close the current view"), \
344 REQ_(QUIT, "Close all views and quit"), \
346 REQ_GROUP("Cursor navigation") \
347 REQ_(MOVE_UP, "Move cursor one line up"), \
348 REQ_(MOVE_DOWN, "Move cursor one line down"), \
349 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
350 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
351 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
352 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
354 REQ_GROUP("Scrolling") \
355 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
356 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
357 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
358 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
360 REQ_GROUP("Searching") \
361 REQ_(SEARCH, "Search the view"), \
362 REQ_(SEARCH_BACK, "Search backwards in the view"), \
363 REQ_(FIND_NEXT, "Find next search match"), \
364 REQ_(FIND_PREV, "Find previous search match"), \
367 REQ_(PROMPT, "Bring up the prompt"), \
368 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
369 REQ_(SCREEN_RESIZE, "Resize the screen"), \
370 REQ_(SHOW_VERSION, "Show version information"), \
371 REQ_(STOP_LOADING, "Stop all loading views"), \
372 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
373 REQ_(TOGGLE_DATE, "Toggle date display"), \
374 REQ_(TOGGLE_AUTHOR, "Toggle author display"), \
375 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"), \
376 REQ_(TOGGLE_REFS, "Toggle reference display (tags/branches)"), \
377 REQ_(STATUS_UPDATE, "Update file status"), \
378 REQ_(STATUS_MERGE, "Merge file using external tool"), \
379 REQ_(TREE_PARENT, "Switch to parent directory in tree view"), \
380 REQ_(EDIT, "Open in editor"), \
381 REQ_(NONE, "Do nothing")
384 /* User action requests. */
386 #define REQ_GROUP(help)
387 #define REQ_(req, help) REQ_##req
389 /* Offset all requests to avoid conflicts with ncurses getch values. */
390 REQ_OFFSET = KEY_MAX + 1,
397 struct request_info {
398 enum request request;
404 static struct request_info req_info[] = {
405 #define REQ_GROUP(help) { 0, NULL, 0, (help) },
406 #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
413 get_request(const char *name)
415 int namelen = strlen(name);
418 for (i = 0; i < ARRAY_SIZE(req_info); i++)
419 if (req_info[i].namelen == namelen &&
420 !string_enum_compare(req_info[i].name, name, namelen))
421 return req_info[i].request;
431 static const char usage[] =
432 "tig " TIG_VERSION " (" __DATE__ ")\n"
434 "Usage: tig [options] [revs] [--] [paths]\n"
435 " or: tig show [options] [revs] [--] [paths]\n"
436 " or: tig blame [rev] path\n"
438 " or: tig < [git command output]\n"
441 " -v, --version Show version and exit\n"
442 " -h, --help Show help message and exit";
444 /* Option and state variables. */
445 static bool opt_date = TRUE;
446 static bool opt_author = TRUE;
447 static bool opt_line_number = FALSE;
448 static bool opt_line_graphics = TRUE;
449 static bool opt_rev_graph = FALSE;
450 static bool opt_show_refs = TRUE;
451 static int opt_num_interval = NUMBER_INTERVAL;
452 static int opt_tab_size = TAB_SIZE;
453 static enum request opt_request = REQ_VIEW_MAIN;
454 static char opt_cmd[SIZEOF_STR] = "";
455 static char opt_path[SIZEOF_STR] = "";
456 static char opt_file[SIZEOF_STR] = "";
457 static char opt_ref[SIZEOF_REF] = "";
458 static char opt_head[SIZEOF_REF] = "";
459 static char opt_remote[SIZEOF_REF] = "";
460 static bool opt_no_head = TRUE;
461 static FILE *opt_pipe = NULL;
462 static char opt_encoding[20] = "UTF-8";
463 static bool opt_utf8 = TRUE;
464 static char opt_codeset[20] = "UTF-8";
465 static iconv_t opt_iconv = ICONV_NONE;
466 static char opt_search[SIZEOF_STR] = "";
467 static char opt_cdup[SIZEOF_STR] = "";
468 static char opt_git_dir[SIZEOF_STR] = "";
469 static signed char opt_is_inside_work_tree = -1; /* set to TRUE or FALSE */
470 static char opt_editor[SIZEOF_STR] = "";
473 parse_options(int argc, char *argv[])
477 bool seen_dashdash = FALSE;
480 if (!isatty(STDIN_FILENO)) {
481 opt_request = REQ_VIEW_PAGER;
489 subcommand = argv[1];
490 if (!strcmp(subcommand, "status") || !strcmp(subcommand, "-S")) {
491 opt_request = REQ_VIEW_STATUS;
492 if (!strcmp(subcommand, "-S"))
493 warn("`-S' has been deprecated; use `tig status' instead");
495 warn("ignoring arguments after `%s'", subcommand);
498 } else if (!strcmp(subcommand, "blame")) {
499 opt_request = REQ_VIEW_BLAME;
500 if (argc <= 2 || argc > 4)
501 die("invalid number of options to blame\n\n%s", usage);
505 string_ncopy(opt_ref, argv[i], strlen(argv[i]));
509 string_ncopy(opt_file, argv[i], strlen(argv[i]));
512 } else if (!strcmp(subcommand, "show")) {
513 opt_request = REQ_VIEW_DIFF;
515 } else if (!strcmp(subcommand, "log") || !strcmp(subcommand, "diff")) {
516 opt_request = subcommand[0] == 'l'
517 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
518 warn("`tig %s' has been deprecated", subcommand);
525 /* XXX: This is vulnerable to the user overriding
526 * options required for the main view parser. */
527 string_copy(opt_cmd, "git log --no-color --pretty=raw --boundary --parents");
529 string_format(opt_cmd, "git %s", subcommand);
531 buf_size = strlen(opt_cmd);
533 for (i = 1 + !!subcommand; i < argc; i++) {
536 if (seen_dashdash || !strcmp(opt, "--")) {
537 seen_dashdash = TRUE;
539 } else if (!strcmp(opt, "-v") || !strcmp(opt, "--version")) {
540 printf("tig version %s\n", TIG_VERSION);
543 } else if (!strcmp(opt, "-h") || !strcmp(opt, "--help")) {
544 printf("%s\n", usage);
548 opt_cmd[buf_size++] = ' ';
549 buf_size = sq_quote(opt_cmd, buf_size, opt);
550 if (buf_size >= sizeof(opt_cmd))
551 die("command too long");
554 opt_cmd[buf_size] = 0;
561 * Line-oriented content detection.
565 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
566 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
567 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
568 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
569 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
570 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
571 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
572 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
573 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
574 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
575 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
576 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
577 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
578 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
579 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
580 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
581 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
582 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
583 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
584 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
585 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
586 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
587 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
588 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
589 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
590 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
591 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
592 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
593 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
594 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
595 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
596 LINE(DELIMITER, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
597 LINE(DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
598 LINE(LINE_NUMBER, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
599 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
600 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
601 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
602 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
603 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
604 LINE(MAIN_LOCAL_TAG,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
605 LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \
606 LINE(MAIN_TRACKED, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
607 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
608 LINE(MAIN_HEAD, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
609 LINE(MAIN_REVGRAPH,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
610 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
611 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
612 LINE(STAT_HEAD, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \
613 LINE(STAT_SECTION, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
614 LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
615 LINE(STAT_STAGED, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
616 LINE(STAT_UNSTAGED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
617 LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
618 LINE(BLAME_ID, "", COLOR_MAGENTA, COLOR_DEFAULT, 0)
621 #define LINE(type, line, fg, bg, attr) \
629 const char *name; /* Option name. */
630 int namelen; /* Size of option name. */
631 const char *line; /* The start of line to match. */
632 int linelen; /* Size of string to match. */
633 int fg, bg, attr; /* Color and text attributes for the lines. */
636 static struct line_info line_info[] = {
637 #define LINE(type, line, fg, bg, attr) \
638 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
643 static enum line_type
644 get_line_type(char *line)
646 int linelen = strlen(line);
649 for (type = 0; type < ARRAY_SIZE(line_info); type++)
650 /* Case insensitive search matches Signed-off-by lines better. */
651 if (linelen >= line_info[type].linelen &&
652 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
659 get_line_attr(enum line_type type)
661 assert(type < ARRAY_SIZE(line_info));
662 return COLOR_PAIR(type) | line_info[type].attr;
665 static struct line_info *
666 get_line_info(char *name)
668 size_t namelen = strlen(name);
671 for (type = 0; type < ARRAY_SIZE(line_info); type++)
672 if (namelen == line_info[type].namelen &&
673 !string_enum_compare(line_info[type].name, name, namelen))
674 return &line_info[type];
682 int default_bg = line_info[LINE_DEFAULT].bg;
683 int default_fg = line_info[LINE_DEFAULT].fg;
688 if (assume_default_colors(default_fg, default_bg) == ERR) {
689 default_bg = COLOR_BLACK;
690 default_fg = COLOR_WHITE;
693 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
694 struct line_info *info = &line_info[type];
695 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
696 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
698 init_pair(type, fg, bg);
706 unsigned int selected:1;
707 unsigned int dirty:1;
709 void *data; /* User data */
719 enum request request;
720 struct keybinding *next;
723 static struct keybinding default_keybindings[] = {
725 { 'm', REQ_VIEW_MAIN },
726 { 'd', REQ_VIEW_DIFF },
727 { 'l', REQ_VIEW_LOG },
728 { 't', REQ_VIEW_TREE },
729 { 'f', REQ_VIEW_BLOB },
730 { 'B', REQ_VIEW_BLAME },
731 { 'p', REQ_VIEW_PAGER },
732 { 'h', REQ_VIEW_HELP },
733 { 'S', REQ_VIEW_STATUS },
734 { 'c', REQ_VIEW_STAGE },
736 /* View manipulation */
737 { 'q', REQ_VIEW_CLOSE },
738 { KEY_TAB, REQ_VIEW_NEXT },
739 { KEY_RETURN, REQ_ENTER },
740 { KEY_UP, REQ_PREVIOUS },
741 { KEY_DOWN, REQ_NEXT },
742 { 'R', REQ_REFRESH },
743 { KEY_F(5), REQ_REFRESH },
744 { 'O', REQ_MAXIMIZE },
746 /* Cursor navigation */
747 { 'k', REQ_MOVE_UP },
748 { 'j', REQ_MOVE_DOWN },
749 { KEY_HOME, REQ_MOVE_FIRST_LINE },
750 { KEY_END, REQ_MOVE_LAST_LINE },
751 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
752 { ' ', REQ_MOVE_PAGE_DOWN },
753 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
754 { 'b', REQ_MOVE_PAGE_UP },
755 { '-', REQ_MOVE_PAGE_UP },
758 { KEY_IC, REQ_SCROLL_LINE_UP },
759 { KEY_DC, REQ_SCROLL_LINE_DOWN },
760 { 'w', REQ_SCROLL_PAGE_UP },
761 { 's', REQ_SCROLL_PAGE_DOWN },
765 { '?', REQ_SEARCH_BACK },
766 { 'n', REQ_FIND_NEXT },
767 { 'N', REQ_FIND_PREV },
771 { 'z', REQ_STOP_LOADING },
772 { 'v', REQ_SHOW_VERSION },
773 { 'r', REQ_SCREEN_REDRAW },
774 { '.', REQ_TOGGLE_LINENO },
775 { 'D', REQ_TOGGLE_DATE },
776 { 'A', REQ_TOGGLE_AUTHOR },
777 { 'g', REQ_TOGGLE_REV_GRAPH },
778 { 'F', REQ_TOGGLE_REFS },
780 { 'u', REQ_STATUS_UPDATE },
781 { 'M', REQ_STATUS_MERGE },
782 { ',', REQ_TREE_PARENT },
785 /* Using the ncurses SIGWINCH handler. */
786 { KEY_RESIZE, REQ_SCREEN_RESIZE },
789 #define KEYMAP_INFO \
803 #define KEYMAP_(name) KEYMAP_##name
808 static struct int_map keymap_table[] = {
809 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
814 #define set_keymap(map, name) \
815 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
817 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
820 add_keybinding(enum keymap keymap, enum request request, int key)
822 struct keybinding *keybinding;
824 keybinding = calloc(1, sizeof(*keybinding));
826 die("Failed to allocate keybinding");
828 keybinding->alias = key;
829 keybinding->request = request;
830 keybinding->next = keybindings[keymap];
831 keybindings[keymap] = keybinding;
834 /* Looks for a key binding first in the given map, then in the generic map, and
835 * lastly in the default keybindings. */
837 get_keybinding(enum keymap keymap, int key)
839 struct keybinding *kbd;
842 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
843 if (kbd->alias == key)
846 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
847 if (kbd->alias == key)
850 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
851 if (default_keybindings[i].alias == key)
852 return default_keybindings[i].request;
854 return (enum request) key;
863 static struct key key_table[] = {
864 { "Enter", KEY_RETURN },
866 { "Backspace", KEY_BACKSPACE },
868 { "Escape", KEY_ESC },
869 { "Left", KEY_LEFT },
870 { "Right", KEY_RIGHT },
872 { "Down", KEY_DOWN },
873 { "Insert", KEY_IC },
874 { "Delete", KEY_DC },
876 { "Home", KEY_HOME },
878 { "PageUp", KEY_PPAGE },
879 { "PageDown", KEY_NPAGE },
889 { "F10", KEY_F(10) },
890 { "F11", KEY_F(11) },
891 { "F12", KEY_F(12) },
895 get_key_value(const char *name)
899 for (i = 0; i < ARRAY_SIZE(key_table); i++)
900 if (!strcasecmp(key_table[i].name, name))
901 return key_table[i].value;
903 if (strlen(name) == 1 && isprint(*name))
910 get_key_name(int key_value)
912 static char key_char[] = "'X'";
916 for (key = 0; key < ARRAY_SIZE(key_table); key++)
917 if (key_table[key].value == key_value)
918 seq = key_table[key].name;
922 isprint(key_value)) {
923 key_char[1] = (char) key_value;
927 return seq ? seq : "'?'";
931 get_key(enum request request)
933 static char buf[BUFSIZ];
940 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
941 struct keybinding *keybinding = &default_keybindings[i];
943 if (keybinding->request != request)
946 if (!string_format_from(buf, &pos, "%s%s", sep,
947 get_key_name(keybinding->alias)))
948 return "Too many keybindings!";
958 char cmd[SIZEOF_STR];
961 static struct run_request *run_request;
962 static size_t run_requests;
965 add_run_request(enum keymap keymap, int key, int argc, char **argv)
967 struct run_request *req;
968 char cmd[SIZEOF_STR];
971 for (bufpos = 0; argc > 0; argc--, argv++)
972 if (!string_format_from(cmd, &bufpos, "%s ", *argv))
975 req = realloc(run_request, (run_requests + 1) * sizeof(*run_request));
980 req = &run_request[run_requests++];
981 string_copy(req->cmd, cmd);
982 req->keymap = keymap;
985 return REQ_NONE + run_requests;
988 static struct run_request *
989 get_run_request(enum request request)
991 if (request <= REQ_NONE)
993 return &run_request[request - REQ_NONE - 1];
997 add_builtin_run_requests(void)
1004 { KEYMAP_MAIN, 'C', { "git cherry-pick %(commit)" } },
1005 { KEYMAP_GENERIC, 'G', { "git gc" } },
1009 for (i = 0; i < ARRAY_SIZE(reqs); i++) {
1012 req = add_run_request(reqs[i].keymap, reqs[i].key, 1, reqs[i].argv);
1013 if (req != REQ_NONE)
1014 add_keybinding(reqs[i].keymap, req, reqs[i].key);
1019 * User config file handling.
1022 static struct int_map color_map[] = {
1023 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
1035 #define set_color(color, name) \
1036 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
1038 static struct int_map attr_map[] = {
1039 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
1046 ATTR_MAP(UNDERLINE),
1049 #define set_attribute(attr, name) \
1050 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
1052 static int config_lineno;
1053 static bool config_errors;
1054 static char *config_msg;
1056 /* Wants: object fgcolor bgcolor [attr] */
1058 option_color_command(int argc, char *argv[])
1060 struct line_info *info;
1062 if (argc != 3 && argc != 4) {
1063 config_msg = "Wrong number of arguments given to color command";
1067 info = get_line_info(argv[0]);
1069 if (!string_enum_compare(argv[0], "main-delim", strlen("main-delim"))) {
1070 info = get_line_info("delimiter");
1072 } else if (!string_enum_compare(argv[0], "main-date", strlen("main-date"))) {
1073 info = get_line_info("date");
1076 config_msg = "Unknown color name";
1081 if (set_color(&info->fg, argv[1]) == ERR ||
1082 set_color(&info->bg, argv[2]) == ERR) {
1083 config_msg = "Unknown color";
1087 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
1088 config_msg = "Unknown attribute";
1095 static bool parse_bool(const char *s)
1097 return (!strcmp(s, "1") || !strcmp(s, "true") ||
1098 !strcmp(s, "yes")) ? TRUE : FALSE;
1101 /* Wants: name = value */
1103 option_set_command(int argc, char *argv[])
1106 config_msg = "Wrong number of arguments given to set command";
1110 if (strcmp(argv[1], "=")) {
1111 config_msg = "No value assigned";
1115 if (!strcmp(argv[0], "show-author")) {
1116 opt_author = parse_bool(argv[2]);
1120 if (!strcmp(argv[0], "show-date")) {
1121 opt_date = parse_bool(argv[2]);
1125 if (!strcmp(argv[0], "show-rev-graph")) {
1126 opt_rev_graph = parse_bool(argv[2]);
1130 if (!strcmp(argv[0], "show-refs")) {
1131 opt_show_refs = parse_bool(argv[2]);
1135 if (!strcmp(argv[0], "show-line-numbers")) {
1136 opt_line_number = parse_bool(argv[2]);
1140 if (!strcmp(argv[0], "line-graphics")) {
1141 opt_line_graphics = parse_bool(argv[2]);
1145 if (!strcmp(argv[0], "line-number-interval")) {
1146 opt_num_interval = atoi(argv[2]);
1150 if (!strcmp(argv[0], "tab-size")) {
1151 opt_tab_size = atoi(argv[2]);
1155 if (!strcmp(argv[0], "commit-encoding")) {
1156 char *arg = argv[2];
1157 int delimiter = *arg;
1160 switch (delimiter) {
1163 for (arg++, i = 0; arg[i]; i++)
1164 if (arg[i] == delimiter) {
1169 string_ncopy(opt_encoding, arg, strlen(arg));
1174 config_msg = "Unknown variable name";
1178 /* Wants: mode request key */
1180 option_bind_command(int argc, char *argv[])
1182 enum request request;
1187 config_msg = "Wrong number of arguments given to bind command";
1191 if (set_keymap(&keymap, argv[0]) == ERR) {
1192 config_msg = "Unknown key map";
1196 key = get_key_value(argv[1]);
1198 config_msg = "Unknown key";
1202 request = get_request(argv[2]);
1203 if (request == REQ_NONE) {
1204 const char *obsolete[] = { "cherry-pick" };
1205 size_t namelen = strlen(argv[2]);
1208 for (i = 0; i < ARRAY_SIZE(obsolete); i++) {
1209 if (namelen == strlen(obsolete[i]) &&
1210 !string_enum_compare(obsolete[i], argv[2], namelen)) {
1211 config_msg = "Obsolete request name";
1216 if (request == REQ_NONE && *argv[2]++ == '!')
1217 request = add_run_request(keymap, key, argc - 2, argv + 2);
1218 if (request == REQ_NONE) {
1219 config_msg = "Unknown request name";
1223 add_keybinding(keymap, request, key);
1229 set_option(char *opt, char *value)
1236 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1237 argv[argc++] = value;
1240 /* Nothing more to tokenize or last available token. */
1241 if (!*value || argc >= ARRAY_SIZE(argv))
1245 while (isspace(*value))
1249 if (!strcmp(opt, "color"))
1250 return option_color_command(argc, argv);
1252 if (!strcmp(opt, "set"))
1253 return option_set_command(argc, argv);
1255 if (!strcmp(opt, "bind"))
1256 return option_bind_command(argc, argv);
1258 config_msg = "Unknown option command";
1263 read_option(char *opt, size_t optlen, char *value, size_t valuelen)
1268 config_msg = "Internal error";
1270 /* Check for comment markers, since read_properties() will
1271 * only ensure opt and value are split at first " \t". */
1272 optlen = strcspn(opt, "#");
1276 if (opt[optlen] != 0) {
1277 config_msg = "No option value";
1281 /* Look for comment endings in the value. */
1282 size_t len = strcspn(value, "#");
1284 if (len < valuelen) {
1286 value[valuelen] = 0;
1289 status = set_option(opt, value);
1292 if (status == ERR) {
1293 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1294 config_lineno, (int) optlen, opt, config_msg);
1295 config_errors = TRUE;
1298 /* Always keep going if errors are encountered. */
1303 load_option_file(const char *path)
1307 /* It's ok that the file doesn't exist. */
1308 file = fopen(path, "r");
1313 config_errors = FALSE;
1315 if (read_properties(file, " \t", read_option) == ERR ||
1316 config_errors == TRUE)
1317 fprintf(stderr, "Errors while loading %s.\n", path);
1323 char *home = getenv("HOME");
1324 char *tigrc_user = getenv("TIGRC_USER");
1325 char *tigrc_system = getenv("TIGRC_SYSTEM");
1326 char buf[SIZEOF_STR];
1328 add_builtin_run_requests();
1330 if (!tigrc_system) {
1331 if (!string_format(buf, "%s/tigrc", SYSCONFDIR))
1335 load_option_file(tigrc_system);
1338 if (!home || !string_format(buf, "%s/.tigrc", home))
1342 load_option_file(tigrc_user);
1355 /* The display array of active views and the index of the current view. */
1356 static struct view *display[2];
1357 static unsigned int current_view;
1359 /* Reading from the prompt? */
1360 static bool input_mode = FALSE;
1362 #define foreach_displayed_view(view, i) \
1363 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1365 #define displayed_views() (display[1] != NULL ? 2 : 1)
1367 /* Current head and commit ID */
1368 static char ref_blob[SIZEOF_REF] = "";
1369 static char ref_commit[SIZEOF_REF] = "HEAD";
1370 static char ref_head[SIZEOF_REF] = "HEAD";
1373 const char *name; /* View name */
1374 const char *cmd_fmt; /* Default command line format */
1375 const char *cmd_env; /* Command line set via environment */
1376 const char *id; /* Points to either of ref_{head,commit,blob} */
1378 struct view_ops *ops; /* View operations */
1380 enum keymap keymap; /* What keymap does this view have */
1381 bool git_dir; /* Whether the view requires a git directory. */
1383 char cmd[SIZEOF_STR]; /* Command buffer */
1384 char ref[SIZEOF_REF]; /* Hovered commit reference */
1385 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1387 int height, width; /* The width and height of the main window */
1388 WINDOW *win; /* The main window */
1389 WINDOW *title; /* The title window living below the main window */
1392 unsigned long offset; /* Offset of the window top */
1393 unsigned long lineno; /* Current line number */
1396 char grep[SIZEOF_STR]; /* Search string */
1397 regex_t *regex; /* Pre-compiled regex */
1399 /* If non-NULL, points to the view that opened this view. If this view
1400 * is closed tig will switch back to the parent view. */
1401 struct view *parent;
1404 size_t lines; /* Total number of lines */
1405 struct line *line; /* Line index */
1406 size_t line_alloc; /* Total number of allocated lines */
1407 size_t line_size; /* Total number of used lines */
1408 unsigned int digits; /* Number of digits in the lines member. */
1411 struct line *curline; /* Line currently being drawn. */
1412 enum line_type curtype; /* Attribute currently used for drawing. */
1413 unsigned long col; /* Column when drawing. */
1421 /* What type of content being displayed. Used in the title bar. */
1423 /* Open and reads in all view content. */
1424 bool (*open)(struct view *view);
1425 /* Read one line; updates view->line. */
1426 bool (*read)(struct view *view, char *data);
1427 /* Draw one line; @lineno must be < view->height. */
1428 bool (*draw)(struct view *view, struct line *line, unsigned int lineno);
1429 /* Depending on view handle a special requests. */
1430 enum request (*request)(struct view *view, enum request request, struct line *line);
1431 /* Search for regex in a line. */
1432 bool (*grep)(struct view *view, struct line *line);
1434 void (*select)(struct view *view, struct line *line);
1437 static struct view_ops pager_ops;
1438 static struct view_ops main_ops;
1439 static struct view_ops tree_ops;
1440 static struct view_ops blob_ops;
1441 static struct view_ops blame_ops;
1442 static struct view_ops help_ops;
1443 static struct view_ops status_ops;
1444 static struct view_ops stage_ops;
1446 #define VIEW_STR(name, cmd, env, ref, ops, map, git) \
1447 { name, cmd, #env, ref, ops, map, git }
1449 #define VIEW_(id, name, ops, git, ref) \
1450 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id, git)
1453 static struct view views[] = {
1454 VIEW_(MAIN, "main", &main_ops, TRUE, ref_head),
1455 VIEW_(DIFF, "diff", &pager_ops, TRUE, ref_commit),
1456 VIEW_(LOG, "log", &pager_ops, TRUE, ref_head),
1457 VIEW_(TREE, "tree", &tree_ops, TRUE, ref_commit),
1458 VIEW_(BLOB, "blob", &blob_ops, TRUE, ref_blob),
1459 VIEW_(BLAME, "blame", &blame_ops, TRUE, ref_commit),
1460 VIEW_(HELP, "help", &help_ops, FALSE, ""),
1461 VIEW_(PAGER, "pager", &pager_ops, FALSE, "stdin"),
1462 VIEW_(STATUS, "status", &status_ops, TRUE, ""),
1463 VIEW_(STAGE, "stage", &stage_ops, TRUE, ""),
1466 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1467 #define VIEW_REQ(view) ((view) - views + REQ_OFFSET + 1)
1469 #define foreach_view(view, i) \
1470 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1472 #define view_is_displayed(view) \
1473 (view == display[0] || view == display[1])
1480 static int line_graphics[] = {
1481 /* LINE_GRAPHIC_VLINE: */ '|'
1485 set_view_attr(struct view *view, enum line_type type)
1487 if (!view->curline->selected && view->curtype != type) {
1488 wattrset(view->win, get_line_attr(type));
1489 wchgat(view->win, -1, 0, type, NULL);
1490 view->curtype = type;
1495 draw_chars(struct view *view, enum line_type type, const char *string,
1496 int max_len, bool use_tilde)
1500 int trimmed = FALSE;
1506 len = utf8_length(string, &col, max_len, &trimmed, use_tilde);
1508 col = len = strlen(string);
1509 if (len > max_len) {
1513 col = len = max_len;
1518 set_view_attr(view, type);
1519 waddnstr(view->win, string, len);
1520 if (trimmed && use_tilde) {
1521 set_view_attr(view, LINE_DELIMITER);
1522 waddch(view->win, '~');
1530 draw_space(struct view *view, enum line_type type, int max, int spaces)
1532 static char space[] = " ";
1535 spaces = MIN(max, spaces);
1537 while (spaces > 0) {
1538 int len = MIN(spaces, sizeof(space) - 1);
1540 col += draw_chars(view, type, space, spaces, FALSE);
1548 draw_lineno(struct view *view, unsigned int lineno)
1551 int digits3 = view->digits < 3 ? 3 : view->digits;
1552 int max_number = MIN(digits3, STRING_SIZE(number));
1553 int max = view->width - view->col;
1556 if (max < max_number)
1559 lineno += view->offset + 1;
1560 if (lineno == 1 || (lineno % opt_num_interval) == 0) {
1561 static char fmt[] = "%1ld";
1563 if (view->digits <= 9)
1564 fmt[1] = '0' + digits3;
1566 if (!string_format(number, fmt, lineno))
1568 col = draw_chars(view, LINE_LINE_NUMBER, number, max_number, TRUE);
1570 col = draw_space(view, LINE_LINE_NUMBER, max_number, max_number);
1574 set_view_attr(view, LINE_DEFAULT);
1575 waddch(view->win, line_graphics[LINE_GRAPHIC_VLINE]);
1580 col += draw_space(view, LINE_DEFAULT, max - col, 1);
1583 return view->width - view->col <= 0;
1587 draw_text(struct view *view, enum line_type type, const char *string, bool trim)
1589 view->col += draw_chars(view, type, string, view->width - view->col, trim);
1590 return view->width - view->col <= 0;
1594 draw_graphic(struct view *view, enum line_type type, chtype graphic[], size_t size)
1596 int max = view->width - view->col;
1602 set_view_attr(view, type);
1603 /* Using waddch() instead of waddnstr() ensures that
1604 * they'll be rendered correctly for the cursor line. */
1605 for (i = 0; i < size; i++)
1606 waddch(view->win, graphic[i]);
1610 waddch(view->win, ' ');
1614 return view->width - view->col <= 0;
1618 draw_field(struct view *view, enum line_type type, char *text, int len, bool trim)
1620 int max = MIN(view->width - view->col, len);
1624 col = draw_chars(view, type, text, max - 1, trim);
1626 col = draw_space(view, type, max - 1, max - 1);
1628 view->col += col + draw_space(view, LINE_DEFAULT, max - col, max - col);
1629 return view->width - view->col <= 0;
1633 draw_date(struct view *view, struct tm *time)
1635 char buf[DATE_COLS];
1640 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, time);
1641 date = timelen ? buf : NULL;
1643 return draw_field(view, LINE_DATE, date, DATE_COLS, FALSE);
1647 draw_view_line(struct view *view, unsigned int lineno)
1650 bool selected = (view->offset + lineno == view->lineno);
1653 assert(view_is_displayed(view));
1655 if (view->offset + lineno >= view->lines)
1658 line = &view->line[view->offset + lineno];
1660 wmove(view->win, lineno, 0);
1662 view->curline = line;
1663 view->curtype = LINE_NONE;
1664 line->selected = FALSE;
1667 set_view_attr(view, LINE_CURSOR);
1668 line->selected = TRUE;
1669 view->ops->select(view, line);
1670 } else if (line->selected) {
1671 wclrtoeol(view->win);
1674 scrollok(view->win, FALSE);
1675 draw_ok = view->ops->draw(view, line, lineno);
1676 scrollok(view->win, TRUE);
1682 redraw_view_dirty(struct view *view)
1687 for (lineno = 0; lineno < view->height; lineno++) {
1688 struct line *line = &view->line[view->offset + lineno];
1694 if (!draw_view_line(view, lineno))
1700 redrawwin(view->win);
1702 wnoutrefresh(view->win);
1704 wrefresh(view->win);
1708 redraw_view_from(struct view *view, int lineno)
1710 assert(0 <= lineno && lineno < view->height);
1712 for (; lineno < view->height; lineno++) {
1713 if (!draw_view_line(view, lineno))
1717 redrawwin(view->win);
1719 wnoutrefresh(view->win);
1721 wrefresh(view->win);
1725 redraw_view(struct view *view)
1728 redraw_view_from(view, 0);
1733 update_view_title(struct view *view)
1735 char buf[SIZEOF_STR];
1736 char state[SIZEOF_STR];
1737 size_t bufpos = 0, statelen = 0;
1739 assert(view_is_displayed(view));
1741 if (view != VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) {
1742 unsigned int view_lines = view->offset + view->height;
1743 unsigned int lines = view->lines
1744 ? MIN(view_lines, view->lines) * 100 / view->lines
1747 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1754 time_t secs = time(NULL) - view->start_time;
1756 /* Three git seconds are a long time ... */
1758 string_format_from(state, &statelen, " %lds", secs);
1762 string_format_from(buf, &bufpos, "[%s]", view->name);
1763 if (*view->ref && bufpos < view->width) {
1764 size_t refsize = strlen(view->ref);
1765 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1767 if (minsize < view->width)
1768 refsize = view->width - minsize + 7;
1769 string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
1772 if (statelen && bufpos < view->width) {
1773 string_format_from(buf, &bufpos, " %s", state);
1776 if (view == display[current_view])
1777 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1779 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1781 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1782 wclrtoeol(view->title);
1783 wmove(view->title, 0, view->width - 1);
1786 wnoutrefresh(view->title);
1788 wrefresh(view->title);
1792 resize_display(void)
1795 struct view *base = display[0];
1796 struct view *view = display[1] ? display[1] : display[0];
1798 /* Setup window dimensions */
1800 getmaxyx(stdscr, base->height, base->width);
1802 /* Make room for the status window. */
1806 /* Horizontal split. */
1807 view->width = base->width;
1808 view->height = SCALE_SPLIT_VIEW(base->height);
1809 base->height -= view->height;
1811 /* Make room for the title bar. */
1815 /* Make room for the title bar. */
1820 foreach_displayed_view (view, i) {
1822 view->win = newwin(view->height, 0, offset, 0);
1824 die("Failed to create %s view", view->name);
1826 scrollok(view->win, TRUE);
1828 view->title = newwin(1, 0, offset + view->height, 0);
1830 die("Failed to create title window");
1833 wresize(view->win, view->height, view->width);
1834 mvwin(view->win, offset, 0);
1835 mvwin(view->title, offset + view->height, 0);
1838 offset += view->height + 1;
1843 redraw_display(void)
1848 foreach_displayed_view (view, i) {
1850 update_view_title(view);
1855 update_display_cursor(struct view *view)
1857 /* Move the cursor to the right-most column of the cursor line.
1859 * XXX: This could turn out to be a bit expensive, but it ensures that
1860 * the cursor does not jump around. */
1862 wmove(view->win, view->lineno - view->offset, view->width - 1);
1863 wrefresh(view->win);
1871 /* Scrolling backend */
1873 do_scroll_view(struct view *view, int lines)
1875 bool redraw_current_line = FALSE;
1877 /* The rendering expects the new offset. */
1878 view->offset += lines;
1880 assert(0 <= view->offset && view->offset < view->lines);
1883 /* Move current line into the view. */
1884 if (view->lineno < view->offset) {
1885 view->lineno = view->offset;
1886 redraw_current_line = TRUE;
1887 } else if (view->lineno >= view->offset + view->height) {
1888 view->lineno = view->offset + view->height - 1;
1889 redraw_current_line = TRUE;
1892 assert(view->offset <= view->lineno && view->lineno < view->lines);
1894 /* Redraw the whole screen if scrolling is pointless. */
1895 if (view->height < ABS(lines)) {
1899 int line = lines > 0 ? view->height - lines : 0;
1900 int end = line + ABS(lines);
1902 wscrl(view->win, lines);
1904 for (; line < end; line++) {
1905 if (!draw_view_line(view, line))
1909 if (redraw_current_line)
1910 draw_view_line(view, view->lineno - view->offset);
1913 redrawwin(view->win);
1914 wrefresh(view->win);
1918 /* Scroll frontend */
1920 scroll_view(struct view *view, enum request request)
1924 assert(view_is_displayed(view));
1927 case REQ_SCROLL_PAGE_DOWN:
1928 lines = view->height;
1929 case REQ_SCROLL_LINE_DOWN:
1930 if (view->offset + lines > view->lines)
1931 lines = view->lines - view->offset;
1933 if (lines == 0 || view->offset + view->height >= view->lines) {
1934 report("Cannot scroll beyond the last line");
1939 case REQ_SCROLL_PAGE_UP:
1940 lines = view->height;
1941 case REQ_SCROLL_LINE_UP:
1942 if (lines > view->offset)
1943 lines = view->offset;
1946 report("Cannot scroll beyond the first line");
1954 die("request %d not handled in switch", request);
1957 do_scroll_view(view, lines);
1962 move_view(struct view *view, enum request request)
1964 int scroll_steps = 0;
1968 case REQ_MOVE_FIRST_LINE:
1969 steps = -view->lineno;
1972 case REQ_MOVE_LAST_LINE:
1973 steps = view->lines - view->lineno - 1;
1976 case REQ_MOVE_PAGE_UP:
1977 steps = view->height > view->lineno
1978 ? -view->lineno : -view->height;
1981 case REQ_MOVE_PAGE_DOWN:
1982 steps = view->lineno + view->height >= view->lines
1983 ? view->lines - view->lineno - 1 : view->height;
1995 die("request %d not handled in switch", request);
1998 if (steps <= 0 && view->lineno == 0) {
1999 report("Cannot move beyond the first line");
2002 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
2003 report("Cannot move beyond the last line");
2007 /* Move the current line */
2008 view->lineno += steps;
2009 assert(0 <= view->lineno && view->lineno < view->lines);
2011 /* Check whether the view needs to be scrolled */
2012 if (view->lineno < view->offset ||
2013 view->lineno >= view->offset + view->height) {
2014 scroll_steps = steps;
2015 if (steps < 0 && -steps > view->offset) {
2016 scroll_steps = -view->offset;
2018 } else if (steps > 0) {
2019 if (view->lineno == view->lines - 1 &&
2020 view->lines > view->height) {
2021 scroll_steps = view->lines - view->offset - 1;
2022 if (scroll_steps >= view->height)
2023 scroll_steps -= view->height - 1;
2028 if (!view_is_displayed(view)) {
2029 view->offset += scroll_steps;
2030 assert(0 <= view->offset && view->offset < view->lines);
2031 view->ops->select(view, &view->line[view->lineno]);
2035 /* Repaint the old "current" line if we be scrolling */
2036 if (ABS(steps) < view->height)
2037 draw_view_line(view, view->lineno - steps - view->offset);
2040 do_scroll_view(view, scroll_steps);
2044 /* Draw the current line */
2045 draw_view_line(view, view->lineno - view->offset);
2047 redrawwin(view->win);
2048 wrefresh(view->win);
2057 static void search_view(struct view *view, enum request request);
2060 find_next_line(struct view *view, unsigned long lineno, struct line *line)
2062 assert(view_is_displayed(view));
2064 if (!view->ops->grep(view, line))
2067 if (lineno - view->offset >= view->height) {
2068 view->offset = lineno;
2069 view->lineno = lineno;
2073 unsigned long old_lineno = view->lineno - view->offset;
2075 view->lineno = lineno;
2076 draw_view_line(view, old_lineno);
2078 draw_view_line(view, view->lineno - view->offset);
2079 redrawwin(view->win);
2080 wrefresh(view->win);
2083 report("Line %ld matches '%s'", lineno + 1, view->grep);
2088 find_next(struct view *view, enum request request)
2090 unsigned long lineno = view->lineno;
2095 report("No previous search");
2097 search_view(view, request);
2107 case REQ_SEARCH_BACK:
2116 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
2117 lineno += direction;
2119 /* Note, lineno is unsigned long so will wrap around in which case it
2120 * will become bigger than view->lines. */
2121 for (; lineno < view->lines; lineno += direction) {
2122 struct line *line = &view->line[lineno];
2124 if (find_next_line(view, lineno, line))
2128 report("No match found for '%s'", view->grep);
2132 search_view(struct view *view, enum request request)
2137 regfree(view->regex);
2140 view->regex = calloc(1, sizeof(*view->regex));
2145 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
2146 if (regex_err != 0) {
2147 char buf[SIZEOF_STR] = "unknown error";
2149 regerror(regex_err, view->regex, buf, sizeof(buf));
2150 report("Search failed: %s", buf);
2154 string_copy(view->grep, opt_search);
2156 find_next(view, request);
2160 * Incremental updating
2164 end_update(struct view *view)
2168 set_nonblocking_input(FALSE);
2169 if (view->pipe == stdin)
2177 begin_update(struct view *view)
2183 string_copy(view->cmd, opt_cmd);
2185 /* When running random commands, initially show the
2186 * command in the title. However, it maybe later be
2187 * overwritten if a commit line is selected. */
2188 if (view == VIEW(REQ_VIEW_PAGER))
2189 string_copy(view->ref, view->cmd);
2193 } else if (view == VIEW(REQ_VIEW_TREE)) {
2194 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2195 char path[SIZEOF_STR];
2197 if (strcmp(view->vid, view->id))
2198 opt_path[0] = path[0] = 0;
2199 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
2202 if (!string_format(view->cmd, format, view->id, path))
2206 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2207 const char *id = view->id;
2209 if (!string_format(view->cmd, format, id, id, id, id, id))
2212 /* Put the current ref_* value to the view title ref
2213 * member. This is needed by the blob view. Most other
2214 * views sets it automatically after loading because the
2215 * first line is a commit line. */
2216 string_copy_rev(view->ref, view->id);
2219 /* Special case for the pager view. */
2221 view->pipe = opt_pipe;
2224 view->pipe = popen(view->cmd, "r");
2230 set_nonblocking_input(TRUE);
2235 string_copy_rev(view->vid, view->id);
2240 for (i = 0; i < view->lines; i++)
2241 if (view->line[i].data)
2242 free(view->line[i].data);
2248 view->start_time = time(NULL);
2253 #define ITEM_CHUNK_SIZE 256
2255 realloc_items(void *mem, size_t *size, size_t new_size, size_t item_size)
2257 size_t num_chunks = *size / ITEM_CHUNK_SIZE;
2258 size_t num_chunks_new = (new_size + ITEM_CHUNK_SIZE - 1) / ITEM_CHUNK_SIZE;
2260 if (mem == NULL || num_chunks != num_chunks_new) {
2261 *size = num_chunks_new * ITEM_CHUNK_SIZE;
2262 mem = realloc(mem, *size * item_size);
2268 static struct line *
2269 realloc_lines(struct view *view, size_t line_size)
2271 size_t alloc = view->line_alloc;
2272 struct line *tmp = realloc_items(view->line, &alloc, line_size,
2273 sizeof(*view->line));
2279 view->line_alloc = alloc;
2280 view->line_size = line_size;
2285 update_view(struct view *view)
2287 char in_buffer[BUFSIZ];
2288 char out_buffer[BUFSIZ * 2];
2290 /* The number of lines to read. If too low it will cause too much
2291 * redrawing (and possible flickering), if too high responsiveness
2293 unsigned long lines = view->height;
2294 int redraw_from = -1;
2299 /* Only redraw if lines are visible. */
2300 if (view->offset + view->height >= view->lines)
2301 redraw_from = view->lines - view->offset;
2303 /* FIXME: This is probably not perfect for backgrounded views. */
2304 if (!realloc_lines(view, view->lines + lines))
2307 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
2308 size_t linelen = strlen(line);
2311 line[linelen - 1] = 0;
2313 if (opt_iconv != ICONV_NONE) {
2314 ICONV_CONST char *inbuf = line;
2315 size_t inlen = linelen;
2317 char *outbuf = out_buffer;
2318 size_t outlen = sizeof(out_buffer);
2322 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
2323 if (ret != (size_t) -1) {
2325 linelen = strlen(out_buffer);
2329 if (!view->ops->read(view, line))
2339 lines = view->lines;
2340 for (digits = 0; lines; digits++)
2343 /* Keep the displayed view in sync with line number scaling. */
2344 if (digits != view->digits) {
2345 view->digits = digits;
2350 if (!view_is_displayed(view))
2353 if (view == VIEW(REQ_VIEW_TREE)) {
2354 /* Clear the view and redraw everything since the tree sorting
2355 * might have rearranged things. */
2358 } else if (redraw_from >= 0) {
2359 /* If this is an incremental update, redraw the previous line
2360 * since for commits some members could have changed when
2361 * loading the main view. */
2362 if (redraw_from > 0)
2365 /* Since revision graph visualization requires knowledge
2366 * about the parent commit, it causes a further one-off
2367 * needed to be redrawn for incremental updates. */
2368 if (redraw_from > 0 && opt_rev_graph)
2371 /* Incrementally draw avoids flickering. */
2372 redraw_view_from(view, redraw_from);
2375 if (view == VIEW(REQ_VIEW_BLAME))
2376 redraw_view_dirty(view);
2378 /* Update the title _after_ the redraw so that if the redraw picks up a
2379 * commit reference in view->ref it'll be available here. */
2380 update_view_title(view);
2383 if (ferror(view->pipe)) {
2384 report("Failed to read: %s", strerror(errno));
2387 } else if (feof(view->pipe)) {
2395 report("Allocation failure");
2398 if (view->ops->read(view, NULL))
2403 static struct line *
2404 add_line_data(struct view *view, void *data, enum line_type type)
2406 struct line *line = &view->line[view->lines++];
2408 memset(line, 0, sizeof(*line));
2415 static struct line *
2416 add_line_text(struct view *view, char *data, enum line_type type)
2419 data = strdup(data);
2421 return data ? add_line_data(view, data, type) : NULL;
2430 OPEN_DEFAULT = 0, /* Use default view switching. */
2431 OPEN_SPLIT = 1, /* Split current view. */
2432 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2433 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2434 OPEN_NOMAXIMIZE = 8 /* Do not maximize the current view. */
2438 open_view(struct view *prev, enum request request, enum open_flags flags)
2440 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2441 bool split = !!(flags & OPEN_SPLIT);
2442 bool reload = !!(flags & OPEN_RELOAD);
2443 bool nomaximize = !!(flags & OPEN_NOMAXIMIZE);
2444 struct view *view = VIEW(request);
2445 int nviews = displayed_views();
2446 struct view *base_view = display[0];
2448 if (view == prev && nviews == 1 && !reload) {
2449 report("Already in %s view", view->name);
2453 if (view->git_dir && !opt_git_dir[0]) {
2454 report("The %s view is disabled in pager view", view->name);
2462 } else if (!nomaximize) {
2463 /* Maximize the current view. */
2464 memset(display, 0, sizeof(display));
2466 display[current_view] = view;
2469 /* Resize the view when switching between split- and full-screen,
2470 * or when switching between two different full-screen views. */
2471 if (nviews != displayed_views() ||
2472 (nviews == 1 && base_view != display[0]))
2475 if (view->ops->open) {
2476 if (!view->ops->open(view)) {
2477 report("Failed to load %s view", view->name);
2481 } else if ((reload || strcmp(view->vid, view->id)) &&
2482 !begin_update(view)) {
2483 report("Failed to load %s view", view->name);
2487 if (split && prev->lineno - prev->offset >= prev->height) {
2488 /* Take the title line into account. */
2489 int lines = prev->lineno - prev->offset - prev->height + 1;
2491 /* Scroll the view that was split if the current line is
2492 * outside the new limited view. */
2493 do_scroll_view(prev, lines);
2496 if (prev && view != prev) {
2497 if (split && !backgrounded) {
2498 /* "Blur" the previous view. */
2499 update_view_title(prev);
2502 view->parent = prev;
2505 if (view->pipe && view->lines == 0) {
2506 /* Clear the old view and let the incremental updating refill
2515 /* If the view is backgrounded the above calls to report()
2516 * won't redraw the view title. */
2518 update_view_title(view);
2522 open_external_viewer(const char *cmd)
2524 def_prog_mode(); /* save current tty modes */
2525 endwin(); /* restore original tty modes */
2527 fprintf(stderr, "Press Enter to continue");
2534 open_mergetool(const char *file)
2536 char cmd[SIZEOF_STR];
2537 char file_sq[SIZEOF_STR];
2539 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2540 string_format(cmd, "git mergetool %s", file_sq)) {
2541 open_external_viewer(cmd);
2546 open_editor(bool from_root, const char *file)
2548 char cmd[SIZEOF_STR];
2549 char file_sq[SIZEOF_STR];
2551 char *prefix = from_root ? opt_cdup : "";
2553 editor = getenv("GIT_EDITOR");
2554 if (!editor && *opt_editor)
2555 editor = opt_editor;
2557 editor = getenv("VISUAL");
2559 editor = getenv("EDITOR");
2563 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2564 string_format(cmd, "%s %s%s", editor, prefix, file_sq)) {
2565 open_external_viewer(cmd);
2570 open_run_request(enum request request)
2572 struct run_request *req = get_run_request(request);
2573 char buf[SIZEOF_STR * 2];
2578 report("Unknown run request");
2586 char *next = strstr(cmd, "%(");
2587 int len = next - cmd;
2594 } else if (!strncmp(next, "%(head)", 7)) {
2597 } else if (!strncmp(next, "%(commit)", 9)) {
2600 } else if (!strncmp(next, "%(blob)", 7)) {
2604 report("Unknown replacement in run request: `%s`", req->cmd);
2608 if (!string_format_from(buf, &bufpos, "%.*s%s", len, cmd, value))
2612 next = strchr(next, ')') + 1;
2616 open_external_viewer(buf);
2620 * User request switch noodle
2624 view_driver(struct view *view, enum request request)
2628 if (request == REQ_NONE) {
2633 if (request > REQ_NONE) {
2634 open_run_request(request);
2635 /* FIXME: When all views can refresh always do this. */
2636 if (view == VIEW(REQ_VIEW_STATUS) ||
2637 view == VIEW(REQ_VIEW_STAGE))
2638 request = REQ_REFRESH;
2643 if (view && view->lines) {
2644 request = view->ops->request(view, request, &view->line[view->lineno]);
2645 if (request == REQ_NONE)
2652 case REQ_MOVE_PAGE_UP:
2653 case REQ_MOVE_PAGE_DOWN:
2654 case REQ_MOVE_FIRST_LINE:
2655 case REQ_MOVE_LAST_LINE:
2656 move_view(view, request);
2659 case REQ_SCROLL_LINE_DOWN:
2660 case REQ_SCROLL_LINE_UP:
2661 case REQ_SCROLL_PAGE_DOWN:
2662 case REQ_SCROLL_PAGE_UP:
2663 scroll_view(view, request);
2666 case REQ_VIEW_BLAME:
2668 report("No file chosen, press %s to open tree view",
2669 get_key(REQ_VIEW_TREE));
2672 open_view(view, request, OPEN_DEFAULT);
2677 report("No file chosen, press %s to open tree view",
2678 get_key(REQ_VIEW_TREE));
2681 open_view(view, request, OPEN_DEFAULT);
2684 case REQ_VIEW_PAGER:
2685 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2686 report("No pager content, press %s to run command from prompt",
2687 get_key(REQ_PROMPT));
2690 open_view(view, request, OPEN_DEFAULT);
2693 case REQ_VIEW_STAGE:
2694 if (!VIEW(REQ_VIEW_STAGE)->lines) {
2695 report("No stage content, press %s to open the status view and choose file",
2696 get_key(REQ_VIEW_STATUS));
2699 open_view(view, request, OPEN_DEFAULT);
2702 case REQ_VIEW_STATUS:
2703 if (opt_is_inside_work_tree == FALSE) {
2704 report("The status view requires a working tree");
2707 open_view(view, request, OPEN_DEFAULT);
2715 open_view(view, request, OPEN_DEFAULT);
2720 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2722 if ((view == VIEW(REQ_VIEW_DIFF) &&
2723 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2724 (view == VIEW(REQ_VIEW_DIFF) &&
2725 view->parent == VIEW(REQ_VIEW_BLAME)) ||
2726 (view == VIEW(REQ_VIEW_STAGE) &&
2727 view->parent == VIEW(REQ_VIEW_STATUS)) ||
2728 (view == VIEW(REQ_VIEW_BLOB) &&
2729 view->parent == VIEW(REQ_VIEW_TREE))) {
2732 view = view->parent;
2733 line = view->lineno;
2734 move_view(view, request);
2735 if (view_is_displayed(view))
2736 update_view_title(view);
2737 if (line != view->lineno)
2738 view->ops->request(view, REQ_ENTER,
2739 &view->line[view->lineno]);
2742 move_view(view, request);
2748 int nviews = displayed_views();
2749 int next_view = (current_view + 1) % nviews;
2751 if (next_view == current_view) {
2752 report("Only one view is displayed");
2756 current_view = next_view;
2757 /* Blur out the title of the previous view. */
2758 update_view_title(view);
2763 report("Refreshing is not yet supported for the %s view", view->name);
2767 if (displayed_views() == 2)
2768 open_view(view, VIEW_REQ(view), OPEN_DEFAULT);
2771 case REQ_TOGGLE_LINENO:
2772 opt_line_number = !opt_line_number;
2776 case REQ_TOGGLE_DATE:
2777 opt_date = !opt_date;
2781 case REQ_TOGGLE_AUTHOR:
2782 opt_author = !opt_author;
2786 case REQ_TOGGLE_REV_GRAPH:
2787 opt_rev_graph = !opt_rev_graph;
2791 case REQ_TOGGLE_REFS:
2792 opt_show_refs = !opt_show_refs;
2797 /* Always reload^Wrerun commands from the prompt. */
2798 open_view(view, opt_request, OPEN_RELOAD);
2802 case REQ_SEARCH_BACK:
2803 search_view(view, request);
2808 find_next(view, request);
2811 case REQ_STOP_LOADING:
2812 for (i = 0; i < ARRAY_SIZE(views); i++) {
2815 report("Stopped loading the %s view", view->name),
2820 case REQ_SHOW_VERSION:
2821 report("tig-%s (built %s)", TIG_VERSION, __DATE__);
2824 case REQ_SCREEN_RESIZE:
2827 case REQ_SCREEN_REDRAW:
2832 report("Nothing to edit");
2837 report("Nothing to enter");
2841 case REQ_VIEW_CLOSE:
2842 /* XXX: Mark closed views by letting view->parent point to the
2843 * view itself. Parents to closed view should never be
2846 view->parent->parent != view->parent) {
2847 memset(display, 0, sizeof(display));
2849 display[current_view] = view->parent;
2850 view->parent = view;
2860 /* An unknown key will show most commonly used commands. */
2861 report("Unknown key, press 'h' for help");
2874 pager_draw(struct view *view, struct line *line, unsigned int lineno)
2876 char *text = line->data;
2878 if (opt_line_number && draw_lineno(view, lineno))
2881 draw_text(view, line->type, text, TRUE);
2886 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2888 char refbuf[SIZEOF_STR];
2892 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2895 pipe = popen(refbuf, "r");
2899 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2900 ref = chomp_string(ref);
2906 /* This is the only fatal call, since it can "corrupt" the buffer. */
2907 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2914 add_pager_refs(struct view *view, struct line *line)
2916 char buf[SIZEOF_STR];
2917 char *commit_id = (char *)line->data + STRING_SIZE("commit ");
2919 size_t bufpos = 0, refpos = 0;
2920 const char *sep = "Refs: ";
2921 bool is_tag = FALSE;
2923 assert(line->type == LINE_COMMIT);
2925 refs = get_refs(commit_id);
2927 if (view == VIEW(REQ_VIEW_DIFF))
2928 goto try_add_describe_ref;
2933 struct ref *ref = refs[refpos];
2934 char *fmt = ref->tag ? "%s[%s]" :
2935 ref->remote ? "%s<%s>" : "%s%s";
2937 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2942 } while (refs[refpos++]->next);
2944 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2945 try_add_describe_ref:
2946 /* Add <tag>-g<commit_id> "fake" reference. */
2947 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2954 if (!realloc_lines(view, view->line_size + 1))
2957 add_line_text(view, buf, LINE_PP_REFS);
2961 pager_read(struct view *view, char *data)
2968 line = add_line_text(view, data, get_line_type(data));
2972 if (line->type == LINE_COMMIT &&
2973 (view == VIEW(REQ_VIEW_DIFF) ||
2974 view == VIEW(REQ_VIEW_LOG)))
2975 add_pager_refs(view, line);
2981 pager_request(struct view *view, enum request request, struct line *line)
2985 if (request != REQ_ENTER)
2988 if (line->type == LINE_COMMIT &&
2989 (view == VIEW(REQ_VIEW_LOG) ||
2990 view == VIEW(REQ_VIEW_PAGER))) {
2991 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2995 /* Always scroll the view even if it was split. That way
2996 * you can use Enter to scroll through the log view and
2997 * split open each commit diff. */
2998 scroll_view(view, REQ_SCROLL_LINE_DOWN);
3000 /* FIXME: A minor workaround. Scrolling the view will call report("")
3001 * but if we are scrolling a non-current view this won't properly
3002 * update the view title. */
3004 update_view_title(view);
3010 pager_grep(struct view *view, struct line *line)
3013 char *text = line->data;
3018 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
3025 pager_select(struct view *view, struct line *line)
3027 if (line->type == LINE_COMMIT) {
3028 char *text = (char *)line->data + STRING_SIZE("commit ");
3030 if (view != VIEW(REQ_VIEW_PAGER))
3031 string_copy_rev(view->ref, text);
3032 string_copy_rev(ref_commit, text);
3036 static struct view_ops pager_ops = {
3052 help_open(struct view *view)
3055 int lines = ARRAY_SIZE(req_info) + 2;
3058 if (view->lines > 0)
3061 for (i = 0; i < ARRAY_SIZE(req_info); i++)
3062 if (!req_info[i].request)
3065 lines += run_requests + 1;
3067 view->line = calloc(lines, sizeof(*view->line));
3071 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
3073 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
3076 if (req_info[i].request == REQ_NONE)
3079 if (!req_info[i].request) {
3080 add_line_text(view, "", LINE_DEFAULT);
3081 add_line_text(view, req_info[i].help, LINE_DEFAULT);
3085 key = get_key(req_info[i].request);
3087 key = "(no key defined)";
3089 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
3092 add_line_text(view, buf, LINE_DEFAULT);
3096 add_line_text(view, "", LINE_DEFAULT);
3097 add_line_text(view, "External commands:", LINE_DEFAULT);
3100 for (i = 0; i < run_requests; i++) {
3101 struct run_request *req = get_run_request(REQ_NONE + i + 1);
3107 key = get_key_name(req->key);
3109 key = "(no key defined)";
3111 if (!string_format(buf, " %-10s %-14s `%s`",
3112 keymap_table[req->keymap].name,
3116 add_line_text(view, buf, LINE_DEFAULT);
3122 static struct view_ops help_ops = {
3137 struct tree_stack_entry {
3138 struct tree_stack_entry *prev; /* Entry below this in the stack */
3139 unsigned long lineno; /* Line number to restore */
3140 char *name; /* Position of name in opt_path */
3143 /* The top of the path stack. */
3144 static struct tree_stack_entry *tree_stack = NULL;
3145 unsigned long tree_lineno = 0;
3148 pop_tree_stack_entry(void)
3150 struct tree_stack_entry *entry = tree_stack;
3152 tree_lineno = entry->lineno;
3154 tree_stack = entry->prev;
3159 push_tree_stack_entry(char *name, unsigned long lineno)
3161 struct tree_stack_entry *entry = calloc(1, sizeof(*entry));
3162 size_t pathlen = strlen(opt_path);
3167 entry->prev = tree_stack;
3168 entry->name = opt_path + pathlen;
3171 if (!string_format_from(opt_path, &pathlen, "%s/", name)) {
3172 pop_tree_stack_entry();
3176 /* Move the current line to the first tree entry. */
3178 entry->lineno = lineno;
3181 /* Parse output from git-ls-tree(1):
3183 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
3184 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
3185 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
3186 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
3189 #define SIZEOF_TREE_ATTR \
3190 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
3192 #define TREE_UP_FORMAT "040000 tree %s\t.."
3195 tree_compare_entry(enum line_type type1, char *name1,
3196 enum line_type type2, char *name2)
3198 if (type1 != type2) {
3199 if (type1 == LINE_TREE_DIR)
3204 return strcmp(name1, name2);
3208 tree_path(struct line *line)
3210 char *path = line->data;
3212 return path + SIZEOF_TREE_ATTR;
3216 tree_read(struct view *view, char *text)
3218 size_t textlen = text ? strlen(text) : 0;
3219 char buf[SIZEOF_STR];
3221 enum line_type type;
3222 bool first_read = view->lines == 0;
3226 if (textlen <= SIZEOF_TREE_ATTR)
3229 type = text[STRING_SIZE("100644 ")] == 't'
3230 ? LINE_TREE_DIR : LINE_TREE_FILE;
3233 /* Add path info line */
3234 if (!string_format(buf, "Directory path /%s", opt_path) ||
3235 !realloc_lines(view, view->line_size + 1) ||
3236 !add_line_text(view, buf, LINE_DEFAULT))
3239 /* Insert "link" to parent directory. */
3241 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
3242 !realloc_lines(view, view->line_size + 1) ||
3243 !add_line_text(view, buf, LINE_TREE_DIR))
3248 /* Strip the path part ... */
3250 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
3251 size_t striplen = strlen(opt_path);
3252 char *path = text + SIZEOF_TREE_ATTR;
3254 if (pathlen > striplen)
3255 memmove(path, path + striplen,
3256 pathlen - striplen + 1);
3259 /* Skip "Directory ..." and ".." line. */
3260 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
3261 struct line *line = &view->line[pos];
3262 char *path1 = tree_path(line);
3263 char *path2 = text + SIZEOF_TREE_ATTR;
3264 int cmp = tree_compare_entry(line->type, path1, type, path2);
3269 text = strdup(text);
3273 if (view->lines > pos)
3274 memmove(&view->line[pos + 1], &view->line[pos],
3275 (view->lines - pos) * sizeof(*line));
3277 line = &view->line[pos];
3284 if (!add_line_text(view, text, type))
3287 if (tree_lineno > view->lineno) {
3288 view->lineno = tree_lineno;
3296 tree_request(struct view *view, enum request request, struct line *line)
3298 enum open_flags flags;
3300 if (request == REQ_VIEW_BLAME) {
3301 char *filename = tree_path(line);
3303 if (line->type == LINE_TREE_DIR) {
3304 report("Cannot show blame for directory %s", opt_path);
3308 string_copy(opt_ref, view->vid);
3309 string_format(opt_file, "%s%s", opt_path, filename);
3312 if (request == REQ_TREE_PARENT) {
3315 request = REQ_ENTER;
3316 line = &view->line[1];
3318 /* quit view if at top of tree */
3319 return REQ_VIEW_CLOSE;
3322 if (request != REQ_ENTER)
3325 /* Cleanup the stack if the tree view is at a different tree. */
3326 while (!*opt_path && tree_stack)
3327 pop_tree_stack_entry();
3329 switch (line->type) {
3331 /* Depending on whether it is a subdir or parent (updir?) link
3332 * mangle the path buffer. */
3333 if (line == &view->line[1] && *opt_path) {
3334 pop_tree_stack_entry();
3337 char *basename = tree_path(line);
3339 push_tree_stack_entry(basename, view->lineno);
3342 /* Trees and subtrees share the same ID, so they are not not
3343 * unique like blobs. */
3344 flags = OPEN_RELOAD;
3345 request = REQ_VIEW_TREE;
3348 case LINE_TREE_FILE:
3349 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3350 request = REQ_VIEW_BLOB;
3357 open_view(view, request, flags);
3358 if (request == REQ_VIEW_TREE) {
3359 view->lineno = tree_lineno;
3366 tree_select(struct view *view, struct line *line)
3368 char *text = (char *)line->data + STRING_SIZE("100644 blob ");
3370 if (line->type == LINE_TREE_FILE) {
3371 string_copy_rev(ref_blob, text);
3373 } else if (line->type != LINE_TREE_DIR) {
3377 string_copy_rev(view->ref, text);
3380 static struct view_ops tree_ops = {
3391 blob_read(struct view *view, char *line)
3395 return add_line_text(view, line, LINE_DEFAULT) != NULL;
3398 static struct view_ops blob_ops = {
3411 * Loading the blame view is a two phase job:
3413 * 1. File content is read either using opt_file from the
3414 * filesystem or using git-cat-file.
3415 * 2. Then blame information is incrementally added by
3416 * reading output from git-blame.
3419 struct blame_commit {
3420 char id[SIZEOF_REV]; /* SHA1 ID. */
3421 char title[128]; /* First line of the commit message. */
3422 char author[75]; /* Author of the commit. */
3423 struct tm time; /* Date from the author ident. */
3424 char filename[128]; /* Name of file. */
3428 struct blame_commit *commit;
3429 unsigned int header:1;
3433 #define BLAME_CAT_FILE_CMD "git cat-file blob %s:%s"
3434 #define BLAME_INCREMENTAL_CMD "git blame --incremental %s %s"
3437 blame_open(struct view *view)
3439 char path[SIZEOF_STR];
3440 char ref[SIZEOF_STR] = "";
3442 if (sq_quote(path, 0, opt_file) >= sizeof(path))
3445 if (*opt_ref && sq_quote(ref, 0, opt_ref) >= sizeof(ref))
3449 if (!string_format(view->cmd, BLAME_CAT_FILE_CMD, ref, path))
3452 view->pipe = fopen(opt_file, "r");
3454 !string_format(view->cmd, BLAME_CAT_FILE_CMD, "HEAD", path))
3459 view->pipe = popen(view->cmd, "r");
3463 if (!string_format(view->cmd, BLAME_INCREMENTAL_CMD, ref, path))
3466 string_format(view->ref, "%s ...", opt_file);
3467 string_copy_rev(view->vid, opt_file);
3468 set_nonblocking_input(TRUE);
3473 for (i = 0; i < view->lines; i++)
3474 free(view->line[i].data);
3478 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3479 view->offset = view->lines = view->lineno = 0;
3481 view->start_time = time(NULL);
3486 static struct blame_commit *
3487 get_blame_commit(struct view *view, const char *id)
3491 for (i = 0; i < view->lines; i++) {
3492 struct blame *blame = view->line[i].data;
3497 if (!strncmp(blame->commit->id, id, SIZEOF_REV - 1))
3498 return blame->commit;
3502 struct blame_commit *commit = calloc(1, sizeof(*commit));
3505 string_ncopy(commit->id, id, SIZEOF_REV);
3511 parse_number(char **posref, size_t *number, size_t min, size_t max)
3513 char *pos = *posref;
3516 pos = strchr(pos + 1, ' ');
3517 if (!pos || !isdigit(pos[1]))
3519 *number = atoi(pos + 1);
3520 if (*number < min || *number > max)
3527 static struct blame_commit *
3528 parse_blame_commit(struct view *view, char *text, int *blamed)
3530 struct blame_commit *commit;
3531 struct blame *blame;
3532 char *pos = text + SIZEOF_REV - 1;
3536 if (strlen(text) <= SIZEOF_REV || *pos != ' ')
3539 if (!parse_number(&pos, &lineno, 1, view->lines) ||
3540 !parse_number(&pos, &group, 1, view->lines - lineno + 1))
3543 commit = get_blame_commit(view, text);
3549 struct line *line = &view->line[lineno + group - 1];
3552 blame->commit = commit;
3553 blame->header = !group;
3561 blame_read_file(struct view *view, char *line)
3566 if (view->lines > 0)
3567 pipe = popen(view->cmd, "r");
3568 else if (!view->parent)
3569 die("No blame exist for %s", view->vid);
3572 report("Failed to load blame data");
3581 size_t linelen = strlen(line);
3582 struct blame *blame = malloc(sizeof(*blame) + linelen);
3587 blame->commit = NULL;
3588 strncpy(blame->text, line, linelen);
3589 blame->text[linelen] = 0;
3590 return add_line_data(view, blame, LINE_BLAME_ID) != NULL;
3595 match_blame_header(const char *name, char **line)
3597 size_t namelen = strlen(name);
3598 bool matched = !strncmp(name, *line, namelen);
3607 blame_read(struct view *view, char *line)
3609 static struct blame_commit *commit = NULL;
3610 static int blamed = 0;
3611 static time_t author_time;
3614 return blame_read_file(view, line);
3620 string_format(view->ref, "%s", view->vid);
3621 if (view_is_displayed(view)) {
3622 update_view_title(view);
3623 redraw_view_from(view, 0);
3629 commit = parse_blame_commit(view, line, &blamed);
3630 string_format(view->ref, "%s %2d%%", view->vid,
3631 blamed * 100 / view->lines);
3633 } else if (match_blame_header("author ", &line)) {
3634 string_ncopy(commit->author, line, strlen(line));
3636 } else if (match_blame_header("author-time ", &line)) {
3637 author_time = (time_t) atol(line);
3639 } else if (match_blame_header("author-tz ", &line)) {
3642 tz = ('0' - line[1]) * 60 * 60 * 10;
3643 tz += ('0' - line[2]) * 60 * 60;
3644 tz += ('0' - line[3]) * 60;
3645 tz += ('0' - line[4]) * 60;
3651 gmtime_r(&author_time, &commit->time);
3653 } else if (match_blame_header("summary ", &line)) {
3654 string_ncopy(commit->title, line, strlen(line));
3656 } else if (match_blame_header("filename ", &line)) {
3657 string_ncopy(commit->filename, line, strlen(line));
3665 blame_draw(struct view *view, struct line *line, unsigned int lineno)
3667 struct blame *blame = line->data;
3668 struct tm *time = NULL;
3669 char *id = NULL, *author = NULL;
3671 if (blame->commit && *blame->commit->filename) {
3672 id = blame->commit->id;
3673 author = blame->commit->author;
3674 time = &blame->commit->time;
3677 if (opt_date && draw_date(view, time))
3681 draw_field(view, LINE_MAIN_AUTHOR, author, AUTHOR_COLS, TRUE))
3684 if (draw_field(view, LINE_BLAME_ID, id, ID_COLS, FALSE))
3687 if (draw_lineno(view, lineno))
3690 draw_text(view, LINE_DEFAULT, blame->text, TRUE);
3695 blame_request(struct view *view, enum request request, struct line *line)
3697 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3698 struct blame *blame = line->data;
3702 if (!blame->commit) {
3703 report("No commit loaded yet");
3707 if (!strcmp(blame->commit->id, NULL_ID)) {
3708 char path[SIZEOF_STR];
3710 if (sq_quote(path, 0, view->vid) >= sizeof(path))
3712 string_format(opt_cmd, "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s 2>/dev/null", path);
3715 open_view(view, REQ_VIEW_DIFF, flags);
3726 blame_grep(struct view *view, struct line *line)
3728 struct blame *blame = line->data;
3729 struct blame_commit *commit = blame->commit;
3732 #define MATCH(text, on) \
3733 (on && *text && regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3736 char buf[DATE_COLS + 1];
3738 if (MATCH(commit->title, 1) ||
3739 MATCH(commit->author, opt_author) ||
3740 MATCH(commit->id, opt_date))
3743 if (strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time) &&
3748 return MATCH(blame->text, 1);
3754 blame_select(struct view *view, struct line *line)
3756 struct blame *blame = line->data;
3757 struct blame_commit *commit = blame->commit;
3762 if (!strcmp(commit->id, NULL_ID))
3763 string_ncopy(ref_commit, "HEAD", 4);
3765 string_copy_rev(ref_commit, commit->id);
3768 static struct view_ops blame_ops = {
3786 char rev[SIZEOF_REV];
3787 char name[SIZEOF_STR];
3791 char rev[SIZEOF_REV];
3792 char name[SIZEOF_STR];
3796 static char status_onbranch[SIZEOF_STR];
3797 static struct status stage_status;
3798 static enum line_type stage_line_type;
3800 /* Get fields from the diff line:
3801 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
3804 status_get_diff(struct status *file, char *buf, size_t bufsize)
3806 char *old_mode = buf + 1;
3807 char *new_mode = buf + 8;
3808 char *old_rev = buf + 15;
3809 char *new_rev = buf + 56;
3810 char *status = buf + 97;
3813 old_mode[-1] != ':' ||
3814 new_mode[-1] != ' ' ||
3815 old_rev[-1] != ' ' ||
3816 new_rev[-1] != ' ' ||
3820 file->status = *status;
3822 string_copy_rev(file->old.rev, old_rev);
3823 string_copy_rev(file->new.rev, new_rev);
3825 file->old.mode = strtoul(old_mode, NULL, 8);
3826 file->new.mode = strtoul(new_mode, NULL, 8);
3828 file->old.name[0] = file->new.name[0] = 0;
3834 status_run(struct view *view, const char cmd[], char status, enum line_type type)
3836 struct status *file = NULL;
3837 struct status *unmerged = NULL;
3838 char buf[SIZEOF_STR * 4];
3842 pipe = popen(cmd, "r");
3846 add_line_data(view, NULL, type);
3848 while (!feof(pipe) && !ferror(pipe)) {
3852 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
3855 bufsize += readsize;
3857 /* Process while we have NUL chars. */
3858 while ((sep = memchr(buf, 0, bufsize))) {
3859 size_t sepsize = sep - buf + 1;
3862 if (!realloc_lines(view, view->line_size + 1))
3865 file = calloc(1, sizeof(*file));
3869 add_line_data(view, file, type);
3872 /* Parse diff info part. */
3874 file->status = status;
3876 string_copy(file->old.rev, NULL_ID);
3878 } else if (!file->status) {
3879 if (!status_get_diff(file, buf, sepsize))
3883 memmove(buf, sep + 1, bufsize);
3885 sep = memchr(buf, 0, bufsize);
3888 sepsize = sep - buf + 1;
3890 /* Collapse all 'M'odified entries that
3891 * follow a associated 'U'nmerged entry.
3893 if (file->status == 'U') {
3896 } else if (unmerged) {
3897 int collapse = !strcmp(buf, unmerged->new.name);
3908 /* Grab the old name for rename/copy. */
3909 if (!*file->old.name &&
3910 (file->status == 'R' || file->status == 'C')) {
3911 sepsize = sep - buf + 1;
3912 string_ncopy(file->old.name, buf, sepsize);
3914 memmove(buf, sep + 1, bufsize);
3916 sep = memchr(buf, 0, bufsize);
3919 sepsize = sep - buf + 1;
3922 /* git-ls-files just delivers a NUL separated
3923 * list of file names similar to the second half
3924 * of the git-diff-* output. */
3925 string_ncopy(file->new.name, buf, sepsize);
3926 if (!*file->old.name)
3927 string_copy(file->old.name, file->new.name);
3929 memmove(buf, sep + 1, bufsize);
3940 if (!view->line[view->lines - 1].data)
3941 add_line_data(view, NULL, LINE_STAT_NONE);
3947 /* Don't show unmerged entries in the staged section. */
3948 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached -M HEAD"
3949 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
3950 #define STATUS_LIST_OTHER_CMD \
3951 "git ls-files -z --others --exclude-per-directory=.gitignore"
3952 #define STATUS_LIST_NO_HEAD_CMD \
3953 "git ls-files -z --cached --exclude-per-directory=.gitignore"
3955 #define STATUS_DIFF_INDEX_SHOW_CMD \
3956 "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s %s 2>/dev/null"
3958 #define STATUS_DIFF_FILES_SHOW_CMD \
3959 "git diff-files --root --patch-with-stat -C -M -- %s %s 2>/dev/null"
3961 #define STATUS_DIFF_NO_HEAD_SHOW_CMD \
3962 "git diff --no-color --patch-with-stat /dev/null %s 2>/dev/null"
3964 /* First parse staged info using git-diff-index(1), then parse unstaged
3965 * info using git-diff-files(1), and finally untracked files using
3966 * git-ls-files(1). */
3968 status_open(struct view *view)
3970 struct stat statbuf;
3971 char exclude[SIZEOF_STR];
3972 char indexcmd[SIZEOF_STR] = STATUS_DIFF_INDEX_CMD;
3973 char othercmd[SIZEOF_STR] = STATUS_LIST_OTHER_CMD;
3974 unsigned long prev_lineno = view->lineno;
3975 char indexstatus = 0;
3978 for (i = 0; i < view->lines; i++)
3979 free(view->line[i].data);
3981 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3984 if (!realloc_lines(view, view->line_size + 7))
3987 add_line_data(view, NULL, LINE_STAT_HEAD);
3989 string_copy(status_onbranch, "Initial commit");
3990 else if (!*opt_head)
3991 string_copy(status_onbranch, "Not currently on any branch");
3992 else if (!string_format(status_onbranch, "On branch %s", opt_head))
3996 string_copy(indexcmd, STATUS_LIST_NO_HEAD_CMD);
4000 if (!string_format(exclude, "%s/info/exclude", opt_git_dir))
4003 if (stat(exclude, &statbuf) >= 0) {
4004 size_t cmdsize = strlen(othercmd);
4006 if (!string_format_from(othercmd, &cmdsize, " %s", "--exclude-from=") ||
4007 sq_quote(othercmd, cmdsize, exclude) >= sizeof(othercmd))
4010 cmdsize = strlen(indexcmd);
4012 (!string_format_from(indexcmd, &cmdsize, " %s", "--exclude-from=") ||
4013 sq_quote(indexcmd, cmdsize, exclude) >= sizeof(indexcmd)))
4017 system("git update-index -q --refresh >/dev/null 2>/dev/null");
4019 if (!status_run(view, indexcmd, indexstatus, LINE_STAT_STAGED) ||
4020 !status_run(view, STATUS_DIFF_FILES_CMD, 0, LINE_STAT_UNSTAGED) ||
4021 !status_run(view, othercmd, '?', LINE_STAT_UNTRACKED))
4024 /* If all went well restore the previous line number to stay in
4025 * the context or select a line with something that can be
4027 if (prev_lineno >= view->lines)
4028 prev_lineno = view->lines - 1;
4029 while (prev_lineno < view->lines && !view->line[prev_lineno].data)
4031 while (prev_lineno > 0 && !view->line[prev_lineno].data)
4034 /* If the above fails, always skip the "On branch" line. */
4035 if (prev_lineno < view->lines)
4036 view->lineno = prev_lineno;
4040 if (view->lineno < view->offset)
4041 view->offset = view->lineno;
4042 else if (view->offset + view->height <= view->lineno)
4043 view->offset = view->lineno - view->height + 1;
4049 status_draw(struct view *view, struct line *line, unsigned int lineno)
4051 struct status *status = line->data;
4052 enum line_type type;
4056 switch (line->type) {
4057 case LINE_STAT_STAGED:
4058 type = LINE_STAT_SECTION;
4059 text = "Changes to be committed:";
4062 case LINE_STAT_UNSTAGED:
4063 type = LINE_STAT_SECTION;
4064 text = "Changed but not updated:";
4067 case LINE_STAT_UNTRACKED:
4068 type = LINE_STAT_SECTION;
4069 text = "Untracked files:";
4072 case LINE_STAT_NONE:
4073 type = LINE_DEFAULT;
4074 text = " (no files)";
4077 case LINE_STAT_HEAD:
4078 type = LINE_STAT_HEAD;
4079 text = status_onbranch;
4086 static char buf[] = { '?', ' ', ' ', ' ', 0 };
4088 buf[0] = status->status;
4089 if (draw_text(view, line->type, buf, TRUE))
4091 type = LINE_DEFAULT;
4092 text = status->new.name;
4095 draw_text(view, type, text, TRUE);
4100 status_enter(struct view *view, struct line *line)
4102 struct status *status = line->data;
4103 char oldpath[SIZEOF_STR] = "";
4104 char newpath[SIZEOF_STR] = "";
4107 enum open_flags split;
4109 if (line->type == LINE_STAT_NONE ||
4110 (!status && line[1].type == LINE_STAT_NONE)) {
4111 report("No file to diff");
4116 if (sq_quote(oldpath, 0, status->old.name) >= sizeof(oldpath))
4118 /* Diffs for unmerged entries are empty when pasing the
4119 * new path, so leave it empty. */
4120 if (status->status != 'U' &&
4121 sq_quote(newpath, 0, status->new.name) >= sizeof(newpath))
4126 line->type != LINE_STAT_UNTRACKED &&
4127 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
4130 switch (line->type) {
4131 case LINE_STAT_STAGED:
4133 if (!string_format_from(opt_cmd, &cmdsize,
4134 STATUS_DIFF_NO_HEAD_SHOW_CMD,
4138 if (!string_format_from(opt_cmd, &cmdsize,
4139 STATUS_DIFF_INDEX_SHOW_CMD,
4145 info = "Staged changes to %s";
4147 info = "Staged changes";
4150 case LINE_STAT_UNSTAGED:
4151 if (!string_format_from(opt_cmd, &cmdsize,
4152 STATUS_DIFF_FILES_SHOW_CMD, oldpath, newpath))
4155 info = "Unstaged changes to %s";
4157 info = "Unstaged changes";
4160 case LINE_STAT_UNTRACKED:
4165 report("No file to show");
4169 opt_pipe = fopen(status->new.name, "r");
4170 info = "Untracked file %s";
4173 case LINE_STAT_HEAD:
4177 die("line type %d not handled in switch", line->type);
4180 split = view_is_displayed(view) ? OPEN_SPLIT : 0;
4181 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | split);
4182 if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) {
4184 stage_status = *status;
4186 memset(&stage_status, 0, sizeof(stage_status));
4189 stage_line_type = line->type;
4190 string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.new.name);
4197 status_exists(struct status *status, enum line_type type)
4199 struct view *view = VIEW(REQ_VIEW_STATUS);
4202 for (line = view->line; line < view->line + view->lines; line++) {
4203 struct status *pos = line->data;
4205 if (line->type == type && pos &&
4206 !strcmp(status->new.name, pos->new.name))
4215 status_update_prepare(enum line_type type)
4217 char cmd[SIZEOF_STR];
4221 type != LINE_STAT_UNTRACKED &&
4222 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4226 case LINE_STAT_STAGED:
4227 string_add(cmd, cmdsize, "git update-index -z --index-info");
4230 case LINE_STAT_UNSTAGED:
4231 case LINE_STAT_UNTRACKED:
4232 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
4236 die("line type %d not handled in switch", type);
4239 return popen(cmd, "w");
4243 status_update_write(FILE *pipe, struct status *status, enum line_type type)
4245 char buf[SIZEOF_STR];
4250 case LINE_STAT_STAGED:
4251 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
4254 status->old.name, 0))
4258 case LINE_STAT_UNSTAGED:
4259 case LINE_STAT_UNTRACKED:
4260 if (!string_format_from(buf, &bufsize, "%s%c", status->new.name, 0))
4265 die("line type %d not handled in switch", type);
4268 while (!ferror(pipe) && written < bufsize) {
4269 written += fwrite(buf + written, 1, bufsize - written, pipe);
4272 return written == bufsize;
4276 status_update_file(struct status *status, enum line_type type)
4278 FILE *pipe = status_update_prepare(type);
4284 result = status_update_write(pipe, status, type);
4290 status_update_files(struct view *view, struct line *line)
4292 FILE *pipe = status_update_prepare(line->type);
4294 struct line *pos = view->line + view->lines;
4301 for (pos = line; pos < view->line + view->lines && pos->data; pos++)
4304 for (file = 0, done = 0; result && file < files; line++, file++) {
4305 int almost_done = file * 100 / files;
4307 if (almost_done > done) {
4309 string_format(view->ref, "updating file %u of %u (%d%% done)",
4311 update_view_title(view);
4313 result = status_update_write(pipe, line->data, line->type);
4321 status_update(struct view *view)
4323 struct line *line = &view->line[view->lineno];
4325 assert(view->lines);
4328 /* This should work even for the "On branch" line. */
4329 if (line < view->line + view->lines && !line[1].data) {
4330 report("Nothing to update");
4334 if (!status_update_files(view, line + 1)) {
4335 report("Failed to update file status");
4339 } else if (!status_update_file(line->data, line->type)) {
4340 report("Failed to update file status");
4348 status_request(struct view *view, enum request request, struct line *line)
4350 struct status *status = line->data;
4353 case REQ_STATUS_UPDATE:
4354 if (!status_update(view))
4358 case REQ_STATUS_MERGE:
4359 if (!status || status->status != 'U') {
4360 report("Merging only possible for files with unmerged status ('U').");
4363 open_mergetool(status->new.name);
4370 open_editor(status->status != '?', status->new.name);
4373 case REQ_VIEW_BLAME:
4375 string_copy(opt_file, status->new.name);
4381 /* After returning the status view has been split to
4382 * show the stage view. No further reloading is
4384 status_enter(view, line);
4388 /* Simply reload the view. */
4395 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
4401 status_select(struct view *view, struct line *line)
4403 struct status *status = line->data;
4404 char file[SIZEOF_STR] = "all files";
4408 if (status && !string_format(file, "'%s'", status->new.name))
4411 if (!status && line[1].type == LINE_STAT_NONE)
4414 switch (line->type) {
4415 case LINE_STAT_STAGED:
4416 text = "Press %s to unstage %s for commit";
4419 case LINE_STAT_UNSTAGED:
4420 text = "Press %s to stage %s for commit";
4423 case LINE_STAT_UNTRACKED:
4424 text = "Press %s to stage %s for addition";
4427 case LINE_STAT_HEAD:
4428 case LINE_STAT_NONE:
4429 text = "Nothing to update";
4433 die("line type %d not handled in switch", line->type);
4436 if (status && status->status == 'U') {
4437 text = "Press %s to resolve conflict in %s";
4438 key = get_key(REQ_STATUS_MERGE);
4441 key = get_key(REQ_STATUS_UPDATE);
4444 string_format(view->ref, text, key, file);
4448 status_grep(struct view *view, struct line *line)
4450 struct status *status = line->data;
4451 enum { S_STATUS, S_NAME, S_END } state;
4458 for (state = S_STATUS; state < S_END; state++) {
4462 case S_NAME: text = status->new.name; break;
4464 buf[0] = status->status;
4472 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4479 static struct view_ops status_ops = {
4491 stage_diff_line(FILE *pipe, struct line *line)
4493 char *buf = line->data;
4494 size_t bufsize = strlen(buf);
4497 while (!ferror(pipe) && written < bufsize) {
4498 written += fwrite(buf + written, 1, bufsize - written, pipe);
4503 return written == bufsize;
4507 stage_diff_write(FILE *pipe, struct line *line, struct line *end)
4509 while (line < end) {
4510 if (!stage_diff_line(pipe, line++))
4512 if (line->type == LINE_DIFF_CHUNK ||
4513 line->type == LINE_DIFF_HEADER)
4520 static struct line *
4521 stage_diff_find(struct view *view, struct line *line, enum line_type type)
4523 for (; view->line < line; line--)
4524 if (line->type == type)
4531 stage_update_chunk(struct view *view, struct line *chunk)
4533 char cmd[SIZEOF_STR];
4535 struct line *diff_hdr;
4538 diff_hdr = stage_diff_find(view, chunk, LINE_DIFF_HEADER);
4543 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4546 if (!string_format_from(cmd, &cmdsize,
4547 "git apply --whitespace=nowarn --cached %s - && "
4548 "git update-index -q --unmerged --refresh 2>/dev/null",
4549 stage_line_type == LINE_STAT_STAGED ? "-R" : ""))
4552 pipe = popen(cmd, "w");
4556 if (!stage_diff_write(pipe, diff_hdr, chunk) ||
4557 !stage_diff_write(pipe, chunk, view->line + view->lines))
4562 return chunk ? TRUE : FALSE;
4566 stage_update(struct view *view, struct line *line)
4568 struct line *chunk = NULL;
4570 if (!opt_no_head && stage_line_type != LINE_STAT_UNTRACKED)
4571 chunk = stage_diff_find(view, line, LINE_DIFF_CHUNK);
4574 if (!stage_update_chunk(view, chunk)) {
4575 report("Failed to apply chunk");
4579 } else if (!stage_status.status) {
4580 view = VIEW(REQ_VIEW_STATUS);
4582 for (line = view->line; line < view->line + view->lines; line++)
4583 if (line->type == stage_line_type)
4586 if (!status_update_files(view, line + 1)) {
4587 report("Failed to update files");
4591 } else if (!status_update_file(&stage_status, stage_line_type)) {
4592 report("Failed to update file");
4600 stage_request(struct view *view, enum request request, struct line *line)
4603 case REQ_STATUS_UPDATE:
4604 if (!stage_update(view, line))
4609 if (!stage_status.new.name[0])
4612 open_editor(stage_status.status != '?', stage_status.new.name);
4616 /* Reload everything ... */
4619 case REQ_VIEW_BLAME:
4620 if (stage_status.new.name[0]) {
4621 string_copy(opt_file, stage_status.new.name);
4627 return pager_request(view, request, line);
4633 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD | OPEN_NOMAXIMIZE);
4635 /* Check whether the staged entry still exists, and close the
4636 * stage view if it doesn't. */
4637 if (!status_exists(&stage_status, stage_line_type))
4638 return REQ_VIEW_CLOSE;
4640 if (stage_line_type == LINE_STAT_UNTRACKED)
4641 opt_pipe = fopen(stage_status.new.name, "r");
4643 string_copy(opt_cmd, view->cmd);
4644 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | OPEN_NOMAXIMIZE);
4649 static struct view_ops stage_ops = {
4665 char id[SIZEOF_REV]; /* SHA1 ID. */
4666 char title[128]; /* First line of the commit message. */
4667 char author[75]; /* Author of the commit. */
4668 struct tm time; /* Date from the author ident. */
4669 struct ref **refs; /* Repository references. */
4670 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
4671 size_t graph_size; /* The width of the graph array. */
4672 bool has_parents; /* Rewritten --parents seen. */
4675 /* Size of rev graph with no "padding" columns */
4676 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
4679 struct rev_graph *prev, *next, *parents;
4680 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
4682 struct commit *commit;
4684 unsigned int boundary:1;
4687 /* Parents of the commit being visualized. */
4688 static struct rev_graph graph_parents[4];
4690 /* The current stack of revisions on the graph. */
4691 static struct rev_graph graph_stacks[4] = {
4692 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
4693 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
4694 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
4695 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
4699 graph_parent_is_merge(struct rev_graph *graph)
4701 return graph->parents->size > 1;
4705 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
4707 struct commit *commit = graph->commit;
4709 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
4710 commit->graph[commit->graph_size++] = symbol;
4714 done_rev_graph(struct rev_graph *graph)
4716 if (graph_parent_is_merge(graph) &&
4717 graph->pos < graph->size - 1 &&
4718 graph->next->size == graph->size + graph->parents->size - 1) {
4719 size_t i = graph->pos + graph->parents->size - 1;
4721 graph->commit->graph_size = i * 2;
4722 while (i < graph->next->size - 1) {
4723 append_to_rev_graph(graph, ' ');
4724 append_to_rev_graph(graph, '\\');
4729 graph->size = graph->pos = 0;
4730 graph->commit = NULL;
4731 memset(graph->parents, 0, sizeof(*graph->parents));
4735 push_rev_graph(struct rev_graph *graph, char *parent)
4739 /* "Collapse" duplicate parents lines.
4741 * FIXME: This needs to also update update the drawn graph but
4742 * for now it just serves as a method for pruning graph lines. */
4743 for (i = 0; i < graph->size; i++)
4744 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
4747 if (graph->size < SIZEOF_REVITEMS) {
4748 string_copy_rev(graph->rev[graph->size++], parent);
4753 get_rev_graph_symbol(struct rev_graph *graph)
4757 if (graph->boundary)
4758 symbol = REVGRAPH_BOUND;
4759 else if (graph->parents->size == 0)
4760 symbol = REVGRAPH_INIT;
4761 else if (graph_parent_is_merge(graph))
4762 symbol = REVGRAPH_MERGE;
4763 else if (graph->pos >= graph->size)
4764 symbol = REVGRAPH_BRANCH;
4766 symbol = REVGRAPH_COMMIT;
4772 draw_rev_graph(struct rev_graph *graph)
4775 chtype separator, line;
4777 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
4778 static struct rev_filler fillers[] = {
4784 chtype symbol = get_rev_graph_symbol(graph);
4785 struct rev_filler *filler;
4788 if (opt_line_graphics)
4789 fillers[DEFAULT].line = line_graphics[LINE_GRAPHIC_VLINE];
4791 filler = &fillers[DEFAULT];
4793 for (i = 0; i < graph->pos; i++) {
4794 append_to_rev_graph(graph, filler->line);
4795 if (graph_parent_is_merge(graph->prev) &&
4796 graph->prev->pos == i)
4797 filler = &fillers[RSHARP];
4799 append_to_rev_graph(graph, filler->separator);
4802 /* Place the symbol for this revision. */
4803 append_to_rev_graph(graph, symbol);
4805 if (graph->prev->size > graph->size)
4806 filler = &fillers[RDIAG];
4808 filler = &fillers[DEFAULT];
4812 for (; i < graph->size; i++) {
4813 append_to_rev_graph(graph, filler->separator);
4814 append_to_rev_graph(graph, filler->line);
4815 if (graph_parent_is_merge(graph->prev) &&
4816 i < graph->prev->pos + graph->parents->size)
4817 filler = &fillers[RSHARP];
4818 if (graph->prev->size > graph->size)
4819 filler = &fillers[LDIAG];
4822 if (graph->prev->size > graph->size) {
4823 append_to_rev_graph(graph, filler->separator);
4824 if (filler->line != ' ')
4825 append_to_rev_graph(graph, filler->line);
4829 /* Prepare the next rev graph */
4831 prepare_rev_graph(struct rev_graph *graph)
4835 /* First, traverse all lines of revisions up to the active one. */
4836 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
4837 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
4840 push_rev_graph(graph->next, graph->rev[graph->pos]);
4843 /* Interleave the new revision parent(s). */
4844 for (i = 0; !graph->boundary && i < graph->parents->size; i++)
4845 push_rev_graph(graph->next, graph->parents->rev[i]);
4847 /* Lastly, put any remaining revisions. */
4848 for (i = graph->pos + 1; i < graph->size; i++)
4849 push_rev_graph(graph->next, graph->rev[i]);
4853 update_rev_graph(struct rev_graph *graph)
4855 /* If this is the finalizing update ... */
4857 prepare_rev_graph(graph);
4859 /* Graph visualization needs a one rev look-ahead,
4860 * so the first update doesn't visualize anything. */
4861 if (!graph->prev->commit)
4864 draw_rev_graph(graph->prev);
4865 done_rev_graph(graph->prev->prev);
4874 main_draw(struct view *view, struct line *line, unsigned int lineno)
4876 struct commit *commit = line->data;
4878 if (!*commit->author)
4881 if (opt_date && draw_date(view, &commit->time))
4885 draw_field(view, LINE_MAIN_AUTHOR, commit->author, AUTHOR_COLS, TRUE))
4888 if (opt_rev_graph && commit->graph_size &&
4889 draw_graphic(view, LINE_MAIN_REVGRAPH, commit->graph, commit->graph_size))
4892 if (opt_show_refs && commit->refs) {
4896 enum line_type type;
4898 if (commit->refs[i]->head)
4899 type = LINE_MAIN_HEAD;
4900 else if (commit->refs[i]->ltag)
4901 type = LINE_MAIN_LOCAL_TAG;
4902 else if (commit->refs[i]->tag)
4903 type = LINE_MAIN_TAG;
4904 else if (commit->refs[i]->tracked)
4905 type = LINE_MAIN_TRACKED;
4906 else if (commit->refs[i]->remote)
4907 type = LINE_MAIN_REMOTE;
4909 type = LINE_MAIN_REF;
4911 if (draw_text(view, type, "[", TRUE) ||
4912 draw_text(view, type, commit->refs[i]->name, TRUE) ||
4913 draw_text(view, type, "]", TRUE))
4916 if (draw_text(view, LINE_DEFAULT, " ", TRUE))
4918 } while (commit->refs[i++]->next);
4921 draw_text(view, LINE_DEFAULT, commit->title, TRUE);
4925 /* Reads git log --pretty=raw output and parses it into the commit struct. */
4927 main_read(struct view *view, char *line)
4929 static struct rev_graph *graph = graph_stacks;
4930 enum line_type type;
4931 struct commit *commit;
4934 if (!view->lines && !view->parent)
4935 die("No revisions match the given arguments.");
4936 update_rev_graph(graph);
4940 type = get_line_type(line);
4941 if (type == LINE_COMMIT) {
4942 commit = calloc(1, sizeof(struct commit));
4946 line += STRING_SIZE("commit ");
4948 graph->boundary = 1;
4952 string_copy_rev(commit->id, line);
4953 commit->refs = get_refs(commit->id);
4954 graph->commit = commit;
4955 add_line_data(view, commit, LINE_MAIN_COMMIT);
4957 while ((line = strchr(line, ' '))) {
4959 push_rev_graph(graph->parents, line);
4960 commit->has_parents = TRUE;
4967 commit = view->line[view->lines - 1].data;
4971 if (commit->has_parents)
4973 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
4978 /* Parse author lines where the name may be empty:
4979 * author <email@address.tld> 1138474660 +0100
4981 char *ident = line + STRING_SIZE("author ");
4982 char *nameend = strchr(ident, '<');
4983 char *emailend = strchr(ident, '>');
4985 if (!nameend || !emailend)
4988 update_rev_graph(graph);
4989 graph = graph->next;
4991 *nameend = *emailend = 0;
4992 ident = chomp_string(ident);
4994 ident = chomp_string(nameend + 1);
4999 string_ncopy(commit->author, ident, strlen(ident));
5001 /* Parse epoch and timezone */
5002 if (emailend[1] == ' ') {
5003 char *secs = emailend + 2;
5004 char *zone = strchr(secs, ' ');
5005 time_t time = (time_t) atol(secs);
5007 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
5011 tz = ('0' - zone[1]) * 60 * 60 * 10;
5012 tz += ('0' - zone[2]) * 60 * 60;
5013 tz += ('0' - zone[3]) * 60;
5014 tz += ('0' - zone[4]) * 60;
5022 gmtime_r(&time, &commit->time);
5027 /* Fill in the commit title if it has not already been set. */
5028 if (commit->title[0])
5031 /* Require titles to start with a non-space character at the
5032 * offset used by git log. */
5033 if (strncmp(line, " ", 4))
5036 /* Well, if the title starts with a whitespace character,
5037 * try to be forgiving. Otherwise we end up with no title. */
5038 while (isspace(*line))
5042 /* FIXME: More graceful handling of titles; append "..." to
5043 * shortened titles, etc. */
5045 string_ncopy(commit->title, line, strlen(line));
5052 main_request(struct view *view, enum request request, struct line *line)
5054 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
5056 if (request == REQ_ENTER)
5057 open_view(view, REQ_VIEW_DIFF, flags);
5065 grep_refs(struct ref **refs, regex_t *regex)
5073 if (regexec(regex, refs[i]->name, 1, &pmatch, 0) != REG_NOMATCH)
5075 } while (refs[i++]->next);
5081 main_grep(struct view *view, struct line *line)
5083 struct commit *commit = line->data;
5084 enum { S_TITLE, S_AUTHOR, S_DATE, S_REFS, S_END } state;
5085 char buf[DATE_COLS + 1];
5088 for (state = S_TITLE; state < S_END; state++) {
5092 case S_TITLE: text = commit->title; break;
5096 text = commit->author;
5101 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
5108 if (grep_refs(commit->refs, view->regex) == TRUE)
5115 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
5123 main_select(struct view *view, struct line *line)
5125 struct commit *commit = line->data;
5127 string_copy_rev(view->ref, commit->id);
5128 string_copy_rev(ref_commit, view->ref);
5131 static struct view_ops main_ops = {
5143 * Unicode / UTF-8 handling
5145 * NOTE: Much of the following code for dealing with unicode is derived from
5146 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
5147 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
5150 /* I've (over)annotated a lot of code snippets because I am not entirely
5151 * confident that the approach taken by this small UTF-8 interface is correct.
5155 unicode_width(unsigned long c)
5158 (c <= 0x115f /* Hangul Jamo */
5161 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
5163 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
5164 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
5165 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
5166 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
5167 || (c >= 0xffe0 && c <= 0xffe6)
5168 || (c >= 0x20000 && c <= 0x2fffd)
5169 || (c >= 0x30000 && c <= 0x3fffd)))
5173 return opt_tab_size;
5178 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
5179 * Illegal bytes are set one. */
5180 static const unsigned char utf8_bytes[256] = {
5181 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,
5182 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,
5183 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,
5184 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,
5185 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,
5186 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,
5187 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,
5188 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,
5191 /* Decode UTF-8 multi-byte representation into a unicode character. */
5192 static inline unsigned long
5193 utf8_to_unicode(const char *string, size_t length)
5195 unsigned long unicode;
5199 unicode = string[0];
5202 unicode = (string[0] & 0x1f) << 6;
5203 unicode += (string[1] & 0x3f);
5206 unicode = (string[0] & 0x0f) << 12;
5207 unicode += ((string[1] & 0x3f) << 6);
5208 unicode += (string[2] & 0x3f);
5211 unicode = (string[0] & 0x0f) << 18;
5212 unicode += ((string[1] & 0x3f) << 12);
5213 unicode += ((string[2] & 0x3f) << 6);
5214 unicode += (string[3] & 0x3f);
5217 unicode = (string[0] & 0x0f) << 24;
5218 unicode += ((string[1] & 0x3f) << 18);
5219 unicode += ((string[2] & 0x3f) << 12);
5220 unicode += ((string[3] & 0x3f) << 6);
5221 unicode += (string[4] & 0x3f);
5224 unicode = (string[0] & 0x01) << 30;
5225 unicode += ((string[1] & 0x3f) << 24);
5226 unicode += ((string[2] & 0x3f) << 18);
5227 unicode += ((string[3] & 0x3f) << 12);
5228 unicode += ((string[4] & 0x3f) << 6);
5229 unicode += (string[5] & 0x3f);
5232 die("Invalid unicode length");
5235 /* Invalid characters could return the special 0xfffd value but NUL
5236 * should be just as good. */
5237 return unicode > 0xffff ? 0 : unicode;
5240 /* Calculates how much of string can be shown within the given maximum width
5241 * and sets trimmed parameter to non-zero value if all of string could not be
5242 * shown. If the reserve flag is TRUE, it will reserve at least one
5243 * trailing character, which can be useful when drawing a delimiter.
5245 * Returns the number of bytes to output from string to satisfy max_width. */
5247 utf8_length(const char *string, int *width, size_t max_width, int *trimmed, bool reserve)
5249 const char *start = string;
5250 const char *end = strchr(string, '\0');
5251 unsigned char last_bytes = 0;
5252 size_t last_ucwidth = 0;
5257 while (string < end) {
5258 int c = *(unsigned char *) string;
5259 unsigned char bytes = utf8_bytes[c];
5261 unsigned long unicode;
5263 if (string + bytes > end)
5266 /* Change representation to figure out whether
5267 * it is a single- or double-width character. */
5269 unicode = utf8_to_unicode(string, bytes);
5270 /* FIXME: Graceful handling of invalid unicode character. */
5274 ucwidth = unicode_width(unicode);
5276 if (*width > max_width) {
5279 if (reserve && *width == max_width) {
5280 string -= last_bytes;
5281 *width -= last_ucwidth;
5288 last_ucwidth = ucwidth;
5291 return string - start;
5299 /* Whether or not the curses interface has been initialized. */
5300 static bool cursed = FALSE;
5302 /* The status window is used for polling keystrokes. */
5303 static WINDOW *status_win;
5305 static bool status_empty = TRUE;
5307 /* Update status and title window. */
5309 report(const char *msg, ...)
5311 struct view *view = display[current_view];
5317 char buf[SIZEOF_STR];
5320 va_start(args, msg);
5321 if (vsnprintf(buf, sizeof(buf), msg, args) >= sizeof(buf)) {
5322 buf[sizeof(buf) - 1] = 0;
5323 buf[sizeof(buf) - 2] = '.';
5324 buf[sizeof(buf) - 3] = '.';
5325 buf[sizeof(buf) - 4] = '.';
5331 if (!status_empty || *msg) {
5334 va_start(args, msg);
5336 wmove(status_win, 0, 0);
5338 vwprintw(status_win, msg, args);
5339 status_empty = FALSE;
5341 status_empty = TRUE;
5343 wclrtoeol(status_win);
5344 wrefresh(status_win);
5349 update_view_title(view);
5350 update_display_cursor(view);
5353 /* Controls when nodelay should be in effect when polling user input. */
5355 set_nonblocking_input(bool loading)
5357 static unsigned int loading_views;
5359 if ((loading == FALSE && loading_views-- == 1) ||
5360 (loading == TRUE && loading_views++ == 0))
5361 nodelay(status_win, loading);
5369 /* Initialize the curses library */
5370 if (isatty(STDIN_FILENO)) {
5371 cursed = !!initscr();
5373 /* Leave stdin and stdout alone when acting as a pager. */
5374 FILE *io = fopen("/dev/tty", "r+");
5377 die("Failed to open /dev/tty");
5378 cursed = !!newterm(NULL, io, io);
5382 die("Failed to initialize curses");
5384 nonl(); /* Tell curses not to do NL->CR/NL on output */
5385 cbreak(); /* Take input chars one at a time, no wait for \n */
5386 noecho(); /* Don't echo input */
5387 leaveok(stdscr, TRUE);
5392 getmaxyx(stdscr, y, x);
5393 status_win = newwin(1, 0, y - 1, 0);
5395 die("Failed to create status window");
5397 /* Enable keyboard mapping */
5398 keypad(status_win, TRUE);
5399 wbkgdset(status_win, get_line_attr(LINE_STATUS));
5401 TABSIZE = opt_tab_size;
5402 if (opt_line_graphics) {
5403 line_graphics[LINE_GRAPHIC_VLINE] = ACS_VLINE;
5408 read_prompt(const char *prompt)
5410 enum { READING, STOP, CANCEL } status = READING;
5411 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
5414 while (status == READING) {
5420 foreach_view (view, i)
5425 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
5426 wclrtoeol(status_win);
5428 /* Refresh, accept single keystroke of input */
5429 key = wgetch(status_win);
5434 status = pos ? STOP : CANCEL;
5452 if (pos >= sizeof(buf)) {
5453 report("Input string too long");
5458 buf[pos++] = (char) key;
5462 /* Clear the status window */
5463 status_empty = FALSE;
5466 if (status == CANCEL)
5475 * Repository references
5478 static struct ref *refs = NULL;
5479 static size_t refs_alloc = 0;
5480 static size_t refs_size = 0;
5482 /* Id <-> ref store */
5483 static struct ref ***id_refs = NULL;
5484 static size_t id_refs_alloc = 0;
5485 static size_t id_refs_size = 0;
5487 static struct ref **
5490 struct ref ***tmp_id_refs;
5491 struct ref **ref_list = NULL;
5492 size_t ref_list_alloc = 0;
5493 size_t ref_list_size = 0;
5496 for (i = 0; i < id_refs_size; i++)
5497 if (!strcmp(id, id_refs[i][0]->id))
5500 tmp_id_refs = realloc_items(id_refs, &id_refs_alloc, id_refs_size + 1,
5505 id_refs = tmp_id_refs;
5507 for (i = 0; i < refs_size; i++) {
5510 if (strcmp(id, refs[i].id))
5513 tmp = realloc_items(ref_list, &ref_list_alloc,
5514 ref_list_size + 1, sizeof(*ref_list));
5522 if (ref_list_size > 0)
5523 ref_list[ref_list_size - 1]->next = 1;
5524 ref_list[ref_list_size] = &refs[i];
5526 /* XXX: The properties of the commit chains ensures that we can
5527 * safely modify the shared ref. The repo references will
5528 * always be similar for the same id. */
5529 ref_list[ref_list_size]->next = 0;
5534 id_refs[id_refs_size++] = ref_list;
5540 read_ref(char *id, size_t idlen, char *name, size_t namelen)
5545 bool remote = FALSE;
5546 bool tracked = FALSE;
5547 bool check_replace = FALSE;
5550 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
5551 if (!strcmp(name + namelen - 3, "^{}")) {
5554 if (refs_size > 0 && refs[refs_size - 1].ltag == TRUE)
5555 check_replace = TRUE;
5561 namelen -= STRING_SIZE("refs/tags/");
5562 name += STRING_SIZE("refs/tags/");
5564 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
5566 namelen -= STRING_SIZE("refs/remotes/");
5567 name += STRING_SIZE("refs/remotes/");
5568 tracked = !strcmp(opt_remote, name);
5570 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5571 namelen -= STRING_SIZE("refs/heads/");
5572 name += STRING_SIZE("refs/heads/");
5573 head = !strncmp(opt_head, name, namelen);
5575 } else if (!strcmp(name, "HEAD")) {
5576 opt_no_head = FALSE;
5580 if (check_replace && !strcmp(name, refs[refs_size - 1].name)) {
5581 /* it's an annotated tag, replace the previous sha1 with the
5582 * resolved commit id; relies on the fact git-ls-remote lists
5583 * the commit id of an annotated tag right beofre the commit id
5585 refs[refs_size - 1].ltag = ltag;
5586 string_copy_rev(refs[refs_size - 1].id, id);
5590 refs = realloc_items(refs, &refs_alloc, refs_size + 1, sizeof(*refs));
5594 ref = &refs[refs_size++];
5595 ref->name = malloc(namelen + 1);
5599 strncpy(ref->name, name, namelen);
5600 ref->name[namelen] = 0;
5604 ref->remote = remote;
5605 ref->tracked = tracked;
5606 string_copy_rev(ref->id, id);
5614 const char *cmd_env = getenv("TIG_LS_REMOTE");
5615 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
5617 return read_properties(popen(cmd, "r"), "\t", read_ref);
5621 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
5623 if (!strcmp(name, "i18n.commitencoding"))
5624 string_ncopy(opt_encoding, value, valuelen);
5626 if (!strcmp(name, "core.editor"))
5627 string_ncopy(opt_editor, value, valuelen);
5629 /* branch.<head>.remote */
5631 !strncmp(name, "branch.", 7) &&
5632 !strncmp(name + 7, opt_head, strlen(opt_head)) &&
5633 !strcmp(name + 7 + strlen(opt_head), ".remote"))
5634 string_ncopy(opt_remote, value, valuelen);
5636 if (*opt_head && *opt_remote &&
5637 !strncmp(name, "branch.", 7) &&
5638 !strncmp(name + 7, opt_head, strlen(opt_head)) &&
5639 !strcmp(name + 7 + strlen(opt_head), ".merge")) {
5640 size_t from = strlen(opt_remote);
5642 if (!strncmp(value, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5643 value += STRING_SIZE("refs/heads/");
5644 valuelen -= STRING_SIZE("refs/heads/");
5647 if (!string_format_from(opt_remote, &from, "/%s", value))
5655 load_git_config(void)
5657 return read_properties(popen(GIT_CONFIG " --list", "r"),
5658 "=", read_repo_config_option);
5662 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
5664 if (!opt_git_dir[0]) {
5665 string_ncopy(opt_git_dir, name, namelen);
5667 } else if (opt_is_inside_work_tree == -1) {
5668 /* This can be 3 different values depending on the
5669 * version of git being used. If git-rev-parse does not
5670 * understand --is-inside-work-tree it will simply echo
5671 * the option else either "true" or "false" is printed.
5672 * Default to true for the unknown case. */
5673 opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
5675 } else if (opt_cdup[0] == ' ') {
5676 string_ncopy(opt_cdup, name, namelen);
5678 if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5679 namelen -= STRING_SIZE("refs/heads/");
5680 name += STRING_SIZE("refs/heads/");
5681 string_ncopy(opt_head, name, namelen);
5689 load_repo_info(void)
5692 FILE *pipe = popen("(git rev-parse --git-dir --is-inside-work-tree "
5693 " --show-cdup; git symbolic-ref HEAD) 2>/dev/null", "r");
5695 /* XXX: The line outputted by "--show-cdup" can be empty so
5696 * initialize it to something invalid to make it possible to
5697 * detect whether it has been set or not. */
5700 result = read_properties(pipe, "=", read_repo_info);
5701 if (opt_cdup[0] == ' ')
5708 read_properties(FILE *pipe, const char *separators,
5709 int (*read_property)(char *, size_t, char *, size_t))
5711 char buffer[BUFSIZ];
5718 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
5723 name = chomp_string(name);
5724 namelen = strcspn(name, separators);
5726 if (name[namelen]) {
5728 value = chomp_string(name + namelen + 1);
5729 valuelen = strlen(value);
5736 state = read_property(name, namelen, value, valuelen);
5739 if (state != ERR && ferror(pipe))
5752 static void __NORETURN
5755 /* XXX: Restore tty modes and let the OS cleanup the rest! */
5761 static void __NORETURN
5762 die(const char *err, ...)
5768 va_start(args, err);
5769 fputs("tig: ", stderr);
5770 vfprintf(stderr, err, args);
5771 fputs("\n", stderr);
5778 warn(const char *msg, ...)
5782 va_start(args, msg);
5783 fputs("tig warning: ", stderr);
5784 vfprintf(stderr, msg, args);
5785 fputs("\n", stderr);
5790 main(int argc, char *argv[])
5793 enum request request;
5796 signal(SIGINT, quit);
5798 if (setlocale(LC_ALL, "")) {
5799 char *codeset = nl_langinfo(CODESET);
5801 string_ncopy(opt_codeset, codeset, strlen(codeset));
5804 if (load_repo_info() == ERR)
5805 die("Failed to load repo info.");
5807 if (load_options() == ERR)
5808 die("Failed to load user config.");
5810 if (load_git_config() == ERR)
5811 die("Failed to load repo config.");
5813 if (!parse_options(argc, argv))
5816 /* Require a git repository unless when running in pager mode. */
5817 if (!opt_git_dir[0] && opt_request != REQ_VIEW_PAGER)
5818 die("Not a git repository");
5820 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
5823 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
5824 opt_iconv = iconv_open(opt_codeset, opt_encoding);
5825 if (opt_iconv == ICONV_NONE)
5826 die("Failed to initialize character set conversion");
5829 if (*opt_git_dir && load_refs() == ERR)
5830 die("Failed to load refs.");
5832 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
5833 view->cmd_env = getenv(view->cmd_env);
5835 request = opt_request;
5839 while (view_driver(display[current_view], request)) {
5843 foreach_view (view, i)
5846 /* Refresh, accept single keystroke of input */
5847 key = wgetch(status_win);
5849 /* wgetch() with nodelay() enabled returns ERR when there's no
5856 request = get_keybinding(display[current_view]->keymap, key);
5858 /* Some low-level request handling. This keeps access to
5859 * status_win restricted. */
5863 char *cmd = read_prompt(":");
5865 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
5866 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
5867 opt_request = REQ_VIEW_DIFF;
5869 opt_request = REQ_VIEW_PAGER;
5878 case REQ_SEARCH_BACK:
5880 const char *prompt = request == REQ_SEARCH
5882 char *search = read_prompt(prompt);
5885 string_ncopy(opt_search, search, strlen(search));
5890 case REQ_SCREEN_RESIZE:
5894 getmaxyx(stdscr, height, width);
5896 /* Resize the status view and let the view driver take
5897 * care of resizing the displayed views. */
5898 wresize(status_win, 1, width);
5899 mvwin(status_win, height - 1, 0);
5900 wrefresh(status_win);