71ddbb5ae41775c84ac9abaa2c59db5884504208
1 /* Copyright (c) 2006-2007 Jonas Fonseca <fonseca@diku.dk>
2 *
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.
7 *
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.
12 */
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 #include <curses.h>
47 #if __GNUC__ >= 3
48 #define __NORETURN __attribute__((__noreturn__))
49 #else
50 #define __NORETURN
51 #endif
53 static void __NORETURN die(const char *err, ...);
54 static void warn(const char *msg, ...);
55 static void report(const char *msg, ...);
56 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, size_t, char *, size_t));
57 static void set_nonblocking_input(bool loading);
58 static size_t utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed);
60 #define ABS(x) ((x) >= 0 ? (x) : -(x))
61 #define MIN(x, y) ((x) < (y) ? (x) : (y))
63 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
64 #define STRING_SIZE(x) (sizeof(x) - 1)
66 #define SIZEOF_STR 1024 /* Default string size. */
67 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
68 #define SIZEOF_REV 41 /* Holds a SHA-1 and an ending NUL */
70 /* Revision graph */
72 #define REVGRAPH_INIT 'I'
73 #define REVGRAPH_MERGE 'M'
74 #define REVGRAPH_BRANCH '+'
75 #define REVGRAPH_COMMIT '*'
76 #define REVGRAPH_BOUND '^'
77 #define REVGRAPH_LINE '|'
79 #define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
81 /* This color name can be used to refer to the default term colors. */
82 #define COLOR_DEFAULT (-1)
84 #define ICONV_NONE ((iconv_t) -1)
85 #ifndef ICONV_CONST
86 #define ICONV_CONST /* nothing */
87 #endif
89 /* The format and size of the date column in the main view. */
90 #define DATE_FORMAT "%Y-%m-%d %H:%M"
91 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
93 #define AUTHOR_COLS 20
95 /* The default interval between line numbers. */
96 #define NUMBER_INTERVAL 1
98 #define TABSIZE 8
100 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
102 #ifndef GIT_CONFIG
103 #define GIT_CONFIG "git config"
104 #endif
106 #define TIG_LS_REMOTE \
107 "git ls-remote $(git rev-parse --git-dir) 2>/dev/null"
109 #define TIG_DIFF_CMD \
110 "git show --no-color --root --patch-with-stat --find-copies-harder -C %s 2>/dev/null"
112 #define TIG_LOG_CMD \
113 "git log --no-color --cc --stat -n100 %s 2>/dev/null"
115 #define TIG_MAIN_CMD \
116 "git log --no-color --topo-order --boundary --pretty=raw %s 2>/dev/null"
118 #define TIG_TREE_CMD \
119 "git ls-tree %s %s"
121 #define TIG_BLOB_CMD \
122 "git cat-file blob %s"
124 /* XXX: Needs to be defined to the empty string. */
125 #define TIG_HELP_CMD ""
126 #define TIG_PAGER_CMD ""
127 #define TIG_STATUS_CMD ""
128 #define TIG_STAGE_CMD ""
130 /* Some ascii-shorthands fitted into the ncurses namespace. */
131 #define KEY_TAB '\t'
132 #define KEY_RETURN '\r'
133 #define KEY_ESC 27
136 struct ref {
137 char *name; /* Ref name; tag or head names are shortened. */
138 char id[SIZEOF_REV]; /* Commit SHA1 ID */
139 unsigned int tag:1; /* Is it a tag? */
140 unsigned int remote:1; /* Is it a remote ref? */
141 unsigned int next:1; /* For ref lists: are there more refs? */
142 };
144 static struct ref **get_refs(char *id);
146 struct int_map {
147 const char *name;
148 int namelen;
149 int value;
150 };
152 static int
153 set_from_int_map(struct int_map *map, size_t map_size,
154 int *value, const char *name, int namelen)
155 {
157 int i;
159 for (i = 0; i < map_size; i++)
160 if (namelen == map[i].namelen &&
161 !strncasecmp(name, map[i].name, namelen)) {
162 *value = map[i].value;
163 return OK;
164 }
166 return ERR;
167 }
170 /*
171 * String helpers
172 */
174 static inline void
175 string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
176 {
177 if (srclen > dstlen - 1)
178 srclen = dstlen - 1;
180 strncpy(dst, src, srclen);
181 dst[srclen] = 0;
182 }
184 /* Shorthands for safely copying into a fixed buffer. */
186 #define string_copy(dst, src) \
187 string_ncopy_do(dst, sizeof(dst), src, sizeof(src))
189 #define string_ncopy(dst, src, srclen) \
190 string_ncopy_do(dst, sizeof(dst), src, srclen)
192 #define string_copy_rev(dst, src) \
193 string_ncopy_do(dst, SIZEOF_REV, src, SIZEOF_REV - 1)
195 #define string_add(dst, from, src) \
196 string_ncopy_do(dst + (from), sizeof(dst) - (from), src, sizeof(src))
198 static char *
199 chomp_string(char *name)
200 {
201 int namelen;
203 while (isspace(*name))
204 name++;
206 namelen = strlen(name) - 1;
207 while (namelen > 0 && isspace(name[namelen]))
208 name[namelen--] = 0;
210 return name;
211 }
213 static bool
214 string_nformat(char *buf, size_t bufsize, size_t *bufpos, const char *fmt, ...)
215 {
216 va_list args;
217 size_t pos = bufpos ? *bufpos : 0;
219 va_start(args, fmt);
220 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
221 va_end(args);
223 if (bufpos)
224 *bufpos = pos;
226 return pos >= bufsize ? FALSE : TRUE;
227 }
229 #define string_format(buf, fmt, args...) \
230 string_nformat(buf, sizeof(buf), NULL, fmt, args)
232 #define string_format_from(buf, from, fmt, args...) \
233 string_nformat(buf, sizeof(buf), from, fmt, args)
235 static int
236 string_enum_compare(const char *str1, const char *str2, int len)
237 {
238 size_t i;
240 #define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
242 /* Diff-Header == DIFF_HEADER */
243 for (i = 0; i < len; i++) {
244 if (toupper(str1[i]) == toupper(str2[i]))
245 continue;
247 if (string_enum_sep(str1[i]) &&
248 string_enum_sep(str2[i]))
249 continue;
251 return str1[i] - str2[i];
252 }
254 return 0;
255 }
257 /* Shell quoting
258 *
259 * NOTE: The following is a slightly modified copy of the git project's shell
260 * quoting routines found in the quote.c file.
261 *
262 * Help to copy the thing properly quoted for the shell safety. any single
263 * quote is replaced with '\'', any exclamation point is replaced with '\!',
264 * and the whole thing is enclosed in a
265 *
266 * E.g.
267 * original sq_quote result
268 * name ==> name ==> 'name'
269 * a b ==> a b ==> 'a b'
270 * a'b ==> a'\''b ==> 'a'\''b'
271 * a!b ==> a'\!'b ==> 'a'\!'b'
272 */
274 static size_t
275 sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
276 {
277 char c;
279 #define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0)
281 BUFPUT('\'');
282 while ((c = *src++)) {
283 if (c == '\'' || c == '!') {
284 BUFPUT('\'');
285 BUFPUT('\\');
286 BUFPUT(c);
287 BUFPUT('\'');
288 } else {
289 BUFPUT(c);
290 }
291 }
292 BUFPUT('\'');
294 if (bufsize < SIZEOF_STR)
295 buf[bufsize] = 0;
297 return bufsize;
298 }
301 /*
302 * User requests
303 */
305 #define REQ_INFO \
306 /* XXX: Keep the view request first and in sync with views[]. */ \
307 REQ_GROUP("View switching") \
308 REQ_(VIEW_MAIN, "Show main view"), \
309 REQ_(VIEW_DIFF, "Show diff view"), \
310 REQ_(VIEW_LOG, "Show log view"), \
311 REQ_(VIEW_TREE, "Show tree view"), \
312 REQ_(VIEW_BLOB, "Show blob view"), \
313 REQ_(VIEW_HELP, "Show help page"), \
314 REQ_(VIEW_PAGER, "Show pager view"), \
315 REQ_(VIEW_STATUS, "Show status view"), \
316 REQ_(VIEW_STAGE, "Show stage view"), \
317 \
318 REQ_GROUP("View manipulation") \
319 REQ_(ENTER, "Enter current line and scroll"), \
320 REQ_(NEXT, "Move to next"), \
321 REQ_(PREVIOUS, "Move to previous"), \
322 REQ_(VIEW_NEXT, "Move focus to next view"), \
323 REQ_(REFRESH, "Reload and refresh"), \
324 REQ_(VIEW_CLOSE, "Close the current view"), \
325 REQ_(QUIT, "Close all views and quit"), \
326 \
327 REQ_GROUP("Cursor navigation") \
328 REQ_(MOVE_UP, "Move cursor one line up"), \
329 REQ_(MOVE_DOWN, "Move cursor one line down"), \
330 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
331 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
332 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
333 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
334 \
335 REQ_GROUP("Scrolling") \
336 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
337 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
338 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
339 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
340 \
341 REQ_GROUP("Searching") \
342 REQ_(SEARCH, "Search the view"), \
343 REQ_(SEARCH_BACK, "Search backwards in the view"), \
344 REQ_(FIND_NEXT, "Find next search match"), \
345 REQ_(FIND_PREV, "Find previous search match"), \
346 \
347 REQ_GROUP("Misc") \
348 REQ_(PROMPT, "Bring up the prompt"), \
349 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
350 REQ_(SCREEN_RESIZE, "Resize the screen"), \
351 REQ_(SHOW_VERSION, "Show version information"), \
352 REQ_(STOP_LOADING, "Stop all loading views"), \
353 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
354 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"), \
355 REQ_(STATUS_UPDATE, "Update file status"), \
356 REQ_(STATUS_MERGE, "Merge file using external tool"), \
357 REQ_(TREE_PARENT, "Switch to parent directory in tree view"), \
358 REQ_(EDIT, "Open in editor"), \
359 REQ_(NONE, "Do nothing")
362 /* User action requests. */
363 enum request {
364 #define REQ_GROUP(help)
365 #define REQ_(req, help) REQ_##req
367 /* Offset all requests to avoid conflicts with ncurses getch values. */
368 REQ_OFFSET = KEY_MAX + 1,
369 REQ_INFO
371 #undef REQ_GROUP
372 #undef REQ_
373 };
375 struct request_info {
376 enum request request;
377 char *name;
378 int namelen;
379 char *help;
380 };
382 static struct request_info req_info[] = {
383 #define REQ_GROUP(help) { 0, NULL, 0, (help) },
384 #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
385 REQ_INFO
386 #undef REQ_GROUP
387 #undef REQ_
388 };
390 static enum request
391 get_request(const char *name)
392 {
393 int namelen = strlen(name);
394 int i;
396 for (i = 0; i < ARRAY_SIZE(req_info); i++)
397 if (req_info[i].namelen == namelen &&
398 !string_enum_compare(req_info[i].name, name, namelen))
399 return req_info[i].request;
401 return REQ_NONE;
402 }
405 /*
406 * Options
407 */
409 static const char usage[] =
410 "tig " TIG_VERSION " (" __DATE__ ")\n"
411 "\n"
412 "Usage: tig [options] [revs] [--] [paths]\n"
413 " or: tig show [options] [revs] [--] [paths]\n"
414 " or: tig status\n"
415 " or: tig < [git command output]\n"
416 "\n"
417 "Options:\n"
418 " -v, --version Show version and exit\n"
419 " -h, --help Show help message and exit\n";
421 /* Option and state variables. */
422 static bool opt_line_number = FALSE;
423 static bool opt_rev_graph = FALSE;
424 static int opt_num_interval = NUMBER_INTERVAL;
425 static int opt_tab_size = TABSIZE;
426 static enum request opt_request = REQ_VIEW_MAIN;
427 static char opt_cmd[SIZEOF_STR] = "";
428 static char opt_path[SIZEOF_STR] = "";
429 static FILE *opt_pipe = NULL;
430 static char opt_encoding[20] = "UTF-8";
431 static bool opt_utf8 = TRUE;
432 static char opt_codeset[20] = "UTF-8";
433 static iconv_t opt_iconv = ICONV_NONE;
434 static char opt_search[SIZEOF_STR] = "";
435 static char opt_cdup[SIZEOF_STR] = "";
436 static char opt_git_dir[SIZEOF_STR] = "";
437 static char opt_is_inside_work_tree = -1; /* set to TRUE or FALSE */
438 static char opt_editor[SIZEOF_STR] = "";
440 enum option_type {
441 OPT_NONE,
442 OPT_INT,
443 };
445 static bool
446 check_option(char *opt, char short_name, char *name, enum option_type type, ...)
447 {
448 va_list args;
449 char *value = "";
450 int *number;
452 if (opt[0] != '-')
453 return FALSE;
455 if (opt[1] == '-') {
456 int namelen = strlen(name);
458 opt += 2;
460 if (strncmp(opt, name, namelen))
461 return FALSE;
463 if (opt[namelen] == '=')
464 value = opt + namelen + 1;
466 } else {
467 if (!short_name || opt[1] != short_name)
468 return FALSE;
469 value = opt + 2;
470 }
472 va_start(args, type);
473 if (type == OPT_INT) {
474 number = va_arg(args, int *);
475 if (isdigit(*value))
476 *number = atoi(value);
477 }
478 va_end(args);
480 return TRUE;
481 }
483 /* Returns the index of log or diff command or -1 to exit. */
484 static bool
485 parse_options(int argc, char *argv[])
486 {
487 char *altargv[1024];
488 int altargc = 0;
489 char *subcommand = NULL;
490 int i;
492 for (i = 1; i < argc; i++) {
493 char *opt = argv[i];
495 if (!strcmp(opt, "log") ||
496 !strcmp(opt, "diff")) {
497 subcommand = opt;
498 opt_request = opt[0] == 'l'
499 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
500 warn("`tig %s' has been deprecated", opt);
501 break;
502 }
504 if (!strcmp(opt, "show")) {
505 subcommand = opt;
506 opt_request = REQ_VIEW_DIFF;
507 break;
508 }
510 if (!strcmp(opt, "status")) {
511 subcommand = opt;
512 opt_request = REQ_VIEW_STATUS;
513 break;
514 }
516 if (opt[0] && opt[0] != '-')
517 break;
519 if (!strcmp(opt, "--")) {
520 i++;
521 break;
522 }
524 if (check_option(opt, 'v', "version", OPT_NONE)) {
525 printf("tig version %s\n", TIG_VERSION);
526 return FALSE;
527 }
529 if (check_option(opt, 'h', "help", OPT_NONE)) {
530 printf(usage);
531 return FALSE;
532 }
534 if (!strcmp(opt, "-S")) {
535 warn("`%s' has been deprecated; use `tig status' instead", opt);
536 opt_request = REQ_VIEW_STATUS;
537 continue;
538 }
540 if (!strcmp(opt, "-l")) {
541 opt_request = REQ_VIEW_LOG;
542 } else if (!strcmp(opt, "-d")) {
543 opt_request = REQ_VIEW_DIFF;
544 } else if (check_option(opt, 'n', "line-number", OPT_INT, &opt_num_interval)) {
545 opt_line_number = TRUE;
546 } else if (check_option(opt, 'b', "tab-size", OPT_INT, &opt_tab_size)) {
547 opt_tab_size = MIN(opt_tab_size, TABSIZE);
548 } else {
549 if (altargc >= ARRAY_SIZE(altargv))
550 die("maximum number of arguments exceeded");
551 altargv[altargc++] = opt;
552 continue;
553 }
555 warn("`%s' has been deprecated", opt);
556 }
558 /* Check that no 'alt' arguments occured before a subcommand. */
559 if (subcommand && i < argc && altargc > 0)
560 die("unknown arguments before `%s'", argv[i]);
562 if (!isatty(STDIN_FILENO)) {
563 opt_request = REQ_VIEW_PAGER;
564 opt_pipe = stdin;
566 } else if (opt_request == REQ_VIEW_STATUS) {
567 if (argc - i > 1)
568 warn("ignoring arguments after `%s'", argv[i]);
570 } else if (i < argc || altargc > 0) {
571 int alti = 0;
572 size_t buf_size;
574 if (opt_request == REQ_VIEW_MAIN)
575 /* XXX: This is vulnerable to the user overriding
576 * options required for the main view parser. */
577 string_copy(opt_cmd, "git log --no-color --pretty=raw --boundary");
578 else
579 string_copy(opt_cmd, "git");
580 buf_size = strlen(opt_cmd);
582 while (buf_size < sizeof(opt_cmd) && alti < altargc) {
583 opt_cmd[buf_size++] = ' ';
584 buf_size = sq_quote(opt_cmd, buf_size, altargv[alti++]);
585 }
587 while (buf_size < sizeof(opt_cmd) && i < argc) {
588 opt_cmd[buf_size++] = ' ';
589 buf_size = sq_quote(opt_cmd, buf_size, argv[i++]);
590 }
592 if (buf_size >= sizeof(opt_cmd))
593 die("command too long");
595 opt_cmd[buf_size] = 0;
596 }
598 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
599 opt_utf8 = FALSE;
601 return TRUE;
602 }
605 /*
606 * Line-oriented content detection.
607 */
609 #define LINE_INFO \
610 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
611 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
612 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
613 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
614 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
615 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
616 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
617 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
618 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
619 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
620 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
621 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
622 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
623 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
624 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
625 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
626 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
627 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
628 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
629 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
630 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
631 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
632 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
633 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
634 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
635 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
636 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
637 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
638 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
639 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
640 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
641 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
642 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
643 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
644 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
645 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
646 LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
647 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
648 LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
649 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
650 LINE(MAIN_REVGRAPH,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
651 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
652 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
653 LINE(STAT_SECTION, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
654 LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
655 LINE(STAT_STAGED, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
656 LINE(STAT_UNSTAGED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
657 LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0)
659 enum line_type {
660 #define LINE(type, line, fg, bg, attr) \
661 LINE_##type
662 LINE_INFO
663 #undef LINE
664 };
666 struct line_info {
667 const char *name; /* Option name. */
668 int namelen; /* Size of option name. */
669 const char *line; /* The start of line to match. */
670 int linelen; /* Size of string to match. */
671 int fg, bg, attr; /* Color and text attributes for the lines. */
672 };
674 static struct line_info line_info[] = {
675 #define LINE(type, line, fg, bg, attr) \
676 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
677 LINE_INFO
678 #undef LINE
679 };
681 static enum line_type
682 get_line_type(char *line)
683 {
684 int linelen = strlen(line);
685 enum line_type type;
687 for (type = 0; type < ARRAY_SIZE(line_info); type++)
688 /* Case insensitive search matches Signed-off-by lines better. */
689 if (linelen >= line_info[type].linelen &&
690 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
691 return type;
693 return LINE_DEFAULT;
694 }
696 static inline int
697 get_line_attr(enum line_type type)
698 {
699 assert(type < ARRAY_SIZE(line_info));
700 return COLOR_PAIR(type) | line_info[type].attr;
701 }
703 static struct line_info *
704 get_line_info(char *name, int namelen)
705 {
706 enum line_type type;
708 for (type = 0; type < ARRAY_SIZE(line_info); type++)
709 if (namelen == line_info[type].namelen &&
710 !string_enum_compare(line_info[type].name, name, namelen))
711 return &line_info[type];
713 return NULL;
714 }
716 static void
717 init_colors(void)
718 {
719 int default_bg = line_info[LINE_DEFAULT].bg;
720 int default_fg = line_info[LINE_DEFAULT].fg;
721 enum line_type type;
723 start_color();
725 if (assume_default_colors(default_fg, default_bg) == ERR) {
726 default_bg = COLOR_BLACK;
727 default_fg = COLOR_WHITE;
728 }
730 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
731 struct line_info *info = &line_info[type];
732 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
733 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
735 init_pair(type, fg, bg);
736 }
737 }
739 struct line {
740 enum line_type type;
742 /* State flags */
743 unsigned int selected:1;
745 void *data; /* User data */
746 };
749 /*
750 * Keys
751 */
753 struct keybinding {
754 int alias;
755 enum request request;
756 struct keybinding *next;
757 };
759 static struct keybinding default_keybindings[] = {
760 /* View switching */
761 { 'm', REQ_VIEW_MAIN },
762 { 'd', REQ_VIEW_DIFF },
763 { 'l', REQ_VIEW_LOG },
764 { 't', REQ_VIEW_TREE },
765 { 'f', REQ_VIEW_BLOB },
766 { 'p', REQ_VIEW_PAGER },
767 { 'h', REQ_VIEW_HELP },
768 { 'S', REQ_VIEW_STATUS },
769 { 'c', REQ_VIEW_STAGE },
771 /* View manipulation */
772 { 'q', REQ_VIEW_CLOSE },
773 { KEY_TAB, REQ_VIEW_NEXT },
774 { KEY_RETURN, REQ_ENTER },
775 { KEY_UP, REQ_PREVIOUS },
776 { KEY_DOWN, REQ_NEXT },
777 { 'R', REQ_REFRESH },
779 /* Cursor navigation */
780 { 'k', REQ_MOVE_UP },
781 { 'j', REQ_MOVE_DOWN },
782 { KEY_HOME, REQ_MOVE_FIRST_LINE },
783 { KEY_END, REQ_MOVE_LAST_LINE },
784 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
785 { ' ', REQ_MOVE_PAGE_DOWN },
786 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
787 { 'b', REQ_MOVE_PAGE_UP },
788 { '-', REQ_MOVE_PAGE_UP },
790 /* Scrolling */
791 { KEY_IC, REQ_SCROLL_LINE_UP },
792 { KEY_DC, REQ_SCROLL_LINE_DOWN },
793 { 'w', REQ_SCROLL_PAGE_UP },
794 { 's', REQ_SCROLL_PAGE_DOWN },
796 /* Searching */
797 { '/', REQ_SEARCH },
798 { '?', REQ_SEARCH_BACK },
799 { 'n', REQ_FIND_NEXT },
800 { 'N', REQ_FIND_PREV },
802 /* Misc */
803 { 'Q', REQ_QUIT },
804 { 'z', REQ_STOP_LOADING },
805 { 'v', REQ_SHOW_VERSION },
806 { 'r', REQ_SCREEN_REDRAW },
807 { '.', REQ_TOGGLE_LINENO },
808 { 'g', REQ_TOGGLE_REV_GRAPH },
809 { ':', REQ_PROMPT },
810 { 'u', REQ_STATUS_UPDATE },
811 { 'M', REQ_STATUS_MERGE },
812 { ',', REQ_TREE_PARENT },
813 { 'e', REQ_EDIT },
815 /* Using the ncurses SIGWINCH handler. */
816 { KEY_RESIZE, REQ_SCREEN_RESIZE },
817 };
819 #define KEYMAP_INFO \
820 KEYMAP_(GENERIC), \
821 KEYMAP_(MAIN), \
822 KEYMAP_(DIFF), \
823 KEYMAP_(LOG), \
824 KEYMAP_(TREE), \
825 KEYMAP_(BLOB), \
826 KEYMAP_(PAGER), \
827 KEYMAP_(HELP), \
828 KEYMAP_(STATUS), \
829 KEYMAP_(STAGE)
831 enum keymap {
832 #define KEYMAP_(name) KEYMAP_##name
833 KEYMAP_INFO
834 #undef KEYMAP_
835 };
837 static struct int_map keymap_table[] = {
838 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
839 KEYMAP_INFO
840 #undef KEYMAP_
841 };
843 #define set_keymap(map, name) \
844 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
846 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
848 static void
849 add_keybinding(enum keymap keymap, enum request request, int key)
850 {
851 struct keybinding *keybinding;
853 keybinding = calloc(1, sizeof(*keybinding));
854 if (!keybinding)
855 die("Failed to allocate keybinding");
857 keybinding->alias = key;
858 keybinding->request = request;
859 keybinding->next = keybindings[keymap];
860 keybindings[keymap] = keybinding;
861 }
863 /* Looks for a key binding first in the given map, then in the generic map, and
864 * lastly in the default keybindings. */
865 static enum request
866 get_keybinding(enum keymap keymap, int key)
867 {
868 struct keybinding *kbd;
869 int i;
871 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
872 if (kbd->alias == key)
873 return kbd->request;
875 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
876 if (kbd->alias == key)
877 return kbd->request;
879 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
880 if (default_keybindings[i].alias == key)
881 return default_keybindings[i].request;
883 return (enum request) key;
884 }
887 struct key {
888 char *name;
889 int value;
890 };
892 static struct key key_table[] = {
893 { "Enter", KEY_RETURN },
894 { "Space", ' ' },
895 { "Backspace", KEY_BACKSPACE },
896 { "Tab", KEY_TAB },
897 { "Escape", KEY_ESC },
898 { "Left", KEY_LEFT },
899 { "Right", KEY_RIGHT },
900 { "Up", KEY_UP },
901 { "Down", KEY_DOWN },
902 { "Insert", KEY_IC },
903 { "Delete", KEY_DC },
904 { "Hash", '#' },
905 { "Home", KEY_HOME },
906 { "End", KEY_END },
907 { "PageUp", KEY_PPAGE },
908 { "PageDown", KEY_NPAGE },
909 { "F1", KEY_F(1) },
910 { "F2", KEY_F(2) },
911 { "F3", KEY_F(3) },
912 { "F4", KEY_F(4) },
913 { "F5", KEY_F(5) },
914 { "F6", KEY_F(6) },
915 { "F7", KEY_F(7) },
916 { "F8", KEY_F(8) },
917 { "F9", KEY_F(9) },
918 { "F10", KEY_F(10) },
919 { "F11", KEY_F(11) },
920 { "F12", KEY_F(12) },
921 };
923 static int
924 get_key_value(const char *name)
925 {
926 int i;
928 for (i = 0; i < ARRAY_SIZE(key_table); i++)
929 if (!strcasecmp(key_table[i].name, name))
930 return key_table[i].value;
932 if (strlen(name) == 1 && isprint(*name))
933 return (int) *name;
935 return ERR;
936 }
938 static char *
939 get_key_name(int key_value)
940 {
941 static char key_char[] = "'X'";
942 char *seq = NULL;
943 int key;
945 for (key = 0; key < ARRAY_SIZE(key_table); key++)
946 if (key_table[key].value == key_value)
947 seq = key_table[key].name;
949 if (seq == NULL &&
950 key_value < 127 &&
951 isprint(key_value)) {
952 key_char[1] = (char) key_value;
953 seq = key_char;
954 }
956 return seq ? seq : "'?'";
957 }
959 static char *
960 get_key(enum request request)
961 {
962 static char buf[BUFSIZ];
963 size_t pos = 0;
964 char *sep = "";
965 int i;
967 buf[pos] = 0;
969 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
970 struct keybinding *keybinding = &default_keybindings[i];
972 if (keybinding->request != request)
973 continue;
975 if (!string_format_from(buf, &pos, "%s%s", sep,
976 get_key_name(keybinding->alias)))
977 return "Too many keybindings!";
978 sep = ", ";
979 }
981 return buf;
982 }
984 struct run_request {
985 enum keymap keymap;
986 int key;
987 char cmd[SIZEOF_STR];
988 };
990 static struct run_request *run_request;
991 static size_t run_requests;
993 static enum request
994 add_run_request(enum keymap keymap, int key, int argc, char **argv)
995 {
996 struct run_request *tmp;
997 struct run_request req = { keymap, key };
998 size_t bufpos;
1000 for (bufpos = 0; argc > 0; argc--, argv++)
1001 if (!string_format_from(req.cmd, &bufpos, "%s ", *argv))
1002 return REQ_NONE;
1004 req.cmd[bufpos - 1] = 0;
1006 tmp = realloc(run_request, (run_requests + 1) * sizeof(*run_request));
1007 if (!tmp)
1008 return REQ_NONE;
1010 run_request = tmp;
1011 run_request[run_requests++] = req;
1013 return REQ_NONE + run_requests;
1014 }
1016 static struct run_request *
1017 get_run_request(enum request request)
1018 {
1019 if (request <= REQ_NONE)
1020 return NULL;
1021 return &run_request[request - REQ_NONE - 1];
1022 }
1024 static void
1025 add_builtin_run_requests(void)
1026 {
1027 struct {
1028 enum keymap keymap;
1029 int key;
1030 char *argv[1];
1031 } reqs[] = {
1032 { KEYMAP_MAIN, 'C', { "git cherry-pick %(commit)" } },
1033 { KEYMAP_GENERIC, 'G', { "git gc" } },
1034 };
1035 int i;
1037 for (i = 0; i < ARRAY_SIZE(reqs); i++) {
1038 enum request req;
1040 req = add_run_request(reqs[i].keymap, reqs[i].key, 1, reqs[i].argv);
1041 if (req != REQ_NONE)
1042 add_keybinding(reqs[i].keymap, req, reqs[i].key);
1043 }
1044 }
1046 /*
1047 * User config file handling.
1048 */
1050 static struct int_map color_map[] = {
1051 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
1052 COLOR_MAP(DEFAULT),
1053 COLOR_MAP(BLACK),
1054 COLOR_MAP(BLUE),
1055 COLOR_MAP(CYAN),
1056 COLOR_MAP(GREEN),
1057 COLOR_MAP(MAGENTA),
1058 COLOR_MAP(RED),
1059 COLOR_MAP(WHITE),
1060 COLOR_MAP(YELLOW),
1061 };
1063 #define set_color(color, name) \
1064 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
1066 static struct int_map attr_map[] = {
1067 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
1068 ATTR_MAP(NORMAL),
1069 ATTR_MAP(BLINK),
1070 ATTR_MAP(BOLD),
1071 ATTR_MAP(DIM),
1072 ATTR_MAP(REVERSE),
1073 ATTR_MAP(STANDOUT),
1074 ATTR_MAP(UNDERLINE),
1075 };
1077 #define set_attribute(attr, name) \
1078 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
1080 static int config_lineno;
1081 static bool config_errors;
1082 static char *config_msg;
1084 /* Wants: object fgcolor bgcolor [attr] */
1085 static int
1086 option_color_command(int argc, char *argv[])
1087 {
1088 struct line_info *info;
1090 if (argc != 3 && argc != 4) {
1091 config_msg = "Wrong number of arguments given to color command";
1092 return ERR;
1093 }
1095 info = get_line_info(argv[0], strlen(argv[0]));
1096 if (!info) {
1097 config_msg = "Unknown color name";
1098 return ERR;
1099 }
1101 if (set_color(&info->fg, argv[1]) == ERR ||
1102 set_color(&info->bg, argv[2]) == ERR) {
1103 config_msg = "Unknown color";
1104 return ERR;
1105 }
1107 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
1108 config_msg = "Unknown attribute";
1109 return ERR;
1110 }
1112 return OK;
1113 }
1115 /* Wants: name = value */
1116 static int
1117 option_set_command(int argc, char *argv[])
1118 {
1119 if (argc != 3) {
1120 config_msg = "Wrong number of arguments given to set command";
1121 return ERR;
1122 }
1124 if (strcmp(argv[1], "=")) {
1125 config_msg = "No value assigned";
1126 return ERR;
1127 }
1129 if (!strcmp(argv[0], "show-rev-graph")) {
1130 opt_rev_graph = (!strcmp(argv[2], "1") ||
1131 !strcmp(argv[2], "true") ||
1132 !strcmp(argv[2], "yes"));
1133 return OK;
1134 }
1136 if (!strcmp(argv[0], "line-number-interval")) {
1137 opt_num_interval = atoi(argv[2]);
1138 return OK;
1139 }
1141 if (!strcmp(argv[0], "tab-size")) {
1142 opt_tab_size = atoi(argv[2]);
1143 return OK;
1144 }
1146 if (!strcmp(argv[0], "commit-encoding")) {
1147 char *arg = argv[2];
1148 int delimiter = *arg;
1149 int i;
1151 switch (delimiter) {
1152 case '"':
1153 case '\'':
1154 for (arg++, i = 0; arg[i]; i++)
1155 if (arg[i] == delimiter) {
1156 arg[i] = 0;
1157 break;
1158 }
1159 default:
1160 string_ncopy(opt_encoding, arg, strlen(arg));
1161 return OK;
1162 }
1163 }
1165 config_msg = "Unknown variable name";
1166 return ERR;
1167 }
1169 /* Wants: mode request key */
1170 static int
1171 option_bind_command(int argc, char *argv[])
1172 {
1173 enum request request;
1174 int keymap;
1175 int key;
1177 if (argc < 3) {
1178 config_msg = "Wrong number of arguments given to bind command";
1179 return ERR;
1180 }
1182 if (set_keymap(&keymap, argv[0]) == ERR) {
1183 config_msg = "Unknown key map";
1184 return ERR;
1185 }
1187 key = get_key_value(argv[1]);
1188 if (key == ERR) {
1189 config_msg = "Unknown key";
1190 return ERR;
1191 }
1193 request = get_request(argv[2]);
1194 if (request == REQ_NONE) {
1195 const char *obsolete[] = { "cherry-pick" };
1196 size_t namelen = strlen(argv[2]);
1197 int i;
1199 for (i = 0; i < ARRAY_SIZE(obsolete); i++) {
1200 if (namelen == strlen(obsolete[i]) &&
1201 !string_enum_compare(obsolete[i], argv[2], namelen)) {
1202 config_msg = "Obsolete request name";
1203 return ERR;
1204 }
1205 }
1206 }
1207 if (request == REQ_NONE && *argv[2]++ == '!')
1208 request = add_run_request(keymap, key, argc - 2, argv + 2);
1209 if (request == REQ_NONE) {
1210 config_msg = "Unknown request name";
1211 return ERR;
1212 }
1214 add_keybinding(keymap, request, key);
1216 return OK;
1217 }
1219 static int
1220 set_option(char *opt, char *value)
1221 {
1222 char *argv[16];
1223 int valuelen;
1224 int argc = 0;
1226 /* Tokenize */
1227 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1228 argv[argc++] = value;
1229 value += valuelen;
1231 /* Nothing more to tokenize or last available token. */
1232 if (!*value || argc >= ARRAY_SIZE(argv))
1233 break;
1235 *value++ = 0;
1236 while (isspace(*value))
1237 value++;
1238 }
1240 if (!strcmp(opt, "color"))
1241 return option_color_command(argc, argv);
1243 if (!strcmp(opt, "set"))
1244 return option_set_command(argc, argv);
1246 if (!strcmp(opt, "bind"))
1247 return option_bind_command(argc, argv);
1249 config_msg = "Unknown option command";
1250 return ERR;
1251 }
1253 static int
1254 read_option(char *opt, size_t optlen, char *value, size_t valuelen)
1255 {
1256 int status = OK;
1258 config_lineno++;
1259 config_msg = "Internal error";
1261 /* Check for comment markers, since read_properties() will
1262 * only ensure opt and value are split at first " \t". */
1263 optlen = strcspn(opt, "#");
1264 if (optlen == 0)
1265 return OK;
1267 if (opt[optlen] != 0) {
1268 config_msg = "No option value";
1269 status = ERR;
1271 } else {
1272 /* Look for comment endings in the value. */
1273 size_t len = strcspn(value, "#");
1275 if (len < valuelen) {
1276 valuelen = len;
1277 value[valuelen] = 0;
1278 }
1280 status = set_option(opt, value);
1281 }
1283 if (status == ERR) {
1284 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1285 config_lineno, (int) optlen, opt, config_msg);
1286 config_errors = TRUE;
1287 }
1289 /* Always keep going if errors are encountered. */
1290 return OK;
1291 }
1293 static void
1294 load_option_file(const char *path)
1295 {
1296 FILE *file;
1298 /* It's ok that the file doesn't exist. */
1299 file = fopen(path, "r");
1300 if (!file)
1301 return;
1303 config_lineno = 0;
1304 config_errors = FALSE;
1306 if (read_properties(file, " \t", read_option) == ERR ||
1307 config_errors == TRUE)
1308 fprintf(stderr, "Errors while loading %s.\n", path);
1309 }
1311 static int
1312 load_options(void)
1313 {
1314 char *home = getenv("HOME");
1315 char *tigrc_user = getenv("TIGRC_USER");
1316 char *tigrc_system = getenv("TIGRC_SYSTEM");
1317 char buf[SIZEOF_STR];
1319 add_builtin_run_requests();
1321 if (!tigrc_system) {
1322 if (!string_format(buf, "%s/tigrc", SYSCONFDIR))
1323 return ERR;
1324 tigrc_system = buf;
1325 }
1326 load_option_file(tigrc_system);
1328 if (!tigrc_user) {
1329 if (!home || !string_format(buf, "%s/.tigrc", home))
1330 return ERR;
1331 tigrc_user = buf;
1332 }
1333 load_option_file(tigrc_user);
1335 return OK;
1336 }
1339 /*
1340 * The viewer
1341 */
1343 struct view;
1344 struct view_ops;
1346 /* The display array of active views and the index of the current view. */
1347 static struct view *display[2];
1348 static unsigned int current_view;
1350 /* Reading from the prompt? */
1351 static bool input_mode = FALSE;
1353 #define foreach_displayed_view(view, i) \
1354 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1356 #define displayed_views() (display[1] != NULL ? 2 : 1)
1358 /* Current head and commit ID */
1359 static char ref_blob[SIZEOF_REF] = "";
1360 static char ref_commit[SIZEOF_REF] = "HEAD";
1361 static char ref_head[SIZEOF_REF] = "HEAD";
1363 struct view {
1364 const char *name; /* View name */
1365 const char *cmd_fmt; /* Default command line format */
1366 const char *cmd_env; /* Command line set via environment */
1367 const char *id; /* Points to either of ref_{head,commit,blob} */
1369 struct view_ops *ops; /* View operations */
1371 enum keymap keymap; /* What keymap does this view have */
1373 char cmd[SIZEOF_STR]; /* Command buffer */
1374 char ref[SIZEOF_REF]; /* Hovered commit reference */
1375 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1377 int height, width; /* The width and height of the main window */
1378 WINDOW *win; /* The main window */
1379 WINDOW *title; /* The title window living below the main window */
1381 /* Navigation */
1382 unsigned long offset; /* Offset of the window top */
1383 unsigned long lineno; /* Current line number */
1385 /* Searching */
1386 char grep[SIZEOF_STR]; /* Search string */
1387 regex_t *regex; /* Pre-compiled regex */
1389 /* If non-NULL, points to the view that opened this view. If this view
1390 * is closed tig will switch back to the parent view. */
1391 struct view *parent;
1393 /* Buffering */
1394 unsigned long lines; /* Total number of lines */
1395 struct line *line; /* Line index */
1396 unsigned long line_size;/* Total number of allocated lines */
1397 unsigned int digits; /* Number of digits in the lines member. */
1399 /* Loading */
1400 FILE *pipe;
1401 time_t start_time;
1402 };
1404 struct view_ops {
1405 /* What type of content being displayed. Used in the title bar. */
1406 const char *type;
1407 /* Open and reads in all view content. */
1408 bool (*open)(struct view *view);
1409 /* Read one line; updates view->line. */
1410 bool (*read)(struct view *view, char *data);
1411 /* Draw one line; @lineno must be < view->height. */
1412 bool (*draw)(struct view *view, struct line *line, unsigned int lineno, bool selected);
1413 /* Depending on view handle a special requests. */
1414 enum request (*request)(struct view *view, enum request request, struct line *line);
1415 /* Search for regex in a line. */
1416 bool (*grep)(struct view *view, struct line *line);
1417 /* Select line */
1418 void (*select)(struct view *view, struct line *line);
1419 };
1421 static struct view_ops pager_ops;
1422 static struct view_ops main_ops;
1423 static struct view_ops tree_ops;
1424 static struct view_ops blob_ops;
1425 static struct view_ops help_ops;
1426 static struct view_ops status_ops;
1427 static struct view_ops stage_ops;
1429 #define VIEW_STR(name, cmd, env, ref, ops, map) \
1430 { name, cmd, #env, ref, ops, map}
1432 #define VIEW_(id, name, ops, ref) \
1433 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id)
1436 static struct view views[] = {
1437 VIEW_(MAIN, "main", &main_ops, ref_head),
1438 VIEW_(DIFF, "diff", &pager_ops, ref_commit),
1439 VIEW_(LOG, "log", &pager_ops, ref_head),
1440 VIEW_(TREE, "tree", &tree_ops, ref_commit),
1441 VIEW_(BLOB, "blob", &blob_ops, ref_blob),
1442 VIEW_(HELP, "help", &help_ops, ""),
1443 VIEW_(PAGER, "pager", &pager_ops, "stdin"),
1444 VIEW_(STATUS, "status", &status_ops, ""),
1445 VIEW_(STAGE, "stage", &stage_ops, ""),
1446 };
1448 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1450 #define foreach_view(view, i) \
1451 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1453 #define view_is_displayed(view) \
1454 (view == display[0] || view == display[1])
1456 static bool
1457 draw_view_line(struct view *view, unsigned int lineno)
1458 {
1459 struct line *line;
1460 bool selected = (view->offset + lineno == view->lineno);
1461 bool draw_ok;
1463 assert(view_is_displayed(view));
1465 if (view->offset + lineno >= view->lines)
1466 return FALSE;
1468 line = &view->line[view->offset + lineno];
1470 if (selected) {
1471 line->selected = TRUE;
1472 view->ops->select(view, line);
1473 } else if (line->selected) {
1474 line->selected = FALSE;
1475 wmove(view->win, lineno, 0);
1476 wclrtoeol(view->win);
1477 }
1479 scrollok(view->win, FALSE);
1480 draw_ok = view->ops->draw(view, line, lineno, selected);
1481 scrollok(view->win, TRUE);
1483 return draw_ok;
1484 }
1486 static void
1487 redraw_view_from(struct view *view, int lineno)
1488 {
1489 assert(0 <= lineno && lineno < view->height);
1491 for (; lineno < view->height; lineno++) {
1492 if (!draw_view_line(view, lineno))
1493 break;
1494 }
1496 redrawwin(view->win);
1497 if (input_mode)
1498 wnoutrefresh(view->win);
1499 else
1500 wrefresh(view->win);
1501 }
1503 static void
1504 redraw_view(struct view *view)
1505 {
1506 wclear(view->win);
1507 redraw_view_from(view, 0);
1508 }
1511 static void
1512 update_view_title(struct view *view)
1513 {
1514 char buf[SIZEOF_STR];
1515 char state[SIZEOF_STR];
1516 size_t bufpos = 0, statelen = 0;
1518 assert(view_is_displayed(view));
1520 if (view != VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) {
1521 unsigned int view_lines = view->offset + view->height;
1522 unsigned int lines = view->lines
1523 ? MIN(view_lines, view->lines) * 100 / view->lines
1524 : 0;
1526 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1527 view->ops->type,
1528 view->lineno + 1,
1529 view->lines,
1530 lines);
1532 if (view->pipe) {
1533 time_t secs = time(NULL) - view->start_time;
1535 /* Three git seconds are a long time ... */
1536 if (secs > 2)
1537 string_format_from(state, &statelen, " %lds", secs);
1538 }
1539 }
1541 string_format_from(buf, &bufpos, "[%s]", view->name);
1542 if (*view->ref && bufpos < view->width) {
1543 size_t refsize = strlen(view->ref);
1544 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1546 if (minsize < view->width)
1547 refsize = view->width - minsize + 7;
1548 string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
1549 }
1551 if (statelen && bufpos < view->width) {
1552 string_format_from(buf, &bufpos, " %s", state);
1553 }
1555 if (view == display[current_view])
1556 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1557 else
1558 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1560 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1561 wclrtoeol(view->title);
1562 wmove(view->title, 0, view->width - 1);
1564 if (input_mode)
1565 wnoutrefresh(view->title);
1566 else
1567 wrefresh(view->title);
1568 }
1570 static void
1571 resize_display(void)
1572 {
1573 int offset, i;
1574 struct view *base = display[0];
1575 struct view *view = display[1] ? display[1] : display[0];
1577 /* Setup window dimensions */
1579 getmaxyx(stdscr, base->height, base->width);
1581 /* Make room for the status window. */
1582 base->height -= 1;
1584 if (view != base) {
1585 /* Horizontal split. */
1586 view->width = base->width;
1587 view->height = SCALE_SPLIT_VIEW(base->height);
1588 base->height -= view->height;
1590 /* Make room for the title bar. */
1591 view->height -= 1;
1592 }
1594 /* Make room for the title bar. */
1595 base->height -= 1;
1597 offset = 0;
1599 foreach_displayed_view (view, i) {
1600 if (!view->win) {
1601 view->win = newwin(view->height, 0, offset, 0);
1602 if (!view->win)
1603 die("Failed to create %s view", view->name);
1605 scrollok(view->win, TRUE);
1607 view->title = newwin(1, 0, offset + view->height, 0);
1608 if (!view->title)
1609 die("Failed to create title window");
1611 } else {
1612 wresize(view->win, view->height, view->width);
1613 mvwin(view->win, offset, 0);
1614 mvwin(view->title, offset + view->height, 0);
1615 }
1617 offset += view->height + 1;
1618 }
1619 }
1621 static void
1622 redraw_display(void)
1623 {
1624 struct view *view;
1625 int i;
1627 foreach_displayed_view (view, i) {
1628 redraw_view(view);
1629 update_view_title(view);
1630 }
1631 }
1633 static void
1634 update_display_cursor(struct view *view)
1635 {
1636 /* Move the cursor to the right-most column of the cursor line.
1637 *
1638 * XXX: This could turn out to be a bit expensive, but it ensures that
1639 * the cursor does not jump around. */
1640 if (view->lines) {
1641 wmove(view->win, view->lineno - view->offset, view->width - 1);
1642 wrefresh(view->win);
1643 }
1644 }
1646 /*
1647 * Navigation
1648 */
1650 /* Scrolling backend */
1651 static void
1652 do_scroll_view(struct view *view, int lines)
1653 {
1654 bool redraw_current_line = FALSE;
1656 /* The rendering expects the new offset. */
1657 view->offset += lines;
1659 assert(0 <= view->offset && view->offset < view->lines);
1660 assert(lines);
1662 /* Move current line into the view. */
1663 if (view->lineno < view->offset) {
1664 view->lineno = view->offset;
1665 redraw_current_line = TRUE;
1666 } else if (view->lineno >= view->offset + view->height) {
1667 view->lineno = view->offset + view->height - 1;
1668 redraw_current_line = TRUE;
1669 }
1671 assert(view->offset <= view->lineno && view->lineno < view->lines);
1673 /* Redraw the whole screen if scrolling is pointless. */
1674 if (view->height < ABS(lines)) {
1675 redraw_view(view);
1677 } else {
1678 int line = lines > 0 ? view->height - lines : 0;
1679 int end = line + ABS(lines);
1681 wscrl(view->win, lines);
1683 for (; line < end; line++) {
1684 if (!draw_view_line(view, line))
1685 break;
1686 }
1688 if (redraw_current_line)
1689 draw_view_line(view, view->lineno - view->offset);
1690 }
1692 redrawwin(view->win);
1693 wrefresh(view->win);
1694 report("");
1695 }
1697 /* Scroll frontend */
1698 static void
1699 scroll_view(struct view *view, enum request request)
1700 {
1701 int lines = 1;
1703 assert(view_is_displayed(view));
1705 switch (request) {
1706 case REQ_SCROLL_PAGE_DOWN:
1707 lines = view->height;
1708 case REQ_SCROLL_LINE_DOWN:
1709 if (view->offset + lines > view->lines)
1710 lines = view->lines - view->offset;
1712 if (lines == 0 || view->offset + view->height >= view->lines) {
1713 report("Cannot scroll beyond the last line");
1714 return;
1715 }
1716 break;
1718 case REQ_SCROLL_PAGE_UP:
1719 lines = view->height;
1720 case REQ_SCROLL_LINE_UP:
1721 if (lines > view->offset)
1722 lines = view->offset;
1724 if (lines == 0) {
1725 report("Cannot scroll beyond the first line");
1726 return;
1727 }
1729 lines = -lines;
1730 break;
1732 default:
1733 die("request %d not handled in switch", request);
1734 }
1736 do_scroll_view(view, lines);
1737 }
1739 /* Cursor moving */
1740 static void
1741 move_view(struct view *view, enum request request)
1742 {
1743 int scroll_steps = 0;
1744 int steps;
1746 switch (request) {
1747 case REQ_MOVE_FIRST_LINE:
1748 steps = -view->lineno;
1749 break;
1751 case REQ_MOVE_LAST_LINE:
1752 steps = view->lines - view->lineno - 1;
1753 break;
1755 case REQ_MOVE_PAGE_UP:
1756 steps = view->height > view->lineno
1757 ? -view->lineno : -view->height;
1758 break;
1760 case REQ_MOVE_PAGE_DOWN:
1761 steps = view->lineno + view->height >= view->lines
1762 ? view->lines - view->lineno - 1 : view->height;
1763 break;
1765 case REQ_MOVE_UP:
1766 steps = -1;
1767 break;
1769 case REQ_MOVE_DOWN:
1770 steps = 1;
1771 break;
1773 default:
1774 die("request %d not handled in switch", request);
1775 }
1777 if (steps <= 0 && view->lineno == 0) {
1778 report("Cannot move beyond the first line");
1779 return;
1781 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1782 report("Cannot move beyond the last line");
1783 return;
1784 }
1786 /* Move the current line */
1787 view->lineno += steps;
1788 assert(0 <= view->lineno && view->lineno < view->lines);
1790 /* Check whether the view needs to be scrolled */
1791 if (view->lineno < view->offset ||
1792 view->lineno >= view->offset + view->height) {
1793 scroll_steps = steps;
1794 if (steps < 0 && -steps > view->offset) {
1795 scroll_steps = -view->offset;
1797 } else if (steps > 0) {
1798 if (view->lineno == view->lines - 1 &&
1799 view->lines > view->height) {
1800 scroll_steps = view->lines - view->offset - 1;
1801 if (scroll_steps >= view->height)
1802 scroll_steps -= view->height - 1;
1803 }
1804 }
1805 }
1807 if (!view_is_displayed(view)) {
1808 view->offset += scroll_steps;
1809 assert(0 <= view->offset && view->offset < view->lines);
1810 view->ops->select(view, &view->line[view->lineno]);
1811 return;
1812 }
1814 /* Repaint the old "current" line if we be scrolling */
1815 if (ABS(steps) < view->height)
1816 draw_view_line(view, view->lineno - steps - view->offset);
1818 if (scroll_steps) {
1819 do_scroll_view(view, scroll_steps);
1820 return;
1821 }
1823 /* Draw the current line */
1824 draw_view_line(view, view->lineno - view->offset);
1826 redrawwin(view->win);
1827 wrefresh(view->win);
1828 report("");
1829 }
1832 /*
1833 * Searching
1834 */
1836 static void search_view(struct view *view, enum request request);
1838 static bool
1839 find_next_line(struct view *view, unsigned long lineno, struct line *line)
1840 {
1841 assert(view_is_displayed(view));
1843 if (!view->ops->grep(view, line))
1844 return FALSE;
1846 if (lineno - view->offset >= view->height) {
1847 view->offset = lineno;
1848 view->lineno = lineno;
1849 redraw_view(view);
1851 } else {
1852 unsigned long old_lineno = view->lineno - view->offset;
1854 view->lineno = lineno;
1855 draw_view_line(view, old_lineno);
1857 draw_view_line(view, view->lineno - view->offset);
1858 redrawwin(view->win);
1859 wrefresh(view->win);
1860 }
1862 report("Line %ld matches '%s'", lineno + 1, view->grep);
1863 return TRUE;
1864 }
1866 static void
1867 find_next(struct view *view, enum request request)
1868 {
1869 unsigned long lineno = view->lineno;
1870 int direction;
1872 if (!*view->grep) {
1873 if (!*opt_search)
1874 report("No previous search");
1875 else
1876 search_view(view, request);
1877 return;
1878 }
1880 switch (request) {
1881 case REQ_SEARCH:
1882 case REQ_FIND_NEXT:
1883 direction = 1;
1884 break;
1886 case REQ_SEARCH_BACK:
1887 case REQ_FIND_PREV:
1888 direction = -1;
1889 break;
1891 default:
1892 return;
1893 }
1895 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
1896 lineno += direction;
1898 /* Note, lineno is unsigned long so will wrap around in which case it
1899 * will become bigger than view->lines. */
1900 for (; lineno < view->lines; lineno += direction) {
1901 struct line *line = &view->line[lineno];
1903 if (find_next_line(view, lineno, line))
1904 return;
1905 }
1907 report("No match found for '%s'", view->grep);
1908 }
1910 static void
1911 search_view(struct view *view, enum request request)
1912 {
1913 int regex_err;
1915 if (view->regex) {
1916 regfree(view->regex);
1917 *view->grep = 0;
1918 } else {
1919 view->regex = calloc(1, sizeof(*view->regex));
1920 if (!view->regex)
1921 return;
1922 }
1924 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
1925 if (regex_err != 0) {
1926 char buf[SIZEOF_STR] = "unknown error";
1928 regerror(regex_err, view->regex, buf, sizeof(buf));
1929 report("Search failed: %s", buf);
1930 return;
1931 }
1933 string_copy(view->grep, opt_search);
1935 find_next(view, request);
1936 }
1938 /*
1939 * Incremental updating
1940 */
1942 static void
1943 end_update(struct view *view)
1944 {
1945 if (!view->pipe)
1946 return;
1947 set_nonblocking_input(FALSE);
1948 if (view->pipe == stdin)
1949 fclose(view->pipe);
1950 else
1951 pclose(view->pipe);
1952 view->pipe = NULL;
1953 }
1955 static bool
1956 begin_update(struct view *view)
1957 {
1958 if (view->pipe)
1959 end_update(view);
1961 if (opt_cmd[0]) {
1962 string_copy(view->cmd, opt_cmd);
1963 opt_cmd[0] = 0;
1964 /* When running random commands, initially show the
1965 * command in the title. However, it maybe later be
1966 * overwritten if a commit line is selected. */
1967 if (view == VIEW(REQ_VIEW_PAGER))
1968 string_copy(view->ref, view->cmd);
1969 else
1970 view->ref[0] = 0;
1972 } else if (view == VIEW(REQ_VIEW_TREE)) {
1973 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1974 char path[SIZEOF_STR];
1976 if (strcmp(view->vid, view->id))
1977 opt_path[0] = path[0] = 0;
1978 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
1979 return FALSE;
1981 if (!string_format(view->cmd, format, view->id, path))
1982 return FALSE;
1984 } else {
1985 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1986 const char *id = view->id;
1988 if (!string_format(view->cmd, format, id, id, id, id, id))
1989 return FALSE;
1991 /* Put the current ref_* value to the view title ref
1992 * member. This is needed by the blob view. Most other
1993 * views sets it automatically after loading because the
1994 * first line is a commit line. */
1995 string_copy_rev(view->ref, view->id);
1996 }
1998 /* Special case for the pager view. */
1999 if (opt_pipe) {
2000 view->pipe = opt_pipe;
2001 opt_pipe = NULL;
2002 } else {
2003 view->pipe = popen(view->cmd, "r");
2004 }
2006 if (!view->pipe)
2007 return FALSE;
2009 set_nonblocking_input(TRUE);
2011 view->offset = 0;
2012 view->lines = 0;
2013 view->lineno = 0;
2014 string_copy_rev(view->vid, view->id);
2016 if (view->line) {
2017 int i;
2019 for (i = 0; i < view->lines; i++)
2020 if (view->line[i].data)
2021 free(view->line[i].data);
2023 free(view->line);
2024 view->line = NULL;
2025 }
2027 view->start_time = time(NULL);
2029 return TRUE;
2030 }
2032 static struct line *
2033 realloc_lines(struct view *view, size_t line_size)
2034 {
2035 struct line *tmp = realloc(view->line, sizeof(*view->line) * line_size);
2037 if (!tmp)
2038 return NULL;
2040 view->line = tmp;
2041 view->line_size = line_size;
2042 return view->line;
2043 }
2045 static bool
2046 update_view(struct view *view)
2047 {
2048 char in_buffer[BUFSIZ];
2049 char out_buffer[BUFSIZ * 2];
2050 char *line;
2051 /* The number of lines to read. If too low it will cause too much
2052 * redrawing (and possible flickering), if too high responsiveness
2053 * will suffer. */
2054 unsigned long lines = view->height;
2055 int redraw_from = -1;
2057 if (!view->pipe)
2058 return TRUE;
2060 /* Only redraw if lines are visible. */
2061 if (view->offset + view->height >= view->lines)
2062 redraw_from = view->lines - view->offset;
2064 /* FIXME: This is probably not perfect for backgrounded views. */
2065 if (!realloc_lines(view, view->lines + lines))
2066 goto alloc_error;
2068 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
2069 size_t linelen = strlen(line);
2071 if (linelen)
2072 line[linelen - 1] = 0;
2074 if (opt_iconv != ICONV_NONE) {
2075 ICONV_CONST char *inbuf = line;
2076 size_t inlen = linelen;
2078 char *outbuf = out_buffer;
2079 size_t outlen = sizeof(out_buffer);
2081 size_t ret;
2083 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
2084 if (ret != (size_t) -1) {
2085 line = out_buffer;
2086 linelen = strlen(out_buffer);
2087 }
2088 }
2090 if (!view->ops->read(view, line))
2091 goto alloc_error;
2093 if (lines-- == 1)
2094 break;
2095 }
2097 {
2098 int digits;
2100 lines = view->lines;
2101 for (digits = 0; lines; digits++)
2102 lines /= 10;
2104 /* Keep the displayed view in sync with line number scaling. */
2105 if (digits != view->digits) {
2106 view->digits = digits;
2107 redraw_from = 0;
2108 }
2109 }
2111 if (!view_is_displayed(view))
2112 goto check_pipe;
2114 if (view == VIEW(REQ_VIEW_TREE)) {
2115 /* Clear the view and redraw everything since the tree sorting
2116 * might have rearranged things. */
2117 redraw_view(view);
2119 } else if (redraw_from >= 0) {
2120 /* If this is an incremental update, redraw the previous line
2121 * since for commits some members could have changed when
2122 * loading the main view. */
2123 if (redraw_from > 0)
2124 redraw_from--;
2126 /* Since revision graph visualization requires knowledge
2127 * about the parent commit, it causes a further one-off
2128 * needed to be redrawn for incremental updates. */
2129 if (redraw_from > 0 && opt_rev_graph)
2130 redraw_from--;
2132 /* Incrementally draw avoids flickering. */
2133 redraw_view_from(view, redraw_from);
2134 }
2136 /* Update the title _after_ the redraw so that if the redraw picks up a
2137 * commit reference in view->ref it'll be available here. */
2138 update_view_title(view);
2140 check_pipe:
2141 if (ferror(view->pipe)) {
2142 report("Failed to read: %s", strerror(errno));
2143 goto end;
2145 } else if (feof(view->pipe)) {
2146 report("");
2147 goto end;
2148 }
2150 return TRUE;
2152 alloc_error:
2153 report("Allocation failure");
2155 end:
2156 view->ops->read(view, NULL);
2157 end_update(view);
2158 return FALSE;
2159 }
2161 static struct line *
2162 add_line_data(struct view *view, void *data, enum line_type type)
2163 {
2164 struct line *line = &view->line[view->lines++];
2166 memset(line, 0, sizeof(*line));
2167 line->type = type;
2168 line->data = data;
2170 return line;
2171 }
2173 static struct line *
2174 add_line_text(struct view *view, char *data, enum line_type type)
2175 {
2176 if (data)
2177 data = strdup(data);
2179 return data ? add_line_data(view, data, type) : NULL;
2180 }
2183 /*
2184 * View opening
2185 */
2187 enum open_flags {
2188 OPEN_DEFAULT = 0, /* Use default view switching. */
2189 OPEN_SPLIT = 1, /* Split current view. */
2190 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2191 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2192 };
2194 static void
2195 open_view(struct view *prev, enum request request, enum open_flags flags)
2196 {
2197 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2198 bool split = !!(flags & OPEN_SPLIT);
2199 bool reload = !!(flags & OPEN_RELOAD);
2200 struct view *view = VIEW(request);
2201 int nviews = displayed_views();
2202 struct view *base_view = display[0];
2204 if (view == prev && nviews == 1 && !reload) {
2205 report("Already in %s view", view->name);
2206 return;
2207 }
2209 if (view->ops->open) {
2210 if (!view->ops->open(view)) {
2211 report("Failed to load %s view", view->name);
2212 return;
2213 }
2215 } else if ((reload || strcmp(view->vid, view->id)) &&
2216 !begin_update(view)) {
2217 report("Failed to load %s view", view->name);
2218 return;
2219 }
2221 if (split) {
2222 display[1] = view;
2223 if (!backgrounded)
2224 current_view = 1;
2225 } else {
2226 /* Maximize the current view. */
2227 memset(display, 0, sizeof(display));
2228 current_view = 0;
2229 display[current_view] = view;
2230 }
2232 /* Resize the view when switching between split- and full-screen,
2233 * or when switching between two different full-screen views. */
2234 if (nviews != displayed_views() ||
2235 (nviews == 1 && base_view != display[0]))
2236 resize_display();
2238 if (split && prev->lineno - prev->offset >= prev->height) {
2239 /* Take the title line into account. */
2240 int lines = prev->lineno - prev->offset - prev->height + 1;
2242 /* Scroll the view that was split if the current line is
2243 * outside the new limited view. */
2244 do_scroll_view(prev, lines);
2245 }
2247 if (prev && view != prev) {
2248 if (split && !backgrounded) {
2249 /* "Blur" the previous view. */
2250 update_view_title(prev);
2251 }
2253 view->parent = prev;
2254 }
2256 if (view->pipe && view->lines == 0) {
2257 /* Clear the old view and let the incremental updating refill
2258 * the screen. */
2259 wclear(view->win);
2260 report("");
2261 } else {
2262 redraw_view(view);
2263 report("");
2264 }
2266 /* If the view is backgrounded the above calls to report()
2267 * won't redraw the view title. */
2268 if (backgrounded)
2269 update_view_title(view);
2270 }
2272 static void
2273 open_external_viewer(const char *cmd)
2274 {
2275 def_prog_mode(); /* save current tty modes */
2276 endwin(); /* restore original tty modes */
2277 system(cmd);
2278 fprintf(stderr, "Press Enter to continue");
2279 getc(stdin);
2280 reset_prog_mode();
2281 redraw_display();
2282 }
2284 static void
2285 open_mergetool(const char *file)
2286 {
2287 char cmd[SIZEOF_STR];
2288 char file_sq[SIZEOF_STR];
2290 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2291 string_format(cmd, "git mergetool %s", file_sq)) {
2292 open_external_viewer(cmd);
2293 }
2294 }
2296 static void
2297 open_editor(bool from_root, const char *file)
2298 {
2299 char cmd[SIZEOF_STR];
2300 char file_sq[SIZEOF_STR];
2301 char *editor;
2302 char *prefix = from_root ? opt_cdup : "";
2304 editor = getenv("GIT_EDITOR");
2305 if (!editor && *opt_editor)
2306 editor = opt_editor;
2307 if (!editor)
2308 editor = getenv("VISUAL");
2309 if (!editor)
2310 editor = getenv("EDITOR");
2311 if (!editor)
2312 editor = "vi";
2314 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2315 string_format(cmd, "%s %s%s", editor, prefix, file_sq)) {
2316 open_external_viewer(cmd);
2317 }
2318 }
2320 static void
2321 open_run_request(enum request request)
2322 {
2323 struct run_request *req = get_run_request(request);
2324 char buf[SIZEOF_STR * 2];
2325 size_t bufpos;
2326 char *cmd;
2328 if (!req) {
2329 report("Unknown run request");
2330 return;
2331 }
2333 bufpos = 0;
2334 cmd = req->cmd;
2336 while (cmd) {
2337 char *next = strstr(cmd, "%(");
2338 int len = next - cmd;
2339 char *value;
2341 if (!next) {
2342 len = strlen(cmd);
2343 value = "";
2345 } else if (!strncmp(next, "%(head)", 7)) {
2346 value = ref_head;
2348 } else if (!strncmp(next, "%(commit)", 9)) {
2349 value = ref_commit;
2351 } else if (!strncmp(next, "%(blob)", 7)) {
2352 value = ref_blob;
2354 } else {
2355 report("Unknown replacement in run request: `%s`", req->cmd);
2356 return;
2357 }
2359 if (!string_format_from(buf, &bufpos, "%.*s%s", len, cmd, value))
2360 return;
2362 if (next)
2363 next = strchr(next, ')') + 1;
2364 cmd = next;
2365 }
2367 open_external_viewer(buf);
2368 }
2370 /*
2371 * User request switch noodle
2372 */
2374 static int
2375 view_driver(struct view *view, enum request request)
2376 {
2377 int i;
2379 if (request == REQ_NONE) {
2380 doupdate();
2381 return TRUE;
2382 }
2384 if (request > REQ_NONE) {
2385 open_run_request(request);
2386 return TRUE;
2387 }
2389 if (view && view->lines) {
2390 request = view->ops->request(view, request, &view->line[view->lineno]);
2391 if (request == REQ_NONE)
2392 return TRUE;
2393 }
2395 switch (request) {
2396 case REQ_MOVE_UP:
2397 case REQ_MOVE_DOWN:
2398 case REQ_MOVE_PAGE_UP:
2399 case REQ_MOVE_PAGE_DOWN:
2400 case REQ_MOVE_FIRST_LINE:
2401 case REQ_MOVE_LAST_LINE:
2402 move_view(view, request);
2403 break;
2405 case REQ_SCROLL_LINE_DOWN:
2406 case REQ_SCROLL_LINE_UP:
2407 case REQ_SCROLL_PAGE_DOWN:
2408 case REQ_SCROLL_PAGE_UP:
2409 scroll_view(view, request);
2410 break;
2412 case REQ_VIEW_BLOB:
2413 if (!ref_blob[0]) {
2414 report("No file chosen, press %s to open tree view",
2415 get_key(REQ_VIEW_TREE));
2416 break;
2417 }
2418 open_view(view, request, OPEN_DEFAULT);
2419 break;
2421 case REQ_VIEW_PAGER:
2422 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2423 report("No pager content, press %s to run command from prompt",
2424 get_key(REQ_PROMPT));
2425 break;
2426 }
2427 open_view(view, request, OPEN_DEFAULT);
2428 break;
2430 case REQ_VIEW_STAGE:
2431 if (!VIEW(REQ_VIEW_STAGE)->lines) {
2432 report("No stage content, press %s to open the status view and choose file",
2433 get_key(REQ_VIEW_STATUS));
2434 break;
2435 }
2436 open_view(view, request, OPEN_DEFAULT);
2437 break;
2439 case REQ_VIEW_STATUS:
2440 if (opt_is_inside_work_tree == FALSE) {
2441 report("The status view requires a working tree");
2442 break;
2443 }
2444 open_view(view, request, OPEN_DEFAULT);
2445 break;
2447 case REQ_VIEW_MAIN:
2448 case REQ_VIEW_DIFF:
2449 case REQ_VIEW_LOG:
2450 case REQ_VIEW_TREE:
2451 case REQ_VIEW_HELP:
2452 open_view(view, request, OPEN_DEFAULT);
2453 break;
2455 case REQ_NEXT:
2456 case REQ_PREVIOUS:
2457 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2459 if ((view == VIEW(REQ_VIEW_DIFF) &&
2460 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2461 (view == VIEW(REQ_VIEW_STAGE) &&
2462 view->parent == VIEW(REQ_VIEW_STATUS)) ||
2463 (view == VIEW(REQ_VIEW_BLOB) &&
2464 view->parent == VIEW(REQ_VIEW_TREE))) {
2465 int line;
2467 view = view->parent;
2468 line = view->lineno;
2469 move_view(view, request);
2470 if (view_is_displayed(view))
2471 update_view_title(view);
2472 if (line != view->lineno)
2473 view->ops->request(view, REQ_ENTER,
2474 &view->line[view->lineno]);
2476 } else {
2477 move_view(view, request);
2478 }
2479 break;
2481 case REQ_VIEW_NEXT:
2482 {
2483 int nviews = displayed_views();
2484 int next_view = (current_view + 1) % nviews;
2486 if (next_view == current_view) {
2487 report("Only one view is displayed");
2488 break;
2489 }
2491 current_view = next_view;
2492 /* Blur out the title of the previous view. */
2493 update_view_title(view);
2494 report("");
2495 break;
2496 }
2497 case REQ_REFRESH:
2498 report("Refreshing is not yet supported for the %s view", view->name);
2499 break;
2501 case REQ_TOGGLE_LINENO:
2502 opt_line_number = !opt_line_number;
2503 redraw_display();
2504 break;
2506 case REQ_TOGGLE_REV_GRAPH:
2507 opt_rev_graph = !opt_rev_graph;
2508 redraw_display();
2509 break;
2511 case REQ_PROMPT:
2512 /* Always reload^Wrerun commands from the prompt. */
2513 open_view(view, opt_request, OPEN_RELOAD);
2514 break;
2516 case REQ_SEARCH:
2517 case REQ_SEARCH_BACK:
2518 search_view(view, request);
2519 break;
2521 case REQ_FIND_NEXT:
2522 case REQ_FIND_PREV:
2523 find_next(view, request);
2524 break;
2526 case REQ_STOP_LOADING:
2527 for (i = 0; i < ARRAY_SIZE(views); i++) {
2528 view = &views[i];
2529 if (view->pipe)
2530 report("Stopped loading the %s view", view->name),
2531 end_update(view);
2532 }
2533 break;
2535 case REQ_SHOW_VERSION:
2536 report("tig-%s (built %s)", TIG_VERSION, __DATE__);
2537 return TRUE;
2539 case REQ_SCREEN_RESIZE:
2540 resize_display();
2541 /* Fall-through */
2542 case REQ_SCREEN_REDRAW:
2543 redraw_display();
2544 break;
2546 case REQ_EDIT:
2547 report("Nothing to edit");
2548 break;
2551 case REQ_ENTER:
2552 report("Nothing to enter");
2553 break;
2556 case REQ_VIEW_CLOSE:
2557 /* XXX: Mark closed views by letting view->parent point to the
2558 * view itself. Parents to closed view should never be
2559 * followed. */
2560 if (view->parent &&
2561 view->parent->parent != view->parent) {
2562 memset(display, 0, sizeof(display));
2563 current_view = 0;
2564 display[current_view] = view->parent;
2565 view->parent = view;
2566 resize_display();
2567 redraw_display();
2568 break;
2569 }
2570 /* Fall-through */
2571 case REQ_QUIT:
2572 return FALSE;
2574 default:
2575 /* An unknown key will show most commonly used commands. */
2576 report("Unknown key, press 'h' for help");
2577 return TRUE;
2578 }
2580 return TRUE;
2581 }
2584 /*
2585 * Pager backend
2586 */
2588 static bool
2589 pager_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2590 {
2591 char *text = line->data;
2592 enum line_type type = line->type;
2593 int textlen = strlen(text);
2594 int attr;
2596 wmove(view->win, lineno, 0);
2598 if (selected) {
2599 type = LINE_CURSOR;
2600 wchgat(view->win, -1, 0, type, NULL);
2601 }
2603 attr = get_line_attr(type);
2604 wattrset(view->win, attr);
2606 if (opt_line_number || opt_tab_size < TABSIZE) {
2607 static char spaces[] = " ";
2608 int col_offset = 0, col = 0;
2610 if (opt_line_number) {
2611 unsigned long real_lineno = view->offset + lineno + 1;
2613 if (real_lineno == 1 ||
2614 (real_lineno % opt_num_interval) == 0) {
2615 wprintw(view->win, "%.*d", view->digits, real_lineno);
2617 } else {
2618 waddnstr(view->win, spaces,
2619 MIN(view->digits, STRING_SIZE(spaces)));
2620 }
2621 waddstr(view->win, ": ");
2622 col_offset = view->digits + 2;
2623 }
2625 while (text && col_offset + col < view->width) {
2626 int cols_max = view->width - col_offset - col;
2627 char *pos = text;
2628 int cols;
2630 if (*text == '\t') {
2631 text++;
2632 assert(sizeof(spaces) > TABSIZE);
2633 pos = spaces;
2634 cols = opt_tab_size - (col % opt_tab_size);
2636 } else {
2637 text = strchr(text, '\t');
2638 cols = line ? text - pos : strlen(pos);
2639 }
2641 waddnstr(view->win, pos, MIN(cols, cols_max));
2642 col += cols;
2643 }
2645 } else {
2646 int col = 0, pos = 0;
2648 for (; pos < textlen && col < view->width; pos++, col++)
2649 if (text[pos] == '\t')
2650 col += TABSIZE - (col % TABSIZE) - 1;
2652 waddnstr(view->win, text, pos);
2653 }
2655 return TRUE;
2656 }
2658 static bool
2659 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2660 {
2661 char refbuf[SIZEOF_STR];
2662 char *ref = NULL;
2663 FILE *pipe;
2665 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2666 return TRUE;
2668 pipe = popen(refbuf, "r");
2669 if (!pipe)
2670 return TRUE;
2672 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2673 ref = chomp_string(ref);
2674 pclose(pipe);
2676 if (!ref || !*ref)
2677 return TRUE;
2679 /* This is the only fatal call, since it can "corrupt" the buffer. */
2680 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2681 return FALSE;
2683 return TRUE;
2684 }
2686 static void
2687 add_pager_refs(struct view *view, struct line *line)
2688 {
2689 char buf[SIZEOF_STR];
2690 char *commit_id = line->data + STRING_SIZE("commit ");
2691 struct ref **refs;
2692 size_t bufpos = 0, refpos = 0;
2693 const char *sep = "Refs: ";
2694 bool is_tag = FALSE;
2696 assert(line->type == LINE_COMMIT);
2698 refs = get_refs(commit_id);
2699 if (!refs) {
2700 if (view == VIEW(REQ_VIEW_DIFF))
2701 goto try_add_describe_ref;
2702 return;
2703 }
2705 do {
2706 struct ref *ref = refs[refpos];
2707 char *fmt = ref->tag ? "%s[%s]" :
2708 ref->remote ? "%s<%s>" : "%s%s";
2710 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2711 return;
2712 sep = ", ";
2713 if (ref->tag)
2714 is_tag = TRUE;
2715 } while (refs[refpos++]->next);
2717 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2718 try_add_describe_ref:
2719 /* Add <tag>-g<commit_id> "fake" reference. */
2720 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2721 return;
2722 }
2724 if (bufpos == 0)
2725 return;
2727 if (!realloc_lines(view, view->line_size + 1))
2728 return;
2730 add_line_text(view, buf, LINE_PP_REFS);
2731 }
2733 static bool
2734 pager_read(struct view *view, char *data)
2735 {
2736 struct line *line;
2738 if (!data)
2739 return TRUE;
2741 line = add_line_text(view, data, get_line_type(data));
2742 if (!line)
2743 return FALSE;
2745 if (line->type == LINE_COMMIT &&
2746 (view == VIEW(REQ_VIEW_DIFF) ||
2747 view == VIEW(REQ_VIEW_LOG)))
2748 add_pager_refs(view, line);
2750 return TRUE;
2751 }
2753 static enum request
2754 pager_request(struct view *view, enum request request, struct line *line)
2755 {
2756 int split = 0;
2758 if (request != REQ_ENTER)
2759 return request;
2761 if (line->type == LINE_COMMIT &&
2762 (view == VIEW(REQ_VIEW_LOG) ||
2763 view == VIEW(REQ_VIEW_PAGER))) {
2764 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2765 split = 1;
2766 }
2768 /* Always scroll the view even if it was split. That way
2769 * you can use Enter to scroll through the log view and
2770 * split open each commit diff. */
2771 scroll_view(view, REQ_SCROLL_LINE_DOWN);
2773 /* FIXME: A minor workaround. Scrolling the view will call report("")
2774 * but if we are scrolling a non-current view this won't properly
2775 * update the view title. */
2776 if (split)
2777 update_view_title(view);
2779 return REQ_NONE;
2780 }
2782 static bool
2783 pager_grep(struct view *view, struct line *line)
2784 {
2785 regmatch_t pmatch;
2786 char *text = line->data;
2788 if (!*text)
2789 return FALSE;
2791 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
2792 return FALSE;
2794 return TRUE;
2795 }
2797 static void
2798 pager_select(struct view *view, struct line *line)
2799 {
2800 if (line->type == LINE_COMMIT) {
2801 char *text = line->data + STRING_SIZE("commit ");
2803 if (view != VIEW(REQ_VIEW_PAGER))
2804 string_copy_rev(view->ref, text);
2805 string_copy_rev(ref_commit, text);
2806 }
2807 }
2809 static struct view_ops pager_ops = {
2810 "line",
2811 NULL,
2812 pager_read,
2813 pager_draw,
2814 pager_request,
2815 pager_grep,
2816 pager_select,
2817 };
2820 /*
2821 * Help backend
2822 */
2824 static bool
2825 help_open(struct view *view)
2826 {
2827 char buf[BUFSIZ];
2828 int lines = ARRAY_SIZE(req_info) + 2;
2829 int i;
2831 if (view->lines > 0)
2832 return TRUE;
2834 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2835 if (!req_info[i].request)
2836 lines++;
2838 lines += run_requests + 1;
2840 view->line = calloc(lines, sizeof(*view->line));
2841 if (!view->line)
2842 return FALSE;
2844 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
2846 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2847 char *key;
2849 if (req_info[i].request == REQ_NONE)
2850 continue;
2852 if (!req_info[i].request) {
2853 add_line_text(view, "", LINE_DEFAULT);
2854 add_line_text(view, req_info[i].help, LINE_DEFAULT);
2855 continue;
2856 }
2858 key = get_key(req_info[i].request);
2859 if (!*key)
2860 key = "(no key defined)";
2862 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
2863 continue;
2865 add_line_text(view, buf, LINE_DEFAULT);
2866 }
2868 if (run_requests) {
2869 add_line_text(view, "", LINE_DEFAULT);
2870 add_line_text(view, "External commands:", LINE_DEFAULT);
2871 }
2873 for (i = 0; i < run_requests; i++) {
2874 struct run_request *req = get_run_request(REQ_NONE + i + 1);
2875 char *key;
2877 if (!req)
2878 continue;
2880 key = get_key_name(req->key);
2881 if (!*key)
2882 key = "(no key defined)";
2884 if (!string_format(buf, " %-10s %-14s `%s`",
2885 keymap_table[req->keymap].name,
2886 key, req->cmd))
2887 continue;
2889 add_line_text(view, buf, LINE_DEFAULT);
2890 }
2892 return TRUE;
2893 }
2895 static struct view_ops help_ops = {
2896 "line",
2897 help_open,
2898 NULL,
2899 pager_draw,
2900 pager_request,
2901 pager_grep,
2902 pager_select,
2903 };
2906 /*
2907 * Tree backend
2908 */
2910 struct tree_stack_entry {
2911 struct tree_stack_entry *prev; /* Entry below this in the stack */
2912 unsigned long lineno; /* Line number to restore */
2913 char *name; /* Position of name in opt_path */
2914 };
2916 /* The top of the path stack. */
2917 static struct tree_stack_entry *tree_stack = NULL;
2918 unsigned long tree_lineno = 0;
2920 static void
2921 pop_tree_stack_entry(void)
2922 {
2923 struct tree_stack_entry *entry = tree_stack;
2925 tree_lineno = entry->lineno;
2926 entry->name[0] = 0;
2927 tree_stack = entry->prev;
2928 free(entry);
2929 }
2931 static void
2932 push_tree_stack_entry(char *name, unsigned long lineno)
2933 {
2934 struct tree_stack_entry *entry = calloc(1, sizeof(*entry));
2935 size_t pathlen = strlen(opt_path);
2937 if (!entry)
2938 return;
2940 entry->prev = tree_stack;
2941 entry->name = opt_path + pathlen;
2942 tree_stack = entry;
2944 if (!string_format_from(opt_path, &pathlen, "%s/", name)) {
2945 pop_tree_stack_entry();
2946 return;
2947 }
2949 /* Move the current line to the first tree entry. */
2950 tree_lineno = 1;
2951 entry->lineno = lineno;
2952 }
2954 /* Parse output from git-ls-tree(1):
2955 *
2956 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
2957 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
2958 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
2959 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
2960 */
2962 #define SIZEOF_TREE_ATTR \
2963 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
2965 #define TREE_UP_FORMAT "040000 tree %s\t.."
2967 static int
2968 tree_compare_entry(enum line_type type1, char *name1,
2969 enum line_type type2, char *name2)
2970 {
2971 if (type1 != type2) {
2972 if (type1 == LINE_TREE_DIR)
2973 return -1;
2974 return 1;
2975 }
2977 return strcmp(name1, name2);
2978 }
2980 static bool
2981 tree_read(struct view *view, char *text)
2982 {
2983 size_t textlen = text ? strlen(text) : 0;
2984 char buf[SIZEOF_STR];
2985 unsigned long pos;
2986 enum line_type type;
2987 bool first_read = view->lines == 0;
2989 if (textlen <= SIZEOF_TREE_ATTR)
2990 return FALSE;
2992 type = text[STRING_SIZE("100644 ")] == 't'
2993 ? LINE_TREE_DIR : LINE_TREE_FILE;
2995 if (first_read) {
2996 /* Add path info line */
2997 if (!string_format(buf, "Directory path /%s", opt_path) ||
2998 !realloc_lines(view, view->line_size + 1) ||
2999 !add_line_text(view, buf, LINE_DEFAULT))
3000 return FALSE;
3002 /* Insert "link" to parent directory. */
3003 if (*opt_path) {
3004 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
3005 !realloc_lines(view, view->line_size + 1) ||
3006 !add_line_text(view, buf, LINE_TREE_DIR))
3007 return FALSE;
3008 }
3009 }
3011 /* Strip the path part ... */
3012 if (*opt_path) {
3013 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
3014 size_t striplen = strlen(opt_path);
3015 char *path = text + SIZEOF_TREE_ATTR;
3017 if (pathlen > striplen)
3018 memmove(path, path + striplen,
3019 pathlen - striplen + 1);
3020 }
3022 /* Skip "Directory ..." and ".." line. */
3023 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
3024 struct line *line = &view->line[pos];
3025 char *path1 = ((char *) line->data) + SIZEOF_TREE_ATTR;
3026 char *path2 = text + SIZEOF_TREE_ATTR;
3027 int cmp = tree_compare_entry(line->type, path1, type, path2);
3029 if (cmp <= 0)
3030 continue;
3032 text = strdup(text);
3033 if (!text)
3034 return FALSE;
3036 if (view->lines > pos)
3037 memmove(&view->line[pos + 1], &view->line[pos],
3038 (view->lines - pos) * sizeof(*line));
3040 line = &view->line[pos];
3041 line->data = text;
3042 line->type = type;
3043 view->lines++;
3044 return TRUE;
3045 }
3047 if (!add_line_text(view, text, type))
3048 return FALSE;
3050 if (tree_lineno > view->lineno) {
3051 view->lineno = tree_lineno;
3052 tree_lineno = 0;
3053 }
3055 return TRUE;
3056 }
3058 static enum request
3059 tree_request(struct view *view, enum request request, struct line *line)
3060 {
3061 enum open_flags flags;
3063 if (request == REQ_TREE_PARENT) {
3064 if (*opt_path) {
3065 /* fake 'cd ..' */
3066 request = REQ_ENTER;
3067 line = &view->line[1];
3068 } else {
3069 /* quit view if at top of tree */
3070 return REQ_VIEW_CLOSE;
3071 }
3072 }
3073 if (request != REQ_ENTER)
3074 return request;
3076 /* Cleanup the stack if the tree view is at a different tree. */
3077 while (!*opt_path && tree_stack)
3078 pop_tree_stack_entry();
3080 switch (line->type) {
3081 case LINE_TREE_DIR:
3082 /* Depending on whether it is a subdir or parent (updir?) link
3083 * mangle the path buffer. */
3084 if (line == &view->line[1] && *opt_path) {
3085 pop_tree_stack_entry();
3087 } else {
3088 char *data = line->data;
3089 char *basename = data + SIZEOF_TREE_ATTR;
3091 push_tree_stack_entry(basename, view->lineno);
3092 }
3094 /* Trees and subtrees share the same ID, so they are not not
3095 * unique like blobs. */
3096 flags = OPEN_RELOAD;
3097 request = REQ_VIEW_TREE;
3098 break;
3100 case LINE_TREE_FILE:
3101 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3102 request = REQ_VIEW_BLOB;
3103 break;
3105 default:
3106 return TRUE;
3107 }
3109 open_view(view, request, flags);
3110 if (request == REQ_VIEW_TREE) {
3111 view->lineno = tree_lineno;
3112 }
3114 return REQ_NONE;
3115 }
3117 static void
3118 tree_select(struct view *view, struct line *line)
3119 {
3120 char *text = line->data + STRING_SIZE("100644 blob ");
3122 if (line->type == LINE_TREE_FILE) {
3123 string_copy_rev(ref_blob, text);
3125 } else if (line->type != LINE_TREE_DIR) {
3126 return;
3127 }
3129 string_copy_rev(view->ref, text);
3130 }
3132 static struct view_ops tree_ops = {
3133 "file",
3134 NULL,
3135 tree_read,
3136 pager_draw,
3137 tree_request,
3138 pager_grep,
3139 tree_select,
3140 };
3142 static bool
3143 blob_read(struct view *view, char *line)
3144 {
3145 return add_line_text(view, line, LINE_DEFAULT) != NULL;
3146 }
3148 static struct view_ops blob_ops = {
3149 "line",
3150 NULL,
3151 blob_read,
3152 pager_draw,
3153 pager_request,
3154 pager_grep,
3155 pager_select,
3156 };
3159 /*
3160 * Status backend
3161 */
3163 struct status {
3164 char status;
3165 struct {
3166 mode_t mode;
3167 char rev[SIZEOF_REV];
3168 char name[SIZEOF_STR];
3169 } old;
3170 struct {
3171 mode_t mode;
3172 char rev[SIZEOF_REV];
3173 char name[SIZEOF_STR];
3174 } new;
3175 };
3177 static struct status stage_status;
3178 static enum line_type stage_line_type;
3180 /* Get fields from the diff line:
3181 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
3182 */
3183 static inline bool
3184 status_get_diff(struct status *file, char *buf, size_t bufsize)
3185 {
3186 char *old_mode = buf + 1;
3187 char *new_mode = buf + 8;
3188 char *old_rev = buf + 15;
3189 char *new_rev = buf + 56;
3190 char *status = buf + 97;
3192 if (bufsize < 99 ||
3193 old_mode[-1] != ':' ||
3194 new_mode[-1] != ' ' ||
3195 old_rev[-1] != ' ' ||
3196 new_rev[-1] != ' ' ||
3197 status[-1] != ' ')
3198 return FALSE;
3200 file->status = *status;
3202 string_copy_rev(file->old.rev, old_rev);
3203 string_copy_rev(file->new.rev, new_rev);
3205 file->old.mode = strtoul(old_mode, NULL, 8);
3206 file->new.mode = strtoul(new_mode, NULL, 8);
3208 file->old.name[0] = file->new.name[0] = 0;
3210 return TRUE;
3211 }
3213 static bool
3214 status_run(struct view *view, const char cmd[], bool diff, enum line_type type)
3215 {
3216 struct status *file = NULL;
3217 struct status *unmerged = NULL;
3218 char buf[SIZEOF_STR * 4];
3219 size_t bufsize = 0;
3220 FILE *pipe;
3222 pipe = popen(cmd, "r");
3223 if (!pipe)
3224 return FALSE;
3226 add_line_data(view, NULL, type);
3228 while (!feof(pipe) && !ferror(pipe)) {
3229 char *sep;
3230 size_t readsize;
3232 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
3233 if (!readsize)
3234 break;
3235 bufsize += readsize;
3237 /* Process while we have NUL chars. */
3238 while ((sep = memchr(buf, 0, bufsize))) {
3239 size_t sepsize = sep - buf + 1;
3241 if (!file) {
3242 if (!realloc_lines(view, view->line_size + 1))
3243 goto error_out;
3245 file = calloc(1, sizeof(*file));
3246 if (!file)
3247 goto error_out;
3249 add_line_data(view, file, type);
3250 }
3252 /* Parse diff info part. */
3253 if (!diff) {
3254 file->status = '?';
3256 } else if (!file->status) {
3257 if (!status_get_diff(file, buf, sepsize))
3258 goto error_out;
3260 bufsize -= sepsize;
3261 memmove(buf, sep + 1, bufsize);
3263 sep = memchr(buf, 0, bufsize);
3264 if (!sep)
3265 break;
3266 sepsize = sep - buf + 1;
3268 /* Collapse all 'M'odified entries that
3269 * follow a associated 'U'nmerged entry.
3270 */
3271 if (file->status == 'U') {
3272 unmerged = file;
3274 } else if (unmerged) {
3275 int collapse = !strcmp(buf, unmerged->new.name);
3277 unmerged = NULL;
3278 if (collapse) {
3279 free(file);
3280 view->lines--;
3281 continue;
3282 }
3283 }
3284 }
3286 /* Grab the old name for rename/copy. */
3287 if (!*file->old.name &&
3288 (file->status == 'R' || file->status == 'C')) {
3289 sepsize = sep - buf + 1;
3290 string_ncopy(file->old.name, buf, sepsize);
3291 bufsize -= sepsize;
3292 memmove(buf, sep + 1, bufsize);
3294 sep = memchr(buf, 0, bufsize);
3295 if (!sep)
3296 break;
3297 sepsize = sep - buf + 1;
3298 }
3300 /* git-ls-files just delivers a NUL separated
3301 * list of file names similar to the second half
3302 * of the git-diff-* output. */
3303 string_ncopy(file->new.name, buf, sepsize);
3304 if (!*file->old.name)
3305 string_copy(file->old.name, file->new.name);
3306 bufsize -= sepsize;
3307 memmove(buf, sep + 1, bufsize);
3308 file = NULL;
3309 }
3310 }
3312 if (ferror(pipe)) {
3313 error_out:
3314 pclose(pipe);
3315 return FALSE;
3316 }
3318 if (!view->line[view->lines - 1].data)
3319 add_line_data(view, NULL, LINE_STAT_NONE);
3321 pclose(pipe);
3322 return TRUE;
3323 }
3325 /* Don't show unmerged entries in the staged section. */
3326 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached -M HEAD"
3327 #define STATUS_DIFF_FILES_CMD "git update-index -q --refresh && git diff-files -z"
3328 #define STATUS_LIST_OTHER_CMD \
3329 "git ls-files -z --others --exclude-per-directory=.gitignore"
3331 #define STATUS_DIFF_INDEX_SHOW_CMD \
3332 "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s %s 2>/dev/null"
3334 #define STATUS_DIFF_FILES_SHOW_CMD \
3335 "git diff-files --root --patch-with-stat -C -M -- %s %s 2>/dev/null"
3337 /* First parse staged info using git-diff-index(1), then parse unstaged
3338 * info using git-diff-files(1), and finally untracked files using
3339 * git-ls-files(1). */
3340 static bool
3341 status_open(struct view *view)
3342 {
3343 struct stat statbuf;
3344 char exclude[SIZEOF_STR];
3345 char cmd[SIZEOF_STR];
3346 unsigned long prev_lineno = view->lineno;
3347 size_t i;
3349 for (i = 0; i < view->lines; i++)
3350 free(view->line[i].data);
3351 free(view->line);
3352 view->lines = view->line_size = view->lineno = 0;
3353 view->line = NULL;
3355 if (!realloc_lines(view, view->line_size + 6))
3356 return FALSE;
3358 if (!string_format(exclude, "%s/info/exclude", opt_git_dir))
3359 return FALSE;
3361 string_copy(cmd, STATUS_LIST_OTHER_CMD);
3363 if (stat(exclude, &statbuf) >= 0) {
3364 size_t cmdsize = strlen(cmd);
3366 if (!string_format_from(cmd, &cmdsize, " %s", "--exclude-from=") ||
3367 sq_quote(cmd, cmdsize, exclude) >= sizeof(cmd))
3368 return FALSE;
3369 }
3371 if (!status_run(view, STATUS_DIFF_INDEX_CMD, TRUE, LINE_STAT_STAGED) ||
3372 !status_run(view, STATUS_DIFF_FILES_CMD, TRUE, LINE_STAT_UNSTAGED) ||
3373 !status_run(view, cmd, FALSE, LINE_STAT_UNTRACKED))
3374 return FALSE;
3376 /* If all went well restore the previous line number to stay in
3377 * the context. */
3378 if (prev_lineno < view->lines)
3379 view->lineno = prev_lineno;
3380 else
3381 view->lineno = view->lines - 1;
3383 return TRUE;
3384 }
3386 static bool
3387 status_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
3388 {
3389 struct status *status = line->data;
3391 wmove(view->win, lineno, 0);
3393 if (selected) {
3394 wattrset(view->win, get_line_attr(LINE_CURSOR));
3395 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
3397 } else if (!status && line->type != LINE_STAT_NONE) {
3398 wattrset(view->win, get_line_attr(LINE_STAT_SECTION));
3399 wchgat(view->win, -1, 0, LINE_STAT_SECTION, NULL);
3401 } else {
3402 wattrset(view->win, get_line_attr(line->type));
3403 }
3405 if (!status) {
3406 char *text;
3408 switch (line->type) {
3409 case LINE_STAT_STAGED:
3410 text = "Changes to be committed:";
3411 break;
3413 case LINE_STAT_UNSTAGED:
3414 text = "Changed but not updated:";
3415 break;
3417 case LINE_STAT_UNTRACKED:
3418 text = "Untracked files:";
3419 break;
3421 case LINE_STAT_NONE:
3422 text = " (no files)";
3423 break;
3425 default:
3426 return FALSE;
3427 }
3429 waddstr(view->win, text);
3430 return TRUE;
3431 }
3433 waddch(view->win, status->status);
3434 if (!selected)
3435 wattrset(view->win, A_NORMAL);
3436 wmove(view->win, lineno, 4);
3437 waddstr(view->win, status->new.name);
3439 return TRUE;
3440 }
3442 static enum request
3443 status_enter(struct view *view, struct line *line)
3444 {
3445 struct status *status = line->data;
3446 char oldpath[SIZEOF_STR] = "";
3447 char newpath[SIZEOF_STR] = "";
3448 char *info;
3449 size_t cmdsize = 0;
3451 if (line->type == LINE_STAT_NONE ||
3452 (!status && line[1].type == LINE_STAT_NONE)) {
3453 report("No file to diff");
3454 return REQ_NONE;
3455 }
3457 if (status) {
3458 if (sq_quote(oldpath, 0, status->old.name) >= sizeof(oldpath))
3459 return REQ_QUIT;
3460 /* Diffs for unmerged entries are empty when pasing the
3461 * new path, so leave it empty. */
3462 if (status->status != 'U' &&
3463 sq_quote(newpath, 0, status->new.name) >= sizeof(newpath))
3464 return REQ_QUIT;
3465 }
3467 if (opt_cdup[0] &&
3468 line->type != LINE_STAT_UNTRACKED &&
3469 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
3470 return REQ_QUIT;
3472 switch (line->type) {
3473 case LINE_STAT_STAGED:
3474 if (!string_format_from(opt_cmd, &cmdsize,
3475 STATUS_DIFF_INDEX_SHOW_CMD, oldpath, newpath))
3476 return REQ_QUIT;
3477 if (status)
3478 info = "Staged changes to %s";
3479 else
3480 info = "Staged changes";
3481 break;
3483 case LINE_STAT_UNSTAGED:
3484 if (!string_format_from(opt_cmd, &cmdsize,
3485 STATUS_DIFF_FILES_SHOW_CMD, oldpath, newpath))
3486 return REQ_QUIT;
3487 if (status)
3488 info = "Unstaged changes to %s";
3489 else
3490 info = "Unstaged changes";
3491 break;
3493 case LINE_STAT_UNTRACKED:
3494 if (opt_pipe)
3495 return REQ_QUIT;
3498 if (!status) {
3499 report("No file to show");
3500 return REQ_NONE;
3501 }
3503 opt_pipe = fopen(status->new.name, "r");
3504 info = "Untracked file %s";
3505 break;
3507 default:
3508 die("line type %d not handled in switch", line->type);
3509 }
3511 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | OPEN_SPLIT);
3512 if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) {
3513 if (status) {
3514 stage_status = *status;
3515 } else {
3516 memset(&stage_status, 0, sizeof(stage_status));
3517 }
3519 stage_line_type = line->type;
3520 string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.new.name);
3521 }
3523 return REQ_NONE;
3524 }
3527 static bool
3528 status_update_file(struct view *view, struct status *status, enum line_type type)
3529 {
3530 char cmd[SIZEOF_STR];
3531 char buf[SIZEOF_STR];
3532 size_t cmdsize = 0;
3533 size_t bufsize = 0;
3534 size_t written = 0;
3535 FILE *pipe;
3537 if (opt_cdup[0] &&
3538 type != LINE_STAT_UNTRACKED &&
3539 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
3540 return FALSE;
3542 switch (type) {
3543 case LINE_STAT_STAGED:
3544 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
3545 status->old.mode,
3546 status->old.rev,
3547 status->old.name, 0))
3548 return FALSE;
3550 string_add(cmd, cmdsize, "git update-index -z --index-info");
3551 break;
3553 case LINE_STAT_UNSTAGED:
3554 case LINE_STAT_UNTRACKED:
3555 if (!string_format_from(buf, &bufsize, "%s%c", status->new.name, 0))
3556 return FALSE;
3558 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
3559 break;
3561 default:
3562 die("line type %d not handled in switch", type);
3563 }
3565 pipe = popen(cmd, "w");
3566 if (!pipe)
3567 return FALSE;
3569 while (!ferror(pipe) && written < bufsize) {
3570 written += fwrite(buf + written, 1, bufsize - written, pipe);
3571 }
3573 pclose(pipe);
3575 if (written != bufsize)
3576 return FALSE;
3578 return TRUE;
3579 }
3581 static void
3582 status_update(struct view *view)
3583 {
3584 struct line *line = &view->line[view->lineno];
3586 assert(view->lines);
3588 if (!line->data) {
3589 while (++line < view->line + view->lines && line->data) {
3590 if (!status_update_file(view, line->data, line->type))
3591 report("Failed to update file status");
3592 }
3594 if (!line[-1].data) {
3595 report("Nothing to update");
3596 return;
3597 }
3599 } else if (!status_update_file(view, line->data, line->type)) {
3600 report("Failed to update file status");
3601 }
3602 }
3604 static enum request
3605 status_request(struct view *view, enum request request, struct line *line)
3606 {
3607 struct status *status = line->data;
3609 switch (request) {
3610 case REQ_STATUS_UPDATE:
3611 status_update(view);
3612 break;
3614 case REQ_STATUS_MERGE:
3615 if (!status || status->status != 'U') {
3616 report("Merging only possible for files with unmerged status ('U').");
3617 return REQ_NONE;
3618 }
3619 open_mergetool(status->new.name);
3620 break;
3622 case REQ_EDIT:
3623 if (!status)
3624 return request;
3626 open_editor(status->status != '?', status->new.name);
3627 break;
3629 case REQ_ENTER:
3630 /* After returning the status view has been split to
3631 * show the stage view. No further reloading is
3632 * necessary. */
3633 status_enter(view, line);
3634 return REQ_NONE;
3636 case REQ_REFRESH:
3637 /* Simply reload the view. */
3638 break;
3640 default:
3641 return request;
3642 }
3644 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
3646 return REQ_NONE;
3647 }
3649 static void
3650 status_select(struct view *view, struct line *line)
3651 {
3652 struct status *status = line->data;
3653 char file[SIZEOF_STR] = "all files";
3654 char *text;
3655 char *key;
3657 if (status && !string_format(file, "'%s'", status->new.name))
3658 return;
3660 if (!status && line[1].type == LINE_STAT_NONE)
3661 line++;
3663 switch (line->type) {
3664 case LINE_STAT_STAGED:
3665 text = "Press %s to unstage %s for commit";
3666 break;
3668 case LINE_STAT_UNSTAGED:
3669 text = "Press %s to stage %s for commit";
3670 break;
3672 case LINE_STAT_UNTRACKED:
3673 text = "Press %s to stage %s for addition";
3674 break;
3676 case LINE_STAT_NONE:
3677 text = "Nothing to update";
3678 break;
3680 default:
3681 die("line type %d not handled in switch", line->type);
3682 }
3684 if (status && status->status == 'U') {
3685 text = "Press %s to resolve conflict in %s";
3686 key = get_key(REQ_STATUS_MERGE);
3688 } else {
3689 key = get_key(REQ_STATUS_UPDATE);
3690 }
3692 string_format(view->ref, text, key, file);
3693 }
3695 static bool
3696 status_grep(struct view *view, struct line *line)
3697 {
3698 struct status *status = line->data;
3699 enum { S_STATUS, S_NAME, S_END } state;
3700 char buf[2] = "?";
3701 regmatch_t pmatch;
3703 if (!status)
3704 return FALSE;
3706 for (state = S_STATUS; state < S_END; state++) {
3707 char *text;
3709 switch (state) {
3710 case S_NAME: text = status->new.name; break;
3711 case S_STATUS:
3712 buf[0] = status->status;
3713 text = buf;
3714 break;
3716 default:
3717 return FALSE;
3718 }
3720 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3721 return TRUE;
3722 }
3724 return FALSE;
3725 }
3727 static struct view_ops status_ops = {
3728 "file",
3729 status_open,
3730 NULL,
3731 status_draw,
3732 status_request,
3733 status_grep,
3734 status_select,
3735 };
3738 static bool
3739 stage_diff_line(FILE *pipe, struct line *line)
3740 {
3741 char *buf = line->data;
3742 size_t bufsize = strlen(buf);
3743 size_t written = 0;
3745 while (!ferror(pipe) && written < bufsize) {
3746 written += fwrite(buf + written, 1, bufsize - written, pipe);
3747 }
3749 fputc('\n', pipe);
3751 return written == bufsize;
3752 }
3754 static struct line *
3755 stage_diff_hdr(struct view *view, struct line *line)
3756 {
3757 int diff_hdr_dir = line->type == LINE_DIFF_CHUNK ? -1 : 1;
3758 struct line *diff_hdr;
3760 if (line->type == LINE_DIFF_CHUNK)
3761 diff_hdr = line - 1;
3762 else
3763 diff_hdr = view->line + 1;
3765 while (diff_hdr > view->line && diff_hdr < view->line + view->lines) {
3766 if (diff_hdr->type == LINE_DIFF_HEADER)
3767 return diff_hdr;
3769 diff_hdr += diff_hdr_dir;
3770 }
3772 return NULL;
3773 }
3775 static bool
3776 stage_update_chunk(struct view *view, struct line *line)
3777 {
3778 char cmd[SIZEOF_STR];
3779 size_t cmdsize = 0;
3780 struct line *diff_hdr, *diff_chunk, *diff_end;
3781 FILE *pipe;
3783 diff_hdr = stage_diff_hdr(view, line);
3784 if (!diff_hdr)
3785 return FALSE;
3787 if (opt_cdup[0] &&
3788 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
3789 return FALSE;
3791 if (!string_format_from(cmd, &cmdsize,
3792 "git apply --cached %s - && "
3793 "git update-index -q --unmerged --refresh 2>/dev/null",
3794 stage_line_type == LINE_STAT_STAGED ? "-R" : ""))
3795 return FALSE;
3797 pipe = popen(cmd, "w");
3798 if (!pipe)
3799 return FALSE;
3801 diff_end = view->line + view->lines;
3802 if (line->type != LINE_DIFF_CHUNK) {
3803 diff_chunk = diff_hdr;
3805 } else {
3806 for (diff_chunk = line + 1; diff_chunk < diff_end; diff_chunk++)
3807 if (diff_chunk->type == LINE_DIFF_CHUNK ||
3808 diff_chunk->type == LINE_DIFF_HEADER)
3809 diff_end = diff_chunk;
3811 diff_chunk = line;
3813 while (diff_hdr->type != LINE_DIFF_CHUNK) {
3814 switch (diff_hdr->type) {
3815 case LINE_DIFF_HEADER:
3816 case LINE_DIFF_INDEX:
3817 case LINE_DIFF_ADD:
3818 case LINE_DIFF_DEL:
3819 break;
3821 default:
3822 diff_hdr++;
3823 continue;
3824 }
3826 if (!stage_diff_line(pipe, diff_hdr++)) {
3827 pclose(pipe);
3828 return FALSE;
3829 }
3830 }
3831 }
3833 while (diff_chunk < diff_end && stage_diff_line(pipe, diff_chunk))
3834 diff_chunk++;
3836 pclose(pipe);
3838 if (diff_chunk != diff_end)
3839 return FALSE;
3841 return TRUE;
3842 }
3844 static void
3845 stage_update(struct view *view, struct line *line)
3846 {
3847 if (stage_line_type != LINE_STAT_UNTRACKED &&
3848 (line->type == LINE_DIFF_CHUNK || !stage_status.status)) {
3849 if (!stage_update_chunk(view, line)) {
3850 report("Failed to apply chunk");
3851 return;
3852 }
3854 } else if (!status_update_file(view, &stage_status, stage_line_type)) {
3855 report("Failed to update file");
3856 return;
3857 }
3859 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
3861 view = VIEW(REQ_VIEW_STATUS);
3862 if (view_is_displayed(view))
3863 status_enter(view, &view->line[view->lineno]);
3864 }
3866 static enum request
3867 stage_request(struct view *view, enum request request, struct line *line)
3868 {
3869 switch (request) {
3870 case REQ_STATUS_UPDATE:
3871 stage_update(view, line);
3872 break;
3874 case REQ_EDIT:
3875 if (!stage_status.new.name[0])
3876 return request;
3878 open_editor(stage_status.status != '?', stage_status.new.name);
3879 break;
3881 case REQ_ENTER:
3882 pager_request(view, request, line);
3883 break;
3885 default:
3886 return request;
3887 }
3889 return REQ_NONE;
3890 }
3892 static struct view_ops stage_ops = {
3893 "line",
3894 NULL,
3895 pager_read,
3896 pager_draw,
3897 stage_request,
3898 pager_grep,
3899 pager_select,
3900 };
3903 /*
3904 * Revision graph
3905 */
3907 struct commit {
3908 char id[SIZEOF_REV]; /* SHA1 ID. */
3909 char title[128]; /* First line of the commit message. */
3910 char author[75]; /* Author of the commit. */
3911 struct tm time; /* Date from the author ident. */
3912 struct ref **refs; /* Repository references. */
3913 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
3914 size_t graph_size; /* The width of the graph array. */
3915 };
3917 /* Size of rev graph with no "padding" columns */
3918 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
3920 struct rev_graph {
3921 struct rev_graph *prev, *next, *parents;
3922 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
3923 size_t size;
3924 struct commit *commit;
3925 size_t pos;
3926 unsigned int boundary:1;
3927 };
3929 /* Parents of the commit being visualized. */
3930 static struct rev_graph graph_parents[4];
3932 /* The current stack of revisions on the graph. */
3933 static struct rev_graph graph_stacks[4] = {
3934 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
3935 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
3936 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
3937 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
3938 };
3940 static inline bool
3941 graph_parent_is_merge(struct rev_graph *graph)
3942 {
3943 return graph->parents->size > 1;
3944 }
3946 static inline void
3947 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
3948 {
3949 struct commit *commit = graph->commit;
3951 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
3952 commit->graph[commit->graph_size++] = symbol;
3953 }
3955 static void
3956 done_rev_graph(struct rev_graph *graph)
3957 {
3958 if (graph_parent_is_merge(graph) &&
3959 graph->pos < graph->size - 1 &&
3960 graph->next->size == graph->size + graph->parents->size - 1) {
3961 size_t i = graph->pos + graph->parents->size - 1;
3963 graph->commit->graph_size = i * 2;
3964 while (i < graph->next->size - 1) {
3965 append_to_rev_graph(graph, ' ');
3966 append_to_rev_graph(graph, '\\');
3967 i++;
3968 }
3969 }
3971 graph->size = graph->pos = 0;
3972 graph->commit = NULL;
3973 memset(graph->parents, 0, sizeof(*graph->parents));
3974 }
3976 static void
3977 push_rev_graph(struct rev_graph *graph, char *parent)
3978 {
3979 int i;
3981 /* "Collapse" duplicate parents lines.
3982 *
3983 * FIXME: This needs to also update update the drawn graph but
3984 * for now it just serves as a method for pruning graph lines. */
3985 for (i = 0; i < graph->size; i++)
3986 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
3987 return;
3989 if (graph->size < SIZEOF_REVITEMS) {
3990 string_copy_rev(graph->rev[graph->size++], parent);
3991 }
3992 }
3994 static chtype
3995 get_rev_graph_symbol(struct rev_graph *graph)
3996 {
3997 chtype symbol;
3999 if (graph->boundary)
4000 symbol = REVGRAPH_BOUND;
4001 else if (graph->parents->size == 0)
4002 symbol = REVGRAPH_INIT;
4003 else if (graph_parent_is_merge(graph))
4004 symbol = REVGRAPH_MERGE;
4005 else if (graph->pos >= graph->size)
4006 symbol = REVGRAPH_BRANCH;
4007 else
4008 symbol = REVGRAPH_COMMIT;
4010 return symbol;
4011 }
4013 static void
4014 draw_rev_graph(struct rev_graph *graph)
4015 {
4016 struct rev_filler {
4017 chtype separator, line;
4018 };
4019 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
4020 static struct rev_filler fillers[] = {
4021 { ' ', REVGRAPH_LINE },
4022 { '`', '.' },
4023 { '\'', ' ' },
4024 { '/', ' ' },
4025 };
4026 chtype symbol = get_rev_graph_symbol(graph);
4027 struct rev_filler *filler;
4028 size_t i;
4030 filler = &fillers[DEFAULT];
4032 for (i = 0; i < graph->pos; i++) {
4033 append_to_rev_graph(graph, filler->line);
4034 if (graph_parent_is_merge(graph->prev) &&
4035 graph->prev->pos == i)
4036 filler = &fillers[RSHARP];
4038 append_to_rev_graph(graph, filler->separator);
4039 }
4041 /* Place the symbol for this revision. */
4042 append_to_rev_graph(graph, symbol);
4044 if (graph->prev->size > graph->size)
4045 filler = &fillers[RDIAG];
4046 else
4047 filler = &fillers[DEFAULT];
4049 i++;
4051 for (; i < graph->size; i++) {
4052 append_to_rev_graph(graph, filler->separator);
4053 append_to_rev_graph(graph, filler->line);
4054 if (graph_parent_is_merge(graph->prev) &&
4055 i < graph->prev->pos + graph->parents->size)
4056 filler = &fillers[RSHARP];
4057 if (graph->prev->size > graph->size)
4058 filler = &fillers[LDIAG];
4059 }
4061 if (graph->prev->size > graph->size) {
4062 append_to_rev_graph(graph, filler->separator);
4063 if (filler->line != ' ')
4064 append_to_rev_graph(graph, filler->line);
4065 }
4066 }
4068 /* Prepare the next rev graph */
4069 static void
4070 prepare_rev_graph(struct rev_graph *graph)
4071 {
4072 size_t i;
4074 /* First, traverse all lines of revisions up to the active one. */
4075 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
4076 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
4077 break;
4079 push_rev_graph(graph->next, graph->rev[graph->pos]);
4080 }
4082 /* Interleave the new revision parent(s). */
4083 for (i = 0; !graph->boundary && i < graph->parents->size; i++)
4084 push_rev_graph(graph->next, graph->parents->rev[i]);
4086 /* Lastly, put any remaining revisions. */
4087 for (i = graph->pos + 1; i < graph->size; i++)
4088 push_rev_graph(graph->next, graph->rev[i]);
4089 }
4091 static void
4092 update_rev_graph(struct rev_graph *graph)
4093 {
4094 /* If this is the finalizing update ... */
4095 if (graph->commit)
4096 prepare_rev_graph(graph);
4098 /* Graph visualization needs a one rev look-ahead,
4099 * so the first update doesn't visualize anything. */
4100 if (!graph->prev->commit)
4101 return;
4103 draw_rev_graph(graph->prev);
4104 done_rev_graph(graph->prev->prev);
4105 }
4108 /*
4109 * Main view backend
4110 */
4112 static bool
4113 main_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
4114 {
4115 char buf[DATE_COLS + 1];
4116 struct commit *commit = line->data;
4117 enum line_type type;
4118 int col = 0;
4119 size_t timelen;
4120 size_t authorlen;
4121 int trimmed = 1;
4123 if (!*commit->author)
4124 return FALSE;
4126 wmove(view->win, lineno, col);
4128 if (selected) {
4129 type = LINE_CURSOR;
4130 wattrset(view->win, get_line_attr(type));
4131 wchgat(view->win, -1, 0, type, NULL);
4133 } else {
4134 type = LINE_MAIN_COMMIT;
4135 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
4136 }
4138 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
4139 waddnstr(view->win, buf, timelen);
4140 waddstr(view->win, " ");
4142 col += DATE_COLS;
4143 wmove(view->win, lineno, col);
4144 if (type != LINE_CURSOR)
4145 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
4147 if (opt_utf8) {
4148 authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
4149 } else {
4150 authorlen = strlen(commit->author);
4151 if (authorlen > AUTHOR_COLS - 2) {
4152 authorlen = AUTHOR_COLS - 2;
4153 trimmed = 1;
4154 }
4155 }
4157 if (trimmed) {
4158 waddnstr(view->win, commit->author, authorlen);
4159 if (type != LINE_CURSOR)
4160 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
4161 waddch(view->win, '~');
4162 } else {
4163 waddstr(view->win, commit->author);
4164 }
4166 col += AUTHOR_COLS;
4168 if (opt_rev_graph && commit->graph_size) {
4169 size_t i;
4171 if (type != LINE_CURSOR)
4172 wattrset(view->win, get_line_attr(LINE_MAIN_REVGRAPH));
4173 wmove(view->win, lineno, col);
4174 /* Using waddch() instead of waddnstr() ensures that
4175 * they'll be rendered correctly for the cursor line. */
4176 for (i = 0; i < commit->graph_size; i++)
4177 waddch(view->win, commit->graph[i]);
4179 waddch(view->win, ' ');
4180 col += commit->graph_size + 1;
4181 }
4182 if (type != LINE_CURSOR)
4183 wattrset(view->win, A_NORMAL);
4185 wmove(view->win, lineno, col);
4187 if (commit->refs) {
4188 size_t i = 0;
4190 do {
4191 if (type == LINE_CURSOR)
4192 ;
4193 else if (commit->refs[i]->tag)
4194 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
4195 else if (commit->refs[i]->remote)
4196 wattrset(view->win, get_line_attr(LINE_MAIN_REMOTE));
4197 else
4198 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
4199 waddstr(view->win, "[");
4200 waddstr(view->win, commit->refs[i]->name);
4201 waddstr(view->win, "]");
4202 if (type != LINE_CURSOR)
4203 wattrset(view->win, A_NORMAL);
4204 waddstr(view->win, " ");
4205 col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
4206 } while (commit->refs[i++]->next);
4207 }
4209 if (type != LINE_CURSOR)
4210 wattrset(view->win, get_line_attr(type));
4212 {
4213 int titlelen = strlen(commit->title);
4215 if (col + titlelen > view->width)
4216 titlelen = view->width - col;
4218 waddnstr(view->win, commit->title, titlelen);
4219 }
4221 return TRUE;
4222 }
4224 /* Reads git log --pretty=raw output and parses it into the commit struct. */
4225 static bool
4226 main_read(struct view *view, char *line)
4227 {
4228 static struct rev_graph *graph = graph_stacks;
4229 enum line_type type;
4230 struct commit *commit;
4232 if (!line) {
4233 update_rev_graph(graph);
4234 return TRUE;
4235 }
4237 type = get_line_type(line);
4238 if (type == LINE_COMMIT) {
4239 commit = calloc(1, sizeof(struct commit));
4240 if (!commit)
4241 return FALSE;
4243 line += STRING_SIZE("commit ");
4244 if (*line == '-') {
4245 graph->boundary = 1;
4246 line++;
4247 }
4249 string_copy_rev(commit->id, line);
4250 commit->refs = get_refs(commit->id);
4251 graph->commit = commit;
4252 add_line_data(view, commit, LINE_MAIN_COMMIT);
4253 return TRUE;
4254 }
4256 if (!view->lines)
4257 return TRUE;
4258 commit = view->line[view->lines - 1].data;
4260 switch (type) {
4261 case LINE_PARENT:
4262 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
4263 break;
4265 case LINE_AUTHOR:
4266 {
4267 /* Parse author lines where the name may be empty:
4268 * author <email@address.tld> 1138474660 +0100
4269 */
4270 char *ident = line + STRING_SIZE("author ");
4271 char *nameend = strchr(ident, '<');
4272 char *emailend = strchr(ident, '>');
4274 if (!nameend || !emailend)
4275 break;
4277 update_rev_graph(graph);
4278 graph = graph->next;
4280 *nameend = *emailend = 0;
4281 ident = chomp_string(ident);
4282 if (!*ident) {
4283 ident = chomp_string(nameend + 1);
4284 if (!*ident)
4285 ident = "Unknown";
4286 }
4288 string_ncopy(commit->author, ident, strlen(ident));
4290 /* Parse epoch and timezone */
4291 if (emailend[1] == ' ') {
4292 char *secs = emailend + 2;
4293 char *zone = strchr(secs, ' ');
4294 time_t time = (time_t) atol(secs);
4296 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
4297 long tz;
4299 zone++;
4300 tz = ('0' - zone[1]) * 60 * 60 * 10;
4301 tz += ('0' - zone[2]) * 60 * 60;
4302 tz += ('0' - zone[3]) * 60;
4303 tz += ('0' - zone[4]) * 60;
4305 if (zone[0] == '-')
4306 tz = -tz;
4308 time -= tz;
4309 }
4311 gmtime_r(&time, &commit->time);
4312 }
4313 break;
4314 }
4315 default:
4316 /* Fill in the commit title if it has not already been set. */
4317 if (commit->title[0])
4318 break;
4320 /* Require titles to start with a non-space character at the
4321 * offset used by git log. */
4322 if (strncmp(line, " ", 4))
4323 break;
4324 line += 4;
4325 /* Well, if the title starts with a whitespace character,
4326 * try to be forgiving. Otherwise we end up with no title. */
4327 while (isspace(*line))
4328 line++;
4329 if (*line == '\0')
4330 break;
4331 /* FIXME: More graceful handling of titles; append "..." to
4332 * shortened titles, etc. */
4334 string_ncopy(commit->title, line, strlen(line));
4335 }
4337 return TRUE;
4338 }
4340 static enum request
4341 main_request(struct view *view, enum request request, struct line *line)
4342 {
4343 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
4345 if (request == REQ_ENTER)
4346 open_view(view, REQ_VIEW_DIFF, flags);
4347 else
4348 return request;
4350 return REQ_NONE;
4351 }
4353 static bool
4354 main_grep(struct view *view, struct line *line)
4355 {
4356 struct commit *commit = line->data;
4357 enum { S_TITLE, S_AUTHOR, S_DATE, S_END } state;
4358 char buf[DATE_COLS + 1];
4359 regmatch_t pmatch;
4361 for (state = S_TITLE; state < S_END; state++) {
4362 char *text;
4364 switch (state) {
4365 case S_TITLE: text = commit->title; break;
4366 case S_AUTHOR: text = commit->author; break;
4367 case S_DATE:
4368 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
4369 continue;
4370 text = buf;
4371 break;
4373 default:
4374 return FALSE;
4375 }
4377 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4378 return TRUE;
4379 }
4381 return FALSE;
4382 }
4384 static void
4385 main_select(struct view *view, struct line *line)
4386 {
4387 struct commit *commit = line->data;
4389 string_copy_rev(view->ref, commit->id);
4390 string_copy_rev(ref_commit, view->ref);
4391 }
4393 static struct view_ops main_ops = {
4394 "commit",
4395 NULL,
4396 main_read,
4397 main_draw,
4398 main_request,
4399 main_grep,
4400 main_select,
4401 };
4404 /*
4405 * Unicode / UTF-8 handling
4406 *
4407 * NOTE: Much of the following code for dealing with unicode is derived from
4408 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
4409 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
4410 */
4412 /* I've (over)annotated a lot of code snippets because I am not entirely
4413 * confident that the approach taken by this small UTF-8 interface is correct.
4414 * --jonas */
4416 static inline int
4417 unicode_width(unsigned long c)
4418 {
4419 if (c >= 0x1100 &&
4420 (c <= 0x115f /* Hangul Jamo */
4421 || c == 0x2329
4422 || c == 0x232a
4423 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
4424 /* CJK ... Yi */
4425 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
4426 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
4427 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
4428 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
4429 || (c >= 0xffe0 && c <= 0xffe6)
4430 || (c >= 0x20000 && c <= 0x2fffd)
4431 || (c >= 0x30000 && c <= 0x3fffd)))
4432 return 2;
4434 return 1;
4435 }
4437 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
4438 * Illegal bytes are set one. */
4439 static const unsigned char utf8_bytes[256] = {
4440 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,
4441 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,
4442 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,
4443 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,
4444 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,
4445 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,
4446 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,
4447 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,
4448 };
4450 /* Decode UTF-8 multi-byte representation into a unicode character. */
4451 static inline unsigned long
4452 utf8_to_unicode(const char *string, size_t length)
4453 {
4454 unsigned long unicode;
4456 switch (length) {
4457 case 1:
4458 unicode = string[0];
4459 break;
4460 case 2:
4461 unicode = (string[0] & 0x1f) << 6;
4462 unicode += (string[1] & 0x3f);
4463 break;
4464 case 3:
4465 unicode = (string[0] & 0x0f) << 12;
4466 unicode += ((string[1] & 0x3f) << 6);
4467 unicode += (string[2] & 0x3f);
4468 break;
4469 case 4:
4470 unicode = (string[0] & 0x0f) << 18;
4471 unicode += ((string[1] & 0x3f) << 12);
4472 unicode += ((string[2] & 0x3f) << 6);
4473 unicode += (string[3] & 0x3f);
4474 break;
4475 case 5:
4476 unicode = (string[0] & 0x0f) << 24;
4477 unicode += ((string[1] & 0x3f) << 18);
4478 unicode += ((string[2] & 0x3f) << 12);
4479 unicode += ((string[3] & 0x3f) << 6);
4480 unicode += (string[4] & 0x3f);
4481 break;
4482 case 6:
4483 unicode = (string[0] & 0x01) << 30;
4484 unicode += ((string[1] & 0x3f) << 24);
4485 unicode += ((string[2] & 0x3f) << 18);
4486 unicode += ((string[3] & 0x3f) << 12);
4487 unicode += ((string[4] & 0x3f) << 6);
4488 unicode += (string[5] & 0x3f);
4489 break;
4490 default:
4491 die("Invalid unicode length");
4492 }
4494 /* Invalid characters could return the special 0xfffd value but NUL
4495 * should be just as good. */
4496 return unicode > 0xffff ? 0 : unicode;
4497 }
4499 /* Calculates how much of string can be shown within the given maximum width
4500 * and sets trimmed parameter to non-zero value if all of string could not be
4501 * shown.
4502 *
4503 * Additionally, adds to coloffset how many many columns to move to align with
4504 * the expected position. Takes into account how multi-byte and double-width
4505 * characters will effect the cursor position.
4506 *
4507 * Returns the number of bytes to output from string to satisfy max_width. */
4508 static size_t
4509 utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
4510 {
4511 const char *start = string;
4512 const char *end = strchr(string, '\0');
4513 size_t mbwidth = 0;
4514 size_t width = 0;
4516 *trimmed = 0;
4518 while (string < end) {
4519 int c = *(unsigned char *) string;
4520 unsigned char bytes = utf8_bytes[c];
4521 size_t ucwidth;
4522 unsigned long unicode;
4524 if (string + bytes > end)
4525 break;
4527 /* Change representation to figure out whether
4528 * it is a single- or double-width character. */
4530 unicode = utf8_to_unicode(string, bytes);
4531 /* FIXME: Graceful handling of invalid unicode character. */
4532 if (!unicode)
4533 break;
4535 ucwidth = unicode_width(unicode);
4536 width += ucwidth;
4537 if (width > max_width) {
4538 *trimmed = 1;
4539 break;
4540 }
4542 /* The column offset collects the differences between the
4543 * number of bytes encoding a character and the number of
4544 * columns will be used for rendering said character.
4545 *
4546 * So if some character A is encoded in 2 bytes, but will be
4547 * represented on the screen using only 1 byte this will and up
4548 * adding 1 to the multi-byte column offset.
4549 *
4550 * Assumes that no double-width character can be encoding in
4551 * less than two bytes. */
4552 if (bytes > ucwidth)
4553 mbwidth += bytes - ucwidth;
4555 string += bytes;
4556 }
4558 *coloffset += mbwidth;
4560 return string - start;
4561 }
4564 /*
4565 * Status management
4566 */
4568 /* Whether or not the curses interface has been initialized. */
4569 static bool cursed = FALSE;
4571 /* The status window is used for polling keystrokes. */
4572 static WINDOW *status_win;
4574 static bool status_empty = TRUE;
4576 /* Update status and title window. */
4577 static void
4578 report(const char *msg, ...)
4579 {
4580 struct view *view = display[current_view];
4582 if (input_mode)
4583 return;
4585 if (!view) {
4586 char buf[SIZEOF_STR];
4587 va_list args;
4589 va_start(args, msg);
4590 if (vsnprintf(buf, sizeof(buf), msg, args) >= sizeof(buf)) {
4591 buf[sizeof(buf) - 1] = 0;
4592 buf[sizeof(buf) - 2] = '.';
4593 buf[sizeof(buf) - 3] = '.';
4594 buf[sizeof(buf) - 4] = '.';
4595 }
4596 va_end(args);
4597 die("%s", buf);
4598 }
4600 if (!status_empty || *msg) {
4601 va_list args;
4603 va_start(args, msg);
4605 wmove(status_win, 0, 0);
4606 if (*msg) {
4607 vwprintw(status_win, msg, args);
4608 status_empty = FALSE;
4609 } else {
4610 status_empty = TRUE;
4611 }
4612 wclrtoeol(status_win);
4613 wrefresh(status_win);
4615 va_end(args);
4616 }
4618 update_view_title(view);
4619 update_display_cursor(view);
4620 }
4622 /* Controls when nodelay should be in effect when polling user input. */
4623 static void
4624 set_nonblocking_input(bool loading)
4625 {
4626 static unsigned int loading_views;
4628 if ((loading == FALSE && loading_views-- == 1) ||
4629 (loading == TRUE && loading_views++ == 0))
4630 nodelay(status_win, loading);
4631 }
4633 static void
4634 init_display(void)
4635 {
4636 int x, y;
4638 /* Initialize the curses library */
4639 if (isatty(STDIN_FILENO)) {
4640 cursed = !!initscr();
4641 } else {
4642 /* Leave stdin and stdout alone when acting as a pager. */
4643 FILE *io = fopen("/dev/tty", "r+");
4645 if (!io)
4646 die("Failed to open /dev/tty");
4647 cursed = !!newterm(NULL, io, io);
4648 }
4650 if (!cursed)
4651 die("Failed to initialize curses");
4653 nonl(); /* Tell curses not to do NL->CR/NL on output */
4654 cbreak(); /* Take input chars one at a time, no wait for \n */
4655 noecho(); /* Don't echo input */
4656 leaveok(stdscr, TRUE);
4658 if (has_colors())
4659 init_colors();
4661 getmaxyx(stdscr, y, x);
4662 status_win = newwin(1, 0, y - 1, 0);
4663 if (!status_win)
4664 die("Failed to create status window");
4666 /* Enable keyboard mapping */
4667 keypad(status_win, TRUE);
4668 wbkgdset(status_win, get_line_attr(LINE_STATUS));
4669 }
4671 static char *
4672 read_prompt(const char *prompt)
4673 {
4674 enum { READING, STOP, CANCEL } status = READING;
4675 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
4676 int pos = 0;
4678 while (status == READING) {
4679 struct view *view;
4680 int i, key;
4682 input_mode = TRUE;
4684 foreach_view (view, i)
4685 update_view(view);
4687 input_mode = FALSE;
4689 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
4690 wclrtoeol(status_win);
4692 /* Refresh, accept single keystroke of input */
4693 key = wgetch(status_win);
4694 switch (key) {
4695 case KEY_RETURN:
4696 case KEY_ENTER:
4697 case '\n':
4698 status = pos ? STOP : CANCEL;
4699 break;
4701 case KEY_BACKSPACE:
4702 if (pos > 0)
4703 pos--;
4704 else
4705 status = CANCEL;
4706 break;
4708 case KEY_ESC:
4709 status = CANCEL;
4710 break;
4712 case ERR:
4713 break;
4715 default:
4716 if (pos >= sizeof(buf)) {
4717 report("Input string too long");
4718 return NULL;
4719 }
4721 if (isprint(key))
4722 buf[pos++] = (char) key;
4723 }
4724 }
4726 /* Clear the status window */
4727 status_empty = FALSE;
4728 report("");
4730 if (status == CANCEL)
4731 return NULL;
4733 buf[pos++] = 0;
4735 return buf;
4736 }
4738 /*
4739 * Repository references
4740 */
4742 static struct ref *refs;
4743 static size_t refs_size;
4745 /* Id <-> ref store */
4746 static struct ref ***id_refs;
4747 static size_t id_refs_size;
4749 static struct ref **
4750 get_refs(char *id)
4751 {
4752 struct ref ***tmp_id_refs;
4753 struct ref **ref_list = NULL;
4754 size_t ref_list_size = 0;
4755 size_t i;
4757 for (i = 0; i < id_refs_size; i++)
4758 if (!strcmp(id, id_refs[i][0]->id))
4759 return id_refs[i];
4761 tmp_id_refs = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs));
4762 if (!tmp_id_refs)
4763 return NULL;
4765 id_refs = tmp_id_refs;
4767 for (i = 0; i < refs_size; i++) {
4768 struct ref **tmp;
4770 if (strcmp(id, refs[i].id))
4771 continue;
4773 tmp = realloc(ref_list, (ref_list_size + 1) * sizeof(*ref_list));
4774 if (!tmp) {
4775 if (ref_list)
4776 free(ref_list);
4777 return NULL;
4778 }
4780 ref_list = tmp;
4781 if (ref_list_size > 0)
4782 ref_list[ref_list_size - 1]->next = 1;
4783 ref_list[ref_list_size] = &refs[i];
4785 /* XXX: The properties of the commit chains ensures that we can
4786 * safely modify the shared ref. The repo references will
4787 * always be similar for the same id. */
4788 ref_list[ref_list_size]->next = 0;
4789 ref_list_size++;
4790 }
4792 if (ref_list)
4793 id_refs[id_refs_size++] = ref_list;
4795 return ref_list;
4796 }
4798 static int
4799 read_ref(char *id, size_t idlen, char *name, size_t namelen)
4800 {
4801 struct ref *ref;
4802 bool tag = FALSE;
4803 bool remote = FALSE;
4805 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
4806 /* Commits referenced by tags has "^{}" appended. */
4807 if (name[namelen - 1] != '}')
4808 return OK;
4810 while (namelen > 0 && name[namelen] != '^')
4811 namelen--;
4813 tag = TRUE;
4814 namelen -= STRING_SIZE("refs/tags/");
4815 name += STRING_SIZE("refs/tags/");
4817 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
4818 remote = TRUE;
4819 namelen -= STRING_SIZE("refs/remotes/");
4820 name += STRING_SIZE("refs/remotes/");
4822 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
4823 namelen -= STRING_SIZE("refs/heads/");
4824 name += STRING_SIZE("refs/heads/");
4826 } else if (!strcmp(name, "HEAD")) {
4827 return OK;
4828 }
4830 refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
4831 if (!refs)
4832 return ERR;
4834 ref = &refs[refs_size++];
4835 ref->name = malloc(namelen + 1);
4836 if (!ref->name)
4837 return ERR;
4839 strncpy(ref->name, name, namelen);
4840 ref->name[namelen] = 0;
4841 ref->tag = tag;
4842 ref->remote = remote;
4843 string_copy_rev(ref->id, id);
4845 return OK;
4846 }
4848 static int
4849 load_refs(void)
4850 {
4851 const char *cmd_env = getenv("TIG_LS_REMOTE");
4852 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
4854 return read_properties(popen(cmd, "r"), "\t", read_ref);
4855 }
4857 static int
4858 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
4859 {
4860 if (!strcmp(name, "i18n.commitencoding"))
4861 string_ncopy(opt_encoding, value, valuelen);
4863 if (!strcmp(name, "core.editor"))
4864 string_ncopy(opt_editor, value, valuelen);
4866 return OK;
4867 }
4869 static int
4870 load_repo_config(void)
4871 {
4872 return read_properties(popen(GIT_CONFIG " --list", "r"),
4873 "=", read_repo_config_option);
4874 }
4876 static int
4877 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
4878 {
4879 if (!opt_git_dir[0]) {
4880 string_ncopy(opt_git_dir, name, namelen);
4882 } else if (opt_is_inside_work_tree == -1) {
4883 /* This can be 3 different values depending on the
4884 * version of git being used. If git-rev-parse does not
4885 * understand --is-inside-work-tree it will simply echo
4886 * the option else either "true" or "false" is printed.
4887 * Default to true for the unknown case. */
4888 opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
4890 } else {
4891 string_ncopy(opt_cdup, name, namelen);
4892 }
4894 return OK;
4895 }
4897 /* XXX: The line outputted by "--show-cdup" can be empty so the option
4898 * must be the last one! */
4899 static int
4900 load_repo_info(void)
4901 {
4902 return read_properties(popen("git rev-parse --git-dir --is-inside-work-tree --show-cdup 2>/dev/null", "r"),
4903 "=", read_repo_info);
4904 }
4906 static int
4907 read_properties(FILE *pipe, const char *separators,
4908 int (*read_property)(char *, size_t, char *, size_t))
4909 {
4910 char buffer[BUFSIZ];
4911 char *name;
4912 int state = OK;
4914 if (!pipe)
4915 return ERR;
4917 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
4918 char *value;
4919 size_t namelen;
4920 size_t valuelen;
4922 name = chomp_string(name);
4923 namelen = strcspn(name, separators);
4925 if (name[namelen]) {
4926 name[namelen] = 0;
4927 value = chomp_string(name + namelen + 1);
4928 valuelen = strlen(value);
4930 } else {
4931 value = "";
4932 valuelen = 0;
4933 }
4935 state = read_property(name, namelen, value, valuelen);
4936 }
4938 if (state != ERR && ferror(pipe))
4939 state = ERR;
4941 pclose(pipe);
4943 return state;
4944 }
4947 /*
4948 * Main
4949 */
4951 static void __NORETURN
4952 quit(int sig)
4953 {
4954 /* XXX: Restore tty modes and let the OS cleanup the rest! */
4955 if (cursed)
4956 endwin();
4957 exit(0);
4958 }
4960 static void __NORETURN
4961 die(const char *err, ...)
4962 {
4963 va_list args;
4965 endwin();
4967 va_start(args, err);
4968 fputs("tig: ", stderr);
4969 vfprintf(stderr, err, args);
4970 fputs("\n", stderr);
4971 va_end(args);
4973 exit(1);
4974 }
4976 static void
4977 warn(const char *msg, ...)
4978 {
4979 va_list args;
4981 va_start(args, msg);
4982 fputs("tig warning: ", stderr);
4983 vfprintf(stderr, msg, args);
4984 fputs("\n", stderr);
4985 va_end(args);
4986 }
4988 int
4989 main(int argc, char *argv[])
4990 {
4991 struct view *view;
4992 enum request request;
4993 size_t i;
4995 signal(SIGINT, quit);
4997 if (setlocale(LC_ALL, "")) {
4998 char *codeset = nl_langinfo(CODESET);
5000 string_ncopy(opt_codeset, codeset, strlen(codeset));
5001 }
5003 if (load_repo_info() == ERR)
5004 die("Failed to load repo info.");
5006 if (load_options() == ERR)
5007 die("Failed to load user config.");
5009 /* Load the repo config file so options can be overwritten from
5010 * the command line. */
5011 if (load_repo_config() == ERR)
5012 die("Failed to load repo config.");
5014 if (!parse_options(argc, argv))
5015 return 0;
5017 /* Require a git repository unless when running in pager mode. */
5018 if (!opt_git_dir[0])
5019 die("Not a git repository");
5021 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
5022 opt_iconv = iconv_open(opt_codeset, opt_encoding);
5023 if (opt_iconv == ICONV_NONE)
5024 die("Failed to initialize character set conversion");
5025 }
5027 if (load_refs() == ERR)
5028 die("Failed to load refs.");
5030 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
5031 view->cmd_env = getenv(view->cmd_env);
5033 request = opt_request;
5035 init_display();
5037 while (view_driver(display[current_view], request)) {
5038 int key;
5039 int i;
5041 foreach_view (view, i)
5042 update_view(view);
5044 /* Refresh, accept single keystroke of input */
5045 key = wgetch(status_win);
5047 /* wgetch() with nodelay() enabled returns ERR when there's no
5048 * input. */
5049 if (key == ERR) {
5050 request = REQ_NONE;
5051 continue;
5052 }
5054 request = get_keybinding(display[current_view]->keymap, key);
5056 /* Some low-level request handling. This keeps access to
5057 * status_win restricted. */
5058 switch (request) {
5059 case REQ_PROMPT:
5060 {
5061 char *cmd = read_prompt(":");
5063 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
5064 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
5065 opt_request = REQ_VIEW_DIFF;
5066 } else {
5067 opt_request = REQ_VIEW_PAGER;
5068 }
5069 break;
5070 }
5072 request = REQ_NONE;
5073 break;
5074 }
5075 case REQ_SEARCH:
5076 case REQ_SEARCH_BACK:
5077 {
5078 const char *prompt = request == REQ_SEARCH
5079 ? "/" : "?";
5080 char *search = read_prompt(prompt);
5082 if (search)
5083 string_ncopy(opt_search, search, strlen(search));
5084 else
5085 request = REQ_NONE;
5086 break;
5087 }
5088 case REQ_SCREEN_RESIZE:
5089 {
5090 int height, width;
5092 getmaxyx(stdscr, height, width);
5094 /* Resize the status view and let the view driver take
5095 * care of resizing the displayed views. */
5096 wresize(status_win, 1, width);
5097 mvwin(status_win, height - 1, 0);
5098 wrefresh(status_win);
5099 break;
5100 }
5101 default:
5102 break;
5103 }
5104 }
5106 quit(0);
5108 return 0;
5109 }