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