1 /* Copyright (c) 2006-2008 Jonas Fonseca <fonseca@diku.dk>
3 * This program is free software; you can redistribute it and/or
4 * modify it under the terms of the GNU General Public License as
5 * published by the Free Software Foundation; either version 2 of
6 * the License, or (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
19 #define TIG_VERSION "unknown-version"
34 #include <sys/types.h>
45 /* ncurses(3): Must be defined to have extended wide-character functions. */
46 #define _XOPEN_SOURCE_EXTENDED
48 #ifdef HAVE_NCURSESW_NCURSES_H
49 #include <ncursesw/ncurses.h>
51 #ifdef HAVE_NCURSES_NCURSES_H
52 #include <ncurses/ncurses.h>
59 #define __NORETURN __attribute__((__noreturn__))
64 static void __NORETURN die(const char *err, ...);
65 static void warn(const char *msg, ...);
66 static void report(const char *msg, ...);
67 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, size_t, char *, size_t));
68 static void set_nonblocking_input(bool loading);
69 static size_t utf8_length(const char *string, int *width, size_t max_width, int *trimmed, bool reserve);
70 static bool prompt_yesno(const char *prompt);
71 static int load_refs(void);
73 #define ABS(x) ((x) >= 0 ? (x) : -(x))
74 #define MIN(x, y) ((x) < (y) ? (x) : (y))
76 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
77 #define STRING_SIZE(x) (sizeof(x) - 1)
79 #define SIZEOF_STR 1024 /* Default string size. */
80 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
81 #define SIZEOF_REV 41 /* Holds a SHA-1 and an ending NUL. */
82 #define SIZEOF_ARG 32 /* Default argument array size. */
86 #define REVGRAPH_INIT 'I'
87 #define REVGRAPH_MERGE 'M'
88 #define REVGRAPH_BRANCH '+'
89 #define REVGRAPH_COMMIT '*'
90 #define REVGRAPH_BOUND '^'
92 #define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
94 /* This color name can be used to refer to the default term colors. */
95 #define COLOR_DEFAULT (-1)
97 #define ICONV_NONE ((iconv_t) -1)
99 #define ICONV_CONST /* nothing */
102 /* The format and size of the date column in the main view. */
103 #define DATE_FORMAT "%Y-%m-%d %H:%M"
104 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
106 #define AUTHOR_COLS 20
109 /* The default interval between line numbers. */
110 #define NUMBER_INTERVAL 5
114 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
116 #define NULL_ID "0000000000000000000000000000000000000000"
119 #define GIT_CONFIG "config"
122 #define TIG_LS_REMOTE \
123 "git ls-remote . 2>/dev/null"
125 #define TIG_DIFF_CMD \
126 "git show --pretty=fuller --no-color --root --patch-with-stat --find-copies-harder -C %s 2>/dev/null"
128 #define TIG_LOG_CMD \
129 "git log --no-color --cc --stat -n100 %s 2>/dev/null"
131 #define TIG_MAIN_BASE \
132 "git log --no-color --pretty=raw --parents --topo-order"
134 #define TIG_MAIN_CMD \
135 TIG_MAIN_BASE " %s 2>/dev/null"
137 #define TIG_TREE_CMD \
140 #define TIG_BLOB_CMD \
141 "git cat-file blob %s"
143 /* XXX: Needs to be defined to the empty string. */
144 #define TIG_HELP_CMD ""
145 #define TIG_PAGER_CMD ""
146 #define TIG_STATUS_CMD ""
147 #define TIG_STAGE_CMD ""
148 #define TIG_BLAME_CMD ""
150 /* Some ascii-shorthands fitted into the ncurses namespace. */
152 #define KEY_RETURN '\r'
157 char *name; /* Ref name; tag or head names are shortened. */
158 char id[SIZEOF_REV]; /* Commit SHA1 ID */
159 unsigned int head:1; /* Is it the current HEAD? */
160 unsigned int tag:1; /* Is it a tag? */
161 unsigned int ltag:1; /* If so, is the tag local? */
162 unsigned int remote:1; /* Is it a remote ref? */
163 unsigned int tracked:1; /* Is it the remote for the current HEAD? */
164 unsigned int next:1; /* For ref lists: are there more refs? */
167 static struct ref **get_refs(const char *id);
170 FORMAT_ALL, /* Perform replacement in all arguments. */
171 FORMAT_DASH, /* Perform replacement up until "--". */
172 FORMAT_NONE /* No replacement should be performed. */
175 static bool format_command(char dst[], const char *src[], enum format_flags flags);
176 static bool format_argv(const char *dst[], const char *src[], enum format_flags flags);
185 set_from_int_map(struct int_map *map, size_t map_size,
186 int *value, const char *name, int namelen)
191 for (i = 0; i < map_size; i++)
192 if (namelen == map[i].namelen &&
193 !strncasecmp(name, map[i].name, namelen)) {
194 *value = map[i].value;
207 string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
209 if (srclen > dstlen - 1)
212 strncpy(dst, src, srclen);
216 /* Shorthands for safely copying into a fixed buffer. */
218 #define string_copy(dst, src) \
219 string_ncopy_do(dst, sizeof(dst), src, sizeof(src))
221 #define string_ncopy(dst, src, srclen) \
222 string_ncopy_do(dst, sizeof(dst), src, srclen)
224 #define string_copy_rev(dst, src) \
225 string_ncopy_do(dst, SIZEOF_REV, src, SIZEOF_REV - 1)
227 #define string_add(dst, from, src) \
228 string_ncopy_do(dst + (from), sizeof(dst) - (from), src, sizeof(src))
231 chomp_string(char *name)
235 while (isspace(*name))
238 namelen = strlen(name) - 1;
239 while (namelen > 0 && isspace(name[namelen]))
246 string_nformat(char *buf, size_t bufsize, size_t *bufpos, const char *fmt, ...)
249 size_t pos = bufpos ? *bufpos : 0;
252 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
258 return pos >= bufsize ? FALSE : TRUE;
261 #define string_format(buf, fmt, args...) \
262 string_nformat(buf, sizeof(buf), NULL, fmt, args)
264 #define string_format_from(buf, from, fmt, args...) \
265 string_nformat(buf, sizeof(buf), from, fmt, args)
268 string_enum_compare(const char *str1, const char *str2, int len)
272 #define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
274 /* Diff-Header == DIFF_HEADER */
275 for (i = 0; i < len; i++) {
276 if (toupper(str1[i]) == toupper(str2[i]))
279 if (string_enum_sep(str1[i]) &&
280 string_enum_sep(str2[i]))
283 return str1[i] - str2[i];
289 #define prefixcmp(str1, str2) \
290 strncmp(str1, str2, STRING_SIZE(str2))
293 suffixcmp(const char *str, int slen, const char *suffix)
295 size_t len = slen >= 0 ? slen : strlen(str);
296 size_t suffixlen = strlen(suffix);
298 return suffixlen < len ? strcmp(str + len - suffixlen, suffix) : -1;
303 * NOTE: The following is a slightly modified copy of the git project's shell
304 * quoting routines found in the quote.c file.
306 * Help to copy the thing properly quoted for the shell safety. any single
307 * quote is replaced with '\'', any exclamation point is replaced with '\!',
308 * and the whole thing is enclosed in a
311 * original sq_quote result
312 * name ==> name ==> 'name'
313 * a b ==> a b ==> 'a b'
314 * a'b ==> a'\''b ==> 'a'\''b'
315 * a!b ==> a'\!'b ==> 'a'\!'b'
319 sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
323 #define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0)
326 while ((c = *src++)) {
327 if (c == '\'' || c == '!') {
338 if (bufsize < SIZEOF_STR)
345 argv_from_string(const char *argv[SIZEOF_ARG], int *argc, char *cmd)
349 while (*cmd && *argc < SIZEOF_ARG && (valuelen = strcspn(cmd, " \t"))) {
350 bool advance = cmd[valuelen] != 0;
353 argv[(*argc)++] = chomp_string(cmd);
354 cmd += valuelen + advance;
357 if (*argc < SIZEOF_ARG)
359 return *argc < SIZEOF_ARG;
364 * Executing external commands.
368 IO_FD, /* File descriptor based IO. */
369 IO_RD, /* Read only fork+exec IO. */
370 IO_WR, /* Write only fork+exec IO. */
374 enum io_type type; /* The requested type of pipe. */
375 FILE *pipe; /* Pipe for reading or writing. */
376 int error; /* Error status. */
377 char sh[SIZEOF_STR]; /* Shell command buffer. */
378 char *buf; /* Read/write buffer. */
379 size_t bufalloc; /* Allocated buffer size. */
383 reset_io(struct io *io)
392 init_io(struct io *io, enum io_type type)
399 init_io_fd(struct io *io, FILE *pipe)
403 return io->pipe != NULL;
407 done_io(struct io *io)
410 if (io->type == IO_FD)
419 start_io(struct io *io)
421 io->pipe = popen(io->sh, io->type == IO_RD ? "r" : "w");
422 return io->pipe != NULL;
426 run_io(struct io *io, enum io_type type, const char *cmd)
429 string_ncopy(io->sh, cmd, strlen(cmd));
434 run_io_format(struct io *io, const char *cmd, ...)
441 if (vsnprintf(io->sh, sizeof(io->sh), cmd, args) >= sizeof(io->sh))
445 return io->sh[0] ? start_io(io) : FALSE;
449 io_eof(struct io *io)
451 return feof(io->pipe);
455 io_error(struct io *io)
461 io_strerror(struct io *io)
463 return strerror(io->error);
467 io_gets(struct io *io)
470 io->buf = malloc(BUFSIZ);
473 io->bufalloc = BUFSIZ;
476 if (!fgets(io->buf, io->bufalloc, io->pipe)) {
477 if (ferror(io->pipe))
491 /* XXX: Keep the view request first and in sync with views[]. */ \
492 REQ_GROUP("View switching") \
493 REQ_(VIEW_MAIN, "Show main view"), \
494 REQ_(VIEW_DIFF, "Show diff view"), \
495 REQ_(VIEW_LOG, "Show log view"), \
496 REQ_(VIEW_TREE, "Show tree view"), \
497 REQ_(VIEW_BLOB, "Show blob view"), \
498 REQ_(VIEW_BLAME, "Show blame view"), \
499 REQ_(VIEW_HELP, "Show help page"), \
500 REQ_(VIEW_PAGER, "Show pager view"), \
501 REQ_(VIEW_STATUS, "Show status view"), \
502 REQ_(VIEW_STAGE, "Show stage view"), \
504 REQ_GROUP("View manipulation") \
505 REQ_(ENTER, "Enter current line and scroll"), \
506 REQ_(NEXT, "Move to next"), \
507 REQ_(PREVIOUS, "Move to previous"), \
508 REQ_(VIEW_NEXT, "Move focus to next view"), \
509 REQ_(REFRESH, "Reload and refresh"), \
510 REQ_(MAXIMIZE, "Maximize the current view"), \
511 REQ_(VIEW_CLOSE, "Close the current view"), \
512 REQ_(QUIT, "Close all views and quit"), \
514 REQ_GROUP("View specific requests") \
515 REQ_(STATUS_UPDATE, "Update file status"), \
516 REQ_(STATUS_REVERT, "Revert file changes"), \
517 REQ_(STATUS_MERGE, "Merge file using external tool"), \
518 REQ_(STAGE_NEXT, "Find next chunk to stage"), \
519 REQ_(TREE_PARENT, "Switch to parent directory in tree view"), \
521 REQ_GROUP("Cursor navigation") \
522 REQ_(MOVE_UP, "Move cursor one line up"), \
523 REQ_(MOVE_DOWN, "Move cursor one line down"), \
524 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
525 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
526 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
527 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
529 REQ_GROUP("Scrolling") \
530 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
531 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
532 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
533 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
535 REQ_GROUP("Searching") \
536 REQ_(SEARCH, "Search the view"), \
537 REQ_(SEARCH_BACK, "Search backwards in the view"), \
538 REQ_(FIND_NEXT, "Find next search match"), \
539 REQ_(FIND_PREV, "Find previous search match"), \
541 REQ_GROUP("Option manipulation") \
542 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
543 REQ_(TOGGLE_DATE, "Toggle date display"), \
544 REQ_(TOGGLE_AUTHOR, "Toggle author display"), \
545 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"), \
546 REQ_(TOGGLE_REFS, "Toggle reference display (tags/branches)"), \
549 REQ_(PROMPT, "Bring up the prompt"), \
550 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
551 REQ_(SCREEN_RESIZE, "Resize the screen"), \
552 REQ_(SHOW_VERSION, "Show version information"), \
553 REQ_(STOP_LOADING, "Stop all loading views"), \
554 REQ_(EDIT, "Open in editor"), \
555 REQ_(NONE, "Do nothing")
558 /* User action requests. */
560 #define REQ_GROUP(help)
561 #define REQ_(req, help) REQ_##req
563 /* Offset all requests to avoid conflicts with ncurses getch values. */
564 REQ_OFFSET = KEY_MAX + 1,
571 struct request_info {
572 enum request request;
578 static struct request_info req_info[] = {
579 #define REQ_GROUP(help) { 0, NULL, 0, (help) },
580 #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
587 get_request(const char *name)
589 int namelen = strlen(name);
592 for (i = 0; i < ARRAY_SIZE(req_info); i++)
593 if (req_info[i].namelen == namelen &&
594 !string_enum_compare(req_info[i].name, name, namelen))
595 return req_info[i].request;
605 static const char usage[] =
606 "tig " TIG_VERSION " (" __DATE__ ")\n"
608 "Usage: tig [options] [revs] [--] [paths]\n"
609 " or: tig show [options] [revs] [--] [paths]\n"
610 " or: tig blame [rev] path\n"
612 " or: tig < [git command output]\n"
615 " -v, --version Show version and exit\n"
616 " -h, --help Show help message and exit";
618 /* Option and state variables. */
619 static bool opt_date = TRUE;
620 static bool opt_author = TRUE;
621 static bool opt_line_number = FALSE;
622 static bool opt_line_graphics = TRUE;
623 static bool opt_rev_graph = FALSE;
624 static bool opt_show_refs = TRUE;
625 static int opt_num_interval = NUMBER_INTERVAL;
626 static int opt_tab_size = TAB_SIZE;
627 static int opt_author_cols = AUTHOR_COLS-1;
628 static char opt_cmd[SIZEOF_STR] = "";
629 static char opt_path[SIZEOF_STR] = "";
630 static char opt_file[SIZEOF_STR] = "";
631 static char opt_ref[SIZEOF_REF] = "";
632 static char opt_head[SIZEOF_REF] = "";
633 static char opt_head_rev[SIZEOF_REV] = "";
634 static char opt_remote[SIZEOF_REF] = "";
635 static FILE *opt_pipe = NULL;
636 static char opt_encoding[20] = "UTF-8";
637 static bool opt_utf8 = TRUE;
638 static char opt_codeset[20] = "UTF-8";
639 static iconv_t opt_iconv = ICONV_NONE;
640 static char opt_search[SIZEOF_STR] = "";
641 static char opt_cdup[SIZEOF_STR] = "";
642 static char opt_git_dir[SIZEOF_STR] = "";
643 static signed char opt_is_inside_work_tree = -1; /* set to TRUE or FALSE */
644 static char opt_editor[SIZEOF_STR] = "";
645 static FILE *opt_tty = NULL;
647 #define is_initial_commit() (!*opt_head_rev)
648 #define is_head_commit(rev) (!strcmp((rev), "HEAD") || !strcmp(opt_head_rev, (rev)))
651 parse_options(int argc, const char *argv[])
653 enum request request = REQ_VIEW_MAIN;
655 const char *subcommand;
656 bool seen_dashdash = FALSE;
659 if (!isatty(STDIN_FILENO)) {
661 return REQ_VIEW_PAGER;
665 return REQ_VIEW_MAIN;
667 subcommand = argv[1];
668 if (!strcmp(subcommand, "status") || !strcmp(subcommand, "-S")) {
669 if (!strcmp(subcommand, "-S"))
670 warn("`-S' has been deprecated; use `tig status' instead");
672 warn("ignoring arguments after `%s'", subcommand);
673 return REQ_VIEW_STATUS;
675 } else if (!strcmp(subcommand, "blame")) {
676 if (argc <= 2 || argc > 4)
677 die("invalid number of options to blame\n\n%s", usage);
681 string_ncopy(opt_ref, argv[i], strlen(argv[i]));
685 string_ncopy(opt_file, argv[i], strlen(argv[i]));
686 return REQ_VIEW_BLAME;
688 } else if (!strcmp(subcommand, "show")) {
689 request = REQ_VIEW_DIFF;
691 } else if (!strcmp(subcommand, "log") || !strcmp(subcommand, "diff")) {
692 request = subcommand[0] == 'l' ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
693 warn("`tig %s' has been deprecated", subcommand);
700 /* XXX: This is vulnerable to the user overriding
701 * options required for the main view parser. */
702 string_copy(opt_cmd, TIG_MAIN_BASE);
704 string_format(opt_cmd, "git %s", subcommand);
706 buf_size = strlen(opt_cmd);
708 for (i = 1 + !!subcommand; i < argc; i++) {
709 const char *opt = argv[i];
711 if (seen_dashdash || !strcmp(opt, "--")) {
712 seen_dashdash = TRUE;
714 } else if (!strcmp(opt, "-v") || !strcmp(opt, "--version")) {
715 printf("tig version %s\n", TIG_VERSION);
718 } else if (!strcmp(opt, "-h") || !strcmp(opt, "--help")) {
719 printf("%s\n", usage);
723 opt_cmd[buf_size++] = ' ';
724 buf_size = sq_quote(opt_cmd, buf_size, opt);
725 if (buf_size >= sizeof(opt_cmd))
726 die("command too long");
729 opt_cmd[buf_size] = 0;
736 * Line-oriented content detection.
740 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
741 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
742 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
743 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
744 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
745 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
746 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
747 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
748 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
749 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
750 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
751 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
752 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
753 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
754 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
755 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
756 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
757 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
758 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
759 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
760 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
761 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
762 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
763 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
764 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
765 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
766 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
767 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
768 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
769 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
770 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
771 LINE(DELIMITER, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
772 LINE(DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
773 LINE(LINE_NUMBER, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
774 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
775 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
776 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
777 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
778 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
779 LINE(MAIN_LOCAL_TAG,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
780 LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \
781 LINE(MAIN_TRACKED, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
782 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
783 LINE(MAIN_HEAD, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
784 LINE(MAIN_REVGRAPH,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
785 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
786 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
787 LINE(STAT_HEAD, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \
788 LINE(STAT_SECTION, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
789 LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
790 LINE(STAT_STAGED, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
791 LINE(STAT_UNSTAGED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
792 LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
793 LINE(BLAME_ID, "", COLOR_MAGENTA, COLOR_DEFAULT, 0)
796 #define LINE(type, line, fg, bg, attr) \
804 const char *name; /* Option name. */
805 int namelen; /* Size of option name. */
806 const char *line; /* The start of line to match. */
807 int linelen; /* Size of string to match. */
808 int fg, bg, attr; /* Color and text attributes for the lines. */
811 static struct line_info line_info[] = {
812 #define LINE(type, line, fg, bg, attr) \
813 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
818 static enum line_type
819 get_line_type(const char *line)
821 int linelen = strlen(line);
824 for (type = 0; type < ARRAY_SIZE(line_info); type++)
825 /* Case insensitive search matches Signed-off-by lines better. */
826 if (linelen >= line_info[type].linelen &&
827 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
834 get_line_attr(enum line_type type)
836 assert(type < ARRAY_SIZE(line_info));
837 return COLOR_PAIR(type) | line_info[type].attr;
840 static struct line_info *
841 get_line_info(const char *name)
843 size_t namelen = strlen(name);
846 for (type = 0; type < ARRAY_SIZE(line_info); type++)
847 if (namelen == line_info[type].namelen &&
848 !string_enum_compare(line_info[type].name, name, namelen))
849 return &line_info[type];
857 int default_bg = line_info[LINE_DEFAULT].bg;
858 int default_fg = line_info[LINE_DEFAULT].fg;
863 if (assume_default_colors(default_fg, default_bg) == ERR) {
864 default_bg = COLOR_BLACK;
865 default_fg = COLOR_WHITE;
868 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
869 struct line_info *info = &line_info[type];
870 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
871 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
873 init_pair(type, fg, bg);
881 unsigned int selected:1;
882 unsigned int dirty:1;
884 void *data; /* User data */
894 enum request request;
897 static struct keybinding default_keybindings[] = {
899 { 'm', REQ_VIEW_MAIN },
900 { 'd', REQ_VIEW_DIFF },
901 { 'l', REQ_VIEW_LOG },
902 { 't', REQ_VIEW_TREE },
903 { 'f', REQ_VIEW_BLOB },
904 { 'B', REQ_VIEW_BLAME },
905 { 'p', REQ_VIEW_PAGER },
906 { 'h', REQ_VIEW_HELP },
907 { 'S', REQ_VIEW_STATUS },
908 { 'c', REQ_VIEW_STAGE },
910 /* View manipulation */
911 { 'q', REQ_VIEW_CLOSE },
912 { KEY_TAB, REQ_VIEW_NEXT },
913 { KEY_RETURN, REQ_ENTER },
914 { KEY_UP, REQ_PREVIOUS },
915 { KEY_DOWN, REQ_NEXT },
916 { 'R', REQ_REFRESH },
917 { KEY_F(5), REQ_REFRESH },
918 { 'O', REQ_MAXIMIZE },
920 /* Cursor navigation */
921 { 'k', REQ_MOVE_UP },
922 { 'j', REQ_MOVE_DOWN },
923 { KEY_HOME, REQ_MOVE_FIRST_LINE },
924 { KEY_END, REQ_MOVE_LAST_LINE },
925 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
926 { ' ', REQ_MOVE_PAGE_DOWN },
927 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
928 { 'b', REQ_MOVE_PAGE_UP },
929 { '-', REQ_MOVE_PAGE_UP },
932 { KEY_IC, REQ_SCROLL_LINE_UP },
933 { KEY_DC, REQ_SCROLL_LINE_DOWN },
934 { 'w', REQ_SCROLL_PAGE_UP },
935 { 's', REQ_SCROLL_PAGE_DOWN },
939 { '?', REQ_SEARCH_BACK },
940 { 'n', REQ_FIND_NEXT },
941 { 'N', REQ_FIND_PREV },
945 { 'z', REQ_STOP_LOADING },
946 { 'v', REQ_SHOW_VERSION },
947 { 'r', REQ_SCREEN_REDRAW },
948 { '.', REQ_TOGGLE_LINENO },
949 { 'D', REQ_TOGGLE_DATE },
950 { 'A', REQ_TOGGLE_AUTHOR },
951 { 'g', REQ_TOGGLE_REV_GRAPH },
952 { 'F', REQ_TOGGLE_REFS },
954 { 'u', REQ_STATUS_UPDATE },
955 { '!', REQ_STATUS_REVERT },
956 { 'M', REQ_STATUS_MERGE },
957 { '@', REQ_STAGE_NEXT },
958 { ',', REQ_TREE_PARENT },
961 /* Using the ncurses SIGWINCH handler. */
962 { KEY_RESIZE, REQ_SCREEN_RESIZE },
965 #define KEYMAP_INFO \
979 #define KEYMAP_(name) KEYMAP_##name
984 static struct int_map keymap_table[] = {
985 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
990 #define set_keymap(map, name) \
991 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
993 struct keybinding_table {
994 struct keybinding *data;
998 static struct keybinding_table keybindings[ARRAY_SIZE(keymap_table)];
1001 add_keybinding(enum keymap keymap, enum request request, int key)
1003 struct keybinding_table *table = &keybindings[keymap];
1005 table->data = realloc(table->data, (table->size + 1) * sizeof(*table->data));
1007 die("Failed to allocate keybinding");
1008 table->data[table->size].alias = key;
1009 table->data[table->size++].request = request;
1012 /* Looks for a key binding first in the given map, then in the generic map, and
1013 * lastly in the default keybindings. */
1015 get_keybinding(enum keymap keymap, int key)
1019 for (i = 0; i < keybindings[keymap].size; i++)
1020 if (keybindings[keymap].data[i].alias == key)
1021 return keybindings[keymap].data[i].request;
1023 for (i = 0; i < keybindings[KEYMAP_GENERIC].size; i++)
1024 if (keybindings[KEYMAP_GENERIC].data[i].alias == key)
1025 return keybindings[KEYMAP_GENERIC].data[i].request;
1027 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
1028 if (default_keybindings[i].alias == key)
1029 return default_keybindings[i].request;
1031 return (enum request) key;
1040 static struct key key_table[] = {
1041 { "Enter", KEY_RETURN },
1043 { "Backspace", KEY_BACKSPACE },
1045 { "Escape", KEY_ESC },
1046 { "Left", KEY_LEFT },
1047 { "Right", KEY_RIGHT },
1049 { "Down", KEY_DOWN },
1050 { "Insert", KEY_IC },
1051 { "Delete", KEY_DC },
1053 { "Home", KEY_HOME },
1055 { "PageUp", KEY_PPAGE },
1056 { "PageDown", KEY_NPAGE },
1066 { "F10", KEY_F(10) },
1067 { "F11", KEY_F(11) },
1068 { "F12", KEY_F(12) },
1072 get_key_value(const char *name)
1076 for (i = 0; i < ARRAY_SIZE(key_table); i++)
1077 if (!strcasecmp(key_table[i].name, name))
1078 return key_table[i].value;
1080 if (strlen(name) == 1 && isprint(*name))
1087 get_key_name(int key_value)
1089 static char key_char[] = "'X'";
1090 const char *seq = NULL;
1093 for (key = 0; key < ARRAY_SIZE(key_table); key++)
1094 if (key_table[key].value == key_value)
1095 seq = key_table[key].name;
1099 isprint(key_value)) {
1100 key_char[1] = (char) key_value;
1104 return seq ? seq : "(no key)";
1108 get_key(enum request request)
1110 static char buf[BUFSIZ];
1117 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
1118 struct keybinding *keybinding = &default_keybindings[i];
1120 if (keybinding->request != request)
1123 if (!string_format_from(buf, &pos, "%s%s", sep,
1124 get_key_name(keybinding->alias)))
1125 return "Too many keybindings!";
1132 struct run_request {
1135 const char *argv[SIZEOF_ARG];
1138 static struct run_request *run_request;
1139 static size_t run_requests;
1142 add_run_request(enum keymap keymap, int key, int argc, const char **argv)
1144 struct run_request *req;
1146 if (argc >= ARRAY_SIZE(req->argv) - 1)
1149 req = realloc(run_request, (run_requests + 1) * sizeof(*run_request));
1154 req = &run_request[run_requests];
1155 req->keymap = keymap;
1157 req->argv[0] = NULL;
1159 if (!format_argv(req->argv, argv, FORMAT_NONE))
1162 return REQ_NONE + ++run_requests;
1165 static struct run_request *
1166 get_run_request(enum request request)
1168 if (request <= REQ_NONE)
1170 return &run_request[request - REQ_NONE - 1];
1174 add_builtin_run_requests(void)
1176 const char *cherry_pick[] = { "git", "cherry-pick", "%(commit)", NULL };
1177 const char *gc[] = { "git", "gc", NULL };
1184 { KEYMAP_MAIN, 'C', ARRAY_SIZE(cherry_pick) - 1, cherry_pick },
1185 { KEYMAP_GENERIC, 'G', ARRAY_SIZE(gc) - 1, gc },
1189 for (i = 0; i < ARRAY_SIZE(reqs); i++) {
1192 req = add_run_request(reqs[i].keymap, reqs[i].key, reqs[i].argc, reqs[i].argv);
1193 if (req != REQ_NONE)
1194 add_keybinding(reqs[i].keymap, req, reqs[i].key);
1199 * User config file handling.
1202 static struct int_map color_map[] = {
1203 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
1215 #define set_color(color, name) \
1216 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
1218 static struct int_map attr_map[] = {
1219 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
1226 ATTR_MAP(UNDERLINE),
1229 #define set_attribute(attr, name) \
1230 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
1232 static int config_lineno;
1233 static bool config_errors;
1234 static const char *config_msg;
1236 /* Wants: object fgcolor bgcolor [attr] */
1238 option_color_command(int argc, const char *argv[])
1240 struct line_info *info;
1242 if (argc != 3 && argc != 4) {
1243 config_msg = "Wrong number of arguments given to color command";
1247 info = get_line_info(argv[0]);
1249 if (!string_enum_compare(argv[0], "main-delim", strlen("main-delim"))) {
1250 info = get_line_info("delimiter");
1252 } else if (!string_enum_compare(argv[0], "main-date", strlen("main-date"))) {
1253 info = get_line_info("date");
1256 config_msg = "Unknown color name";
1261 if (set_color(&info->fg, argv[1]) == ERR ||
1262 set_color(&info->bg, argv[2]) == ERR) {
1263 config_msg = "Unknown color";
1267 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
1268 config_msg = "Unknown attribute";
1275 static bool parse_bool(const char *s)
1277 return (!strcmp(s, "1") || !strcmp(s, "true") ||
1278 !strcmp(s, "yes")) ? TRUE : FALSE;
1282 parse_int(const char *s, int default_value, int min, int max)
1284 int value = atoi(s);
1286 return (value < min || value > max) ? default_value : value;
1289 /* Wants: name = value */
1291 option_set_command(int argc, const char *argv[])
1294 config_msg = "Wrong number of arguments given to set command";
1298 if (strcmp(argv[1], "=")) {
1299 config_msg = "No value assigned";
1303 if (!strcmp(argv[0], "show-author")) {
1304 opt_author = parse_bool(argv[2]);
1308 if (!strcmp(argv[0], "show-date")) {
1309 opt_date = parse_bool(argv[2]);
1313 if (!strcmp(argv[0], "show-rev-graph")) {
1314 opt_rev_graph = parse_bool(argv[2]);
1318 if (!strcmp(argv[0], "show-refs")) {
1319 opt_show_refs = parse_bool(argv[2]);
1323 if (!strcmp(argv[0], "show-line-numbers")) {
1324 opt_line_number = parse_bool(argv[2]);
1328 if (!strcmp(argv[0], "line-graphics")) {
1329 opt_line_graphics = parse_bool(argv[2]);
1333 if (!strcmp(argv[0], "line-number-interval")) {
1334 opt_num_interval = parse_int(argv[2], opt_num_interval, 1, 1024);
1338 if (!strcmp(argv[0], "author-width")) {
1339 opt_author_cols = parse_int(argv[2], opt_author_cols, 0, 1024);
1343 if (!strcmp(argv[0], "tab-size")) {
1344 opt_tab_size = parse_int(argv[2], opt_tab_size, 1, 1024);
1348 if (!strcmp(argv[0], "commit-encoding")) {
1349 const char *arg = argv[2];
1350 int arglen = strlen(arg);
1355 if (arglen == 1 || arg[arglen - 1] != arg[0]) {
1356 config_msg = "Unmatched quotation";
1359 arg += 1; arglen -= 2;
1361 string_ncopy(opt_encoding, arg, strlen(arg));
1366 config_msg = "Unknown variable name";
1370 /* Wants: mode request key */
1372 option_bind_command(int argc, const char *argv[])
1374 enum request request;
1379 config_msg = "Wrong number of arguments given to bind command";
1383 if (set_keymap(&keymap, argv[0]) == ERR) {
1384 config_msg = "Unknown key map";
1388 key = get_key_value(argv[1]);
1390 config_msg = "Unknown key";
1394 request = get_request(argv[2]);
1395 if (request == REQ_NONE) {
1396 const char *obsolete[] = { "cherry-pick" };
1397 size_t namelen = strlen(argv[2]);
1400 for (i = 0; i < ARRAY_SIZE(obsolete); i++) {
1401 if (namelen == strlen(obsolete[i]) &&
1402 !string_enum_compare(obsolete[i], argv[2], namelen)) {
1403 config_msg = "Obsolete request name";
1408 if (request == REQ_NONE && *argv[2]++ == '!')
1409 request = add_run_request(keymap, key, argc - 2, argv + 2);
1410 if (request == REQ_NONE) {
1411 config_msg = "Unknown request name";
1415 add_keybinding(keymap, request, key);
1421 set_option(const char *opt, char *value)
1423 const char *argv[SIZEOF_ARG];
1426 if (!argv_from_string(argv, &argc, value)) {
1427 config_msg = "Too many option arguments";
1431 if (!strcmp(opt, "color"))
1432 return option_color_command(argc, argv);
1434 if (!strcmp(opt, "set"))
1435 return option_set_command(argc, argv);
1437 if (!strcmp(opt, "bind"))
1438 return option_bind_command(argc, argv);
1440 config_msg = "Unknown option command";
1445 read_option(char *opt, size_t optlen, char *value, size_t valuelen)
1450 config_msg = "Internal error";
1452 /* Check for comment markers, since read_properties() will
1453 * only ensure opt and value are split at first " \t". */
1454 optlen = strcspn(opt, "#");
1458 if (opt[optlen] != 0) {
1459 config_msg = "No option value";
1463 /* Look for comment endings in the value. */
1464 size_t len = strcspn(value, "#");
1466 if (len < valuelen) {
1468 value[valuelen] = 0;
1471 status = set_option(opt, value);
1474 if (status == ERR) {
1475 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1476 config_lineno, (int) optlen, opt, config_msg);
1477 config_errors = TRUE;
1480 /* Always keep going if errors are encountered. */
1485 load_option_file(const char *path)
1489 /* It's ok that the file doesn't exist. */
1490 file = fopen(path, "r");
1495 config_errors = FALSE;
1497 if (read_properties(file, " \t", read_option) == ERR ||
1498 config_errors == TRUE)
1499 fprintf(stderr, "Errors while loading %s.\n", path);
1505 const char *home = getenv("HOME");
1506 const char *tigrc_user = getenv("TIGRC_USER");
1507 const char *tigrc_system = getenv("TIGRC_SYSTEM");
1508 char buf[SIZEOF_STR];
1510 add_builtin_run_requests();
1512 if (!tigrc_system) {
1513 if (!string_format(buf, "%s/tigrc", SYSCONFDIR))
1517 load_option_file(tigrc_system);
1520 if (!home || !string_format(buf, "%s/.tigrc", home))
1524 load_option_file(tigrc_user);
1537 /* The display array of active views and the index of the current view. */
1538 static struct view *display[2];
1539 static unsigned int current_view;
1541 /* Reading from the prompt? */
1542 static bool input_mode = FALSE;
1544 #define foreach_displayed_view(view, i) \
1545 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1547 #define displayed_views() (display[1] != NULL ? 2 : 1)
1549 /* Current head and commit ID */
1550 static char ref_blob[SIZEOF_REF] = "";
1551 static char ref_commit[SIZEOF_REF] = "HEAD";
1552 static char ref_head[SIZEOF_REF] = "HEAD";
1555 const char *name; /* View name */
1556 const char *cmd_fmt; /* Default command line format */
1557 const char *cmd_env; /* Command line set via environment */
1558 const char *id; /* Points to either of ref_{head,commit,blob} */
1560 struct view_ops *ops; /* View operations */
1562 enum keymap keymap; /* What keymap does this view have */
1563 bool git_dir; /* Whether the view requires a git directory. */
1565 char ref[SIZEOF_REF]; /* Hovered commit reference */
1566 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1568 int height, width; /* The width and height of the main window */
1569 WINDOW *win; /* The main window */
1570 WINDOW *title; /* The title window living below the main window */
1573 unsigned long offset; /* Offset of the window top */
1574 unsigned long lineno; /* Current line number */
1577 char grep[SIZEOF_STR]; /* Search string */
1578 regex_t *regex; /* Pre-compiled regex */
1580 /* If non-NULL, points to the view that opened this view. If this view
1581 * is closed tig will switch back to the parent view. */
1582 struct view *parent;
1585 size_t lines; /* Total number of lines */
1586 struct line *line; /* Line index */
1587 size_t line_alloc; /* Total number of allocated lines */
1588 size_t line_size; /* Total number of used lines */
1589 unsigned int digits; /* Number of digits in the lines member. */
1592 struct line *curline; /* Line currently being drawn. */
1593 enum line_type curtype; /* Attribute currently used for drawing. */
1594 unsigned long col; /* Column when drawing. */
1603 /* What type of content being displayed. Used in the title bar. */
1605 /* Open and reads in all view content. */
1606 bool (*open)(struct view *view);
1607 /* Read one line; updates view->line. */
1608 bool (*read)(struct view *view, char *data);
1609 /* Draw one line; @lineno must be < view->height. */
1610 bool (*draw)(struct view *view, struct line *line, unsigned int lineno);
1611 /* Depending on view handle a special requests. */
1612 enum request (*request)(struct view *view, enum request request, struct line *line);
1613 /* Search for regex in a line. */
1614 bool (*grep)(struct view *view, struct line *line);
1616 void (*select)(struct view *view, struct line *line);
1619 static struct view_ops blame_ops;
1620 static struct view_ops blob_ops;
1621 static struct view_ops help_ops;
1622 static struct view_ops log_ops;
1623 static struct view_ops main_ops;
1624 static struct view_ops pager_ops;
1625 static struct view_ops stage_ops;
1626 static struct view_ops status_ops;
1627 static struct view_ops tree_ops;
1629 #define VIEW_STR(name, cmd, env, ref, ops, map, git) \
1630 { name, cmd, #env, ref, ops, map, git }
1632 #define VIEW_(id, name, ops, git, ref) \
1633 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id, git)
1636 static struct view views[] = {
1637 VIEW_(MAIN, "main", &main_ops, TRUE, ref_head),
1638 VIEW_(DIFF, "diff", &pager_ops, TRUE, ref_commit),
1639 VIEW_(LOG, "log", &log_ops, TRUE, ref_head),
1640 VIEW_(TREE, "tree", &tree_ops, TRUE, ref_commit),
1641 VIEW_(BLOB, "blob", &blob_ops, TRUE, ref_blob),
1642 VIEW_(BLAME, "blame", &blame_ops, TRUE, ref_commit),
1643 VIEW_(HELP, "help", &help_ops, FALSE, ""),
1644 VIEW_(PAGER, "pager", &pager_ops, FALSE, "stdin"),
1645 VIEW_(STATUS, "status", &status_ops, TRUE, ""),
1646 VIEW_(STAGE, "stage", &stage_ops, TRUE, ""),
1649 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1650 #define VIEW_REQ(view) ((view) - views + REQ_OFFSET + 1)
1652 #define foreach_view(view, i) \
1653 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1655 #define view_is_displayed(view) \
1656 (view == display[0] || view == display[1])
1663 static int line_graphics[] = {
1664 /* LINE_GRAPHIC_VLINE: */ '|'
1668 set_view_attr(struct view *view, enum line_type type)
1670 if (!view->curline->selected && view->curtype != type) {
1671 wattrset(view->win, get_line_attr(type));
1672 wchgat(view->win, -1, 0, type, NULL);
1673 view->curtype = type;
1678 draw_chars(struct view *view, enum line_type type, const char *string,
1679 int max_len, bool use_tilde)
1683 int trimmed = FALSE;
1689 len = utf8_length(string, &col, max_len, &trimmed, use_tilde);
1691 col = len = strlen(string);
1692 if (len > max_len) {
1696 col = len = max_len;
1701 set_view_attr(view, type);
1702 waddnstr(view->win, string, len);
1703 if (trimmed && use_tilde) {
1704 set_view_attr(view, LINE_DELIMITER);
1705 waddch(view->win, '~');
1713 draw_space(struct view *view, enum line_type type, int max, int spaces)
1715 static char space[] = " ";
1718 spaces = MIN(max, spaces);
1720 while (spaces > 0) {
1721 int len = MIN(spaces, sizeof(space) - 1);
1723 col += draw_chars(view, type, space, spaces, FALSE);
1731 draw_lineno(struct view *view, unsigned int lineno)
1734 int digits3 = view->digits < 3 ? 3 : view->digits;
1735 int max_number = MIN(digits3, STRING_SIZE(number));
1736 int max = view->width - view->col;
1739 if (max < max_number)
1742 lineno += view->offset + 1;
1743 if (lineno == 1 || (lineno % opt_num_interval) == 0) {
1744 static char fmt[] = "%1ld";
1746 if (view->digits <= 9)
1747 fmt[1] = '0' + digits3;
1749 if (!string_format(number, fmt, lineno))
1751 col = draw_chars(view, LINE_LINE_NUMBER, number, max_number, TRUE);
1753 col = draw_space(view, LINE_LINE_NUMBER, max_number, max_number);
1757 set_view_attr(view, LINE_DEFAULT);
1758 waddch(view->win, line_graphics[LINE_GRAPHIC_VLINE]);
1763 col += draw_space(view, LINE_DEFAULT, max - col, 1);
1766 return view->width - view->col <= 0;
1770 draw_text(struct view *view, enum line_type type, const char *string, bool trim)
1772 view->col += draw_chars(view, type, string, view->width - view->col, trim);
1773 return view->width - view->col <= 0;
1777 draw_graphic(struct view *view, enum line_type type, chtype graphic[], size_t size)
1779 int max = view->width - view->col;
1785 set_view_attr(view, type);
1786 /* Using waddch() instead of waddnstr() ensures that
1787 * they'll be rendered correctly for the cursor line. */
1788 for (i = 0; i < size; i++)
1789 waddch(view->win, graphic[i]);
1793 waddch(view->win, ' ');
1797 return view->width - view->col <= 0;
1801 draw_field(struct view *view, enum line_type type, const char *text, int len, bool trim)
1803 int max = MIN(view->width - view->col, len);
1807 col = draw_chars(view, type, text, max - 1, trim);
1809 col = draw_space(view, type, max - 1, max - 1);
1811 view->col += col + draw_space(view, LINE_DEFAULT, max - col, max - col);
1812 return view->width - view->col <= 0;
1816 draw_date(struct view *view, struct tm *time)
1818 char buf[DATE_COLS];
1823 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, time);
1824 date = timelen ? buf : NULL;
1826 return draw_field(view, LINE_DATE, date, DATE_COLS, FALSE);
1830 draw_view_line(struct view *view, unsigned int lineno)
1833 bool selected = (view->offset + lineno == view->lineno);
1836 assert(view_is_displayed(view));
1838 if (view->offset + lineno >= view->lines)
1841 line = &view->line[view->offset + lineno];
1843 wmove(view->win, lineno, 0);
1845 view->curline = line;
1846 view->curtype = LINE_NONE;
1847 line->selected = FALSE;
1850 set_view_attr(view, LINE_CURSOR);
1851 line->selected = TRUE;
1852 view->ops->select(view, line);
1853 } else if (line->selected) {
1854 wclrtoeol(view->win);
1857 scrollok(view->win, FALSE);
1858 draw_ok = view->ops->draw(view, line, lineno);
1859 scrollok(view->win, TRUE);
1865 redraw_view_dirty(struct view *view)
1870 for (lineno = 0; lineno < view->height; lineno++) {
1871 struct line *line = &view->line[view->offset + lineno];
1877 if (!draw_view_line(view, lineno))
1883 redrawwin(view->win);
1885 wnoutrefresh(view->win);
1887 wrefresh(view->win);
1891 redraw_view_from(struct view *view, int lineno)
1893 assert(0 <= lineno && lineno < view->height);
1895 for (; lineno < view->height; lineno++) {
1896 if (!draw_view_line(view, lineno))
1900 redrawwin(view->win);
1902 wnoutrefresh(view->win);
1904 wrefresh(view->win);
1908 redraw_view(struct view *view)
1911 redraw_view_from(view, 0);
1916 update_view_title(struct view *view)
1918 char buf[SIZEOF_STR];
1919 char state[SIZEOF_STR];
1920 size_t bufpos = 0, statelen = 0;
1922 assert(view_is_displayed(view));
1924 if (view != VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) {
1925 unsigned int view_lines = view->offset + view->height;
1926 unsigned int lines = view->lines
1927 ? MIN(view_lines, view->lines) * 100 / view->lines
1930 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1937 time_t secs = time(NULL) - view->start_time;
1939 /* Three git seconds are a long time ... */
1941 string_format_from(state, &statelen, " %lds", secs);
1945 string_format_from(buf, &bufpos, "[%s]", view->name);
1946 if (*view->ref && bufpos < view->width) {
1947 size_t refsize = strlen(view->ref);
1948 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1950 if (minsize < view->width)
1951 refsize = view->width - minsize + 7;
1952 string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
1955 if (statelen && bufpos < view->width) {
1956 string_format_from(buf, &bufpos, " %s", state);
1959 if (view == display[current_view])
1960 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1962 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1964 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1965 wclrtoeol(view->title);
1966 wmove(view->title, 0, view->width - 1);
1969 wnoutrefresh(view->title);
1971 wrefresh(view->title);
1975 resize_display(void)
1978 struct view *base = display[0];
1979 struct view *view = display[1] ? display[1] : display[0];
1981 /* Setup window dimensions */
1983 getmaxyx(stdscr, base->height, base->width);
1985 /* Make room for the status window. */
1989 /* Horizontal split. */
1990 view->width = base->width;
1991 view->height = SCALE_SPLIT_VIEW(base->height);
1992 base->height -= view->height;
1994 /* Make room for the title bar. */
1998 /* Make room for the title bar. */
2003 foreach_displayed_view (view, i) {
2005 view->win = newwin(view->height, 0, offset, 0);
2007 die("Failed to create %s view", view->name);
2009 scrollok(view->win, TRUE);
2011 view->title = newwin(1, 0, offset + view->height, 0);
2013 die("Failed to create title window");
2016 wresize(view->win, view->height, view->width);
2017 mvwin(view->win, offset, 0);
2018 mvwin(view->title, offset + view->height, 0);
2021 offset += view->height + 1;
2026 redraw_display(void)
2031 foreach_displayed_view (view, i) {
2033 update_view_title(view);
2038 update_display_cursor(struct view *view)
2040 /* Move the cursor to the right-most column of the cursor line.
2042 * XXX: This could turn out to be a bit expensive, but it ensures that
2043 * the cursor does not jump around. */
2045 wmove(view->win, view->lineno - view->offset, view->width - 1);
2046 wrefresh(view->win);
2054 /* Scrolling backend */
2056 do_scroll_view(struct view *view, int lines)
2058 bool redraw_current_line = FALSE;
2060 /* The rendering expects the new offset. */
2061 view->offset += lines;
2063 assert(0 <= view->offset && view->offset < view->lines);
2066 /* Move current line into the view. */
2067 if (view->lineno < view->offset) {
2068 view->lineno = view->offset;
2069 redraw_current_line = TRUE;
2070 } else if (view->lineno >= view->offset + view->height) {
2071 view->lineno = view->offset + view->height - 1;
2072 redraw_current_line = TRUE;
2075 assert(view->offset <= view->lineno && view->lineno < view->lines);
2077 /* Redraw the whole screen if scrolling is pointless. */
2078 if (view->height < ABS(lines)) {
2082 int line = lines > 0 ? view->height - lines : 0;
2083 int end = line + ABS(lines);
2085 wscrl(view->win, lines);
2087 for (; line < end; line++) {
2088 if (!draw_view_line(view, line))
2092 if (redraw_current_line)
2093 draw_view_line(view, view->lineno - view->offset);
2096 redrawwin(view->win);
2097 wrefresh(view->win);
2101 /* Scroll frontend */
2103 scroll_view(struct view *view, enum request request)
2107 assert(view_is_displayed(view));
2110 case REQ_SCROLL_PAGE_DOWN:
2111 lines = view->height;
2112 case REQ_SCROLL_LINE_DOWN:
2113 if (view->offset + lines > view->lines)
2114 lines = view->lines - view->offset;
2116 if (lines == 0 || view->offset + view->height >= view->lines) {
2117 report("Cannot scroll beyond the last line");
2122 case REQ_SCROLL_PAGE_UP:
2123 lines = view->height;
2124 case REQ_SCROLL_LINE_UP:
2125 if (lines > view->offset)
2126 lines = view->offset;
2129 report("Cannot scroll beyond the first line");
2137 die("request %d not handled in switch", request);
2140 do_scroll_view(view, lines);
2145 move_view(struct view *view, enum request request)
2147 int scroll_steps = 0;
2151 case REQ_MOVE_FIRST_LINE:
2152 steps = -view->lineno;
2155 case REQ_MOVE_LAST_LINE:
2156 steps = view->lines - view->lineno - 1;
2159 case REQ_MOVE_PAGE_UP:
2160 steps = view->height > view->lineno
2161 ? -view->lineno : -view->height;
2164 case REQ_MOVE_PAGE_DOWN:
2165 steps = view->lineno + view->height >= view->lines
2166 ? view->lines - view->lineno - 1 : view->height;
2178 die("request %d not handled in switch", request);
2181 if (steps <= 0 && view->lineno == 0) {
2182 report("Cannot move beyond the first line");
2185 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
2186 report("Cannot move beyond the last line");
2190 /* Move the current line */
2191 view->lineno += steps;
2192 assert(0 <= view->lineno && view->lineno < view->lines);
2194 /* Check whether the view needs to be scrolled */
2195 if (view->lineno < view->offset ||
2196 view->lineno >= view->offset + view->height) {
2197 scroll_steps = steps;
2198 if (steps < 0 && -steps > view->offset) {
2199 scroll_steps = -view->offset;
2201 } else if (steps > 0) {
2202 if (view->lineno == view->lines - 1 &&
2203 view->lines > view->height) {
2204 scroll_steps = view->lines - view->offset - 1;
2205 if (scroll_steps >= view->height)
2206 scroll_steps -= view->height - 1;
2211 if (!view_is_displayed(view)) {
2212 view->offset += scroll_steps;
2213 assert(0 <= view->offset && view->offset < view->lines);
2214 view->ops->select(view, &view->line[view->lineno]);
2218 /* Repaint the old "current" line if we be scrolling */
2219 if (ABS(steps) < view->height)
2220 draw_view_line(view, view->lineno - steps - view->offset);
2223 do_scroll_view(view, scroll_steps);
2227 /* Draw the current line */
2228 draw_view_line(view, view->lineno - view->offset);
2230 redrawwin(view->win);
2231 wrefresh(view->win);
2240 static void search_view(struct view *view, enum request request);
2243 find_next_line(struct view *view, unsigned long lineno, struct line *line)
2245 assert(view_is_displayed(view));
2247 if (!view->ops->grep(view, line))
2250 if (lineno - view->offset >= view->height) {
2251 view->offset = lineno;
2252 view->lineno = lineno;
2256 unsigned long old_lineno = view->lineno - view->offset;
2258 view->lineno = lineno;
2259 draw_view_line(view, old_lineno);
2261 draw_view_line(view, view->lineno - view->offset);
2262 redrawwin(view->win);
2263 wrefresh(view->win);
2266 report("Line %ld matches '%s'", lineno + 1, view->grep);
2271 find_next(struct view *view, enum request request)
2273 unsigned long lineno = view->lineno;
2278 report("No previous search");
2280 search_view(view, request);
2290 case REQ_SEARCH_BACK:
2299 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
2300 lineno += direction;
2302 /* Note, lineno is unsigned long so will wrap around in which case it
2303 * will become bigger than view->lines. */
2304 for (; lineno < view->lines; lineno += direction) {
2305 struct line *line = &view->line[lineno];
2307 if (find_next_line(view, lineno, line))
2311 report("No match found for '%s'", view->grep);
2315 search_view(struct view *view, enum request request)
2320 regfree(view->regex);
2323 view->regex = calloc(1, sizeof(*view->regex));
2328 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
2329 if (regex_err != 0) {
2330 char buf[SIZEOF_STR] = "unknown error";
2332 regerror(regex_err, view->regex, buf, sizeof(buf));
2333 report("Search failed: %s", buf);
2337 string_copy(view->grep, opt_search);
2339 find_next(view, request);
2343 * Incremental updating
2347 reset_view(struct view *view)
2351 for (i = 0; i < view->lines; i++)
2352 free(view->line[i].data);
2359 view->line_size = 0;
2360 view->line_alloc = 0;
2365 free_argv(const char *argv[])
2369 for (argc = 0; argv[argc]; argc++)
2370 free((void *) argv[argc]);
2374 format_argv(const char *dst_argv[], const char *src_argv[], enum format_flags flags)
2376 char buf[SIZEOF_STR];
2378 bool noreplace = flags == FORMAT_NONE;
2380 free_argv(dst_argv);
2382 for (argc = 0; src_argv[argc]; argc++) {
2383 const char *arg = src_argv[argc];
2387 char *next = strstr(arg, "%(");
2388 int len = next - arg;
2391 if (!next || noreplace) {
2392 if (flags == FORMAT_DASH && !strcmp(arg, "--"))
2397 } else if (!prefixcmp(next, "%(directory)")) {
2400 } else if (!prefixcmp(next, "%(file)")) {
2403 } else if (!prefixcmp(next, "%(ref)")) {
2404 value = *opt_ref ? opt_ref : "HEAD";
2406 } else if (!prefixcmp(next, "%(head)")) {
2409 } else if (!prefixcmp(next, "%(commit)")) {
2412 } else if (!prefixcmp(next, "%(blob)")) {
2416 report("Unknown replacement: `%s`", next);
2420 if (!string_format_from(buf, &bufpos, "%.*s%s", len, arg, value))
2423 arg = next && !noreplace ? strchr(next, ')') + 1 : NULL;
2426 dst_argv[argc] = strdup(buf);
2427 if (!dst_argv[argc])
2431 dst_argv[argc] = NULL;
2433 return src_argv[argc] == NULL;
2437 format_command(char dst[], const char *src_argv[], enum format_flags flags)
2439 const char *dst_argv[SIZEOF_ARG * 2] = { NULL };
2443 if (!format_argv(dst_argv, src_argv, flags)) {
2444 free_argv(dst_argv);
2448 for (argc = 0; dst_argv[argc] && bufsize < SIZEOF_STR; argc++) {
2450 dst[bufsize++] = ' ';
2451 bufsize = sq_quote(dst, bufsize, dst_argv[argc]);
2454 if (bufsize < SIZEOF_STR)
2456 free_argv(dst_argv);
2458 return src_argv[argc] == NULL && bufsize < SIZEOF_STR;
2462 end_update(struct view *view, bool force)
2466 while (!view->ops->read(view, NULL))
2469 set_nonblocking_input(FALSE);
2470 done_io(view->pipe);
2475 setup_update(struct view *view, const char *vid)
2477 set_nonblocking_input(TRUE);
2479 string_copy_rev(view->vid, vid);
2480 view->pipe = &view->io;
2481 view->start_time = time(NULL);
2485 begin_update(struct view *view, bool refresh)
2487 if (init_io_fd(&view->io, opt_pipe)) {
2490 } else if (opt_cmd[0]) {
2491 if (!run_io(&view->io, IO_RD, opt_cmd))
2493 /* When running random commands, initially show the
2494 * command in the title. However, it maybe later be
2495 * overwritten if a commit line is selected. */
2496 if (view == VIEW(REQ_VIEW_PAGER))
2497 string_copy(view->ref, opt_cmd);
2502 } else if (refresh) {
2503 if (!start_io(&view->io))
2506 } else if (view == VIEW(REQ_VIEW_TREE)) {
2507 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2508 char path[SIZEOF_STR];
2510 if (strcmp(view->vid, view->id))
2511 opt_path[0] = path[0] = 0;
2512 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
2515 if (!run_io_format(&view->io, format, view->id, path))
2519 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2520 const char *id = view->id;
2522 if (!run_io_format(&view->io, format, id, id, id, id, id))
2525 /* Put the current ref_* value to the view title ref
2526 * member. This is needed by the blob view. Most other
2527 * views sets it automatically after loading because the
2528 * first line is a commit line. */
2529 string_copy_rev(view->ref, view->id);
2532 setup_update(view, view->id);
2537 #define ITEM_CHUNK_SIZE 256
2539 realloc_items(void *mem, size_t *size, size_t new_size, size_t item_size)
2541 size_t num_chunks = *size / ITEM_CHUNK_SIZE;
2542 size_t num_chunks_new = (new_size + ITEM_CHUNK_SIZE - 1) / ITEM_CHUNK_SIZE;
2544 if (mem == NULL || num_chunks != num_chunks_new) {
2545 *size = num_chunks_new * ITEM_CHUNK_SIZE;
2546 mem = realloc(mem, *size * item_size);
2552 static struct line *
2553 realloc_lines(struct view *view, size_t line_size)
2555 size_t alloc = view->line_alloc;
2556 struct line *tmp = realloc_items(view->line, &alloc, line_size,
2557 sizeof(*view->line));
2563 view->line_alloc = alloc;
2564 view->line_size = line_size;
2569 update_view(struct view *view)
2571 char out_buffer[BUFSIZ * 2];
2573 /* The number of lines to read. If too low it will cause too much
2574 * redrawing (and possible flickering), if too high responsiveness
2576 unsigned long lines = view->height;
2577 int redraw_from = -1;
2582 /* Only redraw if lines are visible. */
2583 if (view->offset + view->height >= view->lines)
2584 redraw_from = view->lines - view->offset;
2586 /* FIXME: This is probably not perfect for backgrounded views. */
2587 if (!realloc_lines(view, view->lines + lines))
2590 while ((line = io_gets(view->pipe))) {
2591 size_t linelen = strlen(line);
2594 line[linelen - 1] = 0;
2596 if (opt_iconv != ICONV_NONE) {
2597 ICONV_CONST char *inbuf = line;
2598 size_t inlen = linelen;
2600 char *outbuf = out_buffer;
2601 size_t outlen = sizeof(out_buffer);
2605 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
2606 if (ret != (size_t) -1) {
2608 linelen = strlen(out_buffer);
2612 if (!view->ops->read(view, line))
2622 lines = view->lines;
2623 for (digits = 0; lines; digits++)
2626 /* Keep the displayed view in sync with line number scaling. */
2627 if (digits != view->digits) {
2628 view->digits = digits;
2633 if (io_error(view->pipe)) {
2634 report("Failed to read: %s", io_strerror(view->pipe));
2635 end_update(view, TRUE);
2637 } else if (io_eof(view->pipe)) {
2639 end_update(view, FALSE);
2642 if (!view_is_displayed(view))
2645 if (view == VIEW(REQ_VIEW_TREE)) {
2646 /* Clear the view and redraw everything since the tree sorting
2647 * might have rearranged things. */
2650 } else if (redraw_from >= 0) {
2651 /* If this is an incremental update, redraw the previous line
2652 * since for commits some members could have changed when
2653 * loading the main view. */
2654 if (redraw_from > 0)
2657 /* Since revision graph visualization requires knowledge
2658 * about the parent commit, it causes a further one-off
2659 * needed to be redrawn for incremental updates. */
2660 if (redraw_from > 0 && opt_rev_graph)
2663 /* Incrementally draw avoids flickering. */
2664 redraw_view_from(view, redraw_from);
2667 if (view == VIEW(REQ_VIEW_BLAME))
2668 redraw_view_dirty(view);
2670 /* Update the title _after_ the redraw so that if the redraw picks up a
2671 * commit reference in view->ref it'll be available here. */
2672 update_view_title(view);
2676 report("Allocation failure");
2677 end_update(view, TRUE);
2681 static struct line *
2682 add_line_data(struct view *view, void *data, enum line_type type)
2684 struct line *line = &view->line[view->lines++];
2686 memset(line, 0, sizeof(*line));
2693 static struct line *
2694 add_line_text(struct view *view, const char *text, enum line_type type)
2696 char *data = text ? strdup(text) : NULL;
2698 return data ? add_line_data(view, data, type) : NULL;
2707 OPEN_DEFAULT = 0, /* Use default view switching. */
2708 OPEN_SPLIT = 1, /* Split current view. */
2709 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2710 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2711 OPEN_NOMAXIMIZE = 8, /* Do not maximize the current view. */
2712 OPEN_REFRESH = 16, /* Refresh view using previous command. */
2716 open_view(struct view *prev, enum request request, enum open_flags flags)
2718 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2719 bool split = !!(flags & OPEN_SPLIT);
2720 bool reload = !!(flags & (OPEN_RELOAD | OPEN_REFRESH));
2721 bool nomaximize = !!(flags & (OPEN_NOMAXIMIZE | OPEN_REFRESH));
2722 struct view *view = VIEW(request);
2723 int nviews = displayed_views();
2724 struct view *base_view = display[0];
2726 if (view == prev && nviews == 1 && !reload) {
2727 report("Already in %s view", view->name);
2731 if (view->git_dir && !opt_git_dir[0]) {
2732 report("The %s view is disabled in pager view", view->name);
2740 } else if (!nomaximize) {
2741 /* Maximize the current view. */
2742 memset(display, 0, sizeof(display));
2744 display[current_view] = view;
2747 /* Resize the view when switching between split- and full-screen,
2748 * or when switching between two different full-screen views. */
2749 if (nviews != displayed_views() ||
2750 (nviews == 1 && base_view != display[0]))
2754 end_update(view, TRUE);
2756 if (view->ops->open) {
2757 if (!view->ops->open(view)) {
2758 report("Failed to load %s view", view->name);
2762 } else if ((reload || strcmp(view->vid, view->id)) &&
2763 !begin_update(view, flags & OPEN_REFRESH)) {
2764 report("Failed to load %s view", view->name);
2768 if (split && prev->lineno - prev->offset >= prev->height) {
2769 /* Take the title line into account. */
2770 int lines = prev->lineno - prev->offset - prev->height + 1;
2772 /* Scroll the view that was split if the current line is
2773 * outside the new limited view. */
2774 do_scroll_view(prev, lines);
2777 if (prev && view != prev) {
2778 if (split && !backgrounded) {
2779 /* "Blur" the previous view. */
2780 update_view_title(prev);
2783 view->parent = prev;
2786 if (view->pipe && view->lines == 0) {
2787 /* Clear the old view and let the incremental updating refill
2791 } else if (view_is_displayed(view)) {
2796 /* If the view is backgrounded the above calls to report()
2797 * won't redraw the view title. */
2799 update_view_title(view);
2803 run_confirm(const char *cmd, const char *prompt)
2805 bool confirmation = prompt_yesno(prompt);
2810 return confirmation;
2814 open_external_viewer(const char *cmd)
2816 def_prog_mode(); /* save current tty modes */
2817 endwin(); /* restore original tty modes */
2819 fprintf(stderr, "Press Enter to continue");
2826 open_mergetool(const char *file)
2828 char cmd[SIZEOF_STR];
2829 char file_sq[SIZEOF_STR];
2831 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2832 string_format(cmd, "git mergetool %s", file_sq)) {
2833 open_external_viewer(cmd);
2838 open_editor(bool from_root, const char *file)
2840 char cmd[SIZEOF_STR];
2841 char file_sq[SIZEOF_STR];
2843 char *prefix = from_root ? opt_cdup : "";
2845 editor = getenv("GIT_EDITOR");
2846 if (!editor && *opt_editor)
2847 editor = opt_editor;
2849 editor = getenv("VISUAL");
2851 editor = getenv("EDITOR");
2855 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2856 string_format(cmd, "%s %s%s", editor, prefix, file_sq)) {
2857 open_external_viewer(cmd);
2862 open_run_request(enum request request)
2864 struct run_request *req = get_run_request(request);
2865 char buf[SIZEOF_STR * 2];
2868 report("Unknown run request");
2872 if (format_command(buf, req->argv, FORMAT_ALL))
2873 open_external_viewer(buf);
2877 * User request switch noodle
2881 view_driver(struct view *view, enum request request)
2885 if (request == REQ_NONE) {
2890 if (request > REQ_NONE) {
2891 open_run_request(request);
2892 /* FIXME: When all views can refresh always do this. */
2893 if (view == VIEW(REQ_VIEW_STATUS) ||
2894 view == VIEW(REQ_VIEW_MAIN) ||
2895 view == VIEW(REQ_VIEW_LOG) ||
2896 view == VIEW(REQ_VIEW_STAGE))
2897 request = REQ_REFRESH;
2902 if (view && view->lines) {
2903 request = view->ops->request(view, request, &view->line[view->lineno]);
2904 if (request == REQ_NONE)
2911 case REQ_MOVE_PAGE_UP:
2912 case REQ_MOVE_PAGE_DOWN:
2913 case REQ_MOVE_FIRST_LINE:
2914 case REQ_MOVE_LAST_LINE:
2915 move_view(view, request);
2918 case REQ_SCROLL_LINE_DOWN:
2919 case REQ_SCROLL_LINE_UP:
2920 case REQ_SCROLL_PAGE_DOWN:
2921 case REQ_SCROLL_PAGE_UP:
2922 scroll_view(view, request);
2925 case REQ_VIEW_BLAME:
2927 report("No file chosen, press %s to open tree view",
2928 get_key(REQ_VIEW_TREE));
2931 open_view(view, request, OPEN_DEFAULT);
2936 report("No file chosen, press %s to open tree view",
2937 get_key(REQ_VIEW_TREE));
2940 open_view(view, request, OPEN_DEFAULT);
2943 case REQ_VIEW_PAGER:
2944 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2945 report("No pager content, press %s to run command from prompt",
2946 get_key(REQ_PROMPT));
2949 open_view(view, request, OPEN_DEFAULT);
2952 case REQ_VIEW_STAGE:
2953 if (!VIEW(REQ_VIEW_STAGE)->lines) {
2954 report("No stage content, press %s to open the status view and choose file",
2955 get_key(REQ_VIEW_STATUS));
2958 open_view(view, request, OPEN_DEFAULT);
2961 case REQ_VIEW_STATUS:
2962 if (opt_is_inside_work_tree == FALSE) {
2963 report("The status view requires a working tree");
2966 open_view(view, request, OPEN_DEFAULT);
2974 open_view(view, request, OPEN_DEFAULT);
2979 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2981 if ((view == VIEW(REQ_VIEW_DIFF) &&
2982 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2983 (view == VIEW(REQ_VIEW_DIFF) &&
2984 view->parent == VIEW(REQ_VIEW_BLAME)) ||
2985 (view == VIEW(REQ_VIEW_STAGE) &&
2986 view->parent == VIEW(REQ_VIEW_STATUS)) ||
2987 (view == VIEW(REQ_VIEW_BLOB) &&
2988 view->parent == VIEW(REQ_VIEW_TREE))) {
2991 view = view->parent;
2992 line = view->lineno;
2993 move_view(view, request);
2994 if (view_is_displayed(view))
2995 update_view_title(view);
2996 if (line != view->lineno)
2997 view->ops->request(view, REQ_ENTER,
2998 &view->line[view->lineno]);
3001 move_view(view, request);
3007 int nviews = displayed_views();
3008 int next_view = (current_view + 1) % nviews;
3010 if (next_view == current_view) {
3011 report("Only one view is displayed");
3015 current_view = next_view;
3016 /* Blur out the title of the previous view. */
3017 update_view_title(view);
3022 report("Refreshing is not yet supported for the %s view", view->name);
3026 if (displayed_views() == 2)
3027 open_view(view, VIEW_REQ(view), OPEN_DEFAULT);
3030 case REQ_TOGGLE_LINENO:
3031 opt_line_number = !opt_line_number;
3035 case REQ_TOGGLE_DATE:
3036 opt_date = !opt_date;
3040 case REQ_TOGGLE_AUTHOR:
3041 opt_author = !opt_author;
3045 case REQ_TOGGLE_REV_GRAPH:
3046 opt_rev_graph = !opt_rev_graph;
3050 case REQ_TOGGLE_REFS:
3051 opt_show_refs = !opt_show_refs;
3056 case REQ_SEARCH_BACK:
3057 search_view(view, request);
3062 find_next(view, request);
3065 case REQ_STOP_LOADING:
3066 for (i = 0; i < ARRAY_SIZE(views); i++) {
3069 report("Stopped loading the %s view", view->name),
3070 end_update(view, TRUE);
3074 case REQ_SHOW_VERSION:
3075 report("tig-%s (built %s)", TIG_VERSION, __DATE__);
3078 case REQ_SCREEN_RESIZE:
3081 case REQ_SCREEN_REDRAW:
3086 report("Nothing to edit");
3090 report("Nothing to enter");
3093 case REQ_VIEW_CLOSE:
3094 /* XXX: Mark closed views by letting view->parent point to the
3095 * view itself. Parents to closed view should never be
3098 view->parent->parent != view->parent) {
3099 memset(display, 0, sizeof(display));
3101 display[current_view] = view->parent;
3102 view->parent = view;
3113 report("Unknown key, press 'h' for help");
3126 pager_draw(struct view *view, struct line *line, unsigned int lineno)
3128 char *text = line->data;
3130 if (opt_line_number && draw_lineno(view, lineno))
3133 draw_text(view, line->type, text, TRUE);
3138 add_describe_ref(char *buf, size_t *bufpos, const char *commit_id, const char *sep)
3140 char refbuf[SIZEOF_STR];
3144 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
3147 pipe = popen(refbuf, "r");
3151 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
3152 ref = chomp_string(ref);
3158 /* This is the only fatal call, since it can "corrupt" the buffer. */
3159 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
3166 add_pager_refs(struct view *view, struct line *line)
3168 char buf[SIZEOF_STR];
3169 char *commit_id = (char *)line->data + STRING_SIZE("commit ");
3171 size_t bufpos = 0, refpos = 0;
3172 const char *sep = "Refs: ";
3173 bool is_tag = FALSE;
3175 assert(line->type == LINE_COMMIT);
3177 refs = get_refs(commit_id);
3179 if (view == VIEW(REQ_VIEW_DIFF))
3180 goto try_add_describe_ref;
3185 struct ref *ref = refs[refpos];
3186 const char *fmt = ref->tag ? "%s[%s]" :
3187 ref->remote ? "%s<%s>" : "%s%s";
3189 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
3194 } while (refs[refpos++]->next);
3196 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
3197 try_add_describe_ref:
3198 /* Add <tag>-g<commit_id> "fake" reference. */
3199 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
3206 if (!realloc_lines(view, view->line_size + 1))
3209 add_line_text(view, buf, LINE_PP_REFS);
3213 pager_read(struct view *view, char *data)
3220 line = add_line_text(view, data, get_line_type(data));
3224 if (line->type == LINE_COMMIT &&
3225 (view == VIEW(REQ_VIEW_DIFF) ||
3226 view == VIEW(REQ_VIEW_LOG)))
3227 add_pager_refs(view, line);
3233 pager_request(struct view *view, enum request request, struct line *line)
3237 if (request != REQ_ENTER)
3240 if (line->type == LINE_COMMIT &&
3241 (view == VIEW(REQ_VIEW_LOG) ||
3242 view == VIEW(REQ_VIEW_PAGER))) {
3243 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
3247 /* Always scroll the view even if it was split. That way
3248 * you can use Enter to scroll through the log view and
3249 * split open each commit diff. */
3250 scroll_view(view, REQ_SCROLL_LINE_DOWN);
3252 /* FIXME: A minor workaround. Scrolling the view will call report("")
3253 * but if we are scrolling a non-current view this won't properly
3254 * update the view title. */
3256 update_view_title(view);
3262 pager_grep(struct view *view, struct line *line)
3265 char *text = line->data;
3270 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
3277 pager_select(struct view *view, struct line *line)
3279 if (line->type == LINE_COMMIT) {
3280 char *text = (char *)line->data + STRING_SIZE("commit ");
3282 if (view != VIEW(REQ_VIEW_PAGER))
3283 string_copy_rev(view->ref, text);
3284 string_copy_rev(ref_commit, text);
3288 static struct view_ops pager_ops = {
3299 log_request(struct view *view, enum request request, struct line *line)
3304 open_view(view, REQ_VIEW_LOG, OPEN_REFRESH);
3307 return pager_request(view, request, line);
3311 static struct view_ops log_ops = {
3327 help_open(struct view *view)
3330 int lines = ARRAY_SIZE(req_info) + 2;
3333 if (view->lines > 0)
3336 for (i = 0; i < ARRAY_SIZE(req_info); i++)
3337 if (!req_info[i].request)
3340 lines += run_requests + 1;
3342 view->line = calloc(lines, sizeof(*view->line));
3346 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
3348 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
3351 if (req_info[i].request == REQ_NONE)
3354 if (!req_info[i].request) {
3355 add_line_text(view, "", LINE_DEFAULT);
3356 add_line_text(view, req_info[i].help, LINE_DEFAULT);
3360 key = get_key(req_info[i].request);
3362 key = "(no key defined)";
3364 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
3367 add_line_text(view, buf, LINE_DEFAULT);
3371 add_line_text(view, "", LINE_DEFAULT);
3372 add_line_text(view, "External commands:", LINE_DEFAULT);
3375 for (i = 0; i < run_requests; i++) {
3376 struct run_request *req = get_run_request(REQ_NONE + i + 1);
3378 char cmd[SIZEOF_STR];
3385 key = get_key_name(req->key);
3387 key = "(no key defined)";
3389 for (bufpos = 0, argc = 0; req->argv[argc]; argc++)
3390 if (!string_format_from(cmd, &bufpos, "%s%s",
3391 argc ? " " : "", req->argv[argc]))
3394 if (!string_format(buf, " %-10s %-14s `%s`",
3395 keymap_table[req->keymap].name, key, cmd))
3398 add_line_text(view, buf, LINE_DEFAULT);
3404 static struct view_ops help_ops = {
3419 struct tree_stack_entry {
3420 struct tree_stack_entry *prev; /* Entry below this in the stack */
3421 unsigned long lineno; /* Line number to restore */
3422 char *name; /* Position of name in opt_path */
3425 /* The top of the path stack. */
3426 static struct tree_stack_entry *tree_stack = NULL;
3427 unsigned long tree_lineno = 0;
3430 pop_tree_stack_entry(void)
3432 struct tree_stack_entry *entry = tree_stack;
3434 tree_lineno = entry->lineno;
3436 tree_stack = entry->prev;
3441 push_tree_stack_entry(const char *name, unsigned long lineno)
3443 struct tree_stack_entry *entry = calloc(1, sizeof(*entry));
3444 size_t pathlen = strlen(opt_path);
3449 entry->prev = tree_stack;
3450 entry->name = opt_path + pathlen;
3453 if (!string_format_from(opt_path, &pathlen, "%s/", name)) {
3454 pop_tree_stack_entry();
3458 /* Move the current line to the first tree entry. */
3460 entry->lineno = lineno;
3463 /* Parse output from git-ls-tree(1):
3465 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
3466 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
3467 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
3468 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
3471 #define SIZEOF_TREE_ATTR \
3472 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
3474 #define TREE_UP_FORMAT "040000 tree %s\t.."
3477 tree_compare_entry(enum line_type type1, const char *name1,
3478 enum line_type type2, const char *name2)
3480 if (type1 != type2) {
3481 if (type1 == LINE_TREE_DIR)
3486 return strcmp(name1, name2);
3490 tree_path(struct line *line)
3492 const char *path = line->data;
3494 return path + SIZEOF_TREE_ATTR;
3498 tree_read(struct view *view, char *text)
3500 size_t textlen = text ? strlen(text) : 0;
3501 char buf[SIZEOF_STR];
3503 enum line_type type;
3504 bool first_read = view->lines == 0;
3508 if (textlen <= SIZEOF_TREE_ATTR)
3511 type = text[STRING_SIZE("100644 ")] == 't'
3512 ? LINE_TREE_DIR : LINE_TREE_FILE;
3515 /* Add path info line */
3516 if (!string_format(buf, "Directory path /%s", opt_path) ||
3517 !realloc_lines(view, view->line_size + 1) ||
3518 !add_line_text(view, buf, LINE_DEFAULT))
3521 /* Insert "link" to parent directory. */
3523 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
3524 !realloc_lines(view, view->line_size + 1) ||
3525 !add_line_text(view, buf, LINE_TREE_DIR))
3530 /* Strip the path part ... */
3532 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
3533 size_t striplen = strlen(opt_path);
3534 char *path = text + SIZEOF_TREE_ATTR;
3536 if (pathlen > striplen)
3537 memmove(path, path + striplen,
3538 pathlen - striplen + 1);
3541 /* Skip "Directory ..." and ".." line. */
3542 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
3543 struct line *line = &view->line[pos];
3544 const char *path1 = tree_path(line);
3545 char *path2 = text + SIZEOF_TREE_ATTR;
3546 int cmp = tree_compare_entry(line->type, path1, type, path2);
3551 text = strdup(text);
3555 if (view->lines > pos)
3556 memmove(&view->line[pos + 1], &view->line[pos],
3557 (view->lines - pos) * sizeof(*line));
3559 line = &view->line[pos];
3566 if (!add_line_text(view, text, type))
3569 if (tree_lineno > view->lineno) {
3570 view->lineno = tree_lineno;
3578 tree_request(struct view *view, enum request request, struct line *line)
3580 enum open_flags flags;
3583 case REQ_VIEW_BLAME:
3584 if (line->type != LINE_TREE_FILE) {
3585 report("Blame only supported for files");
3589 string_copy(opt_ref, view->vid);
3593 if (line->type != LINE_TREE_FILE) {
3594 report("Edit only supported for files");
3595 } else if (!is_head_commit(view->vid)) {
3596 report("Edit only supported for files in the current work tree");
3598 open_editor(TRUE, opt_file);
3602 case REQ_TREE_PARENT:
3604 /* quit view if at top of tree */
3605 return REQ_VIEW_CLOSE;
3608 line = &view->line[1];
3618 /* Cleanup the stack if the tree view is at a different tree. */
3619 while (!*opt_path && tree_stack)
3620 pop_tree_stack_entry();
3622 switch (line->type) {
3624 /* Depending on whether it is a subdir or parent (updir?) link
3625 * mangle the path buffer. */
3626 if (line == &view->line[1] && *opt_path) {
3627 pop_tree_stack_entry();
3630 const char *basename = tree_path(line);
3632 push_tree_stack_entry(basename, view->lineno);
3635 /* Trees and subtrees share the same ID, so they are not not
3636 * unique like blobs. */
3637 flags = OPEN_RELOAD;
3638 request = REQ_VIEW_TREE;
3641 case LINE_TREE_FILE:
3642 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3643 request = REQ_VIEW_BLOB;
3650 open_view(view, request, flags);
3651 if (request == REQ_VIEW_TREE) {
3652 view->lineno = tree_lineno;
3659 tree_select(struct view *view, struct line *line)
3661 char *text = (char *)line->data + STRING_SIZE("100644 blob ");
3663 if (line->type == LINE_TREE_FILE) {
3664 string_copy_rev(ref_blob, text);
3665 string_format(opt_file, "%s%s", opt_path, tree_path(line));
3667 } else if (line->type != LINE_TREE_DIR) {
3671 string_copy_rev(view->ref, text);
3674 static struct view_ops tree_ops = {
3685 blob_read(struct view *view, char *line)
3689 return add_line_text(view, line, LINE_DEFAULT) != NULL;
3692 static struct view_ops blob_ops = {
3705 * Loading the blame view is a two phase job:
3707 * 1. File content is read either using opt_file from the
3708 * filesystem or using git-cat-file.
3709 * 2. Then blame information is incrementally added by
3710 * reading output from git-blame.
3713 struct blame_commit {
3714 char id[SIZEOF_REV]; /* SHA1 ID. */
3715 char title[128]; /* First line of the commit message. */
3716 char author[75]; /* Author of the commit. */
3717 struct tm time; /* Date from the author ident. */
3718 char filename[128]; /* Name of file. */
3722 struct blame_commit *commit;
3726 #define BLAME_CAT_FILE_CMD "git cat-file blob %s:%s"
3727 #define BLAME_INCREMENTAL_CMD "git blame --incremental %s -- %s"
3730 blame_open(struct view *view)
3732 char path[SIZEOF_STR];
3733 char ref[SIZEOF_STR] = "";
3735 if (sq_quote(path, 0, opt_file) >= sizeof(path))
3738 if (*opt_ref && sq_quote(ref, 0, opt_ref) >= sizeof(ref))
3741 if (*opt_ref || !init_io_fd(&view->io, fopen(opt_file, "r"))) {
3742 const char *id = *opt_ref ? ref : "HEAD";
3744 if (!run_io_format(&view->io, BLAME_CAT_FILE_CMD, id, path))
3748 setup_update(view, opt_file);
3749 string_format(view->ref, "%s ...", opt_file);
3754 static struct blame_commit *
3755 get_blame_commit(struct view *view, const char *id)
3759 for (i = 0; i < view->lines; i++) {
3760 struct blame *blame = view->line[i].data;
3765 if (!strncmp(blame->commit->id, id, SIZEOF_REV - 1))
3766 return blame->commit;
3770 struct blame_commit *commit = calloc(1, sizeof(*commit));
3773 string_ncopy(commit->id, id, SIZEOF_REV);
3779 parse_number(const char **posref, size_t *number, size_t min, size_t max)
3781 const char *pos = *posref;
3784 pos = strchr(pos + 1, ' ');
3785 if (!pos || !isdigit(pos[1]))
3787 *number = atoi(pos + 1);
3788 if (*number < min || *number > max)
3795 static struct blame_commit *
3796 parse_blame_commit(struct view *view, const char *text, int *blamed)
3798 struct blame_commit *commit;
3799 struct blame *blame;
3800 const char *pos = text + SIZEOF_REV - 1;
3804 if (strlen(text) <= SIZEOF_REV || *pos != ' ')
3807 if (!parse_number(&pos, &lineno, 1, view->lines) ||
3808 !parse_number(&pos, &group, 1, view->lines - lineno + 1))
3811 commit = get_blame_commit(view, text);
3817 struct line *line = &view->line[lineno + group - 1];
3820 blame->commit = commit;
3828 blame_read_file(struct view *view, const char *line, bool *read_file)
3831 char ref[SIZEOF_STR] = "";
3832 char path[SIZEOF_STR];
3835 if (view->lines == 0 && !view->parent)
3836 die("No blame exist for %s", view->vid);
3838 if (view->lines == 0 ||
3839 sq_quote(path, 0, opt_file) >= sizeof(path) ||
3840 (*opt_ref && sq_quote(ref, 0, opt_ref) >= sizeof(ref)) ||
3841 !run_io_format(&io, BLAME_INCREMENTAL_CMD, ref, path)) {
3842 report("Failed to load blame data");
3846 done_io(view->pipe);
3852 size_t linelen = strlen(line);
3853 struct blame *blame = malloc(sizeof(*blame) + linelen);
3855 blame->commit = NULL;
3856 strncpy(blame->text, line, linelen);
3857 blame->text[linelen] = 0;
3858 return add_line_data(view, blame, LINE_BLAME_ID) != NULL;
3863 match_blame_header(const char *name, char **line)
3865 size_t namelen = strlen(name);
3866 bool matched = !strncmp(name, *line, namelen);
3875 blame_read(struct view *view, char *line)
3877 static struct blame_commit *commit = NULL;
3878 static int blamed = 0;
3879 static time_t author_time;
3880 static bool read_file = TRUE;
3883 return blame_read_file(view, line, &read_file);
3890 string_format(view->ref, "%s", view->vid);
3891 if (view_is_displayed(view)) {
3892 update_view_title(view);
3893 redraw_view_from(view, 0);
3899 commit = parse_blame_commit(view, line, &blamed);
3900 string_format(view->ref, "%s %2d%%", view->vid,
3901 blamed * 100 / view->lines);
3903 } else if (match_blame_header("author ", &line)) {
3904 string_ncopy(commit->author, line, strlen(line));
3906 } else if (match_blame_header("author-time ", &line)) {
3907 author_time = (time_t) atol(line);
3909 } else if (match_blame_header("author-tz ", &line)) {
3912 tz = ('0' - line[1]) * 60 * 60 * 10;
3913 tz += ('0' - line[2]) * 60 * 60;
3914 tz += ('0' - line[3]) * 60;
3915 tz += ('0' - line[4]) * 60;
3921 gmtime_r(&author_time, &commit->time);
3923 } else if (match_blame_header("summary ", &line)) {
3924 string_ncopy(commit->title, line, strlen(line));
3926 } else if (match_blame_header("filename ", &line)) {
3927 string_ncopy(commit->filename, line, strlen(line));
3935 blame_draw(struct view *view, struct line *line, unsigned int lineno)
3937 struct blame *blame = line->data;
3938 struct tm *time = NULL;
3939 const char *id = NULL, *author = NULL;
3941 if (blame->commit && *blame->commit->filename) {
3942 id = blame->commit->id;
3943 author = blame->commit->author;
3944 time = &blame->commit->time;
3947 if (opt_date && draw_date(view, time))
3951 draw_field(view, LINE_MAIN_AUTHOR, author, opt_author_cols, TRUE))
3954 if (draw_field(view, LINE_BLAME_ID, id, ID_COLS, FALSE))
3957 if (draw_lineno(view, lineno))
3960 draw_text(view, LINE_DEFAULT, blame->text, TRUE);
3965 blame_request(struct view *view, enum request request, struct line *line)
3967 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3968 struct blame *blame = line->data;
3971 case REQ_VIEW_BLAME:
3972 if (!blame->commit || !strcmp(blame->commit->id, NULL_ID)) {
3973 report("Commit ID unknown");
3976 string_copy(opt_ref, blame->commit->id);
3977 open_view(view, REQ_VIEW_BLAME, OPEN_REFRESH);
3981 if (!blame->commit) {
3982 report("No commit loaded yet");
3986 if (view_is_displayed(VIEW(REQ_VIEW_DIFF)) &&
3987 !strcmp(blame->commit->id, VIEW(REQ_VIEW_DIFF)->ref))
3990 if (!strcmp(blame->commit->id, NULL_ID)) {
3991 char path[SIZEOF_STR];
3993 if (sq_quote(path, 0, view->vid) >= sizeof(path))
3995 string_format(opt_cmd, "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s 2>/dev/null", path);
3998 open_view(view, REQ_VIEW_DIFF, flags);
4009 blame_grep(struct view *view, struct line *line)
4011 struct blame *blame = line->data;
4012 struct blame_commit *commit = blame->commit;
4015 #define MATCH(text, on) \
4016 (on && *text && regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4019 char buf[DATE_COLS + 1];
4021 if (MATCH(commit->title, 1) ||
4022 MATCH(commit->author, opt_author) ||
4023 MATCH(commit->id, opt_date))
4026 if (strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time) &&
4031 return MATCH(blame->text, 1);
4037 blame_select(struct view *view, struct line *line)
4039 struct blame *blame = line->data;
4040 struct blame_commit *commit = blame->commit;
4045 if (!strcmp(commit->id, NULL_ID))
4046 string_ncopy(ref_commit, "HEAD", 4);
4048 string_copy_rev(ref_commit, commit->id);
4051 static struct view_ops blame_ops = {
4069 char rev[SIZEOF_REV];
4070 char name[SIZEOF_STR];
4074 char rev[SIZEOF_REV];
4075 char name[SIZEOF_STR];
4079 static char status_onbranch[SIZEOF_STR];
4080 static struct status stage_status;
4081 static enum line_type stage_line_type;
4082 static size_t stage_chunks;
4083 static int *stage_chunk;
4085 /* This should work even for the "On branch" line. */
4087 status_has_none(struct view *view, struct line *line)
4089 return line < view->line + view->lines && !line[1].data;
4092 /* Get fields from the diff line:
4093 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
4096 status_get_diff(struct status *file, const char *buf, size_t bufsize)
4098 const char *old_mode = buf + 1;
4099 const char *new_mode = buf + 8;
4100 const char *old_rev = buf + 15;
4101 const char *new_rev = buf + 56;
4102 const char *status = buf + 97;
4105 old_mode[-1] != ':' ||
4106 new_mode[-1] != ' ' ||
4107 old_rev[-1] != ' ' ||
4108 new_rev[-1] != ' ' ||
4112 file->status = *status;
4114 string_copy_rev(file->old.rev, old_rev);
4115 string_copy_rev(file->new.rev, new_rev);
4117 file->old.mode = strtoul(old_mode, NULL, 8);
4118 file->new.mode = strtoul(new_mode, NULL, 8);
4120 file->old.name[0] = file->new.name[0] = 0;
4126 status_run(struct view *view, const char cmd[], char status, enum line_type type)
4128 struct status *file = NULL;
4129 struct status *unmerged = NULL;
4130 char buf[SIZEOF_STR * 4];
4134 pipe = popen(cmd, "r");
4138 add_line_data(view, NULL, type);
4140 while (!feof(pipe) && !ferror(pipe)) {
4144 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
4147 bufsize += readsize;
4149 /* Process while we have NUL chars. */
4150 while ((sep = memchr(buf, 0, bufsize))) {
4151 size_t sepsize = sep - buf + 1;
4154 if (!realloc_lines(view, view->line_size + 1))
4157 file = calloc(1, sizeof(*file));
4161 add_line_data(view, file, type);
4164 /* Parse diff info part. */
4166 file->status = status;
4168 string_copy(file->old.rev, NULL_ID);
4170 } else if (!file->status) {
4171 if (!status_get_diff(file, buf, sepsize))
4175 memmove(buf, sep + 1, bufsize);
4177 sep = memchr(buf, 0, bufsize);
4180 sepsize = sep - buf + 1;
4182 /* Collapse all 'M'odified entries that
4183 * follow a associated 'U'nmerged entry.
4185 if (file->status == 'U') {
4188 } else if (unmerged) {
4189 int collapse = !strcmp(buf, unmerged->new.name);
4200 /* Grab the old name for rename/copy. */
4201 if (!*file->old.name &&
4202 (file->status == 'R' || file->status == 'C')) {
4203 sepsize = sep - buf + 1;
4204 string_ncopy(file->old.name, buf, sepsize);
4206 memmove(buf, sep + 1, bufsize);
4208 sep = memchr(buf, 0, bufsize);
4211 sepsize = sep - buf + 1;
4214 /* git-ls-files just delivers a NUL separated
4215 * list of file names similar to the second half
4216 * of the git-diff-* output. */
4217 string_ncopy(file->new.name, buf, sepsize);
4218 if (!*file->old.name)
4219 string_copy(file->old.name, file->new.name);
4221 memmove(buf, sep + 1, bufsize);
4232 if (!view->line[view->lines - 1].data)
4233 add_line_data(view, NULL, LINE_STAT_NONE);
4239 /* Don't show unmerged entries in the staged section. */
4240 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached -M HEAD"
4241 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
4242 #define STATUS_LIST_OTHER_CMD \
4243 "git ls-files -z --others --exclude-standard"
4244 #define STATUS_LIST_NO_HEAD_CMD \
4245 "git ls-files -z --cached --exclude-standard"
4247 #define STATUS_DIFF_INDEX_SHOW_CMD \
4248 "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s %s 2>/dev/null"
4250 #define STATUS_DIFF_FILES_SHOW_CMD \
4251 "git diff-files --root --patch-with-stat -C -M -- %s %s 2>/dev/null"
4253 #define STATUS_DIFF_NO_HEAD_SHOW_CMD \
4254 "git diff --no-color --patch-with-stat /dev/null %s 2>/dev/null"
4256 /* First parse staged info using git-diff-index(1), then parse unstaged
4257 * info using git-diff-files(1), and finally untracked files using
4258 * git-ls-files(1). */
4260 status_open(struct view *view)
4262 unsigned long prev_lineno = view->lineno;
4266 if (!realloc_lines(view, view->line_size + 7))
4269 add_line_data(view, NULL, LINE_STAT_HEAD);
4270 if (is_initial_commit())
4271 string_copy(status_onbranch, "Initial commit");
4272 else if (!*opt_head)
4273 string_copy(status_onbranch, "Not currently on any branch");
4274 else if (!string_format(status_onbranch, "On branch %s", opt_head))
4277 system("git update-index -q --refresh >/dev/null 2>/dev/null");
4279 if (is_initial_commit()) {
4280 if (!status_run(view, STATUS_LIST_NO_HEAD_CMD, 'A', LINE_STAT_STAGED))
4282 } else if (!status_run(view, STATUS_DIFF_INDEX_CMD, 0, LINE_STAT_STAGED)) {
4286 if (!status_run(view, STATUS_DIFF_FILES_CMD, 0, LINE_STAT_UNSTAGED) ||
4287 !status_run(view, STATUS_LIST_OTHER_CMD, '?', LINE_STAT_UNTRACKED))
4290 /* If all went well restore the previous line number to stay in
4291 * the context or select a line with something that can be
4293 if (prev_lineno >= view->lines)
4294 prev_lineno = view->lines - 1;
4295 while (prev_lineno < view->lines && !view->line[prev_lineno].data)
4297 while (prev_lineno > 0 && !view->line[prev_lineno].data)
4300 /* If the above fails, always skip the "On branch" line. */
4301 if (prev_lineno < view->lines)
4302 view->lineno = prev_lineno;
4306 if (view->lineno < view->offset)
4307 view->offset = view->lineno;
4308 else if (view->offset + view->height <= view->lineno)
4309 view->offset = view->lineno - view->height + 1;
4315 status_draw(struct view *view, struct line *line, unsigned int lineno)
4317 struct status *status = line->data;
4318 enum line_type type;
4322 switch (line->type) {
4323 case LINE_STAT_STAGED:
4324 type = LINE_STAT_SECTION;
4325 text = "Changes to be committed:";
4328 case LINE_STAT_UNSTAGED:
4329 type = LINE_STAT_SECTION;
4330 text = "Changed but not updated:";
4333 case LINE_STAT_UNTRACKED:
4334 type = LINE_STAT_SECTION;
4335 text = "Untracked files:";
4338 case LINE_STAT_NONE:
4339 type = LINE_DEFAULT;
4340 text = " (no files)";
4343 case LINE_STAT_HEAD:
4344 type = LINE_STAT_HEAD;
4345 text = status_onbranch;
4352 static char buf[] = { '?', ' ', ' ', ' ', 0 };
4354 buf[0] = status->status;
4355 if (draw_text(view, line->type, buf, TRUE))
4357 type = LINE_DEFAULT;
4358 text = status->new.name;
4361 draw_text(view, type, text, TRUE);
4366 status_enter(struct view *view, struct line *line)
4368 struct status *status = line->data;
4369 char oldpath[SIZEOF_STR] = "";
4370 char newpath[SIZEOF_STR] = "";
4373 enum open_flags split;
4375 if (line->type == LINE_STAT_NONE ||
4376 (!status && line[1].type == LINE_STAT_NONE)) {
4377 report("No file to diff");
4382 if (sq_quote(oldpath, 0, status->old.name) >= sizeof(oldpath))
4384 /* Diffs for unmerged entries are empty when pasing the
4385 * new path, so leave it empty. */
4386 if (status->status != 'U' &&
4387 sq_quote(newpath, 0, status->new.name) >= sizeof(newpath))
4392 line->type != LINE_STAT_UNTRACKED &&
4393 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
4396 switch (line->type) {
4397 case LINE_STAT_STAGED:
4398 if (is_initial_commit()) {
4399 if (!string_format_from(opt_cmd, &cmdsize,
4400 STATUS_DIFF_NO_HEAD_SHOW_CMD,
4404 if (!string_format_from(opt_cmd, &cmdsize,
4405 STATUS_DIFF_INDEX_SHOW_CMD,
4411 info = "Staged changes to %s";
4413 info = "Staged changes";
4416 case LINE_STAT_UNSTAGED:
4417 if (!string_format_from(opt_cmd, &cmdsize,
4418 STATUS_DIFF_FILES_SHOW_CMD, oldpath, newpath))
4421 info = "Unstaged changes to %s";
4423 info = "Unstaged changes";
4426 case LINE_STAT_UNTRACKED:
4431 report("No file to show");
4435 if (!suffixcmp(status->new.name, -1, "/")) {
4436 report("Cannot display a directory");
4440 opt_pipe = fopen(status->new.name, "r");
4441 info = "Untracked file %s";
4444 case LINE_STAT_HEAD:
4448 die("line type %d not handled in switch", line->type);
4451 split = view_is_displayed(view) ? OPEN_SPLIT : 0;
4452 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | split);
4453 if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) {
4455 stage_status = *status;
4457 memset(&stage_status, 0, sizeof(stage_status));
4460 stage_line_type = line->type;
4462 string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.new.name);
4469 status_exists(struct status *status, enum line_type type)
4471 struct view *view = VIEW(REQ_VIEW_STATUS);
4474 for (line = view->line; line < view->line + view->lines; line++) {
4475 struct status *pos = line->data;
4477 if (line->type == type && pos &&
4478 !strcmp(status->new.name, pos->new.name))
4487 status_update_prepare(enum line_type type)
4489 char cmd[SIZEOF_STR];
4493 type != LINE_STAT_UNTRACKED &&
4494 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4498 case LINE_STAT_STAGED:
4499 string_add(cmd, cmdsize, "git update-index -z --index-info");
4502 case LINE_STAT_UNSTAGED:
4503 case LINE_STAT_UNTRACKED:
4504 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
4508 die("line type %d not handled in switch", type);
4511 return popen(cmd, "w");
4515 status_update_write(FILE *pipe, struct status *status, enum line_type type)
4517 char buf[SIZEOF_STR];
4522 case LINE_STAT_STAGED:
4523 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
4526 status->old.name, 0))
4530 case LINE_STAT_UNSTAGED:
4531 case LINE_STAT_UNTRACKED:
4532 if (!string_format_from(buf, &bufsize, "%s%c", status->new.name, 0))
4537 die("line type %d not handled in switch", type);
4540 while (!ferror(pipe) && written < bufsize) {
4541 written += fwrite(buf + written, 1, bufsize - written, pipe);
4544 return written == bufsize;
4548 status_update_file(struct status *status, enum line_type type)
4550 FILE *pipe = status_update_prepare(type);
4556 result = status_update_write(pipe, status, type);
4562 status_update_files(struct view *view, struct line *line)
4564 FILE *pipe = status_update_prepare(line->type);
4566 struct line *pos = view->line + view->lines;
4573 for (pos = line; pos < view->line + view->lines && pos->data; pos++)
4576 for (file = 0, done = 0; result && file < files; line++, file++) {
4577 int almost_done = file * 100 / files;
4579 if (almost_done > done) {
4581 string_format(view->ref, "updating file %u of %u (%d%% done)",
4583 update_view_title(view);
4585 result = status_update_write(pipe, line->data, line->type);
4593 status_update(struct view *view)
4595 struct line *line = &view->line[view->lineno];
4597 assert(view->lines);
4600 /* This should work even for the "On branch" line. */
4601 if (line < view->line + view->lines && !line[1].data) {
4602 report("Nothing to update");
4606 if (!status_update_files(view, line + 1)) {
4607 report("Failed to update file status");
4611 } else if (!status_update_file(line->data, line->type)) {
4612 report("Failed to update file status");
4620 status_revert(struct status *status, enum line_type type, bool has_none)
4622 if (!status || type != LINE_STAT_UNSTAGED) {
4623 if (type == LINE_STAT_STAGED) {
4624 report("Cannot revert changes to staged files");
4625 } else if (type == LINE_STAT_UNTRACKED) {
4626 report("Cannot revert changes to untracked files");
4627 } else if (has_none) {
4628 report("Nothing to revert");
4630 report("Cannot revert changes to multiple files");
4635 char cmd[SIZEOF_STR];
4636 char file_sq[SIZEOF_STR];
4638 if (sq_quote(file_sq, 0, status->old.name) >= sizeof(file_sq) ||
4639 !string_format(cmd, "git checkout -- %s%s", opt_cdup, file_sq))
4642 return run_confirm(cmd, "Are you sure you want to overwrite any changes?");
4647 status_request(struct view *view, enum request request, struct line *line)
4649 struct status *status = line->data;
4652 case REQ_STATUS_UPDATE:
4653 if (!status_update(view))
4657 case REQ_STATUS_REVERT:
4658 if (!status_revert(status, line->type, status_has_none(view, line)))
4662 case REQ_STATUS_MERGE:
4663 if (!status || status->status != 'U') {
4664 report("Merging only possible for files with unmerged status ('U').");
4667 open_mergetool(status->new.name);
4673 if (status->status == 'D') {
4674 report("File has been deleted.");
4678 open_editor(status->status != '?', status->new.name);
4681 case REQ_VIEW_BLAME:
4683 string_copy(opt_file, status->new.name);
4689 /* After returning the status view has been split to
4690 * show the stage view. No further reloading is
4692 status_enter(view, line);
4696 /* Simply reload the view. */
4703 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
4709 status_select(struct view *view, struct line *line)
4711 struct status *status = line->data;
4712 char file[SIZEOF_STR] = "all files";
4716 if (status && !string_format(file, "'%s'", status->new.name))
4719 if (!status && line[1].type == LINE_STAT_NONE)
4722 switch (line->type) {
4723 case LINE_STAT_STAGED:
4724 text = "Press %s to unstage %s for commit";
4727 case LINE_STAT_UNSTAGED:
4728 text = "Press %s to stage %s for commit";
4731 case LINE_STAT_UNTRACKED:
4732 text = "Press %s to stage %s for addition";
4735 case LINE_STAT_HEAD:
4736 case LINE_STAT_NONE:
4737 text = "Nothing to update";
4741 die("line type %d not handled in switch", line->type);
4744 if (status && status->status == 'U') {
4745 text = "Press %s to resolve conflict in %s";
4746 key = get_key(REQ_STATUS_MERGE);
4749 key = get_key(REQ_STATUS_UPDATE);
4752 string_format(view->ref, text, key, file);
4756 status_grep(struct view *view, struct line *line)
4758 struct status *status = line->data;
4759 enum { S_STATUS, S_NAME, S_END } state;
4766 for (state = S_STATUS; state < S_END; state++) {
4770 case S_NAME: text = status->new.name; break;
4772 buf[0] = status->status;
4780 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4787 static struct view_ops status_ops = {
4799 stage_diff_line(FILE *pipe, struct line *line)
4801 const char *buf = line->data;
4802 size_t bufsize = strlen(buf);
4805 while (!ferror(pipe) && written < bufsize) {
4806 written += fwrite(buf + written, 1, bufsize - written, pipe);
4811 return written == bufsize;
4815 stage_diff_write(FILE *pipe, struct line *line, struct line *end)
4817 while (line < end) {
4818 if (!stage_diff_line(pipe, line++))
4820 if (line->type == LINE_DIFF_CHUNK ||
4821 line->type == LINE_DIFF_HEADER)
4828 static struct line *
4829 stage_diff_find(struct view *view, struct line *line, enum line_type type)
4831 for (; view->line < line; line--)
4832 if (line->type == type)
4839 stage_apply_chunk(struct view *view, struct line *chunk, bool revert)
4841 char cmd[SIZEOF_STR];
4843 struct line *diff_hdr;
4846 diff_hdr = stage_diff_find(view, chunk, LINE_DIFF_HEADER);
4851 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4854 if (!string_format_from(cmd, &cmdsize,
4855 "git apply --whitespace=nowarn %s %s - && "
4856 "git update-index -q --unmerged --refresh 2>/dev/null",
4857 revert ? "" : "--cached",
4858 revert || stage_line_type == LINE_STAT_STAGED ? "-R" : ""))
4861 pipe = popen(cmd, "w");
4865 if (!stage_diff_write(pipe, diff_hdr, chunk) ||
4866 !stage_diff_write(pipe, chunk, view->line + view->lines))
4871 return chunk ? TRUE : FALSE;
4875 stage_update(struct view *view, struct line *line)
4877 struct line *chunk = NULL;
4879 if (!is_initial_commit() && stage_line_type != LINE_STAT_UNTRACKED)
4880 chunk = stage_diff_find(view, line, LINE_DIFF_CHUNK);
4883 if (!stage_apply_chunk(view, chunk, FALSE)) {
4884 report("Failed to apply chunk");
4888 } else if (!stage_status.status) {
4889 view = VIEW(REQ_VIEW_STATUS);
4891 for (line = view->line; line < view->line + view->lines; line++)
4892 if (line->type == stage_line_type)
4895 if (!status_update_files(view, line + 1)) {
4896 report("Failed to update files");
4900 } else if (!status_update_file(&stage_status, stage_line_type)) {
4901 report("Failed to update file");
4909 stage_revert(struct view *view, struct line *line)
4911 struct line *chunk = NULL;
4913 if (!is_initial_commit() && stage_line_type == LINE_STAT_UNSTAGED)
4914 chunk = stage_diff_find(view, line, LINE_DIFF_CHUNK);
4917 if (!prompt_yesno("Are you sure you want to revert changes?"))
4920 if (!stage_apply_chunk(view, chunk, TRUE)) {
4921 report("Failed to revert chunk");
4927 return status_revert(stage_status.status ? &stage_status : NULL,
4928 stage_line_type, FALSE);
4934 stage_next(struct view *view, struct line *line)
4938 if (!stage_chunks) {
4939 static size_t alloc = 0;
4942 for (line = view->line; line < view->line + view->lines; line++) {
4943 if (line->type != LINE_DIFF_CHUNK)
4946 tmp = realloc_items(stage_chunk, &alloc,
4947 stage_chunks, sizeof(*tmp));
4949 report("Allocation failure");
4954 stage_chunk[stage_chunks++] = line - view->line;
4958 for (i = 0; i < stage_chunks; i++) {
4959 if (stage_chunk[i] > view->lineno) {
4960 do_scroll_view(view, stage_chunk[i] - view->lineno);
4961 report("Chunk %d of %d", i + 1, stage_chunks);
4966 report("No next chunk found");
4970 stage_request(struct view *view, enum request request, struct line *line)
4973 case REQ_STATUS_UPDATE:
4974 if (!stage_update(view, line))
4978 case REQ_STATUS_REVERT:
4979 if (!stage_revert(view, line))
4983 case REQ_STAGE_NEXT:
4984 if (stage_line_type == LINE_STAT_UNTRACKED) {
4985 report("File is untracked; press %s to add",
4986 get_key(REQ_STATUS_UPDATE));
4989 stage_next(view, line);
4993 if (!stage_status.new.name[0])
4995 if (stage_status.status == 'D') {
4996 report("File has been deleted.");
5000 open_editor(stage_status.status != '?', stage_status.new.name);
5004 /* Reload everything ... */
5007 case REQ_VIEW_BLAME:
5008 if (stage_status.new.name[0]) {
5009 string_copy(opt_file, stage_status.new.name);
5015 return pager_request(view, request, line);
5021 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD | OPEN_NOMAXIMIZE);
5023 /* Check whether the staged entry still exists, and close the
5024 * stage view if it doesn't. */
5025 if (!status_exists(&stage_status, stage_line_type))
5026 return REQ_VIEW_CLOSE;
5028 if (stage_line_type == LINE_STAT_UNTRACKED) {
5029 if (!suffixcmp(stage_status.new.name, -1, "/")) {
5030 report("Cannot display a directory");
5034 opt_pipe = fopen(stage_status.new.name, "r");
5036 open_view(view, REQ_VIEW_STAGE, OPEN_REFRESH);
5041 static struct view_ops stage_ops = {
5057 char id[SIZEOF_REV]; /* SHA1 ID. */
5058 char title[128]; /* First line of the commit message. */
5059 char author[75]; /* Author of the commit. */
5060 struct tm time; /* Date from the author ident. */
5061 struct ref **refs; /* Repository references. */
5062 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
5063 size_t graph_size; /* The width of the graph array. */
5064 bool has_parents; /* Rewritten --parents seen. */
5067 /* Size of rev graph with no "padding" columns */
5068 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
5071 struct rev_graph *prev, *next, *parents;
5072 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
5074 struct commit *commit;
5076 unsigned int boundary:1;
5079 /* Parents of the commit being visualized. */
5080 static struct rev_graph graph_parents[4];
5082 /* The current stack of revisions on the graph. */
5083 static struct rev_graph graph_stacks[4] = {
5084 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
5085 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
5086 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
5087 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
5091 graph_parent_is_merge(struct rev_graph *graph)
5093 return graph->parents->size > 1;
5097 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
5099 struct commit *commit = graph->commit;
5101 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
5102 commit->graph[commit->graph_size++] = symbol;
5106 clear_rev_graph(struct rev_graph *graph)
5108 graph->boundary = 0;
5109 graph->size = graph->pos = 0;
5110 graph->commit = NULL;
5111 memset(graph->parents, 0, sizeof(*graph->parents));
5115 done_rev_graph(struct rev_graph *graph)
5117 if (graph_parent_is_merge(graph) &&
5118 graph->pos < graph->size - 1 &&
5119 graph->next->size == graph->size + graph->parents->size - 1) {
5120 size_t i = graph->pos + graph->parents->size - 1;
5122 graph->commit->graph_size = i * 2;
5123 while (i < graph->next->size - 1) {
5124 append_to_rev_graph(graph, ' ');
5125 append_to_rev_graph(graph, '\\');
5130 clear_rev_graph(graph);
5134 push_rev_graph(struct rev_graph *graph, const char *parent)
5138 /* "Collapse" duplicate parents lines.
5140 * FIXME: This needs to also update update the drawn graph but
5141 * for now it just serves as a method for pruning graph lines. */
5142 for (i = 0; i < graph->size; i++)
5143 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
5146 if (graph->size < SIZEOF_REVITEMS) {
5147 string_copy_rev(graph->rev[graph->size++], parent);
5152 get_rev_graph_symbol(struct rev_graph *graph)
5156 if (graph->boundary)
5157 symbol = REVGRAPH_BOUND;
5158 else if (graph->parents->size == 0)
5159 symbol = REVGRAPH_INIT;
5160 else if (graph_parent_is_merge(graph))
5161 symbol = REVGRAPH_MERGE;
5162 else if (graph->pos >= graph->size)
5163 symbol = REVGRAPH_BRANCH;
5165 symbol = REVGRAPH_COMMIT;
5171 draw_rev_graph(struct rev_graph *graph)
5174 chtype separator, line;
5176 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
5177 static struct rev_filler fillers[] = {
5183 chtype symbol = get_rev_graph_symbol(graph);
5184 struct rev_filler *filler;
5187 if (opt_line_graphics)
5188 fillers[DEFAULT].line = line_graphics[LINE_GRAPHIC_VLINE];
5190 filler = &fillers[DEFAULT];
5192 for (i = 0; i < graph->pos; i++) {
5193 append_to_rev_graph(graph, filler->line);
5194 if (graph_parent_is_merge(graph->prev) &&
5195 graph->prev->pos == i)
5196 filler = &fillers[RSHARP];
5198 append_to_rev_graph(graph, filler->separator);
5201 /* Place the symbol for this revision. */
5202 append_to_rev_graph(graph, symbol);
5204 if (graph->prev->size > graph->size)
5205 filler = &fillers[RDIAG];
5207 filler = &fillers[DEFAULT];
5211 for (; i < graph->size; i++) {
5212 append_to_rev_graph(graph, filler->separator);
5213 append_to_rev_graph(graph, filler->line);
5214 if (graph_parent_is_merge(graph->prev) &&
5215 i < graph->prev->pos + graph->parents->size)
5216 filler = &fillers[RSHARP];
5217 if (graph->prev->size > graph->size)
5218 filler = &fillers[LDIAG];
5221 if (graph->prev->size > graph->size) {
5222 append_to_rev_graph(graph, filler->separator);
5223 if (filler->line != ' ')
5224 append_to_rev_graph(graph, filler->line);
5228 /* Prepare the next rev graph */
5230 prepare_rev_graph(struct rev_graph *graph)
5234 /* First, traverse all lines of revisions up to the active one. */
5235 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
5236 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
5239 push_rev_graph(graph->next, graph->rev[graph->pos]);
5242 /* Interleave the new revision parent(s). */
5243 for (i = 0; !graph->boundary && i < graph->parents->size; i++)
5244 push_rev_graph(graph->next, graph->parents->rev[i]);
5246 /* Lastly, put any remaining revisions. */
5247 for (i = graph->pos + 1; i < graph->size; i++)
5248 push_rev_graph(graph->next, graph->rev[i]);
5252 update_rev_graph(struct rev_graph *graph)
5254 /* If this is the finalizing update ... */
5256 prepare_rev_graph(graph);
5258 /* Graph visualization needs a one rev look-ahead,
5259 * so the first update doesn't visualize anything. */
5260 if (!graph->prev->commit)
5263 draw_rev_graph(graph->prev);
5264 done_rev_graph(graph->prev->prev);
5273 main_draw(struct view *view, struct line *line, unsigned int lineno)
5275 struct commit *commit = line->data;
5277 if (!*commit->author)
5280 if (opt_date && draw_date(view, &commit->time))
5284 draw_field(view, LINE_MAIN_AUTHOR, commit->author, opt_author_cols, TRUE))
5287 if (opt_rev_graph && commit->graph_size &&
5288 draw_graphic(view, LINE_MAIN_REVGRAPH, commit->graph, commit->graph_size))
5291 if (opt_show_refs && commit->refs) {
5295 enum line_type type;
5297 if (commit->refs[i]->head)
5298 type = LINE_MAIN_HEAD;
5299 else if (commit->refs[i]->ltag)
5300 type = LINE_MAIN_LOCAL_TAG;
5301 else if (commit->refs[i]->tag)
5302 type = LINE_MAIN_TAG;
5303 else if (commit->refs[i]->tracked)
5304 type = LINE_MAIN_TRACKED;
5305 else if (commit->refs[i]->remote)
5306 type = LINE_MAIN_REMOTE;
5308 type = LINE_MAIN_REF;
5310 if (draw_text(view, type, "[", TRUE) ||
5311 draw_text(view, type, commit->refs[i]->name, TRUE) ||
5312 draw_text(view, type, "]", TRUE))
5315 if (draw_text(view, LINE_DEFAULT, " ", TRUE))
5317 } while (commit->refs[i++]->next);
5320 draw_text(view, LINE_DEFAULT, commit->title, TRUE);
5324 /* Reads git log --pretty=raw output and parses it into the commit struct. */
5326 main_read(struct view *view, char *line)
5328 static struct rev_graph *graph = graph_stacks;
5329 enum line_type type;
5330 struct commit *commit;
5335 if (!view->lines && !view->parent)
5336 die("No revisions match the given arguments.");
5337 if (view->lines > 0) {
5338 commit = view->line[view->lines - 1].data;
5339 if (!*commit->author) {
5342 graph->commit = NULL;
5345 update_rev_graph(graph);
5347 for (i = 0; i < ARRAY_SIZE(graph_stacks); i++)
5348 clear_rev_graph(&graph_stacks[i]);
5352 type = get_line_type(line);
5353 if (type == LINE_COMMIT) {
5354 commit = calloc(1, sizeof(struct commit));
5358 line += STRING_SIZE("commit ");
5360 graph->boundary = 1;
5364 string_copy_rev(commit->id, line);
5365 commit->refs = get_refs(commit->id);
5366 graph->commit = commit;
5367 add_line_data(view, commit, LINE_MAIN_COMMIT);
5369 while ((line = strchr(line, ' '))) {
5371 push_rev_graph(graph->parents, line);
5372 commit->has_parents = TRUE;
5379 commit = view->line[view->lines - 1].data;
5383 if (commit->has_parents)
5385 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
5390 /* Parse author lines where the name may be empty:
5391 * author <email@address.tld> 1138474660 +0100
5393 char *ident = line + STRING_SIZE("author ");
5394 char *nameend = strchr(ident, '<');
5395 char *emailend = strchr(ident, '>');
5397 if (!nameend || !emailend)
5400 update_rev_graph(graph);
5401 graph = graph->next;
5403 *nameend = *emailend = 0;
5404 ident = chomp_string(ident);
5406 ident = chomp_string(nameend + 1);
5411 string_ncopy(commit->author, ident, strlen(ident));
5413 /* Parse epoch and timezone */
5414 if (emailend[1] == ' ') {
5415 char *secs = emailend + 2;
5416 char *zone = strchr(secs, ' ');
5417 time_t time = (time_t) atol(secs);
5419 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
5423 tz = ('0' - zone[1]) * 60 * 60 * 10;
5424 tz += ('0' - zone[2]) * 60 * 60;
5425 tz += ('0' - zone[3]) * 60;
5426 tz += ('0' - zone[4]) * 60;
5434 gmtime_r(&time, &commit->time);
5439 /* Fill in the commit title if it has not already been set. */
5440 if (commit->title[0])
5443 /* Require titles to start with a non-space character at the
5444 * offset used by git log. */
5445 if (strncmp(line, " ", 4))
5448 /* Well, if the title starts with a whitespace character,
5449 * try to be forgiving. Otherwise we end up with no title. */
5450 while (isspace(*line))
5454 /* FIXME: More graceful handling of titles; append "..." to
5455 * shortened titles, etc. */
5457 string_ncopy(commit->title, line, strlen(line));
5464 main_request(struct view *view, enum request request, struct line *line)
5466 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
5470 open_view(view, REQ_VIEW_DIFF, flags);
5474 open_view(view, REQ_VIEW_MAIN, OPEN_REFRESH);
5484 grep_refs(struct ref **refs, regex_t *regex)
5492 if (regexec(regex, refs[i]->name, 1, &pmatch, 0) != REG_NOMATCH)
5494 } while (refs[i++]->next);
5500 main_grep(struct view *view, struct line *line)
5502 struct commit *commit = line->data;
5503 enum { S_TITLE, S_AUTHOR, S_DATE, S_REFS, S_END } state;
5504 char buf[DATE_COLS + 1];
5507 for (state = S_TITLE; state < S_END; state++) {
5511 case S_TITLE: text = commit->title; break;
5515 text = commit->author;
5520 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
5527 if (grep_refs(commit->refs, view->regex) == TRUE)
5534 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
5542 main_select(struct view *view, struct line *line)
5544 struct commit *commit = line->data;
5546 string_copy_rev(view->ref, commit->id);
5547 string_copy_rev(ref_commit, view->ref);
5550 static struct view_ops main_ops = {
5562 * Unicode / UTF-8 handling
5564 * NOTE: Much of the following code for dealing with unicode is derived from
5565 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
5566 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
5569 /* I've (over)annotated a lot of code snippets because I am not entirely
5570 * confident that the approach taken by this small UTF-8 interface is correct.
5574 unicode_width(unsigned long c)
5577 (c <= 0x115f /* Hangul Jamo */
5580 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
5582 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
5583 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
5584 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
5585 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
5586 || (c >= 0xffe0 && c <= 0xffe6)
5587 || (c >= 0x20000 && c <= 0x2fffd)
5588 || (c >= 0x30000 && c <= 0x3fffd)))
5592 return opt_tab_size;
5597 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
5598 * Illegal bytes are set one. */
5599 static const unsigned char utf8_bytes[256] = {
5600 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,
5601 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,
5602 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,
5603 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,
5604 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,
5605 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,
5606 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,
5607 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,
5610 /* Decode UTF-8 multi-byte representation into a unicode character. */
5611 static inline unsigned long
5612 utf8_to_unicode(const char *string, size_t length)
5614 unsigned long unicode;
5618 unicode = string[0];
5621 unicode = (string[0] & 0x1f) << 6;
5622 unicode += (string[1] & 0x3f);
5625 unicode = (string[0] & 0x0f) << 12;
5626 unicode += ((string[1] & 0x3f) << 6);
5627 unicode += (string[2] & 0x3f);
5630 unicode = (string[0] & 0x0f) << 18;
5631 unicode += ((string[1] & 0x3f) << 12);
5632 unicode += ((string[2] & 0x3f) << 6);
5633 unicode += (string[3] & 0x3f);
5636 unicode = (string[0] & 0x0f) << 24;
5637 unicode += ((string[1] & 0x3f) << 18);
5638 unicode += ((string[2] & 0x3f) << 12);
5639 unicode += ((string[3] & 0x3f) << 6);
5640 unicode += (string[4] & 0x3f);
5643 unicode = (string[0] & 0x01) << 30;
5644 unicode += ((string[1] & 0x3f) << 24);
5645 unicode += ((string[2] & 0x3f) << 18);
5646 unicode += ((string[3] & 0x3f) << 12);
5647 unicode += ((string[4] & 0x3f) << 6);
5648 unicode += (string[5] & 0x3f);
5651 die("Invalid unicode length");
5654 /* Invalid characters could return the special 0xfffd value but NUL
5655 * should be just as good. */
5656 return unicode > 0xffff ? 0 : unicode;
5659 /* Calculates how much of string can be shown within the given maximum width
5660 * and sets trimmed parameter to non-zero value if all of string could not be
5661 * shown. If the reserve flag is TRUE, it will reserve at least one
5662 * trailing character, which can be useful when drawing a delimiter.
5664 * Returns the number of bytes to output from string to satisfy max_width. */
5666 utf8_length(const char *string, int *width, size_t max_width, int *trimmed, bool reserve)
5668 const char *start = string;
5669 const char *end = strchr(string, '\0');
5670 unsigned char last_bytes = 0;
5671 size_t last_ucwidth = 0;
5676 while (string < end) {
5677 int c = *(unsigned char *) string;
5678 unsigned char bytes = utf8_bytes[c];
5680 unsigned long unicode;
5682 if (string + bytes > end)
5685 /* Change representation to figure out whether
5686 * it is a single- or double-width character. */
5688 unicode = utf8_to_unicode(string, bytes);
5689 /* FIXME: Graceful handling of invalid unicode character. */
5693 ucwidth = unicode_width(unicode);
5695 if (*width > max_width) {
5698 if (reserve && *width == max_width) {
5699 string -= last_bytes;
5700 *width -= last_ucwidth;
5707 last_ucwidth = ucwidth;
5710 return string - start;
5718 /* Whether or not the curses interface has been initialized. */
5719 static bool cursed = FALSE;
5721 /* The status window is used for polling keystrokes. */
5722 static WINDOW *status_win;
5724 static bool status_empty = TRUE;
5726 /* Update status and title window. */
5728 report(const char *msg, ...)
5730 struct view *view = display[current_view];
5736 char buf[SIZEOF_STR];
5739 va_start(args, msg);
5740 if (vsnprintf(buf, sizeof(buf), msg, args) >= sizeof(buf)) {
5741 buf[sizeof(buf) - 1] = 0;
5742 buf[sizeof(buf) - 2] = '.';
5743 buf[sizeof(buf) - 3] = '.';
5744 buf[sizeof(buf) - 4] = '.';
5750 if (!status_empty || *msg) {
5753 va_start(args, msg);
5755 wmove(status_win, 0, 0);
5757 vwprintw(status_win, msg, args);
5758 status_empty = FALSE;
5760 status_empty = TRUE;
5762 wclrtoeol(status_win);
5763 wrefresh(status_win);
5768 update_view_title(view);
5769 update_display_cursor(view);
5772 /* Controls when nodelay should be in effect when polling user input. */
5774 set_nonblocking_input(bool loading)
5776 static unsigned int loading_views;
5778 if ((loading == FALSE && loading_views-- == 1) ||
5779 (loading == TRUE && loading_views++ == 0))
5780 nodelay(status_win, loading);
5788 /* Initialize the curses library */
5789 if (isatty(STDIN_FILENO)) {
5790 cursed = !!initscr();
5793 /* Leave stdin and stdout alone when acting as a pager. */
5794 opt_tty = fopen("/dev/tty", "r+");
5796 die("Failed to open /dev/tty");
5797 cursed = !!newterm(NULL, opt_tty, opt_tty);
5801 die("Failed to initialize curses");
5803 nonl(); /* Tell curses not to do NL->CR/NL on output */
5804 cbreak(); /* Take input chars one at a time, no wait for \n */
5805 noecho(); /* Don't echo input */
5806 leaveok(stdscr, TRUE);
5811 getmaxyx(stdscr, y, x);
5812 status_win = newwin(1, 0, y - 1, 0);
5814 die("Failed to create status window");
5816 /* Enable keyboard mapping */
5817 keypad(status_win, TRUE);
5818 wbkgdset(status_win, get_line_attr(LINE_STATUS));
5820 TABSIZE = opt_tab_size;
5821 if (opt_line_graphics) {
5822 line_graphics[LINE_GRAPHIC_VLINE] = ACS_VLINE;
5827 prompt_yesno(const char *prompt)
5829 enum { WAIT, STOP, CANCEL } status = WAIT;
5830 bool answer = FALSE;
5832 while (status == WAIT) {
5838 foreach_view (view, i)
5843 mvwprintw(status_win, 0, 0, "%s [Yy]/[Nn]", prompt);
5844 wclrtoeol(status_win);
5846 /* Refresh, accept single keystroke of input */
5847 key = wgetch(status_win);
5871 /* Clear the status window */
5872 status_empty = FALSE;
5879 read_prompt(const char *prompt)
5881 enum { READING, STOP, CANCEL } status = READING;
5882 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
5885 while (status == READING) {
5891 foreach_view (view, i)
5896 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
5897 wclrtoeol(status_win);
5899 /* Refresh, accept single keystroke of input */
5900 key = wgetch(status_win);
5905 status = pos ? STOP : CANCEL;
5923 if (pos >= sizeof(buf)) {
5924 report("Input string too long");
5929 buf[pos++] = (char) key;
5933 /* Clear the status window */
5934 status_empty = FALSE;
5937 if (status == CANCEL)
5946 * Repository references
5949 static struct ref *refs = NULL;
5950 static size_t refs_alloc = 0;
5951 static size_t refs_size = 0;
5953 /* Id <-> ref store */
5954 static struct ref ***id_refs = NULL;
5955 static size_t id_refs_alloc = 0;
5956 static size_t id_refs_size = 0;
5959 compare_refs(const void *ref1_, const void *ref2_)
5961 const struct ref *ref1 = *(const struct ref **)ref1_;
5962 const struct ref *ref2 = *(const struct ref **)ref2_;
5964 if (ref1->tag != ref2->tag)
5965 return ref2->tag - ref1->tag;
5966 if (ref1->ltag != ref2->ltag)
5967 return ref2->ltag - ref2->ltag;
5968 if (ref1->head != ref2->head)
5969 return ref2->head - ref1->head;
5970 if (ref1->tracked != ref2->tracked)
5971 return ref2->tracked - ref1->tracked;
5972 if (ref1->remote != ref2->remote)
5973 return ref2->remote - ref1->remote;
5974 return strcmp(ref1->name, ref2->name);
5977 static struct ref **
5978 get_refs(const char *id)
5980 struct ref ***tmp_id_refs;
5981 struct ref **ref_list = NULL;
5982 size_t ref_list_alloc = 0;
5983 size_t ref_list_size = 0;
5986 for (i = 0; i < id_refs_size; i++)
5987 if (!strcmp(id, id_refs[i][0]->id))
5990 tmp_id_refs = realloc_items(id_refs, &id_refs_alloc, id_refs_size + 1,
5995 id_refs = tmp_id_refs;
5997 for (i = 0; i < refs_size; i++) {
6000 if (strcmp(id, refs[i].id))
6003 tmp = realloc_items(ref_list, &ref_list_alloc,
6004 ref_list_size + 1, sizeof(*ref_list));
6012 ref_list[ref_list_size] = &refs[i];
6013 /* XXX: The properties of the commit chains ensures that we can
6014 * safely modify the shared ref. The repo references will
6015 * always be similar for the same id. */
6016 ref_list[ref_list_size]->next = 1;
6022 qsort(ref_list, ref_list_size, sizeof(*ref_list), compare_refs);
6023 ref_list[ref_list_size - 1]->next = 0;
6024 id_refs[id_refs_size++] = ref_list;
6031 read_ref(char *id, size_t idlen, char *name, size_t namelen)
6036 bool remote = FALSE;
6037 bool tracked = FALSE;
6038 bool check_replace = FALSE;
6041 if (!prefixcmp(name, "refs/tags/")) {
6042 if (!suffixcmp(name, namelen, "^{}")) {
6045 if (refs_size > 0 && refs[refs_size - 1].ltag == TRUE)
6046 check_replace = TRUE;
6052 namelen -= STRING_SIZE("refs/tags/");
6053 name += STRING_SIZE("refs/tags/");
6055 } else if (!prefixcmp(name, "refs/remotes/")) {
6057 namelen -= STRING_SIZE("refs/remotes/");
6058 name += STRING_SIZE("refs/remotes/");
6059 tracked = !strcmp(opt_remote, name);
6061 } else if (!prefixcmp(name, "refs/heads/")) {
6062 namelen -= STRING_SIZE("refs/heads/");
6063 name += STRING_SIZE("refs/heads/");
6064 head = !strncmp(opt_head, name, namelen);
6066 } else if (!strcmp(name, "HEAD")) {
6067 string_ncopy(opt_head_rev, id, idlen);
6071 if (check_replace && !strcmp(name, refs[refs_size - 1].name)) {
6072 /* it's an annotated tag, replace the previous sha1 with the
6073 * resolved commit id; relies on the fact git-ls-remote lists
6074 * the commit id of an annotated tag right before the commit id
6076 refs[refs_size - 1].ltag = ltag;
6077 string_copy_rev(refs[refs_size - 1].id, id);
6081 refs = realloc_items(refs, &refs_alloc, refs_size + 1, sizeof(*refs));
6085 ref = &refs[refs_size++];
6086 ref->name = malloc(namelen + 1);
6090 strncpy(ref->name, name, namelen);
6091 ref->name[namelen] = 0;
6095 ref->remote = remote;
6096 ref->tracked = tracked;
6097 string_copy_rev(ref->id, id);
6105 const char *cmd_env = getenv("TIG_LS_REMOTE");
6106 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
6111 while (refs_size > 0)
6112 free(refs[--refs_size].name);
6113 while (id_refs_size > 0)
6114 free(id_refs[--id_refs_size]);
6116 return read_properties(popen(cmd, "r"), "\t", read_ref);
6120 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
6122 if (!strcmp(name, "i18n.commitencoding"))
6123 string_ncopy(opt_encoding, value, valuelen);
6125 if (!strcmp(name, "core.editor"))
6126 string_ncopy(opt_editor, value, valuelen);
6128 /* branch.<head>.remote */
6130 !strncmp(name, "branch.", 7) &&
6131 !strncmp(name + 7, opt_head, strlen(opt_head)) &&
6132 !strcmp(name + 7 + strlen(opt_head), ".remote"))
6133 string_ncopy(opt_remote, value, valuelen);
6135 if (*opt_head && *opt_remote &&
6136 !strncmp(name, "branch.", 7) &&
6137 !strncmp(name + 7, opt_head, strlen(opt_head)) &&
6138 !strcmp(name + 7 + strlen(opt_head), ".merge")) {
6139 size_t from = strlen(opt_remote);
6141 if (!prefixcmp(value, "refs/heads/")) {
6142 value += STRING_SIZE("refs/heads/");
6143 valuelen -= STRING_SIZE("refs/heads/");
6146 if (!string_format_from(opt_remote, &from, "/%s", value))
6154 load_git_config(void)
6156 return read_properties(popen("git " GIT_CONFIG " --list", "r"),
6157 "=", read_repo_config_option);
6161 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
6163 if (!opt_git_dir[0]) {
6164 string_ncopy(opt_git_dir, name, namelen);
6166 } else if (opt_is_inside_work_tree == -1) {
6167 /* This can be 3 different values depending on the
6168 * version of git being used. If git-rev-parse does not
6169 * understand --is-inside-work-tree it will simply echo
6170 * the option else either "true" or "false" is printed.
6171 * Default to true for the unknown case. */
6172 opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
6174 } else if (opt_cdup[0] == ' ') {
6175 string_ncopy(opt_cdup, name, namelen);
6177 if (!prefixcmp(name, "refs/heads/")) {
6178 namelen -= STRING_SIZE("refs/heads/");
6179 name += STRING_SIZE("refs/heads/");
6180 string_ncopy(opt_head, name, namelen);
6188 load_repo_info(void)
6191 FILE *pipe = popen("(git rev-parse --git-dir --is-inside-work-tree "
6192 " --show-cdup; git symbolic-ref HEAD) 2>/dev/null", "r");
6194 /* XXX: The line outputted by "--show-cdup" can be empty so
6195 * initialize it to something invalid to make it possible to
6196 * detect whether it has been set or not. */
6199 result = read_properties(pipe, "=", read_repo_info);
6200 if (opt_cdup[0] == ' ')
6207 read_properties(FILE *pipe, const char *separators,
6208 int (*read_property)(char *, size_t, char *, size_t))
6210 char buffer[BUFSIZ];
6217 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
6222 name = chomp_string(name);
6223 namelen = strcspn(name, separators);
6225 if (name[namelen]) {
6227 value = chomp_string(name + namelen + 1);
6228 valuelen = strlen(value);
6235 state = read_property(name, namelen, value, valuelen);
6238 if (state != ERR && ferror(pipe))
6251 static void __NORETURN
6254 /* XXX: Restore tty modes and let the OS cleanup the rest! */
6260 static void __NORETURN
6261 die(const char *err, ...)
6267 va_start(args, err);
6268 fputs("tig: ", stderr);
6269 vfprintf(stderr, err, args);
6270 fputs("\n", stderr);
6277 warn(const char *msg, ...)
6281 va_start(args, msg);
6282 fputs("tig warning: ", stderr);
6283 vfprintf(stderr, msg, args);
6284 fputs("\n", stderr);
6289 main(int argc, const char *argv[])
6292 enum request request;
6295 signal(SIGINT, quit);
6297 if (setlocale(LC_ALL, "")) {
6298 char *codeset = nl_langinfo(CODESET);
6300 string_ncopy(opt_codeset, codeset, strlen(codeset));
6303 if (load_repo_info() == ERR)
6304 die("Failed to load repo info.");
6306 if (load_options() == ERR)
6307 die("Failed to load user config.");
6309 if (load_git_config() == ERR)
6310 die("Failed to load repo config.");
6312 request = parse_options(argc, argv);
6313 if (request == REQ_NONE)
6316 /* Require a git repository unless when running in pager mode. */
6317 if (!opt_git_dir[0] && request != REQ_VIEW_PAGER)
6318 die("Not a git repository");
6320 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
6323 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
6324 opt_iconv = iconv_open(opt_codeset, opt_encoding);
6325 if (opt_iconv == ICONV_NONE)
6326 die("Failed to initialize character set conversion");
6329 if (load_refs() == ERR)
6330 die("Failed to load refs.");
6332 foreach_view (view, i)
6333 view->cmd_env = getenv(view->cmd_env);
6337 while (view_driver(display[current_view], request)) {
6341 foreach_view (view, i)
6343 view = display[current_view];
6345 /* Refresh, accept single keystroke of input */
6346 key = wgetch(status_win);
6348 /* wgetch() with nodelay() enabled returns ERR when there's no
6355 request = get_keybinding(view->keymap, key);
6357 /* Some low-level request handling. This keeps access to
6358 * status_win restricted. */
6362 char *cmd = read_prompt(":");
6364 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
6365 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
6366 request = REQ_VIEW_DIFF;
6368 request = REQ_VIEW_PAGER;
6371 /* Always reload^Wrerun commands from the prompt. */
6372 open_view(view, request, OPEN_RELOAD);
6379 case REQ_SEARCH_BACK:
6381 const char *prompt = request == REQ_SEARCH ? "/" : "?";
6382 char *search = read_prompt(prompt);
6385 string_ncopy(opt_search, search, strlen(search));
6390 case REQ_SCREEN_RESIZE:
6394 getmaxyx(stdscr, height, width);
6396 /* Resize the status view and let the view driver take
6397 * care of resizing the displayed views. */
6398 wresize(status_win, 1, width);
6399 mvwin(status_win, height - 1, 0);
6400 wrefresh(status_win);