1 /* Copyright (c) 2006 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 #ifndef VERSION
15 #define VERSION "unknown-version"
16 #endif
18 #ifndef DEBUG
19 #define NDEBUG
20 #endif
22 #include <assert.h>
23 #include <errno.h>
24 #include <ctype.h>
25 #include <signal.h>
26 #include <stdarg.h>
27 #include <stdio.h>
28 #include <stdlib.h>
29 #include <string.h>
30 #include <sys/types.h>
31 #include <sys/stat.h>
32 #include <unistd.h>
33 #include <time.h>
35 #include <regex.h>
37 #include <locale.h>
38 #include <langinfo.h>
39 #include <iconv.h>
41 #include <curses.h>
43 #if __GNUC__ >= 3
44 #define __NORETURN __attribute__((__noreturn__))
45 #else
46 #define __NORETURN
47 #endif
49 static void __NORETURN die(const char *err, ...);
50 static void report(const char *msg, ...);
51 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, size_t, char *, size_t));
52 static void set_nonblocking_input(bool loading);
53 static size_t utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed);
55 #define ABS(x) ((x) >= 0 ? (x) : -(x))
56 #define MIN(x, y) ((x) < (y) ? (x) : (y))
58 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
59 #define STRING_SIZE(x) (sizeof(x) - 1)
61 #define SIZEOF_STR 1024 /* Default string size. */
62 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
63 #define SIZEOF_REV 41 /* Holds a SHA-1 and an ending NUL */
65 /* Revision graph */
67 #define REVGRAPH_INIT 'I'
68 #define REVGRAPH_MERGE 'M'
69 #define REVGRAPH_BRANCH '+'
70 #define REVGRAPH_COMMIT '*'
71 #define REVGRAPH_LINE '|'
73 #define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
75 /* This color name can be used to refer to the default term colors. */
76 #define COLOR_DEFAULT (-1)
78 #define ICONV_NONE ((iconv_t) -1)
80 /* The format and size of the date column in the main view. */
81 #define DATE_FORMAT "%Y-%m-%d %H:%M"
82 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
84 #define AUTHOR_COLS 20
86 /* The default interval between line numbers. */
87 #define NUMBER_INTERVAL 1
89 #define TABSIZE 8
91 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
93 #define TIG_LS_REMOTE \
94 "git ls-remote $(git rev-parse --git-dir) 2>/dev/null"
96 #define TIG_DIFF_CMD \
97 "git show --root --patch-with-stat --find-copies-harder -B -C %s 2>/dev/null"
99 #define TIG_LOG_CMD \
100 "git log --cc --stat -n100 %s 2>/dev/null"
102 #define TIG_MAIN_CMD \
103 "git log --topo-order --pretty=raw %s 2>/dev/null"
105 #define TIG_TREE_CMD \
106 "git ls-tree %s %s"
108 #define TIG_BLOB_CMD \
109 "git cat-file blob %s"
111 /* XXX: Needs to be defined to the empty string. */
112 #define TIG_HELP_CMD ""
113 #define TIG_PAGER_CMD ""
114 #define TIG_STATUS_CMD ""
116 /* Some ascii-shorthands fitted into the ncurses namespace. */
117 #define KEY_TAB '\t'
118 #define KEY_RETURN '\r'
119 #define KEY_ESC 27
122 struct ref {
123 char *name; /* Ref name; tag or head names are shortened. */
124 char id[SIZEOF_REV]; /* Commit SHA1 ID */
125 unsigned int tag:1; /* Is it a tag? */
126 unsigned int remote:1; /* Is it a remote ref? */
127 unsigned int next:1; /* For ref lists: are there more refs? */
128 };
130 static struct ref **get_refs(char *id);
132 struct int_map {
133 const char *name;
134 int namelen;
135 int value;
136 };
138 static int
139 set_from_int_map(struct int_map *map, size_t map_size,
140 int *value, const char *name, int namelen)
141 {
143 int i;
145 for (i = 0; i < map_size; i++)
146 if (namelen == map[i].namelen &&
147 !strncasecmp(name, map[i].name, namelen)) {
148 *value = map[i].value;
149 return OK;
150 }
152 return ERR;
153 }
156 /*
157 * String helpers
158 */
160 static inline void
161 string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
162 {
163 if (srclen > dstlen - 1)
164 srclen = dstlen - 1;
166 strncpy(dst, src, srclen);
167 dst[srclen] = 0;
168 }
170 /* Shorthands for safely copying into a fixed buffer. */
172 #define string_copy(dst, src) \
173 string_ncopy_do(dst, sizeof(dst), src, sizeof(src))
175 #define string_ncopy(dst, src, srclen) \
176 string_ncopy_do(dst, sizeof(dst), src, srclen)
178 #define string_copy_rev(dst, src) \
179 string_ncopy_do(dst, SIZEOF_REV, src, SIZEOF_REV - 1)
181 #define string_add(dst, from, src) \
182 string_ncopy_do(dst + (from), sizeof(dst) - (from), src, sizeof(src))
184 static char *
185 chomp_string(char *name)
186 {
187 int namelen;
189 while (isspace(*name))
190 name++;
192 namelen = strlen(name) - 1;
193 while (namelen > 0 && isspace(name[namelen]))
194 name[namelen--] = 0;
196 return name;
197 }
199 static bool
200 string_nformat(char *buf, size_t bufsize, size_t *bufpos, const char *fmt, ...)
201 {
202 va_list args;
203 size_t pos = bufpos ? *bufpos : 0;
205 va_start(args, fmt);
206 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
207 va_end(args);
209 if (bufpos)
210 *bufpos = pos;
212 return pos >= bufsize ? FALSE : TRUE;
213 }
215 #define string_format(buf, fmt, args...) \
216 string_nformat(buf, sizeof(buf), NULL, fmt, args)
218 #define string_format_from(buf, from, fmt, args...) \
219 string_nformat(buf, sizeof(buf), from, fmt, args)
221 static int
222 string_enum_compare(const char *str1, const char *str2, int len)
223 {
224 size_t i;
226 #define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
228 /* Diff-Header == DIFF_HEADER */
229 for (i = 0; i < len; i++) {
230 if (toupper(str1[i]) == toupper(str2[i]))
231 continue;
233 if (string_enum_sep(str1[i]) &&
234 string_enum_sep(str2[i]))
235 continue;
237 return str1[i] - str2[i];
238 }
240 return 0;
241 }
243 /* Shell quoting
244 *
245 * NOTE: The following is a slightly modified copy of the git project's shell
246 * quoting routines found in the quote.c file.
247 *
248 * Help to copy the thing properly quoted for the shell safety. any single
249 * quote is replaced with '\'', any exclamation point is replaced with '\!',
250 * and the whole thing is enclosed in a
251 *
252 * E.g.
253 * original sq_quote result
254 * name ==> name ==> 'name'
255 * a b ==> a b ==> 'a b'
256 * a'b ==> a'\''b ==> 'a'\''b'
257 * a!b ==> a'\!'b ==> 'a'\!'b'
258 */
260 static size_t
261 sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
262 {
263 char c;
265 #define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0)
267 BUFPUT('\'');
268 while ((c = *src++)) {
269 if (c == '\'' || c == '!') {
270 BUFPUT('\'');
271 BUFPUT('\\');
272 BUFPUT(c);
273 BUFPUT('\'');
274 } else {
275 BUFPUT(c);
276 }
277 }
278 BUFPUT('\'');
280 if (bufsize < SIZEOF_STR)
281 buf[bufsize] = 0;
283 return bufsize;
284 }
287 /*
288 * User requests
289 */
291 #define REQ_INFO \
292 /* XXX: Keep the view request first and in sync with views[]. */ \
293 REQ_GROUP("View switching") \
294 REQ_(VIEW_MAIN, "Show main view"), \
295 REQ_(VIEW_DIFF, "Show diff view"), \
296 REQ_(VIEW_LOG, "Show log view"), \
297 REQ_(VIEW_TREE, "Show tree view"), \
298 REQ_(VIEW_BLOB, "Show blob view"), \
299 REQ_(VIEW_HELP, "Show help page"), \
300 REQ_(VIEW_PAGER, "Show pager view"), \
301 REQ_(VIEW_STATUS, "Show status view"), \
302 \
303 REQ_GROUP("View manipulation") \
304 REQ_(ENTER, "Enter current line and scroll"), \
305 REQ_(NEXT, "Move to next"), \
306 REQ_(PREVIOUS, "Move to previous"), \
307 REQ_(VIEW_NEXT, "Move focus to next view"), \
308 REQ_(VIEW_CLOSE, "Close the current view"), \
309 REQ_(QUIT, "Close all views and quit"), \
310 \
311 REQ_GROUP("Cursor navigation") \
312 REQ_(MOVE_UP, "Move cursor one line up"), \
313 REQ_(MOVE_DOWN, "Move cursor one line down"), \
314 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
315 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
316 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
317 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
318 \
319 REQ_GROUP("Scrolling") \
320 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
321 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
322 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
323 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
324 \
325 REQ_GROUP("Searching") \
326 REQ_(SEARCH, "Search the view"), \
327 REQ_(SEARCH_BACK, "Search backwards in the view"), \
328 REQ_(FIND_NEXT, "Find next search match"), \
329 REQ_(FIND_PREV, "Find previous search match"), \
330 \
331 REQ_GROUP("Misc") \
332 REQ_(NONE, "Do nothing"), \
333 REQ_(PROMPT, "Bring up the prompt"), \
334 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
335 REQ_(SCREEN_RESIZE, "Resize the screen"), \
336 REQ_(SHOW_VERSION, "Show version information"), \
337 REQ_(STOP_LOADING, "Stop all loading views"), \
338 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
339 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization")
342 /* User action requests. */
343 enum request {
344 #define REQ_GROUP(help)
345 #define REQ_(req, help) REQ_##req
347 /* Offset all requests to avoid conflicts with ncurses getch values. */
348 REQ_OFFSET = KEY_MAX + 1,
349 REQ_INFO,
350 REQ_UNKNOWN,
352 #undef REQ_GROUP
353 #undef REQ_
354 };
356 struct request_info {
357 enum request request;
358 char *name;
359 int namelen;
360 char *help;
361 };
363 static struct request_info req_info[] = {
364 #define REQ_GROUP(help) { 0, NULL, 0, (help) },
365 #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
366 REQ_INFO
367 #undef REQ_GROUP
368 #undef REQ_
369 };
371 static enum request
372 get_request(const char *name)
373 {
374 int namelen = strlen(name);
375 int i;
377 for (i = 0; i < ARRAY_SIZE(req_info); i++)
378 if (req_info[i].namelen == namelen &&
379 !string_enum_compare(req_info[i].name, name, namelen))
380 return req_info[i].request;
382 return REQ_UNKNOWN;
383 }
386 /*
387 * Options
388 */
390 static const char usage[] =
391 "tig " VERSION " (" __DATE__ ")\n"
392 "\n"
393 "Usage: tig [options]\n"
394 " or: tig [options] [--] [git log options]\n"
395 " or: tig [options] log [git log options]\n"
396 " or: tig [options] diff [git diff options]\n"
397 " or: tig [options] show [git show options]\n"
398 " or: tig [options] < [git command output]\n"
399 "\n"
400 "Options:\n"
401 " -l Start up in log view\n"
402 " -d Start up in diff view\n"
403 " -S Start up in status view\n"
404 " -n[I], --line-number[=I] Show line numbers with given interval\n"
405 " -b[N], --tab-size[=N] Set number of spaces for tab expansion\n"
406 " -- Mark end of tig options\n"
407 " -v, --version Show version and exit\n"
408 " -h, --help Show help message and exit\n";
410 /* Option and state variables. */
411 static bool opt_line_number = FALSE;
412 static bool opt_rev_graph = FALSE;
413 static int opt_num_interval = NUMBER_INTERVAL;
414 static int opt_tab_size = TABSIZE;
415 static enum request opt_request = REQ_VIEW_MAIN;
416 static char opt_cmd[SIZEOF_STR] = "";
417 static char opt_path[SIZEOF_STR] = "";
418 static FILE *opt_pipe = NULL;
419 static char opt_encoding[20] = "UTF-8";
420 static bool opt_utf8 = TRUE;
421 static char opt_codeset[20] = "UTF-8";
422 static iconv_t opt_iconv = ICONV_NONE;
423 static char opt_search[SIZEOF_STR] = "";
424 static char opt_cdup[SIZEOF_STR] = "";
425 static char opt_git_dir[SIZEOF_STR] = "";
427 enum option_type {
428 OPT_NONE,
429 OPT_INT,
430 };
432 static bool
433 check_option(char *opt, char short_name, char *name, enum option_type type, ...)
434 {
435 va_list args;
436 char *value = "";
437 int *number;
439 if (opt[0] != '-')
440 return FALSE;
442 if (opt[1] == '-') {
443 int namelen = strlen(name);
445 opt += 2;
447 if (strncmp(opt, name, namelen))
448 return FALSE;
450 if (opt[namelen] == '=')
451 value = opt + namelen + 1;
453 } else {
454 if (!short_name || opt[1] != short_name)
455 return FALSE;
456 value = opt + 2;
457 }
459 va_start(args, type);
460 if (type == OPT_INT) {
461 number = va_arg(args, int *);
462 if (isdigit(*value))
463 *number = atoi(value);
464 }
465 va_end(args);
467 return TRUE;
468 }
470 /* Returns the index of log or diff command or -1 to exit. */
471 static bool
472 parse_options(int argc, char *argv[])
473 {
474 int i;
476 for (i = 1; i < argc; i++) {
477 char *opt = argv[i];
479 if (!strcmp(opt, "log") ||
480 !strcmp(opt, "diff") ||
481 !strcmp(opt, "show")) {
482 opt_request = opt[0] == 'l'
483 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
484 break;
485 }
487 if (opt[0] && opt[0] != '-')
488 break;
490 if (!strcmp(opt, "-l")) {
491 opt_request = REQ_VIEW_LOG;
492 continue;
493 }
495 if (!strcmp(opt, "-d")) {
496 opt_request = REQ_VIEW_DIFF;
497 continue;
498 }
500 if (!strcmp(opt, "-S")) {
501 opt_request = REQ_VIEW_STATUS;
502 break;
503 }
505 if (check_option(opt, 'n', "line-number", OPT_INT, &opt_num_interval)) {
506 opt_line_number = TRUE;
507 continue;
508 }
510 if (check_option(opt, 'b', "tab-size", OPT_INT, &opt_tab_size)) {
511 opt_tab_size = MIN(opt_tab_size, TABSIZE);
512 continue;
513 }
515 if (check_option(opt, 'v', "version", OPT_NONE)) {
516 printf("tig version %s\n", VERSION);
517 return FALSE;
518 }
520 if (check_option(opt, 'h', "help", OPT_NONE)) {
521 printf(usage);
522 return FALSE;
523 }
525 if (!strcmp(opt, "--")) {
526 i++;
527 break;
528 }
530 die("unknown option '%s'\n\n%s", opt, usage);
531 }
533 if (!isatty(STDIN_FILENO)) {
534 opt_request = REQ_VIEW_PAGER;
535 opt_pipe = stdin;
537 } else if (i < argc) {
538 size_t buf_size;
540 if (opt_request == REQ_VIEW_MAIN)
541 /* XXX: This is vulnerable to the user overriding
542 * options required for the main view parser. */
543 string_copy(opt_cmd, "git log --pretty=raw");
544 else
545 string_copy(opt_cmd, "git");
546 buf_size = strlen(opt_cmd);
548 while (buf_size < sizeof(opt_cmd) && i < argc) {
549 opt_cmd[buf_size++] = ' ';
550 buf_size = sq_quote(opt_cmd, buf_size, argv[i++]);
551 }
553 if (buf_size >= sizeof(opt_cmd))
554 die("command too long");
556 opt_cmd[buf_size] = 0;
557 }
559 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
560 opt_utf8 = FALSE;
562 return TRUE;
563 }
566 /*
567 * Line-oriented content detection.
568 */
570 #define LINE_INFO \
571 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
572 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
573 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
574 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
575 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
576 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
577 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
578 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
579 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
580 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
581 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
582 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
583 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
584 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
585 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
586 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
587 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
588 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
589 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
590 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
591 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
592 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
593 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
594 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
595 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
596 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
597 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
598 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
599 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
600 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
601 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
602 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
603 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
604 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
605 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
606 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
607 LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
608 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
609 LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
610 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
611 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
612 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
613 LINE(STAT_SECTION, "", COLOR_DEFAULT, COLOR_BLUE, A_BOLD), \
614 LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
615 LINE(STAT_STAGED, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
616 LINE(STAT_UNSTAGED,"", COLOR_YELLOW, COLOR_DEFAULT, 0), \
617 LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0)
619 enum line_type {
620 #define LINE(type, line, fg, bg, attr) \
621 LINE_##type
622 LINE_INFO
623 #undef LINE
624 };
626 struct line_info {
627 const char *name; /* Option name. */
628 int namelen; /* Size of option name. */
629 const char *line; /* The start of line to match. */
630 int linelen; /* Size of string to match. */
631 int fg, bg, attr; /* Color and text attributes for the lines. */
632 };
634 static struct line_info line_info[] = {
635 #define LINE(type, line, fg, bg, attr) \
636 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
637 LINE_INFO
638 #undef LINE
639 };
641 static enum line_type
642 get_line_type(char *line)
643 {
644 int linelen = strlen(line);
645 enum line_type type;
647 for (type = 0; type < ARRAY_SIZE(line_info); type++)
648 /* Case insensitive search matches Signed-off-by lines better. */
649 if (linelen >= line_info[type].linelen &&
650 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
651 return type;
653 return LINE_DEFAULT;
654 }
656 static inline int
657 get_line_attr(enum line_type type)
658 {
659 assert(type < ARRAY_SIZE(line_info));
660 return COLOR_PAIR(type) | line_info[type].attr;
661 }
663 static struct line_info *
664 get_line_info(char *name, int namelen)
665 {
666 enum line_type type;
668 for (type = 0; type < ARRAY_SIZE(line_info); type++)
669 if (namelen == line_info[type].namelen &&
670 !string_enum_compare(line_info[type].name, name, namelen))
671 return &line_info[type];
673 return NULL;
674 }
676 static void
677 init_colors(void)
678 {
679 int default_bg = COLOR_BLACK;
680 int default_fg = COLOR_WHITE;
681 enum line_type type;
683 start_color();
685 if (use_default_colors() != ERR) {
686 default_bg = -1;
687 default_fg = -1;
688 }
690 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
691 struct line_info *info = &line_info[type];
692 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
693 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
695 init_pair(type, fg, bg);
696 }
697 }
699 struct line {
700 enum line_type type;
702 /* State flags */
703 unsigned int selected:1;
705 void *data; /* User data */
706 };
709 /*
710 * Keys
711 */
713 struct keybinding {
714 int alias;
715 enum request request;
716 struct keybinding *next;
717 };
719 static struct keybinding default_keybindings[] = {
720 /* View switching */
721 { 'm', REQ_VIEW_MAIN },
722 { 'd', REQ_VIEW_DIFF },
723 { 'l', REQ_VIEW_LOG },
724 { 't', REQ_VIEW_TREE },
725 { 'f', REQ_VIEW_BLOB },
726 { 'p', REQ_VIEW_PAGER },
727 { 'h', REQ_VIEW_HELP },
728 { 'S', REQ_VIEW_STATUS },
730 /* View manipulation */
731 { 'q', REQ_VIEW_CLOSE },
732 { KEY_TAB, REQ_VIEW_NEXT },
733 { KEY_RETURN, REQ_ENTER },
734 { KEY_UP, REQ_PREVIOUS },
735 { KEY_DOWN, REQ_NEXT },
737 /* Cursor navigation */
738 { 'k', REQ_MOVE_UP },
739 { 'j', REQ_MOVE_DOWN },
740 { KEY_HOME, REQ_MOVE_FIRST_LINE },
741 { KEY_END, REQ_MOVE_LAST_LINE },
742 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
743 { ' ', REQ_MOVE_PAGE_DOWN },
744 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
745 { 'b', REQ_MOVE_PAGE_UP },
746 { '-', REQ_MOVE_PAGE_UP },
748 /* Scrolling */
749 { KEY_IC, REQ_SCROLL_LINE_UP },
750 { KEY_DC, REQ_SCROLL_LINE_DOWN },
751 { 'w', REQ_SCROLL_PAGE_UP },
752 { 's', REQ_SCROLL_PAGE_DOWN },
754 /* Searching */
755 { '/', REQ_SEARCH },
756 { '?', REQ_SEARCH_BACK },
757 { 'n', REQ_FIND_NEXT },
758 { 'N', REQ_FIND_PREV },
760 /* Misc */
761 { 'Q', REQ_QUIT },
762 { 'z', REQ_STOP_LOADING },
763 { 'v', REQ_SHOW_VERSION },
764 { 'r', REQ_SCREEN_REDRAW },
765 { '.', REQ_TOGGLE_LINENO },
766 { 'g', REQ_TOGGLE_REV_GRAPH },
767 { ':', REQ_PROMPT },
769 /* Using the ncurses SIGWINCH handler. */
770 { KEY_RESIZE, REQ_SCREEN_RESIZE },
771 };
773 #define KEYMAP_INFO \
774 KEYMAP_(GENERIC), \
775 KEYMAP_(MAIN), \
776 KEYMAP_(DIFF), \
777 KEYMAP_(LOG), \
778 KEYMAP_(TREE), \
779 KEYMAP_(BLOB), \
780 KEYMAP_(PAGER), \
781 KEYMAP_(HELP), \
782 KEYMAP_(STATUS)
784 enum keymap {
785 #define KEYMAP_(name) KEYMAP_##name
786 KEYMAP_INFO
787 #undef KEYMAP_
788 };
790 static struct int_map keymap_table[] = {
791 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
792 KEYMAP_INFO
793 #undef KEYMAP_
794 };
796 #define set_keymap(map, name) \
797 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
799 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
801 static void
802 add_keybinding(enum keymap keymap, enum request request, int key)
803 {
804 struct keybinding *keybinding;
806 keybinding = calloc(1, sizeof(*keybinding));
807 if (!keybinding)
808 die("Failed to allocate keybinding");
810 keybinding->alias = key;
811 keybinding->request = request;
812 keybinding->next = keybindings[keymap];
813 keybindings[keymap] = keybinding;
814 }
816 /* Looks for a key binding first in the given map, then in the generic map, and
817 * lastly in the default keybindings. */
818 static enum request
819 get_keybinding(enum keymap keymap, int key)
820 {
821 struct keybinding *kbd;
822 int i;
824 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
825 if (kbd->alias == key)
826 return kbd->request;
828 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
829 if (kbd->alias == key)
830 return kbd->request;
832 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
833 if (default_keybindings[i].alias == key)
834 return default_keybindings[i].request;
836 return (enum request) key;
837 }
840 struct key {
841 char *name;
842 int value;
843 };
845 static struct key key_table[] = {
846 { "Enter", KEY_RETURN },
847 { "Space", ' ' },
848 { "Backspace", KEY_BACKSPACE },
849 { "Tab", KEY_TAB },
850 { "Escape", KEY_ESC },
851 { "Left", KEY_LEFT },
852 { "Right", KEY_RIGHT },
853 { "Up", KEY_UP },
854 { "Down", KEY_DOWN },
855 { "Insert", KEY_IC },
856 { "Delete", KEY_DC },
857 { "Hash", '#' },
858 { "Home", KEY_HOME },
859 { "End", KEY_END },
860 { "PageUp", KEY_PPAGE },
861 { "PageDown", KEY_NPAGE },
862 { "F1", KEY_F(1) },
863 { "F2", KEY_F(2) },
864 { "F3", KEY_F(3) },
865 { "F4", KEY_F(4) },
866 { "F5", KEY_F(5) },
867 { "F6", KEY_F(6) },
868 { "F7", KEY_F(7) },
869 { "F8", KEY_F(8) },
870 { "F9", KEY_F(9) },
871 { "F10", KEY_F(10) },
872 { "F11", KEY_F(11) },
873 { "F12", KEY_F(12) },
874 };
876 static int
877 get_key_value(const char *name)
878 {
879 int i;
881 for (i = 0; i < ARRAY_SIZE(key_table); i++)
882 if (!strcasecmp(key_table[i].name, name))
883 return key_table[i].value;
885 if (strlen(name) == 1 && isprint(*name))
886 return (int) *name;
888 return ERR;
889 }
891 static char *
892 get_key(enum request request)
893 {
894 static char buf[BUFSIZ];
895 static char key_char[] = "'X'";
896 size_t pos = 0;
897 char *sep = "";
898 int i;
900 buf[pos] = 0;
902 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
903 struct keybinding *keybinding = &default_keybindings[i];
904 char *seq = NULL;
905 int key;
907 if (keybinding->request != request)
908 continue;
910 for (key = 0; key < ARRAY_SIZE(key_table); key++)
911 if (key_table[key].value == keybinding->alias)
912 seq = key_table[key].name;
914 if (seq == NULL &&
915 keybinding->alias < 127 &&
916 isprint(keybinding->alias)) {
917 key_char[1] = (char) keybinding->alias;
918 seq = key_char;
919 }
921 if (!seq)
922 seq = "'?'";
924 if (!string_format_from(buf, &pos, "%s%s", sep, seq))
925 return "Too many keybindings!";
926 sep = ", ";
927 }
929 return buf;
930 }
933 /*
934 * User config file handling.
935 */
937 static struct int_map color_map[] = {
938 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
939 COLOR_MAP(DEFAULT),
940 COLOR_MAP(BLACK),
941 COLOR_MAP(BLUE),
942 COLOR_MAP(CYAN),
943 COLOR_MAP(GREEN),
944 COLOR_MAP(MAGENTA),
945 COLOR_MAP(RED),
946 COLOR_MAP(WHITE),
947 COLOR_MAP(YELLOW),
948 };
950 #define set_color(color, name) \
951 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
953 static struct int_map attr_map[] = {
954 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
955 ATTR_MAP(NORMAL),
956 ATTR_MAP(BLINK),
957 ATTR_MAP(BOLD),
958 ATTR_MAP(DIM),
959 ATTR_MAP(REVERSE),
960 ATTR_MAP(STANDOUT),
961 ATTR_MAP(UNDERLINE),
962 };
964 #define set_attribute(attr, name) \
965 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
967 static int config_lineno;
968 static bool config_errors;
969 static char *config_msg;
971 /* Wants: object fgcolor bgcolor [attr] */
972 static int
973 option_color_command(int argc, char *argv[])
974 {
975 struct line_info *info;
977 if (argc != 3 && argc != 4) {
978 config_msg = "Wrong number of arguments given to color command";
979 return ERR;
980 }
982 info = get_line_info(argv[0], strlen(argv[0]));
983 if (!info) {
984 config_msg = "Unknown color name";
985 return ERR;
986 }
988 if (set_color(&info->fg, argv[1]) == ERR ||
989 set_color(&info->bg, argv[2]) == ERR) {
990 config_msg = "Unknown color";
991 return ERR;
992 }
994 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
995 config_msg = "Unknown attribute";
996 return ERR;
997 }
999 return OK;
1000 }
1002 /* Wants: name = value */
1003 static int
1004 option_set_command(int argc, char *argv[])
1005 {
1006 if (argc != 3) {
1007 config_msg = "Wrong number of arguments given to set command";
1008 return ERR;
1009 }
1011 if (strcmp(argv[1], "=")) {
1012 config_msg = "No value assigned";
1013 return ERR;
1014 }
1016 if (!strcmp(argv[0], "show-rev-graph")) {
1017 opt_rev_graph = (!strcmp(argv[2], "1") ||
1018 !strcmp(argv[2], "true") ||
1019 !strcmp(argv[2], "yes"));
1020 return OK;
1021 }
1023 if (!strcmp(argv[0], "line-number-interval")) {
1024 opt_num_interval = atoi(argv[2]);
1025 return OK;
1026 }
1028 if (!strcmp(argv[0], "tab-size")) {
1029 opt_tab_size = atoi(argv[2]);
1030 return OK;
1031 }
1033 if (!strcmp(argv[0], "commit-encoding")) {
1034 char *arg = argv[2];
1035 int delimiter = *arg;
1036 int i;
1038 switch (delimiter) {
1039 case '"':
1040 case '\'':
1041 for (arg++, i = 0; arg[i]; i++)
1042 if (arg[i] == delimiter) {
1043 arg[i] = 0;
1044 break;
1045 }
1046 default:
1047 string_ncopy(opt_encoding, arg, strlen(arg));
1048 return OK;
1049 }
1050 }
1052 config_msg = "Unknown variable name";
1053 return ERR;
1054 }
1056 /* Wants: mode request key */
1057 static int
1058 option_bind_command(int argc, char *argv[])
1059 {
1060 enum request request;
1061 int keymap;
1062 int key;
1064 if (argc != 3) {
1065 config_msg = "Wrong number of arguments given to bind command";
1066 return ERR;
1067 }
1069 if (set_keymap(&keymap, argv[0]) == ERR) {
1070 config_msg = "Unknown key map";
1071 return ERR;
1072 }
1074 key = get_key_value(argv[1]);
1075 if (key == ERR) {
1076 config_msg = "Unknown key";
1077 return ERR;
1078 }
1080 request = get_request(argv[2]);
1081 if (request == REQ_UNKNOWN) {
1082 config_msg = "Unknown request name";
1083 return ERR;
1084 }
1086 add_keybinding(keymap, request, key);
1088 return OK;
1089 }
1091 static int
1092 set_option(char *opt, char *value)
1093 {
1094 char *argv[16];
1095 int valuelen;
1096 int argc = 0;
1098 /* Tokenize */
1099 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1100 argv[argc++] = value;
1102 value += valuelen;
1103 if (!*value)
1104 break;
1106 *value++ = 0;
1107 while (isspace(*value))
1108 value++;
1109 }
1111 if (!strcmp(opt, "color"))
1112 return option_color_command(argc, argv);
1114 if (!strcmp(opt, "set"))
1115 return option_set_command(argc, argv);
1117 if (!strcmp(opt, "bind"))
1118 return option_bind_command(argc, argv);
1120 config_msg = "Unknown option command";
1121 return ERR;
1122 }
1124 static int
1125 read_option(char *opt, size_t optlen, char *value, size_t valuelen)
1126 {
1127 int status = OK;
1129 config_lineno++;
1130 config_msg = "Internal error";
1132 /* Check for comment markers, since read_properties() will
1133 * only ensure opt and value are split at first " \t". */
1134 optlen = strcspn(opt, "#");
1135 if (optlen == 0)
1136 return OK;
1138 if (opt[optlen] != 0) {
1139 config_msg = "No option value";
1140 status = ERR;
1142 } else {
1143 /* Look for comment endings in the value. */
1144 size_t len = strcspn(value, "#");
1146 if (len < valuelen) {
1147 valuelen = len;
1148 value[valuelen] = 0;
1149 }
1151 status = set_option(opt, value);
1152 }
1154 if (status == ERR) {
1155 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1156 config_lineno, (int) optlen, opt, config_msg);
1157 config_errors = TRUE;
1158 }
1160 /* Always keep going if errors are encountered. */
1161 return OK;
1162 }
1164 static int
1165 load_options(void)
1166 {
1167 char *home = getenv("HOME");
1168 char buf[SIZEOF_STR];
1169 FILE *file;
1171 config_lineno = 0;
1172 config_errors = FALSE;
1174 if (!home || !string_format(buf, "%s/.tigrc", home))
1175 return ERR;
1177 /* It's ok that the file doesn't exist. */
1178 file = fopen(buf, "r");
1179 if (!file)
1180 return OK;
1182 if (read_properties(file, " \t", read_option) == ERR ||
1183 config_errors == TRUE)
1184 fprintf(stderr, "Errors while loading %s.\n", buf);
1186 return OK;
1187 }
1190 /*
1191 * The viewer
1192 */
1194 struct view;
1195 struct view_ops;
1197 /* The display array of active views and the index of the current view. */
1198 static struct view *display[2];
1199 static unsigned int current_view;
1201 /* Reading from the prompt? */
1202 static bool input_mode = FALSE;
1204 #define foreach_displayed_view(view, i) \
1205 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1207 #define displayed_views() (display[1] != NULL ? 2 : 1)
1209 /* Current head and commit ID */
1210 static char ref_blob[SIZEOF_REF] = "";
1211 static char ref_commit[SIZEOF_REF] = "HEAD";
1212 static char ref_head[SIZEOF_REF] = "HEAD";
1214 struct view {
1215 const char *name; /* View name */
1216 const char *cmd_fmt; /* Default command line format */
1217 const char *cmd_env; /* Command line set via environment */
1218 const char *id; /* Points to either of ref_{head,commit,blob} */
1220 struct view_ops *ops; /* View operations */
1222 enum keymap keymap; /* What keymap does this view have */
1224 char cmd[SIZEOF_STR]; /* Command buffer */
1225 char ref[SIZEOF_REF]; /* Hovered commit reference */
1226 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1228 int height, width; /* The width and height of the main window */
1229 WINDOW *win; /* The main window */
1230 WINDOW *title; /* The title window living below the main window */
1232 /* Navigation */
1233 unsigned long offset; /* Offset of the window top */
1234 unsigned long lineno; /* Current line number */
1236 /* Searching */
1237 char grep[SIZEOF_STR]; /* Search string */
1238 regex_t *regex; /* Pre-compiled regex */
1240 /* If non-NULL, points to the view that opened this view. If this view
1241 * is closed tig will switch back to the parent view. */
1242 struct view *parent;
1244 /* Buffering */
1245 unsigned long lines; /* Total number of lines */
1246 struct line *line; /* Line index */
1247 unsigned long line_size;/* Total number of allocated lines */
1248 unsigned int digits; /* Number of digits in the lines member. */
1250 /* Loading */
1251 FILE *pipe;
1252 time_t start_time;
1253 };
1255 struct view_ops {
1256 /* What type of content being displayed. Used in the title bar. */
1257 const char *type;
1258 /* Open and reads in all view content. */
1259 bool (*open)(struct view *view);
1260 /* Read one line; updates view->line. */
1261 bool (*read)(struct view *view, char *data);
1262 /* Draw one line; @lineno must be < view->height. */
1263 bool (*draw)(struct view *view, struct line *line, unsigned int lineno, bool selected);
1264 /* Depending on view, change display based on current line. */
1265 bool (*enter)(struct view *view, struct line *line);
1266 /* Search for regex in a line. */
1267 bool (*grep)(struct view *view, struct line *line);
1268 /* Select line */
1269 void (*select)(struct view *view, struct line *line);
1270 };
1272 static struct view_ops pager_ops;
1273 static struct view_ops main_ops;
1274 static struct view_ops tree_ops;
1275 static struct view_ops blob_ops;
1276 static struct view_ops help_ops;
1277 static struct view_ops status_ops;
1279 #define VIEW_STR(name, cmd, env, ref, ops, map) \
1280 { name, cmd, #env, ref, ops, map}
1282 #define VIEW_(id, name, ops, ref) \
1283 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id)
1286 static struct view views[] = {
1287 VIEW_(MAIN, "main", &main_ops, ref_head),
1288 VIEW_(DIFF, "diff", &pager_ops, ref_commit),
1289 VIEW_(LOG, "log", &pager_ops, ref_head),
1290 VIEW_(TREE, "tree", &tree_ops, ref_commit),
1291 VIEW_(BLOB, "blob", &blob_ops, ref_blob),
1292 VIEW_(HELP, "help", &help_ops, ""),
1293 VIEW_(PAGER, "pager", &pager_ops, "stdin"),
1294 VIEW_(STATUS, "status", &status_ops, ""),
1295 };
1297 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1299 #define foreach_view(view, i) \
1300 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1302 #define view_is_displayed(view) \
1303 (view == display[0] || view == display[1])
1305 static bool
1306 draw_view_line(struct view *view, unsigned int lineno)
1307 {
1308 struct line *line;
1309 bool selected = (view->offset + lineno == view->lineno);
1310 bool draw_ok;
1312 assert(view_is_displayed(view));
1314 if (view->offset + lineno >= view->lines)
1315 return FALSE;
1317 line = &view->line[view->offset + lineno];
1319 if (selected) {
1320 line->selected = TRUE;
1321 view->ops->select(view, line);
1322 } else if (line->selected) {
1323 line->selected = FALSE;
1324 wmove(view->win, lineno, 0);
1325 wclrtoeol(view->win);
1326 }
1328 scrollok(view->win, FALSE);
1329 draw_ok = view->ops->draw(view, line, lineno, selected);
1330 scrollok(view->win, TRUE);
1332 return draw_ok;
1333 }
1335 static void
1336 redraw_view_from(struct view *view, int lineno)
1337 {
1338 assert(0 <= lineno && lineno < view->height);
1340 for (; lineno < view->height; lineno++) {
1341 if (!draw_view_line(view, lineno))
1342 break;
1343 }
1345 redrawwin(view->win);
1346 if (input_mode)
1347 wnoutrefresh(view->win);
1348 else
1349 wrefresh(view->win);
1350 }
1352 static void
1353 redraw_view(struct view *view)
1354 {
1355 wclear(view->win);
1356 redraw_view_from(view, 0);
1357 }
1360 static void
1361 update_view_title(struct view *view)
1362 {
1363 char buf[SIZEOF_STR];
1364 char state[SIZEOF_STR];
1365 size_t bufpos = 0, statelen = 0;
1367 assert(view_is_displayed(view));
1369 if (view->lines || view->pipe) {
1370 unsigned int view_lines = view->offset + view->height;
1371 unsigned int lines = view->lines
1372 ? MIN(view_lines, view->lines) * 100 / view->lines
1373 : 0;
1375 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1376 view->ops->type,
1377 view->lineno + 1,
1378 view->lines,
1379 lines);
1381 if (view->pipe) {
1382 time_t secs = time(NULL) - view->start_time;
1384 /* Three git seconds are a long time ... */
1385 if (secs > 2)
1386 string_format_from(state, &statelen, " %lds", secs);
1387 }
1388 }
1390 string_format_from(buf, &bufpos, "[%s]", view->name);
1391 if (*view->ref && bufpos < view->width) {
1392 size_t refsize = strlen(view->ref);
1393 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1395 if (minsize < view->width)
1396 refsize = view->width - minsize + 7;
1397 string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
1398 }
1400 if (statelen && bufpos < view->width) {
1401 string_format_from(buf, &bufpos, " %s", state);
1402 }
1404 if (view == display[current_view])
1405 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1406 else
1407 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1409 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1410 wclrtoeol(view->title);
1411 wmove(view->title, 0, view->width - 1);
1413 if (input_mode)
1414 wnoutrefresh(view->title);
1415 else
1416 wrefresh(view->title);
1417 }
1419 static void
1420 resize_display(void)
1421 {
1422 int offset, i;
1423 struct view *base = display[0];
1424 struct view *view = display[1] ? display[1] : display[0];
1426 /* Setup window dimensions */
1428 getmaxyx(stdscr, base->height, base->width);
1430 /* Make room for the status window. */
1431 base->height -= 1;
1433 if (view != base) {
1434 /* Horizontal split. */
1435 view->width = base->width;
1436 view->height = SCALE_SPLIT_VIEW(base->height);
1437 base->height -= view->height;
1439 /* Make room for the title bar. */
1440 view->height -= 1;
1441 }
1443 /* Make room for the title bar. */
1444 base->height -= 1;
1446 offset = 0;
1448 foreach_displayed_view (view, i) {
1449 if (!view->win) {
1450 view->win = newwin(view->height, 0, offset, 0);
1451 if (!view->win)
1452 die("Failed to create %s view", view->name);
1454 scrollok(view->win, TRUE);
1456 view->title = newwin(1, 0, offset + view->height, 0);
1457 if (!view->title)
1458 die("Failed to create title window");
1460 } else {
1461 wresize(view->win, view->height, view->width);
1462 mvwin(view->win, offset, 0);
1463 mvwin(view->title, offset + view->height, 0);
1464 }
1466 offset += view->height + 1;
1467 }
1468 }
1470 static void
1471 redraw_display(void)
1472 {
1473 struct view *view;
1474 int i;
1476 foreach_displayed_view (view, i) {
1477 redraw_view(view);
1478 update_view_title(view);
1479 }
1480 }
1482 static void
1483 update_display_cursor(struct view *view)
1484 {
1485 /* Move the cursor to the right-most column of the cursor line.
1486 *
1487 * XXX: This could turn out to be a bit expensive, but it ensures that
1488 * the cursor does not jump around. */
1489 if (view->lines) {
1490 wmove(view->win, view->lineno - view->offset, view->width - 1);
1491 wrefresh(view->win);
1492 }
1493 }
1495 /*
1496 * Navigation
1497 */
1499 /* Scrolling backend */
1500 static void
1501 do_scroll_view(struct view *view, int lines)
1502 {
1503 bool redraw_current_line = FALSE;
1505 /* The rendering expects the new offset. */
1506 view->offset += lines;
1508 assert(0 <= view->offset && view->offset < view->lines);
1509 assert(lines);
1511 /* Move current line into the view. */
1512 if (view->lineno < view->offset) {
1513 view->lineno = view->offset;
1514 redraw_current_line = TRUE;
1515 } else if (view->lineno >= view->offset + view->height) {
1516 view->lineno = view->offset + view->height - 1;
1517 redraw_current_line = TRUE;
1518 }
1520 assert(view->offset <= view->lineno && view->lineno < view->lines);
1522 /* Redraw the whole screen if scrolling is pointless. */
1523 if (view->height < ABS(lines)) {
1524 redraw_view(view);
1526 } else {
1527 int line = lines > 0 ? view->height - lines : 0;
1528 int end = line + ABS(lines);
1530 wscrl(view->win, lines);
1532 for (; line < end; line++) {
1533 if (!draw_view_line(view, line))
1534 break;
1535 }
1537 if (redraw_current_line)
1538 draw_view_line(view, view->lineno - view->offset);
1539 }
1541 redrawwin(view->win);
1542 wrefresh(view->win);
1543 report("");
1544 }
1546 /* Scroll frontend */
1547 static void
1548 scroll_view(struct view *view, enum request request)
1549 {
1550 int lines = 1;
1552 assert(view_is_displayed(view));
1554 switch (request) {
1555 case REQ_SCROLL_PAGE_DOWN:
1556 lines = view->height;
1557 case REQ_SCROLL_LINE_DOWN:
1558 if (view->offset + lines > view->lines)
1559 lines = view->lines - view->offset;
1561 if (lines == 0 || view->offset + view->height >= view->lines) {
1562 report("Cannot scroll beyond the last line");
1563 return;
1564 }
1565 break;
1567 case REQ_SCROLL_PAGE_UP:
1568 lines = view->height;
1569 case REQ_SCROLL_LINE_UP:
1570 if (lines > view->offset)
1571 lines = view->offset;
1573 if (lines == 0) {
1574 report("Cannot scroll beyond the first line");
1575 return;
1576 }
1578 lines = -lines;
1579 break;
1581 default:
1582 die("request %d not handled in switch", request);
1583 }
1585 do_scroll_view(view, lines);
1586 }
1588 /* Cursor moving */
1589 static void
1590 move_view(struct view *view, enum request request)
1591 {
1592 int scroll_steps = 0;
1593 int steps;
1595 switch (request) {
1596 case REQ_MOVE_FIRST_LINE:
1597 steps = -view->lineno;
1598 break;
1600 case REQ_MOVE_LAST_LINE:
1601 steps = view->lines - view->lineno - 1;
1602 break;
1604 case REQ_MOVE_PAGE_UP:
1605 steps = view->height > view->lineno
1606 ? -view->lineno : -view->height;
1607 break;
1609 case REQ_MOVE_PAGE_DOWN:
1610 steps = view->lineno + view->height >= view->lines
1611 ? view->lines - view->lineno - 1 : view->height;
1612 break;
1614 case REQ_MOVE_UP:
1615 steps = -1;
1616 break;
1618 case REQ_MOVE_DOWN:
1619 steps = 1;
1620 break;
1622 default:
1623 die("request %d not handled in switch", request);
1624 }
1626 if (steps <= 0 && view->lineno == 0) {
1627 report("Cannot move beyond the first line");
1628 return;
1630 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1631 report("Cannot move beyond the last line");
1632 return;
1633 }
1635 /* Move the current line */
1636 view->lineno += steps;
1637 assert(0 <= view->lineno && view->lineno < view->lines);
1639 /* Check whether the view needs to be scrolled */
1640 if (view->lineno < view->offset ||
1641 view->lineno >= view->offset + view->height) {
1642 scroll_steps = steps;
1643 if (steps < 0 && -steps > view->offset) {
1644 scroll_steps = -view->offset;
1646 } else if (steps > 0) {
1647 if (view->lineno == view->lines - 1 &&
1648 view->lines > view->height) {
1649 scroll_steps = view->lines - view->offset - 1;
1650 if (scroll_steps >= view->height)
1651 scroll_steps -= view->height - 1;
1652 }
1653 }
1654 }
1656 if (!view_is_displayed(view)) {
1657 view->offset += scroll_steps;
1658 assert(0 <= view->offset && view->offset < view->lines);
1659 view->ops->select(view, &view->line[view->lineno]);
1660 return;
1661 }
1663 /* Repaint the old "current" line if we be scrolling */
1664 if (ABS(steps) < view->height)
1665 draw_view_line(view, view->lineno - steps - view->offset);
1667 if (scroll_steps) {
1668 do_scroll_view(view, scroll_steps);
1669 return;
1670 }
1672 /* Draw the current line */
1673 draw_view_line(view, view->lineno - view->offset);
1675 redrawwin(view->win);
1676 wrefresh(view->win);
1677 report("");
1678 }
1681 /*
1682 * Searching
1683 */
1685 static void search_view(struct view *view, enum request request);
1687 static bool
1688 find_next_line(struct view *view, unsigned long lineno, struct line *line)
1689 {
1690 assert(view_is_displayed(view));
1692 if (!view->ops->grep(view, line))
1693 return FALSE;
1695 if (lineno - view->offset >= view->height) {
1696 view->offset = lineno;
1697 view->lineno = lineno;
1698 redraw_view(view);
1700 } else {
1701 unsigned long old_lineno = view->lineno - view->offset;
1703 view->lineno = lineno;
1704 draw_view_line(view, old_lineno);
1706 draw_view_line(view, view->lineno - view->offset);
1707 redrawwin(view->win);
1708 wrefresh(view->win);
1709 }
1711 report("Line %ld matches '%s'", lineno + 1, view->grep);
1712 return TRUE;
1713 }
1715 static void
1716 find_next(struct view *view, enum request request)
1717 {
1718 unsigned long lineno = view->lineno;
1719 int direction;
1721 if (!*view->grep) {
1722 if (!*opt_search)
1723 report("No previous search");
1724 else
1725 search_view(view, request);
1726 return;
1727 }
1729 switch (request) {
1730 case REQ_SEARCH:
1731 case REQ_FIND_NEXT:
1732 direction = 1;
1733 break;
1735 case REQ_SEARCH_BACK:
1736 case REQ_FIND_PREV:
1737 direction = -1;
1738 break;
1740 default:
1741 return;
1742 }
1744 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
1745 lineno += direction;
1747 /* Note, lineno is unsigned long so will wrap around in which case it
1748 * will become bigger than view->lines. */
1749 for (; lineno < view->lines; lineno += direction) {
1750 struct line *line = &view->line[lineno];
1752 if (find_next_line(view, lineno, line))
1753 return;
1754 }
1756 report("No match found for '%s'", view->grep);
1757 }
1759 static void
1760 search_view(struct view *view, enum request request)
1761 {
1762 int regex_err;
1764 if (view->regex) {
1765 regfree(view->regex);
1766 *view->grep = 0;
1767 } else {
1768 view->regex = calloc(1, sizeof(*view->regex));
1769 if (!view->regex)
1770 return;
1771 }
1773 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
1774 if (regex_err != 0) {
1775 char buf[SIZEOF_STR] = "unknown error";
1777 regerror(regex_err, view->regex, buf, sizeof(buf));
1778 report("Search failed: %s", buf);
1779 return;
1780 }
1782 string_copy(view->grep, opt_search);
1784 find_next(view, request);
1785 }
1787 /*
1788 * Incremental updating
1789 */
1791 static void
1792 end_update(struct view *view)
1793 {
1794 if (!view->pipe)
1795 return;
1796 set_nonblocking_input(FALSE);
1797 if (view->pipe == stdin)
1798 fclose(view->pipe);
1799 else
1800 pclose(view->pipe);
1801 view->pipe = NULL;
1802 }
1804 static bool
1805 begin_update(struct view *view)
1806 {
1807 if (view->pipe)
1808 end_update(view);
1810 if (opt_cmd[0]) {
1811 string_copy(view->cmd, opt_cmd);
1812 opt_cmd[0] = 0;
1813 /* When running random commands, initially show the
1814 * command in the title. However, it maybe later be
1815 * overwritten if a commit line is selected. */
1816 string_copy(view->ref, view->cmd);
1818 } else if (view == VIEW(REQ_VIEW_TREE)) {
1819 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1820 char path[SIZEOF_STR];
1822 if (strcmp(view->vid, view->id))
1823 opt_path[0] = path[0] = 0;
1824 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
1825 return FALSE;
1827 if (!string_format(view->cmd, format, view->id, path))
1828 return FALSE;
1830 } else {
1831 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
1832 const char *id = view->id;
1834 if (!string_format(view->cmd, format, id, id, id, id, id))
1835 return FALSE;
1837 /* Put the current ref_* value to the view title ref
1838 * member. This is needed by the blob view. Most other
1839 * views sets it automatically after loading because the
1840 * first line is a commit line. */
1841 string_copy_rev(view->ref, view->id);
1842 }
1844 /* Special case for the pager view. */
1845 if (opt_pipe) {
1846 view->pipe = opt_pipe;
1847 opt_pipe = NULL;
1848 } else {
1849 view->pipe = popen(view->cmd, "r");
1850 }
1852 if (!view->pipe)
1853 return FALSE;
1855 set_nonblocking_input(TRUE);
1857 view->offset = 0;
1858 view->lines = 0;
1859 view->lineno = 0;
1860 string_copy_rev(view->vid, view->id);
1862 if (view->line) {
1863 int i;
1865 for (i = 0; i < view->lines; i++)
1866 if (view->line[i].data)
1867 free(view->line[i].data);
1869 free(view->line);
1870 view->line = NULL;
1871 }
1873 view->start_time = time(NULL);
1875 return TRUE;
1876 }
1878 static struct line *
1879 realloc_lines(struct view *view, size_t line_size)
1880 {
1881 struct line *tmp = realloc(view->line, sizeof(*view->line) * line_size);
1883 if (!tmp)
1884 return NULL;
1886 view->line = tmp;
1887 view->line_size = line_size;
1888 return view->line;
1889 }
1891 static bool
1892 update_view(struct view *view)
1893 {
1894 char in_buffer[BUFSIZ];
1895 char out_buffer[BUFSIZ * 2];
1896 char *line;
1897 /* The number of lines to read. If too low it will cause too much
1898 * redrawing (and possible flickering), if too high responsiveness
1899 * will suffer. */
1900 unsigned long lines = view->height;
1901 int redraw_from = -1;
1903 if (!view->pipe)
1904 return TRUE;
1906 /* Only redraw if lines are visible. */
1907 if (view->offset + view->height >= view->lines)
1908 redraw_from = view->lines - view->offset;
1910 /* FIXME: This is probably not perfect for backgrounded views. */
1911 if (!realloc_lines(view, view->lines + lines))
1912 goto alloc_error;
1914 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
1915 size_t linelen = strlen(line);
1917 if (linelen)
1918 line[linelen - 1] = 0;
1920 if (opt_iconv != ICONV_NONE) {
1921 char *inbuf = line;
1922 size_t inlen = linelen;
1924 char *outbuf = out_buffer;
1925 size_t outlen = sizeof(out_buffer);
1927 size_t ret;
1929 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
1930 if (ret != (size_t) -1) {
1931 line = out_buffer;
1932 linelen = strlen(out_buffer);
1933 }
1934 }
1936 if (!view->ops->read(view, line))
1937 goto alloc_error;
1939 if (lines-- == 1)
1940 break;
1941 }
1943 {
1944 int digits;
1946 lines = view->lines;
1947 for (digits = 0; lines; digits++)
1948 lines /= 10;
1950 /* Keep the displayed view in sync with line number scaling. */
1951 if (digits != view->digits) {
1952 view->digits = digits;
1953 redraw_from = 0;
1954 }
1955 }
1957 if (!view_is_displayed(view))
1958 goto check_pipe;
1960 if (view == VIEW(REQ_VIEW_TREE)) {
1961 /* Clear the view and redraw everything since the tree sorting
1962 * might have rearranged things. */
1963 redraw_view(view);
1965 } else if (redraw_from >= 0) {
1966 /* If this is an incremental update, redraw the previous line
1967 * since for commits some members could have changed when
1968 * loading the main view. */
1969 if (redraw_from > 0)
1970 redraw_from--;
1972 /* Since revision graph visualization requires knowledge
1973 * about the parent commit, it causes a further one-off
1974 * needed to be redrawn for incremental updates. */
1975 if (redraw_from > 0 && opt_rev_graph)
1976 redraw_from--;
1978 /* Incrementally draw avoids flickering. */
1979 redraw_view_from(view, redraw_from);
1980 }
1982 /* Update the title _after_ the redraw so that if the redraw picks up a
1983 * commit reference in view->ref it'll be available here. */
1984 update_view_title(view);
1986 check_pipe:
1987 if (ferror(view->pipe)) {
1988 report("Failed to read: %s", strerror(errno));
1989 goto end;
1991 } else if (feof(view->pipe)) {
1992 report("");
1993 goto end;
1994 }
1996 return TRUE;
1998 alloc_error:
1999 report("Allocation failure");
2001 end:
2002 view->ops->read(view, NULL);
2003 end_update(view);
2004 return FALSE;
2005 }
2007 static struct line *
2008 add_line_data(struct view *view, void *data, enum line_type type)
2009 {
2010 struct line *line = &view->line[view->lines++];
2012 memset(line, 0, sizeof(*line));
2013 line->type = type;
2014 line->data = data;
2016 return line;
2017 }
2019 static struct line *
2020 add_line_text(struct view *view, char *data, enum line_type type)
2021 {
2022 if (data)
2023 data = strdup(data);
2025 return data ? add_line_data(view, data, type) : NULL;
2026 }
2029 /*
2030 * View opening
2031 */
2033 enum open_flags {
2034 OPEN_DEFAULT = 0, /* Use default view switching. */
2035 OPEN_SPLIT = 1, /* Split current view. */
2036 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2037 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2038 };
2040 static void
2041 open_view(struct view *prev, enum request request, enum open_flags flags)
2042 {
2043 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2044 bool split = !!(flags & OPEN_SPLIT);
2045 bool reload = !!(flags & OPEN_RELOAD);
2046 struct view *view = VIEW(request);
2047 int nviews = displayed_views();
2048 struct view *base_view = display[0];
2050 if (view == prev && nviews == 1 && !reload) {
2051 report("Already in %s view", view->name);
2052 return;
2053 }
2055 if (view->ops->open) {
2056 if (!view->ops->open(view)) {
2057 report("Failed to load %s view", view->name);
2058 return;
2059 }
2061 } else if ((reload || strcmp(view->vid, view->id)) &&
2062 !begin_update(view)) {
2063 report("Failed to load %s view", view->name);
2064 return;
2065 }
2067 if (split) {
2068 display[1] = view;
2069 if (!backgrounded)
2070 current_view = 1;
2071 } else {
2072 /* Maximize the current view. */
2073 memset(display, 0, sizeof(display));
2074 current_view = 0;
2075 display[current_view] = view;
2076 }
2078 /* Resize the view when switching between split- and full-screen,
2079 * or when switching between two different full-screen views. */
2080 if (nviews != displayed_views() ||
2081 (nviews == 1 && base_view != display[0]))
2082 resize_display();
2084 if (split && prev->lineno - prev->offset >= prev->height) {
2085 /* Take the title line into account. */
2086 int lines = prev->lineno - prev->offset - prev->height + 1;
2088 /* Scroll the view that was split if the current line is
2089 * outside the new limited view. */
2090 do_scroll_view(prev, lines);
2091 }
2093 if (prev && view != prev) {
2094 if (split && !backgrounded) {
2095 /* "Blur" the previous view. */
2096 update_view_title(prev);
2097 }
2099 view->parent = prev;
2100 }
2102 if (view->pipe && view->lines == 0) {
2103 /* Clear the old view and let the incremental updating refill
2104 * the screen. */
2105 wclear(view->win);
2106 report("");
2107 } else {
2108 redraw_view(view);
2109 report("");
2110 }
2112 /* If the view is backgrounded the above calls to report()
2113 * won't redraw the view title. */
2114 if (backgrounded)
2115 update_view_title(view);
2116 }
2119 /*
2120 * User request switch noodle
2121 */
2123 static int
2124 view_driver(struct view *view, enum request request)
2125 {
2126 int i;
2128 switch (request) {
2129 case REQ_MOVE_UP:
2130 case REQ_MOVE_DOWN:
2131 case REQ_MOVE_PAGE_UP:
2132 case REQ_MOVE_PAGE_DOWN:
2133 case REQ_MOVE_FIRST_LINE:
2134 case REQ_MOVE_LAST_LINE:
2135 move_view(view, request);
2136 break;
2138 case REQ_SCROLL_LINE_DOWN:
2139 case REQ_SCROLL_LINE_UP:
2140 case REQ_SCROLL_PAGE_DOWN:
2141 case REQ_SCROLL_PAGE_UP:
2142 scroll_view(view, request);
2143 break;
2145 case REQ_VIEW_BLOB:
2146 if (!ref_blob[0]) {
2147 report("No file chosen, press %s to open tree view",
2148 get_key(REQ_VIEW_TREE));
2149 break;
2150 }
2151 open_view(view, request, OPEN_DEFAULT);
2152 break;
2154 case REQ_VIEW_PAGER:
2155 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2156 report("No pager content, press %s to run command from prompt",
2157 get_key(REQ_PROMPT));
2158 break;
2159 }
2160 open_view(view, request, OPEN_DEFAULT);
2161 break;
2163 case REQ_VIEW_MAIN:
2164 case REQ_VIEW_DIFF:
2165 case REQ_VIEW_LOG:
2166 case REQ_VIEW_TREE:
2167 case REQ_VIEW_HELP:
2168 case REQ_VIEW_STATUS:
2169 open_view(view, request, OPEN_DEFAULT);
2170 break;
2172 case REQ_NEXT:
2173 case REQ_PREVIOUS:
2174 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2176 if ((view == VIEW(REQ_VIEW_DIFF) &&
2177 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2178 (view == VIEW(REQ_VIEW_BLOB) &&
2179 view->parent == VIEW(REQ_VIEW_TREE))) {
2180 view = view->parent;
2181 move_view(view, request);
2182 if (view_is_displayed(view))
2183 update_view_title(view);
2184 } else {
2185 move_view(view, request);
2186 break;
2187 }
2188 /* Fall-through */
2190 case REQ_ENTER:
2191 if (!view->lines) {
2192 report("Nothing to enter");
2193 break;
2194 }
2195 return view->ops->enter(view, &view->line[view->lineno]);
2197 case REQ_VIEW_NEXT:
2198 {
2199 int nviews = displayed_views();
2200 int next_view = (current_view + 1) % nviews;
2202 if (next_view == current_view) {
2203 report("Only one view is displayed");
2204 break;
2205 }
2207 current_view = next_view;
2208 /* Blur out the title of the previous view. */
2209 update_view_title(view);
2210 report("");
2211 break;
2212 }
2213 case REQ_TOGGLE_LINENO:
2214 opt_line_number = !opt_line_number;
2215 redraw_display();
2216 break;
2218 case REQ_TOGGLE_REV_GRAPH:
2219 opt_rev_graph = !opt_rev_graph;
2220 redraw_display();
2221 break;
2223 case REQ_PROMPT:
2224 /* Always reload^Wrerun commands from the prompt. */
2225 open_view(view, opt_request, OPEN_RELOAD);
2226 break;
2228 case REQ_SEARCH:
2229 case REQ_SEARCH_BACK:
2230 search_view(view, request);
2231 break;
2233 case REQ_FIND_NEXT:
2234 case REQ_FIND_PREV:
2235 find_next(view, request);
2236 break;
2238 case REQ_STOP_LOADING:
2239 for (i = 0; i < ARRAY_SIZE(views); i++) {
2240 view = &views[i];
2241 if (view->pipe)
2242 report("Stopped loading the %s view", view->name),
2243 end_update(view);
2244 }
2245 break;
2247 case REQ_SHOW_VERSION:
2248 report("tig-%s (built %s)", VERSION, __DATE__);
2249 return TRUE;
2251 case REQ_SCREEN_RESIZE:
2252 resize_display();
2253 /* Fall-through */
2254 case REQ_SCREEN_REDRAW:
2255 redraw_display();
2256 break;
2258 case REQ_NONE:
2259 doupdate();
2260 return TRUE;
2262 case REQ_VIEW_CLOSE:
2263 /* XXX: Mark closed views by letting view->parent point to the
2264 * view itself. Parents to closed view should never be
2265 * followed. */
2266 if (view->parent &&
2267 view->parent->parent != view->parent) {
2268 memset(display, 0, sizeof(display));
2269 current_view = 0;
2270 display[current_view] = view->parent;
2271 view->parent = view;
2272 resize_display();
2273 redraw_display();
2274 break;
2275 }
2276 /* Fall-through */
2277 case REQ_QUIT:
2278 return FALSE;
2280 default:
2281 /* An unknown key will show most commonly used commands. */
2282 report("Unknown key, press 'h' for help");
2283 return TRUE;
2284 }
2286 return TRUE;
2287 }
2290 /*
2291 * Pager backend
2292 */
2294 static bool
2295 pager_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2296 {
2297 char *text = line->data;
2298 enum line_type type = line->type;
2299 int textlen = strlen(text);
2300 int attr;
2302 wmove(view->win, lineno, 0);
2304 if (selected) {
2305 type = LINE_CURSOR;
2306 wchgat(view->win, -1, 0, type, NULL);
2307 }
2309 attr = get_line_attr(type);
2310 wattrset(view->win, attr);
2312 if (opt_line_number || opt_tab_size < TABSIZE) {
2313 static char spaces[] = " ";
2314 int col_offset = 0, col = 0;
2316 if (opt_line_number) {
2317 unsigned long real_lineno = view->offset + lineno + 1;
2319 if (real_lineno == 1 ||
2320 (real_lineno % opt_num_interval) == 0) {
2321 wprintw(view->win, "%.*d", view->digits, real_lineno);
2323 } else {
2324 waddnstr(view->win, spaces,
2325 MIN(view->digits, STRING_SIZE(spaces)));
2326 }
2327 waddstr(view->win, ": ");
2328 col_offset = view->digits + 2;
2329 }
2331 while (text && col_offset + col < view->width) {
2332 int cols_max = view->width - col_offset - col;
2333 char *pos = text;
2334 int cols;
2336 if (*text == '\t') {
2337 text++;
2338 assert(sizeof(spaces) > TABSIZE);
2339 pos = spaces;
2340 cols = opt_tab_size - (col % opt_tab_size);
2342 } else {
2343 text = strchr(text, '\t');
2344 cols = line ? text - pos : strlen(pos);
2345 }
2347 waddnstr(view->win, pos, MIN(cols, cols_max));
2348 col += cols;
2349 }
2351 } else {
2352 int col = 0, pos = 0;
2354 for (; pos < textlen && col < view->width; pos++, col++)
2355 if (text[pos] == '\t')
2356 col += TABSIZE - (col % TABSIZE) - 1;
2358 waddnstr(view->win, text, pos);
2359 }
2361 return TRUE;
2362 }
2364 static bool
2365 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2366 {
2367 char refbuf[SIZEOF_STR];
2368 char *ref = NULL;
2369 FILE *pipe;
2371 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2372 return TRUE;
2374 pipe = popen(refbuf, "r");
2375 if (!pipe)
2376 return TRUE;
2378 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2379 ref = chomp_string(ref);
2380 pclose(pipe);
2382 if (!ref || !*ref)
2383 return TRUE;
2385 /* This is the only fatal call, since it can "corrupt" the buffer. */
2386 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2387 return FALSE;
2389 return TRUE;
2390 }
2392 static void
2393 add_pager_refs(struct view *view, struct line *line)
2394 {
2395 char buf[SIZEOF_STR];
2396 char *commit_id = line->data + STRING_SIZE("commit ");
2397 struct ref **refs;
2398 size_t bufpos = 0, refpos = 0;
2399 const char *sep = "Refs: ";
2400 bool is_tag = FALSE;
2402 assert(line->type == LINE_COMMIT);
2404 refs = get_refs(commit_id);
2405 if (!refs) {
2406 if (view == VIEW(REQ_VIEW_DIFF))
2407 goto try_add_describe_ref;
2408 return;
2409 }
2411 do {
2412 struct ref *ref = refs[refpos];
2413 char *fmt = ref->tag ? "%s[%s]" :
2414 ref->remote ? "%s<%s>" : "%s%s";
2416 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2417 return;
2418 sep = ", ";
2419 if (ref->tag)
2420 is_tag = TRUE;
2421 } while (refs[refpos++]->next);
2423 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2424 try_add_describe_ref:
2425 /* Add <tag>-g<commit_id> "fake" reference. */
2426 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2427 return;
2428 }
2430 if (bufpos == 0)
2431 return;
2433 if (!realloc_lines(view, view->line_size + 1))
2434 return;
2436 add_line_text(view, buf, LINE_PP_REFS);
2437 }
2439 static bool
2440 pager_read(struct view *view, char *data)
2441 {
2442 struct line *line;
2444 if (!data)
2445 return TRUE;
2447 line = add_line_text(view, data, get_line_type(data));
2448 if (!line)
2449 return FALSE;
2451 if (line->type == LINE_COMMIT &&
2452 (view == VIEW(REQ_VIEW_DIFF) ||
2453 view == VIEW(REQ_VIEW_LOG)))
2454 add_pager_refs(view, line);
2456 return TRUE;
2457 }
2459 static bool
2460 pager_enter(struct view *view, struct line *line)
2461 {
2462 int split = 0;
2464 if (line->type == LINE_COMMIT &&
2465 (view == VIEW(REQ_VIEW_LOG) ||
2466 view == VIEW(REQ_VIEW_PAGER))) {
2467 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2468 split = 1;
2469 }
2471 /* Always scroll the view even if it was split. That way
2472 * you can use Enter to scroll through the log view and
2473 * split open each commit diff. */
2474 scroll_view(view, REQ_SCROLL_LINE_DOWN);
2476 /* FIXME: A minor workaround. Scrolling the view will call report("")
2477 * but if we are scrolling a non-current view this won't properly
2478 * update the view title. */
2479 if (split)
2480 update_view_title(view);
2482 return TRUE;
2483 }
2485 static bool
2486 pager_grep(struct view *view, struct line *line)
2487 {
2488 regmatch_t pmatch;
2489 char *text = line->data;
2491 if (!*text)
2492 return FALSE;
2494 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
2495 return FALSE;
2497 return TRUE;
2498 }
2500 static void
2501 pager_select(struct view *view, struct line *line)
2502 {
2503 if (line->type == LINE_COMMIT) {
2504 char *text = line->data + STRING_SIZE("commit ");
2506 if (view != VIEW(REQ_VIEW_PAGER))
2507 string_copy_rev(view->ref, text);
2508 string_copy_rev(ref_commit, text);
2509 }
2510 }
2512 static struct view_ops pager_ops = {
2513 "line",
2514 NULL,
2515 pager_read,
2516 pager_draw,
2517 pager_enter,
2518 pager_grep,
2519 pager_select,
2520 };
2523 /*
2524 * Help backend
2525 */
2527 static bool
2528 help_open(struct view *view)
2529 {
2530 char buf[BUFSIZ];
2531 int lines = ARRAY_SIZE(req_info) + 2;
2532 int i;
2534 if (view->lines > 0)
2535 return TRUE;
2537 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2538 if (!req_info[i].request)
2539 lines++;
2541 view->line = calloc(lines, sizeof(*view->line));
2542 if (!view->line)
2543 return FALSE;
2545 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
2547 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2548 char *key;
2550 if (!req_info[i].request) {
2551 add_line_text(view, "", LINE_DEFAULT);
2552 add_line_text(view, req_info[i].help, LINE_DEFAULT);
2553 continue;
2554 }
2556 key = get_key(req_info[i].request);
2557 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
2558 continue;
2560 add_line_text(view, buf, LINE_DEFAULT);
2561 }
2563 return TRUE;
2564 }
2566 static struct view_ops help_ops = {
2567 "line",
2568 help_open,
2569 NULL,
2570 pager_draw,
2571 pager_enter,
2572 pager_grep,
2573 pager_select,
2574 };
2577 /*
2578 * Tree backend
2579 */
2581 /* Parse output from git-ls-tree(1):
2582 *
2583 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
2584 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
2585 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
2586 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
2587 */
2589 #define SIZEOF_TREE_ATTR \
2590 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
2592 #define TREE_UP_FORMAT "040000 tree %s\t.."
2594 static int
2595 tree_compare_entry(enum line_type type1, char *name1,
2596 enum line_type type2, char *name2)
2597 {
2598 if (type1 != type2) {
2599 if (type1 == LINE_TREE_DIR)
2600 return -1;
2601 return 1;
2602 }
2604 return strcmp(name1, name2);
2605 }
2607 static bool
2608 tree_read(struct view *view, char *text)
2609 {
2610 size_t textlen = text ? strlen(text) : 0;
2611 char buf[SIZEOF_STR];
2612 unsigned long pos;
2613 enum line_type type;
2614 bool first_read = view->lines == 0;
2616 if (textlen <= SIZEOF_TREE_ATTR)
2617 return FALSE;
2619 type = text[STRING_SIZE("100644 ")] == 't'
2620 ? LINE_TREE_DIR : LINE_TREE_FILE;
2622 if (first_read) {
2623 /* Add path info line */
2624 if (!string_format(buf, "Directory path /%s", opt_path) ||
2625 !realloc_lines(view, view->line_size + 1) ||
2626 !add_line_text(view, buf, LINE_DEFAULT))
2627 return FALSE;
2629 /* Insert "link" to parent directory. */
2630 if (*opt_path) {
2631 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
2632 !realloc_lines(view, view->line_size + 1) ||
2633 !add_line_text(view, buf, LINE_TREE_DIR))
2634 return FALSE;
2635 }
2636 }
2638 /* Strip the path part ... */
2639 if (*opt_path) {
2640 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
2641 size_t striplen = strlen(opt_path);
2642 char *path = text + SIZEOF_TREE_ATTR;
2644 if (pathlen > striplen)
2645 memmove(path, path + striplen,
2646 pathlen - striplen + 1);
2647 }
2649 /* Skip "Directory ..." and ".." line. */
2650 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
2651 struct line *line = &view->line[pos];
2652 char *path1 = ((char *) line->data) + SIZEOF_TREE_ATTR;
2653 char *path2 = text + SIZEOF_TREE_ATTR;
2654 int cmp = tree_compare_entry(line->type, path1, type, path2);
2656 if (cmp <= 0)
2657 continue;
2659 text = strdup(text);
2660 if (!text)
2661 return FALSE;
2663 if (view->lines > pos)
2664 memmove(&view->line[pos + 1], &view->line[pos],
2665 (view->lines - pos) * sizeof(*line));
2667 line = &view->line[pos];
2668 line->data = text;
2669 line->type = type;
2670 view->lines++;
2671 return TRUE;
2672 }
2674 if (!add_line_text(view, text, type))
2675 return FALSE;
2677 /* Move the current line to the first tree entry. */
2678 if (first_read)
2679 view->lineno++;
2681 return TRUE;
2682 }
2684 static bool
2685 tree_enter(struct view *view, struct line *line)
2686 {
2687 enum open_flags flags;
2688 enum request request;
2690 switch (line->type) {
2691 case LINE_TREE_DIR:
2692 /* Depending on whether it is a subdir or parent (updir?) link
2693 * mangle the path buffer. */
2694 if (line == &view->line[1] && *opt_path) {
2695 size_t path_len = strlen(opt_path);
2696 char *dirsep = opt_path + path_len - 1;
2698 while (dirsep > opt_path && dirsep[-1] != '/')
2699 dirsep--;
2701 dirsep[0] = 0;
2703 } else {
2704 size_t pathlen = strlen(opt_path);
2705 size_t origlen = pathlen;
2706 char *data = line->data;
2707 char *basename = data + SIZEOF_TREE_ATTR;
2709 if (!string_format_from(opt_path, &pathlen, "%s/", basename)) {
2710 opt_path[origlen] = 0;
2711 return TRUE;
2712 }
2713 }
2715 /* Trees and subtrees share the same ID, so they are not not
2716 * unique like blobs. */
2717 flags = OPEN_RELOAD;
2718 request = REQ_VIEW_TREE;
2719 break;
2721 case LINE_TREE_FILE:
2722 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
2723 request = REQ_VIEW_BLOB;
2724 break;
2726 default:
2727 return TRUE;
2728 }
2730 open_view(view, request, flags);
2732 return TRUE;
2733 }
2735 static void
2736 tree_select(struct view *view, struct line *line)
2737 {
2738 char *text = line->data + STRING_SIZE("100644 blob ");
2740 if (line->type == LINE_TREE_FILE) {
2741 string_copy_rev(ref_blob, text);
2743 } else if (line->type != LINE_TREE_DIR) {
2744 return;
2745 }
2747 string_copy_rev(view->ref, text);
2748 }
2750 static struct view_ops tree_ops = {
2751 "file",
2752 NULL,
2753 tree_read,
2754 pager_draw,
2755 tree_enter,
2756 pager_grep,
2757 tree_select,
2758 };
2760 static bool
2761 blob_read(struct view *view, char *line)
2762 {
2763 return add_line_text(view, line, LINE_DEFAULT);
2764 }
2766 static struct view_ops blob_ops = {
2767 "line",
2768 NULL,
2769 blob_read,
2770 pager_draw,
2771 pager_enter,
2772 pager_grep,
2773 pager_select,
2774 };
2777 /*
2778 * Status backend
2779 */
2781 struct status {
2782 char status;
2783 struct {
2784 mode_t mode;
2785 char rev[SIZEOF_REV];
2786 } old;
2787 struct {
2788 mode_t mode;
2789 char rev[SIZEOF_REV];
2790 } new;
2791 char name[SIZEOF_STR];
2792 };
2794 /* Get fields from the diff line:
2795 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
2796 */
2797 static inline bool
2798 status_get_diff(struct status *file, char *buf, size_t bufsize)
2799 {
2800 char *old_mode = buf + 1;
2801 char *new_mode = buf + 8;
2802 char *old_rev = buf + 15;
2803 char *new_rev = buf + 56;
2804 char *status = buf + 97;
2806 if (bufsize != 99 ||
2807 old_mode[-1] != ':' ||
2808 new_mode[-1] != ' ' ||
2809 old_rev[-1] != ' ' ||
2810 new_rev[-1] != ' ' ||
2811 status[-1] != ' ')
2812 return FALSE;
2814 file->status = *status;
2816 string_copy_rev(file->old.rev, old_rev);
2817 string_copy_rev(file->new.rev, new_rev);
2819 file->old.mode = strtoul(old_mode, NULL, 8);
2820 file->new.mode = strtoul(new_mode, NULL, 8);
2822 file->name[0] = 0;
2824 return TRUE;
2825 }
2827 static bool
2828 status_run(struct view *view, const char cmd[], bool diff, enum line_type type)
2829 {
2830 struct status *file = NULL;
2831 char buf[SIZEOF_STR * 4];
2832 size_t bufsize = 0;
2833 FILE *pipe;
2835 pipe = popen(cmd, "r");
2836 if (!pipe)
2837 return FALSE;
2839 add_line_data(view, NULL, type);
2841 while (!feof(pipe) && !ferror(pipe)) {
2842 char *sep;
2843 size_t readsize;
2845 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
2846 if (!readsize)
2847 break;
2848 bufsize += readsize;
2850 /* Process while we have NUL chars. */
2851 while ((sep = memchr(buf, 0, bufsize))) {
2852 size_t sepsize = sep - buf + 1;
2854 if (!file) {
2855 if (!realloc_lines(view, view->line_size + 1))
2856 goto error_out;
2858 file = calloc(1, sizeof(*file));
2859 if (!file)
2860 goto error_out;
2862 add_line_data(view, file, type);
2863 }
2865 /* Parse diff info part. */
2866 if (!diff) {
2867 file->status = '?';
2869 } else if (!file->status) {
2870 if (!status_get_diff(file, buf, sepsize))
2871 goto error_out;
2873 bufsize -= sepsize;
2874 memmove(buf, sep + 1, bufsize);
2876 sep = memchr(buf, 0, bufsize);
2877 if (!sep)
2878 break;
2879 sepsize = sep - buf + 1;
2880 }
2882 /* git-ls-files just delivers a NUL separated
2883 * list of file names similar to the second half
2884 * of the git-diff-* output. */
2885 string_ncopy(file->name, buf, sepsize);
2886 bufsize -= sepsize;
2887 memmove(buf, sep + 1, bufsize);
2888 file = NULL;
2889 }
2890 }
2892 if (ferror(pipe)) {
2893 error_out:
2894 pclose(pipe);
2895 return FALSE;
2896 }
2898 if (!view->line[view->lines - 1].data)
2899 add_line_data(view, NULL, LINE_STAT_NONE);
2901 pclose(pipe);
2902 return TRUE;
2903 }
2905 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --cached HEAD"
2906 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
2907 #define STATUS_LIST_OTHER_CMD \
2908 "git ls-files -z --others --exclude-per-directory=.gitignore"
2910 /* First parse staged info using git-diff-index(1), then parse unstaged
2911 * info using git-diff-files(1), and finally untracked files using
2912 * git-ls-files(1). */
2913 static bool
2914 status_open(struct view *view)
2915 {
2916 struct stat statbuf;
2917 char exclude[SIZEOF_STR];
2918 char cmd[SIZEOF_STR];
2919 size_t i;
2921 for (i = 0; i < view->lines; i++)
2922 free(view->line[i].data);
2923 free(view->line);
2924 view->lines = view->line_size = 0;
2925 view->line = NULL;
2927 if (!realloc_lines(view, view->line_size + 6))
2928 return FALSE;
2930 if (!string_format(exclude, "%s/info/exclude", opt_git_dir))
2931 return FALSE;
2933 string_copy(cmd, STATUS_LIST_OTHER_CMD);
2935 if (stat(exclude, &statbuf) >= 0) {
2936 size_t cmdsize = strlen(cmd);
2938 if (!string_format_from(cmd, &cmdsize, " %s", "--exclude-from=") ||
2939 sq_quote(cmd, cmdsize, exclude) >= sizeof(cmd))
2940 return FALSE;
2941 }
2943 if (!status_run(view, STATUS_DIFF_INDEX_CMD, TRUE, LINE_STAT_STAGED) ||
2944 !status_run(view, STATUS_DIFF_FILES_CMD, TRUE, LINE_STAT_UNSTAGED) ||
2945 !status_run(view, cmd, FALSE, LINE_STAT_UNTRACKED))
2946 return FALSE;
2948 return TRUE;
2949 }
2951 static bool
2952 status_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2953 {
2954 struct status *status = line->data;
2956 wmove(view->win, lineno, 0);
2958 if (selected) {
2959 wattrset(view->win, get_line_attr(LINE_CURSOR));
2960 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
2962 } else if (!status && line->type != LINE_STAT_NONE) {
2963 wattrset(view->win, get_line_attr(LINE_STAT_SECTION));
2964 wchgat(view->win, -1, 0, LINE_STAT_SECTION, NULL);
2966 } else {
2967 wattrset(view->win, get_line_attr(line->type));
2968 }
2970 if (!status) {
2971 char *text;
2973 switch (line->type) {
2974 case LINE_STAT_STAGED:
2975 text = "Changes to be committed:";
2976 break;
2978 case LINE_STAT_UNSTAGED:
2979 text = "Changed but not updated:";
2980 break;
2982 case LINE_STAT_UNTRACKED:
2983 text = "Untracked files:";
2984 break;
2986 case LINE_STAT_NONE:
2987 text = " (no files)";
2988 break;
2990 default:
2991 return FALSE;
2992 }
2994 waddstr(view->win, text);
2995 return TRUE;
2996 }
2998 waddch(view->win, status->status);
2999 if (!selected)
3000 wattrset(view->win, A_NORMAL);
3001 wmove(view->win, lineno, 4);
3002 waddstr(view->win, status->name);
3004 return TRUE;
3005 }
3007 static bool
3008 status_enter(struct view *view, struct line *line)
3009 {
3010 struct status *status = line->data;
3011 char cmd[SIZEOF_STR];
3012 char buf[SIZEOF_STR];
3013 size_t cmdsize = 0;
3014 size_t bufsize = 0;
3015 size_t written = 0;
3016 FILE *pipe;
3018 if (!status)
3019 return TRUE;
3021 if (opt_cdup[0] &&
3022 line->type != LINE_STAT_UNTRACKED &&
3023 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
3024 return FALSE;
3026 switch (line->type) {
3027 case LINE_STAT_STAGED:
3028 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
3029 status->old.mode,
3030 status->old.rev,
3031 status->name, 0))
3032 return FALSE;
3034 string_add(cmd, cmdsize, "git update-index -z --index-info");
3035 break;
3037 case LINE_STAT_UNSTAGED:
3038 case LINE_STAT_UNTRACKED:
3039 if (!string_format_from(buf, &bufsize, "%s%c", status->name, 0))
3040 return FALSE;
3042 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
3043 break;
3045 default:
3046 die("w00t");
3047 }
3049 pipe = popen(cmd, "w");
3050 if (!pipe)
3051 return FALSE;
3053 while (!ferror(pipe) && written < bufsize) {
3054 written += fwrite(buf + written, 1, bufsize - written, pipe);
3055 }
3057 pclose(pipe);
3059 if (written != bufsize)
3060 return FALSE;
3062 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
3063 return TRUE;
3064 }
3066 static void
3067 status_select(struct view *view, struct line *line)
3068 {
3069 char *text;
3071 switch (line->type) {
3072 case LINE_STAT_STAGED:
3073 text = "Press Enter to unstage file for commit";
3074 break;
3076 case LINE_STAT_UNSTAGED:
3077 text = "Press Enter to stage file for commit ";
3078 break;
3080 case LINE_STAT_UNTRACKED:
3081 text = "Press Enter to stage file for addition";
3082 break;
3084 case LINE_STAT_NONE:
3085 return;
3087 default:
3088 die("w00t");
3089 }
3091 string_ncopy(view->ref, text, strlen(text));
3092 }
3094 static bool
3095 status_grep(struct view *view, struct line *line)
3096 {
3097 struct status *status = line->data;
3098 enum { S_STATUS, S_NAME, S_END } state;
3099 char buf[2] = "?";
3100 regmatch_t pmatch;
3102 if (!status)
3103 return FALSE;
3105 for (state = S_STATUS; state < S_END; state++) {
3106 char *text;
3108 switch (state) {
3109 case S_NAME: text = status->name; break;
3110 case S_STATUS:
3111 buf[0] = status->status;
3112 text = buf;
3113 break;
3115 default:
3116 return FALSE;
3117 }
3119 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3120 return TRUE;
3121 }
3123 return FALSE;
3124 }
3126 static struct view_ops status_ops = {
3127 "file",
3128 status_open,
3129 NULL,
3130 status_draw,
3131 status_enter,
3132 status_grep,
3133 status_select,
3134 };
3137 /*
3138 * Revision graph
3139 */
3141 struct commit {
3142 char id[SIZEOF_REV]; /* SHA1 ID. */
3143 char title[128]; /* First line of the commit message. */
3144 char author[75]; /* Author of the commit. */
3145 struct tm time; /* Date from the author ident. */
3146 struct ref **refs; /* Repository references. */
3147 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
3148 size_t graph_size; /* The width of the graph array. */
3149 };
3151 /* Size of rev graph with no "padding" columns */
3152 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
3154 struct rev_graph {
3155 struct rev_graph *prev, *next, *parents;
3156 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
3157 size_t size;
3158 struct commit *commit;
3159 size_t pos;
3160 };
3162 /* Parents of the commit being visualized. */
3163 static struct rev_graph graph_parents[4];
3165 /* The current stack of revisions on the graph. */
3166 static struct rev_graph graph_stacks[4] = {
3167 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
3168 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
3169 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
3170 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
3171 };
3173 static inline bool
3174 graph_parent_is_merge(struct rev_graph *graph)
3175 {
3176 return graph->parents->size > 1;
3177 }
3179 static inline void
3180 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
3181 {
3182 struct commit *commit = graph->commit;
3184 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
3185 commit->graph[commit->graph_size++] = symbol;
3186 }
3188 static void
3189 done_rev_graph(struct rev_graph *graph)
3190 {
3191 if (graph_parent_is_merge(graph) &&
3192 graph->pos < graph->size - 1 &&
3193 graph->next->size == graph->size + graph->parents->size - 1) {
3194 size_t i = graph->pos + graph->parents->size - 1;
3196 graph->commit->graph_size = i * 2;
3197 while (i < graph->next->size - 1) {
3198 append_to_rev_graph(graph, ' ');
3199 append_to_rev_graph(graph, '\\');
3200 i++;
3201 }
3202 }
3204 graph->size = graph->pos = 0;
3205 graph->commit = NULL;
3206 memset(graph->parents, 0, sizeof(*graph->parents));
3207 }
3209 static void
3210 push_rev_graph(struct rev_graph *graph, char *parent)
3211 {
3212 int i;
3214 /* "Collapse" duplicate parents lines.
3215 *
3216 * FIXME: This needs to also update update the drawn graph but
3217 * for now it just serves as a method for pruning graph lines. */
3218 for (i = 0; i < graph->size; i++)
3219 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
3220 return;
3222 if (graph->size < SIZEOF_REVITEMS) {
3223 string_copy_rev(graph->rev[graph->size++], parent);
3224 }
3225 }
3227 static chtype
3228 get_rev_graph_symbol(struct rev_graph *graph)
3229 {
3230 chtype symbol;
3232 if (graph->parents->size == 0)
3233 symbol = REVGRAPH_INIT;
3234 else if (graph_parent_is_merge(graph))
3235 symbol = REVGRAPH_MERGE;
3236 else if (graph->pos >= graph->size)
3237 symbol = REVGRAPH_BRANCH;
3238 else
3239 symbol = REVGRAPH_COMMIT;
3241 return symbol;
3242 }
3244 static void
3245 draw_rev_graph(struct rev_graph *graph)
3246 {
3247 struct rev_filler {
3248 chtype separator, line;
3249 };
3250 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
3251 static struct rev_filler fillers[] = {
3252 { ' ', REVGRAPH_LINE },
3253 { '`', '.' },
3254 { '\'', ' ' },
3255 { '/', ' ' },
3256 };
3257 chtype symbol = get_rev_graph_symbol(graph);
3258 struct rev_filler *filler;
3259 size_t i;
3261 filler = &fillers[DEFAULT];
3263 for (i = 0; i < graph->pos; i++) {
3264 append_to_rev_graph(graph, filler->line);
3265 if (graph_parent_is_merge(graph->prev) &&
3266 graph->prev->pos == i)
3267 filler = &fillers[RSHARP];
3269 append_to_rev_graph(graph, filler->separator);
3270 }
3272 /* Place the symbol for this revision. */
3273 append_to_rev_graph(graph, symbol);
3275 if (graph->prev->size > graph->size)
3276 filler = &fillers[RDIAG];
3277 else
3278 filler = &fillers[DEFAULT];
3280 i++;
3282 for (; i < graph->size; i++) {
3283 append_to_rev_graph(graph, filler->separator);
3284 append_to_rev_graph(graph, filler->line);
3285 if (graph_parent_is_merge(graph->prev) &&
3286 i < graph->prev->pos + graph->parents->size)
3287 filler = &fillers[RSHARP];
3288 if (graph->prev->size > graph->size)
3289 filler = &fillers[LDIAG];
3290 }
3292 if (graph->prev->size > graph->size) {
3293 append_to_rev_graph(graph, filler->separator);
3294 if (filler->line != ' ')
3295 append_to_rev_graph(graph, filler->line);
3296 }
3297 }
3299 /* Prepare the next rev graph */
3300 static void
3301 prepare_rev_graph(struct rev_graph *graph)
3302 {
3303 size_t i;
3305 /* First, traverse all lines of revisions up to the active one. */
3306 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
3307 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
3308 break;
3310 push_rev_graph(graph->next, graph->rev[graph->pos]);
3311 }
3313 /* Interleave the new revision parent(s). */
3314 for (i = 0; i < graph->parents->size; i++)
3315 push_rev_graph(graph->next, graph->parents->rev[i]);
3317 /* Lastly, put any remaining revisions. */
3318 for (i = graph->pos + 1; i < graph->size; i++)
3319 push_rev_graph(graph->next, graph->rev[i]);
3320 }
3322 static void
3323 update_rev_graph(struct rev_graph *graph)
3324 {
3325 /* If this is the finalizing update ... */
3326 if (graph->commit)
3327 prepare_rev_graph(graph);
3329 /* Graph visualization needs a one rev look-ahead,
3330 * so the first update doesn't visualize anything. */
3331 if (!graph->prev->commit)
3332 return;
3334 draw_rev_graph(graph->prev);
3335 done_rev_graph(graph->prev->prev);
3336 }
3339 /*
3340 * Main view backend
3341 */
3343 static bool
3344 main_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
3345 {
3346 char buf[DATE_COLS + 1];
3347 struct commit *commit = line->data;
3348 enum line_type type;
3349 int col = 0;
3350 size_t timelen;
3351 size_t authorlen;
3352 int trimmed = 1;
3354 if (!*commit->author)
3355 return FALSE;
3357 wmove(view->win, lineno, col);
3359 if (selected) {
3360 type = LINE_CURSOR;
3361 wattrset(view->win, get_line_attr(type));
3362 wchgat(view->win, -1, 0, type, NULL);
3364 } else {
3365 type = LINE_MAIN_COMMIT;
3366 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
3367 }
3369 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
3370 waddnstr(view->win, buf, timelen);
3371 waddstr(view->win, " ");
3373 col += DATE_COLS;
3374 wmove(view->win, lineno, col);
3375 if (type != LINE_CURSOR)
3376 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
3378 if (opt_utf8) {
3379 authorlen = utf8_length(commit->author, AUTHOR_COLS - 2, &col, &trimmed);
3380 } else {
3381 authorlen = strlen(commit->author);
3382 if (authorlen > AUTHOR_COLS - 2) {
3383 authorlen = AUTHOR_COLS - 2;
3384 trimmed = 1;
3385 }
3386 }
3388 if (trimmed) {
3389 waddnstr(view->win, commit->author, authorlen);
3390 if (type != LINE_CURSOR)
3391 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
3392 waddch(view->win, '~');
3393 } else {
3394 waddstr(view->win, commit->author);
3395 }
3397 col += AUTHOR_COLS;
3398 if (type != LINE_CURSOR)
3399 wattrset(view->win, A_NORMAL);
3401 if (opt_rev_graph && commit->graph_size) {
3402 size_t i;
3404 wmove(view->win, lineno, col);
3405 /* Using waddch() instead of waddnstr() ensures that
3406 * they'll be rendered correctly for the cursor line. */
3407 for (i = 0; i < commit->graph_size; i++)
3408 waddch(view->win, commit->graph[i]);
3410 waddch(view->win, ' ');
3411 col += commit->graph_size + 1;
3412 }
3414 wmove(view->win, lineno, col);
3416 if (commit->refs) {
3417 size_t i = 0;
3419 do {
3420 if (type == LINE_CURSOR)
3421 ;
3422 else if (commit->refs[i]->tag)
3423 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
3424 else if (commit->refs[i]->remote)
3425 wattrset(view->win, get_line_attr(LINE_MAIN_REMOTE));
3426 else
3427 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
3428 waddstr(view->win, "[");
3429 waddstr(view->win, commit->refs[i]->name);
3430 waddstr(view->win, "]");
3431 if (type != LINE_CURSOR)
3432 wattrset(view->win, A_NORMAL);
3433 waddstr(view->win, " ");
3434 col += strlen(commit->refs[i]->name) + STRING_SIZE("[] ");
3435 } while (commit->refs[i++]->next);
3436 }
3438 if (type != LINE_CURSOR)
3439 wattrset(view->win, get_line_attr(type));
3441 {
3442 int titlelen = strlen(commit->title);
3444 if (col + titlelen > view->width)
3445 titlelen = view->width - col;
3447 waddnstr(view->win, commit->title, titlelen);
3448 }
3450 return TRUE;
3451 }
3453 /* Reads git log --pretty=raw output and parses it into the commit struct. */
3454 static bool
3455 main_read(struct view *view, char *line)
3456 {
3457 static struct rev_graph *graph = graph_stacks;
3458 enum line_type type;
3459 struct commit *commit;
3461 if (!line) {
3462 update_rev_graph(graph);
3463 return TRUE;
3464 }
3466 type = get_line_type(line);
3467 if (type == LINE_COMMIT) {
3468 commit = calloc(1, sizeof(struct commit));
3469 if (!commit)
3470 return FALSE;
3472 string_copy_rev(commit->id, line + STRING_SIZE("commit "));
3473 commit->refs = get_refs(commit->id);
3474 graph->commit = commit;
3475 add_line_data(view, commit, LINE_MAIN_COMMIT);
3476 return TRUE;
3477 }
3479 if (!view->lines)
3480 return TRUE;
3481 commit = view->line[view->lines - 1].data;
3483 switch (type) {
3484 case LINE_PARENT:
3485 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
3486 break;
3488 case LINE_AUTHOR:
3489 {
3490 /* Parse author lines where the name may be empty:
3491 * author <email@address.tld> 1138474660 +0100
3492 */
3493 char *ident = line + STRING_SIZE("author ");
3494 char *nameend = strchr(ident, '<');
3495 char *emailend = strchr(ident, '>');
3497 if (!nameend || !emailend)
3498 break;
3500 update_rev_graph(graph);
3501 graph = graph->next;
3503 *nameend = *emailend = 0;
3504 ident = chomp_string(ident);
3505 if (!*ident) {
3506 ident = chomp_string(nameend + 1);
3507 if (!*ident)
3508 ident = "Unknown";
3509 }
3511 string_ncopy(commit->author, ident, strlen(ident));
3513 /* Parse epoch and timezone */
3514 if (emailend[1] == ' ') {
3515 char *secs = emailend + 2;
3516 char *zone = strchr(secs, ' ');
3517 time_t time = (time_t) atol(secs);
3519 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
3520 long tz;
3522 zone++;
3523 tz = ('0' - zone[1]) * 60 * 60 * 10;
3524 tz += ('0' - zone[2]) * 60 * 60;
3525 tz += ('0' - zone[3]) * 60;
3526 tz += ('0' - zone[4]) * 60;
3528 if (zone[0] == '-')
3529 tz = -tz;
3531 time -= tz;
3532 }
3534 gmtime_r(&time, &commit->time);
3535 }
3536 break;
3537 }
3538 default:
3539 /* Fill in the commit title if it has not already been set. */
3540 if (commit->title[0])
3541 break;
3543 /* Require titles to start with a non-space character at the
3544 * offset used by git log. */
3545 if (strncmp(line, " ", 4))
3546 break;
3547 line += 4;
3548 /* Well, if the title starts with a whitespace character,
3549 * try to be forgiving. Otherwise we end up with no title. */
3550 while (isspace(*line))
3551 line++;
3552 if (*line == '\0')
3553 break;
3554 /* FIXME: More graceful handling of titles; append "..." to
3555 * shortened titles, etc. */
3557 string_ncopy(commit->title, line, strlen(line));
3558 }
3560 return TRUE;
3561 }
3563 static bool
3564 main_enter(struct view *view, struct line *line)
3565 {
3566 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3568 open_view(view, REQ_VIEW_DIFF, flags);
3569 return TRUE;
3570 }
3572 static bool
3573 main_grep(struct view *view, struct line *line)
3574 {
3575 struct commit *commit = line->data;
3576 enum { S_TITLE, S_AUTHOR, S_DATE, S_END } state;
3577 char buf[DATE_COLS + 1];
3578 regmatch_t pmatch;
3580 for (state = S_TITLE; state < S_END; state++) {
3581 char *text;
3583 switch (state) {
3584 case S_TITLE: text = commit->title; break;
3585 case S_AUTHOR: text = commit->author; break;
3586 case S_DATE:
3587 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
3588 continue;
3589 text = buf;
3590 break;
3592 default:
3593 return FALSE;
3594 }
3596 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3597 return TRUE;
3598 }
3600 return FALSE;
3601 }
3603 static void
3604 main_select(struct view *view, struct line *line)
3605 {
3606 struct commit *commit = line->data;
3608 string_copy_rev(view->ref, commit->id);
3609 string_copy_rev(ref_commit, view->ref);
3610 }
3612 static struct view_ops main_ops = {
3613 "commit",
3614 NULL,
3615 main_read,
3616 main_draw,
3617 main_enter,
3618 main_grep,
3619 main_select,
3620 };
3623 /*
3624 * Unicode / UTF-8 handling
3625 *
3626 * NOTE: Much of the following code for dealing with unicode is derived from
3627 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
3628 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
3629 */
3631 /* I've (over)annotated a lot of code snippets because I am not entirely
3632 * confident that the approach taken by this small UTF-8 interface is correct.
3633 * --jonas */
3635 static inline int
3636 unicode_width(unsigned long c)
3637 {
3638 if (c >= 0x1100 &&
3639 (c <= 0x115f /* Hangul Jamo */
3640 || c == 0x2329
3641 || c == 0x232a
3642 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
3643 /* CJK ... Yi */
3644 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
3645 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
3646 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
3647 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
3648 || (c >= 0xffe0 && c <= 0xffe6)
3649 || (c >= 0x20000 && c <= 0x2fffd)
3650 || (c >= 0x30000 && c <= 0x3fffd)))
3651 return 2;
3653 return 1;
3654 }
3656 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
3657 * Illegal bytes are set one. */
3658 static const unsigned char utf8_bytes[256] = {
3659 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,
3660 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,
3661 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,
3662 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,
3663 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,
3664 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,
3665 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,
3666 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,
3667 };
3669 /* Decode UTF-8 multi-byte representation into a unicode character. */
3670 static inline unsigned long
3671 utf8_to_unicode(const char *string, size_t length)
3672 {
3673 unsigned long unicode;
3675 switch (length) {
3676 case 1:
3677 unicode = string[0];
3678 break;
3679 case 2:
3680 unicode = (string[0] & 0x1f) << 6;
3681 unicode += (string[1] & 0x3f);
3682 break;
3683 case 3:
3684 unicode = (string[0] & 0x0f) << 12;
3685 unicode += ((string[1] & 0x3f) << 6);
3686 unicode += (string[2] & 0x3f);
3687 break;
3688 case 4:
3689 unicode = (string[0] & 0x0f) << 18;
3690 unicode += ((string[1] & 0x3f) << 12);
3691 unicode += ((string[2] & 0x3f) << 6);
3692 unicode += (string[3] & 0x3f);
3693 break;
3694 case 5:
3695 unicode = (string[0] & 0x0f) << 24;
3696 unicode += ((string[1] & 0x3f) << 18);
3697 unicode += ((string[2] & 0x3f) << 12);
3698 unicode += ((string[3] & 0x3f) << 6);
3699 unicode += (string[4] & 0x3f);
3700 break;
3701 case 6:
3702 unicode = (string[0] & 0x01) << 30;
3703 unicode += ((string[1] & 0x3f) << 24);
3704 unicode += ((string[2] & 0x3f) << 18);
3705 unicode += ((string[3] & 0x3f) << 12);
3706 unicode += ((string[4] & 0x3f) << 6);
3707 unicode += (string[5] & 0x3f);
3708 break;
3709 default:
3710 die("Invalid unicode length");
3711 }
3713 /* Invalid characters could return the special 0xfffd value but NUL
3714 * should be just as good. */
3715 return unicode > 0xffff ? 0 : unicode;
3716 }
3718 /* Calculates how much of string can be shown within the given maximum width
3719 * and sets trimmed parameter to non-zero value if all of string could not be
3720 * shown.
3721 *
3722 * Additionally, adds to coloffset how many many columns to move to align with
3723 * the expected position. Takes into account how multi-byte and double-width
3724 * characters will effect the cursor position.
3725 *
3726 * Returns the number of bytes to output from string to satisfy max_width. */
3727 static size_t
3728 utf8_length(const char *string, size_t max_width, int *coloffset, int *trimmed)
3729 {
3730 const char *start = string;
3731 const char *end = strchr(string, '\0');
3732 size_t mbwidth = 0;
3733 size_t width = 0;
3735 *trimmed = 0;
3737 while (string < end) {
3738 int c = *(unsigned char *) string;
3739 unsigned char bytes = utf8_bytes[c];
3740 size_t ucwidth;
3741 unsigned long unicode;
3743 if (string + bytes > end)
3744 break;
3746 /* Change representation to figure out whether
3747 * it is a single- or double-width character. */
3749 unicode = utf8_to_unicode(string, bytes);
3750 /* FIXME: Graceful handling of invalid unicode character. */
3751 if (!unicode)
3752 break;
3754 ucwidth = unicode_width(unicode);
3755 width += ucwidth;
3756 if (width > max_width) {
3757 *trimmed = 1;
3758 break;
3759 }
3761 /* The column offset collects the differences between the
3762 * number of bytes encoding a character and the number of
3763 * columns will be used for rendering said character.
3764 *
3765 * So if some character A is encoded in 2 bytes, but will be
3766 * represented on the screen using only 1 byte this will and up
3767 * adding 1 to the multi-byte column offset.
3768 *
3769 * Assumes that no double-width character can be encoding in
3770 * less than two bytes. */
3771 if (bytes > ucwidth)
3772 mbwidth += bytes - ucwidth;
3774 string += bytes;
3775 }
3777 *coloffset += mbwidth;
3779 return string - start;
3780 }
3783 /*
3784 * Status management
3785 */
3787 /* Whether or not the curses interface has been initialized. */
3788 static bool cursed = FALSE;
3790 /* The status window is used for polling keystrokes. */
3791 static WINDOW *status_win;
3793 static bool status_empty = TRUE;
3795 /* Update status and title window. */
3796 static void
3797 report(const char *msg, ...)
3798 {
3799 struct view *view = display[current_view];
3801 if (input_mode)
3802 return;
3804 if (!status_empty || *msg) {
3805 va_list args;
3807 va_start(args, msg);
3809 wmove(status_win, 0, 0);
3810 if (*msg) {
3811 vwprintw(status_win, msg, args);
3812 status_empty = FALSE;
3813 } else {
3814 status_empty = TRUE;
3815 }
3816 wclrtoeol(status_win);
3817 wrefresh(status_win);
3819 va_end(args);
3820 }
3822 update_view_title(view);
3823 update_display_cursor(view);
3824 }
3826 /* Controls when nodelay should be in effect when polling user input. */
3827 static void
3828 set_nonblocking_input(bool loading)
3829 {
3830 static unsigned int loading_views;
3832 if ((loading == FALSE && loading_views-- == 1) ||
3833 (loading == TRUE && loading_views++ == 0))
3834 nodelay(status_win, loading);
3835 }
3837 static void
3838 init_display(void)
3839 {
3840 int x, y;
3842 /* Initialize the curses library */
3843 if (isatty(STDIN_FILENO)) {
3844 cursed = !!initscr();
3845 } else {
3846 /* Leave stdin and stdout alone when acting as a pager. */
3847 FILE *io = fopen("/dev/tty", "r+");
3849 if (!io)
3850 die("Failed to open /dev/tty");
3851 cursed = !!newterm(NULL, io, io);
3852 }
3854 if (!cursed)
3855 die("Failed to initialize curses");
3857 nonl(); /* Tell curses not to do NL->CR/NL on output */
3858 cbreak(); /* Take input chars one at a time, no wait for \n */
3859 noecho(); /* Don't echo input */
3860 leaveok(stdscr, TRUE);
3862 if (has_colors())
3863 init_colors();
3865 getmaxyx(stdscr, y, x);
3866 status_win = newwin(1, 0, y - 1, 0);
3867 if (!status_win)
3868 die("Failed to create status window");
3870 /* Enable keyboard mapping */
3871 keypad(status_win, TRUE);
3872 wbkgdset(status_win, get_line_attr(LINE_STATUS));
3873 }
3875 static char *
3876 read_prompt(const char *prompt)
3877 {
3878 enum { READING, STOP, CANCEL } status = READING;
3879 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
3880 int pos = 0;
3882 while (status == READING) {
3883 struct view *view;
3884 int i, key;
3886 input_mode = TRUE;
3888 foreach_view (view, i)
3889 update_view(view);
3891 input_mode = FALSE;
3893 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
3894 wclrtoeol(status_win);
3896 /* Refresh, accept single keystroke of input */
3897 key = wgetch(status_win);
3898 switch (key) {
3899 case KEY_RETURN:
3900 case KEY_ENTER:
3901 case '\n':
3902 status = pos ? STOP : CANCEL;
3903 break;
3905 case KEY_BACKSPACE:
3906 if (pos > 0)
3907 pos--;
3908 else
3909 status = CANCEL;
3910 break;
3912 case KEY_ESC:
3913 status = CANCEL;
3914 break;
3916 case ERR:
3917 break;
3919 default:
3920 if (pos >= sizeof(buf)) {
3921 report("Input string too long");
3922 return NULL;
3923 }
3925 if (isprint(key))
3926 buf[pos++] = (char) key;
3927 }
3928 }
3930 /* Clear the status window */
3931 status_empty = FALSE;
3932 report("");
3934 if (status == CANCEL)
3935 return NULL;
3937 buf[pos++] = 0;
3939 return buf;
3940 }
3942 /*
3943 * Repository references
3944 */
3946 static struct ref *refs;
3947 static size_t refs_size;
3949 /* Id <-> ref store */
3950 static struct ref ***id_refs;
3951 static size_t id_refs_size;
3953 static struct ref **
3954 get_refs(char *id)
3955 {
3956 struct ref ***tmp_id_refs;
3957 struct ref **ref_list = NULL;
3958 size_t ref_list_size = 0;
3959 size_t i;
3961 for (i = 0; i < id_refs_size; i++)
3962 if (!strcmp(id, id_refs[i][0]->id))
3963 return id_refs[i];
3965 tmp_id_refs = realloc(id_refs, (id_refs_size + 1) * sizeof(*id_refs));
3966 if (!tmp_id_refs)
3967 return NULL;
3969 id_refs = tmp_id_refs;
3971 for (i = 0; i < refs_size; i++) {
3972 struct ref **tmp;
3974 if (strcmp(id, refs[i].id))
3975 continue;
3977 tmp = realloc(ref_list, (ref_list_size + 1) * sizeof(*ref_list));
3978 if (!tmp) {
3979 if (ref_list)
3980 free(ref_list);
3981 return NULL;
3982 }
3984 ref_list = tmp;
3985 if (ref_list_size > 0)
3986 ref_list[ref_list_size - 1]->next = 1;
3987 ref_list[ref_list_size] = &refs[i];
3989 /* XXX: The properties of the commit chains ensures that we can
3990 * safely modify the shared ref. The repo references will
3991 * always be similar for the same id. */
3992 ref_list[ref_list_size]->next = 0;
3993 ref_list_size++;
3994 }
3996 if (ref_list)
3997 id_refs[id_refs_size++] = ref_list;
3999 return ref_list;
4000 }
4002 static int
4003 read_ref(char *id, size_t idlen, char *name, size_t namelen)
4004 {
4005 struct ref *ref;
4006 bool tag = FALSE;
4007 bool remote = FALSE;
4009 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
4010 /* Commits referenced by tags has "^{}" appended. */
4011 if (name[namelen - 1] != '}')
4012 return OK;
4014 while (namelen > 0 && name[namelen] != '^')
4015 namelen--;
4017 tag = TRUE;
4018 namelen -= STRING_SIZE("refs/tags/");
4019 name += STRING_SIZE("refs/tags/");
4021 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
4022 remote = TRUE;
4023 namelen -= STRING_SIZE("refs/remotes/");
4024 name += STRING_SIZE("refs/remotes/");
4026 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
4027 namelen -= STRING_SIZE("refs/heads/");
4028 name += STRING_SIZE("refs/heads/");
4030 } else if (!strcmp(name, "HEAD")) {
4031 return OK;
4032 }
4034 refs = realloc(refs, sizeof(*refs) * (refs_size + 1));
4035 if (!refs)
4036 return ERR;
4038 ref = &refs[refs_size++];
4039 ref->name = malloc(namelen + 1);
4040 if (!ref->name)
4041 return ERR;
4043 strncpy(ref->name, name, namelen);
4044 ref->name[namelen] = 0;
4045 ref->tag = tag;
4046 ref->remote = remote;
4047 string_copy_rev(ref->id, id);
4049 return OK;
4050 }
4052 static int
4053 load_refs(void)
4054 {
4055 const char *cmd_env = getenv("TIG_LS_REMOTE");
4056 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
4058 return read_properties(popen(cmd, "r"), "\t", read_ref);
4059 }
4061 static int
4062 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
4063 {
4064 if (!strcmp(name, "i18n.commitencoding"))
4065 string_ncopy(opt_encoding, value, valuelen);
4067 return OK;
4068 }
4070 static int
4071 load_repo_config(void)
4072 {
4073 return read_properties(popen("git repo-config --list", "r"),
4074 "=", read_repo_config_option);
4075 }
4077 static int
4078 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
4079 {
4080 if (!opt_git_dir[0])
4081 string_ncopy(opt_git_dir, name, namelen);
4082 else
4083 string_ncopy(opt_cdup, name, namelen);
4084 return OK;
4085 }
4087 /* XXX: The line outputted by "--show-cdup" can be empty so the option
4088 * must be the last one! */
4089 static int
4090 load_repo_info(void)
4091 {
4092 return read_properties(popen("git rev-parse --git-dir --show-cdup 2>/dev/null", "r"),
4093 "=", read_repo_info);
4094 }
4096 static int
4097 read_properties(FILE *pipe, const char *separators,
4098 int (*read_property)(char *, size_t, char *, size_t))
4099 {
4100 char buffer[BUFSIZ];
4101 char *name;
4102 int state = OK;
4104 if (!pipe)
4105 return ERR;
4107 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
4108 char *value;
4109 size_t namelen;
4110 size_t valuelen;
4112 name = chomp_string(name);
4113 namelen = strcspn(name, separators);
4115 if (name[namelen]) {
4116 name[namelen] = 0;
4117 value = chomp_string(name + namelen + 1);
4118 valuelen = strlen(value);
4120 } else {
4121 value = "";
4122 valuelen = 0;
4123 }
4125 state = read_property(name, namelen, value, valuelen);
4126 }
4128 if (state != ERR && ferror(pipe))
4129 state = ERR;
4131 pclose(pipe);
4133 return state;
4134 }
4137 /*
4138 * Main
4139 */
4141 static void __NORETURN
4142 quit(int sig)
4143 {
4144 /* XXX: Restore tty modes and let the OS cleanup the rest! */
4145 if (cursed)
4146 endwin();
4147 exit(0);
4148 }
4150 static void __NORETURN
4151 die(const char *err, ...)
4152 {
4153 va_list args;
4155 endwin();
4157 va_start(args, err);
4158 fputs("tig: ", stderr);
4159 vfprintf(stderr, err, args);
4160 fputs("\n", stderr);
4161 va_end(args);
4163 exit(1);
4164 }
4166 int
4167 main(int argc, char *argv[])
4168 {
4169 struct view *view;
4170 enum request request;
4171 size_t i;
4173 signal(SIGINT, quit);
4175 if (setlocale(LC_ALL, "")) {
4176 char *codeset = nl_langinfo(CODESET);
4178 string_ncopy(opt_codeset, codeset, strlen(codeset));
4179 }
4181 if (load_repo_info() == ERR)
4182 die("Failed to load repo info.");
4184 /* Require a git repository unless when running in pager mode. */
4185 if (!opt_git_dir[0])
4186 die("Not a git repository");
4188 if (load_options() == ERR)
4189 die("Failed to load user config.");
4191 /* Load the repo config file so options can be overwritten from
4192 * the command line. */
4193 if (load_repo_config() == ERR)
4194 die("Failed to load repo config.");
4196 if (!parse_options(argc, argv))
4197 return 0;
4199 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
4200 opt_iconv = iconv_open(opt_codeset, opt_encoding);
4201 if (opt_iconv == ICONV_NONE)
4202 die("Failed to initialize character set conversion");
4203 }
4205 if (load_refs() == ERR)
4206 die("Failed to load refs.");
4208 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
4209 view->cmd_env = getenv(view->cmd_env);
4211 request = opt_request;
4213 init_display();
4215 while (view_driver(display[current_view], request)) {
4216 int key;
4217 int i;
4219 foreach_view (view, i)
4220 update_view(view);
4222 /* Refresh, accept single keystroke of input */
4223 key = wgetch(status_win);
4225 /* wgetch() with nodelay() enabled returns ERR when there's no
4226 * input. */
4227 if (key == ERR) {
4228 request = REQ_NONE;
4229 continue;
4230 }
4232 request = get_keybinding(display[current_view]->keymap, key);
4234 /* Some low-level request handling. This keeps access to
4235 * status_win restricted. */
4236 switch (request) {
4237 case REQ_PROMPT:
4238 {
4239 char *cmd = read_prompt(":");
4241 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
4242 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
4243 opt_request = REQ_VIEW_DIFF;
4244 } else {
4245 opt_request = REQ_VIEW_PAGER;
4246 }
4247 break;
4248 }
4250 request = REQ_NONE;
4251 break;
4252 }
4253 case REQ_SEARCH:
4254 case REQ_SEARCH_BACK:
4255 {
4256 const char *prompt = request == REQ_SEARCH
4257 ? "/" : "?";
4258 char *search = read_prompt(prompt);
4260 if (search)
4261 string_ncopy(opt_search, search, strlen(search));
4262 else
4263 request = REQ_NONE;
4264 break;
4265 }
4266 case REQ_SCREEN_RESIZE:
4267 {
4268 int height, width;
4270 getmaxyx(stdscr, height, width);
4272 /* Resize the status view and let the view driver take
4273 * care of resizing the displayed views. */
4274 wresize(status_win, 1, width);
4275 mvwin(status_win, height - 1, 0);
4276 wrefresh(status_win);
4277 break;
4278 }
4279 default:
4280 break;
4281 }
4282 }
4284 quit(0);
4286 return 0;
4287 }