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