1 /* Copyright (c) 2006-2008 Jonas Fonseca <fonseca@diku.dk>
3 * This program is free software; you can redistribute it and/or
4 * modify it under the terms of the GNU General Public License as
5 * published by the Free Software Foundation; either version 2 of
6 * the License, or (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
19 #define TIG_VERSION "unknown-version"
34 #include <sys/types.h>
45 /* ncurses(3): Must be defined to have extended wide-character functions. */
46 #define _XOPEN_SOURCE_EXTENDED
51 #define __NORETURN __attribute__((__noreturn__))
56 static void __NORETURN die(const char *err, ...);
57 static void warn(const char *msg, ...);
58 static void report(const char *msg, ...);
59 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, size_t, char *, size_t));
60 static void set_nonblocking_input(bool loading);
61 static size_t utf8_length(const char *string, size_t max_width, int *trimmed, bool reserve);
63 #define ABS(x) ((x) >= 0 ? (x) : -(x))
64 #define MIN(x, y) ((x) < (y) ? (x) : (y))
66 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
67 #define STRING_SIZE(x) (sizeof(x) - 1)
69 #define SIZEOF_STR 1024 /* Default string size. */
70 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
71 #define SIZEOF_REV 41 /* Holds a SHA-1 and an ending NUL */
75 #define REVGRAPH_INIT 'I'
76 #define REVGRAPH_MERGE 'M'
77 #define REVGRAPH_BRANCH '+'
78 #define REVGRAPH_COMMIT '*'
79 #define REVGRAPH_BOUND '^'
80 #define REVGRAPH_LINE '|'
82 #define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
84 /* This color name can be used to refer to the default term colors. */
85 #define COLOR_DEFAULT (-1)
87 #define ICONV_NONE ((iconv_t) -1)
89 #define ICONV_CONST /* nothing */
92 /* The format and size of the date column in the main view. */
93 #define DATE_FORMAT "%Y-%m-%d %H:%M"
94 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
96 #define AUTHOR_COLS 20
99 /* The default interval between line numbers. */
100 #define NUMBER_INTERVAL 5
104 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
106 #define NULL_ID "0000000000000000000000000000000000000000"
109 #define GIT_CONFIG "git config"
112 #define TIG_LS_REMOTE \
113 "git ls-remote $(git rev-parse --git-dir) 2>/dev/null"
115 #define TIG_DIFF_CMD \
116 "git show --pretty=fuller --no-color --root --patch-with-stat --find-copies-harder -C %s 2>/dev/null"
118 #define TIG_LOG_CMD \
119 "git log --no-color --cc --stat -n100 %s 2>/dev/null"
121 #define TIG_MAIN_CMD \
122 "git log --no-color --topo-order --parents --boundary --pretty=raw %s 2>/dev/null"
124 #define TIG_TREE_CMD \
127 #define TIG_BLOB_CMD \
128 "git cat-file blob %s"
130 /* XXX: Needs to be defined to the empty string. */
131 #define TIG_HELP_CMD ""
132 #define TIG_PAGER_CMD ""
133 #define TIG_STATUS_CMD ""
134 #define TIG_STAGE_CMD ""
135 #define TIG_BLAME_CMD ""
137 /* Some ascii-shorthands fitted into the ncurses namespace. */
139 #define KEY_RETURN '\r'
144 char *name; /* Ref name; tag or head names are shortened. */
145 char id[SIZEOF_REV]; /* Commit SHA1 ID */
146 unsigned int tag:1; /* Is it a tag? */
147 unsigned int ltag:1; /* If so, is the tag local? */
148 unsigned int remote:1; /* Is it a remote ref? */
149 unsigned int next:1; /* For ref lists: are there more refs? */
150 unsigned int head:1; /* Is it the current HEAD? */
153 static struct ref **get_refs(char *id);
162 set_from_int_map(struct int_map *map, size_t map_size,
163 int *value, const char *name, int namelen)
168 for (i = 0; i < map_size; i++)
169 if (namelen == map[i].namelen &&
170 !strncasecmp(name, map[i].name, namelen)) {
171 *value = map[i].value;
184 string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
186 if (srclen > dstlen - 1)
189 strncpy(dst, src, srclen);
193 /* Shorthands for safely copying into a fixed buffer. */
195 #define string_copy(dst, src) \
196 string_ncopy_do(dst, sizeof(dst), src, sizeof(src))
198 #define string_ncopy(dst, src, srclen) \
199 string_ncopy_do(dst, sizeof(dst), src, srclen)
201 #define string_copy_rev(dst, src) \
202 string_ncopy_do(dst, SIZEOF_REV, src, SIZEOF_REV - 1)
204 #define string_add(dst, from, src) \
205 string_ncopy_do(dst + (from), sizeof(dst) - (from), src, sizeof(src))
208 chomp_string(char *name)
212 while (isspace(*name))
215 namelen = strlen(name) - 1;
216 while (namelen > 0 && isspace(name[namelen]))
223 string_nformat(char *buf, size_t bufsize, size_t *bufpos, const char *fmt, ...)
226 size_t pos = bufpos ? *bufpos : 0;
229 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
235 return pos >= bufsize ? FALSE : TRUE;
238 #define string_format(buf, fmt, args...) \
239 string_nformat(buf, sizeof(buf), NULL, fmt, args)
241 #define string_format_from(buf, from, fmt, args...) \
242 string_nformat(buf, sizeof(buf), from, fmt, args)
245 string_enum_compare(const char *str1, const char *str2, int len)
249 #define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
251 /* Diff-Header == DIFF_HEADER */
252 for (i = 0; i < len; i++) {
253 if (toupper(str1[i]) == toupper(str2[i]))
256 if (string_enum_sep(str1[i]) &&
257 string_enum_sep(str2[i]))
260 return str1[i] - str2[i];
268 * NOTE: The following is a slightly modified copy of the git project's shell
269 * quoting routines found in the quote.c file.
271 * Help to copy the thing properly quoted for the shell safety. any single
272 * quote is replaced with '\'', any exclamation point is replaced with '\!',
273 * and the whole thing is enclosed in a
276 * original sq_quote result
277 * name ==> name ==> 'name'
278 * a b ==> a b ==> 'a b'
279 * a'b ==> a'\''b ==> 'a'\''b'
280 * a!b ==> a'\!'b ==> 'a'\!'b'
284 sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
288 #define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0)
291 while ((c = *src++)) {
292 if (c == '\'' || c == '!') {
303 if (bufsize < SIZEOF_STR)
315 /* XXX: Keep the view request first and in sync with views[]. */ \
316 REQ_GROUP("View switching") \
317 REQ_(VIEW_MAIN, "Show main view"), \
318 REQ_(VIEW_DIFF, "Show diff view"), \
319 REQ_(VIEW_LOG, "Show log view"), \
320 REQ_(VIEW_TREE, "Show tree view"), \
321 REQ_(VIEW_BLOB, "Show blob view"), \
322 REQ_(VIEW_BLAME, "Show blame view"), \
323 REQ_(VIEW_HELP, "Show help page"), \
324 REQ_(VIEW_PAGER, "Show pager view"), \
325 REQ_(VIEW_STATUS, "Show status view"), \
326 REQ_(VIEW_STAGE, "Show stage view"), \
328 REQ_GROUP("View manipulation") \
329 REQ_(ENTER, "Enter current line and scroll"), \
330 REQ_(NEXT, "Move to next"), \
331 REQ_(PREVIOUS, "Move to previous"), \
332 REQ_(VIEW_NEXT, "Move focus to next view"), \
333 REQ_(REFRESH, "Reload and refresh"), \
334 REQ_(MAXIMIZE, "Maximize the current view"), \
335 REQ_(VIEW_CLOSE, "Close the current view"), \
336 REQ_(QUIT, "Close all views and quit"), \
338 REQ_GROUP("Cursor navigation") \
339 REQ_(MOVE_UP, "Move cursor one line up"), \
340 REQ_(MOVE_DOWN, "Move cursor one line down"), \
341 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
342 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
343 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
344 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
346 REQ_GROUP("Scrolling") \
347 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
348 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
349 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
350 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
352 REQ_GROUP("Searching") \
353 REQ_(SEARCH, "Search the view"), \
354 REQ_(SEARCH_BACK, "Search backwards in the view"), \
355 REQ_(FIND_NEXT, "Find next search match"), \
356 REQ_(FIND_PREV, "Find previous search match"), \
359 REQ_(PROMPT, "Bring up the prompt"), \
360 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
361 REQ_(SCREEN_RESIZE, "Resize the screen"), \
362 REQ_(SHOW_VERSION, "Show version information"), \
363 REQ_(STOP_LOADING, "Stop all loading views"), \
364 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
365 REQ_(TOGGLE_DATE, "Toggle date display"), \
366 REQ_(TOGGLE_AUTHOR, "Toggle author display"), \
367 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"), \
368 REQ_(TOGGLE_REFS, "Toggle reference display (tags/branches)"), \
369 REQ_(STATUS_UPDATE, "Update file status"), \
370 REQ_(STATUS_MERGE, "Merge file using external tool"), \
371 REQ_(TREE_PARENT, "Switch to parent directory in tree view"), \
372 REQ_(EDIT, "Open in editor"), \
373 REQ_(NONE, "Do nothing")
376 /* User action requests. */
378 #define REQ_GROUP(help)
379 #define REQ_(req, help) REQ_##req
381 /* Offset all requests to avoid conflicts with ncurses getch values. */
382 REQ_OFFSET = KEY_MAX + 1,
389 struct request_info {
390 enum request request;
396 static struct request_info req_info[] = {
397 #define REQ_GROUP(help) { 0, NULL, 0, (help) },
398 #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
405 get_request(const char *name)
407 int namelen = strlen(name);
410 for (i = 0; i < ARRAY_SIZE(req_info); i++)
411 if (req_info[i].namelen == namelen &&
412 !string_enum_compare(req_info[i].name, name, namelen))
413 return req_info[i].request;
423 static const char usage[] =
424 "tig " TIG_VERSION " (" __DATE__ ")\n"
426 "Usage: tig [options] [revs] [--] [paths]\n"
427 " or: tig show [options] [revs] [--] [paths]\n"
428 " or: tig blame [rev] path\n"
430 " or: tig < [git command output]\n"
433 " -v, --version Show version and exit\n"
434 " -h, --help Show help message and exit";
436 /* Option and state variables. */
437 static bool opt_date = TRUE;
438 static bool opt_author = TRUE;
439 static bool opt_line_number = FALSE;
440 static bool opt_rev_graph = FALSE;
441 static bool opt_show_refs = TRUE;
442 static int opt_num_interval = NUMBER_INTERVAL;
443 static int opt_tab_size = TABSIZE;
444 static enum request opt_request = REQ_VIEW_MAIN;
445 static char opt_cmd[SIZEOF_STR] = "";
446 static char opt_path[SIZEOF_STR] = "";
447 static char opt_file[SIZEOF_STR] = "";
448 static char opt_ref[SIZEOF_REF] = "";
449 static char opt_head[SIZEOF_REF] = "";
450 static bool opt_no_head = TRUE;
451 static FILE *opt_pipe = NULL;
452 static char opt_encoding[20] = "UTF-8";
453 static bool opt_utf8 = TRUE;
454 static char opt_codeset[20] = "UTF-8";
455 static iconv_t opt_iconv = ICONV_NONE;
456 static char opt_search[SIZEOF_STR] = "";
457 static char opt_cdup[SIZEOF_STR] = "";
458 static char opt_git_dir[SIZEOF_STR] = "";
459 static signed char opt_is_inside_work_tree = -1; /* set to TRUE or FALSE */
460 static char opt_editor[SIZEOF_STR] = "";
463 parse_options(int argc, char *argv[])
467 bool seen_dashdash = FALSE;
470 if (!isatty(STDIN_FILENO)) {
471 opt_request = REQ_VIEW_PAGER;
479 subcommand = argv[1];
480 if (!strcmp(subcommand, "status") || !strcmp(subcommand, "-S")) {
481 opt_request = REQ_VIEW_STATUS;
482 if (!strcmp(subcommand, "-S"))
483 warn("`-S' has been deprecated; use `tig status' instead");
485 warn("ignoring arguments after `%s'", subcommand);
488 } else if (!strcmp(subcommand, "blame")) {
489 opt_request = REQ_VIEW_BLAME;
490 if (argc <= 2 || argc > 4)
491 die("invalid number of options to blame\n\n%s", usage);
495 string_ncopy(opt_ref, argv[i], strlen(argv[i]));
499 string_ncopy(opt_file, argv[i], strlen(argv[i]));
502 } else if (!strcmp(subcommand, "show")) {
503 opt_request = REQ_VIEW_DIFF;
505 } else if (!strcmp(subcommand, "log") || !strcmp(subcommand, "diff")) {
506 opt_request = subcommand[0] == 'l'
507 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
508 warn("`tig %s' has been deprecated", subcommand);
515 /* XXX: This is vulnerable to the user overriding
516 * options required for the main view parser. */
517 string_copy(opt_cmd, "git log --no-color --pretty=raw --boundary --parents");
519 string_format(opt_cmd, "git %s", subcommand);
521 buf_size = strlen(opt_cmd);
523 for (i = 1 + !!subcommand; i < argc; i++) {
526 if (seen_dashdash || !strcmp(opt, "--")) {
527 seen_dashdash = TRUE;
529 } else if (!strcmp(opt, "-v") || !strcmp(opt, "--version")) {
530 printf("tig version %s\n", TIG_VERSION);
533 } else if (!strcmp(opt, "-h") || !strcmp(opt, "--help")) {
534 printf("%s\n", usage);
538 opt_cmd[buf_size++] = ' ';
539 buf_size = sq_quote(opt_cmd, buf_size, opt);
540 if (buf_size >= sizeof(opt_cmd))
541 die("command too long");
544 opt_cmd[buf_size] = 0;
551 * Line-oriented content detection.
555 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
556 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
557 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
558 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
559 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
560 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
561 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
562 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
563 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
564 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
565 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
566 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
567 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
568 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
569 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
570 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
571 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
572 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
573 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
574 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
575 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
576 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
577 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
578 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
579 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
580 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
581 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
582 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
583 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
584 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
585 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
586 LINE(DELIMITER, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
587 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
588 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
589 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
590 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
591 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
592 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
593 LINE(MAIN_LOCAL_TAG,"", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
594 LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
595 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
596 LINE(MAIN_HEAD, "", COLOR_RED, COLOR_DEFAULT, A_BOLD), \
597 LINE(MAIN_REVGRAPH,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
598 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
599 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
600 LINE(STAT_HEAD, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \
601 LINE(STAT_SECTION, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
602 LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
603 LINE(STAT_STAGED, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
604 LINE(STAT_UNSTAGED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
605 LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
606 LINE(BLAME_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
607 LINE(BLAME_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
608 LINE(BLAME_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
609 LINE(BLAME_ID, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
610 LINE(BLAME_LINENO, "", COLOR_CYAN, COLOR_DEFAULT, 0)
613 #define LINE(type, line, fg, bg, attr) \
620 const char *name; /* Option name. */
621 int namelen; /* Size of option name. */
622 const char *line; /* The start of line to match. */
623 int linelen; /* Size of string to match. */
624 int fg, bg, attr; /* Color and text attributes for the lines. */
627 static struct line_info line_info[] = {
628 #define LINE(type, line, fg, bg, attr) \
629 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
634 static enum line_type
635 get_line_type(char *line)
637 int linelen = strlen(line);
640 for (type = 0; type < ARRAY_SIZE(line_info); type++)
641 /* Case insensitive search matches Signed-off-by lines better. */
642 if (linelen >= line_info[type].linelen &&
643 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
650 get_line_attr(enum line_type type)
652 assert(type < ARRAY_SIZE(line_info));
653 return COLOR_PAIR(type) | line_info[type].attr;
656 static struct line_info *
657 get_line_info(char *name)
659 size_t namelen = strlen(name);
662 for (type = 0; type < ARRAY_SIZE(line_info); type++)
663 if (namelen == line_info[type].namelen &&
664 !string_enum_compare(line_info[type].name, name, namelen))
665 return &line_info[type];
673 int default_bg = line_info[LINE_DEFAULT].bg;
674 int default_fg = line_info[LINE_DEFAULT].fg;
679 if (assume_default_colors(default_fg, default_bg) == ERR) {
680 default_bg = COLOR_BLACK;
681 default_fg = COLOR_WHITE;
684 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
685 struct line_info *info = &line_info[type];
686 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
687 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
689 init_pair(type, fg, bg);
697 unsigned int selected:1;
698 unsigned int dirty:1;
700 void *data; /* User data */
710 enum request request;
711 struct keybinding *next;
714 static struct keybinding default_keybindings[] = {
716 { 'm', REQ_VIEW_MAIN },
717 { 'd', REQ_VIEW_DIFF },
718 { 'l', REQ_VIEW_LOG },
719 { 't', REQ_VIEW_TREE },
720 { 'f', REQ_VIEW_BLOB },
721 { 'B', REQ_VIEW_BLAME },
722 { 'p', REQ_VIEW_PAGER },
723 { 'h', REQ_VIEW_HELP },
724 { 'S', REQ_VIEW_STATUS },
725 { 'c', REQ_VIEW_STAGE },
727 /* View manipulation */
728 { 'q', REQ_VIEW_CLOSE },
729 { KEY_TAB, REQ_VIEW_NEXT },
730 { KEY_RETURN, REQ_ENTER },
731 { KEY_UP, REQ_PREVIOUS },
732 { KEY_DOWN, REQ_NEXT },
733 { 'R', REQ_REFRESH },
734 { 'M', REQ_MAXIMIZE },
736 /* Cursor navigation */
737 { 'k', REQ_MOVE_UP },
738 { 'j', REQ_MOVE_DOWN },
739 { KEY_HOME, REQ_MOVE_FIRST_LINE },
740 { KEY_END, REQ_MOVE_LAST_LINE },
741 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
742 { ' ', REQ_MOVE_PAGE_DOWN },
743 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
744 { 'b', REQ_MOVE_PAGE_UP },
745 { '-', REQ_MOVE_PAGE_UP },
748 { KEY_IC, REQ_SCROLL_LINE_UP },
749 { KEY_DC, REQ_SCROLL_LINE_DOWN },
750 { 'w', REQ_SCROLL_PAGE_UP },
751 { 's', REQ_SCROLL_PAGE_DOWN },
755 { '?', REQ_SEARCH_BACK },
756 { 'n', REQ_FIND_NEXT },
757 { 'N', REQ_FIND_PREV },
761 { 'z', REQ_STOP_LOADING },
762 { 'v', REQ_SHOW_VERSION },
763 { 'r', REQ_SCREEN_REDRAW },
764 { '.', REQ_TOGGLE_LINENO },
765 { 'D', REQ_TOGGLE_DATE },
766 { 'A', REQ_TOGGLE_AUTHOR },
767 { 'g', REQ_TOGGLE_REV_GRAPH },
768 { 'F', REQ_TOGGLE_REFS },
770 { 'u', REQ_STATUS_UPDATE },
771 { 'M', REQ_STATUS_MERGE },
772 { ',', REQ_TREE_PARENT },
775 /* Using the ncurses SIGWINCH handler. */
776 { KEY_RESIZE, REQ_SCREEN_RESIZE },
779 #define KEYMAP_INFO \
793 #define KEYMAP_(name) KEYMAP_##name
798 static struct int_map keymap_table[] = {
799 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
804 #define set_keymap(map, name) \
805 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
807 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
810 add_keybinding(enum keymap keymap, enum request request, int key)
812 struct keybinding *keybinding;
814 keybinding = calloc(1, sizeof(*keybinding));
816 die("Failed to allocate keybinding");
818 keybinding->alias = key;
819 keybinding->request = request;
820 keybinding->next = keybindings[keymap];
821 keybindings[keymap] = keybinding;
824 /* Looks for a key binding first in the given map, then in the generic map, and
825 * lastly in the default keybindings. */
827 get_keybinding(enum keymap keymap, int key)
829 struct keybinding *kbd;
832 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
833 if (kbd->alias == key)
836 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
837 if (kbd->alias == key)
840 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
841 if (default_keybindings[i].alias == key)
842 return default_keybindings[i].request;
844 return (enum request) key;
853 static struct key key_table[] = {
854 { "Enter", KEY_RETURN },
856 { "Backspace", KEY_BACKSPACE },
858 { "Escape", KEY_ESC },
859 { "Left", KEY_LEFT },
860 { "Right", KEY_RIGHT },
862 { "Down", KEY_DOWN },
863 { "Insert", KEY_IC },
864 { "Delete", KEY_DC },
866 { "Home", KEY_HOME },
868 { "PageUp", KEY_PPAGE },
869 { "PageDown", KEY_NPAGE },
879 { "F10", KEY_F(10) },
880 { "F11", KEY_F(11) },
881 { "F12", KEY_F(12) },
885 get_key_value(const char *name)
889 for (i = 0; i < ARRAY_SIZE(key_table); i++)
890 if (!strcasecmp(key_table[i].name, name))
891 return key_table[i].value;
893 if (strlen(name) == 1 && isprint(*name))
900 get_key_name(int key_value)
902 static char key_char[] = "'X'";
906 for (key = 0; key < ARRAY_SIZE(key_table); key++)
907 if (key_table[key].value == key_value)
908 seq = key_table[key].name;
912 isprint(key_value)) {
913 key_char[1] = (char) key_value;
917 return seq ? seq : "'?'";
921 get_key(enum request request)
923 static char buf[BUFSIZ];
930 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
931 struct keybinding *keybinding = &default_keybindings[i];
933 if (keybinding->request != request)
936 if (!string_format_from(buf, &pos, "%s%s", sep,
937 get_key_name(keybinding->alias)))
938 return "Too many keybindings!";
948 char cmd[SIZEOF_STR];
951 static struct run_request *run_request;
952 static size_t run_requests;
955 add_run_request(enum keymap keymap, int key, int argc, char **argv)
957 struct run_request *tmp;
958 struct run_request req = { keymap, key };
961 for (bufpos = 0; argc > 0; argc--, argv++)
962 if (!string_format_from(req.cmd, &bufpos, "%s ", *argv))
965 req.cmd[bufpos - 1] = 0;
967 tmp = realloc(run_request, (run_requests + 1) * sizeof(*run_request));
972 run_request[run_requests++] = req;
974 return REQ_NONE + run_requests;
977 static struct run_request *
978 get_run_request(enum request request)
980 if (request <= REQ_NONE)
982 return &run_request[request - REQ_NONE - 1];
986 add_builtin_run_requests(void)
993 { KEYMAP_MAIN, 'C', { "git cherry-pick %(commit)" } },
994 { KEYMAP_GENERIC, 'G', { "git gc" } },
998 for (i = 0; i < ARRAY_SIZE(reqs); i++) {
1001 req = add_run_request(reqs[i].keymap, reqs[i].key, 1, reqs[i].argv);
1002 if (req != REQ_NONE)
1003 add_keybinding(reqs[i].keymap, req, reqs[i].key);
1008 * User config file handling.
1011 static struct int_map color_map[] = {
1012 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
1024 #define set_color(color, name) \
1025 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
1027 static struct int_map attr_map[] = {
1028 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
1035 ATTR_MAP(UNDERLINE),
1038 #define set_attribute(attr, name) \
1039 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
1041 static int config_lineno;
1042 static bool config_errors;
1043 static char *config_msg;
1045 /* Wants: object fgcolor bgcolor [attr] */
1047 option_color_command(int argc, char *argv[])
1049 struct line_info *info;
1051 if (argc != 3 && argc != 4) {
1052 config_msg = "Wrong number of arguments given to color command";
1056 info = get_line_info(argv[0]);
1058 if (!string_enum_compare(argv[0], "main-delim", strlen("main-delim"))) {
1059 info = get_line_info("delimiter");
1062 config_msg = "Unknown color name";
1067 if (set_color(&info->fg, argv[1]) == ERR ||
1068 set_color(&info->bg, argv[2]) == ERR) {
1069 config_msg = "Unknown color";
1073 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
1074 config_msg = "Unknown attribute";
1081 static bool parse_bool(const char *s)
1083 return (!strcmp(s, "1") || !strcmp(s, "true") ||
1084 !strcmp(s, "yes")) ? TRUE : FALSE;
1087 /* Wants: name = value */
1089 option_set_command(int argc, char *argv[])
1092 config_msg = "Wrong number of arguments given to set command";
1096 if (strcmp(argv[1], "=")) {
1097 config_msg = "No value assigned";
1101 if (!strcmp(argv[0], "show-author")) {
1102 opt_author = parse_bool(argv[2]);
1106 if (!strcmp(argv[0], "show-date")) {
1107 opt_date = parse_bool(argv[2]);
1111 if (!strcmp(argv[0], "show-rev-graph")) {
1112 opt_rev_graph = parse_bool(argv[2]);
1116 if (!strcmp(argv[0], "show-refs")) {
1117 opt_show_refs = parse_bool(argv[2]);
1121 if (!strcmp(argv[0], "show-line-numbers")) {
1122 opt_line_number = parse_bool(argv[2]);
1126 if (!strcmp(argv[0], "line-number-interval")) {
1127 opt_num_interval = atoi(argv[2]);
1131 if (!strcmp(argv[0], "tab-size")) {
1132 opt_tab_size = atoi(argv[2]);
1136 if (!strcmp(argv[0], "commit-encoding")) {
1137 char *arg = argv[2];
1138 int delimiter = *arg;
1141 switch (delimiter) {
1144 for (arg++, i = 0; arg[i]; i++)
1145 if (arg[i] == delimiter) {
1150 string_ncopy(opt_encoding, arg, strlen(arg));
1155 config_msg = "Unknown variable name";
1159 /* Wants: mode request key */
1161 option_bind_command(int argc, char *argv[])
1163 enum request request;
1168 config_msg = "Wrong number of arguments given to bind command";
1172 if (set_keymap(&keymap, argv[0]) == ERR) {
1173 config_msg = "Unknown key map";
1177 key = get_key_value(argv[1]);
1179 config_msg = "Unknown key";
1183 request = get_request(argv[2]);
1184 if (request == REQ_NONE) {
1185 const char *obsolete[] = { "cherry-pick" };
1186 size_t namelen = strlen(argv[2]);
1189 for (i = 0; i < ARRAY_SIZE(obsolete); i++) {
1190 if (namelen == strlen(obsolete[i]) &&
1191 !string_enum_compare(obsolete[i], argv[2], namelen)) {
1192 config_msg = "Obsolete request name";
1197 if (request == REQ_NONE && *argv[2]++ == '!')
1198 request = add_run_request(keymap, key, argc - 2, argv + 2);
1199 if (request == REQ_NONE) {
1200 config_msg = "Unknown request name";
1204 add_keybinding(keymap, request, key);
1210 set_option(char *opt, char *value)
1217 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1218 argv[argc++] = value;
1221 /* Nothing more to tokenize or last available token. */
1222 if (!*value || argc >= ARRAY_SIZE(argv))
1226 while (isspace(*value))
1230 if (!strcmp(opt, "color"))
1231 return option_color_command(argc, argv);
1233 if (!strcmp(opt, "set"))
1234 return option_set_command(argc, argv);
1236 if (!strcmp(opt, "bind"))
1237 return option_bind_command(argc, argv);
1239 config_msg = "Unknown option command";
1244 read_option(char *opt, size_t optlen, char *value, size_t valuelen)
1249 config_msg = "Internal error";
1251 /* Check for comment markers, since read_properties() will
1252 * only ensure opt and value are split at first " \t". */
1253 optlen = strcspn(opt, "#");
1257 if (opt[optlen] != 0) {
1258 config_msg = "No option value";
1262 /* Look for comment endings in the value. */
1263 size_t len = strcspn(value, "#");
1265 if (len < valuelen) {
1267 value[valuelen] = 0;
1270 status = set_option(opt, value);
1273 if (status == ERR) {
1274 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1275 config_lineno, (int) optlen, opt, config_msg);
1276 config_errors = TRUE;
1279 /* Always keep going if errors are encountered. */
1284 load_option_file(const char *path)
1288 /* It's ok that the file doesn't exist. */
1289 file = fopen(path, "r");
1294 config_errors = FALSE;
1296 if (read_properties(file, " \t", read_option) == ERR ||
1297 config_errors == TRUE)
1298 fprintf(stderr, "Errors while loading %s.\n", path);
1304 char *home = getenv("HOME");
1305 char *tigrc_user = getenv("TIGRC_USER");
1306 char *tigrc_system = getenv("TIGRC_SYSTEM");
1307 char buf[SIZEOF_STR];
1309 add_builtin_run_requests();
1311 if (!tigrc_system) {
1312 if (!string_format(buf, "%s/tigrc", SYSCONFDIR))
1316 load_option_file(tigrc_system);
1319 if (!home || !string_format(buf, "%s/.tigrc", home))
1323 load_option_file(tigrc_user);
1336 /* The display array of active views and the index of the current view. */
1337 static struct view *display[2];
1338 static unsigned int current_view;
1340 /* Reading from the prompt? */
1341 static bool input_mode = FALSE;
1343 #define foreach_displayed_view(view, i) \
1344 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1346 #define displayed_views() (display[1] != NULL ? 2 : 1)
1348 /* Current head and commit ID */
1349 static char ref_blob[SIZEOF_REF] = "";
1350 static char ref_commit[SIZEOF_REF] = "HEAD";
1351 static char ref_head[SIZEOF_REF] = "HEAD";
1354 const char *name; /* View name */
1355 const char *cmd_fmt; /* Default command line format */
1356 const char *cmd_env; /* Command line set via environment */
1357 const char *id; /* Points to either of ref_{head,commit,blob} */
1359 struct view_ops *ops; /* View operations */
1361 enum keymap keymap; /* What keymap does this view have */
1362 bool git_dir; /* Whether the view requires a git directory. */
1364 char cmd[SIZEOF_STR]; /* Command buffer */
1365 char ref[SIZEOF_REF]; /* Hovered commit reference */
1366 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1368 int height, width; /* The width and height of the main window */
1369 WINDOW *win; /* The main window */
1370 WINDOW *title; /* The title window living below the main window */
1373 unsigned long offset; /* Offset of the window top */
1374 unsigned long lineno; /* Current line number */
1377 char grep[SIZEOF_STR]; /* Search string */
1378 regex_t *regex; /* Pre-compiled regex */
1380 /* If non-NULL, points to the view that opened this view. If this view
1381 * is closed tig will switch back to the parent view. */
1382 struct view *parent;
1385 size_t lines; /* Total number of lines */
1386 struct line *line; /* Line index */
1387 size_t line_alloc; /* Total number of allocated lines */
1388 size_t line_size; /* Total number of used lines */
1389 unsigned int digits; /* Number of digits in the lines member. */
1397 /* What type of content being displayed. Used in the title bar. */
1399 /* Open and reads in all view content. */
1400 bool (*open)(struct view *view);
1401 /* Read one line; updates view->line. */
1402 bool (*read)(struct view *view, char *data);
1403 /* Draw one line; @lineno must be < view->height. */
1404 bool (*draw)(struct view *view, struct line *line, unsigned int lineno, bool selected);
1405 /* Depending on view handle a special requests. */
1406 enum request (*request)(struct view *view, enum request request, struct line *line);
1407 /* Search for regex in a line. */
1408 bool (*grep)(struct view *view, struct line *line);
1410 void (*select)(struct view *view, struct line *line);
1413 static struct view_ops pager_ops;
1414 static struct view_ops main_ops;
1415 static struct view_ops tree_ops;
1416 static struct view_ops blob_ops;
1417 static struct view_ops blame_ops;
1418 static struct view_ops help_ops;
1419 static struct view_ops status_ops;
1420 static struct view_ops stage_ops;
1422 #define VIEW_STR(name, cmd, env, ref, ops, map, git) \
1423 { name, cmd, #env, ref, ops, map, git }
1425 #define VIEW_(id, name, ops, git, ref) \
1426 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id, git)
1429 static struct view views[] = {
1430 VIEW_(MAIN, "main", &main_ops, TRUE, ref_head),
1431 VIEW_(DIFF, "diff", &pager_ops, TRUE, ref_commit),
1432 VIEW_(LOG, "log", &pager_ops, TRUE, ref_head),
1433 VIEW_(TREE, "tree", &tree_ops, TRUE, ref_commit),
1434 VIEW_(BLOB, "blob", &blob_ops, TRUE, ref_blob),
1435 VIEW_(BLAME, "blame", &blame_ops, TRUE, ref_commit),
1436 VIEW_(HELP, "help", &help_ops, FALSE, ""),
1437 VIEW_(PAGER, "pager", &pager_ops, FALSE, "stdin"),
1438 VIEW_(STATUS, "status", &status_ops, TRUE, ""),
1439 VIEW_(STAGE, "stage", &stage_ops, TRUE, ""),
1442 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1443 #define VIEW_REQ(view) ((view) - views + REQ_OFFSET + 1)
1445 #define foreach_view(view, i) \
1446 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1448 #define view_is_displayed(view) \
1449 (view == display[0] || view == display[1])
1452 draw_text(struct view *view, const char *string, int max_len,
1453 bool use_tilde, bool selected)
1456 int trimmed = FALSE;
1462 len = utf8_length(string, max_len, &trimmed, use_tilde);
1464 len = strlen(string);
1465 if (len > max_len) {
1474 waddnstr(view->win, string, len);
1475 if (trimmed && use_tilde) {
1477 wattrset(view->win, get_line_attr(LINE_DELIMITER));
1478 waddch(view->win, '~');
1486 draw_lineno(struct view *view, unsigned int lineno, int max, bool selected)
1488 static char fmt[] = "%1ld";
1489 char number[10] = " ";
1490 int max_number = MIN(view->digits, STRING_SIZE(number));
1491 bool showtrimmed = FALSE;
1494 lineno += view->offset + 1;
1495 if (lineno == 1 || (lineno % opt_num_interval) == 0) {
1496 if (view->digits <= 9)
1497 fmt[1] = '0' + view->digits;
1499 if (!string_format(number, fmt, lineno))
1504 if (max < max_number)
1507 col = draw_text(view, number, max_number, showtrimmed, selected);
1510 wattrset(view->win, A_NORMAL);
1511 waddch(view->win, ACS_VLINE);
1515 waddch(view->win, ' ');
1523 draw_view_line(struct view *view, unsigned int lineno)
1526 bool selected = (view->offset + lineno == view->lineno);
1529 assert(view_is_displayed(view));
1531 if (view->offset + lineno >= view->lines)
1534 line = &view->line[view->offset + lineno];
1537 line->selected = TRUE;
1538 view->ops->select(view, line);
1539 } else if (line->selected) {
1540 line->selected = FALSE;
1541 wmove(view->win, lineno, 0);
1542 wclrtoeol(view->win);
1545 scrollok(view->win, FALSE);
1546 draw_ok = view->ops->draw(view, line, lineno, selected);
1547 scrollok(view->win, TRUE);
1553 redraw_view_dirty(struct view *view)
1558 for (lineno = 0; lineno < view->height; lineno++) {
1559 struct line *line = &view->line[view->offset + lineno];
1565 if (!draw_view_line(view, lineno))
1571 redrawwin(view->win);
1573 wnoutrefresh(view->win);
1575 wrefresh(view->win);
1579 redraw_view_from(struct view *view, int lineno)
1581 assert(0 <= lineno && lineno < view->height);
1583 for (; lineno < view->height; lineno++) {
1584 if (!draw_view_line(view, lineno))
1588 redrawwin(view->win);
1590 wnoutrefresh(view->win);
1592 wrefresh(view->win);
1596 redraw_view(struct view *view)
1599 redraw_view_from(view, 0);
1604 update_view_title(struct view *view)
1606 char buf[SIZEOF_STR];
1607 char state[SIZEOF_STR];
1608 size_t bufpos = 0, statelen = 0;
1610 assert(view_is_displayed(view));
1612 if (view != VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) {
1613 unsigned int view_lines = view->offset + view->height;
1614 unsigned int lines = view->lines
1615 ? MIN(view_lines, view->lines) * 100 / view->lines
1618 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1625 time_t secs = time(NULL) - view->start_time;
1627 /* Three git seconds are a long time ... */
1629 string_format_from(state, &statelen, " %lds", secs);
1633 string_format_from(buf, &bufpos, "[%s]", view->name);
1634 if (*view->ref && bufpos < view->width) {
1635 size_t refsize = strlen(view->ref);
1636 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1638 if (minsize < view->width)
1639 refsize = view->width - minsize + 7;
1640 string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
1643 if (statelen && bufpos < view->width) {
1644 string_format_from(buf, &bufpos, " %s", state);
1647 if (view == display[current_view])
1648 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1650 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1652 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1653 wclrtoeol(view->title);
1654 wmove(view->title, 0, view->width - 1);
1657 wnoutrefresh(view->title);
1659 wrefresh(view->title);
1663 resize_display(void)
1666 struct view *base = display[0];
1667 struct view *view = display[1] ? display[1] : display[0];
1669 /* Setup window dimensions */
1671 getmaxyx(stdscr, base->height, base->width);
1673 /* Make room for the status window. */
1677 /* Horizontal split. */
1678 view->width = base->width;
1679 view->height = SCALE_SPLIT_VIEW(base->height);
1680 base->height -= view->height;
1682 /* Make room for the title bar. */
1686 /* Make room for the title bar. */
1691 foreach_displayed_view (view, i) {
1693 view->win = newwin(view->height, 0, offset, 0);
1695 die("Failed to create %s view", view->name);
1697 scrollok(view->win, TRUE);
1699 view->title = newwin(1, 0, offset + view->height, 0);
1701 die("Failed to create title window");
1704 wresize(view->win, view->height, view->width);
1705 mvwin(view->win, offset, 0);
1706 mvwin(view->title, offset + view->height, 0);
1709 offset += view->height + 1;
1714 redraw_display(void)
1719 foreach_displayed_view (view, i) {
1721 update_view_title(view);
1726 update_display_cursor(struct view *view)
1728 /* Move the cursor to the right-most column of the cursor line.
1730 * XXX: This could turn out to be a bit expensive, but it ensures that
1731 * the cursor does not jump around. */
1733 wmove(view->win, view->lineno - view->offset, view->width - 1);
1734 wrefresh(view->win);
1742 /* Scrolling backend */
1744 do_scroll_view(struct view *view, int lines)
1746 bool redraw_current_line = FALSE;
1748 /* The rendering expects the new offset. */
1749 view->offset += lines;
1751 assert(0 <= view->offset && view->offset < view->lines);
1754 /* Move current line into the view. */
1755 if (view->lineno < view->offset) {
1756 view->lineno = view->offset;
1757 redraw_current_line = TRUE;
1758 } else if (view->lineno >= view->offset + view->height) {
1759 view->lineno = view->offset + view->height - 1;
1760 redraw_current_line = TRUE;
1763 assert(view->offset <= view->lineno && view->lineno < view->lines);
1765 /* Redraw the whole screen if scrolling is pointless. */
1766 if (view->height < ABS(lines)) {
1770 int line = lines > 0 ? view->height - lines : 0;
1771 int end = line + ABS(lines);
1773 wscrl(view->win, lines);
1775 for (; line < end; line++) {
1776 if (!draw_view_line(view, line))
1780 if (redraw_current_line)
1781 draw_view_line(view, view->lineno - view->offset);
1784 redrawwin(view->win);
1785 wrefresh(view->win);
1789 /* Scroll frontend */
1791 scroll_view(struct view *view, enum request request)
1795 assert(view_is_displayed(view));
1798 case REQ_SCROLL_PAGE_DOWN:
1799 lines = view->height;
1800 case REQ_SCROLL_LINE_DOWN:
1801 if (view->offset + lines > view->lines)
1802 lines = view->lines - view->offset;
1804 if (lines == 0 || view->offset + view->height >= view->lines) {
1805 report("Cannot scroll beyond the last line");
1810 case REQ_SCROLL_PAGE_UP:
1811 lines = view->height;
1812 case REQ_SCROLL_LINE_UP:
1813 if (lines > view->offset)
1814 lines = view->offset;
1817 report("Cannot scroll beyond the first line");
1825 die("request %d not handled in switch", request);
1828 do_scroll_view(view, lines);
1833 move_view(struct view *view, enum request request)
1835 int scroll_steps = 0;
1839 case REQ_MOVE_FIRST_LINE:
1840 steps = -view->lineno;
1843 case REQ_MOVE_LAST_LINE:
1844 steps = view->lines - view->lineno - 1;
1847 case REQ_MOVE_PAGE_UP:
1848 steps = view->height > view->lineno
1849 ? -view->lineno : -view->height;
1852 case REQ_MOVE_PAGE_DOWN:
1853 steps = view->lineno + view->height >= view->lines
1854 ? view->lines - view->lineno - 1 : view->height;
1866 die("request %d not handled in switch", request);
1869 if (steps <= 0 && view->lineno == 0) {
1870 report("Cannot move beyond the first line");
1873 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1874 report("Cannot move beyond the last line");
1878 /* Move the current line */
1879 view->lineno += steps;
1880 assert(0 <= view->lineno && view->lineno < view->lines);
1882 /* Check whether the view needs to be scrolled */
1883 if (view->lineno < view->offset ||
1884 view->lineno >= view->offset + view->height) {
1885 scroll_steps = steps;
1886 if (steps < 0 && -steps > view->offset) {
1887 scroll_steps = -view->offset;
1889 } else if (steps > 0) {
1890 if (view->lineno == view->lines - 1 &&
1891 view->lines > view->height) {
1892 scroll_steps = view->lines - view->offset - 1;
1893 if (scroll_steps >= view->height)
1894 scroll_steps -= view->height - 1;
1899 if (!view_is_displayed(view)) {
1900 view->offset += scroll_steps;
1901 assert(0 <= view->offset && view->offset < view->lines);
1902 view->ops->select(view, &view->line[view->lineno]);
1906 /* Repaint the old "current" line if we be scrolling */
1907 if (ABS(steps) < view->height)
1908 draw_view_line(view, view->lineno - steps - view->offset);
1911 do_scroll_view(view, scroll_steps);
1915 /* Draw the current line */
1916 draw_view_line(view, view->lineno - view->offset);
1918 redrawwin(view->win);
1919 wrefresh(view->win);
1928 static void search_view(struct view *view, enum request request);
1931 find_next_line(struct view *view, unsigned long lineno, struct line *line)
1933 assert(view_is_displayed(view));
1935 if (!view->ops->grep(view, line))
1938 if (lineno - view->offset >= view->height) {
1939 view->offset = lineno;
1940 view->lineno = lineno;
1944 unsigned long old_lineno = view->lineno - view->offset;
1946 view->lineno = lineno;
1947 draw_view_line(view, old_lineno);
1949 draw_view_line(view, view->lineno - view->offset);
1950 redrawwin(view->win);
1951 wrefresh(view->win);
1954 report("Line %ld matches '%s'", lineno + 1, view->grep);
1959 find_next(struct view *view, enum request request)
1961 unsigned long lineno = view->lineno;
1966 report("No previous search");
1968 search_view(view, request);
1978 case REQ_SEARCH_BACK:
1987 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
1988 lineno += direction;
1990 /* Note, lineno is unsigned long so will wrap around in which case it
1991 * will become bigger than view->lines. */
1992 for (; lineno < view->lines; lineno += direction) {
1993 struct line *line = &view->line[lineno];
1995 if (find_next_line(view, lineno, line))
1999 report("No match found for '%s'", view->grep);
2003 search_view(struct view *view, enum request request)
2008 regfree(view->regex);
2011 view->regex = calloc(1, sizeof(*view->regex));
2016 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
2017 if (regex_err != 0) {
2018 char buf[SIZEOF_STR] = "unknown error";
2020 regerror(regex_err, view->regex, buf, sizeof(buf));
2021 report("Search failed: %s", buf);
2025 string_copy(view->grep, opt_search);
2027 find_next(view, request);
2031 * Incremental updating
2035 end_update(struct view *view)
2039 set_nonblocking_input(FALSE);
2040 if (view->pipe == stdin)
2048 begin_update(struct view *view)
2054 string_copy(view->cmd, opt_cmd);
2056 /* When running random commands, initially show the
2057 * command in the title. However, it maybe later be
2058 * overwritten if a commit line is selected. */
2059 if (view == VIEW(REQ_VIEW_PAGER))
2060 string_copy(view->ref, view->cmd);
2064 } else if (view == VIEW(REQ_VIEW_TREE)) {
2065 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2066 char path[SIZEOF_STR];
2068 if (strcmp(view->vid, view->id))
2069 opt_path[0] = path[0] = 0;
2070 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
2073 if (!string_format(view->cmd, format, view->id, path))
2077 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2078 const char *id = view->id;
2080 if (!string_format(view->cmd, format, id, id, id, id, id))
2083 /* Put the current ref_* value to the view title ref
2084 * member. This is needed by the blob view. Most other
2085 * views sets it automatically after loading because the
2086 * first line is a commit line. */
2087 string_copy_rev(view->ref, view->id);
2090 /* Special case for the pager view. */
2092 view->pipe = opt_pipe;
2095 view->pipe = popen(view->cmd, "r");
2101 set_nonblocking_input(TRUE);
2106 string_copy_rev(view->vid, view->id);
2111 for (i = 0; i < view->lines; i++)
2112 if (view->line[i].data)
2113 free(view->line[i].data);
2119 view->start_time = time(NULL);
2124 #define ITEM_CHUNK_SIZE 256
2126 realloc_items(void *mem, size_t *size, size_t new_size, size_t item_size)
2128 size_t num_chunks = *size / ITEM_CHUNK_SIZE;
2129 size_t num_chunks_new = (new_size + ITEM_CHUNK_SIZE - 1) / ITEM_CHUNK_SIZE;
2131 if (mem == NULL || num_chunks != num_chunks_new) {
2132 *size = num_chunks_new * ITEM_CHUNK_SIZE;
2133 mem = realloc(mem, *size * item_size);
2139 static struct line *
2140 realloc_lines(struct view *view, size_t line_size)
2142 size_t alloc = view->line_alloc;
2143 struct line *tmp = realloc_items(view->line, &alloc, line_size,
2144 sizeof(*view->line));
2150 view->line_alloc = alloc;
2151 view->line_size = line_size;
2156 update_view(struct view *view)
2158 char in_buffer[BUFSIZ];
2159 char out_buffer[BUFSIZ * 2];
2161 /* The number of lines to read. If too low it will cause too much
2162 * redrawing (and possible flickering), if too high responsiveness
2164 unsigned long lines = view->height;
2165 int redraw_from = -1;
2170 /* Only redraw if lines are visible. */
2171 if (view->offset + view->height >= view->lines)
2172 redraw_from = view->lines - view->offset;
2174 /* FIXME: This is probably not perfect for backgrounded views. */
2175 if (!realloc_lines(view, view->lines + lines))
2178 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
2179 size_t linelen = strlen(line);
2182 line[linelen - 1] = 0;
2184 if (opt_iconv != ICONV_NONE) {
2185 ICONV_CONST char *inbuf = line;
2186 size_t inlen = linelen;
2188 char *outbuf = out_buffer;
2189 size_t outlen = sizeof(out_buffer);
2193 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
2194 if (ret != (size_t) -1) {
2196 linelen = strlen(out_buffer);
2200 if (!view->ops->read(view, line))
2210 lines = view->lines;
2211 for (digits = 0; lines; digits++)
2214 /* Keep the displayed view in sync with line number scaling. */
2215 if (digits != view->digits) {
2216 view->digits = digits;
2221 if (!view_is_displayed(view))
2224 if (view == VIEW(REQ_VIEW_TREE)) {
2225 /* Clear the view and redraw everything since the tree sorting
2226 * might have rearranged things. */
2229 } else if (redraw_from >= 0) {
2230 /* If this is an incremental update, redraw the previous line
2231 * since for commits some members could have changed when
2232 * loading the main view. */
2233 if (redraw_from > 0)
2236 /* Since revision graph visualization requires knowledge
2237 * about the parent commit, it causes a further one-off
2238 * needed to be redrawn for incremental updates. */
2239 if (redraw_from > 0 && opt_rev_graph)
2242 /* Incrementally draw avoids flickering. */
2243 redraw_view_from(view, redraw_from);
2246 if (view == VIEW(REQ_VIEW_BLAME))
2247 redraw_view_dirty(view);
2249 /* Update the title _after_ the redraw so that if the redraw picks up a
2250 * commit reference in view->ref it'll be available here. */
2251 update_view_title(view);
2254 if (ferror(view->pipe)) {
2255 report("Failed to read: %s", strerror(errno));
2258 } else if (feof(view->pipe)) {
2266 report("Allocation failure");
2269 if (view->ops->read(view, NULL))
2274 static struct line *
2275 add_line_data(struct view *view, void *data, enum line_type type)
2277 struct line *line = &view->line[view->lines++];
2279 memset(line, 0, sizeof(*line));
2286 static struct line *
2287 add_line_text(struct view *view, char *data, enum line_type type)
2290 data = strdup(data);
2292 return data ? add_line_data(view, data, type) : NULL;
2301 OPEN_DEFAULT = 0, /* Use default view switching. */
2302 OPEN_SPLIT = 1, /* Split current view. */
2303 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2304 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2308 open_view(struct view *prev, enum request request, enum open_flags flags)
2310 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2311 bool split = !!(flags & OPEN_SPLIT);
2312 bool reload = !!(flags & OPEN_RELOAD);
2313 struct view *view = VIEW(request);
2314 int nviews = displayed_views();
2315 struct view *base_view = display[0];
2317 if (view == prev && nviews == 1 && !reload) {
2318 report("Already in %s view", view->name);
2322 if (view->git_dir && !opt_git_dir[0]) {
2323 report("The %s view is disabled in pager view", view->name);
2332 /* Maximize the current view. */
2333 memset(display, 0, sizeof(display));
2335 display[current_view] = view;
2338 /* Resize the view when switching between split- and full-screen,
2339 * or when switching between two different full-screen views. */
2340 if (nviews != displayed_views() ||
2341 (nviews == 1 && base_view != display[0]))
2344 if (view->ops->open) {
2345 if (!view->ops->open(view)) {
2346 report("Failed to load %s view", view->name);
2350 } else if ((reload || strcmp(view->vid, view->id)) &&
2351 !begin_update(view)) {
2352 report("Failed to load %s view", view->name);
2356 if (split && prev->lineno - prev->offset >= prev->height) {
2357 /* Take the title line into account. */
2358 int lines = prev->lineno - prev->offset - prev->height + 1;
2360 /* Scroll the view that was split if the current line is
2361 * outside the new limited view. */
2362 do_scroll_view(prev, lines);
2365 if (prev && view != prev) {
2366 if (split && !backgrounded) {
2367 /* "Blur" the previous view. */
2368 update_view_title(prev);
2371 view->parent = prev;
2374 if (view->pipe && view->lines == 0) {
2375 /* Clear the old view and let the incremental updating refill
2384 /* If the view is backgrounded the above calls to report()
2385 * won't redraw the view title. */
2387 update_view_title(view);
2391 open_external_viewer(const char *cmd)
2393 def_prog_mode(); /* save current tty modes */
2394 endwin(); /* restore original tty modes */
2396 fprintf(stderr, "Press Enter to continue");
2403 open_mergetool(const char *file)
2405 char cmd[SIZEOF_STR];
2406 char file_sq[SIZEOF_STR];
2408 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2409 string_format(cmd, "git mergetool %s", file_sq)) {
2410 open_external_viewer(cmd);
2415 open_editor(bool from_root, const char *file)
2417 char cmd[SIZEOF_STR];
2418 char file_sq[SIZEOF_STR];
2420 char *prefix = from_root ? opt_cdup : "";
2422 editor = getenv("GIT_EDITOR");
2423 if (!editor && *opt_editor)
2424 editor = opt_editor;
2426 editor = getenv("VISUAL");
2428 editor = getenv("EDITOR");
2432 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2433 string_format(cmd, "%s %s%s", editor, prefix, file_sq)) {
2434 open_external_viewer(cmd);
2439 open_run_request(enum request request)
2441 struct run_request *req = get_run_request(request);
2442 char buf[SIZEOF_STR * 2];
2447 report("Unknown run request");
2455 char *next = strstr(cmd, "%(");
2456 int len = next - cmd;
2463 } else if (!strncmp(next, "%(head)", 7)) {
2466 } else if (!strncmp(next, "%(commit)", 9)) {
2469 } else if (!strncmp(next, "%(blob)", 7)) {
2473 report("Unknown replacement in run request: `%s`", req->cmd);
2477 if (!string_format_from(buf, &bufpos, "%.*s%s", len, cmd, value))
2481 next = strchr(next, ')') + 1;
2485 open_external_viewer(buf);
2489 * User request switch noodle
2493 view_driver(struct view *view, enum request request)
2497 if (request == REQ_NONE) {
2502 if (request > REQ_NONE) {
2503 open_run_request(request);
2507 if (view && view->lines) {
2508 request = view->ops->request(view, request, &view->line[view->lineno]);
2509 if (request == REQ_NONE)
2516 case REQ_MOVE_PAGE_UP:
2517 case REQ_MOVE_PAGE_DOWN:
2518 case REQ_MOVE_FIRST_LINE:
2519 case REQ_MOVE_LAST_LINE:
2520 move_view(view, request);
2523 case REQ_SCROLL_LINE_DOWN:
2524 case REQ_SCROLL_LINE_UP:
2525 case REQ_SCROLL_PAGE_DOWN:
2526 case REQ_SCROLL_PAGE_UP:
2527 scroll_view(view, request);
2530 case REQ_VIEW_BLAME:
2532 report("No file chosen, press %s to open tree view",
2533 get_key(REQ_VIEW_TREE));
2536 open_view(view, request, OPEN_DEFAULT);
2541 report("No file chosen, press %s to open tree view",
2542 get_key(REQ_VIEW_TREE));
2545 open_view(view, request, OPEN_DEFAULT);
2548 case REQ_VIEW_PAGER:
2549 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2550 report("No pager content, press %s to run command from prompt",
2551 get_key(REQ_PROMPT));
2554 open_view(view, request, OPEN_DEFAULT);
2557 case REQ_VIEW_STAGE:
2558 if (!VIEW(REQ_VIEW_STAGE)->lines) {
2559 report("No stage content, press %s to open the status view and choose file",
2560 get_key(REQ_VIEW_STATUS));
2563 open_view(view, request, OPEN_DEFAULT);
2566 case REQ_VIEW_STATUS:
2567 if (opt_is_inside_work_tree == FALSE) {
2568 report("The status view requires a working tree");
2571 open_view(view, request, OPEN_DEFAULT);
2579 open_view(view, request, OPEN_DEFAULT);
2584 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2586 if ((view == VIEW(REQ_VIEW_DIFF) &&
2587 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2588 (view == VIEW(REQ_VIEW_DIFF) &&
2589 view->parent == VIEW(REQ_VIEW_BLAME)) ||
2590 (view == VIEW(REQ_VIEW_STAGE) &&
2591 view->parent == VIEW(REQ_VIEW_STATUS)) ||
2592 (view == VIEW(REQ_VIEW_BLOB) &&
2593 view->parent == VIEW(REQ_VIEW_TREE))) {
2596 view = view->parent;
2597 line = view->lineno;
2598 move_view(view, request);
2599 if (view_is_displayed(view))
2600 update_view_title(view);
2601 if (line != view->lineno)
2602 view->ops->request(view, REQ_ENTER,
2603 &view->line[view->lineno]);
2606 move_view(view, request);
2612 int nviews = displayed_views();
2613 int next_view = (current_view + 1) % nviews;
2615 if (next_view == current_view) {
2616 report("Only one view is displayed");
2620 current_view = next_view;
2621 /* Blur out the title of the previous view. */
2622 update_view_title(view);
2627 report("Refreshing is not yet supported for the %s view", view->name);
2631 if (displayed_views() == 2)
2632 open_view(view, VIEW_REQ(view), OPEN_DEFAULT);
2635 case REQ_TOGGLE_LINENO:
2636 opt_line_number = !opt_line_number;
2640 case REQ_TOGGLE_DATE:
2641 opt_date = !opt_date;
2645 case REQ_TOGGLE_AUTHOR:
2646 opt_author = !opt_author;
2650 case REQ_TOGGLE_REV_GRAPH:
2651 opt_rev_graph = !opt_rev_graph;
2655 case REQ_TOGGLE_REFS:
2656 opt_show_refs = !opt_show_refs;
2661 /* Always reload^Wrerun commands from the prompt. */
2662 open_view(view, opt_request, OPEN_RELOAD);
2666 case REQ_SEARCH_BACK:
2667 search_view(view, request);
2672 find_next(view, request);
2675 case REQ_STOP_LOADING:
2676 for (i = 0; i < ARRAY_SIZE(views); i++) {
2679 report("Stopped loading the %s view", view->name),
2684 case REQ_SHOW_VERSION:
2685 report("tig-%s (built %s)", TIG_VERSION, __DATE__);
2688 case REQ_SCREEN_RESIZE:
2691 case REQ_SCREEN_REDRAW:
2696 report("Nothing to edit");
2701 report("Nothing to enter");
2705 case REQ_VIEW_CLOSE:
2706 /* XXX: Mark closed views by letting view->parent point to the
2707 * view itself. Parents to closed view should never be
2710 view->parent->parent != view->parent) {
2711 memset(display, 0, sizeof(display));
2713 display[current_view] = view->parent;
2714 view->parent = view;
2724 /* An unknown key will show most commonly used commands. */
2725 report("Unknown key, press 'h' for help");
2738 pager_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2740 static char spaces[] = " ";
2741 char *text = line->data;
2742 enum line_type type = line->type;
2743 int attr = A_NORMAL;
2746 wmove(view->win, lineno, 0);
2750 wchgat(view->win, -1, 0, type, NULL);
2751 attr = get_line_attr(type);
2753 wattrset(view->win, attr);
2755 if (opt_line_number) {
2756 col += draw_lineno(view, lineno, view->width, selected);
2757 if (col >= view->width)
2762 attr = get_line_attr(type);
2763 wattrset(view->win, attr);
2765 if (opt_tab_size < TABSIZE) {
2766 int col_offset = col;
2769 while (text && col_offset + col < view->width) {
2770 int cols_max = view->width - col_offset - col;
2774 if (*text == '\t') {
2776 assert(sizeof(spaces) > TABSIZE);
2778 cols = opt_tab_size - (col % opt_tab_size);
2781 text = strchr(text, '\t');
2782 cols = line ? text - pos : strlen(pos);
2785 waddnstr(view->win, pos, MIN(cols, cols_max));
2790 draw_text(view, text, view->width - col, TRUE, selected);
2797 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2799 char refbuf[SIZEOF_STR];
2803 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2806 pipe = popen(refbuf, "r");
2810 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2811 ref = chomp_string(ref);
2817 /* This is the only fatal call, since it can "corrupt" the buffer. */
2818 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2825 add_pager_refs(struct view *view, struct line *line)
2827 char buf[SIZEOF_STR];
2828 char *commit_id = (char *)line->data + STRING_SIZE("commit ");
2830 size_t bufpos = 0, refpos = 0;
2831 const char *sep = "Refs: ";
2832 bool is_tag = FALSE;
2834 assert(line->type == LINE_COMMIT);
2836 refs = get_refs(commit_id);
2838 if (view == VIEW(REQ_VIEW_DIFF))
2839 goto try_add_describe_ref;
2844 struct ref *ref = refs[refpos];
2845 char *fmt = ref->tag ? "%s[%s]" :
2846 ref->remote ? "%s<%s>" : "%s%s";
2848 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2853 } while (refs[refpos++]->next);
2855 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2856 try_add_describe_ref:
2857 /* Add <tag>-g<commit_id> "fake" reference. */
2858 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2865 if (!realloc_lines(view, view->line_size + 1))
2868 add_line_text(view, buf, LINE_PP_REFS);
2872 pager_read(struct view *view, char *data)
2879 line = add_line_text(view, data, get_line_type(data));
2883 if (line->type == LINE_COMMIT &&
2884 (view == VIEW(REQ_VIEW_DIFF) ||
2885 view == VIEW(REQ_VIEW_LOG)))
2886 add_pager_refs(view, line);
2892 pager_request(struct view *view, enum request request, struct line *line)
2896 if (request != REQ_ENTER)
2899 if (line->type == LINE_COMMIT &&
2900 (view == VIEW(REQ_VIEW_LOG) ||
2901 view == VIEW(REQ_VIEW_PAGER))) {
2902 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2906 /* Always scroll the view even if it was split. That way
2907 * you can use Enter to scroll through the log view and
2908 * split open each commit diff. */
2909 scroll_view(view, REQ_SCROLL_LINE_DOWN);
2911 /* FIXME: A minor workaround. Scrolling the view will call report("")
2912 * but if we are scrolling a non-current view this won't properly
2913 * update the view title. */
2915 update_view_title(view);
2921 pager_grep(struct view *view, struct line *line)
2924 char *text = line->data;
2929 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
2936 pager_select(struct view *view, struct line *line)
2938 if (line->type == LINE_COMMIT) {
2939 char *text = (char *)line->data + STRING_SIZE("commit ");
2941 if (view != VIEW(REQ_VIEW_PAGER))
2942 string_copy_rev(view->ref, text);
2943 string_copy_rev(ref_commit, text);
2947 static struct view_ops pager_ops = {
2963 help_open(struct view *view)
2966 int lines = ARRAY_SIZE(req_info) + 2;
2969 if (view->lines > 0)
2972 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2973 if (!req_info[i].request)
2976 lines += run_requests + 1;
2978 view->line = calloc(lines, sizeof(*view->line));
2982 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
2984 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2987 if (req_info[i].request == REQ_NONE)
2990 if (!req_info[i].request) {
2991 add_line_text(view, "", LINE_DEFAULT);
2992 add_line_text(view, req_info[i].help, LINE_DEFAULT);
2996 key = get_key(req_info[i].request);
2998 key = "(no key defined)";
3000 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
3003 add_line_text(view, buf, LINE_DEFAULT);
3007 add_line_text(view, "", LINE_DEFAULT);
3008 add_line_text(view, "External commands:", LINE_DEFAULT);
3011 for (i = 0; i < run_requests; i++) {
3012 struct run_request *req = get_run_request(REQ_NONE + i + 1);
3018 key = get_key_name(req->key);
3020 key = "(no key defined)";
3022 if (!string_format(buf, " %-10s %-14s `%s`",
3023 keymap_table[req->keymap].name,
3027 add_line_text(view, buf, LINE_DEFAULT);
3033 static struct view_ops help_ops = {
3048 struct tree_stack_entry {
3049 struct tree_stack_entry *prev; /* Entry below this in the stack */
3050 unsigned long lineno; /* Line number to restore */
3051 char *name; /* Position of name in opt_path */
3054 /* The top of the path stack. */
3055 static struct tree_stack_entry *tree_stack = NULL;
3056 unsigned long tree_lineno = 0;
3059 pop_tree_stack_entry(void)
3061 struct tree_stack_entry *entry = tree_stack;
3063 tree_lineno = entry->lineno;
3065 tree_stack = entry->prev;
3070 push_tree_stack_entry(char *name, unsigned long lineno)
3072 struct tree_stack_entry *entry = calloc(1, sizeof(*entry));
3073 size_t pathlen = strlen(opt_path);
3078 entry->prev = tree_stack;
3079 entry->name = opt_path + pathlen;
3082 if (!string_format_from(opt_path, &pathlen, "%s/", name)) {
3083 pop_tree_stack_entry();
3087 /* Move the current line to the first tree entry. */
3089 entry->lineno = lineno;
3092 /* Parse output from git-ls-tree(1):
3094 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
3095 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
3096 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
3097 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
3100 #define SIZEOF_TREE_ATTR \
3101 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
3103 #define TREE_UP_FORMAT "040000 tree %s\t.."
3106 tree_compare_entry(enum line_type type1, char *name1,
3107 enum line_type type2, char *name2)
3109 if (type1 != type2) {
3110 if (type1 == LINE_TREE_DIR)
3115 return strcmp(name1, name2);
3119 tree_path(struct line *line)
3121 char *path = line->data;
3123 return path + SIZEOF_TREE_ATTR;
3127 tree_read(struct view *view, char *text)
3129 size_t textlen = text ? strlen(text) : 0;
3130 char buf[SIZEOF_STR];
3132 enum line_type type;
3133 bool first_read = view->lines == 0;
3137 if (textlen <= SIZEOF_TREE_ATTR)
3140 type = text[STRING_SIZE("100644 ")] == 't'
3141 ? LINE_TREE_DIR : LINE_TREE_FILE;
3144 /* Add path info line */
3145 if (!string_format(buf, "Directory path /%s", opt_path) ||
3146 !realloc_lines(view, view->line_size + 1) ||
3147 !add_line_text(view, buf, LINE_DEFAULT))
3150 /* Insert "link" to parent directory. */
3152 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
3153 !realloc_lines(view, view->line_size + 1) ||
3154 !add_line_text(view, buf, LINE_TREE_DIR))
3159 /* Strip the path part ... */
3161 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
3162 size_t striplen = strlen(opt_path);
3163 char *path = text + SIZEOF_TREE_ATTR;
3165 if (pathlen > striplen)
3166 memmove(path, path + striplen,
3167 pathlen - striplen + 1);
3170 /* Skip "Directory ..." and ".." line. */
3171 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
3172 struct line *line = &view->line[pos];
3173 char *path1 = tree_path(line);
3174 char *path2 = text + SIZEOF_TREE_ATTR;
3175 int cmp = tree_compare_entry(line->type, path1, type, path2);
3180 text = strdup(text);
3184 if (view->lines > pos)
3185 memmove(&view->line[pos + 1], &view->line[pos],
3186 (view->lines - pos) * sizeof(*line));
3188 line = &view->line[pos];
3195 if (!add_line_text(view, text, type))
3198 if (tree_lineno > view->lineno) {
3199 view->lineno = tree_lineno;
3207 tree_request(struct view *view, enum request request, struct line *line)
3209 enum open_flags flags;
3211 if (request == REQ_VIEW_BLAME) {
3212 char *filename = tree_path(line);
3214 if (line->type == LINE_TREE_DIR) {
3215 report("Cannot show blame for directory %s", opt_path);
3219 string_copy(opt_ref, view->vid);
3220 string_format(opt_file, "%s%s", opt_path, filename);
3223 if (request == REQ_TREE_PARENT) {
3226 request = REQ_ENTER;
3227 line = &view->line[1];
3229 /* quit view if at top of tree */
3230 return REQ_VIEW_CLOSE;
3233 if (request != REQ_ENTER)
3236 /* Cleanup the stack if the tree view is at a different tree. */
3237 while (!*opt_path && tree_stack)
3238 pop_tree_stack_entry();
3240 switch (line->type) {
3242 /* Depending on whether it is a subdir or parent (updir?) link
3243 * mangle the path buffer. */
3244 if (line == &view->line[1] && *opt_path) {
3245 pop_tree_stack_entry();
3248 char *basename = tree_path(line);
3250 push_tree_stack_entry(basename, view->lineno);
3253 /* Trees and subtrees share the same ID, so they are not not
3254 * unique like blobs. */
3255 flags = OPEN_RELOAD;
3256 request = REQ_VIEW_TREE;
3259 case LINE_TREE_FILE:
3260 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3261 request = REQ_VIEW_BLOB;
3268 open_view(view, request, flags);
3269 if (request == REQ_VIEW_TREE) {
3270 view->lineno = tree_lineno;
3277 tree_select(struct view *view, struct line *line)
3279 char *text = (char *)line->data + STRING_SIZE("100644 blob ");
3281 if (line->type == LINE_TREE_FILE) {
3282 string_copy_rev(ref_blob, text);
3284 } else if (line->type != LINE_TREE_DIR) {
3288 string_copy_rev(view->ref, text);
3291 static struct view_ops tree_ops = {
3302 blob_read(struct view *view, char *line)
3306 return add_line_text(view, line, LINE_DEFAULT) != NULL;
3309 static struct view_ops blob_ops = {
3322 * Loading the blame view is a two phase job:
3324 * 1. File content is read either using opt_file from the
3325 * filesystem or using git-cat-file.
3326 * 2. Then blame information is incrementally added by
3327 * reading output from git-blame.
3330 struct blame_commit {
3331 char id[SIZEOF_REV]; /* SHA1 ID. */
3332 char title[128]; /* First line of the commit message. */
3333 char author[75]; /* Author of the commit. */
3334 struct tm time; /* Date from the author ident. */
3335 char filename[128]; /* Name of file. */
3339 struct blame_commit *commit;
3340 unsigned int header:1;
3344 #define BLAME_CAT_FILE_CMD "git cat-file blob %s:%s"
3345 #define BLAME_INCREMENTAL_CMD "git blame --incremental %s %s"
3348 blame_open(struct view *view)
3350 char path[SIZEOF_STR];
3351 char ref[SIZEOF_STR] = "";
3353 if (sq_quote(path, 0, opt_file) >= sizeof(path))
3356 if (*opt_ref && sq_quote(ref, 0, opt_ref) >= sizeof(ref))
3360 if (!string_format(view->cmd, BLAME_CAT_FILE_CMD, ref, path))
3363 view->pipe = fopen(opt_file, "r");
3365 !string_format(view->cmd, BLAME_CAT_FILE_CMD, "HEAD", path))
3370 view->pipe = popen(view->cmd, "r");
3374 if (!string_format(view->cmd, BLAME_INCREMENTAL_CMD, ref, path))
3377 string_format(view->ref, "%s ...", opt_file);
3378 string_copy_rev(view->vid, opt_file);
3379 set_nonblocking_input(TRUE);
3384 for (i = 0; i < view->lines; i++)
3385 free(view->line[i].data);
3389 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3390 view->offset = view->lines = view->lineno = 0;
3392 view->start_time = time(NULL);
3397 static struct blame_commit *
3398 get_blame_commit(struct view *view, const char *id)
3402 for (i = 0; i < view->lines; i++) {
3403 struct blame *blame = view->line[i].data;
3408 if (!strncmp(blame->commit->id, id, SIZEOF_REV - 1))
3409 return blame->commit;
3413 struct blame_commit *commit = calloc(1, sizeof(*commit));
3416 string_ncopy(commit->id, id, SIZEOF_REV);
3422 parse_number(char **posref, size_t *number, size_t min, size_t max)
3424 char *pos = *posref;
3427 pos = strchr(pos + 1, ' ');
3428 if (!pos || !isdigit(pos[1]))
3430 *number = atoi(pos + 1);
3431 if (*number < min || *number > max)
3438 static struct blame_commit *
3439 parse_blame_commit(struct view *view, char *text, int *blamed)
3441 struct blame_commit *commit;
3442 struct blame *blame;
3443 char *pos = text + SIZEOF_REV - 1;
3447 if (strlen(text) <= SIZEOF_REV || *pos != ' ')
3450 if (!parse_number(&pos, &lineno, 1, view->lines) ||
3451 !parse_number(&pos, &group, 1, view->lines - lineno + 1))
3454 commit = get_blame_commit(view, text);
3460 struct line *line = &view->line[lineno + group - 1];
3463 blame->commit = commit;
3464 blame->header = !group;
3472 blame_read_file(struct view *view, char *line)
3477 if (view->lines > 0)
3478 pipe = popen(view->cmd, "r");
3481 report("Failed to load blame data");
3490 size_t linelen = strlen(line);
3491 struct blame *blame = malloc(sizeof(*blame) + linelen);
3496 blame->commit = NULL;
3497 strncpy(blame->text, line, linelen);
3498 blame->text[linelen] = 0;
3499 return add_line_data(view, blame, LINE_BLAME_COMMIT) != NULL;
3504 match_blame_header(const char *name, char **line)
3506 size_t namelen = strlen(name);
3507 bool matched = !strncmp(name, *line, namelen);
3516 blame_read(struct view *view, char *line)
3518 static struct blame_commit *commit = NULL;
3519 static int blamed = 0;
3520 static time_t author_time;
3523 return blame_read_file(view, line);
3529 string_format(view->ref, "%s", view->vid);
3530 if (view_is_displayed(view)) {
3531 update_view_title(view);
3532 redraw_view_from(view, 0);
3538 commit = parse_blame_commit(view, line, &blamed);
3539 string_format(view->ref, "%s %2d%%", view->vid,
3540 blamed * 100 / view->lines);
3542 } else if (match_blame_header("author ", &line)) {
3543 string_ncopy(commit->author, line, strlen(line));
3545 } else if (match_blame_header("author-time ", &line)) {
3546 author_time = (time_t) atol(line);
3548 } else if (match_blame_header("author-tz ", &line)) {
3551 tz = ('0' - line[1]) * 60 * 60 * 10;
3552 tz += ('0' - line[2]) * 60 * 60;
3553 tz += ('0' - line[3]) * 60;
3554 tz += ('0' - line[4]) * 60;
3560 gmtime_r(&author_time, &commit->time);
3562 } else if (match_blame_header("summary ", &line)) {
3563 string_ncopy(commit->title, line, strlen(line));
3565 } else if (match_blame_header("filename ", &line)) {
3566 string_ncopy(commit->filename, line, strlen(line));
3574 blame_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
3576 struct blame *blame = line->data;
3579 wmove(view->win, lineno, 0);
3582 wattrset(view->win, get_line_attr(LINE_CURSOR));
3583 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
3585 wattrset(view->win, A_NORMAL);
3592 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
3593 if (blame->commit) {
3594 char buf[DATE_COLS + 1];
3597 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &blame->commit->time);
3598 n = draw_text(view, buf, view->width - col, FALSE, selected);
3599 draw_text(view, " ", view->width - col - n, FALSE, selected);
3603 wmove(view->win, lineno, col);
3604 if (col >= view->width)
3609 int max = MIN(AUTHOR_COLS - 1, view->width - col);
3612 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
3614 draw_text(view, blame->commit->author, max, TRUE, selected);
3616 if (col >= view->width)
3618 wmove(view->win, lineno, col);
3622 int max = MIN(ID_COLS - 1, view->width - col);
3625 wattrset(view->win, get_line_attr(LINE_BLAME_ID));
3627 draw_text(view, blame->commit->id, max, FALSE, -1);
3629 if (col >= view->width)
3631 wmove(view->win, lineno, col);
3636 wattrset(view->win, get_line_attr(LINE_BLAME_LINENO));
3637 col += draw_lineno(view, lineno, view->width - col, selected);
3638 if (col >= view->width)
3642 col += draw_text(view, blame->text, view->width - col, TRUE, selected);
3648 blame_request(struct view *view, enum request request, struct line *line)
3650 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3651 struct blame *blame = line->data;
3655 if (!blame->commit) {
3656 report("No commit loaded yet");
3660 if (!strcmp(blame->commit->id, NULL_ID)) {
3661 char path[SIZEOF_STR];
3663 if (sq_quote(path, 0, view->vid) >= sizeof(path))
3665 string_format(opt_cmd, "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s 2>/dev/null", path);
3668 open_view(view, REQ_VIEW_DIFF, flags);
3679 blame_grep(struct view *view, struct line *line)
3681 struct blame *blame = line->data;
3682 struct blame_commit *commit = blame->commit;
3685 #define MATCH(text) \
3686 (*text && regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3689 char buf[DATE_COLS + 1];
3691 if (MATCH(commit->title) ||
3692 MATCH(commit->author) ||
3696 if (strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time) &&
3701 return MATCH(blame->text);
3707 blame_select(struct view *view, struct line *line)
3709 struct blame *blame = line->data;
3710 struct blame_commit *commit = blame->commit;
3715 if (!strcmp(commit->id, NULL_ID))
3716 string_ncopy(ref_commit, "HEAD", 4);
3718 string_copy_rev(ref_commit, commit->id);
3721 static struct view_ops blame_ops = {
3739 char rev[SIZEOF_REV];
3740 char name[SIZEOF_STR];
3744 char rev[SIZEOF_REV];
3745 char name[SIZEOF_STR];
3749 static char status_onbranch[SIZEOF_STR];
3750 static struct status stage_status;
3751 static enum line_type stage_line_type;
3753 /* Get fields from the diff line:
3754 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
3757 status_get_diff(struct status *file, char *buf, size_t bufsize)
3759 char *old_mode = buf + 1;
3760 char *new_mode = buf + 8;
3761 char *old_rev = buf + 15;
3762 char *new_rev = buf + 56;
3763 char *status = buf + 97;
3766 old_mode[-1] != ':' ||
3767 new_mode[-1] != ' ' ||
3768 old_rev[-1] != ' ' ||
3769 new_rev[-1] != ' ' ||
3773 file->status = *status;
3775 string_copy_rev(file->old.rev, old_rev);
3776 string_copy_rev(file->new.rev, new_rev);
3778 file->old.mode = strtoul(old_mode, NULL, 8);
3779 file->new.mode = strtoul(new_mode, NULL, 8);
3781 file->old.name[0] = file->new.name[0] = 0;
3787 status_run(struct view *view, const char cmd[], char status, enum line_type type)
3789 struct status *file = NULL;
3790 struct status *unmerged = NULL;
3791 char buf[SIZEOF_STR * 4];
3795 pipe = popen(cmd, "r");
3799 add_line_data(view, NULL, type);
3801 while (!feof(pipe) && !ferror(pipe)) {
3805 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
3808 bufsize += readsize;
3810 /* Process while we have NUL chars. */
3811 while ((sep = memchr(buf, 0, bufsize))) {
3812 size_t sepsize = sep - buf + 1;
3815 if (!realloc_lines(view, view->line_size + 1))
3818 file = calloc(1, sizeof(*file));
3822 add_line_data(view, file, type);
3825 /* Parse diff info part. */
3827 file->status = status;
3829 string_copy(file->old.rev, NULL_ID);
3831 } else if (!file->status) {
3832 if (!status_get_diff(file, buf, sepsize))
3836 memmove(buf, sep + 1, bufsize);
3838 sep = memchr(buf, 0, bufsize);
3841 sepsize = sep - buf + 1;
3843 /* Collapse all 'M'odified entries that
3844 * follow a associated 'U'nmerged entry.
3846 if (file->status == 'U') {
3849 } else if (unmerged) {
3850 int collapse = !strcmp(buf, unmerged->new.name);
3861 /* Grab the old name for rename/copy. */
3862 if (!*file->old.name &&
3863 (file->status == 'R' || file->status == 'C')) {
3864 sepsize = sep - buf + 1;
3865 string_ncopy(file->old.name, buf, sepsize);
3867 memmove(buf, sep + 1, bufsize);
3869 sep = memchr(buf, 0, bufsize);
3872 sepsize = sep - buf + 1;
3875 /* git-ls-files just delivers a NUL separated
3876 * list of file names similar to the second half
3877 * of the git-diff-* output. */
3878 string_ncopy(file->new.name, buf, sepsize);
3879 if (!*file->old.name)
3880 string_copy(file->old.name, file->new.name);
3882 memmove(buf, sep + 1, bufsize);
3893 if (!view->line[view->lines - 1].data)
3894 add_line_data(view, NULL, LINE_STAT_NONE);
3900 /* Don't show unmerged entries in the staged section. */
3901 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached -M HEAD"
3902 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
3903 #define STATUS_LIST_OTHER_CMD \
3904 "git ls-files -z --others --exclude-per-directory=.gitignore"
3905 #define STATUS_LIST_NO_HEAD_CMD \
3906 "git ls-files -z --cached --exclude-per-directory=.gitignore"
3908 #define STATUS_DIFF_INDEX_SHOW_CMD \
3909 "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s %s 2>/dev/null"
3911 #define STATUS_DIFF_FILES_SHOW_CMD \
3912 "git diff-files --root --patch-with-stat -C -M -- %s %s 2>/dev/null"
3914 #define STATUS_DIFF_NO_HEAD_SHOW_CMD \
3915 "git diff --no-color --patch-with-stat /dev/null %s 2>/dev/null"
3917 /* First parse staged info using git-diff-index(1), then parse unstaged
3918 * info using git-diff-files(1), and finally untracked files using
3919 * git-ls-files(1). */
3921 status_open(struct view *view)
3923 struct stat statbuf;
3924 char exclude[SIZEOF_STR];
3925 char indexcmd[SIZEOF_STR] = STATUS_DIFF_INDEX_CMD;
3926 char othercmd[SIZEOF_STR] = STATUS_LIST_OTHER_CMD;
3927 unsigned long prev_lineno = view->lineno;
3928 char indexstatus = 0;
3931 for (i = 0; i < view->lines; i++)
3932 free(view->line[i].data);
3934 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3937 if (!realloc_lines(view, view->line_size + 7))
3940 add_line_data(view, NULL, LINE_STAT_HEAD);
3942 string_copy(status_onbranch, "Initial commit");
3943 else if (!*opt_head)
3944 string_copy(status_onbranch, "Not currently on any branch");
3945 else if (!string_format(status_onbranch, "On branch %s", opt_head))
3949 string_copy(indexcmd, STATUS_LIST_NO_HEAD_CMD);
3953 if (!string_format(exclude, "%s/info/exclude", opt_git_dir))
3956 if (stat(exclude, &statbuf) >= 0) {
3957 size_t cmdsize = strlen(othercmd);
3959 if (!string_format_from(othercmd, &cmdsize, " %s", "--exclude-from=") ||
3960 sq_quote(othercmd, cmdsize, exclude) >= sizeof(othercmd))
3963 cmdsize = strlen(indexcmd);
3965 (!string_format_from(indexcmd, &cmdsize, " %s", "--exclude-from=") ||
3966 sq_quote(indexcmd, cmdsize, exclude) >= sizeof(indexcmd)))
3970 system("git update-index -q --refresh");
3972 if (!status_run(view, indexcmd, indexstatus, LINE_STAT_STAGED) ||
3973 !status_run(view, STATUS_DIFF_FILES_CMD, 0, LINE_STAT_UNSTAGED) ||
3974 !status_run(view, othercmd, '?', LINE_STAT_UNTRACKED))
3977 /* If all went well restore the previous line number to stay in
3978 * the context or select a line with something that can be
3980 if (prev_lineno >= view->lines)
3981 prev_lineno = view->lines - 1;
3982 while (prev_lineno < view->lines && !view->line[prev_lineno].data)
3984 while (prev_lineno > 0 && !view->line[prev_lineno].data)
3987 /* If the above fails, always skip the "On branch" line. */
3988 if (prev_lineno < view->lines)
3989 view->lineno = prev_lineno;
3993 if (view->lineno < view->offset)
3994 view->offset = view->lineno;
3995 else if (view->offset + view->height <= view->lineno)
3996 view->offset = view->lineno - view->height + 1;
4002 status_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
4004 struct status *status = line->data;
4006 wmove(view->win, lineno, 0);
4009 wattrset(view->win, get_line_attr(LINE_CURSOR));
4010 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
4012 } else if (line->type == LINE_STAT_HEAD) {
4013 wattrset(view->win, get_line_attr(LINE_STAT_HEAD));
4014 wchgat(view->win, -1, 0, LINE_STAT_HEAD, NULL);
4016 } else if (!status && line->type != LINE_STAT_NONE) {
4017 wattrset(view->win, get_line_attr(LINE_STAT_SECTION));
4018 wchgat(view->win, -1, 0, LINE_STAT_SECTION, NULL);
4021 wattrset(view->win, get_line_attr(line->type));
4027 switch (line->type) {
4028 case LINE_STAT_STAGED:
4029 text = "Changes to be committed:";
4032 case LINE_STAT_UNSTAGED:
4033 text = "Changed but not updated:";
4036 case LINE_STAT_UNTRACKED:
4037 text = "Untracked files:";
4040 case LINE_STAT_NONE:
4041 text = " (no files)";
4044 case LINE_STAT_HEAD:
4045 text = status_onbranch;
4052 draw_text(view, text, view->width, TRUE, selected);
4056 waddch(view->win, status->status);
4058 wattrset(view->win, A_NORMAL);
4059 wmove(view->win, lineno, 4);
4060 if (view->width < 5)
4063 draw_text(view, status->new.name, view->width - 5, TRUE, selected);
4068 status_enter(struct view *view, struct line *line)
4070 struct status *status = line->data;
4071 char oldpath[SIZEOF_STR] = "";
4072 char newpath[SIZEOF_STR] = "";
4076 if (line->type == LINE_STAT_NONE ||
4077 (!status && line[1].type == LINE_STAT_NONE)) {
4078 report("No file to diff");
4083 if (sq_quote(oldpath, 0, status->old.name) >= sizeof(oldpath))
4085 /* Diffs for unmerged entries are empty when pasing the
4086 * new path, so leave it empty. */
4087 if (status->status != 'U' &&
4088 sq_quote(newpath, 0, status->new.name) >= sizeof(newpath))
4093 line->type != LINE_STAT_UNTRACKED &&
4094 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
4097 switch (line->type) {
4098 case LINE_STAT_STAGED:
4100 if (!string_format_from(opt_cmd, &cmdsize,
4101 STATUS_DIFF_NO_HEAD_SHOW_CMD,
4105 if (!string_format_from(opt_cmd, &cmdsize,
4106 STATUS_DIFF_INDEX_SHOW_CMD,
4112 info = "Staged changes to %s";
4114 info = "Staged changes";
4117 case LINE_STAT_UNSTAGED:
4118 if (!string_format_from(opt_cmd, &cmdsize,
4119 STATUS_DIFF_FILES_SHOW_CMD, oldpath, newpath))
4122 info = "Unstaged changes to %s";
4124 info = "Unstaged changes";
4127 case LINE_STAT_UNTRACKED:
4132 report("No file to show");
4136 opt_pipe = fopen(status->new.name, "r");
4137 info = "Untracked file %s";
4140 case LINE_STAT_HEAD:
4144 die("line type %d not handled in switch", line->type);
4147 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | OPEN_SPLIT);
4148 if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) {
4150 stage_status = *status;
4152 memset(&stage_status, 0, sizeof(stage_status));
4155 stage_line_type = line->type;
4156 string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.new.name);
4164 status_update_prepare(enum line_type type)
4166 char cmd[SIZEOF_STR];
4170 type != LINE_STAT_UNTRACKED &&
4171 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4175 case LINE_STAT_STAGED:
4176 string_add(cmd, cmdsize, "git update-index -z --index-info");
4179 case LINE_STAT_UNSTAGED:
4180 case LINE_STAT_UNTRACKED:
4181 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
4185 die("line type %d not handled in switch", type);
4188 return popen(cmd, "w");
4192 status_update_write(FILE *pipe, struct status *status, enum line_type type)
4194 char buf[SIZEOF_STR];
4199 case LINE_STAT_STAGED:
4200 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
4203 status->old.name, 0))
4207 case LINE_STAT_UNSTAGED:
4208 case LINE_STAT_UNTRACKED:
4209 if (!string_format_from(buf, &bufsize, "%s%c", status->new.name, 0))
4214 die("line type %d not handled in switch", type);
4217 while (!ferror(pipe) && written < bufsize) {
4218 written += fwrite(buf + written, 1, bufsize - written, pipe);
4221 return written == bufsize;
4225 status_update_file(struct status *status, enum line_type type)
4227 FILE *pipe = status_update_prepare(type);
4233 result = status_update_write(pipe, status, type);
4239 status_update_files(struct view *view, struct line *line)
4241 FILE *pipe = status_update_prepare(line->type);
4243 struct line *pos = view->line + view->lines;
4250 for (pos = line; pos < view->line + view->lines && pos->data; pos++)
4253 for (file = 0, done = 0; result && file < files; line++, file++) {
4254 int almost_done = file * 100 / files;
4256 if (almost_done > done) {
4258 string_format(view->ref, "updating file %u of %u (%d%% done)",
4260 update_view_title(view);
4262 result = status_update_write(pipe, line->data, line->type);
4270 status_update(struct view *view)
4272 struct line *line = &view->line[view->lineno];
4274 assert(view->lines);
4277 /* This should work even for the "On branch" line. */
4278 if (line < view->line + view->lines && !line[1].data) {
4279 report("Nothing to update");
4283 if (!status_update_files(view, line + 1))
4284 report("Failed to update file status");
4286 } else if (!status_update_file(line->data, line->type)) {
4287 report("Failed to update file status");
4294 status_request(struct view *view, enum request request, struct line *line)
4296 struct status *status = line->data;
4299 case REQ_STATUS_UPDATE:
4300 if (!status_update(view))
4304 case REQ_STATUS_MERGE:
4305 if (!status || status->status != 'U') {
4306 report("Merging only possible for files with unmerged status ('U').");
4309 open_mergetool(status->new.name);
4316 open_editor(status->status != '?', status->new.name);
4319 case REQ_VIEW_BLAME:
4321 string_copy(opt_file, status->new.name);
4327 /* After returning the status view has been split to
4328 * show the stage view. No further reloading is
4330 status_enter(view, line);
4334 /* Simply reload the view. */
4341 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
4347 status_select(struct view *view, struct line *line)
4349 struct status *status = line->data;
4350 char file[SIZEOF_STR] = "all files";
4354 if (status && !string_format(file, "'%s'", status->new.name))
4357 if (!status && line[1].type == LINE_STAT_NONE)
4360 switch (line->type) {
4361 case LINE_STAT_STAGED:
4362 text = "Press %s to unstage %s for commit";
4365 case LINE_STAT_UNSTAGED:
4366 text = "Press %s to stage %s for commit";
4369 case LINE_STAT_UNTRACKED:
4370 text = "Press %s to stage %s for addition";
4373 case LINE_STAT_HEAD:
4374 case LINE_STAT_NONE:
4375 text = "Nothing to update";
4379 die("line type %d not handled in switch", line->type);
4382 if (status && status->status == 'U') {
4383 text = "Press %s to resolve conflict in %s";
4384 key = get_key(REQ_STATUS_MERGE);
4387 key = get_key(REQ_STATUS_UPDATE);
4390 string_format(view->ref, text, key, file);
4394 status_grep(struct view *view, struct line *line)
4396 struct status *status = line->data;
4397 enum { S_STATUS, S_NAME, S_END } state;
4404 for (state = S_STATUS; state < S_END; state++) {
4408 case S_NAME: text = status->new.name; break;
4410 buf[0] = status->status;
4418 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4425 static struct view_ops status_ops = {
4437 stage_diff_line(FILE *pipe, struct line *line)
4439 char *buf = line->data;
4440 size_t bufsize = strlen(buf);
4443 while (!ferror(pipe) && written < bufsize) {
4444 written += fwrite(buf + written, 1, bufsize - written, pipe);
4449 return written == bufsize;
4452 static struct line *
4453 stage_diff_hdr(struct view *view, struct line *line)
4455 int diff_hdr_dir = line->type == LINE_DIFF_CHUNK ? -1 : 1;
4456 struct line *diff_hdr;
4458 if (line->type == LINE_DIFF_CHUNK)
4459 diff_hdr = line - 1;
4461 diff_hdr = view->line + 1;
4463 while (diff_hdr > view->line && diff_hdr < view->line + view->lines) {
4464 if (diff_hdr->type == LINE_DIFF_HEADER)
4467 diff_hdr += diff_hdr_dir;
4474 stage_update_chunk(struct view *view, struct line *line)
4476 char cmd[SIZEOF_STR];
4478 struct line *diff_hdr, *diff_chunk, *diff_end;
4481 diff_hdr = stage_diff_hdr(view, line);
4486 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4489 if (!string_format_from(cmd, &cmdsize,
4490 "git apply --whitespace=nowarn --cached %s - && "
4491 "git update-index -q --unmerged --refresh 2>/dev/null",
4492 stage_line_type == LINE_STAT_STAGED ? "-R" : ""))
4495 pipe = popen(cmd, "w");
4499 diff_end = view->line + view->lines;
4500 if (line->type != LINE_DIFF_CHUNK) {
4501 diff_chunk = diff_hdr;
4504 for (diff_chunk = line + 1; diff_chunk < diff_end; diff_chunk++)
4505 if (diff_chunk->type == LINE_DIFF_CHUNK ||
4506 diff_chunk->type == LINE_DIFF_HEADER)
4507 diff_end = diff_chunk;
4511 while (diff_hdr->type != LINE_DIFF_CHUNK) {
4512 switch (diff_hdr->type) {
4513 case LINE_DIFF_HEADER:
4514 case LINE_DIFF_INDEX:
4524 if (!stage_diff_line(pipe, diff_hdr++)) {
4531 while (diff_chunk < diff_end && stage_diff_line(pipe, diff_chunk))
4536 if (diff_chunk != diff_end)
4543 stage_update(struct view *view, struct line *line)
4545 if (!opt_no_head && stage_line_type != LINE_STAT_UNTRACKED &&
4546 (line->type == LINE_DIFF_CHUNK || !stage_status.status)) {
4547 if (!stage_update_chunk(view, line)) {
4548 report("Failed to apply chunk");
4552 } else if (!status_update_file(&stage_status, stage_line_type)) {
4553 report("Failed to update file");
4557 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
4559 view = VIEW(REQ_VIEW_STATUS);
4560 if (view_is_displayed(view))
4561 status_enter(view, &view->line[view->lineno]);
4565 stage_request(struct view *view, enum request request, struct line *line)
4568 case REQ_STATUS_UPDATE:
4569 stage_update(view, line);
4573 if (!stage_status.new.name[0])
4576 open_editor(stage_status.status != '?', stage_status.new.name);
4579 case REQ_VIEW_BLAME:
4580 if (stage_status.new.name[0]) {
4581 string_copy(opt_file, stage_status.new.name);
4587 pager_request(view, request, line);
4597 static struct view_ops stage_ops = {
4613 char id[SIZEOF_REV]; /* SHA1 ID. */
4614 char title[128]; /* First line of the commit message. */
4615 char author[75]; /* Author of the commit. */
4616 struct tm time; /* Date from the author ident. */
4617 struct ref **refs; /* Repository references. */
4618 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
4619 size_t graph_size; /* The width of the graph array. */
4620 bool has_parents; /* Rewritten --parents seen. */
4623 /* Size of rev graph with no "padding" columns */
4624 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
4627 struct rev_graph *prev, *next, *parents;
4628 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
4630 struct commit *commit;
4632 unsigned int boundary:1;
4635 /* Parents of the commit being visualized. */
4636 static struct rev_graph graph_parents[4];
4638 /* The current stack of revisions on the graph. */
4639 static struct rev_graph graph_stacks[4] = {
4640 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
4641 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
4642 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
4643 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
4647 graph_parent_is_merge(struct rev_graph *graph)
4649 return graph->parents->size > 1;
4653 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
4655 struct commit *commit = graph->commit;
4657 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
4658 commit->graph[commit->graph_size++] = symbol;
4662 done_rev_graph(struct rev_graph *graph)
4664 if (graph_parent_is_merge(graph) &&
4665 graph->pos < graph->size - 1 &&
4666 graph->next->size == graph->size + graph->parents->size - 1) {
4667 size_t i = graph->pos + graph->parents->size - 1;
4669 graph->commit->graph_size = i * 2;
4670 while (i < graph->next->size - 1) {
4671 append_to_rev_graph(graph, ' ');
4672 append_to_rev_graph(graph, '\\');
4677 graph->size = graph->pos = 0;
4678 graph->commit = NULL;
4679 memset(graph->parents, 0, sizeof(*graph->parents));
4683 push_rev_graph(struct rev_graph *graph, char *parent)
4687 /* "Collapse" duplicate parents lines.
4689 * FIXME: This needs to also update update the drawn graph but
4690 * for now it just serves as a method for pruning graph lines. */
4691 for (i = 0; i < graph->size; i++)
4692 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
4695 if (graph->size < SIZEOF_REVITEMS) {
4696 string_copy_rev(graph->rev[graph->size++], parent);
4701 get_rev_graph_symbol(struct rev_graph *graph)
4705 if (graph->boundary)
4706 symbol = REVGRAPH_BOUND;
4707 else if (graph->parents->size == 0)
4708 symbol = REVGRAPH_INIT;
4709 else if (graph_parent_is_merge(graph))
4710 symbol = REVGRAPH_MERGE;
4711 else if (graph->pos >= graph->size)
4712 symbol = REVGRAPH_BRANCH;
4714 symbol = REVGRAPH_COMMIT;
4720 draw_rev_graph(struct rev_graph *graph)
4723 chtype separator, line;
4725 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
4726 static struct rev_filler fillers[] = {
4727 { ' ', REVGRAPH_LINE },
4732 chtype symbol = get_rev_graph_symbol(graph);
4733 struct rev_filler *filler;
4736 filler = &fillers[DEFAULT];
4738 for (i = 0; i < graph->pos; i++) {
4739 append_to_rev_graph(graph, filler->line);
4740 if (graph_parent_is_merge(graph->prev) &&
4741 graph->prev->pos == i)
4742 filler = &fillers[RSHARP];
4744 append_to_rev_graph(graph, filler->separator);
4747 /* Place the symbol for this revision. */
4748 append_to_rev_graph(graph, symbol);
4750 if (graph->prev->size > graph->size)
4751 filler = &fillers[RDIAG];
4753 filler = &fillers[DEFAULT];
4757 for (; i < graph->size; i++) {
4758 append_to_rev_graph(graph, filler->separator);
4759 append_to_rev_graph(graph, filler->line);
4760 if (graph_parent_is_merge(graph->prev) &&
4761 i < graph->prev->pos + graph->parents->size)
4762 filler = &fillers[RSHARP];
4763 if (graph->prev->size > graph->size)
4764 filler = &fillers[LDIAG];
4767 if (graph->prev->size > graph->size) {
4768 append_to_rev_graph(graph, filler->separator);
4769 if (filler->line != ' ')
4770 append_to_rev_graph(graph, filler->line);
4774 /* Prepare the next rev graph */
4776 prepare_rev_graph(struct rev_graph *graph)
4780 /* First, traverse all lines of revisions up to the active one. */
4781 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
4782 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
4785 push_rev_graph(graph->next, graph->rev[graph->pos]);
4788 /* Interleave the new revision parent(s). */
4789 for (i = 0; !graph->boundary && i < graph->parents->size; i++)
4790 push_rev_graph(graph->next, graph->parents->rev[i]);
4792 /* Lastly, put any remaining revisions. */
4793 for (i = graph->pos + 1; i < graph->size; i++)
4794 push_rev_graph(graph->next, graph->rev[i]);
4798 update_rev_graph(struct rev_graph *graph)
4800 /* If this is the finalizing update ... */
4802 prepare_rev_graph(graph);
4804 /* Graph visualization needs a one rev look-ahead,
4805 * so the first update doesn't visualize anything. */
4806 if (!graph->prev->commit)
4809 draw_rev_graph(graph->prev);
4810 done_rev_graph(graph->prev->prev);
4819 main_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
4821 char buf[DATE_COLS + 1];
4822 struct commit *commit = line->data;
4823 enum line_type type;
4828 if (!*commit->author)
4831 space = view->width;
4832 wmove(view->win, lineno, col);
4836 wattrset(view->win, get_line_attr(type));
4837 wchgat(view->win, -1, 0, type, NULL);
4839 type = LINE_MAIN_COMMIT;
4840 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
4846 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
4847 n = draw_text(view, buf, view->width - col, FALSE, selected);
4848 draw_text(view, " ", view->width - col - n, FALSE, selected);
4851 wmove(view->win, lineno, col);
4852 if (col >= view->width)
4855 if (type != LINE_CURSOR)
4856 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
4861 max_len = view->width - col;
4862 if (max_len > AUTHOR_COLS - 1)
4863 max_len = AUTHOR_COLS - 1;
4864 draw_text(view, commit->author, max_len, TRUE, selected);
4866 if (col >= view->width)
4870 if (opt_rev_graph && commit->graph_size) {
4871 size_t graph_size = view->width - col;
4874 if (type != LINE_CURSOR)
4875 wattrset(view->win, get_line_attr(LINE_MAIN_REVGRAPH));
4876 wmove(view->win, lineno, col);
4877 if (graph_size > commit->graph_size)
4878 graph_size = commit->graph_size;
4879 /* Using waddch() instead of waddnstr() ensures that
4880 * they'll be rendered correctly for the cursor line. */
4881 for (i = 0; i < graph_size; i++)
4882 waddch(view->win, commit->graph[i]);
4884 col += commit->graph_size + 1;
4885 if (col >= view->width)
4887 waddch(view->win, ' ');
4889 if (type != LINE_CURSOR)
4890 wattrset(view->win, A_NORMAL);
4892 wmove(view->win, lineno, col);
4894 if (opt_show_refs && commit->refs) {
4898 if (type == LINE_CURSOR)
4900 else if (commit->refs[i]->head)
4901 wattrset(view->win, get_line_attr(LINE_MAIN_HEAD));
4902 else if (commit->refs[i]->ltag)
4903 wattrset(view->win, get_line_attr(LINE_MAIN_LOCAL_TAG));
4904 else if (commit->refs[i]->tag)
4905 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
4906 else if (commit->refs[i]->remote)
4907 wattrset(view->win, get_line_attr(LINE_MAIN_REMOTE));
4909 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
4911 col += draw_text(view, "[", view->width - col, TRUE, selected);
4912 col += draw_text(view, commit->refs[i]->name, view->width - col,
4914 col += draw_text(view, "]", view->width - col, TRUE, selected);
4915 if (type != LINE_CURSOR)
4916 wattrset(view->win, A_NORMAL);
4917 col += draw_text(view, " ", view->width - col, TRUE, selected);
4918 if (col >= view->width)
4920 } while (commit->refs[i++]->next);
4923 if (type != LINE_CURSOR)
4924 wattrset(view->win, get_line_attr(type));
4926 draw_text(view, commit->title, view->width - col, TRUE, selected);
4930 /* Reads git log --pretty=raw output and parses it into the commit struct. */
4932 main_read(struct view *view, char *line)
4934 static struct rev_graph *graph = graph_stacks;
4935 enum line_type type;
4936 struct commit *commit;
4939 update_rev_graph(graph);
4943 type = get_line_type(line);
4944 if (type == LINE_COMMIT) {
4945 commit = calloc(1, sizeof(struct commit));
4949 line += STRING_SIZE("commit ");
4951 graph->boundary = 1;
4955 string_copy_rev(commit->id, line);
4956 commit->refs = get_refs(commit->id);
4957 graph->commit = commit;
4958 add_line_data(view, commit, LINE_MAIN_COMMIT);
4960 while ((line = strchr(line, ' '))) {
4962 push_rev_graph(graph->parents, line);
4963 commit->has_parents = TRUE;
4970 commit = view->line[view->lines - 1].data;
4974 if (commit->has_parents)
4976 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
4981 /* Parse author lines where the name may be empty:
4982 * author <email@address.tld> 1138474660 +0100
4984 char *ident = line + STRING_SIZE("author ");
4985 char *nameend = strchr(ident, '<');
4986 char *emailend = strchr(ident, '>');
4988 if (!nameend || !emailend)
4991 update_rev_graph(graph);
4992 graph = graph->next;
4994 *nameend = *emailend = 0;
4995 ident = chomp_string(ident);
4997 ident = chomp_string(nameend + 1);
5002 string_ncopy(commit->author, ident, strlen(ident));
5004 /* Parse epoch and timezone */
5005 if (emailend[1] == ' ') {
5006 char *secs = emailend + 2;
5007 char *zone = strchr(secs, ' ');
5008 time_t time = (time_t) atol(secs);
5010 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
5014 tz = ('0' - zone[1]) * 60 * 60 * 10;
5015 tz += ('0' - zone[2]) * 60 * 60;
5016 tz += ('0' - zone[3]) * 60;
5017 tz += ('0' - zone[4]) * 60;
5025 gmtime_r(&time, &commit->time);
5030 /* Fill in the commit title if it has not already been set. */
5031 if (commit->title[0])
5034 /* Require titles to start with a non-space character at the
5035 * offset used by git log. */
5036 if (strncmp(line, " ", 4))
5039 /* Well, if the title starts with a whitespace character,
5040 * try to be forgiving. Otherwise we end up with no title. */
5041 while (isspace(*line))
5045 /* FIXME: More graceful handling of titles; append "..." to
5046 * shortened titles, etc. */
5048 string_ncopy(commit->title, line, strlen(line));
5055 main_request(struct view *view, enum request request, struct line *line)
5057 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
5059 if (request == REQ_ENTER)
5060 open_view(view, REQ_VIEW_DIFF, flags);
5068 main_grep(struct view *view, struct line *line)
5070 struct commit *commit = line->data;
5071 enum { S_TITLE, S_AUTHOR, S_DATE, S_END } state;
5072 char buf[DATE_COLS + 1];
5075 for (state = S_TITLE; state < S_END; state++) {
5079 case S_TITLE: text = commit->title; break;
5080 case S_AUTHOR: text = commit->author; break;
5082 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
5091 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
5099 main_select(struct view *view, struct line *line)
5101 struct commit *commit = line->data;
5103 string_copy_rev(view->ref, commit->id);
5104 string_copy_rev(ref_commit, view->ref);
5107 static struct view_ops main_ops = {
5119 * Unicode / UTF-8 handling
5121 * NOTE: Much of the following code for dealing with unicode is derived from
5122 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
5123 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
5126 /* I've (over)annotated a lot of code snippets because I am not entirely
5127 * confident that the approach taken by this small UTF-8 interface is correct.
5131 unicode_width(unsigned long c)
5134 (c <= 0x115f /* Hangul Jamo */
5137 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
5139 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
5140 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
5141 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
5142 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
5143 || (c >= 0xffe0 && c <= 0xffe6)
5144 || (c >= 0x20000 && c <= 0x2fffd)
5145 || (c >= 0x30000 && c <= 0x3fffd)))
5149 return opt_tab_size;
5154 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
5155 * Illegal bytes are set one. */
5156 static const unsigned char utf8_bytes[256] = {
5157 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,
5158 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,
5159 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,
5160 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,
5161 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,
5162 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,
5163 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,
5164 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,
5167 /* Decode UTF-8 multi-byte representation into a unicode character. */
5168 static inline unsigned long
5169 utf8_to_unicode(const char *string, size_t length)
5171 unsigned long unicode;
5175 unicode = string[0];
5178 unicode = (string[0] & 0x1f) << 6;
5179 unicode += (string[1] & 0x3f);
5182 unicode = (string[0] & 0x0f) << 12;
5183 unicode += ((string[1] & 0x3f) << 6);
5184 unicode += (string[2] & 0x3f);
5187 unicode = (string[0] & 0x0f) << 18;
5188 unicode += ((string[1] & 0x3f) << 12);
5189 unicode += ((string[2] & 0x3f) << 6);
5190 unicode += (string[3] & 0x3f);
5193 unicode = (string[0] & 0x0f) << 24;
5194 unicode += ((string[1] & 0x3f) << 18);
5195 unicode += ((string[2] & 0x3f) << 12);
5196 unicode += ((string[3] & 0x3f) << 6);
5197 unicode += (string[4] & 0x3f);
5200 unicode = (string[0] & 0x01) << 30;
5201 unicode += ((string[1] & 0x3f) << 24);
5202 unicode += ((string[2] & 0x3f) << 18);
5203 unicode += ((string[3] & 0x3f) << 12);
5204 unicode += ((string[4] & 0x3f) << 6);
5205 unicode += (string[5] & 0x3f);
5208 die("Invalid unicode length");
5211 /* Invalid characters could return the special 0xfffd value but NUL
5212 * should be just as good. */
5213 return unicode > 0xffff ? 0 : unicode;
5216 /* Calculates how much of string can be shown within the given maximum width
5217 * and sets trimmed parameter to non-zero value if all of string could not be
5218 * shown. If the reserve flag is TRUE, it will reserve at least one
5219 * trailing character, which can be useful when drawing a delimiter.
5221 * Returns the number of bytes to output from string to satisfy max_width. */
5223 utf8_length(const char *string, size_t max_width, int *trimmed, bool reserve)
5225 const char *start = string;
5226 const char *end = strchr(string, '\0');
5227 unsigned char last_bytes = 0;
5232 while (string < end) {
5233 int c = *(unsigned char *) string;
5234 unsigned char bytes = utf8_bytes[c];
5236 unsigned long unicode;
5238 if (string + bytes > end)
5241 /* Change representation to figure out whether
5242 * it is a single- or double-width character. */
5244 unicode = utf8_to_unicode(string, bytes);
5245 /* FIXME: Graceful handling of invalid unicode character. */
5249 ucwidth = unicode_width(unicode);
5251 if (width > max_width) {
5253 if (reserve && width - ucwidth == max_width) {
5254 string -= last_bytes;
5263 return string - start;
5271 /* Whether or not the curses interface has been initialized. */
5272 static bool cursed = FALSE;
5274 /* The status window is used for polling keystrokes. */
5275 static WINDOW *status_win;
5277 static bool status_empty = TRUE;
5279 /* Update status and title window. */
5281 report(const char *msg, ...)
5283 struct view *view = display[current_view];
5289 char buf[SIZEOF_STR];
5292 va_start(args, msg);
5293 if (vsnprintf(buf, sizeof(buf), msg, args) >= sizeof(buf)) {
5294 buf[sizeof(buf) - 1] = 0;
5295 buf[sizeof(buf) - 2] = '.';
5296 buf[sizeof(buf) - 3] = '.';
5297 buf[sizeof(buf) - 4] = '.';
5303 if (!status_empty || *msg) {
5306 va_start(args, msg);
5308 wmove(status_win, 0, 0);
5310 vwprintw(status_win, msg, args);
5311 status_empty = FALSE;
5313 status_empty = TRUE;
5315 wclrtoeol(status_win);
5316 wrefresh(status_win);
5321 update_view_title(view);
5322 update_display_cursor(view);
5325 /* Controls when nodelay should be in effect when polling user input. */
5327 set_nonblocking_input(bool loading)
5329 static unsigned int loading_views;
5331 if ((loading == FALSE && loading_views-- == 1) ||
5332 (loading == TRUE && loading_views++ == 0))
5333 nodelay(status_win, loading);
5341 /* Initialize the curses library */
5342 if (isatty(STDIN_FILENO)) {
5343 cursed = !!initscr();
5345 /* Leave stdin and stdout alone when acting as a pager. */
5346 FILE *io = fopen("/dev/tty", "r+");
5349 die("Failed to open /dev/tty");
5350 cursed = !!newterm(NULL, io, io);
5354 die("Failed to initialize curses");
5356 nonl(); /* Tell curses not to do NL->CR/NL on output */
5357 cbreak(); /* Take input chars one at a time, no wait for \n */
5358 noecho(); /* Don't echo input */
5359 leaveok(stdscr, TRUE);
5364 getmaxyx(stdscr, y, x);
5365 status_win = newwin(1, 0, y - 1, 0);
5367 die("Failed to create status window");
5369 /* Enable keyboard mapping */
5370 keypad(status_win, TRUE);
5371 wbkgdset(status_win, get_line_attr(LINE_STATUS));
5375 read_prompt(const char *prompt)
5377 enum { READING, STOP, CANCEL } status = READING;
5378 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
5381 while (status == READING) {
5387 foreach_view (view, i)
5392 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
5393 wclrtoeol(status_win);
5395 /* Refresh, accept single keystroke of input */
5396 key = wgetch(status_win);
5401 status = pos ? STOP : CANCEL;
5419 if (pos >= sizeof(buf)) {
5420 report("Input string too long");
5425 buf[pos++] = (char) key;
5429 /* Clear the status window */
5430 status_empty = FALSE;
5433 if (status == CANCEL)
5442 * Repository references
5445 static struct ref *refs = NULL;
5446 static size_t refs_alloc = 0;
5447 static size_t refs_size = 0;
5449 /* Id <-> ref store */
5450 static struct ref ***id_refs = NULL;
5451 static size_t id_refs_alloc = 0;
5452 static size_t id_refs_size = 0;
5454 static struct ref **
5457 struct ref ***tmp_id_refs;
5458 struct ref **ref_list = NULL;
5459 size_t ref_list_alloc = 0;
5460 size_t ref_list_size = 0;
5463 for (i = 0; i < id_refs_size; i++)
5464 if (!strcmp(id, id_refs[i][0]->id))
5467 tmp_id_refs = realloc_items(id_refs, &id_refs_alloc, id_refs_size + 1,
5472 id_refs = tmp_id_refs;
5474 for (i = 0; i < refs_size; i++) {
5477 if (strcmp(id, refs[i].id))
5480 tmp = realloc_items(ref_list, &ref_list_alloc,
5481 ref_list_size + 1, sizeof(*ref_list));
5489 if (ref_list_size > 0)
5490 ref_list[ref_list_size - 1]->next = 1;
5491 ref_list[ref_list_size] = &refs[i];
5493 /* XXX: The properties of the commit chains ensures that we can
5494 * safely modify the shared ref. The repo references will
5495 * always be similar for the same id. */
5496 ref_list[ref_list_size]->next = 0;
5501 id_refs[id_refs_size++] = ref_list;
5507 read_ref(char *id, size_t idlen, char *name, size_t namelen)
5512 bool remote = FALSE;
5513 bool check_replace = FALSE;
5516 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
5517 if (!strcmp(name + namelen - 3, "^{}")) {
5520 if (refs_size > 0 && refs[refs_size - 1].ltag == TRUE)
5521 check_replace = TRUE;
5527 namelen -= STRING_SIZE("refs/tags/");
5528 name += STRING_SIZE("refs/tags/");
5530 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
5532 namelen -= STRING_SIZE("refs/remotes/");
5533 name += STRING_SIZE("refs/remotes/");
5535 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5536 namelen -= STRING_SIZE("refs/heads/");
5537 name += STRING_SIZE("refs/heads/");
5538 head = !strncmp(opt_head, name, namelen);
5540 } else if (!strcmp(name, "HEAD")) {
5541 opt_no_head = FALSE;
5545 if (check_replace && !strcmp(name, refs[refs_size - 1].name)) {
5546 /* it's an annotated tag, replace the previous sha1 with the
5547 * resolved commit id; relies on the fact git-ls-remote lists
5548 * the commit id of an annotated tag right beofre the commit id
5550 refs[refs_size - 1].ltag = ltag;
5551 string_copy_rev(refs[refs_size - 1].id, id);
5555 refs = realloc_items(refs, &refs_alloc, refs_size + 1, sizeof(*refs));
5559 ref = &refs[refs_size++];
5560 ref->name = malloc(namelen + 1);
5564 strncpy(ref->name, name, namelen);
5565 ref->name[namelen] = 0;
5568 ref->remote = remote;
5570 string_copy_rev(ref->id, id);
5578 const char *cmd_env = getenv("TIG_LS_REMOTE");
5579 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
5581 return read_properties(popen(cmd, "r"), "\t", read_ref);
5585 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
5587 if (!strcmp(name, "i18n.commitencoding"))
5588 string_ncopy(opt_encoding, value, valuelen);
5590 if (!strcmp(name, "core.editor"))
5591 string_ncopy(opt_editor, value, valuelen);
5597 load_repo_config(void)
5599 return read_properties(popen(GIT_CONFIG " --list", "r"),
5600 "=", read_repo_config_option);
5604 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
5606 if (!opt_git_dir[0]) {
5607 string_ncopy(opt_git_dir, name, namelen);
5609 } else if (opt_is_inside_work_tree == -1) {
5610 /* This can be 3 different values depending on the
5611 * version of git being used. If git-rev-parse does not
5612 * understand --is-inside-work-tree it will simply echo
5613 * the option else either "true" or "false" is printed.
5614 * Default to true for the unknown case. */
5615 opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
5617 } else if (opt_cdup[0] == ' ') {
5618 string_ncopy(opt_cdup, name, namelen);
5620 if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5621 namelen -= STRING_SIZE("refs/heads/");
5622 name += STRING_SIZE("refs/heads/");
5623 string_ncopy(opt_head, name, namelen);
5631 load_repo_info(void)
5634 FILE *pipe = popen("git rev-parse --git-dir --is-inside-work-tree "
5635 " --show-cdup --symbolic-full-name HEAD 2>/dev/null", "r");
5637 /* XXX: The line outputted by "--show-cdup" can be empty so
5638 * initialize it to something invalid to make it possible to
5639 * detect whether it has been set or not. */
5642 result = read_properties(pipe, "=", read_repo_info);
5643 if (opt_cdup[0] == ' ')
5650 read_properties(FILE *pipe, const char *separators,
5651 int (*read_property)(char *, size_t, char *, size_t))
5653 char buffer[BUFSIZ];
5660 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
5665 name = chomp_string(name);
5666 namelen = strcspn(name, separators);
5668 if (name[namelen]) {
5670 value = chomp_string(name + namelen + 1);
5671 valuelen = strlen(value);
5678 state = read_property(name, namelen, value, valuelen);
5681 if (state != ERR && ferror(pipe))
5694 static void __NORETURN
5697 /* XXX: Restore tty modes and let the OS cleanup the rest! */
5703 static void __NORETURN
5704 die(const char *err, ...)
5710 va_start(args, err);
5711 fputs("tig: ", stderr);
5712 vfprintf(stderr, err, args);
5713 fputs("\n", stderr);
5720 warn(const char *msg, ...)
5724 va_start(args, msg);
5725 fputs("tig warning: ", stderr);
5726 vfprintf(stderr, msg, args);
5727 fputs("\n", stderr);
5732 main(int argc, char *argv[])
5735 enum request request;
5738 signal(SIGINT, quit);
5740 if (setlocale(LC_ALL, "")) {
5741 char *codeset = nl_langinfo(CODESET);
5743 string_ncopy(opt_codeset, codeset, strlen(codeset));
5746 if (load_repo_info() == ERR)
5747 die("Failed to load repo info.");
5749 if (load_options() == ERR)
5750 die("Failed to load user config.");
5752 /* Load the repo config file so options can be overwritten from
5753 * the command line. */
5754 if (load_repo_config() == ERR)
5755 die("Failed to load repo config.");
5757 if (!parse_options(argc, argv))
5760 /* Require a git repository unless when running in pager mode. */
5761 if (!opt_git_dir[0] && opt_request != REQ_VIEW_PAGER)
5762 die("Not a git repository");
5764 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
5767 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
5768 opt_iconv = iconv_open(opt_codeset, opt_encoding);
5769 if (opt_iconv == ICONV_NONE)
5770 die("Failed to initialize character set conversion");
5773 if (load_refs() == ERR)
5774 die("Failed to load refs.");
5776 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
5777 view->cmd_env = getenv(view->cmd_env);
5779 request = opt_request;
5783 while (view_driver(display[current_view], request)) {
5787 foreach_view (view, i)
5790 /* Refresh, accept single keystroke of input */
5791 key = wgetch(status_win);
5793 /* wgetch() with nodelay() enabled returns ERR when there's no
5800 request = get_keybinding(display[current_view]->keymap, key);
5802 /* Some low-level request handling. This keeps access to
5803 * status_win restricted. */
5807 char *cmd = read_prompt(":");
5809 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
5810 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
5811 opt_request = REQ_VIEW_DIFF;
5813 opt_request = REQ_VIEW_PAGER;
5822 case REQ_SEARCH_BACK:
5824 const char *prompt = request == REQ_SEARCH
5826 char *search = read_prompt(prompt);
5829 string_ncopy(opt_search, search, strlen(search));
5834 case REQ_SCREEN_RESIZE:
5838 getmaxyx(stdscr, height, width);
5840 /* Resize the status view and let the view driver take
5841 * care of resizing the displayed views. */
5842 wresize(status_win, 1, width);
5843 mvwin(status_win, height - 1, 0);
5844 wrefresh(status_win);