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 . 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 *req;
960 char cmd[SIZEOF_STR];
963 for (bufpos = 0; argc > 0; argc--, argv++)
964 if (!string_format_from(cmd, &bufpos, "%s ", *argv))
967 req = realloc(run_request, (run_requests + 1) * sizeof(*run_request));
972 req = &run_request[run_requests++];
973 string_copy(req->cmd, cmd);
974 req->keymap = keymap;
977 return REQ_NONE + run_requests;
980 static struct run_request *
981 get_run_request(enum request request)
983 if (request <= REQ_NONE)
985 return &run_request[request - REQ_NONE - 1];
989 add_builtin_run_requests(void)
996 { KEYMAP_MAIN, 'C', { "git cherry-pick %(commit)" } },
997 { KEYMAP_GENERIC, 'G', { "git gc" } },
1001 for (i = 0; i < ARRAY_SIZE(reqs); i++) {
1004 req = add_run_request(reqs[i].keymap, reqs[i].key, 1, reqs[i].argv);
1005 if (req != REQ_NONE)
1006 add_keybinding(reqs[i].keymap, req, reqs[i].key);
1011 * User config file handling.
1014 static struct int_map color_map[] = {
1015 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
1027 #define set_color(color, name) \
1028 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
1030 static struct int_map attr_map[] = {
1031 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
1038 ATTR_MAP(UNDERLINE),
1041 #define set_attribute(attr, name) \
1042 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
1044 static int config_lineno;
1045 static bool config_errors;
1046 static char *config_msg;
1048 /* Wants: object fgcolor bgcolor [attr] */
1050 option_color_command(int argc, char *argv[])
1052 struct line_info *info;
1054 if (argc != 3 && argc != 4) {
1055 config_msg = "Wrong number of arguments given to color command";
1059 info = get_line_info(argv[0]);
1061 if (!string_enum_compare(argv[0], "main-delim", strlen("main-delim"))) {
1062 info = get_line_info("delimiter");
1064 } else if (!string_enum_compare(argv[0], "main-date", strlen("main-date"))) {
1065 info = get_line_info("date");
1068 config_msg = "Unknown color name";
1073 if (set_color(&info->fg, argv[1]) == ERR ||
1074 set_color(&info->bg, argv[2]) == ERR) {
1075 config_msg = "Unknown color";
1079 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
1080 config_msg = "Unknown attribute";
1087 static bool parse_bool(const char *s)
1089 return (!strcmp(s, "1") || !strcmp(s, "true") ||
1090 !strcmp(s, "yes")) ? TRUE : FALSE;
1093 /* Wants: name = value */
1095 option_set_command(int argc, char *argv[])
1098 config_msg = "Wrong number of arguments given to set command";
1102 if (strcmp(argv[1], "=")) {
1103 config_msg = "No value assigned";
1107 if (!strcmp(argv[0], "show-author")) {
1108 opt_author = parse_bool(argv[2]);
1112 if (!strcmp(argv[0], "show-date")) {
1113 opt_date = parse_bool(argv[2]);
1117 if (!strcmp(argv[0], "show-rev-graph")) {
1118 opt_rev_graph = parse_bool(argv[2]);
1122 if (!strcmp(argv[0], "show-refs")) {
1123 opt_show_refs = parse_bool(argv[2]);
1127 if (!strcmp(argv[0], "show-line-numbers")) {
1128 opt_line_number = parse_bool(argv[2]);
1132 if (!strcmp(argv[0], "line-graphics")) {
1133 opt_line_graphics = parse_bool(argv[2]);
1137 if (!strcmp(argv[0], "line-number-interval")) {
1138 opt_num_interval = atoi(argv[2]);
1142 if (!strcmp(argv[0], "tab-size")) {
1143 opt_tab_size = atoi(argv[2]);
1147 if (!strcmp(argv[0], "commit-encoding")) {
1148 char *arg = argv[2];
1149 int delimiter = *arg;
1152 switch (delimiter) {
1155 for (arg++, i = 0; arg[i]; i++)
1156 if (arg[i] == delimiter) {
1161 string_ncopy(opt_encoding, arg, strlen(arg));
1166 config_msg = "Unknown variable name";
1170 /* Wants: mode request key */
1172 option_bind_command(int argc, char *argv[])
1174 enum request request;
1179 config_msg = "Wrong number of arguments given to bind command";
1183 if (set_keymap(&keymap, argv[0]) == ERR) {
1184 config_msg = "Unknown key map";
1188 key = get_key_value(argv[1]);
1190 config_msg = "Unknown key";
1194 request = get_request(argv[2]);
1195 if (request == REQ_NONE) {
1196 const char *obsolete[] = { "cherry-pick" };
1197 size_t namelen = strlen(argv[2]);
1200 for (i = 0; i < ARRAY_SIZE(obsolete); i++) {
1201 if (namelen == strlen(obsolete[i]) &&
1202 !string_enum_compare(obsolete[i], argv[2], namelen)) {
1203 config_msg = "Obsolete request name";
1208 if (request == REQ_NONE && *argv[2]++ == '!')
1209 request = add_run_request(keymap, key, argc - 2, argv + 2);
1210 if (request == REQ_NONE) {
1211 config_msg = "Unknown request name";
1215 add_keybinding(keymap, request, key);
1221 set_option(char *opt, char *value)
1228 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1229 argv[argc++] = value;
1232 /* Nothing more to tokenize or last available token. */
1233 if (!*value || argc >= ARRAY_SIZE(argv))
1237 while (isspace(*value))
1241 if (!strcmp(opt, "color"))
1242 return option_color_command(argc, argv);
1244 if (!strcmp(opt, "set"))
1245 return option_set_command(argc, argv);
1247 if (!strcmp(opt, "bind"))
1248 return option_bind_command(argc, argv);
1250 config_msg = "Unknown option command";
1255 read_option(char *opt, size_t optlen, char *value, size_t valuelen)
1260 config_msg = "Internal error";
1262 /* Check for comment markers, since read_properties() will
1263 * only ensure opt and value are split at first " \t". */
1264 optlen = strcspn(opt, "#");
1268 if (opt[optlen] != 0) {
1269 config_msg = "No option value";
1273 /* Look for comment endings in the value. */
1274 size_t len = strcspn(value, "#");
1276 if (len < valuelen) {
1278 value[valuelen] = 0;
1281 status = set_option(opt, value);
1284 if (status == ERR) {
1285 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1286 config_lineno, (int) optlen, opt, config_msg);
1287 config_errors = TRUE;
1290 /* Always keep going if errors are encountered. */
1295 load_option_file(const char *path)
1299 /* It's ok that the file doesn't exist. */
1300 file = fopen(path, "r");
1305 config_errors = FALSE;
1307 if (read_properties(file, " \t", read_option) == ERR ||
1308 config_errors == TRUE)
1309 fprintf(stderr, "Errors while loading %s.\n", path);
1315 char *home = getenv("HOME");
1316 char *tigrc_user = getenv("TIGRC_USER");
1317 char *tigrc_system = getenv("TIGRC_SYSTEM");
1318 char buf[SIZEOF_STR];
1320 add_builtin_run_requests();
1322 if (!tigrc_system) {
1323 if (!string_format(buf, "%s/tigrc", SYSCONFDIR))
1327 load_option_file(tigrc_system);
1330 if (!home || !string_format(buf, "%s/.tigrc", home))
1334 load_option_file(tigrc_user);
1347 /* The display array of active views and the index of the current view. */
1348 static struct view *display[2];
1349 static unsigned int current_view;
1351 /* Reading from the prompt? */
1352 static bool input_mode = FALSE;
1354 #define foreach_displayed_view(view, i) \
1355 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1357 #define displayed_views() (display[1] != NULL ? 2 : 1)
1359 /* Current head and commit ID */
1360 static char ref_blob[SIZEOF_REF] = "";
1361 static char ref_commit[SIZEOF_REF] = "HEAD";
1362 static char ref_head[SIZEOF_REF] = "HEAD";
1365 const char *name; /* View name */
1366 const char *cmd_fmt; /* Default command line format */
1367 const char *cmd_env; /* Command line set via environment */
1368 const char *id; /* Points to either of ref_{head,commit,blob} */
1370 struct view_ops *ops; /* View operations */
1372 enum keymap keymap; /* What keymap does this view have */
1373 bool git_dir; /* Whether the view requires a git directory. */
1375 char cmd[SIZEOF_STR]; /* Command buffer */
1376 char ref[SIZEOF_REF]; /* Hovered commit reference */
1377 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1379 int height, width; /* The width and height of the main window */
1380 WINDOW *win; /* The main window */
1381 WINDOW *title; /* The title window living below the main window */
1384 unsigned long offset; /* Offset of the window top */
1385 unsigned long lineno; /* Current line number */
1388 char grep[SIZEOF_STR]; /* Search string */
1389 regex_t *regex; /* Pre-compiled regex */
1391 /* If non-NULL, points to the view that opened this view. If this view
1392 * is closed tig will switch back to the parent view. */
1393 struct view *parent;
1396 size_t lines; /* Total number of lines */
1397 struct line *line; /* Line index */
1398 size_t line_alloc; /* Total number of allocated lines */
1399 size_t line_size; /* Total number of used lines */
1400 unsigned int digits; /* Number of digits in the lines member. */
1403 struct line *curline; /* Line currently being drawn. */
1404 enum line_type curtype; /* Attribute currently used for drawing. */
1405 unsigned long col; /* Column when drawing. */
1413 /* What type of content being displayed. Used in the title bar. */
1415 /* Open and reads in all view content. */
1416 bool (*open)(struct view *view);
1417 /* Read one line; updates view->line. */
1418 bool (*read)(struct view *view, char *data);
1419 /* Draw one line; @lineno must be < view->height. */
1420 bool (*draw)(struct view *view, struct line *line, unsigned int lineno);
1421 /* Depending on view handle a special requests. */
1422 enum request (*request)(struct view *view, enum request request, struct line *line);
1423 /* Search for regex in a line. */
1424 bool (*grep)(struct view *view, struct line *line);
1426 void (*select)(struct view *view, struct line *line);
1429 static struct view_ops pager_ops;
1430 static struct view_ops main_ops;
1431 static struct view_ops tree_ops;
1432 static struct view_ops blob_ops;
1433 static struct view_ops blame_ops;
1434 static struct view_ops help_ops;
1435 static struct view_ops status_ops;
1436 static struct view_ops stage_ops;
1438 #define VIEW_STR(name, cmd, env, ref, ops, map, git) \
1439 { name, cmd, #env, ref, ops, map, git }
1441 #define VIEW_(id, name, ops, git, ref) \
1442 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id, git)
1445 static struct view views[] = {
1446 VIEW_(MAIN, "main", &main_ops, TRUE, ref_head),
1447 VIEW_(DIFF, "diff", &pager_ops, TRUE, ref_commit),
1448 VIEW_(LOG, "log", &pager_ops, TRUE, ref_head),
1449 VIEW_(TREE, "tree", &tree_ops, TRUE, ref_commit),
1450 VIEW_(BLOB, "blob", &blob_ops, TRUE, ref_blob),
1451 VIEW_(BLAME, "blame", &blame_ops, TRUE, ref_commit),
1452 VIEW_(HELP, "help", &help_ops, FALSE, ""),
1453 VIEW_(PAGER, "pager", &pager_ops, FALSE, "stdin"),
1454 VIEW_(STATUS, "status", &status_ops, TRUE, ""),
1455 VIEW_(STAGE, "stage", &stage_ops, TRUE, ""),
1458 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1459 #define VIEW_REQ(view) ((view) - views + REQ_OFFSET + 1)
1461 #define foreach_view(view, i) \
1462 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1464 #define view_is_displayed(view) \
1465 (view == display[0] || view == display[1])
1472 static int line_graphics[] = {
1473 /* LINE_GRAPHIC_VLINE: */ '|'
1477 set_view_attr(struct view *view, enum line_type type)
1479 if (!view->curline->selected && view->curtype != type) {
1480 wattrset(view->win, get_line_attr(type));
1481 wchgat(view->win, -1, 0, type, NULL);
1482 view->curtype = type;
1487 draw_chars(struct view *view, enum line_type type, const char *string,
1488 int max_len, bool use_tilde)
1492 int trimmed = FALSE;
1498 len = utf8_length(string, &col, max_len, &trimmed, use_tilde);
1500 col = len = strlen(string);
1501 if (len > max_len) {
1505 col = len = max_len;
1510 set_view_attr(view, type);
1511 waddnstr(view->win, string, len);
1512 if (trimmed && use_tilde) {
1513 set_view_attr(view, LINE_DELIMITER);
1514 waddch(view->win, '~');
1522 draw_space(struct view *view, enum line_type type, int max, int spaces)
1524 static char space[] = " ";
1527 spaces = MIN(max, spaces);
1529 while (spaces > 0) {
1530 int len = MIN(spaces, sizeof(space) - 1);
1532 col += draw_chars(view, type, space, spaces, FALSE);
1540 draw_lineno(struct view *view, unsigned int lineno)
1543 int digits3 = view->digits < 3 ? 3 : view->digits;
1544 int max_number = MIN(digits3, STRING_SIZE(number));
1545 int max = view->width - view->col;
1548 if (max < max_number)
1551 lineno += view->offset + 1;
1552 if (lineno == 1 || (lineno % opt_num_interval) == 0) {
1553 static char fmt[] = "%1ld";
1555 if (view->digits <= 9)
1556 fmt[1] = '0' + digits3;
1558 if (!string_format(number, fmt, lineno))
1560 col = draw_chars(view, LINE_LINE_NUMBER, number, max_number, TRUE);
1562 col = draw_space(view, LINE_LINE_NUMBER, max_number, max_number);
1566 set_view_attr(view, LINE_DEFAULT);
1567 waddch(view->win, line_graphics[LINE_GRAPHIC_VLINE]);
1572 col += draw_space(view, LINE_DEFAULT, max - col, 1);
1575 return view->width - view->col <= 0;
1579 draw_text(struct view *view, enum line_type type, const char *string, bool trim)
1581 view->col += draw_chars(view, type, string, view->width - view->col, trim);
1582 return view->width - view->col <= 0;
1586 draw_graphic(struct view *view, enum line_type type, chtype graphic[], size_t size)
1588 int max = view->width - view->col;
1594 set_view_attr(view, type);
1595 /* Using waddch() instead of waddnstr() ensures that
1596 * they'll be rendered correctly for the cursor line. */
1597 for (i = 0; i < size; i++)
1598 waddch(view->win, graphic[i]);
1602 waddch(view->win, ' ');
1606 return view->width - view->col <= 0;
1610 draw_field(struct view *view, enum line_type type, char *text, int len, bool trim)
1612 int max = MIN(view->width - view->col, len);
1616 col = draw_chars(view, type, text, max - 1, trim);
1618 col = draw_space(view, type, max - 1, max - 1);
1620 view->col += col + draw_space(view, LINE_DEFAULT, max - col, max - col);
1621 return view->width - view->col <= 0;
1625 draw_date(struct view *view, struct tm *time)
1627 char buf[DATE_COLS];
1632 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, time);
1633 date = timelen ? buf : NULL;
1635 return draw_field(view, LINE_DATE, date, DATE_COLS, FALSE);
1639 draw_view_line(struct view *view, unsigned int lineno)
1642 bool selected = (view->offset + lineno == view->lineno);
1645 assert(view_is_displayed(view));
1647 if (view->offset + lineno >= view->lines)
1650 line = &view->line[view->offset + lineno];
1652 wmove(view->win, lineno, 0);
1654 view->curline = line;
1655 view->curtype = LINE_NONE;
1656 line->selected = FALSE;
1659 set_view_attr(view, LINE_CURSOR);
1660 line->selected = TRUE;
1661 view->ops->select(view, line);
1662 } else if (line->selected) {
1663 wclrtoeol(view->win);
1666 scrollok(view->win, FALSE);
1667 draw_ok = view->ops->draw(view, line, lineno);
1668 scrollok(view->win, TRUE);
1674 redraw_view_dirty(struct view *view)
1679 for (lineno = 0; lineno < view->height; lineno++) {
1680 struct line *line = &view->line[view->offset + lineno];
1686 if (!draw_view_line(view, lineno))
1692 redrawwin(view->win);
1694 wnoutrefresh(view->win);
1696 wrefresh(view->win);
1700 redraw_view_from(struct view *view, int lineno)
1702 assert(0 <= lineno && lineno < view->height);
1704 for (; lineno < view->height; lineno++) {
1705 if (!draw_view_line(view, lineno))
1709 redrawwin(view->win);
1711 wnoutrefresh(view->win);
1713 wrefresh(view->win);
1717 redraw_view(struct view *view)
1720 redraw_view_from(view, 0);
1725 update_view_title(struct view *view)
1727 char buf[SIZEOF_STR];
1728 char state[SIZEOF_STR];
1729 size_t bufpos = 0, statelen = 0;
1731 assert(view_is_displayed(view));
1733 if (view != VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) {
1734 unsigned int view_lines = view->offset + view->height;
1735 unsigned int lines = view->lines
1736 ? MIN(view_lines, view->lines) * 100 / view->lines
1739 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1746 time_t secs = time(NULL) - view->start_time;
1748 /* Three git seconds are a long time ... */
1750 string_format_from(state, &statelen, " %lds", secs);
1754 string_format_from(buf, &bufpos, "[%s]", view->name);
1755 if (*view->ref && bufpos < view->width) {
1756 size_t refsize = strlen(view->ref);
1757 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1759 if (minsize < view->width)
1760 refsize = view->width - minsize + 7;
1761 string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
1764 if (statelen && bufpos < view->width) {
1765 string_format_from(buf, &bufpos, " %s", state);
1768 if (view == display[current_view])
1769 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1771 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1773 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1774 wclrtoeol(view->title);
1775 wmove(view->title, 0, view->width - 1);
1778 wnoutrefresh(view->title);
1780 wrefresh(view->title);
1784 resize_display(void)
1787 struct view *base = display[0];
1788 struct view *view = display[1] ? display[1] : display[0];
1790 /* Setup window dimensions */
1792 getmaxyx(stdscr, base->height, base->width);
1794 /* Make room for the status window. */
1798 /* Horizontal split. */
1799 view->width = base->width;
1800 view->height = SCALE_SPLIT_VIEW(base->height);
1801 base->height -= view->height;
1803 /* Make room for the title bar. */
1807 /* Make room for the title bar. */
1812 foreach_displayed_view (view, i) {
1814 view->win = newwin(view->height, 0, offset, 0);
1816 die("Failed to create %s view", view->name);
1818 scrollok(view->win, TRUE);
1820 view->title = newwin(1, 0, offset + view->height, 0);
1822 die("Failed to create title window");
1825 wresize(view->win, view->height, view->width);
1826 mvwin(view->win, offset, 0);
1827 mvwin(view->title, offset + view->height, 0);
1830 offset += view->height + 1;
1835 redraw_display(void)
1840 foreach_displayed_view (view, i) {
1842 update_view_title(view);
1847 update_display_cursor(struct view *view)
1849 /* Move the cursor to the right-most column of the cursor line.
1851 * XXX: This could turn out to be a bit expensive, but it ensures that
1852 * the cursor does not jump around. */
1854 wmove(view->win, view->lineno - view->offset, view->width - 1);
1855 wrefresh(view->win);
1863 /* Scrolling backend */
1865 do_scroll_view(struct view *view, int lines)
1867 bool redraw_current_line = FALSE;
1869 /* The rendering expects the new offset. */
1870 view->offset += lines;
1872 assert(0 <= view->offset && view->offset < view->lines);
1875 /* Move current line into the view. */
1876 if (view->lineno < view->offset) {
1877 view->lineno = view->offset;
1878 redraw_current_line = TRUE;
1879 } else if (view->lineno >= view->offset + view->height) {
1880 view->lineno = view->offset + view->height - 1;
1881 redraw_current_line = TRUE;
1884 assert(view->offset <= view->lineno && view->lineno < view->lines);
1886 /* Redraw the whole screen if scrolling is pointless. */
1887 if (view->height < ABS(lines)) {
1891 int line = lines > 0 ? view->height - lines : 0;
1892 int end = line + ABS(lines);
1894 wscrl(view->win, lines);
1896 for (; line < end; line++) {
1897 if (!draw_view_line(view, line))
1901 if (redraw_current_line)
1902 draw_view_line(view, view->lineno - view->offset);
1905 redrawwin(view->win);
1906 wrefresh(view->win);
1910 /* Scroll frontend */
1912 scroll_view(struct view *view, enum request request)
1916 assert(view_is_displayed(view));
1919 case REQ_SCROLL_PAGE_DOWN:
1920 lines = view->height;
1921 case REQ_SCROLL_LINE_DOWN:
1922 if (view->offset + lines > view->lines)
1923 lines = view->lines - view->offset;
1925 if (lines == 0 || view->offset + view->height >= view->lines) {
1926 report("Cannot scroll beyond the last line");
1931 case REQ_SCROLL_PAGE_UP:
1932 lines = view->height;
1933 case REQ_SCROLL_LINE_UP:
1934 if (lines > view->offset)
1935 lines = view->offset;
1938 report("Cannot scroll beyond the first line");
1946 die("request %d not handled in switch", request);
1949 do_scroll_view(view, lines);
1954 move_view(struct view *view, enum request request)
1956 int scroll_steps = 0;
1960 case REQ_MOVE_FIRST_LINE:
1961 steps = -view->lineno;
1964 case REQ_MOVE_LAST_LINE:
1965 steps = view->lines - view->lineno - 1;
1968 case REQ_MOVE_PAGE_UP:
1969 steps = view->height > view->lineno
1970 ? -view->lineno : -view->height;
1973 case REQ_MOVE_PAGE_DOWN:
1974 steps = view->lineno + view->height >= view->lines
1975 ? view->lines - view->lineno - 1 : view->height;
1987 die("request %d not handled in switch", request);
1990 if (steps <= 0 && view->lineno == 0) {
1991 report("Cannot move beyond the first line");
1994 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1995 report("Cannot move beyond the last line");
1999 /* Move the current line */
2000 view->lineno += steps;
2001 assert(0 <= view->lineno && view->lineno < view->lines);
2003 /* Check whether the view needs to be scrolled */
2004 if (view->lineno < view->offset ||
2005 view->lineno >= view->offset + view->height) {
2006 scroll_steps = steps;
2007 if (steps < 0 && -steps > view->offset) {
2008 scroll_steps = -view->offset;
2010 } else if (steps > 0) {
2011 if (view->lineno == view->lines - 1 &&
2012 view->lines > view->height) {
2013 scroll_steps = view->lines - view->offset - 1;
2014 if (scroll_steps >= view->height)
2015 scroll_steps -= view->height - 1;
2020 if (!view_is_displayed(view)) {
2021 view->offset += scroll_steps;
2022 assert(0 <= view->offset && view->offset < view->lines);
2023 view->ops->select(view, &view->line[view->lineno]);
2027 /* Repaint the old "current" line if we be scrolling */
2028 if (ABS(steps) < view->height)
2029 draw_view_line(view, view->lineno - steps - view->offset);
2032 do_scroll_view(view, scroll_steps);
2036 /* Draw the current line */
2037 draw_view_line(view, view->lineno - view->offset);
2039 redrawwin(view->win);
2040 wrefresh(view->win);
2049 static void search_view(struct view *view, enum request request);
2052 find_next_line(struct view *view, unsigned long lineno, struct line *line)
2054 assert(view_is_displayed(view));
2056 if (!view->ops->grep(view, line))
2059 if (lineno - view->offset >= view->height) {
2060 view->offset = lineno;
2061 view->lineno = lineno;
2065 unsigned long old_lineno = view->lineno - view->offset;
2067 view->lineno = lineno;
2068 draw_view_line(view, old_lineno);
2070 draw_view_line(view, view->lineno - view->offset);
2071 redrawwin(view->win);
2072 wrefresh(view->win);
2075 report("Line %ld matches '%s'", lineno + 1, view->grep);
2080 find_next(struct view *view, enum request request)
2082 unsigned long lineno = view->lineno;
2087 report("No previous search");
2089 search_view(view, request);
2099 case REQ_SEARCH_BACK:
2108 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
2109 lineno += direction;
2111 /* Note, lineno is unsigned long so will wrap around in which case it
2112 * will become bigger than view->lines. */
2113 for (; lineno < view->lines; lineno += direction) {
2114 struct line *line = &view->line[lineno];
2116 if (find_next_line(view, lineno, line))
2120 report("No match found for '%s'", view->grep);
2124 search_view(struct view *view, enum request request)
2129 regfree(view->regex);
2132 view->regex = calloc(1, sizeof(*view->regex));
2137 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
2138 if (regex_err != 0) {
2139 char buf[SIZEOF_STR] = "unknown error";
2141 regerror(regex_err, view->regex, buf, sizeof(buf));
2142 report("Search failed: %s", buf);
2146 string_copy(view->grep, opt_search);
2148 find_next(view, request);
2152 * Incremental updating
2156 end_update(struct view *view)
2160 set_nonblocking_input(FALSE);
2161 if (view->pipe == stdin)
2169 begin_update(struct view *view)
2175 string_copy(view->cmd, opt_cmd);
2177 /* When running random commands, initially show the
2178 * command in the title. However, it maybe later be
2179 * overwritten if a commit line is selected. */
2180 if (view == VIEW(REQ_VIEW_PAGER))
2181 string_copy(view->ref, view->cmd);
2185 } else if (view == VIEW(REQ_VIEW_TREE)) {
2186 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2187 char path[SIZEOF_STR];
2189 if (strcmp(view->vid, view->id))
2190 opt_path[0] = path[0] = 0;
2191 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
2194 if (!string_format(view->cmd, format, view->id, path))
2198 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2199 const char *id = view->id;
2201 if (!string_format(view->cmd, format, id, id, id, id, id))
2204 /* Put the current ref_* value to the view title ref
2205 * member. This is needed by the blob view. Most other
2206 * views sets it automatically after loading because the
2207 * first line is a commit line. */
2208 string_copy_rev(view->ref, view->id);
2211 /* Special case for the pager view. */
2213 view->pipe = opt_pipe;
2216 view->pipe = popen(view->cmd, "r");
2222 set_nonblocking_input(TRUE);
2227 string_copy_rev(view->vid, view->id);
2232 for (i = 0; i < view->lines; i++)
2233 if (view->line[i].data)
2234 free(view->line[i].data);
2240 view->start_time = time(NULL);
2245 #define ITEM_CHUNK_SIZE 256
2247 realloc_items(void *mem, size_t *size, size_t new_size, size_t item_size)
2249 size_t num_chunks = *size / ITEM_CHUNK_SIZE;
2250 size_t num_chunks_new = (new_size + ITEM_CHUNK_SIZE - 1) / ITEM_CHUNK_SIZE;
2252 if (mem == NULL || num_chunks != num_chunks_new) {
2253 *size = num_chunks_new * ITEM_CHUNK_SIZE;
2254 mem = realloc(mem, *size * item_size);
2260 static struct line *
2261 realloc_lines(struct view *view, size_t line_size)
2263 size_t alloc = view->line_alloc;
2264 struct line *tmp = realloc_items(view->line, &alloc, line_size,
2265 sizeof(*view->line));
2271 view->line_alloc = alloc;
2272 view->line_size = line_size;
2277 update_view(struct view *view)
2279 char in_buffer[BUFSIZ];
2280 char out_buffer[BUFSIZ * 2];
2282 /* The number of lines to read. If too low it will cause too much
2283 * redrawing (and possible flickering), if too high responsiveness
2285 unsigned long lines = view->height;
2286 int redraw_from = -1;
2291 /* Only redraw if lines are visible. */
2292 if (view->offset + view->height >= view->lines)
2293 redraw_from = view->lines - view->offset;
2295 /* FIXME: This is probably not perfect for backgrounded views. */
2296 if (!realloc_lines(view, view->lines + lines))
2299 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
2300 size_t linelen = strlen(line);
2303 line[linelen - 1] = 0;
2305 if (opt_iconv != ICONV_NONE) {
2306 ICONV_CONST char *inbuf = line;
2307 size_t inlen = linelen;
2309 char *outbuf = out_buffer;
2310 size_t outlen = sizeof(out_buffer);
2314 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
2315 if (ret != (size_t) -1) {
2317 linelen = strlen(out_buffer);
2321 if (!view->ops->read(view, line))
2331 lines = view->lines;
2332 for (digits = 0; lines; digits++)
2335 /* Keep the displayed view in sync with line number scaling. */
2336 if (digits != view->digits) {
2337 view->digits = digits;
2342 if (!view_is_displayed(view))
2345 if (view == VIEW(REQ_VIEW_TREE)) {
2346 /* Clear the view and redraw everything since the tree sorting
2347 * might have rearranged things. */
2350 } else if (redraw_from >= 0) {
2351 /* If this is an incremental update, redraw the previous line
2352 * since for commits some members could have changed when
2353 * loading the main view. */
2354 if (redraw_from > 0)
2357 /* Since revision graph visualization requires knowledge
2358 * about the parent commit, it causes a further one-off
2359 * needed to be redrawn for incremental updates. */
2360 if (redraw_from > 0 && opt_rev_graph)
2363 /* Incrementally draw avoids flickering. */
2364 redraw_view_from(view, redraw_from);
2367 if (view == VIEW(REQ_VIEW_BLAME))
2368 redraw_view_dirty(view);
2370 /* Update the title _after_ the redraw so that if the redraw picks up a
2371 * commit reference in view->ref it'll be available here. */
2372 update_view_title(view);
2375 if (ferror(view->pipe)) {
2376 report("Failed to read: %s", strerror(errno));
2379 } else if (feof(view->pipe)) {
2387 report("Allocation failure");
2390 if (view->ops->read(view, NULL))
2395 static struct line *
2396 add_line_data(struct view *view, void *data, enum line_type type)
2398 struct line *line = &view->line[view->lines++];
2400 memset(line, 0, sizeof(*line));
2407 static struct line *
2408 add_line_text(struct view *view, char *data, enum line_type type)
2411 data = strdup(data);
2413 return data ? add_line_data(view, data, type) : NULL;
2422 OPEN_DEFAULT = 0, /* Use default view switching. */
2423 OPEN_SPLIT = 1, /* Split current view. */
2424 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2425 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2426 OPEN_NOMAXIMIZE = 8 /* Do not maximize the current view. */
2430 open_view(struct view *prev, enum request request, enum open_flags flags)
2432 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2433 bool split = !!(flags & OPEN_SPLIT);
2434 bool reload = !!(flags & OPEN_RELOAD);
2435 bool nomaximize = !!(flags & OPEN_NOMAXIMIZE);
2436 struct view *view = VIEW(request);
2437 int nviews = displayed_views();
2438 struct view *base_view = display[0];
2440 if (view == prev && nviews == 1 && !reload) {
2441 report("Already in %s view", view->name);
2445 if (view->git_dir && !opt_git_dir[0]) {
2446 report("The %s view is disabled in pager view", view->name);
2454 } else if (!nomaximize) {
2455 /* Maximize the current view. */
2456 memset(display, 0, sizeof(display));
2458 display[current_view] = view;
2461 /* Resize the view when switching between split- and full-screen,
2462 * or when switching between two different full-screen views. */
2463 if (nviews != displayed_views() ||
2464 (nviews == 1 && base_view != display[0]))
2467 if (view->ops->open) {
2468 if (!view->ops->open(view)) {
2469 report("Failed to load %s view", view->name);
2473 } else if ((reload || strcmp(view->vid, view->id)) &&
2474 !begin_update(view)) {
2475 report("Failed to load %s view", view->name);
2479 if (split && prev->lineno - prev->offset >= prev->height) {
2480 /* Take the title line into account. */
2481 int lines = prev->lineno - prev->offset - prev->height + 1;
2483 /* Scroll the view that was split if the current line is
2484 * outside the new limited view. */
2485 do_scroll_view(prev, lines);
2488 if (prev && view != prev) {
2489 if (split && !backgrounded) {
2490 /* "Blur" the previous view. */
2491 update_view_title(prev);
2494 view->parent = prev;
2497 if (view->pipe && view->lines == 0) {
2498 /* Clear the old view and let the incremental updating refill
2507 /* If the view is backgrounded the above calls to report()
2508 * won't redraw the view title. */
2510 update_view_title(view);
2514 open_external_viewer(const char *cmd)
2516 def_prog_mode(); /* save current tty modes */
2517 endwin(); /* restore original tty modes */
2519 fprintf(stderr, "Press Enter to continue");
2526 open_mergetool(const char *file)
2528 char cmd[SIZEOF_STR];
2529 char file_sq[SIZEOF_STR];
2531 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2532 string_format(cmd, "git mergetool %s", file_sq)) {
2533 open_external_viewer(cmd);
2538 open_editor(bool from_root, const char *file)
2540 char cmd[SIZEOF_STR];
2541 char file_sq[SIZEOF_STR];
2543 char *prefix = from_root ? opt_cdup : "";
2545 editor = getenv("GIT_EDITOR");
2546 if (!editor && *opt_editor)
2547 editor = opt_editor;
2549 editor = getenv("VISUAL");
2551 editor = getenv("EDITOR");
2555 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2556 string_format(cmd, "%s %s%s", editor, prefix, file_sq)) {
2557 open_external_viewer(cmd);
2562 open_run_request(enum request request)
2564 struct run_request *req = get_run_request(request);
2565 char buf[SIZEOF_STR * 2];
2570 report("Unknown run request");
2578 char *next = strstr(cmd, "%(");
2579 int len = next - cmd;
2586 } else if (!strncmp(next, "%(head)", 7)) {
2589 } else if (!strncmp(next, "%(commit)", 9)) {
2592 } else if (!strncmp(next, "%(blob)", 7)) {
2596 report("Unknown replacement in run request: `%s`", req->cmd);
2600 if (!string_format_from(buf, &bufpos, "%.*s%s", len, cmd, value))
2604 next = strchr(next, ')') + 1;
2608 open_external_viewer(buf);
2612 * User request switch noodle
2616 view_driver(struct view *view, enum request request)
2620 if (request == REQ_NONE) {
2625 if (request > REQ_NONE) {
2626 open_run_request(request);
2627 /* FIXME: When all views can refresh always do this. */
2628 if (view == VIEW(REQ_VIEW_STATUS) ||
2629 view == VIEW(REQ_VIEW_STAGE))
2630 request = REQ_REFRESH;
2635 if (view && view->lines) {
2636 request = view->ops->request(view, request, &view->line[view->lineno]);
2637 if (request == REQ_NONE)
2644 case REQ_MOVE_PAGE_UP:
2645 case REQ_MOVE_PAGE_DOWN:
2646 case REQ_MOVE_FIRST_LINE:
2647 case REQ_MOVE_LAST_LINE:
2648 move_view(view, request);
2651 case REQ_SCROLL_LINE_DOWN:
2652 case REQ_SCROLL_LINE_UP:
2653 case REQ_SCROLL_PAGE_DOWN:
2654 case REQ_SCROLL_PAGE_UP:
2655 scroll_view(view, request);
2658 case REQ_VIEW_BLAME:
2660 report("No file chosen, press %s to open tree view",
2661 get_key(REQ_VIEW_TREE));
2664 open_view(view, request, OPEN_DEFAULT);
2669 report("No file chosen, press %s to open tree view",
2670 get_key(REQ_VIEW_TREE));
2673 open_view(view, request, OPEN_DEFAULT);
2676 case REQ_VIEW_PAGER:
2677 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2678 report("No pager content, press %s to run command from prompt",
2679 get_key(REQ_PROMPT));
2682 open_view(view, request, OPEN_DEFAULT);
2685 case REQ_VIEW_STAGE:
2686 if (!VIEW(REQ_VIEW_STAGE)->lines) {
2687 report("No stage content, press %s to open the status view and choose file",
2688 get_key(REQ_VIEW_STATUS));
2691 open_view(view, request, OPEN_DEFAULT);
2694 case REQ_VIEW_STATUS:
2695 if (opt_is_inside_work_tree == FALSE) {
2696 report("The status view requires a working tree");
2699 open_view(view, request, OPEN_DEFAULT);
2707 open_view(view, request, OPEN_DEFAULT);
2712 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2714 if ((view == VIEW(REQ_VIEW_DIFF) &&
2715 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2716 (view == VIEW(REQ_VIEW_DIFF) &&
2717 view->parent == VIEW(REQ_VIEW_BLAME)) ||
2718 (view == VIEW(REQ_VIEW_STAGE) &&
2719 view->parent == VIEW(REQ_VIEW_STATUS)) ||
2720 (view == VIEW(REQ_VIEW_BLOB) &&
2721 view->parent == VIEW(REQ_VIEW_TREE))) {
2724 view = view->parent;
2725 line = view->lineno;
2726 move_view(view, request);
2727 if (view_is_displayed(view))
2728 update_view_title(view);
2729 if (line != view->lineno)
2730 view->ops->request(view, REQ_ENTER,
2731 &view->line[view->lineno]);
2734 move_view(view, request);
2740 int nviews = displayed_views();
2741 int next_view = (current_view + 1) % nviews;
2743 if (next_view == current_view) {
2744 report("Only one view is displayed");
2748 current_view = next_view;
2749 /* Blur out the title of the previous view. */
2750 update_view_title(view);
2755 report("Refreshing is not yet supported for the %s view", view->name);
2759 if (displayed_views() == 2)
2760 open_view(view, VIEW_REQ(view), OPEN_DEFAULT);
2763 case REQ_TOGGLE_LINENO:
2764 opt_line_number = !opt_line_number;
2768 case REQ_TOGGLE_DATE:
2769 opt_date = !opt_date;
2773 case REQ_TOGGLE_AUTHOR:
2774 opt_author = !opt_author;
2778 case REQ_TOGGLE_REV_GRAPH:
2779 opt_rev_graph = !opt_rev_graph;
2783 case REQ_TOGGLE_REFS:
2784 opt_show_refs = !opt_show_refs;
2789 /* Always reload^Wrerun commands from the prompt. */
2790 open_view(view, opt_request, OPEN_RELOAD);
2794 case REQ_SEARCH_BACK:
2795 search_view(view, request);
2800 find_next(view, request);
2803 case REQ_STOP_LOADING:
2804 for (i = 0; i < ARRAY_SIZE(views); i++) {
2807 report("Stopped loading the %s view", view->name),
2812 case REQ_SHOW_VERSION:
2813 report("tig-%s (built %s)", TIG_VERSION, __DATE__);
2816 case REQ_SCREEN_RESIZE:
2819 case REQ_SCREEN_REDRAW:
2824 report("Nothing to edit");
2829 report("Nothing to enter");
2833 case REQ_VIEW_CLOSE:
2834 /* XXX: Mark closed views by letting view->parent point to the
2835 * view itself. Parents to closed view should never be
2838 view->parent->parent != view->parent) {
2839 memset(display, 0, sizeof(display));
2841 display[current_view] = view->parent;
2842 view->parent = view;
2852 /* An unknown key will show most commonly used commands. */
2853 report("Unknown key, press 'h' for help");
2866 pager_draw(struct view *view, struct line *line, unsigned int lineno)
2868 char *text = line->data;
2870 if (opt_line_number && draw_lineno(view, lineno))
2873 draw_text(view, line->type, text, TRUE);
2878 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2880 char refbuf[SIZEOF_STR];
2884 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2887 pipe = popen(refbuf, "r");
2891 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2892 ref = chomp_string(ref);
2898 /* This is the only fatal call, since it can "corrupt" the buffer. */
2899 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2906 add_pager_refs(struct view *view, struct line *line)
2908 char buf[SIZEOF_STR];
2909 char *commit_id = (char *)line->data + STRING_SIZE("commit ");
2911 size_t bufpos = 0, refpos = 0;
2912 const char *sep = "Refs: ";
2913 bool is_tag = FALSE;
2915 assert(line->type == LINE_COMMIT);
2917 refs = get_refs(commit_id);
2919 if (view == VIEW(REQ_VIEW_DIFF))
2920 goto try_add_describe_ref;
2925 struct ref *ref = refs[refpos];
2926 char *fmt = ref->tag ? "%s[%s]" :
2927 ref->remote ? "%s<%s>" : "%s%s";
2929 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2934 } while (refs[refpos++]->next);
2936 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2937 try_add_describe_ref:
2938 /* Add <tag>-g<commit_id> "fake" reference. */
2939 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2946 if (!realloc_lines(view, view->line_size + 1))
2949 add_line_text(view, buf, LINE_PP_REFS);
2953 pager_read(struct view *view, char *data)
2960 line = add_line_text(view, data, get_line_type(data));
2964 if (line->type == LINE_COMMIT &&
2965 (view == VIEW(REQ_VIEW_DIFF) ||
2966 view == VIEW(REQ_VIEW_LOG)))
2967 add_pager_refs(view, line);
2973 pager_request(struct view *view, enum request request, struct line *line)
2977 if (request != REQ_ENTER)
2980 if (line->type == LINE_COMMIT &&
2981 (view == VIEW(REQ_VIEW_LOG) ||
2982 view == VIEW(REQ_VIEW_PAGER))) {
2983 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2987 /* Always scroll the view even if it was split. That way
2988 * you can use Enter to scroll through the log view and
2989 * split open each commit diff. */
2990 scroll_view(view, REQ_SCROLL_LINE_DOWN);
2992 /* FIXME: A minor workaround. Scrolling the view will call report("")
2993 * but if we are scrolling a non-current view this won't properly
2994 * update the view title. */
2996 update_view_title(view);
3002 pager_grep(struct view *view, struct line *line)
3005 char *text = line->data;
3010 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
3017 pager_select(struct view *view, struct line *line)
3019 if (line->type == LINE_COMMIT) {
3020 char *text = (char *)line->data + STRING_SIZE("commit ");
3022 if (view != VIEW(REQ_VIEW_PAGER))
3023 string_copy_rev(view->ref, text);
3024 string_copy_rev(ref_commit, text);
3028 static struct view_ops pager_ops = {
3044 help_open(struct view *view)
3047 int lines = ARRAY_SIZE(req_info) + 2;
3050 if (view->lines > 0)
3053 for (i = 0; i < ARRAY_SIZE(req_info); i++)
3054 if (!req_info[i].request)
3057 lines += run_requests + 1;
3059 view->line = calloc(lines, sizeof(*view->line));
3063 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
3065 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
3068 if (req_info[i].request == REQ_NONE)
3071 if (!req_info[i].request) {
3072 add_line_text(view, "", LINE_DEFAULT);
3073 add_line_text(view, req_info[i].help, LINE_DEFAULT);
3077 key = get_key(req_info[i].request);
3079 key = "(no key defined)";
3081 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
3084 add_line_text(view, buf, LINE_DEFAULT);
3088 add_line_text(view, "", LINE_DEFAULT);
3089 add_line_text(view, "External commands:", LINE_DEFAULT);
3092 for (i = 0; i < run_requests; i++) {
3093 struct run_request *req = get_run_request(REQ_NONE + i + 1);
3099 key = get_key_name(req->key);
3101 key = "(no key defined)";
3103 if (!string_format(buf, " %-10s %-14s `%s`",
3104 keymap_table[req->keymap].name,
3108 add_line_text(view, buf, LINE_DEFAULT);
3114 static struct view_ops help_ops = {
3129 struct tree_stack_entry {
3130 struct tree_stack_entry *prev; /* Entry below this in the stack */
3131 unsigned long lineno; /* Line number to restore */
3132 char *name; /* Position of name in opt_path */
3135 /* The top of the path stack. */
3136 static struct tree_stack_entry *tree_stack = NULL;
3137 unsigned long tree_lineno = 0;
3140 pop_tree_stack_entry(void)
3142 struct tree_stack_entry *entry = tree_stack;
3144 tree_lineno = entry->lineno;
3146 tree_stack = entry->prev;
3151 push_tree_stack_entry(char *name, unsigned long lineno)
3153 struct tree_stack_entry *entry = calloc(1, sizeof(*entry));
3154 size_t pathlen = strlen(opt_path);
3159 entry->prev = tree_stack;
3160 entry->name = opt_path + pathlen;
3163 if (!string_format_from(opt_path, &pathlen, "%s/", name)) {
3164 pop_tree_stack_entry();
3168 /* Move the current line to the first tree entry. */
3170 entry->lineno = lineno;
3173 /* Parse output from git-ls-tree(1):
3175 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
3176 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
3177 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
3178 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
3181 #define SIZEOF_TREE_ATTR \
3182 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
3184 #define TREE_UP_FORMAT "040000 tree %s\t.."
3187 tree_compare_entry(enum line_type type1, char *name1,
3188 enum line_type type2, char *name2)
3190 if (type1 != type2) {
3191 if (type1 == LINE_TREE_DIR)
3196 return strcmp(name1, name2);
3200 tree_path(struct line *line)
3202 char *path = line->data;
3204 return path + SIZEOF_TREE_ATTR;
3208 tree_read(struct view *view, char *text)
3210 size_t textlen = text ? strlen(text) : 0;
3211 char buf[SIZEOF_STR];
3213 enum line_type type;
3214 bool first_read = view->lines == 0;
3218 if (textlen <= SIZEOF_TREE_ATTR)
3221 type = text[STRING_SIZE("100644 ")] == 't'
3222 ? LINE_TREE_DIR : LINE_TREE_FILE;
3225 /* Add path info line */
3226 if (!string_format(buf, "Directory path /%s", opt_path) ||
3227 !realloc_lines(view, view->line_size + 1) ||
3228 !add_line_text(view, buf, LINE_DEFAULT))
3231 /* Insert "link" to parent directory. */
3233 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
3234 !realloc_lines(view, view->line_size + 1) ||
3235 !add_line_text(view, buf, LINE_TREE_DIR))
3240 /* Strip the path part ... */
3242 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
3243 size_t striplen = strlen(opt_path);
3244 char *path = text + SIZEOF_TREE_ATTR;
3246 if (pathlen > striplen)
3247 memmove(path, path + striplen,
3248 pathlen - striplen + 1);
3251 /* Skip "Directory ..." and ".." line. */
3252 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
3253 struct line *line = &view->line[pos];
3254 char *path1 = tree_path(line);
3255 char *path2 = text + SIZEOF_TREE_ATTR;
3256 int cmp = tree_compare_entry(line->type, path1, type, path2);
3261 text = strdup(text);
3265 if (view->lines > pos)
3266 memmove(&view->line[pos + 1], &view->line[pos],
3267 (view->lines - pos) * sizeof(*line));
3269 line = &view->line[pos];
3276 if (!add_line_text(view, text, type))
3279 if (tree_lineno > view->lineno) {
3280 view->lineno = tree_lineno;
3288 tree_request(struct view *view, enum request request, struct line *line)
3290 enum open_flags flags;
3292 if (request == REQ_VIEW_BLAME) {
3293 char *filename = tree_path(line);
3295 if (line->type == LINE_TREE_DIR) {
3296 report("Cannot show blame for directory %s", opt_path);
3300 string_copy(opt_ref, view->vid);
3301 string_format(opt_file, "%s%s", opt_path, filename);
3304 if (request == REQ_TREE_PARENT) {
3307 request = REQ_ENTER;
3308 line = &view->line[1];
3310 /* quit view if at top of tree */
3311 return REQ_VIEW_CLOSE;
3314 if (request != REQ_ENTER)
3317 /* Cleanup the stack if the tree view is at a different tree. */
3318 while (!*opt_path && tree_stack)
3319 pop_tree_stack_entry();
3321 switch (line->type) {
3323 /* Depending on whether it is a subdir or parent (updir?) link
3324 * mangle the path buffer. */
3325 if (line == &view->line[1] && *opt_path) {
3326 pop_tree_stack_entry();
3329 char *basename = tree_path(line);
3331 push_tree_stack_entry(basename, view->lineno);
3334 /* Trees and subtrees share the same ID, so they are not not
3335 * unique like blobs. */
3336 flags = OPEN_RELOAD;
3337 request = REQ_VIEW_TREE;
3340 case LINE_TREE_FILE:
3341 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3342 request = REQ_VIEW_BLOB;
3349 open_view(view, request, flags);
3350 if (request == REQ_VIEW_TREE) {
3351 view->lineno = tree_lineno;
3358 tree_select(struct view *view, struct line *line)
3360 char *text = (char *)line->data + STRING_SIZE("100644 blob ");
3362 if (line->type == LINE_TREE_FILE) {
3363 string_copy_rev(ref_blob, text);
3365 } else if (line->type != LINE_TREE_DIR) {
3369 string_copy_rev(view->ref, text);
3372 static struct view_ops tree_ops = {
3383 blob_read(struct view *view, char *line)
3387 return add_line_text(view, line, LINE_DEFAULT) != NULL;
3390 static struct view_ops blob_ops = {
3403 * Loading the blame view is a two phase job:
3405 * 1. File content is read either using opt_file from the
3406 * filesystem or using git-cat-file.
3407 * 2. Then blame information is incrementally added by
3408 * reading output from git-blame.
3411 struct blame_commit {
3412 char id[SIZEOF_REV]; /* SHA1 ID. */
3413 char title[128]; /* First line of the commit message. */
3414 char author[75]; /* Author of the commit. */
3415 struct tm time; /* Date from the author ident. */
3416 char filename[128]; /* Name of file. */
3420 struct blame_commit *commit;
3421 unsigned int header:1;
3425 #define BLAME_CAT_FILE_CMD "git cat-file blob %s:%s"
3426 #define BLAME_INCREMENTAL_CMD "git blame --incremental %s %s"
3429 blame_open(struct view *view)
3431 char path[SIZEOF_STR];
3432 char ref[SIZEOF_STR] = "";
3434 if (sq_quote(path, 0, opt_file) >= sizeof(path))
3437 if (*opt_ref && sq_quote(ref, 0, opt_ref) >= sizeof(ref))
3441 if (!string_format(view->cmd, BLAME_CAT_FILE_CMD, ref, path))
3444 view->pipe = fopen(opt_file, "r");
3446 !string_format(view->cmd, BLAME_CAT_FILE_CMD, "HEAD", path))
3451 view->pipe = popen(view->cmd, "r");
3455 if (!string_format(view->cmd, BLAME_INCREMENTAL_CMD, ref, path))
3458 string_format(view->ref, "%s ...", opt_file);
3459 string_copy_rev(view->vid, opt_file);
3460 set_nonblocking_input(TRUE);
3465 for (i = 0; i < view->lines; i++)
3466 free(view->line[i].data);
3470 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3471 view->offset = view->lines = view->lineno = 0;
3473 view->start_time = time(NULL);
3478 static struct blame_commit *
3479 get_blame_commit(struct view *view, const char *id)
3483 for (i = 0; i < view->lines; i++) {
3484 struct blame *blame = view->line[i].data;
3489 if (!strncmp(blame->commit->id, id, SIZEOF_REV - 1))
3490 return blame->commit;
3494 struct blame_commit *commit = calloc(1, sizeof(*commit));
3497 string_ncopy(commit->id, id, SIZEOF_REV);
3503 parse_number(char **posref, size_t *number, size_t min, size_t max)
3505 char *pos = *posref;
3508 pos = strchr(pos + 1, ' ');
3509 if (!pos || !isdigit(pos[1]))
3511 *number = atoi(pos + 1);
3512 if (*number < min || *number > max)
3519 static struct blame_commit *
3520 parse_blame_commit(struct view *view, char *text, int *blamed)
3522 struct blame_commit *commit;
3523 struct blame *blame;
3524 char *pos = text + SIZEOF_REV - 1;
3528 if (strlen(text) <= SIZEOF_REV || *pos != ' ')
3531 if (!parse_number(&pos, &lineno, 1, view->lines) ||
3532 !parse_number(&pos, &group, 1, view->lines - lineno + 1))
3535 commit = get_blame_commit(view, text);
3541 struct line *line = &view->line[lineno + group - 1];
3544 blame->commit = commit;
3545 blame->header = !group;
3553 blame_read_file(struct view *view, char *line)
3558 if (view->lines > 0)
3559 pipe = popen(view->cmd, "r");
3560 else if (!view->parent)
3561 die("No blame exist for %s", view->vid);
3564 report("Failed to load blame data");
3573 size_t linelen = strlen(line);
3574 struct blame *blame = malloc(sizeof(*blame) + linelen);
3579 blame->commit = NULL;
3580 strncpy(blame->text, line, linelen);
3581 blame->text[linelen] = 0;
3582 return add_line_data(view, blame, LINE_BLAME_ID) != NULL;
3587 match_blame_header(const char *name, char **line)
3589 size_t namelen = strlen(name);
3590 bool matched = !strncmp(name, *line, namelen);
3599 blame_read(struct view *view, char *line)
3601 static struct blame_commit *commit = NULL;
3602 static int blamed = 0;
3603 static time_t author_time;
3606 return blame_read_file(view, line);
3612 string_format(view->ref, "%s", view->vid);
3613 if (view_is_displayed(view)) {
3614 update_view_title(view);
3615 redraw_view_from(view, 0);
3621 commit = parse_blame_commit(view, line, &blamed);
3622 string_format(view->ref, "%s %2d%%", view->vid,
3623 blamed * 100 / view->lines);
3625 } else if (match_blame_header("author ", &line)) {
3626 string_ncopy(commit->author, line, strlen(line));
3628 } else if (match_blame_header("author-time ", &line)) {
3629 author_time = (time_t) atol(line);
3631 } else if (match_blame_header("author-tz ", &line)) {
3634 tz = ('0' - line[1]) * 60 * 60 * 10;
3635 tz += ('0' - line[2]) * 60 * 60;
3636 tz += ('0' - line[3]) * 60;
3637 tz += ('0' - line[4]) * 60;
3643 gmtime_r(&author_time, &commit->time);
3645 } else if (match_blame_header("summary ", &line)) {
3646 string_ncopy(commit->title, line, strlen(line));
3648 } else if (match_blame_header("filename ", &line)) {
3649 string_ncopy(commit->filename, line, strlen(line));
3657 blame_draw(struct view *view, struct line *line, unsigned int lineno)
3659 struct blame *blame = line->data;
3660 struct tm *time = NULL;
3661 char *id = NULL, *author = NULL;
3663 if (blame->commit && *blame->commit->filename) {
3664 id = blame->commit->id;
3665 author = blame->commit->author;
3666 time = &blame->commit->time;
3669 if (opt_date && draw_date(view, time))
3673 draw_field(view, LINE_MAIN_AUTHOR, author, AUTHOR_COLS, TRUE))
3676 if (draw_field(view, LINE_BLAME_ID, id, ID_COLS, FALSE))
3679 if (draw_lineno(view, lineno))
3682 draw_text(view, LINE_DEFAULT, blame->text, TRUE);
3687 blame_request(struct view *view, enum request request, struct line *line)
3689 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3690 struct blame *blame = line->data;
3694 if (!blame->commit) {
3695 report("No commit loaded yet");
3699 if (!strcmp(blame->commit->id, NULL_ID)) {
3700 char path[SIZEOF_STR];
3702 if (sq_quote(path, 0, view->vid) >= sizeof(path))
3704 string_format(opt_cmd, "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s 2>/dev/null", path);
3707 open_view(view, REQ_VIEW_DIFF, flags);
3718 blame_grep(struct view *view, struct line *line)
3720 struct blame *blame = line->data;
3721 struct blame_commit *commit = blame->commit;
3724 #define MATCH(text, on) \
3725 (on && *text && regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3728 char buf[DATE_COLS + 1];
3730 if (MATCH(commit->title, 1) ||
3731 MATCH(commit->author, opt_author) ||
3732 MATCH(commit->id, opt_date))
3735 if (strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time) &&
3740 return MATCH(blame->text, 1);
3746 blame_select(struct view *view, struct line *line)
3748 struct blame *blame = line->data;
3749 struct blame_commit *commit = blame->commit;
3754 if (!strcmp(commit->id, NULL_ID))
3755 string_ncopy(ref_commit, "HEAD", 4);
3757 string_copy_rev(ref_commit, commit->id);
3760 static struct view_ops blame_ops = {
3778 char rev[SIZEOF_REV];
3779 char name[SIZEOF_STR];
3783 char rev[SIZEOF_REV];
3784 char name[SIZEOF_STR];
3788 static char status_onbranch[SIZEOF_STR];
3789 static struct status stage_status;
3790 static enum line_type stage_line_type;
3792 /* Get fields from the diff line:
3793 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
3796 status_get_diff(struct status *file, char *buf, size_t bufsize)
3798 char *old_mode = buf + 1;
3799 char *new_mode = buf + 8;
3800 char *old_rev = buf + 15;
3801 char *new_rev = buf + 56;
3802 char *status = buf + 97;
3805 old_mode[-1] != ':' ||
3806 new_mode[-1] != ' ' ||
3807 old_rev[-1] != ' ' ||
3808 new_rev[-1] != ' ' ||
3812 file->status = *status;
3814 string_copy_rev(file->old.rev, old_rev);
3815 string_copy_rev(file->new.rev, new_rev);
3817 file->old.mode = strtoul(old_mode, NULL, 8);
3818 file->new.mode = strtoul(new_mode, NULL, 8);
3820 file->old.name[0] = file->new.name[0] = 0;
3826 status_run(struct view *view, const char cmd[], char status, enum line_type type)
3828 struct status *file = NULL;
3829 struct status *unmerged = NULL;
3830 char buf[SIZEOF_STR * 4];
3834 pipe = popen(cmd, "r");
3838 add_line_data(view, NULL, type);
3840 while (!feof(pipe) && !ferror(pipe)) {
3844 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
3847 bufsize += readsize;
3849 /* Process while we have NUL chars. */
3850 while ((sep = memchr(buf, 0, bufsize))) {
3851 size_t sepsize = sep - buf + 1;
3854 if (!realloc_lines(view, view->line_size + 1))
3857 file = calloc(1, sizeof(*file));
3861 add_line_data(view, file, type);
3864 /* Parse diff info part. */
3866 file->status = status;
3868 string_copy(file->old.rev, NULL_ID);
3870 } else if (!file->status) {
3871 if (!status_get_diff(file, buf, sepsize))
3875 memmove(buf, sep + 1, bufsize);
3877 sep = memchr(buf, 0, bufsize);
3880 sepsize = sep - buf + 1;
3882 /* Collapse all 'M'odified entries that
3883 * follow a associated 'U'nmerged entry.
3885 if (file->status == 'U') {
3888 } else if (unmerged) {
3889 int collapse = !strcmp(buf, unmerged->new.name);
3900 /* Grab the old name for rename/copy. */
3901 if (!*file->old.name &&
3902 (file->status == 'R' || file->status == 'C')) {
3903 sepsize = sep - buf + 1;
3904 string_ncopy(file->old.name, buf, sepsize);
3906 memmove(buf, sep + 1, bufsize);
3908 sep = memchr(buf, 0, bufsize);
3911 sepsize = sep - buf + 1;
3914 /* git-ls-files just delivers a NUL separated
3915 * list of file names similar to the second half
3916 * of the git-diff-* output. */
3917 string_ncopy(file->new.name, buf, sepsize);
3918 if (!*file->old.name)
3919 string_copy(file->old.name, file->new.name);
3921 memmove(buf, sep + 1, bufsize);
3932 if (!view->line[view->lines - 1].data)
3933 add_line_data(view, NULL, LINE_STAT_NONE);
3939 /* Don't show unmerged entries in the staged section. */
3940 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached -M HEAD"
3941 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
3942 #define STATUS_LIST_OTHER_CMD \
3943 "git ls-files -z --others --exclude-per-directory=.gitignore"
3944 #define STATUS_LIST_NO_HEAD_CMD \
3945 "git ls-files -z --cached --exclude-per-directory=.gitignore"
3947 #define STATUS_DIFF_INDEX_SHOW_CMD \
3948 "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s %s 2>/dev/null"
3950 #define STATUS_DIFF_FILES_SHOW_CMD \
3951 "git diff-files --root --patch-with-stat -C -M -- %s %s 2>/dev/null"
3953 #define STATUS_DIFF_NO_HEAD_SHOW_CMD \
3954 "git diff --no-color --patch-with-stat /dev/null %s 2>/dev/null"
3956 /* First parse staged info using git-diff-index(1), then parse unstaged
3957 * info using git-diff-files(1), and finally untracked files using
3958 * git-ls-files(1). */
3960 status_open(struct view *view)
3962 struct stat statbuf;
3963 char exclude[SIZEOF_STR];
3964 char indexcmd[SIZEOF_STR] = STATUS_DIFF_INDEX_CMD;
3965 char othercmd[SIZEOF_STR] = STATUS_LIST_OTHER_CMD;
3966 unsigned long prev_lineno = view->lineno;
3967 char indexstatus = 0;
3970 for (i = 0; i < view->lines; i++)
3971 free(view->line[i].data);
3973 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3976 if (!realloc_lines(view, view->line_size + 7))
3979 add_line_data(view, NULL, LINE_STAT_HEAD);
3981 string_copy(status_onbranch, "Initial commit");
3982 else if (!*opt_head)
3983 string_copy(status_onbranch, "Not currently on any branch");
3984 else if (!string_format(status_onbranch, "On branch %s", opt_head))
3988 string_copy(indexcmd, STATUS_LIST_NO_HEAD_CMD);
3992 if (!string_format(exclude, "%s/info/exclude", opt_git_dir))
3995 if (stat(exclude, &statbuf) >= 0) {
3996 size_t cmdsize = strlen(othercmd);
3998 if (!string_format_from(othercmd, &cmdsize, " %s", "--exclude-from=") ||
3999 sq_quote(othercmd, cmdsize, exclude) >= sizeof(othercmd))
4002 cmdsize = strlen(indexcmd);
4004 (!string_format_from(indexcmd, &cmdsize, " %s", "--exclude-from=") ||
4005 sq_quote(indexcmd, cmdsize, exclude) >= sizeof(indexcmd)))
4009 system("git update-index -q --refresh >/dev/null 2>/dev/null");
4011 if (!status_run(view, indexcmd, indexstatus, LINE_STAT_STAGED) ||
4012 !status_run(view, STATUS_DIFF_FILES_CMD, 0, LINE_STAT_UNSTAGED) ||
4013 !status_run(view, othercmd, '?', LINE_STAT_UNTRACKED))
4016 /* If all went well restore the previous line number to stay in
4017 * the context or select a line with something that can be
4019 if (prev_lineno >= view->lines)
4020 prev_lineno = view->lines - 1;
4021 while (prev_lineno < view->lines && !view->line[prev_lineno].data)
4023 while (prev_lineno > 0 && !view->line[prev_lineno].data)
4026 /* If the above fails, always skip the "On branch" line. */
4027 if (prev_lineno < view->lines)
4028 view->lineno = prev_lineno;
4032 if (view->lineno < view->offset)
4033 view->offset = view->lineno;
4034 else if (view->offset + view->height <= view->lineno)
4035 view->offset = view->lineno - view->height + 1;
4041 status_draw(struct view *view, struct line *line, unsigned int lineno)
4043 struct status *status = line->data;
4044 enum line_type type;
4048 switch (line->type) {
4049 case LINE_STAT_STAGED:
4050 type = LINE_STAT_SECTION;
4051 text = "Changes to be committed:";
4054 case LINE_STAT_UNSTAGED:
4055 type = LINE_STAT_SECTION;
4056 text = "Changed but not updated:";
4059 case LINE_STAT_UNTRACKED:
4060 type = LINE_STAT_SECTION;
4061 text = "Untracked files:";
4064 case LINE_STAT_NONE:
4065 type = LINE_DEFAULT;
4066 text = " (no files)";
4069 case LINE_STAT_HEAD:
4070 type = LINE_STAT_HEAD;
4071 text = status_onbranch;
4078 static char buf[] = { '?', ' ', ' ', ' ', 0 };
4080 buf[0] = status->status;
4081 if (draw_text(view, line->type, buf, TRUE))
4083 type = LINE_DEFAULT;
4084 text = status->new.name;
4087 draw_text(view, type, text, TRUE);
4092 status_enter(struct view *view, struct line *line)
4094 struct status *status = line->data;
4095 char oldpath[SIZEOF_STR] = "";
4096 char newpath[SIZEOF_STR] = "";
4099 enum open_flags split;
4101 if (line->type == LINE_STAT_NONE ||
4102 (!status && line[1].type == LINE_STAT_NONE)) {
4103 report("No file to diff");
4108 if (sq_quote(oldpath, 0, status->old.name) >= sizeof(oldpath))
4110 /* Diffs for unmerged entries are empty when pasing the
4111 * new path, so leave it empty. */
4112 if (status->status != 'U' &&
4113 sq_quote(newpath, 0, status->new.name) >= sizeof(newpath))
4118 line->type != LINE_STAT_UNTRACKED &&
4119 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
4122 switch (line->type) {
4123 case LINE_STAT_STAGED:
4125 if (!string_format_from(opt_cmd, &cmdsize,
4126 STATUS_DIFF_NO_HEAD_SHOW_CMD,
4130 if (!string_format_from(opt_cmd, &cmdsize,
4131 STATUS_DIFF_INDEX_SHOW_CMD,
4137 info = "Staged changes to %s";
4139 info = "Staged changes";
4142 case LINE_STAT_UNSTAGED:
4143 if (!string_format_from(opt_cmd, &cmdsize,
4144 STATUS_DIFF_FILES_SHOW_CMD, oldpath, newpath))
4147 info = "Unstaged changes to %s";
4149 info = "Unstaged changes";
4152 case LINE_STAT_UNTRACKED:
4157 report("No file to show");
4161 opt_pipe = fopen(status->new.name, "r");
4162 info = "Untracked file %s";
4165 case LINE_STAT_HEAD:
4169 die("line type %d not handled in switch", line->type);
4172 split = view_is_displayed(view) ? OPEN_SPLIT : 0;
4173 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | split);
4174 if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) {
4176 stage_status = *status;
4178 memset(&stage_status, 0, sizeof(stage_status));
4181 stage_line_type = line->type;
4182 string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.new.name);
4189 status_exists(struct status *status, enum line_type type)
4191 struct view *view = VIEW(REQ_VIEW_STATUS);
4194 for (line = view->line; line < view->line + view->lines; line++) {
4195 struct status *pos = line->data;
4197 if (line->type == type && pos &&
4198 !strcmp(status->new.name, pos->new.name))
4207 status_update_prepare(enum line_type type)
4209 char cmd[SIZEOF_STR];
4213 type != LINE_STAT_UNTRACKED &&
4214 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4218 case LINE_STAT_STAGED:
4219 string_add(cmd, cmdsize, "git update-index -z --index-info");
4222 case LINE_STAT_UNSTAGED:
4223 case LINE_STAT_UNTRACKED:
4224 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
4228 die("line type %d not handled in switch", type);
4231 return popen(cmd, "w");
4235 status_update_write(FILE *pipe, struct status *status, enum line_type type)
4237 char buf[SIZEOF_STR];
4242 case LINE_STAT_STAGED:
4243 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
4246 status->old.name, 0))
4250 case LINE_STAT_UNSTAGED:
4251 case LINE_STAT_UNTRACKED:
4252 if (!string_format_from(buf, &bufsize, "%s%c", status->new.name, 0))
4257 die("line type %d not handled in switch", type);
4260 while (!ferror(pipe) && written < bufsize) {
4261 written += fwrite(buf + written, 1, bufsize - written, pipe);
4264 return written == bufsize;
4268 status_update_file(struct status *status, enum line_type type)
4270 FILE *pipe = status_update_prepare(type);
4276 result = status_update_write(pipe, status, type);
4282 status_update_files(struct view *view, struct line *line)
4284 FILE *pipe = status_update_prepare(line->type);
4286 struct line *pos = view->line + view->lines;
4293 for (pos = line; pos < view->line + view->lines && pos->data; pos++)
4296 for (file = 0, done = 0; result && file < files; line++, file++) {
4297 int almost_done = file * 100 / files;
4299 if (almost_done > done) {
4301 string_format(view->ref, "updating file %u of %u (%d%% done)",
4303 update_view_title(view);
4305 result = status_update_write(pipe, line->data, line->type);
4313 status_update(struct view *view)
4315 struct line *line = &view->line[view->lineno];
4317 assert(view->lines);
4320 /* This should work even for the "On branch" line. */
4321 if (line < view->line + view->lines && !line[1].data) {
4322 report("Nothing to update");
4326 if (!status_update_files(view, line + 1)) {
4327 report("Failed to update file status");
4331 } else if (!status_update_file(line->data, line->type)) {
4332 report("Failed to update file status");
4340 status_request(struct view *view, enum request request, struct line *line)
4342 struct status *status = line->data;
4345 case REQ_STATUS_UPDATE:
4346 if (!status_update(view))
4350 case REQ_STATUS_MERGE:
4351 if (!status || status->status != 'U') {
4352 report("Merging only possible for files with unmerged status ('U').");
4355 open_mergetool(status->new.name);
4362 open_editor(status->status != '?', status->new.name);
4365 case REQ_VIEW_BLAME:
4367 string_copy(opt_file, status->new.name);
4373 /* After returning the status view has been split to
4374 * show the stage view. No further reloading is
4376 status_enter(view, line);
4380 /* Simply reload the view. */
4387 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
4393 status_select(struct view *view, struct line *line)
4395 struct status *status = line->data;
4396 char file[SIZEOF_STR] = "all files";
4400 if (status && !string_format(file, "'%s'", status->new.name))
4403 if (!status && line[1].type == LINE_STAT_NONE)
4406 switch (line->type) {
4407 case LINE_STAT_STAGED:
4408 text = "Press %s to unstage %s for commit";
4411 case LINE_STAT_UNSTAGED:
4412 text = "Press %s to stage %s for commit";
4415 case LINE_STAT_UNTRACKED:
4416 text = "Press %s to stage %s for addition";
4419 case LINE_STAT_HEAD:
4420 case LINE_STAT_NONE:
4421 text = "Nothing to update";
4425 die("line type %d not handled in switch", line->type);
4428 if (status && status->status == 'U') {
4429 text = "Press %s to resolve conflict in %s";
4430 key = get_key(REQ_STATUS_MERGE);
4433 key = get_key(REQ_STATUS_UPDATE);
4436 string_format(view->ref, text, key, file);
4440 status_grep(struct view *view, struct line *line)
4442 struct status *status = line->data;
4443 enum { S_STATUS, S_NAME, S_END } state;
4450 for (state = S_STATUS; state < S_END; state++) {
4454 case S_NAME: text = status->new.name; break;
4456 buf[0] = status->status;
4464 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4471 static struct view_ops status_ops = {
4483 stage_diff_line(FILE *pipe, struct line *line)
4485 char *buf = line->data;
4486 size_t bufsize = strlen(buf);
4489 while (!ferror(pipe) && written < bufsize) {
4490 written += fwrite(buf + written, 1, bufsize - written, pipe);
4495 return written == bufsize;
4499 stage_diff_write(FILE *pipe, struct line *line, struct line *end)
4501 while (line < end) {
4502 if (!stage_diff_line(pipe, line++))
4504 if (line->type == LINE_DIFF_CHUNK ||
4505 line->type == LINE_DIFF_HEADER)
4512 static struct line *
4513 stage_diff_find(struct view *view, struct line *line, enum line_type type)
4515 for (; view->line < line; line--)
4516 if (line->type == type)
4523 stage_update_chunk(struct view *view, struct line *chunk)
4525 char cmd[SIZEOF_STR];
4527 struct line *diff_hdr;
4530 diff_hdr = stage_diff_find(view, chunk, LINE_DIFF_HEADER);
4535 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4538 if (!string_format_from(cmd, &cmdsize,
4539 "git apply --whitespace=nowarn --cached %s - && "
4540 "git update-index -q --unmerged --refresh 2>/dev/null",
4541 stage_line_type == LINE_STAT_STAGED ? "-R" : ""))
4544 pipe = popen(cmd, "w");
4548 if (!stage_diff_write(pipe, diff_hdr, chunk) ||
4549 !stage_diff_write(pipe, chunk, view->line + view->lines))
4554 return chunk ? TRUE : FALSE;
4558 stage_update(struct view *view, struct line *line)
4560 struct line *chunk = NULL;
4562 if (!opt_no_head && stage_line_type != LINE_STAT_UNTRACKED)
4563 chunk = stage_diff_find(view, line, LINE_DIFF_CHUNK);
4566 if (!stage_update_chunk(view, chunk)) {
4567 report("Failed to apply chunk");
4571 } else if (!stage_status.status) {
4572 view = VIEW(REQ_VIEW_STATUS);
4574 for (line = view->line; line < view->line + view->lines; line++)
4575 if (line->type == stage_line_type)
4578 if (!status_update_files(view, line + 1)) {
4579 report("Failed to update files");
4583 } else if (!status_update_file(&stage_status, stage_line_type)) {
4584 report("Failed to update file");
4592 stage_request(struct view *view, enum request request, struct line *line)
4595 case REQ_STATUS_UPDATE:
4596 if (!stage_update(view, line))
4601 if (!stage_status.new.name[0])
4604 open_editor(stage_status.status != '?', stage_status.new.name);
4608 /* Reload everything ... */
4611 case REQ_VIEW_BLAME:
4612 if (stage_status.new.name[0]) {
4613 string_copy(opt_file, stage_status.new.name);
4619 return pager_request(view, request, line);
4625 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD | OPEN_NOMAXIMIZE);
4627 /* Check whether the staged entry still exists, and close the
4628 * stage view if it doesn't. */
4629 if (!status_exists(&stage_status, stage_line_type))
4630 return REQ_VIEW_CLOSE;
4632 if (stage_line_type == LINE_STAT_UNTRACKED)
4633 opt_pipe = fopen(stage_status.new.name, "r");
4635 string_copy(opt_cmd, view->cmd);
4636 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | OPEN_NOMAXIMIZE);
4641 static struct view_ops stage_ops = {
4657 char id[SIZEOF_REV]; /* SHA1 ID. */
4658 char title[128]; /* First line of the commit message. */
4659 char author[75]; /* Author of the commit. */
4660 struct tm time; /* Date from the author ident. */
4661 struct ref **refs; /* Repository references. */
4662 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
4663 size_t graph_size; /* The width of the graph array. */
4664 bool has_parents; /* Rewritten --parents seen. */
4667 /* Size of rev graph with no "padding" columns */
4668 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
4671 struct rev_graph *prev, *next, *parents;
4672 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
4674 struct commit *commit;
4676 unsigned int boundary:1;
4679 /* Parents of the commit being visualized. */
4680 static struct rev_graph graph_parents[4];
4682 /* The current stack of revisions on the graph. */
4683 static struct rev_graph graph_stacks[4] = {
4684 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
4685 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
4686 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
4687 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
4691 graph_parent_is_merge(struct rev_graph *graph)
4693 return graph->parents->size > 1;
4697 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
4699 struct commit *commit = graph->commit;
4701 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
4702 commit->graph[commit->graph_size++] = symbol;
4706 done_rev_graph(struct rev_graph *graph)
4708 if (graph_parent_is_merge(graph) &&
4709 graph->pos < graph->size - 1 &&
4710 graph->next->size == graph->size + graph->parents->size - 1) {
4711 size_t i = graph->pos + graph->parents->size - 1;
4713 graph->commit->graph_size = i * 2;
4714 while (i < graph->next->size - 1) {
4715 append_to_rev_graph(graph, ' ');
4716 append_to_rev_graph(graph, '\\');
4721 graph->size = graph->pos = 0;
4722 graph->commit = NULL;
4723 memset(graph->parents, 0, sizeof(*graph->parents));
4727 push_rev_graph(struct rev_graph *graph, char *parent)
4731 /* "Collapse" duplicate parents lines.
4733 * FIXME: This needs to also update update the drawn graph but
4734 * for now it just serves as a method for pruning graph lines. */
4735 for (i = 0; i < graph->size; i++)
4736 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
4739 if (graph->size < SIZEOF_REVITEMS) {
4740 string_copy_rev(graph->rev[graph->size++], parent);
4745 get_rev_graph_symbol(struct rev_graph *graph)
4749 if (graph->boundary)
4750 symbol = REVGRAPH_BOUND;
4751 else if (graph->parents->size == 0)
4752 symbol = REVGRAPH_INIT;
4753 else if (graph_parent_is_merge(graph))
4754 symbol = REVGRAPH_MERGE;
4755 else if (graph->pos >= graph->size)
4756 symbol = REVGRAPH_BRANCH;
4758 symbol = REVGRAPH_COMMIT;
4764 draw_rev_graph(struct rev_graph *graph)
4767 chtype separator, line;
4769 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
4770 static struct rev_filler fillers[] = {
4776 chtype symbol = get_rev_graph_symbol(graph);
4777 struct rev_filler *filler;
4780 if (opt_line_graphics)
4781 fillers[DEFAULT].line = line_graphics[LINE_GRAPHIC_VLINE];
4783 filler = &fillers[DEFAULT];
4785 for (i = 0; i < graph->pos; i++) {
4786 append_to_rev_graph(graph, filler->line);
4787 if (graph_parent_is_merge(graph->prev) &&
4788 graph->prev->pos == i)
4789 filler = &fillers[RSHARP];
4791 append_to_rev_graph(graph, filler->separator);
4794 /* Place the symbol for this revision. */
4795 append_to_rev_graph(graph, symbol);
4797 if (graph->prev->size > graph->size)
4798 filler = &fillers[RDIAG];
4800 filler = &fillers[DEFAULT];
4804 for (; i < graph->size; i++) {
4805 append_to_rev_graph(graph, filler->separator);
4806 append_to_rev_graph(graph, filler->line);
4807 if (graph_parent_is_merge(graph->prev) &&
4808 i < graph->prev->pos + graph->parents->size)
4809 filler = &fillers[RSHARP];
4810 if (graph->prev->size > graph->size)
4811 filler = &fillers[LDIAG];
4814 if (graph->prev->size > graph->size) {
4815 append_to_rev_graph(graph, filler->separator);
4816 if (filler->line != ' ')
4817 append_to_rev_graph(graph, filler->line);
4821 /* Prepare the next rev graph */
4823 prepare_rev_graph(struct rev_graph *graph)
4827 /* First, traverse all lines of revisions up to the active one. */
4828 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
4829 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
4832 push_rev_graph(graph->next, graph->rev[graph->pos]);
4835 /* Interleave the new revision parent(s). */
4836 for (i = 0; !graph->boundary && i < graph->parents->size; i++)
4837 push_rev_graph(graph->next, graph->parents->rev[i]);
4839 /* Lastly, put any remaining revisions. */
4840 for (i = graph->pos + 1; i < graph->size; i++)
4841 push_rev_graph(graph->next, graph->rev[i]);
4845 update_rev_graph(struct rev_graph *graph)
4847 /* If this is the finalizing update ... */
4849 prepare_rev_graph(graph);
4851 /* Graph visualization needs a one rev look-ahead,
4852 * so the first update doesn't visualize anything. */
4853 if (!graph->prev->commit)
4856 draw_rev_graph(graph->prev);
4857 done_rev_graph(graph->prev->prev);
4866 main_draw(struct view *view, struct line *line, unsigned int lineno)
4868 struct commit *commit = line->data;
4870 if (!*commit->author)
4873 if (opt_date && draw_date(view, &commit->time))
4877 draw_field(view, LINE_MAIN_AUTHOR, commit->author, AUTHOR_COLS, TRUE))
4880 if (opt_rev_graph && commit->graph_size &&
4881 draw_graphic(view, LINE_MAIN_REVGRAPH, commit->graph, commit->graph_size))
4884 if (opt_show_refs && commit->refs) {
4888 enum line_type type;
4890 if (commit->refs[i]->head)
4891 type = LINE_MAIN_HEAD;
4892 else if (commit->refs[i]->ltag)
4893 type = LINE_MAIN_LOCAL_TAG;
4894 else if (commit->refs[i]->tag)
4895 type = LINE_MAIN_TAG;
4896 else if (commit->refs[i]->tracked)
4897 type = LINE_MAIN_TRACKED;
4898 else if (commit->refs[i]->remote)
4899 type = LINE_MAIN_REMOTE;
4901 type = LINE_MAIN_REF;
4903 if (draw_text(view, type, "[", TRUE) ||
4904 draw_text(view, type, commit->refs[i]->name, TRUE) ||
4905 draw_text(view, type, "]", TRUE))
4908 if (draw_text(view, LINE_DEFAULT, " ", TRUE))
4910 } while (commit->refs[i++]->next);
4913 draw_text(view, LINE_DEFAULT, commit->title, TRUE);
4917 /* Reads git log --pretty=raw output and parses it into the commit struct. */
4919 main_read(struct view *view, char *line)
4921 static struct rev_graph *graph = graph_stacks;
4922 enum line_type type;
4923 struct commit *commit;
4926 if (!view->lines && !view->parent)
4927 die("No revisions match the given arguments.");
4928 update_rev_graph(graph);
4932 type = get_line_type(line);
4933 if (type == LINE_COMMIT) {
4934 commit = calloc(1, sizeof(struct commit));
4938 line += STRING_SIZE("commit ");
4940 graph->boundary = 1;
4944 string_copy_rev(commit->id, line);
4945 commit->refs = get_refs(commit->id);
4946 graph->commit = commit;
4947 add_line_data(view, commit, LINE_MAIN_COMMIT);
4949 while ((line = strchr(line, ' '))) {
4951 push_rev_graph(graph->parents, line);
4952 commit->has_parents = TRUE;
4959 commit = view->line[view->lines - 1].data;
4963 if (commit->has_parents)
4965 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
4970 /* Parse author lines where the name may be empty:
4971 * author <email@address.tld> 1138474660 +0100
4973 char *ident = line + STRING_SIZE("author ");
4974 char *nameend = strchr(ident, '<');
4975 char *emailend = strchr(ident, '>');
4977 if (!nameend || !emailend)
4980 update_rev_graph(graph);
4981 graph = graph->next;
4983 *nameend = *emailend = 0;
4984 ident = chomp_string(ident);
4986 ident = chomp_string(nameend + 1);
4991 string_ncopy(commit->author, ident, strlen(ident));
4993 /* Parse epoch and timezone */
4994 if (emailend[1] == ' ') {
4995 char *secs = emailend + 2;
4996 char *zone = strchr(secs, ' ');
4997 time_t time = (time_t) atol(secs);
4999 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
5003 tz = ('0' - zone[1]) * 60 * 60 * 10;
5004 tz += ('0' - zone[2]) * 60 * 60;
5005 tz += ('0' - zone[3]) * 60;
5006 tz += ('0' - zone[4]) * 60;
5014 gmtime_r(&time, &commit->time);
5019 /* Fill in the commit title if it has not already been set. */
5020 if (commit->title[0])
5023 /* Require titles to start with a non-space character at the
5024 * offset used by git log. */
5025 if (strncmp(line, " ", 4))
5028 /* Well, if the title starts with a whitespace character,
5029 * try to be forgiving. Otherwise we end up with no title. */
5030 while (isspace(*line))
5034 /* FIXME: More graceful handling of titles; append "..." to
5035 * shortened titles, etc. */
5037 string_ncopy(commit->title, line, strlen(line));
5044 main_request(struct view *view, enum request request, struct line *line)
5046 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
5048 if (request == REQ_ENTER)
5049 open_view(view, REQ_VIEW_DIFF, flags);
5057 grep_refs(struct ref **refs, regex_t *regex)
5065 if (regexec(regex, refs[i]->name, 1, &pmatch, 0) != REG_NOMATCH)
5067 } while (refs[i++]->next);
5073 main_grep(struct view *view, struct line *line)
5075 struct commit *commit = line->data;
5076 enum { S_TITLE, S_AUTHOR, S_DATE, S_REFS, S_END } state;
5077 char buf[DATE_COLS + 1];
5080 for (state = S_TITLE; state < S_END; state++) {
5084 case S_TITLE: text = commit->title; break;
5088 text = commit->author;
5093 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
5100 if (grep_refs(commit->refs, view->regex) == TRUE)
5107 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
5115 main_select(struct view *view, struct line *line)
5117 struct commit *commit = line->data;
5119 string_copy_rev(view->ref, commit->id);
5120 string_copy_rev(ref_commit, view->ref);
5123 static struct view_ops main_ops = {
5135 * Unicode / UTF-8 handling
5137 * NOTE: Much of the following code for dealing with unicode is derived from
5138 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
5139 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
5142 /* I've (over)annotated a lot of code snippets because I am not entirely
5143 * confident that the approach taken by this small UTF-8 interface is correct.
5147 unicode_width(unsigned long c)
5150 (c <= 0x115f /* Hangul Jamo */
5153 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
5155 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
5156 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
5157 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
5158 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
5159 || (c >= 0xffe0 && c <= 0xffe6)
5160 || (c >= 0x20000 && c <= 0x2fffd)
5161 || (c >= 0x30000 && c <= 0x3fffd)))
5165 return opt_tab_size;
5170 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
5171 * Illegal bytes are set one. */
5172 static const unsigned char utf8_bytes[256] = {
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 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,
5178 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,
5179 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,
5180 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,
5183 /* Decode UTF-8 multi-byte representation into a unicode character. */
5184 static inline unsigned long
5185 utf8_to_unicode(const char *string, size_t length)
5187 unsigned long unicode;
5191 unicode = string[0];
5194 unicode = (string[0] & 0x1f) << 6;
5195 unicode += (string[1] & 0x3f);
5198 unicode = (string[0] & 0x0f) << 12;
5199 unicode += ((string[1] & 0x3f) << 6);
5200 unicode += (string[2] & 0x3f);
5203 unicode = (string[0] & 0x0f) << 18;
5204 unicode += ((string[1] & 0x3f) << 12);
5205 unicode += ((string[2] & 0x3f) << 6);
5206 unicode += (string[3] & 0x3f);
5209 unicode = (string[0] & 0x0f) << 24;
5210 unicode += ((string[1] & 0x3f) << 18);
5211 unicode += ((string[2] & 0x3f) << 12);
5212 unicode += ((string[3] & 0x3f) << 6);
5213 unicode += (string[4] & 0x3f);
5216 unicode = (string[0] & 0x01) << 30;
5217 unicode += ((string[1] & 0x3f) << 24);
5218 unicode += ((string[2] & 0x3f) << 18);
5219 unicode += ((string[3] & 0x3f) << 12);
5220 unicode += ((string[4] & 0x3f) << 6);
5221 unicode += (string[5] & 0x3f);
5224 die("Invalid unicode length");
5227 /* Invalid characters could return the special 0xfffd value but NUL
5228 * should be just as good. */
5229 return unicode > 0xffff ? 0 : unicode;
5232 /* Calculates how much of string can be shown within the given maximum width
5233 * and sets trimmed parameter to non-zero value if all of string could not be
5234 * shown. If the reserve flag is TRUE, it will reserve at least one
5235 * trailing character, which can be useful when drawing a delimiter.
5237 * Returns the number of bytes to output from string to satisfy max_width. */
5239 utf8_length(const char *string, int *width, size_t max_width, int *trimmed, bool reserve)
5241 const char *start = string;
5242 const char *end = strchr(string, '\0');
5243 unsigned char last_bytes = 0;
5244 size_t last_ucwidth = 0;
5249 while (string < end) {
5250 int c = *(unsigned char *) string;
5251 unsigned char bytes = utf8_bytes[c];
5253 unsigned long unicode;
5255 if (string + bytes > end)
5258 /* Change representation to figure out whether
5259 * it is a single- or double-width character. */
5261 unicode = utf8_to_unicode(string, bytes);
5262 /* FIXME: Graceful handling of invalid unicode character. */
5266 ucwidth = unicode_width(unicode);
5268 if (*width > max_width) {
5271 if (reserve && *width == max_width) {
5272 string -= last_bytes;
5273 *width -= last_ucwidth;
5280 last_ucwidth = ucwidth;
5283 return string - start;
5291 /* Whether or not the curses interface has been initialized. */
5292 static bool cursed = FALSE;
5294 /* The status window is used for polling keystrokes. */
5295 static WINDOW *status_win;
5297 static bool status_empty = TRUE;
5299 /* Update status and title window. */
5301 report(const char *msg, ...)
5303 struct view *view = display[current_view];
5309 char buf[SIZEOF_STR];
5312 va_start(args, msg);
5313 if (vsnprintf(buf, sizeof(buf), msg, args) >= sizeof(buf)) {
5314 buf[sizeof(buf) - 1] = 0;
5315 buf[sizeof(buf) - 2] = '.';
5316 buf[sizeof(buf) - 3] = '.';
5317 buf[sizeof(buf) - 4] = '.';
5323 if (!status_empty || *msg) {
5326 va_start(args, msg);
5328 wmove(status_win, 0, 0);
5330 vwprintw(status_win, msg, args);
5331 status_empty = FALSE;
5333 status_empty = TRUE;
5335 wclrtoeol(status_win);
5336 wrefresh(status_win);
5341 update_view_title(view);
5342 update_display_cursor(view);
5345 /* Controls when nodelay should be in effect when polling user input. */
5347 set_nonblocking_input(bool loading)
5349 static unsigned int loading_views;
5351 if ((loading == FALSE && loading_views-- == 1) ||
5352 (loading == TRUE && loading_views++ == 0))
5353 nodelay(status_win, loading);
5361 /* Initialize the curses library */
5362 if (isatty(STDIN_FILENO)) {
5363 cursed = !!initscr();
5365 /* Leave stdin and stdout alone when acting as a pager. */
5366 FILE *io = fopen("/dev/tty", "r+");
5369 die("Failed to open /dev/tty");
5370 cursed = !!newterm(NULL, io, io);
5374 die("Failed to initialize curses");
5376 nonl(); /* Tell curses not to do NL->CR/NL on output */
5377 cbreak(); /* Take input chars one at a time, no wait for \n */
5378 noecho(); /* Don't echo input */
5379 leaveok(stdscr, TRUE);
5384 getmaxyx(stdscr, y, x);
5385 status_win = newwin(1, 0, y - 1, 0);
5387 die("Failed to create status window");
5389 /* Enable keyboard mapping */
5390 keypad(status_win, TRUE);
5391 wbkgdset(status_win, get_line_attr(LINE_STATUS));
5393 TABSIZE = opt_tab_size;
5394 if (opt_line_graphics) {
5395 line_graphics[LINE_GRAPHIC_VLINE] = ACS_VLINE;
5400 read_prompt(const char *prompt)
5402 enum { READING, STOP, CANCEL } status = READING;
5403 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
5406 while (status == READING) {
5412 foreach_view (view, i)
5417 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
5418 wclrtoeol(status_win);
5420 /* Refresh, accept single keystroke of input */
5421 key = wgetch(status_win);
5426 status = pos ? STOP : CANCEL;
5444 if (pos >= sizeof(buf)) {
5445 report("Input string too long");
5450 buf[pos++] = (char) key;
5454 /* Clear the status window */
5455 status_empty = FALSE;
5458 if (status == CANCEL)
5467 * Repository references
5470 static struct ref *refs = NULL;
5471 static size_t refs_alloc = 0;
5472 static size_t refs_size = 0;
5474 /* Id <-> ref store */
5475 static struct ref ***id_refs = NULL;
5476 static size_t id_refs_alloc = 0;
5477 static size_t id_refs_size = 0;
5479 static struct ref **
5482 struct ref ***tmp_id_refs;
5483 struct ref **ref_list = NULL;
5484 size_t ref_list_alloc = 0;
5485 size_t ref_list_size = 0;
5488 for (i = 0; i < id_refs_size; i++)
5489 if (!strcmp(id, id_refs[i][0]->id))
5492 tmp_id_refs = realloc_items(id_refs, &id_refs_alloc, id_refs_size + 1,
5497 id_refs = tmp_id_refs;
5499 for (i = 0; i < refs_size; i++) {
5502 if (strcmp(id, refs[i].id))
5505 tmp = realloc_items(ref_list, &ref_list_alloc,
5506 ref_list_size + 1, sizeof(*ref_list));
5514 if (ref_list_size > 0)
5515 ref_list[ref_list_size - 1]->next = 1;
5516 ref_list[ref_list_size] = &refs[i];
5518 /* XXX: The properties of the commit chains ensures that we can
5519 * safely modify the shared ref. The repo references will
5520 * always be similar for the same id. */
5521 ref_list[ref_list_size]->next = 0;
5526 id_refs[id_refs_size++] = ref_list;
5532 read_ref(char *id, size_t idlen, char *name, size_t namelen)
5537 bool remote = FALSE;
5538 bool tracked = FALSE;
5539 bool check_replace = FALSE;
5542 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
5543 if (!strcmp(name + namelen - 3, "^{}")) {
5546 if (refs_size > 0 && refs[refs_size - 1].ltag == TRUE)
5547 check_replace = TRUE;
5553 namelen -= STRING_SIZE("refs/tags/");
5554 name += STRING_SIZE("refs/tags/");
5556 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
5558 namelen -= STRING_SIZE("refs/remotes/");
5559 name += STRING_SIZE("refs/remotes/");
5560 tracked = !strcmp(opt_remote, name);
5562 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5563 namelen -= STRING_SIZE("refs/heads/");
5564 name += STRING_SIZE("refs/heads/");
5565 head = !strncmp(opt_head, name, namelen);
5567 } else if (!strcmp(name, "HEAD")) {
5568 opt_no_head = FALSE;
5572 if (check_replace && !strcmp(name, refs[refs_size - 1].name)) {
5573 /* it's an annotated tag, replace the previous sha1 with the
5574 * resolved commit id; relies on the fact git-ls-remote lists
5575 * the commit id of an annotated tag right beofre the commit id
5577 refs[refs_size - 1].ltag = ltag;
5578 string_copy_rev(refs[refs_size - 1].id, id);
5582 refs = realloc_items(refs, &refs_alloc, refs_size + 1, sizeof(*refs));
5586 ref = &refs[refs_size++];
5587 ref->name = malloc(namelen + 1);
5591 strncpy(ref->name, name, namelen);
5592 ref->name[namelen] = 0;
5596 ref->remote = remote;
5597 ref->tracked = tracked;
5598 string_copy_rev(ref->id, id);
5606 const char *cmd_env = getenv("TIG_LS_REMOTE");
5607 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
5609 return read_properties(popen(cmd, "r"), "\t", read_ref);
5613 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
5615 if (!strcmp(name, "i18n.commitencoding"))
5616 string_ncopy(opt_encoding, value, valuelen);
5618 if (!strcmp(name, "core.editor"))
5619 string_ncopy(opt_editor, value, valuelen);
5621 /* branch.<head>.remote */
5623 !strncmp(name, "branch.", 7) &&
5624 !strncmp(name + 7, opt_head, strlen(opt_head)) &&
5625 !strcmp(name + 7 + strlen(opt_head), ".remote"))
5626 string_ncopy(opt_remote, value, valuelen);
5628 if (*opt_head && *opt_remote &&
5629 !strncmp(name, "branch.", 7) &&
5630 !strncmp(name + 7, opt_head, strlen(opt_head)) &&
5631 !strcmp(name + 7 + strlen(opt_head), ".merge")) {
5632 size_t from = strlen(opt_remote);
5634 if (!strncmp(value, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5635 value += STRING_SIZE("refs/heads/");
5636 valuelen -= STRING_SIZE("refs/heads/");
5639 if (!string_format_from(opt_remote, &from, "/%s", value))
5647 load_git_config(void)
5649 return read_properties(popen(GIT_CONFIG " --list", "r"),
5650 "=", read_repo_config_option);
5654 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
5656 if (!opt_git_dir[0]) {
5657 string_ncopy(opt_git_dir, name, namelen);
5659 } else if (opt_is_inside_work_tree == -1) {
5660 /* This can be 3 different values depending on the
5661 * version of git being used. If git-rev-parse does not
5662 * understand --is-inside-work-tree it will simply echo
5663 * the option else either "true" or "false" is printed.
5664 * Default to true for the unknown case. */
5665 opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
5667 } else if (opt_cdup[0] == ' ') {
5668 string_ncopy(opt_cdup, name, namelen);
5670 if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5671 namelen -= STRING_SIZE("refs/heads/");
5672 name += STRING_SIZE("refs/heads/");
5673 string_ncopy(opt_head, name, namelen);
5681 load_repo_info(void)
5684 FILE *pipe = popen("(git rev-parse --git-dir --is-inside-work-tree "
5685 " --show-cdup; git symbolic-ref HEAD) 2>/dev/null", "r");
5687 /* XXX: The line outputted by "--show-cdup" can be empty so
5688 * initialize it to something invalid to make it possible to
5689 * detect whether it has been set or not. */
5692 result = read_properties(pipe, "=", read_repo_info);
5693 if (opt_cdup[0] == ' ')
5700 read_properties(FILE *pipe, const char *separators,
5701 int (*read_property)(char *, size_t, char *, size_t))
5703 char buffer[BUFSIZ];
5710 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
5715 name = chomp_string(name);
5716 namelen = strcspn(name, separators);
5718 if (name[namelen]) {
5720 value = chomp_string(name + namelen + 1);
5721 valuelen = strlen(value);
5728 state = read_property(name, namelen, value, valuelen);
5731 if (state != ERR && ferror(pipe))
5744 static void __NORETURN
5747 /* XXX: Restore tty modes and let the OS cleanup the rest! */
5753 static void __NORETURN
5754 die(const char *err, ...)
5760 va_start(args, err);
5761 fputs("tig: ", stderr);
5762 vfprintf(stderr, err, args);
5763 fputs("\n", stderr);
5770 warn(const char *msg, ...)
5774 va_start(args, msg);
5775 fputs("tig warning: ", stderr);
5776 vfprintf(stderr, msg, args);
5777 fputs("\n", stderr);
5782 main(int argc, char *argv[])
5785 enum request request;
5788 signal(SIGINT, quit);
5790 if (setlocale(LC_ALL, "")) {
5791 char *codeset = nl_langinfo(CODESET);
5793 string_ncopy(opt_codeset, codeset, strlen(codeset));
5796 if (load_repo_info() == ERR)
5797 die("Failed to load repo info.");
5799 if (load_options() == ERR)
5800 die("Failed to load user config.");
5802 if (load_git_config() == ERR)
5803 die("Failed to load repo config.");
5805 if (!parse_options(argc, argv))
5808 /* Require a git repository unless when running in pager mode. */
5809 if (!opt_git_dir[0] && opt_request != REQ_VIEW_PAGER)
5810 die("Not a git repository");
5812 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
5815 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
5816 opt_iconv = iconv_open(opt_codeset, opt_encoding);
5817 if (opt_iconv == ICONV_NONE)
5818 die("Failed to initialize character set conversion");
5821 if (*opt_git_dir && load_refs() == ERR)
5822 die("Failed to load refs.");
5824 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
5825 view->cmd_env = getenv(view->cmd_env);
5827 request = opt_request;
5831 while (view_driver(display[current_view], request)) {
5835 foreach_view (view, i)
5838 /* Refresh, accept single keystroke of input */
5839 key = wgetch(status_win);
5841 /* wgetch() with nodelay() enabled returns ERR when there's no
5848 request = get_keybinding(display[current_view]->keymap, key);
5850 /* Some low-level request handling. This keeps access to
5851 * status_win restricted. */
5855 char *cmd = read_prompt(":");
5857 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
5858 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
5859 opt_request = REQ_VIEW_DIFF;
5861 opt_request = REQ_VIEW_PAGER;
5870 case REQ_SEARCH_BACK:
5872 const char *prompt = request == REQ_SEARCH
5874 char *search = read_prompt(prompt);
5877 string_ncopy(opt_search, search, strlen(search));
5882 case REQ_SCREEN_RESIZE:
5886 getmaxyx(stdscr, height, width);
5888 /* Resize the status view and let the view driver take
5889 * care of resizing the displayed views. */
5890 wresize(status_win, 1, width);
5891 mvwin(status_win, height - 1, 0);
5892 wrefresh(status_win);