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