1 /* Copyright (c) 2006-2008 Jonas Fonseca <fonseca@diku.dk>
3 * This program is free software; you can redistribute it and/or
4 * modify it under the terms of the GNU General Public License as
5 * published by the Free Software Foundation; either version 2 of
6 * the License, or (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
19 #define TIG_VERSION "unknown-version"
34 #include <sys/types.h>
45 /* ncurses(3): Must be defined to have extended wide-character functions. */
46 #define _XOPEN_SOURCE_EXTENDED
48 #ifdef HAVE_NCURSESW_NCURSES_H
49 #include <ncursesw/ncurses.h>
51 #ifdef HAVE_NCURSES_NCURSES_H
52 #include <ncurses/ncurses.h>
59 #define __NORETURN __attribute__((__noreturn__))
64 static void __NORETURN die(const char *err, ...);
65 static void warn(const char *msg, ...);
66 static void report(const char *msg, ...);
67 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, size_t, char *, size_t));
68 static void set_nonblocking_input(bool loading);
69 static size_t utf8_length(const char *string, int *width, size_t max_width, int *trimmed, bool reserve);
70 static bool prompt_yesno(const char *prompt);
71 static int load_refs(void);
73 #define ABS(x) ((x) >= 0 ? (x) : -(x))
74 #define MIN(x, y) ((x) < (y) ? (x) : (y))
76 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
77 #define STRING_SIZE(x) (sizeof(x) - 1)
79 #define SIZEOF_STR 1024 /* Default string size. */
80 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
81 #define SIZEOF_REV 41 /* Holds a SHA-1 and an ending NUL. */
82 #define SIZEOF_ARG 32 /* Default argument array size. */
86 #define REVGRAPH_INIT 'I'
87 #define REVGRAPH_MERGE 'M'
88 #define REVGRAPH_BRANCH '+'
89 #define REVGRAPH_COMMIT '*'
90 #define REVGRAPH_BOUND '^'
92 #define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
94 /* This color name can be used to refer to the default term colors. */
95 #define COLOR_DEFAULT (-1)
97 #define ICONV_NONE ((iconv_t) -1)
99 #define ICONV_CONST /* nothing */
102 /* The format and size of the date column in the main view. */
103 #define DATE_FORMAT "%Y-%m-%d %H:%M"
104 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
106 #define AUTHOR_COLS 20
109 /* The default interval between line numbers. */
110 #define NUMBER_INTERVAL 5
114 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
116 #define NULL_ID "0000000000000000000000000000000000000000"
119 #define GIT_CONFIG "config"
122 #define TIG_LS_REMOTE \
123 "git ls-remote . 2>/dev/null"
125 #define TIG_DIFF_CMD \
126 "git show --pretty=fuller --no-color --root --patch-with-stat --find-copies-harder -C %s 2>/dev/null"
128 #define TIG_LOG_CMD \
129 "git log --no-color --cc --stat -n100 %s 2>/dev/null"
131 #define TIG_MAIN_BASE \
132 "git log --no-color --pretty=raw --parents --topo-order"
134 #define TIG_MAIN_CMD \
135 TIG_MAIN_BASE " %s 2>/dev/null"
137 #define TIG_TREE_CMD \
140 #define TIG_BLOB_CMD \
141 "git cat-file blob %s"
143 /* XXX: Needs to be defined to the empty string. */
144 #define TIG_HELP_CMD ""
145 #define TIG_PAGER_CMD ""
146 #define TIG_STATUS_CMD ""
147 #define TIG_STAGE_CMD ""
148 #define TIG_BLAME_CMD ""
150 /* Some ascii-shorthands fitted into the ncurses namespace. */
152 #define KEY_RETURN '\r'
157 char *name; /* Ref name; tag or head names are shortened. */
158 char id[SIZEOF_REV]; /* Commit SHA1 ID */
159 unsigned int head:1; /* Is it the current HEAD? */
160 unsigned int tag:1; /* Is it a tag? */
161 unsigned int ltag:1; /* If so, is the tag local? */
162 unsigned int remote:1; /* Is it a remote ref? */
163 unsigned int tracked:1; /* Is it the remote for the current HEAD? */
164 unsigned int next:1; /* For ref lists: are there more refs? */
167 static struct ref **get_refs(const char *id);
176 set_from_int_map(struct int_map *map, size_t map_size,
177 int *value, const char *name, int namelen)
182 for (i = 0; i < map_size; i++)
183 if (namelen == map[i].namelen &&
184 !strncasecmp(name, map[i].name, namelen)) {
185 *value = map[i].value;
198 string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
200 if (srclen > dstlen - 1)
203 strncpy(dst, src, srclen);
207 /* Shorthands for safely copying into a fixed buffer. */
209 #define string_copy(dst, src) \
210 string_ncopy_do(dst, sizeof(dst), src, sizeof(src))
212 #define string_ncopy(dst, src, srclen) \
213 string_ncopy_do(dst, sizeof(dst), src, srclen)
215 #define string_copy_rev(dst, src) \
216 string_ncopy_do(dst, SIZEOF_REV, src, SIZEOF_REV - 1)
218 #define string_add(dst, from, src) \
219 string_ncopy_do(dst + (from), sizeof(dst) - (from), src, sizeof(src))
222 chomp_string(char *name)
226 while (isspace(*name))
229 namelen = strlen(name) - 1;
230 while (namelen > 0 && isspace(name[namelen]))
237 string_nformat(char *buf, size_t bufsize, size_t *bufpos, const char *fmt, ...)
240 size_t pos = bufpos ? *bufpos : 0;
243 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
249 return pos >= bufsize ? FALSE : TRUE;
252 #define string_format(buf, fmt, args...) \
253 string_nformat(buf, sizeof(buf), NULL, fmt, args)
255 #define string_format_from(buf, from, fmt, args...) \
256 string_nformat(buf, sizeof(buf), from, fmt, args)
259 string_enum_compare(const char *str1, const char *str2, int len)
263 #define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
265 /* Diff-Header == DIFF_HEADER */
266 for (i = 0; i < len; i++) {
267 if (toupper(str1[i]) == toupper(str2[i]))
270 if (string_enum_sep(str1[i]) &&
271 string_enum_sep(str2[i]))
274 return str1[i] - str2[i];
280 #define prefixcmp(str1, str2) \
281 strncmp(str1, str2, STRING_SIZE(str2))
285 * NOTE: The following is a slightly modified copy of the git project's shell
286 * quoting routines found in the quote.c file.
288 * Help to copy the thing properly quoted for the shell safety. any single
289 * quote is replaced with '\'', any exclamation point is replaced with '\!',
290 * and the whole thing is enclosed in a
293 * original sq_quote result
294 * name ==> name ==> 'name'
295 * a b ==> a b ==> 'a b'
296 * a'b ==> a'\''b ==> 'a'\''b'
297 * a!b ==> a'\!'b ==> 'a'\!'b'
301 sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
305 #define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0)
308 while ((c = *src++)) {
309 if (c == '\'' || c == '!') {
320 if (bufsize < SIZEOF_STR)
332 /* XXX: Keep the view request first and in sync with views[]. */ \
333 REQ_GROUP("View switching") \
334 REQ_(VIEW_MAIN, "Show main view"), \
335 REQ_(VIEW_DIFF, "Show diff view"), \
336 REQ_(VIEW_LOG, "Show log view"), \
337 REQ_(VIEW_TREE, "Show tree view"), \
338 REQ_(VIEW_BLOB, "Show blob view"), \
339 REQ_(VIEW_BLAME, "Show blame view"), \
340 REQ_(VIEW_HELP, "Show help page"), \
341 REQ_(VIEW_PAGER, "Show pager view"), \
342 REQ_(VIEW_STATUS, "Show status view"), \
343 REQ_(VIEW_STAGE, "Show stage view"), \
345 REQ_GROUP("View manipulation") \
346 REQ_(ENTER, "Enter current line and scroll"), \
347 REQ_(NEXT, "Move to next"), \
348 REQ_(PREVIOUS, "Move to previous"), \
349 REQ_(VIEW_NEXT, "Move focus to next view"), \
350 REQ_(REFRESH, "Reload and refresh"), \
351 REQ_(MAXIMIZE, "Maximize the current view"), \
352 REQ_(VIEW_CLOSE, "Close the current view"), \
353 REQ_(QUIT, "Close all views and quit"), \
355 REQ_GROUP("View specific requests") \
356 REQ_(STATUS_UPDATE, "Update file status"), \
357 REQ_(STATUS_REVERT, "Revert file changes"), \
358 REQ_(STATUS_MERGE, "Merge file using external tool"), \
359 REQ_(STAGE_NEXT, "Find next chunk to stage"), \
360 REQ_(TREE_PARENT, "Switch to parent directory in tree view"), \
362 REQ_GROUP("Cursor navigation") \
363 REQ_(MOVE_UP, "Move cursor one line up"), \
364 REQ_(MOVE_DOWN, "Move cursor one line down"), \
365 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
366 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
367 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
368 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
370 REQ_GROUP("Scrolling") \
371 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
372 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
373 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
374 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
376 REQ_GROUP("Searching") \
377 REQ_(SEARCH, "Search the view"), \
378 REQ_(SEARCH_BACK, "Search backwards in the view"), \
379 REQ_(FIND_NEXT, "Find next search match"), \
380 REQ_(FIND_PREV, "Find previous search match"), \
382 REQ_GROUP("Option manipulation") \
383 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
384 REQ_(TOGGLE_DATE, "Toggle date display"), \
385 REQ_(TOGGLE_AUTHOR, "Toggle author display"), \
386 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"), \
387 REQ_(TOGGLE_REFS, "Toggle reference display (tags/branches)"), \
390 REQ_(PROMPT, "Bring up the prompt"), \
391 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
392 REQ_(SCREEN_RESIZE, "Resize the screen"), \
393 REQ_(SHOW_VERSION, "Show version information"), \
394 REQ_(STOP_LOADING, "Stop all loading views"), \
395 REQ_(EDIT, "Open in editor"), \
396 REQ_(NONE, "Do nothing")
399 /* User action requests. */
401 #define REQ_GROUP(help)
402 #define REQ_(req, help) REQ_##req
404 /* Offset all requests to avoid conflicts with ncurses getch values. */
405 REQ_OFFSET = KEY_MAX + 1,
412 struct request_info {
413 enum request request;
419 static struct request_info req_info[] = {
420 #define REQ_GROUP(help) { 0, NULL, 0, (help) },
421 #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
428 get_request(const char *name)
430 int namelen = strlen(name);
433 for (i = 0; i < ARRAY_SIZE(req_info); i++)
434 if (req_info[i].namelen == namelen &&
435 !string_enum_compare(req_info[i].name, name, namelen))
436 return req_info[i].request;
446 static const char usage[] =
447 "tig " TIG_VERSION " (" __DATE__ ")\n"
449 "Usage: tig [options] [revs] [--] [paths]\n"
450 " or: tig show [options] [revs] [--] [paths]\n"
451 " or: tig blame [rev] path\n"
453 " or: tig < [git command output]\n"
456 " -v, --version Show version and exit\n"
457 " -h, --help Show help message and exit";
459 /* Option and state variables. */
460 static bool opt_date = TRUE;
461 static bool opt_author = TRUE;
462 static bool opt_line_number = FALSE;
463 static bool opt_line_graphics = TRUE;
464 static bool opt_rev_graph = FALSE;
465 static bool opt_show_refs = TRUE;
466 static int opt_num_interval = NUMBER_INTERVAL;
467 static int opt_tab_size = TAB_SIZE;
468 static int opt_author_cols = AUTHOR_COLS-1;
469 static char opt_cmd[SIZEOF_STR] = "";
470 static char opt_path[SIZEOF_STR] = "";
471 static char opt_file[SIZEOF_STR] = "";
472 static char opt_ref[SIZEOF_REF] = "";
473 static char opt_head[SIZEOF_REF] = "";
474 static char opt_remote[SIZEOF_REF] = "";
475 static bool opt_no_head = TRUE;
476 static FILE *opt_pipe = NULL;
477 static char opt_encoding[20] = "UTF-8";
478 static bool opt_utf8 = TRUE;
479 static char opt_codeset[20] = "UTF-8";
480 static iconv_t opt_iconv = ICONV_NONE;
481 static char opt_search[SIZEOF_STR] = "";
482 static char opt_cdup[SIZEOF_STR] = "";
483 static char opt_git_dir[SIZEOF_STR] = "";
484 static signed char opt_is_inside_work_tree = -1; /* set to TRUE or FALSE */
485 static char opt_editor[SIZEOF_STR] = "";
486 static FILE *opt_tty = NULL;
489 parse_options(int argc, const char *argv[])
491 enum request request = REQ_VIEW_MAIN;
493 const char *subcommand;
494 bool seen_dashdash = FALSE;
497 if (!isatty(STDIN_FILENO)) {
499 return REQ_VIEW_PAGER;
503 return REQ_VIEW_MAIN;
505 subcommand = argv[1];
506 if (!strcmp(subcommand, "status") || !strcmp(subcommand, "-S")) {
507 if (!strcmp(subcommand, "-S"))
508 warn("`-S' has been deprecated; use `tig status' instead");
510 warn("ignoring arguments after `%s'", subcommand);
511 return REQ_VIEW_STATUS;
513 } else if (!strcmp(subcommand, "blame")) {
514 if (argc <= 2 || argc > 4)
515 die("invalid number of options to blame\n\n%s", usage);
519 string_ncopy(opt_ref, argv[i], strlen(argv[i]));
523 string_ncopy(opt_file, argv[i], strlen(argv[i]));
524 return REQ_VIEW_BLAME;
526 } else if (!strcmp(subcommand, "show")) {
527 request = REQ_VIEW_DIFF;
529 } else if (!strcmp(subcommand, "log") || !strcmp(subcommand, "diff")) {
530 request = subcommand[0] == 'l' ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
531 warn("`tig %s' has been deprecated", subcommand);
538 /* XXX: This is vulnerable to the user overriding
539 * options required for the main view parser. */
540 string_copy(opt_cmd, TIG_MAIN_BASE);
542 string_format(opt_cmd, "git %s", subcommand);
544 buf_size = strlen(opt_cmd);
546 for (i = 1 + !!subcommand; i < argc; i++) {
547 const char *opt = argv[i];
549 if (seen_dashdash || !strcmp(opt, "--")) {
550 seen_dashdash = TRUE;
552 } else if (!strcmp(opt, "-v") || !strcmp(opt, "--version")) {
553 printf("tig version %s\n", TIG_VERSION);
556 } else if (!strcmp(opt, "-h") || !strcmp(opt, "--help")) {
557 printf("%s\n", usage);
561 opt_cmd[buf_size++] = ' ';
562 buf_size = sq_quote(opt_cmd, buf_size, opt);
563 if (buf_size >= sizeof(opt_cmd))
564 die("command too long");
567 opt_cmd[buf_size] = 0;
574 * Line-oriented content detection.
578 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
579 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
580 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
581 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
582 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
583 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
584 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
585 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
586 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
587 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
588 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
589 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
590 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
591 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
592 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
593 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
594 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
595 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
596 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
597 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
598 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
599 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
600 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
601 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
602 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
603 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
604 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
605 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
606 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
607 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
608 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
609 LINE(DELIMITER, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
610 LINE(DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
611 LINE(LINE_NUMBER, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
612 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
613 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
614 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
615 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
616 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
617 LINE(MAIN_LOCAL_TAG,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
618 LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \
619 LINE(MAIN_TRACKED, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
620 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
621 LINE(MAIN_HEAD, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
622 LINE(MAIN_REVGRAPH,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
623 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
624 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
625 LINE(STAT_HEAD, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \
626 LINE(STAT_SECTION, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
627 LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
628 LINE(STAT_STAGED, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
629 LINE(STAT_UNSTAGED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
630 LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
631 LINE(BLAME_ID, "", COLOR_MAGENTA, COLOR_DEFAULT, 0)
634 #define LINE(type, line, fg, bg, attr) \
642 const char *name; /* Option name. */
643 int namelen; /* Size of option name. */
644 const char *line; /* The start of line to match. */
645 int linelen; /* Size of string to match. */
646 int fg, bg, attr; /* Color and text attributes for the lines. */
649 static struct line_info line_info[] = {
650 #define LINE(type, line, fg, bg, attr) \
651 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
656 static enum line_type
657 get_line_type(const char *line)
659 int linelen = strlen(line);
662 for (type = 0; type < ARRAY_SIZE(line_info); type++)
663 /* Case insensitive search matches Signed-off-by lines better. */
664 if (linelen >= line_info[type].linelen &&
665 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
672 get_line_attr(enum line_type type)
674 assert(type < ARRAY_SIZE(line_info));
675 return COLOR_PAIR(type) | line_info[type].attr;
678 static struct line_info *
679 get_line_info(const char *name)
681 size_t namelen = strlen(name);
684 for (type = 0; type < ARRAY_SIZE(line_info); type++)
685 if (namelen == line_info[type].namelen &&
686 !string_enum_compare(line_info[type].name, name, namelen))
687 return &line_info[type];
695 int default_bg = line_info[LINE_DEFAULT].bg;
696 int default_fg = line_info[LINE_DEFAULT].fg;
701 if (assume_default_colors(default_fg, default_bg) == ERR) {
702 default_bg = COLOR_BLACK;
703 default_fg = COLOR_WHITE;
706 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
707 struct line_info *info = &line_info[type];
708 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
709 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
711 init_pair(type, fg, bg);
719 unsigned int selected:1;
720 unsigned int dirty:1;
722 void *data; /* User data */
732 enum request request;
733 struct keybinding *next;
736 static struct keybinding default_keybindings[] = {
738 { 'm', REQ_VIEW_MAIN },
739 { 'd', REQ_VIEW_DIFF },
740 { 'l', REQ_VIEW_LOG },
741 { 't', REQ_VIEW_TREE },
742 { 'f', REQ_VIEW_BLOB },
743 { 'B', REQ_VIEW_BLAME },
744 { 'p', REQ_VIEW_PAGER },
745 { 'h', REQ_VIEW_HELP },
746 { 'S', REQ_VIEW_STATUS },
747 { 'c', REQ_VIEW_STAGE },
749 /* View manipulation */
750 { 'q', REQ_VIEW_CLOSE },
751 { KEY_TAB, REQ_VIEW_NEXT },
752 { KEY_RETURN, REQ_ENTER },
753 { KEY_UP, REQ_PREVIOUS },
754 { KEY_DOWN, REQ_NEXT },
755 { 'R', REQ_REFRESH },
756 { KEY_F(5), REQ_REFRESH },
757 { 'O', REQ_MAXIMIZE },
759 /* Cursor navigation */
760 { 'k', REQ_MOVE_UP },
761 { 'j', REQ_MOVE_DOWN },
762 { KEY_HOME, REQ_MOVE_FIRST_LINE },
763 { KEY_END, REQ_MOVE_LAST_LINE },
764 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
765 { ' ', REQ_MOVE_PAGE_DOWN },
766 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
767 { 'b', REQ_MOVE_PAGE_UP },
768 { '-', REQ_MOVE_PAGE_UP },
771 { KEY_IC, REQ_SCROLL_LINE_UP },
772 { KEY_DC, REQ_SCROLL_LINE_DOWN },
773 { 'w', REQ_SCROLL_PAGE_UP },
774 { 's', REQ_SCROLL_PAGE_DOWN },
778 { '?', REQ_SEARCH_BACK },
779 { 'n', REQ_FIND_NEXT },
780 { 'N', REQ_FIND_PREV },
784 { 'z', REQ_STOP_LOADING },
785 { 'v', REQ_SHOW_VERSION },
786 { 'r', REQ_SCREEN_REDRAW },
787 { '.', REQ_TOGGLE_LINENO },
788 { 'D', REQ_TOGGLE_DATE },
789 { 'A', REQ_TOGGLE_AUTHOR },
790 { 'g', REQ_TOGGLE_REV_GRAPH },
791 { 'F', REQ_TOGGLE_REFS },
793 { 'u', REQ_STATUS_UPDATE },
794 { '!', REQ_STATUS_REVERT },
795 { 'M', REQ_STATUS_MERGE },
796 { '@', REQ_STAGE_NEXT },
797 { ',', REQ_TREE_PARENT },
800 /* Using the ncurses SIGWINCH handler. */
801 { KEY_RESIZE, REQ_SCREEN_RESIZE },
804 #define KEYMAP_INFO \
818 #define KEYMAP_(name) KEYMAP_##name
823 static struct int_map keymap_table[] = {
824 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
829 #define set_keymap(map, name) \
830 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
832 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
835 add_keybinding(enum keymap keymap, enum request request, int key)
837 struct keybinding *keybinding;
839 keybinding = calloc(1, sizeof(*keybinding));
841 die("Failed to allocate keybinding");
843 keybinding->alias = key;
844 keybinding->request = request;
845 keybinding->next = keybindings[keymap];
846 keybindings[keymap] = keybinding;
849 /* Looks for a key binding first in the given map, then in the generic map, and
850 * lastly in the default keybindings. */
852 get_keybinding(enum keymap keymap, int key)
854 struct keybinding *kbd;
857 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
858 if (kbd->alias == key)
861 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
862 if (kbd->alias == key)
865 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
866 if (default_keybindings[i].alias == key)
867 return default_keybindings[i].request;
869 return (enum request) key;
878 static struct key key_table[] = {
879 { "Enter", KEY_RETURN },
881 { "Backspace", KEY_BACKSPACE },
883 { "Escape", KEY_ESC },
884 { "Left", KEY_LEFT },
885 { "Right", KEY_RIGHT },
887 { "Down", KEY_DOWN },
888 { "Insert", KEY_IC },
889 { "Delete", KEY_DC },
891 { "Home", KEY_HOME },
893 { "PageUp", KEY_PPAGE },
894 { "PageDown", KEY_NPAGE },
904 { "F10", KEY_F(10) },
905 { "F11", KEY_F(11) },
906 { "F12", KEY_F(12) },
910 get_key_value(const char *name)
914 for (i = 0; i < ARRAY_SIZE(key_table); i++)
915 if (!strcasecmp(key_table[i].name, name))
916 return key_table[i].value;
918 if (strlen(name) == 1 && isprint(*name))
925 get_key_name(int key_value)
927 static char key_char[] = "'X'";
928 const char *seq = NULL;
931 for (key = 0; key < ARRAY_SIZE(key_table); key++)
932 if (key_table[key].value == key_value)
933 seq = key_table[key].name;
937 isprint(key_value)) {
938 key_char[1] = (char) key_value;
942 return seq ? seq : "(no key)";
946 get_key(enum request request)
948 static char buf[BUFSIZ];
955 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
956 struct keybinding *keybinding = &default_keybindings[i];
958 if (keybinding->request != request)
961 if (!string_format_from(buf, &pos, "%s%s", sep,
962 get_key_name(keybinding->alias)))
963 return "Too many keybindings!";
973 char cmd[SIZEOF_STR];
976 static struct run_request *run_request;
977 static size_t run_requests;
980 add_run_request(enum keymap keymap, int key, int argc, const char **argv)
982 struct run_request *req;
983 char cmd[SIZEOF_STR];
986 for (bufpos = 0; argc > 0; argc--, argv++)
987 if (!string_format_from(cmd, &bufpos, "%s ", *argv))
990 req = realloc(run_request, (run_requests + 1) * sizeof(*run_request));
995 req = &run_request[run_requests++];
996 string_copy(req->cmd, cmd);
997 req->keymap = keymap;
1000 return REQ_NONE + run_requests;
1003 static struct run_request *
1004 get_run_request(enum request request)
1006 if (request <= REQ_NONE)
1008 return &run_request[request - REQ_NONE - 1];
1012 add_builtin_run_requests(void)
1017 const char *argv[1];
1019 { KEYMAP_MAIN, 'C', { "git cherry-pick %(commit)" } },
1020 { KEYMAP_GENERIC, 'G', { "git gc" } },
1024 for (i = 0; i < ARRAY_SIZE(reqs); i++) {
1027 req = add_run_request(reqs[i].keymap, reqs[i].key, 1, reqs[i].argv);
1028 if (req != REQ_NONE)
1029 add_keybinding(reqs[i].keymap, req, reqs[i].key);
1034 * User config file handling.
1037 static struct int_map color_map[] = {
1038 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
1050 #define set_color(color, name) \
1051 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
1053 static struct int_map attr_map[] = {
1054 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
1061 ATTR_MAP(UNDERLINE),
1064 #define set_attribute(attr, name) \
1065 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
1067 static int config_lineno;
1068 static bool config_errors;
1069 static const char *config_msg;
1071 /* Wants: object fgcolor bgcolor [attr] */
1073 option_color_command(int argc, const char *argv[])
1075 struct line_info *info;
1077 if (argc != 3 && argc != 4) {
1078 config_msg = "Wrong number of arguments given to color command";
1082 info = get_line_info(argv[0]);
1084 if (!string_enum_compare(argv[0], "main-delim", strlen("main-delim"))) {
1085 info = get_line_info("delimiter");
1087 } else if (!string_enum_compare(argv[0], "main-date", strlen("main-date"))) {
1088 info = get_line_info("date");
1091 config_msg = "Unknown color name";
1096 if (set_color(&info->fg, argv[1]) == ERR ||
1097 set_color(&info->bg, argv[2]) == ERR) {
1098 config_msg = "Unknown color";
1102 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
1103 config_msg = "Unknown attribute";
1110 static bool parse_bool(const char *s)
1112 return (!strcmp(s, "1") || !strcmp(s, "true") ||
1113 !strcmp(s, "yes")) ? TRUE : FALSE;
1117 parse_int(const char *s, int default_value, int min, int max)
1119 int value = atoi(s);
1121 return (value < min || value > max) ? default_value : value;
1124 /* Wants: name = value */
1126 option_set_command(int argc, const char *argv[])
1129 config_msg = "Wrong number of arguments given to set command";
1133 if (strcmp(argv[1], "=")) {
1134 config_msg = "No value assigned";
1138 if (!strcmp(argv[0], "show-author")) {
1139 opt_author = parse_bool(argv[2]);
1143 if (!strcmp(argv[0], "show-date")) {
1144 opt_date = parse_bool(argv[2]);
1148 if (!strcmp(argv[0], "show-rev-graph")) {
1149 opt_rev_graph = parse_bool(argv[2]);
1153 if (!strcmp(argv[0], "show-refs")) {
1154 opt_show_refs = parse_bool(argv[2]);
1158 if (!strcmp(argv[0], "show-line-numbers")) {
1159 opt_line_number = parse_bool(argv[2]);
1163 if (!strcmp(argv[0], "line-graphics")) {
1164 opt_line_graphics = parse_bool(argv[2]);
1168 if (!strcmp(argv[0], "line-number-interval")) {
1169 opt_num_interval = parse_int(argv[2], opt_num_interval, 1, 1024);
1173 if (!strcmp(argv[0], "author-width")) {
1174 opt_author_cols = parse_int(argv[2], opt_author_cols, 0, 1024);
1178 if (!strcmp(argv[0], "tab-size")) {
1179 opt_tab_size = parse_int(argv[2], opt_tab_size, 1, 1024);
1183 if (!strcmp(argv[0], "commit-encoding")) {
1184 const char *arg = argv[2];
1185 int arglen = strlen(arg);
1190 if (arglen == 1 || arg[arglen - 1] != arg[0]) {
1191 config_msg = "Unmatched quotation";
1194 arg += 1; arglen -= 2;
1196 string_ncopy(opt_encoding, arg, strlen(arg));
1201 config_msg = "Unknown variable name";
1205 /* Wants: mode request key */
1207 option_bind_command(int argc, const char *argv[])
1209 enum request request;
1214 config_msg = "Wrong number of arguments given to bind command";
1218 if (set_keymap(&keymap, argv[0]) == ERR) {
1219 config_msg = "Unknown key map";
1223 key = get_key_value(argv[1]);
1225 config_msg = "Unknown key";
1229 request = get_request(argv[2]);
1230 if (request == REQ_NONE) {
1231 const char *obsolete[] = { "cherry-pick" };
1232 size_t namelen = strlen(argv[2]);
1235 for (i = 0; i < ARRAY_SIZE(obsolete); i++) {
1236 if (namelen == strlen(obsolete[i]) &&
1237 !string_enum_compare(obsolete[i], argv[2], namelen)) {
1238 config_msg = "Obsolete request name";
1243 if (request == REQ_NONE && *argv[2]++ == '!')
1244 request = add_run_request(keymap, key, argc - 2, argv + 2);
1245 if (request == REQ_NONE) {
1246 config_msg = "Unknown request name";
1250 add_keybinding(keymap, request, key);
1256 set_option(const char *opt, char *value)
1258 const char *argv[SIZEOF_ARG];
1263 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1264 argv[argc++] = value;
1267 /* Nothing more to tokenize or last available token. */
1268 if (!*value || argc >= ARRAY_SIZE(argv))
1272 while (isspace(*value))
1276 if (!strcmp(opt, "color"))
1277 return option_color_command(argc, argv);
1279 if (!strcmp(opt, "set"))
1280 return option_set_command(argc, argv);
1282 if (!strcmp(opt, "bind"))
1283 return option_bind_command(argc, argv);
1285 config_msg = "Unknown option command";
1290 read_option(char *opt, size_t optlen, char *value, size_t valuelen)
1295 config_msg = "Internal error";
1297 /* Check for comment markers, since read_properties() will
1298 * only ensure opt and value are split at first " \t". */
1299 optlen = strcspn(opt, "#");
1303 if (opt[optlen] != 0) {
1304 config_msg = "No option value";
1308 /* Look for comment endings in the value. */
1309 size_t len = strcspn(value, "#");
1311 if (len < valuelen) {
1313 value[valuelen] = 0;
1316 status = set_option(opt, value);
1319 if (status == ERR) {
1320 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1321 config_lineno, (int) optlen, opt, config_msg);
1322 config_errors = TRUE;
1325 /* Always keep going if errors are encountered. */
1330 load_option_file(const char *path)
1334 /* It's ok that the file doesn't exist. */
1335 file = fopen(path, "r");
1340 config_errors = FALSE;
1342 if (read_properties(file, " \t", read_option) == ERR ||
1343 config_errors == TRUE)
1344 fprintf(stderr, "Errors while loading %s.\n", path);
1350 const char *home = getenv("HOME");
1351 const char *tigrc_user = getenv("TIGRC_USER");
1352 const char *tigrc_system = getenv("TIGRC_SYSTEM");
1353 char buf[SIZEOF_STR];
1355 add_builtin_run_requests();
1357 if (!tigrc_system) {
1358 if (!string_format(buf, "%s/tigrc", SYSCONFDIR))
1362 load_option_file(tigrc_system);
1365 if (!home || !string_format(buf, "%s/.tigrc", home))
1369 load_option_file(tigrc_user);
1382 /* The display array of active views and the index of the current view. */
1383 static struct view *display[2];
1384 static unsigned int current_view;
1386 /* Reading from the prompt? */
1387 static bool input_mode = FALSE;
1389 #define foreach_displayed_view(view, i) \
1390 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1392 #define displayed_views() (display[1] != NULL ? 2 : 1)
1394 /* Current head and commit ID */
1395 static char ref_blob[SIZEOF_REF] = "";
1396 static char ref_commit[SIZEOF_REF] = "HEAD";
1397 static char ref_head[SIZEOF_REF] = "HEAD";
1400 const char *name; /* View name */
1401 const char *cmd_fmt; /* Default command line format */
1402 const char *cmd_env; /* Command line set via environment */
1403 const char *id; /* Points to either of ref_{head,commit,blob} */
1405 struct view_ops *ops; /* View operations */
1407 enum keymap keymap; /* What keymap does this view have */
1408 bool git_dir; /* Whether the view requires a git directory. */
1410 char cmd[SIZEOF_STR]; /* Command buffer */
1411 char ref[SIZEOF_REF]; /* Hovered commit reference */
1412 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1414 int height, width; /* The width and height of the main window */
1415 WINDOW *win; /* The main window */
1416 WINDOW *title; /* The title window living below the main window */
1419 unsigned long offset; /* Offset of the window top */
1420 unsigned long lineno; /* Current line number */
1423 char grep[SIZEOF_STR]; /* Search string */
1424 regex_t *regex; /* Pre-compiled regex */
1426 /* If non-NULL, points to the view that opened this view. If this view
1427 * is closed tig will switch back to the parent view. */
1428 struct view *parent;
1431 size_t lines; /* Total number of lines */
1432 struct line *line; /* Line index */
1433 size_t line_alloc; /* Total number of allocated lines */
1434 size_t line_size; /* Total number of used lines */
1435 unsigned int digits; /* Number of digits in the lines member. */
1438 struct line *curline; /* Line currently being drawn. */
1439 enum line_type curtype; /* Attribute currently used for drawing. */
1440 unsigned long col; /* Column when drawing. */
1448 /* What type of content being displayed. Used in the title bar. */
1450 /* Open and reads in all view content. */
1451 bool (*open)(struct view *view);
1452 /* Read one line; updates view->line. */
1453 bool (*read)(struct view *view, char *data);
1454 /* Draw one line; @lineno must be < view->height. */
1455 bool (*draw)(struct view *view, struct line *line, unsigned int lineno);
1456 /* Depending on view handle a special requests. */
1457 enum request (*request)(struct view *view, enum request request, struct line *line);
1458 /* Search for regex in a line. */
1459 bool (*grep)(struct view *view, struct line *line);
1461 void (*select)(struct view *view, struct line *line);
1464 static struct view_ops blame_ops;
1465 static struct view_ops blob_ops;
1466 static struct view_ops help_ops;
1467 static struct view_ops log_ops;
1468 static struct view_ops main_ops;
1469 static struct view_ops pager_ops;
1470 static struct view_ops stage_ops;
1471 static struct view_ops status_ops;
1472 static struct view_ops tree_ops;
1474 #define VIEW_STR(name, cmd, env, ref, ops, map, git) \
1475 { name, cmd, #env, ref, ops, map, git }
1477 #define VIEW_(id, name, ops, git, ref) \
1478 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id, git)
1481 static struct view views[] = {
1482 VIEW_(MAIN, "main", &main_ops, TRUE, ref_head),
1483 VIEW_(DIFF, "diff", &pager_ops, TRUE, ref_commit),
1484 VIEW_(LOG, "log", &log_ops, TRUE, ref_head),
1485 VIEW_(TREE, "tree", &tree_ops, TRUE, ref_commit),
1486 VIEW_(BLOB, "blob", &blob_ops, TRUE, ref_blob),
1487 VIEW_(BLAME, "blame", &blame_ops, TRUE, ref_commit),
1488 VIEW_(HELP, "help", &help_ops, FALSE, ""),
1489 VIEW_(PAGER, "pager", &pager_ops, FALSE, "stdin"),
1490 VIEW_(STATUS, "status", &status_ops, TRUE, ""),
1491 VIEW_(STAGE, "stage", &stage_ops, TRUE, ""),
1494 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1495 #define VIEW_REQ(view) ((view) - views + REQ_OFFSET + 1)
1497 #define foreach_view(view, i) \
1498 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1500 #define view_is_displayed(view) \
1501 (view == display[0] || view == display[1])
1508 static int line_graphics[] = {
1509 /* LINE_GRAPHIC_VLINE: */ '|'
1513 set_view_attr(struct view *view, enum line_type type)
1515 if (!view->curline->selected && view->curtype != type) {
1516 wattrset(view->win, get_line_attr(type));
1517 wchgat(view->win, -1, 0, type, NULL);
1518 view->curtype = type;
1523 draw_chars(struct view *view, enum line_type type, const char *string,
1524 int max_len, bool use_tilde)
1528 int trimmed = FALSE;
1534 len = utf8_length(string, &col, max_len, &trimmed, use_tilde);
1536 col = len = strlen(string);
1537 if (len > max_len) {
1541 col = len = max_len;
1546 set_view_attr(view, type);
1547 waddnstr(view->win, string, len);
1548 if (trimmed && use_tilde) {
1549 set_view_attr(view, LINE_DELIMITER);
1550 waddch(view->win, '~');
1558 draw_space(struct view *view, enum line_type type, int max, int spaces)
1560 static char space[] = " ";
1563 spaces = MIN(max, spaces);
1565 while (spaces > 0) {
1566 int len = MIN(spaces, sizeof(space) - 1);
1568 col += draw_chars(view, type, space, spaces, FALSE);
1576 draw_lineno(struct view *view, unsigned int lineno)
1579 int digits3 = view->digits < 3 ? 3 : view->digits;
1580 int max_number = MIN(digits3, STRING_SIZE(number));
1581 int max = view->width - view->col;
1584 if (max < max_number)
1587 lineno += view->offset + 1;
1588 if (lineno == 1 || (lineno % opt_num_interval) == 0) {
1589 static char fmt[] = "%1ld";
1591 if (view->digits <= 9)
1592 fmt[1] = '0' + digits3;
1594 if (!string_format(number, fmt, lineno))
1596 col = draw_chars(view, LINE_LINE_NUMBER, number, max_number, TRUE);
1598 col = draw_space(view, LINE_LINE_NUMBER, max_number, max_number);
1602 set_view_attr(view, LINE_DEFAULT);
1603 waddch(view->win, line_graphics[LINE_GRAPHIC_VLINE]);
1608 col += draw_space(view, LINE_DEFAULT, max - col, 1);
1611 return view->width - view->col <= 0;
1615 draw_text(struct view *view, enum line_type type, const char *string, bool trim)
1617 view->col += draw_chars(view, type, string, view->width - view->col, trim);
1618 return view->width - view->col <= 0;
1622 draw_graphic(struct view *view, enum line_type type, chtype graphic[], size_t size)
1624 int max = view->width - view->col;
1630 set_view_attr(view, type);
1631 /* Using waddch() instead of waddnstr() ensures that
1632 * they'll be rendered correctly for the cursor line. */
1633 for (i = 0; i < size; i++)
1634 waddch(view->win, graphic[i]);
1638 waddch(view->win, ' ');
1642 return view->width - view->col <= 0;
1646 draw_field(struct view *view, enum line_type type, const char *text, int len, bool trim)
1648 int max = MIN(view->width - view->col, len);
1652 col = draw_chars(view, type, text, max - 1, trim);
1654 col = draw_space(view, type, max - 1, max - 1);
1656 view->col += col + draw_space(view, LINE_DEFAULT, max - col, max - col);
1657 return view->width - view->col <= 0;
1661 draw_date(struct view *view, struct tm *time)
1663 char buf[DATE_COLS];
1668 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, time);
1669 date = timelen ? buf : NULL;
1671 return draw_field(view, LINE_DATE, date, DATE_COLS, FALSE);
1675 draw_view_line(struct view *view, unsigned int lineno)
1678 bool selected = (view->offset + lineno == view->lineno);
1681 assert(view_is_displayed(view));
1683 if (view->offset + lineno >= view->lines)
1686 line = &view->line[view->offset + lineno];
1688 wmove(view->win, lineno, 0);
1690 view->curline = line;
1691 view->curtype = LINE_NONE;
1692 line->selected = FALSE;
1695 set_view_attr(view, LINE_CURSOR);
1696 line->selected = TRUE;
1697 view->ops->select(view, line);
1698 } else if (line->selected) {
1699 wclrtoeol(view->win);
1702 scrollok(view->win, FALSE);
1703 draw_ok = view->ops->draw(view, line, lineno);
1704 scrollok(view->win, TRUE);
1710 redraw_view_dirty(struct view *view)
1715 for (lineno = 0; lineno < view->height; lineno++) {
1716 struct line *line = &view->line[view->offset + lineno];
1722 if (!draw_view_line(view, lineno))
1728 redrawwin(view->win);
1730 wnoutrefresh(view->win);
1732 wrefresh(view->win);
1736 redraw_view_from(struct view *view, int lineno)
1738 assert(0 <= lineno && lineno < view->height);
1740 for (; lineno < view->height; lineno++) {
1741 if (!draw_view_line(view, lineno))
1745 redrawwin(view->win);
1747 wnoutrefresh(view->win);
1749 wrefresh(view->win);
1753 redraw_view(struct view *view)
1756 redraw_view_from(view, 0);
1761 update_view_title(struct view *view)
1763 char buf[SIZEOF_STR];
1764 char state[SIZEOF_STR];
1765 size_t bufpos = 0, statelen = 0;
1767 assert(view_is_displayed(view));
1769 if (view != VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) {
1770 unsigned int view_lines = view->offset + view->height;
1771 unsigned int lines = view->lines
1772 ? MIN(view_lines, view->lines) * 100 / view->lines
1775 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1782 time_t secs = time(NULL) - view->start_time;
1784 /* Three git seconds are a long time ... */
1786 string_format_from(state, &statelen, " %lds", secs);
1790 string_format_from(buf, &bufpos, "[%s]", view->name);
1791 if (*view->ref && bufpos < view->width) {
1792 size_t refsize = strlen(view->ref);
1793 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1795 if (minsize < view->width)
1796 refsize = view->width - minsize + 7;
1797 string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
1800 if (statelen && bufpos < view->width) {
1801 string_format_from(buf, &bufpos, " %s", state);
1804 if (view == display[current_view])
1805 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1807 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1809 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1810 wclrtoeol(view->title);
1811 wmove(view->title, 0, view->width - 1);
1814 wnoutrefresh(view->title);
1816 wrefresh(view->title);
1820 resize_display(void)
1823 struct view *base = display[0];
1824 struct view *view = display[1] ? display[1] : display[0];
1826 /* Setup window dimensions */
1828 getmaxyx(stdscr, base->height, base->width);
1830 /* Make room for the status window. */
1834 /* Horizontal split. */
1835 view->width = base->width;
1836 view->height = SCALE_SPLIT_VIEW(base->height);
1837 base->height -= view->height;
1839 /* Make room for the title bar. */
1843 /* Make room for the title bar. */
1848 foreach_displayed_view (view, i) {
1850 view->win = newwin(view->height, 0, offset, 0);
1852 die("Failed to create %s view", view->name);
1854 scrollok(view->win, TRUE);
1856 view->title = newwin(1, 0, offset + view->height, 0);
1858 die("Failed to create title window");
1861 wresize(view->win, view->height, view->width);
1862 mvwin(view->win, offset, 0);
1863 mvwin(view->title, offset + view->height, 0);
1866 offset += view->height + 1;
1871 redraw_display(void)
1876 foreach_displayed_view (view, i) {
1878 update_view_title(view);
1883 update_display_cursor(struct view *view)
1885 /* Move the cursor to the right-most column of the cursor line.
1887 * XXX: This could turn out to be a bit expensive, but it ensures that
1888 * the cursor does not jump around. */
1890 wmove(view->win, view->lineno - view->offset, view->width - 1);
1891 wrefresh(view->win);
1899 /* Scrolling backend */
1901 do_scroll_view(struct view *view, int lines)
1903 bool redraw_current_line = FALSE;
1905 /* The rendering expects the new offset. */
1906 view->offset += lines;
1908 assert(0 <= view->offset && view->offset < view->lines);
1911 /* Move current line into the view. */
1912 if (view->lineno < view->offset) {
1913 view->lineno = view->offset;
1914 redraw_current_line = TRUE;
1915 } else if (view->lineno >= view->offset + view->height) {
1916 view->lineno = view->offset + view->height - 1;
1917 redraw_current_line = TRUE;
1920 assert(view->offset <= view->lineno && view->lineno < view->lines);
1922 /* Redraw the whole screen if scrolling is pointless. */
1923 if (view->height < ABS(lines)) {
1927 int line = lines > 0 ? view->height - lines : 0;
1928 int end = line + ABS(lines);
1930 wscrl(view->win, lines);
1932 for (; line < end; line++) {
1933 if (!draw_view_line(view, line))
1937 if (redraw_current_line)
1938 draw_view_line(view, view->lineno - view->offset);
1941 redrawwin(view->win);
1942 wrefresh(view->win);
1946 /* Scroll frontend */
1948 scroll_view(struct view *view, enum request request)
1952 assert(view_is_displayed(view));
1955 case REQ_SCROLL_PAGE_DOWN:
1956 lines = view->height;
1957 case REQ_SCROLL_LINE_DOWN:
1958 if (view->offset + lines > view->lines)
1959 lines = view->lines - view->offset;
1961 if (lines == 0 || view->offset + view->height >= view->lines) {
1962 report("Cannot scroll beyond the last line");
1967 case REQ_SCROLL_PAGE_UP:
1968 lines = view->height;
1969 case REQ_SCROLL_LINE_UP:
1970 if (lines > view->offset)
1971 lines = view->offset;
1974 report("Cannot scroll beyond the first line");
1982 die("request %d not handled in switch", request);
1985 do_scroll_view(view, lines);
1990 move_view(struct view *view, enum request request)
1992 int scroll_steps = 0;
1996 case REQ_MOVE_FIRST_LINE:
1997 steps = -view->lineno;
2000 case REQ_MOVE_LAST_LINE:
2001 steps = view->lines - view->lineno - 1;
2004 case REQ_MOVE_PAGE_UP:
2005 steps = view->height > view->lineno
2006 ? -view->lineno : -view->height;
2009 case REQ_MOVE_PAGE_DOWN:
2010 steps = view->lineno + view->height >= view->lines
2011 ? view->lines - view->lineno - 1 : view->height;
2023 die("request %d not handled in switch", request);
2026 if (steps <= 0 && view->lineno == 0) {
2027 report("Cannot move beyond the first line");
2030 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
2031 report("Cannot move beyond the last line");
2035 /* Move the current line */
2036 view->lineno += steps;
2037 assert(0 <= view->lineno && view->lineno < view->lines);
2039 /* Check whether the view needs to be scrolled */
2040 if (view->lineno < view->offset ||
2041 view->lineno >= view->offset + view->height) {
2042 scroll_steps = steps;
2043 if (steps < 0 && -steps > view->offset) {
2044 scroll_steps = -view->offset;
2046 } else if (steps > 0) {
2047 if (view->lineno == view->lines - 1 &&
2048 view->lines > view->height) {
2049 scroll_steps = view->lines - view->offset - 1;
2050 if (scroll_steps >= view->height)
2051 scroll_steps -= view->height - 1;
2056 if (!view_is_displayed(view)) {
2057 view->offset += scroll_steps;
2058 assert(0 <= view->offset && view->offset < view->lines);
2059 view->ops->select(view, &view->line[view->lineno]);
2063 /* Repaint the old "current" line if we be scrolling */
2064 if (ABS(steps) < view->height)
2065 draw_view_line(view, view->lineno - steps - view->offset);
2068 do_scroll_view(view, scroll_steps);
2072 /* Draw the current line */
2073 draw_view_line(view, view->lineno - view->offset);
2075 redrawwin(view->win);
2076 wrefresh(view->win);
2085 static void search_view(struct view *view, enum request request);
2088 find_next_line(struct view *view, unsigned long lineno, struct line *line)
2090 assert(view_is_displayed(view));
2092 if (!view->ops->grep(view, line))
2095 if (lineno - view->offset >= view->height) {
2096 view->offset = lineno;
2097 view->lineno = lineno;
2101 unsigned long old_lineno = view->lineno - view->offset;
2103 view->lineno = lineno;
2104 draw_view_line(view, old_lineno);
2106 draw_view_line(view, view->lineno - view->offset);
2107 redrawwin(view->win);
2108 wrefresh(view->win);
2111 report("Line %ld matches '%s'", lineno + 1, view->grep);
2116 find_next(struct view *view, enum request request)
2118 unsigned long lineno = view->lineno;
2123 report("No previous search");
2125 search_view(view, request);
2135 case REQ_SEARCH_BACK:
2144 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
2145 lineno += direction;
2147 /* Note, lineno is unsigned long so will wrap around in which case it
2148 * will become bigger than view->lines. */
2149 for (; lineno < view->lines; lineno += direction) {
2150 struct line *line = &view->line[lineno];
2152 if (find_next_line(view, lineno, line))
2156 report("No match found for '%s'", view->grep);
2160 search_view(struct view *view, enum request request)
2165 regfree(view->regex);
2168 view->regex = calloc(1, sizeof(*view->regex));
2173 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
2174 if (regex_err != 0) {
2175 char buf[SIZEOF_STR] = "unknown error";
2177 regerror(regex_err, view->regex, buf, sizeof(buf));
2178 report("Search failed: %s", buf);
2182 string_copy(view->grep, opt_search);
2184 find_next(view, request);
2188 * Incremental updating
2192 reset_view(struct view *view)
2196 for (i = 0; i < view->lines; i++)
2197 free(view->line[i].data);
2204 view->line_size = 0;
2205 view->line_alloc = 0;
2210 end_update(struct view *view, bool force)
2214 while (!view->ops->read(view, NULL))
2217 set_nonblocking_input(FALSE);
2218 if (view->pipe == stdin)
2226 begin_update(struct view *view, bool refresh)
2229 string_copy(view->cmd, opt_cmd);
2231 /* When running random commands, initially show the
2232 * command in the title. However, it maybe later be
2233 * overwritten if a commit line is selected. */
2234 if (view == VIEW(REQ_VIEW_PAGER))
2235 string_copy(view->ref, view->cmd);
2239 } else if (view == VIEW(REQ_VIEW_TREE)) {
2240 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2241 char path[SIZEOF_STR];
2243 if (strcmp(view->vid, view->id))
2244 opt_path[0] = path[0] = 0;
2245 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
2248 if (!string_format(view->cmd, format, view->id, path))
2251 } else if (!refresh) {
2252 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2253 const char *id = view->id;
2255 if (!string_format(view->cmd, format, id, id, id, id, id))
2258 /* Put the current ref_* value to the view title ref
2259 * member. This is needed by the blob view. Most other
2260 * views sets it automatically after loading because the
2261 * first line is a commit line. */
2262 string_copy_rev(view->ref, view->id);
2265 /* Special case for the pager view. */
2267 view->pipe = opt_pipe;
2270 view->pipe = popen(view->cmd, "r");
2276 set_nonblocking_input(TRUE);
2278 string_copy_rev(view->vid, view->id);
2280 view->start_time = time(NULL);
2285 #define ITEM_CHUNK_SIZE 256
2287 realloc_items(void *mem, size_t *size, size_t new_size, size_t item_size)
2289 size_t num_chunks = *size / ITEM_CHUNK_SIZE;
2290 size_t num_chunks_new = (new_size + ITEM_CHUNK_SIZE - 1) / ITEM_CHUNK_SIZE;
2292 if (mem == NULL || num_chunks != num_chunks_new) {
2293 *size = num_chunks_new * ITEM_CHUNK_SIZE;
2294 mem = realloc(mem, *size * item_size);
2300 static struct line *
2301 realloc_lines(struct view *view, size_t line_size)
2303 size_t alloc = view->line_alloc;
2304 struct line *tmp = realloc_items(view->line, &alloc, line_size,
2305 sizeof(*view->line));
2311 view->line_alloc = alloc;
2312 view->line_size = line_size;
2317 update_view(struct view *view)
2319 char in_buffer[BUFSIZ];
2320 char out_buffer[BUFSIZ * 2];
2322 /* The number of lines to read. If too low it will cause too much
2323 * redrawing (and possible flickering), if too high responsiveness
2325 unsigned long lines = view->height;
2326 int redraw_from = -1;
2331 /* Only redraw if lines are visible. */
2332 if (view->offset + view->height >= view->lines)
2333 redraw_from = view->lines - view->offset;
2335 /* FIXME: This is probably not perfect for backgrounded views. */
2336 if (!realloc_lines(view, view->lines + lines))
2339 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
2340 size_t linelen = strlen(line);
2343 line[linelen - 1] = 0;
2345 if (opt_iconv != ICONV_NONE) {
2346 ICONV_CONST char *inbuf = line;
2347 size_t inlen = linelen;
2349 char *outbuf = out_buffer;
2350 size_t outlen = sizeof(out_buffer);
2354 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
2355 if (ret != (size_t) -1) {
2357 linelen = strlen(out_buffer);
2361 if (!view->ops->read(view, line))
2371 lines = view->lines;
2372 for (digits = 0; lines; digits++)
2375 /* Keep the displayed view in sync with line number scaling. */
2376 if (digits != view->digits) {
2377 view->digits = digits;
2382 if (ferror(view->pipe) && errno != 0) {
2383 report("Failed to read: %s", strerror(errno));
2384 end_update(view, TRUE);
2386 } else if (feof(view->pipe)) {
2388 end_update(view, FALSE);
2391 if (view == VIEW(REQ_VIEW_TREE)) {
2392 /* Clear the view and redraw everything since the tree sorting
2393 * might have rearranged things. */
2396 } else if (redraw_from >= 0) {
2397 /* If this is an incremental update, redraw the previous line
2398 * since for commits some members could have changed when
2399 * loading the main view. */
2400 if (redraw_from > 0)
2403 /* Since revision graph visualization requires knowledge
2404 * about the parent commit, it causes a further one-off
2405 * needed to be redrawn for incremental updates. */
2406 if (redraw_from > 0 && opt_rev_graph)
2409 /* Incrementally draw avoids flickering. */
2410 redraw_view_from(view, redraw_from);
2413 if (view == VIEW(REQ_VIEW_BLAME))
2414 redraw_view_dirty(view);
2416 /* Update the title _after_ the redraw so that if the redraw picks up a
2417 * commit reference in view->ref it'll be available here. */
2418 update_view_title(view);
2422 report("Allocation failure");
2423 end_update(view, TRUE);
2427 static struct line *
2428 add_line_data(struct view *view, void *data, enum line_type type)
2430 struct line *line = &view->line[view->lines++];
2432 memset(line, 0, sizeof(*line));
2439 static struct line *
2440 add_line_text(struct view *view, const char *text, enum line_type type)
2442 char *data = text ? strdup(text) : NULL;
2444 return data ? add_line_data(view, data, type) : NULL;
2453 OPEN_DEFAULT = 0, /* Use default view switching. */
2454 OPEN_SPLIT = 1, /* Split current view. */
2455 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2456 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2457 OPEN_NOMAXIMIZE = 8, /* Do not maximize the current view. */
2458 OPEN_REFRESH = 16, /* Refresh view using previous command. */
2462 open_view(struct view *prev, enum request request, enum open_flags flags)
2464 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2465 bool split = !!(flags & OPEN_SPLIT);
2466 bool reload = !!(flags & (OPEN_RELOAD | OPEN_REFRESH));
2467 bool nomaximize = !!(flags & (OPEN_NOMAXIMIZE | OPEN_REFRESH));
2468 struct view *view = VIEW(request);
2469 int nviews = displayed_views();
2470 struct view *base_view = display[0];
2472 if (view == prev && nviews == 1 && !reload) {
2473 report("Already in %s view", view->name);
2477 if (view->git_dir && !opt_git_dir[0]) {
2478 report("The %s view is disabled in pager view", view->name);
2486 } else if (!nomaximize) {
2487 /* Maximize the current view. */
2488 memset(display, 0, sizeof(display));
2490 display[current_view] = view;
2493 /* Resize the view when switching between split- and full-screen,
2494 * or when switching between two different full-screen views. */
2495 if (nviews != displayed_views() ||
2496 (nviews == 1 && base_view != display[0]))
2500 end_update(view, TRUE);
2502 if (view->ops->open) {
2503 if (!view->ops->open(view)) {
2504 report("Failed to load %s view", view->name);
2508 } else if ((reload || strcmp(view->vid, view->id)) &&
2509 !begin_update(view, flags & OPEN_REFRESH)) {
2510 report("Failed to load %s view", view->name);
2514 if (split && prev->lineno - prev->offset >= prev->height) {
2515 /* Take the title line into account. */
2516 int lines = prev->lineno - prev->offset - prev->height + 1;
2518 /* Scroll the view that was split if the current line is
2519 * outside the new limited view. */
2520 do_scroll_view(prev, lines);
2523 if (prev && view != prev) {
2524 if (split && !backgrounded) {
2525 /* "Blur" the previous view. */
2526 update_view_title(prev);
2529 view->parent = prev;
2532 if (view->pipe && view->lines == 0) {
2533 /* Clear the old view and let the incremental updating refill
2537 } else if (view_is_displayed(view)) {
2542 /* If the view is backgrounded the above calls to report()
2543 * won't redraw the view title. */
2545 update_view_title(view);
2549 run_confirm(const char *cmd, const char *prompt)
2551 bool confirmation = prompt_yesno(prompt);
2556 return confirmation;
2560 open_external_viewer(const char *cmd)
2562 def_prog_mode(); /* save current tty modes */
2563 endwin(); /* restore original tty modes */
2565 fprintf(stderr, "Press Enter to continue");
2572 open_mergetool(const char *file)
2574 char cmd[SIZEOF_STR];
2575 char file_sq[SIZEOF_STR];
2577 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2578 string_format(cmd, "git mergetool %s", file_sq)) {
2579 open_external_viewer(cmd);
2584 open_editor(bool from_root, const char *file)
2586 char cmd[SIZEOF_STR];
2587 char file_sq[SIZEOF_STR];
2589 char *prefix = from_root ? opt_cdup : "";
2591 editor = getenv("GIT_EDITOR");
2592 if (!editor && *opt_editor)
2593 editor = opt_editor;
2595 editor = getenv("VISUAL");
2597 editor = getenv("EDITOR");
2601 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2602 string_format(cmd, "%s %s%s", editor, prefix, file_sq)) {
2603 open_external_viewer(cmd);
2608 open_run_request(enum request request)
2610 struct run_request *req = get_run_request(request);
2611 char buf[SIZEOF_STR * 2];
2616 report("Unknown run request");
2624 char *next = strstr(cmd, "%(");
2625 int len = next - cmd;
2632 } else if (!strncmp(next, "%(head)", 7)) {
2635 } else if (!strncmp(next, "%(commit)", 9)) {
2638 } else if (!strncmp(next, "%(blob)", 7)) {
2642 report("Unknown replacement in run request: `%s`", req->cmd);
2646 if (!string_format_from(buf, &bufpos, "%.*s%s", len, cmd, value))
2650 next = strchr(next, ')') + 1;
2654 open_external_viewer(buf);
2658 * User request switch noodle
2662 view_driver(struct view *view, enum request request)
2666 if (request == REQ_NONE) {
2671 if (request > REQ_NONE) {
2672 open_run_request(request);
2673 /* FIXME: When all views can refresh always do this. */
2674 if (view == VIEW(REQ_VIEW_STATUS) ||
2675 view == VIEW(REQ_VIEW_MAIN) ||
2676 view == VIEW(REQ_VIEW_LOG) ||
2677 view == VIEW(REQ_VIEW_STAGE))
2678 request = REQ_REFRESH;
2683 if (view && view->lines) {
2684 request = view->ops->request(view, request, &view->line[view->lineno]);
2685 if (request == REQ_NONE)
2692 case REQ_MOVE_PAGE_UP:
2693 case REQ_MOVE_PAGE_DOWN:
2694 case REQ_MOVE_FIRST_LINE:
2695 case REQ_MOVE_LAST_LINE:
2696 move_view(view, request);
2699 case REQ_SCROLL_LINE_DOWN:
2700 case REQ_SCROLL_LINE_UP:
2701 case REQ_SCROLL_PAGE_DOWN:
2702 case REQ_SCROLL_PAGE_UP:
2703 scroll_view(view, request);
2706 case REQ_VIEW_BLAME:
2708 report("No file chosen, press %s to open tree view",
2709 get_key(REQ_VIEW_TREE));
2712 open_view(view, request, OPEN_DEFAULT);
2717 report("No file chosen, press %s to open tree view",
2718 get_key(REQ_VIEW_TREE));
2721 open_view(view, request, OPEN_DEFAULT);
2724 case REQ_VIEW_PAGER:
2725 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2726 report("No pager content, press %s to run command from prompt",
2727 get_key(REQ_PROMPT));
2730 open_view(view, request, OPEN_DEFAULT);
2733 case REQ_VIEW_STAGE:
2734 if (!VIEW(REQ_VIEW_STAGE)->lines) {
2735 report("No stage content, press %s to open the status view and choose file",
2736 get_key(REQ_VIEW_STATUS));
2739 open_view(view, request, OPEN_DEFAULT);
2742 case REQ_VIEW_STATUS:
2743 if (opt_is_inside_work_tree == FALSE) {
2744 report("The status view requires a working tree");
2747 open_view(view, request, OPEN_DEFAULT);
2755 open_view(view, request, OPEN_DEFAULT);
2760 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2762 if ((view == VIEW(REQ_VIEW_DIFF) &&
2763 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2764 (view == VIEW(REQ_VIEW_DIFF) &&
2765 view->parent == VIEW(REQ_VIEW_BLAME)) ||
2766 (view == VIEW(REQ_VIEW_STAGE) &&
2767 view->parent == VIEW(REQ_VIEW_STATUS)) ||
2768 (view == VIEW(REQ_VIEW_BLOB) &&
2769 view->parent == VIEW(REQ_VIEW_TREE))) {
2772 view = view->parent;
2773 line = view->lineno;
2774 move_view(view, request);
2775 if (view_is_displayed(view))
2776 update_view_title(view);
2777 if (line != view->lineno)
2778 view->ops->request(view, REQ_ENTER,
2779 &view->line[view->lineno]);
2782 move_view(view, request);
2788 int nviews = displayed_views();
2789 int next_view = (current_view + 1) % nviews;
2791 if (next_view == current_view) {
2792 report("Only one view is displayed");
2796 current_view = next_view;
2797 /* Blur out the title of the previous view. */
2798 update_view_title(view);
2803 report("Refreshing is not yet supported for the %s view", view->name);
2807 if (displayed_views() == 2)
2808 open_view(view, VIEW_REQ(view), OPEN_DEFAULT);
2811 case REQ_TOGGLE_LINENO:
2812 opt_line_number = !opt_line_number;
2816 case REQ_TOGGLE_DATE:
2817 opt_date = !opt_date;
2821 case REQ_TOGGLE_AUTHOR:
2822 opt_author = !opt_author;
2826 case REQ_TOGGLE_REV_GRAPH:
2827 opt_rev_graph = !opt_rev_graph;
2831 case REQ_TOGGLE_REFS:
2832 opt_show_refs = !opt_show_refs;
2837 case REQ_SEARCH_BACK:
2838 search_view(view, request);
2843 find_next(view, request);
2846 case REQ_STOP_LOADING:
2847 for (i = 0; i < ARRAY_SIZE(views); i++) {
2850 report("Stopped loading the %s view", view->name),
2851 end_update(view, TRUE);
2855 case REQ_SHOW_VERSION:
2856 report("tig-%s (built %s)", TIG_VERSION, __DATE__);
2859 case REQ_SCREEN_RESIZE:
2862 case REQ_SCREEN_REDRAW:
2867 report("Nothing to edit");
2871 report("Nothing to enter");
2874 case REQ_VIEW_CLOSE:
2875 /* XXX: Mark closed views by letting view->parent point to the
2876 * view itself. Parents to closed view should never be
2879 view->parent->parent != view->parent) {
2880 memset(display, 0, sizeof(display));
2882 display[current_view] = view->parent;
2883 view->parent = view;
2894 report("Unknown key, press 'h' for help");
2907 pager_draw(struct view *view, struct line *line, unsigned int lineno)
2909 char *text = line->data;
2911 if (opt_line_number && draw_lineno(view, lineno))
2914 draw_text(view, line->type, text, TRUE);
2919 add_describe_ref(char *buf, size_t *bufpos, const char *commit_id, const char *sep)
2921 char refbuf[SIZEOF_STR];
2925 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2928 pipe = popen(refbuf, "r");
2932 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2933 ref = chomp_string(ref);
2939 /* This is the only fatal call, since it can "corrupt" the buffer. */
2940 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2947 add_pager_refs(struct view *view, struct line *line)
2949 char buf[SIZEOF_STR];
2950 char *commit_id = (char *)line->data + STRING_SIZE("commit ");
2952 size_t bufpos = 0, refpos = 0;
2953 const char *sep = "Refs: ";
2954 bool is_tag = FALSE;
2956 assert(line->type == LINE_COMMIT);
2958 refs = get_refs(commit_id);
2960 if (view == VIEW(REQ_VIEW_DIFF))
2961 goto try_add_describe_ref;
2966 struct ref *ref = refs[refpos];
2967 const char *fmt = ref->tag ? "%s[%s]" :
2968 ref->remote ? "%s<%s>" : "%s%s";
2970 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2975 } while (refs[refpos++]->next);
2977 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2978 try_add_describe_ref:
2979 /* Add <tag>-g<commit_id> "fake" reference. */
2980 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2987 if (!realloc_lines(view, view->line_size + 1))
2990 add_line_text(view, buf, LINE_PP_REFS);
2994 pager_read(struct view *view, char *data)
3001 line = add_line_text(view, data, get_line_type(data));
3005 if (line->type == LINE_COMMIT &&
3006 (view == VIEW(REQ_VIEW_DIFF) ||
3007 view == VIEW(REQ_VIEW_LOG)))
3008 add_pager_refs(view, line);
3014 pager_request(struct view *view, enum request request, struct line *line)
3018 if (request != REQ_ENTER)
3021 if (line->type == LINE_COMMIT &&
3022 (view == VIEW(REQ_VIEW_LOG) ||
3023 view == VIEW(REQ_VIEW_PAGER))) {
3024 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
3028 /* Always scroll the view even if it was split. That way
3029 * you can use Enter to scroll through the log view and
3030 * split open each commit diff. */
3031 scroll_view(view, REQ_SCROLL_LINE_DOWN);
3033 /* FIXME: A minor workaround. Scrolling the view will call report("")
3034 * but if we are scrolling a non-current view this won't properly
3035 * update the view title. */
3037 update_view_title(view);
3043 pager_grep(struct view *view, struct line *line)
3046 char *text = line->data;
3051 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
3058 pager_select(struct view *view, struct line *line)
3060 if (line->type == LINE_COMMIT) {
3061 char *text = (char *)line->data + STRING_SIZE("commit ");
3063 if (view != VIEW(REQ_VIEW_PAGER))
3064 string_copy_rev(view->ref, text);
3065 string_copy_rev(ref_commit, text);
3069 static struct view_ops pager_ops = {
3080 log_request(struct view *view, enum request request, struct line *line)
3085 open_view(view, REQ_VIEW_LOG, OPEN_REFRESH);
3088 return pager_request(view, request, line);
3092 static struct view_ops log_ops = {
3108 help_open(struct view *view)
3111 int lines = ARRAY_SIZE(req_info) + 2;
3114 if (view->lines > 0)
3117 for (i = 0; i < ARRAY_SIZE(req_info); i++)
3118 if (!req_info[i].request)
3121 lines += run_requests + 1;
3123 view->line = calloc(lines, sizeof(*view->line));
3127 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
3129 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
3132 if (req_info[i].request == REQ_NONE)
3135 if (!req_info[i].request) {
3136 add_line_text(view, "", LINE_DEFAULT);
3137 add_line_text(view, req_info[i].help, LINE_DEFAULT);
3141 key = get_key(req_info[i].request);
3143 key = "(no key defined)";
3145 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
3148 add_line_text(view, buf, LINE_DEFAULT);
3152 add_line_text(view, "", LINE_DEFAULT);
3153 add_line_text(view, "External commands:", LINE_DEFAULT);
3156 for (i = 0; i < run_requests; i++) {
3157 struct run_request *req = get_run_request(REQ_NONE + i + 1);
3163 key = get_key_name(req->key);
3165 key = "(no key defined)";
3167 if (!string_format(buf, " %-10s %-14s `%s`",
3168 keymap_table[req->keymap].name,
3172 add_line_text(view, buf, LINE_DEFAULT);
3178 static struct view_ops help_ops = {
3193 struct tree_stack_entry {
3194 struct tree_stack_entry *prev; /* Entry below this in the stack */
3195 unsigned long lineno; /* Line number to restore */
3196 char *name; /* Position of name in opt_path */
3199 /* The top of the path stack. */
3200 static struct tree_stack_entry *tree_stack = NULL;
3201 unsigned long tree_lineno = 0;
3204 pop_tree_stack_entry(void)
3206 struct tree_stack_entry *entry = tree_stack;
3208 tree_lineno = entry->lineno;
3210 tree_stack = entry->prev;
3215 push_tree_stack_entry(const char *name, unsigned long lineno)
3217 struct tree_stack_entry *entry = calloc(1, sizeof(*entry));
3218 size_t pathlen = strlen(opt_path);
3223 entry->prev = tree_stack;
3224 entry->name = opt_path + pathlen;
3227 if (!string_format_from(opt_path, &pathlen, "%s/", name)) {
3228 pop_tree_stack_entry();
3232 /* Move the current line to the first tree entry. */
3234 entry->lineno = lineno;
3237 /* Parse output from git-ls-tree(1):
3239 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
3240 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
3241 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
3242 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
3245 #define SIZEOF_TREE_ATTR \
3246 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
3248 #define TREE_UP_FORMAT "040000 tree %s\t.."
3251 tree_compare_entry(enum line_type type1, const char *name1,
3252 enum line_type type2, const char *name2)
3254 if (type1 != type2) {
3255 if (type1 == LINE_TREE_DIR)
3260 return strcmp(name1, name2);
3264 tree_path(struct line *line)
3266 const char *path = line->data;
3268 return path + SIZEOF_TREE_ATTR;
3272 tree_read(struct view *view, char *text)
3274 size_t textlen = text ? strlen(text) : 0;
3275 char buf[SIZEOF_STR];
3277 enum line_type type;
3278 bool first_read = view->lines == 0;
3282 if (textlen <= SIZEOF_TREE_ATTR)
3285 type = text[STRING_SIZE("100644 ")] == 't'
3286 ? LINE_TREE_DIR : LINE_TREE_FILE;
3289 /* Add path info line */
3290 if (!string_format(buf, "Directory path /%s", opt_path) ||
3291 !realloc_lines(view, view->line_size + 1) ||
3292 !add_line_text(view, buf, LINE_DEFAULT))
3295 /* Insert "link" to parent directory. */
3297 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
3298 !realloc_lines(view, view->line_size + 1) ||
3299 !add_line_text(view, buf, LINE_TREE_DIR))
3304 /* Strip the path part ... */
3306 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
3307 size_t striplen = strlen(opt_path);
3308 char *path = text + SIZEOF_TREE_ATTR;
3310 if (pathlen > striplen)
3311 memmove(path, path + striplen,
3312 pathlen - striplen + 1);
3315 /* Skip "Directory ..." and ".." line. */
3316 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
3317 struct line *line = &view->line[pos];
3318 const char *path1 = tree_path(line);
3319 char *path2 = text + SIZEOF_TREE_ATTR;
3320 int cmp = tree_compare_entry(line->type, path1, type, path2);
3325 text = strdup(text);
3329 if (view->lines > pos)
3330 memmove(&view->line[pos + 1], &view->line[pos],
3331 (view->lines - pos) * sizeof(*line));
3333 line = &view->line[pos];
3340 if (!add_line_text(view, text, type))
3343 if (tree_lineno > view->lineno) {
3344 view->lineno = tree_lineno;
3352 tree_request(struct view *view, enum request request, struct line *line)
3354 enum open_flags flags;
3356 if (request == REQ_VIEW_BLAME) {
3357 const char *filename = tree_path(line);
3359 if (line->type == LINE_TREE_DIR) {
3360 report("Cannot show blame for directory %s", opt_path);
3364 string_copy(opt_ref, view->vid);
3365 string_format(opt_file, "%s%s", opt_path, filename);
3368 if (request == REQ_TREE_PARENT) {
3371 request = REQ_ENTER;
3372 line = &view->line[1];
3374 /* quit view if at top of tree */
3375 return REQ_VIEW_CLOSE;
3378 if (request != REQ_ENTER)
3381 /* Cleanup the stack if the tree view is at a different tree. */
3382 while (!*opt_path && tree_stack)
3383 pop_tree_stack_entry();
3385 switch (line->type) {
3387 /* Depending on whether it is a subdir or parent (updir?) link
3388 * mangle the path buffer. */
3389 if (line == &view->line[1] && *opt_path) {
3390 pop_tree_stack_entry();
3393 const char *basename = tree_path(line);
3395 push_tree_stack_entry(basename, view->lineno);
3398 /* Trees and subtrees share the same ID, so they are not not
3399 * unique like blobs. */
3400 flags = OPEN_RELOAD;
3401 request = REQ_VIEW_TREE;
3404 case LINE_TREE_FILE:
3405 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3406 request = REQ_VIEW_BLOB;
3413 open_view(view, request, flags);
3414 if (request == REQ_VIEW_TREE) {
3415 view->lineno = tree_lineno;
3422 tree_select(struct view *view, struct line *line)
3424 char *text = (char *)line->data + STRING_SIZE("100644 blob ");
3426 if (line->type == LINE_TREE_FILE) {
3427 string_copy_rev(ref_blob, text);
3429 } else if (line->type != LINE_TREE_DIR) {
3433 string_copy_rev(view->ref, text);
3436 static struct view_ops tree_ops = {
3447 blob_read(struct view *view, char *line)
3451 return add_line_text(view, line, LINE_DEFAULT) != NULL;
3454 static struct view_ops blob_ops = {
3467 * Loading the blame view is a two phase job:
3469 * 1. File content is read either using opt_file from the
3470 * filesystem or using git-cat-file.
3471 * 2. Then blame information is incrementally added by
3472 * reading output from git-blame.
3475 struct blame_commit {
3476 char id[SIZEOF_REV]; /* SHA1 ID. */
3477 char title[128]; /* First line of the commit message. */
3478 char author[75]; /* Author of the commit. */
3479 struct tm time; /* Date from the author ident. */
3480 char filename[128]; /* Name of file. */
3484 struct blame_commit *commit;
3485 unsigned int header:1;
3489 #define BLAME_CAT_FILE_CMD "git cat-file blob %s:%s"
3490 #define BLAME_INCREMENTAL_CMD "git blame --incremental %s -- %s"
3493 blame_open(struct view *view)
3495 char path[SIZEOF_STR];
3496 char ref[SIZEOF_STR] = "";
3498 if (sq_quote(path, 0, opt_file) >= sizeof(path))
3501 if (*opt_ref && sq_quote(ref, 0, opt_ref) >= sizeof(ref))
3505 if (!string_format(view->cmd, BLAME_CAT_FILE_CMD, ref, path))
3508 view->pipe = fopen(opt_file, "r");
3510 !string_format(view->cmd, BLAME_CAT_FILE_CMD, "HEAD", path))
3515 view->pipe = popen(view->cmd, "r");
3519 if (!string_format(view->cmd, BLAME_INCREMENTAL_CMD, ref, path))
3523 string_format(view->ref, "%s ...", opt_file);
3524 string_copy_rev(view->vid, opt_file);
3525 set_nonblocking_input(TRUE);
3526 view->start_time = time(NULL);
3531 static struct blame_commit *
3532 get_blame_commit(struct view *view, const char *id)
3536 for (i = 0; i < view->lines; i++) {
3537 struct blame *blame = view->line[i].data;
3542 if (!strncmp(blame->commit->id, id, SIZEOF_REV - 1))
3543 return blame->commit;
3547 struct blame_commit *commit = calloc(1, sizeof(*commit));
3550 string_ncopy(commit->id, id, SIZEOF_REV);
3556 parse_number(const char **posref, size_t *number, size_t min, size_t max)
3558 const char *pos = *posref;
3561 pos = strchr(pos + 1, ' ');
3562 if (!pos || !isdigit(pos[1]))
3564 *number = atoi(pos + 1);
3565 if (*number < min || *number > max)
3572 static struct blame_commit *
3573 parse_blame_commit(struct view *view, const char *text, int *blamed)
3575 struct blame_commit *commit;
3576 struct blame *blame;
3577 const char *pos = text + SIZEOF_REV - 1;
3581 if (strlen(text) <= SIZEOF_REV || *pos != ' ')
3584 if (!parse_number(&pos, &lineno, 1, view->lines) ||
3585 !parse_number(&pos, &group, 1, view->lines - lineno + 1))
3588 commit = get_blame_commit(view, text);
3594 struct line *line = &view->line[lineno + group - 1];
3597 blame->commit = commit;
3598 blame->header = !group;
3606 blame_read_file(struct view *view, const char *line)
3611 if (view->lines > 0)
3612 pipe = popen(view->cmd, "r");
3613 else if (!view->parent)
3614 die("No blame exist for %s", view->vid);
3617 report("Failed to load blame data");
3626 size_t linelen = strlen(line);
3627 struct blame *blame = malloc(sizeof(*blame) + linelen);
3629 blame->commit = NULL;
3630 strncpy(blame->text, line, linelen);
3631 blame->text[linelen] = 0;
3632 return add_line_data(view, blame, LINE_BLAME_ID) != NULL;
3637 match_blame_header(const char *name, char **line)
3639 size_t namelen = strlen(name);
3640 bool matched = !strncmp(name, *line, namelen);
3649 blame_read(struct view *view, char *line)
3651 static struct blame_commit *commit = NULL;
3652 static int blamed = 0;
3653 static time_t author_time;
3656 return blame_read_file(view, line);
3662 string_format(view->ref, "%s", view->vid);
3663 if (view_is_displayed(view)) {
3664 update_view_title(view);
3665 redraw_view_from(view, 0);
3671 commit = parse_blame_commit(view, line, &blamed);
3672 string_format(view->ref, "%s %2d%%", view->vid,
3673 blamed * 100 / view->lines);
3675 } else if (match_blame_header("author ", &line)) {
3676 string_ncopy(commit->author, line, strlen(line));
3678 } else if (match_blame_header("author-time ", &line)) {
3679 author_time = (time_t) atol(line);
3681 } else if (match_blame_header("author-tz ", &line)) {
3684 tz = ('0' - line[1]) * 60 * 60 * 10;
3685 tz += ('0' - line[2]) * 60 * 60;
3686 tz += ('0' - line[3]) * 60;
3687 tz += ('0' - line[4]) * 60;
3693 gmtime_r(&author_time, &commit->time);
3695 } else if (match_blame_header("summary ", &line)) {
3696 string_ncopy(commit->title, line, strlen(line));
3698 } else if (match_blame_header("filename ", &line)) {
3699 string_ncopy(commit->filename, line, strlen(line));
3707 blame_draw(struct view *view, struct line *line, unsigned int lineno)
3709 struct blame *blame = line->data;
3710 struct tm *time = NULL;
3711 const char *id = NULL, *author = NULL;
3713 if (blame->commit && *blame->commit->filename) {
3714 id = blame->commit->id;
3715 author = blame->commit->author;
3716 time = &blame->commit->time;
3719 if (opt_date && draw_date(view, time))
3723 draw_field(view, LINE_MAIN_AUTHOR, author, opt_author_cols, TRUE))
3726 if (draw_field(view, LINE_BLAME_ID, id, ID_COLS, FALSE))
3729 if (draw_lineno(view, lineno))
3732 draw_text(view, LINE_DEFAULT, blame->text, TRUE);
3737 blame_request(struct view *view, enum request request, struct line *line)
3739 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3740 struct blame *blame = line->data;
3744 if (!blame->commit) {
3745 report("No commit loaded yet");
3749 if (!strcmp(blame->commit->id, NULL_ID)) {
3750 char path[SIZEOF_STR];
3752 if (sq_quote(path, 0, view->vid) >= sizeof(path))
3754 string_format(opt_cmd, "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s 2>/dev/null", path);
3757 open_view(view, REQ_VIEW_DIFF, flags);
3768 blame_grep(struct view *view, struct line *line)
3770 struct blame *blame = line->data;
3771 struct blame_commit *commit = blame->commit;
3774 #define MATCH(text, on) \
3775 (on && *text && regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3778 char buf[DATE_COLS + 1];
3780 if (MATCH(commit->title, 1) ||
3781 MATCH(commit->author, opt_author) ||
3782 MATCH(commit->id, opt_date))
3785 if (strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time) &&
3790 return MATCH(blame->text, 1);
3796 blame_select(struct view *view, struct line *line)
3798 struct blame *blame = line->data;
3799 struct blame_commit *commit = blame->commit;
3804 if (!strcmp(commit->id, NULL_ID))
3805 string_ncopy(ref_commit, "HEAD", 4);
3807 string_copy_rev(ref_commit, commit->id);
3810 static struct view_ops blame_ops = {
3828 char rev[SIZEOF_REV];
3829 char name[SIZEOF_STR];
3833 char rev[SIZEOF_REV];
3834 char name[SIZEOF_STR];
3838 static char status_onbranch[SIZEOF_STR];
3839 static struct status stage_status;
3840 static enum line_type stage_line_type;
3841 static size_t stage_chunks;
3842 static int *stage_chunk;
3844 /* This should work even for the "On branch" line. */
3846 status_has_none(struct view *view, struct line *line)
3848 return line < view->line + view->lines && !line[1].data;
3851 /* Get fields from the diff line:
3852 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
3855 status_get_diff(struct status *file, const char *buf, size_t bufsize)
3857 const char *old_mode = buf + 1;
3858 const char *new_mode = buf + 8;
3859 const char *old_rev = buf + 15;
3860 const char *new_rev = buf + 56;
3861 const char *status = buf + 97;
3864 old_mode[-1] != ':' ||
3865 new_mode[-1] != ' ' ||
3866 old_rev[-1] != ' ' ||
3867 new_rev[-1] != ' ' ||
3871 file->status = *status;
3873 string_copy_rev(file->old.rev, old_rev);
3874 string_copy_rev(file->new.rev, new_rev);
3876 file->old.mode = strtoul(old_mode, NULL, 8);
3877 file->new.mode = strtoul(new_mode, NULL, 8);
3879 file->old.name[0] = file->new.name[0] = 0;
3885 status_run(struct view *view, const char cmd[], char status, enum line_type type)
3887 struct status *file = NULL;
3888 struct status *unmerged = NULL;
3889 char buf[SIZEOF_STR * 4];
3893 pipe = popen(cmd, "r");
3897 add_line_data(view, NULL, type);
3899 while (!feof(pipe) && !ferror(pipe)) {
3903 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
3906 bufsize += readsize;
3908 /* Process while we have NUL chars. */
3909 while ((sep = memchr(buf, 0, bufsize))) {
3910 size_t sepsize = sep - buf + 1;
3913 if (!realloc_lines(view, view->line_size + 1))
3916 file = calloc(1, sizeof(*file));
3920 add_line_data(view, file, type);
3923 /* Parse diff info part. */
3925 file->status = status;
3927 string_copy(file->old.rev, NULL_ID);
3929 } else if (!file->status) {
3930 if (!status_get_diff(file, buf, sepsize))
3934 memmove(buf, sep + 1, bufsize);
3936 sep = memchr(buf, 0, bufsize);
3939 sepsize = sep - buf + 1;
3941 /* Collapse all 'M'odified entries that
3942 * follow a associated 'U'nmerged entry.
3944 if (file->status == 'U') {
3947 } else if (unmerged) {
3948 int collapse = !strcmp(buf, unmerged->new.name);
3959 /* Grab the old name for rename/copy. */
3960 if (!*file->old.name &&
3961 (file->status == 'R' || file->status == 'C')) {
3962 sepsize = sep - buf + 1;
3963 string_ncopy(file->old.name, buf, sepsize);
3965 memmove(buf, sep + 1, bufsize);
3967 sep = memchr(buf, 0, bufsize);
3970 sepsize = sep - buf + 1;
3973 /* git-ls-files just delivers a NUL separated
3974 * list of file names similar to the second half
3975 * of the git-diff-* output. */
3976 string_ncopy(file->new.name, buf, sepsize);
3977 if (!*file->old.name)
3978 string_copy(file->old.name, file->new.name);
3980 memmove(buf, sep + 1, bufsize);
3991 if (!view->line[view->lines - 1].data)
3992 add_line_data(view, NULL, LINE_STAT_NONE);
3998 /* Don't show unmerged entries in the staged section. */
3999 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached -M HEAD"
4000 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
4001 #define STATUS_LIST_OTHER_CMD \
4002 "git ls-files -z --others --exclude-standard"
4003 #define STATUS_LIST_NO_HEAD_CMD \
4004 "git ls-files -z --cached --exclude-standard"
4006 #define STATUS_DIFF_INDEX_SHOW_CMD \
4007 "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s %s 2>/dev/null"
4009 #define STATUS_DIFF_FILES_SHOW_CMD \
4010 "git diff-files --root --patch-with-stat -C -M -- %s %s 2>/dev/null"
4012 #define STATUS_DIFF_NO_HEAD_SHOW_CMD \
4013 "git diff --no-color --patch-with-stat /dev/null %s 2>/dev/null"
4015 /* First parse staged info using git-diff-index(1), then parse unstaged
4016 * info using git-diff-files(1), and finally untracked files using
4017 * git-ls-files(1). */
4019 status_open(struct view *view)
4021 unsigned long prev_lineno = view->lineno;
4025 if (!realloc_lines(view, view->line_size + 7))
4028 add_line_data(view, NULL, LINE_STAT_HEAD);
4030 string_copy(status_onbranch, "Initial commit");
4031 else if (!*opt_head)
4032 string_copy(status_onbranch, "Not currently on any branch");
4033 else if (!string_format(status_onbranch, "On branch %s", opt_head))
4036 system("git update-index -q --refresh >/dev/null 2>/dev/null");
4039 if (!status_run(view, STATUS_LIST_NO_HEAD_CMD, 'A', LINE_STAT_STAGED))
4041 } else if (!status_run(view, STATUS_DIFF_INDEX_CMD, 0, LINE_STAT_STAGED)) {
4045 if (!status_run(view, STATUS_DIFF_FILES_CMD, 0, LINE_STAT_UNSTAGED) ||
4046 !status_run(view, STATUS_LIST_OTHER_CMD, '?', LINE_STAT_UNTRACKED))
4049 /* If all went well restore the previous line number to stay in
4050 * the context or select a line with something that can be
4052 if (prev_lineno >= view->lines)
4053 prev_lineno = view->lines - 1;
4054 while (prev_lineno < view->lines && !view->line[prev_lineno].data)
4056 while (prev_lineno > 0 && !view->line[prev_lineno].data)
4059 /* If the above fails, always skip the "On branch" line. */
4060 if (prev_lineno < view->lines)
4061 view->lineno = prev_lineno;
4065 if (view->lineno < view->offset)
4066 view->offset = view->lineno;
4067 else if (view->offset + view->height <= view->lineno)
4068 view->offset = view->lineno - view->height + 1;
4074 status_draw(struct view *view, struct line *line, unsigned int lineno)
4076 struct status *status = line->data;
4077 enum line_type type;
4081 switch (line->type) {
4082 case LINE_STAT_STAGED:
4083 type = LINE_STAT_SECTION;
4084 text = "Changes to be committed:";
4087 case LINE_STAT_UNSTAGED:
4088 type = LINE_STAT_SECTION;
4089 text = "Changed but not updated:";
4092 case LINE_STAT_UNTRACKED:
4093 type = LINE_STAT_SECTION;
4094 text = "Untracked files:";
4097 case LINE_STAT_NONE:
4098 type = LINE_DEFAULT;
4099 text = " (no files)";
4102 case LINE_STAT_HEAD:
4103 type = LINE_STAT_HEAD;
4104 text = status_onbranch;
4111 static char buf[] = { '?', ' ', ' ', ' ', 0 };
4113 buf[0] = status->status;
4114 if (draw_text(view, line->type, buf, TRUE))
4116 type = LINE_DEFAULT;
4117 text = status->new.name;
4120 draw_text(view, type, text, TRUE);
4125 status_enter(struct view *view, struct line *line)
4127 struct status *status = line->data;
4128 char oldpath[SIZEOF_STR] = "";
4129 char newpath[SIZEOF_STR] = "";
4132 enum open_flags split;
4134 if (line->type == LINE_STAT_NONE ||
4135 (!status && line[1].type == LINE_STAT_NONE)) {
4136 report("No file to diff");
4141 if (sq_quote(oldpath, 0, status->old.name) >= sizeof(oldpath))
4143 /* Diffs for unmerged entries are empty when pasing the
4144 * new path, so leave it empty. */
4145 if (status->status != 'U' &&
4146 sq_quote(newpath, 0, status->new.name) >= sizeof(newpath))
4151 line->type != LINE_STAT_UNTRACKED &&
4152 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
4155 switch (line->type) {
4156 case LINE_STAT_STAGED:
4158 if (!string_format_from(opt_cmd, &cmdsize,
4159 STATUS_DIFF_NO_HEAD_SHOW_CMD,
4163 if (!string_format_from(opt_cmd, &cmdsize,
4164 STATUS_DIFF_INDEX_SHOW_CMD,
4170 info = "Staged changes to %s";
4172 info = "Staged changes";
4175 case LINE_STAT_UNSTAGED:
4176 if (!string_format_from(opt_cmd, &cmdsize,
4177 STATUS_DIFF_FILES_SHOW_CMD, oldpath, newpath))
4180 info = "Unstaged changes to %s";
4182 info = "Unstaged changes";
4185 case LINE_STAT_UNTRACKED:
4190 report("No file to show");
4194 opt_pipe = fopen(status->new.name, "r");
4195 info = "Untracked file %s";
4198 case LINE_STAT_HEAD:
4202 die("line type %d not handled in switch", line->type);
4205 split = view_is_displayed(view) ? OPEN_SPLIT : 0;
4206 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | split);
4207 if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) {
4209 stage_status = *status;
4211 memset(&stage_status, 0, sizeof(stage_status));
4214 stage_line_type = line->type;
4216 string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.new.name);
4223 status_exists(struct status *status, enum line_type type)
4225 struct view *view = VIEW(REQ_VIEW_STATUS);
4228 for (line = view->line; line < view->line + view->lines; line++) {
4229 struct status *pos = line->data;
4231 if (line->type == type && pos &&
4232 !strcmp(status->new.name, pos->new.name))
4241 status_update_prepare(enum line_type type)
4243 char cmd[SIZEOF_STR];
4247 type != LINE_STAT_UNTRACKED &&
4248 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4252 case LINE_STAT_STAGED:
4253 string_add(cmd, cmdsize, "git update-index -z --index-info");
4256 case LINE_STAT_UNSTAGED:
4257 case LINE_STAT_UNTRACKED:
4258 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
4262 die("line type %d not handled in switch", type);
4265 return popen(cmd, "w");
4269 status_update_write(FILE *pipe, struct status *status, enum line_type type)
4271 char buf[SIZEOF_STR];
4276 case LINE_STAT_STAGED:
4277 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
4280 status->old.name, 0))
4284 case LINE_STAT_UNSTAGED:
4285 case LINE_STAT_UNTRACKED:
4286 if (!string_format_from(buf, &bufsize, "%s%c", status->new.name, 0))
4291 die("line type %d not handled in switch", type);
4294 while (!ferror(pipe) && written < bufsize) {
4295 written += fwrite(buf + written, 1, bufsize - written, pipe);
4298 return written == bufsize;
4302 status_update_file(struct status *status, enum line_type type)
4304 FILE *pipe = status_update_prepare(type);
4310 result = status_update_write(pipe, status, type);
4316 status_update_files(struct view *view, struct line *line)
4318 FILE *pipe = status_update_prepare(line->type);
4320 struct line *pos = view->line + view->lines;
4327 for (pos = line; pos < view->line + view->lines && pos->data; pos++)
4330 for (file = 0, done = 0; result && file < files; line++, file++) {
4331 int almost_done = file * 100 / files;
4333 if (almost_done > done) {
4335 string_format(view->ref, "updating file %u of %u (%d%% done)",
4337 update_view_title(view);
4339 result = status_update_write(pipe, line->data, line->type);
4347 status_update(struct view *view)
4349 struct line *line = &view->line[view->lineno];
4351 assert(view->lines);
4354 /* This should work even for the "On branch" line. */
4355 if (line < view->line + view->lines && !line[1].data) {
4356 report("Nothing to update");
4360 if (!status_update_files(view, line + 1)) {
4361 report("Failed to update file status");
4365 } else if (!status_update_file(line->data, line->type)) {
4366 report("Failed to update file status");
4374 status_revert(struct status *status, enum line_type type, bool has_none)
4376 if (!status || type != LINE_STAT_UNSTAGED) {
4377 if (type == LINE_STAT_STAGED) {
4378 report("Cannot revert changes to staged files");
4379 } else if (type == LINE_STAT_UNTRACKED) {
4380 report("Cannot revert changes to untracked files");
4381 } else if (has_none) {
4382 report("Nothing to revert");
4384 report("Cannot revert changes to multiple files");
4389 char cmd[SIZEOF_STR];
4390 char file_sq[SIZEOF_STR];
4392 if (sq_quote(file_sq, 0, status->old.name) >= sizeof(file_sq) ||
4393 !string_format(cmd, "git checkout -- %s%s", opt_cdup, file_sq))
4396 return run_confirm(cmd, "Are you sure you want to overwrite any changes?");
4401 status_request(struct view *view, enum request request, struct line *line)
4403 struct status *status = line->data;
4406 case REQ_STATUS_UPDATE:
4407 if (!status_update(view))
4411 case REQ_STATUS_REVERT:
4412 if (!status_revert(status, line->type, status_has_none(view, line)))
4416 case REQ_STATUS_MERGE:
4417 if (!status || status->status != 'U') {
4418 report("Merging only possible for files with unmerged status ('U').");
4421 open_mergetool(status->new.name);
4428 open_editor(status->status != '?', status->new.name);
4431 case REQ_VIEW_BLAME:
4433 string_copy(opt_file, status->new.name);
4439 /* After returning the status view has been split to
4440 * show the stage view. No further reloading is
4442 status_enter(view, line);
4446 /* Simply reload the view. */
4453 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
4459 status_select(struct view *view, struct line *line)
4461 struct status *status = line->data;
4462 char file[SIZEOF_STR] = "all files";
4466 if (status && !string_format(file, "'%s'", status->new.name))
4469 if (!status && line[1].type == LINE_STAT_NONE)
4472 switch (line->type) {
4473 case LINE_STAT_STAGED:
4474 text = "Press %s to unstage %s for commit";
4477 case LINE_STAT_UNSTAGED:
4478 text = "Press %s to stage %s for commit";
4481 case LINE_STAT_UNTRACKED:
4482 text = "Press %s to stage %s for addition";
4485 case LINE_STAT_HEAD:
4486 case LINE_STAT_NONE:
4487 text = "Nothing to update";
4491 die("line type %d not handled in switch", line->type);
4494 if (status && status->status == 'U') {
4495 text = "Press %s to resolve conflict in %s";
4496 key = get_key(REQ_STATUS_MERGE);
4499 key = get_key(REQ_STATUS_UPDATE);
4502 string_format(view->ref, text, key, file);
4506 status_grep(struct view *view, struct line *line)
4508 struct status *status = line->data;
4509 enum { S_STATUS, S_NAME, S_END } state;
4516 for (state = S_STATUS; state < S_END; state++) {
4520 case S_NAME: text = status->new.name; break;
4522 buf[0] = status->status;
4530 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4537 static struct view_ops status_ops = {
4549 stage_diff_line(FILE *pipe, struct line *line)
4551 const char *buf = line->data;
4552 size_t bufsize = strlen(buf);
4555 while (!ferror(pipe) && written < bufsize) {
4556 written += fwrite(buf + written, 1, bufsize - written, pipe);
4561 return written == bufsize;
4565 stage_diff_write(FILE *pipe, struct line *line, struct line *end)
4567 while (line < end) {
4568 if (!stage_diff_line(pipe, line++))
4570 if (line->type == LINE_DIFF_CHUNK ||
4571 line->type == LINE_DIFF_HEADER)
4578 static struct line *
4579 stage_diff_find(struct view *view, struct line *line, enum line_type type)
4581 for (; view->line < line; line--)
4582 if (line->type == type)
4589 stage_apply_chunk(struct view *view, struct line *chunk, bool revert)
4591 char cmd[SIZEOF_STR];
4593 struct line *diff_hdr;
4596 diff_hdr = stage_diff_find(view, chunk, LINE_DIFF_HEADER);
4601 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4604 if (!string_format_from(cmd, &cmdsize,
4605 "git apply --whitespace=nowarn %s %s - && "
4606 "git update-index -q --unmerged --refresh 2>/dev/null",
4607 revert ? "" : "--cached",
4608 revert || stage_line_type == LINE_STAT_STAGED ? "-R" : ""))
4611 pipe = popen(cmd, "w");
4615 if (!stage_diff_write(pipe, diff_hdr, chunk) ||
4616 !stage_diff_write(pipe, chunk, view->line + view->lines))
4621 return chunk ? TRUE : FALSE;
4625 stage_update(struct view *view, struct line *line)
4627 struct line *chunk = NULL;
4629 if (!opt_no_head && stage_line_type != LINE_STAT_UNTRACKED)
4630 chunk = stage_diff_find(view, line, LINE_DIFF_CHUNK);
4633 if (!stage_apply_chunk(view, chunk, FALSE)) {
4634 report("Failed to apply chunk");
4638 } else if (!stage_status.status) {
4639 view = VIEW(REQ_VIEW_STATUS);
4641 for (line = view->line; line < view->line + view->lines; line++)
4642 if (line->type == stage_line_type)
4645 if (!status_update_files(view, line + 1)) {
4646 report("Failed to update files");
4650 } else if (!status_update_file(&stage_status, stage_line_type)) {
4651 report("Failed to update file");
4659 stage_revert(struct view *view, struct line *line)
4661 struct line *chunk = NULL;
4663 if (!opt_no_head && stage_line_type == LINE_STAT_UNSTAGED)
4664 chunk = stage_diff_find(view, line, LINE_DIFF_CHUNK);
4667 if (!prompt_yesno("Are you sure you want to revert changes?"))
4670 if (!stage_apply_chunk(view, chunk, TRUE)) {
4671 report("Failed to revert chunk");
4677 return status_revert(stage_status.status ? &stage_status : NULL,
4678 stage_line_type, FALSE);
4684 stage_next(struct view *view, struct line *line)
4688 if (!stage_chunks) {
4689 static size_t alloc = 0;
4692 for (line = view->line; line < view->line + view->lines; line++) {
4693 if (line->type != LINE_DIFF_CHUNK)
4696 tmp = realloc_items(stage_chunk, &alloc,
4697 stage_chunks, sizeof(*tmp));
4699 report("Allocation failure");
4704 stage_chunk[stage_chunks++] = line - view->line;
4708 for (i = 0; i < stage_chunks; i++) {
4709 if (stage_chunk[i] > view->lineno) {
4710 do_scroll_view(view, stage_chunk[i] - view->lineno);
4711 report("Chunk %d of %d", i + 1, stage_chunks);
4716 report("No next chunk found");
4720 stage_request(struct view *view, enum request request, struct line *line)
4723 case REQ_STATUS_UPDATE:
4724 if (!stage_update(view, line))
4728 case REQ_STATUS_REVERT:
4729 if (!stage_revert(view, line))
4733 case REQ_STAGE_NEXT:
4734 if (stage_line_type == LINE_STAT_UNTRACKED) {
4735 report("File is untracked; press %s to add",
4736 get_key(REQ_STATUS_UPDATE));
4739 stage_next(view, line);
4743 if (!stage_status.new.name[0])
4746 open_editor(stage_status.status != '?', stage_status.new.name);
4750 /* Reload everything ... */
4753 case REQ_VIEW_BLAME:
4754 if (stage_status.new.name[0]) {
4755 string_copy(opt_file, stage_status.new.name);
4761 return pager_request(view, request, line);
4767 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD | OPEN_NOMAXIMIZE);
4769 /* Check whether the staged entry still exists, and close the
4770 * stage view if it doesn't. */
4771 if (!status_exists(&stage_status, stage_line_type))
4772 return REQ_VIEW_CLOSE;
4774 if (stage_line_type == LINE_STAT_UNTRACKED)
4775 opt_pipe = fopen(stage_status.new.name, "r");
4776 open_view(view, REQ_VIEW_STAGE, OPEN_REFRESH);
4781 static struct view_ops stage_ops = {
4797 char id[SIZEOF_REV]; /* SHA1 ID. */
4798 char title[128]; /* First line of the commit message. */
4799 char author[75]; /* Author of the commit. */
4800 struct tm time; /* Date from the author ident. */
4801 struct ref **refs; /* Repository references. */
4802 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
4803 size_t graph_size; /* The width of the graph array. */
4804 bool has_parents; /* Rewritten --parents seen. */
4807 /* Size of rev graph with no "padding" columns */
4808 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
4811 struct rev_graph *prev, *next, *parents;
4812 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
4814 struct commit *commit;
4816 unsigned int boundary:1;
4819 /* Parents of the commit being visualized. */
4820 static struct rev_graph graph_parents[4];
4822 /* The current stack of revisions on the graph. */
4823 static struct rev_graph graph_stacks[4] = {
4824 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
4825 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
4826 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
4827 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
4831 graph_parent_is_merge(struct rev_graph *graph)
4833 return graph->parents->size > 1;
4837 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
4839 struct commit *commit = graph->commit;
4841 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
4842 commit->graph[commit->graph_size++] = symbol;
4846 clear_rev_graph(struct rev_graph *graph)
4848 graph->boundary = 0;
4849 graph->size = graph->pos = 0;
4850 graph->commit = NULL;
4851 memset(graph->parents, 0, sizeof(*graph->parents));
4855 done_rev_graph(struct rev_graph *graph)
4857 if (graph_parent_is_merge(graph) &&
4858 graph->pos < graph->size - 1 &&
4859 graph->next->size == graph->size + graph->parents->size - 1) {
4860 size_t i = graph->pos + graph->parents->size - 1;
4862 graph->commit->graph_size = i * 2;
4863 while (i < graph->next->size - 1) {
4864 append_to_rev_graph(graph, ' ');
4865 append_to_rev_graph(graph, '\\');
4870 clear_rev_graph(graph);
4874 push_rev_graph(struct rev_graph *graph, const char *parent)
4878 /* "Collapse" duplicate parents lines.
4880 * FIXME: This needs to also update update the drawn graph but
4881 * for now it just serves as a method for pruning graph lines. */
4882 for (i = 0; i < graph->size; i++)
4883 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
4886 if (graph->size < SIZEOF_REVITEMS) {
4887 string_copy_rev(graph->rev[graph->size++], parent);
4892 get_rev_graph_symbol(struct rev_graph *graph)
4896 if (graph->boundary)
4897 symbol = REVGRAPH_BOUND;
4898 else if (graph->parents->size == 0)
4899 symbol = REVGRAPH_INIT;
4900 else if (graph_parent_is_merge(graph))
4901 symbol = REVGRAPH_MERGE;
4902 else if (graph->pos >= graph->size)
4903 symbol = REVGRAPH_BRANCH;
4905 symbol = REVGRAPH_COMMIT;
4911 draw_rev_graph(struct rev_graph *graph)
4914 chtype separator, line;
4916 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
4917 static struct rev_filler fillers[] = {
4923 chtype symbol = get_rev_graph_symbol(graph);
4924 struct rev_filler *filler;
4927 if (opt_line_graphics)
4928 fillers[DEFAULT].line = line_graphics[LINE_GRAPHIC_VLINE];
4930 filler = &fillers[DEFAULT];
4932 for (i = 0; i < graph->pos; i++) {
4933 append_to_rev_graph(graph, filler->line);
4934 if (graph_parent_is_merge(graph->prev) &&
4935 graph->prev->pos == i)
4936 filler = &fillers[RSHARP];
4938 append_to_rev_graph(graph, filler->separator);
4941 /* Place the symbol for this revision. */
4942 append_to_rev_graph(graph, symbol);
4944 if (graph->prev->size > graph->size)
4945 filler = &fillers[RDIAG];
4947 filler = &fillers[DEFAULT];
4951 for (; i < graph->size; i++) {
4952 append_to_rev_graph(graph, filler->separator);
4953 append_to_rev_graph(graph, filler->line);
4954 if (graph_parent_is_merge(graph->prev) &&
4955 i < graph->prev->pos + graph->parents->size)
4956 filler = &fillers[RSHARP];
4957 if (graph->prev->size > graph->size)
4958 filler = &fillers[LDIAG];
4961 if (graph->prev->size > graph->size) {
4962 append_to_rev_graph(graph, filler->separator);
4963 if (filler->line != ' ')
4964 append_to_rev_graph(graph, filler->line);
4968 /* Prepare the next rev graph */
4970 prepare_rev_graph(struct rev_graph *graph)
4974 /* First, traverse all lines of revisions up to the active one. */
4975 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
4976 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
4979 push_rev_graph(graph->next, graph->rev[graph->pos]);
4982 /* Interleave the new revision parent(s). */
4983 for (i = 0; !graph->boundary && i < graph->parents->size; i++)
4984 push_rev_graph(graph->next, graph->parents->rev[i]);
4986 /* Lastly, put any remaining revisions. */
4987 for (i = graph->pos + 1; i < graph->size; i++)
4988 push_rev_graph(graph->next, graph->rev[i]);
4992 update_rev_graph(struct rev_graph *graph)
4994 /* If this is the finalizing update ... */
4996 prepare_rev_graph(graph);
4998 /* Graph visualization needs a one rev look-ahead,
4999 * so the first update doesn't visualize anything. */
5000 if (!graph->prev->commit)
5003 draw_rev_graph(graph->prev);
5004 done_rev_graph(graph->prev->prev);
5013 main_draw(struct view *view, struct line *line, unsigned int lineno)
5015 struct commit *commit = line->data;
5017 if (!*commit->author)
5020 if (opt_date && draw_date(view, &commit->time))
5024 draw_field(view, LINE_MAIN_AUTHOR, commit->author, opt_author_cols, TRUE))
5027 if (opt_rev_graph && commit->graph_size &&
5028 draw_graphic(view, LINE_MAIN_REVGRAPH, commit->graph, commit->graph_size))
5031 if (opt_show_refs && commit->refs) {
5035 enum line_type type;
5037 if (commit->refs[i]->head)
5038 type = LINE_MAIN_HEAD;
5039 else if (commit->refs[i]->ltag)
5040 type = LINE_MAIN_LOCAL_TAG;
5041 else if (commit->refs[i]->tag)
5042 type = LINE_MAIN_TAG;
5043 else if (commit->refs[i]->tracked)
5044 type = LINE_MAIN_TRACKED;
5045 else if (commit->refs[i]->remote)
5046 type = LINE_MAIN_REMOTE;
5048 type = LINE_MAIN_REF;
5050 if (draw_text(view, type, "[", TRUE) ||
5051 draw_text(view, type, commit->refs[i]->name, TRUE) ||
5052 draw_text(view, type, "]", TRUE))
5055 if (draw_text(view, LINE_DEFAULT, " ", TRUE))
5057 } while (commit->refs[i++]->next);
5060 draw_text(view, LINE_DEFAULT, commit->title, TRUE);
5064 /* Reads git log --pretty=raw output and parses it into the commit struct. */
5066 main_read(struct view *view, char *line)
5068 static struct rev_graph *graph = graph_stacks;
5069 enum line_type type;
5070 struct commit *commit;
5075 if (!view->lines && !view->parent)
5076 die("No revisions match the given arguments.");
5077 if (view->lines > 0) {
5078 commit = view->line[view->lines - 1].data;
5079 if (!*commit->author) {
5082 graph->commit = NULL;
5085 update_rev_graph(graph);
5087 for (i = 0; i < ARRAY_SIZE(graph_stacks); i++)
5088 clear_rev_graph(&graph_stacks[i]);
5092 type = get_line_type(line);
5093 if (type == LINE_COMMIT) {
5094 commit = calloc(1, sizeof(struct commit));
5098 line += STRING_SIZE("commit ");
5100 graph->boundary = 1;
5104 string_copy_rev(commit->id, line);
5105 commit->refs = get_refs(commit->id);
5106 graph->commit = commit;
5107 add_line_data(view, commit, LINE_MAIN_COMMIT);
5109 while ((line = strchr(line, ' '))) {
5111 push_rev_graph(graph->parents, line);
5112 commit->has_parents = TRUE;
5119 commit = view->line[view->lines - 1].data;
5123 if (commit->has_parents)
5125 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
5130 /* Parse author lines where the name may be empty:
5131 * author <email@address.tld> 1138474660 +0100
5133 char *ident = line + STRING_SIZE("author ");
5134 char *nameend = strchr(ident, '<');
5135 char *emailend = strchr(ident, '>');
5137 if (!nameend || !emailend)
5140 update_rev_graph(graph);
5141 graph = graph->next;
5143 *nameend = *emailend = 0;
5144 ident = chomp_string(ident);
5146 ident = chomp_string(nameend + 1);
5151 string_ncopy(commit->author, ident, strlen(ident));
5153 /* Parse epoch and timezone */
5154 if (emailend[1] == ' ') {
5155 char *secs = emailend + 2;
5156 char *zone = strchr(secs, ' ');
5157 time_t time = (time_t) atol(secs);
5159 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
5163 tz = ('0' - zone[1]) * 60 * 60 * 10;
5164 tz += ('0' - zone[2]) * 60 * 60;
5165 tz += ('0' - zone[3]) * 60;
5166 tz += ('0' - zone[4]) * 60;
5174 gmtime_r(&time, &commit->time);
5179 /* Fill in the commit title if it has not already been set. */
5180 if (commit->title[0])
5183 /* Require titles to start with a non-space character at the
5184 * offset used by git log. */
5185 if (strncmp(line, " ", 4))
5188 /* Well, if the title starts with a whitespace character,
5189 * try to be forgiving. Otherwise we end up with no title. */
5190 while (isspace(*line))
5194 /* FIXME: More graceful handling of titles; append "..." to
5195 * shortened titles, etc. */
5197 string_ncopy(commit->title, line, strlen(line));
5204 main_request(struct view *view, enum request request, struct line *line)
5206 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
5210 open_view(view, REQ_VIEW_DIFF, flags);
5214 open_view(view, REQ_VIEW_MAIN, OPEN_REFRESH);
5224 grep_refs(struct ref **refs, regex_t *regex)
5232 if (regexec(regex, refs[i]->name, 1, &pmatch, 0) != REG_NOMATCH)
5234 } while (refs[i++]->next);
5240 main_grep(struct view *view, struct line *line)
5242 struct commit *commit = line->data;
5243 enum { S_TITLE, S_AUTHOR, S_DATE, S_REFS, S_END } state;
5244 char buf[DATE_COLS + 1];
5247 for (state = S_TITLE; state < S_END; state++) {
5251 case S_TITLE: text = commit->title; break;
5255 text = commit->author;
5260 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
5267 if (grep_refs(commit->refs, view->regex) == TRUE)
5274 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
5282 main_select(struct view *view, struct line *line)
5284 struct commit *commit = line->data;
5286 string_copy_rev(view->ref, commit->id);
5287 string_copy_rev(ref_commit, view->ref);
5290 static struct view_ops main_ops = {
5302 * Unicode / UTF-8 handling
5304 * NOTE: Much of the following code for dealing with unicode is derived from
5305 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
5306 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
5309 /* I've (over)annotated a lot of code snippets because I am not entirely
5310 * confident that the approach taken by this small UTF-8 interface is correct.
5314 unicode_width(unsigned long c)
5317 (c <= 0x115f /* Hangul Jamo */
5320 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
5322 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
5323 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
5324 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
5325 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
5326 || (c >= 0xffe0 && c <= 0xffe6)
5327 || (c >= 0x20000 && c <= 0x2fffd)
5328 || (c >= 0x30000 && c <= 0x3fffd)))
5332 return opt_tab_size;
5337 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
5338 * Illegal bytes are set one. */
5339 static const unsigned char utf8_bytes[256] = {
5340 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,
5341 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,
5342 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,
5343 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,
5344 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,
5345 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,
5346 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,
5347 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,
5350 /* Decode UTF-8 multi-byte representation into a unicode character. */
5351 static inline unsigned long
5352 utf8_to_unicode(const char *string, size_t length)
5354 unsigned long unicode;
5358 unicode = string[0];
5361 unicode = (string[0] & 0x1f) << 6;
5362 unicode += (string[1] & 0x3f);
5365 unicode = (string[0] & 0x0f) << 12;
5366 unicode += ((string[1] & 0x3f) << 6);
5367 unicode += (string[2] & 0x3f);
5370 unicode = (string[0] & 0x0f) << 18;
5371 unicode += ((string[1] & 0x3f) << 12);
5372 unicode += ((string[2] & 0x3f) << 6);
5373 unicode += (string[3] & 0x3f);
5376 unicode = (string[0] & 0x0f) << 24;
5377 unicode += ((string[1] & 0x3f) << 18);
5378 unicode += ((string[2] & 0x3f) << 12);
5379 unicode += ((string[3] & 0x3f) << 6);
5380 unicode += (string[4] & 0x3f);
5383 unicode = (string[0] & 0x01) << 30;
5384 unicode += ((string[1] & 0x3f) << 24);
5385 unicode += ((string[2] & 0x3f) << 18);
5386 unicode += ((string[3] & 0x3f) << 12);
5387 unicode += ((string[4] & 0x3f) << 6);
5388 unicode += (string[5] & 0x3f);
5391 die("Invalid unicode length");
5394 /* Invalid characters could return the special 0xfffd value but NUL
5395 * should be just as good. */
5396 return unicode > 0xffff ? 0 : unicode;
5399 /* Calculates how much of string can be shown within the given maximum width
5400 * and sets trimmed parameter to non-zero value if all of string could not be
5401 * shown. If the reserve flag is TRUE, it will reserve at least one
5402 * trailing character, which can be useful when drawing a delimiter.
5404 * Returns the number of bytes to output from string to satisfy max_width. */
5406 utf8_length(const char *string, int *width, size_t max_width, int *trimmed, bool reserve)
5408 const char *start = string;
5409 const char *end = strchr(string, '\0');
5410 unsigned char last_bytes = 0;
5411 size_t last_ucwidth = 0;
5416 while (string < end) {
5417 int c = *(unsigned char *) string;
5418 unsigned char bytes = utf8_bytes[c];
5420 unsigned long unicode;
5422 if (string + bytes > end)
5425 /* Change representation to figure out whether
5426 * it is a single- or double-width character. */
5428 unicode = utf8_to_unicode(string, bytes);
5429 /* FIXME: Graceful handling of invalid unicode character. */
5433 ucwidth = unicode_width(unicode);
5435 if (*width > max_width) {
5438 if (reserve && *width == max_width) {
5439 string -= last_bytes;
5440 *width -= last_ucwidth;
5447 last_ucwidth = ucwidth;
5450 return string - start;
5458 /* Whether or not the curses interface has been initialized. */
5459 static bool cursed = FALSE;
5461 /* The status window is used for polling keystrokes. */
5462 static WINDOW *status_win;
5464 static bool status_empty = TRUE;
5466 /* Update status and title window. */
5468 report(const char *msg, ...)
5470 struct view *view = display[current_view];
5476 char buf[SIZEOF_STR];
5479 va_start(args, msg);
5480 if (vsnprintf(buf, sizeof(buf), msg, args) >= sizeof(buf)) {
5481 buf[sizeof(buf) - 1] = 0;
5482 buf[sizeof(buf) - 2] = '.';
5483 buf[sizeof(buf) - 3] = '.';
5484 buf[sizeof(buf) - 4] = '.';
5490 if (!status_empty || *msg) {
5493 va_start(args, msg);
5495 wmove(status_win, 0, 0);
5497 vwprintw(status_win, msg, args);
5498 status_empty = FALSE;
5500 status_empty = TRUE;
5502 wclrtoeol(status_win);
5503 wrefresh(status_win);
5508 update_view_title(view);
5509 update_display_cursor(view);
5512 /* Controls when nodelay should be in effect when polling user input. */
5514 set_nonblocking_input(bool loading)
5516 static unsigned int loading_views;
5518 if ((loading == FALSE && loading_views-- == 1) ||
5519 (loading == TRUE && loading_views++ == 0))
5520 nodelay(status_win, loading);
5528 /* Initialize the curses library */
5529 if (isatty(STDIN_FILENO)) {
5530 cursed = !!initscr();
5533 /* Leave stdin and stdout alone when acting as a pager. */
5534 opt_tty = fopen("/dev/tty", "r+");
5536 die("Failed to open /dev/tty");
5537 cursed = !!newterm(NULL, opt_tty, opt_tty);
5541 die("Failed to initialize curses");
5543 nonl(); /* Tell curses not to do NL->CR/NL on output */
5544 cbreak(); /* Take input chars one at a time, no wait for \n */
5545 noecho(); /* Don't echo input */
5546 leaveok(stdscr, TRUE);
5551 getmaxyx(stdscr, y, x);
5552 status_win = newwin(1, 0, y - 1, 0);
5554 die("Failed to create status window");
5556 /* Enable keyboard mapping */
5557 keypad(status_win, TRUE);
5558 wbkgdset(status_win, get_line_attr(LINE_STATUS));
5560 TABSIZE = opt_tab_size;
5561 if (opt_line_graphics) {
5562 line_graphics[LINE_GRAPHIC_VLINE] = ACS_VLINE;
5567 prompt_yesno(const char *prompt)
5569 enum { WAIT, STOP, CANCEL } status = WAIT;
5570 bool answer = FALSE;
5572 while (status == WAIT) {
5578 foreach_view (view, i)
5583 mvwprintw(status_win, 0, 0, "%s [Yy]/[Nn]", prompt);
5584 wclrtoeol(status_win);
5586 /* Refresh, accept single keystroke of input */
5587 key = wgetch(status_win);
5611 /* Clear the status window */
5612 status_empty = FALSE;
5619 read_prompt(const char *prompt)
5621 enum { READING, STOP, CANCEL } status = READING;
5622 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
5625 while (status == READING) {
5631 foreach_view (view, i)
5636 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
5637 wclrtoeol(status_win);
5639 /* Refresh, accept single keystroke of input */
5640 key = wgetch(status_win);
5645 status = pos ? STOP : CANCEL;
5663 if (pos >= sizeof(buf)) {
5664 report("Input string too long");
5669 buf[pos++] = (char) key;
5673 /* Clear the status window */
5674 status_empty = FALSE;
5677 if (status == CANCEL)
5686 * Repository references
5689 static struct ref *refs = NULL;
5690 static size_t refs_alloc = 0;
5691 static size_t refs_size = 0;
5693 /* Id <-> ref store */
5694 static struct ref ***id_refs = NULL;
5695 static size_t id_refs_alloc = 0;
5696 static size_t id_refs_size = 0;
5699 compare_refs(const void *ref1_, const void *ref2_)
5701 const struct ref *ref1 = *(const struct ref **)ref1_;
5702 const struct ref *ref2 = *(const struct ref **)ref2_;
5704 if (ref1->tag != ref2->tag)
5705 return ref2->tag - ref1->tag;
5706 if (ref1->ltag != ref2->ltag)
5707 return ref2->ltag - ref2->ltag;
5708 if (ref1->head != ref2->head)
5709 return ref2->head - ref1->head;
5710 if (ref1->tracked != ref2->tracked)
5711 return ref2->tracked - ref1->tracked;
5712 if (ref1->remote != ref2->remote)
5713 return ref2->remote - ref1->remote;
5714 return strcmp(ref1->name, ref2->name);
5717 static struct ref **
5718 get_refs(const char *id)
5720 struct ref ***tmp_id_refs;
5721 struct ref **ref_list = NULL;
5722 size_t ref_list_alloc = 0;
5723 size_t ref_list_size = 0;
5726 for (i = 0; i < id_refs_size; i++)
5727 if (!strcmp(id, id_refs[i][0]->id))
5730 tmp_id_refs = realloc_items(id_refs, &id_refs_alloc, id_refs_size + 1,
5735 id_refs = tmp_id_refs;
5737 for (i = 0; i < refs_size; i++) {
5740 if (strcmp(id, refs[i].id))
5743 tmp = realloc_items(ref_list, &ref_list_alloc,
5744 ref_list_size + 1, sizeof(*ref_list));
5752 ref_list[ref_list_size] = &refs[i];
5753 /* XXX: The properties of the commit chains ensures that we can
5754 * safely modify the shared ref. The repo references will
5755 * always be similar for the same id. */
5756 ref_list[ref_list_size]->next = 1;
5762 qsort(ref_list, ref_list_size, sizeof(*ref_list), compare_refs);
5763 ref_list[ref_list_size - 1]->next = 0;
5764 id_refs[id_refs_size++] = ref_list;
5771 read_ref(char *id, size_t idlen, char *name, size_t namelen)
5776 bool remote = FALSE;
5777 bool tracked = FALSE;
5778 bool check_replace = FALSE;
5781 if (!prefixcmp(name, "refs/tags/")) {
5782 if (!strcmp(name + namelen - 3, "^{}")) {
5785 if (refs_size > 0 && refs[refs_size - 1].ltag == TRUE)
5786 check_replace = TRUE;
5792 namelen -= STRING_SIZE("refs/tags/");
5793 name += STRING_SIZE("refs/tags/");
5795 } else if (!prefixcmp(name, "refs/remotes/")) {
5797 namelen -= STRING_SIZE("refs/remotes/");
5798 name += STRING_SIZE("refs/remotes/");
5799 tracked = !strcmp(opt_remote, name);
5801 } else if (!prefixcmp(name, "refs/heads/")) {
5802 namelen -= STRING_SIZE("refs/heads/");
5803 name += STRING_SIZE("refs/heads/");
5804 head = !strncmp(opt_head, name, namelen);
5806 } else if (!strcmp(name, "HEAD")) {
5807 opt_no_head = FALSE;
5811 if (check_replace && !strcmp(name, refs[refs_size - 1].name)) {
5812 /* it's an annotated tag, replace the previous sha1 with the
5813 * resolved commit id; relies on the fact git-ls-remote lists
5814 * the commit id of an annotated tag right before the commit id
5816 refs[refs_size - 1].ltag = ltag;
5817 string_copy_rev(refs[refs_size - 1].id, id);
5821 refs = realloc_items(refs, &refs_alloc, refs_size + 1, sizeof(*refs));
5825 ref = &refs[refs_size++];
5826 ref->name = malloc(namelen + 1);
5830 strncpy(ref->name, name, namelen);
5831 ref->name[namelen] = 0;
5835 ref->remote = remote;
5836 ref->tracked = tracked;
5837 string_copy_rev(ref->id, id);
5845 const char *cmd_env = getenv("TIG_LS_REMOTE");
5846 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
5851 while (refs_size > 0)
5852 free(refs[--refs_size].name);
5853 while (id_refs_size > 0)
5854 free(id_refs[--id_refs_size]);
5856 return read_properties(popen(cmd, "r"), "\t", read_ref);
5860 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
5862 if (!strcmp(name, "i18n.commitencoding"))
5863 string_ncopy(opt_encoding, value, valuelen);
5865 if (!strcmp(name, "core.editor"))
5866 string_ncopy(opt_editor, value, valuelen);
5868 /* branch.<head>.remote */
5870 !strncmp(name, "branch.", 7) &&
5871 !strncmp(name + 7, opt_head, strlen(opt_head)) &&
5872 !strcmp(name + 7 + strlen(opt_head), ".remote"))
5873 string_ncopy(opt_remote, value, valuelen);
5875 if (*opt_head && *opt_remote &&
5876 !strncmp(name, "branch.", 7) &&
5877 !strncmp(name + 7, opt_head, strlen(opt_head)) &&
5878 !strcmp(name + 7 + strlen(opt_head), ".merge")) {
5879 size_t from = strlen(opt_remote);
5881 if (!prefixcmp(value, "refs/heads/")) {
5882 value += STRING_SIZE("refs/heads/");
5883 valuelen -= STRING_SIZE("refs/heads/");
5886 if (!string_format_from(opt_remote, &from, "/%s", value))
5894 load_git_config(void)
5896 return read_properties(popen("git " GIT_CONFIG " --list", "r"),
5897 "=", read_repo_config_option);
5901 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
5903 if (!opt_git_dir[0]) {
5904 string_ncopy(opt_git_dir, name, namelen);
5906 } else if (opt_is_inside_work_tree == -1) {
5907 /* This can be 3 different values depending on the
5908 * version of git being used. If git-rev-parse does not
5909 * understand --is-inside-work-tree it will simply echo
5910 * the option else either "true" or "false" is printed.
5911 * Default to true for the unknown case. */
5912 opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
5914 } else if (opt_cdup[0] == ' ') {
5915 string_ncopy(opt_cdup, name, namelen);
5917 if (!prefixcmp(name, "refs/heads/")) {
5918 namelen -= STRING_SIZE("refs/heads/");
5919 name += STRING_SIZE("refs/heads/");
5920 string_ncopy(opt_head, name, namelen);
5928 load_repo_info(void)
5931 FILE *pipe = popen("(git rev-parse --git-dir --is-inside-work-tree "
5932 " --show-cdup; git symbolic-ref HEAD) 2>/dev/null", "r");
5934 /* XXX: The line outputted by "--show-cdup" can be empty so
5935 * initialize it to something invalid to make it possible to
5936 * detect whether it has been set or not. */
5939 result = read_properties(pipe, "=", read_repo_info);
5940 if (opt_cdup[0] == ' ')
5947 read_properties(FILE *pipe, const char *separators,
5948 int (*read_property)(char *, size_t, char *, size_t))
5950 char buffer[BUFSIZ];
5957 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
5962 name = chomp_string(name);
5963 namelen = strcspn(name, separators);
5965 if (name[namelen]) {
5967 value = chomp_string(name + namelen + 1);
5968 valuelen = strlen(value);
5975 state = read_property(name, namelen, value, valuelen);
5978 if (state != ERR && ferror(pipe))
5991 static void __NORETURN
5994 /* XXX: Restore tty modes and let the OS cleanup the rest! */
6000 static void __NORETURN
6001 die(const char *err, ...)
6007 va_start(args, err);
6008 fputs("tig: ", stderr);
6009 vfprintf(stderr, err, args);
6010 fputs("\n", stderr);
6017 warn(const char *msg, ...)
6021 va_start(args, msg);
6022 fputs("tig warning: ", stderr);
6023 vfprintf(stderr, msg, args);
6024 fputs("\n", stderr);
6029 main(int argc, const char *argv[])
6032 enum request request;
6035 signal(SIGINT, quit);
6037 if (setlocale(LC_ALL, "")) {
6038 char *codeset = nl_langinfo(CODESET);
6040 string_ncopy(opt_codeset, codeset, strlen(codeset));
6043 if (load_repo_info() == ERR)
6044 die("Failed to load repo info.");
6046 if (load_options() == ERR)
6047 die("Failed to load user config.");
6049 if (load_git_config() == ERR)
6050 die("Failed to load repo config.");
6052 request = parse_options(argc, argv);
6053 if (request == REQ_NONE)
6056 /* Require a git repository unless when running in pager mode. */
6057 if (!opt_git_dir[0] && request != REQ_VIEW_PAGER)
6058 die("Not a git repository");
6060 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
6063 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
6064 opt_iconv = iconv_open(opt_codeset, opt_encoding);
6065 if (opt_iconv == ICONV_NONE)
6066 die("Failed to initialize character set conversion");
6069 if (load_refs() == ERR)
6070 die("Failed to load refs.");
6072 foreach_view (view, i)
6073 view->cmd_env = getenv(view->cmd_env);
6077 while (view_driver(display[current_view], request)) {
6081 foreach_view (view, i)
6083 view = display[current_view];
6085 /* Refresh, accept single keystroke of input */
6086 key = wgetch(status_win);
6088 /* wgetch() with nodelay() enabled returns ERR when there's no
6095 request = get_keybinding(view->keymap, key);
6097 /* Some low-level request handling. This keeps access to
6098 * status_win restricted. */
6102 char *cmd = read_prompt(":");
6104 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
6105 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
6106 request = REQ_VIEW_DIFF;
6108 request = REQ_VIEW_PAGER;
6111 /* Always reload^Wrerun commands from the prompt. */
6112 open_view(view, request, OPEN_RELOAD);
6119 case REQ_SEARCH_BACK:
6121 const char *prompt = request == REQ_SEARCH ? "/" : "?";
6122 char *search = read_prompt(prompt);
6125 string_ncopy(opt_search, search, strlen(search));
6130 case REQ_SCREEN_RESIZE:
6134 getmaxyx(stdscr, height, width);
6136 /* Resize the status view and let the view driver take
6137 * care of resizing the displayed views. */
6138 wresize(status_win, 1, width);
6139 mvwin(status_win, height - 1, 0);
6140 wrefresh(status_win);