Various fixes and improvements of the new blame view
[tig] / tig.c
1 /* Copyright (c) 2006-2008 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 */
13
14 #ifdef HAVE_CONFIG_H
15 #include "config.h"
16 #endif
17
18 #ifndef TIG_VERSION
19 #define TIG_VERSION "unknown-version"
20 #endif
21
22 #ifndef DEBUG
23 #define NDEBUG
24 #endif
25
26 #include <assert.h>
27 #include <errno.h>
28 #include <ctype.h>
29 #include <signal.h>
30 #include <stdarg.h>
31 #include <stdio.h>
32 #include <stdlib.h>
33 #include <string.h>
34 #include <sys/types.h>
35 #include <sys/stat.h>
36 #include <unistd.h>
37 #include <time.h>
38
39 #include <regex.h>
40
41 #include <locale.h>
42 #include <langinfo.h>
43 #include <iconv.h>
44
45 /* ncurses(3): Must be defined to have extended wide-character functions. */
46 #define _XOPEN_SOURCE_EXTENDED
47
48 #include <curses.h>
49
50 #if __GNUC__ >= 3
51 #define __NORETURN __attribute__((__noreturn__))
52 #else
53 #define __NORETURN
54 #endif
55
56 static void __NORETURN die(const char *err, ...);
57 static void warn(const char *msg, ...);
58 static void report(const char *msg, ...);
59 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, size_t, char *, size_t));
60 static void set_nonblocking_input(bool loading);
61 static size_t utf8_length(const char *string, size_t max_width, int *trimmed, bool reserve);
62
63 #define ABS(x) ((x) >= 0 ? (x) : -(x))
64 #define MIN(x, y) ((x) < (y) ? (x) : (y))
65
66 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
67 #define STRING_SIZE(x) (sizeof(x) - 1)
68
69 #define SIZEOF_STR 1024 /* Default string size. */
70 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
71 #define SIZEOF_REV 41 /* Holds a SHA-1 and an ending NUL */
72
73 /* Revision graph */
74
75 #define REVGRAPH_INIT 'I'
76 #define REVGRAPH_MERGE 'M'
77 #define REVGRAPH_BRANCH '+'
78 #define REVGRAPH_COMMIT '*'
79 #define REVGRAPH_BOUND '^'
80 #define REVGRAPH_LINE '|'
81
82 #define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
83
84 /* This color name can be used to refer to the default term colors. */
85 #define COLOR_DEFAULT (-1)
86
87 #define ICONV_NONE ((iconv_t) -1)
88 #ifndef ICONV_CONST
89 #define ICONV_CONST /* nothing */
90 #endif
91
92 /* The format and size of the date column in the main view. */
93 #define DATE_FORMAT "%Y-%m-%d %H:%M"
94 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
95
96 #define AUTHOR_COLS 20
97 #define ID_COLS 8
98
99 /* The default interval between line numbers. */
100 #define NUMBER_INTERVAL 5
101
102 #define TABSIZE 8
103
104 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
105
106 #ifndef GIT_CONFIG
107 #define GIT_CONFIG "git config"
108 #endif
109
110 #define TIG_LS_REMOTE \
111 "git ls-remote $(git rev-parse --git-dir) 2>/dev/null"
112
113 #define TIG_DIFF_CMD \
114 "git show --pretty=fuller --no-color --root --patch-with-stat --find-copies-harder -C %s 2>/dev/null"
115
116 #define TIG_LOG_CMD \
117 "git log --no-color --cc --stat -n100 %s 2>/dev/null"
118
119 #define TIG_MAIN_CMD \
120 "git log --no-color --topo-order --parents --boundary --pretty=raw %s 2>/dev/null"
121
122 #define TIG_TREE_CMD \
123 "git ls-tree %s %s"
124
125 #define TIG_BLOB_CMD \
126 "git cat-file blob %s"
127
128 /* XXX: Needs to be defined to the empty string. */
129 #define TIG_HELP_CMD ""
130 #define TIG_PAGER_CMD ""
131 #define TIG_STATUS_CMD ""
132 #define TIG_STAGE_CMD ""
133 #define TIG_BLAME_CMD ""
134
135 /* Some ascii-shorthands fitted into the ncurses namespace. */
136 #define KEY_TAB '\t'
137 #define KEY_RETURN '\r'
138 #define KEY_ESC 27
139
140
141 struct ref {
142 char *name; /* Ref name; tag or head names are shortened. */
143 char id[SIZEOF_REV]; /* Commit SHA1 ID */
144 unsigned int tag:1; /* Is it a tag? */
145 unsigned int ltag:1; /* If so, is the tag local? */
146 unsigned int remote:1; /* Is it a remote ref? */
147 unsigned int next:1; /* For ref lists: are there more refs? */
148 };
149
150 static struct ref **get_refs(char *id);
151
152 struct int_map {
153 const char *name;
154 int namelen;
155 int value;
156 };
157
158 static int
159 set_from_int_map(struct int_map *map, size_t map_size,
160 int *value, const char *name, int namelen)
161 {
162
163 int i;
164
165 for (i = 0; i < map_size; i++)
166 if (namelen == map[i].namelen &&
167 !strncasecmp(name, map[i].name, namelen)) {
168 *value = map[i].value;
169 return OK;
170 }
171
172 return ERR;
173 }
174
175
176 /*
177 * String helpers
178 */
179
180 static inline void
181 string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
182 {
183 if (srclen > dstlen - 1)
184 srclen = dstlen - 1;
185
186 strncpy(dst, src, srclen);
187 dst[srclen] = 0;
188 }
189
190 /* Shorthands for safely copying into a fixed buffer. */
191
192 #define string_copy(dst, src) \
193 string_ncopy_do(dst, sizeof(dst), src, sizeof(src))
194
195 #define string_ncopy(dst, src, srclen) \
196 string_ncopy_do(dst, sizeof(dst), src, srclen)
197
198 #define string_copy_rev(dst, src) \
199 string_ncopy_do(dst, SIZEOF_REV, src, SIZEOF_REV - 1)
200
201 #define string_add(dst, from, src) \
202 string_ncopy_do(dst + (from), sizeof(dst) - (from), src, sizeof(src))
203
204 static char *
205 chomp_string(char *name)
206 {
207 int namelen;
208
209 while (isspace(*name))
210 name++;
211
212 namelen = strlen(name) - 1;
213 while (namelen > 0 && isspace(name[namelen]))
214 name[namelen--] = 0;
215
216 return name;
217 }
218
219 static bool
220 string_nformat(char *buf, size_t bufsize, size_t *bufpos, const char *fmt, ...)
221 {
222 va_list args;
223 size_t pos = bufpos ? *bufpos : 0;
224
225 va_start(args, fmt);
226 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
227 va_end(args);
228
229 if (bufpos)
230 *bufpos = pos;
231
232 return pos >= bufsize ? FALSE : TRUE;
233 }
234
235 #define string_format(buf, fmt, args...) \
236 string_nformat(buf, sizeof(buf), NULL, fmt, args)
237
238 #define string_format_from(buf, from, fmt, args...) \
239 string_nformat(buf, sizeof(buf), from, fmt, args)
240
241 static int
242 string_enum_compare(const char *str1, const char *str2, int len)
243 {
244 size_t i;
245
246 #define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
247
248 /* Diff-Header == DIFF_HEADER */
249 for (i = 0; i < len; i++) {
250 if (toupper(str1[i]) == toupper(str2[i]))
251 continue;
252
253 if (string_enum_sep(str1[i]) &&
254 string_enum_sep(str2[i]))
255 continue;
256
257 return str1[i] - str2[i];
258 }
259
260 return 0;
261 }
262
263 /* Shell quoting
264 *
265 * NOTE: The following is a slightly modified copy of the git project's shell
266 * quoting routines found in the quote.c file.
267 *
268 * Help to copy the thing properly quoted for the shell safety. any single
269 * quote is replaced with '\'', any exclamation point is replaced with '\!',
270 * and the whole thing is enclosed in a
271 *
272 * E.g.
273 * original sq_quote result
274 * name ==> name ==> 'name'
275 * a b ==> a b ==> 'a b'
276 * a'b ==> a'\''b ==> 'a'\''b'
277 * a!b ==> a'\!'b ==> 'a'\!'b'
278 */
279
280 static size_t
281 sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
282 {
283 char c;
284
285 #define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0)
286
287 BUFPUT('\'');
288 while ((c = *src++)) {
289 if (c == '\'' || c == '!') {
290 BUFPUT('\'');
291 BUFPUT('\\');
292 BUFPUT(c);
293 BUFPUT('\'');
294 } else {
295 BUFPUT(c);
296 }
297 }
298 BUFPUT('\'');
299
300 if (bufsize < SIZEOF_STR)
301 buf[bufsize] = 0;
302
303 return bufsize;
304 }
305
306
307 /*
308 * User requests
309 */
310
311 #define REQ_INFO \
312 /* XXX: Keep the view request first and in sync with views[]. */ \
313 REQ_GROUP("View switching") \
314 REQ_(VIEW_MAIN, "Show main view"), \
315 REQ_(VIEW_DIFF, "Show diff view"), \
316 REQ_(VIEW_LOG, "Show log view"), \
317 REQ_(VIEW_TREE, "Show tree view"), \
318 REQ_(VIEW_BLOB, "Show blob view"), \
319 REQ_(VIEW_BLAME, "Show blame view"), \
320 REQ_(VIEW_HELP, "Show help page"), \
321 REQ_(VIEW_PAGER, "Show pager view"), \
322 REQ_(VIEW_STATUS, "Show status view"), \
323 REQ_(VIEW_STAGE, "Show stage view"), \
324 \
325 REQ_GROUP("View manipulation") \
326 REQ_(ENTER, "Enter current line and scroll"), \
327 REQ_(NEXT, "Move to next"), \
328 REQ_(PREVIOUS, "Move to previous"), \
329 REQ_(VIEW_NEXT, "Move focus to next view"), \
330 REQ_(REFRESH, "Reload and refresh"), \
331 REQ_(VIEW_CLOSE, "Close the current view"), \
332 REQ_(QUIT, "Close all views and quit"), \
333 \
334 REQ_GROUP("Cursor navigation") \
335 REQ_(MOVE_UP, "Move cursor one line up"), \
336 REQ_(MOVE_DOWN, "Move cursor one line down"), \
337 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
338 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
339 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
340 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
341 \
342 REQ_GROUP("Scrolling") \
343 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
344 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
345 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
346 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
347 \
348 REQ_GROUP("Searching") \
349 REQ_(SEARCH, "Search the view"), \
350 REQ_(SEARCH_BACK, "Search backwards in the view"), \
351 REQ_(FIND_NEXT, "Find next search match"), \
352 REQ_(FIND_PREV, "Find previous search match"), \
353 \
354 REQ_GROUP("Misc") \
355 REQ_(PROMPT, "Bring up the prompt"), \
356 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
357 REQ_(SCREEN_RESIZE, "Resize the screen"), \
358 REQ_(SHOW_VERSION, "Show version information"), \
359 REQ_(STOP_LOADING, "Stop all loading views"), \
360 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
361 REQ_(TOGGLE_DATE, "Toggle date display"), \
362 REQ_(TOGGLE_AUTHOR, "Toggle author display"), \
363 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"), \
364 REQ_(TOGGLE_REFS, "Toggle reference display (tags/branches)"), \
365 REQ_(STATUS_UPDATE, "Update file status"), \
366 REQ_(STATUS_MERGE, "Merge file using external tool"), \
367 REQ_(TREE_PARENT, "Switch to parent directory in tree view"), \
368 REQ_(EDIT, "Open in editor"), \
369 REQ_(NONE, "Do nothing")
370
371
372 /* User action requests. */
373 enum request {
374 #define REQ_GROUP(help)
375 #define REQ_(req, help) REQ_##req
376
377 /* Offset all requests to avoid conflicts with ncurses getch values. */
378 REQ_OFFSET = KEY_MAX + 1,
379 REQ_INFO
380
381 #undef REQ_GROUP
382 #undef REQ_
383 };
384
385 struct request_info {
386 enum request request;
387 char *name;
388 int namelen;
389 char *help;
390 };
391
392 static struct request_info req_info[] = {
393 #define REQ_GROUP(help) { 0, NULL, 0, (help) },
394 #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
395 REQ_INFO
396 #undef REQ_GROUP
397 #undef REQ_
398 };
399
400 static enum request
401 get_request(const char *name)
402 {
403 int namelen = strlen(name);
404 int i;
405
406 for (i = 0; i < ARRAY_SIZE(req_info); i++)
407 if (req_info[i].namelen == namelen &&
408 !string_enum_compare(req_info[i].name, name, namelen))
409 return req_info[i].request;
410
411 return REQ_NONE;
412 }
413
414
415 /*
416 * Options
417 */
418
419 static const char usage[] =
420 "tig " TIG_VERSION " (" __DATE__ ")\n"
421 "\n"
422 "Usage: tig [options] [revs] [--] [paths]\n"
423 " or: tig show [options] [revs] [--] [paths]\n"
424 " or: tig blame [rev] path\n"
425 " or: tig status\n"
426 " or: tig < [git command output]\n"
427 "\n"
428 "Options:\n"
429 " -v, --version Show version and exit\n"
430 " -h, --help Show help message and exit";
431
432 /* Option and state variables. */
433 static bool opt_date = TRUE;
434 static bool opt_author = TRUE;
435 static bool opt_line_number = FALSE;
436 static bool opt_rev_graph = FALSE;
437 static bool opt_show_refs = TRUE;
438 static int opt_num_interval = NUMBER_INTERVAL;
439 static int opt_tab_size = TABSIZE;
440 static enum request opt_request = REQ_VIEW_MAIN;
441 static char opt_cmd[SIZEOF_STR] = "";
442 static char opt_path[SIZEOF_STR] = "";
443 static char opt_file[SIZEOF_STR] = "";
444 static char opt_ref[SIZEOF_REF] = "";
445 static FILE *opt_pipe = NULL;
446 static char opt_encoding[20] = "UTF-8";
447 static bool opt_utf8 = TRUE;
448 static char opt_codeset[20] = "UTF-8";
449 static iconv_t opt_iconv = ICONV_NONE;
450 static char opt_search[SIZEOF_STR] = "";
451 static char opt_cdup[SIZEOF_STR] = "";
452 static char opt_git_dir[SIZEOF_STR] = "";
453 static signed char opt_is_inside_work_tree = -1; /* set to TRUE or FALSE */
454 static char opt_editor[SIZEOF_STR] = "";
455
456 enum option_type {
457 OPT_NONE,
458 OPT_INT,
459 };
460
461 static bool
462 check_option(char *opt, char short_name, char *name, enum option_type type, ...)
463 {
464 va_list args;
465 char *value = "";
466 int *number;
467
468 if (opt[0] != '-')
469 return FALSE;
470
471 if (opt[1] == '-') {
472 int namelen = strlen(name);
473
474 opt += 2;
475
476 if (strncmp(opt, name, namelen))
477 return FALSE;
478
479 if (opt[namelen] == '=')
480 value = opt + namelen + 1;
481
482 } else {
483 if (!short_name || opt[1] != short_name)
484 return FALSE;
485 value = opt + 2;
486 }
487
488 va_start(args, type);
489 if (type == OPT_INT) {
490 number = va_arg(args, int *);
491 if (isdigit(*value))
492 *number = atoi(value);
493 }
494 va_end(args);
495
496 return TRUE;
497 }
498
499 /* Returns the index of log or diff command or -1 to exit. */
500 static bool
501 parse_options(int argc, char *argv[])
502 {
503 char *altargv[1024];
504 int altargc = 0;
505 char *subcommand;
506 int i;
507
508 if (argc <= 1)
509 return TRUE;
510
511 subcommand = argv[1];
512 if (!strcmp(subcommand, "status")) {
513 opt_request = REQ_VIEW_STATUS;
514 if (argc > 2)
515 warn("ignoring arguments after `%s'", subcommand);
516 return TRUE;
517
518 } else if (!strcmp(subcommand, "blame")) {
519 opt_request = REQ_VIEW_BLAME;
520 if (argc <= 2 || argc > 4)
521 die("invalid number of options to blame\n\n%s", usage);
522
523 i = 2;
524 if (argc == 4) {
525 string_ncopy(opt_ref, argv[i], strlen(argv[i]));
526 i++;
527 }
528
529 string_ncopy(opt_file, argv[i], strlen(argv[i]));
530 return TRUE;
531
532 } else if (!strcmp(subcommand, "show")) {
533 opt_request = REQ_VIEW_DIFF;
534
535 } else if (!strcmp(subcommand, "log") || !strcmp(subcommand, "diff")) {
536 opt_request = subcommand[0] == 'l'
537 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
538 warn("`tig %s' has been deprecated", subcommand);
539
540 } else {
541 subcommand = NULL;
542 }
543
544 for (i = 1 + !!subcommand; i < argc; i++) {
545 char *opt = argv[i];
546
547 if (opt[0] && opt[0] != '-')
548 break;
549
550 if (!strcmp(opt, "--")) {
551 i++;
552 break;
553 }
554
555 if (check_option(opt, 'v', "version", OPT_NONE)) {
556 printf("tig version %s\n", TIG_VERSION);
557 return FALSE;
558 }
559
560 if (check_option(opt, 'h', "help", OPT_NONE)) {
561 printf(usage);
562 return FALSE;
563 }
564
565 if (!strcmp(opt, "-S")) {
566 warn("`%s' has been deprecated; use `tig status' instead", opt);
567 opt_request = REQ_VIEW_STATUS;
568 continue;
569 }
570
571 if (!strcmp(opt, "-l")) {
572 opt_request = REQ_VIEW_LOG;
573 } else if (!strcmp(opt, "-d")) {
574 opt_request = REQ_VIEW_DIFF;
575 } else if (check_option(opt, 'n', "line-number", OPT_INT, &opt_num_interval)) {
576 opt_line_number = TRUE;
577 } else if (check_option(opt, 'b', "tab-size", OPT_INT, &opt_tab_size)) {
578 opt_tab_size = MIN(opt_tab_size, TABSIZE);
579 } else {
580 if (altargc >= ARRAY_SIZE(altargv))
581 die("maximum number of arguments exceeded");
582 altargv[altargc++] = opt;
583 continue;
584 }
585
586 warn("`%s' has been deprecated", opt);
587 }
588
589 if (!isatty(STDIN_FILENO)) {
590 opt_request = REQ_VIEW_PAGER;
591 opt_pipe = stdin;
592
593 } else if (i < argc || altargc > 0) {
594 int alti = 0;
595 size_t buf_size;
596
597 if (opt_request == REQ_VIEW_MAIN)
598 /* XXX: This is vulnerable to the user overriding
599 * options required for the main view parser. */
600 string_copy(opt_cmd, "git log --no-color --pretty=raw --boundary --parents");
601 else
602 string_format(opt_cmd, "git %s", subcommand);
603 buf_size = strlen(opt_cmd);
604
605 while (buf_size < sizeof(opt_cmd) && alti < altargc) {
606 opt_cmd[buf_size++] = ' ';
607 buf_size = sq_quote(opt_cmd, buf_size, altargv[alti++]);
608 }
609
610 while (buf_size < sizeof(opt_cmd) && i < argc) {
611 opt_cmd[buf_size++] = ' ';
612 buf_size = sq_quote(opt_cmd, buf_size, argv[i++]);
613 }
614
615 if (buf_size >= sizeof(opt_cmd))
616 die("command too long");
617
618 opt_cmd[buf_size] = 0;
619 }
620
621 return TRUE;
622 }
623
624
625 /*
626 * Line-oriented content detection.
627 */
628
629 #define LINE_INFO \
630 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
631 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
632 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
633 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
634 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
635 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
636 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
637 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
638 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
639 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
640 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
641 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
642 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
643 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
644 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
645 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
646 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
647 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
648 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
649 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
650 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
651 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
652 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
653 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
654 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
655 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
656 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
657 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
658 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
659 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
660 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
661 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
662 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
663 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
664 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
665 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
666 LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
667 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
668 LINE(MAIN_LOCAL_TAG,"", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
669 LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
670 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
671 LINE(MAIN_REVGRAPH,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
672 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
673 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
674 LINE(STAT_SECTION, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
675 LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
676 LINE(STAT_STAGED, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
677 LINE(STAT_UNSTAGED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
678 LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
679 LINE(BLAME_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
680 LINE(BLAME_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
681 LINE(BLAME_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
682 LINE(BLAME_ID, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
683 LINE(BLAME_LINENO, "", COLOR_CYAN, COLOR_DEFAULT, 0)
684
685 enum line_type {
686 #define LINE(type, line, fg, bg, attr) \
687 LINE_##type
688 LINE_INFO
689 #undef LINE
690 };
691
692 struct line_info {
693 const char *name; /* Option name. */
694 int namelen; /* Size of option name. */
695 const char *line; /* The start of line to match. */
696 int linelen; /* Size of string to match. */
697 int fg, bg, attr; /* Color and text attributes for the lines. */
698 };
699
700 static struct line_info line_info[] = {
701 #define LINE(type, line, fg, bg, attr) \
702 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
703 LINE_INFO
704 #undef LINE
705 };
706
707 static enum line_type
708 get_line_type(char *line)
709 {
710 int linelen = strlen(line);
711 enum line_type type;
712
713 for (type = 0; type < ARRAY_SIZE(line_info); type++)
714 /* Case insensitive search matches Signed-off-by lines better. */
715 if (linelen >= line_info[type].linelen &&
716 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
717 return type;
718
719 return LINE_DEFAULT;
720 }
721
722 static inline int
723 get_line_attr(enum line_type type)
724 {
725 assert(type < ARRAY_SIZE(line_info));
726 return COLOR_PAIR(type) | line_info[type].attr;
727 }
728
729 static struct line_info *
730 get_line_info(char *name, int namelen)
731 {
732 enum line_type type;
733
734 for (type = 0; type < ARRAY_SIZE(line_info); type++)
735 if (namelen == line_info[type].namelen &&
736 !string_enum_compare(line_info[type].name, name, namelen))
737 return &line_info[type];
738
739 return NULL;
740 }
741
742 static void
743 init_colors(void)
744 {
745 int default_bg = line_info[LINE_DEFAULT].bg;
746 int default_fg = line_info[LINE_DEFAULT].fg;
747 enum line_type type;
748
749 start_color();
750
751 if (assume_default_colors(default_fg, default_bg) == ERR) {
752 default_bg = COLOR_BLACK;
753 default_fg = COLOR_WHITE;
754 }
755
756 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
757 struct line_info *info = &line_info[type];
758 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
759 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
760
761 init_pair(type, fg, bg);
762 }
763 }
764
765 struct line {
766 enum line_type type;
767
768 /* State flags */
769 unsigned int selected:1;
770 unsigned int dirty:1;
771
772 void *data; /* User data */
773 };
774
775
776 /*
777 * Keys
778 */
779
780 struct keybinding {
781 int alias;
782 enum request request;
783 struct keybinding *next;
784 };
785
786 static struct keybinding default_keybindings[] = {
787 /* View switching */
788 { 'm', REQ_VIEW_MAIN },
789 { 'd', REQ_VIEW_DIFF },
790 { 'l', REQ_VIEW_LOG },
791 { 't', REQ_VIEW_TREE },
792 { 'f', REQ_VIEW_BLOB },
793 { 'B', REQ_VIEW_BLAME },
794 { 'p', REQ_VIEW_PAGER },
795 { 'h', REQ_VIEW_HELP },
796 { 'S', REQ_VIEW_STATUS },
797 { 'c', REQ_VIEW_STAGE },
798
799 /* View manipulation */
800 { 'q', REQ_VIEW_CLOSE },
801 { KEY_TAB, REQ_VIEW_NEXT },
802 { KEY_RETURN, REQ_ENTER },
803 { KEY_UP, REQ_PREVIOUS },
804 { KEY_DOWN, REQ_NEXT },
805 { 'R', REQ_REFRESH },
806
807 /* Cursor navigation */
808 { 'k', REQ_MOVE_UP },
809 { 'j', REQ_MOVE_DOWN },
810 { KEY_HOME, REQ_MOVE_FIRST_LINE },
811 { KEY_END, REQ_MOVE_LAST_LINE },
812 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
813 { ' ', REQ_MOVE_PAGE_DOWN },
814 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
815 { 'b', REQ_MOVE_PAGE_UP },
816 { '-', REQ_MOVE_PAGE_UP },
817
818 /* Scrolling */
819 { KEY_IC, REQ_SCROLL_LINE_UP },
820 { KEY_DC, REQ_SCROLL_LINE_DOWN },
821 { 'w', REQ_SCROLL_PAGE_UP },
822 { 's', REQ_SCROLL_PAGE_DOWN },
823
824 /* Searching */
825 { '/', REQ_SEARCH },
826 { '?', REQ_SEARCH_BACK },
827 { 'n', REQ_FIND_NEXT },
828 { 'N', REQ_FIND_PREV },
829
830 /* Misc */
831 { 'Q', REQ_QUIT },
832 { 'z', REQ_STOP_LOADING },
833 { 'v', REQ_SHOW_VERSION },
834 { 'r', REQ_SCREEN_REDRAW },
835 { '.', REQ_TOGGLE_LINENO },
836 { 'D', REQ_TOGGLE_DATE },
837 { 'A', REQ_TOGGLE_AUTHOR },
838 { 'g', REQ_TOGGLE_REV_GRAPH },
839 { 'F', REQ_TOGGLE_REFS },
840 { ':', REQ_PROMPT },
841 { 'u', REQ_STATUS_UPDATE },
842 { 'M', REQ_STATUS_MERGE },
843 { ',', REQ_TREE_PARENT },
844 { 'e', REQ_EDIT },
845
846 /* Using the ncurses SIGWINCH handler. */
847 { KEY_RESIZE, REQ_SCREEN_RESIZE },
848 };
849
850 #define KEYMAP_INFO \
851 KEYMAP_(GENERIC), \
852 KEYMAP_(MAIN), \
853 KEYMAP_(DIFF), \
854 KEYMAP_(LOG), \
855 KEYMAP_(TREE), \
856 KEYMAP_(BLOB), \
857 KEYMAP_(BLAME), \
858 KEYMAP_(PAGER), \
859 KEYMAP_(HELP), \
860 KEYMAP_(STATUS), \
861 KEYMAP_(STAGE)
862
863 enum keymap {
864 #define KEYMAP_(name) KEYMAP_##name
865 KEYMAP_INFO
866 #undef KEYMAP_
867 };
868
869 static struct int_map keymap_table[] = {
870 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
871 KEYMAP_INFO
872 #undef KEYMAP_
873 };
874
875 #define set_keymap(map, name) \
876 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
877
878 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
879
880 static void
881 add_keybinding(enum keymap keymap, enum request request, int key)
882 {
883 struct keybinding *keybinding;
884
885 keybinding = calloc(1, sizeof(*keybinding));
886 if (!keybinding)
887 die("Failed to allocate keybinding");
888
889 keybinding->alias = key;
890 keybinding->request = request;
891 keybinding->next = keybindings[keymap];
892 keybindings[keymap] = keybinding;
893 }
894
895 /* Looks for a key binding first in the given map, then in the generic map, and
896 * lastly in the default keybindings. */
897 static enum request
898 get_keybinding(enum keymap keymap, int key)
899 {
900 struct keybinding *kbd;
901 int i;
902
903 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
904 if (kbd->alias == key)
905 return kbd->request;
906
907 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
908 if (kbd->alias == key)
909 return kbd->request;
910
911 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
912 if (default_keybindings[i].alias == key)
913 return default_keybindings[i].request;
914
915 return (enum request) key;
916 }
917
918
919 struct key {
920 char *name;
921 int value;
922 };
923
924 static struct key key_table[] = {
925 { "Enter", KEY_RETURN },
926 { "Space", ' ' },
927 { "Backspace", KEY_BACKSPACE },
928 { "Tab", KEY_TAB },
929 { "Escape", KEY_ESC },
930 { "Left", KEY_LEFT },
931 { "Right", KEY_RIGHT },
932 { "Up", KEY_UP },
933 { "Down", KEY_DOWN },
934 { "Insert", KEY_IC },
935 { "Delete", KEY_DC },
936 { "Hash", '#' },
937 { "Home", KEY_HOME },
938 { "End", KEY_END },
939 { "PageUp", KEY_PPAGE },
940 { "PageDown", KEY_NPAGE },
941 { "F1", KEY_F(1) },
942 { "F2", KEY_F(2) },
943 { "F3", KEY_F(3) },
944 { "F4", KEY_F(4) },
945 { "F5", KEY_F(5) },
946 { "F6", KEY_F(6) },
947 { "F7", KEY_F(7) },
948 { "F8", KEY_F(8) },
949 { "F9", KEY_F(9) },
950 { "F10", KEY_F(10) },
951 { "F11", KEY_F(11) },
952 { "F12", KEY_F(12) },
953 };
954
955 static int
956 get_key_value(const char *name)
957 {
958 int i;
959
960 for (i = 0; i < ARRAY_SIZE(key_table); i++)
961 if (!strcasecmp(key_table[i].name, name))
962 return key_table[i].value;
963
964 if (strlen(name) == 1 && isprint(*name))
965 return (int) *name;
966
967 return ERR;
968 }
969
970 static char *
971 get_key_name(int key_value)
972 {
973 static char key_char[] = "'X'";
974 char *seq = NULL;
975 int key;
976
977 for (key = 0; key < ARRAY_SIZE(key_table); key++)
978 if (key_table[key].value == key_value)
979 seq = key_table[key].name;
980
981 if (seq == NULL &&
982 key_value < 127 &&
983 isprint(key_value)) {
984 key_char[1] = (char) key_value;
985 seq = key_char;
986 }
987
988 return seq ? seq : "'?'";
989 }
990
991 static char *
992 get_key(enum request request)
993 {
994 static char buf[BUFSIZ];
995 size_t pos = 0;
996 char *sep = "";
997 int i;
998
999 buf[pos] = 0;
1000
1001 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
1002 struct keybinding *keybinding = &default_keybindings[i];
1003
1004 if (keybinding->request != request)
1005 continue;
1006
1007 if (!string_format_from(buf, &pos, "%s%s", sep,
1008 get_key_name(keybinding->alias)))
1009 return "Too many keybindings!";
1010 sep = ", ";
1011 }
1012
1013 return buf;
1014 }
1015
1016 struct run_request {
1017 enum keymap keymap;
1018 int key;
1019 char cmd[SIZEOF_STR];
1020 };
1021
1022 static struct run_request *run_request;
1023 static size_t run_requests;
1024
1025 static enum request
1026 add_run_request(enum keymap keymap, int key, int argc, char **argv)
1027 {
1028 struct run_request *tmp;
1029 struct run_request req = { keymap, key };
1030 size_t bufpos;
1031
1032 for (bufpos = 0; argc > 0; argc--, argv++)
1033 if (!string_format_from(req.cmd, &bufpos, "%s ", *argv))
1034 return REQ_NONE;
1035
1036 req.cmd[bufpos - 1] = 0;
1037
1038 tmp = realloc(run_request, (run_requests + 1) * sizeof(*run_request));
1039 if (!tmp)
1040 return REQ_NONE;
1041
1042 run_request = tmp;
1043 run_request[run_requests++] = req;
1044
1045 return REQ_NONE + run_requests;
1046 }
1047
1048 static struct run_request *
1049 get_run_request(enum request request)
1050 {
1051 if (request <= REQ_NONE)
1052 return NULL;
1053 return &run_request[request - REQ_NONE - 1];
1054 }
1055
1056 static void
1057 add_builtin_run_requests(void)
1058 {
1059 struct {
1060 enum keymap keymap;
1061 int key;
1062 char *argv[1];
1063 } reqs[] = {
1064 { KEYMAP_MAIN, 'C', { "git cherry-pick %(commit)" } },
1065 { KEYMAP_GENERIC, 'G', { "git gc" } },
1066 };
1067 int i;
1068
1069 for (i = 0; i < ARRAY_SIZE(reqs); i++) {
1070 enum request req;
1071
1072 req = add_run_request(reqs[i].keymap, reqs[i].key, 1, reqs[i].argv);
1073 if (req != REQ_NONE)
1074 add_keybinding(reqs[i].keymap, req, reqs[i].key);
1075 }
1076 }
1077
1078 /*
1079 * User config file handling.
1080 */
1081
1082 static struct int_map color_map[] = {
1083 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
1084 COLOR_MAP(DEFAULT),
1085 COLOR_MAP(BLACK),
1086 COLOR_MAP(BLUE),
1087 COLOR_MAP(CYAN),
1088 COLOR_MAP(GREEN),
1089 COLOR_MAP(MAGENTA),
1090 COLOR_MAP(RED),
1091 COLOR_MAP(WHITE),
1092 COLOR_MAP(YELLOW),
1093 };
1094
1095 #define set_color(color, name) \
1096 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
1097
1098 static struct int_map attr_map[] = {
1099 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
1100 ATTR_MAP(NORMAL),
1101 ATTR_MAP(BLINK),
1102 ATTR_MAP(BOLD),
1103 ATTR_MAP(DIM),
1104 ATTR_MAP(REVERSE),
1105 ATTR_MAP(STANDOUT),
1106 ATTR_MAP(UNDERLINE),
1107 };
1108
1109 #define set_attribute(attr, name) \
1110 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
1111
1112 static int config_lineno;
1113 static bool config_errors;
1114 static char *config_msg;
1115
1116 /* Wants: object fgcolor bgcolor [attr] */
1117 static int
1118 option_color_command(int argc, char *argv[])
1119 {
1120 struct line_info *info;
1121
1122 if (argc != 3 && argc != 4) {
1123 config_msg = "Wrong number of arguments given to color command";
1124 return ERR;
1125 }
1126
1127 info = get_line_info(argv[0], strlen(argv[0]));
1128 if (!info) {
1129 config_msg = "Unknown color name";
1130 return ERR;
1131 }
1132
1133 if (set_color(&info->fg, argv[1]) == ERR ||
1134 set_color(&info->bg, argv[2]) == ERR) {
1135 config_msg = "Unknown color";
1136 return ERR;
1137 }
1138
1139 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
1140 config_msg = "Unknown attribute";
1141 return ERR;
1142 }
1143
1144 return OK;
1145 }
1146
1147 static bool parse_bool(const char *s)
1148 {
1149 return (!strcmp(s, "1") || !strcmp(s, "true") ||
1150 !strcmp(s, "yes")) ? TRUE : FALSE;
1151 }
1152
1153 /* Wants: name = value */
1154 static int
1155 option_set_command(int argc, char *argv[])
1156 {
1157 if (argc != 3) {
1158 config_msg = "Wrong number of arguments given to set command";
1159 return ERR;
1160 }
1161
1162 if (strcmp(argv[1], "=")) {
1163 config_msg = "No value assigned";
1164 return ERR;
1165 }
1166
1167 if (!strcmp(argv[0], "show-author")) {
1168 opt_author = parse_bool(argv[2]);
1169 return OK;
1170 }
1171
1172 if (!strcmp(argv[0], "show-date")) {
1173 opt_date = parse_bool(argv[2]);
1174 return OK;
1175 }
1176
1177 if (!strcmp(argv[0], "show-rev-graph")) {
1178 opt_rev_graph = parse_bool(argv[2]);
1179 return OK;
1180 }
1181
1182 if (!strcmp(argv[0], "show-refs")) {
1183 opt_show_refs = parse_bool(argv[2]);
1184 return OK;
1185 }
1186
1187 if (!strcmp(argv[0], "show-line-numbers")) {
1188 opt_line_number = parse_bool(argv[2]);
1189 return OK;
1190 }
1191
1192 if (!strcmp(argv[0], "line-number-interval")) {
1193 opt_num_interval = atoi(argv[2]);
1194 return OK;
1195 }
1196
1197 if (!strcmp(argv[0], "tab-size")) {
1198 opt_tab_size = atoi(argv[2]);
1199 return OK;
1200 }
1201
1202 if (!strcmp(argv[0], "commit-encoding")) {
1203 char *arg = argv[2];
1204 int delimiter = *arg;
1205 int i;
1206
1207 switch (delimiter) {
1208 case '"':
1209 case '\'':
1210 for (arg++, i = 0; arg[i]; i++)
1211 if (arg[i] == delimiter) {
1212 arg[i] = 0;
1213 break;
1214 }
1215 default:
1216 string_ncopy(opt_encoding, arg, strlen(arg));
1217 return OK;
1218 }
1219 }
1220
1221 config_msg = "Unknown variable name";
1222 return ERR;
1223 }
1224
1225 /* Wants: mode request key */
1226 static int
1227 option_bind_command(int argc, char *argv[])
1228 {
1229 enum request request;
1230 int keymap;
1231 int key;
1232
1233 if (argc < 3) {
1234 config_msg = "Wrong number of arguments given to bind command";
1235 return ERR;
1236 }
1237
1238 if (set_keymap(&keymap, argv[0]) == ERR) {
1239 config_msg = "Unknown key map";
1240 return ERR;
1241 }
1242
1243 key = get_key_value(argv[1]);
1244 if (key == ERR) {
1245 config_msg = "Unknown key";
1246 return ERR;
1247 }
1248
1249 request = get_request(argv[2]);
1250 if (request == REQ_NONE) {
1251 const char *obsolete[] = { "cherry-pick" };
1252 size_t namelen = strlen(argv[2]);
1253 int i;
1254
1255 for (i = 0; i < ARRAY_SIZE(obsolete); i++) {
1256 if (namelen == strlen(obsolete[i]) &&
1257 !string_enum_compare(obsolete[i], argv[2], namelen)) {
1258 config_msg = "Obsolete request name";
1259 return ERR;
1260 }
1261 }
1262 }
1263 if (request == REQ_NONE && *argv[2]++ == '!')
1264 request = add_run_request(keymap, key, argc - 2, argv + 2);
1265 if (request == REQ_NONE) {
1266 config_msg = "Unknown request name";
1267 return ERR;
1268 }
1269
1270 add_keybinding(keymap, request, key);
1271
1272 return OK;
1273 }
1274
1275 static int
1276 set_option(char *opt, char *value)
1277 {
1278 char *argv[16];
1279 int valuelen;
1280 int argc = 0;
1281
1282 /* Tokenize */
1283 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1284 argv[argc++] = value;
1285 value += valuelen;
1286
1287 /* Nothing more to tokenize or last available token. */
1288 if (!*value || argc >= ARRAY_SIZE(argv))
1289 break;
1290
1291 *value++ = 0;
1292 while (isspace(*value))
1293 value++;
1294 }
1295
1296 if (!strcmp(opt, "color"))
1297 return option_color_command(argc, argv);
1298
1299 if (!strcmp(opt, "set"))
1300 return option_set_command(argc, argv);
1301
1302 if (!strcmp(opt, "bind"))
1303 return option_bind_command(argc, argv);
1304
1305 config_msg = "Unknown option command";
1306 return ERR;
1307 }
1308
1309 static int
1310 read_option(char *opt, size_t optlen, char *value, size_t valuelen)
1311 {
1312 int status = OK;
1313
1314 config_lineno++;
1315 config_msg = "Internal error";
1316
1317 /* Check for comment markers, since read_properties() will
1318 * only ensure opt and value are split at first " \t". */
1319 optlen = strcspn(opt, "#");
1320 if (optlen == 0)
1321 return OK;
1322
1323 if (opt[optlen] != 0) {
1324 config_msg = "No option value";
1325 status = ERR;
1326
1327 } else {
1328 /* Look for comment endings in the value. */
1329 size_t len = strcspn(value, "#");
1330
1331 if (len < valuelen) {
1332 valuelen = len;
1333 value[valuelen] = 0;
1334 }
1335
1336 status = set_option(opt, value);
1337 }
1338
1339 if (status == ERR) {
1340 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1341 config_lineno, (int) optlen, opt, config_msg);
1342 config_errors = TRUE;
1343 }
1344
1345 /* Always keep going if errors are encountered. */
1346 return OK;
1347 }
1348
1349 static void
1350 load_option_file(const char *path)
1351 {
1352 FILE *file;
1353
1354 /* It's ok that the file doesn't exist. */
1355 file = fopen(path, "r");
1356 if (!file)
1357 return;
1358
1359 config_lineno = 0;
1360 config_errors = FALSE;
1361
1362 if (read_properties(file, " \t", read_option) == ERR ||
1363 config_errors == TRUE)
1364 fprintf(stderr, "Errors while loading %s.\n", path);
1365 }
1366
1367 static int
1368 load_options(void)
1369 {
1370 char *home = getenv("HOME");
1371 char *tigrc_user = getenv("TIGRC_USER");
1372 char *tigrc_system = getenv("TIGRC_SYSTEM");
1373 char buf[SIZEOF_STR];
1374
1375 add_builtin_run_requests();
1376
1377 if (!tigrc_system) {
1378 if (!string_format(buf, "%s/tigrc", SYSCONFDIR))
1379 return ERR;
1380 tigrc_system = buf;
1381 }
1382 load_option_file(tigrc_system);
1383
1384 if (!tigrc_user) {
1385 if (!home || !string_format(buf, "%s/.tigrc", home))
1386 return ERR;
1387 tigrc_user = buf;
1388 }
1389 load_option_file(tigrc_user);
1390
1391 return OK;
1392 }
1393
1394
1395 /*
1396 * The viewer
1397 */
1398
1399 struct view;
1400 struct view_ops;
1401
1402 /* The display array of active views and the index of the current view. */
1403 static struct view *display[2];
1404 static unsigned int current_view;
1405
1406 /* Reading from the prompt? */
1407 static bool input_mode = FALSE;
1408
1409 #define foreach_displayed_view(view, i) \
1410 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1411
1412 #define displayed_views() (display[1] != NULL ? 2 : 1)
1413
1414 /* Current head and commit ID */
1415 static char ref_blob[SIZEOF_REF] = "";
1416 static char ref_commit[SIZEOF_REF] = "HEAD";
1417 static char ref_head[SIZEOF_REF] = "HEAD";
1418
1419 struct view {
1420 const char *name; /* View name */
1421 const char *cmd_fmt; /* Default command line format */
1422 const char *cmd_env; /* Command line set via environment */
1423 const char *id; /* Points to either of ref_{head,commit,blob} */
1424
1425 struct view_ops *ops; /* View operations */
1426
1427 enum keymap keymap; /* What keymap does this view have */
1428
1429 char cmd[SIZEOF_STR]; /* Command buffer */
1430 char ref[SIZEOF_REF]; /* Hovered commit reference */
1431 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1432
1433 int height, width; /* The width and height of the main window */
1434 WINDOW *win; /* The main window */
1435 WINDOW *title; /* The title window living below the main window */
1436
1437 /* Navigation */
1438 unsigned long offset; /* Offset of the window top */
1439 unsigned long lineno; /* Current line number */
1440
1441 /* Searching */
1442 char grep[SIZEOF_STR]; /* Search string */
1443 regex_t *regex; /* Pre-compiled regex */
1444
1445 /* If non-NULL, points to the view that opened this view. If this view
1446 * is closed tig will switch back to the parent view. */
1447 struct view *parent;
1448
1449 /* Buffering */
1450 size_t lines; /* Total number of lines */
1451 struct line *line; /* Line index */
1452 size_t line_alloc; /* Total number of allocated lines */
1453 size_t line_size; /* Total number of used lines */
1454 unsigned int digits; /* Number of digits in the lines member. */
1455
1456 /* Loading */
1457 FILE *pipe;
1458 time_t start_time;
1459 };
1460
1461 struct view_ops {
1462 /* What type of content being displayed. Used in the title bar. */
1463 const char *type;
1464 /* Open and reads in all view content. */
1465 bool (*open)(struct view *view);
1466 /* Read one line; updates view->line. */
1467 bool (*read)(struct view *view, char *data);
1468 /* Draw one line; @lineno must be < view->height. */
1469 bool (*draw)(struct view *view, struct line *line, unsigned int lineno, bool selected);
1470 /* Depending on view handle a special requests. */
1471 enum request (*request)(struct view *view, enum request request, struct line *line);
1472 /* Search for regex in a line. */
1473 bool (*grep)(struct view *view, struct line *line);
1474 /* Select line */
1475 void (*select)(struct view *view, struct line *line);
1476 };
1477
1478 static struct view_ops pager_ops;
1479 static struct view_ops main_ops;
1480 static struct view_ops tree_ops;
1481 static struct view_ops blob_ops;
1482 static struct view_ops blame_ops;
1483 static struct view_ops help_ops;
1484 static struct view_ops status_ops;
1485 static struct view_ops stage_ops;
1486
1487 #define VIEW_STR(name, cmd, env, ref, ops, map) \
1488 { name, cmd, #env, ref, ops, map}
1489
1490 #define VIEW_(id, name, ops, ref) \
1491 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id)
1492
1493
1494 static struct view views[] = {
1495 VIEW_(MAIN, "main", &main_ops, ref_head),
1496 VIEW_(DIFF, "diff", &pager_ops, ref_commit),
1497 VIEW_(LOG, "log", &pager_ops, ref_head),
1498 VIEW_(TREE, "tree", &tree_ops, ref_commit),
1499 VIEW_(BLOB, "blob", &blob_ops, ref_blob),
1500 VIEW_(BLAME, "blame", &blame_ops, ref_commit),
1501 VIEW_(HELP, "help", &help_ops, ""),
1502 VIEW_(PAGER, "pager", &pager_ops, "stdin"),
1503 VIEW_(STATUS, "status", &status_ops, ""),
1504 VIEW_(STAGE, "stage", &stage_ops, ""),
1505 };
1506
1507 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1508
1509 #define foreach_view(view, i) \
1510 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1511
1512 #define view_is_displayed(view) \
1513 (view == display[0] || view == display[1])
1514
1515 static int
1516 draw_text(struct view *view, const char *string, int max_len,
1517 bool use_tilde, int tilde_attr)
1518 {
1519 int len = 0;
1520 int trimmed = FALSE;
1521
1522 if (max_len <= 0)
1523 return 0;
1524
1525 if (opt_utf8) {
1526 len = utf8_length(string, max_len, &trimmed, use_tilde);
1527 } else {
1528 len = strlen(string);
1529 if (len > max_len) {
1530 if (use_tilde) {
1531 max_len -= 1;
1532 }
1533 len = max_len;
1534 trimmed = TRUE;
1535 }
1536 }
1537
1538 waddnstr(view->win, string, len);
1539 if (trimmed && use_tilde) {
1540 if (tilde_attr != -1)
1541 wattrset(view->win, tilde_attr);
1542 waddch(view->win, '~');
1543 len++;
1544 }
1545
1546 return len;
1547 }
1548
1549 static bool
1550 draw_view_line(struct view *view, unsigned int lineno)
1551 {
1552 struct line *line;
1553 bool selected = (view->offset + lineno == view->lineno);
1554 bool draw_ok;
1555
1556 assert(view_is_displayed(view));
1557
1558 if (view->offset + lineno >= view->lines)
1559 return FALSE;
1560
1561 line = &view->line[view->offset + lineno];
1562
1563 if (selected) {
1564 line->selected = TRUE;
1565 view->ops->select(view, line);
1566 } else if (line->selected) {
1567 line->selected = FALSE;
1568 wmove(view->win, lineno, 0);
1569 wclrtoeol(view->win);
1570 }
1571
1572 scrollok(view->win, FALSE);
1573 draw_ok = view->ops->draw(view, line, lineno, selected);
1574 scrollok(view->win, TRUE);
1575
1576 return draw_ok;
1577 }
1578
1579 static void
1580 redraw_view_dirty(struct view *view)
1581 {
1582 bool dirty = FALSE;
1583 int lineno;
1584
1585 for (lineno = 0; lineno < view->height; lineno++) {
1586 struct line *line = &view->line[view->offset + lineno];
1587
1588 if (!line->dirty)
1589 continue;
1590 line->dirty = 0;
1591 dirty = TRUE;
1592 if (!draw_view_line(view, lineno))
1593 break;
1594 }
1595
1596 if (!dirty)
1597 return;
1598 redrawwin(view->win);
1599 if (input_mode)
1600 wnoutrefresh(view->win);
1601 else
1602 wrefresh(view->win);
1603 }
1604
1605 static void
1606 redraw_view_from(struct view *view, int lineno)
1607 {
1608 assert(0 <= lineno && lineno < view->height);
1609
1610 for (; lineno < view->height; lineno++) {
1611 if (!draw_view_line(view, lineno))
1612 break;
1613 }
1614
1615 redrawwin(view->win);
1616 if (input_mode)
1617 wnoutrefresh(view->win);
1618 else
1619 wrefresh(view->win);
1620 }
1621
1622 static void
1623 redraw_view(struct view *view)
1624 {
1625 wclear(view->win);
1626 redraw_view_from(view, 0);
1627 }
1628
1629
1630 static void
1631 update_view_title(struct view *view)
1632 {
1633 char buf[SIZEOF_STR];
1634 char state[SIZEOF_STR];
1635 size_t bufpos = 0, statelen = 0;
1636
1637 assert(view_is_displayed(view));
1638
1639 if (view != VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) {
1640 unsigned int view_lines = view->offset + view->height;
1641 unsigned int lines = view->lines
1642 ? MIN(view_lines, view->lines) * 100 / view->lines
1643 : 0;
1644
1645 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1646 view->ops->type,
1647 view->lineno + 1,
1648 view->lines,
1649 lines);
1650
1651 if (view->pipe) {
1652 time_t secs = time(NULL) - view->start_time;
1653
1654 /* Three git seconds are a long time ... */
1655 if (secs > 2)
1656 string_format_from(state, &statelen, " %lds", secs);
1657 }
1658 }
1659
1660 string_format_from(buf, &bufpos, "[%s]", view->name);
1661 if (*view->ref && bufpos < view->width) {
1662 size_t refsize = strlen(view->ref);
1663 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1664
1665 if (minsize < view->width)
1666 refsize = view->width - minsize + 7;
1667 string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
1668 }
1669
1670 if (statelen && bufpos < view->width) {
1671 string_format_from(buf, &bufpos, " %s", state);
1672 }
1673
1674 if (view == display[current_view])
1675 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1676 else
1677 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1678
1679 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1680 wclrtoeol(view->title);
1681 wmove(view->title, 0, view->width - 1);
1682
1683 if (input_mode)
1684 wnoutrefresh(view->title);
1685 else
1686 wrefresh(view->title);
1687 }
1688
1689 static void
1690 resize_display(void)
1691 {
1692 int offset, i;
1693 struct view *base = display[0];
1694 struct view *view = display[1] ? display[1] : display[0];
1695
1696 /* Setup window dimensions */
1697
1698 getmaxyx(stdscr, base->height, base->width);
1699
1700 /* Make room for the status window. */
1701 base->height -= 1;
1702
1703 if (view != base) {
1704 /* Horizontal split. */
1705 view->width = base->width;
1706 view->height = SCALE_SPLIT_VIEW(base->height);
1707 base->height -= view->height;
1708
1709 /* Make room for the title bar. */
1710 view->height -= 1;
1711 }
1712
1713 /* Make room for the title bar. */
1714 base->height -= 1;
1715
1716 offset = 0;
1717
1718 foreach_displayed_view (view, i) {
1719 if (!view->win) {
1720 view->win = newwin(view->height, 0, offset, 0);
1721 if (!view->win)
1722 die("Failed to create %s view", view->name);
1723
1724 scrollok(view->win, TRUE);
1725
1726 view->title = newwin(1, 0, offset + view->height, 0);
1727 if (!view->title)
1728 die("Failed to create title window");
1729
1730 } else {
1731 wresize(view->win, view->height, view->width);
1732 mvwin(view->win, offset, 0);
1733 mvwin(view->title, offset + view->height, 0);
1734 }
1735
1736 offset += view->height + 1;
1737 }
1738 }
1739
1740 static void
1741 redraw_display(void)
1742 {
1743 struct view *view;
1744 int i;
1745
1746 foreach_displayed_view (view, i) {
1747 redraw_view(view);
1748 update_view_title(view);
1749 }
1750 }
1751
1752 static void
1753 update_display_cursor(struct view *view)
1754 {
1755 /* Move the cursor to the right-most column of the cursor line.
1756 *
1757 * XXX: This could turn out to be a bit expensive, but it ensures that
1758 * the cursor does not jump around. */
1759 if (view->lines) {
1760 wmove(view->win, view->lineno - view->offset, view->width - 1);
1761 wrefresh(view->win);
1762 }
1763 }
1764
1765 /*
1766 * Navigation
1767 */
1768
1769 /* Scrolling backend */
1770 static void
1771 do_scroll_view(struct view *view, int lines)
1772 {
1773 bool redraw_current_line = FALSE;
1774
1775 /* The rendering expects the new offset. */
1776 view->offset += lines;
1777
1778 assert(0 <= view->offset && view->offset < view->lines);
1779 assert(lines);
1780
1781 /* Move current line into the view. */
1782 if (view->lineno < view->offset) {
1783 view->lineno = view->offset;
1784 redraw_current_line = TRUE;
1785 } else if (view->lineno >= view->offset + view->height) {
1786 view->lineno = view->offset + view->height - 1;
1787 redraw_current_line = TRUE;
1788 }
1789
1790 assert(view->offset <= view->lineno && view->lineno < view->lines);
1791
1792 /* Redraw the whole screen if scrolling is pointless. */
1793 if (view->height < ABS(lines)) {
1794 redraw_view(view);
1795
1796 } else {
1797 int line = lines > 0 ? view->height - lines : 0;
1798 int end = line + ABS(lines);
1799
1800 wscrl(view->win, lines);
1801
1802 for (; line < end; line++) {
1803 if (!draw_view_line(view, line))
1804 break;
1805 }
1806
1807 if (redraw_current_line)
1808 draw_view_line(view, view->lineno - view->offset);
1809 }
1810
1811 redrawwin(view->win);
1812 wrefresh(view->win);
1813 report("");
1814 }
1815
1816 /* Scroll frontend */
1817 static void
1818 scroll_view(struct view *view, enum request request)
1819 {
1820 int lines = 1;
1821
1822 assert(view_is_displayed(view));
1823
1824 switch (request) {
1825 case REQ_SCROLL_PAGE_DOWN:
1826 lines = view->height;
1827 case REQ_SCROLL_LINE_DOWN:
1828 if (view->offset + lines > view->lines)
1829 lines = view->lines - view->offset;
1830
1831 if (lines == 0 || view->offset + view->height >= view->lines) {
1832 report("Cannot scroll beyond the last line");
1833 return;
1834 }
1835 break;
1836
1837 case REQ_SCROLL_PAGE_UP:
1838 lines = view->height;
1839 case REQ_SCROLL_LINE_UP:
1840 if (lines > view->offset)
1841 lines = view->offset;
1842
1843 if (lines == 0) {
1844 report("Cannot scroll beyond the first line");
1845 return;
1846 }
1847
1848 lines = -lines;
1849 break;
1850
1851 default:
1852 die("request %d not handled in switch", request);
1853 }
1854
1855 do_scroll_view(view, lines);
1856 }
1857
1858 /* Cursor moving */
1859 static void
1860 move_view(struct view *view, enum request request)
1861 {
1862 int scroll_steps = 0;
1863 int steps;
1864
1865 switch (request) {
1866 case REQ_MOVE_FIRST_LINE:
1867 steps = -view->lineno;
1868 break;
1869
1870 case REQ_MOVE_LAST_LINE:
1871 steps = view->lines - view->lineno - 1;
1872 break;
1873
1874 case REQ_MOVE_PAGE_UP:
1875 steps = view->height > view->lineno
1876 ? -view->lineno : -view->height;
1877 break;
1878
1879 case REQ_MOVE_PAGE_DOWN:
1880 steps = view->lineno + view->height >= view->lines
1881 ? view->lines - view->lineno - 1 : view->height;
1882 break;
1883
1884 case REQ_MOVE_UP:
1885 steps = -1;
1886 break;
1887
1888 case REQ_MOVE_DOWN:
1889 steps = 1;
1890 break;
1891
1892 default:
1893 die("request %d not handled in switch", request);
1894 }
1895
1896 if (steps <= 0 && view->lineno == 0) {
1897 report("Cannot move beyond the first line");
1898 return;
1899
1900 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1901 report("Cannot move beyond the last line");
1902 return;
1903 }
1904
1905 /* Move the current line */
1906 view->lineno += steps;
1907 assert(0 <= view->lineno && view->lineno < view->lines);
1908
1909 /* Check whether the view needs to be scrolled */
1910 if (view->lineno < view->offset ||
1911 view->lineno >= view->offset + view->height) {
1912 scroll_steps = steps;
1913 if (steps < 0 && -steps > view->offset) {
1914 scroll_steps = -view->offset;
1915
1916 } else if (steps > 0) {
1917 if (view->lineno == view->lines - 1 &&
1918 view->lines > view->height) {
1919 scroll_steps = view->lines - view->offset - 1;
1920 if (scroll_steps >= view->height)
1921 scroll_steps -= view->height - 1;
1922 }
1923 }
1924 }
1925
1926 if (!view_is_displayed(view)) {
1927 view->offset += scroll_steps;
1928 assert(0 <= view->offset && view->offset < view->lines);
1929 view->ops->select(view, &view->line[view->lineno]);
1930 return;
1931 }
1932
1933 /* Repaint the old "current" line if we be scrolling */
1934 if (ABS(steps) < view->height)
1935 draw_view_line(view, view->lineno - steps - view->offset);
1936
1937 if (scroll_steps) {
1938 do_scroll_view(view, scroll_steps);
1939 return;
1940 }
1941
1942 /* Draw the current line */
1943 draw_view_line(view, view->lineno - view->offset);
1944
1945 redrawwin(view->win);
1946 wrefresh(view->win);
1947 report("");
1948 }
1949
1950
1951 /*
1952 * Searching
1953 */
1954
1955 static void search_view(struct view *view, enum request request);
1956
1957 static bool
1958 find_next_line(struct view *view, unsigned long lineno, struct line *line)
1959 {
1960 assert(view_is_displayed(view));
1961
1962 if (!view->ops->grep(view, line))
1963 return FALSE;
1964
1965 if (lineno - view->offset >= view->height) {
1966 view->offset = lineno;
1967 view->lineno = lineno;
1968 redraw_view(view);
1969
1970 } else {
1971 unsigned long old_lineno = view->lineno - view->offset;
1972
1973 view->lineno = lineno;
1974 draw_view_line(view, old_lineno);
1975
1976 draw_view_line(view, view->lineno - view->offset);
1977 redrawwin(view->win);
1978 wrefresh(view->win);
1979 }
1980
1981 report("Line %ld matches '%s'", lineno + 1, view->grep);
1982 return TRUE;
1983 }
1984
1985 static void
1986 find_next(struct view *view, enum request request)
1987 {
1988 unsigned long lineno = view->lineno;
1989 int direction;
1990
1991 if (!*view->grep) {
1992 if (!*opt_search)
1993 report("No previous search");
1994 else
1995 search_view(view, request);
1996 return;
1997 }
1998
1999 switch (request) {
2000 case REQ_SEARCH:
2001 case REQ_FIND_NEXT:
2002 direction = 1;
2003 break;
2004
2005 case REQ_SEARCH_BACK:
2006 case REQ_FIND_PREV:
2007 direction = -1;
2008 break;
2009
2010 default:
2011 return;
2012 }
2013
2014 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
2015 lineno += direction;
2016
2017 /* Note, lineno is unsigned long so will wrap around in which case it
2018 * will become bigger than view->lines. */
2019 for (; lineno < view->lines; lineno += direction) {
2020 struct line *line = &view->line[lineno];
2021
2022 if (find_next_line(view, lineno, line))
2023 return;
2024 }
2025
2026 report("No match found for '%s'", view->grep);
2027 }
2028
2029 static void
2030 search_view(struct view *view, enum request request)
2031 {
2032 int regex_err;
2033
2034 if (view->regex) {
2035 regfree(view->regex);
2036 *view->grep = 0;
2037 } else {
2038 view->regex = calloc(1, sizeof(*view->regex));
2039 if (!view->regex)
2040 return;
2041 }
2042
2043 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
2044 if (regex_err != 0) {
2045 char buf[SIZEOF_STR] = "unknown error";
2046
2047 regerror(regex_err, view->regex, buf, sizeof(buf));
2048 report("Search failed: %s", buf);
2049 return;
2050 }
2051
2052 string_copy(view->grep, opt_search);
2053
2054 find_next(view, request);
2055 }
2056
2057 /*
2058 * Incremental updating
2059 */
2060
2061 static void
2062 end_update(struct view *view)
2063 {
2064 if (!view->pipe)
2065 return;
2066 set_nonblocking_input(FALSE);
2067 if (view->pipe == stdin)
2068 fclose(view->pipe);
2069 else
2070 pclose(view->pipe);
2071 view->pipe = NULL;
2072 }
2073
2074 static bool
2075 begin_update(struct view *view)
2076 {
2077 if (view->pipe)
2078 end_update(view);
2079
2080 if (opt_cmd[0]) {
2081 string_copy(view->cmd, opt_cmd);
2082 opt_cmd[0] = 0;
2083 /* When running random commands, initially show the
2084 * command in the title. However, it maybe later be
2085 * overwritten if a commit line is selected. */
2086 if (view == VIEW(REQ_VIEW_PAGER))
2087 string_copy(view->ref, view->cmd);
2088 else
2089 view->ref[0] = 0;
2090
2091 } else if (view == VIEW(REQ_VIEW_TREE)) {
2092 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2093 char path[SIZEOF_STR];
2094
2095 if (strcmp(view->vid, view->id))
2096 opt_path[0] = path[0] = 0;
2097 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
2098 return FALSE;
2099
2100 if (!string_format(view->cmd, format, view->id, path))
2101 return FALSE;
2102
2103 } else {
2104 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2105 const char *id = view->id;
2106
2107 if (!string_format(view->cmd, format, id, id, id, id, id))
2108 return FALSE;
2109
2110 /* Put the current ref_* value to the view title ref
2111 * member. This is needed by the blob view. Most other
2112 * views sets it automatically after loading because the
2113 * first line is a commit line. */
2114 string_copy_rev(view->ref, view->id);
2115 }
2116
2117 /* Special case for the pager view. */
2118 if (opt_pipe) {
2119 view->pipe = opt_pipe;
2120 opt_pipe = NULL;
2121 } else {
2122 view->pipe = popen(view->cmd, "r");
2123 }
2124
2125 if (!view->pipe)
2126 return FALSE;
2127
2128 set_nonblocking_input(TRUE);
2129
2130 view->offset = 0;
2131 view->lines = 0;
2132 view->lineno = 0;
2133 string_copy_rev(view->vid, view->id);
2134
2135 if (view->line) {
2136 int i;
2137
2138 for (i = 0; i < view->lines; i++)
2139 if (view->line[i].data)
2140 free(view->line[i].data);
2141
2142 free(view->line);
2143 view->line = NULL;
2144 }
2145
2146 view->start_time = time(NULL);
2147
2148 return TRUE;
2149 }
2150
2151 #define ITEM_CHUNK_SIZE 256
2152 static void *
2153 realloc_items(void *mem, size_t *size, size_t new_size, size_t item_size)
2154 {
2155 size_t num_chunks = *size / ITEM_CHUNK_SIZE;
2156 size_t num_chunks_new = (new_size + ITEM_CHUNK_SIZE - 1) / ITEM_CHUNK_SIZE;
2157
2158 if (mem == NULL || num_chunks != num_chunks_new) {
2159 *size = num_chunks_new * ITEM_CHUNK_SIZE;
2160 mem = realloc(mem, *size * item_size);
2161 }
2162
2163 return mem;
2164 }
2165
2166 static struct line *
2167 realloc_lines(struct view *view, size_t line_size)
2168 {
2169 size_t alloc = view->line_alloc;
2170 struct line *tmp = realloc_items(view->line, &alloc, line_size,
2171 sizeof(*view->line));
2172
2173 if (!tmp)
2174 return NULL;
2175
2176 view->line = tmp;
2177 view->line_alloc = alloc;
2178 view->line_size = line_size;
2179 return view->line;
2180 }
2181
2182 static bool
2183 update_view(struct view *view)
2184 {
2185 char in_buffer[BUFSIZ];
2186 char out_buffer[BUFSIZ * 2];
2187 char *line;
2188 /* The number of lines to read. If too low it will cause too much
2189 * redrawing (and possible flickering), if too high responsiveness
2190 * will suffer. */
2191 unsigned long lines = view->height;
2192 int redraw_from = -1;
2193
2194 if (!view->pipe)
2195 return TRUE;
2196
2197 /* Only redraw if lines are visible. */
2198 if (view->offset + view->height >= view->lines)
2199 redraw_from = view->lines - view->offset;
2200
2201 /* FIXME: This is probably not perfect for backgrounded views. */
2202 if (!realloc_lines(view, view->lines + lines))
2203 goto alloc_error;
2204
2205 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
2206 size_t linelen = strlen(line);
2207
2208 if (linelen)
2209 line[linelen - 1] = 0;
2210
2211 if (opt_iconv != ICONV_NONE) {
2212 ICONV_CONST char *inbuf = line;
2213 size_t inlen = linelen;
2214
2215 char *outbuf = out_buffer;
2216 size_t outlen = sizeof(out_buffer);
2217
2218 size_t ret;
2219
2220 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
2221 if (ret != (size_t) -1) {
2222 line = out_buffer;
2223 linelen = strlen(out_buffer);
2224 }
2225 }
2226
2227 if (!view->ops->read(view, line))
2228 goto alloc_error;
2229
2230 if (lines-- == 1)
2231 break;
2232 }
2233
2234 {
2235 int digits;
2236
2237 lines = view->lines;
2238 for (digits = 0; lines; digits++)
2239 lines /= 10;
2240
2241 /* Keep the displayed view in sync with line number scaling. */
2242 if (digits != view->digits) {
2243 view->digits = digits;
2244 redraw_from = 0;
2245 }
2246 }
2247
2248 if (!view_is_displayed(view))
2249 goto check_pipe;
2250
2251 if (view == VIEW(REQ_VIEW_TREE)) {
2252 /* Clear the view and redraw everything since the tree sorting
2253 * might have rearranged things. */
2254 redraw_view(view);
2255
2256
2257 } else if (redraw_from >= 0) {
2258 /* If this is an incremental update, redraw the previous line
2259 * since for commits some members could have changed when
2260 * loading the main view. */
2261 if (redraw_from > 0)
2262 redraw_from--;
2263
2264 /* Since revision graph visualization requires knowledge
2265 * about the parent commit, it causes a further one-off
2266 * needed to be redrawn for incremental updates. */
2267 if (redraw_from > 0 && opt_rev_graph)
2268 redraw_from--;
2269
2270 /* Incrementally draw avoids flickering. */
2271 redraw_view_from(view, redraw_from);
2272 }
2273
2274 if (view == VIEW(REQ_VIEW_BLAME))
2275 redraw_view_dirty(view);
2276
2277 /* Update the title _after_ the redraw so that if the redraw picks up a
2278 * commit reference in view->ref it'll be available here. */
2279 update_view_title(view);
2280
2281 check_pipe:
2282 if (ferror(view->pipe)) {
2283 report("Failed to read: %s", strerror(errno));
2284 goto end;
2285
2286 } else if (feof(view->pipe)) {
2287 report("");
2288 goto end;
2289 }
2290
2291 return TRUE;
2292
2293 alloc_error:
2294 report("Allocation failure");
2295
2296 end:
2297 if (view->ops->read(view, NULL))
2298 end_update(view);
2299 return FALSE;
2300 }
2301
2302 static struct line *
2303 add_line_data(struct view *view, void *data, enum line_type type)
2304 {
2305 struct line *line = &view->line[view->lines++];
2306
2307 memset(line, 0, sizeof(*line));
2308 line->type = type;
2309 line->data = data;
2310
2311 return line;
2312 }
2313
2314 static struct line *
2315 add_line_text(struct view *view, char *data, enum line_type type)
2316 {
2317 if (data)
2318 data = strdup(data);
2319
2320 return data ? add_line_data(view, data, type) : NULL;
2321 }
2322
2323
2324 /*
2325 * View opening
2326 */
2327
2328 enum open_flags {
2329 OPEN_DEFAULT = 0, /* Use default view switching. */
2330 OPEN_SPLIT = 1, /* Split current view. */
2331 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2332 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2333 };
2334
2335 static void
2336 open_view(struct view *prev, enum request request, enum open_flags flags)
2337 {
2338 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2339 bool split = !!(flags & OPEN_SPLIT);
2340 bool reload = !!(flags & OPEN_RELOAD);
2341 struct view *view = VIEW(request);
2342 int nviews = displayed_views();
2343 struct view *base_view = display[0];
2344
2345 if (view == prev && nviews == 1 && !reload) {
2346 report("Already in %s view", view->name);
2347 return;
2348 }
2349
2350 if (view->ops->open) {
2351 if (!view->ops->open(view)) {
2352 report("Failed to load %s view", view->name);
2353 return;
2354 }
2355
2356 } else if ((reload || strcmp(view->vid, view->id)) &&
2357 !begin_update(view)) {
2358 report("Failed to load %s view", view->name);
2359 return;
2360 }
2361
2362 if (split) {
2363 display[1] = view;
2364 if (!backgrounded)
2365 current_view = 1;
2366 } else {
2367 /* Maximize the current view. */
2368 memset(display, 0, sizeof(display));
2369 current_view = 0;
2370 display[current_view] = view;
2371 }
2372
2373 /* Resize the view when switching between split- and full-screen,
2374 * or when switching between two different full-screen views. */
2375 if (nviews != displayed_views() ||
2376 (nviews == 1 && base_view != display[0]))
2377 resize_display();
2378
2379 if (split && prev->lineno - prev->offset >= prev->height) {
2380 /* Take the title line into account. */
2381 int lines = prev->lineno - prev->offset - prev->height + 1;
2382
2383 /* Scroll the view that was split if the current line is
2384 * outside the new limited view. */
2385 do_scroll_view(prev, lines);
2386 }
2387
2388 if (prev && view != prev) {
2389 if (split && !backgrounded) {
2390 /* "Blur" the previous view. */
2391 update_view_title(prev);
2392 }
2393
2394 view->parent = prev;
2395 }
2396
2397 if (view->pipe && view->lines == 0) {
2398 /* Clear the old view and let the incremental updating refill
2399 * the screen. */
2400 wclear(view->win);
2401 report("");
2402 } else {
2403 redraw_view(view);
2404 report("");
2405 }
2406
2407 /* If the view is backgrounded the above calls to report()
2408 * won't redraw the view title. */
2409 if (backgrounded)
2410 update_view_title(view);
2411 }
2412
2413 static void
2414 open_external_viewer(const char *cmd)
2415 {
2416 def_prog_mode(); /* save current tty modes */
2417 endwin(); /* restore original tty modes */
2418 system(cmd);
2419 fprintf(stderr, "Press Enter to continue");
2420 getc(stdin);
2421 reset_prog_mode();
2422 redraw_display();
2423 }
2424
2425 static void
2426 open_mergetool(const char *file)
2427 {
2428 char cmd[SIZEOF_STR];
2429 char file_sq[SIZEOF_STR];
2430
2431 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2432 string_format(cmd, "git mergetool %s", file_sq)) {
2433 open_external_viewer(cmd);
2434 }
2435 }
2436
2437 static void
2438 open_editor(bool from_root, const char *file)
2439 {
2440 char cmd[SIZEOF_STR];
2441 char file_sq[SIZEOF_STR];
2442 char *editor;
2443 char *prefix = from_root ? opt_cdup : "";
2444
2445 editor = getenv("GIT_EDITOR");
2446 if (!editor && *opt_editor)
2447 editor = opt_editor;
2448 if (!editor)
2449 editor = getenv("VISUAL");
2450 if (!editor)
2451 editor = getenv("EDITOR");
2452 if (!editor)
2453 editor = "vi";
2454
2455 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2456 string_format(cmd, "%s %s%s", editor, prefix, file_sq)) {
2457 open_external_viewer(cmd);
2458 }
2459 }
2460
2461 static void
2462 open_run_request(enum request request)
2463 {
2464 struct run_request *req = get_run_request(request);
2465 char buf[SIZEOF_STR * 2];
2466 size_t bufpos;
2467 char *cmd;
2468
2469 if (!req) {
2470 report("Unknown run request");
2471 return;
2472 }
2473
2474 bufpos = 0;
2475 cmd = req->cmd;
2476
2477 while (cmd) {
2478 char *next = strstr(cmd, "%(");
2479 int len = next - cmd;
2480 char *value;
2481
2482 if (!next) {
2483 len = strlen(cmd);
2484 value = "";
2485
2486 } else if (!strncmp(next, "%(head)", 7)) {
2487 value = ref_head;
2488
2489 } else if (!strncmp(next, "%(commit)", 9)) {
2490 value = ref_commit;
2491
2492 } else if (!strncmp(next, "%(blob)", 7)) {
2493 value = ref_blob;
2494
2495 } else {
2496 report("Unknown replacement in run request: `%s`", req->cmd);
2497 return;
2498 }
2499
2500 if (!string_format_from(buf, &bufpos, "%.*s%s", len, cmd, value))
2501 return;
2502
2503 if (next)
2504 next = strchr(next, ')') + 1;
2505 cmd = next;
2506 }
2507
2508 open_external_viewer(buf);
2509 }
2510
2511 /*
2512 * User request switch noodle
2513 */
2514
2515 static int
2516 view_driver(struct view *view, enum request request)
2517 {
2518 int i;
2519
2520 if (request == REQ_NONE) {
2521 doupdate();
2522 return TRUE;
2523 }
2524
2525 if (request > REQ_NONE) {
2526 open_run_request(request);
2527 return TRUE;
2528 }
2529
2530 if (view && view->lines) {
2531 request = view->ops->request(view, request, &view->line[view->lineno]);
2532 if (request == REQ_NONE)
2533 return TRUE;
2534 }
2535
2536 switch (request) {
2537 case REQ_MOVE_UP:
2538 case REQ_MOVE_DOWN:
2539 case REQ_MOVE_PAGE_UP:
2540 case REQ_MOVE_PAGE_DOWN:
2541 case REQ_MOVE_FIRST_LINE:
2542 case REQ_MOVE_LAST_LINE:
2543 move_view(view, request);
2544 break;
2545
2546 case REQ_SCROLL_LINE_DOWN:
2547 case REQ_SCROLL_LINE_UP:
2548 case REQ_SCROLL_PAGE_DOWN:
2549 case REQ_SCROLL_PAGE_UP:
2550 scroll_view(view, request);
2551 break;
2552
2553 case REQ_VIEW_BLAME:
2554 if (!opt_file[0]) {
2555 report("No file chosen, press %s to open tree view",
2556 get_key(REQ_VIEW_TREE));
2557 break;
2558 }
2559 open_view(view, request, OPEN_DEFAULT);
2560 break;
2561
2562 case REQ_VIEW_BLOB:
2563 if (!ref_blob[0]) {
2564 report("No file chosen, press %s to open tree view",
2565 get_key(REQ_VIEW_TREE));
2566 break;
2567 }
2568 open_view(view, request, OPEN_DEFAULT);
2569 break;
2570
2571 case REQ_VIEW_PAGER:
2572 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2573 report("No pager content, press %s to run command from prompt",
2574 get_key(REQ_PROMPT));
2575 break;
2576 }
2577 open_view(view, request, OPEN_DEFAULT);
2578 break;
2579
2580 case REQ_VIEW_STAGE:
2581 if (!VIEW(REQ_VIEW_STAGE)->lines) {
2582 report("No stage content, press %s to open the status view and choose file",
2583 get_key(REQ_VIEW_STATUS));
2584 break;
2585 }
2586 open_view(view, request, OPEN_DEFAULT);
2587 break;
2588
2589 case REQ_VIEW_STATUS:
2590 if (opt_is_inside_work_tree == FALSE) {
2591 report("The status view requires a working tree");
2592 break;
2593 }
2594 open_view(view, request, OPEN_DEFAULT);
2595 break;
2596
2597 case REQ_VIEW_MAIN:
2598 case REQ_VIEW_DIFF:
2599 case REQ_VIEW_LOG:
2600 case REQ_VIEW_TREE:
2601 case REQ_VIEW_HELP:
2602 open_view(view, request, OPEN_DEFAULT);
2603 break;
2604
2605 case REQ_NEXT:
2606 case REQ_PREVIOUS:
2607 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2608
2609 if ((view == VIEW(REQ_VIEW_DIFF) &&
2610 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2611 (view == VIEW(REQ_VIEW_DIFF) &&
2612 view->parent == VIEW(REQ_VIEW_BLAME)) ||
2613 (view == VIEW(REQ_VIEW_STAGE) &&
2614 view->parent == VIEW(REQ_VIEW_STATUS)) ||
2615 (view == VIEW(REQ_VIEW_BLOB) &&
2616 view->parent == VIEW(REQ_VIEW_TREE))) {
2617 int line;
2618
2619 view = view->parent;
2620 line = view->lineno;
2621 move_view(view, request);
2622 if (view_is_displayed(view))
2623 update_view_title(view);
2624 if (line != view->lineno)
2625 view->ops->request(view, REQ_ENTER,
2626 &view->line[view->lineno]);
2627
2628 } else {
2629 move_view(view, request);
2630 }
2631 break;
2632
2633 case REQ_VIEW_NEXT:
2634 {
2635 int nviews = displayed_views();
2636 int next_view = (current_view + 1) % nviews;
2637
2638 if (next_view == current_view) {
2639 report("Only one view is displayed");
2640 break;
2641 }
2642
2643 current_view = next_view;
2644 /* Blur out the title of the previous view. */
2645 update_view_title(view);
2646 report("");
2647 break;
2648 }
2649 case REQ_REFRESH:
2650 report("Refreshing is not yet supported for the %s view", view->name);
2651 break;
2652
2653 case REQ_TOGGLE_LINENO:
2654 opt_line_number = !opt_line_number;
2655 redraw_display();
2656 break;
2657
2658 case REQ_TOGGLE_DATE:
2659 opt_date = !opt_date;
2660 redraw_display();
2661 break;
2662
2663 case REQ_TOGGLE_AUTHOR:
2664 opt_author = !opt_author;
2665 redraw_display();
2666 break;
2667
2668 case REQ_TOGGLE_REV_GRAPH:
2669 opt_rev_graph = !opt_rev_graph;
2670 redraw_display();
2671 break;
2672
2673 case REQ_TOGGLE_REFS:
2674 opt_show_refs = !opt_show_refs;
2675 redraw_display();
2676 break;
2677
2678 case REQ_PROMPT:
2679 /* Always reload^Wrerun commands from the prompt. */
2680 open_view(view, opt_request, OPEN_RELOAD);
2681 break;
2682
2683 case REQ_SEARCH:
2684 case REQ_SEARCH_BACK:
2685 search_view(view, request);
2686 break;
2687
2688 case REQ_FIND_NEXT:
2689 case REQ_FIND_PREV:
2690 find_next(view, request);
2691 break;
2692
2693 case REQ_STOP_LOADING:
2694 for (i = 0; i < ARRAY_SIZE(views); i++) {
2695 view = &views[i];
2696 if (view->pipe)
2697 report("Stopped loading the %s view", view->name),
2698 end_update(view);
2699 }
2700 break;
2701
2702 case REQ_SHOW_VERSION:
2703 report("tig-%s (built %s)", TIG_VERSION, __DATE__);
2704 return TRUE;
2705
2706 case REQ_SCREEN_RESIZE:
2707 resize_display();
2708 /* Fall-through */
2709 case REQ_SCREEN_REDRAW:
2710 redraw_display();
2711 break;
2712
2713 case REQ_EDIT:
2714 report("Nothing to edit");
2715 break;
2716
2717
2718 case REQ_ENTER:
2719 report("Nothing to enter");
2720 break;
2721
2722
2723 case REQ_VIEW_CLOSE:
2724 /* XXX: Mark closed views by letting view->parent point to the
2725 * view itself. Parents to closed view should never be
2726 * followed. */
2727 if (view->parent &&
2728 view->parent->parent != view->parent) {
2729 memset(display, 0, sizeof(display));
2730 current_view = 0;
2731 display[current_view] = view->parent;
2732 view->parent = view;
2733 resize_display();
2734 redraw_display();
2735 break;
2736 }
2737 /* Fall-through */
2738 case REQ_QUIT:
2739 return FALSE;
2740
2741 default:
2742 /* An unknown key will show most commonly used commands. */
2743 report("Unknown key, press 'h' for help");
2744 return TRUE;
2745 }
2746
2747 return TRUE;
2748 }
2749
2750
2751 /*
2752 * Pager backend
2753 */
2754
2755 static bool
2756 pager_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2757 {
2758 char *text = line->data;
2759 enum line_type type = line->type;
2760 int attr;
2761
2762 wmove(view->win, lineno, 0);
2763
2764 if (selected) {
2765 type = LINE_CURSOR;
2766 wchgat(view->win, -1, 0, type, NULL);
2767 }
2768
2769 attr = get_line_attr(type);
2770 wattrset(view->win, attr);
2771
2772 if (opt_line_number || opt_tab_size < TABSIZE) {
2773 static char spaces[] = " ";
2774 int col_offset = 0, col = 0;
2775
2776 if (opt_line_number) {
2777 unsigned long real_lineno = view->offset + lineno + 1;
2778
2779 if (real_lineno == 1 ||
2780 (real_lineno % opt_num_interval) == 0) {
2781 wprintw(view->win, "%.*d", view->digits, real_lineno);
2782
2783 } else {
2784 waddnstr(view->win, spaces,
2785 MIN(view->digits, STRING_SIZE(spaces)));
2786 }
2787 waddstr(view->win, ": ");
2788 col_offset = view->digits + 2;
2789 }
2790
2791 while (text && col_offset + col < view->width) {
2792 int cols_max = view->width - col_offset - col;
2793 char *pos = text;
2794 int cols;
2795
2796 if (*text == '\t') {
2797 text++;
2798 assert(sizeof(spaces) > TABSIZE);
2799 pos = spaces;
2800 cols = opt_tab_size - (col % opt_tab_size);
2801
2802 } else {
2803 text = strchr(text, '\t');
2804 cols = line ? text - pos : strlen(pos);
2805 }
2806
2807 waddnstr(view->win, pos, MIN(cols, cols_max));
2808 col += cols;
2809 }
2810
2811 } else {
2812 int tilde_attr = get_line_attr(LINE_MAIN_DELIM);
2813
2814 draw_text(view, text, view->width, TRUE, tilde_attr);
2815 }
2816
2817 return TRUE;
2818 }
2819
2820 static bool
2821 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2822 {
2823 char refbuf[SIZEOF_STR];
2824 char *ref = NULL;
2825 FILE *pipe;
2826
2827 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2828 return TRUE;
2829
2830 pipe = popen(refbuf, "r");
2831 if (!pipe)
2832 return TRUE;
2833
2834 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2835 ref = chomp_string(ref);
2836 pclose(pipe);
2837
2838 if (!ref || !*ref)
2839 return TRUE;
2840
2841 /* This is the only fatal call, since it can "corrupt" the buffer. */
2842 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2843 return FALSE;
2844
2845 return TRUE;
2846 }
2847
2848 static void
2849 add_pager_refs(struct view *view, struct line *line)
2850 {
2851 char buf[SIZEOF_STR];
2852 char *commit_id = (char *)line->data + STRING_SIZE("commit ");
2853 struct ref **refs;
2854 size_t bufpos = 0, refpos = 0;
2855 const char *sep = "Refs: ";
2856 bool is_tag = FALSE;
2857
2858 assert(line->type == LINE_COMMIT);
2859
2860 refs = get_refs(commit_id);
2861 if (!refs) {
2862 if (view == VIEW(REQ_VIEW_DIFF))
2863 goto try_add_describe_ref;
2864 return;
2865 }
2866
2867 do {
2868 struct ref *ref = refs[refpos];
2869 char *fmt = ref->tag ? "%s[%s]" :
2870 ref->remote ? "%s<%s>" : "%s%s";
2871
2872 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2873 return;
2874 sep = ", ";
2875 if (ref->tag)
2876 is_tag = TRUE;
2877 } while (refs[refpos++]->next);
2878
2879 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2880 try_add_describe_ref:
2881 /* Add <tag>-g<commit_id> "fake" reference. */
2882 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2883 return;
2884 }
2885
2886 if (bufpos == 0)
2887 return;
2888
2889 if (!realloc_lines(view, view->line_size + 1))
2890 return;
2891
2892 add_line_text(view, buf, LINE_PP_REFS);
2893 }
2894
2895 static bool
2896 pager_read(struct view *view, char *data)
2897 {
2898 struct line *line;
2899
2900 if (!data)
2901 return TRUE;
2902
2903 line = add_line_text(view, data, get_line_type(data));
2904 if (!line)
2905 return FALSE;
2906
2907 if (line->type == LINE_COMMIT &&
2908 (view == VIEW(REQ_VIEW_DIFF) ||
2909 view == VIEW(REQ_VIEW_LOG)))
2910 add_pager_refs(view, line);
2911
2912 return TRUE;
2913 }
2914
2915 static enum request
2916 pager_request(struct view *view, enum request request, struct line *line)
2917 {
2918 int split = 0;
2919
2920 if (request != REQ_ENTER)
2921 return request;
2922
2923 if (line->type == LINE_COMMIT &&
2924 (view == VIEW(REQ_VIEW_LOG) ||
2925 view == VIEW(REQ_VIEW_PAGER))) {
2926 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2927 split = 1;
2928 }
2929
2930 /* Always scroll the view even if it was split. That way
2931 * you can use Enter to scroll through the log view and
2932 * split open each commit diff. */
2933 scroll_view(view, REQ_SCROLL_LINE_DOWN);
2934
2935 /* FIXME: A minor workaround. Scrolling the view will call report("")
2936 * but if we are scrolling a non-current view this won't properly
2937 * update the view title. */
2938 if (split)
2939 update_view_title(view);
2940
2941 return REQ_NONE;
2942 }
2943
2944 static bool
2945 pager_grep(struct view *view, struct line *line)
2946 {
2947 regmatch_t pmatch;
2948 char *text = line->data;
2949
2950 if (!*text)
2951 return FALSE;
2952
2953 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
2954 return FALSE;
2955
2956 return TRUE;
2957 }
2958
2959 static void
2960 pager_select(struct view *view, struct line *line)
2961 {
2962 if (line->type == LINE_COMMIT) {
2963 char *text = (char *)line->data + STRING_SIZE("commit ");
2964
2965 if (view != VIEW(REQ_VIEW_PAGER))
2966 string_copy_rev(view->ref, text);
2967 string_copy_rev(ref_commit, text);
2968 }
2969 }
2970
2971 static struct view_ops pager_ops = {
2972 "line",
2973 NULL,
2974 pager_read,
2975 pager_draw,
2976 pager_request,
2977 pager_grep,
2978 pager_select,
2979 };
2980
2981
2982 /*
2983 * Help backend
2984 */
2985
2986 static bool
2987 help_open(struct view *view)
2988 {
2989 char buf[BUFSIZ];
2990 int lines = ARRAY_SIZE(req_info) + 2;
2991 int i;
2992
2993 if (view->lines > 0)
2994 return TRUE;
2995
2996 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2997 if (!req_info[i].request)
2998 lines++;
2999
3000 lines += run_requests + 1;
3001
3002 view->line = calloc(lines, sizeof(*view->line));
3003 if (!view->line)
3004 return FALSE;
3005
3006 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
3007
3008 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
3009 char *key;
3010
3011 if (req_info[i].request == REQ_NONE)
3012 continue;
3013
3014 if (!req_info[i].request) {
3015 add_line_text(view, "", LINE_DEFAULT);
3016 add_line_text(view, req_info[i].help, LINE_DEFAULT);
3017 continue;
3018 }
3019
3020 key = get_key(req_info[i].request);
3021 if (!*key)
3022 key = "(no key defined)";
3023
3024 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
3025 continue;
3026
3027 add_line_text(view, buf, LINE_DEFAULT);
3028 }
3029
3030 if (run_requests) {
3031 add_line_text(view, "", LINE_DEFAULT);
3032 add_line_text(view, "External commands:", LINE_DEFAULT);
3033 }
3034
3035 for (i = 0; i < run_requests; i++) {
3036 struct run_request *req = get_run_request(REQ_NONE + i + 1);
3037 char *key;
3038
3039 if (!req)
3040 continue;
3041
3042 key = get_key_name(req->key);
3043 if (!*key)
3044 key = "(no key defined)";
3045
3046 if (!string_format(buf, " %-10s %-14s `%s`",
3047 keymap_table[req->keymap].name,
3048 key, req->cmd))
3049 continue;
3050
3051 add_line_text(view, buf, LINE_DEFAULT);
3052 }
3053
3054 return TRUE;
3055 }
3056
3057 static struct view_ops help_ops = {
3058 "line",
3059 help_open,
3060 NULL,
3061 pager_draw,
3062 pager_request,
3063 pager_grep,
3064 pager_select,
3065 };
3066
3067
3068 /*
3069 * Tree backend
3070 */
3071
3072 struct tree_stack_entry {
3073 struct tree_stack_entry *prev; /* Entry below this in the stack */
3074 unsigned long lineno; /* Line number to restore */
3075 char *name; /* Position of name in opt_path */
3076 };
3077
3078 /* The top of the path stack. */
3079 static struct tree_stack_entry *tree_stack = NULL;
3080 unsigned long tree_lineno = 0;
3081
3082 static void
3083 pop_tree_stack_entry(void)
3084 {
3085 struct tree_stack_entry *entry = tree_stack;
3086
3087 tree_lineno = entry->lineno;
3088 entry->name[0] = 0;
3089 tree_stack = entry->prev;
3090 free(entry);
3091 }
3092
3093 static void
3094 push_tree_stack_entry(char *name, unsigned long lineno)
3095 {
3096 struct tree_stack_entry *entry = calloc(1, sizeof(*entry));
3097 size_t pathlen = strlen(opt_path);
3098
3099 if (!entry)
3100 return;
3101
3102 entry->prev = tree_stack;
3103 entry->name = opt_path + pathlen;
3104 tree_stack = entry;
3105
3106 if (!string_format_from(opt_path, &pathlen, "%s/", name)) {
3107 pop_tree_stack_entry();
3108 return;
3109 }
3110
3111 /* Move the current line to the first tree entry. */
3112 tree_lineno = 1;
3113 entry->lineno = lineno;
3114 }
3115
3116 /* Parse output from git-ls-tree(1):
3117 *
3118 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
3119 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
3120 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
3121 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
3122 */
3123
3124 #define SIZEOF_TREE_ATTR \
3125 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
3126
3127 #define TREE_UP_FORMAT "040000 tree %s\t.."
3128
3129 static int
3130 tree_compare_entry(enum line_type type1, char *name1,
3131 enum line_type type2, char *name2)
3132 {
3133 if (type1 != type2) {
3134 if (type1 == LINE_TREE_DIR)
3135 return -1;
3136 return 1;
3137 }
3138
3139 return strcmp(name1, name2);
3140 }
3141
3142 static char *
3143 tree_path(struct line *line)
3144 {
3145 char *path = line->data;
3146
3147 return path + SIZEOF_TREE_ATTR;
3148 }
3149
3150 static bool
3151 tree_read(struct view *view, char *text)
3152 {
3153 size_t textlen = text ? strlen(text) : 0;
3154 char buf[SIZEOF_STR];
3155 unsigned long pos;
3156 enum line_type type;
3157 bool first_read = view->lines == 0;
3158
3159 if (!text)
3160 return TRUE;
3161 if (textlen <= SIZEOF_TREE_ATTR)
3162 return FALSE;
3163
3164 type = text[STRING_SIZE("100644 ")] == 't'
3165 ? LINE_TREE_DIR : LINE_TREE_FILE;
3166
3167 if (first_read) {
3168 /* Add path info line */
3169 if (!string_format(buf, "Directory path /%s", opt_path) ||
3170 !realloc_lines(view, view->line_size + 1) ||
3171 !add_line_text(view, buf, LINE_DEFAULT))
3172 return FALSE;
3173
3174 /* Insert "link" to parent directory. */
3175 if (*opt_path) {
3176 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
3177 !realloc_lines(view, view->line_size + 1) ||
3178 !add_line_text(view, buf, LINE_TREE_DIR))
3179 return FALSE;
3180 }
3181 }
3182
3183 /* Strip the path part ... */
3184 if (*opt_path) {
3185 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
3186 size_t striplen = strlen(opt_path);
3187 char *path = text + SIZEOF_TREE_ATTR;
3188
3189 if (pathlen > striplen)
3190 memmove(path, path + striplen,
3191 pathlen - striplen + 1);
3192 }
3193
3194 /* Skip "Directory ..." and ".." line. */
3195 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
3196 struct line *line = &view->line[pos];
3197 char *path1 = tree_path(line);
3198 char *path2 = text + SIZEOF_TREE_ATTR;
3199 int cmp = tree_compare_entry(line->type, path1, type, path2);
3200
3201 if (cmp <= 0)
3202 continue;
3203
3204 text = strdup(text);
3205 if (!text)
3206 return FALSE;
3207
3208 if (view->lines > pos)
3209 memmove(&view->line[pos + 1], &view->line[pos],
3210 (view->lines - pos) * sizeof(*line));
3211
3212 line = &view->line[pos];
3213 line->data = text;
3214 line->type = type;
3215 view->lines++;
3216 return TRUE;
3217 }
3218
3219 if (!add_line_text(view, text, type))
3220 return FALSE;
3221
3222 if (tree_lineno > view->lineno) {
3223 view->lineno = tree_lineno;
3224 tree_lineno = 0;
3225 }
3226
3227 return TRUE;
3228 }
3229
3230 static enum request
3231 tree_request(struct view *view, enum request request, struct line *line)
3232 {
3233 enum open_flags flags;
3234
3235 if (request == REQ_VIEW_BLAME) {
3236 char *filename = tree_path(line);
3237
3238 if (line->type == LINE_TREE_DIR) {
3239 report("Cannot show blame for directory %s", opt_path);
3240 return REQ_NONE;
3241 }
3242
3243 string_copy(opt_ref, ref_commit);
3244 string_ncopy(opt_file, filename, strlen(filename));
3245 return request;
3246 }
3247 if (request == REQ_TREE_PARENT) {
3248 if (*opt_path) {
3249 /* fake 'cd ..' */
3250 request = REQ_ENTER;
3251 line = &view->line[1];
3252 } else {
3253 /* quit view if at top of tree */
3254 return REQ_VIEW_CLOSE;
3255 }
3256 }
3257 if (request != REQ_ENTER)
3258 return request;
3259
3260 /* Cleanup the stack if the tree view is at a different tree. */
3261 while (!*opt_path && tree_stack)
3262 pop_tree_stack_entry();
3263
3264 switch (line->type) {
3265 case LINE_TREE_DIR:
3266 /* Depending on whether it is a subdir or parent (updir?) link
3267 * mangle the path buffer. */
3268 if (line == &view->line[1] && *opt_path) {
3269 pop_tree_stack_entry();
3270
3271 } else {
3272 char *basename = tree_path(line);
3273
3274 push_tree_stack_entry(basename, view->lineno);
3275 }
3276
3277 /* Trees and subtrees share the same ID, so they are not not
3278 * unique like blobs. */
3279 flags = OPEN_RELOAD;
3280 request = REQ_VIEW_TREE;
3281 break;
3282
3283 case LINE_TREE_FILE:
3284 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3285 request = REQ_VIEW_BLOB;
3286 break;
3287
3288 default:
3289 return TRUE;
3290 }
3291
3292 open_view(view, request, flags);
3293 if (request == REQ_VIEW_TREE) {
3294 view->lineno = tree_lineno;
3295 }
3296
3297 return REQ_NONE;
3298 }
3299
3300 static void
3301 tree_select(struct view *view, struct line *line)
3302 {
3303 char *text = (char *)line->data + STRING_SIZE("100644 blob ");
3304
3305 if (line->type == LINE_TREE_FILE) {
3306 string_copy_rev(ref_blob, text);
3307
3308 } else if (line->type != LINE_TREE_DIR) {
3309 return;
3310 }
3311
3312 string_copy_rev(view->ref, text);
3313 }
3314
3315 static struct view_ops tree_ops = {
3316 "file",
3317 NULL,
3318 tree_read,
3319 pager_draw,
3320 tree_request,
3321 pager_grep,
3322 tree_select,
3323 };
3324
3325 static bool
3326 blob_read(struct view *view, char *line)
3327 {
3328 if (!line)
3329 return TRUE;
3330 return add_line_text(view, line, LINE_DEFAULT) != NULL;
3331 }
3332
3333 static struct view_ops blob_ops = {
3334 "line",
3335 NULL,
3336 blob_read,
3337 pager_draw,
3338 pager_request,
3339 pager_grep,
3340 pager_select,
3341 };
3342
3343 /*
3344 * Blame backend
3345 *
3346 * Loading the blame view is a two phase job:
3347 *
3348 * 1. File content is read either using opt_file from the
3349 * filesystem or using git-cat-file.
3350 * 2. Then blame information is incrementally added by
3351 * reading output from git-blame.
3352 */
3353
3354 struct blame_commit {
3355 char id[SIZEOF_REV]; /* SHA1 ID. */
3356 char title[128]; /* First line of the commit message. */
3357 char author[75]; /* Author of the commit. */
3358 struct tm time; /* Date from the author ident. */
3359 char filename[128]; /* Name of file. */
3360 };
3361
3362 struct blame {
3363 struct blame_commit *commit;
3364 unsigned int header:1;
3365 char text[1];
3366 };
3367
3368 #define BLAME_CAT_FILE_CMD "git cat-file blob %s:%s"
3369 #define BLAME_INCREMENTAL_CMD "git blame --incremental %s %s"
3370
3371 static bool
3372 blame_open(struct view *view)
3373 {
3374 char path[SIZEOF_STR];
3375 char ref[SIZEOF_STR] = "";
3376
3377 if (sq_quote(path, 0, opt_file) >= sizeof(path))
3378 return FALSE;
3379
3380 if (*opt_ref && sq_quote(ref, 0, opt_ref) >= sizeof(ref))
3381 return FALSE;
3382
3383 if (*opt_ref) {
3384 if (!string_format(view->cmd, BLAME_CAT_FILE_CMD, ref, path))
3385 return FALSE;
3386 } else {
3387 view->pipe = fopen(opt_file, "r");
3388 if (!view->pipe &&
3389 !string_format(view->cmd, BLAME_CAT_FILE_CMD, "HEAD", path))
3390 return FALSE;
3391 }
3392
3393 if (!view->pipe)
3394 view->pipe = popen(view->cmd, "r");
3395 if (!view->pipe)
3396 return FALSE;
3397
3398 if (!string_format(view->cmd, BLAME_INCREMENTAL_CMD, ref, path))
3399 return FALSE;
3400
3401 string_format(view->ref, "%s ...", opt_file);
3402 string_copy_rev(view->vid, opt_file);
3403 set_nonblocking_input(TRUE);
3404
3405 if (view->line) {
3406 int i;
3407
3408 for (i = 0; i < view->lines; i++)
3409 free(view->line[i].data);
3410 free(view->line);
3411 }
3412
3413 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3414 view->offset = view->lines = view->lineno = 0;
3415 view->line = NULL;
3416 view->start_time = time(NULL);
3417 }
3418
3419 static struct blame_commit *
3420 get_blame_commit(struct view *view, const char *id)
3421 {
3422 size_t i;
3423
3424 for (i = 0; i < view->lines; i++) {
3425 struct blame *blame = view->line[i].data;
3426
3427 if (!blame->commit)
3428 continue;
3429
3430 if (!strncmp(blame->commit->id, id, SIZEOF_REV - 1))
3431 return blame->commit;
3432 }
3433
3434 {
3435 struct blame_commit *commit = calloc(1, sizeof(*commit));
3436
3437 if (commit)
3438 string_ncopy(commit->id, id, SIZEOF_REV);
3439 return commit;
3440 }
3441 }
3442
3443 static bool
3444 parse_number(char **posref, size_t *number, size_t min, size_t max)
3445 {
3446 char *pos = *posref;
3447
3448 *posref = NULL;
3449 pos = strchr(pos + 1, ' ');
3450 if (!pos || !isdigit(pos[1]))
3451 return FALSE;
3452 *number = atoi(pos + 1);
3453 if (*number < min || *number > max)
3454 return FALSE;
3455
3456 *posref = pos;
3457 return TRUE;
3458 }
3459
3460 static struct blame_commit *
3461 parse_blame_commit(struct view *view, char *text, int *blamed)
3462 {
3463 struct blame_commit *commit;
3464 struct blame *blame;
3465 char *pos = text + SIZEOF_REV - 1;
3466 size_t lineno;
3467 size_t group;
3468 struct line *line;
3469
3470 if (strlen(text) <= SIZEOF_REV || *pos != ' ')
3471 return NULL;
3472
3473 if (!parse_number(&pos, &lineno, 1, view->lines) ||
3474 !parse_number(&pos, &group, 1, view->lines - lineno + 1))
3475 return NULL;
3476
3477 commit = get_blame_commit(view, text);
3478 if (!commit)
3479 return NULL;
3480
3481 *blamed += group;
3482 while (group--) {
3483 struct line *line = &view->line[lineno + group - 1];
3484
3485 blame = line->data;
3486 blame->commit = commit;
3487 line->dirty = 1;
3488 }
3489 blame->header = 1;
3490
3491 return commit;
3492 }
3493
3494 static bool
3495 blame_read_file(struct view *view, char *line)
3496 {
3497 if (!line) {
3498 FILE *pipe = NULL;
3499
3500 if (view->lines > 0)
3501 pipe = popen(view->cmd, "r");
3502 view->cmd[0] = 0;
3503 if (!pipe) {
3504 report("Failed to load blame data");
3505 return TRUE;
3506 }
3507
3508 fclose(view->pipe);
3509 view->pipe = pipe;
3510 return FALSE;
3511
3512 } else {
3513 size_t linelen = strlen(line);
3514 struct blame *blame = malloc(sizeof(*blame) + linelen);
3515
3516 if (!line)
3517 return FALSE;
3518
3519 blame->commit = NULL;
3520 strncpy(blame->text, line, linelen);
3521 blame->text[linelen] = 0;
3522 return add_line_data(view, blame, LINE_BLAME_COMMIT) != NULL;
3523 }
3524 }
3525
3526 static bool
3527 match_blame_header(const char *name, char **line)
3528 {
3529 size_t namelen = strlen(name);
3530 bool matched = !strncmp(name, *line, namelen);
3531
3532 if (matched)
3533 *line += namelen;
3534
3535 return matched;
3536 }
3537
3538 static bool
3539 blame_read(struct view *view, char *line)
3540 {
3541 static struct blame_commit *commit = NULL;
3542 static int blamed = 0;
3543 static time_t author_time;
3544
3545 if (*view->cmd)
3546 return blame_read_file(view, line);
3547
3548 if (!line) {
3549 /* Reset all! */
3550 commit = NULL;
3551 blamed = 0;
3552 string_format(view->ref, "%s", view->vid);
3553 if (view_is_displayed(view)) {
3554 update_view_title(view);
3555 redraw_view_from(view, 0);
3556 }
3557 return TRUE;
3558 }
3559
3560 if (!commit) {
3561 commit = parse_blame_commit(view, line, &blamed);
3562 string_format(view->ref, "%s %2d%%", view->vid,
3563 blamed * 100 / view->lines);
3564
3565 } else if (match_blame_header("author ", &line)) {
3566 string_ncopy(commit->author, line, strlen(line));
3567
3568 } else if (match_blame_header("author-time ", &line)) {
3569 author_time = (time_t) atol(line);
3570
3571 } else if (match_blame_header("author-tz ", &line)) {
3572 long tz;
3573
3574 tz = ('0' - line[1]) * 60 * 60 * 10;
3575 tz += ('0' - line[2]) * 60 * 60;
3576 tz += ('0' - line[3]) * 60;
3577 tz += ('0' - line[4]) * 60;
3578
3579 if (line[0] == '-')
3580 tz = -tz;
3581
3582 author_time -= tz;
3583 gmtime_r(&author_time, &commit->time);
3584
3585 } else if (match_blame_header("summary ", &line)) {
3586 string_ncopy(commit->title, line, strlen(line));
3587
3588 } else if (match_blame_header("filename ", &line)) {
3589 string_ncopy(commit->filename, line, strlen(line));
3590 commit = NULL;
3591 }
3592
3593 return TRUE;
3594 }
3595
3596 static bool
3597 blame_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
3598 {
3599 int tilde_attr = -1;
3600 struct blame *blame = line->data;
3601 int col = 0;
3602
3603 wmove(view->win, lineno, 0);
3604
3605 if (selected) {
3606 wattrset(view->win, get_line_attr(LINE_CURSOR));
3607 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
3608 } else {
3609 wattrset(view->win, A_NORMAL);
3610 tilde_attr = get_line_attr(LINE_MAIN_DELIM);
3611 }
3612
3613 if (opt_date) {
3614 int n;
3615
3616 if (!selected)
3617 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
3618 if (blame->commit) {
3619 char buf[DATE_COLS + 1];
3620 int timelen;
3621
3622 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &blame->commit->time);
3623 n = draw_text(view, buf, view->width - col, FALSE, tilde_attr);
3624 draw_text(view, " ", view->width - col - n, FALSE, tilde_attr);
3625 }
3626
3627 col += DATE_COLS;
3628 wmove(view->win, lineno, col);
3629 if (col >= view->width)
3630 return TRUE;
3631 }
3632
3633 if (opt_author) {
3634 int max = MIN(AUTHOR_COLS - 1, view->width - col);
3635
3636 if (!selected)
3637 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
3638 if (blame->commit)
3639 draw_text(view, blame->commit->author, max, TRUE, tilde_attr);
3640 col += AUTHOR_COLS;
3641 if (col >= view->width)
3642 return TRUE;
3643 wmove(view->win, lineno, col);
3644 }
3645
3646 {
3647 int max = MIN(ID_COLS - 1, view->width - col);
3648
3649 if (!selected)
3650 wattrset(view->win, get_line_attr(LINE_BLAME_ID));
3651 if (blame->commit)
3652 draw_text(view, blame->commit->id, max, FALSE, -1);
3653 col += ID_COLS;
3654 if (col >= view->width)
3655 return TRUE;
3656 wmove(view->win, lineno, col);
3657 }
3658
3659 {
3660 unsigned long real_lineno = view->offset + lineno + 1;
3661 char number[10] = " ";
3662 int max = MIN(view->digits, STRING_SIZE(number));
3663 bool showtrimmed = FALSE;
3664
3665 if (real_lineno == 1 ||
3666 (real_lineno % opt_num_interval) == 0) {
3667 char fmt[] = "%1ld";
3668
3669 if (view->digits <= 9)
3670 fmt[1] = '0' + view->digits;
3671
3672 if (!string_format(number, fmt, real_lineno))
3673 number[0] = 0;
3674 showtrimmed = TRUE;
3675 }
3676
3677 if (max > view->width - col)
3678 max = view->width - col;
3679 if (!selected)
3680 wattrset(view->win, get_line_attr(LINE_BLAME_LINENO));
3681 col += draw_text(view, number, max, showtrimmed, tilde_attr);
3682 if (col >= view->width)
3683 return TRUE;
3684 }
3685
3686 if (!selected)
3687 wattrset(view->win, A_NORMAL);
3688
3689 if (col >= view->width)
3690 return TRUE;
3691 waddch(view->win, ACS_VLINE);
3692 col++;
3693 if (col >= view->width)
3694 return TRUE;
3695 waddch(view->win, ' ');
3696 col++;
3697 col += draw_text(view, blame->text, view->width - col, TRUE, tilde_attr);
3698
3699 return TRUE;
3700 }
3701
3702 static enum request
3703 blame_request(struct view *view, enum request request, struct line *line)
3704 {
3705 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3706 struct blame *blame = line->data;
3707
3708 switch (request) {
3709 case REQ_ENTER:
3710 if (!blame->commit) {
3711 report("No commit loaded yet");
3712 break;
3713 }
3714
3715 if (!strcmp(blame->commit->id, "0000000000000000000000000000000000000000")) {
3716 char path[SIZEOF_STR];
3717
3718 if (sq_quote(path, 0, view->vid) >= sizeof(path))
3719 break;
3720 string_format(opt_cmd, "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s 2>/dev/null", path);
3721 }
3722
3723 open_view(view, REQ_VIEW_DIFF, flags);
3724 break;
3725
3726 default:
3727 return request;
3728 }
3729
3730 return REQ_NONE;
3731 }
3732
3733 static bool
3734 blame_grep(struct view *view, struct line *line)
3735 {
3736 struct blame *blame = line->data;
3737 struct blame_commit *commit = blame->commit;
3738 regmatch_t pmatch;
3739
3740 #define MATCH(text) \
3741 (*text && regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3742
3743 if (commit) {
3744 char buf[DATE_COLS + 1];
3745
3746 if (MATCH(commit->title) ||
3747 MATCH(commit->author) ||
3748 MATCH(commit->id))
3749 return TRUE;
3750
3751 if (strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time) &&
3752 MATCH(buf))
3753 return TRUE;
3754 }
3755
3756 return MATCH(blame->text);
3757
3758 #undef MATCH
3759 }
3760
3761 static void
3762 blame_select(struct view *view, struct line *line)
3763 {
3764 struct blame *blame = line->data;
3765 struct blame_commit *commit = blame->commit;
3766
3767 if (!commit)
3768 return;
3769
3770 if (!strcmp(commit->id, "0000000000000000000000000000000000000000"))
3771 string_ncopy(ref_commit, "HEAD", 4);
3772 else
3773 string_copy_rev(ref_commit, commit->id);
3774 }
3775
3776 static struct view_ops blame_ops = {
3777 "line",
3778 blame_open,
3779 blame_read,
3780 blame_draw,
3781 blame_request,
3782 blame_grep,
3783 blame_select,
3784 };
3785
3786 /*
3787 * Status backend
3788 */
3789
3790 struct status {
3791 char status;
3792 struct {
3793 mode_t mode;
3794 char rev[SIZEOF_REV];
3795 char name[SIZEOF_STR];
3796 } old;
3797 struct {
3798 mode_t mode;
3799 char rev[SIZEOF_REV];
3800 char name[SIZEOF_STR];
3801 } new;
3802 };
3803
3804 static struct status stage_status;
3805 static enum line_type stage_line_type;
3806
3807 /* Get fields from the diff line:
3808 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
3809 */
3810 static inline bool
3811 status_get_diff(struct status *file, char *buf, size_t bufsize)
3812 {
3813 char *old_mode = buf + 1;
3814 char *new_mode = buf + 8;
3815 char *old_rev = buf + 15;
3816 char *new_rev = buf + 56;
3817 char *status = buf + 97;
3818
3819 if (bufsize < 99 ||
3820 old_mode[-1] != ':' ||
3821 new_mode[-1] != ' ' ||
3822 old_rev[-1] != ' ' ||
3823 new_rev[-1] != ' ' ||
3824 status[-1] != ' ')
3825 return FALSE;
3826
3827 file->status = *status;
3828
3829 string_copy_rev(file->old.rev, old_rev);
3830 string_copy_rev(file->new.rev, new_rev);
3831
3832 file->old.mode = strtoul(old_mode, NULL, 8);
3833 file->new.mode = strtoul(new_mode, NULL, 8);
3834
3835 file->old.name[0] = file->new.name[0] = 0;
3836
3837 return TRUE;
3838 }
3839
3840 static bool
3841 status_run(struct view *view, const char cmd[], bool diff, enum line_type type)
3842 {
3843 struct status *file = NULL;
3844 struct status *unmerged = NULL;
3845 char buf[SIZEOF_STR * 4];
3846 size_t bufsize = 0;
3847 FILE *pipe;
3848
3849 pipe = popen(cmd, "r");
3850 if (!pipe)
3851 return FALSE;
3852
3853 add_line_data(view, NULL, type);
3854
3855 while (!feof(pipe) && !ferror(pipe)) {
3856 char *sep;
3857 size_t readsize;
3858
3859 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
3860 if (!readsize)
3861 break;
3862 bufsize += readsize;
3863
3864 /* Process while we have NUL chars. */
3865 while ((sep = memchr(buf, 0, bufsize))) {
3866 size_t sepsize = sep - buf + 1;
3867
3868 if (!file) {
3869 if (!realloc_lines(view, view->line_size + 1))
3870 goto error_out;
3871
3872 file = calloc(1, sizeof(*file));
3873 if (!file)
3874 goto error_out;
3875
3876 add_line_data(view, file, type);
3877 }
3878
3879 /* Parse diff info part. */
3880 if (!diff) {
3881 file->status = '?';
3882
3883 } else if (!file->status) {
3884 if (!status_get_diff(file, buf, sepsize))
3885 goto error_out;
3886
3887 bufsize -= sepsize;
3888 memmove(buf, sep + 1, bufsize);
3889
3890 sep = memchr(buf, 0, bufsize);
3891 if (!sep)
3892 break;
3893 sepsize = sep - buf + 1;
3894
3895 /* Collapse all 'M'odified entries that
3896 * follow a associated 'U'nmerged entry.
3897 */
3898 if (file->status == 'U') {
3899 unmerged = file;
3900
3901 } else if (unmerged) {
3902 int collapse = !strcmp(buf, unmerged->new.name);
3903
3904 unmerged = NULL;
3905 if (collapse) {
3906 free(file);
3907 view->lines--;
3908 continue;
3909 }
3910 }
3911 }
3912
3913 /* Grab the old name for rename/copy. */
3914 if (!*file->old.name &&
3915 (file->status == 'R' || file->status == 'C')) {
3916 sepsize = sep - buf + 1;
3917 string_ncopy(file->old.name, buf, sepsize);
3918 bufsize -= sepsize;
3919 memmove(buf, sep + 1, bufsize);
3920
3921 sep = memchr(buf, 0, bufsize);
3922 if (!sep)
3923 break;
3924 sepsize = sep - buf + 1;
3925 }
3926
3927 /* git-ls-files just delivers a NUL separated
3928 * list of file names similar to the second half
3929 * of the git-diff-* output. */
3930 string_ncopy(file->new.name, buf, sepsize);
3931 if (!*file->old.name)
3932 string_copy(file->old.name, file->new.name);
3933 bufsize -= sepsize;
3934 memmove(buf, sep + 1, bufsize);
3935 file = NULL;
3936 }
3937 }
3938
3939 if (ferror(pipe)) {
3940 error_out:
3941 pclose(pipe);
3942 return FALSE;
3943 }
3944
3945 if (!view->line[view->lines - 1].data)
3946 add_line_data(view, NULL, LINE_STAT_NONE);
3947
3948 pclose(pipe);
3949 return TRUE;
3950 }
3951
3952 /* Don't show unmerged entries in the staged section. */
3953 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached -M HEAD"
3954 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
3955 #define STATUS_LIST_OTHER_CMD \
3956 "git ls-files -z --others --exclude-per-directory=.gitignore"
3957
3958 #define STATUS_DIFF_INDEX_SHOW_CMD \
3959 "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s %s 2>/dev/null"
3960
3961 #define STATUS_DIFF_FILES_SHOW_CMD \
3962 "git diff-files --root --patch-with-stat -C -M -- %s %s 2>/dev/null"
3963
3964 /* First parse staged info using git-diff-index(1), then parse unstaged
3965 * info using git-diff-files(1), and finally untracked files using
3966 * git-ls-files(1). */
3967 static bool
3968 status_open(struct view *view)
3969 {
3970 struct stat statbuf;
3971 char exclude[SIZEOF_STR];
3972 char cmd[SIZEOF_STR];
3973 unsigned long prev_lineno = view->lineno;
3974 size_t i;
3975
3976 for (i = 0; i < view->lines; i++)
3977 free(view->line[i].data);
3978 free(view->line);
3979 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3980 view->line = NULL;
3981
3982 if (!realloc_lines(view, view->line_size + 6))
3983 return FALSE;
3984
3985 if (!string_format(exclude, "%s/info/exclude", opt_git_dir))
3986 return FALSE;
3987
3988 string_copy(cmd, STATUS_LIST_OTHER_CMD);
3989
3990 if (stat(exclude, &statbuf) >= 0) {
3991 size_t cmdsize = strlen(cmd);
3992
3993 if (!string_format_from(cmd, &cmdsize, " %s", "--exclude-from=") ||
3994 sq_quote(cmd, cmdsize, exclude) >= sizeof(cmd))
3995 return FALSE;
3996 }
3997
3998 system("git update-index -q --refresh");
3999
4000 if (!status_run(view, STATUS_DIFF_INDEX_CMD, TRUE, LINE_STAT_STAGED) ||
4001 !status_run(view, STATUS_DIFF_FILES_CMD, TRUE, LINE_STAT_UNSTAGED) ||
4002 !status_run(view, cmd, FALSE, LINE_STAT_UNTRACKED))
4003 return FALSE;
4004
4005 /* If all went well restore the previous line number to stay in
4006 * the context. */
4007 if (prev_lineno < view->lines)
4008 view->lineno = prev_lineno;
4009 else
4010 view->lineno = view->lines - 1;
4011
4012 return TRUE;
4013 }
4014
4015 static bool
4016 status_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
4017 {
4018 struct status *status = line->data;
4019 int tilde_attr = get_line_attr(LINE_MAIN_DELIM);
4020
4021 wmove(view->win, lineno, 0);
4022
4023 if (selected) {
4024 wattrset(view->win, get_line_attr(LINE_CURSOR));
4025 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
4026 tilde_attr = -1;
4027
4028 } else if (!status && line->type != LINE_STAT_NONE) {
4029 wattrset(view->win, get_line_attr(LINE_STAT_SECTION));
4030 wchgat(view->win, -1, 0, LINE_STAT_SECTION, NULL);
4031
4032 } else {
4033 wattrset(view->win, get_line_attr(line->type));
4034 }
4035
4036 if (!status) {
4037 char *text;
4038
4039 switch (line->type) {
4040 case LINE_STAT_STAGED:
4041 text = "Changes to be committed:";
4042 break;
4043
4044 case LINE_STAT_UNSTAGED:
4045 text = "Changed but not updated:";
4046 break;
4047
4048 case LINE_STAT_UNTRACKED:
4049 text = "Untracked files:";
4050 break;
4051
4052 case LINE_STAT_NONE:
4053 text = " (no files)";
4054 break;
4055
4056 default:
4057 return FALSE;
4058 }
4059
4060 draw_text(view, text, view->width, TRUE, tilde_attr);
4061 return TRUE;
4062 }
4063
4064 waddch(view->win, status->status);
4065 if (!selected)
4066 wattrset(view->win, A_NORMAL);
4067 wmove(view->win, lineno, 4);
4068 if (view->width < 5)
4069 return TRUE;
4070
4071 draw_text(view, status->new.name, view->width - 5, TRUE, tilde_attr);
4072 return TRUE;
4073 }
4074
4075 static enum request
4076 status_enter(struct view *view, struct line *line)
4077 {
4078 struct status *status = line->data;
4079 char oldpath[SIZEOF_STR] = "";
4080 char newpath[SIZEOF_STR] = "";
4081 char *info;
4082 size_t cmdsize = 0;
4083
4084 if (line->type == LINE_STAT_NONE ||
4085 (!status && line[1].type == LINE_STAT_NONE)) {
4086 report("No file to diff");
4087 return REQ_NONE;
4088 }
4089
4090 if (status) {
4091 if (sq_quote(oldpath, 0, status->old.name) >= sizeof(oldpath))
4092 return REQ_QUIT;
4093 /* Diffs for unmerged entries are empty when pasing the
4094 * new path, so leave it empty. */
4095 if (status->status != 'U' &&
4096 sq_quote(newpath, 0, status->new.name) >= sizeof(newpath))
4097 return REQ_QUIT;
4098 }
4099
4100 if (opt_cdup[0] &&
4101 line->type != LINE_STAT_UNTRACKED &&
4102 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
4103 return REQ_QUIT;
4104
4105 switch (line->type) {
4106 case LINE_STAT_STAGED:
4107 if (!string_format_from(opt_cmd, &cmdsize,
4108 STATUS_DIFF_INDEX_SHOW_CMD, oldpath, newpath))
4109 return REQ_QUIT;
4110 if (status)
4111 info = "Staged changes to %s";
4112 else
4113 info = "Staged changes";
4114 break;
4115
4116 case LINE_STAT_UNSTAGED:
4117 if (!string_format_from(opt_cmd, &cmdsize,
4118 STATUS_DIFF_FILES_SHOW_CMD, oldpath, newpath))
4119 return REQ_QUIT;
4120 if (status)
4121 info = "Unstaged changes to %s";
4122 else
4123 info = "Unstaged changes";
4124 break;
4125
4126 case LINE_STAT_UNTRACKED:
4127 if (opt_pipe)
4128 return REQ_QUIT;
4129
4130
4131 if (!status) {
4132 report("No file to show");
4133 return REQ_NONE;
4134 }
4135
4136 opt_pipe = fopen(status->new.name, "r");
4137 info = "Untracked file %s";
4138 break;
4139
4140 default:
4141 die("line type %d not handled in switch", line->type);
4142 }
4143
4144 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | OPEN_SPLIT);
4145 if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) {
4146 if (status) {
4147 stage_status = *status;
4148 } else {
4149 memset(&stage_status, 0, sizeof(stage_status));
4150 }
4151
4152 stage_line_type = line->type;
4153 string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.new.name);
4154 }
4155
4156 return REQ_NONE;
4157 }
4158
4159
4160 static bool
4161 status_update_file(struct view *view, struct status *status, enum line_type type)
4162 {
4163 char cmd[SIZEOF_STR];
4164 char buf[SIZEOF_STR];
4165 size_t cmdsize = 0;
4166 size_t bufsize = 0;
4167 size_t written = 0;
4168 FILE *pipe;
4169
4170 if (opt_cdup[0] &&
4171 type != LINE_STAT_UNTRACKED &&
4172 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4173 return FALSE;
4174
4175 switch (type) {
4176 case LINE_STAT_STAGED:
4177 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
4178 status->old.mode,
4179 status->old.rev,
4180 status->old.name, 0))
4181 return FALSE;
4182
4183 string_add(cmd, cmdsize, "git update-index -z --index-info");
4184 break;
4185
4186 case LINE_STAT_UNSTAGED:
4187 case LINE_STAT_UNTRACKED:
4188 if (!string_format_from(buf, &bufsize, "%s%c", status->new.name, 0))
4189 return FALSE;
4190
4191 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
4192 break;
4193
4194 default:
4195 die("line type %d not handled in switch", type);
4196 }
4197
4198 pipe = popen(cmd, "w");
4199 if (!pipe)
4200 return FALSE;
4201
4202 while (!ferror(pipe) && written < bufsize) {
4203 written += fwrite(buf + written, 1, bufsize - written, pipe);
4204 }
4205
4206 pclose(pipe);
4207
4208 if (written != bufsize)
4209 return FALSE;
4210
4211 return TRUE;
4212 }
4213
4214 static void
4215 status_update(struct view *view)
4216 {
4217 struct line *line = &view->line[view->lineno];
4218
4219 assert(view->lines);
4220
4221 if (!line->data) {
4222 while (++line < view->line + view->lines && line->data) {
4223 if (!status_update_file(view, line->data, line->type))
4224 report("Failed to update file status");
4225 }
4226
4227 if (!line[-1].data) {
4228 report("Nothing to update");
4229 return;
4230 }
4231
4232 } else if (!status_update_file(view, line->data, line->type)) {
4233 report("Failed to update file status");
4234 }
4235 }
4236
4237 static enum request
4238 status_request(struct view *view, enum request request, struct line *line)
4239 {
4240 struct status *status = line->data;
4241
4242 switch (request) {
4243 case REQ_STATUS_UPDATE:
4244 status_update(view);
4245 break;
4246
4247 case REQ_STATUS_MERGE:
4248 if (!status || status->status != 'U') {
4249 report("Merging only possible for files with unmerged status ('U').");
4250 return REQ_NONE;
4251 }
4252 open_mergetool(status->new.name);
4253 break;
4254
4255 case REQ_EDIT:
4256 if (!status)
4257 return request;
4258
4259 open_editor(status->status != '?', status->new.name);
4260 break;
4261
4262 case REQ_VIEW_BLAME:
4263 if (status) {
4264 string_copy(opt_file, status->new.name);
4265 opt_ref[0] = 0;
4266 }
4267 return request;
4268
4269 case REQ_ENTER:
4270 /* After returning the status view has been split to
4271 * show the stage view. No further reloading is
4272 * necessary. */
4273 status_enter(view, line);
4274 return REQ_NONE;
4275
4276 case REQ_REFRESH:
4277 /* Simply reload the view. */
4278 break;
4279
4280 default:
4281 return request;
4282 }
4283
4284 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
4285
4286 return REQ_NONE;
4287 }
4288
4289 static void
4290 status_select(struct view *view, struct line *line)
4291 {
4292 struct status *status = line->data;
4293 char file[SIZEOF_STR] = "all files";
4294 char *text;
4295 char *key;
4296
4297 if (status && !string_format(file, "'%s'", status->new.name))
4298 return;
4299
4300 if (!status && line[1].type == LINE_STAT_NONE)
4301 line++;
4302
4303 switch (line->type) {
4304 case LINE_STAT_STAGED:
4305 text = "Press %s to unstage %s for commit";
4306 break;
4307
4308 case LINE_STAT_UNSTAGED:
4309 text = "Press %s to stage %s for commit";
4310 break;
4311
4312 case LINE_STAT_UNTRACKED:
4313 text = "Press %s to stage %s for addition";
4314 break;
4315
4316 case LINE_STAT_NONE:
4317 text = "Nothing to update";
4318 break;
4319
4320 default:
4321 die("line type %d not handled in switch", line->type);
4322 }
4323
4324 if (status && status->status == 'U') {
4325 text = "Press %s to resolve conflict in %s";
4326 key = get_key(REQ_STATUS_MERGE);
4327
4328 } else {
4329 key = get_key(REQ_STATUS_UPDATE);
4330 }
4331
4332 string_format(view->ref, text, key, file);
4333 }
4334
4335 static bool
4336 status_grep(struct view *view, struct line *line)
4337 {
4338 struct status *status = line->data;
4339 enum { S_STATUS, S_NAME, S_END } state;
4340 char buf[2] = "?";
4341 regmatch_t pmatch;
4342
4343 if (!status)
4344 return FALSE;
4345
4346 for (state = S_STATUS; state < S_END; state++) {
4347 char *text;
4348
4349 switch (state) {
4350 case S_NAME: text = status->new.name; break;
4351 case S_STATUS:
4352 buf[0] = status->status;
4353 text = buf;
4354 break;
4355
4356 default:
4357 return FALSE;
4358 }
4359
4360 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4361 return TRUE;
4362 }
4363
4364 return FALSE;
4365 }
4366
4367 static struct view_ops status_ops = {
4368 "file",
4369 status_open,
4370 NULL,
4371 status_draw,
4372 status_request,
4373 status_grep,
4374 status_select,
4375 };
4376
4377
4378 static bool
4379 stage_diff_line(FILE *pipe, struct line *line)
4380 {
4381 char *buf = line->data;
4382 size_t bufsize = strlen(buf);
4383 size_t written = 0;
4384
4385 while (!ferror(pipe) && written < bufsize) {
4386 written += fwrite(buf + written, 1, bufsize - written, pipe);
4387 }
4388
4389 fputc('\n', pipe);
4390
4391 return written == bufsize;
4392 }
4393
4394 static struct line *
4395 stage_diff_hdr(struct view *view, struct line *line)
4396 {
4397 int diff_hdr_dir = line->type == LINE_DIFF_CHUNK ? -1 : 1;
4398 struct line *diff_hdr;
4399
4400 if (line->type == LINE_DIFF_CHUNK)
4401 diff_hdr = line - 1;
4402 else
4403 diff_hdr = view->line + 1;
4404
4405 while (diff_hdr > view->line && diff_hdr < view->line + view->lines) {
4406 if (diff_hdr->type == LINE_DIFF_HEADER)
4407 return diff_hdr;
4408
4409 diff_hdr += diff_hdr_dir;
4410 }
4411
4412 return NULL;
4413 }
4414
4415 static bool
4416 stage_update_chunk(struct view *view, struct line *line)
4417 {
4418 char cmd[SIZEOF_STR];
4419 size_t cmdsize = 0;
4420 struct line *diff_hdr, *diff_chunk, *diff_end;
4421 FILE *pipe;
4422
4423 diff_hdr = stage_diff_hdr(view, line);
4424 if (!diff_hdr)
4425 return FALSE;
4426
4427 if (opt_cdup[0] &&
4428 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4429 return FALSE;
4430
4431 if (!string_format_from(cmd, &cmdsize,
4432 "git apply --cached %s - && "
4433 "git update-index -q --unmerged --refresh 2>/dev/null",
4434 stage_line_type == LINE_STAT_STAGED ? "-R" : ""))
4435 return FALSE;
4436
4437 pipe = popen(cmd, "w");
4438 if (!pipe)
4439 return FALSE;
4440
4441 diff_end = view->line + view->lines;
4442 if (line->type != LINE_DIFF_CHUNK) {
4443 diff_chunk = diff_hdr;
4444
4445 } else {
4446 for (diff_chunk = line + 1; diff_chunk < diff_end; diff_chunk++)
4447 if (diff_chunk->type == LINE_DIFF_CHUNK ||
4448 diff_chunk->type == LINE_DIFF_HEADER)
4449 diff_end = diff_chunk;
4450
4451 diff_chunk = line;
4452
4453 while (diff_hdr->type != LINE_DIFF_CHUNK) {
4454 switch (diff_hdr->type) {
4455 case LINE_DIFF_HEADER:
4456 case LINE_DIFF_INDEX:
4457 case LINE_DIFF_ADD:
4458 case LINE_DIFF_DEL:
4459 break;
4460
4461 default:
4462 diff_hdr++;
4463 continue;
4464 }
4465
4466 if (!stage_diff_line(pipe, diff_hdr++)) {
4467 pclose(pipe);
4468 return FALSE;
4469 }
4470 }
4471 }
4472
4473 while (diff_chunk < diff_end && stage_diff_line(pipe, diff_chunk))
4474 diff_chunk++;
4475
4476 pclose(pipe);
4477
4478 if (diff_chunk != diff_end)
4479 return FALSE;
4480
4481 return TRUE;
4482 }
4483
4484 static void
4485 stage_update(struct view *view, struct line *line)
4486 {
4487 if (stage_line_type != LINE_STAT_UNTRACKED &&
4488 (line->type == LINE_DIFF_CHUNK || !stage_status.status)) {
4489 if (!stage_update_chunk(view, line)) {
4490 report("Failed to apply chunk");
4491 return;
4492 }
4493
4494 } else if (!status_update_file(view, &stage_status, stage_line_type)) {
4495 report("Failed to update file");
4496 return;
4497 }
4498
4499 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
4500
4501 view = VIEW(REQ_VIEW_STATUS);
4502 if (view_is_displayed(view))
4503 status_enter(view, &view->line[view->lineno]);
4504 }
4505
4506 static enum request
4507 stage_request(struct view *view, enum request request, struct line *line)
4508 {
4509 switch (request) {
4510 case REQ_STATUS_UPDATE:
4511 stage_update(view, line);
4512 break;
4513
4514 case REQ_EDIT:
4515 if (!stage_status.new.name[0])
4516 return request;
4517
4518 open_editor(stage_status.status != '?', stage_status.new.name);
4519 break;
4520
4521 case REQ_VIEW_BLAME:
4522 if (stage_status.new.name[0]) {
4523 string_copy(opt_file, stage_status.new.name);
4524 opt_ref[0] = 0;
4525 }
4526 return request;
4527
4528 case REQ_ENTER:
4529 pager_request(view, request, line);
4530 break;
4531
4532 default:
4533 return request;
4534 }
4535
4536 return REQ_NONE;
4537 }
4538
4539 static struct view_ops stage_ops = {
4540 "line",
4541 NULL,
4542 pager_read,
4543 pager_draw,
4544 stage_request,
4545 pager_grep,
4546 pager_select,
4547 };
4548
4549
4550 /*
4551 * Revision graph
4552 */
4553
4554 struct commit {
4555 char id[SIZEOF_REV]; /* SHA1 ID. */
4556 char title[128]; /* First line of the commit message. */
4557 char author[75]; /* Author of the commit. */
4558 struct tm time; /* Date from the author ident. */
4559 struct ref **refs; /* Repository references. */
4560 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
4561 size_t graph_size; /* The width of the graph array. */
4562 bool has_parents; /* Rewritten --parents seen. */
4563 };
4564
4565 /* Size of rev graph with no "padding" columns */
4566 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
4567
4568 struct rev_graph {
4569 struct rev_graph *prev, *next, *parents;
4570 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
4571 size_t size;
4572 struct commit *commit;
4573 size_t pos;
4574 unsigned int boundary:1;
4575 };
4576
4577 /* Parents of the commit being visualized. */
4578 static struct rev_graph graph_parents[4];
4579
4580 /* The current stack of revisions on the graph. */
4581 static struct rev_graph graph_stacks[4] = {
4582 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
4583 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
4584 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
4585 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
4586 };
4587
4588 static inline bool
4589 graph_parent_is_merge(struct rev_graph *graph)
4590 {
4591 return graph->parents->size > 1;
4592 }
4593
4594 static inline void
4595 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
4596 {
4597 struct commit *commit = graph->commit;
4598
4599 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
4600 commit->graph[commit->graph_size++] = symbol;
4601 }
4602
4603 static void
4604 done_rev_graph(struct rev_graph *graph)
4605 {
4606 if (graph_parent_is_merge(graph) &&
4607 graph->pos < graph->size - 1 &&
4608 graph->next->size == graph->size + graph->parents->size - 1) {
4609 size_t i = graph->pos + graph->parents->size - 1;
4610
4611 graph->commit->graph_size = i * 2;
4612 while (i < graph->next->size - 1) {
4613 append_to_rev_graph(graph, ' ');
4614 append_to_rev_graph(graph, '\\');
4615 i++;
4616 }
4617 }
4618
4619 graph->size = graph->pos = 0;
4620 graph->commit = NULL;
4621 memset(graph->parents, 0, sizeof(*graph->parents));
4622 }
4623
4624 static void
4625 push_rev_graph(struct rev_graph *graph, char *parent)
4626 {
4627 int i;
4628
4629 /* "Collapse" duplicate parents lines.
4630 *
4631 * FIXME: This needs to also update update the drawn graph but
4632 * for now it just serves as a method for pruning graph lines. */
4633 for (i = 0; i < graph->size; i++)
4634 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
4635 return;
4636
4637 if (graph->size < SIZEOF_REVITEMS) {
4638 string_copy_rev(graph->rev[graph->size++], parent);
4639 }
4640 }
4641
4642 static chtype
4643 get_rev_graph_symbol(struct rev_graph *graph)
4644 {
4645 chtype symbol;
4646
4647 if (graph->boundary)
4648 symbol = REVGRAPH_BOUND;
4649 else if (graph->parents->size == 0)
4650 symbol = REVGRAPH_INIT;
4651 else if (graph_parent_is_merge(graph))
4652 symbol = REVGRAPH_MERGE;
4653 else if (graph->pos >= graph->size)
4654 symbol = REVGRAPH_BRANCH;
4655 else
4656 symbol = REVGRAPH_COMMIT;
4657
4658 return symbol;
4659 }
4660
4661 static void
4662 draw_rev_graph(struct rev_graph *graph)
4663 {
4664 struct rev_filler {
4665 chtype separator, line;
4666 };
4667 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
4668 static struct rev_filler fillers[] = {
4669 { ' ', REVGRAPH_LINE },
4670 { '`', '.' },
4671 { '\'', ' ' },
4672 { '/', ' ' },
4673 };
4674 chtype symbol = get_rev_graph_symbol(graph);
4675 struct rev_filler *filler;
4676 size_t i;
4677
4678 filler = &fillers[DEFAULT];
4679
4680 for (i = 0; i < graph->pos; i++) {
4681 append_to_rev_graph(graph, filler->line);
4682 if (graph_parent_is_merge(graph->prev) &&
4683 graph->prev->pos == i)
4684 filler = &fillers[RSHARP];
4685
4686 append_to_rev_graph(graph, filler->separator);
4687 }
4688
4689 /* Place the symbol for this revision. */
4690 append_to_rev_graph(graph, symbol);
4691
4692 if (graph->prev->size > graph->size)
4693 filler = &fillers[RDIAG];
4694 else
4695 filler = &fillers[DEFAULT];
4696
4697 i++;
4698
4699 for (; i < graph->size; i++) {
4700 append_to_rev_graph(graph, filler->separator);
4701 append_to_rev_graph(graph, filler->line);
4702 if (graph_parent_is_merge(graph->prev) &&
4703 i < graph->prev->pos + graph->parents->size)
4704 filler = &fillers[RSHARP];
4705 if (graph->prev->size > graph->size)
4706 filler = &fillers[LDIAG];
4707 }
4708
4709 if (graph->prev->size > graph->size) {
4710 append_to_rev_graph(graph, filler->separator);
4711 if (filler->line != ' ')
4712 append_to_rev_graph(graph, filler->line);
4713 }
4714 }
4715
4716 /* Prepare the next rev graph */
4717 static void
4718 prepare_rev_graph(struct rev_graph *graph)
4719 {
4720 size_t i;
4721
4722 /* First, traverse all lines of revisions up to the active one. */
4723 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
4724 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
4725 break;
4726
4727 push_rev_graph(graph->next, graph->rev[graph->pos]);
4728 }
4729
4730 /* Interleave the new revision parent(s). */
4731 for (i = 0; !graph->boundary && i < graph->parents->size; i++)
4732 push_rev_graph(graph->next, graph->parents->rev[i]);
4733
4734 /* Lastly, put any remaining revisions. */
4735 for (i = graph->pos + 1; i < graph->size; i++)
4736 push_rev_graph(graph->next, graph->rev[i]);
4737 }
4738
4739 static void
4740 update_rev_graph(struct rev_graph *graph)
4741 {
4742 /* If this is the finalizing update ... */
4743 if (graph->commit)
4744 prepare_rev_graph(graph);
4745
4746 /* Graph visualization needs a one rev look-ahead,
4747 * so the first update doesn't visualize anything. */
4748 if (!graph->prev->commit)
4749 return;
4750
4751 draw_rev_graph(graph->prev);
4752 done_rev_graph(graph->prev->prev);
4753 }
4754
4755
4756 /*
4757 * Main view backend
4758 */
4759
4760 static bool
4761 main_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
4762 {
4763 char buf[DATE_COLS + 1];
4764 struct commit *commit = line->data;
4765 enum line_type type;
4766 int col = 0;
4767 size_t timelen;
4768 int tilde_attr;
4769 int space;
4770
4771 if (!*commit->author)
4772 return FALSE;
4773
4774 space = view->width;
4775 wmove(view->win, lineno, col);
4776
4777 if (selected) {
4778 type = LINE_CURSOR;
4779 wattrset(view->win, get_line_attr(type));
4780 wchgat(view->win, -1, 0, type, NULL);
4781 tilde_attr = -1;
4782 } else {
4783 type = LINE_MAIN_COMMIT;
4784 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
4785 tilde_attr = get_line_attr(LINE_MAIN_DELIM);
4786 }
4787
4788 if (opt_date) {
4789 int n;
4790
4791 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
4792 n = draw_text(view, buf, view->width - col, FALSE, tilde_attr);
4793 draw_text(view, " ", view->width - col - n, FALSE, tilde_attr);
4794
4795 col += DATE_COLS;
4796 wmove(view->win, lineno, col);
4797 if (col >= view->width)
4798 return TRUE;
4799 }
4800 if (type != LINE_CURSOR)
4801 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
4802
4803 if (opt_author) {
4804 int max_len;
4805
4806 max_len = view->width - col;
4807 if (max_len > AUTHOR_COLS - 1)
4808 max_len = AUTHOR_COLS - 1;
4809 draw_text(view, commit->author, max_len, TRUE, tilde_attr);
4810 col += AUTHOR_COLS;
4811 if (col >= view->width)
4812 return TRUE;
4813 }
4814
4815 if (opt_rev_graph && commit->graph_size) {
4816 size_t graph_size = view->width - col;
4817 size_t i;
4818
4819 if (type != LINE_CURSOR)
4820 wattrset(view->win, get_line_attr(LINE_MAIN_REVGRAPH));
4821 wmove(view->win, lineno, col);
4822 if (graph_size > commit->graph_size)
4823 graph_size = commit->graph_size;
4824 /* Using waddch() instead of waddnstr() ensures that
4825 * they'll be rendered correctly for the cursor line. */
4826 for (i = 0; i < graph_size; i++)
4827 waddch(view->win, commit->graph[i]);
4828
4829 col += commit->graph_size + 1;
4830 if (col >= view->width)
4831 return TRUE;
4832 waddch(view->win, ' ');
4833 }
4834 if (type != LINE_CURSOR)
4835 wattrset(view->win, A_NORMAL);
4836
4837 wmove(view->win, lineno, col);
4838
4839 if (opt_show_refs && commit->refs) {
4840 size_t i = 0;
4841
4842 do {
4843 if (type == LINE_CURSOR)
4844 ;
4845 else if (commit->refs[i]->ltag)
4846 wattrset(view->win, get_line_attr(LINE_MAIN_LOCAL_TAG));
4847 else if (commit->refs[i]->tag)
4848 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
4849 else if (commit->refs[i]->remote)
4850 wattrset(view->win, get_line_attr(LINE_MAIN_REMOTE));
4851 else
4852 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
4853
4854 col += draw_text(view, "[", view->width - col, TRUE, tilde_attr);
4855 col += draw_text(view, commit->refs[i]->name, view->width - col,
4856 TRUE, tilde_attr);
4857 col += draw_text(view, "]", view->width - col, TRUE, tilde_attr);
4858 if (type != LINE_CURSOR)
4859 wattrset(view->win, A_NORMAL);
4860 col += draw_text(view, " ", view->width - col, TRUE, tilde_attr);
4861 if (col >= view->width)
4862 return TRUE;
4863 } while (commit->refs[i++]->next);
4864 }
4865
4866 if (type != LINE_CURSOR)
4867 wattrset(view->win, get_line_attr(type));
4868
4869 draw_text(view, commit->title, view->width - col, TRUE, tilde_attr);
4870 return TRUE;
4871 }
4872
4873 /* Reads git log --pretty=raw output and parses it into the commit struct. */
4874 static bool
4875 main_read(struct view *view, char *line)
4876 {
4877 static struct rev_graph *graph = graph_stacks;
4878 enum line_type type;
4879 struct commit *commit;
4880
4881 if (!line) {
4882 update_rev_graph(graph);
4883 return TRUE;
4884 }
4885
4886 type = get_line_type(line);
4887 if (type == LINE_COMMIT) {
4888 commit = calloc(1, sizeof(struct commit));
4889 if (!commit)
4890 return FALSE;
4891
4892 line += STRING_SIZE("commit ");
4893 if (*line == '-') {
4894 graph->boundary = 1;
4895 line++;
4896 }
4897
4898 string_copy_rev(commit->id, line);
4899 commit->refs = get_refs(commit->id);
4900 graph->commit = commit;
4901 add_line_data(view, commit, LINE_MAIN_COMMIT);
4902
4903 while ((line = strchr(line, ' '))) {
4904 line++;
4905 push_rev_graph(graph->parents, line);
4906 commit->has_parents = TRUE;
4907 }
4908 return TRUE;
4909 }
4910
4911 if (!view->lines)
4912 return TRUE;
4913 commit = view->line[view->lines - 1].data;
4914
4915 switch (type) {
4916 case LINE_PARENT:
4917 if (commit->has_parents)
4918 break;
4919 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
4920 break;
4921
4922 case LINE_AUTHOR:
4923 {
4924 /* Parse author lines where the name may be empty:
4925 * author <email@address.tld> 1138474660 +0100
4926 */
4927 char *ident = line + STRING_SIZE("author ");
4928 char *nameend = strchr(ident, '<');
4929 char *emailend = strchr(ident, '>');
4930
4931 if (!nameend || !emailend)
4932 break;
4933
4934 update_rev_graph(graph);
4935 graph = graph->next;
4936
4937 *nameend = *emailend = 0;
4938 ident = chomp_string(ident);
4939 if (!*ident) {
4940 ident = chomp_string(nameend + 1);
4941 if (!*ident)
4942 ident = "Unknown";
4943 }
4944
4945 string_ncopy(commit->author, ident, strlen(ident));
4946
4947 /* Parse epoch and timezone */
4948 if (emailend[1] == ' ') {
4949 char *secs = emailend + 2;
4950 char *zone = strchr(secs, ' ');
4951 time_t time = (time_t) atol(secs);
4952
4953 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
4954 long tz;
4955
4956 zone++;
4957 tz = ('0' - zone[1]) * 60 * 60 * 10;
4958 tz += ('0' - zone[2]) * 60 * 60;
4959 tz += ('0' - zone[3]) * 60;
4960 tz += ('0' - zone[4]) * 60;
4961
4962 if (zone[0] == '-')
4963 tz = -tz;
4964
4965 time -= tz;
4966 }
4967
4968 gmtime_r(&time, &commit->time);
4969 }
4970 break;
4971 }
4972 default:
4973 /* Fill in the commit title if it has not already been set. */
4974 if (commit->title[0])
4975 break;
4976
4977 /* Require titles to start with a non-space character at the
4978 * offset used by git log. */
4979 if (strncmp(line, " ", 4))
4980 break;
4981 line += 4;
4982 /* Well, if the title starts with a whitespace character,
4983 * try to be forgiving. Otherwise we end up with no title. */
4984 while (isspace(*line))
4985 line++;
4986 if (*line == '\0')
4987 break;
4988 /* FIXME: More graceful handling of titles; append "..." to
4989 * shortened titles, etc. */
4990
4991 string_ncopy(commit->title, line, strlen(line));
4992 }
4993
4994 return TRUE;
4995 }
4996
4997 static enum request
4998 main_request(struct view *view, enum request request, struct line *line)
4999 {
5000 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
5001
5002 if (request == REQ_ENTER)
5003 open_view(view, REQ_VIEW_DIFF, flags);
5004 else
5005 return request;
5006
5007 return REQ_NONE;
5008 }
5009
5010 static bool
5011 main_grep(struct view *view, struct line *line)
5012 {
5013 struct commit *commit = line->data;
5014 enum { S_TITLE, S_AUTHOR, S_DATE, S_END } state;
5015 char buf[DATE_COLS + 1];
5016 regmatch_t pmatch;
5017
5018 for (state = S_TITLE; state < S_END; state++) {
5019 char *text;
5020
5021 switch (state) {
5022 case S_TITLE: text = commit->title; break;
5023 case S_AUTHOR: text = commit->author; break;
5024 case S_DATE:
5025 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
5026 continue;
5027 text = buf;
5028 break;
5029
5030 default:
5031 return FALSE;
5032 }
5033
5034 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
5035 return TRUE;
5036 }
5037
5038 return FALSE;
5039 }
5040
5041 static void
5042 main_select(struct view *view, struct line *line)
5043 {
5044 struct commit *commit = line->data;
5045
5046 string_copy_rev(view->ref, commit->id);
5047 string_copy_rev(ref_commit, view->ref);
5048 }
5049
5050 static struct view_ops main_ops = {
5051 "commit",
5052 NULL,
5053 main_read,
5054 main_draw,
5055 main_request,
5056 main_grep,
5057 main_select,
5058 };
5059
5060
5061 /*
5062 * Unicode / UTF-8 handling
5063 *
5064 * NOTE: Much of the following code for dealing with unicode is derived from
5065 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
5066 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
5067 */
5068
5069 /* I've (over)annotated a lot of code snippets because I am not entirely
5070 * confident that the approach taken by this small UTF-8 interface is correct.
5071 * --jonas */
5072
5073 static inline int
5074 unicode_width(unsigned long c)
5075 {
5076 if (c >= 0x1100 &&
5077 (c <= 0x115f /* Hangul Jamo */
5078 || c == 0x2329
5079 || c == 0x232a
5080 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
5081 /* CJK ... Yi */
5082 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
5083 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
5084 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
5085 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
5086 || (c >= 0xffe0 && c <= 0xffe6)
5087 || (c >= 0x20000 && c <= 0x2fffd)
5088 || (c >= 0x30000 && c <= 0x3fffd)))
5089 return 2;
5090
5091 if (c == '\t')
5092 return opt_tab_size;
5093
5094 return 1;
5095 }
5096
5097 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
5098 * Illegal bytes are set one. */
5099 static const unsigned char utf8_bytes[256] = {
5100 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,
5101 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,
5102 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,
5103 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,
5104 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,
5105 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,
5106 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,
5107 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,
5108 };
5109
5110 /* Decode UTF-8 multi-byte representation into a unicode character. */
5111 static inline unsigned long
5112 utf8_to_unicode(const char *string, size_t length)
5113 {
5114 unsigned long unicode;
5115
5116 switch (length) {
5117 case 1:
5118 unicode = string[0];
5119 break;
5120 case 2:
5121 unicode = (string[0] & 0x1f) << 6;
5122 unicode += (string[1] & 0x3f);
5123 break;
5124 case 3:
5125 unicode = (string[0] & 0x0f) << 12;
5126 unicode += ((string[1] & 0x3f) << 6);
5127 unicode += (string[2] & 0x3f);
5128 break;
5129 case 4:
5130 unicode = (string[0] & 0x0f) << 18;
5131 unicode += ((string[1] & 0x3f) << 12);
5132 unicode += ((string[2] & 0x3f) << 6);
5133 unicode += (string[3] & 0x3f);
5134 break;
5135 case 5:
5136 unicode = (string[0] & 0x0f) << 24;
5137 unicode += ((string[1] & 0x3f) << 18);
5138 unicode += ((string[2] & 0x3f) << 12);
5139 unicode += ((string[3] & 0x3f) << 6);
5140 unicode += (string[4] & 0x3f);
5141 break;
5142 case 6:
5143 unicode = (string[0] & 0x01) << 30;
5144 unicode += ((string[1] & 0x3f) << 24);
5145 unicode += ((string[2] & 0x3f) << 18);
5146 unicode += ((string[3] & 0x3f) << 12);
5147 unicode += ((string[4] & 0x3f) << 6);
5148 unicode += (string[5] & 0x3f);
5149 break;
5150 default:
5151 die("Invalid unicode length");
5152 }
5153
5154 /* Invalid characters could return the special 0xfffd value but NUL
5155 * should be just as good. */
5156 return unicode > 0xffff ? 0 : unicode;
5157 }
5158
5159 /* Calculates how much of string can be shown within the given maximum width
5160 * and sets trimmed parameter to non-zero value if all of string could not be
5161 * shown. If the reserve flag is TRUE, it will reserve at least one
5162 * trailing character, which can be useful when drawing a delimiter.
5163 *
5164 * Returns the number of bytes to output from string to satisfy max_width. */
5165 static size_t
5166 utf8_length(const char *string, size_t max_width, int *trimmed, bool reserve)
5167 {
5168 const char *start = string;
5169 const char *end = strchr(string, '\0');
5170 unsigned char last_bytes = 0;
5171 size_t width = 0;
5172
5173 *trimmed = 0;
5174
5175 while (string < end) {
5176 int c = *(unsigned char *) string;
5177 unsigned char bytes = utf8_bytes[c];
5178 size_t ucwidth;
5179 unsigned long unicode;
5180
5181 if (string + bytes > end)
5182 break;
5183
5184 /* Change representation to figure out whether
5185 * it is a single- or double-width character. */
5186
5187 unicode = utf8_to_unicode(string, bytes);
5188 /* FIXME: Graceful handling of invalid unicode character. */
5189 if (!unicode)
5190 break;
5191
5192 ucwidth = unicode_width(unicode);
5193 width += ucwidth;
5194 if (width > max_width) {
5195 *trimmed = 1;
5196 if (reserve && width - ucwidth == max_width) {
5197 string -= last_bytes;
5198 }
5199 break;
5200 }
5201
5202 string += bytes;
5203 last_bytes = bytes;
5204 }
5205
5206 return string - start;
5207 }
5208
5209
5210 /*
5211 * Status management
5212 */
5213
5214 /* Whether or not the curses interface has been initialized. */
5215 static bool cursed = FALSE;
5216
5217 /* The status window is used for polling keystrokes. */
5218 static WINDOW *status_win;
5219
5220 static bool status_empty = TRUE;
5221
5222 /* Update status and title window. */
5223 static void
5224 report(const char *msg, ...)
5225 {
5226 struct view *view = display[current_view];
5227
5228 if (input_mode)
5229 return;
5230
5231 if (!view) {
5232 char buf[SIZEOF_STR];
5233 va_list args;
5234
5235 va_start(args, msg);
5236 if (vsnprintf(buf, sizeof(buf), msg, args) >= sizeof(buf)) {
5237 buf[sizeof(buf) - 1] = 0;
5238 buf[sizeof(buf) - 2] = '.';
5239 buf[sizeof(buf) - 3] = '.';
5240 buf[sizeof(buf) - 4] = '.';
5241 }
5242 va_end(args);
5243 die("%s", buf);
5244 }
5245
5246 if (!status_empty || *msg) {
5247 va_list args;
5248
5249 va_start(args, msg);
5250
5251 wmove(status_win, 0, 0);
5252 if (*msg) {
5253 vwprintw(status_win, msg, args);
5254 status_empty = FALSE;
5255 } else {
5256 status_empty = TRUE;
5257 }
5258 wclrtoeol(status_win);
5259 wrefresh(status_win);
5260
5261 va_end(args);
5262 }
5263
5264 update_view_title(view);
5265 update_display_cursor(view);
5266 }
5267
5268 /* Controls when nodelay should be in effect when polling user input. */
5269 static void
5270 set_nonblocking_input(bool loading)
5271 {
5272 static unsigned int loading_views;
5273
5274 if ((loading == FALSE && loading_views-- == 1) ||
5275 (loading == TRUE && loading_views++ == 0))
5276 nodelay(status_win, loading);
5277 }
5278
5279 static void
5280 init_display(void)
5281 {
5282 int x, y;
5283
5284 /* Initialize the curses library */
5285 if (isatty(STDIN_FILENO)) {
5286 cursed = !!initscr();
5287 } else {
5288 /* Leave stdin and stdout alone when acting as a pager. */
5289 FILE *io = fopen("/dev/tty", "r+");
5290
5291 if (!io)
5292 die("Failed to open /dev/tty");
5293 cursed = !!newterm(NULL, io, io);
5294 }
5295
5296 if (!cursed)
5297 die("Failed to initialize curses");
5298
5299 nonl(); /* Tell curses not to do NL->CR/NL on output */
5300 cbreak(); /* Take input chars one at a time, no wait for \n */
5301 noecho(); /* Don't echo input */
5302 leaveok(stdscr, TRUE);
5303
5304 if (has_colors())
5305 init_colors();
5306
5307 getmaxyx(stdscr, y, x);
5308 status_win = newwin(1, 0, y - 1, 0);
5309 if (!status_win)
5310 die("Failed to create status window");
5311
5312 /* Enable keyboard mapping */
5313 keypad(status_win, TRUE);
5314 wbkgdset(status_win, get_line_attr(LINE_STATUS));
5315 }
5316
5317 static char *
5318 read_prompt(const char *prompt)
5319 {
5320 enum { READING, STOP, CANCEL } status = READING;
5321 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
5322 int pos = 0;
5323
5324 while (status == READING) {
5325 struct view *view;
5326 int i, key;
5327
5328 input_mode = TRUE;
5329
5330 foreach_view (view, i)
5331 update_view(view);
5332
5333 input_mode = FALSE;
5334
5335 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
5336 wclrtoeol(status_win);
5337
5338 /* Refresh, accept single keystroke of input */
5339 key = wgetch(status_win);
5340 switch (key) {
5341 case KEY_RETURN:
5342 case KEY_ENTER:
5343 case '\n':
5344 status = pos ? STOP : CANCEL;
5345 break;
5346
5347 case KEY_BACKSPACE:
5348 if (pos > 0)
5349 pos--;
5350 else
5351 status = CANCEL;
5352 break;
5353
5354 case KEY_ESC:
5355 status = CANCEL;
5356 break;
5357
5358 case ERR:
5359 break;
5360
5361 default:
5362 if (pos >= sizeof(buf)) {
5363 report("Input string too long");
5364 return NULL;
5365 }
5366
5367 if (isprint(key))
5368 buf[pos++] = (char) key;
5369 }
5370 }
5371
5372 /* Clear the status window */
5373 status_empty = FALSE;
5374 report("");
5375
5376 if (status == CANCEL)
5377 return NULL;
5378
5379 buf[pos++] = 0;
5380
5381 return buf;
5382 }
5383
5384 /*
5385 * Repository references
5386 */
5387
5388 static struct ref *refs = NULL;
5389 static size_t refs_alloc = 0;
5390 static size_t refs_size = 0;
5391
5392 /* Id <-> ref store */
5393 static struct ref ***id_refs = NULL;
5394 static size_t id_refs_alloc = 0;
5395 static size_t id_refs_size = 0;
5396
5397 static struct ref **
5398 get_refs(char *id)
5399 {
5400 struct ref ***tmp_id_refs;
5401 struct ref **ref_list = NULL;
5402 size_t ref_list_alloc = 0;
5403 size_t ref_list_size = 0;
5404 size_t i;
5405
5406 for (i = 0; i < id_refs_size; i++)
5407 if (!strcmp(id, id_refs[i][0]->id))
5408 return id_refs[i];
5409
5410 tmp_id_refs = realloc_items(id_refs, &id_refs_alloc, id_refs_size + 1,
5411 sizeof(*id_refs));
5412 if (!tmp_id_refs)
5413 return NULL;
5414
5415 id_refs = tmp_id_refs;
5416
5417 for (i = 0; i < refs_size; i++) {
5418 struct ref **tmp;
5419
5420 if (strcmp(id, refs[i].id))
5421 continue;
5422
5423 tmp = realloc_items(ref_list, &ref_list_alloc,
5424 ref_list_size + 1, sizeof(*ref_list));
5425 if (!tmp) {
5426 if (ref_list)
5427 free(ref_list);
5428 return NULL;
5429 }
5430
5431 ref_list = tmp;
5432 if (ref_list_size > 0)
5433 ref_list[ref_list_size - 1]->next = 1;
5434 ref_list[ref_list_size] = &refs[i];
5435
5436 /* XXX: The properties of the commit chains ensures that we can
5437 * safely modify the shared ref. The repo references will
5438 * always be similar for the same id. */
5439 ref_list[ref_list_size]->next = 0;
5440 ref_list_size++;
5441 }
5442
5443 if (ref_list)
5444 id_refs[id_refs_size++] = ref_list;
5445
5446 return ref_list;
5447 }
5448
5449 static int
5450 read_ref(char *id, size_t idlen, char *name, size_t namelen)
5451 {
5452 struct ref *ref;
5453 bool tag = FALSE;
5454 bool ltag = FALSE;
5455 bool remote = FALSE;
5456 bool check_replace = FALSE;
5457
5458 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
5459 if (!strcmp(name + namelen - 3, "^{}")) {
5460 namelen -= 3;
5461 name[namelen] = 0;
5462 if (refs_size > 0 && refs[refs_size - 1].ltag == TRUE)
5463 check_replace = TRUE;
5464 } else {
5465 ltag = TRUE;
5466 }
5467
5468 tag = TRUE;
5469 namelen -= STRING_SIZE("refs/tags/");
5470 name += STRING_SIZE("refs/tags/");
5471
5472 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
5473 remote = TRUE;
5474 namelen -= STRING_SIZE("refs/remotes/");
5475 name += STRING_SIZE("refs/remotes/");
5476
5477 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5478 namelen -= STRING_SIZE("refs/heads/");
5479 name += STRING_SIZE("refs/heads/");
5480
5481 } else if (!strcmp(name, "HEAD")) {
5482 return OK;
5483 }
5484
5485 if (check_replace && !strcmp(name, refs[refs_size - 1].name)) {
5486 /* it's an annotated tag, replace the previous sha1 with the
5487 * resolved commit id; relies on the fact git-ls-remote lists
5488 * the commit id of an annotated tag right beofre the commit id
5489 * it points to. */
5490 refs[refs_size - 1].ltag = ltag;
5491 string_copy_rev(refs[refs_size - 1].id, id);
5492
5493 return OK;
5494 }
5495 refs = realloc_items(refs, &refs_alloc, refs_size + 1, sizeof(*refs));
5496 if (!refs)
5497 return ERR;
5498
5499 ref = &refs[refs_size++];
5500 ref->name = malloc(namelen + 1);
5501 if (!ref->name)
5502 return ERR;
5503
5504 strncpy(ref->name, name, namelen);
5505 ref->name[namelen] = 0;
5506 ref->tag = tag;
5507 ref->ltag = ltag;
5508 ref->remote = remote;
5509 string_copy_rev(ref->id, id);
5510
5511 return OK;
5512 }
5513
5514 static int
5515 load_refs(void)
5516 {
5517 const char *cmd_env = getenv("TIG_LS_REMOTE");
5518 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
5519
5520 return read_properties(popen(cmd, "r"), "\t", read_ref);
5521 }
5522
5523 static int
5524 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
5525 {
5526 if (!strcmp(name, "i18n.commitencoding"))
5527 string_ncopy(opt_encoding, value, valuelen);
5528
5529 if (!strcmp(name, "core.editor"))
5530 string_ncopy(opt_editor, value, valuelen);
5531
5532 return OK;
5533 }
5534
5535 static int
5536 load_repo_config(void)
5537 {
5538 return read_properties(popen(GIT_CONFIG " --list", "r"),
5539 "=", read_repo_config_option);
5540 }
5541
5542 static int
5543 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
5544 {
5545 if (!opt_git_dir[0]) {
5546 string_ncopy(opt_git_dir, name, namelen);
5547
5548 } else if (opt_is_inside_work_tree == -1) {
5549 /* This can be 3 different values depending on the
5550 * version of git being used. If git-rev-parse does not
5551 * understand --is-inside-work-tree it will simply echo
5552 * the option else either "true" or "false" is printed.
5553 * Default to true for the unknown case. */
5554 opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
5555
5556 } else {
5557 string_ncopy(opt_cdup, name, namelen);
5558 }
5559
5560 return OK;
5561 }
5562
5563 /* XXX: The line outputted by "--show-cdup" can be empty so the option
5564 * must be the last one! */
5565 static int
5566 load_repo_info(void)
5567 {
5568 return read_properties(popen("git rev-parse --git-dir --is-inside-work-tree --show-cdup 2>/dev/null", "r"),
5569 "=", read_repo_info);
5570 }
5571
5572 static int
5573 read_properties(FILE *pipe, const char *separators,
5574 int (*read_property)(char *, size_t, char *, size_t))
5575 {
5576 char buffer[BUFSIZ];
5577 char *name;
5578 int state = OK;
5579
5580 if (!pipe)
5581 return ERR;
5582
5583 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
5584 char *value;
5585 size_t namelen;
5586 size_t valuelen;
5587
5588 name = chomp_string(name);
5589 namelen = strcspn(name, separators);
5590
5591 if (name[namelen]) {
5592 name[namelen] = 0;
5593 value = chomp_string(name + namelen + 1);
5594 valuelen = strlen(value);
5595
5596 } else {
5597 value = "";
5598 valuelen = 0;
5599 }
5600
5601 state = read_property(name, namelen, value, valuelen);
5602 }
5603
5604 if (state != ERR && ferror(pipe))
5605 state = ERR;
5606
5607 pclose(pipe);
5608
5609 return state;
5610 }
5611
5612
5613 /*
5614 * Main
5615 */
5616
5617 static void __NORETURN
5618 quit(int sig)
5619 {
5620 /* XXX: Restore tty modes and let the OS cleanup the rest! */
5621 if (cursed)
5622 endwin();
5623 exit(0);
5624 }
5625
5626 static void __NORETURN
5627 die(const char *err, ...)
5628 {
5629 va_list args;
5630
5631 endwin();
5632
5633 va_start(args, err);
5634 fputs("tig: ", stderr);
5635 vfprintf(stderr, err, args);
5636 fputs("\n", stderr);
5637 va_end(args);
5638
5639 exit(1);
5640 }
5641
5642 static void
5643 warn(const char *msg, ...)
5644 {
5645 va_list args;
5646
5647 va_start(args, msg);
5648 fputs("tig warning: ", stderr);
5649 vfprintf(stderr, msg, args);
5650 fputs("\n", stderr);
5651 va_end(args);
5652 }
5653
5654 int
5655 main(int argc, char *argv[])
5656 {
5657 struct view *view;
5658 enum request request;
5659 size_t i;
5660
5661 signal(SIGINT, quit);
5662
5663 if (setlocale(LC_ALL, "")) {
5664 char *codeset = nl_langinfo(CODESET);
5665
5666 string_ncopy(opt_codeset, codeset, strlen(codeset));
5667 }
5668
5669 if (load_repo_info() == ERR)
5670 die("Failed to load repo info.");
5671
5672 if (load_options() == ERR)
5673 die("Failed to load user config.");
5674
5675 /* Load the repo config file so options can be overwritten from
5676 * the command line. */
5677 if (load_repo_config() == ERR)
5678 die("Failed to load repo config.");
5679
5680 if (!parse_options(argc, argv))
5681 return 0;
5682
5683 /* Require a git repository unless when running in pager mode. */
5684 if (!opt_git_dir[0])
5685 die("Not a git repository");
5686
5687 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
5688 opt_utf8 = FALSE;
5689
5690 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
5691 opt_iconv = iconv_open(opt_codeset, opt_encoding);
5692 if (opt_iconv == ICONV_NONE)
5693 die("Failed to initialize character set conversion");
5694 }
5695
5696 if (load_refs() == ERR)
5697 die("Failed to load refs.");
5698
5699 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
5700 view->cmd_env = getenv(view->cmd_env);
5701
5702 request = opt_request;
5703
5704 init_display();
5705
5706 while (view_driver(display[current_view], request)) {
5707 int key;
5708 int i;
5709
5710 foreach_view (view, i)
5711 update_view(view);
5712
5713 /* Refresh, accept single keystroke of input */
5714 key = wgetch(status_win);
5715
5716 /* wgetch() with nodelay() enabled returns ERR when there's no
5717 * input. */
5718 if (key == ERR) {
5719 request = REQ_NONE;
5720 continue;
5721 }
5722
5723 request = get_keybinding(display[current_view]->keymap, key);
5724
5725 /* Some low-level request handling. This keeps access to
5726 * status_win restricted. */
5727 switch (request) {
5728 case REQ_PROMPT:
5729 {
5730 char *cmd = read_prompt(":");
5731
5732 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
5733 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
5734 opt_request = REQ_VIEW_DIFF;
5735 } else {
5736 opt_request = REQ_VIEW_PAGER;
5737 }
5738 break;
5739 }
5740
5741 request = REQ_NONE;
5742 break;
5743 }
5744 case REQ_SEARCH:
5745 case REQ_SEARCH_BACK:
5746 {
5747 const char *prompt = request == REQ_SEARCH
5748 ? "/" : "?";
5749 char *search = read_prompt(prompt);
5750
5751 if (search)
5752 string_ncopy(opt_search, search, strlen(search));
5753 else
5754 request = REQ_NONE;
5755 break;
5756 }
5757 case REQ_SCREEN_RESIZE:
5758 {
5759 int height, width;
5760
5761 getmaxyx(stdscr, height, width);
5762
5763 /* Resize the status view and let the view driver take
5764 * care of resizing the displayed views. */
5765 wresize(status_win, 1, width);
5766 mvwin(status_win, height - 1, 0);
5767 wrefresh(status_win);
5768 break;
5769 }
5770 default:
5771 break;
5772 }
5773 }
5774
5775 quit(0);
5776
5777 return 0;
5778 }