Do not reload status and stage views on errors
[tig.git] / tig.c
blob83523de419bc34c1036a014881bb3629f7d8c52d
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.
14 #ifdef HAVE_CONFIG_H
15 #include "config.h"
16 #endif
18 #ifndef TIG_VERSION
19 #define TIG_VERSION "unknown-version"
20 #endif
22 #ifndef DEBUG
23 #define NDEBUG
24 #endif
26 #include <assert.h>
27 #include <errno.h>
28 #include <ctype.h>
29 #include <signal.h>
30 #include <stdarg.h>
31 #include <stdio.h>
32 #include <stdlib.h>
33 #include <string.h>
34 #include <sys/types.h>
35 #include <sys/stat.h>
36 #include <unistd.h>
37 #include <time.h>
39 #include <regex.h>
41 #include <locale.h>
42 #include <langinfo.h>
43 #include <iconv.h>
45 /* ncurses(3): Must be defined to have extended wide-character functions. */
46 #define _XOPEN_SOURCE_EXTENDED
48 #include <curses.h>
50 #if __GNUC__ >= 3
51 #define __NORETURN __attribute__((__noreturn__))
52 #else
53 #define __NORETURN
54 #endif
56 static void __NORETURN die(const char *err, ...);
57 static void warn(const char *msg, ...);
58 static void report(const char *msg, ...);
59 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, size_t, char *, size_t));
60 static void set_nonblocking_input(bool loading);
61 static size_t utf8_length(const char *string, size_t max_width, int *trimmed, bool reserve);
63 #define ABS(x) ((x) >= 0 ? (x) : -(x))
64 #define MIN(x, y) ((x) < (y) ? (x) : (y))
66 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
67 #define STRING_SIZE(x) (sizeof(x) - 1)
69 #define SIZEOF_STR 1024 /* Default string size. */
70 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
71 #define SIZEOF_REV 41 /* Holds a SHA-1 and an ending NUL */
73 /* Revision graph */
75 #define REVGRAPH_INIT 'I'
76 #define REVGRAPH_MERGE 'M'
77 #define REVGRAPH_BRANCH '+'
78 #define REVGRAPH_COMMIT '*'
79 #define REVGRAPH_BOUND '^'
80 #define REVGRAPH_LINE '|'
82 #define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
84 /* This color name can be used to refer to the default term colors. */
85 #define COLOR_DEFAULT (-1)
87 #define ICONV_NONE ((iconv_t) -1)
88 #ifndef ICONV_CONST
89 #define ICONV_CONST /* nothing */
90 #endif
92 /* The format and size of the date column in the main view. */
93 #define DATE_FORMAT "%Y-%m-%d %H:%M"
94 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
96 #define AUTHOR_COLS 20
97 #define ID_COLS 8
99 /* The default interval between line numbers. */
100 #define NUMBER_INTERVAL 5
102 #define TABSIZE 8
104 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
106 #define NULL_ID "0000000000000000000000000000000000000000"
108 #ifndef GIT_CONFIG
109 #define GIT_CONFIG "git config"
110 #endif
112 #define TIG_LS_REMOTE \
113 "git ls-remote $(git rev-parse --git-dir) 2>/dev/null"
115 #define TIG_DIFF_CMD \
116 "git show --pretty=fuller --no-color --root --patch-with-stat --find-copies-harder -C %s 2>/dev/null"
118 #define TIG_LOG_CMD \
119 "git log --no-color --cc --stat -n100 %s 2>/dev/null"
121 #define TIG_MAIN_CMD \
122 "git log --no-color --topo-order --parents --boundary --pretty=raw %s 2>/dev/null"
124 #define TIG_TREE_CMD \
125 "git ls-tree %s %s"
127 #define TIG_BLOB_CMD \
128 "git cat-file blob %s"
130 /* XXX: Needs to be defined to the empty string. */
131 #define TIG_HELP_CMD ""
132 #define TIG_PAGER_CMD ""
133 #define TIG_STATUS_CMD ""
134 #define TIG_STAGE_CMD ""
135 #define TIG_BLAME_CMD ""
137 /* Some ascii-shorthands fitted into the ncurses namespace. */
138 #define KEY_TAB '\t'
139 #define KEY_RETURN '\r'
140 #define KEY_ESC 27
143 struct ref {
144 char *name; /* Ref name; tag or head names are shortened. */
145 char id[SIZEOF_REV]; /* Commit SHA1 ID */
146 unsigned int head:1; /* Is it the current HEAD? */
147 unsigned int tag:1; /* Is it a tag? */
148 unsigned int ltag:1; /* If so, is the tag local? */
149 unsigned int remote:1; /* Is it a remote ref? */
150 unsigned int tracked:1; /* Is it the remote for the current HEAD? */
151 unsigned int next:1; /* For ref lists: are there more refs? */
154 static struct ref **get_refs(char *id);
156 struct int_map {
157 const char *name;
158 int namelen;
159 int value;
162 static int
163 set_from_int_map(struct int_map *map, size_t map_size,
164 int *value, const char *name, int namelen)
167 int i;
169 for (i = 0; i < map_size; i++)
170 if (namelen == map[i].namelen &&
171 !strncasecmp(name, map[i].name, namelen)) {
172 *value = map[i].value;
173 return OK;
176 return ERR;
181 * String helpers
184 static inline void
185 string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
187 if (srclen > dstlen - 1)
188 srclen = dstlen - 1;
190 strncpy(dst, src, srclen);
191 dst[srclen] = 0;
194 /* Shorthands for safely copying into a fixed buffer. */
196 #define string_copy(dst, src) \
197 string_ncopy_do(dst, sizeof(dst), src, sizeof(src))
199 #define string_ncopy(dst, src, srclen) \
200 string_ncopy_do(dst, sizeof(dst), src, srclen)
202 #define string_copy_rev(dst, src) \
203 string_ncopy_do(dst, SIZEOF_REV, src, SIZEOF_REV - 1)
205 #define string_add(dst, from, src) \
206 string_ncopy_do(dst + (from), sizeof(dst) - (from), src, sizeof(src))
208 static char *
209 chomp_string(char *name)
211 int namelen;
213 while (isspace(*name))
214 name++;
216 namelen = strlen(name) - 1;
217 while (namelen > 0 && isspace(name[namelen]))
218 name[namelen--] = 0;
220 return name;
223 static bool
224 string_nformat(char *buf, size_t bufsize, size_t *bufpos, const char *fmt, ...)
226 va_list args;
227 size_t pos = bufpos ? *bufpos : 0;
229 va_start(args, fmt);
230 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
231 va_end(args);
233 if (bufpos)
234 *bufpos = pos;
236 return pos >= bufsize ? FALSE : TRUE;
239 #define string_format(buf, fmt, args...) \
240 string_nformat(buf, sizeof(buf), NULL, fmt, args)
242 #define string_format_from(buf, from, fmt, args...) \
243 string_nformat(buf, sizeof(buf), from, fmt, args)
245 static int
246 string_enum_compare(const char *str1, const char *str2, int len)
248 size_t i;
250 #define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
252 /* Diff-Header == DIFF_HEADER */
253 for (i = 0; i < len; i++) {
254 if (toupper(str1[i]) == toupper(str2[i]))
255 continue;
257 if (string_enum_sep(str1[i]) &&
258 string_enum_sep(str2[i]))
259 continue;
261 return str1[i] - str2[i];
264 return 0;
267 /* Shell quoting
269 * NOTE: The following is a slightly modified copy of the git project's shell
270 * quoting routines found in the quote.c file.
272 * Help to copy the thing properly quoted for the shell safety. any single
273 * quote is replaced with '\'', any exclamation point is replaced with '\!',
274 * and the whole thing is enclosed in a
276 * E.g.
277 * original sq_quote result
278 * name ==> name ==> 'name'
279 * a b ==> a b ==> 'a b'
280 * a'b ==> a'\''b ==> 'a'\''b'
281 * a!b ==> a'\!'b ==> 'a'\!'b'
284 static size_t
285 sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
287 char c;
289 #define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0)
291 BUFPUT('\'');
292 while ((c = *src++)) {
293 if (c == '\'' || c == '!') {
294 BUFPUT('\'');
295 BUFPUT('\\');
296 BUFPUT(c);
297 BUFPUT('\'');
298 } else {
299 BUFPUT(c);
302 BUFPUT('\'');
304 if (bufsize < SIZEOF_STR)
305 buf[bufsize] = 0;
307 return bufsize;
312 * User requests
315 #define REQ_INFO \
316 /* XXX: Keep the view request first and in sync with views[]. */ \
317 REQ_GROUP("View switching") \
318 REQ_(VIEW_MAIN, "Show main view"), \
319 REQ_(VIEW_DIFF, "Show diff view"), \
320 REQ_(VIEW_LOG, "Show log view"), \
321 REQ_(VIEW_TREE, "Show tree view"), \
322 REQ_(VIEW_BLOB, "Show blob view"), \
323 REQ_(VIEW_BLAME, "Show blame view"), \
324 REQ_(VIEW_HELP, "Show help page"), \
325 REQ_(VIEW_PAGER, "Show pager view"), \
326 REQ_(VIEW_STATUS, "Show status view"), \
327 REQ_(VIEW_STAGE, "Show stage view"), \
329 REQ_GROUP("View manipulation") \
330 REQ_(ENTER, "Enter current line and scroll"), \
331 REQ_(NEXT, "Move to next"), \
332 REQ_(PREVIOUS, "Move to previous"), \
333 REQ_(VIEW_NEXT, "Move focus to next view"), \
334 REQ_(REFRESH, "Reload and refresh"), \
335 REQ_(MAXIMIZE, "Maximize the current view"), \
336 REQ_(VIEW_CLOSE, "Close the current view"), \
337 REQ_(QUIT, "Close all views and quit"), \
339 REQ_GROUP("Cursor navigation") \
340 REQ_(MOVE_UP, "Move cursor one line up"), \
341 REQ_(MOVE_DOWN, "Move cursor one line down"), \
342 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
343 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
344 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
345 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
347 REQ_GROUP("Scrolling") \
348 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
349 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
350 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
351 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
353 REQ_GROUP("Searching") \
354 REQ_(SEARCH, "Search the view"), \
355 REQ_(SEARCH_BACK, "Search backwards in the view"), \
356 REQ_(FIND_NEXT, "Find next search match"), \
357 REQ_(FIND_PREV, "Find previous search match"), \
359 REQ_GROUP("Misc") \
360 REQ_(PROMPT, "Bring up the prompt"), \
361 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
362 REQ_(SCREEN_RESIZE, "Resize the screen"), \
363 REQ_(SHOW_VERSION, "Show version information"), \
364 REQ_(STOP_LOADING, "Stop all loading views"), \
365 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
366 REQ_(TOGGLE_DATE, "Toggle date display"), \
367 REQ_(TOGGLE_AUTHOR, "Toggle author display"), \
368 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"), \
369 REQ_(TOGGLE_REFS, "Toggle reference display (tags/branches)"), \
370 REQ_(STATUS_UPDATE, "Update file status"), \
371 REQ_(STATUS_MERGE, "Merge file using external tool"), \
372 REQ_(TREE_PARENT, "Switch to parent directory in tree view"), \
373 REQ_(EDIT, "Open in editor"), \
374 REQ_(NONE, "Do nothing")
377 /* User action requests. */
378 enum request {
379 #define REQ_GROUP(help)
380 #define REQ_(req, help) REQ_##req
382 /* Offset all requests to avoid conflicts with ncurses getch values. */
383 REQ_OFFSET = KEY_MAX + 1,
384 REQ_INFO
386 #undef REQ_GROUP
387 #undef REQ_
390 struct request_info {
391 enum request request;
392 char *name;
393 int namelen;
394 char *help;
397 static struct request_info req_info[] = {
398 #define REQ_GROUP(help) { 0, NULL, 0, (help) },
399 #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
400 REQ_INFO
401 #undef REQ_GROUP
402 #undef REQ_
405 static enum request
406 get_request(const char *name)
408 int namelen = strlen(name);
409 int i;
411 for (i = 0; i < ARRAY_SIZE(req_info); i++)
412 if (req_info[i].namelen == namelen &&
413 !string_enum_compare(req_info[i].name, name, namelen))
414 return req_info[i].request;
416 return REQ_NONE;
421 * Options
424 static const char usage[] =
425 "tig " TIG_VERSION " (" __DATE__ ")\n"
426 "\n"
427 "Usage: tig [options] [revs] [--] [paths]\n"
428 " or: tig show [options] [revs] [--] [paths]\n"
429 " or: tig blame [rev] path\n"
430 " or: tig status\n"
431 " or: tig < [git command output]\n"
432 "\n"
433 "Options:\n"
434 " -v, --version Show version and exit\n"
435 " -h, --help Show help message and exit";
437 /* Option and state variables. */
438 static bool opt_date = TRUE;
439 static bool opt_author = TRUE;
440 static bool opt_line_number = FALSE;
441 static bool opt_rev_graph = FALSE;
442 static bool opt_show_refs = TRUE;
443 static int opt_num_interval = NUMBER_INTERVAL;
444 static int opt_tab_size = TABSIZE;
445 static enum request opt_request = REQ_VIEW_MAIN;
446 static char opt_cmd[SIZEOF_STR] = "";
447 static char opt_path[SIZEOF_STR] = "";
448 static char opt_file[SIZEOF_STR] = "";
449 static char opt_ref[SIZEOF_REF] = "";
450 static char opt_head[SIZEOF_REF] = "";
451 static char opt_remote[SIZEOF_REF] = "";
452 static bool opt_no_head = TRUE;
453 static FILE *opt_pipe = NULL;
454 static char opt_encoding[20] = "UTF-8";
455 static bool opt_utf8 = TRUE;
456 static char opt_codeset[20] = "UTF-8";
457 static iconv_t opt_iconv = ICONV_NONE;
458 static char opt_search[SIZEOF_STR] = "";
459 static char opt_cdup[SIZEOF_STR] = "";
460 static char opt_git_dir[SIZEOF_STR] = "";
461 static signed char opt_is_inside_work_tree = -1; /* set to TRUE or FALSE */
462 static char opt_editor[SIZEOF_STR] = "";
464 static bool
465 parse_options(int argc, char *argv[])
467 size_t buf_size;
468 char *subcommand;
469 bool seen_dashdash = FALSE;
470 int i;
472 if (!isatty(STDIN_FILENO)) {
473 opt_request = REQ_VIEW_PAGER;
474 opt_pipe = stdin;
475 return TRUE;
478 if (argc <= 1)
479 return TRUE;
481 subcommand = argv[1];
482 if (!strcmp(subcommand, "status") || !strcmp(subcommand, "-S")) {
483 opt_request = REQ_VIEW_STATUS;
484 if (!strcmp(subcommand, "-S"))
485 warn("`-S' has been deprecated; use `tig status' instead");
486 if (argc > 2)
487 warn("ignoring arguments after `%s'", subcommand);
488 return TRUE;
490 } else if (!strcmp(subcommand, "blame")) {
491 opt_request = REQ_VIEW_BLAME;
492 if (argc <= 2 || argc > 4)
493 die("invalid number of options to blame\n\n%s", usage);
495 i = 2;
496 if (argc == 4) {
497 string_ncopy(opt_ref, argv[i], strlen(argv[i]));
498 i++;
501 string_ncopy(opt_file, argv[i], strlen(argv[i]));
502 return TRUE;
504 } else if (!strcmp(subcommand, "show")) {
505 opt_request = REQ_VIEW_DIFF;
507 } else if (!strcmp(subcommand, "log") || !strcmp(subcommand, "diff")) {
508 opt_request = subcommand[0] == 'l'
509 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
510 warn("`tig %s' has been deprecated", subcommand);
512 } else {
513 subcommand = NULL;
516 if (!subcommand)
517 /* XXX: This is vulnerable to the user overriding
518 * options required for the main view parser. */
519 string_copy(opt_cmd, "git log --no-color --pretty=raw --boundary --parents");
520 else
521 string_format(opt_cmd, "git %s", subcommand);
523 buf_size = strlen(opt_cmd);
525 for (i = 1 + !!subcommand; i < argc; i++) {
526 char *opt = argv[i];
528 if (seen_dashdash || !strcmp(opt, "--")) {
529 seen_dashdash = TRUE;
531 } else if (!strcmp(opt, "-v") || !strcmp(opt, "--version")) {
532 printf("tig version %s\n", TIG_VERSION);
533 return FALSE;
535 } else if (!strcmp(opt, "-h") || !strcmp(opt, "--help")) {
536 printf("%s\n", usage);
537 return FALSE;
540 opt_cmd[buf_size++] = ' ';
541 buf_size = sq_quote(opt_cmd, buf_size, opt);
542 if (buf_size >= sizeof(opt_cmd))
543 die("command too long");
546 opt_cmd[buf_size] = 0;
548 return TRUE;
553 * Line-oriented content detection.
556 #define LINE_INFO \
557 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
558 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
559 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
560 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
561 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
562 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
563 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
564 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
565 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
566 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
567 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
568 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
569 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
570 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
571 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
572 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
573 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
574 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
575 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
576 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
577 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
578 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
579 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
580 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
581 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
582 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
583 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
584 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
585 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
586 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
587 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
588 LINE(DELIMITER, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
589 LINE(DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
590 LINE(LINE_NUMBER, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
591 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
592 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
593 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
594 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
595 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
596 LINE(MAIN_LOCAL_TAG,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
597 LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \
598 LINE(MAIN_TRACKED, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
599 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
600 LINE(MAIN_HEAD, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
601 LINE(MAIN_REVGRAPH,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
602 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
603 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
604 LINE(STAT_HEAD, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \
605 LINE(STAT_SECTION, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
606 LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
607 LINE(STAT_STAGED, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
608 LINE(STAT_UNSTAGED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
609 LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
610 LINE(BLAME_ID, "", COLOR_MAGENTA, COLOR_DEFAULT, 0)
612 enum line_type {
613 #define LINE(type, line, fg, bg, attr) \
614 LINE_##type
615 LINE_INFO
616 #undef LINE
619 struct line_info {
620 const char *name; /* Option name. */
621 int namelen; /* Size of option name. */
622 const char *line; /* The start of line to match. */
623 int linelen; /* Size of string to match. */
624 int fg, bg, attr; /* Color and text attributes for the lines. */
627 static struct line_info line_info[] = {
628 #define LINE(type, line, fg, bg, attr) \
629 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
630 LINE_INFO
631 #undef LINE
634 static enum line_type
635 get_line_type(char *line)
637 int linelen = strlen(line);
638 enum line_type type;
640 for (type = 0; type < ARRAY_SIZE(line_info); type++)
641 /* Case insensitive search matches Signed-off-by lines better. */
642 if (linelen >= line_info[type].linelen &&
643 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
644 return type;
646 return LINE_DEFAULT;
649 static inline int
650 get_line_attr(enum line_type type)
652 assert(type < ARRAY_SIZE(line_info));
653 return COLOR_PAIR(type) | line_info[type].attr;
656 static struct line_info *
657 get_line_info(char *name)
659 size_t namelen = strlen(name);
660 enum line_type type;
662 for (type = 0; type < ARRAY_SIZE(line_info); type++)
663 if (namelen == line_info[type].namelen &&
664 !string_enum_compare(line_info[type].name, name, namelen))
665 return &line_info[type];
667 return NULL;
670 static void
671 init_colors(void)
673 int default_bg = line_info[LINE_DEFAULT].bg;
674 int default_fg = line_info[LINE_DEFAULT].fg;
675 enum line_type type;
677 start_color();
679 if (assume_default_colors(default_fg, default_bg) == ERR) {
680 default_bg = COLOR_BLACK;
681 default_fg = COLOR_WHITE;
684 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
685 struct line_info *info = &line_info[type];
686 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
687 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
689 init_pair(type, fg, bg);
693 struct line {
694 enum line_type type;
696 /* State flags */
697 unsigned int selected:1;
698 unsigned int dirty:1;
700 void *data; /* User data */
705 * Keys
708 struct keybinding {
709 int alias;
710 enum request request;
711 struct keybinding *next;
714 static struct keybinding default_keybindings[] = {
715 /* View switching */
716 { 'm', REQ_VIEW_MAIN },
717 { 'd', REQ_VIEW_DIFF },
718 { 'l', REQ_VIEW_LOG },
719 { 't', REQ_VIEW_TREE },
720 { 'f', REQ_VIEW_BLOB },
721 { 'B', REQ_VIEW_BLAME },
722 { 'p', REQ_VIEW_PAGER },
723 { 'h', REQ_VIEW_HELP },
724 { 'S', REQ_VIEW_STATUS },
725 { 'c', REQ_VIEW_STAGE },
727 /* View manipulation */
728 { 'q', REQ_VIEW_CLOSE },
729 { KEY_TAB, REQ_VIEW_NEXT },
730 { KEY_RETURN, REQ_ENTER },
731 { KEY_UP, REQ_PREVIOUS },
732 { KEY_DOWN, REQ_NEXT },
733 { 'R', REQ_REFRESH },
734 { 'O', REQ_MAXIMIZE },
736 /* Cursor navigation */
737 { 'k', REQ_MOVE_UP },
738 { 'j', REQ_MOVE_DOWN },
739 { KEY_HOME, REQ_MOVE_FIRST_LINE },
740 { KEY_END, REQ_MOVE_LAST_LINE },
741 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
742 { ' ', REQ_MOVE_PAGE_DOWN },
743 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
744 { 'b', REQ_MOVE_PAGE_UP },
745 { '-', REQ_MOVE_PAGE_UP },
747 /* Scrolling */
748 { KEY_IC, REQ_SCROLL_LINE_UP },
749 { KEY_DC, REQ_SCROLL_LINE_DOWN },
750 { 'w', REQ_SCROLL_PAGE_UP },
751 { 's', REQ_SCROLL_PAGE_DOWN },
753 /* Searching */
754 { '/', REQ_SEARCH },
755 { '?', REQ_SEARCH_BACK },
756 { 'n', REQ_FIND_NEXT },
757 { 'N', REQ_FIND_PREV },
759 /* Misc */
760 { 'Q', REQ_QUIT },
761 { 'z', REQ_STOP_LOADING },
762 { 'v', REQ_SHOW_VERSION },
763 { 'r', REQ_SCREEN_REDRAW },
764 { '.', REQ_TOGGLE_LINENO },
765 { 'D', REQ_TOGGLE_DATE },
766 { 'A', REQ_TOGGLE_AUTHOR },
767 { 'g', REQ_TOGGLE_REV_GRAPH },
768 { 'F', REQ_TOGGLE_REFS },
769 { ':', REQ_PROMPT },
770 { 'u', REQ_STATUS_UPDATE },
771 { 'M', REQ_STATUS_MERGE },
772 { ',', REQ_TREE_PARENT },
773 { 'e', REQ_EDIT },
775 /* Using the ncurses SIGWINCH handler. */
776 { KEY_RESIZE, REQ_SCREEN_RESIZE },
779 #define KEYMAP_INFO \
780 KEYMAP_(GENERIC), \
781 KEYMAP_(MAIN), \
782 KEYMAP_(DIFF), \
783 KEYMAP_(LOG), \
784 KEYMAP_(TREE), \
785 KEYMAP_(BLOB), \
786 KEYMAP_(BLAME), \
787 KEYMAP_(PAGER), \
788 KEYMAP_(HELP), \
789 KEYMAP_(STATUS), \
790 KEYMAP_(STAGE)
792 enum keymap {
793 #define KEYMAP_(name) KEYMAP_##name
794 KEYMAP_INFO
795 #undef KEYMAP_
798 static struct int_map keymap_table[] = {
799 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
800 KEYMAP_INFO
801 #undef KEYMAP_
804 #define set_keymap(map, name) \
805 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
807 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
809 static void
810 add_keybinding(enum keymap keymap, enum request request, int key)
812 struct keybinding *keybinding;
814 keybinding = calloc(1, sizeof(*keybinding));
815 if (!keybinding)
816 die("Failed to allocate keybinding");
818 keybinding->alias = key;
819 keybinding->request = request;
820 keybinding->next = keybindings[keymap];
821 keybindings[keymap] = keybinding;
824 /* Looks for a key binding first in the given map, then in the generic map, and
825 * lastly in the default keybindings. */
826 static enum request
827 get_keybinding(enum keymap keymap, int key)
829 struct keybinding *kbd;
830 int i;
832 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
833 if (kbd->alias == key)
834 return kbd->request;
836 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
837 if (kbd->alias == key)
838 return kbd->request;
840 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
841 if (default_keybindings[i].alias == key)
842 return default_keybindings[i].request;
844 return (enum request) key;
848 struct key {
849 char *name;
850 int value;
853 static struct key key_table[] = {
854 { "Enter", KEY_RETURN },
855 { "Space", ' ' },
856 { "Backspace", KEY_BACKSPACE },
857 { "Tab", KEY_TAB },
858 { "Escape", KEY_ESC },
859 { "Left", KEY_LEFT },
860 { "Right", KEY_RIGHT },
861 { "Up", KEY_UP },
862 { "Down", KEY_DOWN },
863 { "Insert", KEY_IC },
864 { "Delete", KEY_DC },
865 { "Hash", '#' },
866 { "Home", KEY_HOME },
867 { "End", KEY_END },
868 { "PageUp", KEY_PPAGE },
869 { "PageDown", KEY_NPAGE },
870 { "F1", KEY_F(1) },
871 { "F2", KEY_F(2) },
872 { "F3", KEY_F(3) },
873 { "F4", KEY_F(4) },
874 { "F5", KEY_F(5) },
875 { "F6", KEY_F(6) },
876 { "F7", KEY_F(7) },
877 { "F8", KEY_F(8) },
878 { "F9", KEY_F(9) },
879 { "F10", KEY_F(10) },
880 { "F11", KEY_F(11) },
881 { "F12", KEY_F(12) },
884 static int
885 get_key_value(const char *name)
887 int i;
889 for (i = 0; i < ARRAY_SIZE(key_table); i++)
890 if (!strcasecmp(key_table[i].name, name))
891 return key_table[i].value;
893 if (strlen(name) == 1 && isprint(*name))
894 return (int) *name;
896 return ERR;
899 static char *
900 get_key_name(int key_value)
902 static char key_char[] = "'X'";
903 char *seq = NULL;
904 int key;
906 for (key = 0; key < ARRAY_SIZE(key_table); key++)
907 if (key_table[key].value == key_value)
908 seq = key_table[key].name;
910 if (seq == NULL &&
911 key_value < 127 &&
912 isprint(key_value)) {
913 key_char[1] = (char) key_value;
914 seq = key_char;
917 return seq ? seq : "'?'";
920 static char *
921 get_key(enum request request)
923 static char buf[BUFSIZ];
924 size_t pos = 0;
925 char *sep = "";
926 int i;
928 buf[pos] = 0;
930 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
931 struct keybinding *keybinding = &default_keybindings[i];
933 if (keybinding->request != request)
934 continue;
936 if (!string_format_from(buf, &pos, "%s%s", sep,
937 get_key_name(keybinding->alias)))
938 return "Too many keybindings!";
939 sep = ", ";
942 return buf;
945 struct run_request {
946 enum keymap keymap;
947 int key;
948 char cmd[SIZEOF_STR];
951 static struct run_request *run_request;
952 static size_t run_requests;
954 static enum request
955 add_run_request(enum keymap keymap, int key, int argc, char **argv)
957 struct run_request *tmp;
958 struct run_request req = { keymap, key };
959 size_t bufpos;
961 for (bufpos = 0; argc > 0; argc--, argv++)
962 if (!string_format_from(req.cmd, &bufpos, "%s ", *argv))
963 return REQ_NONE;
965 req.cmd[bufpos - 1] = 0;
967 tmp = realloc(run_request, (run_requests + 1) * sizeof(*run_request));
968 if (!tmp)
969 return REQ_NONE;
971 run_request = tmp;
972 run_request[run_requests++] = req;
974 return REQ_NONE + run_requests;
977 static struct run_request *
978 get_run_request(enum request request)
980 if (request <= REQ_NONE)
981 return NULL;
982 return &run_request[request - REQ_NONE - 1];
985 static void
986 add_builtin_run_requests(void)
988 struct {
989 enum keymap keymap;
990 int key;
991 char *argv[1];
992 } reqs[] = {
993 { KEYMAP_MAIN, 'C', { "git cherry-pick %(commit)" } },
994 { KEYMAP_GENERIC, 'G', { "git gc" } },
996 int i;
998 for (i = 0; i < ARRAY_SIZE(reqs); i++) {
999 enum request req;
1001 req = add_run_request(reqs[i].keymap, reqs[i].key, 1, reqs[i].argv);
1002 if (req != REQ_NONE)
1003 add_keybinding(reqs[i].keymap, req, reqs[i].key);
1008 * User config file handling.
1011 static struct int_map color_map[] = {
1012 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
1013 COLOR_MAP(DEFAULT),
1014 COLOR_MAP(BLACK),
1015 COLOR_MAP(BLUE),
1016 COLOR_MAP(CYAN),
1017 COLOR_MAP(GREEN),
1018 COLOR_MAP(MAGENTA),
1019 COLOR_MAP(RED),
1020 COLOR_MAP(WHITE),
1021 COLOR_MAP(YELLOW),
1024 #define set_color(color, name) \
1025 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
1027 static struct int_map attr_map[] = {
1028 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
1029 ATTR_MAP(NORMAL),
1030 ATTR_MAP(BLINK),
1031 ATTR_MAP(BOLD),
1032 ATTR_MAP(DIM),
1033 ATTR_MAP(REVERSE),
1034 ATTR_MAP(STANDOUT),
1035 ATTR_MAP(UNDERLINE),
1038 #define set_attribute(attr, name) \
1039 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
1041 static int config_lineno;
1042 static bool config_errors;
1043 static char *config_msg;
1045 /* Wants: object fgcolor bgcolor [attr] */
1046 static int
1047 option_color_command(int argc, char *argv[])
1049 struct line_info *info;
1051 if (argc != 3 && argc != 4) {
1052 config_msg = "Wrong number of arguments given to color command";
1053 return ERR;
1056 info = get_line_info(argv[0]);
1057 if (!info) {
1058 if (!string_enum_compare(argv[0], "main-delim", strlen("main-delim"))) {
1059 info = get_line_info("delimiter");
1061 } else if (!string_enum_compare(argv[0], "main-date", strlen("main-date"))) {
1062 info = get_line_info("date");
1064 } else {
1065 config_msg = "Unknown color name";
1066 return ERR;
1070 if (set_color(&info->fg, argv[1]) == ERR ||
1071 set_color(&info->bg, argv[2]) == ERR) {
1072 config_msg = "Unknown color";
1073 return ERR;
1076 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
1077 config_msg = "Unknown attribute";
1078 return ERR;
1081 return OK;
1084 static bool parse_bool(const char *s)
1086 return (!strcmp(s, "1") || !strcmp(s, "true") ||
1087 !strcmp(s, "yes")) ? TRUE : FALSE;
1090 /* Wants: name = value */
1091 static int
1092 option_set_command(int argc, char *argv[])
1094 if (argc != 3) {
1095 config_msg = "Wrong number of arguments given to set command";
1096 return ERR;
1099 if (strcmp(argv[1], "=")) {
1100 config_msg = "No value assigned";
1101 return ERR;
1104 if (!strcmp(argv[0], "show-author")) {
1105 opt_author = parse_bool(argv[2]);
1106 return OK;
1109 if (!strcmp(argv[0], "show-date")) {
1110 opt_date = parse_bool(argv[2]);
1111 return OK;
1114 if (!strcmp(argv[0], "show-rev-graph")) {
1115 opt_rev_graph = parse_bool(argv[2]);
1116 return OK;
1119 if (!strcmp(argv[0], "show-refs")) {
1120 opt_show_refs = parse_bool(argv[2]);
1121 return OK;
1124 if (!strcmp(argv[0], "show-line-numbers")) {
1125 opt_line_number = parse_bool(argv[2]);
1126 return OK;
1129 if (!strcmp(argv[0], "line-number-interval")) {
1130 opt_num_interval = atoi(argv[2]);
1131 return OK;
1134 if (!strcmp(argv[0], "tab-size")) {
1135 opt_tab_size = atoi(argv[2]);
1136 return OK;
1139 if (!strcmp(argv[0], "commit-encoding")) {
1140 char *arg = argv[2];
1141 int delimiter = *arg;
1142 int i;
1144 switch (delimiter) {
1145 case '"':
1146 case '\'':
1147 for (arg++, i = 0; arg[i]; i++)
1148 if (arg[i] == delimiter) {
1149 arg[i] = 0;
1150 break;
1152 default:
1153 string_ncopy(opt_encoding, arg, strlen(arg));
1154 return OK;
1158 config_msg = "Unknown variable name";
1159 return ERR;
1162 /* Wants: mode request key */
1163 static int
1164 option_bind_command(int argc, char *argv[])
1166 enum request request;
1167 int keymap;
1168 int key;
1170 if (argc < 3) {
1171 config_msg = "Wrong number of arguments given to bind command";
1172 return ERR;
1175 if (set_keymap(&keymap, argv[0]) == ERR) {
1176 config_msg = "Unknown key map";
1177 return ERR;
1180 key = get_key_value(argv[1]);
1181 if (key == ERR) {
1182 config_msg = "Unknown key";
1183 return ERR;
1186 request = get_request(argv[2]);
1187 if (request == REQ_NONE) {
1188 const char *obsolete[] = { "cherry-pick" };
1189 size_t namelen = strlen(argv[2]);
1190 int i;
1192 for (i = 0; i < ARRAY_SIZE(obsolete); i++) {
1193 if (namelen == strlen(obsolete[i]) &&
1194 !string_enum_compare(obsolete[i], argv[2], namelen)) {
1195 config_msg = "Obsolete request name";
1196 return ERR;
1200 if (request == REQ_NONE && *argv[2]++ == '!')
1201 request = add_run_request(keymap, key, argc - 2, argv + 2);
1202 if (request == REQ_NONE) {
1203 config_msg = "Unknown request name";
1204 return ERR;
1207 add_keybinding(keymap, request, key);
1209 return OK;
1212 static int
1213 set_option(char *opt, char *value)
1215 char *argv[16];
1216 int valuelen;
1217 int argc = 0;
1219 /* Tokenize */
1220 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1221 argv[argc++] = value;
1222 value += valuelen;
1224 /* Nothing more to tokenize or last available token. */
1225 if (!*value || argc >= ARRAY_SIZE(argv))
1226 break;
1228 *value++ = 0;
1229 while (isspace(*value))
1230 value++;
1233 if (!strcmp(opt, "color"))
1234 return option_color_command(argc, argv);
1236 if (!strcmp(opt, "set"))
1237 return option_set_command(argc, argv);
1239 if (!strcmp(opt, "bind"))
1240 return option_bind_command(argc, argv);
1242 config_msg = "Unknown option command";
1243 return ERR;
1246 static int
1247 read_option(char *opt, size_t optlen, char *value, size_t valuelen)
1249 int status = OK;
1251 config_lineno++;
1252 config_msg = "Internal error";
1254 /* Check for comment markers, since read_properties() will
1255 * only ensure opt and value are split at first " \t". */
1256 optlen = strcspn(opt, "#");
1257 if (optlen == 0)
1258 return OK;
1260 if (opt[optlen] != 0) {
1261 config_msg = "No option value";
1262 status = ERR;
1264 } else {
1265 /* Look for comment endings in the value. */
1266 size_t len = strcspn(value, "#");
1268 if (len < valuelen) {
1269 valuelen = len;
1270 value[valuelen] = 0;
1273 status = set_option(opt, value);
1276 if (status == ERR) {
1277 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1278 config_lineno, (int) optlen, opt, config_msg);
1279 config_errors = TRUE;
1282 /* Always keep going if errors are encountered. */
1283 return OK;
1286 static void
1287 load_option_file(const char *path)
1289 FILE *file;
1291 /* It's ok that the file doesn't exist. */
1292 file = fopen(path, "r");
1293 if (!file)
1294 return;
1296 config_lineno = 0;
1297 config_errors = FALSE;
1299 if (read_properties(file, " \t", read_option) == ERR ||
1300 config_errors == TRUE)
1301 fprintf(stderr, "Errors while loading %s.\n", path);
1304 static int
1305 load_options(void)
1307 char *home = getenv("HOME");
1308 char *tigrc_user = getenv("TIGRC_USER");
1309 char *tigrc_system = getenv("TIGRC_SYSTEM");
1310 char buf[SIZEOF_STR];
1312 add_builtin_run_requests();
1314 if (!tigrc_system) {
1315 if (!string_format(buf, "%s/tigrc", SYSCONFDIR))
1316 return ERR;
1317 tigrc_system = buf;
1319 load_option_file(tigrc_system);
1321 if (!tigrc_user) {
1322 if (!home || !string_format(buf, "%s/.tigrc", home))
1323 return ERR;
1324 tigrc_user = buf;
1326 load_option_file(tigrc_user);
1328 return OK;
1333 * The viewer
1336 struct view;
1337 struct view_ops;
1339 /* The display array of active views and the index of the current view. */
1340 static struct view *display[2];
1341 static unsigned int current_view;
1343 /* Reading from the prompt? */
1344 static bool input_mode = FALSE;
1346 #define foreach_displayed_view(view, i) \
1347 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1349 #define displayed_views() (display[1] != NULL ? 2 : 1)
1351 /* Current head and commit ID */
1352 static char ref_blob[SIZEOF_REF] = "";
1353 static char ref_commit[SIZEOF_REF] = "HEAD";
1354 static char ref_head[SIZEOF_REF] = "HEAD";
1356 struct view {
1357 const char *name; /* View name */
1358 const char *cmd_fmt; /* Default command line format */
1359 const char *cmd_env; /* Command line set via environment */
1360 const char *id; /* Points to either of ref_{head,commit,blob} */
1362 struct view_ops *ops; /* View operations */
1364 enum keymap keymap; /* What keymap does this view have */
1365 bool git_dir; /* Whether the view requires a git directory. */
1367 char cmd[SIZEOF_STR]; /* Command buffer */
1368 char ref[SIZEOF_REF]; /* Hovered commit reference */
1369 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1371 int height, width; /* The width and height of the main window */
1372 WINDOW *win; /* The main window */
1373 WINDOW *title; /* The title window living below the main window */
1375 /* Navigation */
1376 unsigned long offset; /* Offset of the window top */
1377 unsigned long lineno; /* Current line number */
1379 /* Searching */
1380 char grep[SIZEOF_STR]; /* Search string */
1381 regex_t *regex; /* Pre-compiled regex */
1383 /* If non-NULL, points to the view that opened this view. If this view
1384 * is closed tig will switch back to the parent view. */
1385 struct view *parent;
1387 /* Buffering */
1388 size_t lines; /* Total number of lines */
1389 struct line *line; /* Line index */
1390 size_t line_alloc; /* Total number of allocated lines */
1391 size_t line_size; /* Total number of used lines */
1392 unsigned int digits; /* Number of digits in the lines member. */
1394 /* Loading */
1395 FILE *pipe;
1396 time_t start_time;
1399 struct view_ops {
1400 /* What type of content being displayed. Used in the title bar. */
1401 const char *type;
1402 /* Open and reads in all view content. */
1403 bool (*open)(struct view *view);
1404 /* Read one line; updates view->line. */
1405 bool (*read)(struct view *view, char *data);
1406 /* Draw one line; @lineno must be < view->height. */
1407 bool (*draw)(struct view *view, struct line *line, unsigned int lineno, bool selected);
1408 /* Depending on view handle a special requests. */
1409 enum request (*request)(struct view *view, enum request request, struct line *line);
1410 /* Search for regex in a line. */
1411 bool (*grep)(struct view *view, struct line *line);
1412 /* Select line */
1413 void (*select)(struct view *view, struct line *line);
1416 static struct view_ops pager_ops;
1417 static struct view_ops main_ops;
1418 static struct view_ops tree_ops;
1419 static struct view_ops blob_ops;
1420 static struct view_ops blame_ops;
1421 static struct view_ops help_ops;
1422 static struct view_ops status_ops;
1423 static struct view_ops stage_ops;
1425 #define VIEW_STR(name, cmd, env, ref, ops, map, git) \
1426 { name, cmd, #env, ref, ops, map, git }
1428 #define VIEW_(id, name, ops, git, ref) \
1429 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id, git)
1432 static struct view views[] = {
1433 VIEW_(MAIN, "main", &main_ops, TRUE, ref_head),
1434 VIEW_(DIFF, "diff", &pager_ops, TRUE, ref_commit),
1435 VIEW_(LOG, "log", &pager_ops, TRUE, ref_head),
1436 VIEW_(TREE, "tree", &tree_ops, TRUE, ref_commit),
1437 VIEW_(BLOB, "blob", &blob_ops, TRUE, ref_blob),
1438 VIEW_(BLAME, "blame", &blame_ops, TRUE, ref_commit),
1439 VIEW_(HELP, "help", &help_ops, FALSE, ""),
1440 VIEW_(PAGER, "pager", &pager_ops, FALSE, "stdin"),
1441 VIEW_(STATUS, "status", &status_ops, TRUE, ""),
1442 VIEW_(STAGE, "stage", &stage_ops, TRUE, ""),
1445 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1446 #define VIEW_REQ(view) ((view) - views + REQ_OFFSET + 1)
1448 #define foreach_view(view, i) \
1449 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1451 #define view_is_displayed(view) \
1452 (view == display[0] || view == display[1])
1454 static int
1455 draw_text(struct view *view, const char *string, int max_len,
1456 bool use_tilde, bool selected)
1458 int len = 0;
1459 int trimmed = FALSE;
1461 if (max_len <= 0)
1462 return 0;
1464 if (opt_utf8) {
1465 len = utf8_length(string, max_len, &trimmed, use_tilde);
1466 } else {
1467 len = strlen(string);
1468 if (len > max_len) {
1469 if (use_tilde) {
1470 max_len -= 1;
1472 len = max_len;
1473 trimmed = TRUE;
1477 waddnstr(view->win, string, len);
1478 if (trimmed && use_tilde) {
1479 if (!selected)
1480 wattrset(view->win, get_line_attr(LINE_DELIMITER));
1481 waddch(view->win, '~');
1482 len++;
1485 return len;
1488 static int
1489 draw_lineno(struct view *view, unsigned int lineno, int max, bool selected)
1491 static char fmt[] = "%1ld";
1492 char number[10] = " ";
1493 int digits3 = view->digits < 3 ? 3 : view->digits;
1494 int max_number = MIN(digits3, STRING_SIZE(number));
1495 bool showtrimmed = FALSE;
1496 int col;
1498 lineno += view->offset + 1;
1499 if (lineno == 1 || (lineno % opt_num_interval) == 0) {
1500 if (view->digits <= 9)
1501 fmt[1] = '0' + digits3;
1503 if (!string_format(number, fmt, lineno))
1504 number[0] = 0;
1505 showtrimmed = TRUE;
1508 if (max < max_number)
1509 max_number = max;
1511 if (!selected)
1512 wattrset(view->win, get_line_attr(LINE_LINE_NUMBER));
1513 col = draw_text(view, number, max_number, showtrimmed, selected);
1514 if (col < max) {
1515 if (!selected)
1516 wattrset(view->win, A_NORMAL);
1517 waddch(view->win, ACS_VLINE);
1518 col++;
1520 if (col < max) {
1521 waddch(view->win, ' ');
1522 col++;
1525 return col;
1528 static int
1529 draw_date(struct view *view, struct tm *time, int max, bool selected)
1531 char buf[DATE_COLS];
1532 int col;
1533 int timelen = 0;
1535 if (max > DATE_COLS)
1536 max = DATE_COLS;
1537 if (time)
1538 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, time);
1539 if (!timelen) {
1540 memset(buf, ' ', sizeof(buf) - 1);
1541 buf[sizeof(buf) - 1] = 0;
1544 if (!selected)
1545 wattrset(view->win, get_line_attr(LINE_DATE));
1546 col = draw_text(view, buf, max, FALSE, selected);
1547 if (col < max) {
1548 if (!selected)
1549 wattrset(view->win, get_line_attr(LINE_DEFAULT));
1550 waddch(view->win, ' ');
1551 col++;
1554 return col;
1557 static bool
1558 draw_view_line(struct view *view, unsigned int lineno)
1560 struct line *line;
1561 bool selected = (view->offset + lineno == view->lineno);
1562 bool draw_ok;
1564 assert(view_is_displayed(view));
1566 if (view->offset + lineno >= view->lines)
1567 return FALSE;
1569 line = &view->line[view->offset + lineno];
1571 wmove(view->win, lineno, 0);
1573 if (selected) {
1574 line->selected = TRUE;
1575 view->ops->select(view, line);
1576 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
1577 wattrset(view->win, get_line_attr(LINE_CURSOR));
1578 } else if (line->selected) {
1579 line->selected = FALSE;
1580 wclrtoeol(view->win);
1583 scrollok(view->win, FALSE);
1584 draw_ok = view->ops->draw(view, line, lineno, selected);
1585 scrollok(view->win, TRUE);
1587 return draw_ok;
1590 static void
1591 redraw_view_dirty(struct view *view)
1593 bool dirty = FALSE;
1594 int lineno;
1596 for (lineno = 0; lineno < view->height; lineno++) {
1597 struct line *line = &view->line[view->offset + lineno];
1599 if (!line->dirty)
1600 continue;
1601 line->dirty = 0;
1602 dirty = TRUE;
1603 if (!draw_view_line(view, lineno))
1604 break;
1607 if (!dirty)
1608 return;
1609 redrawwin(view->win);
1610 if (input_mode)
1611 wnoutrefresh(view->win);
1612 else
1613 wrefresh(view->win);
1616 static void
1617 redraw_view_from(struct view *view, int lineno)
1619 assert(0 <= lineno && lineno < view->height);
1621 for (; lineno < view->height; lineno++) {
1622 if (!draw_view_line(view, lineno))
1623 break;
1626 redrawwin(view->win);
1627 if (input_mode)
1628 wnoutrefresh(view->win);
1629 else
1630 wrefresh(view->win);
1633 static void
1634 redraw_view(struct view *view)
1636 wclear(view->win);
1637 redraw_view_from(view, 0);
1641 static void
1642 update_view_title(struct view *view)
1644 char buf[SIZEOF_STR];
1645 char state[SIZEOF_STR];
1646 size_t bufpos = 0, statelen = 0;
1648 assert(view_is_displayed(view));
1650 if (view != VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) {
1651 unsigned int view_lines = view->offset + view->height;
1652 unsigned int lines = view->lines
1653 ? MIN(view_lines, view->lines) * 100 / view->lines
1654 : 0;
1656 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1657 view->ops->type,
1658 view->lineno + 1,
1659 view->lines,
1660 lines);
1662 if (view->pipe) {
1663 time_t secs = time(NULL) - view->start_time;
1665 /* Three git seconds are a long time ... */
1666 if (secs > 2)
1667 string_format_from(state, &statelen, " %lds", secs);
1671 string_format_from(buf, &bufpos, "[%s]", view->name);
1672 if (*view->ref && bufpos < view->width) {
1673 size_t refsize = strlen(view->ref);
1674 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1676 if (minsize < view->width)
1677 refsize = view->width - minsize + 7;
1678 string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
1681 if (statelen && bufpos < view->width) {
1682 string_format_from(buf, &bufpos, " %s", state);
1685 if (view == display[current_view])
1686 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1687 else
1688 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1690 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1691 wclrtoeol(view->title);
1692 wmove(view->title, 0, view->width - 1);
1694 if (input_mode)
1695 wnoutrefresh(view->title);
1696 else
1697 wrefresh(view->title);
1700 static void
1701 resize_display(void)
1703 int offset, i;
1704 struct view *base = display[0];
1705 struct view *view = display[1] ? display[1] : display[0];
1707 /* Setup window dimensions */
1709 getmaxyx(stdscr, base->height, base->width);
1711 /* Make room for the status window. */
1712 base->height -= 1;
1714 if (view != base) {
1715 /* Horizontal split. */
1716 view->width = base->width;
1717 view->height = SCALE_SPLIT_VIEW(base->height);
1718 base->height -= view->height;
1720 /* Make room for the title bar. */
1721 view->height -= 1;
1724 /* Make room for the title bar. */
1725 base->height -= 1;
1727 offset = 0;
1729 foreach_displayed_view (view, i) {
1730 if (!view->win) {
1731 view->win = newwin(view->height, 0, offset, 0);
1732 if (!view->win)
1733 die("Failed to create %s view", view->name);
1735 scrollok(view->win, TRUE);
1737 view->title = newwin(1, 0, offset + view->height, 0);
1738 if (!view->title)
1739 die("Failed to create title window");
1741 } else {
1742 wresize(view->win, view->height, view->width);
1743 mvwin(view->win, offset, 0);
1744 mvwin(view->title, offset + view->height, 0);
1747 offset += view->height + 1;
1751 static void
1752 redraw_display(void)
1754 struct view *view;
1755 int i;
1757 foreach_displayed_view (view, i) {
1758 redraw_view(view);
1759 update_view_title(view);
1763 static void
1764 update_display_cursor(struct view *view)
1766 /* Move the cursor to the right-most column of the cursor line.
1768 * XXX: This could turn out to be a bit expensive, but it ensures that
1769 * the cursor does not jump around. */
1770 if (view->lines) {
1771 wmove(view->win, view->lineno - view->offset, view->width - 1);
1772 wrefresh(view->win);
1777 * Navigation
1780 /* Scrolling backend */
1781 static void
1782 do_scroll_view(struct view *view, int lines)
1784 bool redraw_current_line = FALSE;
1786 /* The rendering expects the new offset. */
1787 view->offset += lines;
1789 assert(0 <= view->offset && view->offset < view->lines);
1790 assert(lines);
1792 /* Move current line into the view. */
1793 if (view->lineno < view->offset) {
1794 view->lineno = view->offset;
1795 redraw_current_line = TRUE;
1796 } else if (view->lineno >= view->offset + view->height) {
1797 view->lineno = view->offset + view->height - 1;
1798 redraw_current_line = TRUE;
1801 assert(view->offset <= view->lineno && view->lineno < view->lines);
1803 /* Redraw the whole screen if scrolling is pointless. */
1804 if (view->height < ABS(lines)) {
1805 redraw_view(view);
1807 } else {
1808 int line = lines > 0 ? view->height - lines : 0;
1809 int end = line + ABS(lines);
1811 wscrl(view->win, lines);
1813 for (; line < end; line++) {
1814 if (!draw_view_line(view, line))
1815 break;
1818 if (redraw_current_line)
1819 draw_view_line(view, view->lineno - view->offset);
1822 redrawwin(view->win);
1823 wrefresh(view->win);
1824 report("");
1827 /* Scroll frontend */
1828 static void
1829 scroll_view(struct view *view, enum request request)
1831 int lines = 1;
1833 assert(view_is_displayed(view));
1835 switch (request) {
1836 case REQ_SCROLL_PAGE_DOWN:
1837 lines = view->height;
1838 case REQ_SCROLL_LINE_DOWN:
1839 if (view->offset + lines > view->lines)
1840 lines = view->lines - view->offset;
1842 if (lines == 0 || view->offset + view->height >= view->lines) {
1843 report("Cannot scroll beyond the last line");
1844 return;
1846 break;
1848 case REQ_SCROLL_PAGE_UP:
1849 lines = view->height;
1850 case REQ_SCROLL_LINE_UP:
1851 if (lines > view->offset)
1852 lines = view->offset;
1854 if (lines == 0) {
1855 report("Cannot scroll beyond the first line");
1856 return;
1859 lines = -lines;
1860 break;
1862 default:
1863 die("request %d not handled in switch", request);
1866 do_scroll_view(view, lines);
1869 /* Cursor moving */
1870 static void
1871 move_view(struct view *view, enum request request)
1873 int scroll_steps = 0;
1874 int steps;
1876 switch (request) {
1877 case REQ_MOVE_FIRST_LINE:
1878 steps = -view->lineno;
1879 break;
1881 case REQ_MOVE_LAST_LINE:
1882 steps = view->lines - view->lineno - 1;
1883 break;
1885 case REQ_MOVE_PAGE_UP:
1886 steps = view->height > view->lineno
1887 ? -view->lineno : -view->height;
1888 break;
1890 case REQ_MOVE_PAGE_DOWN:
1891 steps = view->lineno + view->height >= view->lines
1892 ? view->lines - view->lineno - 1 : view->height;
1893 break;
1895 case REQ_MOVE_UP:
1896 steps = -1;
1897 break;
1899 case REQ_MOVE_DOWN:
1900 steps = 1;
1901 break;
1903 default:
1904 die("request %d not handled in switch", request);
1907 if (steps <= 0 && view->lineno == 0) {
1908 report("Cannot move beyond the first line");
1909 return;
1911 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1912 report("Cannot move beyond the last line");
1913 return;
1916 /* Move the current line */
1917 view->lineno += steps;
1918 assert(0 <= view->lineno && view->lineno < view->lines);
1920 /* Check whether the view needs to be scrolled */
1921 if (view->lineno < view->offset ||
1922 view->lineno >= view->offset + view->height) {
1923 scroll_steps = steps;
1924 if (steps < 0 && -steps > view->offset) {
1925 scroll_steps = -view->offset;
1927 } else if (steps > 0) {
1928 if (view->lineno == view->lines - 1 &&
1929 view->lines > view->height) {
1930 scroll_steps = view->lines - view->offset - 1;
1931 if (scroll_steps >= view->height)
1932 scroll_steps -= view->height - 1;
1937 if (!view_is_displayed(view)) {
1938 view->offset += scroll_steps;
1939 assert(0 <= view->offset && view->offset < view->lines);
1940 view->ops->select(view, &view->line[view->lineno]);
1941 return;
1944 /* Repaint the old "current" line if we be scrolling */
1945 if (ABS(steps) < view->height)
1946 draw_view_line(view, view->lineno - steps - view->offset);
1948 if (scroll_steps) {
1949 do_scroll_view(view, scroll_steps);
1950 return;
1953 /* Draw the current line */
1954 draw_view_line(view, view->lineno - view->offset);
1956 redrawwin(view->win);
1957 wrefresh(view->win);
1958 report("");
1963 * Searching
1966 static void search_view(struct view *view, enum request request);
1968 static bool
1969 find_next_line(struct view *view, unsigned long lineno, struct line *line)
1971 assert(view_is_displayed(view));
1973 if (!view->ops->grep(view, line))
1974 return FALSE;
1976 if (lineno - view->offset >= view->height) {
1977 view->offset = lineno;
1978 view->lineno = lineno;
1979 redraw_view(view);
1981 } else {
1982 unsigned long old_lineno = view->lineno - view->offset;
1984 view->lineno = lineno;
1985 draw_view_line(view, old_lineno);
1987 draw_view_line(view, view->lineno - view->offset);
1988 redrawwin(view->win);
1989 wrefresh(view->win);
1992 report("Line %ld matches '%s'", lineno + 1, view->grep);
1993 return TRUE;
1996 static void
1997 find_next(struct view *view, enum request request)
1999 unsigned long lineno = view->lineno;
2000 int direction;
2002 if (!*view->grep) {
2003 if (!*opt_search)
2004 report("No previous search");
2005 else
2006 search_view(view, request);
2007 return;
2010 switch (request) {
2011 case REQ_SEARCH:
2012 case REQ_FIND_NEXT:
2013 direction = 1;
2014 break;
2016 case REQ_SEARCH_BACK:
2017 case REQ_FIND_PREV:
2018 direction = -1;
2019 break;
2021 default:
2022 return;
2025 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
2026 lineno += direction;
2028 /* Note, lineno is unsigned long so will wrap around in which case it
2029 * will become bigger than view->lines. */
2030 for (; lineno < view->lines; lineno += direction) {
2031 struct line *line = &view->line[lineno];
2033 if (find_next_line(view, lineno, line))
2034 return;
2037 report("No match found for '%s'", view->grep);
2040 static void
2041 search_view(struct view *view, enum request request)
2043 int regex_err;
2045 if (view->regex) {
2046 regfree(view->regex);
2047 *view->grep = 0;
2048 } else {
2049 view->regex = calloc(1, sizeof(*view->regex));
2050 if (!view->regex)
2051 return;
2054 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
2055 if (regex_err != 0) {
2056 char buf[SIZEOF_STR] = "unknown error";
2058 regerror(regex_err, view->regex, buf, sizeof(buf));
2059 report("Search failed: %s", buf);
2060 return;
2063 string_copy(view->grep, opt_search);
2065 find_next(view, request);
2069 * Incremental updating
2072 static void
2073 end_update(struct view *view)
2075 if (!view->pipe)
2076 return;
2077 set_nonblocking_input(FALSE);
2078 if (view->pipe == stdin)
2079 fclose(view->pipe);
2080 else
2081 pclose(view->pipe);
2082 view->pipe = NULL;
2085 static bool
2086 begin_update(struct view *view)
2088 if (view->pipe)
2089 end_update(view);
2091 if (opt_cmd[0]) {
2092 string_copy(view->cmd, opt_cmd);
2093 opt_cmd[0] = 0;
2094 /* When running random commands, initially show the
2095 * command in the title. However, it maybe later be
2096 * overwritten if a commit line is selected. */
2097 if (view == VIEW(REQ_VIEW_PAGER))
2098 string_copy(view->ref, view->cmd);
2099 else
2100 view->ref[0] = 0;
2102 } else if (view == VIEW(REQ_VIEW_TREE)) {
2103 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2104 char path[SIZEOF_STR];
2106 if (strcmp(view->vid, view->id))
2107 opt_path[0] = path[0] = 0;
2108 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
2109 return FALSE;
2111 if (!string_format(view->cmd, format, view->id, path))
2112 return FALSE;
2114 } else {
2115 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2116 const char *id = view->id;
2118 if (!string_format(view->cmd, format, id, id, id, id, id))
2119 return FALSE;
2121 /* Put the current ref_* value to the view title ref
2122 * member. This is needed by the blob view. Most other
2123 * views sets it automatically after loading because the
2124 * first line is a commit line. */
2125 string_copy_rev(view->ref, view->id);
2128 /* Special case for the pager view. */
2129 if (opt_pipe) {
2130 view->pipe = opt_pipe;
2131 opt_pipe = NULL;
2132 } else {
2133 view->pipe = popen(view->cmd, "r");
2136 if (!view->pipe)
2137 return FALSE;
2139 set_nonblocking_input(TRUE);
2141 view->offset = 0;
2142 view->lines = 0;
2143 view->lineno = 0;
2144 string_copy_rev(view->vid, view->id);
2146 if (view->line) {
2147 int i;
2149 for (i = 0; i < view->lines; i++)
2150 if (view->line[i].data)
2151 free(view->line[i].data);
2153 free(view->line);
2154 view->line = NULL;
2157 view->start_time = time(NULL);
2159 return TRUE;
2162 #define ITEM_CHUNK_SIZE 256
2163 static void *
2164 realloc_items(void *mem, size_t *size, size_t new_size, size_t item_size)
2166 size_t num_chunks = *size / ITEM_CHUNK_SIZE;
2167 size_t num_chunks_new = (new_size + ITEM_CHUNK_SIZE - 1) / ITEM_CHUNK_SIZE;
2169 if (mem == NULL || num_chunks != num_chunks_new) {
2170 *size = num_chunks_new * ITEM_CHUNK_SIZE;
2171 mem = realloc(mem, *size * item_size);
2174 return mem;
2177 static struct line *
2178 realloc_lines(struct view *view, size_t line_size)
2180 size_t alloc = view->line_alloc;
2181 struct line *tmp = realloc_items(view->line, &alloc, line_size,
2182 sizeof(*view->line));
2184 if (!tmp)
2185 return NULL;
2187 view->line = tmp;
2188 view->line_alloc = alloc;
2189 view->line_size = line_size;
2190 return view->line;
2193 static bool
2194 update_view(struct view *view)
2196 char in_buffer[BUFSIZ];
2197 char out_buffer[BUFSIZ * 2];
2198 char *line;
2199 /* The number of lines to read. If too low it will cause too much
2200 * redrawing (and possible flickering), if too high responsiveness
2201 * will suffer. */
2202 unsigned long lines = view->height;
2203 int redraw_from = -1;
2205 if (!view->pipe)
2206 return TRUE;
2208 /* Only redraw if lines are visible. */
2209 if (view->offset + view->height >= view->lines)
2210 redraw_from = view->lines - view->offset;
2212 /* FIXME: This is probably not perfect for backgrounded views. */
2213 if (!realloc_lines(view, view->lines + lines))
2214 goto alloc_error;
2216 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
2217 size_t linelen = strlen(line);
2219 if (linelen)
2220 line[linelen - 1] = 0;
2222 if (opt_iconv != ICONV_NONE) {
2223 ICONV_CONST char *inbuf = line;
2224 size_t inlen = linelen;
2226 char *outbuf = out_buffer;
2227 size_t outlen = sizeof(out_buffer);
2229 size_t ret;
2231 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
2232 if (ret != (size_t) -1) {
2233 line = out_buffer;
2234 linelen = strlen(out_buffer);
2238 if (!view->ops->read(view, line))
2239 goto alloc_error;
2241 if (lines-- == 1)
2242 break;
2246 int digits;
2248 lines = view->lines;
2249 for (digits = 0; lines; digits++)
2250 lines /= 10;
2252 /* Keep the displayed view in sync with line number scaling. */
2253 if (digits != view->digits) {
2254 view->digits = digits;
2255 redraw_from = 0;
2259 if (!view_is_displayed(view))
2260 goto check_pipe;
2262 if (view == VIEW(REQ_VIEW_TREE)) {
2263 /* Clear the view and redraw everything since the tree sorting
2264 * might have rearranged things. */
2265 redraw_view(view);
2267 } else if (redraw_from >= 0) {
2268 /* If this is an incremental update, redraw the previous line
2269 * since for commits some members could have changed when
2270 * loading the main view. */
2271 if (redraw_from > 0)
2272 redraw_from--;
2274 /* Since revision graph visualization requires knowledge
2275 * about the parent commit, it causes a further one-off
2276 * needed to be redrawn for incremental updates. */
2277 if (redraw_from > 0 && opt_rev_graph)
2278 redraw_from--;
2280 /* Incrementally draw avoids flickering. */
2281 redraw_view_from(view, redraw_from);
2284 if (view == VIEW(REQ_VIEW_BLAME))
2285 redraw_view_dirty(view);
2287 /* Update the title _after_ the redraw so that if the redraw picks up a
2288 * commit reference in view->ref it'll be available here. */
2289 update_view_title(view);
2291 check_pipe:
2292 if (ferror(view->pipe)) {
2293 report("Failed to read: %s", strerror(errno));
2294 goto end;
2296 } else if (feof(view->pipe)) {
2297 report("");
2298 goto end;
2301 return TRUE;
2303 alloc_error:
2304 report("Allocation failure");
2306 end:
2307 if (view->ops->read(view, NULL))
2308 end_update(view);
2309 return FALSE;
2312 static struct line *
2313 add_line_data(struct view *view, void *data, enum line_type type)
2315 struct line *line = &view->line[view->lines++];
2317 memset(line, 0, sizeof(*line));
2318 line->type = type;
2319 line->data = data;
2321 return line;
2324 static struct line *
2325 add_line_text(struct view *view, char *data, enum line_type type)
2327 if (data)
2328 data = strdup(data);
2330 return data ? add_line_data(view, data, type) : NULL;
2335 * View opening
2338 enum open_flags {
2339 OPEN_DEFAULT = 0, /* Use default view switching. */
2340 OPEN_SPLIT = 1, /* Split current view. */
2341 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2342 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2343 OPEN_NOMAXIMIZE = 8, /* Do not maximize the current view. */
2346 static void
2347 open_view(struct view *prev, enum request request, enum open_flags flags)
2349 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2350 bool split = !!(flags & OPEN_SPLIT);
2351 bool reload = !!(flags & OPEN_RELOAD);
2352 bool nomaximize = !!(flags & OPEN_NOMAXIMIZE);
2353 struct view *view = VIEW(request);
2354 int nviews = displayed_views();
2355 struct view *base_view = display[0];
2357 if (view == prev && nviews == 1 && !reload) {
2358 report("Already in %s view", view->name);
2359 return;
2362 if (view->git_dir && !opt_git_dir[0]) {
2363 report("The %s view is disabled in pager view", view->name);
2364 return;
2367 if (split) {
2368 display[1] = view;
2369 if (!backgrounded)
2370 current_view = 1;
2371 } else if (!nomaximize) {
2372 /* Maximize the current view. */
2373 memset(display, 0, sizeof(display));
2374 current_view = 0;
2375 display[current_view] = view;
2378 /* Resize the view when switching between split- and full-screen,
2379 * or when switching between two different full-screen views. */
2380 if (nviews != displayed_views() ||
2381 (nviews == 1 && base_view != display[0]))
2382 resize_display();
2384 if (view->ops->open) {
2385 if (!view->ops->open(view)) {
2386 report("Failed to load %s view", view->name);
2387 return;
2390 } else if ((reload || strcmp(view->vid, view->id)) &&
2391 !begin_update(view)) {
2392 report("Failed to load %s view", view->name);
2393 return;
2396 if (split && prev->lineno - prev->offset >= prev->height) {
2397 /* Take the title line into account. */
2398 int lines = prev->lineno - prev->offset - prev->height + 1;
2400 /* Scroll the view that was split if the current line is
2401 * outside the new limited view. */
2402 do_scroll_view(prev, lines);
2405 if (prev && view != prev) {
2406 if (split && !backgrounded) {
2407 /* "Blur" the previous view. */
2408 update_view_title(prev);
2411 view->parent = prev;
2414 if (view->pipe && view->lines == 0) {
2415 /* Clear the old view and let the incremental updating refill
2416 * the screen. */
2417 werase(view->win);
2418 report("");
2419 } else {
2420 redraw_view(view);
2421 report("");
2424 /* If the view is backgrounded the above calls to report()
2425 * won't redraw the view title. */
2426 if (backgrounded)
2427 update_view_title(view);
2430 static void
2431 open_external_viewer(const char *cmd)
2433 def_prog_mode(); /* save current tty modes */
2434 endwin(); /* restore original tty modes */
2435 system(cmd);
2436 fprintf(stderr, "Press Enter to continue");
2437 getc(stdin);
2438 reset_prog_mode();
2439 redraw_display();
2442 static void
2443 open_mergetool(const char *file)
2445 char cmd[SIZEOF_STR];
2446 char file_sq[SIZEOF_STR];
2448 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2449 string_format(cmd, "git mergetool %s", file_sq)) {
2450 open_external_viewer(cmd);
2454 static void
2455 open_editor(bool from_root, const char *file)
2457 char cmd[SIZEOF_STR];
2458 char file_sq[SIZEOF_STR];
2459 char *editor;
2460 char *prefix = from_root ? opt_cdup : "";
2462 editor = getenv("GIT_EDITOR");
2463 if (!editor && *opt_editor)
2464 editor = opt_editor;
2465 if (!editor)
2466 editor = getenv("VISUAL");
2467 if (!editor)
2468 editor = getenv("EDITOR");
2469 if (!editor)
2470 editor = "vi";
2472 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2473 string_format(cmd, "%s %s%s", editor, prefix, file_sq)) {
2474 open_external_viewer(cmd);
2478 static void
2479 open_run_request(enum request request)
2481 struct run_request *req = get_run_request(request);
2482 char buf[SIZEOF_STR * 2];
2483 size_t bufpos;
2484 char *cmd;
2486 if (!req) {
2487 report("Unknown run request");
2488 return;
2491 bufpos = 0;
2492 cmd = req->cmd;
2494 while (cmd) {
2495 char *next = strstr(cmd, "%(");
2496 int len = next - cmd;
2497 char *value;
2499 if (!next) {
2500 len = strlen(cmd);
2501 value = "";
2503 } else if (!strncmp(next, "%(head)", 7)) {
2504 value = ref_head;
2506 } else if (!strncmp(next, "%(commit)", 9)) {
2507 value = ref_commit;
2509 } else if (!strncmp(next, "%(blob)", 7)) {
2510 value = ref_blob;
2512 } else {
2513 report("Unknown replacement in run request: `%s`", req->cmd);
2514 return;
2517 if (!string_format_from(buf, &bufpos, "%.*s%s", len, cmd, value))
2518 return;
2520 if (next)
2521 next = strchr(next, ')') + 1;
2522 cmd = next;
2525 open_external_viewer(buf);
2529 * User request switch noodle
2532 static int
2533 view_driver(struct view *view, enum request request)
2535 int i;
2537 if (request == REQ_NONE) {
2538 doupdate();
2539 return TRUE;
2542 if (request > REQ_NONE) {
2543 open_run_request(request);
2544 /* FIXME: When all views can refresh always do this. */
2545 if (view == VIEW(REQ_VIEW_STATUS) ||
2546 view == VIEW(REQ_VIEW_STAGE))
2547 request = REQ_REFRESH;
2548 else
2549 return TRUE;
2552 if (view && view->lines) {
2553 request = view->ops->request(view, request, &view->line[view->lineno]);
2554 if (request == REQ_NONE)
2555 return TRUE;
2558 switch (request) {
2559 case REQ_MOVE_UP:
2560 case REQ_MOVE_DOWN:
2561 case REQ_MOVE_PAGE_UP:
2562 case REQ_MOVE_PAGE_DOWN:
2563 case REQ_MOVE_FIRST_LINE:
2564 case REQ_MOVE_LAST_LINE:
2565 move_view(view, request);
2566 break;
2568 case REQ_SCROLL_LINE_DOWN:
2569 case REQ_SCROLL_LINE_UP:
2570 case REQ_SCROLL_PAGE_DOWN:
2571 case REQ_SCROLL_PAGE_UP:
2572 scroll_view(view, request);
2573 break;
2575 case REQ_VIEW_BLAME:
2576 if (!opt_file[0]) {
2577 report("No file chosen, press %s to open tree view",
2578 get_key(REQ_VIEW_TREE));
2579 break;
2581 open_view(view, request, OPEN_DEFAULT);
2582 break;
2584 case REQ_VIEW_BLOB:
2585 if (!ref_blob[0]) {
2586 report("No file chosen, press %s to open tree view",
2587 get_key(REQ_VIEW_TREE));
2588 break;
2590 open_view(view, request, OPEN_DEFAULT);
2591 break;
2593 case REQ_VIEW_PAGER:
2594 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2595 report("No pager content, press %s to run command from prompt",
2596 get_key(REQ_PROMPT));
2597 break;
2599 open_view(view, request, OPEN_DEFAULT);
2600 break;
2602 case REQ_VIEW_STAGE:
2603 if (!VIEW(REQ_VIEW_STAGE)->lines) {
2604 report("No stage content, press %s to open the status view and choose file",
2605 get_key(REQ_VIEW_STATUS));
2606 break;
2608 open_view(view, request, OPEN_DEFAULT);
2609 break;
2611 case REQ_VIEW_STATUS:
2612 if (opt_is_inside_work_tree == FALSE) {
2613 report("The status view requires a working tree");
2614 break;
2616 open_view(view, request, OPEN_DEFAULT);
2617 break;
2619 case REQ_VIEW_MAIN:
2620 case REQ_VIEW_DIFF:
2621 case REQ_VIEW_LOG:
2622 case REQ_VIEW_TREE:
2623 case REQ_VIEW_HELP:
2624 open_view(view, request, OPEN_DEFAULT);
2625 break;
2627 case REQ_NEXT:
2628 case REQ_PREVIOUS:
2629 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2631 if ((view == VIEW(REQ_VIEW_DIFF) &&
2632 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2633 (view == VIEW(REQ_VIEW_DIFF) &&
2634 view->parent == VIEW(REQ_VIEW_BLAME)) ||
2635 (view == VIEW(REQ_VIEW_STAGE) &&
2636 view->parent == VIEW(REQ_VIEW_STATUS)) ||
2637 (view == VIEW(REQ_VIEW_BLOB) &&
2638 view->parent == VIEW(REQ_VIEW_TREE))) {
2639 int line;
2641 view = view->parent;
2642 line = view->lineno;
2643 move_view(view, request);
2644 if (view_is_displayed(view))
2645 update_view_title(view);
2646 if (line != view->lineno)
2647 view->ops->request(view, REQ_ENTER,
2648 &view->line[view->lineno]);
2650 } else {
2651 move_view(view, request);
2653 break;
2655 case REQ_VIEW_NEXT:
2657 int nviews = displayed_views();
2658 int next_view = (current_view + 1) % nviews;
2660 if (next_view == current_view) {
2661 report("Only one view is displayed");
2662 break;
2665 current_view = next_view;
2666 /* Blur out the title of the previous view. */
2667 update_view_title(view);
2668 report("");
2669 break;
2671 case REQ_REFRESH:
2672 report("Refreshing is not yet supported for the %s view", view->name);
2673 break;
2675 case REQ_MAXIMIZE:
2676 if (displayed_views() == 2)
2677 open_view(view, VIEW_REQ(view), OPEN_DEFAULT);
2678 break;
2680 case REQ_TOGGLE_LINENO:
2681 opt_line_number = !opt_line_number;
2682 redraw_display();
2683 break;
2685 case REQ_TOGGLE_DATE:
2686 opt_date = !opt_date;
2687 redraw_display();
2688 break;
2690 case REQ_TOGGLE_AUTHOR:
2691 opt_author = !opt_author;
2692 redraw_display();
2693 break;
2695 case REQ_TOGGLE_REV_GRAPH:
2696 opt_rev_graph = !opt_rev_graph;
2697 redraw_display();
2698 break;
2700 case REQ_TOGGLE_REFS:
2701 opt_show_refs = !opt_show_refs;
2702 redraw_display();
2703 break;
2705 case REQ_PROMPT:
2706 /* Always reload^Wrerun commands from the prompt. */
2707 open_view(view, opt_request, OPEN_RELOAD);
2708 break;
2710 case REQ_SEARCH:
2711 case REQ_SEARCH_BACK:
2712 search_view(view, request);
2713 break;
2715 case REQ_FIND_NEXT:
2716 case REQ_FIND_PREV:
2717 find_next(view, request);
2718 break;
2720 case REQ_STOP_LOADING:
2721 for (i = 0; i < ARRAY_SIZE(views); i++) {
2722 view = &views[i];
2723 if (view->pipe)
2724 report("Stopped loading the %s view", view->name),
2725 end_update(view);
2727 break;
2729 case REQ_SHOW_VERSION:
2730 report("tig-%s (built %s)", TIG_VERSION, __DATE__);
2731 return TRUE;
2733 case REQ_SCREEN_RESIZE:
2734 resize_display();
2735 /* Fall-through */
2736 case REQ_SCREEN_REDRAW:
2737 redraw_display();
2738 break;
2740 case REQ_EDIT:
2741 report("Nothing to edit");
2742 break;
2745 case REQ_ENTER:
2746 report("Nothing to enter");
2747 break;
2750 case REQ_VIEW_CLOSE:
2751 /* XXX: Mark closed views by letting view->parent point to the
2752 * view itself. Parents to closed view should never be
2753 * followed. */
2754 if (view->parent &&
2755 view->parent->parent != view->parent) {
2756 memset(display, 0, sizeof(display));
2757 current_view = 0;
2758 display[current_view] = view->parent;
2759 view->parent = view;
2760 resize_display();
2761 redraw_display();
2762 break;
2764 /* Fall-through */
2765 case REQ_QUIT:
2766 return FALSE;
2768 default:
2769 /* An unknown key will show most commonly used commands. */
2770 report("Unknown key, press 'h' for help");
2771 return TRUE;
2774 return TRUE;
2779 * Pager backend
2782 static bool
2783 pager_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2785 static char spaces[] = " ";
2786 char *text = line->data;
2787 int col = 0;
2789 if (opt_line_number) {
2790 col += draw_lineno(view, lineno, view->width, selected);
2791 if (col >= view->width)
2792 return TRUE;
2795 if (!selected)
2796 wattrset(view->win, get_line_attr(line->type));
2798 if (opt_tab_size < TABSIZE) {
2799 int col_offset = col;
2801 col = 0;
2802 while (text && col_offset + col < view->width) {
2803 int cols_max = view->width - col_offset - col;
2804 char *pos = text;
2805 int cols;
2807 if (*text == '\t') {
2808 text++;
2809 assert(sizeof(spaces) > TABSIZE);
2810 pos = spaces;
2811 cols = opt_tab_size - (col % opt_tab_size);
2813 } else {
2814 text = strchr(text, '\t');
2815 cols = line ? text - pos : strlen(pos);
2818 waddnstr(view->win, pos, MIN(cols, cols_max));
2819 col += cols;
2822 } else {
2823 draw_text(view, text, view->width - col, TRUE, selected);
2826 return TRUE;
2829 static bool
2830 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2832 char refbuf[SIZEOF_STR];
2833 char *ref = NULL;
2834 FILE *pipe;
2836 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2837 return TRUE;
2839 pipe = popen(refbuf, "r");
2840 if (!pipe)
2841 return TRUE;
2843 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2844 ref = chomp_string(ref);
2845 pclose(pipe);
2847 if (!ref || !*ref)
2848 return TRUE;
2850 /* This is the only fatal call, since it can "corrupt" the buffer. */
2851 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2852 return FALSE;
2854 return TRUE;
2857 static void
2858 add_pager_refs(struct view *view, struct line *line)
2860 char buf[SIZEOF_STR];
2861 char *commit_id = (char *)line->data + STRING_SIZE("commit ");
2862 struct ref **refs;
2863 size_t bufpos = 0, refpos = 0;
2864 const char *sep = "Refs: ";
2865 bool is_tag = FALSE;
2867 assert(line->type == LINE_COMMIT);
2869 refs = get_refs(commit_id);
2870 if (!refs) {
2871 if (view == VIEW(REQ_VIEW_DIFF))
2872 goto try_add_describe_ref;
2873 return;
2876 do {
2877 struct ref *ref = refs[refpos];
2878 char *fmt = ref->tag ? "%s[%s]" :
2879 ref->remote ? "%s<%s>" : "%s%s";
2881 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2882 return;
2883 sep = ", ";
2884 if (ref->tag)
2885 is_tag = TRUE;
2886 } while (refs[refpos++]->next);
2888 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2889 try_add_describe_ref:
2890 /* Add <tag>-g<commit_id> "fake" reference. */
2891 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2892 return;
2895 if (bufpos == 0)
2896 return;
2898 if (!realloc_lines(view, view->line_size + 1))
2899 return;
2901 add_line_text(view, buf, LINE_PP_REFS);
2904 static bool
2905 pager_read(struct view *view, char *data)
2907 struct line *line;
2909 if (!data)
2910 return TRUE;
2912 line = add_line_text(view, data, get_line_type(data));
2913 if (!line)
2914 return FALSE;
2916 if (line->type == LINE_COMMIT &&
2917 (view == VIEW(REQ_VIEW_DIFF) ||
2918 view == VIEW(REQ_VIEW_LOG)))
2919 add_pager_refs(view, line);
2921 return TRUE;
2924 static enum request
2925 pager_request(struct view *view, enum request request, struct line *line)
2927 int split = 0;
2929 if (request != REQ_ENTER)
2930 return request;
2932 if (line->type == LINE_COMMIT &&
2933 (view == VIEW(REQ_VIEW_LOG) ||
2934 view == VIEW(REQ_VIEW_PAGER))) {
2935 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2936 split = 1;
2939 /* Always scroll the view even if it was split. That way
2940 * you can use Enter to scroll through the log view and
2941 * split open each commit diff. */
2942 scroll_view(view, REQ_SCROLL_LINE_DOWN);
2944 /* FIXME: A minor workaround. Scrolling the view will call report("")
2945 * but if we are scrolling a non-current view this won't properly
2946 * update the view title. */
2947 if (split)
2948 update_view_title(view);
2950 return REQ_NONE;
2953 static bool
2954 pager_grep(struct view *view, struct line *line)
2956 regmatch_t pmatch;
2957 char *text = line->data;
2959 if (!*text)
2960 return FALSE;
2962 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
2963 return FALSE;
2965 return TRUE;
2968 static void
2969 pager_select(struct view *view, struct line *line)
2971 if (line->type == LINE_COMMIT) {
2972 char *text = (char *)line->data + STRING_SIZE("commit ");
2974 if (view != VIEW(REQ_VIEW_PAGER))
2975 string_copy_rev(view->ref, text);
2976 string_copy_rev(ref_commit, text);
2980 static struct view_ops pager_ops = {
2981 "line",
2982 NULL,
2983 pager_read,
2984 pager_draw,
2985 pager_request,
2986 pager_grep,
2987 pager_select,
2992 * Help backend
2995 static bool
2996 help_open(struct view *view)
2998 char buf[BUFSIZ];
2999 int lines = ARRAY_SIZE(req_info) + 2;
3000 int i;
3002 if (view->lines > 0)
3003 return TRUE;
3005 for (i = 0; i < ARRAY_SIZE(req_info); i++)
3006 if (!req_info[i].request)
3007 lines++;
3009 lines += run_requests + 1;
3011 view->line = calloc(lines, sizeof(*view->line));
3012 if (!view->line)
3013 return FALSE;
3015 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
3017 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
3018 char *key;
3020 if (req_info[i].request == REQ_NONE)
3021 continue;
3023 if (!req_info[i].request) {
3024 add_line_text(view, "", LINE_DEFAULT);
3025 add_line_text(view, req_info[i].help, LINE_DEFAULT);
3026 continue;
3029 key = get_key(req_info[i].request);
3030 if (!*key)
3031 key = "(no key defined)";
3033 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
3034 continue;
3036 add_line_text(view, buf, LINE_DEFAULT);
3039 if (run_requests) {
3040 add_line_text(view, "", LINE_DEFAULT);
3041 add_line_text(view, "External commands:", LINE_DEFAULT);
3044 for (i = 0; i < run_requests; i++) {
3045 struct run_request *req = get_run_request(REQ_NONE + i + 1);
3046 char *key;
3048 if (!req)
3049 continue;
3051 key = get_key_name(req->key);
3052 if (!*key)
3053 key = "(no key defined)";
3055 if (!string_format(buf, " %-10s %-14s `%s`",
3056 keymap_table[req->keymap].name,
3057 key, req->cmd))
3058 continue;
3060 add_line_text(view, buf, LINE_DEFAULT);
3063 return TRUE;
3066 static struct view_ops help_ops = {
3067 "line",
3068 help_open,
3069 NULL,
3070 pager_draw,
3071 pager_request,
3072 pager_grep,
3073 pager_select,
3078 * Tree backend
3081 struct tree_stack_entry {
3082 struct tree_stack_entry *prev; /* Entry below this in the stack */
3083 unsigned long lineno; /* Line number to restore */
3084 char *name; /* Position of name in opt_path */
3087 /* The top of the path stack. */
3088 static struct tree_stack_entry *tree_stack = NULL;
3089 unsigned long tree_lineno = 0;
3091 static void
3092 pop_tree_stack_entry(void)
3094 struct tree_stack_entry *entry = tree_stack;
3096 tree_lineno = entry->lineno;
3097 entry->name[0] = 0;
3098 tree_stack = entry->prev;
3099 free(entry);
3102 static void
3103 push_tree_stack_entry(char *name, unsigned long lineno)
3105 struct tree_stack_entry *entry = calloc(1, sizeof(*entry));
3106 size_t pathlen = strlen(opt_path);
3108 if (!entry)
3109 return;
3111 entry->prev = tree_stack;
3112 entry->name = opt_path + pathlen;
3113 tree_stack = entry;
3115 if (!string_format_from(opt_path, &pathlen, "%s/", name)) {
3116 pop_tree_stack_entry();
3117 return;
3120 /* Move the current line to the first tree entry. */
3121 tree_lineno = 1;
3122 entry->lineno = lineno;
3125 /* Parse output from git-ls-tree(1):
3127 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
3128 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
3129 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
3130 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
3133 #define SIZEOF_TREE_ATTR \
3134 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
3136 #define TREE_UP_FORMAT "040000 tree %s\t.."
3138 static int
3139 tree_compare_entry(enum line_type type1, char *name1,
3140 enum line_type type2, char *name2)
3142 if (type1 != type2) {
3143 if (type1 == LINE_TREE_DIR)
3144 return -1;
3145 return 1;
3148 return strcmp(name1, name2);
3151 static char *
3152 tree_path(struct line *line)
3154 char *path = line->data;
3156 return path + SIZEOF_TREE_ATTR;
3159 static bool
3160 tree_read(struct view *view, char *text)
3162 size_t textlen = text ? strlen(text) : 0;
3163 char buf[SIZEOF_STR];
3164 unsigned long pos;
3165 enum line_type type;
3166 bool first_read = view->lines == 0;
3168 if (!text)
3169 return TRUE;
3170 if (textlen <= SIZEOF_TREE_ATTR)
3171 return FALSE;
3173 type = text[STRING_SIZE("100644 ")] == 't'
3174 ? LINE_TREE_DIR : LINE_TREE_FILE;
3176 if (first_read) {
3177 /* Add path info line */
3178 if (!string_format(buf, "Directory path /%s", opt_path) ||
3179 !realloc_lines(view, view->line_size + 1) ||
3180 !add_line_text(view, buf, LINE_DEFAULT))
3181 return FALSE;
3183 /* Insert "link" to parent directory. */
3184 if (*opt_path) {
3185 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
3186 !realloc_lines(view, view->line_size + 1) ||
3187 !add_line_text(view, buf, LINE_TREE_DIR))
3188 return FALSE;
3192 /* Strip the path part ... */
3193 if (*opt_path) {
3194 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
3195 size_t striplen = strlen(opt_path);
3196 char *path = text + SIZEOF_TREE_ATTR;
3198 if (pathlen > striplen)
3199 memmove(path, path + striplen,
3200 pathlen - striplen + 1);
3203 /* Skip "Directory ..." and ".." line. */
3204 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
3205 struct line *line = &view->line[pos];
3206 char *path1 = tree_path(line);
3207 char *path2 = text + SIZEOF_TREE_ATTR;
3208 int cmp = tree_compare_entry(line->type, path1, type, path2);
3210 if (cmp <= 0)
3211 continue;
3213 text = strdup(text);
3214 if (!text)
3215 return FALSE;
3217 if (view->lines > pos)
3218 memmove(&view->line[pos + 1], &view->line[pos],
3219 (view->lines - pos) * sizeof(*line));
3221 line = &view->line[pos];
3222 line->data = text;
3223 line->type = type;
3224 view->lines++;
3225 return TRUE;
3228 if (!add_line_text(view, text, type))
3229 return FALSE;
3231 if (tree_lineno > view->lineno) {
3232 view->lineno = tree_lineno;
3233 tree_lineno = 0;
3236 return TRUE;
3239 static enum request
3240 tree_request(struct view *view, enum request request, struct line *line)
3242 enum open_flags flags;
3244 if (request == REQ_VIEW_BLAME) {
3245 char *filename = tree_path(line);
3247 if (line->type == LINE_TREE_DIR) {
3248 report("Cannot show blame for directory %s", opt_path);
3249 return REQ_NONE;
3252 string_copy(opt_ref, view->vid);
3253 string_format(opt_file, "%s%s", opt_path, filename);
3254 return request;
3256 if (request == REQ_TREE_PARENT) {
3257 if (*opt_path) {
3258 /* fake 'cd ..' */
3259 request = REQ_ENTER;
3260 line = &view->line[1];
3261 } else {
3262 /* quit view if at top of tree */
3263 return REQ_VIEW_CLOSE;
3266 if (request != REQ_ENTER)
3267 return request;
3269 /* Cleanup the stack if the tree view is at a different tree. */
3270 while (!*opt_path && tree_stack)
3271 pop_tree_stack_entry();
3273 switch (line->type) {
3274 case LINE_TREE_DIR:
3275 /* Depending on whether it is a subdir or parent (updir?) link
3276 * mangle the path buffer. */
3277 if (line == &view->line[1] && *opt_path) {
3278 pop_tree_stack_entry();
3280 } else {
3281 char *basename = tree_path(line);
3283 push_tree_stack_entry(basename, view->lineno);
3286 /* Trees and subtrees share the same ID, so they are not not
3287 * unique like blobs. */
3288 flags = OPEN_RELOAD;
3289 request = REQ_VIEW_TREE;
3290 break;
3292 case LINE_TREE_FILE:
3293 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3294 request = REQ_VIEW_BLOB;
3295 break;
3297 default:
3298 return TRUE;
3301 open_view(view, request, flags);
3302 if (request == REQ_VIEW_TREE) {
3303 view->lineno = tree_lineno;
3306 return REQ_NONE;
3309 static void
3310 tree_select(struct view *view, struct line *line)
3312 char *text = (char *)line->data + STRING_SIZE("100644 blob ");
3314 if (line->type == LINE_TREE_FILE) {
3315 string_copy_rev(ref_blob, text);
3317 } else if (line->type != LINE_TREE_DIR) {
3318 return;
3321 string_copy_rev(view->ref, text);
3324 static struct view_ops tree_ops = {
3325 "file",
3326 NULL,
3327 tree_read,
3328 pager_draw,
3329 tree_request,
3330 pager_grep,
3331 tree_select,
3334 static bool
3335 blob_read(struct view *view, char *line)
3337 if (!line)
3338 return TRUE;
3339 return add_line_text(view, line, LINE_DEFAULT) != NULL;
3342 static struct view_ops blob_ops = {
3343 "line",
3344 NULL,
3345 blob_read,
3346 pager_draw,
3347 pager_request,
3348 pager_grep,
3349 pager_select,
3353 * Blame backend
3355 * Loading the blame view is a two phase job:
3357 * 1. File content is read either using opt_file from the
3358 * filesystem or using git-cat-file.
3359 * 2. Then blame information is incrementally added by
3360 * reading output from git-blame.
3363 struct blame_commit {
3364 char id[SIZEOF_REV]; /* SHA1 ID. */
3365 char title[128]; /* First line of the commit message. */
3366 char author[75]; /* Author of the commit. */
3367 struct tm time; /* Date from the author ident. */
3368 char filename[128]; /* Name of file. */
3371 struct blame {
3372 struct blame_commit *commit;
3373 unsigned int header:1;
3374 char text[1];
3377 #define BLAME_CAT_FILE_CMD "git cat-file blob %s:%s"
3378 #define BLAME_INCREMENTAL_CMD "git blame --incremental %s %s"
3380 static bool
3381 blame_open(struct view *view)
3383 char path[SIZEOF_STR];
3384 char ref[SIZEOF_STR] = "";
3386 if (sq_quote(path, 0, opt_file) >= sizeof(path))
3387 return FALSE;
3389 if (*opt_ref && sq_quote(ref, 0, opt_ref) >= sizeof(ref))
3390 return FALSE;
3392 if (*opt_ref) {
3393 if (!string_format(view->cmd, BLAME_CAT_FILE_CMD, ref, path))
3394 return FALSE;
3395 } else {
3396 view->pipe = fopen(opt_file, "r");
3397 if (!view->pipe &&
3398 !string_format(view->cmd, BLAME_CAT_FILE_CMD, "HEAD", path))
3399 return FALSE;
3402 if (!view->pipe)
3403 view->pipe = popen(view->cmd, "r");
3404 if (!view->pipe)
3405 return FALSE;
3407 if (!string_format(view->cmd, BLAME_INCREMENTAL_CMD, ref, path))
3408 return FALSE;
3410 string_format(view->ref, "%s ...", opt_file);
3411 string_copy_rev(view->vid, opt_file);
3412 set_nonblocking_input(TRUE);
3414 if (view->line) {
3415 int i;
3417 for (i = 0; i < view->lines; i++)
3418 free(view->line[i].data);
3419 free(view->line);
3422 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3423 view->offset = view->lines = view->lineno = 0;
3424 view->line = NULL;
3425 view->start_time = time(NULL);
3427 return TRUE;
3430 static struct blame_commit *
3431 get_blame_commit(struct view *view, const char *id)
3433 size_t i;
3435 for (i = 0; i < view->lines; i++) {
3436 struct blame *blame = view->line[i].data;
3438 if (!blame->commit)
3439 continue;
3441 if (!strncmp(blame->commit->id, id, SIZEOF_REV - 1))
3442 return blame->commit;
3446 struct blame_commit *commit = calloc(1, sizeof(*commit));
3448 if (commit)
3449 string_ncopy(commit->id, id, SIZEOF_REV);
3450 return commit;
3454 static bool
3455 parse_number(char **posref, size_t *number, size_t min, size_t max)
3457 char *pos = *posref;
3459 *posref = NULL;
3460 pos = strchr(pos + 1, ' ');
3461 if (!pos || !isdigit(pos[1]))
3462 return FALSE;
3463 *number = atoi(pos + 1);
3464 if (*number < min || *number > max)
3465 return FALSE;
3467 *posref = pos;
3468 return TRUE;
3471 static struct blame_commit *
3472 parse_blame_commit(struct view *view, char *text, int *blamed)
3474 struct blame_commit *commit;
3475 struct blame *blame;
3476 char *pos = text + SIZEOF_REV - 1;
3477 size_t lineno;
3478 size_t group;
3480 if (strlen(text) <= SIZEOF_REV || *pos != ' ')
3481 return NULL;
3483 if (!parse_number(&pos, &lineno, 1, view->lines) ||
3484 !parse_number(&pos, &group, 1, view->lines - lineno + 1))
3485 return NULL;
3487 commit = get_blame_commit(view, text);
3488 if (!commit)
3489 return NULL;
3491 *blamed += group;
3492 while (group--) {
3493 struct line *line = &view->line[lineno + group - 1];
3495 blame = line->data;
3496 blame->commit = commit;
3497 blame->header = !group;
3498 line->dirty = 1;
3501 return commit;
3504 static bool
3505 blame_read_file(struct view *view, char *line)
3507 if (!line) {
3508 FILE *pipe = NULL;
3510 if (view->lines > 0)
3511 pipe = popen(view->cmd, "r");
3512 else if (!view->parent)
3513 die("No blame exist for %s", view->vid);
3514 view->cmd[0] = 0;
3515 if (!pipe) {
3516 report("Failed to load blame data");
3517 return TRUE;
3520 fclose(view->pipe);
3521 view->pipe = pipe;
3522 return FALSE;
3524 } else {
3525 size_t linelen = strlen(line);
3526 struct blame *blame = malloc(sizeof(*blame) + linelen);
3528 if (!line)
3529 return FALSE;
3531 blame->commit = NULL;
3532 strncpy(blame->text, line, linelen);
3533 blame->text[linelen] = 0;
3534 return add_line_data(view, blame, LINE_BLAME_ID) != NULL;
3538 static bool
3539 match_blame_header(const char *name, char **line)
3541 size_t namelen = strlen(name);
3542 bool matched = !strncmp(name, *line, namelen);
3544 if (matched)
3545 *line += namelen;
3547 return matched;
3550 static bool
3551 blame_read(struct view *view, char *line)
3553 static struct blame_commit *commit = NULL;
3554 static int blamed = 0;
3555 static time_t author_time;
3557 if (*view->cmd)
3558 return blame_read_file(view, line);
3560 if (!line) {
3561 /* Reset all! */
3562 commit = NULL;
3563 blamed = 0;
3564 string_format(view->ref, "%s", view->vid);
3565 if (view_is_displayed(view)) {
3566 update_view_title(view);
3567 redraw_view_from(view, 0);
3569 return TRUE;
3572 if (!commit) {
3573 commit = parse_blame_commit(view, line, &blamed);
3574 string_format(view->ref, "%s %2d%%", view->vid,
3575 blamed * 100 / view->lines);
3577 } else if (match_blame_header("author ", &line)) {
3578 string_ncopy(commit->author, line, strlen(line));
3580 } else if (match_blame_header("author-time ", &line)) {
3581 author_time = (time_t) atol(line);
3583 } else if (match_blame_header("author-tz ", &line)) {
3584 long tz;
3586 tz = ('0' - line[1]) * 60 * 60 * 10;
3587 tz += ('0' - line[2]) * 60 * 60;
3588 tz += ('0' - line[3]) * 60;
3589 tz += ('0' - line[4]) * 60;
3591 if (line[0] == '-')
3592 tz = -tz;
3594 author_time -= tz;
3595 gmtime_r(&author_time, &commit->time);
3597 } else if (match_blame_header("summary ", &line)) {
3598 string_ncopy(commit->title, line, strlen(line));
3600 } else if (match_blame_header("filename ", &line)) {
3601 string_ncopy(commit->filename, line, strlen(line));
3602 commit = NULL;
3605 return TRUE;
3608 static bool
3609 blame_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
3611 struct blame *blame = line->data;
3612 int col = 0;
3614 if (opt_date) {
3615 struct tm *time = blame->commit && *blame->commit->filename
3616 ? &blame->commit->time : NULL;
3618 col += draw_date(view, time, view->width, selected);
3619 if (col >= view->width)
3620 return TRUE;
3623 if (opt_author) {
3624 int max = MIN(AUTHOR_COLS - 1, view->width - col);
3626 if (!selected)
3627 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
3628 if (blame->commit)
3629 draw_text(view, blame->commit->author, max, TRUE, selected);
3630 col += AUTHOR_COLS;
3631 if (col >= view->width)
3632 return TRUE;
3633 wmove(view->win, lineno, col);
3637 int max = MIN(ID_COLS - 1, view->width - col);
3639 if (!selected)
3640 wattrset(view->win, get_line_attr(LINE_BLAME_ID));
3641 if (blame->commit)
3642 draw_text(view, blame->commit->id, max, FALSE, -1);
3643 col += ID_COLS;
3644 if (col >= view->width)
3645 return TRUE;
3646 wmove(view->win, lineno, col);
3649 col += draw_lineno(view, lineno, view->width - col, selected);
3650 if (col >= view->width)
3651 return TRUE;
3653 col += draw_text(view, blame->text, view->width - col, TRUE, selected);
3654 return TRUE;
3657 static enum request
3658 blame_request(struct view *view, enum request request, struct line *line)
3660 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3661 struct blame *blame = line->data;
3663 switch (request) {
3664 case REQ_ENTER:
3665 if (!blame->commit) {
3666 report("No commit loaded yet");
3667 break;
3670 if (!strcmp(blame->commit->id, NULL_ID)) {
3671 char path[SIZEOF_STR];
3673 if (sq_quote(path, 0, view->vid) >= sizeof(path))
3674 break;
3675 string_format(opt_cmd, "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s 2>/dev/null", path);
3678 open_view(view, REQ_VIEW_DIFF, flags);
3679 break;
3681 default:
3682 return request;
3685 return REQ_NONE;
3688 static bool
3689 blame_grep(struct view *view, struct line *line)
3691 struct blame *blame = line->data;
3692 struct blame_commit *commit = blame->commit;
3693 regmatch_t pmatch;
3695 #define MATCH(text) \
3696 (*text && regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3698 if (commit) {
3699 char buf[DATE_COLS + 1];
3701 if (MATCH(commit->title) ||
3702 MATCH(commit->author) ||
3703 MATCH(commit->id))
3704 return TRUE;
3706 if (strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time) &&
3707 MATCH(buf))
3708 return TRUE;
3711 return MATCH(blame->text);
3713 #undef MATCH
3716 static void
3717 blame_select(struct view *view, struct line *line)
3719 struct blame *blame = line->data;
3720 struct blame_commit *commit = blame->commit;
3722 if (!commit)
3723 return;
3725 if (!strcmp(commit->id, NULL_ID))
3726 string_ncopy(ref_commit, "HEAD", 4);
3727 else
3728 string_copy_rev(ref_commit, commit->id);
3731 static struct view_ops blame_ops = {
3732 "line",
3733 blame_open,
3734 blame_read,
3735 blame_draw,
3736 blame_request,
3737 blame_grep,
3738 blame_select,
3742 * Status backend
3745 struct status {
3746 char status;
3747 struct {
3748 mode_t mode;
3749 char rev[SIZEOF_REV];
3750 char name[SIZEOF_STR];
3751 } old;
3752 struct {
3753 mode_t mode;
3754 char rev[SIZEOF_REV];
3755 char name[SIZEOF_STR];
3756 } new;
3759 static char status_onbranch[SIZEOF_STR];
3760 static struct status stage_status;
3761 static enum line_type stage_line_type;
3763 /* Get fields from the diff line:
3764 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
3766 static inline bool
3767 status_get_diff(struct status *file, char *buf, size_t bufsize)
3769 char *old_mode = buf + 1;
3770 char *new_mode = buf + 8;
3771 char *old_rev = buf + 15;
3772 char *new_rev = buf + 56;
3773 char *status = buf + 97;
3775 if (bufsize < 99 ||
3776 old_mode[-1] != ':' ||
3777 new_mode[-1] != ' ' ||
3778 old_rev[-1] != ' ' ||
3779 new_rev[-1] != ' ' ||
3780 status[-1] != ' ')
3781 return FALSE;
3783 file->status = *status;
3785 string_copy_rev(file->old.rev, old_rev);
3786 string_copy_rev(file->new.rev, new_rev);
3788 file->old.mode = strtoul(old_mode, NULL, 8);
3789 file->new.mode = strtoul(new_mode, NULL, 8);
3791 file->old.name[0] = file->new.name[0] = 0;
3793 return TRUE;
3796 static bool
3797 status_run(struct view *view, const char cmd[], char status, enum line_type type)
3799 struct status *file = NULL;
3800 struct status *unmerged = NULL;
3801 char buf[SIZEOF_STR * 4];
3802 size_t bufsize = 0;
3803 FILE *pipe;
3805 pipe = popen(cmd, "r");
3806 if (!pipe)
3807 return FALSE;
3809 add_line_data(view, NULL, type);
3811 while (!feof(pipe) && !ferror(pipe)) {
3812 char *sep;
3813 size_t readsize;
3815 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
3816 if (!readsize)
3817 break;
3818 bufsize += readsize;
3820 /* Process while we have NUL chars. */
3821 while ((sep = memchr(buf, 0, bufsize))) {
3822 size_t sepsize = sep - buf + 1;
3824 if (!file) {
3825 if (!realloc_lines(view, view->line_size + 1))
3826 goto error_out;
3828 file = calloc(1, sizeof(*file));
3829 if (!file)
3830 goto error_out;
3832 add_line_data(view, file, type);
3835 /* Parse diff info part. */
3836 if (status) {
3837 file->status = status;
3838 if (status == 'A')
3839 string_copy(file->old.rev, NULL_ID);
3841 } else if (!file->status) {
3842 if (!status_get_diff(file, buf, sepsize))
3843 goto error_out;
3845 bufsize -= sepsize;
3846 memmove(buf, sep + 1, bufsize);
3848 sep = memchr(buf, 0, bufsize);
3849 if (!sep)
3850 break;
3851 sepsize = sep - buf + 1;
3853 /* Collapse all 'M'odified entries that
3854 * follow a associated 'U'nmerged entry.
3856 if (file->status == 'U') {
3857 unmerged = file;
3859 } else if (unmerged) {
3860 int collapse = !strcmp(buf, unmerged->new.name);
3862 unmerged = NULL;
3863 if (collapse) {
3864 free(file);
3865 view->lines--;
3866 continue;
3871 /* Grab the old name for rename/copy. */
3872 if (!*file->old.name &&
3873 (file->status == 'R' || file->status == 'C')) {
3874 sepsize = sep - buf + 1;
3875 string_ncopy(file->old.name, buf, sepsize);
3876 bufsize -= sepsize;
3877 memmove(buf, sep + 1, bufsize);
3879 sep = memchr(buf, 0, bufsize);
3880 if (!sep)
3881 break;
3882 sepsize = sep - buf + 1;
3885 /* git-ls-files just delivers a NUL separated
3886 * list of file names similar to the second half
3887 * of the git-diff-* output. */
3888 string_ncopy(file->new.name, buf, sepsize);
3889 if (!*file->old.name)
3890 string_copy(file->old.name, file->new.name);
3891 bufsize -= sepsize;
3892 memmove(buf, sep + 1, bufsize);
3893 file = NULL;
3897 if (ferror(pipe)) {
3898 error_out:
3899 pclose(pipe);
3900 return FALSE;
3903 if (!view->line[view->lines - 1].data)
3904 add_line_data(view, NULL, LINE_STAT_NONE);
3906 pclose(pipe);
3907 return TRUE;
3910 /* Don't show unmerged entries in the staged section. */
3911 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached -M HEAD"
3912 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
3913 #define STATUS_LIST_OTHER_CMD \
3914 "git ls-files -z --others --exclude-per-directory=.gitignore"
3915 #define STATUS_LIST_NO_HEAD_CMD \
3916 "git ls-files -z --cached --exclude-per-directory=.gitignore"
3918 #define STATUS_DIFF_INDEX_SHOW_CMD \
3919 "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s %s 2>/dev/null"
3921 #define STATUS_DIFF_FILES_SHOW_CMD \
3922 "git diff-files --root --patch-with-stat -C -M -- %s %s 2>/dev/null"
3924 #define STATUS_DIFF_NO_HEAD_SHOW_CMD \
3925 "git diff --no-color --patch-with-stat /dev/null %s 2>/dev/null"
3927 /* First parse staged info using git-diff-index(1), then parse unstaged
3928 * info using git-diff-files(1), and finally untracked files using
3929 * git-ls-files(1). */
3930 static bool
3931 status_open(struct view *view)
3933 struct stat statbuf;
3934 char exclude[SIZEOF_STR];
3935 char indexcmd[SIZEOF_STR] = STATUS_DIFF_INDEX_CMD;
3936 char othercmd[SIZEOF_STR] = STATUS_LIST_OTHER_CMD;
3937 unsigned long prev_lineno = view->lineno;
3938 char indexstatus = 0;
3939 size_t i;
3941 for (i = 0; i < view->lines; i++)
3942 free(view->line[i].data);
3943 free(view->line);
3944 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3945 view->line = NULL;
3947 if (!realloc_lines(view, view->line_size + 7))
3948 return FALSE;
3950 add_line_data(view, NULL, LINE_STAT_HEAD);
3951 if (opt_no_head)
3952 string_copy(status_onbranch, "Initial commit");
3953 else if (!*opt_head)
3954 string_copy(status_onbranch, "Not currently on any branch");
3955 else if (!string_format(status_onbranch, "On branch %s", opt_head))
3956 return FALSE;
3958 if (opt_no_head) {
3959 string_copy(indexcmd, STATUS_LIST_NO_HEAD_CMD);
3960 indexstatus = 'A';
3963 if (!string_format(exclude, "%s/info/exclude", opt_git_dir))
3964 return FALSE;
3966 if (stat(exclude, &statbuf) >= 0) {
3967 size_t cmdsize = strlen(othercmd);
3969 if (!string_format_from(othercmd, &cmdsize, " %s", "--exclude-from=") ||
3970 sq_quote(othercmd, cmdsize, exclude) >= sizeof(othercmd))
3971 return FALSE;
3973 cmdsize = strlen(indexcmd);
3974 if (opt_no_head &&
3975 (!string_format_from(indexcmd, &cmdsize, " %s", "--exclude-from=") ||
3976 sq_quote(indexcmd, cmdsize, exclude) >= sizeof(indexcmd)))
3977 return FALSE;
3980 system("git update-index -q --refresh >/dev/null 2>/dev/null");
3982 if (!status_run(view, indexcmd, indexstatus, LINE_STAT_STAGED) ||
3983 !status_run(view, STATUS_DIFF_FILES_CMD, 0, LINE_STAT_UNSTAGED) ||
3984 !status_run(view, othercmd, '?', LINE_STAT_UNTRACKED))
3985 return FALSE;
3987 /* If all went well restore the previous line number to stay in
3988 * the context or select a line with something that can be
3989 * updated. */
3990 if (prev_lineno >= view->lines)
3991 prev_lineno = view->lines - 1;
3992 while (prev_lineno < view->lines && !view->line[prev_lineno].data)
3993 prev_lineno++;
3994 while (prev_lineno > 0 && !view->line[prev_lineno].data)
3995 prev_lineno--;
3997 /* If the above fails, always skip the "On branch" line. */
3998 if (prev_lineno < view->lines)
3999 view->lineno = prev_lineno;
4000 else
4001 view->lineno = 1;
4003 if (view->lineno < view->offset)
4004 view->offset = view->lineno;
4005 else if (view->offset + view->height <= view->lineno)
4006 view->offset = view->lineno - view->height + 1;
4008 return TRUE;
4011 static bool
4012 status_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
4014 struct status *status = line->data;
4015 char *text;
4016 int col = 0;
4018 if (selected) {
4019 /* No attributes. */
4021 } else if (line->type == LINE_STAT_HEAD) {
4022 wattrset(view->win, get_line_attr(LINE_STAT_HEAD));
4023 wchgat(view->win, -1, 0, LINE_STAT_HEAD, NULL);
4025 } else if (!status && line->type != LINE_STAT_NONE) {
4026 wattrset(view->win, get_line_attr(LINE_STAT_SECTION));
4027 wchgat(view->win, -1, 0, LINE_STAT_SECTION, NULL);
4029 } else {
4030 wattrset(view->win, get_line_attr(line->type));
4033 if (!status) {
4034 switch (line->type) {
4035 case LINE_STAT_STAGED:
4036 text = "Changes to be committed:";
4037 break;
4039 case LINE_STAT_UNSTAGED:
4040 text = "Changed but not updated:";
4041 break;
4043 case LINE_STAT_UNTRACKED:
4044 text = "Untracked files:";
4045 break;
4047 case LINE_STAT_NONE:
4048 text = " (no files)";
4049 break;
4051 case LINE_STAT_HEAD:
4052 text = status_onbranch;
4053 break;
4055 default:
4056 return FALSE;
4058 } else {
4059 char buf[] = { status->status, ' ', ' ', ' ', 0 };
4061 col += draw_text(view, buf, view->width, TRUE, selected);
4062 if (!selected)
4063 wattrset(view->win, A_NORMAL);
4064 text = status->new.name;
4067 draw_text(view, text, view->width - col, TRUE, selected);
4068 return TRUE;
4071 static enum request
4072 status_enter(struct view *view, struct line *line)
4074 struct status *status = line->data;
4075 char oldpath[SIZEOF_STR] = "";
4076 char newpath[SIZEOF_STR] = "";
4077 char *info;
4078 size_t cmdsize = 0;
4079 enum open_flags split;
4081 if (line->type == LINE_STAT_NONE ||
4082 (!status && line[1].type == LINE_STAT_NONE)) {
4083 report("No file to diff");
4084 return REQ_NONE;
4087 if (status) {
4088 if (sq_quote(oldpath, 0, status->old.name) >= sizeof(oldpath))
4089 return REQ_QUIT;
4090 /* Diffs for unmerged entries are empty when pasing the
4091 * new path, so leave it empty. */
4092 if (status->status != 'U' &&
4093 sq_quote(newpath, 0, status->new.name) >= sizeof(newpath))
4094 return REQ_QUIT;
4097 if (opt_cdup[0] &&
4098 line->type != LINE_STAT_UNTRACKED &&
4099 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
4100 return REQ_QUIT;
4102 switch (line->type) {
4103 case LINE_STAT_STAGED:
4104 if (opt_no_head) {
4105 if (!string_format_from(opt_cmd, &cmdsize,
4106 STATUS_DIFF_NO_HEAD_SHOW_CMD,
4107 newpath))
4108 return REQ_QUIT;
4109 } else {
4110 if (!string_format_from(opt_cmd, &cmdsize,
4111 STATUS_DIFF_INDEX_SHOW_CMD,
4112 oldpath, newpath))
4113 return REQ_QUIT;
4116 if (status)
4117 info = "Staged changes to %s";
4118 else
4119 info = "Staged changes";
4120 break;
4122 case LINE_STAT_UNSTAGED:
4123 if (!string_format_from(opt_cmd, &cmdsize,
4124 STATUS_DIFF_FILES_SHOW_CMD, oldpath, newpath))
4125 return REQ_QUIT;
4126 if (status)
4127 info = "Unstaged changes to %s";
4128 else
4129 info = "Unstaged changes";
4130 break;
4132 case LINE_STAT_UNTRACKED:
4133 if (opt_pipe)
4134 return REQ_QUIT;
4136 if (!status) {
4137 report("No file to show");
4138 return REQ_NONE;
4141 opt_pipe = fopen(status->new.name, "r");
4142 info = "Untracked file %s";
4143 break;
4145 case LINE_STAT_HEAD:
4146 return REQ_NONE;
4148 default:
4149 die("line type %d not handled in switch", line->type);
4152 split = view_is_displayed(view) ? OPEN_SPLIT : 0;
4153 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | split);
4154 if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) {
4155 if (status) {
4156 stage_status = *status;
4157 } else {
4158 memset(&stage_status, 0, sizeof(stage_status));
4161 stage_line_type = line->type;
4162 string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.new.name);
4165 return REQ_NONE;
4168 static bool
4169 status_exists(struct status *status, enum line_type type)
4171 struct view *view = VIEW(REQ_VIEW_STATUS);
4172 struct line *line;
4174 for (line = view->line; line < view->line + view->lines; line++) {
4175 struct status *pos = line->data;
4177 if (line->type == type && pos &&
4178 !strcmp(status->new.name, pos->new.name))
4179 return TRUE;
4182 return FALSE;
4186 static FILE *
4187 status_update_prepare(enum line_type type)
4189 char cmd[SIZEOF_STR];
4190 size_t cmdsize = 0;
4192 if (opt_cdup[0] &&
4193 type != LINE_STAT_UNTRACKED &&
4194 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4195 return NULL;
4197 switch (type) {
4198 case LINE_STAT_STAGED:
4199 string_add(cmd, cmdsize, "git update-index -z --index-info");
4200 break;
4202 case LINE_STAT_UNSTAGED:
4203 case LINE_STAT_UNTRACKED:
4204 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
4205 break;
4207 default:
4208 die("line type %d not handled in switch", type);
4211 return popen(cmd, "w");
4214 static bool
4215 status_update_write(FILE *pipe, struct status *status, enum line_type type)
4217 char buf[SIZEOF_STR];
4218 size_t bufsize = 0;
4219 size_t written = 0;
4221 switch (type) {
4222 case LINE_STAT_STAGED:
4223 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
4224 status->old.mode,
4225 status->old.rev,
4226 status->old.name, 0))
4227 return FALSE;
4228 break;
4230 case LINE_STAT_UNSTAGED:
4231 case LINE_STAT_UNTRACKED:
4232 if (!string_format_from(buf, &bufsize, "%s%c", status->new.name, 0))
4233 return FALSE;
4234 break;
4236 default:
4237 die("line type %d not handled in switch", type);
4240 while (!ferror(pipe) && written < bufsize) {
4241 written += fwrite(buf + written, 1, bufsize - written, pipe);
4244 return written == bufsize;
4247 static bool
4248 status_update_file(struct status *status, enum line_type type)
4250 FILE *pipe = status_update_prepare(type);
4251 bool result;
4253 if (!pipe)
4254 return FALSE;
4256 result = status_update_write(pipe, status, type);
4257 pclose(pipe);
4258 return result;
4261 static bool
4262 status_update_files(struct view *view, struct line *line)
4264 FILE *pipe = status_update_prepare(line->type);
4265 bool result = TRUE;
4266 struct line *pos = view->line + view->lines;
4267 int files = 0;
4268 int file, done;
4270 if (!pipe)
4271 return FALSE;
4273 for (pos = line; pos < view->line + view->lines && pos->data; pos++)
4274 files++;
4276 for (file = 0, done = 0; result && file < files; line++, file++) {
4277 int almost_done = file * 100 / files;
4279 if (almost_done > done) {
4280 done = almost_done;
4281 string_format(view->ref, "updating file %u of %u (%d%% done)",
4282 file, files, done);
4283 update_view_title(view);
4285 result = status_update_write(pipe, line->data, line->type);
4288 pclose(pipe);
4289 return result;
4292 static bool
4293 status_update(struct view *view)
4295 struct line *line = &view->line[view->lineno];
4297 assert(view->lines);
4299 if (!line->data) {
4300 /* This should work even for the "On branch" line. */
4301 if (line < view->line + view->lines && !line[1].data) {
4302 report("Nothing to update");
4303 return FALSE;
4306 if (!status_update_files(view, line + 1)) {
4307 report("Failed to update file status");
4308 return FALSE;
4311 } else if (!status_update_file(line->data, line->type)) {
4312 report("Failed to update file status");
4313 return FALSE;
4316 return TRUE;
4319 static enum request
4320 status_request(struct view *view, enum request request, struct line *line)
4322 struct status *status = line->data;
4324 switch (request) {
4325 case REQ_STATUS_UPDATE:
4326 if (!status_update(view))
4327 return REQ_NONE;
4328 break;
4330 case REQ_STATUS_MERGE:
4331 if (!status || status->status != 'U') {
4332 report("Merging only possible for files with unmerged status ('U').");
4333 return REQ_NONE;
4335 open_mergetool(status->new.name);
4336 break;
4338 case REQ_EDIT:
4339 if (!status)
4340 return request;
4342 open_editor(status->status != '?', status->new.name);
4343 break;
4345 case REQ_VIEW_BLAME:
4346 if (status) {
4347 string_copy(opt_file, status->new.name);
4348 opt_ref[0] = 0;
4350 return request;
4352 case REQ_ENTER:
4353 /* After returning the status view has been split to
4354 * show the stage view. No further reloading is
4355 * necessary. */
4356 status_enter(view, line);
4357 return REQ_NONE;
4359 case REQ_REFRESH:
4360 /* Simply reload the view. */
4361 break;
4363 default:
4364 return request;
4367 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
4369 return REQ_NONE;
4372 static void
4373 status_select(struct view *view, struct line *line)
4375 struct status *status = line->data;
4376 char file[SIZEOF_STR] = "all files";
4377 char *text;
4378 char *key;
4380 if (status && !string_format(file, "'%s'", status->new.name))
4381 return;
4383 if (!status && line[1].type == LINE_STAT_NONE)
4384 line++;
4386 switch (line->type) {
4387 case LINE_STAT_STAGED:
4388 text = "Press %s to unstage %s for commit";
4389 break;
4391 case LINE_STAT_UNSTAGED:
4392 text = "Press %s to stage %s for commit";
4393 break;
4395 case LINE_STAT_UNTRACKED:
4396 text = "Press %s to stage %s for addition";
4397 break;
4399 case LINE_STAT_HEAD:
4400 case LINE_STAT_NONE:
4401 text = "Nothing to update";
4402 break;
4404 default:
4405 die("line type %d not handled in switch", line->type);
4408 if (status && status->status == 'U') {
4409 text = "Press %s to resolve conflict in %s";
4410 key = get_key(REQ_STATUS_MERGE);
4412 } else {
4413 key = get_key(REQ_STATUS_UPDATE);
4416 string_format(view->ref, text, key, file);
4419 static bool
4420 status_grep(struct view *view, struct line *line)
4422 struct status *status = line->data;
4423 enum { S_STATUS, S_NAME, S_END } state;
4424 char buf[2] = "?";
4425 regmatch_t pmatch;
4427 if (!status)
4428 return FALSE;
4430 for (state = S_STATUS; state < S_END; state++) {
4431 char *text;
4433 switch (state) {
4434 case S_NAME: text = status->new.name; break;
4435 case S_STATUS:
4436 buf[0] = status->status;
4437 text = buf;
4438 break;
4440 default:
4441 return FALSE;
4444 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4445 return TRUE;
4448 return FALSE;
4451 static struct view_ops status_ops = {
4452 "file",
4453 status_open,
4454 NULL,
4455 status_draw,
4456 status_request,
4457 status_grep,
4458 status_select,
4462 static bool
4463 stage_diff_line(FILE *pipe, struct line *line)
4465 char *buf = line->data;
4466 size_t bufsize = strlen(buf);
4467 size_t written = 0;
4469 while (!ferror(pipe) && written < bufsize) {
4470 written += fwrite(buf + written, 1, bufsize - written, pipe);
4473 fputc('\n', pipe);
4475 return written == bufsize;
4478 static bool
4479 stage_diff_write(FILE *pipe, struct line *line, struct line *end)
4481 while (line < end) {
4482 if (!stage_diff_line(pipe, line++))
4483 return FALSE;
4484 if (line->type == LINE_DIFF_CHUNK ||
4485 line->type == LINE_DIFF_HEADER)
4486 break;
4489 return TRUE;
4492 static struct line *
4493 stage_diff_find(struct view *view, struct line *line, enum line_type type)
4495 for (; view->line < line; line--)
4496 if (line->type == type)
4497 return line;
4499 return NULL;
4502 static bool
4503 stage_update_chunk(struct view *view, struct line *chunk)
4505 char cmd[SIZEOF_STR];
4506 size_t cmdsize = 0;
4507 struct line *diff_hdr;
4508 FILE *pipe;
4510 diff_hdr = stage_diff_find(view, chunk, LINE_DIFF_HEADER);
4511 if (!diff_hdr)
4512 return FALSE;
4514 if (opt_cdup[0] &&
4515 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4516 return FALSE;
4518 if (!string_format_from(cmd, &cmdsize,
4519 "git apply --whitespace=nowarn --cached %s - && "
4520 "git update-index -q --unmerged --refresh 2>/dev/null",
4521 stage_line_type == LINE_STAT_STAGED ? "-R" : ""))
4522 return FALSE;
4524 pipe = popen(cmd, "w");
4525 if (!pipe)
4526 return FALSE;
4528 if (!stage_diff_write(pipe, diff_hdr, chunk) ||
4529 !stage_diff_write(pipe, chunk, view->line + view->lines))
4530 chunk = NULL;
4532 pclose(pipe);
4534 return chunk ? TRUE : FALSE;
4537 static bool
4538 stage_update(struct view *view, struct line *line)
4540 struct line *chunk = NULL;
4542 if (!opt_no_head && stage_line_type != LINE_STAT_UNTRACKED)
4543 chunk = stage_diff_find(view, line, LINE_DIFF_CHUNK);
4545 if (chunk) {
4546 if (!stage_update_chunk(view, chunk)) {
4547 report("Failed to apply chunk");
4548 return FALSE;
4551 } else if (!status_update_file(&stage_status, stage_line_type)) {
4552 report("Failed to update file");
4553 return FALSE;
4556 return TRUE;
4559 static enum request
4560 stage_request(struct view *view, enum request request, struct line *line)
4562 switch (request) {
4563 case REQ_STATUS_UPDATE:
4564 if (!stage_update(view, line))
4565 return REQ_NONE;
4566 break;
4568 case REQ_EDIT:
4569 if (!stage_status.new.name[0])
4570 return request;
4572 open_editor(stage_status.status != '?', stage_status.new.name);
4573 break;
4575 case REQ_REFRESH:
4576 /* Reload everything ... */
4577 break;
4579 case REQ_VIEW_BLAME:
4580 if (stage_status.new.name[0]) {
4581 string_copy(opt_file, stage_status.new.name);
4582 opt_ref[0] = 0;
4584 return request;
4586 case REQ_ENTER:
4587 return pager_request(view, request, line);
4589 default:
4590 return request;
4593 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD | OPEN_NOMAXIMIZE);
4595 /* Check whether the staged entry still exists, and close the
4596 * stage view if it doesn't. */
4597 if (!status_exists(&stage_status, stage_line_type))
4598 return REQ_VIEW_CLOSE;
4600 if (stage_line_type == LINE_STAT_UNTRACKED)
4601 opt_pipe = fopen(stage_status.new.name, "r");
4602 else
4603 string_copy(opt_cmd, view->cmd);
4604 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | OPEN_NOMAXIMIZE);
4606 return REQ_NONE;
4609 static struct view_ops stage_ops = {
4610 "line",
4611 NULL,
4612 pager_read,
4613 pager_draw,
4614 stage_request,
4615 pager_grep,
4616 pager_select,
4621 * Revision graph
4624 struct commit {
4625 char id[SIZEOF_REV]; /* SHA1 ID. */
4626 char title[128]; /* First line of the commit message. */
4627 char author[75]; /* Author of the commit. */
4628 struct tm time; /* Date from the author ident. */
4629 struct ref **refs; /* Repository references. */
4630 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
4631 size_t graph_size; /* The width of the graph array. */
4632 bool has_parents; /* Rewritten --parents seen. */
4635 /* Size of rev graph with no "padding" columns */
4636 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
4638 struct rev_graph {
4639 struct rev_graph *prev, *next, *parents;
4640 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
4641 size_t size;
4642 struct commit *commit;
4643 size_t pos;
4644 unsigned int boundary:1;
4647 /* Parents of the commit being visualized. */
4648 static struct rev_graph graph_parents[4];
4650 /* The current stack of revisions on the graph. */
4651 static struct rev_graph graph_stacks[4] = {
4652 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
4653 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
4654 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
4655 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
4658 static inline bool
4659 graph_parent_is_merge(struct rev_graph *graph)
4661 return graph->parents->size > 1;
4664 static inline void
4665 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
4667 struct commit *commit = graph->commit;
4669 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
4670 commit->graph[commit->graph_size++] = symbol;
4673 static void
4674 done_rev_graph(struct rev_graph *graph)
4676 if (graph_parent_is_merge(graph) &&
4677 graph->pos < graph->size - 1 &&
4678 graph->next->size == graph->size + graph->parents->size - 1) {
4679 size_t i = graph->pos + graph->parents->size - 1;
4681 graph->commit->graph_size = i * 2;
4682 while (i < graph->next->size - 1) {
4683 append_to_rev_graph(graph, ' ');
4684 append_to_rev_graph(graph, '\\');
4685 i++;
4689 graph->size = graph->pos = 0;
4690 graph->commit = NULL;
4691 memset(graph->parents, 0, sizeof(*graph->parents));
4694 static void
4695 push_rev_graph(struct rev_graph *graph, char *parent)
4697 int i;
4699 /* "Collapse" duplicate parents lines.
4701 * FIXME: This needs to also update update the drawn graph but
4702 * for now it just serves as a method for pruning graph lines. */
4703 for (i = 0; i < graph->size; i++)
4704 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
4705 return;
4707 if (graph->size < SIZEOF_REVITEMS) {
4708 string_copy_rev(graph->rev[graph->size++], parent);
4712 static chtype
4713 get_rev_graph_symbol(struct rev_graph *graph)
4715 chtype symbol;
4717 if (graph->boundary)
4718 symbol = REVGRAPH_BOUND;
4719 else if (graph->parents->size == 0)
4720 symbol = REVGRAPH_INIT;
4721 else if (graph_parent_is_merge(graph))
4722 symbol = REVGRAPH_MERGE;
4723 else if (graph->pos >= graph->size)
4724 symbol = REVGRAPH_BRANCH;
4725 else
4726 symbol = REVGRAPH_COMMIT;
4728 return symbol;
4731 static void
4732 draw_rev_graph(struct rev_graph *graph)
4734 struct rev_filler {
4735 chtype separator, line;
4737 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
4738 static struct rev_filler fillers[] = {
4739 { ' ', REVGRAPH_LINE },
4740 { '`', '.' },
4741 { '\'', ' ' },
4742 { '/', ' ' },
4744 chtype symbol = get_rev_graph_symbol(graph);
4745 struct rev_filler *filler;
4746 size_t i;
4748 filler = &fillers[DEFAULT];
4750 for (i = 0; i < graph->pos; i++) {
4751 append_to_rev_graph(graph, filler->line);
4752 if (graph_parent_is_merge(graph->prev) &&
4753 graph->prev->pos == i)
4754 filler = &fillers[RSHARP];
4756 append_to_rev_graph(graph, filler->separator);
4759 /* Place the symbol for this revision. */
4760 append_to_rev_graph(graph, symbol);
4762 if (graph->prev->size > graph->size)
4763 filler = &fillers[RDIAG];
4764 else
4765 filler = &fillers[DEFAULT];
4767 i++;
4769 for (; i < graph->size; i++) {
4770 append_to_rev_graph(graph, filler->separator);
4771 append_to_rev_graph(graph, filler->line);
4772 if (graph_parent_is_merge(graph->prev) &&
4773 i < graph->prev->pos + graph->parents->size)
4774 filler = &fillers[RSHARP];
4775 if (graph->prev->size > graph->size)
4776 filler = &fillers[LDIAG];
4779 if (graph->prev->size > graph->size) {
4780 append_to_rev_graph(graph, filler->separator);
4781 if (filler->line != ' ')
4782 append_to_rev_graph(graph, filler->line);
4786 /* Prepare the next rev graph */
4787 static void
4788 prepare_rev_graph(struct rev_graph *graph)
4790 size_t i;
4792 /* First, traverse all lines of revisions up to the active one. */
4793 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
4794 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
4795 break;
4797 push_rev_graph(graph->next, graph->rev[graph->pos]);
4800 /* Interleave the new revision parent(s). */
4801 for (i = 0; !graph->boundary && i < graph->parents->size; i++)
4802 push_rev_graph(graph->next, graph->parents->rev[i]);
4804 /* Lastly, put any remaining revisions. */
4805 for (i = graph->pos + 1; i < graph->size; i++)
4806 push_rev_graph(graph->next, graph->rev[i]);
4809 static void
4810 update_rev_graph(struct rev_graph *graph)
4812 /* If this is the finalizing update ... */
4813 if (graph->commit)
4814 prepare_rev_graph(graph);
4816 /* Graph visualization needs a one rev look-ahead,
4817 * so the first update doesn't visualize anything. */
4818 if (!graph->prev->commit)
4819 return;
4821 draw_rev_graph(graph->prev);
4822 done_rev_graph(graph->prev->prev);
4827 * Main view backend
4830 static bool
4831 main_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
4833 struct commit *commit = line->data;
4834 enum line_type type;
4835 int col = 0;
4837 if (!*commit->author)
4838 return FALSE;
4840 if (selected) {
4841 type = LINE_CURSOR;
4842 } else {
4843 type = LINE_MAIN_COMMIT;
4846 if (opt_date) {
4847 col += draw_date(view, &commit->time, view->width, selected);
4848 if (col >= view->width)
4849 return TRUE;
4851 if (type != LINE_CURSOR)
4852 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
4854 if (opt_author) {
4855 int max_len;
4857 max_len = view->width - col;
4858 if (max_len > AUTHOR_COLS - 1)
4859 max_len = AUTHOR_COLS - 1;
4860 draw_text(view, commit->author, max_len, TRUE, selected);
4861 col += AUTHOR_COLS;
4862 if (col >= view->width)
4863 return TRUE;
4866 if (opt_rev_graph && commit->graph_size) {
4867 size_t graph_size = view->width - col;
4868 size_t i;
4870 if (type != LINE_CURSOR)
4871 wattrset(view->win, get_line_attr(LINE_MAIN_REVGRAPH));
4872 wmove(view->win, lineno, col);
4873 if (graph_size > commit->graph_size)
4874 graph_size = commit->graph_size;
4875 /* Using waddch() instead of waddnstr() ensures that
4876 * they'll be rendered correctly for the cursor line. */
4877 for (i = 0; i < graph_size; i++)
4878 waddch(view->win, commit->graph[i]);
4880 col += commit->graph_size + 1;
4881 if (col >= view->width)
4882 return TRUE;
4883 waddch(view->win, ' ');
4885 if (type != LINE_CURSOR)
4886 wattrset(view->win, A_NORMAL);
4888 wmove(view->win, lineno, col);
4890 if (opt_show_refs && commit->refs) {
4891 size_t i = 0;
4893 do {
4894 if (type == LINE_CURSOR)
4896 else if (commit->refs[i]->head)
4897 wattrset(view->win, get_line_attr(LINE_MAIN_HEAD));
4898 else if (commit->refs[i]->ltag)
4899 wattrset(view->win, get_line_attr(LINE_MAIN_LOCAL_TAG));
4900 else if (commit->refs[i]->tag)
4901 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
4902 else if (commit->refs[i]->tracked)
4903 wattrset(view->win, get_line_attr(LINE_MAIN_TRACKED));
4904 else if (commit->refs[i]->remote)
4905 wattrset(view->win, get_line_attr(LINE_MAIN_REMOTE));
4906 else
4907 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
4909 col += draw_text(view, "[", view->width - col, TRUE, selected);
4910 col += draw_text(view, commit->refs[i]->name, view->width - col,
4911 TRUE, selected);
4912 col += draw_text(view, "]", view->width - col, TRUE, selected);
4913 if (type != LINE_CURSOR)
4914 wattrset(view->win, A_NORMAL);
4915 col += draw_text(view, " ", view->width - col, TRUE, selected);
4916 if (col >= view->width)
4917 return TRUE;
4918 } while (commit->refs[i++]->next);
4921 if (type != LINE_CURSOR)
4922 wattrset(view->win, get_line_attr(type));
4924 draw_text(view, commit->title, view->width - col, TRUE, selected);
4925 return TRUE;
4928 /* Reads git log --pretty=raw output and parses it into the commit struct. */
4929 static bool
4930 main_read(struct view *view, char *line)
4932 static struct rev_graph *graph = graph_stacks;
4933 enum line_type type;
4934 struct commit *commit;
4936 if (!line) {
4937 if (!view->lines && !view->parent)
4938 die("No revisions match the given arguments.");
4939 update_rev_graph(graph);
4940 return TRUE;
4943 type = get_line_type(line);
4944 if (type == LINE_COMMIT) {
4945 commit = calloc(1, sizeof(struct commit));
4946 if (!commit)
4947 return FALSE;
4949 line += STRING_SIZE("commit ");
4950 if (*line == '-') {
4951 graph->boundary = 1;
4952 line++;
4955 string_copy_rev(commit->id, line);
4956 commit->refs = get_refs(commit->id);
4957 graph->commit = commit;
4958 add_line_data(view, commit, LINE_MAIN_COMMIT);
4960 while ((line = strchr(line, ' '))) {
4961 line++;
4962 push_rev_graph(graph->parents, line);
4963 commit->has_parents = TRUE;
4965 return TRUE;
4968 if (!view->lines)
4969 return TRUE;
4970 commit = view->line[view->lines - 1].data;
4972 switch (type) {
4973 case LINE_PARENT:
4974 if (commit->has_parents)
4975 break;
4976 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
4977 break;
4979 case LINE_AUTHOR:
4981 /* Parse author lines where the name may be empty:
4982 * author <email@address.tld> 1138474660 +0100
4984 char *ident = line + STRING_SIZE("author ");
4985 char *nameend = strchr(ident, '<');
4986 char *emailend = strchr(ident, '>');
4988 if (!nameend || !emailend)
4989 break;
4991 update_rev_graph(graph);
4992 graph = graph->next;
4994 *nameend = *emailend = 0;
4995 ident = chomp_string(ident);
4996 if (!*ident) {
4997 ident = chomp_string(nameend + 1);
4998 if (!*ident)
4999 ident = "Unknown";
5002 string_ncopy(commit->author, ident, strlen(ident));
5004 /* Parse epoch and timezone */
5005 if (emailend[1] == ' ') {
5006 char *secs = emailend + 2;
5007 char *zone = strchr(secs, ' ');
5008 time_t time = (time_t) atol(secs);
5010 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
5011 long tz;
5013 zone++;
5014 tz = ('0' - zone[1]) * 60 * 60 * 10;
5015 tz += ('0' - zone[2]) * 60 * 60;
5016 tz += ('0' - zone[3]) * 60;
5017 tz += ('0' - zone[4]) * 60;
5019 if (zone[0] == '-')
5020 tz = -tz;
5022 time -= tz;
5025 gmtime_r(&time, &commit->time);
5027 break;
5029 default:
5030 /* Fill in the commit title if it has not already been set. */
5031 if (commit->title[0])
5032 break;
5034 /* Require titles to start with a non-space character at the
5035 * offset used by git log. */
5036 if (strncmp(line, " ", 4))
5037 break;
5038 line += 4;
5039 /* Well, if the title starts with a whitespace character,
5040 * try to be forgiving. Otherwise we end up with no title. */
5041 while (isspace(*line))
5042 line++;
5043 if (*line == '\0')
5044 break;
5045 /* FIXME: More graceful handling of titles; append "..." to
5046 * shortened titles, etc. */
5048 string_ncopy(commit->title, line, strlen(line));
5051 return TRUE;
5054 static enum request
5055 main_request(struct view *view, enum request request, struct line *line)
5057 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
5059 if (request == REQ_ENTER)
5060 open_view(view, REQ_VIEW_DIFF, flags);
5061 else
5062 return request;
5064 return REQ_NONE;
5067 static bool
5068 main_grep(struct view *view, struct line *line)
5070 struct commit *commit = line->data;
5071 enum { S_TITLE, S_AUTHOR, S_DATE, S_END } state;
5072 char buf[DATE_COLS + 1];
5073 regmatch_t pmatch;
5075 for (state = S_TITLE; state < S_END; state++) {
5076 char *text;
5078 switch (state) {
5079 case S_TITLE: text = commit->title; break;
5080 case S_AUTHOR: text = commit->author; break;
5081 case S_DATE:
5082 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
5083 continue;
5084 text = buf;
5085 break;
5087 default:
5088 return FALSE;
5091 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
5092 return TRUE;
5095 return FALSE;
5098 static void
5099 main_select(struct view *view, struct line *line)
5101 struct commit *commit = line->data;
5103 string_copy_rev(view->ref, commit->id);
5104 string_copy_rev(ref_commit, view->ref);
5107 static struct view_ops main_ops = {
5108 "commit",
5109 NULL,
5110 main_read,
5111 main_draw,
5112 main_request,
5113 main_grep,
5114 main_select,
5119 * Unicode / UTF-8 handling
5121 * NOTE: Much of the following code for dealing with unicode is derived from
5122 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
5123 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
5126 /* I've (over)annotated a lot of code snippets because I am not entirely
5127 * confident that the approach taken by this small UTF-8 interface is correct.
5128 * --jonas */
5130 static inline int
5131 unicode_width(unsigned long c)
5133 if (c >= 0x1100 &&
5134 (c <= 0x115f /* Hangul Jamo */
5135 || c == 0x2329
5136 || c == 0x232a
5137 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
5138 /* CJK ... Yi */
5139 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
5140 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
5141 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
5142 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
5143 || (c >= 0xffe0 && c <= 0xffe6)
5144 || (c >= 0x20000 && c <= 0x2fffd)
5145 || (c >= 0x30000 && c <= 0x3fffd)))
5146 return 2;
5148 if (c == '\t')
5149 return opt_tab_size;
5151 return 1;
5154 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
5155 * Illegal bytes are set one. */
5156 static const unsigned char utf8_bytes[256] = {
5157 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,
5158 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,
5159 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
5160 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
5161 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
5162 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
5163 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,
5164 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,
5167 /* Decode UTF-8 multi-byte representation into a unicode character. */
5168 static inline unsigned long
5169 utf8_to_unicode(const char *string, size_t length)
5171 unsigned long unicode;
5173 switch (length) {
5174 case 1:
5175 unicode = string[0];
5176 break;
5177 case 2:
5178 unicode = (string[0] & 0x1f) << 6;
5179 unicode += (string[1] & 0x3f);
5180 break;
5181 case 3:
5182 unicode = (string[0] & 0x0f) << 12;
5183 unicode += ((string[1] & 0x3f) << 6);
5184 unicode += (string[2] & 0x3f);
5185 break;
5186 case 4:
5187 unicode = (string[0] & 0x0f) << 18;
5188 unicode += ((string[1] & 0x3f) << 12);
5189 unicode += ((string[2] & 0x3f) << 6);
5190 unicode += (string[3] & 0x3f);
5191 break;
5192 case 5:
5193 unicode = (string[0] & 0x0f) << 24;
5194 unicode += ((string[1] & 0x3f) << 18);
5195 unicode += ((string[2] & 0x3f) << 12);
5196 unicode += ((string[3] & 0x3f) << 6);
5197 unicode += (string[4] & 0x3f);
5198 break;
5199 case 6:
5200 unicode = (string[0] & 0x01) << 30;
5201 unicode += ((string[1] & 0x3f) << 24);
5202 unicode += ((string[2] & 0x3f) << 18);
5203 unicode += ((string[3] & 0x3f) << 12);
5204 unicode += ((string[4] & 0x3f) << 6);
5205 unicode += (string[5] & 0x3f);
5206 break;
5207 default:
5208 die("Invalid unicode length");
5211 /* Invalid characters could return the special 0xfffd value but NUL
5212 * should be just as good. */
5213 return unicode > 0xffff ? 0 : unicode;
5216 /* Calculates how much of string can be shown within the given maximum width
5217 * and sets trimmed parameter to non-zero value if all of string could not be
5218 * shown. If the reserve flag is TRUE, it will reserve at least one
5219 * trailing character, which can be useful when drawing a delimiter.
5221 * Returns the number of bytes to output from string to satisfy max_width. */
5222 static size_t
5223 utf8_length(const char *string, size_t max_width, int *trimmed, bool reserve)
5225 const char *start = string;
5226 const char *end = strchr(string, '\0');
5227 unsigned char last_bytes = 0;
5228 size_t width = 0;
5230 *trimmed = 0;
5232 while (string < end) {
5233 int c = *(unsigned char *) string;
5234 unsigned char bytes = utf8_bytes[c];
5235 size_t ucwidth;
5236 unsigned long unicode;
5238 if (string + bytes > end)
5239 break;
5241 /* Change representation to figure out whether
5242 * it is a single- or double-width character. */
5244 unicode = utf8_to_unicode(string, bytes);
5245 /* FIXME: Graceful handling of invalid unicode character. */
5246 if (!unicode)
5247 break;
5249 ucwidth = unicode_width(unicode);
5250 width += ucwidth;
5251 if (width > max_width) {
5252 *trimmed = 1;
5253 if (reserve && width - ucwidth == max_width) {
5254 string -= last_bytes;
5256 break;
5259 string += bytes;
5260 last_bytes = bytes;
5263 return string - start;
5268 * Status management
5271 /* Whether or not the curses interface has been initialized. */
5272 static bool cursed = FALSE;
5274 /* The status window is used for polling keystrokes. */
5275 static WINDOW *status_win;
5277 static bool status_empty = TRUE;
5279 /* Update status and title window. */
5280 static void
5281 report(const char *msg, ...)
5283 struct view *view = display[current_view];
5285 if (input_mode)
5286 return;
5288 if (!view) {
5289 char buf[SIZEOF_STR];
5290 va_list args;
5292 va_start(args, msg);
5293 if (vsnprintf(buf, sizeof(buf), msg, args) >= sizeof(buf)) {
5294 buf[sizeof(buf) - 1] = 0;
5295 buf[sizeof(buf) - 2] = '.';
5296 buf[sizeof(buf) - 3] = '.';
5297 buf[sizeof(buf) - 4] = '.';
5299 va_end(args);
5300 die("%s", buf);
5303 if (!status_empty || *msg) {
5304 va_list args;
5306 va_start(args, msg);
5308 wmove(status_win, 0, 0);
5309 if (*msg) {
5310 vwprintw(status_win, msg, args);
5311 status_empty = FALSE;
5312 } else {
5313 status_empty = TRUE;
5315 wclrtoeol(status_win);
5316 wrefresh(status_win);
5318 va_end(args);
5321 update_view_title(view);
5322 update_display_cursor(view);
5325 /* Controls when nodelay should be in effect when polling user input. */
5326 static void
5327 set_nonblocking_input(bool loading)
5329 static unsigned int loading_views;
5331 if ((loading == FALSE && loading_views-- == 1) ||
5332 (loading == TRUE && loading_views++ == 0))
5333 nodelay(status_win, loading);
5336 static void
5337 init_display(void)
5339 int x, y;
5341 /* Initialize the curses library */
5342 if (isatty(STDIN_FILENO)) {
5343 cursed = !!initscr();
5344 } else {
5345 /* Leave stdin and stdout alone when acting as a pager. */
5346 FILE *io = fopen("/dev/tty", "r+");
5348 if (!io)
5349 die("Failed to open /dev/tty");
5350 cursed = !!newterm(NULL, io, io);
5353 if (!cursed)
5354 die("Failed to initialize curses");
5356 nonl(); /* Tell curses not to do NL->CR/NL on output */
5357 cbreak(); /* Take input chars one at a time, no wait for \n */
5358 noecho(); /* Don't echo input */
5359 leaveok(stdscr, TRUE);
5361 if (has_colors())
5362 init_colors();
5364 getmaxyx(stdscr, y, x);
5365 status_win = newwin(1, 0, y - 1, 0);
5366 if (!status_win)
5367 die("Failed to create status window");
5369 /* Enable keyboard mapping */
5370 keypad(status_win, TRUE);
5371 wbkgdset(status_win, get_line_attr(LINE_STATUS));
5374 static char *
5375 read_prompt(const char *prompt)
5377 enum { READING, STOP, CANCEL } status = READING;
5378 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
5379 int pos = 0;
5381 while (status == READING) {
5382 struct view *view;
5383 int i, key;
5385 input_mode = TRUE;
5387 foreach_view (view, i)
5388 update_view(view);
5390 input_mode = FALSE;
5392 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
5393 wclrtoeol(status_win);
5395 /* Refresh, accept single keystroke of input */
5396 key = wgetch(status_win);
5397 switch (key) {
5398 case KEY_RETURN:
5399 case KEY_ENTER:
5400 case '\n':
5401 status = pos ? STOP : CANCEL;
5402 break;
5404 case KEY_BACKSPACE:
5405 if (pos > 0)
5406 pos--;
5407 else
5408 status = CANCEL;
5409 break;
5411 case KEY_ESC:
5412 status = CANCEL;
5413 break;
5415 case ERR:
5416 break;
5418 default:
5419 if (pos >= sizeof(buf)) {
5420 report("Input string too long");
5421 return NULL;
5424 if (isprint(key))
5425 buf[pos++] = (char) key;
5429 /* Clear the status window */
5430 status_empty = FALSE;
5431 report("");
5433 if (status == CANCEL)
5434 return NULL;
5436 buf[pos++] = 0;
5438 return buf;
5442 * Repository references
5445 static struct ref *refs = NULL;
5446 static size_t refs_alloc = 0;
5447 static size_t refs_size = 0;
5449 /* Id <-> ref store */
5450 static struct ref ***id_refs = NULL;
5451 static size_t id_refs_alloc = 0;
5452 static size_t id_refs_size = 0;
5454 static struct ref **
5455 get_refs(char *id)
5457 struct ref ***tmp_id_refs;
5458 struct ref **ref_list = NULL;
5459 size_t ref_list_alloc = 0;
5460 size_t ref_list_size = 0;
5461 size_t i;
5463 for (i = 0; i < id_refs_size; i++)
5464 if (!strcmp(id, id_refs[i][0]->id))
5465 return id_refs[i];
5467 tmp_id_refs = realloc_items(id_refs, &id_refs_alloc, id_refs_size + 1,
5468 sizeof(*id_refs));
5469 if (!tmp_id_refs)
5470 return NULL;
5472 id_refs = tmp_id_refs;
5474 for (i = 0; i < refs_size; i++) {
5475 struct ref **tmp;
5477 if (strcmp(id, refs[i].id))
5478 continue;
5480 tmp = realloc_items(ref_list, &ref_list_alloc,
5481 ref_list_size + 1, sizeof(*ref_list));
5482 if (!tmp) {
5483 if (ref_list)
5484 free(ref_list);
5485 return NULL;
5488 ref_list = tmp;
5489 if (ref_list_size > 0)
5490 ref_list[ref_list_size - 1]->next = 1;
5491 ref_list[ref_list_size] = &refs[i];
5493 /* XXX: The properties of the commit chains ensures that we can
5494 * safely modify the shared ref. The repo references will
5495 * always be similar for the same id. */
5496 ref_list[ref_list_size]->next = 0;
5497 ref_list_size++;
5500 if (ref_list)
5501 id_refs[id_refs_size++] = ref_list;
5503 return ref_list;
5506 static int
5507 read_ref(char *id, size_t idlen, char *name, size_t namelen)
5509 struct ref *ref;
5510 bool tag = FALSE;
5511 bool ltag = FALSE;
5512 bool remote = FALSE;
5513 bool tracked = FALSE;
5514 bool check_replace = FALSE;
5515 bool head = FALSE;
5517 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
5518 if (!strcmp(name + namelen - 3, "^{}")) {
5519 namelen -= 3;
5520 name[namelen] = 0;
5521 if (refs_size > 0 && refs[refs_size - 1].ltag == TRUE)
5522 check_replace = TRUE;
5523 } else {
5524 ltag = TRUE;
5527 tag = TRUE;
5528 namelen -= STRING_SIZE("refs/tags/");
5529 name += STRING_SIZE("refs/tags/");
5531 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
5532 remote = TRUE;
5533 namelen -= STRING_SIZE("refs/remotes/");
5534 name += STRING_SIZE("refs/remotes/");
5535 tracked = !strcmp(opt_remote, name);
5537 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5538 namelen -= STRING_SIZE("refs/heads/");
5539 name += STRING_SIZE("refs/heads/");
5540 head = !strncmp(opt_head, name, namelen);
5542 } else if (!strcmp(name, "HEAD")) {
5543 opt_no_head = FALSE;
5544 return OK;
5547 if (check_replace && !strcmp(name, refs[refs_size - 1].name)) {
5548 /* it's an annotated tag, replace the previous sha1 with the
5549 * resolved commit id; relies on the fact git-ls-remote lists
5550 * the commit id of an annotated tag right beofre the commit id
5551 * it points to. */
5552 refs[refs_size - 1].ltag = ltag;
5553 string_copy_rev(refs[refs_size - 1].id, id);
5555 return OK;
5557 refs = realloc_items(refs, &refs_alloc, refs_size + 1, sizeof(*refs));
5558 if (!refs)
5559 return ERR;
5561 ref = &refs[refs_size++];
5562 ref->name = malloc(namelen + 1);
5563 if (!ref->name)
5564 return ERR;
5566 strncpy(ref->name, name, namelen);
5567 ref->name[namelen] = 0;
5568 ref->head = head;
5569 ref->tag = tag;
5570 ref->ltag = ltag;
5571 ref->remote = remote;
5572 ref->tracked = tracked;
5573 string_copy_rev(ref->id, id);
5575 return OK;
5578 static int
5579 load_refs(void)
5581 const char *cmd_env = getenv("TIG_LS_REMOTE");
5582 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
5584 return read_properties(popen(cmd, "r"), "\t", read_ref);
5587 static int
5588 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
5590 if (!strcmp(name, "i18n.commitencoding"))
5591 string_ncopy(opt_encoding, value, valuelen);
5593 if (!strcmp(name, "core.editor"))
5594 string_ncopy(opt_editor, value, valuelen);
5596 /* branch.<head>.remote */
5597 if (*opt_head &&
5598 !strncmp(name, "branch.", 7) &&
5599 !strncmp(name + 7, opt_head, strlen(opt_head)) &&
5600 !strcmp(name + 7 + strlen(opt_head), ".remote"))
5601 string_ncopy(opt_remote, value, valuelen);
5603 if (*opt_head && *opt_remote &&
5604 !strncmp(name, "branch.", 7) &&
5605 !strncmp(name + 7, opt_head, strlen(opt_head)) &&
5606 !strcmp(name + 7 + strlen(opt_head), ".merge")) {
5607 size_t from = strlen(opt_remote);
5609 if (!strncmp(value, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5610 value += STRING_SIZE("refs/heads/");
5611 valuelen -= STRING_SIZE("refs/heads/");
5614 if (!string_format_from(opt_remote, &from, "/%s", value))
5615 opt_remote[0] = 0;
5618 return OK;
5621 static int
5622 load_git_config(void)
5624 return read_properties(popen(GIT_CONFIG " --list", "r"),
5625 "=", read_repo_config_option);
5628 static int
5629 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
5631 if (!opt_git_dir[0]) {
5632 string_ncopy(opt_git_dir, name, namelen);
5634 } else if (opt_is_inside_work_tree == -1) {
5635 /* This can be 3 different values depending on the
5636 * version of git being used. If git-rev-parse does not
5637 * understand --is-inside-work-tree it will simply echo
5638 * the option else either "true" or "false" is printed.
5639 * Default to true for the unknown case. */
5640 opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
5642 } else if (opt_cdup[0] == ' ') {
5643 string_ncopy(opt_cdup, name, namelen);
5644 } else {
5645 if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5646 namelen -= STRING_SIZE("refs/heads/");
5647 name += STRING_SIZE("refs/heads/");
5648 string_ncopy(opt_head, name, namelen);
5652 return OK;
5655 static int
5656 load_repo_info(void)
5658 int result;
5659 FILE *pipe = popen("(git rev-parse --git-dir --is-inside-work-tree "
5660 " --show-cdup; git symbolic-ref HEAD) 2>/dev/null", "r");
5662 /* XXX: The line outputted by "--show-cdup" can be empty so
5663 * initialize it to something invalid to make it possible to
5664 * detect whether it has been set or not. */
5665 opt_cdup[0] = ' ';
5667 result = read_properties(pipe, "=", read_repo_info);
5668 if (opt_cdup[0] == ' ')
5669 opt_cdup[0] = 0;
5671 return result;
5674 static int
5675 read_properties(FILE *pipe, const char *separators,
5676 int (*read_property)(char *, size_t, char *, size_t))
5678 char buffer[BUFSIZ];
5679 char *name;
5680 int state = OK;
5682 if (!pipe)
5683 return ERR;
5685 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
5686 char *value;
5687 size_t namelen;
5688 size_t valuelen;
5690 name = chomp_string(name);
5691 namelen = strcspn(name, separators);
5693 if (name[namelen]) {
5694 name[namelen] = 0;
5695 value = chomp_string(name + namelen + 1);
5696 valuelen = strlen(value);
5698 } else {
5699 value = "";
5700 valuelen = 0;
5703 state = read_property(name, namelen, value, valuelen);
5706 if (state != ERR && ferror(pipe))
5707 state = ERR;
5709 pclose(pipe);
5711 return state;
5716 * Main
5719 static void __NORETURN
5720 quit(int sig)
5722 /* XXX: Restore tty modes and let the OS cleanup the rest! */
5723 if (cursed)
5724 endwin();
5725 exit(0);
5728 static void __NORETURN
5729 die(const char *err, ...)
5731 va_list args;
5733 endwin();
5735 va_start(args, err);
5736 fputs("tig: ", stderr);
5737 vfprintf(stderr, err, args);
5738 fputs("\n", stderr);
5739 va_end(args);
5741 exit(1);
5744 static void
5745 warn(const char *msg, ...)
5747 va_list args;
5749 va_start(args, msg);
5750 fputs("tig warning: ", stderr);
5751 vfprintf(stderr, msg, args);
5752 fputs("\n", stderr);
5753 va_end(args);
5757 main(int argc, char *argv[])
5759 struct view *view;
5760 enum request request;
5761 size_t i;
5763 signal(SIGINT, quit);
5765 if (setlocale(LC_ALL, "")) {
5766 char *codeset = nl_langinfo(CODESET);
5768 string_ncopy(opt_codeset, codeset, strlen(codeset));
5771 if (load_repo_info() == ERR)
5772 die("Failed to load repo info.");
5774 if (load_options() == ERR)
5775 die("Failed to load user config.");
5777 if (load_git_config() == ERR)
5778 die("Failed to load repo config.");
5780 if (!parse_options(argc, argv))
5781 return 0;
5783 /* Require a git repository unless when running in pager mode. */
5784 if (!opt_git_dir[0] && opt_request != REQ_VIEW_PAGER)
5785 die("Not a git repository");
5787 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
5788 opt_utf8 = FALSE;
5790 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
5791 opt_iconv = iconv_open(opt_codeset, opt_encoding);
5792 if (opt_iconv == ICONV_NONE)
5793 die("Failed to initialize character set conversion");
5796 if (*opt_git_dir && load_refs() == ERR)
5797 die("Failed to load refs.");
5799 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
5800 view->cmd_env = getenv(view->cmd_env);
5802 request = opt_request;
5804 init_display();
5806 while (view_driver(display[current_view], request)) {
5807 int key;
5808 int i;
5810 foreach_view (view, i)
5811 update_view(view);
5813 /* Refresh, accept single keystroke of input */
5814 key = wgetch(status_win);
5816 /* wgetch() with nodelay() enabled returns ERR when there's no
5817 * input. */
5818 if (key == ERR) {
5819 request = REQ_NONE;
5820 continue;
5823 request = get_keybinding(display[current_view]->keymap, key);
5825 /* Some low-level request handling. This keeps access to
5826 * status_win restricted. */
5827 switch (request) {
5828 case REQ_PROMPT:
5830 char *cmd = read_prompt(":");
5832 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
5833 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
5834 opt_request = REQ_VIEW_DIFF;
5835 } else {
5836 opt_request = REQ_VIEW_PAGER;
5838 break;
5841 request = REQ_NONE;
5842 break;
5844 case REQ_SEARCH:
5845 case REQ_SEARCH_BACK:
5847 const char *prompt = request == REQ_SEARCH
5848 ? "/" : "?";
5849 char *search = read_prompt(prompt);
5851 if (search)
5852 string_ncopy(opt_search, search, strlen(search));
5853 else
5854 request = REQ_NONE;
5855 break;
5857 case REQ_SCREEN_RESIZE:
5859 int height, width;
5861 getmaxyx(stdscr, height, width);
5863 /* Resize the status view and let the view driver take
5864 * care of resizing the displayed views. */
5865 wresize(status_win, 1, width);
5866 mvwin(status_win, height - 1, 0);
5867 wrefresh(status_win);
5868 break;
5870 default:
5871 break;
5875 quit(0);
5877 return 0;