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