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