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