1 /* Copyright (c) 2006-2008 Jonas Fonseca <fonseca@diku.dk>
3 * This program is free software; you can redistribute it and/or
4 * modify it under the terms of the GNU General Public License as
5 * published by the Free Software Foundation; either version 2 of
6 * the License, or (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
19 #define TIG_VERSION "unknown-version"
34 #include <sys/types.h>
45 /* ncurses(3): Must be defined to have extended wide-character functions. */
46 #define _XOPEN_SOURCE_EXTENDED
51 #define __NORETURN __attribute__((__noreturn__))
56 static void __NORETURN die(const char *err, ...);
57 static void warn(const char *msg, ...);
58 static void report(const char *msg, ...);
59 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, size_t, char *, size_t));
60 static void set_nonblocking_input(bool loading);
61 static size_t utf8_length(const char *string, size_t max_width, int *trimmed, bool reserve);
63 #define ABS(x) ((x) >= 0 ? (x) : -(x))
64 #define MIN(x, y) ((x) < (y) ? (x) : (y))
66 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
67 #define STRING_SIZE(x) (sizeof(x) - 1)
69 #define SIZEOF_STR 1024 /* Default string size. */
70 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
71 #define SIZEOF_REV 41 /* Holds a SHA-1 and an ending NUL */
75 #define REVGRAPH_INIT 'I'
76 #define REVGRAPH_MERGE 'M'
77 #define REVGRAPH_BRANCH '+'
78 #define REVGRAPH_COMMIT '*'
79 #define REVGRAPH_BOUND '^'
80 #define REVGRAPH_LINE '|'
82 #define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
84 /* This color name can be used to refer to the default term colors. */
85 #define COLOR_DEFAULT (-1)
87 #define ICONV_NONE ((iconv_t) -1)
89 #define ICONV_CONST /* nothing */
92 /* The format and size of the date column in the main view. */
93 #define DATE_FORMAT "%Y-%m-%d %H:%M"
94 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
96 #define AUTHOR_COLS 20
99 /* The default interval between line numbers. */
100 #define NUMBER_INTERVAL 5
104 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
106 #define NULL_ID "0000000000000000000000000000000000000000"
109 #define GIT_CONFIG "git config"
112 #define TIG_LS_REMOTE \
113 "git ls-remote $(git rev-parse --git-dir) 2>/dev/null"
115 #define TIG_DIFF_CMD \
116 "git show --pretty=fuller --no-color --root --patch-with-stat --find-copies-harder -C %s 2>/dev/null"
118 #define TIG_LOG_CMD \
119 "git log --no-color --cc --stat -n100 %s 2>/dev/null"
121 #define TIG_MAIN_CMD \
122 "git log --no-color --topo-order --parents --boundary --pretty=raw %s 2>/dev/null"
124 #define TIG_TREE_CMD \
127 #define TIG_BLOB_CMD \
128 "git cat-file blob %s"
130 /* XXX: Needs to be defined to the empty string. */
131 #define TIG_HELP_CMD ""
132 #define TIG_PAGER_CMD ""
133 #define TIG_STATUS_CMD ""
134 #define TIG_STAGE_CMD ""
135 #define TIG_BLAME_CMD ""
137 /* Some ascii-shorthands fitted into the ncurses namespace. */
139 #define KEY_RETURN '\r'
144 char *name; /* Ref name; tag or head names are shortened. */
145 char id[SIZEOF_REV]; /* Commit SHA1 ID */
146 unsigned int head:1; /* Is it the current HEAD? */
147 unsigned int tag:1; /* Is it a tag? */
148 unsigned int ltag:1; /* If so, is the tag local? */
149 unsigned int remote:1; /* Is it a remote ref? */
150 unsigned int tracked:1; /* Is it the remote for the current HEAD? */
151 unsigned int next:1; /* For ref lists: are there more refs? */
154 static struct ref **get_refs(char *id);
163 set_from_int_map(struct int_map *map, size_t map_size,
164 int *value, const char *name, int namelen)
169 for (i = 0; i < map_size; i++)
170 if (namelen == map[i].namelen &&
171 !strncasecmp(name, map[i].name, namelen)) {
172 *value = map[i].value;
185 string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
187 if (srclen > dstlen - 1)
190 strncpy(dst, src, srclen);
194 /* Shorthands for safely copying into a fixed buffer. */
196 #define string_copy(dst, src) \
197 string_ncopy_do(dst, sizeof(dst), src, sizeof(src))
199 #define string_ncopy(dst, src, srclen) \
200 string_ncopy_do(dst, sizeof(dst), src, srclen)
202 #define string_copy_rev(dst, src) \
203 string_ncopy_do(dst, SIZEOF_REV, src, SIZEOF_REV - 1)
205 #define string_add(dst, from, src) \
206 string_ncopy_do(dst + (from), sizeof(dst) - (from), src, sizeof(src))
209 chomp_string(char *name)
213 while (isspace(*name))
216 namelen = strlen(name) - 1;
217 while (namelen > 0 && isspace(name[namelen]))
224 string_nformat(char *buf, size_t bufsize, size_t *bufpos, const char *fmt, ...)
227 size_t pos = bufpos ? *bufpos : 0;
230 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
236 return pos >= bufsize ? FALSE : TRUE;
239 #define string_format(buf, fmt, args...) \
240 string_nformat(buf, sizeof(buf), NULL, fmt, args)
242 #define string_format_from(buf, from, fmt, args...) \
243 string_nformat(buf, sizeof(buf), from, fmt, args)
246 string_enum_compare(const char *str1, const char *str2, int len)
250 #define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
252 /* Diff-Header == DIFF_HEADER */
253 for (i = 0; i < len; i++) {
254 if (toupper(str1[i]) == toupper(str2[i]))
257 if (string_enum_sep(str1[i]) &&
258 string_enum_sep(str2[i]))
261 return str1[i] - str2[i];
269 * NOTE: The following is a slightly modified copy of the git project's shell
270 * quoting routines found in the quote.c file.
272 * Help to copy the thing properly quoted for the shell safety. any single
273 * quote is replaced with '\'', any exclamation point is replaced with '\!',
274 * and the whole thing is enclosed in a
277 * original sq_quote result
278 * name ==> name ==> 'name'
279 * a b ==> a b ==> 'a b'
280 * a'b ==> a'\''b ==> 'a'\''b'
281 * a!b ==> a'\!'b ==> 'a'\!'b'
285 sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
289 #define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0)
292 while ((c = *src++)) {
293 if (c == '\'' || c == '!') {
304 if (bufsize < SIZEOF_STR)
316 /* XXX: Keep the view request first and in sync with views[]. */ \
317 REQ_GROUP("View switching") \
318 REQ_(VIEW_MAIN, "Show main view"), \
319 REQ_(VIEW_DIFF, "Show diff view"), \
320 REQ_(VIEW_LOG, "Show log view"), \
321 REQ_(VIEW_TREE, "Show tree view"), \
322 REQ_(VIEW_BLOB, "Show blob view"), \
323 REQ_(VIEW_BLAME, "Show blame view"), \
324 REQ_(VIEW_HELP, "Show help page"), \
325 REQ_(VIEW_PAGER, "Show pager view"), \
326 REQ_(VIEW_STATUS, "Show status view"), \
327 REQ_(VIEW_STAGE, "Show stage view"), \
329 REQ_GROUP("View manipulation") \
330 REQ_(ENTER, "Enter current line and scroll"), \
331 REQ_(NEXT, "Move to next"), \
332 REQ_(PREVIOUS, "Move to previous"), \
333 REQ_(VIEW_NEXT, "Move focus to next view"), \
334 REQ_(REFRESH, "Reload and refresh"), \
335 REQ_(MAXIMIZE, "Maximize the current view"), \
336 REQ_(VIEW_CLOSE, "Close the current view"), \
337 REQ_(QUIT, "Close all views and quit"), \
339 REQ_GROUP("Cursor navigation") \
340 REQ_(MOVE_UP, "Move cursor one line up"), \
341 REQ_(MOVE_DOWN, "Move cursor one line down"), \
342 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
343 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
344 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
345 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
347 REQ_GROUP("Scrolling") \
348 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
349 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
350 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
351 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
353 REQ_GROUP("Searching") \
354 REQ_(SEARCH, "Search the view"), \
355 REQ_(SEARCH_BACK, "Search backwards in the view"), \
356 REQ_(FIND_NEXT, "Find next search match"), \
357 REQ_(FIND_PREV, "Find previous search match"), \
360 REQ_(PROMPT, "Bring up the prompt"), \
361 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
362 REQ_(SCREEN_RESIZE, "Resize the screen"), \
363 REQ_(SHOW_VERSION, "Show version information"), \
364 REQ_(STOP_LOADING, "Stop all loading views"), \
365 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
366 REQ_(TOGGLE_DATE, "Toggle date display"), \
367 REQ_(TOGGLE_AUTHOR, "Toggle author display"), \
368 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"), \
369 REQ_(TOGGLE_REFS, "Toggle reference display (tags/branches)"), \
370 REQ_(STATUS_UPDATE, "Update file status"), \
371 REQ_(STATUS_MERGE, "Merge file using external tool"), \
372 REQ_(TREE_PARENT, "Switch to parent directory in tree view"), \
373 REQ_(EDIT, "Open in editor"), \
374 REQ_(NONE, "Do nothing")
377 /* User action requests. */
379 #define REQ_GROUP(help)
380 #define REQ_(req, help) REQ_##req
382 /* Offset all requests to avoid conflicts with ncurses getch values. */
383 REQ_OFFSET = KEY_MAX + 1,
390 struct request_info {
391 enum request request;
397 static struct request_info req_info[] = {
398 #define REQ_GROUP(help) { 0, NULL, 0, (help) },
399 #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
406 get_request(const char *name)
408 int namelen = strlen(name);
411 for (i = 0; i < ARRAY_SIZE(req_info); i++)
412 if (req_info[i].namelen == namelen &&
413 !string_enum_compare(req_info[i].name, name, namelen))
414 return req_info[i].request;
424 static const char usage[] =
425 "tig " TIG_VERSION " (" __DATE__ ")\n"
427 "Usage: tig [options] [revs] [--] [paths]\n"
428 " or: tig show [options] [revs] [--] [paths]\n"
429 " or: tig blame [rev] path\n"
431 " or: tig < [git command output]\n"
434 " -v, --version Show version and exit\n"
435 " -h, --help Show help message and exit";
437 /* Option and state variables. */
438 static bool opt_date = TRUE;
439 static bool opt_author = TRUE;
440 static bool opt_line_number = FALSE;
441 static bool opt_rev_graph = FALSE;
442 static bool opt_show_refs = TRUE;
443 static int opt_num_interval = NUMBER_INTERVAL;
444 static int opt_tab_size = TABSIZE;
445 static enum request opt_request = REQ_VIEW_MAIN;
446 static char opt_cmd[SIZEOF_STR] = "";
447 static char opt_path[SIZEOF_STR] = "";
448 static char opt_file[SIZEOF_STR] = "";
449 static char opt_ref[SIZEOF_REF] = "";
450 static char opt_head[SIZEOF_REF] = "";
451 static char opt_remote[SIZEOF_REF] = "";
452 static bool opt_no_head = TRUE;
453 static FILE *opt_pipe = NULL;
454 static char opt_encoding[20] = "UTF-8";
455 static bool opt_utf8 = TRUE;
456 static char opt_codeset[20] = "UTF-8";
457 static iconv_t opt_iconv = ICONV_NONE;
458 static char opt_search[SIZEOF_STR] = "";
459 static char opt_cdup[SIZEOF_STR] = "";
460 static char opt_git_dir[SIZEOF_STR] = "";
461 static signed char opt_is_inside_work_tree = -1; /* set to TRUE or FALSE */
462 static char opt_editor[SIZEOF_STR] = "";
465 parse_options(int argc, char *argv[])
469 bool seen_dashdash = FALSE;
472 if (!isatty(STDIN_FILENO)) {
473 opt_request = REQ_VIEW_PAGER;
481 subcommand = argv[1];
482 if (!strcmp(subcommand, "status") || !strcmp(subcommand, "-S")) {
483 opt_request = REQ_VIEW_STATUS;
484 if (!strcmp(subcommand, "-S"))
485 warn("`-S' has been deprecated; use `tig status' instead");
487 warn("ignoring arguments after `%s'", subcommand);
490 } else if (!strcmp(subcommand, "blame")) {
491 opt_request = REQ_VIEW_BLAME;
492 if (argc <= 2 || argc > 4)
493 die("invalid number of options to blame\n\n%s", usage);
497 string_ncopy(opt_ref, argv[i], strlen(argv[i]));
501 string_ncopy(opt_file, argv[i], strlen(argv[i]));
504 } else if (!strcmp(subcommand, "show")) {
505 opt_request = REQ_VIEW_DIFF;
507 } else if (!strcmp(subcommand, "log") || !strcmp(subcommand, "diff")) {
508 opt_request = subcommand[0] == 'l'
509 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
510 warn("`tig %s' has been deprecated", subcommand);
517 /* XXX: This is vulnerable to the user overriding
518 * options required for the main view parser. */
519 string_copy(opt_cmd, "git log --no-color --pretty=raw --boundary --parents");
521 string_format(opt_cmd, "git %s", subcommand);
523 buf_size = strlen(opt_cmd);
525 for (i = 1 + !!subcommand; i < argc; i++) {
528 if (seen_dashdash || !strcmp(opt, "--")) {
529 seen_dashdash = TRUE;
531 } else if (!strcmp(opt, "-v") || !strcmp(opt, "--version")) {
532 printf("tig version %s\n", TIG_VERSION);
535 } else if (!strcmp(opt, "-h") || !strcmp(opt, "--help")) {
536 printf("%s\n", usage);
540 opt_cmd[buf_size++] = ' ';
541 buf_size = sq_quote(opt_cmd, buf_size, opt);
542 if (buf_size >= sizeof(opt_cmd))
543 die("command too long");
546 opt_cmd[buf_size] = 0;
553 * Line-oriented content detection.
557 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
558 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
559 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
560 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
561 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
562 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
563 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
564 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
565 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
566 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
567 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
568 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
569 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
570 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
571 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
572 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
573 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
574 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
575 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
576 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
577 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
578 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
579 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
580 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
581 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
582 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
583 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
584 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
585 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
586 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
587 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
588 LINE(DELIMITER, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
589 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
590 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
591 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
592 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
593 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
594 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
595 LINE(MAIN_LOCAL_TAG,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
596 LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \
597 LINE(MAIN_TRACKED, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
598 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
599 LINE(MAIN_HEAD, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
600 LINE(MAIN_REVGRAPH,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
601 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
602 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
603 LINE(STAT_HEAD, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \
604 LINE(STAT_SECTION, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
605 LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
606 LINE(STAT_STAGED, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
607 LINE(STAT_UNSTAGED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
608 LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
609 LINE(BLAME_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
610 LINE(BLAME_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
611 LINE(BLAME_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
612 LINE(BLAME_ID, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
613 LINE(BLAME_LINENO, "", COLOR_CYAN, COLOR_DEFAULT, 0)
616 #define LINE(type, line, fg, bg, attr) \
623 const char *name; /* Option name. */
624 int namelen; /* Size of option name. */
625 const char *line; /* The start of line to match. */
626 int linelen; /* Size of string to match. */
627 int fg, bg, attr; /* Color and text attributes for the lines. */
630 static struct line_info line_info[] = {
631 #define LINE(type, line, fg, bg, attr) \
632 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
637 static enum line_type
638 get_line_type(char *line)
640 int linelen = strlen(line);
643 for (type = 0; type < ARRAY_SIZE(line_info); type++)
644 /* Case insensitive search matches Signed-off-by lines better. */
645 if (linelen >= line_info[type].linelen &&
646 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
653 get_line_attr(enum line_type type)
655 assert(type < ARRAY_SIZE(line_info));
656 return COLOR_PAIR(type) | line_info[type].attr;
659 static struct line_info *
660 get_line_info(char *name)
662 size_t namelen = strlen(name);
665 for (type = 0; type < ARRAY_SIZE(line_info); type++)
666 if (namelen == line_info[type].namelen &&
667 !string_enum_compare(line_info[type].name, name, namelen))
668 return &line_info[type];
676 int default_bg = line_info[LINE_DEFAULT].bg;
677 int default_fg = line_info[LINE_DEFAULT].fg;
682 if (assume_default_colors(default_fg, default_bg) == ERR) {
683 default_bg = COLOR_BLACK;
684 default_fg = COLOR_WHITE;
687 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
688 struct line_info *info = &line_info[type];
689 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
690 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
692 init_pair(type, fg, bg);
700 unsigned int selected:1;
701 unsigned int dirty:1;
703 void *data; /* User data */
713 enum request request;
714 struct keybinding *next;
717 static struct keybinding default_keybindings[] = {
719 { 'm', REQ_VIEW_MAIN },
720 { 'd', REQ_VIEW_DIFF },
721 { 'l', REQ_VIEW_LOG },
722 { 't', REQ_VIEW_TREE },
723 { 'f', REQ_VIEW_BLOB },
724 { 'B', REQ_VIEW_BLAME },
725 { 'p', REQ_VIEW_PAGER },
726 { 'h', REQ_VIEW_HELP },
727 { 'S', REQ_VIEW_STATUS },
728 { 'c', REQ_VIEW_STAGE },
730 /* View manipulation */
731 { 'q', REQ_VIEW_CLOSE },
732 { KEY_TAB, REQ_VIEW_NEXT },
733 { KEY_RETURN, REQ_ENTER },
734 { KEY_UP, REQ_PREVIOUS },
735 { KEY_DOWN, REQ_NEXT },
736 { 'R', REQ_REFRESH },
737 { 'M', REQ_MAXIMIZE },
739 /* Cursor navigation */
740 { 'k', REQ_MOVE_UP },
741 { 'j', REQ_MOVE_DOWN },
742 { KEY_HOME, REQ_MOVE_FIRST_LINE },
743 { KEY_END, REQ_MOVE_LAST_LINE },
744 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
745 { ' ', REQ_MOVE_PAGE_DOWN },
746 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
747 { 'b', REQ_MOVE_PAGE_UP },
748 { '-', REQ_MOVE_PAGE_UP },
751 { KEY_IC, REQ_SCROLL_LINE_UP },
752 { KEY_DC, REQ_SCROLL_LINE_DOWN },
753 { 'w', REQ_SCROLL_PAGE_UP },
754 { 's', REQ_SCROLL_PAGE_DOWN },
758 { '?', REQ_SEARCH_BACK },
759 { 'n', REQ_FIND_NEXT },
760 { 'N', REQ_FIND_PREV },
764 { 'z', REQ_STOP_LOADING },
765 { 'v', REQ_SHOW_VERSION },
766 { 'r', REQ_SCREEN_REDRAW },
767 { '.', REQ_TOGGLE_LINENO },
768 { 'D', REQ_TOGGLE_DATE },
769 { 'A', REQ_TOGGLE_AUTHOR },
770 { 'g', REQ_TOGGLE_REV_GRAPH },
771 { 'F', REQ_TOGGLE_REFS },
773 { 'u', REQ_STATUS_UPDATE },
774 { 'M', REQ_STATUS_MERGE },
775 { ',', REQ_TREE_PARENT },
778 /* Using the ncurses SIGWINCH handler. */
779 { KEY_RESIZE, REQ_SCREEN_RESIZE },
782 #define KEYMAP_INFO \
796 #define KEYMAP_(name) KEYMAP_##name
801 static struct int_map keymap_table[] = {
802 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
807 #define set_keymap(map, name) \
808 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
810 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
813 add_keybinding(enum keymap keymap, enum request request, int key)
815 struct keybinding *keybinding;
817 keybinding = calloc(1, sizeof(*keybinding));
819 die("Failed to allocate keybinding");
821 keybinding->alias = key;
822 keybinding->request = request;
823 keybinding->next = keybindings[keymap];
824 keybindings[keymap] = keybinding;
827 /* Looks for a key binding first in the given map, then in the generic map, and
828 * lastly in the default keybindings. */
830 get_keybinding(enum keymap keymap, int key)
832 struct keybinding *kbd;
835 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
836 if (kbd->alias == key)
839 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
840 if (kbd->alias == key)
843 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
844 if (default_keybindings[i].alias == key)
845 return default_keybindings[i].request;
847 return (enum request) key;
856 static struct key key_table[] = {
857 { "Enter", KEY_RETURN },
859 { "Backspace", KEY_BACKSPACE },
861 { "Escape", KEY_ESC },
862 { "Left", KEY_LEFT },
863 { "Right", KEY_RIGHT },
865 { "Down", KEY_DOWN },
866 { "Insert", KEY_IC },
867 { "Delete", KEY_DC },
869 { "Home", KEY_HOME },
871 { "PageUp", KEY_PPAGE },
872 { "PageDown", KEY_NPAGE },
882 { "F10", KEY_F(10) },
883 { "F11", KEY_F(11) },
884 { "F12", KEY_F(12) },
888 get_key_value(const char *name)
892 for (i = 0; i < ARRAY_SIZE(key_table); i++)
893 if (!strcasecmp(key_table[i].name, name))
894 return key_table[i].value;
896 if (strlen(name) == 1 && isprint(*name))
903 get_key_name(int key_value)
905 static char key_char[] = "'X'";
909 for (key = 0; key < ARRAY_SIZE(key_table); key++)
910 if (key_table[key].value == key_value)
911 seq = key_table[key].name;
915 isprint(key_value)) {
916 key_char[1] = (char) key_value;
920 return seq ? seq : "'?'";
924 get_key(enum request request)
926 static char buf[BUFSIZ];
933 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
934 struct keybinding *keybinding = &default_keybindings[i];
936 if (keybinding->request != request)
939 if (!string_format_from(buf, &pos, "%s%s", sep,
940 get_key_name(keybinding->alias)))
941 return "Too many keybindings!";
951 char cmd[SIZEOF_STR];
954 static struct run_request *run_request;
955 static size_t run_requests;
958 add_run_request(enum keymap keymap, int key, int argc, char **argv)
960 struct run_request *tmp;
961 struct run_request req = { keymap, key };
964 for (bufpos = 0; argc > 0; argc--, argv++)
965 if (!string_format_from(req.cmd, &bufpos, "%s ", *argv))
968 req.cmd[bufpos - 1] = 0;
970 tmp = realloc(run_request, (run_requests + 1) * sizeof(*run_request));
975 run_request[run_requests++] = req;
977 return REQ_NONE + run_requests;
980 static struct run_request *
981 get_run_request(enum request request)
983 if (request <= REQ_NONE)
985 return &run_request[request - REQ_NONE - 1];
989 add_builtin_run_requests(void)
996 { KEYMAP_MAIN, 'C', { "git cherry-pick %(commit)" } },
997 { KEYMAP_GENERIC, 'G', { "git gc" } },
1001 for (i = 0; i < ARRAY_SIZE(reqs); i++) {
1004 req = add_run_request(reqs[i].keymap, reqs[i].key, 1, reqs[i].argv);
1005 if (req != REQ_NONE)
1006 add_keybinding(reqs[i].keymap, req, reqs[i].key);
1011 * User config file handling.
1014 static struct int_map color_map[] = {
1015 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
1027 #define set_color(color, name) \
1028 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
1030 static struct int_map attr_map[] = {
1031 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
1038 ATTR_MAP(UNDERLINE),
1041 #define set_attribute(attr, name) \
1042 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
1044 static int config_lineno;
1045 static bool config_errors;
1046 static char *config_msg;
1048 /* Wants: object fgcolor bgcolor [attr] */
1050 option_color_command(int argc, char *argv[])
1052 struct line_info *info;
1054 if (argc != 3 && argc != 4) {
1055 config_msg = "Wrong number of arguments given to color command";
1059 info = get_line_info(argv[0]);
1061 if (!string_enum_compare(argv[0], "main-delim", strlen("main-delim"))) {
1062 info = get_line_info("delimiter");
1065 config_msg = "Unknown color name";
1070 if (set_color(&info->fg, argv[1]) == ERR ||
1071 set_color(&info->bg, argv[2]) == ERR) {
1072 config_msg = "Unknown color";
1076 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
1077 config_msg = "Unknown attribute";
1084 static bool parse_bool(const char *s)
1086 return (!strcmp(s, "1") || !strcmp(s, "true") ||
1087 !strcmp(s, "yes")) ? TRUE : FALSE;
1090 /* Wants: name = value */
1092 option_set_command(int argc, char *argv[])
1095 config_msg = "Wrong number of arguments given to set command";
1099 if (strcmp(argv[1], "=")) {
1100 config_msg = "No value assigned";
1104 if (!strcmp(argv[0], "show-author")) {
1105 opt_author = parse_bool(argv[2]);
1109 if (!strcmp(argv[0], "show-date")) {
1110 opt_date = parse_bool(argv[2]);
1114 if (!strcmp(argv[0], "show-rev-graph")) {
1115 opt_rev_graph = parse_bool(argv[2]);
1119 if (!strcmp(argv[0], "show-refs")) {
1120 opt_show_refs = parse_bool(argv[2]);
1124 if (!strcmp(argv[0], "show-line-numbers")) {
1125 opt_line_number = parse_bool(argv[2]);
1129 if (!strcmp(argv[0], "line-number-interval")) {
1130 opt_num_interval = atoi(argv[2]);
1134 if (!strcmp(argv[0], "tab-size")) {
1135 opt_tab_size = atoi(argv[2]);
1139 if (!strcmp(argv[0], "commit-encoding")) {
1140 char *arg = argv[2];
1141 int delimiter = *arg;
1144 switch (delimiter) {
1147 for (arg++, i = 0; arg[i]; i++)
1148 if (arg[i] == delimiter) {
1153 string_ncopy(opt_encoding, arg, strlen(arg));
1158 config_msg = "Unknown variable name";
1162 /* Wants: mode request key */
1164 option_bind_command(int argc, char *argv[])
1166 enum request request;
1171 config_msg = "Wrong number of arguments given to bind command";
1175 if (set_keymap(&keymap, argv[0]) == ERR) {
1176 config_msg = "Unknown key map";
1180 key = get_key_value(argv[1]);
1182 config_msg = "Unknown key";
1186 request = get_request(argv[2]);
1187 if (request == REQ_NONE) {
1188 const char *obsolete[] = { "cherry-pick" };
1189 size_t namelen = strlen(argv[2]);
1192 for (i = 0; i < ARRAY_SIZE(obsolete); i++) {
1193 if (namelen == strlen(obsolete[i]) &&
1194 !string_enum_compare(obsolete[i], argv[2], namelen)) {
1195 config_msg = "Obsolete request name";
1200 if (request == REQ_NONE && *argv[2]++ == '!')
1201 request = add_run_request(keymap, key, argc - 2, argv + 2);
1202 if (request == REQ_NONE) {
1203 config_msg = "Unknown request name";
1207 add_keybinding(keymap, request, key);
1213 set_option(char *opt, char *value)
1220 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1221 argv[argc++] = value;
1224 /* Nothing more to tokenize or last available token. */
1225 if (!*value || argc >= ARRAY_SIZE(argv))
1229 while (isspace(*value))
1233 if (!strcmp(opt, "color"))
1234 return option_color_command(argc, argv);
1236 if (!strcmp(opt, "set"))
1237 return option_set_command(argc, argv);
1239 if (!strcmp(opt, "bind"))
1240 return option_bind_command(argc, argv);
1242 config_msg = "Unknown option command";
1247 read_option(char *opt, size_t optlen, char *value, size_t valuelen)
1252 config_msg = "Internal error";
1254 /* Check for comment markers, since read_properties() will
1255 * only ensure opt and value are split at first " \t". */
1256 optlen = strcspn(opt, "#");
1260 if (opt[optlen] != 0) {
1261 config_msg = "No option value";
1265 /* Look for comment endings in the value. */
1266 size_t len = strcspn(value, "#");
1268 if (len < valuelen) {
1270 value[valuelen] = 0;
1273 status = set_option(opt, value);
1276 if (status == ERR) {
1277 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1278 config_lineno, (int) optlen, opt, config_msg);
1279 config_errors = TRUE;
1282 /* Always keep going if errors are encountered. */
1287 load_option_file(const char *path)
1291 /* It's ok that the file doesn't exist. */
1292 file = fopen(path, "r");
1297 config_errors = FALSE;
1299 if (read_properties(file, " \t", read_option) == ERR ||
1300 config_errors == TRUE)
1301 fprintf(stderr, "Errors while loading %s.\n", path);
1307 char *home = getenv("HOME");
1308 char *tigrc_user = getenv("TIGRC_USER");
1309 char *tigrc_system = getenv("TIGRC_SYSTEM");
1310 char buf[SIZEOF_STR];
1312 add_builtin_run_requests();
1314 if (!tigrc_system) {
1315 if (!string_format(buf, "%s/tigrc", SYSCONFDIR))
1319 load_option_file(tigrc_system);
1322 if (!home || !string_format(buf, "%s/.tigrc", home))
1326 load_option_file(tigrc_user);
1339 /* The display array of active views and the index of the current view. */
1340 static struct view *display[2];
1341 static unsigned int current_view;
1343 /* Reading from the prompt? */
1344 static bool input_mode = FALSE;
1346 #define foreach_displayed_view(view, i) \
1347 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1349 #define displayed_views() (display[1] != NULL ? 2 : 1)
1351 /* Current head and commit ID */
1352 static char ref_blob[SIZEOF_REF] = "";
1353 static char ref_commit[SIZEOF_REF] = "HEAD";
1354 static char ref_head[SIZEOF_REF] = "HEAD";
1357 const char *name; /* View name */
1358 const char *cmd_fmt; /* Default command line format */
1359 const char *cmd_env; /* Command line set via environment */
1360 const char *id; /* Points to either of ref_{head,commit,blob} */
1362 struct view_ops *ops; /* View operations */
1364 enum keymap keymap; /* What keymap does this view have */
1365 bool git_dir; /* Whether the view requires a git directory. */
1367 char cmd[SIZEOF_STR]; /* Command buffer */
1368 char ref[SIZEOF_REF]; /* Hovered commit reference */
1369 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1371 int height, width; /* The width and height of the main window */
1372 WINDOW *win; /* The main window */
1373 WINDOW *title; /* The title window living below the main window */
1376 unsigned long offset; /* Offset of the window top */
1377 unsigned long lineno; /* Current line number */
1380 char grep[SIZEOF_STR]; /* Search string */
1381 regex_t *regex; /* Pre-compiled regex */
1383 /* If non-NULL, points to the view that opened this view. If this view
1384 * is closed tig will switch back to the parent view. */
1385 struct view *parent;
1388 size_t lines; /* Total number of lines */
1389 struct line *line; /* Line index */
1390 size_t line_alloc; /* Total number of allocated lines */
1391 size_t line_size; /* Total number of used lines */
1392 unsigned int digits; /* Number of digits in the lines member. */
1400 /* What type of content being displayed. Used in the title bar. */
1402 /* Open and reads in all view content. */
1403 bool (*open)(struct view *view);
1404 /* Read one line; updates view->line. */
1405 bool (*read)(struct view *view, char *data);
1406 /* Draw one line; @lineno must be < view->height. */
1407 bool (*draw)(struct view *view, struct line *line, unsigned int lineno, bool selected);
1408 /* Depending on view handle a special requests. */
1409 enum request (*request)(struct view *view, enum request request, struct line *line);
1410 /* Search for regex in a line. */
1411 bool (*grep)(struct view *view, struct line *line);
1413 void (*select)(struct view *view, struct line *line);
1416 static struct view_ops pager_ops;
1417 static struct view_ops main_ops;
1418 static struct view_ops tree_ops;
1419 static struct view_ops blob_ops;
1420 static struct view_ops blame_ops;
1421 static struct view_ops help_ops;
1422 static struct view_ops status_ops;
1423 static struct view_ops stage_ops;
1425 #define VIEW_STR(name, cmd, env, ref, ops, map, git) \
1426 { name, cmd, #env, ref, ops, map, git }
1428 #define VIEW_(id, name, ops, git, ref) \
1429 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id, git)
1432 static struct view views[] = {
1433 VIEW_(MAIN, "main", &main_ops, TRUE, ref_head),
1434 VIEW_(DIFF, "diff", &pager_ops, TRUE, ref_commit),
1435 VIEW_(LOG, "log", &pager_ops, TRUE, ref_head),
1436 VIEW_(TREE, "tree", &tree_ops, TRUE, ref_commit),
1437 VIEW_(BLOB, "blob", &blob_ops, TRUE, ref_blob),
1438 VIEW_(BLAME, "blame", &blame_ops, TRUE, ref_commit),
1439 VIEW_(HELP, "help", &help_ops, FALSE, ""),
1440 VIEW_(PAGER, "pager", &pager_ops, FALSE, "stdin"),
1441 VIEW_(STATUS, "status", &status_ops, TRUE, ""),
1442 VIEW_(STAGE, "stage", &stage_ops, TRUE, ""),
1445 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1446 #define VIEW_REQ(view) ((view) - views + REQ_OFFSET + 1)
1448 #define foreach_view(view, i) \
1449 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1451 #define view_is_displayed(view) \
1452 (view == display[0] || view == display[1])
1455 draw_text(struct view *view, const char *string, int max_len,
1456 bool use_tilde, bool selected)
1459 int trimmed = FALSE;
1465 len = utf8_length(string, max_len, &trimmed, use_tilde);
1467 len = strlen(string);
1468 if (len > max_len) {
1477 waddnstr(view->win, string, len);
1478 if (trimmed && use_tilde) {
1480 wattrset(view->win, get_line_attr(LINE_DELIMITER));
1481 waddch(view->win, '~');
1489 draw_lineno(struct view *view, unsigned int lineno, int max, bool selected)
1491 static char fmt[] = "%1ld";
1492 char number[10] = " ";
1493 int max_number = MIN(view->digits, STRING_SIZE(number));
1494 bool showtrimmed = FALSE;
1497 lineno += view->offset + 1;
1498 if (lineno == 1 || (lineno % opt_num_interval) == 0) {
1499 if (view->digits <= 9)
1500 fmt[1] = '0' + view->digits;
1502 if (!string_format(number, fmt, lineno))
1507 if (max < max_number)
1510 col = draw_text(view, number, max_number, showtrimmed, selected);
1513 wattrset(view->win, A_NORMAL);
1514 waddch(view->win, ACS_VLINE);
1518 waddch(view->win, ' ');
1526 draw_view_line(struct view *view, unsigned int lineno)
1529 bool selected = (view->offset + lineno == view->lineno);
1532 assert(view_is_displayed(view));
1534 if (view->offset + lineno >= view->lines)
1537 line = &view->line[view->offset + lineno];
1540 line->selected = TRUE;
1541 view->ops->select(view, line);
1542 } else if (line->selected) {
1543 line->selected = FALSE;
1544 wmove(view->win, lineno, 0);
1545 wclrtoeol(view->win);
1548 scrollok(view->win, FALSE);
1549 draw_ok = view->ops->draw(view, line, lineno, selected);
1550 scrollok(view->win, TRUE);
1556 redraw_view_dirty(struct view *view)
1561 for (lineno = 0; lineno < view->height; lineno++) {
1562 struct line *line = &view->line[view->offset + lineno];
1568 if (!draw_view_line(view, lineno))
1574 redrawwin(view->win);
1576 wnoutrefresh(view->win);
1578 wrefresh(view->win);
1582 redraw_view_from(struct view *view, int lineno)
1584 assert(0 <= lineno && lineno < view->height);
1586 for (; lineno < view->height; lineno++) {
1587 if (!draw_view_line(view, lineno))
1591 redrawwin(view->win);
1593 wnoutrefresh(view->win);
1595 wrefresh(view->win);
1599 redraw_view(struct view *view)
1602 redraw_view_from(view, 0);
1607 update_view_title(struct view *view)
1609 char buf[SIZEOF_STR];
1610 char state[SIZEOF_STR];
1611 size_t bufpos = 0, statelen = 0;
1613 assert(view_is_displayed(view));
1615 if (view != VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) {
1616 unsigned int view_lines = view->offset + view->height;
1617 unsigned int lines = view->lines
1618 ? MIN(view_lines, view->lines) * 100 / view->lines
1621 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1628 time_t secs = time(NULL) - view->start_time;
1630 /* Three git seconds are a long time ... */
1632 string_format_from(state, &statelen, " %lds", secs);
1636 string_format_from(buf, &bufpos, "[%s]", view->name);
1637 if (*view->ref && bufpos < view->width) {
1638 size_t refsize = strlen(view->ref);
1639 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1641 if (minsize < view->width)
1642 refsize = view->width - minsize + 7;
1643 string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
1646 if (statelen && bufpos < view->width) {
1647 string_format_from(buf, &bufpos, " %s", state);
1650 if (view == display[current_view])
1651 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1653 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1655 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1656 wclrtoeol(view->title);
1657 wmove(view->title, 0, view->width - 1);
1660 wnoutrefresh(view->title);
1662 wrefresh(view->title);
1666 resize_display(void)
1669 struct view *base = display[0];
1670 struct view *view = display[1] ? display[1] : display[0];
1672 /* Setup window dimensions */
1674 getmaxyx(stdscr, base->height, base->width);
1676 /* Make room for the status window. */
1680 /* Horizontal split. */
1681 view->width = base->width;
1682 view->height = SCALE_SPLIT_VIEW(base->height);
1683 base->height -= view->height;
1685 /* Make room for the title bar. */
1689 /* Make room for the title bar. */
1694 foreach_displayed_view (view, i) {
1696 view->win = newwin(view->height, 0, offset, 0);
1698 die("Failed to create %s view", view->name);
1700 scrollok(view->win, TRUE);
1702 view->title = newwin(1, 0, offset + view->height, 0);
1704 die("Failed to create title window");
1707 wresize(view->win, view->height, view->width);
1708 mvwin(view->win, offset, 0);
1709 mvwin(view->title, offset + view->height, 0);
1712 offset += view->height + 1;
1717 redraw_display(void)
1722 foreach_displayed_view (view, i) {
1724 update_view_title(view);
1729 update_display_cursor(struct view *view)
1731 /* Move the cursor to the right-most column of the cursor line.
1733 * XXX: This could turn out to be a bit expensive, but it ensures that
1734 * the cursor does not jump around. */
1736 wmove(view->win, view->lineno - view->offset, view->width - 1);
1737 wrefresh(view->win);
1745 /* Scrolling backend */
1747 do_scroll_view(struct view *view, int lines)
1749 bool redraw_current_line = FALSE;
1751 /* The rendering expects the new offset. */
1752 view->offset += lines;
1754 assert(0 <= view->offset && view->offset < view->lines);
1757 /* Move current line into the view. */
1758 if (view->lineno < view->offset) {
1759 view->lineno = view->offset;
1760 redraw_current_line = TRUE;
1761 } else if (view->lineno >= view->offset + view->height) {
1762 view->lineno = view->offset + view->height - 1;
1763 redraw_current_line = TRUE;
1766 assert(view->offset <= view->lineno && view->lineno < view->lines);
1768 /* Redraw the whole screen if scrolling is pointless. */
1769 if (view->height < ABS(lines)) {
1773 int line = lines > 0 ? view->height - lines : 0;
1774 int end = line + ABS(lines);
1776 wscrl(view->win, lines);
1778 for (; line < end; line++) {
1779 if (!draw_view_line(view, line))
1783 if (redraw_current_line)
1784 draw_view_line(view, view->lineno - view->offset);
1787 redrawwin(view->win);
1788 wrefresh(view->win);
1792 /* Scroll frontend */
1794 scroll_view(struct view *view, enum request request)
1798 assert(view_is_displayed(view));
1801 case REQ_SCROLL_PAGE_DOWN:
1802 lines = view->height;
1803 case REQ_SCROLL_LINE_DOWN:
1804 if (view->offset + lines > view->lines)
1805 lines = view->lines - view->offset;
1807 if (lines == 0 || view->offset + view->height >= view->lines) {
1808 report("Cannot scroll beyond the last line");
1813 case REQ_SCROLL_PAGE_UP:
1814 lines = view->height;
1815 case REQ_SCROLL_LINE_UP:
1816 if (lines > view->offset)
1817 lines = view->offset;
1820 report("Cannot scroll beyond the first line");
1828 die("request %d not handled in switch", request);
1831 do_scroll_view(view, lines);
1836 move_view(struct view *view, enum request request)
1838 int scroll_steps = 0;
1842 case REQ_MOVE_FIRST_LINE:
1843 steps = -view->lineno;
1846 case REQ_MOVE_LAST_LINE:
1847 steps = view->lines - view->lineno - 1;
1850 case REQ_MOVE_PAGE_UP:
1851 steps = view->height > view->lineno
1852 ? -view->lineno : -view->height;
1855 case REQ_MOVE_PAGE_DOWN:
1856 steps = view->lineno + view->height >= view->lines
1857 ? view->lines - view->lineno - 1 : view->height;
1869 die("request %d not handled in switch", request);
1872 if (steps <= 0 && view->lineno == 0) {
1873 report("Cannot move beyond the first line");
1876 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1877 report("Cannot move beyond the last line");
1881 /* Move the current line */
1882 view->lineno += steps;
1883 assert(0 <= view->lineno && view->lineno < view->lines);
1885 /* Check whether the view needs to be scrolled */
1886 if (view->lineno < view->offset ||
1887 view->lineno >= view->offset + view->height) {
1888 scroll_steps = steps;
1889 if (steps < 0 && -steps > view->offset) {
1890 scroll_steps = -view->offset;
1892 } else if (steps > 0) {
1893 if (view->lineno == view->lines - 1 &&
1894 view->lines > view->height) {
1895 scroll_steps = view->lines - view->offset - 1;
1896 if (scroll_steps >= view->height)
1897 scroll_steps -= view->height - 1;
1902 if (!view_is_displayed(view)) {
1903 view->offset += scroll_steps;
1904 assert(0 <= view->offset && view->offset < view->lines);
1905 view->ops->select(view, &view->line[view->lineno]);
1909 /* Repaint the old "current" line if we be scrolling */
1910 if (ABS(steps) < view->height)
1911 draw_view_line(view, view->lineno - steps - view->offset);
1914 do_scroll_view(view, scroll_steps);
1918 /* Draw the current line */
1919 draw_view_line(view, view->lineno - view->offset);
1921 redrawwin(view->win);
1922 wrefresh(view->win);
1931 static void search_view(struct view *view, enum request request);
1934 find_next_line(struct view *view, unsigned long lineno, struct line *line)
1936 assert(view_is_displayed(view));
1938 if (!view->ops->grep(view, line))
1941 if (lineno - view->offset >= view->height) {
1942 view->offset = lineno;
1943 view->lineno = lineno;
1947 unsigned long old_lineno = view->lineno - view->offset;
1949 view->lineno = lineno;
1950 draw_view_line(view, old_lineno);
1952 draw_view_line(view, view->lineno - view->offset);
1953 redrawwin(view->win);
1954 wrefresh(view->win);
1957 report("Line %ld matches '%s'", lineno + 1, view->grep);
1962 find_next(struct view *view, enum request request)
1964 unsigned long lineno = view->lineno;
1969 report("No previous search");
1971 search_view(view, request);
1981 case REQ_SEARCH_BACK:
1990 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
1991 lineno += direction;
1993 /* Note, lineno is unsigned long so will wrap around in which case it
1994 * will become bigger than view->lines. */
1995 for (; lineno < view->lines; lineno += direction) {
1996 struct line *line = &view->line[lineno];
1998 if (find_next_line(view, lineno, line))
2002 report("No match found for '%s'", view->grep);
2006 search_view(struct view *view, enum request request)
2011 regfree(view->regex);
2014 view->regex = calloc(1, sizeof(*view->regex));
2019 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
2020 if (regex_err != 0) {
2021 char buf[SIZEOF_STR] = "unknown error";
2023 regerror(regex_err, view->regex, buf, sizeof(buf));
2024 report("Search failed: %s", buf);
2028 string_copy(view->grep, opt_search);
2030 find_next(view, request);
2034 * Incremental updating
2038 end_update(struct view *view)
2042 set_nonblocking_input(FALSE);
2043 if (view->pipe == stdin)
2051 begin_update(struct view *view)
2057 string_copy(view->cmd, opt_cmd);
2059 /* When running random commands, initially show the
2060 * command in the title. However, it maybe later be
2061 * overwritten if a commit line is selected. */
2062 if (view == VIEW(REQ_VIEW_PAGER))
2063 string_copy(view->ref, view->cmd);
2067 } else if (view == VIEW(REQ_VIEW_TREE)) {
2068 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2069 char path[SIZEOF_STR];
2071 if (strcmp(view->vid, view->id))
2072 opt_path[0] = path[0] = 0;
2073 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
2076 if (!string_format(view->cmd, format, view->id, path))
2080 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2081 const char *id = view->id;
2083 if (!string_format(view->cmd, format, id, id, id, id, id))
2086 /* Put the current ref_* value to the view title ref
2087 * member. This is needed by the blob view. Most other
2088 * views sets it automatically after loading because the
2089 * first line is a commit line. */
2090 string_copy_rev(view->ref, view->id);
2093 /* Special case for the pager view. */
2095 view->pipe = opt_pipe;
2098 view->pipe = popen(view->cmd, "r");
2104 set_nonblocking_input(TRUE);
2109 string_copy_rev(view->vid, view->id);
2114 for (i = 0; i < view->lines; i++)
2115 if (view->line[i].data)
2116 free(view->line[i].data);
2122 view->start_time = time(NULL);
2127 #define ITEM_CHUNK_SIZE 256
2129 realloc_items(void *mem, size_t *size, size_t new_size, size_t item_size)
2131 size_t num_chunks = *size / ITEM_CHUNK_SIZE;
2132 size_t num_chunks_new = (new_size + ITEM_CHUNK_SIZE - 1) / ITEM_CHUNK_SIZE;
2134 if (mem == NULL || num_chunks != num_chunks_new) {
2135 *size = num_chunks_new * ITEM_CHUNK_SIZE;
2136 mem = realloc(mem, *size * item_size);
2142 static struct line *
2143 realloc_lines(struct view *view, size_t line_size)
2145 size_t alloc = view->line_alloc;
2146 struct line *tmp = realloc_items(view->line, &alloc, line_size,
2147 sizeof(*view->line));
2153 view->line_alloc = alloc;
2154 view->line_size = line_size;
2159 update_view(struct view *view)
2161 char in_buffer[BUFSIZ];
2162 char out_buffer[BUFSIZ * 2];
2164 /* The number of lines to read. If too low it will cause too much
2165 * redrawing (and possible flickering), if too high responsiveness
2167 unsigned long lines = view->height;
2168 int redraw_from = -1;
2173 /* Only redraw if lines are visible. */
2174 if (view->offset + view->height >= view->lines)
2175 redraw_from = view->lines - view->offset;
2177 /* FIXME: This is probably not perfect for backgrounded views. */
2178 if (!realloc_lines(view, view->lines + lines))
2181 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
2182 size_t linelen = strlen(line);
2185 line[linelen - 1] = 0;
2187 if (opt_iconv != ICONV_NONE) {
2188 ICONV_CONST char *inbuf = line;
2189 size_t inlen = linelen;
2191 char *outbuf = out_buffer;
2192 size_t outlen = sizeof(out_buffer);
2196 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
2197 if (ret != (size_t) -1) {
2199 linelen = strlen(out_buffer);
2203 if (!view->ops->read(view, line))
2213 lines = view->lines;
2214 for (digits = 0; lines; digits++)
2217 /* Keep the displayed view in sync with line number scaling. */
2218 if (digits != view->digits) {
2219 view->digits = digits;
2224 if (!view_is_displayed(view))
2227 if (view == VIEW(REQ_VIEW_TREE)) {
2228 /* Clear the view and redraw everything since the tree sorting
2229 * might have rearranged things. */
2232 } else if (redraw_from >= 0) {
2233 /* If this is an incremental update, redraw the previous line
2234 * since for commits some members could have changed when
2235 * loading the main view. */
2236 if (redraw_from > 0)
2239 /* Since revision graph visualization requires knowledge
2240 * about the parent commit, it causes a further one-off
2241 * needed to be redrawn for incremental updates. */
2242 if (redraw_from > 0 && opt_rev_graph)
2245 /* Incrementally draw avoids flickering. */
2246 redraw_view_from(view, redraw_from);
2249 if (view == VIEW(REQ_VIEW_BLAME))
2250 redraw_view_dirty(view);
2252 /* Update the title _after_ the redraw so that if the redraw picks up a
2253 * commit reference in view->ref it'll be available here. */
2254 update_view_title(view);
2257 if (ferror(view->pipe)) {
2258 report("Failed to read: %s", strerror(errno));
2261 } else if (feof(view->pipe)) {
2269 report("Allocation failure");
2272 if (view->ops->read(view, NULL))
2277 static struct line *
2278 add_line_data(struct view *view, void *data, enum line_type type)
2280 struct line *line = &view->line[view->lines++];
2282 memset(line, 0, sizeof(*line));
2289 static struct line *
2290 add_line_text(struct view *view, char *data, enum line_type type)
2293 data = strdup(data);
2295 return data ? add_line_data(view, data, type) : NULL;
2304 OPEN_DEFAULT = 0, /* Use default view switching. */
2305 OPEN_SPLIT = 1, /* Split current view. */
2306 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2307 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2311 open_view(struct view *prev, enum request request, enum open_flags flags)
2313 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2314 bool split = !!(flags & OPEN_SPLIT);
2315 bool reload = !!(flags & OPEN_RELOAD);
2316 struct view *view = VIEW(request);
2317 int nviews = displayed_views();
2318 struct view *base_view = display[0];
2320 if (view == prev && nviews == 1 && !reload) {
2321 report("Already in %s view", view->name);
2325 if (view->git_dir && !opt_git_dir[0]) {
2326 report("The %s view is disabled in pager view", view->name);
2335 /* Maximize the current view. */
2336 memset(display, 0, sizeof(display));
2338 display[current_view] = view;
2341 /* Resize the view when switching between split- and full-screen,
2342 * or when switching between two different full-screen views. */
2343 if (nviews != displayed_views() ||
2344 (nviews == 1 && base_view != display[0]))
2347 if (view->ops->open) {
2348 if (!view->ops->open(view)) {
2349 report("Failed to load %s view", view->name);
2353 } else if ((reload || strcmp(view->vid, view->id)) &&
2354 !begin_update(view)) {
2355 report("Failed to load %s view", view->name);
2359 if (split && prev->lineno - prev->offset >= prev->height) {
2360 /* Take the title line into account. */
2361 int lines = prev->lineno - prev->offset - prev->height + 1;
2363 /* Scroll the view that was split if the current line is
2364 * outside the new limited view. */
2365 do_scroll_view(prev, lines);
2368 if (prev && view != prev) {
2369 if (split && !backgrounded) {
2370 /* "Blur" the previous view. */
2371 update_view_title(prev);
2374 view->parent = prev;
2377 if (view->pipe && view->lines == 0) {
2378 /* Clear the old view and let the incremental updating refill
2387 /* If the view is backgrounded the above calls to report()
2388 * won't redraw the view title. */
2390 update_view_title(view);
2394 open_external_viewer(const char *cmd)
2396 def_prog_mode(); /* save current tty modes */
2397 endwin(); /* restore original tty modes */
2399 fprintf(stderr, "Press Enter to continue");
2406 open_mergetool(const char *file)
2408 char cmd[SIZEOF_STR];
2409 char file_sq[SIZEOF_STR];
2411 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2412 string_format(cmd, "git mergetool %s", file_sq)) {
2413 open_external_viewer(cmd);
2418 open_editor(bool from_root, const char *file)
2420 char cmd[SIZEOF_STR];
2421 char file_sq[SIZEOF_STR];
2423 char *prefix = from_root ? opt_cdup : "";
2425 editor = getenv("GIT_EDITOR");
2426 if (!editor && *opt_editor)
2427 editor = opt_editor;
2429 editor = getenv("VISUAL");
2431 editor = getenv("EDITOR");
2435 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2436 string_format(cmd, "%s %s%s", editor, prefix, file_sq)) {
2437 open_external_viewer(cmd);
2442 open_run_request(enum request request)
2444 struct run_request *req = get_run_request(request);
2445 char buf[SIZEOF_STR * 2];
2450 report("Unknown run request");
2458 char *next = strstr(cmd, "%(");
2459 int len = next - cmd;
2466 } else if (!strncmp(next, "%(head)", 7)) {
2469 } else if (!strncmp(next, "%(commit)", 9)) {
2472 } else if (!strncmp(next, "%(blob)", 7)) {
2476 report("Unknown replacement in run request: `%s`", req->cmd);
2480 if (!string_format_from(buf, &bufpos, "%.*s%s", len, cmd, value))
2484 next = strchr(next, ')') + 1;
2488 open_external_viewer(buf);
2492 * User request switch noodle
2496 view_driver(struct view *view, enum request request)
2500 if (request == REQ_NONE) {
2505 if (request > REQ_NONE) {
2506 open_run_request(request);
2510 if (view && view->lines) {
2511 request = view->ops->request(view, request, &view->line[view->lineno]);
2512 if (request == REQ_NONE)
2519 case REQ_MOVE_PAGE_UP:
2520 case REQ_MOVE_PAGE_DOWN:
2521 case REQ_MOVE_FIRST_LINE:
2522 case REQ_MOVE_LAST_LINE:
2523 move_view(view, request);
2526 case REQ_SCROLL_LINE_DOWN:
2527 case REQ_SCROLL_LINE_UP:
2528 case REQ_SCROLL_PAGE_DOWN:
2529 case REQ_SCROLL_PAGE_UP:
2530 scroll_view(view, request);
2533 case REQ_VIEW_BLAME:
2535 report("No file chosen, press %s to open tree view",
2536 get_key(REQ_VIEW_TREE));
2539 open_view(view, request, OPEN_DEFAULT);
2544 report("No file chosen, press %s to open tree view",
2545 get_key(REQ_VIEW_TREE));
2548 open_view(view, request, OPEN_DEFAULT);
2551 case REQ_VIEW_PAGER:
2552 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2553 report("No pager content, press %s to run command from prompt",
2554 get_key(REQ_PROMPT));
2557 open_view(view, request, OPEN_DEFAULT);
2560 case REQ_VIEW_STAGE:
2561 if (!VIEW(REQ_VIEW_STAGE)->lines) {
2562 report("No stage content, press %s to open the status view and choose file",
2563 get_key(REQ_VIEW_STATUS));
2566 open_view(view, request, OPEN_DEFAULT);
2569 case REQ_VIEW_STATUS:
2570 if (opt_is_inside_work_tree == FALSE) {
2571 report("The status view requires a working tree");
2574 open_view(view, request, OPEN_DEFAULT);
2582 open_view(view, request, OPEN_DEFAULT);
2587 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2589 if ((view == VIEW(REQ_VIEW_DIFF) &&
2590 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2591 (view == VIEW(REQ_VIEW_DIFF) &&
2592 view->parent == VIEW(REQ_VIEW_BLAME)) ||
2593 (view == VIEW(REQ_VIEW_STAGE) &&
2594 view->parent == VIEW(REQ_VIEW_STATUS)) ||
2595 (view == VIEW(REQ_VIEW_BLOB) &&
2596 view->parent == VIEW(REQ_VIEW_TREE))) {
2599 view = view->parent;
2600 line = view->lineno;
2601 move_view(view, request);
2602 if (view_is_displayed(view))
2603 update_view_title(view);
2604 if (line != view->lineno)
2605 view->ops->request(view, REQ_ENTER,
2606 &view->line[view->lineno]);
2609 move_view(view, request);
2615 int nviews = displayed_views();
2616 int next_view = (current_view + 1) % nviews;
2618 if (next_view == current_view) {
2619 report("Only one view is displayed");
2623 current_view = next_view;
2624 /* Blur out the title of the previous view. */
2625 update_view_title(view);
2630 report("Refreshing is not yet supported for the %s view", view->name);
2634 if (displayed_views() == 2)
2635 open_view(view, VIEW_REQ(view), OPEN_DEFAULT);
2638 case REQ_TOGGLE_LINENO:
2639 opt_line_number = !opt_line_number;
2643 case REQ_TOGGLE_DATE:
2644 opt_date = !opt_date;
2648 case REQ_TOGGLE_AUTHOR:
2649 opt_author = !opt_author;
2653 case REQ_TOGGLE_REV_GRAPH:
2654 opt_rev_graph = !opt_rev_graph;
2658 case REQ_TOGGLE_REFS:
2659 opt_show_refs = !opt_show_refs;
2664 /* Always reload^Wrerun commands from the prompt. */
2665 open_view(view, opt_request, OPEN_RELOAD);
2669 case REQ_SEARCH_BACK:
2670 search_view(view, request);
2675 find_next(view, request);
2678 case REQ_STOP_LOADING:
2679 for (i = 0; i < ARRAY_SIZE(views); i++) {
2682 report("Stopped loading the %s view", view->name),
2687 case REQ_SHOW_VERSION:
2688 report("tig-%s (built %s)", TIG_VERSION, __DATE__);
2691 case REQ_SCREEN_RESIZE:
2694 case REQ_SCREEN_REDRAW:
2699 report("Nothing to edit");
2704 report("Nothing to enter");
2708 case REQ_VIEW_CLOSE:
2709 /* XXX: Mark closed views by letting view->parent point to the
2710 * view itself. Parents to closed view should never be
2713 view->parent->parent != view->parent) {
2714 memset(display, 0, sizeof(display));
2716 display[current_view] = view->parent;
2717 view->parent = view;
2727 /* An unknown key will show most commonly used commands. */
2728 report("Unknown key, press 'h' for help");
2741 pager_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2743 static char spaces[] = " ";
2744 char *text = line->data;
2745 enum line_type type = line->type;
2746 int attr = A_NORMAL;
2749 wmove(view->win, lineno, 0);
2753 wchgat(view->win, -1, 0, type, NULL);
2754 attr = get_line_attr(type);
2756 wattrset(view->win, attr);
2758 if (opt_line_number) {
2759 col += draw_lineno(view, lineno, view->width, selected);
2760 if (col >= view->width)
2765 attr = get_line_attr(type);
2766 wattrset(view->win, attr);
2768 if (opt_tab_size < TABSIZE) {
2769 int col_offset = col;
2772 while (text && col_offset + col < view->width) {
2773 int cols_max = view->width - col_offset - col;
2777 if (*text == '\t') {
2779 assert(sizeof(spaces) > TABSIZE);
2781 cols = opt_tab_size - (col % opt_tab_size);
2784 text = strchr(text, '\t');
2785 cols = line ? text - pos : strlen(pos);
2788 waddnstr(view->win, pos, MIN(cols, cols_max));
2793 draw_text(view, text, view->width - col, TRUE, selected);
2800 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2802 char refbuf[SIZEOF_STR];
2806 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2809 pipe = popen(refbuf, "r");
2813 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2814 ref = chomp_string(ref);
2820 /* This is the only fatal call, since it can "corrupt" the buffer. */
2821 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2828 add_pager_refs(struct view *view, struct line *line)
2830 char buf[SIZEOF_STR];
2831 char *commit_id = (char *)line->data + STRING_SIZE("commit ");
2833 size_t bufpos = 0, refpos = 0;
2834 const char *sep = "Refs: ";
2835 bool is_tag = FALSE;
2837 assert(line->type == LINE_COMMIT);
2839 refs = get_refs(commit_id);
2841 if (view == VIEW(REQ_VIEW_DIFF))
2842 goto try_add_describe_ref;
2847 struct ref *ref = refs[refpos];
2848 char *fmt = ref->tag ? "%s[%s]" :
2849 ref->remote ? "%s<%s>" : "%s%s";
2851 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2856 } while (refs[refpos++]->next);
2858 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2859 try_add_describe_ref:
2860 /* Add <tag>-g<commit_id> "fake" reference. */
2861 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2868 if (!realloc_lines(view, view->line_size + 1))
2871 add_line_text(view, buf, LINE_PP_REFS);
2875 pager_read(struct view *view, char *data)
2882 line = add_line_text(view, data, get_line_type(data));
2886 if (line->type == LINE_COMMIT &&
2887 (view == VIEW(REQ_VIEW_DIFF) ||
2888 view == VIEW(REQ_VIEW_LOG)))
2889 add_pager_refs(view, line);
2895 pager_request(struct view *view, enum request request, struct line *line)
2899 if (request != REQ_ENTER)
2902 if (line->type == LINE_COMMIT &&
2903 (view == VIEW(REQ_VIEW_LOG) ||
2904 view == VIEW(REQ_VIEW_PAGER))) {
2905 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2909 /* Always scroll the view even if it was split. That way
2910 * you can use Enter to scroll through the log view and
2911 * split open each commit diff. */
2912 scroll_view(view, REQ_SCROLL_LINE_DOWN);
2914 /* FIXME: A minor workaround. Scrolling the view will call report("")
2915 * but if we are scrolling a non-current view this won't properly
2916 * update the view title. */
2918 update_view_title(view);
2924 pager_grep(struct view *view, struct line *line)
2927 char *text = line->data;
2932 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
2939 pager_select(struct view *view, struct line *line)
2941 if (line->type == LINE_COMMIT) {
2942 char *text = (char *)line->data + STRING_SIZE("commit ");
2944 if (view != VIEW(REQ_VIEW_PAGER))
2945 string_copy_rev(view->ref, text);
2946 string_copy_rev(ref_commit, text);
2950 static struct view_ops pager_ops = {
2966 help_open(struct view *view)
2969 int lines = ARRAY_SIZE(req_info) + 2;
2972 if (view->lines > 0)
2975 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2976 if (!req_info[i].request)
2979 lines += run_requests + 1;
2981 view->line = calloc(lines, sizeof(*view->line));
2985 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
2987 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2990 if (req_info[i].request == REQ_NONE)
2993 if (!req_info[i].request) {
2994 add_line_text(view, "", LINE_DEFAULT);
2995 add_line_text(view, req_info[i].help, LINE_DEFAULT);
2999 key = get_key(req_info[i].request);
3001 key = "(no key defined)";
3003 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
3006 add_line_text(view, buf, LINE_DEFAULT);
3010 add_line_text(view, "", LINE_DEFAULT);
3011 add_line_text(view, "External commands:", LINE_DEFAULT);
3014 for (i = 0; i < run_requests; i++) {
3015 struct run_request *req = get_run_request(REQ_NONE + i + 1);
3021 key = get_key_name(req->key);
3023 key = "(no key defined)";
3025 if (!string_format(buf, " %-10s %-14s `%s`",
3026 keymap_table[req->keymap].name,
3030 add_line_text(view, buf, LINE_DEFAULT);
3036 static struct view_ops help_ops = {
3051 struct tree_stack_entry {
3052 struct tree_stack_entry *prev; /* Entry below this in the stack */
3053 unsigned long lineno; /* Line number to restore */
3054 char *name; /* Position of name in opt_path */
3057 /* The top of the path stack. */
3058 static struct tree_stack_entry *tree_stack = NULL;
3059 unsigned long tree_lineno = 0;
3062 pop_tree_stack_entry(void)
3064 struct tree_stack_entry *entry = tree_stack;
3066 tree_lineno = entry->lineno;
3068 tree_stack = entry->prev;
3073 push_tree_stack_entry(char *name, unsigned long lineno)
3075 struct tree_stack_entry *entry = calloc(1, sizeof(*entry));
3076 size_t pathlen = strlen(opt_path);
3081 entry->prev = tree_stack;
3082 entry->name = opt_path + pathlen;
3085 if (!string_format_from(opt_path, &pathlen, "%s/", name)) {
3086 pop_tree_stack_entry();
3090 /* Move the current line to the first tree entry. */
3092 entry->lineno = lineno;
3095 /* Parse output from git-ls-tree(1):
3097 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
3098 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
3099 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
3100 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
3103 #define SIZEOF_TREE_ATTR \
3104 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
3106 #define TREE_UP_FORMAT "040000 tree %s\t.."
3109 tree_compare_entry(enum line_type type1, char *name1,
3110 enum line_type type2, char *name2)
3112 if (type1 != type2) {
3113 if (type1 == LINE_TREE_DIR)
3118 return strcmp(name1, name2);
3122 tree_path(struct line *line)
3124 char *path = line->data;
3126 return path + SIZEOF_TREE_ATTR;
3130 tree_read(struct view *view, char *text)
3132 size_t textlen = text ? strlen(text) : 0;
3133 char buf[SIZEOF_STR];
3135 enum line_type type;
3136 bool first_read = view->lines == 0;
3140 if (textlen <= SIZEOF_TREE_ATTR)
3143 type = text[STRING_SIZE("100644 ")] == 't'
3144 ? LINE_TREE_DIR : LINE_TREE_FILE;
3147 /* Add path info line */
3148 if (!string_format(buf, "Directory path /%s", opt_path) ||
3149 !realloc_lines(view, view->line_size + 1) ||
3150 !add_line_text(view, buf, LINE_DEFAULT))
3153 /* Insert "link" to parent directory. */
3155 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
3156 !realloc_lines(view, view->line_size + 1) ||
3157 !add_line_text(view, buf, LINE_TREE_DIR))
3162 /* Strip the path part ... */
3164 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
3165 size_t striplen = strlen(opt_path);
3166 char *path = text + SIZEOF_TREE_ATTR;
3168 if (pathlen > striplen)
3169 memmove(path, path + striplen,
3170 pathlen - striplen + 1);
3173 /* Skip "Directory ..." and ".." line. */
3174 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
3175 struct line *line = &view->line[pos];
3176 char *path1 = tree_path(line);
3177 char *path2 = text + SIZEOF_TREE_ATTR;
3178 int cmp = tree_compare_entry(line->type, path1, type, path2);
3183 text = strdup(text);
3187 if (view->lines > pos)
3188 memmove(&view->line[pos + 1], &view->line[pos],
3189 (view->lines - pos) * sizeof(*line));
3191 line = &view->line[pos];
3198 if (!add_line_text(view, text, type))
3201 if (tree_lineno > view->lineno) {
3202 view->lineno = tree_lineno;
3210 tree_request(struct view *view, enum request request, struct line *line)
3212 enum open_flags flags;
3214 if (request == REQ_VIEW_BLAME) {
3215 char *filename = tree_path(line);
3217 if (line->type == LINE_TREE_DIR) {
3218 report("Cannot show blame for directory %s", opt_path);
3222 string_copy(opt_ref, view->vid);
3223 string_format(opt_file, "%s%s", opt_path, filename);
3226 if (request == REQ_TREE_PARENT) {
3229 request = REQ_ENTER;
3230 line = &view->line[1];
3232 /* quit view if at top of tree */
3233 return REQ_VIEW_CLOSE;
3236 if (request != REQ_ENTER)
3239 /* Cleanup the stack if the tree view is at a different tree. */
3240 while (!*opt_path && tree_stack)
3241 pop_tree_stack_entry();
3243 switch (line->type) {
3245 /* Depending on whether it is a subdir or parent (updir?) link
3246 * mangle the path buffer. */
3247 if (line == &view->line[1] && *opt_path) {
3248 pop_tree_stack_entry();
3251 char *basename = tree_path(line);
3253 push_tree_stack_entry(basename, view->lineno);
3256 /* Trees and subtrees share the same ID, so they are not not
3257 * unique like blobs. */
3258 flags = OPEN_RELOAD;
3259 request = REQ_VIEW_TREE;
3262 case LINE_TREE_FILE:
3263 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3264 request = REQ_VIEW_BLOB;
3271 open_view(view, request, flags);
3272 if (request == REQ_VIEW_TREE) {
3273 view->lineno = tree_lineno;
3280 tree_select(struct view *view, struct line *line)
3282 char *text = (char *)line->data + STRING_SIZE("100644 blob ");
3284 if (line->type == LINE_TREE_FILE) {
3285 string_copy_rev(ref_blob, text);
3287 } else if (line->type != LINE_TREE_DIR) {
3291 string_copy_rev(view->ref, text);
3294 static struct view_ops tree_ops = {
3305 blob_read(struct view *view, char *line)
3309 return add_line_text(view, line, LINE_DEFAULT) != NULL;
3312 static struct view_ops blob_ops = {
3325 * Loading the blame view is a two phase job:
3327 * 1. File content is read either using opt_file from the
3328 * filesystem or using git-cat-file.
3329 * 2. Then blame information is incrementally added by
3330 * reading output from git-blame.
3333 struct blame_commit {
3334 char id[SIZEOF_REV]; /* SHA1 ID. */
3335 char title[128]; /* First line of the commit message. */
3336 char author[75]; /* Author of the commit. */
3337 struct tm time; /* Date from the author ident. */
3338 char filename[128]; /* Name of file. */
3342 struct blame_commit *commit;
3343 unsigned int header:1;
3347 #define BLAME_CAT_FILE_CMD "git cat-file blob %s:%s"
3348 #define BLAME_INCREMENTAL_CMD "git blame --incremental %s %s"
3351 blame_open(struct view *view)
3353 char path[SIZEOF_STR];
3354 char ref[SIZEOF_STR] = "";
3356 if (sq_quote(path, 0, opt_file) >= sizeof(path))
3359 if (*opt_ref && sq_quote(ref, 0, opt_ref) >= sizeof(ref))
3363 if (!string_format(view->cmd, BLAME_CAT_FILE_CMD, ref, path))
3366 view->pipe = fopen(opt_file, "r");
3368 !string_format(view->cmd, BLAME_CAT_FILE_CMD, "HEAD", path))
3373 view->pipe = popen(view->cmd, "r");
3377 if (!string_format(view->cmd, BLAME_INCREMENTAL_CMD, ref, path))
3380 string_format(view->ref, "%s ...", opt_file);
3381 string_copy_rev(view->vid, opt_file);
3382 set_nonblocking_input(TRUE);
3387 for (i = 0; i < view->lines; i++)
3388 free(view->line[i].data);
3392 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3393 view->offset = view->lines = view->lineno = 0;
3395 view->start_time = time(NULL);
3400 static struct blame_commit *
3401 get_blame_commit(struct view *view, const char *id)
3405 for (i = 0; i < view->lines; i++) {
3406 struct blame *blame = view->line[i].data;
3411 if (!strncmp(blame->commit->id, id, SIZEOF_REV - 1))
3412 return blame->commit;
3416 struct blame_commit *commit = calloc(1, sizeof(*commit));
3419 string_ncopy(commit->id, id, SIZEOF_REV);
3425 parse_number(char **posref, size_t *number, size_t min, size_t max)
3427 char *pos = *posref;
3430 pos = strchr(pos + 1, ' ');
3431 if (!pos || !isdigit(pos[1]))
3433 *number = atoi(pos + 1);
3434 if (*number < min || *number > max)
3441 static struct blame_commit *
3442 parse_blame_commit(struct view *view, char *text, int *blamed)
3444 struct blame_commit *commit;
3445 struct blame *blame;
3446 char *pos = text + SIZEOF_REV - 1;
3450 if (strlen(text) <= SIZEOF_REV || *pos != ' ')
3453 if (!parse_number(&pos, &lineno, 1, view->lines) ||
3454 !parse_number(&pos, &group, 1, view->lines - lineno + 1))
3457 commit = get_blame_commit(view, text);
3463 struct line *line = &view->line[lineno + group - 1];
3466 blame->commit = commit;
3467 blame->header = !group;
3475 blame_read_file(struct view *view, char *line)
3480 if (view->lines > 0)
3481 pipe = popen(view->cmd, "r");
3484 report("Failed to load blame data");
3493 size_t linelen = strlen(line);
3494 struct blame *blame = malloc(sizeof(*blame) + linelen);
3499 blame->commit = NULL;
3500 strncpy(blame->text, line, linelen);
3501 blame->text[linelen] = 0;
3502 return add_line_data(view, blame, LINE_BLAME_COMMIT) != NULL;
3507 match_blame_header(const char *name, char **line)
3509 size_t namelen = strlen(name);
3510 bool matched = !strncmp(name, *line, namelen);
3519 blame_read(struct view *view, char *line)
3521 static struct blame_commit *commit = NULL;
3522 static int blamed = 0;
3523 static time_t author_time;
3526 return blame_read_file(view, line);
3532 string_format(view->ref, "%s", view->vid);
3533 if (view_is_displayed(view)) {
3534 update_view_title(view);
3535 redraw_view_from(view, 0);
3541 commit = parse_blame_commit(view, line, &blamed);
3542 string_format(view->ref, "%s %2d%%", view->vid,
3543 blamed * 100 / view->lines);
3545 } else if (match_blame_header("author ", &line)) {
3546 string_ncopy(commit->author, line, strlen(line));
3548 } else if (match_blame_header("author-time ", &line)) {
3549 author_time = (time_t) atol(line);
3551 } else if (match_blame_header("author-tz ", &line)) {
3554 tz = ('0' - line[1]) * 60 * 60 * 10;
3555 tz += ('0' - line[2]) * 60 * 60;
3556 tz += ('0' - line[3]) * 60;
3557 tz += ('0' - line[4]) * 60;
3563 gmtime_r(&author_time, &commit->time);
3565 } else if (match_blame_header("summary ", &line)) {
3566 string_ncopy(commit->title, line, strlen(line));
3568 } else if (match_blame_header("filename ", &line)) {
3569 string_ncopy(commit->filename, line, strlen(line));
3577 blame_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
3579 struct blame *blame = line->data;
3582 wmove(view->win, lineno, 0);
3585 wattrset(view->win, get_line_attr(LINE_CURSOR));
3586 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
3588 wattrset(view->win, A_NORMAL);
3595 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
3596 if (blame->commit) {
3597 char buf[DATE_COLS + 1];
3600 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &blame->commit->time);
3601 n = draw_text(view, buf, view->width - col, FALSE, selected);
3602 draw_text(view, " ", view->width - col - n, FALSE, selected);
3606 wmove(view->win, lineno, col);
3607 if (col >= view->width)
3612 int max = MIN(AUTHOR_COLS - 1, view->width - col);
3615 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
3617 draw_text(view, blame->commit->author, max, TRUE, selected);
3619 if (col >= view->width)
3621 wmove(view->win, lineno, col);
3625 int max = MIN(ID_COLS - 1, view->width - col);
3628 wattrset(view->win, get_line_attr(LINE_BLAME_ID));
3630 draw_text(view, blame->commit->id, max, FALSE, -1);
3632 if (col >= view->width)
3634 wmove(view->win, lineno, col);
3639 wattrset(view->win, get_line_attr(LINE_BLAME_LINENO));
3640 col += draw_lineno(view, lineno, view->width - col, selected);
3641 if (col >= view->width)
3645 col += draw_text(view, blame->text, view->width - col, TRUE, selected);
3651 blame_request(struct view *view, enum request request, struct line *line)
3653 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3654 struct blame *blame = line->data;
3658 if (!blame->commit) {
3659 report("No commit loaded yet");
3663 if (!strcmp(blame->commit->id, NULL_ID)) {
3664 char path[SIZEOF_STR];
3666 if (sq_quote(path, 0, view->vid) >= sizeof(path))
3668 string_format(opt_cmd, "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s 2>/dev/null", path);
3671 open_view(view, REQ_VIEW_DIFF, flags);
3682 blame_grep(struct view *view, struct line *line)
3684 struct blame *blame = line->data;
3685 struct blame_commit *commit = blame->commit;
3688 #define MATCH(text) \
3689 (*text && regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3692 char buf[DATE_COLS + 1];
3694 if (MATCH(commit->title) ||
3695 MATCH(commit->author) ||
3699 if (strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time) &&
3704 return MATCH(blame->text);
3710 blame_select(struct view *view, struct line *line)
3712 struct blame *blame = line->data;
3713 struct blame_commit *commit = blame->commit;
3718 if (!strcmp(commit->id, NULL_ID))
3719 string_ncopy(ref_commit, "HEAD", 4);
3721 string_copy_rev(ref_commit, commit->id);
3724 static struct view_ops blame_ops = {
3742 char rev[SIZEOF_REV];
3743 char name[SIZEOF_STR];
3747 char rev[SIZEOF_REV];
3748 char name[SIZEOF_STR];
3752 static char status_onbranch[SIZEOF_STR];
3753 static struct status stage_status;
3754 static enum line_type stage_line_type;
3756 /* Get fields from the diff line:
3757 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
3760 status_get_diff(struct status *file, char *buf, size_t bufsize)
3762 char *old_mode = buf + 1;
3763 char *new_mode = buf + 8;
3764 char *old_rev = buf + 15;
3765 char *new_rev = buf + 56;
3766 char *status = buf + 97;
3769 old_mode[-1] != ':' ||
3770 new_mode[-1] != ' ' ||
3771 old_rev[-1] != ' ' ||
3772 new_rev[-1] != ' ' ||
3776 file->status = *status;
3778 string_copy_rev(file->old.rev, old_rev);
3779 string_copy_rev(file->new.rev, new_rev);
3781 file->old.mode = strtoul(old_mode, NULL, 8);
3782 file->new.mode = strtoul(new_mode, NULL, 8);
3784 file->old.name[0] = file->new.name[0] = 0;
3790 status_run(struct view *view, const char cmd[], char status, enum line_type type)
3792 struct status *file = NULL;
3793 struct status *unmerged = NULL;
3794 char buf[SIZEOF_STR * 4];
3798 pipe = popen(cmd, "r");
3802 add_line_data(view, NULL, type);
3804 while (!feof(pipe) && !ferror(pipe)) {
3808 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
3811 bufsize += readsize;
3813 /* Process while we have NUL chars. */
3814 while ((sep = memchr(buf, 0, bufsize))) {
3815 size_t sepsize = sep - buf + 1;
3818 if (!realloc_lines(view, view->line_size + 1))
3821 file = calloc(1, sizeof(*file));
3825 add_line_data(view, file, type);
3828 /* Parse diff info part. */
3830 file->status = status;
3832 string_copy(file->old.rev, NULL_ID);
3834 } else if (!file->status) {
3835 if (!status_get_diff(file, buf, sepsize))
3839 memmove(buf, sep + 1, bufsize);
3841 sep = memchr(buf, 0, bufsize);
3844 sepsize = sep - buf + 1;
3846 /* Collapse all 'M'odified entries that
3847 * follow a associated 'U'nmerged entry.
3849 if (file->status == 'U') {
3852 } else if (unmerged) {
3853 int collapse = !strcmp(buf, unmerged->new.name);
3864 /* Grab the old name for rename/copy. */
3865 if (!*file->old.name &&
3866 (file->status == 'R' || file->status == 'C')) {
3867 sepsize = sep - buf + 1;
3868 string_ncopy(file->old.name, buf, sepsize);
3870 memmove(buf, sep + 1, bufsize);
3872 sep = memchr(buf, 0, bufsize);
3875 sepsize = sep - buf + 1;
3878 /* git-ls-files just delivers a NUL separated
3879 * list of file names similar to the second half
3880 * of the git-diff-* output. */
3881 string_ncopy(file->new.name, buf, sepsize);
3882 if (!*file->old.name)
3883 string_copy(file->old.name, file->new.name);
3885 memmove(buf, sep + 1, bufsize);
3896 if (!view->line[view->lines - 1].data)
3897 add_line_data(view, NULL, LINE_STAT_NONE);
3903 /* Don't show unmerged entries in the staged section. */
3904 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached -M HEAD"
3905 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
3906 #define STATUS_LIST_OTHER_CMD \
3907 "git ls-files -z --others --exclude-per-directory=.gitignore"
3908 #define STATUS_LIST_NO_HEAD_CMD \
3909 "git ls-files -z --cached --exclude-per-directory=.gitignore"
3911 #define STATUS_DIFF_INDEX_SHOW_CMD \
3912 "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s %s 2>/dev/null"
3914 #define STATUS_DIFF_FILES_SHOW_CMD \
3915 "git diff-files --root --patch-with-stat -C -M -- %s %s 2>/dev/null"
3917 #define STATUS_DIFF_NO_HEAD_SHOW_CMD \
3918 "git diff --no-color --patch-with-stat /dev/null %s 2>/dev/null"
3920 /* First parse staged info using git-diff-index(1), then parse unstaged
3921 * info using git-diff-files(1), and finally untracked files using
3922 * git-ls-files(1). */
3924 status_open(struct view *view)
3926 struct stat statbuf;
3927 char exclude[SIZEOF_STR];
3928 char indexcmd[SIZEOF_STR] = STATUS_DIFF_INDEX_CMD;
3929 char othercmd[SIZEOF_STR] = STATUS_LIST_OTHER_CMD;
3930 unsigned long prev_lineno = view->lineno;
3931 char indexstatus = 0;
3934 for (i = 0; i < view->lines; i++)
3935 free(view->line[i].data);
3937 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3940 if (!realloc_lines(view, view->line_size + 7))
3943 add_line_data(view, NULL, LINE_STAT_HEAD);
3945 string_copy(status_onbranch, "Initial commit");
3946 else if (!*opt_head)
3947 string_copy(status_onbranch, "Not currently on any branch");
3948 else if (!string_format(status_onbranch, "On branch %s", opt_head))
3952 string_copy(indexcmd, STATUS_LIST_NO_HEAD_CMD);
3956 if (!string_format(exclude, "%s/info/exclude", opt_git_dir))
3959 if (stat(exclude, &statbuf) >= 0) {
3960 size_t cmdsize = strlen(othercmd);
3962 if (!string_format_from(othercmd, &cmdsize, " %s", "--exclude-from=") ||
3963 sq_quote(othercmd, cmdsize, exclude) >= sizeof(othercmd))
3966 cmdsize = strlen(indexcmd);
3968 (!string_format_from(indexcmd, &cmdsize, " %s", "--exclude-from=") ||
3969 sq_quote(indexcmd, cmdsize, exclude) >= sizeof(indexcmd)))
3973 system("git update-index -q --refresh");
3975 if (!status_run(view, indexcmd, indexstatus, LINE_STAT_STAGED) ||
3976 !status_run(view, STATUS_DIFF_FILES_CMD, 0, LINE_STAT_UNSTAGED) ||
3977 !status_run(view, othercmd, '?', LINE_STAT_UNTRACKED))
3980 /* If all went well restore the previous line number to stay in
3981 * the context or select a line with something that can be
3983 if (prev_lineno >= view->lines)
3984 prev_lineno = view->lines - 1;
3985 while (prev_lineno < view->lines && !view->line[prev_lineno].data)
3987 while (prev_lineno > 0 && !view->line[prev_lineno].data)
3990 /* If the above fails, always skip the "On branch" line. */
3991 if (prev_lineno < view->lines)
3992 view->lineno = prev_lineno;
3996 if (view->lineno < view->offset)
3997 view->offset = view->lineno;
3998 else if (view->offset + view->height <= view->lineno)
3999 view->offset = view->lineno - view->height + 1;
4005 status_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
4007 struct status *status = line->data;
4011 wmove(view->win, lineno, 0);
4014 wattrset(view->win, get_line_attr(LINE_CURSOR));
4015 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
4017 } else if (line->type == LINE_STAT_HEAD) {
4018 wattrset(view->win, get_line_attr(LINE_STAT_HEAD));
4019 wchgat(view->win, -1, 0, LINE_STAT_HEAD, NULL);
4021 } else if (!status && line->type != LINE_STAT_NONE) {
4022 wattrset(view->win, get_line_attr(LINE_STAT_SECTION));
4023 wchgat(view->win, -1, 0, LINE_STAT_SECTION, NULL);
4026 wattrset(view->win, get_line_attr(line->type));
4030 switch (line->type) {
4031 case LINE_STAT_STAGED:
4032 text = "Changes to be committed:";
4035 case LINE_STAT_UNSTAGED:
4036 text = "Changed but not updated:";
4039 case LINE_STAT_UNTRACKED:
4040 text = "Untracked files:";
4043 case LINE_STAT_NONE:
4044 text = " (no files)";
4047 case LINE_STAT_HEAD:
4048 text = status_onbranch;
4055 char buf[] = { status->status, ' ', ' ', ' ', 0 };
4057 col += draw_text(view, buf, view->width, TRUE, selected);
4059 wattrset(view->win, A_NORMAL);
4060 text = status->new.name;
4063 draw_text(view, text, view->width - col, TRUE, selected);
4068 status_enter(struct view *view, struct line *line)
4070 struct status *status = line->data;
4071 char oldpath[SIZEOF_STR] = "";
4072 char newpath[SIZEOF_STR] = "";
4076 if (line->type == LINE_STAT_NONE ||
4077 (!status && line[1].type == LINE_STAT_NONE)) {
4078 report("No file to diff");
4083 if (sq_quote(oldpath, 0, status->old.name) >= sizeof(oldpath))
4085 /* Diffs for unmerged entries are empty when pasing the
4086 * new path, so leave it empty. */
4087 if (status->status != 'U' &&
4088 sq_quote(newpath, 0, status->new.name) >= sizeof(newpath))
4093 line->type != LINE_STAT_UNTRACKED &&
4094 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
4097 switch (line->type) {
4098 case LINE_STAT_STAGED:
4100 if (!string_format_from(opt_cmd, &cmdsize,
4101 STATUS_DIFF_NO_HEAD_SHOW_CMD,
4105 if (!string_format_from(opt_cmd, &cmdsize,
4106 STATUS_DIFF_INDEX_SHOW_CMD,
4112 info = "Staged changes to %s";
4114 info = "Staged changes";
4117 case LINE_STAT_UNSTAGED:
4118 if (!string_format_from(opt_cmd, &cmdsize,
4119 STATUS_DIFF_FILES_SHOW_CMD, oldpath, newpath))
4122 info = "Unstaged changes to %s";
4124 info = "Unstaged changes";
4127 case LINE_STAT_UNTRACKED:
4132 report("No file to show");
4136 opt_pipe = fopen(status->new.name, "r");
4137 info = "Untracked file %s";
4140 case LINE_STAT_HEAD:
4144 die("line type %d not handled in switch", line->type);
4147 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | OPEN_SPLIT);
4148 if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) {
4150 stage_status = *status;
4152 memset(&stage_status, 0, sizeof(stage_status));
4155 stage_line_type = line->type;
4156 string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.new.name);
4164 status_update_prepare(enum line_type type)
4166 char cmd[SIZEOF_STR];
4170 type != LINE_STAT_UNTRACKED &&
4171 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4175 case LINE_STAT_STAGED:
4176 string_add(cmd, cmdsize, "git update-index -z --index-info");
4179 case LINE_STAT_UNSTAGED:
4180 case LINE_STAT_UNTRACKED:
4181 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
4185 die("line type %d not handled in switch", type);
4188 return popen(cmd, "w");
4192 status_update_write(FILE *pipe, struct status *status, enum line_type type)
4194 char buf[SIZEOF_STR];
4199 case LINE_STAT_STAGED:
4200 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
4203 status->old.name, 0))
4207 case LINE_STAT_UNSTAGED:
4208 case LINE_STAT_UNTRACKED:
4209 if (!string_format_from(buf, &bufsize, "%s%c", status->new.name, 0))
4214 die("line type %d not handled in switch", type);
4217 while (!ferror(pipe) && written < bufsize) {
4218 written += fwrite(buf + written, 1, bufsize - written, pipe);
4221 return written == bufsize;
4225 status_update_file(struct status *status, enum line_type type)
4227 FILE *pipe = status_update_prepare(type);
4233 result = status_update_write(pipe, status, type);
4239 status_update_files(struct view *view, struct line *line)
4241 FILE *pipe = status_update_prepare(line->type);
4243 struct line *pos = view->line + view->lines;
4250 for (pos = line; pos < view->line + view->lines && pos->data; pos++)
4253 for (file = 0, done = 0; result && file < files; line++, file++) {
4254 int almost_done = file * 100 / files;
4256 if (almost_done > done) {
4258 string_format(view->ref, "updating file %u of %u (%d%% done)",
4260 update_view_title(view);
4262 result = status_update_write(pipe, line->data, line->type);
4270 status_update(struct view *view)
4272 struct line *line = &view->line[view->lineno];
4274 assert(view->lines);
4277 /* This should work even for the "On branch" line. */
4278 if (line < view->line + view->lines && !line[1].data) {
4279 report("Nothing to update");
4283 if (!status_update_files(view, line + 1))
4284 report("Failed to update file status");
4286 } else if (!status_update_file(line->data, line->type)) {
4287 report("Failed to update file status");
4294 status_request(struct view *view, enum request request, struct line *line)
4296 struct status *status = line->data;
4299 case REQ_STATUS_UPDATE:
4300 if (!status_update(view))
4304 case REQ_STATUS_MERGE:
4305 if (!status || status->status != 'U') {
4306 report("Merging only possible for files with unmerged status ('U').");
4309 open_mergetool(status->new.name);
4316 open_editor(status->status != '?', status->new.name);
4319 case REQ_VIEW_BLAME:
4321 string_copy(opt_file, status->new.name);
4327 /* After returning the status view has been split to
4328 * show the stage view. No further reloading is
4330 status_enter(view, line);
4334 /* Simply reload the view. */
4341 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
4347 status_select(struct view *view, struct line *line)
4349 struct status *status = line->data;
4350 char file[SIZEOF_STR] = "all files";
4354 if (status && !string_format(file, "'%s'", status->new.name))
4357 if (!status && line[1].type == LINE_STAT_NONE)
4360 switch (line->type) {
4361 case LINE_STAT_STAGED:
4362 text = "Press %s to unstage %s for commit";
4365 case LINE_STAT_UNSTAGED:
4366 text = "Press %s to stage %s for commit";
4369 case LINE_STAT_UNTRACKED:
4370 text = "Press %s to stage %s for addition";
4373 case LINE_STAT_HEAD:
4374 case LINE_STAT_NONE:
4375 text = "Nothing to update";
4379 die("line type %d not handled in switch", line->type);
4382 if (status && status->status == 'U') {
4383 text = "Press %s to resolve conflict in %s";
4384 key = get_key(REQ_STATUS_MERGE);
4387 key = get_key(REQ_STATUS_UPDATE);
4390 string_format(view->ref, text, key, file);
4394 status_grep(struct view *view, struct line *line)
4396 struct status *status = line->data;
4397 enum { S_STATUS, S_NAME, S_END } state;
4404 for (state = S_STATUS; state < S_END; state++) {
4408 case S_NAME: text = status->new.name; break;
4410 buf[0] = status->status;
4418 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4425 static struct view_ops status_ops = {
4437 stage_diff_line(FILE *pipe, struct line *line)
4439 char *buf = line->data;
4440 size_t bufsize = strlen(buf);
4443 while (!ferror(pipe) && written < bufsize) {
4444 written += fwrite(buf + written, 1, bufsize - written, pipe);
4449 return written == bufsize;
4452 static struct line *
4453 stage_diff_hdr(struct view *view, struct line *line)
4455 int diff_hdr_dir = line->type == LINE_DIFF_CHUNK ? -1 : 1;
4456 struct line *diff_hdr;
4458 if (line->type == LINE_DIFF_CHUNK)
4459 diff_hdr = line - 1;
4461 diff_hdr = view->line + 1;
4463 while (diff_hdr > view->line && diff_hdr < view->line + view->lines) {
4464 if (diff_hdr->type == LINE_DIFF_HEADER)
4467 diff_hdr += diff_hdr_dir;
4474 stage_update_chunk(struct view *view, struct line *line)
4476 char cmd[SIZEOF_STR];
4478 struct line *diff_hdr, *diff_chunk, *diff_end;
4481 diff_hdr = stage_diff_hdr(view, line);
4486 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4489 if (!string_format_from(cmd, &cmdsize,
4490 "git apply --whitespace=nowarn --cached %s - && "
4491 "git update-index -q --unmerged --refresh 2>/dev/null",
4492 stage_line_type == LINE_STAT_STAGED ? "-R" : ""))
4495 pipe = popen(cmd, "w");
4499 diff_end = view->line + view->lines;
4500 if (line->type != LINE_DIFF_CHUNK) {
4501 diff_chunk = diff_hdr;
4504 for (diff_chunk = line + 1; diff_chunk < diff_end; diff_chunk++)
4505 if (diff_chunk->type == LINE_DIFF_CHUNK ||
4506 diff_chunk->type == LINE_DIFF_HEADER)
4507 diff_end = diff_chunk;
4511 while (diff_hdr->type != LINE_DIFF_CHUNK) {
4512 switch (diff_hdr->type) {
4513 case LINE_DIFF_HEADER:
4514 case LINE_DIFF_INDEX:
4524 if (!stage_diff_line(pipe, diff_hdr++)) {
4531 while (diff_chunk < diff_end && stage_diff_line(pipe, diff_chunk))
4536 if (diff_chunk != diff_end)
4543 stage_update(struct view *view, struct line *line)
4545 if (!opt_no_head && stage_line_type != LINE_STAT_UNTRACKED &&
4546 (line->type == LINE_DIFF_CHUNK || !stage_status.status)) {
4547 if (!stage_update_chunk(view, line)) {
4548 report("Failed to apply chunk");
4552 } else if (!status_update_file(&stage_status, stage_line_type)) {
4553 report("Failed to update file");
4557 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
4559 view = VIEW(REQ_VIEW_STATUS);
4560 if (view_is_displayed(view))
4561 status_enter(view, &view->line[view->lineno]);
4565 stage_request(struct view *view, enum request request, struct line *line)
4568 case REQ_STATUS_UPDATE:
4569 stage_update(view, line);
4573 if (!stage_status.new.name[0])
4576 open_editor(stage_status.status != '?', stage_status.new.name);
4579 case REQ_VIEW_BLAME:
4580 if (stage_status.new.name[0]) {
4581 string_copy(opt_file, stage_status.new.name);
4587 pager_request(view, request, line);
4597 static struct view_ops stage_ops = {
4613 char id[SIZEOF_REV]; /* SHA1 ID. */
4614 char title[128]; /* First line of the commit message. */
4615 char author[75]; /* Author of the commit. */
4616 struct tm time; /* Date from the author ident. */
4617 struct ref **refs; /* Repository references. */
4618 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
4619 size_t graph_size; /* The width of the graph array. */
4620 bool has_parents; /* Rewritten --parents seen. */
4623 /* Size of rev graph with no "padding" columns */
4624 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
4627 struct rev_graph *prev, *next, *parents;
4628 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
4630 struct commit *commit;
4632 unsigned int boundary:1;
4635 /* Parents of the commit being visualized. */
4636 static struct rev_graph graph_parents[4];
4638 /* The current stack of revisions on the graph. */
4639 static struct rev_graph graph_stacks[4] = {
4640 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
4641 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
4642 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
4643 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
4647 graph_parent_is_merge(struct rev_graph *graph)
4649 return graph->parents->size > 1;
4653 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
4655 struct commit *commit = graph->commit;
4657 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
4658 commit->graph[commit->graph_size++] = symbol;
4662 done_rev_graph(struct rev_graph *graph)
4664 if (graph_parent_is_merge(graph) &&
4665 graph->pos < graph->size - 1 &&
4666 graph->next->size == graph->size + graph->parents->size - 1) {
4667 size_t i = graph->pos + graph->parents->size - 1;
4669 graph->commit->graph_size = i * 2;
4670 while (i < graph->next->size - 1) {
4671 append_to_rev_graph(graph, ' ');
4672 append_to_rev_graph(graph, '\\');
4677 graph->size = graph->pos = 0;
4678 graph->commit = NULL;
4679 memset(graph->parents, 0, sizeof(*graph->parents));
4683 push_rev_graph(struct rev_graph *graph, char *parent)
4687 /* "Collapse" duplicate parents lines.
4689 * FIXME: This needs to also update update the drawn graph but
4690 * for now it just serves as a method for pruning graph lines. */
4691 for (i = 0; i < graph->size; i++)
4692 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
4695 if (graph->size < SIZEOF_REVITEMS) {
4696 string_copy_rev(graph->rev[graph->size++], parent);
4701 get_rev_graph_symbol(struct rev_graph *graph)
4705 if (graph->boundary)
4706 symbol = REVGRAPH_BOUND;
4707 else if (graph->parents->size == 0)
4708 symbol = REVGRAPH_INIT;
4709 else if (graph_parent_is_merge(graph))
4710 symbol = REVGRAPH_MERGE;
4711 else if (graph->pos >= graph->size)
4712 symbol = REVGRAPH_BRANCH;
4714 symbol = REVGRAPH_COMMIT;
4720 draw_rev_graph(struct rev_graph *graph)
4723 chtype separator, line;
4725 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
4726 static struct rev_filler fillers[] = {
4727 { ' ', REVGRAPH_LINE },
4732 chtype symbol = get_rev_graph_symbol(graph);
4733 struct rev_filler *filler;
4736 filler = &fillers[DEFAULT];
4738 for (i = 0; i < graph->pos; i++) {
4739 append_to_rev_graph(graph, filler->line);
4740 if (graph_parent_is_merge(graph->prev) &&
4741 graph->prev->pos == i)
4742 filler = &fillers[RSHARP];
4744 append_to_rev_graph(graph, filler->separator);
4747 /* Place the symbol for this revision. */
4748 append_to_rev_graph(graph, symbol);
4750 if (graph->prev->size > graph->size)
4751 filler = &fillers[RDIAG];
4753 filler = &fillers[DEFAULT];
4757 for (; i < graph->size; i++) {
4758 append_to_rev_graph(graph, filler->separator);
4759 append_to_rev_graph(graph, filler->line);
4760 if (graph_parent_is_merge(graph->prev) &&
4761 i < graph->prev->pos + graph->parents->size)
4762 filler = &fillers[RSHARP];
4763 if (graph->prev->size > graph->size)
4764 filler = &fillers[LDIAG];
4767 if (graph->prev->size > graph->size) {
4768 append_to_rev_graph(graph, filler->separator);
4769 if (filler->line != ' ')
4770 append_to_rev_graph(graph, filler->line);
4774 /* Prepare the next rev graph */
4776 prepare_rev_graph(struct rev_graph *graph)
4780 /* First, traverse all lines of revisions up to the active one. */
4781 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
4782 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
4785 push_rev_graph(graph->next, graph->rev[graph->pos]);
4788 /* Interleave the new revision parent(s). */
4789 for (i = 0; !graph->boundary && i < graph->parents->size; i++)
4790 push_rev_graph(graph->next, graph->parents->rev[i]);
4792 /* Lastly, put any remaining revisions. */
4793 for (i = graph->pos + 1; i < graph->size; i++)
4794 push_rev_graph(graph->next, graph->rev[i]);
4798 update_rev_graph(struct rev_graph *graph)
4800 /* If this is the finalizing update ... */
4802 prepare_rev_graph(graph);
4804 /* Graph visualization needs a one rev look-ahead,
4805 * so the first update doesn't visualize anything. */
4806 if (!graph->prev->commit)
4809 draw_rev_graph(graph->prev);
4810 done_rev_graph(graph->prev->prev);
4819 main_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
4821 char buf[DATE_COLS + 1];
4822 struct commit *commit = line->data;
4823 enum line_type type;
4828 if (!*commit->author)
4831 space = view->width;
4832 wmove(view->win, lineno, col);
4836 wattrset(view->win, get_line_attr(type));
4837 wchgat(view->win, -1, 0, type, NULL);
4839 type = LINE_MAIN_COMMIT;
4840 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
4846 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
4847 n = draw_text(view, buf, view->width - col, FALSE, selected);
4848 draw_text(view, " ", view->width - col - n, FALSE, selected);
4851 wmove(view->win, lineno, col);
4852 if (col >= view->width)
4855 if (type != LINE_CURSOR)
4856 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
4861 max_len = view->width - col;
4862 if (max_len > AUTHOR_COLS - 1)
4863 max_len = AUTHOR_COLS - 1;
4864 draw_text(view, commit->author, max_len, TRUE, selected);
4866 if (col >= view->width)
4870 if (opt_rev_graph && commit->graph_size) {
4871 size_t graph_size = view->width - col;
4874 if (type != LINE_CURSOR)
4875 wattrset(view->win, get_line_attr(LINE_MAIN_REVGRAPH));
4876 wmove(view->win, lineno, col);
4877 if (graph_size > commit->graph_size)
4878 graph_size = commit->graph_size;
4879 /* Using waddch() instead of waddnstr() ensures that
4880 * they'll be rendered correctly for the cursor line. */
4881 for (i = 0; i < graph_size; i++)
4882 waddch(view->win, commit->graph[i]);
4884 col += commit->graph_size + 1;
4885 if (col >= view->width)
4887 waddch(view->win, ' ');
4889 if (type != LINE_CURSOR)
4890 wattrset(view->win, A_NORMAL);
4892 wmove(view->win, lineno, col);
4894 if (opt_show_refs && commit->refs) {
4898 if (type == LINE_CURSOR)
4900 else if (commit->refs[i]->head)
4901 wattrset(view->win, get_line_attr(LINE_MAIN_HEAD));
4902 else if (commit->refs[i]->ltag)
4903 wattrset(view->win, get_line_attr(LINE_MAIN_LOCAL_TAG));
4904 else if (commit->refs[i]->tag)
4905 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
4906 else if (commit->refs[i]->tracked)
4907 wattrset(view->win, get_line_attr(LINE_MAIN_TRACKED));
4908 else if (commit->refs[i]->remote)
4909 wattrset(view->win, get_line_attr(LINE_MAIN_REMOTE));
4911 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
4913 col += draw_text(view, "[", view->width - col, TRUE, selected);
4914 col += draw_text(view, commit->refs[i]->name, view->width - col,
4916 col += draw_text(view, "]", view->width - col, TRUE, selected);
4917 if (type != LINE_CURSOR)
4918 wattrset(view->win, A_NORMAL);
4919 col += draw_text(view, " ", view->width - col, TRUE, selected);
4920 if (col >= view->width)
4922 } while (commit->refs[i++]->next);
4925 if (type != LINE_CURSOR)
4926 wattrset(view->win, get_line_attr(type));
4928 draw_text(view, commit->title, view->width - col, TRUE, selected);
4932 /* Reads git log --pretty=raw output and parses it into the commit struct. */
4934 main_read(struct view *view, char *line)
4936 static struct rev_graph *graph = graph_stacks;
4937 enum line_type type;
4938 struct commit *commit;
4941 update_rev_graph(graph);
4945 type = get_line_type(line);
4946 if (type == LINE_COMMIT) {
4947 commit = calloc(1, sizeof(struct commit));
4951 line += STRING_SIZE("commit ");
4953 graph->boundary = 1;
4957 string_copy_rev(commit->id, line);
4958 commit->refs = get_refs(commit->id);
4959 graph->commit = commit;
4960 add_line_data(view, commit, LINE_MAIN_COMMIT);
4962 while ((line = strchr(line, ' '))) {
4964 push_rev_graph(graph->parents, line);
4965 commit->has_parents = TRUE;
4972 commit = view->line[view->lines - 1].data;
4976 if (commit->has_parents)
4978 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
4983 /* Parse author lines where the name may be empty:
4984 * author <email@address.tld> 1138474660 +0100
4986 char *ident = line + STRING_SIZE("author ");
4987 char *nameend = strchr(ident, '<');
4988 char *emailend = strchr(ident, '>');
4990 if (!nameend || !emailend)
4993 update_rev_graph(graph);
4994 graph = graph->next;
4996 *nameend = *emailend = 0;
4997 ident = chomp_string(ident);
4999 ident = chomp_string(nameend + 1);
5004 string_ncopy(commit->author, ident, strlen(ident));
5006 /* Parse epoch and timezone */
5007 if (emailend[1] == ' ') {
5008 char *secs = emailend + 2;
5009 char *zone = strchr(secs, ' ');
5010 time_t time = (time_t) atol(secs);
5012 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
5016 tz = ('0' - zone[1]) * 60 * 60 * 10;
5017 tz += ('0' - zone[2]) * 60 * 60;
5018 tz += ('0' - zone[3]) * 60;
5019 tz += ('0' - zone[4]) * 60;
5027 gmtime_r(&time, &commit->time);
5032 /* Fill in the commit title if it has not already been set. */
5033 if (commit->title[0])
5036 /* Require titles to start with a non-space character at the
5037 * offset used by git log. */
5038 if (strncmp(line, " ", 4))
5041 /* Well, if the title starts with a whitespace character,
5042 * try to be forgiving. Otherwise we end up with no title. */
5043 while (isspace(*line))
5047 /* FIXME: More graceful handling of titles; append "..." to
5048 * shortened titles, etc. */
5050 string_ncopy(commit->title, line, strlen(line));
5057 main_request(struct view *view, enum request request, struct line *line)
5059 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
5061 if (request == REQ_ENTER)
5062 open_view(view, REQ_VIEW_DIFF, flags);
5070 main_grep(struct view *view, struct line *line)
5072 struct commit *commit = line->data;
5073 enum { S_TITLE, S_AUTHOR, S_DATE, S_END } state;
5074 char buf[DATE_COLS + 1];
5077 for (state = S_TITLE; state < S_END; state++) {
5081 case S_TITLE: text = commit->title; break;
5082 case S_AUTHOR: text = commit->author; break;
5084 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
5093 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
5101 main_select(struct view *view, struct line *line)
5103 struct commit *commit = line->data;
5105 string_copy_rev(view->ref, commit->id);
5106 string_copy_rev(ref_commit, view->ref);
5109 static struct view_ops main_ops = {
5121 * Unicode / UTF-8 handling
5123 * NOTE: Much of the following code for dealing with unicode is derived from
5124 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
5125 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
5128 /* I've (over)annotated a lot of code snippets because I am not entirely
5129 * confident that the approach taken by this small UTF-8 interface is correct.
5133 unicode_width(unsigned long c)
5136 (c <= 0x115f /* Hangul Jamo */
5139 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
5141 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
5142 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
5143 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
5144 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
5145 || (c >= 0xffe0 && c <= 0xffe6)
5146 || (c >= 0x20000 && c <= 0x2fffd)
5147 || (c >= 0x30000 && c <= 0x3fffd)))
5151 return opt_tab_size;
5156 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
5157 * Illegal bytes are set one. */
5158 static const unsigned char utf8_bytes[256] = {
5159 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
5160 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
5161 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
5162 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
5163 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,
5164 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,
5165 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,
5166 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,
5169 /* Decode UTF-8 multi-byte representation into a unicode character. */
5170 static inline unsigned long
5171 utf8_to_unicode(const char *string, size_t length)
5173 unsigned long unicode;
5177 unicode = string[0];
5180 unicode = (string[0] & 0x1f) << 6;
5181 unicode += (string[1] & 0x3f);
5184 unicode = (string[0] & 0x0f) << 12;
5185 unicode += ((string[1] & 0x3f) << 6);
5186 unicode += (string[2] & 0x3f);
5189 unicode = (string[0] & 0x0f) << 18;
5190 unicode += ((string[1] & 0x3f) << 12);
5191 unicode += ((string[2] & 0x3f) << 6);
5192 unicode += (string[3] & 0x3f);
5195 unicode = (string[0] & 0x0f) << 24;
5196 unicode += ((string[1] & 0x3f) << 18);
5197 unicode += ((string[2] & 0x3f) << 12);
5198 unicode += ((string[3] & 0x3f) << 6);
5199 unicode += (string[4] & 0x3f);
5202 unicode = (string[0] & 0x01) << 30;
5203 unicode += ((string[1] & 0x3f) << 24);
5204 unicode += ((string[2] & 0x3f) << 18);
5205 unicode += ((string[3] & 0x3f) << 12);
5206 unicode += ((string[4] & 0x3f) << 6);
5207 unicode += (string[5] & 0x3f);
5210 die("Invalid unicode length");
5213 /* Invalid characters could return the special 0xfffd value but NUL
5214 * should be just as good. */
5215 return unicode > 0xffff ? 0 : unicode;
5218 /* Calculates how much of string can be shown within the given maximum width
5219 * and sets trimmed parameter to non-zero value if all of string could not be
5220 * shown. If the reserve flag is TRUE, it will reserve at least one
5221 * trailing character, which can be useful when drawing a delimiter.
5223 * Returns the number of bytes to output from string to satisfy max_width. */
5225 utf8_length(const char *string, size_t max_width, int *trimmed, bool reserve)
5227 const char *start = string;
5228 const char *end = strchr(string, '\0');
5229 unsigned char last_bytes = 0;
5234 while (string < end) {
5235 int c = *(unsigned char *) string;
5236 unsigned char bytes = utf8_bytes[c];
5238 unsigned long unicode;
5240 if (string + bytes > end)
5243 /* Change representation to figure out whether
5244 * it is a single- or double-width character. */
5246 unicode = utf8_to_unicode(string, bytes);
5247 /* FIXME: Graceful handling of invalid unicode character. */
5251 ucwidth = unicode_width(unicode);
5253 if (width > max_width) {
5255 if (reserve && width - ucwidth == max_width) {
5256 string -= last_bytes;
5265 return string - start;
5273 /* Whether or not the curses interface has been initialized. */
5274 static bool cursed = FALSE;
5276 /* The status window is used for polling keystrokes. */
5277 static WINDOW *status_win;
5279 static bool status_empty = TRUE;
5281 /* Update status and title window. */
5283 report(const char *msg, ...)
5285 struct view *view = display[current_view];
5291 char buf[SIZEOF_STR];
5294 va_start(args, msg);
5295 if (vsnprintf(buf, sizeof(buf), msg, args) >= sizeof(buf)) {
5296 buf[sizeof(buf) - 1] = 0;
5297 buf[sizeof(buf) - 2] = '.';
5298 buf[sizeof(buf) - 3] = '.';
5299 buf[sizeof(buf) - 4] = '.';
5305 if (!status_empty || *msg) {
5308 va_start(args, msg);
5310 wmove(status_win, 0, 0);
5312 vwprintw(status_win, msg, args);
5313 status_empty = FALSE;
5315 status_empty = TRUE;
5317 wclrtoeol(status_win);
5318 wrefresh(status_win);
5323 update_view_title(view);
5324 update_display_cursor(view);
5327 /* Controls when nodelay should be in effect when polling user input. */
5329 set_nonblocking_input(bool loading)
5331 static unsigned int loading_views;
5333 if ((loading == FALSE && loading_views-- == 1) ||
5334 (loading == TRUE && loading_views++ == 0))
5335 nodelay(status_win, loading);
5343 /* Initialize the curses library */
5344 if (isatty(STDIN_FILENO)) {
5345 cursed = !!initscr();
5347 /* Leave stdin and stdout alone when acting as a pager. */
5348 FILE *io = fopen("/dev/tty", "r+");
5351 die("Failed to open /dev/tty");
5352 cursed = !!newterm(NULL, io, io);
5356 die("Failed to initialize curses");
5358 nonl(); /* Tell curses not to do NL->CR/NL on output */
5359 cbreak(); /* Take input chars one at a time, no wait for \n */
5360 noecho(); /* Don't echo input */
5361 leaveok(stdscr, TRUE);
5366 getmaxyx(stdscr, y, x);
5367 status_win = newwin(1, 0, y - 1, 0);
5369 die("Failed to create status window");
5371 /* Enable keyboard mapping */
5372 keypad(status_win, TRUE);
5373 wbkgdset(status_win, get_line_attr(LINE_STATUS));
5377 read_prompt(const char *prompt)
5379 enum { READING, STOP, CANCEL } status = READING;
5380 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
5383 while (status == READING) {
5389 foreach_view (view, i)
5394 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
5395 wclrtoeol(status_win);
5397 /* Refresh, accept single keystroke of input */
5398 key = wgetch(status_win);
5403 status = pos ? STOP : CANCEL;
5421 if (pos >= sizeof(buf)) {
5422 report("Input string too long");
5427 buf[pos++] = (char) key;
5431 /* Clear the status window */
5432 status_empty = FALSE;
5435 if (status == CANCEL)
5444 * Repository references
5447 static struct ref *refs = NULL;
5448 static size_t refs_alloc = 0;
5449 static size_t refs_size = 0;
5451 /* Id <-> ref store */
5452 static struct ref ***id_refs = NULL;
5453 static size_t id_refs_alloc = 0;
5454 static size_t id_refs_size = 0;
5456 static struct ref **
5459 struct ref ***tmp_id_refs;
5460 struct ref **ref_list = NULL;
5461 size_t ref_list_alloc = 0;
5462 size_t ref_list_size = 0;
5465 for (i = 0; i < id_refs_size; i++)
5466 if (!strcmp(id, id_refs[i][0]->id))
5469 tmp_id_refs = realloc_items(id_refs, &id_refs_alloc, id_refs_size + 1,
5474 id_refs = tmp_id_refs;
5476 for (i = 0; i < refs_size; i++) {
5479 if (strcmp(id, refs[i].id))
5482 tmp = realloc_items(ref_list, &ref_list_alloc,
5483 ref_list_size + 1, sizeof(*ref_list));
5491 if (ref_list_size > 0)
5492 ref_list[ref_list_size - 1]->next = 1;
5493 ref_list[ref_list_size] = &refs[i];
5495 /* XXX: The properties of the commit chains ensures that we can
5496 * safely modify the shared ref. The repo references will
5497 * always be similar for the same id. */
5498 ref_list[ref_list_size]->next = 0;
5503 id_refs[id_refs_size++] = ref_list;
5509 read_ref(char *id, size_t idlen, char *name, size_t namelen)
5514 bool remote = FALSE;
5515 bool tracked = FALSE;
5516 bool check_replace = FALSE;
5519 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
5520 if (!strcmp(name + namelen - 3, "^{}")) {
5523 if (refs_size > 0 && refs[refs_size - 1].ltag == TRUE)
5524 check_replace = TRUE;
5530 namelen -= STRING_SIZE("refs/tags/");
5531 name += STRING_SIZE("refs/tags/");
5533 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
5535 namelen -= STRING_SIZE("refs/remotes/");
5536 name += STRING_SIZE("refs/remotes/");
5537 tracked = !strcmp(opt_remote, name);
5539 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5540 namelen -= STRING_SIZE("refs/heads/");
5541 name += STRING_SIZE("refs/heads/");
5542 head = !strncmp(opt_head, name, namelen);
5544 } else if (!strcmp(name, "HEAD")) {
5545 opt_no_head = FALSE;
5549 if (check_replace && !strcmp(name, refs[refs_size - 1].name)) {
5550 /* it's an annotated tag, replace the previous sha1 with the
5551 * resolved commit id; relies on the fact git-ls-remote lists
5552 * the commit id of an annotated tag right beofre the commit id
5554 refs[refs_size - 1].ltag = ltag;
5555 string_copy_rev(refs[refs_size - 1].id, id);
5559 refs = realloc_items(refs, &refs_alloc, refs_size + 1, sizeof(*refs));
5563 ref = &refs[refs_size++];
5564 ref->name = malloc(namelen + 1);
5568 strncpy(ref->name, name, namelen);
5569 ref->name[namelen] = 0;
5573 ref->remote = remote;
5574 ref->tracked = tracked;
5575 string_copy_rev(ref->id, id);
5583 const char *cmd_env = getenv("TIG_LS_REMOTE");
5584 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
5586 return read_properties(popen(cmd, "r"), "\t", read_ref);
5590 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
5592 if (!strcmp(name, "i18n.commitencoding"))
5593 string_ncopy(opt_encoding, value, valuelen);
5595 if (!strcmp(name, "core.editor"))
5596 string_ncopy(opt_editor, value, valuelen);
5598 /* branch.<head>.remote */
5600 !strncmp(name, "branch.", 7) &&
5601 !strncmp(name + 7, opt_head, strlen(opt_head)) &&
5602 !strcmp(name + 7 + strlen(opt_head), ".remote"))
5603 string_ncopy(opt_remote, value, valuelen);
5605 if (*opt_head && *opt_remote &&
5606 !strncmp(name, "branch.", 7) &&
5607 !strncmp(name + 7, opt_head, strlen(opt_head)) &&
5608 !strcmp(name + 7 + strlen(opt_head), ".merge")) {
5609 size_t from = strlen(opt_remote);
5611 if (!strncmp(value, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5612 value += STRING_SIZE("refs/heads/");
5613 valuelen -= STRING_SIZE("refs/heads/");
5616 if (!string_format_from(opt_remote, &from, "/%s", value))
5624 load_git_config(void)
5626 return read_properties(popen(GIT_CONFIG " --list", "r"),
5627 "=", read_repo_config_option);
5631 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
5633 if (!opt_git_dir[0]) {
5634 string_ncopy(opt_git_dir, name, namelen);
5636 } else if (opt_is_inside_work_tree == -1) {
5637 /* This can be 3 different values depending on the
5638 * version of git being used. If git-rev-parse does not
5639 * understand --is-inside-work-tree it will simply echo
5640 * the option else either "true" or "false" is printed.
5641 * Default to true for the unknown case. */
5642 opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
5644 } else if (opt_cdup[0] == ' ') {
5645 string_ncopy(opt_cdup, name, namelen);
5647 if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5648 namelen -= STRING_SIZE("refs/heads/");
5649 name += STRING_SIZE("refs/heads/");
5650 string_ncopy(opt_head, name, namelen);
5658 load_repo_info(void)
5661 FILE *pipe = popen("git rev-parse --git-dir --is-inside-work-tree "
5662 " --show-cdup --symbolic-full-name HEAD 2>/dev/null", "r");
5664 /* XXX: The line outputted by "--show-cdup" can be empty so
5665 * initialize it to something invalid to make it possible to
5666 * detect whether it has been set or not. */
5669 result = read_properties(pipe, "=", read_repo_info);
5670 if (opt_cdup[0] == ' ')
5677 read_properties(FILE *pipe, const char *separators,
5678 int (*read_property)(char *, size_t, char *, size_t))
5680 char buffer[BUFSIZ];
5687 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
5692 name = chomp_string(name);
5693 namelen = strcspn(name, separators);
5695 if (name[namelen]) {
5697 value = chomp_string(name + namelen + 1);
5698 valuelen = strlen(value);
5705 state = read_property(name, namelen, value, valuelen);
5708 if (state != ERR && ferror(pipe))
5721 static void __NORETURN
5724 /* XXX: Restore tty modes and let the OS cleanup the rest! */
5730 static void __NORETURN
5731 die(const char *err, ...)
5737 va_start(args, err);
5738 fputs("tig: ", stderr);
5739 vfprintf(stderr, err, args);
5740 fputs("\n", stderr);
5747 warn(const char *msg, ...)
5751 va_start(args, msg);
5752 fputs("tig warning: ", stderr);
5753 vfprintf(stderr, msg, args);
5754 fputs("\n", stderr);
5759 main(int argc, char *argv[])
5762 enum request request;
5765 signal(SIGINT, quit);
5767 if (setlocale(LC_ALL, "")) {
5768 char *codeset = nl_langinfo(CODESET);
5770 string_ncopy(opt_codeset, codeset, strlen(codeset));
5773 if (load_repo_info() == ERR)
5774 die("Failed to load repo info.");
5776 if (load_options() == ERR)
5777 die("Failed to load user config.");
5779 if (load_git_config() == ERR)
5780 die("Failed to load repo config.");
5782 if (!parse_options(argc, argv))
5785 /* Require a git repository unless when running in pager mode. */
5786 if (!opt_git_dir[0] && opt_request != REQ_VIEW_PAGER)
5787 die("Not a git repository");
5789 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
5792 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
5793 opt_iconv = iconv_open(opt_codeset, opt_encoding);
5794 if (opt_iconv == ICONV_NONE)
5795 die("Failed to initialize character set conversion");
5798 if (*opt_git_dir && load_refs() == ERR)
5799 die("Failed to load refs.");
5801 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
5802 view->cmd_env = getenv(view->cmd_env);
5804 request = opt_request;
5808 while (view_driver(display[current_view], request)) {
5812 foreach_view (view, i)
5815 /* Refresh, accept single keystroke of input */
5816 key = wgetch(status_win);
5818 /* wgetch() with nodelay() enabled returns ERR when there's no
5825 request = get_keybinding(display[current_view]->keymap, key);
5827 /* Some low-level request handling. This keeps access to
5828 * status_win restricted. */
5832 char *cmd = read_prompt(":");
5834 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
5835 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
5836 opt_request = REQ_VIEW_DIFF;
5838 opt_request = REQ_VIEW_PAGER;
5847 case REQ_SEARCH_BACK:
5849 const char *prompt = request == REQ_SEARCH
5851 char *search = read_prompt(prompt);
5854 string_ncopy(opt_search, search, strlen(search));
5859 case REQ_SCREEN_RESIZE:
5863 getmaxyx(stdscr, height, width);
5865 /* Resize the status view and let the view driver take
5866 * care of resizing the displayed views. */
5867 wresize(status_win, 1, width);
5868 mvwin(status_win, height - 1, 0);
5869 wrefresh(status_win);