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_view_line(struct view *view, unsigned int lineno)
1489 bool selected = (view->offset + lineno == view->lineno);
1492 assert(view_is_displayed(view));
1494 if (view->offset + lineno >= view->lines)
1497 line = &view->line[view->offset + lineno];
1500 line->selected = TRUE;
1501 view->ops->select(view, line);
1502 } else if (line->selected) {
1503 line->selected = FALSE;
1504 wmove(view->win, lineno, 0);
1505 wclrtoeol(view->win);
1508 scrollok(view->win, FALSE);
1509 draw_ok = view->ops->draw(view, line, lineno, selected);
1510 scrollok(view->win, TRUE);
1516 redraw_view_dirty(struct view *view)
1521 for (lineno = 0; lineno < view->height; lineno++) {
1522 struct line *line = &view->line[view->offset + lineno];
1528 if (!draw_view_line(view, lineno))
1534 redrawwin(view->win);
1536 wnoutrefresh(view->win);
1538 wrefresh(view->win);
1542 redraw_view_from(struct view *view, int lineno)
1544 assert(0 <= lineno && lineno < view->height);
1546 for (; lineno < view->height; lineno++) {
1547 if (!draw_view_line(view, lineno))
1551 redrawwin(view->win);
1553 wnoutrefresh(view->win);
1555 wrefresh(view->win);
1559 redraw_view(struct view *view)
1562 redraw_view_from(view, 0);
1567 update_view_title(struct view *view)
1569 char buf[SIZEOF_STR];
1570 char state[SIZEOF_STR];
1571 size_t bufpos = 0, statelen = 0;
1573 assert(view_is_displayed(view));
1575 if (view != VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) {
1576 unsigned int view_lines = view->offset + view->height;
1577 unsigned int lines = view->lines
1578 ? MIN(view_lines, view->lines) * 100 / view->lines
1581 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1588 time_t secs = time(NULL) - view->start_time;
1590 /* Three git seconds are a long time ... */
1592 string_format_from(state, &statelen, " %lds", secs);
1596 string_format_from(buf, &bufpos, "[%s]", view->name);
1597 if (*view->ref && bufpos < view->width) {
1598 size_t refsize = strlen(view->ref);
1599 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1601 if (minsize < view->width)
1602 refsize = view->width - minsize + 7;
1603 string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
1606 if (statelen && bufpos < view->width) {
1607 string_format_from(buf, &bufpos, " %s", state);
1610 if (view == display[current_view])
1611 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1613 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1615 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1616 wclrtoeol(view->title);
1617 wmove(view->title, 0, view->width - 1);
1620 wnoutrefresh(view->title);
1622 wrefresh(view->title);
1626 resize_display(void)
1629 struct view *base = display[0];
1630 struct view *view = display[1] ? display[1] : display[0];
1632 /* Setup window dimensions */
1634 getmaxyx(stdscr, base->height, base->width);
1636 /* Make room for the status window. */
1640 /* Horizontal split. */
1641 view->width = base->width;
1642 view->height = SCALE_SPLIT_VIEW(base->height);
1643 base->height -= view->height;
1645 /* Make room for the title bar. */
1649 /* Make room for the title bar. */
1654 foreach_displayed_view (view, i) {
1656 view->win = newwin(view->height, 0, offset, 0);
1658 die("Failed to create %s view", view->name);
1660 scrollok(view->win, TRUE);
1662 view->title = newwin(1, 0, offset + view->height, 0);
1664 die("Failed to create title window");
1667 wresize(view->win, view->height, view->width);
1668 mvwin(view->win, offset, 0);
1669 mvwin(view->title, offset + view->height, 0);
1672 offset += view->height + 1;
1677 redraw_display(void)
1682 foreach_displayed_view (view, i) {
1684 update_view_title(view);
1689 update_display_cursor(struct view *view)
1691 /* Move the cursor to the right-most column of the cursor line.
1693 * XXX: This could turn out to be a bit expensive, but it ensures that
1694 * the cursor does not jump around. */
1696 wmove(view->win, view->lineno - view->offset, view->width - 1);
1697 wrefresh(view->win);
1705 /* Scrolling backend */
1707 do_scroll_view(struct view *view, int lines)
1709 bool redraw_current_line = FALSE;
1711 /* The rendering expects the new offset. */
1712 view->offset += lines;
1714 assert(0 <= view->offset && view->offset < view->lines);
1717 /* Move current line into the view. */
1718 if (view->lineno < view->offset) {
1719 view->lineno = view->offset;
1720 redraw_current_line = TRUE;
1721 } else if (view->lineno >= view->offset + view->height) {
1722 view->lineno = view->offset + view->height - 1;
1723 redraw_current_line = TRUE;
1726 assert(view->offset <= view->lineno && view->lineno < view->lines);
1728 /* Redraw the whole screen if scrolling is pointless. */
1729 if (view->height < ABS(lines)) {
1733 int line = lines > 0 ? view->height - lines : 0;
1734 int end = line + ABS(lines);
1736 wscrl(view->win, lines);
1738 for (; line < end; line++) {
1739 if (!draw_view_line(view, line))
1743 if (redraw_current_line)
1744 draw_view_line(view, view->lineno - view->offset);
1747 redrawwin(view->win);
1748 wrefresh(view->win);
1752 /* Scroll frontend */
1754 scroll_view(struct view *view, enum request request)
1758 assert(view_is_displayed(view));
1761 case REQ_SCROLL_PAGE_DOWN:
1762 lines = view->height;
1763 case REQ_SCROLL_LINE_DOWN:
1764 if (view->offset + lines > view->lines)
1765 lines = view->lines - view->offset;
1767 if (lines == 0 || view->offset + view->height >= view->lines) {
1768 report("Cannot scroll beyond the last line");
1773 case REQ_SCROLL_PAGE_UP:
1774 lines = view->height;
1775 case REQ_SCROLL_LINE_UP:
1776 if (lines > view->offset)
1777 lines = view->offset;
1780 report("Cannot scroll beyond the first line");
1788 die("request %d not handled in switch", request);
1791 do_scroll_view(view, lines);
1796 move_view(struct view *view, enum request request)
1798 int scroll_steps = 0;
1802 case REQ_MOVE_FIRST_LINE:
1803 steps = -view->lineno;
1806 case REQ_MOVE_LAST_LINE:
1807 steps = view->lines - view->lineno - 1;
1810 case REQ_MOVE_PAGE_UP:
1811 steps = view->height > view->lineno
1812 ? -view->lineno : -view->height;
1815 case REQ_MOVE_PAGE_DOWN:
1816 steps = view->lineno + view->height >= view->lines
1817 ? view->lines - view->lineno - 1 : view->height;
1829 die("request %d not handled in switch", request);
1832 if (steps <= 0 && view->lineno == 0) {
1833 report("Cannot move beyond the first line");
1836 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1837 report("Cannot move beyond the last line");
1841 /* Move the current line */
1842 view->lineno += steps;
1843 assert(0 <= view->lineno && view->lineno < view->lines);
1845 /* Check whether the view needs to be scrolled */
1846 if (view->lineno < view->offset ||
1847 view->lineno >= view->offset + view->height) {
1848 scroll_steps = steps;
1849 if (steps < 0 && -steps > view->offset) {
1850 scroll_steps = -view->offset;
1852 } else if (steps > 0) {
1853 if (view->lineno == view->lines - 1 &&
1854 view->lines > view->height) {
1855 scroll_steps = view->lines - view->offset - 1;
1856 if (scroll_steps >= view->height)
1857 scroll_steps -= view->height - 1;
1862 if (!view_is_displayed(view)) {
1863 view->offset += scroll_steps;
1864 assert(0 <= view->offset && view->offset < view->lines);
1865 view->ops->select(view, &view->line[view->lineno]);
1869 /* Repaint the old "current" line if we be scrolling */
1870 if (ABS(steps) < view->height)
1871 draw_view_line(view, view->lineno - steps - view->offset);
1874 do_scroll_view(view, scroll_steps);
1878 /* Draw the current line */
1879 draw_view_line(view, view->lineno - view->offset);
1881 redrawwin(view->win);
1882 wrefresh(view->win);
1891 static void search_view(struct view *view, enum request request);
1894 find_next_line(struct view *view, unsigned long lineno, struct line *line)
1896 assert(view_is_displayed(view));
1898 if (!view->ops->grep(view, line))
1901 if (lineno - view->offset >= view->height) {
1902 view->offset = lineno;
1903 view->lineno = lineno;
1907 unsigned long old_lineno = view->lineno - view->offset;
1909 view->lineno = lineno;
1910 draw_view_line(view, old_lineno);
1912 draw_view_line(view, view->lineno - view->offset);
1913 redrawwin(view->win);
1914 wrefresh(view->win);
1917 report("Line %ld matches '%s'", lineno + 1, view->grep);
1922 find_next(struct view *view, enum request request)
1924 unsigned long lineno = view->lineno;
1929 report("No previous search");
1931 search_view(view, request);
1941 case REQ_SEARCH_BACK:
1950 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
1951 lineno += direction;
1953 /* Note, lineno is unsigned long so will wrap around in which case it
1954 * will become bigger than view->lines. */
1955 for (; lineno < view->lines; lineno += direction) {
1956 struct line *line = &view->line[lineno];
1958 if (find_next_line(view, lineno, line))
1962 report("No match found for '%s'", view->grep);
1966 search_view(struct view *view, enum request request)
1971 regfree(view->regex);
1974 view->regex = calloc(1, sizeof(*view->regex));
1979 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
1980 if (regex_err != 0) {
1981 char buf[SIZEOF_STR] = "unknown error";
1983 regerror(regex_err, view->regex, buf, sizeof(buf));
1984 report("Search failed: %s", buf);
1988 string_copy(view->grep, opt_search);
1990 find_next(view, request);
1994 * Incremental updating
1998 end_update(struct view *view)
2002 set_nonblocking_input(FALSE);
2003 if (view->pipe == stdin)
2011 begin_update(struct view *view)
2017 string_copy(view->cmd, opt_cmd);
2019 /* When running random commands, initially show the
2020 * command in the title. However, it maybe later be
2021 * overwritten if a commit line is selected. */
2022 if (view == VIEW(REQ_VIEW_PAGER))
2023 string_copy(view->ref, view->cmd);
2027 } else if (view == VIEW(REQ_VIEW_TREE)) {
2028 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2029 char path[SIZEOF_STR];
2031 if (strcmp(view->vid, view->id))
2032 opt_path[0] = path[0] = 0;
2033 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
2036 if (!string_format(view->cmd, format, view->id, path))
2040 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2041 const char *id = view->id;
2043 if (!string_format(view->cmd, format, id, id, id, id, id))
2046 /* Put the current ref_* value to the view title ref
2047 * member. This is needed by the blob view. Most other
2048 * views sets it automatically after loading because the
2049 * first line is a commit line. */
2050 string_copy_rev(view->ref, view->id);
2053 /* Special case for the pager view. */
2055 view->pipe = opt_pipe;
2058 view->pipe = popen(view->cmd, "r");
2064 set_nonblocking_input(TRUE);
2069 string_copy_rev(view->vid, view->id);
2074 for (i = 0; i < view->lines; i++)
2075 if (view->line[i].data)
2076 free(view->line[i].data);
2082 view->start_time = time(NULL);
2087 #define ITEM_CHUNK_SIZE 256
2089 realloc_items(void *mem, size_t *size, size_t new_size, size_t item_size)
2091 size_t num_chunks = *size / ITEM_CHUNK_SIZE;
2092 size_t num_chunks_new = (new_size + ITEM_CHUNK_SIZE - 1) / ITEM_CHUNK_SIZE;
2094 if (mem == NULL || num_chunks != num_chunks_new) {
2095 *size = num_chunks_new * ITEM_CHUNK_SIZE;
2096 mem = realloc(mem, *size * item_size);
2102 static struct line *
2103 realloc_lines(struct view *view, size_t line_size)
2105 size_t alloc = view->line_alloc;
2106 struct line *tmp = realloc_items(view->line, &alloc, line_size,
2107 sizeof(*view->line));
2113 view->line_alloc = alloc;
2114 view->line_size = line_size;
2119 update_view(struct view *view)
2121 char in_buffer[BUFSIZ];
2122 char out_buffer[BUFSIZ * 2];
2124 /* The number of lines to read. If too low it will cause too much
2125 * redrawing (and possible flickering), if too high responsiveness
2127 unsigned long lines = view->height;
2128 int redraw_from = -1;
2133 /* Only redraw if lines are visible. */
2134 if (view->offset + view->height >= view->lines)
2135 redraw_from = view->lines - view->offset;
2137 /* FIXME: This is probably not perfect for backgrounded views. */
2138 if (!realloc_lines(view, view->lines + lines))
2141 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
2142 size_t linelen = strlen(line);
2145 line[linelen - 1] = 0;
2147 if (opt_iconv != ICONV_NONE) {
2148 ICONV_CONST char *inbuf = line;
2149 size_t inlen = linelen;
2151 char *outbuf = out_buffer;
2152 size_t outlen = sizeof(out_buffer);
2156 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
2157 if (ret != (size_t) -1) {
2159 linelen = strlen(out_buffer);
2163 if (!view->ops->read(view, line))
2173 lines = view->lines;
2174 for (digits = 0; lines; digits++)
2177 /* Keep the displayed view in sync with line number scaling. */
2178 if (digits != view->digits) {
2179 view->digits = digits;
2184 if (!view_is_displayed(view))
2187 if (view == VIEW(REQ_VIEW_TREE)) {
2188 /* Clear the view and redraw everything since the tree sorting
2189 * might have rearranged things. */
2192 } else if (redraw_from >= 0) {
2193 /* If this is an incremental update, redraw the previous line
2194 * since for commits some members could have changed when
2195 * loading the main view. */
2196 if (redraw_from > 0)
2199 /* Since revision graph visualization requires knowledge
2200 * about the parent commit, it causes a further one-off
2201 * needed to be redrawn for incremental updates. */
2202 if (redraw_from > 0 && opt_rev_graph)
2205 /* Incrementally draw avoids flickering. */
2206 redraw_view_from(view, redraw_from);
2209 if (view == VIEW(REQ_VIEW_BLAME))
2210 redraw_view_dirty(view);
2212 /* Update the title _after_ the redraw so that if the redraw picks up a
2213 * commit reference in view->ref it'll be available here. */
2214 update_view_title(view);
2217 if (ferror(view->pipe)) {
2218 report("Failed to read: %s", strerror(errno));
2221 } else if (feof(view->pipe)) {
2229 report("Allocation failure");
2232 if (view->ops->read(view, NULL))
2237 static struct line *
2238 add_line_data(struct view *view, void *data, enum line_type type)
2240 struct line *line = &view->line[view->lines++];
2242 memset(line, 0, sizeof(*line));
2249 static struct line *
2250 add_line_text(struct view *view, char *data, enum line_type type)
2253 data = strdup(data);
2255 return data ? add_line_data(view, data, type) : NULL;
2264 OPEN_DEFAULT = 0, /* Use default view switching. */
2265 OPEN_SPLIT = 1, /* Split current view. */
2266 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2267 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2271 open_view(struct view *prev, enum request request, enum open_flags flags)
2273 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2274 bool split = !!(flags & OPEN_SPLIT);
2275 bool reload = !!(flags & OPEN_RELOAD);
2276 struct view *view = VIEW(request);
2277 int nviews = displayed_views();
2278 struct view *base_view = display[0];
2280 if (view == prev && nviews == 1 && !reload) {
2281 report("Already in %s view", view->name);
2285 if (view->git_dir && !opt_git_dir[0]) {
2286 report("The %s view is disabled in pager view", view->name);
2295 /* Maximize the current view. */
2296 memset(display, 0, sizeof(display));
2298 display[current_view] = view;
2301 /* Resize the view when switching between split- and full-screen,
2302 * or when switching between two different full-screen views. */
2303 if (nviews != displayed_views() ||
2304 (nviews == 1 && base_view != display[0]))
2307 if (view->ops->open) {
2308 if (!view->ops->open(view)) {
2309 report("Failed to load %s view", view->name);
2313 } else if ((reload || strcmp(view->vid, view->id)) &&
2314 !begin_update(view)) {
2315 report("Failed to load %s view", view->name);
2319 if (split && prev->lineno - prev->offset >= prev->height) {
2320 /* Take the title line into account. */
2321 int lines = prev->lineno - prev->offset - prev->height + 1;
2323 /* Scroll the view that was split if the current line is
2324 * outside the new limited view. */
2325 do_scroll_view(prev, lines);
2328 if (prev && view != prev) {
2329 if (split && !backgrounded) {
2330 /* "Blur" the previous view. */
2331 update_view_title(prev);
2334 view->parent = prev;
2337 if (view->pipe && view->lines == 0) {
2338 /* Clear the old view and let the incremental updating refill
2347 /* If the view is backgrounded the above calls to report()
2348 * won't redraw the view title. */
2350 update_view_title(view);
2354 open_external_viewer(const char *cmd)
2356 def_prog_mode(); /* save current tty modes */
2357 endwin(); /* restore original tty modes */
2359 fprintf(stderr, "Press Enter to continue");
2366 open_mergetool(const char *file)
2368 char cmd[SIZEOF_STR];
2369 char file_sq[SIZEOF_STR];
2371 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2372 string_format(cmd, "git mergetool %s", file_sq)) {
2373 open_external_viewer(cmd);
2378 open_editor(bool from_root, const char *file)
2380 char cmd[SIZEOF_STR];
2381 char file_sq[SIZEOF_STR];
2383 char *prefix = from_root ? opt_cdup : "";
2385 editor = getenv("GIT_EDITOR");
2386 if (!editor && *opt_editor)
2387 editor = opt_editor;
2389 editor = getenv("VISUAL");
2391 editor = getenv("EDITOR");
2395 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2396 string_format(cmd, "%s %s%s", editor, prefix, file_sq)) {
2397 open_external_viewer(cmd);
2402 open_run_request(enum request request)
2404 struct run_request *req = get_run_request(request);
2405 char buf[SIZEOF_STR * 2];
2410 report("Unknown run request");
2418 char *next = strstr(cmd, "%(");
2419 int len = next - cmd;
2426 } else if (!strncmp(next, "%(head)", 7)) {
2429 } else if (!strncmp(next, "%(commit)", 9)) {
2432 } else if (!strncmp(next, "%(blob)", 7)) {
2436 report("Unknown replacement in run request: `%s`", req->cmd);
2440 if (!string_format_from(buf, &bufpos, "%.*s%s", len, cmd, value))
2444 next = strchr(next, ')') + 1;
2448 open_external_viewer(buf);
2452 * User request switch noodle
2456 view_driver(struct view *view, enum request request)
2460 if (request == REQ_NONE) {
2465 if (request > REQ_NONE) {
2466 open_run_request(request);
2470 if (view && view->lines) {
2471 request = view->ops->request(view, request, &view->line[view->lineno]);
2472 if (request == REQ_NONE)
2479 case REQ_MOVE_PAGE_UP:
2480 case REQ_MOVE_PAGE_DOWN:
2481 case REQ_MOVE_FIRST_LINE:
2482 case REQ_MOVE_LAST_LINE:
2483 move_view(view, request);
2486 case REQ_SCROLL_LINE_DOWN:
2487 case REQ_SCROLL_LINE_UP:
2488 case REQ_SCROLL_PAGE_DOWN:
2489 case REQ_SCROLL_PAGE_UP:
2490 scroll_view(view, request);
2493 case REQ_VIEW_BLAME:
2495 report("No file chosen, press %s to open tree view",
2496 get_key(REQ_VIEW_TREE));
2499 open_view(view, request, OPEN_DEFAULT);
2504 report("No file chosen, press %s to open tree view",
2505 get_key(REQ_VIEW_TREE));
2508 open_view(view, request, OPEN_DEFAULT);
2511 case REQ_VIEW_PAGER:
2512 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2513 report("No pager content, press %s to run command from prompt",
2514 get_key(REQ_PROMPT));
2517 open_view(view, request, OPEN_DEFAULT);
2520 case REQ_VIEW_STAGE:
2521 if (!VIEW(REQ_VIEW_STAGE)->lines) {
2522 report("No stage content, press %s to open the status view and choose file",
2523 get_key(REQ_VIEW_STATUS));
2526 open_view(view, request, OPEN_DEFAULT);
2529 case REQ_VIEW_STATUS:
2530 if (opt_is_inside_work_tree == FALSE) {
2531 report("The status view requires a working tree");
2534 open_view(view, request, OPEN_DEFAULT);
2542 open_view(view, request, OPEN_DEFAULT);
2547 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2549 if ((view == VIEW(REQ_VIEW_DIFF) &&
2550 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2551 (view == VIEW(REQ_VIEW_DIFF) &&
2552 view->parent == VIEW(REQ_VIEW_BLAME)) ||
2553 (view == VIEW(REQ_VIEW_STAGE) &&
2554 view->parent == VIEW(REQ_VIEW_STATUS)) ||
2555 (view == VIEW(REQ_VIEW_BLOB) &&
2556 view->parent == VIEW(REQ_VIEW_TREE))) {
2559 view = view->parent;
2560 line = view->lineno;
2561 move_view(view, request);
2562 if (view_is_displayed(view))
2563 update_view_title(view);
2564 if (line != view->lineno)
2565 view->ops->request(view, REQ_ENTER,
2566 &view->line[view->lineno]);
2569 move_view(view, request);
2575 int nviews = displayed_views();
2576 int next_view = (current_view + 1) % nviews;
2578 if (next_view == current_view) {
2579 report("Only one view is displayed");
2583 current_view = next_view;
2584 /* Blur out the title of the previous view. */
2585 update_view_title(view);
2590 report("Refreshing is not yet supported for the %s view", view->name);
2594 if (displayed_views() == 2)
2595 open_view(view, VIEW_REQ(view), OPEN_DEFAULT);
2598 case REQ_TOGGLE_LINENO:
2599 opt_line_number = !opt_line_number;
2603 case REQ_TOGGLE_DATE:
2604 opt_date = !opt_date;
2608 case REQ_TOGGLE_AUTHOR:
2609 opt_author = !opt_author;
2613 case REQ_TOGGLE_REV_GRAPH:
2614 opt_rev_graph = !opt_rev_graph;
2618 case REQ_TOGGLE_REFS:
2619 opt_show_refs = !opt_show_refs;
2624 /* Always reload^Wrerun commands from the prompt. */
2625 open_view(view, opt_request, OPEN_RELOAD);
2629 case REQ_SEARCH_BACK:
2630 search_view(view, request);
2635 find_next(view, request);
2638 case REQ_STOP_LOADING:
2639 for (i = 0; i < ARRAY_SIZE(views); i++) {
2642 report("Stopped loading the %s view", view->name),
2647 case REQ_SHOW_VERSION:
2648 report("tig-%s (built %s)", TIG_VERSION, __DATE__);
2651 case REQ_SCREEN_RESIZE:
2654 case REQ_SCREEN_REDRAW:
2659 report("Nothing to edit");
2664 report("Nothing to enter");
2668 case REQ_VIEW_CLOSE:
2669 /* XXX: Mark closed views by letting view->parent point to the
2670 * view itself. Parents to closed view should never be
2673 view->parent->parent != view->parent) {
2674 memset(display, 0, sizeof(display));
2676 display[current_view] = view->parent;
2677 view->parent = view;
2687 /* An unknown key will show most commonly used commands. */
2688 report("Unknown key, press 'h' for help");
2701 pager_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2703 char *text = line->data;
2704 enum line_type type = line->type;
2707 wmove(view->win, lineno, 0);
2711 wchgat(view->win, -1, 0, type, NULL);
2714 attr = get_line_attr(type);
2715 wattrset(view->win, attr);
2717 if (opt_line_number || opt_tab_size < TABSIZE) {
2718 static char spaces[] = " ";
2719 int col_offset = 0, col = 0;
2721 if (opt_line_number) {
2722 unsigned long real_lineno = view->offset + lineno + 1;
2724 if (real_lineno == 1 ||
2725 (real_lineno % opt_num_interval) == 0) {
2726 wprintw(view->win, "%.*d", view->digits, real_lineno);
2729 waddnstr(view->win, spaces,
2730 MIN(view->digits, STRING_SIZE(spaces)));
2732 waddstr(view->win, ": ");
2733 col_offset = view->digits + 2;
2736 while (text && col_offset + col < view->width) {
2737 int cols_max = view->width - col_offset - col;
2741 if (*text == '\t') {
2743 assert(sizeof(spaces) > TABSIZE);
2745 cols = opt_tab_size - (col % opt_tab_size);
2748 text = strchr(text, '\t');
2749 cols = line ? text - pos : strlen(pos);
2752 waddnstr(view->win, pos, MIN(cols, cols_max));
2757 draw_text(view, text, view->width, TRUE, selected);
2764 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2766 char refbuf[SIZEOF_STR];
2770 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2773 pipe = popen(refbuf, "r");
2777 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2778 ref = chomp_string(ref);
2784 /* This is the only fatal call, since it can "corrupt" the buffer. */
2785 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2792 add_pager_refs(struct view *view, struct line *line)
2794 char buf[SIZEOF_STR];
2795 char *commit_id = (char *)line->data + STRING_SIZE("commit ");
2797 size_t bufpos = 0, refpos = 0;
2798 const char *sep = "Refs: ";
2799 bool is_tag = FALSE;
2801 assert(line->type == LINE_COMMIT);
2803 refs = get_refs(commit_id);
2805 if (view == VIEW(REQ_VIEW_DIFF))
2806 goto try_add_describe_ref;
2811 struct ref *ref = refs[refpos];
2812 char *fmt = ref->tag ? "%s[%s]" :
2813 ref->remote ? "%s<%s>" : "%s%s";
2815 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2820 } while (refs[refpos++]->next);
2822 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2823 try_add_describe_ref:
2824 /* Add <tag>-g<commit_id> "fake" reference. */
2825 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2832 if (!realloc_lines(view, view->line_size + 1))
2835 add_line_text(view, buf, LINE_PP_REFS);
2839 pager_read(struct view *view, char *data)
2846 line = add_line_text(view, data, get_line_type(data));
2850 if (line->type == LINE_COMMIT &&
2851 (view == VIEW(REQ_VIEW_DIFF) ||
2852 view == VIEW(REQ_VIEW_LOG)))
2853 add_pager_refs(view, line);
2859 pager_request(struct view *view, enum request request, struct line *line)
2863 if (request != REQ_ENTER)
2866 if (line->type == LINE_COMMIT &&
2867 (view == VIEW(REQ_VIEW_LOG) ||
2868 view == VIEW(REQ_VIEW_PAGER))) {
2869 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2873 /* Always scroll the view even if it was split. That way
2874 * you can use Enter to scroll through the log view and
2875 * split open each commit diff. */
2876 scroll_view(view, REQ_SCROLL_LINE_DOWN);
2878 /* FIXME: A minor workaround. Scrolling the view will call report("")
2879 * but if we are scrolling a non-current view this won't properly
2880 * update the view title. */
2882 update_view_title(view);
2888 pager_grep(struct view *view, struct line *line)
2891 char *text = line->data;
2896 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
2903 pager_select(struct view *view, struct line *line)
2905 if (line->type == LINE_COMMIT) {
2906 char *text = (char *)line->data + STRING_SIZE("commit ");
2908 if (view != VIEW(REQ_VIEW_PAGER))
2909 string_copy_rev(view->ref, text);
2910 string_copy_rev(ref_commit, text);
2914 static struct view_ops pager_ops = {
2930 help_open(struct view *view)
2933 int lines = ARRAY_SIZE(req_info) + 2;
2936 if (view->lines > 0)
2939 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2940 if (!req_info[i].request)
2943 lines += run_requests + 1;
2945 view->line = calloc(lines, sizeof(*view->line));
2949 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
2951 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2954 if (req_info[i].request == REQ_NONE)
2957 if (!req_info[i].request) {
2958 add_line_text(view, "", LINE_DEFAULT);
2959 add_line_text(view, req_info[i].help, LINE_DEFAULT);
2963 key = get_key(req_info[i].request);
2965 key = "(no key defined)";
2967 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
2970 add_line_text(view, buf, LINE_DEFAULT);
2974 add_line_text(view, "", LINE_DEFAULT);
2975 add_line_text(view, "External commands:", LINE_DEFAULT);
2978 for (i = 0; i < run_requests; i++) {
2979 struct run_request *req = get_run_request(REQ_NONE + i + 1);
2985 key = get_key_name(req->key);
2987 key = "(no key defined)";
2989 if (!string_format(buf, " %-10s %-14s `%s`",
2990 keymap_table[req->keymap].name,
2994 add_line_text(view, buf, LINE_DEFAULT);
3000 static struct view_ops help_ops = {
3015 struct tree_stack_entry {
3016 struct tree_stack_entry *prev; /* Entry below this in the stack */
3017 unsigned long lineno; /* Line number to restore */
3018 char *name; /* Position of name in opt_path */
3021 /* The top of the path stack. */
3022 static struct tree_stack_entry *tree_stack = NULL;
3023 unsigned long tree_lineno = 0;
3026 pop_tree_stack_entry(void)
3028 struct tree_stack_entry *entry = tree_stack;
3030 tree_lineno = entry->lineno;
3032 tree_stack = entry->prev;
3037 push_tree_stack_entry(char *name, unsigned long lineno)
3039 struct tree_stack_entry *entry = calloc(1, sizeof(*entry));
3040 size_t pathlen = strlen(opt_path);
3045 entry->prev = tree_stack;
3046 entry->name = opt_path + pathlen;
3049 if (!string_format_from(opt_path, &pathlen, "%s/", name)) {
3050 pop_tree_stack_entry();
3054 /* Move the current line to the first tree entry. */
3056 entry->lineno = lineno;
3059 /* Parse output from git-ls-tree(1):
3061 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
3062 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
3063 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
3064 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
3067 #define SIZEOF_TREE_ATTR \
3068 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
3070 #define TREE_UP_FORMAT "040000 tree %s\t.."
3073 tree_compare_entry(enum line_type type1, char *name1,
3074 enum line_type type2, char *name2)
3076 if (type1 != type2) {
3077 if (type1 == LINE_TREE_DIR)
3082 return strcmp(name1, name2);
3086 tree_path(struct line *line)
3088 char *path = line->data;
3090 return path + SIZEOF_TREE_ATTR;
3094 tree_read(struct view *view, char *text)
3096 size_t textlen = text ? strlen(text) : 0;
3097 char buf[SIZEOF_STR];
3099 enum line_type type;
3100 bool first_read = view->lines == 0;
3104 if (textlen <= SIZEOF_TREE_ATTR)
3107 type = text[STRING_SIZE("100644 ")] == 't'
3108 ? LINE_TREE_DIR : LINE_TREE_FILE;
3111 /* Add path info line */
3112 if (!string_format(buf, "Directory path /%s", opt_path) ||
3113 !realloc_lines(view, view->line_size + 1) ||
3114 !add_line_text(view, buf, LINE_DEFAULT))
3117 /* Insert "link" to parent directory. */
3119 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
3120 !realloc_lines(view, view->line_size + 1) ||
3121 !add_line_text(view, buf, LINE_TREE_DIR))
3126 /* Strip the path part ... */
3128 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
3129 size_t striplen = strlen(opt_path);
3130 char *path = text + SIZEOF_TREE_ATTR;
3132 if (pathlen > striplen)
3133 memmove(path, path + striplen,
3134 pathlen - striplen + 1);
3137 /* Skip "Directory ..." and ".." line. */
3138 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
3139 struct line *line = &view->line[pos];
3140 char *path1 = tree_path(line);
3141 char *path2 = text + SIZEOF_TREE_ATTR;
3142 int cmp = tree_compare_entry(line->type, path1, type, path2);
3147 text = strdup(text);
3151 if (view->lines > pos)
3152 memmove(&view->line[pos + 1], &view->line[pos],
3153 (view->lines - pos) * sizeof(*line));
3155 line = &view->line[pos];
3162 if (!add_line_text(view, text, type))
3165 if (tree_lineno > view->lineno) {
3166 view->lineno = tree_lineno;
3174 tree_request(struct view *view, enum request request, struct line *line)
3176 enum open_flags flags;
3178 if (request == REQ_VIEW_BLAME) {
3179 char *filename = tree_path(line);
3181 if (line->type == LINE_TREE_DIR) {
3182 report("Cannot show blame for directory %s", opt_path);
3186 string_copy(opt_ref, view->vid);
3187 string_format(opt_file, "%s%s", opt_path, filename);
3190 if (request == REQ_TREE_PARENT) {
3193 request = REQ_ENTER;
3194 line = &view->line[1];
3196 /* quit view if at top of tree */
3197 return REQ_VIEW_CLOSE;
3200 if (request != REQ_ENTER)
3203 /* Cleanup the stack if the tree view is at a different tree. */
3204 while (!*opt_path && tree_stack)
3205 pop_tree_stack_entry();
3207 switch (line->type) {
3209 /* Depending on whether it is a subdir or parent (updir?) link
3210 * mangle the path buffer. */
3211 if (line == &view->line[1] && *opt_path) {
3212 pop_tree_stack_entry();
3215 char *basename = tree_path(line);
3217 push_tree_stack_entry(basename, view->lineno);
3220 /* Trees and subtrees share the same ID, so they are not not
3221 * unique like blobs. */
3222 flags = OPEN_RELOAD;
3223 request = REQ_VIEW_TREE;
3226 case LINE_TREE_FILE:
3227 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3228 request = REQ_VIEW_BLOB;
3235 open_view(view, request, flags);
3236 if (request == REQ_VIEW_TREE) {
3237 view->lineno = tree_lineno;
3244 tree_select(struct view *view, struct line *line)
3246 char *text = (char *)line->data + STRING_SIZE("100644 blob ");
3248 if (line->type == LINE_TREE_FILE) {
3249 string_copy_rev(ref_blob, text);
3251 } else if (line->type != LINE_TREE_DIR) {
3255 string_copy_rev(view->ref, text);
3258 static struct view_ops tree_ops = {
3269 blob_read(struct view *view, char *line)
3273 return add_line_text(view, line, LINE_DEFAULT) != NULL;
3276 static struct view_ops blob_ops = {
3289 * Loading the blame view is a two phase job:
3291 * 1. File content is read either using opt_file from the
3292 * filesystem or using git-cat-file.
3293 * 2. Then blame information is incrementally added by
3294 * reading output from git-blame.
3297 struct blame_commit {
3298 char id[SIZEOF_REV]; /* SHA1 ID. */
3299 char title[128]; /* First line of the commit message. */
3300 char author[75]; /* Author of the commit. */
3301 struct tm time; /* Date from the author ident. */
3302 char filename[128]; /* Name of file. */
3306 struct blame_commit *commit;
3307 unsigned int header:1;
3311 #define BLAME_CAT_FILE_CMD "git cat-file blob %s:%s"
3312 #define BLAME_INCREMENTAL_CMD "git blame --incremental %s %s"
3315 blame_open(struct view *view)
3317 char path[SIZEOF_STR];
3318 char ref[SIZEOF_STR] = "";
3320 if (sq_quote(path, 0, opt_file) >= sizeof(path))
3323 if (*opt_ref && sq_quote(ref, 0, opt_ref) >= sizeof(ref))
3327 if (!string_format(view->cmd, BLAME_CAT_FILE_CMD, ref, path))
3330 view->pipe = fopen(opt_file, "r");
3332 !string_format(view->cmd, BLAME_CAT_FILE_CMD, "HEAD", path))
3337 view->pipe = popen(view->cmd, "r");
3341 if (!string_format(view->cmd, BLAME_INCREMENTAL_CMD, ref, path))
3344 string_format(view->ref, "%s ...", opt_file);
3345 string_copy_rev(view->vid, opt_file);
3346 set_nonblocking_input(TRUE);
3351 for (i = 0; i < view->lines; i++)
3352 free(view->line[i].data);
3356 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3357 view->offset = view->lines = view->lineno = 0;
3359 view->start_time = time(NULL);
3364 static struct blame_commit *
3365 get_blame_commit(struct view *view, const char *id)
3369 for (i = 0; i < view->lines; i++) {
3370 struct blame *blame = view->line[i].data;
3375 if (!strncmp(blame->commit->id, id, SIZEOF_REV - 1))
3376 return blame->commit;
3380 struct blame_commit *commit = calloc(1, sizeof(*commit));
3383 string_ncopy(commit->id, id, SIZEOF_REV);
3389 parse_number(char **posref, size_t *number, size_t min, size_t max)
3391 char *pos = *posref;
3394 pos = strchr(pos + 1, ' ');
3395 if (!pos || !isdigit(pos[1]))
3397 *number = atoi(pos + 1);
3398 if (*number < min || *number > max)
3405 static struct blame_commit *
3406 parse_blame_commit(struct view *view, char *text, int *blamed)
3408 struct blame_commit *commit;
3409 struct blame *blame;
3410 char *pos = text + SIZEOF_REV - 1;
3414 if (strlen(text) <= SIZEOF_REV || *pos != ' ')
3417 if (!parse_number(&pos, &lineno, 1, view->lines) ||
3418 !parse_number(&pos, &group, 1, view->lines - lineno + 1))
3421 commit = get_blame_commit(view, text);
3427 struct line *line = &view->line[lineno + group - 1];
3430 blame->commit = commit;
3431 blame->header = !group;
3439 blame_read_file(struct view *view, char *line)
3444 if (view->lines > 0)
3445 pipe = popen(view->cmd, "r");
3448 report("Failed to load blame data");
3457 size_t linelen = strlen(line);
3458 struct blame *blame = malloc(sizeof(*blame) + linelen);
3463 blame->commit = NULL;
3464 strncpy(blame->text, line, linelen);
3465 blame->text[linelen] = 0;
3466 return add_line_data(view, blame, LINE_BLAME_COMMIT) != NULL;
3471 match_blame_header(const char *name, char **line)
3473 size_t namelen = strlen(name);
3474 bool matched = !strncmp(name, *line, namelen);
3483 blame_read(struct view *view, char *line)
3485 static struct blame_commit *commit = NULL;
3486 static int blamed = 0;
3487 static time_t author_time;
3490 return blame_read_file(view, line);
3496 string_format(view->ref, "%s", view->vid);
3497 if (view_is_displayed(view)) {
3498 update_view_title(view);
3499 redraw_view_from(view, 0);
3505 commit = parse_blame_commit(view, line, &blamed);
3506 string_format(view->ref, "%s %2d%%", view->vid,
3507 blamed * 100 / view->lines);
3509 } else if (match_blame_header("author ", &line)) {
3510 string_ncopy(commit->author, line, strlen(line));
3512 } else if (match_blame_header("author-time ", &line)) {
3513 author_time = (time_t) atol(line);
3515 } else if (match_blame_header("author-tz ", &line)) {
3518 tz = ('0' - line[1]) * 60 * 60 * 10;
3519 tz += ('0' - line[2]) * 60 * 60;
3520 tz += ('0' - line[3]) * 60;
3521 tz += ('0' - line[4]) * 60;
3527 gmtime_r(&author_time, &commit->time);
3529 } else if (match_blame_header("summary ", &line)) {
3530 string_ncopy(commit->title, line, strlen(line));
3532 } else if (match_blame_header("filename ", &line)) {
3533 string_ncopy(commit->filename, line, strlen(line));
3541 blame_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
3543 struct blame *blame = line->data;
3546 wmove(view->win, lineno, 0);
3549 wattrset(view->win, get_line_attr(LINE_CURSOR));
3550 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
3552 wattrset(view->win, A_NORMAL);
3559 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
3560 if (blame->commit) {
3561 char buf[DATE_COLS + 1];
3564 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &blame->commit->time);
3565 n = draw_text(view, buf, view->width - col, FALSE, selected);
3566 draw_text(view, " ", view->width - col - n, FALSE, selected);
3570 wmove(view->win, lineno, col);
3571 if (col >= view->width)
3576 int max = MIN(AUTHOR_COLS - 1, view->width - col);
3579 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
3581 draw_text(view, blame->commit->author, max, TRUE, selected);
3583 if (col >= view->width)
3585 wmove(view->win, lineno, col);
3589 int max = MIN(ID_COLS - 1, view->width - col);
3592 wattrset(view->win, get_line_attr(LINE_BLAME_ID));
3594 draw_text(view, blame->commit->id, max, FALSE, -1);
3596 if (col >= view->width)
3598 wmove(view->win, lineno, col);
3602 unsigned long real_lineno = view->offset + lineno + 1;
3603 char number[10] = " ";
3604 int max = MIN(view->digits, STRING_SIZE(number));
3605 bool showtrimmed = FALSE;
3607 if (real_lineno == 1 ||
3608 (real_lineno % opt_num_interval) == 0) {
3609 char fmt[] = "%1ld";
3611 if (view->digits <= 9)
3612 fmt[1] = '0' + view->digits;
3614 if (!string_format(number, fmt, real_lineno))
3619 if (max > view->width - col)
3620 max = view->width - col;
3622 wattrset(view->win, get_line_attr(LINE_BLAME_LINENO));
3623 col += draw_text(view, number, max, showtrimmed, selected);
3624 if (col >= view->width)
3629 wattrset(view->win, A_NORMAL);
3631 if (col >= view->width)
3633 waddch(view->win, ACS_VLINE);
3635 if (col >= view->width)
3637 waddch(view->win, ' ');
3639 col += draw_text(view, blame->text, view->width - col, TRUE, selected);
3645 blame_request(struct view *view, enum request request, struct line *line)
3647 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3648 struct blame *blame = line->data;
3652 if (!blame->commit) {
3653 report("No commit loaded yet");
3657 if (!strcmp(blame->commit->id, NULL_ID)) {
3658 char path[SIZEOF_STR];
3660 if (sq_quote(path, 0, view->vid) >= sizeof(path))
3662 string_format(opt_cmd, "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s 2>/dev/null", path);
3665 open_view(view, REQ_VIEW_DIFF, flags);
3676 blame_grep(struct view *view, struct line *line)
3678 struct blame *blame = line->data;
3679 struct blame_commit *commit = blame->commit;
3682 #define MATCH(text) \
3683 (*text && regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3686 char buf[DATE_COLS + 1];
3688 if (MATCH(commit->title) ||
3689 MATCH(commit->author) ||
3693 if (strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time) &&
3698 return MATCH(blame->text);
3704 blame_select(struct view *view, struct line *line)
3706 struct blame *blame = line->data;
3707 struct blame_commit *commit = blame->commit;
3712 if (!strcmp(commit->id, NULL_ID))
3713 string_ncopy(ref_commit, "HEAD", 4);
3715 string_copy_rev(ref_commit, commit->id);
3718 static struct view_ops blame_ops = {
3736 char rev[SIZEOF_REV];
3737 char name[SIZEOF_STR];
3741 char rev[SIZEOF_REV];
3742 char name[SIZEOF_STR];
3746 static char status_onbranch[SIZEOF_STR];
3747 static struct status stage_status;
3748 static enum line_type stage_line_type;
3750 /* Get fields from the diff line:
3751 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
3754 status_get_diff(struct status *file, char *buf, size_t bufsize)
3756 char *old_mode = buf + 1;
3757 char *new_mode = buf + 8;
3758 char *old_rev = buf + 15;
3759 char *new_rev = buf + 56;
3760 char *status = buf + 97;
3763 old_mode[-1] != ':' ||
3764 new_mode[-1] != ' ' ||
3765 old_rev[-1] != ' ' ||
3766 new_rev[-1] != ' ' ||
3770 file->status = *status;
3772 string_copy_rev(file->old.rev, old_rev);
3773 string_copy_rev(file->new.rev, new_rev);
3775 file->old.mode = strtoul(old_mode, NULL, 8);
3776 file->new.mode = strtoul(new_mode, NULL, 8);
3778 file->old.name[0] = file->new.name[0] = 0;
3784 status_run(struct view *view, const char cmd[], char status, enum line_type type)
3786 struct status *file = NULL;
3787 struct status *unmerged = NULL;
3788 char buf[SIZEOF_STR * 4];
3792 pipe = popen(cmd, "r");
3796 add_line_data(view, NULL, type);
3798 while (!feof(pipe) && !ferror(pipe)) {
3802 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
3805 bufsize += readsize;
3807 /* Process while we have NUL chars. */
3808 while ((sep = memchr(buf, 0, bufsize))) {
3809 size_t sepsize = sep - buf + 1;
3812 if (!realloc_lines(view, view->line_size + 1))
3815 file = calloc(1, sizeof(*file));
3819 add_line_data(view, file, type);
3822 /* Parse diff info part. */
3824 file->status = status;
3826 string_copy(file->old.rev, NULL_ID);
3828 } else if (!file->status) {
3829 if (!status_get_diff(file, buf, sepsize))
3833 memmove(buf, sep + 1, bufsize);
3835 sep = memchr(buf, 0, bufsize);
3838 sepsize = sep - buf + 1;
3840 /* Collapse all 'M'odified entries that
3841 * follow a associated 'U'nmerged entry.
3843 if (file->status == 'U') {
3846 } else if (unmerged) {
3847 int collapse = !strcmp(buf, unmerged->new.name);
3858 /* Grab the old name for rename/copy. */
3859 if (!*file->old.name &&
3860 (file->status == 'R' || file->status == 'C')) {
3861 sepsize = sep - buf + 1;
3862 string_ncopy(file->old.name, buf, sepsize);
3864 memmove(buf, sep + 1, bufsize);
3866 sep = memchr(buf, 0, bufsize);
3869 sepsize = sep - buf + 1;
3872 /* git-ls-files just delivers a NUL separated
3873 * list of file names similar to the second half
3874 * of the git-diff-* output. */
3875 string_ncopy(file->new.name, buf, sepsize);
3876 if (!*file->old.name)
3877 string_copy(file->old.name, file->new.name);
3879 memmove(buf, sep + 1, bufsize);
3890 if (!view->line[view->lines - 1].data)
3891 add_line_data(view, NULL, LINE_STAT_NONE);
3897 /* Don't show unmerged entries in the staged section. */
3898 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached -M HEAD"
3899 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
3900 #define STATUS_LIST_OTHER_CMD \
3901 "git ls-files -z --others --exclude-per-directory=.gitignore"
3902 #define STATUS_LIST_NO_HEAD_CMD \
3903 "git ls-files -z --cached --exclude-per-directory=.gitignore"
3905 #define STATUS_DIFF_INDEX_SHOW_CMD \
3906 "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s %s 2>/dev/null"
3908 #define STATUS_DIFF_FILES_SHOW_CMD \
3909 "git diff-files --root --patch-with-stat -C -M -- %s %s 2>/dev/null"
3911 #define STATUS_DIFF_NO_HEAD_SHOW_CMD \
3912 "git diff --no-color --patch-with-stat /dev/null %s 2>/dev/null"
3914 /* First parse staged info using git-diff-index(1), then parse unstaged
3915 * info using git-diff-files(1), and finally untracked files using
3916 * git-ls-files(1). */
3918 status_open(struct view *view)
3920 struct stat statbuf;
3921 char exclude[SIZEOF_STR];
3922 char indexcmd[SIZEOF_STR] = STATUS_DIFF_INDEX_CMD;
3923 char othercmd[SIZEOF_STR] = STATUS_LIST_OTHER_CMD;
3924 unsigned long prev_lineno = view->lineno;
3925 char indexstatus = 0;
3928 for (i = 0; i < view->lines; i++)
3929 free(view->line[i].data);
3931 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3934 if (!realloc_lines(view, view->line_size + 7))
3937 add_line_data(view, NULL, LINE_STAT_HEAD);
3939 string_copy(status_onbranch, "Initial commit");
3940 else if (!*opt_head)
3941 string_copy(status_onbranch, "Not currently on any branch");
3942 else if (!string_format(status_onbranch, "On branch %s", opt_head))
3946 string_copy(indexcmd, STATUS_LIST_NO_HEAD_CMD);
3950 if (!string_format(exclude, "%s/info/exclude", opt_git_dir))
3953 if (stat(exclude, &statbuf) >= 0) {
3954 size_t cmdsize = strlen(othercmd);
3956 if (!string_format_from(othercmd, &cmdsize, " %s", "--exclude-from=") ||
3957 sq_quote(othercmd, cmdsize, exclude) >= sizeof(othercmd))
3960 cmdsize = strlen(indexcmd);
3962 (!string_format_from(indexcmd, &cmdsize, " %s", "--exclude-from=") ||
3963 sq_quote(indexcmd, cmdsize, exclude) >= sizeof(indexcmd)))
3967 system("git update-index -q --refresh");
3969 if (!status_run(view, indexcmd, indexstatus, LINE_STAT_STAGED) ||
3970 !status_run(view, STATUS_DIFF_FILES_CMD, 0, LINE_STAT_UNSTAGED) ||
3971 !status_run(view, othercmd, '?', LINE_STAT_UNTRACKED))
3974 /* If all went well restore the previous line number to stay in
3975 * the context or select a line with something that can be
3977 if (prev_lineno >= view->lines)
3978 prev_lineno = view->lines - 1;
3979 while (prev_lineno < view->lines && !view->line[prev_lineno].data)
3981 while (prev_lineno > 0 && !view->line[prev_lineno].data)
3984 /* If the above fails, always skip the "On branch" line. */
3985 if (prev_lineno < view->lines)
3986 view->lineno = prev_lineno;
3990 if (view->lineno < view->offset)
3991 view->offset = view->lineno;
3992 else if (view->offset + view->height <= view->lineno)
3993 view->offset = view->lineno - view->height + 1;
3999 status_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
4001 struct status *status = line->data;
4003 wmove(view->win, lineno, 0);
4006 wattrset(view->win, get_line_attr(LINE_CURSOR));
4007 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
4009 } else if (line->type == LINE_STAT_HEAD) {
4010 wattrset(view->win, get_line_attr(LINE_STAT_HEAD));
4011 wchgat(view->win, -1, 0, LINE_STAT_HEAD, NULL);
4013 } else if (!status && line->type != LINE_STAT_NONE) {
4014 wattrset(view->win, get_line_attr(LINE_STAT_SECTION));
4015 wchgat(view->win, -1, 0, LINE_STAT_SECTION, NULL);
4018 wattrset(view->win, get_line_attr(line->type));
4024 switch (line->type) {
4025 case LINE_STAT_STAGED:
4026 text = "Changes to be committed:";
4029 case LINE_STAT_UNSTAGED:
4030 text = "Changed but not updated:";
4033 case LINE_STAT_UNTRACKED:
4034 text = "Untracked files:";
4037 case LINE_STAT_NONE:
4038 text = " (no files)";
4041 case LINE_STAT_HEAD:
4042 text = status_onbranch;
4049 draw_text(view, text, view->width, TRUE, selected);
4053 waddch(view->win, status->status);
4055 wattrset(view->win, A_NORMAL);
4056 wmove(view->win, lineno, 4);
4057 if (view->width < 5)
4060 draw_text(view, status->new.name, view->width - 5, TRUE, selected);
4065 status_enter(struct view *view, struct line *line)
4067 struct status *status = line->data;
4068 char oldpath[SIZEOF_STR] = "";
4069 char newpath[SIZEOF_STR] = "";
4073 if (line->type == LINE_STAT_NONE ||
4074 (!status && line[1].type == LINE_STAT_NONE)) {
4075 report("No file to diff");
4080 if (sq_quote(oldpath, 0, status->old.name) >= sizeof(oldpath))
4082 /* Diffs for unmerged entries are empty when pasing the
4083 * new path, so leave it empty. */
4084 if (status->status != 'U' &&
4085 sq_quote(newpath, 0, status->new.name) >= sizeof(newpath))
4090 line->type != LINE_STAT_UNTRACKED &&
4091 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
4094 switch (line->type) {
4095 case LINE_STAT_STAGED:
4097 if (!string_format_from(opt_cmd, &cmdsize,
4098 STATUS_DIFF_NO_HEAD_SHOW_CMD,
4102 if (!string_format_from(opt_cmd, &cmdsize,
4103 STATUS_DIFF_INDEX_SHOW_CMD,
4109 info = "Staged changes to %s";
4111 info = "Staged changes";
4114 case LINE_STAT_UNSTAGED:
4115 if (!string_format_from(opt_cmd, &cmdsize,
4116 STATUS_DIFF_FILES_SHOW_CMD, oldpath, newpath))
4119 info = "Unstaged changes to %s";
4121 info = "Unstaged changes";
4124 case LINE_STAT_UNTRACKED:
4129 report("No file to show");
4133 opt_pipe = fopen(status->new.name, "r");
4134 info = "Untracked file %s";
4137 case LINE_STAT_HEAD:
4141 die("line type %d not handled in switch", line->type);
4144 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | OPEN_SPLIT);
4145 if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) {
4147 stage_status = *status;
4149 memset(&stage_status, 0, sizeof(stage_status));
4152 stage_line_type = line->type;
4153 string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.new.name);
4161 status_update_prepare(enum line_type type)
4163 char cmd[SIZEOF_STR];
4167 type != LINE_STAT_UNTRACKED &&
4168 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4172 case LINE_STAT_STAGED:
4173 string_add(cmd, cmdsize, "git update-index -z --index-info");
4176 case LINE_STAT_UNSTAGED:
4177 case LINE_STAT_UNTRACKED:
4178 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
4182 die("line type %d not handled in switch", type);
4185 return popen(cmd, "w");
4189 status_update_write(FILE *pipe, struct status *status, enum line_type type)
4191 char buf[SIZEOF_STR];
4196 case LINE_STAT_STAGED:
4197 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
4200 status->old.name, 0))
4204 case LINE_STAT_UNSTAGED:
4205 case LINE_STAT_UNTRACKED:
4206 if (!string_format_from(buf, &bufsize, "%s%c", status->new.name, 0))
4211 die("line type %d not handled in switch", type);
4214 while (!ferror(pipe) && written < bufsize) {
4215 written += fwrite(buf + written, 1, bufsize - written, pipe);
4218 return written == bufsize;
4222 status_update_file(struct status *status, enum line_type type)
4224 FILE *pipe = status_update_prepare(type);
4230 result = status_update_write(pipe, status, type);
4236 status_update_files(struct view *view, struct line *line)
4238 FILE *pipe = status_update_prepare(line->type);
4240 struct line *pos = view->line + view->lines;
4247 for (pos = line; pos < view->line + view->lines && pos->data; pos++)
4250 for (file = 0, done = 0; result && file < files; line++, file++) {
4251 int almost_done = file * 100 / files;
4253 if (almost_done > done) {
4255 string_format(view->ref, "updating file %u of %u (%d%% done)",
4257 update_view_title(view);
4259 result = status_update_write(pipe, line->data, line->type);
4267 status_update(struct view *view)
4269 struct line *line = &view->line[view->lineno];
4271 assert(view->lines);
4274 /* This should work even for the "On branch" line. */
4275 if (line < view->line + view->lines && !line[1].data) {
4276 report("Nothing to update");
4280 if (!status_update_files(view, line + 1))
4281 report("Failed to update file status");
4283 } else if (!status_update_file(line->data, line->type)) {
4284 report("Failed to update file status");
4291 status_request(struct view *view, enum request request, struct line *line)
4293 struct status *status = line->data;
4296 case REQ_STATUS_UPDATE:
4297 if (!status_update(view))
4301 case REQ_STATUS_MERGE:
4302 if (!status || status->status != 'U') {
4303 report("Merging only possible for files with unmerged status ('U').");
4306 open_mergetool(status->new.name);
4313 open_editor(status->status != '?', status->new.name);
4316 case REQ_VIEW_BLAME:
4318 string_copy(opt_file, status->new.name);
4324 /* After returning the status view has been split to
4325 * show the stage view. No further reloading is
4327 status_enter(view, line);
4331 /* Simply reload the view. */
4338 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
4344 status_select(struct view *view, struct line *line)
4346 struct status *status = line->data;
4347 char file[SIZEOF_STR] = "all files";
4351 if (status && !string_format(file, "'%s'", status->new.name))
4354 if (!status && line[1].type == LINE_STAT_NONE)
4357 switch (line->type) {
4358 case LINE_STAT_STAGED:
4359 text = "Press %s to unstage %s for commit";
4362 case LINE_STAT_UNSTAGED:
4363 text = "Press %s to stage %s for commit";
4366 case LINE_STAT_UNTRACKED:
4367 text = "Press %s to stage %s for addition";
4370 case LINE_STAT_HEAD:
4371 case LINE_STAT_NONE:
4372 text = "Nothing to update";
4376 die("line type %d not handled in switch", line->type);
4379 if (status && status->status == 'U') {
4380 text = "Press %s to resolve conflict in %s";
4381 key = get_key(REQ_STATUS_MERGE);
4384 key = get_key(REQ_STATUS_UPDATE);
4387 string_format(view->ref, text, key, file);
4391 status_grep(struct view *view, struct line *line)
4393 struct status *status = line->data;
4394 enum { S_STATUS, S_NAME, S_END } state;
4401 for (state = S_STATUS; state < S_END; state++) {
4405 case S_NAME: text = status->new.name; break;
4407 buf[0] = status->status;
4415 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4422 static struct view_ops status_ops = {
4434 stage_diff_line(FILE *pipe, struct line *line)
4436 char *buf = line->data;
4437 size_t bufsize = strlen(buf);
4440 while (!ferror(pipe) && written < bufsize) {
4441 written += fwrite(buf + written, 1, bufsize - written, pipe);
4446 return written == bufsize;
4449 static struct line *
4450 stage_diff_hdr(struct view *view, struct line *line)
4452 int diff_hdr_dir = line->type == LINE_DIFF_CHUNK ? -1 : 1;
4453 struct line *diff_hdr;
4455 if (line->type == LINE_DIFF_CHUNK)
4456 diff_hdr = line - 1;
4458 diff_hdr = view->line + 1;
4460 while (diff_hdr > view->line && diff_hdr < view->line + view->lines) {
4461 if (diff_hdr->type == LINE_DIFF_HEADER)
4464 diff_hdr += diff_hdr_dir;
4471 stage_update_chunk(struct view *view, struct line *line)
4473 char cmd[SIZEOF_STR];
4475 struct line *diff_hdr, *diff_chunk, *diff_end;
4478 diff_hdr = stage_diff_hdr(view, line);
4483 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4486 if (!string_format_from(cmd, &cmdsize,
4487 "git apply --whitespace=nowarn --cached %s - && "
4488 "git update-index -q --unmerged --refresh 2>/dev/null",
4489 stage_line_type == LINE_STAT_STAGED ? "-R" : ""))
4492 pipe = popen(cmd, "w");
4496 diff_end = view->line + view->lines;
4497 if (line->type != LINE_DIFF_CHUNK) {
4498 diff_chunk = diff_hdr;
4501 for (diff_chunk = line + 1; diff_chunk < diff_end; diff_chunk++)
4502 if (diff_chunk->type == LINE_DIFF_CHUNK ||
4503 diff_chunk->type == LINE_DIFF_HEADER)
4504 diff_end = diff_chunk;
4508 while (diff_hdr->type != LINE_DIFF_CHUNK) {
4509 switch (diff_hdr->type) {
4510 case LINE_DIFF_HEADER:
4511 case LINE_DIFF_INDEX:
4521 if (!stage_diff_line(pipe, diff_hdr++)) {
4528 while (diff_chunk < diff_end && stage_diff_line(pipe, diff_chunk))
4533 if (diff_chunk != diff_end)
4540 stage_update(struct view *view, struct line *line)
4542 if (!opt_no_head && stage_line_type != LINE_STAT_UNTRACKED &&
4543 (line->type == LINE_DIFF_CHUNK || !stage_status.status)) {
4544 if (!stage_update_chunk(view, line)) {
4545 report("Failed to apply chunk");
4549 } else if (!status_update_file(&stage_status, stage_line_type)) {
4550 report("Failed to update file");
4554 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
4556 view = VIEW(REQ_VIEW_STATUS);
4557 if (view_is_displayed(view))
4558 status_enter(view, &view->line[view->lineno]);
4562 stage_request(struct view *view, enum request request, struct line *line)
4565 case REQ_STATUS_UPDATE:
4566 stage_update(view, line);
4570 if (!stage_status.new.name[0])
4573 open_editor(stage_status.status != '?', stage_status.new.name);
4576 case REQ_VIEW_BLAME:
4577 if (stage_status.new.name[0]) {
4578 string_copy(opt_file, stage_status.new.name);
4584 pager_request(view, request, line);
4594 static struct view_ops stage_ops = {
4610 char id[SIZEOF_REV]; /* SHA1 ID. */
4611 char title[128]; /* First line of the commit message. */
4612 char author[75]; /* Author of the commit. */
4613 struct tm time; /* Date from the author ident. */
4614 struct ref **refs; /* Repository references. */
4615 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
4616 size_t graph_size; /* The width of the graph array. */
4617 bool has_parents; /* Rewritten --parents seen. */
4620 /* Size of rev graph with no "padding" columns */
4621 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
4624 struct rev_graph *prev, *next, *parents;
4625 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
4627 struct commit *commit;
4629 unsigned int boundary:1;
4632 /* Parents of the commit being visualized. */
4633 static struct rev_graph graph_parents[4];
4635 /* The current stack of revisions on the graph. */
4636 static struct rev_graph graph_stacks[4] = {
4637 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
4638 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
4639 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
4640 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
4644 graph_parent_is_merge(struct rev_graph *graph)
4646 return graph->parents->size > 1;
4650 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
4652 struct commit *commit = graph->commit;
4654 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
4655 commit->graph[commit->graph_size++] = symbol;
4659 done_rev_graph(struct rev_graph *graph)
4661 if (graph_parent_is_merge(graph) &&
4662 graph->pos < graph->size - 1 &&
4663 graph->next->size == graph->size + graph->parents->size - 1) {
4664 size_t i = graph->pos + graph->parents->size - 1;
4666 graph->commit->graph_size = i * 2;
4667 while (i < graph->next->size - 1) {
4668 append_to_rev_graph(graph, ' ');
4669 append_to_rev_graph(graph, '\\');
4674 graph->size = graph->pos = 0;
4675 graph->commit = NULL;
4676 memset(graph->parents, 0, sizeof(*graph->parents));
4680 push_rev_graph(struct rev_graph *graph, char *parent)
4684 /* "Collapse" duplicate parents lines.
4686 * FIXME: This needs to also update update the drawn graph but
4687 * for now it just serves as a method for pruning graph lines. */
4688 for (i = 0; i < graph->size; i++)
4689 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
4692 if (graph->size < SIZEOF_REVITEMS) {
4693 string_copy_rev(graph->rev[graph->size++], parent);
4698 get_rev_graph_symbol(struct rev_graph *graph)
4702 if (graph->boundary)
4703 symbol = REVGRAPH_BOUND;
4704 else if (graph->parents->size == 0)
4705 symbol = REVGRAPH_INIT;
4706 else if (graph_parent_is_merge(graph))
4707 symbol = REVGRAPH_MERGE;
4708 else if (graph->pos >= graph->size)
4709 symbol = REVGRAPH_BRANCH;
4711 symbol = REVGRAPH_COMMIT;
4717 draw_rev_graph(struct rev_graph *graph)
4720 chtype separator, line;
4722 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
4723 static struct rev_filler fillers[] = {
4724 { ' ', REVGRAPH_LINE },
4729 chtype symbol = get_rev_graph_symbol(graph);
4730 struct rev_filler *filler;
4733 filler = &fillers[DEFAULT];
4735 for (i = 0; i < graph->pos; i++) {
4736 append_to_rev_graph(graph, filler->line);
4737 if (graph_parent_is_merge(graph->prev) &&
4738 graph->prev->pos == i)
4739 filler = &fillers[RSHARP];
4741 append_to_rev_graph(graph, filler->separator);
4744 /* Place the symbol for this revision. */
4745 append_to_rev_graph(graph, symbol);
4747 if (graph->prev->size > graph->size)
4748 filler = &fillers[RDIAG];
4750 filler = &fillers[DEFAULT];
4754 for (; i < graph->size; i++) {
4755 append_to_rev_graph(graph, filler->separator);
4756 append_to_rev_graph(graph, filler->line);
4757 if (graph_parent_is_merge(graph->prev) &&
4758 i < graph->prev->pos + graph->parents->size)
4759 filler = &fillers[RSHARP];
4760 if (graph->prev->size > graph->size)
4761 filler = &fillers[LDIAG];
4764 if (graph->prev->size > graph->size) {
4765 append_to_rev_graph(graph, filler->separator);
4766 if (filler->line != ' ')
4767 append_to_rev_graph(graph, filler->line);
4771 /* Prepare the next rev graph */
4773 prepare_rev_graph(struct rev_graph *graph)
4777 /* First, traverse all lines of revisions up to the active one. */
4778 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
4779 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
4782 push_rev_graph(graph->next, graph->rev[graph->pos]);
4785 /* Interleave the new revision parent(s). */
4786 for (i = 0; !graph->boundary && i < graph->parents->size; i++)
4787 push_rev_graph(graph->next, graph->parents->rev[i]);
4789 /* Lastly, put any remaining revisions. */
4790 for (i = graph->pos + 1; i < graph->size; i++)
4791 push_rev_graph(graph->next, graph->rev[i]);
4795 update_rev_graph(struct rev_graph *graph)
4797 /* If this is the finalizing update ... */
4799 prepare_rev_graph(graph);
4801 /* Graph visualization needs a one rev look-ahead,
4802 * so the first update doesn't visualize anything. */
4803 if (!graph->prev->commit)
4806 draw_rev_graph(graph->prev);
4807 done_rev_graph(graph->prev->prev);
4816 main_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
4818 char buf[DATE_COLS + 1];
4819 struct commit *commit = line->data;
4820 enum line_type type;
4825 if (!*commit->author)
4828 space = view->width;
4829 wmove(view->win, lineno, col);
4833 wattrset(view->win, get_line_attr(type));
4834 wchgat(view->win, -1, 0, type, NULL);
4836 type = LINE_MAIN_COMMIT;
4837 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
4843 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
4844 n = draw_text(view, buf, view->width - col, FALSE, selected);
4845 draw_text(view, " ", view->width - col - n, FALSE, selected);
4848 wmove(view->win, lineno, col);
4849 if (col >= view->width)
4852 if (type != LINE_CURSOR)
4853 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
4858 max_len = view->width - col;
4859 if (max_len > AUTHOR_COLS - 1)
4860 max_len = AUTHOR_COLS - 1;
4861 draw_text(view, commit->author, max_len, TRUE, selected);
4863 if (col >= view->width)
4867 if (opt_rev_graph && commit->graph_size) {
4868 size_t graph_size = view->width - col;
4871 if (type != LINE_CURSOR)
4872 wattrset(view->win, get_line_attr(LINE_MAIN_REVGRAPH));
4873 wmove(view->win, lineno, col);
4874 if (graph_size > commit->graph_size)
4875 graph_size = commit->graph_size;
4876 /* Using waddch() instead of waddnstr() ensures that
4877 * they'll be rendered correctly for the cursor line. */
4878 for (i = 0; i < graph_size; i++)
4879 waddch(view->win, commit->graph[i]);
4881 col += commit->graph_size + 1;
4882 if (col >= view->width)
4884 waddch(view->win, ' ');
4886 if (type != LINE_CURSOR)
4887 wattrset(view->win, A_NORMAL);
4889 wmove(view->win, lineno, col);
4891 if (opt_show_refs && commit->refs) {
4895 if (type == LINE_CURSOR)
4897 else if (commit->refs[i]->head)
4898 wattrset(view->win, get_line_attr(LINE_MAIN_HEAD));
4899 else if (commit->refs[i]->ltag)
4900 wattrset(view->win, get_line_attr(LINE_MAIN_LOCAL_TAG));
4901 else if (commit->refs[i]->tag)
4902 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
4903 else if (commit->refs[i]->remote)
4904 wattrset(view->win, get_line_attr(LINE_MAIN_REMOTE));
4906 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
4908 col += draw_text(view, "[", view->width - col, TRUE, selected);
4909 col += draw_text(view, commit->refs[i]->name, view->width - col,
4911 col += draw_text(view, "]", view->width - col, TRUE, selected);
4912 if (type != LINE_CURSOR)
4913 wattrset(view->win, A_NORMAL);
4914 col += draw_text(view, " ", view->width - col, TRUE, selected);
4915 if (col >= view->width)
4917 } while (commit->refs[i++]->next);
4920 if (type != LINE_CURSOR)
4921 wattrset(view->win, get_line_attr(type));
4923 draw_text(view, commit->title, view->width - col, TRUE, selected);
4927 /* Reads git log --pretty=raw output and parses it into the commit struct. */
4929 main_read(struct view *view, char *line)
4931 static struct rev_graph *graph = graph_stacks;
4932 enum line_type type;
4933 struct commit *commit;
4936 update_rev_graph(graph);
4940 type = get_line_type(line);
4941 if (type == LINE_COMMIT) {
4942 commit = calloc(1, sizeof(struct commit));
4946 line += STRING_SIZE("commit ");
4948 graph->boundary = 1;
4952 string_copy_rev(commit->id, line);
4953 commit->refs = get_refs(commit->id);
4954 graph->commit = commit;
4955 add_line_data(view, commit, LINE_MAIN_COMMIT);
4957 while ((line = strchr(line, ' '))) {
4959 push_rev_graph(graph->parents, line);
4960 commit->has_parents = TRUE;
4967 commit = view->line[view->lines - 1].data;
4971 if (commit->has_parents)
4973 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
4978 /* Parse author lines where the name may be empty:
4979 * author <email@address.tld> 1138474660 +0100
4981 char *ident = line + STRING_SIZE("author ");
4982 char *nameend = strchr(ident, '<');
4983 char *emailend = strchr(ident, '>');
4985 if (!nameend || !emailend)
4988 update_rev_graph(graph);
4989 graph = graph->next;
4991 *nameend = *emailend = 0;
4992 ident = chomp_string(ident);
4994 ident = chomp_string(nameend + 1);
4999 string_ncopy(commit->author, ident, strlen(ident));
5001 /* Parse epoch and timezone */
5002 if (emailend[1] == ' ') {
5003 char *secs = emailend + 2;
5004 char *zone = strchr(secs, ' ');
5005 time_t time = (time_t) atol(secs);
5007 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
5011 tz = ('0' - zone[1]) * 60 * 60 * 10;
5012 tz += ('0' - zone[2]) * 60 * 60;
5013 tz += ('0' - zone[3]) * 60;
5014 tz += ('0' - zone[4]) * 60;
5022 gmtime_r(&time, &commit->time);
5027 /* Fill in the commit title if it has not already been set. */
5028 if (commit->title[0])
5031 /* Require titles to start with a non-space character at the
5032 * offset used by git log. */
5033 if (strncmp(line, " ", 4))
5036 /* Well, if the title starts with a whitespace character,
5037 * try to be forgiving. Otherwise we end up with no title. */
5038 while (isspace(*line))
5042 /* FIXME: More graceful handling of titles; append "..." to
5043 * shortened titles, etc. */
5045 string_ncopy(commit->title, line, strlen(line));
5052 main_request(struct view *view, enum request request, struct line *line)
5054 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
5056 if (request == REQ_ENTER)
5057 open_view(view, REQ_VIEW_DIFF, flags);
5065 main_grep(struct view *view, struct line *line)
5067 struct commit *commit = line->data;
5068 enum { S_TITLE, S_AUTHOR, S_DATE, S_END } state;
5069 char buf[DATE_COLS + 1];
5072 for (state = S_TITLE; state < S_END; state++) {
5076 case S_TITLE: text = commit->title; break;
5077 case S_AUTHOR: text = commit->author; break;
5079 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
5088 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
5096 main_select(struct view *view, struct line *line)
5098 struct commit *commit = line->data;
5100 string_copy_rev(view->ref, commit->id);
5101 string_copy_rev(ref_commit, view->ref);
5104 static struct view_ops main_ops = {
5116 * Unicode / UTF-8 handling
5118 * NOTE: Much of the following code for dealing with unicode is derived from
5119 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
5120 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
5123 /* I've (over)annotated a lot of code snippets because I am not entirely
5124 * confident that the approach taken by this small UTF-8 interface is correct.
5128 unicode_width(unsigned long c)
5131 (c <= 0x115f /* Hangul Jamo */
5134 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
5136 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
5137 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
5138 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
5139 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
5140 || (c >= 0xffe0 && c <= 0xffe6)
5141 || (c >= 0x20000 && c <= 0x2fffd)
5142 || (c >= 0x30000 && c <= 0x3fffd)))
5146 return opt_tab_size;
5151 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
5152 * Illegal bytes are set one. */
5153 static const unsigned char utf8_bytes[256] = {
5154 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,
5155 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,
5156 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,
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 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,
5161 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,
5164 /* Decode UTF-8 multi-byte representation into a unicode character. */
5165 static inline unsigned long
5166 utf8_to_unicode(const char *string, size_t length)
5168 unsigned long unicode;
5172 unicode = string[0];
5175 unicode = (string[0] & 0x1f) << 6;
5176 unicode += (string[1] & 0x3f);
5179 unicode = (string[0] & 0x0f) << 12;
5180 unicode += ((string[1] & 0x3f) << 6);
5181 unicode += (string[2] & 0x3f);
5184 unicode = (string[0] & 0x0f) << 18;
5185 unicode += ((string[1] & 0x3f) << 12);
5186 unicode += ((string[2] & 0x3f) << 6);
5187 unicode += (string[3] & 0x3f);
5190 unicode = (string[0] & 0x0f) << 24;
5191 unicode += ((string[1] & 0x3f) << 18);
5192 unicode += ((string[2] & 0x3f) << 12);
5193 unicode += ((string[3] & 0x3f) << 6);
5194 unicode += (string[4] & 0x3f);
5197 unicode = (string[0] & 0x01) << 30;
5198 unicode += ((string[1] & 0x3f) << 24);
5199 unicode += ((string[2] & 0x3f) << 18);
5200 unicode += ((string[3] & 0x3f) << 12);
5201 unicode += ((string[4] & 0x3f) << 6);
5202 unicode += (string[5] & 0x3f);
5205 die("Invalid unicode length");
5208 /* Invalid characters could return the special 0xfffd value but NUL
5209 * should be just as good. */
5210 return unicode > 0xffff ? 0 : unicode;
5213 /* Calculates how much of string can be shown within the given maximum width
5214 * and sets trimmed parameter to non-zero value if all of string could not be
5215 * shown. If the reserve flag is TRUE, it will reserve at least one
5216 * trailing character, which can be useful when drawing a delimiter.
5218 * Returns the number of bytes to output from string to satisfy max_width. */
5220 utf8_length(const char *string, size_t max_width, int *trimmed, bool reserve)
5222 const char *start = string;
5223 const char *end = strchr(string, '\0');
5224 unsigned char last_bytes = 0;
5229 while (string < end) {
5230 int c = *(unsigned char *) string;
5231 unsigned char bytes = utf8_bytes[c];
5233 unsigned long unicode;
5235 if (string + bytes > end)
5238 /* Change representation to figure out whether
5239 * it is a single- or double-width character. */
5241 unicode = utf8_to_unicode(string, bytes);
5242 /* FIXME: Graceful handling of invalid unicode character. */
5246 ucwidth = unicode_width(unicode);
5248 if (width > max_width) {
5250 if (reserve && width - ucwidth == max_width) {
5251 string -= last_bytes;
5260 return string - start;
5268 /* Whether or not the curses interface has been initialized. */
5269 static bool cursed = FALSE;
5271 /* The status window is used for polling keystrokes. */
5272 static WINDOW *status_win;
5274 static bool status_empty = TRUE;
5276 /* Update status and title window. */
5278 report(const char *msg, ...)
5280 struct view *view = display[current_view];
5286 char buf[SIZEOF_STR];
5289 va_start(args, msg);
5290 if (vsnprintf(buf, sizeof(buf), msg, args) >= sizeof(buf)) {
5291 buf[sizeof(buf) - 1] = 0;
5292 buf[sizeof(buf) - 2] = '.';
5293 buf[sizeof(buf) - 3] = '.';
5294 buf[sizeof(buf) - 4] = '.';
5300 if (!status_empty || *msg) {
5303 va_start(args, msg);
5305 wmove(status_win, 0, 0);
5307 vwprintw(status_win, msg, args);
5308 status_empty = FALSE;
5310 status_empty = TRUE;
5312 wclrtoeol(status_win);
5313 wrefresh(status_win);
5318 update_view_title(view);
5319 update_display_cursor(view);
5322 /* Controls when nodelay should be in effect when polling user input. */
5324 set_nonblocking_input(bool loading)
5326 static unsigned int loading_views;
5328 if ((loading == FALSE && loading_views-- == 1) ||
5329 (loading == TRUE && loading_views++ == 0))
5330 nodelay(status_win, loading);
5338 /* Initialize the curses library */
5339 if (isatty(STDIN_FILENO)) {
5340 cursed = !!initscr();
5342 /* Leave stdin and stdout alone when acting as a pager. */
5343 FILE *io = fopen("/dev/tty", "r+");
5346 die("Failed to open /dev/tty");
5347 cursed = !!newterm(NULL, io, io);
5351 die("Failed to initialize curses");
5353 nonl(); /* Tell curses not to do NL->CR/NL on output */
5354 cbreak(); /* Take input chars one at a time, no wait for \n */
5355 noecho(); /* Don't echo input */
5356 leaveok(stdscr, TRUE);
5361 getmaxyx(stdscr, y, x);
5362 status_win = newwin(1, 0, y - 1, 0);
5364 die("Failed to create status window");
5366 /* Enable keyboard mapping */
5367 keypad(status_win, TRUE);
5368 wbkgdset(status_win, get_line_attr(LINE_STATUS));
5372 read_prompt(const char *prompt)
5374 enum { READING, STOP, CANCEL } status = READING;
5375 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
5378 while (status == READING) {
5384 foreach_view (view, i)
5389 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
5390 wclrtoeol(status_win);
5392 /* Refresh, accept single keystroke of input */
5393 key = wgetch(status_win);
5398 status = pos ? STOP : CANCEL;
5416 if (pos >= sizeof(buf)) {
5417 report("Input string too long");
5422 buf[pos++] = (char) key;
5426 /* Clear the status window */
5427 status_empty = FALSE;
5430 if (status == CANCEL)
5439 * Repository references
5442 static struct ref *refs = NULL;
5443 static size_t refs_alloc = 0;
5444 static size_t refs_size = 0;
5446 /* Id <-> ref store */
5447 static struct ref ***id_refs = NULL;
5448 static size_t id_refs_alloc = 0;
5449 static size_t id_refs_size = 0;
5451 static struct ref **
5454 struct ref ***tmp_id_refs;
5455 struct ref **ref_list = NULL;
5456 size_t ref_list_alloc = 0;
5457 size_t ref_list_size = 0;
5460 for (i = 0; i < id_refs_size; i++)
5461 if (!strcmp(id, id_refs[i][0]->id))
5464 tmp_id_refs = realloc_items(id_refs, &id_refs_alloc, id_refs_size + 1,
5469 id_refs = tmp_id_refs;
5471 for (i = 0; i < refs_size; i++) {
5474 if (strcmp(id, refs[i].id))
5477 tmp = realloc_items(ref_list, &ref_list_alloc,
5478 ref_list_size + 1, sizeof(*ref_list));
5486 if (ref_list_size > 0)
5487 ref_list[ref_list_size - 1]->next = 1;
5488 ref_list[ref_list_size] = &refs[i];
5490 /* XXX: The properties of the commit chains ensures that we can
5491 * safely modify the shared ref. The repo references will
5492 * always be similar for the same id. */
5493 ref_list[ref_list_size]->next = 0;
5498 id_refs[id_refs_size++] = ref_list;
5504 read_ref(char *id, size_t idlen, char *name, size_t namelen)
5509 bool remote = FALSE;
5510 bool check_replace = FALSE;
5513 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
5514 if (!strcmp(name + namelen - 3, "^{}")) {
5517 if (refs_size > 0 && refs[refs_size - 1].ltag == TRUE)
5518 check_replace = TRUE;
5524 namelen -= STRING_SIZE("refs/tags/");
5525 name += STRING_SIZE("refs/tags/");
5527 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
5529 namelen -= STRING_SIZE("refs/remotes/");
5530 name += STRING_SIZE("refs/remotes/");
5532 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5533 namelen -= STRING_SIZE("refs/heads/");
5534 name += STRING_SIZE("refs/heads/");
5535 head = !strncmp(opt_head, name, namelen);
5537 } else if (!strcmp(name, "HEAD")) {
5538 opt_no_head = FALSE;
5542 if (check_replace && !strcmp(name, refs[refs_size - 1].name)) {
5543 /* it's an annotated tag, replace the previous sha1 with the
5544 * resolved commit id; relies on the fact git-ls-remote lists
5545 * the commit id of an annotated tag right beofre the commit id
5547 refs[refs_size - 1].ltag = ltag;
5548 string_copy_rev(refs[refs_size - 1].id, id);
5552 refs = realloc_items(refs, &refs_alloc, refs_size + 1, sizeof(*refs));
5556 ref = &refs[refs_size++];
5557 ref->name = malloc(namelen + 1);
5561 strncpy(ref->name, name, namelen);
5562 ref->name[namelen] = 0;
5565 ref->remote = remote;
5567 string_copy_rev(ref->id, id);
5575 const char *cmd_env = getenv("TIG_LS_REMOTE");
5576 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
5578 return read_properties(popen(cmd, "r"), "\t", read_ref);
5582 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
5584 if (!strcmp(name, "i18n.commitencoding"))
5585 string_ncopy(opt_encoding, value, valuelen);
5587 if (!strcmp(name, "core.editor"))
5588 string_ncopy(opt_editor, value, valuelen);
5594 load_repo_config(void)
5596 return read_properties(popen(GIT_CONFIG " --list", "r"),
5597 "=", read_repo_config_option);
5601 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
5603 if (!opt_git_dir[0]) {
5604 string_ncopy(opt_git_dir, name, namelen);
5606 } else if (opt_is_inside_work_tree == -1) {
5607 /* This can be 3 different values depending on the
5608 * version of git being used. If git-rev-parse does not
5609 * understand --is-inside-work-tree it will simply echo
5610 * the option else either "true" or "false" is printed.
5611 * Default to true for the unknown case. */
5612 opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
5614 } else if (opt_cdup[0] == ' ') {
5615 string_ncopy(opt_cdup, name, namelen);
5617 if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5618 namelen -= STRING_SIZE("refs/heads/");
5619 name += STRING_SIZE("refs/heads/");
5620 string_ncopy(opt_head, name, namelen);
5628 load_repo_info(void)
5631 FILE *pipe = popen("git rev-parse --git-dir --is-inside-work-tree "
5632 " --show-cdup --symbolic-full-name HEAD 2>/dev/null", "r");
5634 /* XXX: The line outputted by "--show-cdup" can be empty so
5635 * initialize it to something invalid to make it possible to
5636 * detect whether it has been set or not. */
5639 result = read_properties(pipe, "=", read_repo_info);
5640 if (opt_cdup[0] == ' ')
5647 read_properties(FILE *pipe, const char *separators,
5648 int (*read_property)(char *, size_t, char *, size_t))
5650 char buffer[BUFSIZ];
5657 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
5662 name = chomp_string(name);
5663 namelen = strcspn(name, separators);
5665 if (name[namelen]) {
5667 value = chomp_string(name + namelen + 1);
5668 valuelen = strlen(value);
5675 state = read_property(name, namelen, value, valuelen);
5678 if (state != ERR && ferror(pipe))
5691 static void __NORETURN
5694 /* XXX: Restore tty modes and let the OS cleanup the rest! */
5700 static void __NORETURN
5701 die(const char *err, ...)
5707 va_start(args, err);
5708 fputs("tig: ", stderr);
5709 vfprintf(stderr, err, args);
5710 fputs("\n", stderr);
5717 warn(const char *msg, ...)
5721 va_start(args, msg);
5722 fputs("tig warning: ", stderr);
5723 vfprintf(stderr, msg, args);
5724 fputs("\n", stderr);
5729 main(int argc, char *argv[])
5732 enum request request;
5735 signal(SIGINT, quit);
5737 if (setlocale(LC_ALL, "")) {
5738 char *codeset = nl_langinfo(CODESET);
5740 string_ncopy(opt_codeset, codeset, strlen(codeset));
5743 if (load_repo_info() == ERR)
5744 die("Failed to load repo info.");
5746 if (load_options() == ERR)
5747 die("Failed to load user config.");
5749 /* Load the repo config file so options can be overwritten from
5750 * the command line. */
5751 if (load_repo_config() == ERR)
5752 die("Failed to load repo config.");
5754 if (!parse_options(argc, argv))
5757 /* Require a git repository unless when running in pager mode. */
5758 if (!opt_git_dir[0] && opt_request != REQ_VIEW_PAGER)
5759 die("Not a git repository");
5761 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
5764 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
5765 opt_iconv = iconv_open(opt_codeset, opt_encoding);
5766 if (opt_iconv == ICONV_NONE)
5767 die("Failed to initialize character set conversion");
5770 if (load_refs() == ERR)
5771 die("Failed to load refs.");
5773 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
5774 view->cmd_env = getenv(view->cmd_env);
5776 request = opt_request;
5780 while (view_driver(display[current_view], request)) {
5784 foreach_view (view, i)
5787 /* Refresh, accept single keystroke of input */
5788 key = wgetch(status_win);
5790 /* wgetch() with nodelay() enabled returns ERR when there's no
5797 request = get_keybinding(display[current_view]->keymap, key);
5799 /* Some low-level request handling. This keeps access to
5800 * status_win restricted. */
5804 char *cmd = read_prompt(":");
5806 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
5807 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
5808 opt_request = REQ_VIEW_DIFF;
5810 opt_request = REQ_VIEW_PAGER;
5819 case REQ_SEARCH_BACK:
5821 const char *prompt = request == REQ_SEARCH
5823 char *search = read_prompt(prompt);
5826 string_ncopy(opt_search, search, strlen(search));
5831 case REQ_SCREEN_RESIZE:
5835 getmaxyx(stdscr, height, width);
5837 /* Resize the status view and let the view driver take
5838 * care of resizing the displayed views. */
5839 wresize(status_win, 1, width);
5840 mvwin(status_win, height - 1, 0);
5841 wrefresh(status_win);