Add support for refreshing of the stage view
[tig] / tig.c
1 /* Copyright (c) 2006-2008 Jonas Fonseca <fonseca@diku.dk>
2 *
3 * This program is free software; you can redistribute it and/or
4 * modify it under the terms of the GNU General Public License as
5 * published by the Free Software Foundation; either version 2 of
6 * the License, or (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 */
13
14 #ifdef HAVE_CONFIG_H
15 #include "config.h"
16 #endif
17
18 #ifndef TIG_VERSION
19 #define TIG_VERSION "unknown-version"
20 #endif
21
22 #ifndef DEBUG
23 #define NDEBUG
24 #endif
25
26 #include <assert.h>
27 #include <errno.h>
28 #include <ctype.h>
29 #include <signal.h>
30 #include <stdarg.h>
31 #include <stdio.h>
32 #include <stdlib.h>
33 #include <string.h>
34 #include <sys/types.h>
35 #include <sys/stat.h>
36 #include <unistd.h>
37 #include <time.h>
38
39 #include <regex.h>
40
41 #include <locale.h>
42 #include <langinfo.h>
43 #include <iconv.h>
44
45 /* ncurses(3): Must be defined to have extended wide-character functions. */
46 #define _XOPEN_SOURCE_EXTENDED
47
48 #include <curses.h>
49
50 #if __GNUC__ >= 3
51 #define __NORETURN __attribute__((__noreturn__))
52 #else
53 #define __NORETURN
54 #endif
55
56 static void __NORETURN die(const char *err, ...);
57 static void warn(const char *msg, ...);
58 static void report(const char *msg, ...);
59 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, size_t, char *, size_t));
60 static void set_nonblocking_input(bool loading);
61 static size_t utf8_length(const char *string, size_t max_width, int *trimmed, bool reserve);
62
63 #define ABS(x) ((x) >= 0 ? (x) : -(x))
64 #define MIN(x, y) ((x) < (y) ? (x) : (y))
65
66 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
67 #define STRING_SIZE(x) (sizeof(x) - 1)
68
69 #define SIZEOF_STR 1024 /* Default string size. */
70 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
71 #define SIZEOF_REV 41 /* Holds a SHA-1 and an ending NUL */
72
73 /* Revision graph */
74
75 #define REVGRAPH_INIT 'I'
76 #define REVGRAPH_MERGE 'M'
77 #define REVGRAPH_BRANCH '+'
78 #define REVGRAPH_COMMIT '*'
79 #define REVGRAPH_BOUND '^'
80 #define REVGRAPH_LINE '|'
81
82 #define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
83
84 /* This color name can be used to refer to the default term colors. */
85 #define COLOR_DEFAULT (-1)
86
87 #define ICONV_NONE ((iconv_t) -1)
88 #ifndef ICONV_CONST
89 #define ICONV_CONST /* nothing */
90 #endif
91
92 /* The format and size of the date column in the main view. */
93 #define DATE_FORMAT "%Y-%m-%d %H:%M"
94 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
95
96 #define AUTHOR_COLS 20
97 #define ID_COLS 8
98
99 /* The default interval between line numbers. */
100 #define NUMBER_INTERVAL 5
101
102 #define TABSIZE 8
103
104 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
105
106 #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 OPEN_NOMAXIMIZE = 8, /* Do not maximize the current view. */
2311 };
2312
2313 static void
2314 open_view(struct view *prev, enum request request, enum open_flags flags)
2315 {
2316 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2317 bool split = !!(flags & OPEN_SPLIT);
2318 bool reload = !!(flags & OPEN_RELOAD);
2319 bool nomaximize = !!(flags & OPEN_NOMAXIMIZE);
2320 struct view *view = VIEW(request);
2321 int nviews = displayed_views();
2322 struct view *base_view = display[0];
2323
2324 if (view == prev && nviews == 1 && !reload) {
2325 report("Already in %s view", view->name);
2326 return;
2327 }
2328
2329 if (view->git_dir && !opt_git_dir[0]) {
2330 report("The %s view is disabled in pager view", view->name);
2331 return;
2332 }
2333
2334 if (split) {
2335 display[1] = view;
2336 if (!backgrounded)
2337 current_view = 1;
2338 } else if (!nomaximize) {
2339 /* Maximize the current view. */
2340 memset(display, 0, sizeof(display));
2341 current_view = 0;
2342 display[current_view] = view;
2343 }
2344
2345 /* Resize the view when switching between split- and full-screen,
2346 * or when switching between two different full-screen views. */
2347 if (nviews != displayed_views() ||
2348 (nviews == 1 && base_view != display[0]))
2349 resize_display();
2350
2351 if (view->ops->open) {
2352 if (!view->ops->open(view)) {
2353 report("Failed to load %s view", view->name);
2354 return;
2355 }
2356
2357 } else if ((reload || strcmp(view->vid, view->id)) &&
2358 !begin_update(view)) {
2359 report("Failed to load %s view", view->name);
2360 return;
2361 }
2362
2363 if (split && prev->lineno - prev->offset >= prev->height) {
2364 /* Take the title line into account. */
2365 int lines = prev->lineno - prev->offset - prev->height + 1;
2366
2367 /* Scroll the view that was split if the current line is
2368 * outside the new limited view. */
2369 do_scroll_view(prev, lines);
2370 }
2371
2372 if (prev && view != prev) {
2373 if (split && !backgrounded) {
2374 /* "Blur" the previous view. */
2375 update_view_title(prev);
2376 }
2377
2378 view->parent = prev;
2379 }
2380
2381 if (view->pipe && view->lines == 0) {
2382 /* Clear the old view and let the incremental updating refill
2383 * the screen. */
2384 werase(view->win);
2385 report("");
2386 } else {
2387 redraw_view(view);
2388 report("");
2389 }
2390
2391 /* If the view is backgrounded the above calls to report()
2392 * won't redraw the view title. */
2393 if (backgrounded)
2394 update_view_title(view);
2395 }
2396
2397 static void
2398 open_external_viewer(const char *cmd)
2399 {
2400 def_prog_mode(); /* save current tty modes */
2401 endwin(); /* restore original tty modes */
2402 system(cmd);
2403 fprintf(stderr, "Press Enter to continue");
2404 getc(stdin);
2405 reset_prog_mode();
2406 redraw_display();
2407 }
2408
2409 static void
2410 open_mergetool(const char *file)
2411 {
2412 char cmd[SIZEOF_STR];
2413 char file_sq[SIZEOF_STR];
2414
2415 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2416 string_format(cmd, "git mergetool %s", file_sq)) {
2417 open_external_viewer(cmd);
2418 }
2419 }
2420
2421 static void
2422 open_editor(bool from_root, const char *file)
2423 {
2424 char cmd[SIZEOF_STR];
2425 char file_sq[SIZEOF_STR];
2426 char *editor;
2427 char *prefix = from_root ? opt_cdup : "";
2428
2429 editor = getenv("GIT_EDITOR");
2430 if (!editor && *opt_editor)
2431 editor = opt_editor;
2432 if (!editor)
2433 editor = getenv("VISUAL");
2434 if (!editor)
2435 editor = getenv("EDITOR");
2436 if (!editor)
2437 editor = "vi";
2438
2439 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2440 string_format(cmd, "%s %s%s", editor, prefix, file_sq)) {
2441 open_external_viewer(cmd);
2442 }
2443 }
2444
2445 static void
2446 open_run_request(enum request request)
2447 {
2448 struct run_request *req = get_run_request(request);
2449 char buf[SIZEOF_STR * 2];
2450 size_t bufpos;
2451 char *cmd;
2452
2453 if (!req) {
2454 report("Unknown run request");
2455 return;
2456 }
2457
2458 bufpos = 0;
2459 cmd = req->cmd;
2460
2461 while (cmd) {
2462 char *next = strstr(cmd, "%(");
2463 int len = next - cmd;
2464 char *value;
2465
2466 if (!next) {
2467 len = strlen(cmd);
2468 value = "";
2469
2470 } else if (!strncmp(next, "%(head)", 7)) {
2471 value = ref_head;
2472
2473 } else if (!strncmp(next, "%(commit)", 9)) {
2474 value = ref_commit;
2475
2476 } else if (!strncmp(next, "%(blob)", 7)) {
2477 value = ref_blob;
2478
2479 } else {
2480 report("Unknown replacement in run request: `%s`", req->cmd);
2481 return;
2482 }
2483
2484 if (!string_format_from(buf, &bufpos, "%.*s%s", len, cmd, value))
2485 return;
2486
2487 if (next)
2488 next = strchr(next, ')') + 1;
2489 cmd = next;
2490 }
2491
2492 open_external_viewer(buf);
2493 }
2494
2495 /*
2496 * User request switch noodle
2497 */
2498
2499 static int
2500 view_driver(struct view *view, enum request request)
2501 {
2502 int i;
2503
2504 if (request == REQ_NONE) {
2505 doupdate();
2506 return TRUE;
2507 }
2508
2509 if (request > REQ_NONE) {
2510 open_run_request(request);
2511 return TRUE;
2512 }
2513
2514 if (view && view->lines) {
2515 request = view->ops->request(view, request, &view->line[view->lineno]);
2516 if (request == REQ_NONE)
2517 return TRUE;
2518 }
2519
2520 switch (request) {
2521 case REQ_MOVE_UP:
2522 case REQ_MOVE_DOWN:
2523 case REQ_MOVE_PAGE_UP:
2524 case REQ_MOVE_PAGE_DOWN:
2525 case REQ_MOVE_FIRST_LINE:
2526 case REQ_MOVE_LAST_LINE:
2527 move_view(view, request);
2528 break;
2529
2530 case REQ_SCROLL_LINE_DOWN:
2531 case REQ_SCROLL_LINE_UP:
2532 case REQ_SCROLL_PAGE_DOWN:
2533 case REQ_SCROLL_PAGE_UP:
2534 scroll_view(view, request);
2535 break;
2536
2537 case REQ_VIEW_BLAME:
2538 if (!opt_file[0]) {
2539 report("No file chosen, press %s to open tree view",
2540 get_key(REQ_VIEW_TREE));
2541 break;
2542 }
2543 open_view(view, request, OPEN_DEFAULT);
2544 break;
2545
2546 case REQ_VIEW_BLOB:
2547 if (!ref_blob[0]) {
2548 report("No file chosen, press %s to open tree view",
2549 get_key(REQ_VIEW_TREE));
2550 break;
2551 }
2552 open_view(view, request, OPEN_DEFAULT);
2553 break;
2554
2555 case REQ_VIEW_PAGER:
2556 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2557 report("No pager content, press %s to run command from prompt",
2558 get_key(REQ_PROMPT));
2559 break;
2560 }
2561 open_view(view, request, OPEN_DEFAULT);
2562 break;
2563
2564 case REQ_VIEW_STAGE:
2565 if (!VIEW(REQ_VIEW_STAGE)->lines) {
2566 report("No stage content, press %s to open the status view and choose file",
2567 get_key(REQ_VIEW_STATUS));
2568 break;
2569 }
2570 open_view(view, request, OPEN_DEFAULT);
2571 break;
2572
2573 case REQ_VIEW_STATUS:
2574 if (opt_is_inside_work_tree == FALSE) {
2575 report("The status view requires a working tree");
2576 break;
2577 }
2578 open_view(view, request, OPEN_DEFAULT);
2579 break;
2580
2581 case REQ_VIEW_MAIN:
2582 case REQ_VIEW_DIFF:
2583 case REQ_VIEW_LOG:
2584 case REQ_VIEW_TREE:
2585 case REQ_VIEW_HELP:
2586 open_view(view, request, OPEN_DEFAULT);
2587 break;
2588
2589 case REQ_NEXT:
2590 case REQ_PREVIOUS:
2591 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2592
2593 if ((view == VIEW(REQ_VIEW_DIFF) &&
2594 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2595 (view == VIEW(REQ_VIEW_DIFF) &&
2596 view->parent == VIEW(REQ_VIEW_BLAME)) ||
2597 (view == VIEW(REQ_VIEW_STAGE) &&
2598 view->parent == VIEW(REQ_VIEW_STATUS)) ||
2599 (view == VIEW(REQ_VIEW_BLOB) &&
2600 view->parent == VIEW(REQ_VIEW_TREE))) {
2601 int line;
2602
2603 view = view->parent;
2604 line = view->lineno;
2605 move_view(view, request);
2606 if (view_is_displayed(view))
2607 update_view_title(view);
2608 if (line != view->lineno)
2609 view->ops->request(view, REQ_ENTER,
2610 &view->line[view->lineno]);
2611
2612 } else {
2613 move_view(view, request);
2614 }
2615 break;
2616
2617 case REQ_VIEW_NEXT:
2618 {
2619 int nviews = displayed_views();
2620 int next_view = (current_view + 1) % nviews;
2621
2622 if (next_view == current_view) {
2623 report("Only one view is displayed");
2624 break;
2625 }
2626
2627 current_view = next_view;
2628 /* Blur out the title of the previous view. */
2629 update_view_title(view);
2630 report("");
2631 break;
2632 }
2633 case REQ_REFRESH:
2634 report("Refreshing is not yet supported for the %s view", view->name);
2635 break;
2636
2637 case REQ_MAXIMIZE:
2638 if (displayed_views() == 2)
2639 open_view(view, VIEW_REQ(view), OPEN_DEFAULT);
2640 break;
2641
2642 case REQ_TOGGLE_LINENO:
2643 opt_line_number = !opt_line_number;
2644 redraw_view(view);
2645 break;
2646
2647 case REQ_TOGGLE_DATE:
2648 opt_date = !opt_date;
2649 redraw_view(view);
2650 break;
2651
2652 case REQ_TOGGLE_AUTHOR:
2653 opt_author = !opt_author;
2654 redraw_view(view);
2655 break;
2656
2657 case REQ_TOGGLE_REV_GRAPH:
2658 opt_rev_graph = !opt_rev_graph;
2659 redraw_view(view);
2660 break;
2661
2662 case REQ_TOGGLE_REFS:
2663 opt_show_refs = !opt_show_refs;
2664 redraw_view(view);
2665 break;
2666
2667 case REQ_PROMPT:
2668 /* Always reload^Wrerun commands from the prompt. */
2669 open_view(view, opt_request, OPEN_RELOAD);
2670 break;
2671
2672 case REQ_SEARCH:
2673 case REQ_SEARCH_BACK:
2674 search_view(view, request);
2675 break;
2676
2677 case REQ_FIND_NEXT:
2678 case REQ_FIND_PREV:
2679 find_next(view, request);
2680 break;
2681
2682 case REQ_STOP_LOADING:
2683 for (i = 0; i < ARRAY_SIZE(views); i++) {
2684 view = &views[i];
2685 if (view->pipe)
2686 report("Stopped loading the %s view", view->name),
2687 end_update(view);
2688 }
2689 break;
2690
2691 case REQ_SHOW_VERSION:
2692 report("tig-%s (built %s)", TIG_VERSION, __DATE__);
2693 return TRUE;
2694
2695 case REQ_SCREEN_RESIZE:
2696 resize_display();
2697 /* Fall-through */
2698 case REQ_SCREEN_REDRAW:
2699 redraw_display();
2700 break;
2701
2702 case REQ_EDIT:
2703 report("Nothing to edit");
2704 break;
2705
2706
2707 case REQ_ENTER:
2708 report("Nothing to enter");
2709 break;
2710
2711
2712 case REQ_VIEW_CLOSE:
2713 /* XXX: Mark closed views by letting view->parent point to the
2714 * view itself. Parents to closed view should never be
2715 * followed. */
2716 if (view->parent &&
2717 view->parent->parent != view->parent) {
2718 memset(display, 0, sizeof(display));
2719 current_view = 0;
2720 display[current_view] = view->parent;
2721 view->parent = view;
2722 resize_display();
2723 redraw_display();
2724 break;
2725 }
2726 /* Fall-through */
2727 case REQ_QUIT:
2728 return FALSE;
2729
2730 default:
2731 /* An unknown key will show most commonly used commands. */
2732 report("Unknown key, press 'h' for help");
2733 return TRUE;
2734 }
2735
2736 return TRUE;
2737 }
2738
2739
2740 /*
2741 * Pager backend
2742 */
2743
2744 static bool
2745 pager_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2746 {
2747 static char spaces[] = " ";
2748 char *text = line->data;
2749 enum line_type type = line->type;
2750 int attr = A_NORMAL;
2751 int col = 0;
2752
2753 wmove(view->win, lineno, 0);
2754
2755 if (selected) {
2756 type = LINE_CURSOR;
2757 wchgat(view->win, -1, 0, type, NULL);
2758 attr = get_line_attr(type);
2759 }
2760 wattrset(view->win, attr);
2761
2762 if (opt_line_number) {
2763 col += draw_lineno(view, lineno, view->width, selected);
2764 if (col >= view->width)
2765 return TRUE;
2766 }
2767
2768 if (!selected) {
2769 attr = get_line_attr(type);
2770 wattrset(view->win, attr);
2771 }
2772 if (opt_tab_size < TABSIZE) {
2773 int col_offset = col;
2774
2775 col = 0;
2776 while (text && col_offset + col < view->width) {
2777 int cols_max = view->width - col_offset - col;
2778 char *pos = text;
2779 int cols;
2780
2781 if (*text == '\t') {
2782 text++;
2783 assert(sizeof(spaces) > TABSIZE);
2784 pos = spaces;
2785 cols = opt_tab_size - (col % opt_tab_size);
2786
2787 } else {
2788 text = strchr(text, '\t');
2789 cols = line ? text - pos : strlen(pos);
2790 }
2791
2792 waddnstr(view->win, pos, MIN(cols, cols_max));
2793 col += cols;
2794 }
2795
2796 } else {
2797 draw_text(view, text, view->width - col, TRUE, selected);
2798 }
2799
2800 return TRUE;
2801 }
2802
2803 static bool
2804 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2805 {
2806 char refbuf[SIZEOF_STR];
2807 char *ref = NULL;
2808 FILE *pipe;
2809
2810 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2811 return TRUE;
2812
2813 pipe = popen(refbuf, "r");
2814 if (!pipe)
2815 return TRUE;
2816
2817 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2818 ref = chomp_string(ref);
2819 pclose(pipe);
2820
2821 if (!ref || !*ref)
2822 return TRUE;
2823
2824 /* This is the only fatal call, since it can "corrupt" the buffer. */
2825 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2826 return FALSE;
2827
2828 return TRUE;
2829 }
2830
2831 static void
2832 add_pager_refs(struct view *view, struct line *line)
2833 {
2834 char buf[SIZEOF_STR];
2835 char *commit_id = (char *)line->data + STRING_SIZE("commit ");
2836 struct ref **refs;
2837 size_t bufpos = 0, refpos = 0;
2838 const char *sep = "Refs: ";
2839 bool is_tag = FALSE;
2840
2841 assert(line->type == LINE_COMMIT);
2842
2843 refs = get_refs(commit_id);
2844 if (!refs) {
2845 if (view == VIEW(REQ_VIEW_DIFF))
2846 goto try_add_describe_ref;
2847 return;
2848 }
2849
2850 do {
2851 struct ref *ref = refs[refpos];
2852 char *fmt = ref->tag ? "%s[%s]" :
2853 ref->remote ? "%s<%s>" : "%s%s";
2854
2855 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2856 return;
2857 sep = ", ";
2858 if (ref->tag)
2859 is_tag = TRUE;
2860 } while (refs[refpos++]->next);
2861
2862 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2863 try_add_describe_ref:
2864 /* Add <tag>-g<commit_id> "fake" reference. */
2865 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2866 return;
2867 }
2868
2869 if (bufpos == 0)
2870 return;
2871
2872 if (!realloc_lines(view, view->line_size + 1))
2873 return;
2874
2875 add_line_text(view, buf, LINE_PP_REFS);
2876 }
2877
2878 static bool
2879 pager_read(struct view *view, char *data)
2880 {
2881 struct line *line;
2882
2883 if (!data)
2884 return TRUE;
2885
2886 line = add_line_text(view, data, get_line_type(data));
2887 if (!line)
2888 return FALSE;
2889
2890 if (line->type == LINE_COMMIT &&
2891 (view == VIEW(REQ_VIEW_DIFF) ||
2892 view == VIEW(REQ_VIEW_LOG)))
2893 add_pager_refs(view, line);
2894
2895 return TRUE;
2896 }
2897
2898 static enum request
2899 pager_request(struct view *view, enum request request, struct line *line)
2900 {
2901 int split = 0;
2902
2903 if (request != REQ_ENTER)
2904 return request;
2905
2906 if (line->type == LINE_COMMIT &&
2907 (view == VIEW(REQ_VIEW_LOG) ||
2908 view == VIEW(REQ_VIEW_PAGER))) {
2909 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2910 split = 1;
2911 }
2912
2913 /* Always scroll the view even if it was split. That way
2914 * you can use Enter to scroll through the log view and
2915 * split open each commit diff. */
2916 scroll_view(view, REQ_SCROLL_LINE_DOWN);
2917
2918 /* FIXME: A minor workaround. Scrolling the view will call report("")
2919 * but if we are scrolling a non-current view this won't properly
2920 * update the view title. */
2921 if (split)
2922 update_view_title(view);
2923
2924 return REQ_NONE;
2925 }
2926
2927 static bool
2928 pager_grep(struct view *view, struct line *line)
2929 {
2930 regmatch_t pmatch;
2931 char *text = line->data;
2932
2933 if (!*text)
2934 return FALSE;
2935
2936 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
2937 return FALSE;
2938
2939 return TRUE;
2940 }
2941
2942 static void
2943 pager_select(struct view *view, struct line *line)
2944 {
2945 if (line->type == LINE_COMMIT) {
2946 char *text = (char *)line->data + STRING_SIZE("commit ");
2947
2948 if (view != VIEW(REQ_VIEW_PAGER))
2949 string_copy_rev(view->ref, text);
2950 string_copy_rev(ref_commit, text);
2951 }
2952 }
2953
2954 static struct view_ops pager_ops = {
2955 "line",
2956 NULL,
2957 pager_read,
2958 pager_draw,
2959 pager_request,
2960 pager_grep,
2961 pager_select,
2962 };
2963
2964
2965 /*
2966 * Help backend
2967 */
2968
2969 static bool
2970 help_open(struct view *view)
2971 {
2972 char buf[BUFSIZ];
2973 int lines = ARRAY_SIZE(req_info) + 2;
2974 int i;
2975
2976 if (view->lines > 0)
2977 return TRUE;
2978
2979 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2980 if (!req_info[i].request)
2981 lines++;
2982
2983 lines += run_requests + 1;
2984
2985 view->line = calloc(lines, sizeof(*view->line));
2986 if (!view->line)
2987 return FALSE;
2988
2989 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
2990
2991 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2992 char *key;
2993
2994 if (req_info[i].request == REQ_NONE)
2995 continue;
2996
2997 if (!req_info[i].request) {
2998 add_line_text(view, "", LINE_DEFAULT);
2999 add_line_text(view, req_info[i].help, LINE_DEFAULT);
3000 continue;
3001 }
3002
3003 key = get_key(req_info[i].request);
3004 if (!*key)
3005 key = "(no key defined)";
3006
3007 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
3008 continue;
3009
3010 add_line_text(view, buf, LINE_DEFAULT);
3011 }
3012
3013 if (run_requests) {
3014 add_line_text(view, "", LINE_DEFAULT);
3015 add_line_text(view, "External commands:", LINE_DEFAULT);
3016 }
3017
3018 for (i = 0; i < run_requests; i++) {
3019 struct run_request *req = get_run_request(REQ_NONE + i + 1);
3020 char *key;
3021
3022 if (!req)
3023 continue;
3024
3025 key = get_key_name(req->key);
3026 if (!*key)
3027 key = "(no key defined)";
3028
3029 if (!string_format(buf, " %-10s %-14s `%s`",
3030 keymap_table[req->keymap].name,
3031 key, req->cmd))
3032 continue;
3033
3034 add_line_text(view, buf, LINE_DEFAULT);
3035 }
3036
3037 return TRUE;
3038 }
3039
3040 static struct view_ops help_ops = {
3041 "line",
3042 help_open,
3043 NULL,
3044 pager_draw,
3045 pager_request,
3046 pager_grep,
3047 pager_select,
3048 };
3049
3050
3051 /*
3052 * Tree backend
3053 */
3054
3055 struct tree_stack_entry {
3056 struct tree_stack_entry *prev; /* Entry below this in the stack */
3057 unsigned long lineno; /* Line number to restore */
3058 char *name; /* Position of name in opt_path */
3059 };
3060
3061 /* The top of the path stack. */
3062 static struct tree_stack_entry *tree_stack = NULL;
3063 unsigned long tree_lineno = 0;
3064
3065 static void
3066 pop_tree_stack_entry(void)
3067 {
3068 struct tree_stack_entry *entry = tree_stack;
3069
3070 tree_lineno = entry->lineno;
3071 entry->name[0] = 0;
3072 tree_stack = entry->prev;
3073 free(entry);
3074 }
3075
3076 static void
3077 push_tree_stack_entry(char *name, unsigned long lineno)
3078 {
3079 struct tree_stack_entry *entry = calloc(1, sizeof(*entry));
3080 size_t pathlen = strlen(opt_path);
3081
3082 if (!entry)
3083 return;
3084
3085 entry->prev = tree_stack;
3086 entry->name = opt_path + pathlen;
3087 tree_stack = entry;
3088
3089 if (!string_format_from(opt_path, &pathlen, "%s/", name)) {
3090 pop_tree_stack_entry();
3091 return;
3092 }
3093
3094 /* Move the current line to the first tree entry. */
3095 tree_lineno = 1;
3096 entry->lineno = lineno;
3097 }
3098
3099 /* Parse output from git-ls-tree(1):
3100 *
3101 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
3102 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
3103 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
3104 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
3105 */
3106
3107 #define SIZEOF_TREE_ATTR \
3108 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
3109
3110 #define TREE_UP_FORMAT "040000 tree %s\t.."
3111
3112 static int
3113 tree_compare_entry(enum line_type type1, char *name1,
3114 enum line_type type2, char *name2)
3115 {
3116 if (type1 != type2) {
3117 if (type1 == LINE_TREE_DIR)
3118 return -1;
3119 return 1;
3120 }
3121
3122 return strcmp(name1, name2);
3123 }
3124
3125 static char *
3126 tree_path(struct line *line)
3127 {
3128 char *path = line->data;
3129
3130 return path + SIZEOF_TREE_ATTR;
3131 }
3132
3133 static bool
3134 tree_read(struct view *view, char *text)
3135 {
3136 size_t textlen = text ? strlen(text) : 0;
3137 char buf[SIZEOF_STR];
3138 unsigned long pos;
3139 enum line_type type;
3140 bool first_read = view->lines == 0;
3141
3142 if (!text)
3143 return TRUE;
3144 if (textlen <= SIZEOF_TREE_ATTR)
3145 return FALSE;
3146
3147 type = text[STRING_SIZE("100644 ")] == 't'
3148 ? LINE_TREE_DIR : LINE_TREE_FILE;
3149
3150 if (first_read) {
3151 /* Add path info line */
3152 if (!string_format(buf, "Directory path /%s", opt_path) ||
3153 !realloc_lines(view, view->line_size + 1) ||
3154 !add_line_text(view, buf, LINE_DEFAULT))
3155 return FALSE;
3156
3157 /* Insert "link" to parent directory. */
3158 if (*opt_path) {
3159 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
3160 !realloc_lines(view, view->line_size + 1) ||
3161 !add_line_text(view, buf, LINE_TREE_DIR))
3162 return FALSE;
3163 }
3164 }
3165
3166 /* Strip the path part ... */
3167 if (*opt_path) {
3168 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
3169 size_t striplen = strlen(opt_path);
3170 char *path = text + SIZEOF_TREE_ATTR;
3171
3172 if (pathlen > striplen)
3173 memmove(path, path + striplen,
3174 pathlen - striplen + 1);
3175 }
3176
3177 /* Skip "Directory ..." and ".." line. */
3178 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
3179 struct line *line = &view->line[pos];
3180 char *path1 = tree_path(line);
3181 char *path2 = text + SIZEOF_TREE_ATTR;
3182 int cmp = tree_compare_entry(line->type, path1, type, path2);
3183
3184 if (cmp <= 0)
3185 continue;
3186
3187 text = strdup(text);
3188 if (!text)
3189 return FALSE;
3190
3191 if (view->lines > pos)
3192 memmove(&view->line[pos + 1], &view->line[pos],
3193 (view->lines - pos) * sizeof(*line));
3194
3195 line = &view->line[pos];
3196 line->data = text;
3197 line->type = type;
3198 view->lines++;
3199 return TRUE;
3200 }
3201
3202 if (!add_line_text(view, text, type))
3203 return FALSE;
3204
3205 if (tree_lineno > view->lineno) {
3206 view->lineno = tree_lineno;
3207 tree_lineno = 0;
3208 }
3209
3210 return TRUE;
3211 }
3212
3213 static enum request
3214 tree_request(struct view *view, enum request request, struct line *line)
3215 {
3216 enum open_flags flags;
3217
3218 if (request == REQ_VIEW_BLAME) {
3219 char *filename = tree_path(line);
3220
3221 if (line->type == LINE_TREE_DIR) {
3222 report("Cannot show blame for directory %s", opt_path);
3223 return REQ_NONE;
3224 }
3225
3226 string_copy(opt_ref, view->vid);
3227 string_format(opt_file, "%s%s", opt_path, filename);
3228 return request;
3229 }
3230 if (request == REQ_TREE_PARENT) {
3231 if (*opt_path) {
3232 /* fake 'cd ..' */
3233 request = REQ_ENTER;
3234 line = &view->line[1];
3235 } else {
3236 /* quit view if at top of tree */
3237 return REQ_VIEW_CLOSE;
3238 }
3239 }
3240 if (request != REQ_ENTER)
3241 return request;
3242
3243 /* Cleanup the stack if the tree view is at a different tree. */
3244 while (!*opt_path && tree_stack)
3245 pop_tree_stack_entry();
3246
3247 switch (line->type) {
3248 case LINE_TREE_DIR:
3249 /* Depending on whether it is a subdir or parent (updir?) link
3250 * mangle the path buffer. */
3251 if (line == &view->line[1] && *opt_path) {
3252 pop_tree_stack_entry();
3253
3254 } else {
3255 char *basename = tree_path(line);
3256
3257 push_tree_stack_entry(basename, view->lineno);
3258 }
3259
3260 /* Trees and subtrees share the same ID, so they are not not
3261 * unique like blobs. */
3262 flags = OPEN_RELOAD;
3263 request = REQ_VIEW_TREE;
3264 break;
3265
3266 case LINE_TREE_FILE:
3267 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3268 request = REQ_VIEW_BLOB;
3269 break;
3270
3271 default:
3272 return TRUE;
3273 }
3274
3275 open_view(view, request, flags);
3276 if (request == REQ_VIEW_TREE) {
3277 view->lineno = tree_lineno;
3278 }
3279
3280 return REQ_NONE;
3281 }
3282
3283 static void
3284 tree_select(struct view *view, struct line *line)
3285 {
3286 char *text = (char *)line->data + STRING_SIZE("100644 blob ");
3287
3288 if (line->type == LINE_TREE_FILE) {
3289 string_copy_rev(ref_blob, text);
3290
3291 } else if (line->type != LINE_TREE_DIR) {
3292 return;
3293 }
3294
3295 string_copy_rev(view->ref, text);
3296 }
3297
3298 static struct view_ops tree_ops = {
3299 "file",
3300 NULL,
3301 tree_read,
3302 pager_draw,
3303 tree_request,
3304 pager_grep,
3305 tree_select,
3306 };
3307
3308 static bool
3309 blob_read(struct view *view, char *line)
3310 {
3311 if (!line)
3312 return TRUE;
3313 return add_line_text(view, line, LINE_DEFAULT) != NULL;
3314 }
3315
3316 static struct view_ops blob_ops = {
3317 "line",
3318 NULL,
3319 blob_read,
3320 pager_draw,
3321 pager_request,
3322 pager_grep,
3323 pager_select,
3324 };
3325
3326 /*
3327 * Blame backend
3328 *
3329 * Loading the blame view is a two phase job:
3330 *
3331 * 1. File content is read either using opt_file from the
3332 * filesystem or using git-cat-file.
3333 * 2. Then blame information is incrementally added by
3334 * reading output from git-blame.
3335 */
3336
3337 struct blame_commit {
3338 char id[SIZEOF_REV]; /* SHA1 ID. */
3339 char title[128]; /* First line of the commit message. */
3340 char author[75]; /* Author of the commit. */
3341 struct tm time; /* Date from the author ident. */
3342 char filename[128]; /* Name of file. */
3343 };
3344
3345 struct blame {
3346 struct blame_commit *commit;
3347 unsigned int header:1;
3348 char text[1];
3349 };
3350
3351 #define BLAME_CAT_FILE_CMD "git cat-file blob %s:%s"
3352 #define BLAME_INCREMENTAL_CMD "git blame --incremental %s %s"
3353
3354 static bool
3355 blame_open(struct view *view)
3356 {
3357 char path[SIZEOF_STR];
3358 char ref[SIZEOF_STR] = "";
3359
3360 if (sq_quote(path, 0, opt_file) >= sizeof(path))
3361 return FALSE;
3362
3363 if (*opt_ref && sq_quote(ref, 0, opt_ref) >= sizeof(ref))
3364 return FALSE;
3365
3366 if (*opt_ref) {
3367 if (!string_format(view->cmd, BLAME_CAT_FILE_CMD, ref, path))
3368 return FALSE;
3369 } else {
3370 view->pipe = fopen(opt_file, "r");
3371 if (!view->pipe &&
3372 !string_format(view->cmd, BLAME_CAT_FILE_CMD, "HEAD", path))
3373 return FALSE;
3374 }
3375
3376 if (!view->pipe)
3377 view->pipe = popen(view->cmd, "r");
3378 if (!view->pipe)
3379 return FALSE;
3380
3381 if (!string_format(view->cmd, BLAME_INCREMENTAL_CMD, ref, path))
3382 return FALSE;
3383
3384 string_format(view->ref, "%s ...", opt_file);
3385 string_copy_rev(view->vid, opt_file);
3386 set_nonblocking_input(TRUE);
3387
3388 if (view->line) {
3389 int i;
3390
3391 for (i = 0; i < view->lines; i++)
3392 free(view->line[i].data);
3393 free(view->line);
3394 }
3395
3396 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3397 view->offset = view->lines = view->lineno = 0;
3398 view->line = NULL;
3399 view->start_time = time(NULL);
3400
3401 return TRUE;
3402 }
3403
3404 static struct blame_commit *
3405 get_blame_commit(struct view *view, const char *id)
3406 {
3407 size_t i;
3408
3409 for (i = 0; i < view->lines; i++) {
3410 struct blame *blame = view->line[i].data;
3411
3412 if (!blame->commit)
3413 continue;
3414
3415 if (!strncmp(blame->commit->id, id, SIZEOF_REV - 1))
3416 return blame->commit;
3417 }
3418
3419 {
3420 struct blame_commit *commit = calloc(1, sizeof(*commit));
3421
3422 if (commit)
3423 string_ncopy(commit->id, id, SIZEOF_REV);
3424 return commit;
3425 }
3426 }
3427
3428 static bool
3429 parse_number(char **posref, size_t *number, size_t min, size_t max)
3430 {
3431 char *pos = *posref;
3432
3433 *posref = NULL;
3434 pos = strchr(pos + 1, ' ');
3435 if (!pos || !isdigit(pos[1]))
3436 return FALSE;
3437 *number = atoi(pos + 1);
3438 if (*number < min || *number > max)
3439 return FALSE;
3440
3441 *posref = pos;
3442 return TRUE;
3443 }
3444
3445 static struct blame_commit *
3446 parse_blame_commit(struct view *view, char *text, int *blamed)
3447 {
3448 struct blame_commit *commit;
3449 struct blame *blame;
3450 char *pos = text + SIZEOF_REV - 1;
3451 size_t lineno;
3452 size_t group;
3453
3454 if (strlen(text) <= SIZEOF_REV || *pos != ' ')
3455 return NULL;
3456
3457 if (!parse_number(&pos, &lineno, 1, view->lines) ||
3458 !parse_number(&pos, &group, 1, view->lines - lineno + 1))
3459 return NULL;
3460
3461 commit = get_blame_commit(view, text);
3462 if (!commit)
3463 return NULL;
3464
3465 *blamed += group;
3466 while (group--) {
3467 struct line *line = &view->line[lineno + group - 1];
3468
3469 blame = line->data;
3470 blame->commit = commit;
3471 blame->header = !group;
3472 line->dirty = 1;
3473 }
3474
3475 return commit;
3476 }
3477
3478 static bool
3479 blame_read_file(struct view *view, char *line)
3480 {
3481 if (!line) {
3482 FILE *pipe = NULL;
3483
3484 if (view->lines > 0)
3485 pipe = popen(view->cmd, "r");
3486 view->cmd[0] = 0;
3487 if (!pipe) {
3488 report("Failed to load blame data");
3489 return TRUE;
3490 }
3491
3492 fclose(view->pipe);
3493 view->pipe = pipe;
3494 return FALSE;
3495
3496 } else {
3497 size_t linelen = strlen(line);
3498 struct blame *blame = malloc(sizeof(*blame) + linelen);
3499
3500 if (!line)
3501 return FALSE;
3502
3503 blame->commit = NULL;
3504 strncpy(blame->text, line, linelen);
3505 blame->text[linelen] = 0;
3506 return add_line_data(view, blame, LINE_BLAME_COMMIT) != NULL;
3507 }
3508 }
3509
3510 static bool
3511 match_blame_header(const char *name, char **line)
3512 {
3513 size_t namelen = strlen(name);
3514 bool matched = !strncmp(name, *line, namelen);
3515
3516 if (matched)
3517 *line += namelen;
3518
3519 return matched;
3520 }
3521
3522 static bool
3523 blame_read(struct view *view, char *line)
3524 {
3525 static struct blame_commit *commit = NULL;
3526 static int blamed = 0;
3527 static time_t author_time;
3528
3529 if (*view->cmd)
3530 return blame_read_file(view, line);
3531
3532 if (!line) {
3533 /* Reset all! */
3534 commit = NULL;
3535 blamed = 0;
3536 string_format(view->ref, "%s", view->vid);
3537 if (view_is_displayed(view)) {
3538 update_view_title(view);
3539 redraw_view_from(view, 0);
3540 }
3541 return TRUE;
3542 }
3543
3544 if (!commit) {
3545 commit = parse_blame_commit(view, line, &blamed);
3546 string_format(view->ref, "%s %2d%%", view->vid,
3547 blamed * 100 / view->lines);
3548
3549 } else if (match_blame_header("author ", &line)) {
3550 string_ncopy(commit->author, line, strlen(line));
3551
3552 } else if (match_blame_header("author-time ", &line)) {
3553 author_time = (time_t) atol(line);
3554
3555 } else if (match_blame_header("author-tz ", &line)) {
3556 long tz;
3557
3558 tz = ('0' - line[1]) * 60 * 60 * 10;
3559 tz += ('0' - line[2]) * 60 * 60;
3560 tz += ('0' - line[3]) * 60;
3561 tz += ('0' - line[4]) * 60;
3562
3563 if (line[0] == '-')
3564 tz = -tz;
3565
3566 author_time -= tz;
3567 gmtime_r(&author_time, &commit->time);
3568
3569 } else if (match_blame_header("summary ", &line)) {
3570 string_ncopy(commit->title, line, strlen(line));
3571
3572 } else if (match_blame_header("filename ", &line)) {
3573 string_ncopy(commit->filename, line, strlen(line));
3574 commit = NULL;
3575 }
3576
3577 return TRUE;
3578 }
3579
3580 static bool
3581 blame_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
3582 {
3583 struct blame *blame = line->data;
3584 int col = 0;
3585
3586 wmove(view->win, lineno, 0);
3587
3588 if (selected) {
3589 wattrset(view->win, get_line_attr(LINE_CURSOR));
3590 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
3591 } else {
3592 wattrset(view->win, A_NORMAL);
3593 }
3594
3595 if (opt_date) {
3596 int n;
3597
3598 if (!selected)
3599 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
3600 if (blame->commit) {
3601 char buf[DATE_COLS + 1];
3602 int timelen;
3603
3604 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &blame->commit->time);
3605 n = draw_text(view, buf, view->width - col, FALSE, selected);
3606 draw_text(view, " ", view->width - col - n, FALSE, selected);
3607 }
3608
3609 col += DATE_COLS;
3610 wmove(view->win, lineno, col);
3611 if (col >= view->width)
3612 return TRUE;
3613 }
3614
3615 if (opt_author) {
3616 int max = MIN(AUTHOR_COLS - 1, view->width - col);
3617
3618 if (!selected)
3619 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
3620 if (blame->commit)
3621 draw_text(view, blame->commit->author, max, TRUE, selected);
3622 col += AUTHOR_COLS;
3623 if (col >= view->width)
3624 return TRUE;
3625 wmove(view->win, lineno, col);
3626 }
3627
3628 {
3629 int max = MIN(ID_COLS - 1, view->width - col);
3630
3631 if (!selected)
3632 wattrset(view->win, get_line_attr(LINE_BLAME_ID));
3633 if (blame->commit)
3634 draw_text(view, blame->commit->id, max, FALSE, -1);
3635 col += ID_COLS;
3636 if (col >= view->width)
3637 return TRUE;
3638 wmove(view->win, lineno, col);
3639 }
3640
3641 {
3642 col += draw_lineno(view, lineno, view->width - col, selected);
3643 if (col >= view->width)
3644 return TRUE;
3645 }
3646
3647 col += draw_text(view, blame->text, view->width - col, TRUE, selected);
3648
3649 return TRUE;
3650 }
3651
3652 static enum request
3653 blame_request(struct view *view, enum request request, struct line *line)
3654 {
3655 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3656 struct blame *blame = line->data;
3657
3658 switch (request) {
3659 case REQ_ENTER:
3660 if (!blame->commit) {
3661 report("No commit loaded yet");
3662 break;
3663 }
3664
3665 if (!strcmp(blame->commit->id, NULL_ID)) {
3666 char path[SIZEOF_STR];
3667
3668 if (sq_quote(path, 0, view->vid) >= sizeof(path))
3669 break;
3670 string_format(opt_cmd, "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s 2>/dev/null", path);
3671 }
3672
3673 open_view(view, REQ_VIEW_DIFF, flags);
3674 break;
3675
3676 default:
3677 return request;
3678 }
3679
3680 return REQ_NONE;
3681 }
3682
3683 static bool
3684 blame_grep(struct view *view, struct line *line)
3685 {
3686 struct blame *blame = line->data;
3687 struct blame_commit *commit = blame->commit;
3688 regmatch_t pmatch;
3689
3690 #define MATCH(text) \
3691 (*text && regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3692
3693 if (commit) {
3694 char buf[DATE_COLS + 1];
3695
3696 if (MATCH(commit->title) ||
3697 MATCH(commit->author) ||
3698 MATCH(commit->id))
3699 return TRUE;
3700
3701 if (strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time) &&
3702 MATCH(buf))
3703 return TRUE;
3704 }
3705
3706 return MATCH(blame->text);
3707
3708 #undef MATCH
3709 }
3710
3711 static void
3712 blame_select(struct view *view, struct line *line)
3713 {
3714 struct blame *blame = line->data;
3715 struct blame_commit *commit = blame->commit;
3716
3717 if (!commit)
3718 return;
3719
3720 if (!strcmp(commit->id, NULL_ID))
3721 string_ncopy(ref_commit, "HEAD", 4);
3722 else
3723 string_copy_rev(ref_commit, commit->id);
3724 }
3725
3726 static struct view_ops blame_ops = {
3727 "line",
3728 blame_open,
3729 blame_read,
3730 blame_draw,
3731 blame_request,
3732 blame_grep,
3733 blame_select,
3734 };
3735
3736 /*
3737 * Status backend
3738 */
3739
3740 struct status {
3741 char status;
3742 struct {
3743 mode_t mode;
3744 char rev[SIZEOF_REV];
3745 char name[SIZEOF_STR];
3746 } old;
3747 struct {
3748 mode_t mode;
3749 char rev[SIZEOF_REV];
3750 char name[SIZEOF_STR];
3751 } new;
3752 };
3753
3754 static char status_onbranch[SIZEOF_STR];
3755 static struct status stage_status;
3756 static enum line_type stage_line_type;
3757
3758 /* Get fields from the diff line:
3759 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
3760 */
3761 static inline bool
3762 status_get_diff(struct status *file, char *buf, size_t bufsize)
3763 {
3764 char *old_mode = buf + 1;
3765 char *new_mode = buf + 8;
3766 char *old_rev = buf + 15;
3767 char *new_rev = buf + 56;
3768 char *status = buf + 97;
3769
3770 if (bufsize < 99 ||
3771 old_mode[-1] != ':' ||
3772 new_mode[-1] != ' ' ||
3773 old_rev[-1] != ' ' ||
3774 new_rev[-1] != ' ' ||
3775 status[-1] != ' ')
3776 return FALSE;
3777
3778 file->status = *status;
3779
3780 string_copy_rev(file->old.rev, old_rev);
3781 string_copy_rev(file->new.rev, new_rev);
3782
3783 file->old.mode = strtoul(old_mode, NULL, 8);
3784 file->new.mode = strtoul(new_mode, NULL, 8);
3785
3786 file->old.name[0] = file->new.name[0] = 0;
3787
3788 return TRUE;
3789 }
3790
3791 static bool
3792 status_run(struct view *view, const char cmd[], char status, enum line_type type)
3793 {
3794 struct status *file = NULL;
3795 struct status *unmerged = NULL;
3796 char buf[SIZEOF_STR * 4];
3797 size_t bufsize = 0;
3798 FILE *pipe;
3799
3800 pipe = popen(cmd, "r");
3801 if (!pipe)
3802 return FALSE;
3803
3804 add_line_data(view, NULL, type);
3805
3806 while (!feof(pipe) && !ferror(pipe)) {
3807 char *sep;
3808 size_t readsize;
3809
3810 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
3811 if (!readsize)
3812 break;
3813 bufsize += readsize;
3814
3815 /* Process while we have NUL chars. */
3816 while ((sep = memchr(buf, 0, bufsize))) {
3817 size_t sepsize = sep - buf + 1;
3818
3819 if (!file) {
3820 if (!realloc_lines(view, view->line_size + 1))
3821 goto error_out;
3822
3823 file = calloc(1, sizeof(*file));
3824 if (!file)
3825 goto error_out;
3826
3827 add_line_data(view, file, type);
3828 }
3829
3830 /* Parse diff info part. */
3831 if (status) {
3832 file->status = status;
3833 if (status == 'A')
3834 string_copy(file->old.rev, NULL_ID);
3835
3836 } else if (!file->status) {
3837 if (!status_get_diff(file, buf, sepsize))
3838 goto error_out;
3839
3840 bufsize -= sepsize;
3841 memmove(buf, sep + 1, bufsize);
3842
3843 sep = memchr(buf, 0, bufsize);
3844 if (!sep)
3845 break;
3846 sepsize = sep - buf + 1;
3847
3848 /* Collapse all 'M'odified entries that
3849 * follow a associated 'U'nmerged entry.
3850 */
3851 if (file->status == 'U') {
3852 unmerged = file;
3853
3854 } else if (unmerged) {
3855 int collapse = !strcmp(buf, unmerged->new.name);
3856
3857 unmerged = NULL;
3858 if (collapse) {
3859 free(file);
3860 view->lines--;
3861 continue;
3862 }
3863 }
3864 }
3865
3866 /* Grab the old name for rename/copy. */
3867 if (!*file->old.name &&
3868 (file->status == 'R' || file->status == 'C')) {
3869 sepsize = sep - buf + 1;
3870 string_ncopy(file->old.name, buf, sepsize);
3871 bufsize -= sepsize;
3872 memmove(buf, sep + 1, bufsize);
3873
3874 sep = memchr(buf, 0, bufsize);
3875 if (!sep)
3876 break;
3877 sepsize = sep - buf + 1;
3878 }
3879
3880 /* git-ls-files just delivers a NUL separated
3881 * list of file names similar to the second half
3882 * of the git-diff-* output. */
3883 string_ncopy(file->new.name, buf, sepsize);
3884 if (!*file->old.name)
3885 string_copy(file->old.name, file->new.name);
3886 bufsize -= sepsize;
3887 memmove(buf, sep + 1, bufsize);
3888 file = NULL;
3889 }
3890 }
3891
3892 if (ferror(pipe)) {
3893 error_out:
3894 pclose(pipe);
3895 return FALSE;
3896 }
3897
3898 if (!view->line[view->lines - 1].data)
3899 add_line_data(view, NULL, LINE_STAT_NONE);
3900
3901 pclose(pipe);
3902 return TRUE;
3903 }
3904
3905 /* Don't show unmerged entries in the staged section. */
3906 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached -M HEAD"
3907 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
3908 #define STATUS_LIST_OTHER_CMD \
3909 "git ls-files -z --others --exclude-per-directory=.gitignore"
3910 #define STATUS_LIST_NO_HEAD_CMD \
3911 "git ls-files -z --cached --exclude-per-directory=.gitignore"
3912
3913 #define STATUS_DIFF_INDEX_SHOW_CMD \
3914 "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s %s 2>/dev/null"
3915
3916 #define STATUS_DIFF_FILES_SHOW_CMD \
3917 "git diff-files --root --patch-with-stat -C -M -- %s %s 2>/dev/null"
3918
3919 #define STATUS_DIFF_NO_HEAD_SHOW_CMD \
3920 "git diff --no-color --patch-with-stat /dev/null %s 2>/dev/null"
3921
3922 /* First parse staged info using git-diff-index(1), then parse unstaged
3923 * info using git-diff-files(1), and finally untracked files using
3924 * git-ls-files(1). */
3925 static bool
3926 status_open(struct view *view)
3927 {
3928 struct stat statbuf;
3929 char exclude[SIZEOF_STR];
3930 char indexcmd[SIZEOF_STR] = STATUS_DIFF_INDEX_CMD;
3931 char othercmd[SIZEOF_STR] = STATUS_LIST_OTHER_CMD;
3932 unsigned long prev_lineno = view->lineno;
3933 char indexstatus = 0;
3934 size_t i;
3935
3936 for (i = 0; i < view->lines; i++)
3937 free(view->line[i].data);
3938 free(view->line);
3939 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3940 view->line = NULL;
3941
3942 if (!realloc_lines(view, view->line_size + 7))
3943 return FALSE;
3944
3945 add_line_data(view, NULL, LINE_STAT_HEAD);
3946 if (opt_no_head)
3947 string_copy(status_onbranch, "Initial commit");
3948 else if (!*opt_head)
3949 string_copy(status_onbranch, "Not currently on any branch");
3950 else if (!string_format(status_onbranch, "On branch %s", opt_head))
3951 return FALSE;
3952
3953 if (opt_no_head) {
3954 string_copy(indexcmd, STATUS_LIST_NO_HEAD_CMD);
3955 indexstatus = 'A';
3956 }
3957
3958 if (!string_format(exclude, "%s/info/exclude", opt_git_dir))
3959 return FALSE;
3960
3961 if (stat(exclude, &statbuf) >= 0) {
3962 size_t cmdsize = strlen(othercmd);
3963
3964 if (!string_format_from(othercmd, &cmdsize, " %s", "--exclude-from=") ||
3965 sq_quote(othercmd, cmdsize, exclude) >= sizeof(othercmd))
3966 return FALSE;
3967
3968 cmdsize = strlen(indexcmd);
3969 if (opt_no_head &&
3970 (!string_format_from(indexcmd, &cmdsize, " %s", "--exclude-from=") ||
3971 sq_quote(indexcmd, cmdsize, exclude) >= sizeof(indexcmd)))
3972 return FALSE;
3973 }
3974
3975 system("git update-index -q --refresh");
3976
3977 if (!status_run(view, indexcmd, indexstatus, LINE_STAT_STAGED) ||
3978 !status_run(view, STATUS_DIFF_FILES_CMD, 0, LINE_STAT_UNSTAGED) ||
3979 !status_run(view, othercmd, '?', LINE_STAT_UNTRACKED))
3980 return FALSE;
3981
3982 /* If all went well restore the previous line number to stay in
3983 * the context or select a line with something that can be
3984 * updated. */
3985 if (prev_lineno >= view->lines)
3986 prev_lineno = view->lines - 1;
3987 while (prev_lineno < view->lines && !view->line[prev_lineno].data)
3988 prev_lineno++;
3989 while (prev_lineno > 0 && !view->line[prev_lineno].data)
3990 prev_lineno--;
3991
3992 /* If the above fails, always skip the "On branch" line. */
3993 if (prev_lineno < view->lines)
3994 view->lineno = prev_lineno;
3995 else
3996 view->lineno = 1;
3997
3998 if (view->lineno < view->offset)
3999 view->offset = view->lineno;
4000 else if (view->offset + view->height <= view->lineno)
4001 view->offset = view->lineno - view->height + 1;
4002
4003 return TRUE;
4004 }
4005
4006 static bool
4007 status_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
4008 {
4009 struct status *status = line->data;
4010 char *text;
4011 int col = 0;
4012
4013 wmove(view->win, lineno, 0);
4014
4015 if (selected) {
4016 wattrset(view->win, get_line_attr(LINE_CURSOR));
4017 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
4018
4019 } else if (line->type == LINE_STAT_HEAD) {
4020 wattrset(view->win, get_line_attr(LINE_STAT_HEAD));
4021 wchgat(view->win, -1, 0, LINE_STAT_HEAD, NULL);
4022
4023 } else if (!status && line->type != LINE_STAT_NONE) {
4024 wattrset(view->win, get_line_attr(LINE_STAT_SECTION));
4025 wchgat(view->win, -1, 0, LINE_STAT_SECTION, NULL);
4026
4027 } else {
4028 wattrset(view->win, get_line_attr(line->type));
4029 }
4030
4031 if (!status) {
4032 switch (line->type) {
4033 case LINE_STAT_STAGED:
4034 text = "Changes to be committed:";
4035 break;
4036
4037 case LINE_STAT_UNSTAGED:
4038 text = "Changed but not updated:";
4039 break;
4040
4041 case LINE_STAT_UNTRACKED:
4042 text = "Untracked files:";
4043 break;
4044
4045 case LINE_STAT_NONE:
4046 text = " (no files)";
4047 break;
4048
4049 case LINE_STAT_HEAD:
4050 text = status_onbranch;
4051 break;
4052
4053 default:
4054 return FALSE;
4055 }
4056 } else {
4057 char buf[] = { status->status, ' ', ' ', ' ', 0 };
4058
4059 col += draw_text(view, buf, view->width, TRUE, selected);
4060 if (!selected)
4061 wattrset(view->win, A_NORMAL);
4062 text = status->new.name;
4063 }
4064
4065 draw_text(view, text, view->width - col, TRUE, selected);
4066 return TRUE;
4067 }
4068
4069 static enum request
4070 status_enter(struct view *view, struct line *line)
4071 {
4072 struct status *status = line->data;
4073 char oldpath[SIZEOF_STR] = "";
4074 char newpath[SIZEOF_STR] = "";
4075 char *info;
4076 size_t cmdsize = 0;
4077
4078 if (line->type == LINE_STAT_NONE ||
4079 (!status && line[1].type == LINE_STAT_NONE)) {
4080 report("No file to diff");
4081 return REQ_NONE;
4082 }
4083
4084 if (status) {
4085 if (sq_quote(oldpath, 0, status->old.name) >= sizeof(oldpath))
4086 return REQ_QUIT;
4087 /* Diffs for unmerged entries are empty when pasing the
4088 * new path, so leave it empty. */
4089 if (status->status != 'U' &&
4090 sq_quote(newpath, 0, status->new.name) >= sizeof(newpath))
4091 return REQ_QUIT;
4092 }
4093
4094 if (opt_cdup[0] &&
4095 line->type != LINE_STAT_UNTRACKED &&
4096 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
4097 return REQ_QUIT;
4098
4099 switch (line->type) {
4100 case LINE_STAT_STAGED:
4101 if (opt_no_head) {
4102 if (!string_format_from(opt_cmd, &cmdsize,
4103 STATUS_DIFF_NO_HEAD_SHOW_CMD,
4104 newpath))
4105 return REQ_QUIT;
4106 } else {
4107 if (!string_format_from(opt_cmd, &cmdsize,
4108 STATUS_DIFF_INDEX_SHOW_CMD,
4109 oldpath, newpath))
4110 return REQ_QUIT;
4111 }
4112
4113 if (status)
4114 info = "Staged changes to %s";
4115 else
4116 info = "Staged changes";
4117 break;
4118
4119 case LINE_STAT_UNSTAGED:
4120 if (!string_format_from(opt_cmd, &cmdsize,
4121 STATUS_DIFF_FILES_SHOW_CMD, oldpath, newpath))
4122 return REQ_QUIT;
4123 if (status)
4124 info = "Unstaged changes to %s";
4125 else
4126 info = "Unstaged changes";
4127 break;
4128
4129 case LINE_STAT_UNTRACKED:
4130 if (opt_pipe)
4131 return REQ_QUIT;
4132
4133 if (!status) {
4134 report("No file to show");
4135 return REQ_NONE;
4136 }
4137
4138 opt_pipe = fopen(status->new.name, "r");
4139 info = "Untracked file %s";
4140 break;
4141
4142 case LINE_STAT_HEAD:
4143 return REQ_NONE;
4144
4145 default:
4146 die("line type %d not handled in switch", line->type);
4147 }
4148
4149 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | OPEN_SPLIT);
4150 if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) {
4151 if (status) {
4152 stage_status = *status;
4153 } else {
4154 memset(&stage_status, 0, sizeof(stage_status));
4155 }
4156
4157 stage_line_type = line->type;
4158 string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.new.name);
4159 }
4160
4161 return REQ_NONE;
4162 }
4163
4164 static bool
4165 status_exists(struct status *status, enum line_type type)
4166 {
4167 struct view *view = VIEW(REQ_VIEW_STATUS);
4168 struct line *line;
4169
4170 for (line = view->line; line < view->line + view->lines; line++) {
4171 struct status *pos = line->data;
4172
4173 if (line->type == type && pos &&
4174 !strcmp(status->new.name, pos->new.name))
4175 return TRUE;
4176 }
4177
4178 return FALSE;
4179 }
4180
4181
4182 static FILE *
4183 status_update_prepare(enum line_type type)
4184 {
4185 char cmd[SIZEOF_STR];
4186 size_t cmdsize = 0;
4187
4188 if (opt_cdup[0] &&
4189 type != LINE_STAT_UNTRACKED &&
4190 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4191 return NULL;
4192
4193 switch (type) {
4194 case LINE_STAT_STAGED:
4195 string_add(cmd, cmdsize, "git update-index -z --index-info");
4196 break;
4197
4198 case LINE_STAT_UNSTAGED:
4199 case LINE_STAT_UNTRACKED:
4200 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
4201 break;
4202
4203 default:
4204 die("line type %d not handled in switch", type);
4205 }
4206
4207 return popen(cmd, "w");
4208 }
4209
4210 static bool
4211 status_update_write(FILE *pipe, struct status *status, enum line_type type)
4212 {
4213 char buf[SIZEOF_STR];
4214 size_t bufsize = 0;
4215 size_t written = 0;
4216
4217 switch (type) {
4218 case LINE_STAT_STAGED:
4219 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
4220 status->old.mode,
4221 status->old.rev,
4222 status->old.name, 0))
4223 return FALSE;
4224 break;
4225
4226 case LINE_STAT_UNSTAGED:
4227 case LINE_STAT_UNTRACKED:
4228 if (!string_format_from(buf, &bufsize, "%s%c", status->new.name, 0))
4229 return FALSE;
4230 break;
4231
4232 default:
4233 die("line type %d not handled in switch", type);
4234 }
4235
4236 while (!ferror(pipe) && written < bufsize) {
4237 written += fwrite(buf + written, 1, bufsize - written, pipe);
4238 }
4239
4240 return written == bufsize;
4241 }
4242
4243 static bool
4244 status_update_file(struct status *status, enum line_type type)
4245 {
4246 FILE *pipe = status_update_prepare(type);
4247 bool result;
4248
4249 if (!pipe)
4250 return FALSE;
4251
4252 result = status_update_write(pipe, status, type);
4253 pclose(pipe);
4254 return result;
4255 }
4256
4257 static bool
4258 status_update_files(struct view *view, struct line *line)
4259 {
4260 FILE *pipe = status_update_prepare(line->type);
4261 bool result = TRUE;
4262 struct line *pos = view->line + view->lines;
4263 int files = 0;
4264 int file, done;
4265
4266 if (!pipe)
4267 return FALSE;
4268
4269 for (pos = line; pos < view->line + view->lines && pos->data; pos++)
4270 files++;
4271
4272 for (file = 0, done = 0; result && file < files; line++, file++) {
4273 int almost_done = file * 100 / files;
4274
4275 if (almost_done > done) {
4276 done = almost_done;
4277 string_format(view->ref, "updating file %u of %u (%d%% done)",
4278 file, files, done);
4279 update_view_title(view);
4280 }
4281 result = status_update_write(pipe, line->data, line->type);
4282 }
4283
4284 pclose(pipe);
4285 return result;
4286 }
4287
4288 static bool
4289 status_update(struct view *view)
4290 {
4291 struct line *line = &view->line[view->lineno];
4292
4293 assert(view->lines);
4294
4295 if (!line->data) {
4296 /* This should work even for the "On branch" line. */
4297 if (line < view->line + view->lines && !line[1].data) {
4298 report("Nothing to update");
4299 return FALSE;
4300 }
4301
4302 if (!status_update_files(view, line + 1))
4303 report("Failed to update file status");
4304
4305 } else if (!status_update_file(line->data, line->type)) {
4306 report("Failed to update file status");
4307 }
4308
4309 return TRUE;
4310 }
4311
4312 static enum request
4313 status_request(struct view *view, enum request request, struct line *line)
4314 {
4315 struct status *status = line->data;
4316
4317 switch (request) {
4318 case REQ_STATUS_UPDATE:
4319 if (!status_update(view))
4320 return REQ_NONE;
4321 break;
4322
4323 case REQ_STATUS_MERGE:
4324 if (!status || status->status != 'U') {
4325 report("Merging only possible for files with unmerged status ('U').");
4326 return REQ_NONE;
4327 }
4328 open_mergetool(status->new.name);
4329 break;
4330
4331 case REQ_EDIT:
4332 if (!status)
4333 return request;
4334
4335 open_editor(status->status != '?', status->new.name);
4336 break;
4337
4338 case REQ_VIEW_BLAME:
4339 if (status) {
4340 string_copy(opt_file, status->new.name);
4341 opt_ref[0] = 0;
4342 }
4343 return request;
4344
4345 case REQ_ENTER:
4346 /* After returning the status view has been split to
4347 * show the stage view. No further reloading is
4348 * necessary. */
4349 status_enter(view, line);
4350 return REQ_NONE;
4351
4352 case REQ_REFRESH:
4353 /* Simply reload the view. */
4354 break;
4355
4356 default:
4357 return request;
4358 }
4359
4360 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
4361
4362 return REQ_NONE;
4363 }
4364
4365 static void
4366 status_select(struct view *view, struct line *line)
4367 {
4368 struct status *status = line->data;
4369 char file[SIZEOF_STR] = "all files";
4370 char *text;
4371 char *key;
4372
4373 if (status && !string_format(file, "'%s'", status->new.name))
4374 return;
4375
4376 if (!status && line[1].type == LINE_STAT_NONE)
4377 line++;
4378
4379 switch (line->type) {
4380 case LINE_STAT_STAGED:
4381 text = "Press %s to unstage %s for commit";
4382 break;
4383
4384 case LINE_STAT_UNSTAGED:
4385 text = "Press %s to stage %s for commit";
4386 break;
4387
4388 case LINE_STAT_UNTRACKED:
4389 text = "Press %s to stage %s for addition";
4390 break;
4391
4392 case LINE_STAT_HEAD:
4393 case LINE_STAT_NONE:
4394 text = "Nothing to update";
4395 break;
4396
4397 default:
4398 die("line type %d not handled in switch", line->type);
4399 }
4400
4401 if (status && status->status == 'U') {
4402 text = "Press %s to resolve conflict in %s";
4403 key = get_key(REQ_STATUS_MERGE);
4404
4405 } else {
4406 key = get_key(REQ_STATUS_UPDATE);
4407 }
4408
4409 string_format(view->ref, text, key, file);
4410 }
4411
4412 static bool
4413 status_grep(struct view *view, struct line *line)
4414 {
4415 struct status *status = line->data;
4416 enum { S_STATUS, S_NAME, S_END } state;
4417 char buf[2] = "?";
4418 regmatch_t pmatch;
4419
4420 if (!status)
4421 return FALSE;
4422
4423 for (state = S_STATUS; state < S_END; state++) {
4424 char *text;
4425
4426 switch (state) {
4427 case S_NAME: text = status->new.name; break;
4428 case S_STATUS:
4429 buf[0] = status->status;
4430 text = buf;
4431 break;
4432
4433 default:
4434 return FALSE;
4435 }
4436
4437 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4438 return TRUE;
4439 }
4440
4441 return FALSE;
4442 }
4443
4444 static struct view_ops status_ops = {
4445 "file",
4446 status_open,
4447 NULL,
4448 status_draw,
4449 status_request,
4450 status_grep,
4451 status_select,
4452 };
4453
4454
4455 static bool
4456 stage_diff_line(FILE *pipe, struct line *line)
4457 {
4458 char *buf = line->data;
4459 size_t bufsize = strlen(buf);
4460 size_t written = 0;
4461
4462 while (!ferror(pipe) && written < bufsize) {
4463 written += fwrite(buf + written, 1, bufsize - written, pipe);
4464 }
4465
4466 fputc('\n', pipe);
4467
4468 return written == bufsize;
4469 }
4470
4471 static struct line *
4472 stage_diff_hdr(struct view *view, struct line *line)
4473 {
4474 int diff_hdr_dir = line->type == LINE_DIFF_CHUNK ? -1 : 1;
4475 struct line *diff_hdr;
4476
4477 if (line->type == LINE_DIFF_CHUNK)
4478 diff_hdr = line - 1;
4479 else
4480 diff_hdr = view->line + 1;
4481
4482 while (diff_hdr > view->line && diff_hdr < view->line + view->lines) {
4483 if (diff_hdr->type == LINE_DIFF_HEADER)
4484 return diff_hdr;
4485
4486 diff_hdr += diff_hdr_dir;
4487 }
4488
4489 return NULL;
4490 }
4491
4492 static bool
4493 stage_update_chunk(struct view *view, struct line *line)
4494 {
4495 char cmd[SIZEOF_STR];
4496 size_t cmdsize = 0;
4497 struct line *diff_hdr, *diff_chunk, *diff_end;
4498 FILE *pipe;
4499
4500 diff_hdr = stage_diff_hdr(view, line);
4501 if (!diff_hdr)
4502 return FALSE;
4503
4504 if (opt_cdup[0] &&
4505 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4506 return FALSE;
4507
4508 if (!string_format_from(cmd, &cmdsize,
4509 "git apply --whitespace=nowarn --cached %s - && "
4510 "git update-index -q --unmerged --refresh 2>/dev/null",
4511 stage_line_type == LINE_STAT_STAGED ? "-R" : ""))
4512 return FALSE;
4513
4514 pipe = popen(cmd, "w");
4515 if (!pipe)
4516 return FALSE;
4517
4518 diff_end = view->line + view->lines;
4519 if (line->type != LINE_DIFF_CHUNK) {
4520 diff_chunk = diff_hdr;
4521
4522 } else {
4523 for (diff_chunk = line + 1; diff_chunk < diff_end; diff_chunk++)
4524 if (diff_chunk->type == LINE_DIFF_CHUNK ||
4525 diff_chunk->type == LINE_DIFF_HEADER)
4526 diff_end = diff_chunk;
4527
4528 diff_chunk = line;
4529
4530 while (diff_hdr->type != LINE_DIFF_CHUNK) {
4531 switch (diff_hdr->type) {
4532 case LINE_DIFF_HEADER:
4533 case LINE_DIFF_INDEX:
4534 case LINE_DIFF_ADD:
4535 case LINE_DIFF_DEL:
4536 break;
4537
4538 default:
4539 diff_hdr++;
4540 continue;
4541 }
4542
4543 if (!stage_diff_line(pipe, diff_hdr++)) {
4544 pclose(pipe);
4545 return FALSE;
4546 }
4547 }
4548 }
4549
4550 while (diff_chunk < diff_end && stage_diff_line(pipe, diff_chunk))
4551 diff_chunk++;
4552
4553 pclose(pipe);
4554
4555 if (diff_chunk != diff_end)
4556 return FALSE;
4557
4558 return TRUE;
4559 }
4560
4561 static bool
4562 stage_update(struct view *view, struct line *line)
4563 {
4564 if (!opt_no_head && stage_line_type != LINE_STAT_UNTRACKED &&
4565 (line->type == LINE_DIFF_CHUNK || !stage_status.status)) {
4566 if (!stage_update_chunk(view, line)) {
4567 report("Failed to apply chunk");
4568 return FALSE;
4569 }
4570
4571 } else if (!status_update_file(&stage_status, stage_line_type)) {
4572 report("Failed to update file");
4573 return FALSE;
4574 }
4575
4576 return TRUE;
4577 }
4578
4579 static enum request
4580 stage_request(struct view *view, enum request request, struct line *line)
4581 {
4582 switch (request) {
4583 case REQ_STATUS_UPDATE:
4584 stage_update(view, line);
4585 break;
4586
4587 case REQ_EDIT:
4588 if (!stage_status.new.name[0])
4589 return request;
4590
4591 open_editor(stage_status.status != '?', stage_status.new.name);
4592 break;
4593
4594 case REQ_REFRESH:
4595 /* Reload everything ... */
4596 break;
4597
4598 case REQ_VIEW_BLAME:
4599 if (stage_status.new.name[0]) {
4600 string_copy(opt_file, stage_status.new.name);
4601 opt_ref[0] = 0;
4602 }
4603 return request;
4604
4605 case REQ_ENTER:
4606 return pager_request(view, request, line);
4607
4608 default:
4609 return request;
4610 }
4611
4612 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD | OPEN_NOMAXIMIZE);
4613
4614 /* Check whether the staged entry still exists, and close the
4615 * stage view if it doesn't. */
4616 if (!status_exists(&stage_status, stage_line_type))
4617 return REQ_VIEW_CLOSE;
4618
4619 if (stage_line_type == LINE_STAT_UNTRACKED)
4620 opt_pipe = fopen(stage_status.new.name, "r");
4621 else
4622 string_copy(opt_cmd, view->cmd);
4623 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | OPEN_NOMAXIMIZE);
4624
4625 return REQ_NONE;
4626 }
4627
4628 static struct view_ops stage_ops = {
4629 "line",
4630 NULL,
4631 pager_read,
4632 pager_draw,
4633 stage_request,
4634 pager_grep,
4635 pager_select,
4636 };
4637
4638
4639 /*
4640 * Revision graph
4641 */
4642
4643 struct commit {
4644 char id[SIZEOF_REV]; /* SHA1 ID. */
4645 char title[128]; /* First line of the commit message. */
4646 char author[75]; /* Author of the commit. */
4647 struct tm time; /* Date from the author ident. */
4648 struct ref **refs; /* Repository references. */
4649 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
4650 size_t graph_size; /* The width of the graph array. */
4651 bool has_parents; /* Rewritten --parents seen. */
4652 };
4653
4654 /* Size of rev graph with no "padding" columns */
4655 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
4656
4657 struct rev_graph {
4658 struct rev_graph *prev, *next, *parents;
4659 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
4660 size_t size;
4661 struct commit *commit;
4662 size_t pos;
4663 unsigned int boundary:1;
4664 };
4665
4666 /* Parents of the commit being visualized. */
4667 static struct rev_graph graph_parents[4];
4668
4669 /* The current stack of revisions on the graph. */
4670 static struct rev_graph graph_stacks[4] = {
4671 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
4672 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
4673 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
4674 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
4675 };
4676
4677 static inline bool
4678 graph_parent_is_merge(struct rev_graph *graph)
4679 {
4680 return graph->parents->size > 1;
4681 }
4682
4683 static inline void
4684 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
4685 {
4686 struct commit *commit = graph->commit;
4687
4688 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
4689 commit->graph[commit->graph_size++] = symbol;
4690 }
4691
4692 static void
4693 done_rev_graph(struct rev_graph *graph)
4694 {
4695 if (graph_parent_is_merge(graph) &&
4696 graph->pos < graph->size - 1 &&
4697 graph->next->size == graph->size + graph->parents->size - 1) {
4698 size_t i = graph->pos + graph->parents->size - 1;
4699
4700 graph->commit->graph_size = i * 2;
4701 while (i < graph->next->size - 1) {
4702 append_to_rev_graph(graph, ' ');
4703 append_to_rev_graph(graph, '\\');
4704 i++;
4705 }
4706 }
4707
4708 graph->size = graph->pos = 0;
4709 graph->commit = NULL;
4710 memset(graph->parents, 0, sizeof(*graph->parents));
4711 }
4712
4713 static void
4714 push_rev_graph(struct rev_graph *graph, char *parent)
4715 {
4716 int i;
4717
4718 /* "Collapse" duplicate parents lines.
4719 *
4720 * FIXME: This needs to also update update the drawn graph but
4721 * for now it just serves as a method for pruning graph lines. */
4722 for (i = 0; i < graph->size; i++)
4723 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
4724 return;
4725
4726 if (graph->size < SIZEOF_REVITEMS) {
4727 string_copy_rev(graph->rev[graph->size++], parent);
4728 }
4729 }
4730
4731 static chtype
4732 get_rev_graph_symbol(struct rev_graph *graph)
4733 {
4734 chtype symbol;
4735
4736 if (graph->boundary)
4737 symbol = REVGRAPH_BOUND;
4738 else if (graph->parents->size == 0)
4739 symbol = REVGRAPH_INIT;
4740 else if (graph_parent_is_merge(graph))
4741 symbol = REVGRAPH_MERGE;
4742 else if (graph->pos >= graph->size)
4743 symbol = REVGRAPH_BRANCH;
4744 else
4745 symbol = REVGRAPH_COMMIT;
4746
4747 return symbol;
4748 }
4749
4750 static void
4751 draw_rev_graph(struct rev_graph *graph)
4752 {
4753 struct rev_filler {
4754 chtype separator, line;
4755 };
4756 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
4757 static struct rev_filler fillers[] = {
4758 { ' ', REVGRAPH_LINE },
4759 { '`', '.' },
4760 { '\'', ' ' },
4761 { '/', ' ' },
4762 };
4763 chtype symbol = get_rev_graph_symbol(graph);
4764 struct rev_filler *filler;
4765 size_t i;
4766
4767 filler = &fillers[DEFAULT];
4768
4769 for (i = 0; i < graph->pos; i++) {
4770 append_to_rev_graph(graph, filler->line);
4771 if (graph_parent_is_merge(graph->prev) &&
4772 graph->prev->pos == i)
4773 filler = &fillers[RSHARP];
4774
4775 append_to_rev_graph(graph, filler->separator);
4776 }
4777
4778 /* Place the symbol for this revision. */
4779 append_to_rev_graph(graph, symbol);
4780
4781 if (graph->prev->size > graph->size)
4782 filler = &fillers[RDIAG];
4783 else
4784 filler = &fillers[DEFAULT];
4785
4786 i++;
4787
4788 for (; i < graph->size; i++) {
4789 append_to_rev_graph(graph, filler->separator);
4790 append_to_rev_graph(graph, filler->line);
4791 if (graph_parent_is_merge(graph->prev) &&
4792 i < graph->prev->pos + graph->parents->size)
4793 filler = &fillers[RSHARP];
4794 if (graph->prev->size > graph->size)
4795 filler = &fillers[LDIAG];
4796 }
4797
4798 if (graph->prev->size > graph->size) {
4799 append_to_rev_graph(graph, filler->separator);
4800 if (filler->line != ' ')
4801 append_to_rev_graph(graph, filler->line);
4802 }
4803 }
4804
4805 /* Prepare the next rev graph */
4806 static void
4807 prepare_rev_graph(struct rev_graph *graph)
4808 {
4809 size_t i;
4810
4811 /* First, traverse all lines of revisions up to the active one. */
4812 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
4813 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
4814 break;
4815
4816 push_rev_graph(graph->next, graph->rev[graph->pos]);
4817 }
4818
4819 /* Interleave the new revision parent(s). */
4820 for (i = 0; !graph->boundary && i < graph->parents->size; i++)
4821 push_rev_graph(graph->next, graph->parents->rev[i]);
4822
4823 /* Lastly, put any remaining revisions. */
4824 for (i = graph->pos + 1; i < graph->size; i++)
4825 push_rev_graph(graph->next, graph->rev[i]);
4826 }
4827
4828 static void
4829 update_rev_graph(struct rev_graph *graph)
4830 {
4831 /* If this is the finalizing update ... */
4832 if (graph->commit)
4833 prepare_rev_graph(graph);
4834
4835 /* Graph visualization needs a one rev look-ahead,
4836 * so the first update doesn't visualize anything. */
4837 if (!graph->prev->commit)
4838 return;
4839
4840 draw_rev_graph(graph->prev);
4841 done_rev_graph(graph->prev->prev);
4842 }
4843
4844
4845 /*
4846 * Main view backend
4847 */
4848
4849 static bool
4850 main_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
4851 {
4852 char buf[DATE_COLS + 1];
4853 struct commit *commit = line->data;
4854 enum line_type type;
4855 int col = 0;
4856 size_t timelen;
4857 int space;
4858
4859 if (!*commit->author)
4860 return FALSE;
4861
4862 space = view->width;
4863 wmove(view->win, lineno, col);
4864
4865 if (selected) {
4866 type = LINE_CURSOR;
4867 wattrset(view->win, get_line_attr(type));
4868 wchgat(view->win, -1, 0, type, NULL);
4869 } else {
4870 type = LINE_MAIN_COMMIT;
4871 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
4872 }
4873
4874 if (opt_date) {
4875 int n;
4876
4877 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
4878 n = draw_text(view, buf, view->width - col, FALSE, selected);
4879 draw_text(view, " ", view->width - col - n, FALSE, selected);
4880
4881 col += DATE_COLS;
4882 wmove(view->win, lineno, col);
4883 if (col >= view->width)
4884 return TRUE;
4885 }
4886 if (type != LINE_CURSOR)
4887 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
4888
4889 if (opt_author) {
4890 int max_len;
4891
4892 max_len = view->width - col;
4893 if (max_len > AUTHOR_COLS - 1)
4894 max_len = AUTHOR_COLS - 1;
4895 draw_text(view, commit->author, max_len, TRUE, selected);
4896 col += AUTHOR_COLS;
4897 if (col >= view->width)
4898 return TRUE;
4899 }
4900
4901 if (opt_rev_graph && commit->graph_size) {
4902 size_t graph_size = view->width - col;
4903 size_t i;
4904
4905 if (type != LINE_CURSOR)
4906 wattrset(view->win, get_line_attr(LINE_MAIN_REVGRAPH));
4907 wmove(view->win, lineno, col);
4908 if (graph_size > commit->graph_size)
4909 graph_size = commit->graph_size;
4910 /* Using waddch() instead of waddnstr() ensures that
4911 * they'll be rendered correctly for the cursor line. */
4912 for (i = 0; i < graph_size; i++)
4913 waddch(view->win, commit->graph[i]);
4914
4915 col += commit->graph_size + 1;
4916 if (col >= view->width)
4917 return TRUE;
4918 waddch(view->win, ' ');
4919 }
4920 if (type != LINE_CURSOR)
4921 wattrset(view->win, A_NORMAL);
4922
4923 wmove(view->win, lineno, col);
4924
4925 if (opt_show_refs && commit->refs) {
4926 size_t i = 0;
4927
4928 do {
4929 if (type == LINE_CURSOR)
4930 ;
4931 else if (commit->refs[i]->head)
4932 wattrset(view->win, get_line_attr(LINE_MAIN_HEAD));
4933 else if (commit->refs[i]->ltag)
4934 wattrset(view->win, get_line_attr(LINE_MAIN_LOCAL_TAG));
4935 else if (commit->refs[i]->tag)
4936 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
4937 else if (commit->refs[i]->tracked)
4938 wattrset(view->win, get_line_attr(LINE_MAIN_TRACKED));
4939 else if (commit->refs[i]->remote)
4940 wattrset(view->win, get_line_attr(LINE_MAIN_REMOTE));
4941 else
4942 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
4943
4944 col += draw_text(view, "[", view->width - col, TRUE, selected);
4945 col += draw_text(view, commit->refs[i]->name, view->width - col,
4946 TRUE, selected);
4947 col += draw_text(view, "]", view->width - col, TRUE, selected);
4948 if (type != LINE_CURSOR)
4949 wattrset(view->win, A_NORMAL);
4950 col += draw_text(view, " ", view->width - col, TRUE, selected);
4951 if (col >= view->width)
4952 return TRUE;
4953 } while (commit->refs[i++]->next);
4954 }
4955
4956 if (type != LINE_CURSOR)
4957 wattrset(view->win, get_line_attr(type));
4958
4959 draw_text(view, commit->title, view->width - col, TRUE, selected);
4960 return TRUE;
4961 }
4962
4963 /* Reads git log --pretty=raw output and parses it into the commit struct. */
4964 static bool
4965 main_read(struct view *view, char *line)
4966 {
4967 static struct rev_graph *graph = graph_stacks;
4968 enum line_type type;
4969 struct commit *commit;
4970
4971 if (!line) {
4972 update_rev_graph(graph);
4973 return TRUE;
4974 }
4975
4976 type = get_line_type(line);
4977 if (type == LINE_COMMIT) {
4978 commit = calloc(1, sizeof(struct commit));
4979 if (!commit)
4980 return FALSE;
4981
4982 line += STRING_SIZE("commit ");
4983 if (*line == '-') {
4984 graph->boundary = 1;
4985 line++;
4986 }
4987
4988 string_copy_rev(commit->id, line);
4989 commit->refs = get_refs(commit->id);
4990 graph->commit = commit;
4991 add_line_data(view, commit, LINE_MAIN_COMMIT);
4992
4993 while ((line = strchr(line, ' '))) {
4994 line++;
4995 push_rev_graph(graph->parents, line);
4996 commit->has_parents = TRUE;
4997 }
4998 return TRUE;
4999 }
5000
5001 if (!view->lines)
5002 return TRUE;
5003 commit = view->line[view->lines - 1].data;
5004
5005 switch (type) {
5006 case LINE_PARENT:
5007 if (commit->has_parents)
5008 break;
5009 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
5010 break;
5011
5012 case LINE_AUTHOR:
5013 {
5014 /* Parse author lines where the name may be empty:
5015 * author <email@address.tld> 1138474660 +0100
5016 */
5017 char *ident = line + STRING_SIZE("author ");
5018 char *nameend = strchr(ident, '<');
5019 char *emailend = strchr(ident, '>');
5020
5021 if (!nameend || !emailend)
5022 break;
5023
5024 update_rev_graph(graph);
5025 graph = graph->next;
5026
5027 *nameend = *emailend = 0;
5028 ident = chomp_string(ident);
5029 if (!*ident) {
5030 ident = chomp_string(nameend + 1);
5031 if (!*ident)
5032 ident = "Unknown";
5033 }
5034
5035 string_ncopy(commit->author, ident, strlen(ident));
5036
5037 /* Parse epoch and timezone */
5038 if (emailend[1] == ' ') {
5039 char *secs = emailend + 2;
5040 char *zone = strchr(secs, ' ');
5041 time_t time = (time_t) atol(secs);
5042
5043 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
5044 long tz;
5045
5046 zone++;
5047 tz = ('0' - zone[1]) * 60 * 60 * 10;
5048 tz += ('0' - zone[2]) * 60 * 60;
5049 tz += ('0' - zone[3]) * 60;
5050 tz += ('0' - zone[4]) * 60;
5051
5052 if (zone[0] == '-')
5053 tz = -tz;
5054
5055 time -= tz;
5056 }
5057
5058 gmtime_r(&time, &commit->time);
5059 }
5060 break;
5061 }
5062 default:
5063 /* Fill in the commit title if it has not already been set. */
5064 if (commit->title[0])
5065 break;
5066
5067 /* Require titles to start with a non-space character at the
5068 * offset used by git log. */
5069 if (strncmp(line, " ", 4))
5070 break;
5071 line += 4;
5072 /* Well, if the title starts with a whitespace character,
5073 * try to be forgiving. Otherwise we end up with no title. */
5074 while (isspace(*line))
5075 line++;
5076 if (*line == '\0')
5077 break;
5078 /* FIXME: More graceful handling of titles; append "..." to
5079 * shortened titles, etc. */
5080
5081 string_ncopy(commit->title, line, strlen(line));
5082 }
5083
5084 return TRUE;
5085 }
5086
5087 static enum request
5088 main_request(struct view *view, enum request request, struct line *line)
5089 {
5090 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
5091
5092 if (request == REQ_ENTER)
5093 open_view(view, REQ_VIEW_DIFF, flags);
5094 else
5095 return request;
5096
5097 return REQ_NONE;
5098 }
5099
5100 static bool
5101 main_grep(struct view *view, struct line *line)
5102 {
5103 struct commit *commit = line->data;
5104 enum { S_TITLE, S_AUTHOR, S_DATE, S_END } state;
5105 char buf[DATE_COLS + 1];
5106 regmatch_t pmatch;
5107
5108 for (state = S_TITLE; state < S_END; state++) {
5109 char *text;
5110
5111 switch (state) {
5112 case S_TITLE: text = commit->title; break;
5113 case S_AUTHOR: text = commit->author; break;
5114 case S_DATE:
5115 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
5116 continue;
5117 text = buf;
5118 break;
5119
5120 default:
5121 return FALSE;
5122 }
5123
5124 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
5125 return TRUE;
5126 }
5127
5128 return FALSE;
5129 }
5130
5131 static void
5132 main_select(struct view *view, struct line *line)
5133 {
5134 struct commit *commit = line->data;
5135
5136 string_copy_rev(view->ref, commit->id);
5137 string_copy_rev(ref_commit, view->ref);
5138 }
5139
5140 static struct view_ops main_ops = {
5141 "commit",
5142 NULL,
5143 main_read,
5144 main_draw,
5145 main_request,
5146 main_grep,
5147 main_select,
5148 };
5149
5150
5151 /*
5152 * Unicode / UTF-8 handling
5153 *
5154 * NOTE: Much of the following code for dealing with unicode is derived from
5155 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
5156 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
5157 */
5158
5159 /* I've (over)annotated a lot of code snippets because I am not entirely
5160 * confident that the approach taken by this small UTF-8 interface is correct.
5161 * --jonas */
5162
5163 static inline int
5164 unicode_width(unsigned long c)
5165 {
5166 if (c >= 0x1100 &&
5167 (c <= 0x115f /* Hangul Jamo */
5168 || c == 0x2329
5169 || c == 0x232a
5170 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
5171 /* CJK ... Yi */
5172 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
5173 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
5174 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
5175 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
5176 || (c >= 0xffe0 && c <= 0xffe6)
5177 || (c >= 0x20000 && c <= 0x2fffd)
5178 || (c >= 0x30000 && c <= 0x3fffd)))
5179 return 2;
5180
5181 if (c == '\t')
5182 return opt_tab_size;
5183
5184 return 1;
5185 }
5186
5187 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
5188 * Illegal bytes are set one. */
5189 static const unsigned char utf8_bytes[256] = {
5190 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,
5191 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,
5192 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,
5193 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,
5194 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,
5195 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,
5196 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,
5197 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,
5198 };
5199
5200 /* Decode UTF-8 multi-byte representation into a unicode character. */
5201 static inline unsigned long
5202 utf8_to_unicode(const char *string, size_t length)
5203 {
5204 unsigned long unicode;
5205
5206 switch (length) {
5207 case 1:
5208 unicode = string[0];
5209 break;
5210 case 2:
5211 unicode = (string[0] & 0x1f) << 6;
5212 unicode += (string[1] & 0x3f);
5213 break;
5214 case 3:
5215 unicode = (string[0] & 0x0f) << 12;
5216 unicode += ((string[1] & 0x3f) << 6);
5217 unicode += (string[2] & 0x3f);
5218 break;
5219 case 4:
5220 unicode = (string[0] & 0x0f) << 18;
5221 unicode += ((string[1] & 0x3f) << 12);
5222 unicode += ((string[2] & 0x3f) << 6);
5223 unicode += (string[3] & 0x3f);
5224 break;
5225 case 5:
5226 unicode = (string[0] & 0x0f) << 24;
5227 unicode += ((string[1] & 0x3f) << 18);
5228 unicode += ((string[2] & 0x3f) << 12);
5229 unicode += ((string[3] & 0x3f) << 6);
5230 unicode += (string[4] & 0x3f);
5231 break;
5232 case 6:
5233 unicode = (string[0] & 0x01) << 30;
5234 unicode += ((string[1] & 0x3f) << 24);
5235 unicode += ((string[2] & 0x3f) << 18);
5236 unicode += ((string[3] & 0x3f) << 12);
5237 unicode += ((string[4] & 0x3f) << 6);
5238 unicode += (string[5] & 0x3f);
5239 break;
5240 default:
5241 die("Invalid unicode length");
5242 }
5243
5244 /* Invalid characters could return the special 0xfffd value but NUL
5245 * should be just as good. */
5246 return unicode > 0xffff ? 0 : unicode;
5247 }
5248
5249 /* Calculates how much of string can be shown within the given maximum width
5250 * and sets trimmed parameter to non-zero value if all of string could not be
5251 * shown. If the reserve flag is TRUE, it will reserve at least one
5252 * trailing character, which can be useful when drawing a delimiter.
5253 *
5254 * Returns the number of bytes to output from string to satisfy max_width. */
5255 static size_t
5256 utf8_length(const char *string, size_t max_width, int *trimmed, bool reserve)
5257 {
5258 const char *start = string;
5259 const char *end = strchr(string, '\0');
5260 unsigned char last_bytes = 0;
5261 size_t width = 0;
5262
5263 *trimmed = 0;
5264
5265 while (string < end) {
5266 int c = *(unsigned char *) string;
5267 unsigned char bytes = utf8_bytes[c];
5268 size_t ucwidth;
5269 unsigned long unicode;
5270
5271 if (string + bytes > end)
5272 break;
5273
5274 /* Change representation to figure out whether
5275 * it is a single- or double-width character. */
5276
5277 unicode = utf8_to_unicode(string, bytes);
5278 /* FIXME: Graceful handling of invalid unicode character. */
5279 if (!unicode)
5280 break;
5281
5282 ucwidth = unicode_width(unicode);
5283 width += ucwidth;
5284 if (width > max_width) {
5285 *trimmed = 1;
5286 if (reserve && width - ucwidth == max_width) {
5287 string -= last_bytes;
5288 }
5289 break;
5290 }
5291
5292 string += bytes;
5293 last_bytes = bytes;
5294 }
5295
5296 return string - start;
5297 }
5298
5299
5300 /*
5301 * Status management
5302 */
5303
5304 /* Whether or not the curses interface has been initialized. */
5305 static bool cursed = FALSE;
5306
5307 /* The status window is used for polling keystrokes. */
5308 static WINDOW *status_win;
5309
5310 static bool status_empty = TRUE;
5311
5312 /* Update status and title window. */
5313 static void
5314 report(const char *msg, ...)
5315 {
5316 struct view *view = display[current_view];
5317
5318 if (input_mode)
5319 return;
5320
5321 if (!view) {
5322 char buf[SIZEOF_STR];
5323 va_list args;
5324
5325 va_start(args, msg);
5326 if (vsnprintf(buf, sizeof(buf), msg, args) >= sizeof(buf)) {
5327 buf[sizeof(buf) - 1] = 0;
5328 buf[sizeof(buf) - 2] = '.';
5329 buf[sizeof(buf) - 3] = '.';
5330 buf[sizeof(buf) - 4] = '.';
5331 }
5332 va_end(args);
5333 die("%s", buf);
5334 }
5335
5336 if (!status_empty || *msg) {
5337 va_list args;
5338
5339 va_start(args, msg);
5340
5341 wmove(status_win, 0, 0);
5342 if (*msg) {
5343 vwprintw(status_win, msg, args);
5344 status_empty = FALSE;
5345 } else {
5346 status_empty = TRUE;
5347 }
5348 wclrtoeol(status_win);
5349 wrefresh(status_win);
5350
5351 va_end(args);
5352 }
5353
5354 update_view_title(view);
5355 update_display_cursor(view);
5356 }
5357
5358 /* Controls when nodelay should be in effect when polling user input. */
5359 static void
5360 set_nonblocking_input(bool loading)
5361 {
5362 static unsigned int loading_views;
5363
5364 if ((loading == FALSE && loading_views-- == 1) ||
5365 (loading == TRUE && loading_views++ == 0))
5366 nodelay(status_win, loading);
5367 }
5368
5369 static void
5370 init_display(void)
5371 {
5372 int x, y;
5373
5374 /* Initialize the curses library */
5375 if (isatty(STDIN_FILENO)) {
5376 cursed = !!initscr();
5377 } else {
5378 /* Leave stdin and stdout alone when acting as a pager. */
5379 FILE *io = fopen("/dev/tty", "r+");
5380
5381 if (!io)
5382 die("Failed to open /dev/tty");
5383 cursed = !!newterm(NULL, io, io);
5384 }
5385
5386 if (!cursed)
5387 die("Failed to initialize curses");
5388
5389 nonl(); /* Tell curses not to do NL->CR/NL on output */
5390 cbreak(); /* Take input chars one at a time, no wait for \n */
5391 noecho(); /* Don't echo input */
5392 leaveok(stdscr, TRUE);
5393
5394 if (has_colors())
5395 init_colors();
5396
5397 getmaxyx(stdscr, y, x);
5398 status_win = newwin(1, 0, y - 1, 0);
5399 if (!status_win)
5400 die("Failed to create status window");
5401
5402 /* Enable keyboard mapping */
5403 keypad(status_win, TRUE);
5404 wbkgdset(status_win, get_line_attr(LINE_STATUS));
5405 }
5406
5407 static char *
5408 read_prompt(const char *prompt)
5409 {
5410 enum { READING, STOP, CANCEL } status = READING;
5411 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
5412 int pos = 0;
5413
5414 while (status == READING) {
5415 struct view *view;
5416 int i, key;
5417
5418 input_mode = TRUE;
5419
5420 foreach_view (view, i)
5421 update_view(view);
5422
5423 input_mode = FALSE;
5424
5425 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
5426 wclrtoeol(status_win);
5427
5428 /* Refresh, accept single keystroke of input */
5429 key = wgetch(status_win);
5430 switch (key) {
5431 case KEY_RETURN:
5432 case KEY_ENTER:
5433 case '\n':
5434 status = pos ? STOP : CANCEL;
5435 break;
5436
5437 case KEY_BACKSPACE:
5438 if (pos > 0)
5439 pos--;
5440 else
5441 status = CANCEL;
5442 break;
5443
5444 case KEY_ESC:
5445 status = CANCEL;
5446 break;
5447
5448 case ERR:
5449 break;
5450
5451 default:
5452 if (pos >= sizeof(buf)) {
5453 report("Input string too long");
5454 return NULL;
5455 }
5456
5457 if (isprint(key))
5458 buf[pos++] = (char) key;
5459 }
5460 }
5461
5462 /* Clear the status window */
5463 status_empty = FALSE;
5464 report("");
5465
5466 if (status == CANCEL)
5467 return NULL;
5468
5469 buf[pos++] = 0;
5470
5471 return buf;
5472 }
5473
5474 /*
5475 * Repository references
5476 */
5477
5478 static struct ref *refs = NULL;
5479 static size_t refs_alloc = 0;
5480 static size_t refs_size = 0;
5481
5482 /* Id <-> ref store */
5483 static struct ref ***id_refs = NULL;
5484 static size_t id_refs_alloc = 0;
5485 static size_t id_refs_size = 0;
5486
5487 static struct ref **
5488 get_refs(char *id)
5489 {
5490 struct ref ***tmp_id_refs;
5491 struct ref **ref_list = NULL;
5492 size_t ref_list_alloc = 0;
5493 size_t ref_list_size = 0;
5494 size_t i;
5495
5496 for (i = 0; i < id_refs_size; i++)
5497 if (!strcmp(id, id_refs[i][0]->id))
5498 return id_refs[i];
5499
5500 tmp_id_refs = realloc_items(id_refs, &id_refs_alloc, id_refs_size + 1,
5501 sizeof(*id_refs));
5502 if (!tmp_id_refs)
5503 return NULL;
5504
5505 id_refs = tmp_id_refs;
5506
5507 for (i = 0; i < refs_size; i++) {
5508 struct ref **tmp;
5509
5510 if (strcmp(id, refs[i].id))
5511 continue;
5512
5513 tmp = realloc_items(ref_list, &ref_list_alloc,
5514 ref_list_size + 1, sizeof(*ref_list));
5515 if (!tmp) {
5516 if (ref_list)
5517 free(ref_list);
5518 return NULL;
5519 }
5520
5521 ref_list = tmp;
5522 if (ref_list_size > 0)
5523 ref_list[ref_list_size - 1]->next = 1;
5524 ref_list[ref_list_size] = &refs[i];
5525
5526 /* XXX: The properties of the commit chains ensures that we can
5527 * safely modify the shared ref. The repo references will
5528 * always be similar for the same id. */
5529 ref_list[ref_list_size]->next = 0;
5530 ref_list_size++;
5531 }
5532
5533 if (ref_list)
5534 id_refs[id_refs_size++] = ref_list;
5535
5536 return ref_list;
5537 }
5538
5539 static int
5540 read_ref(char *id, size_t idlen, char *name, size_t namelen)
5541 {
5542 struct ref *ref;
5543 bool tag = FALSE;
5544 bool ltag = FALSE;
5545 bool remote = FALSE;
5546 bool tracked = FALSE;
5547 bool check_replace = FALSE;
5548 bool head = FALSE;
5549
5550 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
5551 if (!strcmp(name + namelen - 3, "^{}")) {
5552 namelen -= 3;
5553 name[namelen] = 0;
5554 if (refs_size > 0 && refs[refs_size - 1].ltag == TRUE)
5555 check_replace = TRUE;
5556 } else {
5557 ltag = TRUE;
5558 }
5559
5560 tag = TRUE;
5561 namelen -= STRING_SIZE("refs/tags/");
5562 name += STRING_SIZE("refs/tags/");
5563
5564 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
5565 remote = TRUE;
5566 namelen -= STRING_SIZE("refs/remotes/");
5567 name += STRING_SIZE("refs/remotes/");
5568 tracked = !strcmp(opt_remote, name);
5569
5570 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5571 namelen -= STRING_SIZE("refs/heads/");
5572 name += STRING_SIZE("refs/heads/");
5573 head = !strncmp(opt_head, name, namelen);
5574
5575 } else if (!strcmp(name, "HEAD")) {
5576 opt_no_head = FALSE;
5577 return OK;
5578 }
5579
5580 if (check_replace && !strcmp(name, refs[refs_size - 1].name)) {
5581 /* it's an annotated tag, replace the previous sha1 with the
5582 * resolved commit id; relies on the fact git-ls-remote lists
5583 * the commit id of an annotated tag right beofre the commit id
5584 * it points to. */
5585 refs[refs_size - 1].ltag = ltag;
5586 string_copy_rev(refs[refs_size - 1].id, id);
5587
5588 return OK;
5589 }
5590 refs = realloc_items(refs, &refs_alloc, refs_size + 1, sizeof(*refs));
5591 if (!refs)
5592 return ERR;
5593
5594 ref = &refs[refs_size++];
5595 ref->name = malloc(namelen + 1);
5596 if (!ref->name)
5597 return ERR;
5598
5599 strncpy(ref->name, name, namelen);
5600 ref->name[namelen] = 0;
5601 ref->head = head;
5602 ref->tag = tag;
5603 ref->ltag = ltag;
5604 ref->remote = remote;
5605 ref->tracked = tracked;
5606 string_copy_rev(ref->id, id);
5607
5608 return OK;
5609 }
5610
5611 static int
5612 load_refs(void)
5613 {
5614 const char *cmd_env = getenv("TIG_LS_REMOTE");
5615 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
5616
5617 return read_properties(popen(cmd, "r"), "\t", read_ref);
5618 }
5619
5620 static int
5621 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
5622 {
5623 if (!strcmp(name, "i18n.commitencoding"))
5624 string_ncopy(opt_encoding, value, valuelen);
5625
5626 if (!strcmp(name, "core.editor"))
5627 string_ncopy(opt_editor, value, valuelen);
5628
5629 /* branch.<head>.remote */
5630 if (*opt_head &&
5631 !strncmp(name, "branch.", 7) &&
5632 !strncmp(name + 7, opt_head, strlen(opt_head)) &&
5633 !strcmp(name + 7 + strlen(opt_head), ".remote"))
5634 string_ncopy(opt_remote, value, valuelen);
5635
5636 if (*opt_head && *opt_remote &&
5637 !strncmp(name, "branch.", 7) &&
5638 !strncmp(name + 7, opt_head, strlen(opt_head)) &&
5639 !strcmp(name + 7 + strlen(opt_head), ".merge")) {
5640 size_t from = strlen(opt_remote);
5641
5642 if (!strncmp(value, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5643 value += STRING_SIZE("refs/heads/");
5644 valuelen -= STRING_SIZE("refs/heads/");
5645 }
5646
5647 if (!string_format_from(opt_remote, &from, "/%s", value))
5648 opt_remote[0] = 0;
5649 }
5650
5651 return OK;
5652 }
5653
5654 static int
5655 load_git_config(void)
5656 {
5657 return read_properties(popen(GIT_CONFIG " --list", "r"),
5658 "=", read_repo_config_option);
5659 }
5660
5661 static int
5662 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
5663 {
5664 if (!opt_git_dir[0]) {
5665 string_ncopy(opt_git_dir, name, namelen);
5666
5667 } else if (opt_is_inside_work_tree == -1) {
5668 /* This can be 3 different values depending on the
5669 * version of git being used. If git-rev-parse does not
5670 * understand --is-inside-work-tree it will simply echo
5671 * the option else either "true" or "false" is printed.
5672 * Default to true for the unknown case. */
5673 opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
5674
5675 } else if (opt_cdup[0] == ' ') {
5676 string_ncopy(opt_cdup, name, namelen);
5677 } else {
5678 if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5679 namelen -= STRING_SIZE("refs/heads/");
5680 name += STRING_SIZE("refs/heads/");
5681 string_ncopy(opt_head, name, namelen);
5682 }
5683 }
5684
5685 return OK;
5686 }
5687
5688 static int
5689 load_repo_info(void)
5690 {
5691 int result;
5692 FILE *pipe = popen("git rev-parse --git-dir --is-inside-work-tree "
5693 " --show-cdup --symbolic-full-name HEAD 2>/dev/null", "r");
5694
5695 /* XXX: The line outputted by "--show-cdup" can be empty so
5696 * initialize it to something invalid to make it possible to
5697 * detect whether it has been set or not. */
5698 opt_cdup[0] = ' ';
5699
5700 result = read_properties(pipe, "=", read_repo_info);
5701 if (opt_cdup[0] == ' ')
5702 opt_cdup[0] = 0;
5703
5704 return result;
5705 }
5706
5707 static int
5708 read_properties(FILE *pipe, const char *separators,
5709 int (*read_property)(char *, size_t, char *, size_t))
5710 {
5711 char buffer[BUFSIZ];
5712 char *name;
5713 int state = OK;
5714
5715 if (!pipe)
5716 return ERR;
5717
5718 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
5719 char *value;
5720 size_t namelen;
5721 size_t valuelen;
5722
5723 name = chomp_string(name);
5724 namelen = strcspn(name, separators);
5725
5726 if (name[namelen]) {
5727 name[namelen] = 0;
5728 value = chomp_string(name + namelen + 1);
5729 valuelen = strlen(value);
5730
5731 } else {
5732 value = "";
5733 valuelen = 0;
5734 }
5735
5736 state = read_property(name, namelen, value, valuelen);
5737 }
5738
5739 if (state != ERR && ferror(pipe))
5740 state = ERR;
5741
5742 pclose(pipe);
5743
5744 return state;
5745 }
5746
5747
5748 /*
5749 * Main
5750 */
5751
5752 static void __NORETURN
5753 quit(int sig)
5754 {
5755 /* XXX: Restore tty modes and let the OS cleanup the rest! */
5756 if (cursed)
5757 endwin();
5758 exit(0);
5759 }
5760
5761 static void __NORETURN
5762 die(const char *err, ...)
5763 {
5764 va_list args;
5765
5766 endwin();
5767
5768 va_start(args, err);
5769 fputs("tig: ", stderr);
5770 vfprintf(stderr, err, args);
5771 fputs("\n", stderr);
5772 va_end(args);
5773
5774 exit(1);
5775 }
5776
5777 static void
5778 warn(const char *msg, ...)
5779 {
5780 va_list args;
5781
5782 va_start(args, msg);
5783 fputs("tig warning: ", stderr);
5784 vfprintf(stderr, msg, args);
5785 fputs("\n", stderr);
5786 va_end(args);
5787 }
5788
5789 int
5790 main(int argc, char *argv[])
5791 {
5792 struct view *view;
5793 enum request request;
5794 size_t i;
5795
5796 signal(SIGINT, quit);
5797
5798 if (setlocale(LC_ALL, "")) {
5799 char *codeset = nl_langinfo(CODESET);
5800
5801 string_ncopy(opt_codeset, codeset, strlen(codeset));
5802 }
5803
5804 if (load_repo_info() == ERR)
5805 die("Failed to load repo info.");
5806
5807 if (load_options() == ERR)
5808 die("Failed to load user config.");
5809
5810 if (load_git_config() == ERR)
5811 die("Failed to load repo config.");
5812
5813 if (!parse_options(argc, argv))
5814 return 0;
5815
5816 /* Require a git repository unless when running in pager mode. */
5817 if (!opt_git_dir[0] && opt_request != REQ_VIEW_PAGER)
5818 die("Not a git repository");
5819
5820 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
5821 opt_utf8 = FALSE;
5822
5823 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
5824 opt_iconv = iconv_open(opt_codeset, opt_encoding);
5825 if (opt_iconv == ICONV_NONE)
5826 die("Failed to initialize character set conversion");
5827 }
5828
5829 if (*opt_git_dir && load_refs() == ERR)
5830 die("Failed to load refs.");
5831
5832 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
5833 view->cmd_env = getenv(view->cmd_env);
5834
5835 request = opt_request;
5836
5837 init_display();
5838
5839 while (view_driver(display[current_view], request)) {
5840 int key;
5841 int i;
5842
5843 foreach_view (view, i)
5844 update_view(view);
5845
5846 /* Refresh, accept single keystroke of input */
5847 key = wgetch(status_win);
5848
5849 /* wgetch() with nodelay() enabled returns ERR when there's no
5850 * input. */
5851 if (key == ERR) {
5852 request = REQ_NONE;
5853 continue;
5854 }
5855
5856 request = get_keybinding(display[current_view]->keymap, key);
5857
5858 /* Some low-level request handling. This keeps access to
5859 * status_win restricted. */
5860 switch (request) {
5861 case REQ_PROMPT:
5862 {
5863 char *cmd = read_prompt(":");
5864
5865 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
5866 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
5867 opt_request = REQ_VIEW_DIFF;
5868 } else {
5869 opt_request = REQ_VIEW_PAGER;
5870 }
5871 break;
5872 }
5873
5874 request = REQ_NONE;
5875 break;
5876 }
5877 case REQ_SEARCH:
5878 case REQ_SEARCH_BACK:
5879 {
5880 const char *prompt = request == REQ_SEARCH
5881 ? "/" : "?";
5882 char *search = read_prompt(prompt);
5883
5884 if (search)
5885 string_ncopy(opt_search, search, strlen(search));
5886 else
5887 request = REQ_NONE;
5888 break;
5889 }
5890 case REQ_SCREEN_RESIZE:
5891 {
5892 int height, width;
5893
5894 getmaxyx(stdscr, height, width);
5895
5896 /* Resize the status view and let the view driver take
5897 * care of resizing the displayed views. */
5898 wresize(status_win, 1, width);
5899 mvwin(status_win, height - 1, 0);
5900 wrefresh(status_win);
5901 break;
5902 }
5903 default:
5904 break;
5905 }
5906 }
5907
5908 quit(0);
5909
5910 return 0;
5911 }