1 /* Copyright (c) 2006-2008 Jonas Fonseca <fonseca@diku.dk>
3 * This program is free software; you can redistribute it and/or
4 * modify it under the terms of the GNU General Public License as
5 * published by the Free Software Foundation; either version 2 of
6 * the License, or (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
19 #define TIG_VERSION "unknown-version"
34 #include <sys/types.h>
45 /* ncurses(3): Must be defined to have extended wide-character functions. */
46 #define _XOPEN_SOURCE_EXTENDED
51 #define __NORETURN __attribute__((__noreturn__))
56 static void __NORETURN die(const char *err, ...);
57 static void warn(const char *msg, ...);
58 static void report(const char *msg, ...);
59 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, size_t, char *, size_t));
60 static void set_nonblocking_input(bool loading);
61 static size_t utf8_length(const char *string, int *width, size_t max_width, int *trimmed, bool reserve);
63 #define ABS(x) ((x) >= 0 ? (x) : -(x))
64 #define MIN(x, y) ((x) < (y) ? (x) : (y))
66 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
67 #define STRING_SIZE(x) (sizeof(x) - 1)
69 #define SIZEOF_STR 1024 /* Default string size. */
70 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
71 #define SIZEOF_REV 41 /* Holds a SHA-1 and an ending NUL */
75 #define REVGRAPH_INIT 'I'
76 #define REVGRAPH_MERGE 'M'
77 #define REVGRAPH_BRANCH '+'
78 #define REVGRAPH_COMMIT '*'
79 #define REVGRAPH_BOUND '^'
81 #define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
83 /* This color name can be used to refer to the default term colors. */
84 #define COLOR_DEFAULT (-1)
86 #define ICONV_NONE ((iconv_t) -1)
88 #define ICONV_CONST /* nothing */
91 /* The format and size of the date column in the main view. */
92 #define DATE_FORMAT "%Y-%m-%d %H:%M"
93 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
95 #define AUTHOR_COLS 20
98 /* The default interval between line numbers. */
99 #define NUMBER_INTERVAL 5
103 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
105 #define NULL_ID "0000000000000000000000000000000000000000"
108 #define GIT_CONFIG "git config"
111 #define TIG_LS_REMOTE \
112 "git ls-remote $(git rev-parse --git-dir) 2>/dev/null"
114 #define TIG_DIFF_CMD \
115 "git show --pretty=fuller --no-color --root --patch-with-stat --find-copies-harder -C %s 2>/dev/null"
117 #define TIG_LOG_CMD \
118 "git log --no-color --cc --stat -n100 %s 2>/dev/null"
120 #define TIG_MAIN_CMD \
121 "git log --no-color --topo-order --parents --boundary --pretty=raw %s 2>/dev/null"
123 #define TIG_TREE_CMD \
126 #define TIG_BLOB_CMD \
127 "git cat-file blob %s"
129 /* XXX: Needs to be defined to the empty string. */
130 #define TIG_HELP_CMD ""
131 #define TIG_PAGER_CMD ""
132 #define TIG_STATUS_CMD ""
133 #define TIG_STAGE_CMD ""
134 #define TIG_BLAME_CMD ""
136 /* Some ascii-shorthands fitted into the ncurses namespace. */
138 #define KEY_RETURN '\r'
143 char *name; /* Ref name; tag or head names are shortened. */
144 char id[SIZEOF_REV]; /* Commit SHA1 ID */
145 unsigned int head:1; /* Is it the current HEAD? */
146 unsigned int tag:1; /* Is it a tag? */
147 unsigned int ltag:1; /* If so, is the tag local? */
148 unsigned int remote:1; /* Is it a remote ref? */
149 unsigned int tracked:1; /* Is it the remote for the current HEAD? */
150 unsigned int next:1; /* For ref lists: are there more refs? */
153 static struct ref **get_refs(char *id);
162 set_from_int_map(struct int_map *map, size_t map_size,
163 int *value, const char *name, int namelen)
168 for (i = 0; i < map_size; i++)
169 if (namelen == map[i].namelen &&
170 !strncasecmp(name, map[i].name, namelen)) {
171 *value = map[i].value;
184 string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
186 if (srclen > dstlen - 1)
189 strncpy(dst, src, srclen);
193 /* Shorthands for safely copying into a fixed buffer. */
195 #define string_copy(dst, src) \
196 string_ncopy_do(dst, sizeof(dst), src, sizeof(src))
198 #define string_ncopy(dst, src, srclen) \
199 string_ncopy_do(dst, sizeof(dst), src, srclen)
201 #define string_copy_rev(dst, src) \
202 string_ncopy_do(dst, SIZEOF_REV, src, SIZEOF_REV - 1)
204 #define string_add(dst, from, src) \
205 string_ncopy_do(dst + (from), sizeof(dst) - (from), src, sizeof(src))
208 chomp_string(char *name)
212 while (isspace(*name))
215 namelen = strlen(name) - 1;
216 while (namelen > 0 && isspace(name[namelen]))
223 string_nformat(char *buf, size_t bufsize, size_t *bufpos, const char *fmt, ...)
226 size_t pos = bufpos ? *bufpos : 0;
229 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
235 return pos >= bufsize ? FALSE : TRUE;
238 #define string_format(buf, fmt, args...) \
239 string_nformat(buf, sizeof(buf), NULL, fmt, args)
241 #define string_format_from(buf, from, fmt, args...) \
242 string_nformat(buf, sizeof(buf), from, fmt, args)
245 string_enum_compare(const char *str1, const char *str2, int len)
249 #define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
251 /* Diff-Header == DIFF_HEADER */
252 for (i = 0; i < len; i++) {
253 if (toupper(str1[i]) == toupper(str2[i]))
256 if (string_enum_sep(str1[i]) &&
257 string_enum_sep(str2[i]))
260 return str1[i] - str2[i];
268 * NOTE: The following is a slightly modified copy of the git project's shell
269 * quoting routines found in the quote.c file.
271 * Help to copy the thing properly quoted for the shell safety. any single
272 * quote is replaced with '\'', any exclamation point is replaced with '\!',
273 * and the whole thing is enclosed in a
276 * original sq_quote result
277 * name ==> name ==> 'name'
278 * a b ==> a b ==> 'a b'
279 * a'b ==> a'\''b ==> 'a'\''b'
280 * a!b ==> a'\!'b ==> 'a'\!'b'
284 sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
288 #define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0)
291 while ((c = *src++)) {
292 if (c == '\'' || c == '!') {
303 if (bufsize < SIZEOF_STR)
315 /* XXX: Keep the view request first and in sync with views[]. */ \
316 REQ_GROUP("View switching") \
317 REQ_(VIEW_MAIN, "Show main view"), \
318 REQ_(VIEW_DIFF, "Show diff view"), \
319 REQ_(VIEW_LOG, "Show log view"), \
320 REQ_(VIEW_TREE, "Show tree view"), \
321 REQ_(VIEW_BLOB, "Show blob view"), \
322 REQ_(VIEW_BLAME, "Show blame view"), \
323 REQ_(VIEW_HELP, "Show help page"), \
324 REQ_(VIEW_PAGER, "Show pager view"), \
325 REQ_(VIEW_STATUS, "Show status view"), \
326 REQ_(VIEW_STAGE, "Show stage view"), \
328 REQ_GROUP("View manipulation") \
329 REQ_(ENTER, "Enter current line and scroll"), \
330 REQ_(NEXT, "Move to next"), \
331 REQ_(PREVIOUS, "Move to previous"), \
332 REQ_(VIEW_NEXT, "Move focus to next view"), \
333 REQ_(REFRESH, "Reload and refresh"), \
334 REQ_(MAXIMIZE, "Maximize the current view"), \
335 REQ_(VIEW_CLOSE, "Close the current view"), \
336 REQ_(QUIT, "Close all views and quit"), \
338 REQ_GROUP("Cursor navigation") \
339 REQ_(MOVE_UP, "Move cursor one line up"), \
340 REQ_(MOVE_DOWN, "Move cursor one line down"), \
341 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
342 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
343 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
344 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
346 REQ_GROUP("Scrolling") \
347 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
348 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
349 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
350 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
352 REQ_GROUP("Searching") \
353 REQ_(SEARCH, "Search the view"), \
354 REQ_(SEARCH_BACK, "Search backwards in the view"), \
355 REQ_(FIND_NEXT, "Find next search match"), \
356 REQ_(FIND_PREV, "Find previous search match"), \
359 REQ_(PROMPT, "Bring up the prompt"), \
360 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
361 REQ_(SCREEN_RESIZE, "Resize the screen"), \
362 REQ_(SHOW_VERSION, "Show version information"), \
363 REQ_(STOP_LOADING, "Stop all loading views"), \
364 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
365 REQ_(TOGGLE_DATE, "Toggle date display"), \
366 REQ_(TOGGLE_AUTHOR, "Toggle author display"), \
367 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"), \
368 REQ_(TOGGLE_REFS, "Toggle reference display (tags/branches)"), \
369 REQ_(STATUS_UPDATE, "Update file status"), \
370 REQ_(STATUS_MERGE, "Merge file using external tool"), \
371 REQ_(TREE_PARENT, "Switch to parent directory in tree view"), \
372 REQ_(EDIT, "Open in editor"), \
373 REQ_(NONE, "Do nothing")
376 /* User action requests. */
378 #define REQ_GROUP(help)
379 #define REQ_(req, help) REQ_##req
381 /* Offset all requests to avoid conflicts with ncurses getch values. */
382 REQ_OFFSET = KEY_MAX + 1,
389 struct request_info {
390 enum request request;
396 static struct request_info req_info[] = {
397 #define REQ_GROUP(help) { 0, NULL, 0, (help) },
398 #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
405 get_request(const char *name)
407 int namelen = strlen(name);
410 for (i = 0; i < ARRAY_SIZE(req_info); i++)
411 if (req_info[i].namelen == namelen &&
412 !string_enum_compare(req_info[i].name, name, namelen))
413 return req_info[i].request;
423 static const char usage[] =
424 "tig " TIG_VERSION " (" __DATE__ ")\n"
426 "Usage: tig [options] [revs] [--] [paths]\n"
427 " or: tig show [options] [revs] [--] [paths]\n"
428 " or: tig blame [rev] path\n"
430 " or: tig < [git command output]\n"
433 " -v, --version Show version and exit\n"
434 " -h, --help Show help message and exit";
436 /* Option and state variables. */
437 static bool opt_date = TRUE;
438 static bool opt_author = TRUE;
439 static bool opt_line_number = FALSE;
440 static bool opt_line_graphics = TRUE;
441 static bool opt_rev_graph = FALSE;
442 static bool opt_show_refs = TRUE;
443 static int opt_num_interval = NUMBER_INTERVAL;
444 static int opt_tab_size = TAB_SIZE;
445 static enum request opt_request = REQ_VIEW_MAIN;
446 static char opt_cmd[SIZEOF_STR] = "";
447 static char opt_path[SIZEOF_STR] = "";
448 static char opt_file[SIZEOF_STR] = "";
449 static char opt_ref[SIZEOF_REF] = "";
450 static char opt_head[SIZEOF_REF] = "";
451 static char opt_remote[SIZEOF_REF] = "";
452 static bool opt_no_head = TRUE;
453 static FILE *opt_pipe = NULL;
454 static char opt_encoding[20] = "UTF-8";
455 static bool opt_utf8 = TRUE;
456 static char opt_codeset[20] = "UTF-8";
457 static iconv_t opt_iconv = ICONV_NONE;
458 static char opt_search[SIZEOF_STR] = "";
459 static char opt_cdup[SIZEOF_STR] = "";
460 static char opt_git_dir[SIZEOF_STR] = "";
461 static signed char opt_is_inside_work_tree = -1; /* set to TRUE or FALSE */
462 static char opt_editor[SIZEOF_STR] = "";
465 parse_options(int argc, char *argv[])
469 bool seen_dashdash = FALSE;
472 if (!isatty(STDIN_FILENO)) {
473 opt_request = REQ_VIEW_PAGER;
481 subcommand = argv[1];
482 if (!strcmp(subcommand, "status") || !strcmp(subcommand, "-S")) {
483 opt_request = REQ_VIEW_STATUS;
484 if (!strcmp(subcommand, "-S"))
485 warn("`-S' has been deprecated; use `tig status' instead");
487 warn("ignoring arguments after `%s'", subcommand);
490 } else if (!strcmp(subcommand, "blame")) {
491 opt_request = REQ_VIEW_BLAME;
492 if (argc <= 2 || argc > 4)
493 die("invalid number of options to blame\n\n%s", usage);
497 string_ncopy(opt_ref, argv[i], strlen(argv[i]));
501 string_ncopy(opt_file, argv[i], strlen(argv[i]));
504 } else if (!strcmp(subcommand, "show")) {
505 opt_request = REQ_VIEW_DIFF;
507 } else if (!strcmp(subcommand, "log") || !strcmp(subcommand, "diff")) {
508 opt_request = subcommand[0] == 'l'
509 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
510 warn("`tig %s' has been deprecated", subcommand);
517 /* XXX: This is vulnerable to the user overriding
518 * options required for the main view parser. */
519 string_copy(opt_cmd, "git log --no-color --pretty=raw --boundary --parents");
521 string_format(opt_cmd, "git %s", subcommand);
523 buf_size = strlen(opt_cmd);
525 for (i = 1 + !!subcommand; i < argc; i++) {
528 if (seen_dashdash || !strcmp(opt, "--")) {
529 seen_dashdash = TRUE;
531 } else if (!strcmp(opt, "-v") || !strcmp(opt, "--version")) {
532 printf("tig version %s\n", TIG_VERSION);
535 } else if (!strcmp(opt, "-h") || !strcmp(opt, "--help")) {
536 printf("%s\n", usage);
540 opt_cmd[buf_size++] = ' ';
541 buf_size = sq_quote(opt_cmd, buf_size, opt);
542 if (buf_size >= sizeof(opt_cmd))
543 die("command too long");
546 opt_cmd[buf_size] = 0;
553 * Line-oriented content detection.
557 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
558 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
559 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
560 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
561 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
562 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
563 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
564 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
565 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
566 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
567 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
568 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
569 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
570 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
571 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
572 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
573 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
574 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
575 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
576 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
577 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
578 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
579 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
580 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
581 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
582 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
583 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
584 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
585 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
586 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
587 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
588 LINE(DELIMITER, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
589 LINE(DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
590 LINE(LINE_NUMBER, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
591 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
592 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
593 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
594 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
595 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
596 LINE(MAIN_LOCAL_TAG,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
597 LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \
598 LINE(MAIN_TRACKED, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
599 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
600 LINE(MAIN_HEAD, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
601 LINE(MAIN_REVGRAPH,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
602 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
603 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
604 LINE(STAT_HEAD, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \
605 LINE(STAT_SECTION, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
606 LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
607 LINE(STAT_STAGED, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
608 LINE(STAT_UNSTAGED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
609 LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
610 LINE(BLAME_ID, "", COLOR_MAGENTA, COLOR_DEFAULT, 0)
613 #define LINE(type, line, fg, bg, attr) \
621 const char *name; /* Option name. */
622 int namelen; /* Size of option name. */
623 const char *line; /* The start of line to match. */
624 int linelen; /* Size of string to match. */
625 int fg, bg, attr; /* Color and text attributes for the lines. */
628 static struct line_info line_info[] = {
629 #define LINE(type, line, fg, bg, attr) \
630 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
635 static enum line_type
636 get_line_type(char *line)
638 int linelen = strlen(line);
641 for (type = 0; type < ARRAY_SIZE(line_info); type++)
642 /* Case insensitive search matches Signed-off-by lines better. */
643 if (linelen >= line_info[type].linelen &&
644 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
651 get_line_attr(enum line_type type)
653 assert(type < ARRAY_SIZE(line_info));
654 return COLOR_PAIR(type) | line_info[type].attr;
657 static struct line_info *
658 get_line_info(char *name)
660 size_t namelen = strlen(name);
663 for (type = 0; type < ARRAY_SIZE(line_info); type++)
664 if (namelen == line_info[type].namelen &&
665 !string_enum_compare(line_info[type].name, name, namelen))
666 return &line_info[type];
674 int default_bg = line_info[LINE_DEFAULT].bg;
675 int default_fg = line_info[LINE_DEFAULT].fg;
680 if (assume_default_colors(default_fg, default_bg) == ERR) {
681 default_bg = COLOR_BLACK;
682 default_fg = COLOR_WHITE;
685 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
686 struct line_info *info = &line_info[type];
687 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
688 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
690 init_pair(type, fg, bg);
698 unsigned int selected:1;
699 unsigned int dirty:1;
701 void *data; /* User data */
711 enum request request;
712 struct keybinding *next;
715 static struct keybinding default_keybindings[] = {
717 { 'm', REQ_VIEW_MAIN },
718 { 'd', REQ_VIEW_DIFF },
719 { 'l', REQ_VIEW_LOG },
720 { 't', REQ_VIEW_TREE },
721 { 'f', REQ_VIEW_BLOB },
722 { 'B', REQ_VIEW_BLAME },
723 { 'p', REQ_VIEW_PAGER },
724 { 'h', REQ_VIEW_HELP },
725 { 'S', REQ_VIEW_STATUS },
726 { 'c', REQ_VIEW_STAGE },
728 /* View manipulation */
729 { 'q', REQ_VIEW_CLOSE },
730 { KEY_TAB, REQ_VIEW_NEXT },
731 { KEY_RETURN, REQ_ENTER },
732 { KEY_UP, REQ_PREVIOUS },
733 { KEY_DOWN, REQ_NEXT },
734 { 'R', REQ_REFRESH },
735 { KEY_F(5), REQ_REFRESH },
736 { 'O', REQ_MAXIMIZE },
738 /* Cursor navigation */
739 { 'k', REQ_MOVE_UP },
740 { 'j', REQ_MOVE_DOWN },
741 { KEY_HOME, REQ_MOVE_FIRST_LINE },
742 { KEY_END, REQ_MOVE_LAST_LINE },
743 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
744 { ' ', REQ_MOVE_PAGE_DOWN },
745 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
746 { 'b', REQ_MOVE_PAGE_UP },
747 { '-', REQ_MOVE_PAGE_UP },
750 { KEY_IC, REQ_SCROLL_LINE_UP },
751 { KEY_DC, REQ_SCROLL_LINE_DOWN },
752 { 'w', REQ_SCROLL_PAGE_UP },
753 { 's', REQ_SCROLL_PAGE_DOWN },
757 { '?', REQ_SEARCH_BACK },
758 { 'n', REQ_FIND_NEXT },
759 { 'N', REQ_FIND_PREV },
763 { 'z', REQ_STOP_LOADING },
764 { 'v', REQ_SHOW_VERSION },
765 { 'r', REQ_SCREEN_REDRAW },
766 { '.', REQ_TOGGLE_LINENO },
767 { 'D', REQ_TOGGLE_DATE },
768 { 'A', REQ_TOGGLE_AUTHOR },
769 { 'g', REQ_TOGGLE_REV_GRAPH },
770 { 'F', REQ_TOGGLE_REFS },
772 { 'u', REQ_STATUS_UPDATE },
773 { 'M', REQ_STATUS_MERGE },
774 { ',', REQ_TREE_PARENT },
777 /* Using the ncurses SIGWINCH handler. */
778 { KEY_RESIZE, REQ_SCREEN_RESIZE },
781 #define KEYMAP_INFO \
795 #define KEYMAP_(name) KEYMAP_##name
800 static struct int_map keymap_table[] = {
801 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
806 #define set_keymap(map, name) \
807 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
809 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
812 add_keybinding(enum keymap keymap, enum request request, int key)
814 struct keybinding *keybinding;
816 keybinding = calloc(1, sizeof(*keybinding));
818 die("Failed to allocate keybinding");
820 keybinding->alias = key;
821 keybinding->request = request;
822 keybinding->next = keybindings[keymap];
823 keybindings[keymap] = keybinding;
826 /* Looks for a key binding first in the given map, then in the generic map, and
827 * lastly in the default keybindings. */
829 get_keybinding(enum keymap keymap, int key)
831 struct keybinding *kbd;
834 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
835 if (kbd->alias == key)
838 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
839 if (kbd->alias == key)
842 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
843 if (default_keybindings[i].alias == key)
844 return default_keybindings[i].request;
846 return (enum request) key;
855 static struct key key_table[] = {
856 { "Enter", KEY_RETURN },
858 { "Backspace", KEY_BACKSPACE },
860 { "Escape", KEY_ESC },
861 { "Left", KEY_LEFT },
862 { "Right", KEY_RIGHT },
864 { "Down", KEY_DOWN },
865 { "Insert", KEY_IC },
866 { "Delete", KEY_DC },
868 { "Home", KEY_HOME },
870 { "PageUp", KEY_PPAGE },
871 { "PageDown", KEY_NPAGE },
881 { "F10", KEY_F(10) },
882 { "F11", KEY_F(11) },
883 { "F12", KEY_F(12) },
887 get_key_value(const char *name)
891 for (i = 0; i < ARRAY_SIZE(key_table); i++)
892 if (!strcasecmp(key_table[i].name, name))
893 return key_table[i].value;
895 if (strlen(name) == 1 && isprint(*name))
902 get_key_name(int key_value)
904 static char key_char[] = "'X'";
908 for (key = 0; key < ARRAY_SIZE(key_table); key++)
909 if (key_table[key].value == key_value)
910 seq = key_table[key].name;
914 isprint(key_value)) {
915 key_char[1] = (char) key_value;
919 return seq ? seq : "'?'";
923 get_key(enum request request)
925 static char buf[BUFSIZ];
932 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
933 struct keybinding *keybinding = &default_keybindings[i];
935 if (keybinding->request != request)
938 if (!string_format_from(buf, &pos, "%s%s", sep,
939 get_key_name(keybinding->alias)))
940 return "Too many keybindings!";
950 char cmd[SIZEOF_STR];
953 static struct run_request *run_request;
954 static size_t run_requests;
957 add_run_request(enum keymap keymap, int key, int argc, char **argv)
959 struct run_request *tmp;
960 struct run_request req = { keymap, key };
963 for (bufpos = 0; argc > 0; argc--, argv++)
964 if (!string_format_from(req.cmd, &bufpos, "%s ", *argv))
967 req.cmd[bufpos - 1] = 0;
969 tmp = realloc(run_request, (run_requests + 1) * sizeof(*run_request));
974 run_request[run_requests++] = req;
976 return REQ_NONE + run_requests;
979 static struct run_request *
980 get_run_request(enum request request)
982 if (request <= REQ_NONE)
984 return &run_request[request - REQ_NONE - 1];
988 add_builtin_run_requests(void)
995 { KEYMAP_MAIN, 'C', { "git cherry-pick %(commit)" } },
996 { KEYMAP_GENERIC, 'G', { "git gc" } },
1000 for (i = 0; i < ARRAY_SIZE(reqs); i++) {
1003 req = add_run_request(reqs[i].keymap, reqs[i].key, 1, reqs[i].argv);
1004 if (req != REQ_NONE)
1005 add_keybinding(reqs[i].keymap, req, reqs[i].key);
1010 * User config file handling.
1013 static struct int_map color_map[] = {
1014 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
1026 #define set_color(color, name) \
1027 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
1029 static struct int_map attr_map[] = {
1030 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
1037 ATTR_MAP(UNDERLINE),
1040 #define set_attribute(attr, name) \
1041 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
1043 static int config_lineno;
1044 static bool config_errors;
1045 static char *config_msg;
1047 /* Wants: object fgcolor bgcolor [attr] */
1049 option_color_command(int argc, char *argv[])
1051 struct line_info *info;
1053 if (argc != 3 && argc != 4) {
1054 config_msg = "Wrong number of arguments given to color command";
1058 info = get_line_info(argv[0]);
1060 if (!string_enum_compare(argv[0], "main-delim", strlen("main-delim"))) {
1061 info = get_line_info("delimiter");
1063 } else if (!string_enum_compare(argv[0], "main-date", strlen("main-date"))) {
1064 info = get_line_info("date");
1067 config_msg = "Unknown color name";
1072 if (set_color(&info->fg, argv[1]) == ERR ||
1073 set_color(&info->bg, argv[2]) == ERR) {
1074 config_msg = "Unknown color";
1078 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
1079 config_msg = "Unknown attribute";
1086 static bool parse_bool(const char *s)
1088 return (!strcmp(s, "1") || !strcmp(s, "true") ||
1089 !strcmp(s, "yes")) ? TRUE : FALSE;
1092 /* Wants: name = value */
1094 option_set_command(int argc, char *argv[])
1097 config_msg = "Wrong number of arguments given to set command";
1101 if (strcmp(argv[1], "=")) {
1102 config_msg = "No value assigned";
1106 if (!strcmp(argv[0], "show-author")) {
1107 opt_author = parse_bool(argv[2]);
1111 if (!strcmp(argv[0], "show-date")) {
1112 opt_date = parse_bool(argv[2]);
1116 if (!strcmp(argv[0], "show-rev-graph")) {
1117 opt_rev_graph = parse_bool(argv[2]);
1121 if (!strcmp(argv[0], "show-refs")) {
1122 opt_show_refs = parse_bool(argv[2]);
1126 if (!strcmp(argv[0], "show-line-numbers")) {
1127 opt_line_number = parse_bool(argv[2]);
1131 if (!strcmp(argv[0], "line-graphics")) {
1132 opt_line_graphics = parse_bool(argv[2]);
1136 if (!strcmp(argv[0], "line-number-interval")) {
1137 opt_num_interval = atoi(argv[2]);
1141 if (!strcmp(argv[0], "tab-size")) {
1142 opt_tab_size = atoi(argv[2]);
1146 if (!strcmp(argv[0], "commit-encoding")) {
1147 char *arg = argv[2];
1148 int delimiter = *arg;
1151 switch (delimiter) {
1154 for (arg++, i = 0; arg[i]; i++)
1155 if (arg[i] == delimiter) {
1160 string_ncopy(opt_encoding, arg, strlen(arg));
1165 config_msg = "Unknown variable name";
1169 /* Wants: mode request key */
1171 option_bind_command(int argc, char *argv[])
1173 enum request request;
1178 config_msg = "Wrong number of arguments given to bind command";
1182 if (set_keymap(&keymap, argv[0]) == ERR) {
1183 config_msg = "Unknown key map";
1187 key = get_key_value(argv[1]);
1189 config_msg = "Unknown key";
1193 request = get_request(argv[2]);
1194 if (request == REQ_NONE) {
1195 const char *obsolete[] = { "cherry-pick" };
1196 size_t namelen = strlen(argv[2]);
1199 for (i = 0; i < ARRAY_SIZE(obsolete); i++) {
1200 if (namelen == strlen(obsolete[i]) &&
1201 !string_enum_compare(obsolete[i], argv[2], namelen)) {
1202 config_msg = "Obsolete request name";
1207 if (request == REQ_NONE && *argv[2]++ == '!')
1208 request = add_run_request(keymap, key, argc - 2, argv + 2);
1209 if (request == REQ_NONE) {
1210 config_msg = "Unknown request name";
1214 add_keybinding(keymap, request, key);
1220 set_option(char *opt, char *value)
1227 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1228 argv[argc++] = value;
1231 /* Nothing more to tokenize or last available token. */
1232 if (!*value || argc >= ARRAY_SIZE(argv))
1236 while (isspace(*value))
1240 if (!strcmp(opt, "color"))
1241 return option_color_command(argc, argv);
1243 if (!strcmp(opt, "set"))
1244 return option_set_command(argc, argv);
1246 if (!strcmp(opt, "bind"))
1247 return option_bind_command(argc, argv);
1249 config_msg = "Unknown option command";
1254 read_option(char *opt, size_t optlen, char *value, size_t valuelen)
1259 config_msg = "Internal error";
1261 /* Check for comment markers, since read_properties() will
1262 * only ensure opt and value are split at first " \t". */
1263 optlen = strcspn(opt, "#");
1267 if (opt[optlen] != 0) {
1268 config_msg = "No option value";
1272 /* Look for comment endings in the value. */
1273 size_t len = strcspn(value, "#");
1275 if (len < valuelen) {
1277 value[valuelen] = 0;
1280 status = set_option(opt, value);
1283 if (status == ERR) {
1284 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1285 config_lineno, (int) optlen, opt, config_msg);
1286 config_errors = TRUE;
1289 /* Always keep going if errors are encountered. */
1294 load_option_file(const char *path)
1298 /* It's ok that the file doesn't exist. */
1299 file = fopen(path, "r");
1304 config_errors = FALSE;
1306 if (read_properties(file, " \t", read_option) == ERR ||
1307 config_errors == TRUE)
1308 fprintf(stderr, "Errors while loading %s.\n", path);
1314 char *home = getenv("HOME");
1315 char *tigrc_user = getenv("TIGRC_USER");
1316 char *tigrc_system = getenv("TIGRC_SYSTEM");
1317 char buf[SIZEOF_STR];
1319 add_builtin_run_requests();
1321 if (!tigrc_system) {
1322 if (!string_format(buf, "%s/tigrc", SYSCONFDIR))
1326 load_option_file(tigrc_system);
1329 if (!home || !string_format(buf, "%s/.tigrc", home))
1333 load_option_file(tigrc_user);
1346 /* The display array of active views and the index of the current view. */
1347 static struct view *display[2];
1348 static unsigned int current_view;
1350 /* Reading from the prompt? */
1351 static bool input_mode = FALSE;
1353 #define foreach_displayed_view(view, i) \
1354 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1356 #define displayed_views() (display[1] != NULL ? 2 : 1)
1358 /* Current head and commit ID */
1359 static char ref_blob[SIZEOF_REF] = "";
1360 static char ref_commit[SIZEOF_REF] = "HEAD";
1361 static char ref_head[SIZEOF_REF] = "HEAD";
1364 const char *name; /* View name */
1365 const char *cmd_fmt; /* Default command line format */
1366 const char *cmd_env; /* Command line set via environment */
1367 const char *id; /* Points to either of ref_{head,commit,blob} */
1369 struct view_ops *ops; /* View operations */
1371 enum keymap keymap; /* What keymap does this view have */
1372 bool git_dir; /* Whether the view requires a git directory. */
1374 char cmd[SIZEOF_STR]; /* Command buffer */
1375 char ref[SIZEOF_REF]; /* Hovered commit reference */
1376 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1378 int height, width; /* The width and height of the main window */
1379 WINDOW *win; /* The main window */
1380 WINDOW *title; /* The title window living below the main window */
1383 unsigned long offset; /* Offset of the window top */
1384 unsigned long lineno; /* Current line number */
1387 char grep[SIZEOF_STR]; /* Search string */
1388 regex_t *regex; /* Pre-compiled regex */
1390 /* If non-NULL, points to the view that opened this view. If this view
1391 * is closed tig will switch back to the parent view. */
1392 struct view *parent;
1395 size_t lines; /* Total number of lines */
1396 struct line *line; /* Line index */
1397 size_t line_alloc; /* Total number of allocated lines */
1398 size_t line_size; /* Total number of used lines */
1399 unsigned int digits; /* Number of digits in the lines member. */
1402 struct line *curline; /* Line currently being drawn. */
1403 enum line_type curtype; /* Attribute currently used for drawing. */
1404 unsigned long col; /* Column when drawing. */
1412 /* What type of content being displayed. Used in the title bar. */
1414 /* Open and reads in all view content. */
1415 bool (*open)(struct view *view);
1416 /* Read one line; updates view->line. */
1417 bool (*read)(struct view *view, char *data);
1418 /* Draw one line; @lineno must be < view->height. */
1419 bool (*draw)(struct view *view, struct line *line, unsigned int lineno);
1420 /* Depending on view handle a special requests. */
1421 enum request (*request)(struct view *view, enum request request, struct line *line);
1422 /* Search for regex in a line. */
1423 bool (*grep)(struct view *view, struct line *line);
1425 void (*select)(struct view *view, struct line *line);
1428 static struct view_ops pager_ops;
1429 static struct view_ops main_ops;
1430 static struct view_ops tree_ops;
1431 static struct view_ops blob_ops;
1432 static struct view_ops blame_ops;
1433 static struct view_ops help_ops;
1434 static struct view_ops status_ops;
1435 static struct view_ops stage_ops;
1437 #define VIEW_STR(name, cmd, env, ref, ops, map, git) \
1438 { name, cmd, #env, ref, ops, map, git }
1440 #define VIEW_(id, name, ops, git, ref) \
1441 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id, git)
1444 static struct view views[] = {
1445 VIEW_(MAIN, "main", &main_ops, TRUE, ref_head),
1446 VIEW_(DIFF, "diff", &pager_ops, TRUE, ref_commit),
1447 VIEW_(LOG, "log", &pager_ops, TRUE, ref_head),
1448 VIEW_(TREE, "tree", &tree_ops, TRUE, ref_commit),
1449 VIEW_(BLOB, "blob", &blob_ops, TRUE, ref_blob),
1450 VIEW_(BLAME, "blame", &blame_ops, TRUE, ref_commit),
1451 VIEW_(HELP, "help", &help_ops, FALSE, ""),
1452 VIEW_(PAGER, "pager", &pager_ops, FALSE, "stdin"),
1453 VIEW_(STATUS, "status", &status_ops, TRUE, ""),
1454 VIEW_(STAGE, "stage", &stage_ops, TRUE, ""),
1457 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1458 #define VIEW_REQ(view) ((view) - views + REQ_OFFSET + 1)
1460 #define foreach_view(view, i) \
1461 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1463 #define view_is_displayed(view) \
1464 (view == display[0] || view == display[1])
1471 static int line_graphics[] = {
1472 /* LINE_GRAPHIC_VLINE: */ '|'
1476 set_view_attr(struct view *view, enum line_type type)
1478 if (!view->curline->selected && view->curtype != type) {
1479 wattrset(view->win, get_line_attr(type));
1480 wchgat(view->win, -1, 0, type, NULL);
1481 view->curtype = type;
1486 draw_chars(struct view *view, enum line_type type, const char *string,
1487 int max_len, bool use_tilde)
1491 int trimmed = FALSE;
1497 len = utf8_length(string, &col, max_len, &trimmed, use_tilde);
1499 col = len = strlen(string);
1500 if (len > max_len) {
1504 col = len = max_len;
1509 set_view_attr(view, type);
1510 waddnstr(view->win, string, len);
1511 if (trimmed && use_tilde) {
1512 set_view_attr(view, LINE_DELIMITER);
1513 waddch(view->win, '~');
1521 draw_space(struct view *view, enum line_type type, int max, int spaces)
1523 static char space[] = " ";
1526 spaces = MIN(max, spaces);
1528 while (spaces > 0) {
1529 int len = MIN(spaces, sizeof(space) - 1);
1531 col += draw_chars(view, type, space, spaces, FALSE);
1539 draw_lineno(struct view *view, unsigned int lineno)
1542 int digits3 = view->digits < 3 ? 3 : view->digits;
1543 int max_number = MIN(digits3, STRING_SIZE(number));
1544 int max = view->width - view->col;
1547 if (max < max_number)
1550 lineno += view->offset + 1;
1551 if (lineno == 1 || (lineno % opt_num_interval) == 0) {
1552 static char fmt[] = "%1ld";
1554 if (view->digits <= 9)
1555 fmt[1] = '0' + digits3;
1557 if (!string_format(number, fmt, lineno))
1559 col = draw_chars(view, LINE_LINE_NUMBER, number, max_number, TRUE);
1561 col = draw_space(view, LINE_LINE_NUMBER, max_number, max_number);
1565 set_view_attr(view, LINE_DEFAULT);
1566 waddch(view->win, line_graphics[LINE_GRAPHIC_VLINE]);
1571 col += draw_space(view, LINE_DEFAULT, max - col, 1);
1574 return view->width - view->col <= 0;
1578 draw_text(struct view *view, enum line_type type, const char *string, bool trim)
1580 view->col += draw_chars(view, type, string, view->width - view->col, trim);
1581 return view->width - view->col <= 0;
1585 draw_graphic(struct view *view, enum line_type type, chtype graphic[], size_t size)
1587 int max = view->width - view->col;
1593 set_view_attr(view, type);
1594 /* Using waddch() instead of waddnstr() ensures that
1595 * they'll be rendered correctly for the cursor line. */
1596 for (i = 0; i < size; i++)
1597 waddch(view->win, graphic[i]);
1601 waddch(view->win, ' ');
1605 return view->width - view->col <= 0;
1609 draw_field(struct view *view, enum line_type type, char *text, int len, bool trim)
1611 int max = MIN(view->width - view->col, len);
1615 col = draw_chars(view, type, text, max - 1, trim);
1617 col = draw_space(view, type, max - 1, max - 1);
1619 view->col += col + draw_space(view, LINE_DEFAULT, max - col, max - col);
1620 return view->width - view->col <= 0;
1624 draw_date(struct view *view, struct tm *time)
1626 char buf[DATE_COLS];
1631 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, time);
1632 date = timelen ? buf : NULL;
1634 return draw_field(view, LINE_DATE, date, DATE_COLS, FALSE);
1638 draw_view_line(struct view *view, unsigned int lineno)
1641 bool selected = (view->offset + lineno == view->lineno);
1644 assert(view_is_displayed(view));
1646 if (view->offset + lineno >= view->lines)
1649 line = &view->line[view->offset + lineno];
1651 wmove(view->win, lineno, 0);
1653 view->curline = line;
1654 view->curtype = LINE_NONE;
1655 line->selected = FALSE;
1658 set_view_attr(view, LINE_CURSOR);
1659 line->selected = TRUE;
1660 view->ops->select(view, line);
1661 } else if (line->selected) {
1662 wclrtoeol(view->win);
1665 scrollok(view->win, FALSE);
1666 draw_ok = view->ops->draw(view, line, lineno);
1667 scrollok(view->win, TRUE);
1673 redraw_view_dirty(struct view *view)
1678 for (lineno = 0; lineno < view->height; lineno++) {
1679 struct line *line = &view->line[view->offset + lineno];
1685 if (!draw_view_line(view, lineno))
1691 redrawwin(view->win);
1693 wnoutrefresh(view->win);
1695 wrefresh(view->win);
1699 redraw_view_from(struct view *view, int lineno)
1701 assert(0 <= lineno && lineno < view->height);
1703 for (; lineno < view->height; lineno++) {
1704 if (!draw_view_line(view, lineno))
1708 redrawwin(view->win);
1710 wnoutrefresh(view->win);
1712 wrefresh(view->win);
1716 redraw_view(struct view *view)
1719 redraw_view_from(view, 0);
1724 update_view_title(struct view *view)
1726 char buf[SIZEOF_STR];
1727 char state[SIZEOF_STR];
1728 size_t bufpos = 0, statelen = 0;
1730 assert(view_is_displayed(view));
1732 if (view != VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) {
1733 unsigned int view_lines = view->offset + view->height;
1734 unsigned int lines = view->lines
1735 ? MIN(view_lines, view->lines) * 100 / view->lines
1738 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1745 time_t secs = time(NULL) - view->start_time;
1747 /* Three git seconds are a long time ... */
1749 string_format_from(state, &statelen, " %lds", secs);
1753 string_format_from(buf, &bufpos, "[%s]", view->name);
1754 if (*view->ref && bufpos < view->width) {
1755 size_t refsize = strlen(view->ref);
1756 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1758 if (minsize < view->width)
1759 refsize = view->width - minsize + 7;
1760 string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
1763 if (statelen && bufpos < view->width) {
1764 string_format_from(buf, &bufpos, " %s", state);
1767 if (view == display[current_view])
1768 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1770 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1772 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1773 wclrtoeol(view->title);
1774 wmove(view->title, 0, view->width - 1);
1777 wnoutrefresh(view->title);
1779 wrefresh(view->title);
1783 resize_display(void)
1786 struct view *base = display[0];
1787 struct view *view = display[1] ? display[1] : display[0];
1789 /* Setup window dimensions */
1791 getmaxyx(stdscr, base->height, base->width);
1793 /* Make room for the status window. */
1797 /* Horizontal split. */
1798 view->width = base->width;
1799 view->height = SCALE_SPLIT_VIEW(base->height);
1800 base->height -= view->height;
1802 /* Make room for the title bar. */
1806 /* Make room for the title bar. */
1811 foreach_displayed_view (view, i) {
1813 view->win = newwin(view->height, 0, offset, 0);
1815 die("Failed to create %s view", view->name);
1817 scrollok(view->win, TRUE);
1819 view->title = newwin(1, 0, offset + view->height, 0);
1821 die("Failed to create title window");
1824 wresize(view->win, view->height, view->width);
1825 mvwin(view->win, offset, 0);
1826 mvwin(view->title, offset + view->height, 0);
1829 offset += view->height + 1;
1834 redraw_display(void)
1839 foreach_displayed_view (view, i) {
1841 update_view_title(view);
1846 update_display_cursor(struct view *view)
1848 /* Move the cursor to the right-most column of the cursor line.
1850 * XXX: This could turn out to be a bit expensive, but it ensures that
1851 * the cursor does not jump around. */
1853 wmove(view->win, view->lineno - view->offset, view->width - 1);
1854 wrefresh(view->win);
1862 /* Scrolling backend */
1864 do_scroll_view(struct view *view, int lines)
1866 bool redraw_current_line = FALSE;
1868 /* The rendering expects the new offset. */
1869 view->offset += lines;
1871 assert(0 <= view->offset && view->offset < view->lines);
1874 /* Move current line into the view. */
1875 if (view->lineno < view->offset) {
1876 view->lineno = view->offset;
1877 redraw_current_line = TRUE;
1878 } else if (view->lineno >= view->offset + view->height) {
1879 view->lineno = view->offset + view->height - 1;
1880 redraw_current_line = TRUE;
1883 assert(view->offset <= view->lineno && view->lineno < view->lines);
1885 /* Redraw the whole screen if scrolling is pointless. */
1886 if (view->height < ABS(lines)) {
1890 int line = lines > 0 ? view->height - lines : 0;
1891 int end = line + ABS(lines);
1893 wscrl(view->win, lines);
1895 for (; line < end; line++) {
1896 if (!draw_view_line(view, line))
1900 if (redraw_current_line)
1901 draw_view_line(view, view->lineno - view->offset);
1904 redrawwin(view->win);
1905 wrefresh(view->win);
1909 /* Scroll frontend */
1911 scroll_view(struct view *view, enum request request)
1915 assert(view_is_displayed(view));
1918 case REQ_SCROLL_PAGE_DOWN:
1919 lines = view->height;
1920 case REQ_SCROLL_LINE_DOWN:
1921 if (view->offset + lines > view->lines)
1922 lines = view->lines - view->offset;
1924 if (lines == 0 || view->offset + view->height >= view->lines) {
1925 report("Cannot scroll beyond the last line");
1930 case REQ_SCROLL_PAGE_UP:
1931 lines = view->height;
1932 case REQ_SCROLL_LINE_UP:
1933 if (lines > view->offset)
1934 lines = view->offset;
1937 report("Cannot scroll beyond the first line");
1945 die("request %d not handled in switch", request);
1948 do_scroll_view(view, lines);
1953 move_view(struct view *view, enum request request)
1955 int scroll_steps = 0;
1959 case REQ_MOVE_FIRST_LINE:
1960 steps = -view->lineno;
1963 case REQ_MOVE_LAST_LINE:
1964 steps = view->lines - view->lineno - 1;
1967 case REQ_MOVE_PAGE_UP:
1968 steps = view->height > view->lineno
1969 ? -view->lineno : -view->height;
1972 case REQ_MOVE_PAGE_DOWN:
1973 steps = view->lineno + view->height >= view->lines
1974 ? view->lines - view->lineno - 1 : view->height;
1986 die("request %d not handled in switch", request);
1989 if (steps <= 0 && view->lineno == 0) {
1990 report("Cannot move beyond the first line");
1993 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1994 report("Cannot move beyond the last line");
1998 /* Move the current line */
1999 view->lineno += steps;
2000 assert(0 <= view->lineno && view->lineno < view->lines);
2002 /* Check whether the view needs to be scrolled */
2003 if (view->lineno < view->offset ||
2004 view->lineno >= view->offset + view->height) {
2005 scroll_steps = steps;
2006 if (steps < 0 && -steps > view->offset) {
2007 scroll_steps = -view->offset;
2009 } else if (steps > 0) {
2010 if (view->lineno == view->lines - 1 &&
2011 view->lines > view->height) {
2012 scroll_steps = view->lines - view->offset - 1;
2013 if (scroll_steps >= view->height)
2014 scroll_steps -= view->height - 1;
2019 if (!view_is_displayed(view)) {
2020 view->offset += scroll_steps;
2021 assert(0 <= view->offset && view->offset < view->lines);
2022 view->ops->select(view, &view->line[view->lineno]);
2026 /* Repaint the old "current" line if we be scrolling */
2027 if (ABS(steps) < view->height)
2028 draw_view_line(view, view->lineno - steps - view->offset);
2031 do_scroll_view(view, scroll_steps);
2035 /* Draw the current line */
2036 draw_view_line(view, view->lineno - view->offset);
2038 redrawwin(view->win);
2039 wrefresh(view->win);
2048 static void search_view(struct view *view, enum request request);
2051 find_next_line(struct view *view, unsigned long lineno, struct line *line)
2053 assert(view_is_displayed(view));
2055 if (!view->ops->grep(view, line))
2058 if (lineno - view->offset >= view->height) {
2059 view->offset = lineno;
2060 view->lineno = lineno;
2064 unsigned long old_lineno = view->lineno - view->offset;
2066 view->lineno = lineno;
2067 draw_view_line(view, old_lineno);
2069 draw_view_line(view, view->lineno - view->offset);
2070 redrawwin(view->win);
2071 wrefresh(view->win);
2074 report("Line %ld matches '%s'", lineno + 1, view->grep);
2079 find_next(struct view *view, enum request request)
2081 unsigned long lineno = view->lineno;
2086 report("No previous search");
2088 search_view(view, request);
2098 case REQ_SEARCH_BACK:
2107 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
2108 lineno += direction;
2110 /* Note, lineno is unsigned long so will wrap around in which case it
2111 * will become bigger than view->lines. */
2112 for (; lineno < view->lines; lineno += direction) {
2113 struct line *line = &view->line[lineno];
2115 if (find_next_line(view, lineno, line))
2119 report("No match found for '%s'", view->grep);
2123 search_view(struct view *view, enum request request)
2128 regfree(view->regex);
2131 view->regex = calloc(1, sizeof(*view->regex));
2136 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
2137 if (regex_err != 0) {
2138 char buf[SIZEOF_STR] = "unknown error";
2140 regerror(regex_err, view->regex, buf, sizeof(buf));
2141 report("Search failed: %s", buf);
2145 string_copy(view->grep, opt_search);
2147 find_next(view, request);
2151 * Incremental updating
2155 end_update(struct view *view)
2159 set_nonblocking_input(FALSE);
2160 if (view->pipe == stdin)
2168 begin_update(struct view *view)
2174 string_copy(view->cmd, opt_cmd);
2176 /* When running random commands, initially show the
2177 * command in the title. However, it maybe later be
2178 * overwritten if a commit line is selected. */
2179 if (view == VIEW(REQ_VIEW_PAGER))
2180 string_copy(view->ref, view->cmd);
2184 } else if (view == VIEW(REQ_VIEW_TREE)) {
2185 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2186 char path[SIZEOF_STR];
2188 if (strcmp(view->vid, view->id))
2189 opt_path[0] = path[0] = 0;
2190 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
2193 if (!string_format(view->cmd, format, view->id, path))
2197 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2198 const char *id = view->id;
2200 if (!string_format(view->cmd, format, id, id, id, id, id))
2203 /* Put the current ref_* value to the view title ref
2204 * member. This is needed by the blob view. Most other
2205 * views sets it automatically after loading because the
2206 * first line is a commit line. */
2207 string_copy_rev(view->ref, view->id);
2210 /* Special case for the pager view. */
2212 view->pipe = opt_pipe;
2215 view->pipe = popen(view->cmd, "r");
2221 set_nonblocking_input(TRUE);
2226 string_copy_rev(view->vid, view->id);
2231 for (i = 0; i < view->lines; i++)
2232 if (view->line[i].data)
2233 free(view->line[i].data);
2239 view->start_time = time(NULL);
2244 #define ITEM_CHUNK_SIZE 256
2246 realloc_items(void *mem, size_t *size, size_t new_size, size_t item_size)
2248 size_t num_chunks = *size / ITEM_CHUNK_SIZE;
2249 size_t num_chunks_new = (new_size + ITEM_CHUNK_SIZE - 1) / ITEM_CHUNK_SIZE;
2251 if (mem == NULL || num_chunks != num_chunks_new) {
2252 *size = num_chunks_new * ITEM_CHUNK_SIZE;
2253 mem = realloc(mem, *size * item_size);
2259 static struct line *
2260 realloc_lines(struct view *view, size_t line_size)
2262 size_t alloc = view->line_alloc;
2263 struct line *tmp = realloc_items(view->line, &alloc, line_size,
2264 sizeof(*view->line));
2270 view->line_alloc = alloc;
2271 view->line_size = line_size;
2276 update_view(struct view *view)
2278 char in_buffer[BUFSIZ];
2279 char out_buffer[BUFSIZ * 2];
2281 /* The number of lines to read. If too low it will cause too much
2282 * redrawing (and possible flickering), if too high responsiveness
2284 unsigned long lines = view->height;
2285 int redraw_from = -1;
2290 /* Only redraw if lines are visible. */
2291 if (view->offset + view->height >= view->lines)
2292 redraw_from = view->lines - view->offset;
2294 /* FIXME: This is probably not perfect for backgrounded views. */
2295 if (!realloc_lines(view, view->lines + lines))
2298 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
2299 size_t linelen = strlen(line);
2302 line[linelen - 1] = 0;
2304 if (opt_iconv != ICONV_NONE) {
2305 ICONV_CONST char *inbuf = line;
2306 size_t inlen = linelen;
2308 char *outbuf = out_buffer;
2309 size_t outlen = sizeof(out_buffer);
2313 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
2314 if (ret != (size_t) -1) {
2316 linelen = strlen(out_buffer);
2320 if (!view->ops->read(view, line))
2330 lines = view->lines;
2331 for (digits = 0; lines; digits++)
2334 /* Keep the displayed view in sync with line number scaling. */
2335 if (digits != view->digits) {
2336 view->digits = digits;
2341 if (!view_is_displayed(view))
2344 if (view == VIEW(REQ_VIEW_TREE)) {
2345 /* Clear the view and redraw everything since the tree sorting
2346 * might have rearranged things. */
2349 } else if (redraw_from >= 0) {
2350 /* If this is an incremental update, redraw the previous line
2351 * since for commits some members could have changed when
2352 * loading the main view. */
2353 if (redraw_from > 0)
2356 /* Since revision graph visualization requires knowledge
2357 * about the parent commit, it causes a further one-off
2358 * needed to be redrawn for incremental updates. */
2359 if (redraw_from > 0 && opt_rev_graph)
2362 /* Incrementally draw avoids flickering. */
2363 redraw_view_from(view, redraw_from);
2366 if (view == VIEW(REQ_VIEW_BLAME))
2367 redraw_view_dirty(view);
2369 /* Update the title _after_ the redraw so that if the redraw picks up a
2370 * commit reference in view->ref it'll be available here. */
2371 update_view_title(view);
2374 if (ferror(view->pipe)) {
2375 report("Failed to read: %s", strerror(errno));
2378 } else if (feof(view->pipe)) {
2386 report("Allocation failure");
2389 if (view->ops->read(view, NULL))
2394 static struct line *
2395 add_line_data(struct view *view, void *data, enum line_type type)
2397 struct line *line = &view->line[view->lines++];
2399 memset(line, 0, sizeof(*line));
2406 static struct line *
2407 add_line_text(struct view *view, char *data, enum line_type type)
2410 data = strdup(data);
2412 return data ? add_line_data(view, data, type) : NULL;
2421 OPEN_DEFAULT = 0, /* Use default view switching. */
2422 OPEN_SPLIT = 1, /* Split current view. */
2423 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2424 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2425 OPEN_NOMAXIMIZE = 8, /* Do not maximize the current view. */
2429 open_view(struct view *prev, enum request request, enum open_flags flags)
2431 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2432 bool split = !!(flags & OPEN_SPLIT);
2433 bool reload = !!(flags & OPEN_RELOAD);
2434 bool nomaximize = !!(flags & OPEN_NOMAXIMIZE);
2435 struct view *view = VIEW(request);
2436 int nviews = displayed_views();
2437 struct view *base_view = display[0];
2439 if (view == prev && nviews == 1 && !reload) {
2440 report("Already in %s view", view->name);
2444 if (view->git_dir && !opt_git_dir[0]) {
2445 report("The %s view is disabled in pager view", view->name);
2453 } else if (!nomaximize) {
2454 /* Maximize the current view. */
2455 memset(display, 0, sizeof(display));
2457 display[current_view] = view;
2460 /* Resize the view when switching between split- and full-screen,
2461 * or when switching between two different full-screen views. */
2462 if (nviews != displayed_views() ||
2463 (nviews == 1 && base_view != display[0]))
2466 if (view->ops->open) {
2467 if (!view->ops->open(view)) {
2468 report("Failed to load %s view", view->name);
2472 } else if ((reload || strcmp(view->vid, view->id)) &&
2473 !begin_update(view)) {
2474 report("Failed to load %s view", view->name);
2478 if (split && prev->lineno - prev->offset >= prev->height) {
2479 /* Take the title line into account. */
2480 int lines = prev->lineno - prev->offset - prev->height + 1;
2482 /* Scroll the view that was split if the current line is
2483 * outside the new limited view. */
2484 do_scroll_view(prev, lines);
2487 if (prev && view != prev) {
2488 if (split && !backgrounded) {
2489 /* "Blur" the previous view. */
2490 update_view_title(prev);
2493 view->parent = prev;
2496 if (view->pipe && view->lines == 0) {
2497 /* Clear the old view and let the incremental updating refill
2506 /* If the view is backgrounded the above calls to report()
2507 * won't redraw the view title. */
2509 update_view_title(view);
2513 open_external_viewer(const char *cmd)
2515 def_prog_mode(); /* save current tty modes */
2516 endwin(); /* restore original tty modes */
2518 fprintf(stderr, "Press Enter to continue");
2525 open_mergetool(const char *file)
2527 char cmd[SIZEOF_STR];
2528 char file_sq[SIZEOF_STR];
2530 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2531 string_format(cmd, "git mergetool %s", file_sq)) {
2532 open_external_viewer(cmd);
2537 open_editor(bool from_root, const char *file)
2539 char cmd[SIZEOF_STR];
2540 char file_sq[SIZEOF_STR];
2542 char *prefix = from_root ? opt_cdup : "";
2544 editor = getenv("GIT_EDITOR");
2545 if (!editor && *opt_editor)
2546 editor = opt_editor;
2548 editor = getenv("VISUAL");
2550 editor = getenv("EDITOR");
2554 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2555 string_format(cmd, "%s %s%s", editor, prefix, file_sq)) {
2556 open_external_viewer(cmd);
2561 open_run_request(enum request request)
2563 struct run_request *req = get_run_request(request);
2564 char buf[SIZEOF_STR * 2];
2569 report("Unknown run request");
2577 char *next = strstr(cmd, "%(");
2578 int len = next - cmd;
2585 } else if (!strncmp(next, "%(head)", 7)) {
2588 } else if (!strncmp(next, "%(commit)", 9)) {
2591 } else if (!strncmp(next, "%(blob)", 7)) {
2595 report("Unknown replacement in run request: `%s`", req->cmd);
2599 if (!string_format_from(buf, &bufpos, "%.*s%s", len, cmd, value))
2603 next = strchr(next, ')') + 1;
2607 open_external_viewer(buf);
2611 * User request switch noodle
2615 view_driver(struct view *view, enum request request)
2619 if (request == REQ_NONE) {
2624 if (request > REQ_NONE) {
2625 open_run_request(request);
2626 /* FIXME: When all views can refresh always do this. */
2627 if (view == VIEW(REQ_VIEW_STATUS) ||
2628 view == VIEW(REQ_VIEW_STAGE))
2629 request = REQ_REFRESH;
2634 if (view && view->lines) {
2635 request = view->ops->request(view, request, &view->line[view->lineno]);
2636 if (request == REQ_NONE)
2643 case REQ_MOVE_PAGE_UP:
2644 case REQ_MOVE_PAGE_DOWN:
2645 case REQ_MOVE_FIRST_LINE:
2646 case REQ_MOVE_LAST_LINE:
2647 move_view(view, request);
2650 case REQ_SCROLL_LINE_DOWN:
2651 case REQ_SCROLL_LINE_UP:
2652 case REQ_SCROLL_PAGE_DOWN:
2653 case REQ_SCROLL_PAGE_UP:
2654 scroll_view(view, request);
2657 case REQ_VIEW_BLAME:
2659 report("No file chosen, press %s to open tree view",
2660 get_key(REQ_VIEW_TREE));
2663 open_view(view, request, OPEN_DEFAULT);
2668 report("No file chosen, press %s to open tree view",
2669 get_key(REQ_VIEW_TREE));
2672 open_view(view, request, OPEN_DEFAULT);
2675 case REQ_VIEW_PAGER:
2676 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2677 report("No pager content, press %s to run command from prompt",
2678 get_key(REQ_PROMPT));
2681 open_view(view, request, OPEN_DEFAULT);
2684 case REQ_VIEW_STAGE:
2685 if (!VIEW(REQ_VIEW_STAGE)->lines) {
2686 report("No stage content, press %s to open the status view and choose file",
2687 get_key(REQ_VIEW_STATUS));
2690 open_view(view, request, OPEN_DEFAULT);
2693 case REQ_VIEW_STATUS:
2694 if (opt_is_inside_work_tree == FALSE) {
2695 report("The status view requires a working tree");
2698 open_view(view, request, OPEN_DEFAULT);
2706 open_view(view, request, OPEN_DEFAULT);
2711 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2713 if ((view == VIEW(REQ_VIEW_DIFF) &&
2714 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2715 (view == VIEW(REQ_VIEW_DIFF) &&
2716 view->parent == VIEW(REQ_VIEW_BLAME)) ||
2717 (view == VIEW(REQ_VIEW_STAGE) &&
2718 view->parent == VIEW(REQ_VIEW_STATUS)) ||
2719 (view == VIEW(REQ_VIEW_BLOB) &&
2720 view->parent == VIEW(REQ_VIEW_TREE))) {
2723 view = view->parent;
2724 line = view->lineno;
2725 move_view(view, request);
2726 if (view_is_displayed(view))
2727 update_view_title(view);
2728 if (line != view->lineno)
2729 view->ops->request(view, REQ_ENTER,
2730 &view->line[view->lineno]);
2733 move_view(view, request);
2739 int nviews = displayed_views();
2740 int next_view = (current_view + 1) % nviews;
2742 if (next_view == current_view) {
2743 report("Only one view is displayed");
2747 current_view = next_view;
2748 /* Blur out the title of the previous view. */
2749 update_view_title(view);
2754 report("Refreshing is not yet supported for the %s view", view->name);
2758 if (displayed_views() == 2)
2759 open_view(view, VIEW_REQ(view), OPEN_DEFAULT);
2762 case REQ_TOGGLE_LINENO:
2763 opt_line_number = !opt_line_number;
2767 case REQ_TOGGLE_DATE:
2768 opt_date = !opt_date;
2772 case REQ_TOGGLE_AUTHOR:
2773 opt_author = !opt_author;
2777 case REQ_TOGGLE_REV_GRAPH:
2778 opt_rev_graph = !opt_rev_graph;
2782 case REQ_TOGGLE_REFS:
2783 opt_show_refs = !opt_show_refs;
2788 /* Always reload^Wrerun commands from the prompt. */
2789 open_view(view, opt_request, OPEN_RELOAD);
2793 case REQ_SEARCH_BACK:
2794 search_view(view, request);
2799 find_next(view, request);
2802 case REQ_STOP_LOADING:
2803 for (i = 0; i < ARRAY_SIZE(views); i++) {
2806 report("Stopped loading the %s view", view->name),
2811 case REQ_SHOW_VERSION:
2812 report("tig-%s (built %s)", TIG_VERSION, __DATE__);
2815 case REQ_SCREEN_RESIZE:
2818 case REQ_SCREEN_REDRAW:
2823 report("Nothing to edit");
2828 report("Nothing to enter");
2832 case REQ_VIEW_CLOSE:
2833 /* XXX: Mark closed views by letting view->parent point to the
2834 * view itself. Parents to closed view should never be
2837 view->parent->parent != view->parent) {
2838 memset(display, 0, sizeof(display));
2840 display[current_view] = view->parent;
2841 view->parent = view;
2851 /* An unknown key will show most commonly used commands. */
2852 report("Unknown key, press 'h' for help");
2865 pager_draw(struct view *view, struct line *line, unsigned int lineno)
2867 char *text = line->data;
2869 if (opt_line_number && draw_lineno(view, lineno))
2872 draw_text(view, line->type, text, TRUE);
2877 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2879 char refbuf[SIZEOF_STR];
2883 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2886 pipe = popen(refbuf, "r");
2890 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2891 ref = chomp_string(ref);
2897 /* This is the only fatal call, since it can "corrupt" the buffer. */
2898 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2905 add_pager_refs(struct view *view, struct line *line)
2907 char buf[SIZEOF_STR];
2908 char *commit_id = (char *)line->data + STRING_SIZE("commit ");
2910 size_t bufpos = 0, refpos = 0;
2911 const char *sep = "Refs: ";
2912 bool is_tag = FALSE;
2914 assert(line->type == LINE_COMMIT);
2916 refs = get_refs(commit_id);
2918 if (view == VIEW(REQ_VIEW_DIFF))
2919 goto try_add_describe_ref;
2924 struct ref *ref = refs[refpos];
2925 char *fmt = ref->tag ? "%s[%s]" :
2926 ref->remote ? "%s<%s>" : "%s%s";
2928 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2933 } while (refs[refpos++]->next);
2935 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2936 try_add_describe_ref:
2937 /* Add <tag>-g<commit_id> "fake" reference. */
2938 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2945 if (!realloc_lines(view, view->line_size + 1))
2948 add_line_text(view, buf, LINE_PP_REFS);
2952 pager_read(struct view *view, char *data)
2959 line = add_line_text(view, data, get_line_type(data));
2963 if (line->type == LINE_COMMIT &&
2964 (view == VIEW(REQ_VIEW_DIFF) ||
2965 view == VIEW(REQ_VIEW_LOG)))
2966 add_pager_refs(view, line);
2972 pager_request(struct view *view, enum request request, struct line *line)
2976 if (request != REQ_ENTER)
2979 if (line->type == LINE_COMMIT &&
2980 (view == VIEW(REQ_VIEW_LOG) ||
2981 view == VIEW(REQ_VIEW_PAGER))) {
2982 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2986 /* Always scroll the view even if it was split. That way
2987 * you can use Enter to scroll through the log view and
2988 * split open each commit diff. */
2989 scroll_view(view, REQ_SCROLL_LINE_DOWN);
2991 /* FIXME: A minor workaround. Scrolling the view will call report("")
2992 * but if we are scrolling a non-current view this won't properly
2993 * update the view title. */
2995 update_view_title(view);
3001 pager_grep(struct view *view, struct line *line)
3004 char *text = line->data;
3009 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
3016 pager_select(struct view *view, struct line *line)
3018 if (line->type == LINE_COMMIT) {
3019 char *text = (char *)line->data + STRING_SIZE("commit ");
3021 if (view != VIEW(REQ_VIEW_PAGER))
3022 string_copy_rev(view->ref, text);
3023 string_copy_rev(ref_commit, text);
3027 static struct view_ops pager_ops = {
3043 help_open(struct view *view)
3046 int lines = ARRAY_SIZE(req_info) + 2;
3049 if (view->lines > 0)
3052 for (i = 0; i < ARRAY_SIZE(req_info); i++)
3053 if (!req_info[i].request)
3056 lines += run_requests + 1;
3058 view->line = calloc(lines, sizeof(*view->line));
3062 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
3064 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
3067 if (req_info[i].request == REQ_NONE)
3070 if (!req_info[i].request) {
3071 add_line_text(view, "", LINE_DEFAULT);
3072 add_line_text(view, req_info[i].help, LINE_DEFAULT);
3076 key = get_key(req_info[i].request);
3078 key = "(no key defined)";
3080 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
3083 add_line_text(view, buf, LINE_DEFAULT);
3087 add_line_text(view, "", LINE_DEFAULT);
3088 add_line_text(view, "External commands:", LINE_DEFAULT);
3091 for (i = 0; i < run_requests; i++) {
3092 struct run_request *req = get_run_request(REQ_NONE + i + 1);
3098 key = get_key_name(req->key);
3100 key = "(no key defined)";
3102 if (!string_format(buf, " %-10s %-14s `%s`",
3103 keymap_table[req->keymap].name,
3107 add_line_text(view, buf, LINE_DEFAULT);
3113 static struct view_ops help_ops = {
3128 struct tree_stack_entry {
3129 struct tree_stack_entry *prev; /* Entry below this in the stack */
3130 unsigned long lineno; /* Line number to restore */
3131 char *name; /* Position of name in opt_path */
3134 /* The top of the path stack. */
3135 static struct tree_stack_entry *tree_stack = NULL;
3136 unsigned long tree_lineno = 0;
3139 pop_tree_stack_entry(void)
3141 struct tree_stack_entry *entry = tree_stack;
3143 tree_lineno = entry->lineno;
3145 tree_stack = entry->prev;
3150 push_tree_stack_entry(char *name, unsigned long lineno)
3152 struct tree_stack_entry *entry = calloc(1, sizeof(*entry));
3153 size_t pathlen = strlen(opt_path);
3158 entry->prev = tree_stack;
3159 entry->name = opt_path + pathlen;
3162 if (!string_format_from(opt_path, &pathlen, "%s/", name)) {
3163 pop_tree_stack_entry();
3167 /* Move the current line to the first tree entry. */
3169 entry->lineno = lineno;
3172 /* Parse output from git-ls-tree(1):
3174 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
3175 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
3176 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
3177 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
3180 #define SIZEOF_TREE_ATTR \
3181 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
3183 #define TREE_UP_FORMAT "040000 tree %s\t.."
3186 tree_compare_entry(enum line_type type1, char *name1,
3187 enum line_type type2, char *name2)
3189 if (type1 != type2) {
3190 if (type1 == LINE_TREE_DIR)
3195 return strcmp(name1, name2);
3199 tree_path(struct line *line)
3201 char *path = line->data;
3203 return path + SIZEOF_TREE_ATTR;
3207 tree_read(struct view *view, char *text)
3209 size_t textlen = text ? strlen(text) : 0;
3210 char buf[SIZEOF_STR];
3212 enum line_type type;
3213 bool first_read = view->lines == 0;
3217 if (textlen <= SIZEOF_TREE_ATTR)
3220 type = text[STRING_SIZE("100644 ")] == 't'
3221 ? LINE_TREE_DIR : LINE_TREE_FILE;
3224 /* Add path info line */
3225 if (!string_format(buf, "Directory path /%s", opt_path) ||
3226 !realloc_lines(view, view->line_size + 1) ||
3227 !add_line_text(view, buf, LINE_DEFAULT))
3230 /* Insert "link" to parent directory. */
3232 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
3233 !realloc_lines(view, view->line_size + 1) ||
3234 !add_line_text(view, buf, LINE_TREE_DIR))
3239 /* Strip the path part ... */
3241 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
3242 size_t striplen = strlen(opt_path);
3243 char *path = text + SIZEOF_TREE_ATTR;
3245 if (pathlen > striplen)
3246 memmove(path, path + striplen,
3247 pathlen - striplen + 1);
3250 /* Skip "Directory ..." and ".." line. */
3251 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
3252 struct line *line = &view->line[pos];
3253 char *path1 = tree_path(line);
3254 char *path2 = text + SIZEOF_TREE_ATTR;
3255 int cmp = tree_compare_entry(line->type, path1, type, path2);
3260 text = strdup(text);
3264 if (view->lines > pos)
3265 memmove(&view->line[pos + 1], &view->line[pos],
3266 (view->lines - pos) * sizeof(*line));
3268 line = &view->line[pos];
3275 if (!add_line_text(view, text, type))
3278 if (tree_lineno > view->lineno) {
3279 view->lineno = tree_lineno;
3287 tree_request(struct view *view, enum request request, struct line *line)
3289 enum open_flags flags;
3291 if (request == REQ_VIEW_BLAME) {
3292 char *filename = tree_path(line);
3294 if (line->type == LINE_TREE_DIR) {
3295 report("Cannot show blame for directory %s", opt_path);
3299 string_copy(opt_ref, view->vid);
3300 string_format(opt_file, "%s%s", opt_path, filename);
3303 if (request == REQ_TREE_PARENT) {
3306 request = REQ_ENTER;
3307 line = &view->line[1];
3309 /* quit view if at top of tree */
3310 return REQ_VIEW_CLOSE;
3313 if (request != REQ_ENTER)
3316 /* Cleanup the stack if the tree view is at a different tree. */
3317 while (!*opt_path && tree_stack)
3318 pop_tree_stack_entry();
3320 switch (line->type) {
3322 /* Depending on whether it is a subdir or parent (updir?) link
3323 * mangle the path buffer. */
3324 if (line == &view->line[1] && *opt_path) {
3325 pop_tree_stack_entry();
3328 char *basename = tree_path(line);
3330 push_tree_stack_entry(basename, view->lineno);
3333 /* Trees and subtrees share the same ID, so they are not not
3334 * unique like blobs. */
3335 flags = OPEN_RELOAD;
3336 request = REQ_VIEW_TREE;
3339 case LINE_TREE_FILE:
3340 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3341 request = REQ_VIEW_BLOB;
3348 open_view(view, request, flags);
3349 if (request == REQ_VIEW_TREE) {
3350 view->lineno = tree_lineno;
3357 tree_select(struct view *view, struct line *line)
3359 char *text = (char *)line->data + STRING_SIZE("100644 blob ");
3361 if (line->type == LINE_TREE_FILE) {
3362 string_copy_rev(ref_blob, text);
3364 } else if (line->type != LINE_TREE_DIR) {
3368 string_copy_rev(view->ref, text);
3371 static struct view_ops tree_ops = {
3382 blob_read(struct view *view, char *line)
3386 return add_line_text(view, line, LINE_DEFAULT) != NULL;
3389 static struct view_ops blob_ops = {
3402 * Loading the blame view is a two phase job:
3404 * 1. File content is read either using opt_file from the
3405 * filesystem or using git-cat-file.
3406 * 2. Then blame information is incrementally added by
3407 * reading output from git-blame.
3410 struct blame_commit {
3411 char id[SIZEOF_REV]; /* SHA1 ID. */
3412 char title[128]; /* First line of the commit message. */
3413 char author[75]; /* Author of the commit. */
3414 struct tm time; /* Date from the author ident. */
3415 char filename[128]; /* Name of file. */
3419 struct blame_commit *commit;
3420 unsigned int header:1;
3424 #define BLAME_CAT_FILE_CMD "git cat-file blob %s:%s"
3425 #define BLAME_INCREMENTAL_CMD "git blame --incremental %s %s"
3428 blame_open(struct view *view)
3430 char path[SIZEOF_STR];
3431 char ref[SIZEOF_STR] = "";
3433 if (sq_quote(path, 0, opt_file) >= sizeof(path))
3436 if (*opt_ref && sq_quote(ref, 0, opt_ref) >= sizeof(ref))
3440 if (!string_format(view->cmd, BLAME_CAT_FILE_CMD, ref, path))
3443 view->pipe = fopen(opt_file, "r");
3445 !string_format(view->cmd, BLAME_CAT_FILE_CMD, "HEAD", path))
3450 view->pipe = popen(view->cmd, "r");
3454 if (!string_format(view->cmd, BLAME_INCREMENTAL_CMD, ref, path))
3457 string_format(view->ref, "%s ...", opt_file);
3458 string_copy_rev(view->vid, opt_file);
3459 set_nonblocking_input(TRUE);
3464 for (i = 0; i < view->lines; i++)
3465 free(view->line[i].data);
3469 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3470 view->offset = view->lines = view->lineno = 0;
3472 view->start_time = time(NULL);
3477 static struct blame_commit *
3478 get_blame_commit(struct view *view, const char *id)
3482 for (i = 0; i < view->lines; i++) {
3483 struct blame *blame = view->line[i].data;
3488 if (!strncmp(blame->commit->id, id, SIZEOF_REV - 1))
3489 return blame->commit;
3493 struct blame_commit *commit = calloc(1, sizeof(*commit));
3496 string_ncopy(commit->id, id, SIZEOF_REV);
3502 parse_number(char **posref, size_t *number, size_t min, size_t max)
3504 char *pos = *posref;
3507 pos = strchr(pos + 1, ' ');
3508 if (!pos || !isdigit(pos[1]))
3510 *number = atoi(pos + 1);
3511 if (*number < min || *number > max)
3518 static struct blame_commit *
3519 parse_blame_commit(struct view *view, char *text, int *blamed)
3521 struct blame_commit *commit;
3522 struct blame *blame;
3523 char *pos = text + SIZEOF_REV - 1;
3527 if (strlen(text) <= SIZEOF_REV || *pos != ' ')
3530 if (!parse_number(&pos, &lineno, 1, view->lines) ||
3531 !parse_number(&pos, &group, 1, view->lines - lineno + 1))
3534 commit = get_blame_commit(view, text);
3540 struct line *line = &view->line[lineno + group - 1];
3543 blame->commit = commit;
3544 blame->header = !group;
3552 blame_read_file(struct view *view, char *line)
3557 if (view->lines > 0)
3558 pipe = popen(view->cmd, "r");
3559 else if (!view->parent)
3560 die("No blame exist for %s", view->vid);
3563 report("Failed to load blame data");
3572 size_t linelen = strlen(line);
3573 struct blame *blame = malloc(sizeof(*blame) + linelen);
3578 blame->commit = NULL;
3579 strncpy(blame->text, line, linelen);
3580 blame->text[linelen] = 0;
3581 return add_line_data(view, blame, LINE_BLAME_ID) != NULL;
3586 match_blame_header(const char *name, char **line)
3588 size_t namelen = strlen(name);
3589 bool matched = !strncmp(name, *line, namelen);
3598 blame_read(struct view *view, char *line)
3600 static struct blame_commit *commit = NULL;
3601 static int blamed = 0;
3602 static time_t author_time;
3605 return blame_read_file(view, line);
3611 string_format(view->ref, "%s", view->vid);
3612 if (view_is_displayed(view)) {
3613 update_view_title(view);
3614 redraw_view_from(view, 0);
3620 commit = parse_blame_commit(view, line, &blamed);
3621 string_format(view->ref, "%s %2d%%", view->vid,
3622 blamed * 100 / view->lines);
3624 } else if (match_blame_header("author ", &line)) {
3625 string_ncopy(commit->author, line, strlen(line));
3627 } else if (match_blame_header("author-time ", &line)) {
3628 author_time = (time_t) atol(line);
3630 } else if (match_blame_header("author-tz ", &line)) {
3633 tz = ('0' - line[1]) * 60 * 60 * 10;
3634 tz += ('0' - line[2]) * 60 * 60;
3635 tz += ('0' - line[3]) * 60;
3636 tz += ('0' - line[4]) * 60;
3642 gmtime_r(&author_time, &commit->time);
3644 } else if (match_blame_header("summary ", &line)) {
3645 string_ncopy(commit->title, line, strlen(line));
3647 } else if (match_blame_header("filename ", &line)) {
3648 string_ncopy(commit->filename, line, strlen(line));
3656 blame_draw(struct view *view, struct line *line, unsigned int lineno)
3658 struct blame *blame = line->data;
3659 struct tm *time = NULL;
3660 char *id = NULL, *author = NULL;
3662 if (blame->commit && *blame->commit->filename) {
3663 id = blame->commit->id;
3664 author = blame->commit->author;
3665 time = &blame->commit->time;
3668 if (opt_date && draw_date(view, time))
3672 draw_field(view, LINE_MAIN_AUTHOR, author, AUTHOR_COLS, TRUE))
3675 if (draw_field(view, LINE_BLAME_ID, id, ID_COLS, FALSE))
3678 if (draw_lineno(view, lineno))
3681 draw_text(view, LINE_DEFAULT, blame->text, TRUE);
3686 blame_request(struct view *view, enum request request, struct line *line)
3688 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3689 struct blame *blame = line->data;
3693 if (!blame->commit) {
3694 report("No commit loaded yet");
3698 if (!strcmp(blame->commit->id, NULL_ID)) {
3699 char path[SIZEOF_STR];
3701 if (sq_quote(path, 0, view->vid) >= sizeof(path))
3703 string_format(opt_cmd, "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s 2>/dev/null", path);
3706 open_view(view, REQ_VIEW_DIFF, flags);
3717 blame_grep(struct view *view, struct line *line)
3719 struct blame *blame = line->data;
3720 struct blame_commit *commit = blame->commit;
3723 #define MATCH(text, on) \
3724 (on && *text && regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3727 char buf[DATE_COLS + 1];
3729 if (MATCH(commit->title, 1) ||
3730 MATCH(commit->author, opt_author) ||
3731 MATCH(commit->id, opt_date))
3734 if (strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time) &&
3739 return MATCH(blame->text, 1);
3745 blame_select(struct view *view, struct line *line)
3747 struct blame *blame = line->data;
3748 struct blame_commit *commit = blame->commit;
3753 if (!strcmp(commit->id, NULL_ID))
3754 string_ncopy(ref_commit, "HEAD", 4);
3756 string_copy_rev(ref_commit, commit->id);
3759 static struct view_ops blame_ops = {
3777 char rev[SIZEOF_REV];
3778 char name[SIZEOF_STR];
3782 char rev[SIZEOF_REV];
3783 char name[SIZEOF_STR];
3787 static char status_onbranch[SIZEOF_STR];
3788 static struct status stage_status;
3789 static enum line_type stage_line_type;
3791 /* Get fields from the diff line:
3792 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
3795 status_get_diff(struct status *file, char *buf, size_t bufsize)
3797 char *old_mode = buf + 1;
3798 char *new_mode = buf + 8;
3799 char *old_rev = buf + 15;
3800 char *new_rev = buf + 56;
3801 char *status = buf + 97;
3804 old_mode[-1] != ':' ||
3805 new_mode[-1] != ' ' ||
3806 old_rev[-1] != ' ' ||
3807 new_rev[-1] != ' ' ||
3811 file->status = *status;
3813 string_copy_rev(file->old.rev, old_rev);
3814 string_copy_rev(file->new.rev, new_rev);
3816 file->old.mode = strtoul(old_mode, NULL, 8);
3817 file->new.mode = strtoul(new_mode, NULL, 8);
3819 file->old.name[0] = file->new.name[0] = 0;
3825 status_run(struct view *view, const char cmd[], char status, enum line_type type)
3827 struct status *file = NULL;
3828 struct status *unmerged = NULL;
3829 char buf[SIZEOF_STR * 4];
3833 pipe = popen(cmd, "r");
3837 add_line_data(view, NULL, type);
3839 while (!feof(pipe) && !ferror(pipe)) {
3843 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
3846 bufsize += readsize;
3848 /* Process while we have NUL chars. */
3849 while ((sep = memchr(buf, 0, bufsize))) {
3850 size_t sepsize = sep - buf + 1;
3853 if (!realloc_lines(view, view->line_size + 1))
3856 file = calloc(1, sizeof(*file));
3860 add_line_data(view, file, type);
3863 /* Parse diff info part. */
3865 file->status = status;
3867 string_copy(file->old.rev, NULL_ID);
3869 } else if (!file->status) {
3870 if (!status_get_diff(file, buf, sepsize))
3874 memmove(buf, sep + 1, bufsize);
3876 sep = memchr(buf, 0, bufsize);
3879 sepsize = sep - buf + 1;
3881 /* Collapse all 'M'odified entries that
3882 * follow a associated 'U'nmerged entry.
3884 if (file->status == 'U') {
3887 } else if (unmerged) {
3888 int collapse = !strcmp(buf, unmerged->new.name);
3899 /* Grab the old name for rename/copy. */
3900 if (!*file->old.name &&
3901 (file->status == 'R' || file->status == 'C')) {
3902 sepsize = sep - buf + 1;
3903 string_ncopy(file->old.name, buf, sepsize);
3905 memmove(buf, sep + 1, bufsize);
3907 sep = memchr(buf, 0, bufsize);
3910 sepsize = sep - buf + 1;
3913 /* git-ls-files just delivers a NUL separated
3914 * list of file names similar to the second half
3915 * of the git-diff-* output. */
3916 string_ncopy(file->new.name, buf, sepsize);
3917 if (!*file->old.name)
3918 string_copy(file->old.name, file->new.name);
3920 memmove(buf, sep + 1, bufsize);
3931 if (!view->line[view->lines - 1].data)
3932 add_line_data(view, NULL, LINE_STAT_NONE);
3938 /* Don't show unmerged entries in the staged section. */
3939 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached -M HEAD"
3940 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
3941 #define STATUS_LIST_OTHER_CMD \
3942 "git ls-files -z --others --exclude-per-directory=.gitignore"
3943 #define STATUS_LIST_NO_HEAD_CMD \
3944 "git ls-files -z --cached --exclude-per-directory=.gitignore"
3946 #define STATUS_DIFF_INDEX_SHOW_CMD \
3947 "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s %s 2>/dev/null"
3949 #define STATUS_DIFF_FILES_SHOW_CMD \
3950 "git diff-files --root --patch-with-stat -C -M -- %s %s 2>/dev/null"
3952 #define STATUS_DIFF_NO_HEAD_SHOW_CMD \
3953 "git diff --no-color --patch-with-stat /dev/null %s 2>/dev/null"
3955 /* First parse staged info using git-diff-index(1), then parse unstaged
3956 * info using git-diff-files(1), and finally untracked files using
3957 * git-ls-files(1). */
3959 status_open(struct view *view)
3961 struct stat statbuf;
3962 char exclude[SIZEOF_STR];
3963 char indexcmd[SIZEOF_STR] = STATUS_DIFF_INDEX_CMD;
3964 char othercmd[SIZEOF_STR] = STATUS_LIST_OTHER_CMD;
3965 unsigned long prev_lineno = view->lineno;
3966 char indexstatus = 0;
3969 for (i = 0; i < view->lines; i++)
3970 free(view->line[i].data);
3972 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3975 if (!realloc_lines(view, view->line_size + 7))
3978 add_line_data(view, NULL, LINE_STAT_HEAD);
3980 string_copy(status_onbranch, "Initial commit");
3981 else if (!*opt_head)
3982 string_copy(status_onbranch, "Not currently on any branch");
3983 else if (!string_format(status_onbranch, "On branch %s", opt_head))
3987 string_copy(indexcmd, STATUS_LIST_NO_HEAD_CMD);
3991 if (!string_format(exclude, "%s/info/exclude", opt_git_dir))
3994 if (stat(exclude, &statbuf) >= 0) {
3995 size_t cmdsize = strlen(othercmd);
3997 if (!string_format_from(othercmd, &cmdsize, " %s", "--exclude-from=") ||
3998 sq_quote(othercmd, cmdsize, exclude) >= sizeof(othercmd))
4001 cmdsize = strlen(indexcmd);
4003 (!string_format_from(indexcmd, &cmdsize, " %s", "--exclude-from=") ||
4004 sq_quote(indexcmd, cmdsize, exclude) >= sizeof(indexcmd)))
4008 system("git update-index -q --refresh >/dev/null 2>/dev/null");
4010 if (!status_run(view, indexcmd, indexstatus, LINE_STAT_STAGED) ||
4011 !status_run(view, STATUS_DIFF_FILES_CMD, 0, LINE_STAT_UNSTAGED) ||
4012 !status_run(view, othercmd, '?', LINE_STAT_UNTRACKED))
4015 /* If all went well restore the previous line number to stay in
4016 * the context or select a line with something that can be
4018 if (prev_lineno >= view->lines)
4019 prev_lineno = view->lines - 1;
4020 while (prev_lineno < view->lines && !view->line[prev_lineno].data)
4022 while (prev_lineno > 0 && !view->line[prev_lineno].data)
4025 /* If the above fails, always skip the "On branch" line. */
4026 if (prev_lineno < view->lines)
4027 view->lineno = prev_lineno;
4031 if (view->lineno < view->offset)
4032 view->offset = view->lineno;
4033 else if (view->offset + view->height <= view->lineno)
4034 view->offset = view->lineno - view->height + 1;
4040 status_draw(struct view *view, struct line *line, unsigned int lineno)
4042 struct status *status = line->data;
4043 enum line_type type;
4047 switch (line->type) {
4048 case LINE_STAT_STAGED:
4049 type = LINE_STAT_SECTION;
4050 text = "Changes to be committed:";
4053 case LINE_STAT_UNSTAGED:
4054 type = LINE_STAT_SECTION;
4055 text = "Changed but not updated:";
4058 case LINE_STAT_UNTRACKED:
4059 type = LINE_STAT_SECTION;
4060 text = "Untracked files:";
4063 case LINE_STAT_NONE:
4064 type = LINE_DEFAULT;
4065 text = " (no files)";
4068 case LINE_STAT_HEAD:
4069 type = LINE_STAT_HEAD;
4070 text = status_onbranch;
4077 char buf[] = { status->status, ' ', ' ', ' ', 0 };
4079 if (draw_text(view, line->type, buf, TRUE))
4081 type = LINE_DEFAULT;
4082 text = status->new.name;
4085 draw_text(view, type, text, TRUE);
4090 status_enter(struct view *view, struct line *line)
4092 struct status *status = line->data;
4093 char oldpath[SIZEOF_STR] = "";
4094 char newpath[SIZEOF_STR] = "";
4097 enum open_flags split;
4099 if (line->type == LINE_STAT_NONE ||
4100 (!status && line[1].type == LINE_STAT_NONE)) {
4101 report("No file to diff");
4106 if (sq_quote(oldpath, 0, status->old.name) >= sizeof(oldpath))
4108 /* Diffs for unmerged entries are empty when pasing the
4109 * new path, so leave it empty. */
4110 if (status->status != 'U' &&
4111 sq_quote(newpath, 0, status->new.name) >= sizeof(newpath))
4116 line->type != LINE_STAT_UNTRACKED &&
4117 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
4120 switch (line->type) {
4121 case LINE_STAT_STAGED:
4123 if (!string_format_from(opt_cmd, &cmdsize,
4124 STATUS_DIFF_NO_HEAD_SHOW_CMD,
4128 if (!string_format_from(opt_cmd, &cmdsize,
4129 STATUS_DIFF_INDEX_SHOW_CMD,
4135 info = "Staged changes to %s";
4137 info = "Staged changes";
4140 case LINE_STAT_UNSTAGED:
4141 if (!string_format_from(opt_cmd, &cmdsize,
4142 STATUS_DIFF_FILES_SHOW_CMD, oldpath, newpath))
4145 info = "Unstaged changes to %s";
4147 info = "Unstaged changes";
4150 case LINE_STAT_UNTRACKED:
4155 report("No file to show");
4159 opt_pipe = fopen(status->new.name, "r");
4160 info = "Untracked file %s";
4163 case LINE_STAT_HEAD:
4167 die("line type %d not handled in switch", line->type);
4170 split = view_is_displayed(view) ? OPEN_SPLIT : 0;
4171 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | split);
4172 if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) {
4174 stage_status = *status;
4176 memset(&stage_status, 0, sizeof(stage_status));
4179 stage_line_type = line->type;
4180 string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.new.name);
4187 status_exists(struct status *status, enum line_type type)
4189 struct view *view = VIEW(REQ_VIEW_STATUS);
4192 for (line = view->line; line < view->line + view->lines; line++) {
4193 struct status *pos = line->data;
4195 if (line->type == type && pos &&
4196 !strcmp(status->new.name, pos->new.name))
4205 status_update_prepare(enum line_type type)
4207 char cmd[SIZEOF_STR];
4211 type != LINE_STAT_UNTRACKED &&
4212 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4216 case LINE_STAT_STAGED:
4217 string_add(cmd, cmdsize, "git update-index -z --index-info");
4220 case LINE_STAT_UNSTAGED:
4221 case LINE_STAT_UNTRACKED:
4222 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
4226 die("line type %d not handled in switch", type);
4229 return popen(cmd, "w");
4233 status_update_write(FILE *pipe, struct status *status, enum line_type type)
4235 char buf[SIZEOF_STR];
4240 case LINE_STAT_STAGED:
4241 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
4244 status->old.name, 0))
4248 case LINE_STAT_UNSTAGED:
4249 case LINE_STAT_UNTRACKED:
4250 if (!string_format_from(buf, &bufsize, "%s%c", status->new.name, 0))
4255 die("line type %d not handled in switch", type);
4258 while (!ferror(pipe) && written < bufsize) {
4259 written += fwrite(buf + written, 1, bufsize - written, pipe);
4262 return written == bufsize;
4266 status_update_file(struct status *status, enum line_type type)
4268 FILE *pipe = status_update_prepare(type);
4274 result = status_update_write(pipe, status, type);
4280 status_update_files(struct view *view, struct line *line)
4282 FILE *pipe = status_update_prepare(line->type);
4284 struct line *pos = view->line + view->lines;
4291 for (pos = line; pos < view->line + view->lines && pos->data; pos++)
4294 for (file = 0, done = 0; result && file < files; line++, file++) {
4295 int almost_done = file * 100 / files;
4297 if (almost_done > done) {
4299 string_format(view->ref, "updating file %u of %u (%d%% done)",
4301 update_view_title(view);
4303 result = status_update_write(pipe, line->data, line->type);
4311 status_update(struct view *view)
4313 struct line *line = &view->line[view->lineno];
4315 assert(view->lines);
4318 /* This should work even for the "On branch" line. */
4319 if (line < view->line + view->lines && !line[1].data) {
4320 report("Nothing to update");
4324 if (!status_update_files(view, line + 1)) {
4325 report("Failed to update file status");
4329 } else if (!status_update_file(line->data, line->type)) {
4330 report("Failed to update file status");
4338 status_request(struct view *view, enum request request, struct line *line)
4340 struct status *status = line->data;
4343 case REQ_STATUS_UPDATE:
4344 if (!status_update(view))
4348 case REQ_STATUS_MERGE:
4349 if (!status || status->status != 'U') {
4350 report("Merging only possible for files with unmerged status ('U').");
4353 open_mergetool(status->new.name);
4360 open_editor(status->status != '?', status->new.name);
4363 case REQ_VIEW_BLAME:
4365 string_copy(opt_file, status->new.name);
4371 /* After returning the status view has been split to
4372 * show the stage view. No further reloading is
4374 status_enter(view, line);
4378 /* Simply reload the view. */
4385 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
4391 status_select(struct view *view, struct line *line)
4393 struct status *status = line->data;
4394 char file[SIZEOF_STR] = "all files";
4398 if (status && !string_format(file, "'%s'", status->new.name))
4401 if (!status && line[1].type == LINE_STAT_NONE)
4404 switch (line->type) {
4405 case LINE_STAT_STAGED:
4406 text = "Press %s to unstage %s for commit";
4409 case LINE_STAT_UNSTAGED:
4410 text = "Press %s to stage %s for commit";
4413 case LINE_STAT_UNTRACKED:
4414 text = "Press %s to stage %s for addition";
4417 case LINE_STAT_HEAD:
4418 case LINE_STAT_NONE:
4419 text = "Nothing to update";
4423 die("line type %d not handled in switch", line->type);
4426 if (status && status->status == 'U') {
4427 text = "Press %s to resolve conflict in %s";
4428 key = get_key(REQ_STATUS_MERGE);
4431 key = get_key(REQ_STATUS_UPDATE);
4434 string_format(view->ref, text, key, file);
4438 status_grep(struct view *view, struct line *line)
4440 struct status *status = line->data;
4441 enum { S_STATUS, S_NAME, S_END } state;
4448 for (state = S_STATUS; state < S_END; state++) {
4452 case S_NAME: text = status->new.name; break;
4454 buf[0] = status->status;
4462 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4469 static struct view_ops status_ops = {
4481 stage_diff_line(FILE *pipe, struct line *line)
4483 char *buf = line->data;
4484 size_t bufsize = strlen(buf);
4487 while (!ferror(pipe) && written < bufsize) {
4488 written += fwrite(buf + written, 1, bufsize - written, pipe);
4493 return written == bufsize;
4497 stage_diff_write(FILE *pipe, struct line *line, struct line *end)
4499 while (line < end) {
4500 if (!stage_diff_line(pipe, line++))
4502 if (line->type == LINE_DIFF_CHUNK ||
4503 line->type == LINE_DIFF_HEADER)
4510 static struct line *
4511 stage_diff_find(struct view *view, struct line *line, enum line_type type)
4513 for (; view->line < line; line--)
4514 if (line->type == type)
4521 stage_update_chunk(struct view *view, struct line *chunk)
4523 char cmd[SIZEOF_STR];
4525 struct line *diff_hdr;
4528 diff_hdr = stage_diff_find(view, chunk, LINE_DIFF_HEADER);
4533 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4536 if (!string_format_from(cmd, &cmdsize,
4537 "git apply --whitespace=nowarn --cached %s - && "
4538 "git update-index -q --unmerged --refresh 2>/dev/null",
4539 stage_line_type == LINE_STAT_STAGED ? "-R" : ""))
4542 pipe = popen(cmd, "w");
4546 if (!stage_diff_write(pipe, diff_hdr, chunk) ||
4547 !stage_diff_write(pipe, chunk, view->line + view->lines))
4552 return chunk ? TRUE : FALSE;
4556 stage_update(struct view *view, struct line *line)
4558 struct line *chunk = NULL;
4560 if (!opt_no_head && stage_line_type != LINE_STAT_UNTRACKED)
4561 chunk = stage_diff_find(view, line, LINE_DIFF_CHUNK);
4564 if (!stage_update_chunk(view, chunk)) {
4565 report("Failed to apply chunk");
4569 } else if (!stage_status.status) {
4570 view = VIEW(REQ_VIEW_STATUS);
4572 for (line = view->line; line < view->line + view->lines; line++)
4573 if (line->type == stage_line_type)
4576 if (!status_update_files(view, line + 1)) {
4577 report("Failed to update files");
4581 } else if (!status_update_file(&stage_status, stage_line_type)) {
4582 report("Failed to update file");
4590 stage_request(struct view *view, enum request request, struct line *line)
4593 case REQ_STATUS_UPDATE:
4594 if (!stage_update(view, line))
4599 if (!stage_status.new.name[0])
4602 open_editor(stage_status.status != '?', stage_status.new.name);
4606 /* Reload everything ... */
4609 case REQ_VIEW_BLAME:
4610 if (stage_status.new.name[0]) {
4611 string_copy(opt_file, stage_status.new.name);
4617 return pager_request(view, request, line);
4623 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD | OPEN_NOMAXIMIZE);
4625 /* Check whether the staged entry still exists, and close the
4626 * stage view if it doesn't. */
4627 if (!status_exists(&stage_status, stage_line_type))
4628 return REQ_VIEW_CLOSE;
4630 if (stage_line_type == LINE_STAT_UNTRACKED)
4631 opt_pipe = fopen(stage_status.new.name, "r");
4633 string_copy(opt_cmd, view->cmd);
4634 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | OPEN_NOMAXIMIZE);
4639 static struct view_ops stage_ops = {
4655 char id[SIZEOF_REV]; /* SHA1 ID. */
4656 char title[128]; /* First line of the commit message. */
4657 char author[75]; /* Author of the commit. */
4658 struct tm time; /* Date from the author ident. */
4659 struct ref **refs; /* Repository references. */
4660 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
4661 size_t graph_size; /* The width of the graph array. */
4662 bool has_parents; /* Rewritten --parents seen. */
4665 /* Size of rev graph with no "padding" columns */
4666 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
4669 struct rev_graph *prev, *next, *parents;
4670 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
4672 struct commit *commit;
4674 unsigned int boundary:1;
4677 /* Parents of the commit being visualized. */
4678 static struct rev_graph graph_parents[4];
4680 /* The current stack of revisions on the graph. */
4681 static struct rev_graph graph_stacks[4] = {
4682 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
4683 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
4684 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
4685 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
4689 graph_parent_is_merge(struct rev_graph *graph)
4691 return graph->parents->size > 1;
4695 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
4697 struct commit *commit = graph->commit;
4699 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
4700 commit->graph[commit->graph_size++] = symbol;
4704 done_rev_graph(struct rev_graph *graph)
4706 if (graph_parent_is_merge(graph) &&
4707 graph->pos < graph->size - 1 &&
4708 graph->next->size == graph->size + graph->parents->size - 1) {
4709 size_t i = graph->pos + graph->parents->size - 1;
4711 graph->commit->graph_size = i * 2;
4712 while (i < graph->next->size - 1) {
4713 append_to_rev_graph(graph, ' ');
4714 append_to_rev_graph(graph, '\\');
4719 graph->size = graph->pos = 0;
4720 graph->commit = NULL;
4721 memset(graph->parents, 0, sizeof(*graph->parents));
4725 push_rev_graph(struct rev_graph *graph, char *parent)
4729 /* "Collapse" duplicate parents lines.
4731 * FIXME: This needs to also update update the drawn graph but
4732 * for now it just serves as a method for pruning graph lines. */
4733 for (i = 0; i < graph->size; i++)
4734 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
4737 if (graph->size < SIZEOF_REVITEMS) {
4738 string_copy_rev(graph->rev[graph->size++], parent);
4743 get_rev_graph_symbol(struct rev_graph *graph)
4747 if (graph->boundary)
4748 symbol = REVGRAPH_BOUND;
4749 else if (graph->parents->size == 0)
4750 symbol = REVGRAPH_INIT;
4751 else if (graph_parent_is_merge(graph))
4752 symbol = REVGRAPH_MERGE;
4753 else if (graph->pos >= graph->size)
4754 symbol = REVGRAPH_BRANCH;
4756 symbol = REVGRAPH_COMMIT;
4762 draw_rev_graph(struct rev_graph *graph)
4765 chtype separator, line;
4767 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
4768 static struct rev_filler fillers[] = {
4774 chtype symbol = get_rev_graph_symbol(graph);
4775 struct rev_filler *filler;
4778 if (opt_line_graphics)
4779 fillers[DEFAULT].line = line_graphics[LINE_GRAPHIC_VLINE];
4781 filler = &fillers[DEFAULT];
4783 for (i = 0; i < graph->pos; i++) {
4784 append_to_rev_graph(graph, filler->line);
4785 if (graph_parent_is_merge(graph->prev) &&
4786 graph->prev->pos == i)
4787 filler = &fillers[RSHARP];
4789 append_to_rev_graph(graph, filler->separator);
4792 /* Place the symbol for this revision. */
4793 append_to_rev_graph(graph, symbol);
4795 if (graph->prev->size > graph->size)
4796 filler = &fillers[RDIAG];
4798 filler = &fillers[DEFAULT];
4802 for (; i < graph->size; i++) {
4803 append_to_rev_graph(graph, filler->separator);
4804 append_to_rev_graph(graph, filler->line);
4805 if (graph_parent_is_merge(graph->prev) &&
4806 i < graph->prev->pos + graph->parents->size)
4807 filler = &fillers[RSHARP];
4808 if (graph->prev->size > graph->size)
4809 filler = &fillers[LDIAG];
4812 if (graph->prev->size > graph->size) {
4813 append_to_rev_graph(graph, filler->separator);
4814 if (filler->line != ' ')
4815 append_to_rev_graph(graph, filler->line);
4819 /* Prepare the next rev graph */
4821 prepare_rev_graph(struct rev_graph *graph)
4825 /* First, traverse all lines of revisions up to the active one. */
4826 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
4827 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
4830 push_rev_graph(graph->next, graph->rev[graph->pos]);
4833 /* Interleave the new revision parent(s). */
4834 for (i = 0; !graph->boundary && i < graph->parents->size; i++)
4835 push_rev_graph(graph->next, graph->parents->rev[i]);
4837 /* Lastly, put any remaining revisions. */
4838 for (i = graph->pos + 1; i < graph->size; i++)
4839 push_rev_graph(graph->next, graph->rev[i]);
4843 update_rev_graph(struct rev_graph *graph)
4845 /* If this is the finalizing update ... */
4847 prepare_rev_graph(graph);
4849 /* Graph visualization needs a one rev look-ahead,
4850 * so the first update doesn't visualize anything. */
4851 if (!graph->prev->commit)
4854 draw_rev_graph(graph->prev);
4855 done_rev_graph(graph->prev->prev);
4864 main_draw(struct view *view, struct line *line, unsigned int lineno)
4866 struct commit *commit = line->data;
4868 if (!*commit->author)
4871 if (opt_date && draw_date(view, &commit->time))
4875 draw_field(view, LINE_MAIN_AUTHOR, commit->author, AUTHOR_COLS, TRUE))
4878 if (opt_rev_graph && commit->graph_size &&
4879 draw_graphic(view, LINE_MAIN_REVGRAPH, commit->graph, commit->graph_size))
4882 if (opt_show_refs && commit->refs) {
4886 enum line_type type;
4888 if (commit->refs[i]->head)
4889 type = LINE_MAIN_HEAD;
4890 else if (commit->refs[i]->ltag)
4891 type = LINE_MAIN_LOCAL_TAG;
4892 else if (commit->refs[i]->tag)
4893 type = LINE_MAIN_TAG;
4894 else if (commit->refs[i]->tracked)
4895 type = LINE_MAIN_TRACKED;
4896 else if (commit->refs[i]->remote)
4897 type = LINE_MAIN_REMOTE;
4899 type = LINE_MAIN_REF;
4901 if (draw_text(view, type, "[", TRUE) ||
4902 draw_text(view, type, commit->refs[i]->name, TRUE) ||
4903 draw_text(view, type, "]", TRUE))
4906 if (draw_text(view, LINE_DEFAULT, " ", TRUE))
4908 } while (commit->refs[i++]->next);
4911 draw_text(view, LINE_DEFAULT, commit->title, TRUE);
4915 /* Reads git log --pretty=raw output and parses it into the commit struct. */
4917 main_read(struct view *view, char *line)
4919 static struct rev_graph *graph = graph_stacks;
4920 enum line_type type;
4921 struct commit *commit;
4924 if (!view->lines && !view->parent)
4925 die("No revisions match the given arguments.");
4926 update_rev_graph(graph);
4930 type = get_line_type(line);
4931 if (type == LINE_COMMIT) {
4932 commit = calloc(1, sizeof(struct commit));
4936 line += STRING_SIZE("commit ");
4938 graph->boundary = 1;
4942 string_copy_rev(commit->id, line);
4943 commit->refs = get_refs(commit->id);
4944 graph->commit = commit;
4945 add_line_data(view, commit, LINE_MAIN_COMMIT);
4947 while ((line = strchr(line, ' '))) {
4949 push_rev_graph(graph->parents, line);
4950 commit->has_parents = TRUE;
4957 commit = view->line[view->lines - 1].data;
4961 if (commit->has_parents)
4963 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
4968 /* Parse author lines where the name may be empty:
4969 * author <email@address.tld> 1138474660 +0100
4971 char *ident = line + STRING_SIZE("author ");
4972 char *nameend = strchr(ident, '<');
4973 char *emailend = strchr(ident, '>');
4975 if (!nameend || !emailend)
4978 update_rev_graph(graph);
4979 graph = graph->next;
4981 *nameend = *emailend = 0;
4982 ident = chomp_string(ident);
4984 ident = chomp_string(nameend + 1);
4989 string_ncopy(commit->author, ident, strlen(ident));
4991 /* Parse epoch and timezone */
4992 if (emailend[1] == ' ') {
4993 char *secs = emailend + 2;
4994 char *zone = strchr(secs, ' ');
4995 time_t time = (time_t) atol(secs);
4997 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
5001 tz = ('0' - zone[1]) * 60 * 60 * 10;
5002 tz += ('0' - zone[2]) * 60 * 60;
5003 tz += ('0' - zone[3]) * 60;
5004 tz += ('0' - zone[4]) * 60;
5012 gmtime_r(&time, &commit->time);
5017 /* Fill in the commit title if it has not already been set. */
5018 if (commit->title[0])
5021 /* Require titles to start with a non-space character at the
5022 * offset used by git log. */
5023 if (strncmp(line, " ", 4))
5026 /* Well, if the title starts with a whitespace character,
5027 * try to be forgiving. Otherwise we end up with no title. */
5028 while (isspace(*line))
5032 /* FIXME: More graceful handling of titles; append "..." to
5033 * shortened titles, etc. */
5035 string_ncopy(commit->title, line, strlen(line));
5042 main_request(struct view *view, enum request request, struct line *line)
5044 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
5046 if (request == REQ_ENTER)
5047 open_view(view, REQ_VIEW_DIFF, flags);
5055 grep_refs(struct ref **refs, regex_t *regex)
5063 if (regexec(regex, refs[i]->name, 1, &pmatch, 0) != REG_NOMATCH)
5065 } while (refs[i++]->next);
5071 main_grep(struct view *view, struct line *line)
5073 struct commit *commit = line->data;
5074 enum { S_TITLE, S_AUTHOR, S_DATE, S_REFS, S_END } state;
5075 char buf[DATE_COLS + 1];
5078 for (state = S_TITLE; state < S_END; state++) {
5082 case S_TITLE: text = commit->title; break;
5086 text = commit->author;
5091 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
5098 if (grep_refs(commit->refs, view->regex) == TRUE)
5105 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
5113 main_select(struct view *view, struct line *line)
5115 struct commit *commit = line->data;
5117 string_copy_rev(view->ref, commit->id);
5118 string_copy_rev(ref_commit, view->ref);
5121 static struct view_ops main_ops = {
5133 * Unicode / UTF-8 handling
5135 * NOTE: Much of the following code for dealing with unicode is derived from
5136 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
5137 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
5140 /* I've (over)annotated a lot of code snippets because I am not entirely
5141 * confident that the approach taken by this small UTF-8 interface is correct.
5145 unicode_width(unsigned long c)
5148 (c <= 0x115f /* Hangul Jamo */
5151 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
5153 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
5154 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
5155 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
5156 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
5157 || (c >= 0xffe0 && c <= 0xffe6)
5158 || (c >= 0x20000 && c <= 0x2fffd)
5159 || (c >= 0x30000 && c <= 0x3fffd)))
5163 return opt_tab_size;
5168 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
5169 * Illegal bytes are set one. */
5170 static const unsigned char utf8_bytes[256] = {
5171 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,
5172 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,
5173 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,
5174 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,
5175 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,
5176 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,
5177 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,
5178 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,
5181 /* Decode UTF-8 multi-byte representation into a unicode character. */
5182 static inline unsigned long
5183 utf8_to_unicode(const char *string, size_t length)
5185 unsigned long unicode;
5189 unicode = string[0];
5192 unicode = (string[0] & 0x1f) << 6;
5193 unicode += (string[1] & 0x3f);
5196 unicode = (string[0] & 0x0f) << 12;
5197 unicode += ((string[1] & 0x3f) << 6);
5198 unicode += (string[2] & 0x3f);
5201 unicode = (string[0] & 0x0f) << 18;
5202 unicode += ((string[1] & 0x3f) << 12);
5203 unicode += ((string[2] & 0x3f) << 6);
5204 unicode += (string[3] & 0x3f);
5207 unicode = (string[0] & 0x0f) << 24;
5208 unicode += ((string[1] & 0x3f) << 18);
5209 unicode += ((string[2] & 0x3f) << 12);
5210 unicode += ((string[3] & 0x3f) << 6);
5211 unicode += (string[4] & 0x3f);
5214 unicode = (string[0] & 0x01) << 30;
5215 unicode += ((string[1] & 0x3f) << 24);
5216 unicode += ((string[2] & 0x3f) << 18);
5217 unicode += ((string[3] & 0x3f) << 12);
5218 unicode += ((string[4] & 0x3f) << 6);
5219 unicode += (string[5] & 0x3f);
5222 die("Invalid unicode length");
5225 /* Invalid characters could return the special 0xfffd value but NUL
5226 * should be just as good. */
5227 return unicode > 0xffff ? 0 : unicode;
5230 /* Calculates how much of string can be shown within the given maximum width
5231 * and sets trimmed parameter to non-zero value if all of string could not be
5232 * shown. If the reserve flag is TRUE, it will reserve at least one
5233 * trailing character, which can be useful when drawing a delimiter.
5235 * Returns the number of bytes to output from string to satisfy max_width. */
5237 utf8_length(const char *string, int *width, size_t max_width, int *trimmed, bool reserve)
5239 const char *start = string;
5240 const char *end = strchr(string, '\0');
5241 unsigned char last_bytes = 0;
5242 size_t last_ucwidth = 0;
5247 while (string < end) {
5248 int c = *(unsigned char *) string;
5249 unsigned char bytes = utf8_bytes[c];
5251 unsigned long unicode;
5253 if (string + bytes > end)
5256 /* Change representation to figure out whether
5257 * it is a single- or double-width character. */
5259 unicode = utf8_to_unicode(string, bytes);
5260 /* FIXME: Graceful handling of invalid unicode character. */
5264 ucwidth = unicode_width(unicode);
5266 if (*width > max_width) {
5269 if (reserve && *width == max_width) {
5270 string -= last_bytes;
5271 *width -= last_ucwidth;
5278 last_ucwidth = ucwidth;
5281 return string - start;
5289 /* Whether or not the curses interface has been initialized. */
5290 static bool cursed = FALSE;
5292 /* The status window is used for polling keystrokes. */
5293 static WINDOW *status_win;
5295 static bool status_empty = TRUE;
5297 /* Update status and title window. */
5299 report(const char *msg, ...)
5301 struct view *view = display[current_view];
5307 char buf[SIZEOF_STR];
5310 va_start(args, msg);
5311 if (vsnprintf(buf, sizeof(buf), msg, args) >= sizeof(buf)) {
5312 buf[sizeof(buf) - 1] = 0;
5313 buf[sizeof(buf) - 2] = '.';
5314 buf[sizeof(buf) - 3] = '.';
5315 buf[sizeof(buf) - 4] = '.';
5321 if (!status_empty || *msg) {
5324 va_start(args, msg);
5326 wmove(status_win, 0, 0);
5328 vwprintw(status_win, msg, args);
5329 status_empty = FALSE;
5331 status_empty = TRUE;
5333 wclrtoeol(status_win);
5334 wrefresh(status_win);
5339 update_view_title(view);
5340 update_display_cursor(view);
5343 /* Controls when nodelay should be in effect when polling user input. */
5345 set_nonblocking_input(bool loading)
5347 static unsigned int loading_views;
5349 if ((loading == FALSE && loading_views-- == 1) ||
5350 (loading == TRUE && loading_views++ == 0))
5351 nodelay(status_win, loading);
5359 /* Initialize the curses library */
5360 if (isatty(STDIN_FILENO)) {
5361 cursed = !!initscr();
5363 /* Leave stdin and stdout alone when acting as a pager. */
5364 FILE *io = fopen("/dev/tty", "r+");
5367 die("Failed to open /dev/tty");
5368 cursed = !!newterm(NULL, io, io);
5372 die("Failed to initialize curses");
5374 nonl(); /* Tell curses not to do NL->CR/NL on output */
5375 cbreak(); /* Take input chars one at a time, no wait for \n */
5376 noecho(); /* Don't echo input */
5377 leaveok(stdscr, TRUE);
5382 getmaxyx(stdscr, y, x);
5383 status_win = newwin(1, 0, y - 1, 0);
5385 die("Failed to create status window");
5387 /* Enable keyboard mapping */
5388 keypad(status_win, TRUE);
5389 wbkgdset(status_win, get_line_attr(LINE_STATUS));
5391 TABSIZE = opt_tab_size;
5392 if (opt_line_graphics) {
5393 line_graphics[LINE_GRAPHIC_VLINE] = ACS_VLINE;
5398 read_prompt(const char *prompt)
5400 enum { READING, STOP, CANCEL } status = READING;
5401 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
5404 while (status == READING) {
5410 foreach_view (view, i)
5415 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
5416 wclrtoeol(status_win);
5418 /* Refresh, accept single keystroke of input */
5419 key = wgetch(status_win);
5424 status = pos ? STOP : CANCEL;
5442 if (pos >= sizeof(buf)) {
5443 report("Input string too long");
5448 buf[pos++] = (char) key;
5452 /* Clear the status window */
5453 status_empty = FALSE;
5456 if (status == CANCEL)
5465 * Repository references
5468 static struct ref *refs = NULL;
5469 static size_t refs_alloc = 0;
5470 static size_t refs_size = 0;
5472 /* Id <-> ref store */
5473 static struct ref ***id_refs = NULL;
5474 static size_t id_refs_alloc = 0;
5475 static size_t id_refs_size = 0;
5477 static struct ref **
5480 struct ref ***tmp_id_refs;
5481 struct ref **ref_list = NULL;
5482 size_t ref_list_alloc = 0;
5483 size_t ref_list_size = 0;
5486 for (i = 0; i < id_refs_size; i++)
5487 if (!strcmp(id, id_refs[i][0]->id))
5490 tmp_id_refs = realloc_items(id_refs, &id_refs_alloc, id_refs_size + 1,
5495 id_refs = tmp_id_refs;
5497 for (i = 0; i < refs_size; i++) {
5500 if (strcmp(id, refs[i].id))
5503 tmp = realloc_items(ref_list, &ref_list_alloc,
5504 ref_list_size + 1, sizeof(*ref_list));
5512 if (ref_list_size > 0)
5513 ref_list[ref_list_size - 1]->next = 1;
5514 ref_list[ref_list_size] = &refs[i];
5516 /* XXX: The properties of the commit chains ensures that we can
5517 * safely modify the shared ref. The repo references will
5518 * always be similar for the same id. */
5519 ref_list[ref_list_size]->next = 0;
5524 id_refs[id_refs_size++] = ref_list;
5530 read_ref(char *id, size_t idlen, char *name, size_t namelen)
5535 bool remote = FALSE;
5536 bool tracked = FALSE;
5537 bool check_replace = FALSE;
5540 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
5541 if (!strcmp(name + namelen - 3, "^{}")) {
5544 if (refs_size > 0 && refs[refs_size - 1].ltag == TRUE)
5545 check_replace = TRUE;
5551 namelen -= STRING_SIZE("refs/tags/");
5552 name += STRING_SIZE("refs/tags/");
5554 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
5556 namelen -= STRING_SIZE("refs/remotes/");
5557 name += STRING_SIZE("refs/remotes/");
5558 tracked = !strcmp(opt_remote, name);
5560 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5561 namelen -= STRING_SIZE("refs/heads/");
5562 name += STRING_SIZE("refs/heads/");
5563 head = !strncmp(opt_head, name, namelen);
5565 } else if (!strcmp(name, "HEAD")) {
5566 opt_no_head = FALSE;
5570 if (check_replace && !strcmp(name, refs[refs_size - 1].name)) {
5571 /* it's an annotated tag, replace the previous sha1 with the
5572 * resolved commit id; relies on the fact git-ls-remote lists
5573 * the commit id of an annotated tag right beofre the commit id
5575 refs[refs_size - 1].ltag = ltag;
5576 string_copy_rev(refs[refs_size - 1].id, id);
5580 refs = realloc_items(refs, &refs_alloc, refs_size + 1, sizeof(*refs));
5584 ref = &refs[refs_size++];
5585 ref->name = malloc(namelen + 1);
5589 strncpy(ref->name, name, namelen);
5590 ref->name[namelen] = 0;
5594 ref->remote = remote;
5595 ref->tracked = tracked;
5596 string_copy_rev(ref->id, id);
5604 const char *cmd_env = getenv("TIG_LS_REMOTE");
5605 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
5607 return read_properties(popen(cmd, "r"), "\t", read_ref);
5611 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
5613 if (!strcmp(name, "i18n.commitencoding"))
5614 string_ncopy(opt_encoding, value, valuelen);
5616 if (!strcmp(name, "core.editor"))
5617 string_ncopy(opt_editor, value, valuelen);
5619 /* branch.<head>.remote */
5621 !strncmp(name, "branch.", 7) &&
5622 !strncmp(name + 7, opt_head, strlen(opt_head)) &&
5623 !strcmp(name + 7 + strlen(opt_head), ".remote"))
5624 string_ncopy(opt_remote, value, valuelen);
5626 if (*opt_head && *opt_remote &&
5627 !strncmp(name, "branch.", 7) &&
5628 !strncmp(name + 7, opt_head, strlen(opt_head)) &&
5629 !strcmp(name + 7 + strlen(opt_head), ".merge")) {
5630 size_t from = strlen(opt_remote);
5632 if (!strncmp(value, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5633 value += STRING_SIZE("refs/heads/");
5634 valuelen -= STRING_SIZE("refs/heads/");
5637 if (!string_format_from(opt_remote, &from, "/%s", value))
5645 load_git_config(void)
5647 return read_properties(popen(GIT_CONFIG " --list", "r"),
5648 "=", read_repo_config_option);
5652 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
5654 if (!opt_git_dir[0]) {
5655 string_ncopy(opt_git_dir, name, namelen);
5657 } else if (opt_is_inside_work_tree == -1) {
5658 /* This can be 3 different values depending on the
5659 * version of git being used. If git-rev-parse does not
5660 * understand --is-inside-work-tree it will simply echo
5661 * the option else either "true" or "false" is printed.
5662 * Default to true for the unknown case. */
5663 opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
5665 } else if (opt_cdup[0] == ' ') {
5666 string_ncopy(opt_cdup, name, namelen);
5668 if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5669 namelen -= STRING_SIZE("refs/heads/");
5670 name += STRING_SIZE("refs/heads/");
5671 string_ncopy(opt_head, name, namelen);
5679 load_repo_info(void)
5682 FILE *pipe = popen("(git rev-parse --git-dir --is-inside-work-tree "
5683 " --show-cdup; git symbolic-ref HEAD) 2>/dev/null", "r");
5685 /* XXX: The line outputted by "--show-cdup" can be empty so
5686 * initialize it to something invalid to make it possible to
5687 * detect whether it has been set or not. */
5690 result = read_properties(pipe, "=", read_repo_info);
5691 if (opt_cdup[0] == ' ')
5698 read_properties(FILE *pipe, const char *separators,
5699 int (*read_property)(char *, size_t, char *, size_t))
5701 char buffer[BUFSIZ];
5708 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
5713 name = chomp_string(name);
5714 namelen = strcspn(name, separators);
5716 if (name[namelen]) {
5718 value = chomp_string(name + namelen + 1);
5719 valuelen = strlen(value);
5726 state = read_property(name, namelen, value, valuelen);
5729 if (state != ERR && ferror(pipe))
5742 static void __NORETURN
5745 /* XXX: Restore tty modes and let the OS cleanup the rest! */
5751 static void __NORETURN
5752 die(const char *err, ...)
5758 va_start(args, err);
5759 fputs("tig: ", stderr);
5760 vfprintf(stderr, err, args);
5761 fputs("\n", stderr);
5768 warn(const char *msg, ...)
5772 va_start(args, msg);
5773 fputs("tig warning: ", stderr);
5774 vfprintf(stderr, msg, args);
5775 fputs("\n", stderr);
5780 main(int argc, char *argv[])
5783 enum request request;
5786 signal(SIGINT, quit);
5788 if (setlocale(LC_ALL, "")) {
5789 char *codeset = nl_langinfo(CODESET);
5791 string_ncopy(opt_codeset, codeset, strlen(codeset));
5794 if (load_repo_info() == ERR)
5795 die("Failed to load repo info.");
5797 if (load_options() == ERR)
5798 die("Failed to load user config.");
5800 if (load_git_config() == ERR)
5801 die("Failed to load repo config.");
5803 if (!parse_options(argc, argv))
5806 /* Require a git repository unless when running in pager mode. */
5807 if (!opt_git_dir[0] && opt_request != REQ_VIEW_PAGER)
5808 die("Not a git repository");
5810 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
5813 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
5814 opt_iconv = iconv_open(opt_codeset, opt_encoding);
5815 if (opt_iconv == ICONV_NONE)
5816 die("Failed to initialize character set conversion");
5819 if (*opt_git_dir && load_refs() == ERR)
5820 die("Failed to load refs.");
5822 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
5823 view->cmd_env = getenv(view->cmd_env);
5825 request = opt_request;
5829 while (view_driver(display[current_view], request)) {
5833 foreach_view (view, i)
5836 /* Refresh, accept single keystroke of input */
5837 key = wgetch(status_win);
5839 /* wgetch() with nodelay() enabled returns ERR when there's no
5846 request = get_keybinding(display[current_view]->keymap, key);
5848 /* Some low-level request handling. This keeps access to
5849 * status_win restricted. */
5853 char *cmd = read_prompt(":");
5855 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
5856 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
5857 opt_request = REQ_VIEW_DIFF;
5859 opt_request = REQ_VIEW_PAGER;
5868 case REQ_SEARCH_BACK:
5870 const char *prompt = request == REQ_SEARCH
5872 char *search = read_prompt(prompt);
5875 string_ncopy(opt_search, search, strlen(search));
5880 case REQ_SCREEN_RESIZE:
5884 getmaxyx(stdscr, height, width);
5886 /* Resize the status view and let the view driver take
5887 * care of resizing the displayed views. */
5888 wresize(status_win, 1, width);
5889 mvwin(status_win, height - 1, 0);
5890 wrefresh(status_win);