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