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