1 /* Copyright (c) 2006-2008 Jonas Fonseca <fonseca@diku.dk>
2 *
3 * This program is free software; you can redistribute it and/or
4 * modify it under the terms of the GNU General Public License as
5 * published by the Free Software Foundation; either version 2 of
6 * the License, or (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 */
14 #ifdef HAVE_CONFIG_H
15 #include "config.h"
16 #endif
18 #ifndef TIG_VERSION
19 #define TIG_VERSION "unknown-version"
20 #endif
22 #ifndef DEBUG
23 #define NDEBUG
24 #endif
26 #include <assert.h>
27 #include <errno.h>
28 #include <ctype.h>
29 #include <signal.h>
30 #include <stdarg.h>
31 #include <stdio.h>
32 #include <stdlib.h>
33 #include <string.h>
34 #include <sys/types.h>
35 #include <sys/stat.h>
36 #include <unistd.h>
37 #include <time.h>
39 #include <regex.h>
41 #include <locale.h>
42 #include <langinfo.h>
43 #include <iconv.h>
45 /* ncurses(3): Must be defined to have extended wide-character functions. */
46 #define _XOPEN_SOURCE_EXTENDED
48 #ifdef HAVE_NCURSESW_NCURSES_H
49 #include <ncursesw/ncurses.h>
50 #else
51 #ifdef HAVE_NCURSES_NCURSES_H
52 #include <ncurses/ncurses.h>
53 #else
54 #include <ncurses.h>
55 #endif
56 #endif
58 #if __GNUC__ >= 3
59 #define __NORETURN __attribute__((__noreturn__))
60 #else
61 #define __NORETURN
62 #endif
64 static void __NORETURN die(const char *err, ...);
65 static void warn(const char *msg, ...);
66 static void report(const char *msg, ...);
67 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, size_t, char *, size_t));
68 static void set_nonblocking_input(bool loading);
69 static size_t utf8_length(const char *string, int *width, size_t max_width, int *trimmed, bool reserve);
70 static bool prompt_yesno(const char *prompt);
72 #define ABS(x) ((x) >= 0 ? (x) : -(x))
73 #define MIN(x, y) ((x) < (y) ? (x) : (y))
75 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
76 #define STRING_SIZE(x) (sizeof(x) - 1)
78 #define SIZEOF_STR 1024 /* Default string size. */
79 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
80 #define SIZEOF_REV 41 /* Holds a SHA-1 and an ending NUL */
82 /* Revision graph */
84 #define REVGRAPH_INIT 'I'
85 #define REVGRAPH_MERGE 'M'
86 #define REVGRAPH_BRANCH '+'
87 #define REVGRAPH_COMMIT '*'
88 #define REVGRAPH_BOUND '^'
90 #define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
92 /* This color name can be used to refer to the default term colors. */
93 #define COLOR_DEFAULT (-1)
95 #define ICONV_NONE ((iconv_t) -1)
96 #ifndef ICONV_CONST
97 #define ICONV_CONST /* nothing */
98 #endif
100 /* The format and size of the date column in the main view. */
101 #define DATE_FORMAT "%Y-%m-%d %H:%M"
102 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
104 #define AUTHOR_COLS 20
105 #define ID_COLS 8
107 /* The default interval between line numbers. */
108 #define NUMBER_INTERVAL 5
110 #define TAB_SIZE 8
112 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
114 #define NULL_ID "0000000000000000000000000000000000000000"
116 #ifndef GIT_CONFIG
117 #define GIT_CONFIG "config"
118 #endif
120 #define TIG_LS_REMOTE \
121 "git ls-remote . 2>/dev/null"
123 #define TIG_DIFF_CMD \
124 "git show --pretty=fuller --no-color --root --patch-with-stat --find-copies-harder -C %s 2>/dev/null"
126 #define TIG_LOG_CMD \
127 "git log --no-color --cc --stat -n100 %s 2>/dev/null"
129 #define TIG_MAIN_BASE \
130 "git log --no-color --pretty=raw --parents --topo-order"
132 #define TIG_MAIN_CMD \
133 TIG_MAIN_BASE " %s 2>/dev/null"
135 #define TIG_TREE_CMD \
136 "git ls-tree %s %s"
138 #define TIG_BLOB_CMD \
139 "git cat-file blob %s"
141 /* XXX: Needs to be defined to the empty string. */
142 #define TIG_HELP_CMD ""
143 #define TIG_PAGER_CMD ""
144 #define TIG_STATUS_CMD ""
145 #define TIG_STAGE_CMD ""
146 #define TIG_BLAME_CMD ""
148 /* Some ascii-shorthands fitted into the ncurses namespace. */
149 #define KEY_TAB '\t'
150 #define KEY_RETURN '\r'
151 #define KEY_ESC 27
154 struct ref {
155 char *name; /* Ref name; tag or head names are shortened. */
156 char id[SIZEOF_REV]; /* Commit SHA1 ID */
157 unsigned int head:1; /* Is it the current HEAD? */
158 unsigned int tag:1; /* Is it a tag? */
159 unsigned int ltag:1; /* If so, is the tag local? */
160 unsigned int remote:1; /* Is it a remote ref? */
161 unsigned int tracked:1; /* Is it the remote for the current HEAD? */
162 unsigned int next:1; /* For ref lists: are there more refs? */
163 };
165 static struct ref **get_refs(char *id);
167 struct int_map {
168 const char *name;
169 int namelen;
170 int value;
171 };
173 static int
174 set_from_int_map(struct int_map *map, size_t map_size,
175 int *value, const char *name, int namelen)
176 {
178 int i;
180 for (i = 0; i < map_size; i++)
181 if (namelen == map[i].namelen &&
182 !strncasecmp(name, map[i].name, namelen)) {
183 *value = map[i].value;
184 return OK;
185 }
187 return ERR;
188 }
191 /*
192 * String helpers
193 */
195 static inline void
196 string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
197 {
198 if (srclen > dstlen - 1)
199 srclen = dstlen - 1;
201 strncpy(dst, src, srclen);
202 dst[srclen] = 0;
203 }
205 /* Shorthands for safely copying into a fixed buffer. */
207 #define string_copy(dst, src) \
208 string_ncopy_do(dst, sizeof(dst), src, sizeof(src))
210 #define string_ncopy(dst, src, srclen) \
211 string_ncopy_do(dst, sizeof(dst), src, srclen)
213 #define string_copy_rev(dst, src) \
214 string_ncopy_do(dst, SIZEOF_REV, src, SIZEOF_REV - 1)
216 #define string_add(dst, from, src) \
217 string_ncopy_do(dst + (from), sizeof(dst) - (from), src, sizeof(src))
219 static char *
220 chomp_string(char *name)
221 {
222 int namelen;
224 while (isspace(*name))
225 name++;
227 namelen = strlen(name) - 1;
228 while (namelen > 0 && isspace(name[namelen]))
229 name[namelen--] = 0;
231 return name;
232 }
234 static bool
235 string_nformat(char *buf, size_t bufsize, size_t *bufpos, const char *fmt, ...)
236 {
237 va_list args;
238 size_t pos = bufpos ? *bufpos : 0;
240 va_start(args, fmt);
241 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
242 va_end(args);
244 if (bufpos)
245 *bufpos = pos;
247 return pos >= bufsize ? FALSE : TRUE;
248 }
250 #define string_format(buf, fmt, args...) \
251 string_nformat(buf, sizeof(buf), NULL, fmt, args)
253 #define string_format_from(buf, from, fmt, args...) \
254 string_nformat(buf, sizeof(buf), from, fmt, args)
256 static int
257 string_enum_compare(const char *str1, const char *str2, int len)
258 {
259 size_t i;
261 #define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
263 /* Diff-Header == DIFF_HEADER */
264 for (i = 0; i < len; i++) {
265 if (toupper(str1[i]) == toupper(str2[i]))
266 continue;
268 if (string_enum_sep(str1[i]) &&
269 string_enum_sep(str2[i]))
270 continue;
272 return str1[i] - str2[i];
273 }
275 return 0;
276 }
278 /* Shell quoting
279 *
280 * NOTE: The following is a slightly modified copy of the git project's shell
281 * quoting routines found in the quote.c file.
282 *
283 * Help to copy the thing properly quoted for the shell safety. any single
284 * quote is replaced with '\'', any exclamation point is replaced with '\!',
285 * and the whole thing is enclosed in a
286 *
287 * E.g.
288 * original sq_quote result
289 * name ==> name ==> 'name'
290 * a b ==> a b ==> 'a b'
291 * a'b ==> a'\''b ==> 'a'\''b'
292 * a!b ==> a'\!'b ==> 'a'\!'b'
293 */
295 static size_t
296 sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
297 {
298 char c;
300 #define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0)
302 BUFPUT('\'');
303 while ((c = *src++)) {
304 if (c == '\'' || c == '!') {
305 BUFPUT('\'');
306 BUFPUT('\\');
307 BUFPUT(c);
308 BUFPUT('\'');
309 } else {
310 BUFPUT(c);
311 }
312 }
313 BUFPUT('\'');
315 if (bufsize < SIZEOF_STR)
316 buf[bufsize] = 0;
318 return bufsize;
319 }
322 /*
323 * User requests
324 */
326 #define REQ_INFO \
327 /* XXX: Keep the view request first and in sync with views[]. */ \
328 REQ_GROUP("View switching") \
329 REQ_(VIEW_MAIN, "Show main view"), \
330 REQ_(VIEW_DIFF, "Show diff view"), \
331 REQ_(VIEW_LOG, "Show log view"), \
332 REQ_(VIEW_TREE, "Show tree view"), \
333 REQ_(VIEW_BLOB, "Show blob view"), \
334 REQ_(VIEW_BLAME, "Show blame view"), \
335 REQ_(VIEW_HELP, "Show help page"), \
336 REQ_(VIEW_PAGER, "Show pager view"), \
337 REQ_(VIEW_STATUS, "Show status view"), \
338 REQ_(VIEW_STAGE, "Show stage view"), \
339 \
340 REQ_GROUP("View manipulation") \
341 REQ_(ENTER, "Enter current line and scroll"), \
342 REQ_(NEXT, "Move to next"), \
343 REQ_(PREVIOUS, "Move to previous"), \
344 REQ_(VIEW_NEXT, "Move focus to next view"), \
345 REQ_(REFRESH, "Reload and refresh"), \
346 REQ_(MAXIMIZE, "Maximize the current view"), \
347 REQ_(VIEW_CLOSE, "Close the current view"), \
348 REQ_(QUIT, "Close all views and quit"), \
349 \
350 REQ_GROUP("Cursor navigation") \
351 REQ_(MOVE_UP, "Move cursor one line up"), \
352 REQ_(MOVE_DOWN, "Move cursor one line down"), \
353 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
354 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
355 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
356 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
357 \
358 REQ_GROUP("Scrolling") \
359 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
360 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
361 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
362 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
363 \
364 REQ_GROUP("Searching") \
365 REQ_(SEARCH, "Search the view"), \
366 REQ_(SEARCH_BACK, "Search backwards in the view"), \
367 REQ_(FIND_NEXT, "Find next search match"), \
368 REQ_(FIND_PREV, "Find previous search match"), \
369 \
370 REQ_GROUP("Misc") \
371 REQ_(PROMPT, "Bring up the prompt"), \
372 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
373 REQ_(SCREEN_RESIZE, "Resize the screen"), \
374 REQ_(SHOW_VERSION, "Show version information"), \
375 REQ_(STOP_LOADING, "Stop all loading views"), \
376 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
377 REQ_(TOGGLE_DATE, "Toggle date display"), \
378 REQ_(TOGGLE_AUTHOR, "Toggle author display"), \
379 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"), \
380 REQ_(TOGGLE_REFS, "Toggle reference display (tags/branches)"), \
381 REQ_(STATUS_UPDATE, "Update file status"), \
382 REQ_(STATUS_CHECKOUT, "Checkout file"), \
383 REQ_(STATUS_MERGE, "Merge file using external tool"), \
384 REQ_(STAGE_NEXT, "Find next chunk to stage"), \
385 REQ_(TREE_PARENT, "Switch to parent directory in tree view"), \
386 REQ_(EDIT, "Open in editor"), \
387 REQ_(NONE, "Do nothing")
390 /* User action requests. */
391 enum request {
392 #define REQ_GROUP(help)
393 #define REQ_(req, help) REQ_##req
395 /* Offset all requests to avoid conflicts with ncurses getch values. */
396 REQ_OFFSET = KEY_MAX + 1,
397 REQ_INFO
399 #undef REQ_GROUP
400 #undef REQ_
401 };
403 struct request_info {
404 enum request request;
405 char *name;
406 int namelen;
407 char *help;
408 };
410 static struct request_info req_info[] = {
411 #define REQ_GROUP(help) { 0, NULL, 0, (help) },
412 #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
413 REQ_INFO
414 #undef REQ_GROUP
415 #undef REQ_
416 };
418 static enum request
419 get_request(const char *name)
420 {
421 int namelen = strlen(name);
422 int i;
424 for (i = 0; i < ARRAY_SIZE(req_info); i++)
425 if (req_info[i].namelen == namelen &&
426 !string_enum_compare(req_info[i].name, name, namelen))
427 return req_info[i].request;
429 return REQ_NONE;
430 }
433 /*
434 * Options
435 */
437 static const char usage[] =
438 "tig " TIG_VERSION " (" __DATE__ ")\n"
439 "\n"
440 "Usage: tig [options] [revs] [--] [paths]\n"
441 " or: tig show [options] [revs] [--] [paths]\n"
442 " or: tig blame [rev] path\n"
443 " or: tig status\n"
444 " or: tig < [git command output]\n"
445 "\n"
446 "Options:\n"
447 " -v, --version Show version and exit\n"
448 " -h, --help Show help message and exit";
450 /* Option and state variables. */
451 static bool opt_date = TRUE;
452 static bool opt_author = TRUE;
453 static bool opt_line_number = FALSE;
454 static bool opt_line_graphics = TRUE;
455 static bool opt_rev_graph = FALSE;
456 static bool opt_show_refs = TRUE;
457 static int opt_num_interval = NUMBER_INTERVAL;
458 static int opt_tab_size = TAB_SIZE;
459 static int opt_author_cols = AUTHOR_COLS-1;
460 static char opt_cmd[SIZEOF_STR] = "";
461 static char opt_path[SIZEOF_STR] = "";
462 static char opt_file[SIZEOF_STR] = "";
463 static char opt_ref[SIZEOF_REF] = "";
464 static char opt_head[SIZEOF_REF] = "";
465 static char opt_remote[SIZEOF_REF] = "";
466 static bool opt_no_head = TRUE;
467 static FILE *opt_pipe = NULL;
468 static char opt_encoding[20] = "UTF-8";
469 static bool opt_utf8 = TRUE;
470 static char opt_codeset[20] = "UTF-8";
471 static iconv_t opt_iconv = ICONV_NONE;
472 static char opt_search[SIZEOF_STR] = "";
473 static char opt_cdup[SIZEOF_STR] = "";
474 static char opt_git_dir[SIZEOF_STR] = "";
475 static signed char opt_is_inside_work_tree = -1; /* set to TRUE or FALSE */
476 static char opt_editor[SIZEOF_STR] = "";
478 static enum request
479 parse_options(int argc, char *argv[])
480 {
481 enum request request = REQ_VIEW_MAIN;
482 size_t buf_size;
483 char *subcommand;
484 bool seen_dashdash = FALSE;
485 int i;
487 if (!isatty(STDIN_FILENO)) {
488 opt_pipe = stdin;
489 return REQ_VIEW_PAGER;
490 }
492 if (argc <= 1)
493 return REQ_VIEW_MAIN;
495 subcommand = argv[1];
496 if (!strcmp(subcommand, "status") || !strcmp(subcommand, "-S")) {
497 if (!strcmp(subcommand, "-S"))
498 warn("`-S' has been deprecated; use `tig status' instead");
499 if (argc > 2)
500 warn("ignoring arguments after `%s'", subcommand);
501 return REQ_VIEW_STATUS;
503 } else if (!strcmp(subcommand, "blame")) {
504 if (argc <= 2 || argc > 4)
505 die("invalid number of options to blame\n\n%s", usage);
507 i = 2;
508 if (argc == 4) {
509 string_ncopy(opt_ref, argv[i], strlen(argv[i]));
510 i++;
511 }
513 string_ncopy(opt_file, argv[i], strlen(argv[i]));
514 return REQ_VIEW_BLAME;
516 } else if (!strcmp(subcommand, "show")) {
517 request = REQ_VIEW_DIFF;
519 } else if (!strcmp(subcommand, "log") || !strcmp(subcommand, "diff")) {
520 request = subcommand[0] == 'l' ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
521 warn("`tig %s' has been deprecated", subcommand);
523 } else {
524 subcommand = NULL;
525 }
527 if (!subcommand)
528 /* XXX: This is vulnerable to the user overriding
529 * options required for the main view parser. */
530 string_copy(opt_cmd, TIG_MAIN_BASE);
531 else
532 string_format(opt_cmd, "git %s", subcommand);
534 buf_size = strlen(opt_cmd);
536 for (i = 1 + !!subcommand; i < argc; i++) {
537 char *opt = argv[i];
539 if (seen_dashdash || !strcmp(opt, "--")) {
540 seen_dashdash = TRUE;
542 } else if (!strcmp(opt, "-v") || !strcmp(opt, "--version")) {
543 printf("tig version %s\n", TIG_VERSION);
544 return REQ_NONE;
546 } else if (!strcmp(opt, "-h") || !strcmp(opt, "--help")) {
547 printf("%s\n", usage);
548 return REQ_NONE;
549 }
551 opt_cmd[buf_size++] = ' ';
552 buf_size = sq_quote(opt_cmd, buf_size, opt);
553 if (buf_size >= sizeof(opt_cmd))
554 die("command too long");
555 }
557 opt_cmd[buf_size] = 0;
559 return request;
560 }
563 /*
564 * Line-oriented content detection.
565 */
567 #define LINE_INFO \
568 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
569 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
570 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
571 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
572 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
573 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
574 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
575 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
576 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
577 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
578 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
579 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
580 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
581 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
582 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
583 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
584 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
585 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
586 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
587 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
588 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
589 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
590 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
591 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
592 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
593 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
594 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
595 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
596 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
597 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
598 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
599 LINE(DELIMITER, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
600 LINE(DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
601 LINE(LINE_NUMBER, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
602 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
603 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
604 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
605 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
606 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
607 LINE(MAIN_LOCAL_TAG,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
608 LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \
609 LINE(MAIN_TRACKED, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
610 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
611 LINE(MAIN_HEAD, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
612 LINE(MAIN_REVGRAPH,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
613 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
614 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
615 LINE(STAT_HEAD, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \
616 LINE(STAT_SECTION, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
617 LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
618 LINE(STAT_STAGED, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
619 LINE(STAT_UNSTAGED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
620 LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
621 LINE(BLAME_ID, "", COLOR_MAGENTA, COLOR_DEFAULT, 0)
623 enum line_type {
624 #define LINE(type, line, fg, bg, attr) \
625 LINE_##type
626 LINE_INFO,
627 LINE_NONE
628 #undef LINE
629 };
631 struct line_info {
632 const char *name; /* Option name. */
633 int namelen; /* Size of option name. */
634 const char *line; /* The start of line to match. */
635 int linelen; /* Size of string to match. */
636 int fg, bg, attr; /* Color and text attributes for the lines. */
637 };
639 static struct line_info line_info[] = {
640 #define LINE(type, line, fg, bg, attr) \
641 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
642 LINE_INFO
643 #undef LINE
644 };
646 static enum line_type
647 get_line_type(char *line)
648 {
649 int linelen = strlen(line);
650 enum line_type type;
652 for (type = 0; type < ARRAY_SIZE(line_info); type++)
653 /* Case insensitive search matches Signed-off-by lines better. */
654 if (linelen >= line_info[type].linelen &&
655 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
656 return type;
658 return LINE_DEFAULT;
659 }
661 static inline int
662 get_line_attr(enum line_type type)
663 {
664 assert(type < ARRAY_SIZE(line_info));
665 return COLOR_PAIR(type) | line_info[type].attr;
666 }
668 static struct line_info *
669 get_line_info(char *name)
670 {
671 size_t namelen = strlen(name);
672 enum line_type type;
674 for (type = 0; type < ARRAY_SIZE(line_info); type++)
675 if (namelen == line_info[type].namelen &&
676 !string_enum_compare(line_info[type].name, name, namelen))
677 return &line_info[type];
679 return NULL;
680 }
682 static void
683 init_colors(void)
684 {
685 int default_bg = line_info[LINE_DEFAULT].bg;
686 int default_fg = line_info[LINE_DEFAULT].fg;
687 enum line_type type;
689 start_color();
691 if (assume_default_colors(default_fg, default_bg) == ERR) {
692 default_bg = COLOR_BLACK;
693 default_fg = COLOR_WHITE;
694 }
696 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
697 struct line_info *info = &line_info[type];
698 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
699 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
701 init_pair(type, fg, bg);
702 }
703 }
705 struct line {
706 enum line_type type;
708 /* State flags */
709 unsigned int selected:1;
710 unsigned int dirty:1;
712 void *data; /* User data */
713 };
716 /*
717 * Keys
718 */
720 struct keybinding {
721 int alias;
722 enum request request;
723 struct keybinding *next;
724 };
726 static struct keybinding default_keybindings[] = {
727 /* View switching */
728 { 'm', REQ_VIEW_MAIN },
729 { 'd', REQ_VIEW_DIFF },
730 { 'l', REQ_VIEW_LOG },
731 { 't', REQ_VIEW_TREE },
732 { 'f', REQ_VIEW_BLOB },
733 { 'B', REQ_VIEW_BLAME },
734 { 'p', REQ_VIEW_PAGER },
735 { 'h', REQ_VIEW_HELP },
736 { 'S', REQ_VIEW_STATUS },
737 { 'c', REQ_VIEW_STAGE },
739 /* View manipulation */
740 { 'q', REQ_VIEW_CLOSE },
741 { KEY_TAB, REQ_VIEW_NEXT },
742 { KEY_RETURN, REQ_ENTER },
743 { KEY_UP, REQ_PREVIOUS },
744 { KEY_DOWN, REQ_NEXT },
745 { 'R', REQ_REFRESH },
746 { KEY_F(5), REQ_REFRESH },
747 { 'O', REQ_MAXIMIZE },
749 /* Cursor navigation */
750 { 'k', REQ_MOVE_UP },
751 { 'j', REQ_MOVE_DOWN },
752 { KEY_HOME, REQ_MOVE_FIRST_LINE },
753 { KEY_END, REQ_MOVE_LAST_LINE },
754 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
755 { ' ', REQ_MOVE_PAGE_DOWN },
756 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
757 { 'b', REQ_MOVE_PAGE_UP },
758 { '-', REQ_MOVE_PAGE_UP },
760 /* Scrolling */
761 { KEY_IC, REQ_SCROLL_LINE_UP },
762 { KEY_DC, REQ_SCROLL_LINE_DOWN },
763 { 'w', REQ_SCROLL_PAGE_UP },
764 { 's', REQ_SCROLL_PAGE_DOWN },
766 /* Searching */
767 { '/', REQ_SEARCH },
768 { '?', REQ_SEARCH_BACK },
769 { 'n', REQ_FIND_NEXT },
770 { 'N', REQ_FIND_PREV },
772 /* Misc */
773 { 'Q', REQ_QUIT },
774 { 'z', REQ_STOP_LOADING },
775 { 'v', REQ_SHOW_VERSION },
776 { 'r', REQ_SCREEN_REDRAW },
777 { '.', REQ_TOGGLE_LINENO },
778 { 'D', REQ_TOGGLE_DATE },
779 { 'A', REQ_TOGGLE_AUTHOR },
780 { 'g', REQ_TOGGLE_REV_GRAPH },
781 { 'F', REQ_TOGGLE_REFS },
782 { ':', REQ_PROMPT },
783 { 'u', REQ_STATUS_UPDATE },
784 { '!', REQ_STATUS_CHECKOUT },
785 { 'M', REQ_STATUS_MERGE },
786 { '@', REQ_STAGE_NEXT },
787 { ',', REQ_TREE_PARENT },
788 { 'e', REQ_EDIT },
790 /* Using the ncurses SIGWINCH handler. */
791 { KEY_RESIZE, REQ_SCREEN_RESIZE },
792 };
794 #define KEYMAP_INFO \
795 KEYMAP_(GENERIC), \
796 KEYMAP_(MAIN), \
797 KEYMAP_(DIFF), \
798 KEYMAP_(LOG), \
799 KEYMAP_(TREE), \
800 KEYMAP_(BLOB), \
801 KEYMAP_(BLAME), \
802 KEYMAP_(PAGER), \
803 KEYMAP_(HELP), \
804 KEYMAP_(STATUS), \
805 KEYMAP_(STAGE)
807 enum keymap {
808 #define KEYMAP_(name) KEYMAP_##name
809 KEYMAP_INFO
810 #undef KEYMAP_
811 };
813 static struct int_map keymap_table[] = {
814 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
815 KEYMAP_INFO
816 #undef KEYMAP_
817 };
819 #define set_keymap(map, name) \
820 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
822 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
824 static void
825 add_keybinding(enum keymap keymap, enum request request, int key)
826 {
827 struct keybinding *keybinding;
829 keybinding = calloc(1, sizeof(*keybinding));
830 if (!keybinding)
831 die("Failed to allocate keybinding");
833 keybinding->alias = key;
834 keybinding->request = request;
835 keybinding->next = keybindings[keymap];
836 keybindings[keymap] = keybinding;
837 }
839 /* Looks for a key binding first in the given map, then in the generic map, and
840 * lastly in the default keybindings. */
841 static enum request
842 get_keybinding(enum keymap keymap, int key)
843 {
844 struct keybinding *kbd;
845 int i;
847 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
848 if (kbd->alias == key)
849 return kbd->request;
851 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
852 if (kbd->alias == key)
853 return kbd->request;
855 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
856 if (default_keybindings[i].alias == key)
857 return default_keybindings[i].request;
859 return (enum request) key;
860 }
863 struct key {
864 char *name;
865 int value;
866 };
868 static struct key key_table[] = {
869 { "Enter", KEY_RETURN },
870 { "Space", ' ' },
871 { "Backspace", KEY_BACKSPACE },
872 { "Tab", KEY_TAB },
873 { "Escape", KEY_ESC },
874 { "Left", KEY_LEFT },
875 { "Right", KEY_RIGHT },
876 { "Up", KEY_UP },
877 { "Down", KEY_DOWN },
878 { "Insert", KEY_IC },
879 { "Delete", KEY_DC },
880 { "Hash", '#' },
881 { "Home", KEY_HOME },
882 { "End", KEY_END },
883 { "PageUp", KEY_PPAGE },
884 { "PageDown", KEY_NPAGE },
885 { "F1", KEY_F(1) },
886 { "F2", KEY_F(2) },
887 { "F3", KEY_F(3) },
888 { "F4", KEY_F(4) },
889 { "F5", KEY_F(5) },
890 { "F6", KEY_F(6) },
891 { "F7", KEY_F(7) },
892 { "F8", KEY_F(8) },
893 { "F9", KEY_F(9) },
894 { "F10", KEY_F(10) },
895 { "F11", KEY_F(11) },
896 { "F12", KEY_F(12) },
897 };
899 static int
900 get_key_value(const char *name)
901 {
902 int i;
904 for (i = 0; i < ARRAY_SIZE(key_table); i++)
905 if (!strcasecmp(key_table[i].name, name))
906 return key_table[i].value;
908 if (strlen(name) == 1 && isprint(*name))
909 return (int) *name;
911 return ERR;
912 }
914 static char *
915 get_key_name(int key_value)
916 {
917 static char key_char[] = "'X'";
918 char *seq = NULL;
919 int key;
921 for (key = 0; key < ARRAY_SIZE(key_table); key++)
922 if (key_table[key].value == key_value)
923 seq = key_table[key].name;
925 if (seq == NULL &&
926 key_value < 127 &&
927 isprint(key_value)) {
928 key_char[1] = (char) key_value;
929 seq = key_char;
930 }
932 return seq ? seq : "'?'";
933 }
935 static char *
936 get_key(enum request request)
937 {
938 static char buf[BUFSIZ];
939 size_t pos = 0;
940 char *sep = "";
941 int i;
943 buf[pos] = 0;
945 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
946 struct keybinding *keybinding = &default_keybindings[i];
948 if (keybinding->request != request)
949 continue;
951 if (!string_format_from(buf, &pos, "%s%s", sep,
952 get_key_name(keybinding->alias)))
953 return "Too many keybindings!";
954 sep = ", ";
955 }
957 return buf;
958 }
960 struct run_request {
961 enum keymap keymap;
962 int key;
963 char cmd[SIZEOF_STR];
964 };
966 static struct run_request *run_request;
967 static size_t run_requests;
969 static enum request
970 add_run_request(enum keymap keymap, int key, int argc, char **argv)
971 {
972 struct run_request *req;
973 char cmd[SIZEOF_STR];
974 size_t bufpos;
976 for (bufpos = 0; argc > 0; argc--, argv++)
977 if (!string_format_from(cmd, &bufpos, "%s ", *argv))
978 return REQ_NONE;
980 req = realloc(run_request, (run_requests + 1) * sizeof(*run_request));
981 if (!req)
982 return REQ_NONE;
984 run_request = req;
985 req = &run_request[run_requests++];
986 string_copy(req->cmd, cmd);
987 req->keymap = keymap;
988 req->key = key;
990 return REQ_NONE + run_requests;
991 }
993 static struct run_request *
994 get_run_request(enum request request)
995 {
996 if (request <= REQ_NONE)
997 return NULL;
998 return &run_request[request - REQ_NONE - 1];
999 }
1001 static void
1002 add_builtin_run_requests(void)
1003 {
1004 struct {
1005 enum keymap keymap;
1006 int key;
1007 char *argv[1];
1008 } reqs[] = {
1009 { KEYMAP_MAIN, 'C', { "git cherry-pick %(commit)" } },
1010 { KEYMAP_GENERIC, 'G', { "git gc" } },
1011 };
1012 int i;
1014 for (i = 0; i < ARRAY_SIZE(reqs); i++) {
1015 enum request req;
1017 req = add_run_request(reqs[i].keymap, reqs[i].key, 1, reqs[i].argv);
1018 if (req != REQ_NONE)
1019 add_keybinding(reqs[i].keymap, req, reqs[i].key);
1020 }
1021 }
1023 /*
1024 * User config file handling.
1025 */
1027 static struct int_map color_map[] = {
1028 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
1029 COLOR_MAP(DEFAULT),
1030 COLOR_MAP(BLACK),
1031 COLOR_MAP(BLUE),
1032 COLOR_MAP(CYAN),
1033 COLOR_MAP(GREEN),
1034 COLOR_MAP(MAGENTA),
1035 COLOR_MAP(RED),
1036 COLOR_MAP(WHITE),
1037 COLOR_MAP(YELLOW),
1038 };
1040 #define set_color(color, name) \
1041 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
1043 static struct int_map attr_map[] = {
1044 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
1045 ATTR_MAP(NORMAL),
1046 ATTR_MAP(BLINK),
1047 ATTR_MAP(BOLD),
1048 ATTR_MAP(DIM),
1049 ATTR_MAP(REVERSE),
1050 ATTR_MAP(STANDOUT),
1051 ATTR_MAP(UNDERLINE),
1052 };
1054 #define set_attribute(attr, name) \
1055 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
1057 static int config_lineno;
1058 static bool config_errors;
1059 static char *config_msg;
1061 /* Wants: object fgcolor bgcolor [attr] */
1062 static int
1063 option_color_command(int argc, char *argv[])
1064 {
1065 struct line_info *info;
1067 if (argc != 3 && argc != 4) {
1068 config_msg = "Wrong number of arguments given to color command";
1069 return ERR;
1070 }
1072 info = get_line_info(argv[0]);
1073 if (!info) {
1074 if (!string_enum_compare(argv[0], "main-delim", strlen("main-delim"))) {
1075 info = get_line_info("delimiter");
1077 } else if (!string_enum_compare(argv[0], "main-date", strlen("main-date"))) {
1078 info = get_line_info("date");
1080 } else {
1081 config_msg = "Unknown color name";
1082 return ERR;
1083 }
1084 }
1086 if (set_color(&info->fg, argv[1]) == ERR ||
1087 set_color(&info->bg, argv[2]) == ERR) {
1088 config_msg = "Unknown color";
1089 return ERR;
1090 }
1092 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
1093 config_msg = "Unknown attribute";
1094 return ERR;
1095 }
1097 return OK;
1098 }
1100 static bool parse_bool(const char *s)
1101 {
1102 return (!strcmp(s, "1") || !strcmp(s, "true") ||
1103 !strcmp(s, "yes")) ? TRUE : FALSE;
1104 }
1106 static int
1107 parse_int(const char *s, int default_value, int min, int max)
1108 {
1109 int value = atoi(s);
1111 return (value < min || value > max) ? default_value : value;
1112 }
1114 /* Wants: name = value */
1115 static int
1116 option_set_command(int argc, char *argv[])
1117 {
1118 if (argc != 3) {
1119 config_msg = "Wrong number of arguments given to set command";
1120 return ERR;
1121 }
1123 if (strcmp(argv[1], "=")) {
1124 config_msg = "No value assigned";
1125 return ERR;
1126 }
1128 if (!strcmp(argv[0], "show-author")) {
1129 opt_author = parse_bool(argv[2]);
1130 return OK;
1131 }
1133 if (!strcmp(argv[0], "show-date")) {
1134 opt_date = parse_bool(argv[2]);
1135 return OK;
1136 }
1138 if (!strcmp(argv[0], "show-rev-graph")) {
1139 opt_rev_graph = parse_bool(argv[2]);
1140 return OK;
1141 }
1143 if (!strcmp(argv[0], "show-refs")) {
1144 opt_show_refs = parse_bool(argv[2]);
1145 return OK;
1146 }
1148 if (!strcmp(argv[0], "show-line-numbers")) {
1149 opt_line_number = parse_bool(argv[2]);
1150 return OK;
1151 }
1153 if (!strcmp(argv[0], "line-graphics")) {
1154 opt_line_graphics = parse_bool(argv[2]);
1155 return OK;
1156 }
1158 if (!strcmp(argv[0], "line-number-interval")) {
1159 opt_num_interval = parse_int(argv[2], opt_num_interval, 1, 1024);
1160 return OK;
1161 }
1163 if (!strcmp(argv[0], "author-width")) {
1164 opt_author_cols = parse_int(argv[2], opt_author_cols, 0, 1024);
1165 return OK;
1166 }
1168 if (!strcmp(argv[0], "tab-size")) {
1169 opt_tab_size = parse_int(argv[2], opt_tab_size, 1, 1024);
1170 return OK;
1171 }
1173 if (!strcmp(argv[0], "commit-encoding")) {
1174 const char *arg = argv[2];
1175 int arglen = strlen(arg);
1177 switch (arg[0]) {
1178 case '"':
1179 case '\'':
1180 if (arglen == 1 || arg[arglen - 1] != arg[0]) {
1181 config_msg = "Unmatched quotation";
1182 return ERR;
1183 }
1184 arg += 1; arglen -= 2;
1185 default:
1186 string_ncopy(opt_encoding, arg, strlen(arg));
1187 return OK;
1188 }
1189 }
1191 config_msg = "Unknown variable name";
1192 return ERR;
1193 }
1195 /* Wants: mode request key */
1196 static int
1197 option_bind_command(int argc, char *argv[])
1198 {
1199 enum request request;
1200 int keymap;
1201 int key;
1203 if (argc < 3) {
1204 config_msg = "Wrong number of arguments given to bind command";
1205 return ERR;
1206 }
1208 if (set_keymap(&keymap, argv[0]) == ERR) {
1209 config_msg = "Unknown key map";
1210 return ERR;
1211 }
1213 key = get_key_value(argv[1]);
1214 if (key == ERR) {
1215 config_msg = "Unknown key";
1216 return ERR;
1217 }
1219 request = get_request(argv[2]);
1220 if (request == REQ_NONE) {
1221 const char *obsolete[] = { "cherry-pick" };
1222 size_t namelen = strlen(argv[2]);
1223 int i;
1225 for (i = 0; i < ARRAY_SIZE(obsolete); i++) {
1226 if (namelen == strlen(obsolete[i]) &&
1227 !string_enum_compare(obsolete[i], argv[2], namelen)) {
1228 config_msg = "Obsolete request name";
1229 return ERR;
1230 }
1231 }
1232 }
1233 if (request == REQ_NONE && *argv[2]++ == '!')
1234 request = add_run_request(keymap, key, argc - 2, argv + 2);
1235 if (request == REQ_NONE) {
1236 config_msg = "Unknown request name";
1237 return ERR;
1238 }
1240 add_keybinding(keymap, request, key);
1242 return OK;
1243 }
1245 static int
1246 set_option(char *opt, char *value)
1247 {
1248 char *argv[16];
1249 int valuelen;
1250 int argc = 0;
1252 /* Tokenize */
1253 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1254 argv[argc++] = value;
1255 value += valuelen;
1257 /* Nothing more to tokenize or last available token. */
1258 if (!*value || argc >= ARRAY_SIZE(argv))
1259 break;
1261 *value++ = 0;
1262 while (isspace(*value))
1263 value++;
1264 }
1266 if (!strcmp(opt, "color"))
1267 return option_color_command(argc, argv);
1269 if (!strcmp(opt, "set"))
1270 return option_set_command(argc, argv);
1272 if (!strcmp(opt, "bind"))
1273 return option_bind_command(argc, argv);
1275 config_msg = "Unknown option command";
1276 return ERR;
1277 }
1279 static int
1280 read_option(char *opt, size_t optlen, char *value, size_t valuelen)
1281 {
1282 int status = OK;
1284 config_lineno++;
1285 config_msg = "Internal error";
1287 /* Check for comment markers, since read_properties() will
1288 * only ensure opt and value are split at first " \t". */
1289 optlen = strcspn(opt, "#");
1290 if (optlen == 0)
1291 return OK;
1293 if (opt[optlen] != 0) {
1294 config_msg = "No option value";
1295 status = ERR;
1297 } else {
1298 /* Look for comment endings in the value. */
1299 size_t len = strcspn(value, "#");
1301 if (len < valuelen) {
1302 valuelen = len;
1303 value[valuelen] = 0;
1304 }
1306 status = set_option(opt, value);
1307 }
1309 if (status == ERR) {
1310 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1311 config_lineno, (int) optlen, opt, config_msg);
1312 config_errors = TRUE;
1313 }
1315 /* Always keep going if errors are encountered. */
1316 return OK;
1317 }
1319 static void
1320 load_option_file(const char *path)
1321 {
1322 FILE *file;
1324 /* It's ok that the file doesn't exist. */
1325 file = fopen(path, "r");
1326 if (!file)
1327 return;
1329 config_lineno = 0;
1330 config_errors = FALSE;
1332 if (read_properties(file, " \t", read_option) == ERR ||
1333 config_errors == TRUE)
1334 fprintf(stderr, "Errors while loading %s.\n", path);
1335 }
1337 static int
1338 load_options(void)
1339 {
1340 char *home = getenv("HOME");
1341 char *tigrc_user = getenv("TIGRC_USER");
1342 char *tigrc_system = getenv("TIGRC_SYSTEM");
1343 char buf[SIZEOF_STR];
1345 add_builtin_run_requests();
1347 if (!tigrc_system) {
1348 if (!string_format(buf, "%s/tigrc", SYSCONFDIR))
1349 return ERR;
1350 tigrc_system = buf;
1351 }
1352 load_option_file(tigrc_system);
1354 if (!tigrc_user) {
1355 if (!home || !string_format(buf, "%s/.tigrc", home))
1356 return ERR;
1357 tigrc_user = buf;
1358 }
1359 load_option_file(tigrc_user);
1361 return OK;
1362 }
1365 /*
1366 * The viewer
1367 */
1369 struct view;
1370 struct view_ops;
1372 /* The display array of active views and the index of the current view. */
1373 static struct view *display[2];
1374 static unsigned int current_view;
1376 /* Reading from the prompt? */
1377 static bool input_mode = FALSE;
1379 #define foreach_displayed_view(view, i) \
1380 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1382 #define displayed_views() (display[1] != NULL ? 2 : 1)
1384 /* Current head and commit ID */
1385 static char ref_blob[SIZEOF_REF] = "";
1386 static char ref_commit[SIZEOF_REF] = "HEAD";
1387 static char ref_head[SIZEOF_REF] = "HEAD";
1389 struct view {
1390 const char *name; /* View name */
1391 const char *cmd_fmt; /* Default command line format */
1392 const char *cmd_env; /* Command line set via environment */
1393 const char *id; /* Points to either of ref_{head,commit,blob} */
1395 struct view_ops *ops; /* View operations */
1397 enum keymap keymap; /* What keymap does this view have */
1398 bool git_dir; /* Whether the view requires a git directory. */
1400 char cmd[SIZEOF_STR]; /* Command buffer */
1401 char ref[SIZEOF_REF]; /* Hovered commit reference */
1402 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1404 int height, width; /* The width and height of the main window */
1405 WINDOW *win; /* The main window */
1406 WINDOW *title; /* The title window living below the main window */
1408 /* Navigation */
1409 unsigned long offset; /* Offset of the window top */
1410 unsigned long lineno; /* Current line number */
1412 /* Searching */
1413 char grep[SIZEOF_STR]; /* Search string */
1414 regex_t *regex; /* Pre-compiled regex */
1416 /* If non-NULL, points to the view that opened this view. If this view
1417 * is closed tig will switch back to the parent view. */
1418 struct view *parent;
1420 /* Buffering */
1421 size_t lines; /* Total number of lines */
1422 struct line *line; /* Line index */
1423 size_t line_alloc; /* Total number of allocated lines */
1424 size_t line_size; /* Total number of used lines */
1425 unsigned int digits; /* Number of digits in the lines member. */
1427 /* Drawing */
1428 struct line *curline; /* Line currently being drawn. */
1429 enum line_type curtype; /* Attribute currently used for drawing. */
1430 unsigned long col; /* Column when drawing. */
1432 /* Loading */
1433 FILE *pipe;
1434 time_t start_time;
1435 };
1437 struct view_ops {
1438 /* What type of content being displayed. Used in the title bar. */
1439 const char *type;
1440 /* Open and reads in all view content. */
1441 bool (*open)(struct view *view);
1442 /* Read one line; updates view->line. */
1443 bool (*read)(struct view *view, char *data);
1444 /* Draw one line; @lineno must be < view->height. */
1445 bool (*draw)(struct view *view, struct line *line, unsigned int lineno);
1446 /* Depending on view handle a special requests. */
1447 enum request (*request)(struct view *view, enum request request, struct line *line);
1448 /* Search for regex in a line. */
1449 bool (*grep)(struct view *view, struct line *line);
1450 /* Select line */
1451 void (*select)(struct view *view, struct line *line);
1452 };
1454 static struct view_ops pager_ops;
1455 static struct view_ops main_ops;
1456 static struct view_ops tree_ops;
1457 static struct view_ops blob_ops;
1458 static struct view_ops blame_ops;
1459 static struct view_ops help_ops;
1460 static struct view_ops status_ops;
1461 static struct view_ops stage_ops;
1463 #define VIEW_STR(name, cmd, env, ref, ops, map, git) \
1464 { name, cmd, #env, ref, ops, map, git }
1466 #define VIEW_(id, name, ops, git, ref) \
1467 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id, git)
1470 static struct view views[] = {
1471 VIEW_(MAIN, "main", &main_ops, TRUE, ref_head),
1472 VIEW_(DIFF, "diff", &pager_ops, TRUE, ref_commit),
1473 VIEW_(LOG, "log", &pager_ops, TRUE, ref_head),
1474 VIEW_(TREE, "tree", &tree_ops, TRUE, ref_commit),
1475 VIEW_(BLOB, "blob", &blob_ops, TRUE, ref_blob),
1476 VIEW_(BLAME, "blame", &blame_ops, TRUE, ref_commit),
1477 VIEW_(HELP, "help", &help_ops, FALSE, ""),
1478 VIEW_(PAGER, "pager", &pager_ops, FALSE, "stdin"),
1479 VIEW_(STATUS, "status", &status_ops, TRUE, ""),
1480 VIEW_(STAGE, "stage", &stage_ops, TRUE, ""),
1481 };
1483 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1484 #define VIEW_REQ(view) ((view) - views + REQ_OFFSET + 1)
1486 #define foreach_view(view, i) \
1487 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1489 #define view_is_displayed(view) \
1490 (view == display[0] || view == display[1])
1493 enum line_graphic {
1494 LINE_GRAPHIC_VLINE
1495 };
1497 static int line_graphics[] = {
1498 /* LINE_GRAPHIC_VLINE: */ '|'
1499 };
1501 static inline void
1502 set_view_attr(struct view *view, enum line_type type)
1503 {
1504 if (!view->curline->selected && view->curtype != type) {
1505 wattrset(view->win, get_line_attr(type));
1506 wchgat(view->win, -1, 0, type, NULL);
1507 view->curtype = type;
1508 }
1509 }
1511 static int
1512 draw_chars(struct view *view, enum line_type type, const char *string,
1513 int max_len, bool use_tilde)
1514 {
1515 int len = 0;
1516 int col = 0;
1517 int trimmed = FALSE;
1519 if (max_len <= 0)
1520 return 0;
1522 if (opt_utf8) {
1523 len = utf8_length(string, &col, max_len, &trimmed, use_tilde);
1524 } else {
1525 col = len = strlen(string);
1526 if (len > max_len) {
1527 if (use_tilde) {
1528 max_len -= 1;
1529 }
1530 col = len = max_len;
1531 trimmed = TRUE;
1532 }
1533 }
1535 set_view_attr(view, type);
1536 waddnstr(view->win, string, len);
1537 if (trimmed && use_tilde) {
1538 set_view_attr(view, LINE_DELIMITER);
1539 waddch(view->win, '~');
1540 col++;
1541 }
1543 return col;
1544 }
1546 static int
1547 draw_space(struct view *view, enum line_type type, int max, int spaces)
1548 {
1549 static char space[] = " ";
1550 int col = 0;
1552 spaces = MIN(max, spaces);
1554 while (spaces > 0) {
1555 int len = MIN(spaces, sizeof(space) - 1);
1557 col += draw_chars(view, type, space, spaces, FALSE);
1558 spaces -= len;
1559 }
1561 return col;
1562 }
1564 static bool
1565 draw_lineno(struct view *view, unsigned int lineno)
1566 {
1567 char number[10];
1568 int digits3 = view->digits < 3 ? 3 : view->digits;
1569 int max_number = MIN(digits3, STRING_SIZE(number));
1570 int max = view->width - view->col;
1571 int col;
1573 if (max < max_number)
1574 max_number = max;
1576 lineno += view->offset + 1;
1577 if (lineno == 1 || (lineno % opt_num_interval) == 0) {
1578 static char fmt[] = "%1ld";
1580 if (view->digits <= 9)
1581 fmt[1] = '0' + digits3;
1583 if (!string_format(number, fmt, lineno))
1584 number[0] = 0;
1585 col = draw_chars(view, LINE_LINE_NUMBER, number, max_number, TRUE);
1586 } else {
1587 col = draw_space(view, LINE_LINE_NUMBER, max_number, max_number);
1588 }
1590 if (col < max) {
1591 set_view_attr(view, LINE_DEFAULT);
1592 waddch(view->win, line_graphics[LINE_GRAPHIC_VLINE]);
1593 col++;
1594 }
1596 if (col < max)
1597 col += draw_space(view, LINE_DEFAULT, max - col, 1);
1598 view->col += col;
1600 return view->width - view->col <= 0;
1601 }
1603 static bool
1604 draw_text(struct view *view, enum line_type type, const char *string, bool trim)
1605 {
1606 view->col += draw_chars(view, type, string, view->width - view->col, trim);
1607 return view->width - view->col <= 0;
1608 }
1610 static bool
1611 draw_graphic(struct view *view, enum line_type type, chtype graphic[], size_t size)
1612 {
1613 int max = view->width - view->col;
1614 int i;
1616 if (max < size)
1617 size = max;
1619 set_view_attr(view, type);
1620 /* Using waddch() instead of waddnstr() ensures that
1621 * they'll be rendered correctly for the cursor line. */
1622 for (i = 0; i < size; i++)
1623 waddch(view->win, graphic[i]);
1625 view->col += size;
1626 if (size < max) {
1627 waddch(view->win, ' ');
1628 view->col++;
1629 }
1631 return view->width - view->col <= 0;
1632 }
1634 static bool
1635 draw_field(struct view *view, enum line_type type, char *text, int len, bool trim)
1636 {
1637 int max = MIN(view->width - view->col, len);
1638 int col;
1640 if (text)
1641 col = draw_chars(view, type, text, max - 1, trim);
1642 else
1643 col = draw_space(view, type, max - 1, max - 1);
1645 view->col += col + draw_space(view, LINE_DEFAULT, max - col, max - col);
1646 return view->width - view->col <= 0;
1647 }
1649 static bool
1650 draw_date(struct view *view, struct tm *time)
1651 {
1652 char buf[DATE_COLS];
1653 char *date;
1654 int timelen = 0;
1656 if (time)
1657 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, time);
1658 date = timelen ? buf : NULL;
1660 return draw_field(view, LINE_DATE, date, DATE_COLS, FALSE);
1661 }
1663 static bool
1664 draw_view_line(struct view *view, unsigned int lineno)
1665 {
1666 struct line *line;
1667 bool selected = (view->offset + lineno == view->lineno);
1668 bool draw_ok;
1670 assert(view_is_displayed(view));
1672 if (view->offset + lineno >= view->lines)
1673 return FALSE;
1675 line = &view->line[view->offset + lineno];
1677 wmove(view->win, lineno, 0);
1678 view->col = 0;
1679 view->curline = line;
1680 view->curtype = LINE_NONE;
1681 line->selected = FALSE;
1683 if (selected) {
1684 set_view_attr(view, LINE_CURSOR);
1685 line->selected = TRUE;
1686 view->ops->select(view, line);
1687 } else if (line->selected) {
1688 wclrtoeol(view->win);
1689 }
1691 scrollok(view->win, FALSE);
1692 draw_ok = view->ops->draw(view, line, lineno);
1693 scrollok(view->win, TRUE);
1695 return draw_ok;
1696 }
1698 static void
1699 redraw_view_dirty(struct view *view)
1700 {
1701 bool dirty = FALSE;
1702 int lineno;
1704 for (lineno = 0; lineno < view->height; lineno++) {
1705 struct line *line = &view->line[view->offset + lineno];
1707 if (!line->dirty)
1708 continue;
1709 line->dirty = 0;
1710 dirty = TRUE;
1711 if (!draw_view_line(view, lineno))
1712 break;
1713 }
1715 if (!dirty)
1716 return;
1717 redrawwin(view->win);
1718 if (input_mode)
1719 wnoutrefresh(view->win);
1720 else
1721 wrefresh(view->win);
1722 }
1724 static void
1725 redraw_view_from(struct view *view, int lineno)
1726 {
1727 assert(0 <= lineno && lineno < view->height);
1729 for (; lineno < view->height; lineno++) {
1730 if (!draw_view_line(view, lineno))
1731 break;
1732 }
1734 redrawwin(view->win);
1735 if (input_mode)
1736 wnoutrefresh(view->win);
1737 else
1738 wrefresh(view->win);
1739 }
1741 static void
1742 redraw_view(struct view *view)
1743 {
1744 wclear(view->win);
1745 redraw_view_from(view, 0);
1746 }
1749 static void
1750 update_view_title(struct view *view)
1751 {
1752 char buf[SIZEOF_STR];
1753 char state[SIZEOF_STR];
1754 size_t bufpos = 0, statelen = 0;
1756 assert(view_is_displayed(view));
1758 if (view != VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) {
1759 unsigned int view_lines = view->offset + view->height;
1760 unsigned int lines = view->lines
1761 ? MIN(view_lines, view->lines) * 100 / view->lines
1762 : 0;
1764 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1765 view->ops->type,
1766 view->lineno + 1,
1767 view->lines,
1768 lines);
1770 if (view->pipe) {
1771 time_t secs = time(NULL) - view->start_time;
1773 /* Three git seconds are a long time ... */
1774 if (secs > 2)
1775 string_format_from(state, &statelen, " %lds", secs);
1776 }
1777 }
1779 string_format_from(buf, &bufpos, "[%s]", view->name);
1780 if (*view->ref && bufpos < view->width) {
1781 size_t refsize = strlen(view->ref);
1782 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1784 if (minsize < view->width)
1785 refsize = view->width - minsize + 7;
1786 string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
1787 }
1789 if (statelen && bufpos < view->width) {
1790 string_format_from(buf, &bufpos, " %s", state);
1791 }
1793 if (view == display[current_view])
1794 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1795 else
1796 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1798 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1799 wclrtoeol(view->title);
1800 wmove(view->title, 0, view->width - 1);
1802 if (input_mode)
1803 wnoutrefresh(view->title);
1804 else
1805 wrefresh(view->title);
1806 }
1808 static void
1809 resize_display(void)
1810 {
1811 int offset, i;
1812 struct view *base = display[0];
1813 struct view *view = display[1] ? display[1] : display[0];
1815 /* Setup window dimensions */
1817 getmaxyx(stdscr, base->height, base->width);
1819 /* Make room for the status window. */
1820 base->height -= 1;
1822 if (view != base) {
1823 /* Horizontal split. */
1824 view->width = base->width;
1825 view->height = SCALE_SPLIT_VIEW(base->height);
1826 base->height -= view->height;
1828 /* Make room for the title bar. */
1829 view->height -= 1;
1830 }
1832 /* Make room for the title bar. */
1833 base->height -= 1;
1835 offset = 0;
1837 foreach_displayed_view (view, i) {
1838 if (!view->win) {
1839 view->win = newwin(view->height, 0, offset, 0);
1840 if (!view->win)
1841 die("Failed to create %s view", view->name);
1843 scrollok(view->win, TRUE);
1845 view->title = newwin(1, 0, offset + view->height, 0);
1846 if (!view->title)
1847 die("Failed to create title window");
1849 } else {
1850 wresize(view->win, view->height, view->width);
1851 mvwin(view->win, offset, 0);
1852 mvwin(view->title, offset + view->height, 0);
1853 }
1855 offset += view->height + 1;
1856 }
1857 }
1859 static void
1860 redraw_display(void)
1861 {
1862 struct view *view;
1863 int i;
1865 foreach_displayed_view (view, i) {
1866 redraw_view(view);
1867 update_view_title(view);
1868 }
1869 }
1871 static void
1872 update_display_cursor(struct view *view)
1873 {
1874 /* Move the cursor to the right-most column of the cursor line.
1875 *
1876 * XXX: This could turn out to be a bit expensive, but it ensures that
1877 * the cursor does not jump around. */
1878 if (view->lines) {
1879 wmove(view->win, view->lineno - view->offset, view->width - 1);
1880 wrefresh(view->win);
1881 }
1882 }
1884 /*
1885 * Navigation
1886 */
1888 /* Scrolling backend */
1889 static void
1890 do_scroll_view(struct view *view, int lines)
1891 {
1892 bool redraw_current_line = FALSE;
1894 /* The rendering expects the new offset. */
1895 view->offset += lines;
1897 assert(0 <= view->offset && view->offset < view->lines);
1898 assert(lines);
1900 /* Move current line into the view. */
1901 if (view->lineno < view->offset) {
1902 view->lineno = view->offset;
1903 redraw_current_line = TRUE;
1904 } else if (view->lineno >= view->offset + view->height) {
1905 view->lineno = view->offset + view->height - 1;
1906 redraw_current_line = TRUE;
1907 }
1909 assert(view->offset <= view->lineno && view->lineno < view->lines);
1911 /* Redraw the whole screen if scrolling is pointless. */
1912 if (view->height < ABS(lines)) {
1913 redraw_view(view);
1915 } else {
1916 int line = lines > 0 ? view->height - lines : 0;
1917 int end = line + ABS(lines);
1919 wscrl(view->win, lines);
1921 for (; line < end; line++) {
1922 if (!draw_view_line(view, line))
1923 break;
1924 }
1926 if (redraw_current_line)
1927 draw_view_line(view, view->lineno - view->offset);
1928 }
1930 redrawwin(view->win);
1931 wrefresh(view->win);
1932 report("");
1933 }
1935 /* Scroll frontend */
1936 static void
1937 scroll_view(struct view *view, enum request request)
1938 {
1939 int lines = 1;
1941 assert(view_is_displayed(view));
1943 switch (request) {
1944 case REQ_SCROLL_PAGE_DOWN:
1945 lines = view->height;
1946 case REQ_SCROLL_LINE_DOWN:
1947 if (view->offset + lines > view->lines)
1948 lines = view->lines - view->offset;
1950 if (lines == 0 || view->offset + view->height >= view->lines) {
1951 report("Cannot scroll beyond the last line");
1952 return;
1953 }
1954 break;
1956 case REQ_SCROLL_PAGE_UP:
1957 lines = view->height;
1958 case REQ_SCROLL_LINE_UP:
1959 if (lines > view->offset)
1960 lines = view->offset;
1962 if (lines == 0) {
1963 report("Cannot scroll beyond the first line");
1964 return;
1965 }
1967 lines = -lines;
1968 break;
1970 default:
1971 die("request %d not handled in switch", request);
1972 }
1974 do_scroll_view(view, lines);
1975 }
1977 /* Cursor moving */
1978 static void
1979 move_view(struct view *view, enum request request)
1980 {
1981 int scroll_steps = 0;
1982 int steps;
1984 switch (request) {
1985 case REQ_MOVE_FIRST_LINE:
1986 steps = -view->lineno;
1987 break;
1989 case REQ_MOVE_LAST_LINE:
1990 steps = view->lines - view->lineno - 1;
1991 break;
1993 case REQ_MOVE_PAGE_UP:
1994 steps = view->height > view->lineno
1995 ? -view->lineno : -view->height;
1996 break;
1998 case REQ_MOVE_PAGE_DOWN:
1999 steps = view->lineno + view->height >= view->lines
2000 ? view->lines - view->lineno - 1 : view->height;
2001 break;
2003 case REQ_MOVE_UP:
2004 steps = -1;
2005 break;
2007 case REQ_MOVE_DOWN:
2008 steps = 1;
2009 break;
2011 default:
2012 die("request %d not handled in switch", request);
2013 }
2015 if (steps <= 0 && view->lineno == 0) {
2016 report("Cannot move beyond the first line");
2017 return;
2019 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
2020 report("Cannot move beyond the last line");
2021 return;
2022 }
2024 /* Move the current line */
2025 view->lineno += steps;
2026 assert(0 <= view->lineno && view->lineno < view->lines);
2028 /* Check whether the view needs to be scrolled */
2029 if (view->lineno < view->offset ||
2030 view->lineno >= view->offset + view->height) {
2031 scroll_steps = steps;
2032 if (steps < 0 && -steps > view->offset) {
2033 scroll_steps = -view->offset;
2035 } else if (steps > 0) {
2036 if (view->lineno == view->lines - 1 &&
2037 view->lines > view->height) {
2038 scroll_steps = view->lines - view->offset - 1;
2039 if (scroll_steps >= view->height)
2040 scroll_steps -= view->height - 1;
2041 }
2042 }
2043 }
2045 if (!view_is_displayed(view)) {
2046 view->offset += scroll_steps;
2047 assert(0 <= view->offset && view->offset < view->lines);
2048 view->ops->select(view, &view->line[view->lineno]);
2049 return;
2050 }
2052 /* Repaint the old "current" line if we be scrolling */
2053 if (ABS(steps) < view->height)
2054 draw_view_line(view, view->lineno - steps - view->offset);
2056 if (scroll_steps) {
2057 do_scroll_view(view, scroll_steps);
2058 return;
2059 }
2061 /* Draw the current line */
2062 draw_view_line(view, view->lineno - view->offset);
2064 redrawwin(view->win);
2065 wrefresh(view->win);
2066 report("");
2067 }
2070 /*
2071 * Searching
2072 */
2074 static void search_view(struct view *view, enum request request);
2076 static bool
2077 find_next_line(struct view *view, unsigned long lineno, struct line *line)
2078 {
2079 assert(view_is_displayed(view));
2081 if (!view->ops->grep(view, line))
2082 return FALSE;
2084 if (lineno - view->offset >= view->height) {
2085 view->offset = lineno;
2086 view->lineno = lineno;
2087 redraw_view(view);
2089 } else {
2090 unsigned long old_lineno = view->lineno - view->offset;
2092 view->lineno = lineno;
2093 draw_view_line(view, old_lineno);
2095 draw_view_line(view, view->lineno - view->offset);
2096 redrawwin(view->win);
2097 wrefresh(view->win);
2098 }
2100 report("Line %ld matches '%s'", lineno + 1, view->grep);
2101 return TRUE;
2102 }
2104 static void
2105 find_next(struct view *view, enum request request)
2106 {
2107 unsigned long lineno = view->lineno;
2108 int direction;
2110 if (!*view->grep) {
2111 if (!*opt_search)
2112 report("No previous search");
2113 else
2114 search_view(view, request);
2115 return;
2116 }
2118 switch (request) {
2119 case REQ_SEARCH:
2120 case REQ_FIND_NEXT:
2121 direction = 1;
2122 break;
2124 case REQ_SEARCH_BACK:
2125 case REQ_FIND_PREV:
2126 direction = -1;
2127 break;
2129 default:
2130 return;
2131 }
2133 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
2134 lineno += direction;
2136 /* Note, lineno is unsigned long so will wrap around in which case it
2137 * will become bigger than view->lines. */
2138 for (; lineno < view->lines; lineno += direction) {
2139 struct line *line = &view->line[lineno];
2141 if (find_next_line(view, lineno, line))
2142 return;
2143 }
2145 report("No match found for '%s'", view->grep);
2146 }
2148 static void
2149 search_view(struct view *view, enum request request)
2150 {
2151 int regex_err;
2153 if (view->regex) {
2154 regfree(view->regex);
2155 *view->grep = 0;
2156 } else {
2157 view->regex = calloc(1, sizeof(*view->regex));
2158 if (!view->regex)
2159 return;
2160 }
2162 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
2163 if (regex_err != 0) {
2164 char buf[SIZEOF_STR] = "unknown error";
2166 regerror(regex_err, view->regex, buf, sizeof(buf));
2167 report("Search failed: %s", buf);
2168 return;
2169 }
2171 string_copy(view->grep, opt_search);
2173 find_next(view, request);
2174 }
2176 /*
2177 * Incremental updating
2178 */
2180 static void
2181 end_update(struct view *view, bool force)
2182 {
2183 if (!view->pipe)
2184 return;
2185 while (!view->ops->read(view, NULL))
2186 if (!force)
2187 return;
2188 set_nonblocking_input(FALSE);
2189 if (view->pipe == stdin)
2190 fclose(view->pipe);
2191 else
2192 pclose(view->pipe);
2193 view->pipe = NULL;
2194 }
2196 static bool
2197 begin_update(struct view *view)
2198 {
2199 if (opt_cmd[0]) {
2200 string_copy(view->cmd, opt_cmd);
2201 opt_cmd[0] = 0;
2202 /* When running random commands, initially show the
2203 * command in the title. However, it maybe later be
2204 * overwritten if a commit line is selected. */
2205 if (view == VIEW(REQ_VIEW_PAGER))
2206 string_copy(view->ref, view->cmd);
2207 else
2208 view->ref[0] = 0;
2210 } else if (view == VIEW(REQ_VIEW_TREE)) {
2211 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2212 char path[SIZEOF_STR];
2214 if (strcmp(view->vid, view->id))
2215 opt_path[0] = path[0] = 0;
2216 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
2217 return FALSE;
2219 if (!string_format(view->cmd, format, view->id, path))
2220 return FALSE;
2222 } else {
2223 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2224 const char *id = view->id;
2226 if (!string_format(view->cmd, format, id, id, id, id, id))
2227 return FALSE;
2229 /* Put the current ref_* value to the view title ref
2230 * member. This is needed by the blob view. Most other
2231 * views sets it automatically after loading because the
2232 * first line is a commit line. */
2233 string_copy_rev(view->ref, view->id);
2234 }
2236 /* Special case for the pager view. */
2237 if (opt_pipe) {
2238 view->pipe = opt_pipe;
2239 opt_pipe = NULL;
2240 } else {
2241 view->pipe = popen(view->cmd, "r");
2242 }
2244 if (!view->pipe)
2245 return FALSE;
2247 set_nonblocking_input(TRUE);
2249 view->offset = 0;
2250 view->lines = 0;
2251 view->lineno = 0;
2252 string_copy_rev(view->vid, view->id);
2254 if (view->line) {
2255 int i;
2257 for (i = 0; i < view->lines; i++)
2258 if (view->line[i].data)
2259 free(view->line[i].data);
2261 free(view->line);
2262 view->line = NULL;
2263 }
2265 view->start_time = time(NULL);
2267 return TRUE;
2268 }
2270 #define ITEM_CHUNK_SIZE 256
2271 static void *
2272 realloc_items(void *mem, size_t *size, size_t new_size, size_t item_size)
2273 {
2274 size_t num_chunks = *size / ITEM_CHUNK_SIZE;
2275 size_t num_chunks_new = (new_size + ITEM_CHUNK_SIZE - 1) / ITEM_CHUNK_SIZE;
2277 if (mem == NULL || num_chunks != num_chunks_new) {
2278 *size = num_chunks_new * ITEM_CHUNK_SIZE;
2279 mem = realloc(mem, *size * item_size);
2280 }
2282 return mem;
2283 }
2285 static struct line *
2286 realloc_lines(struct view *view, size_t line_size)
2287 {
2288 size_t alloc = view->line_alloc;
2289 struct line *tmp = realloc_items(view->line, &alloc, line_size,
2290 sizeof(*view->line));
2292 if (!tmp)
2293 return NULL;
2295 view->line = tmp;
2296 view->line_alloc = alloc;
2297 view->line_size = line_size;
2298 return view->line;
2299 }
2301 static bool
2302 update_view(struct view *view)
2303 {
2304 char in_buffer[BUFSIZ];
2305 char out_buffer[BUFSIZ * 2];
2306 char *line;
2307 /* The number of lines to read. If too low it will cause too much
2308 * redrawing (and possible flickering), if too high responsiveness
2309 * will suffer. */
2310 unsigned long lines = view->height;
2311 int redraw_from = -1;
2313 if (!view->pipe)
2314 return TRUE;
2316 /* Only redraw if lines are visible. */
2317 if (view->offset + view->height >= view->lines)
2318 redraw_from = view->lines - view->offset;
2320 /* FIXME: This is probably not perfect for backgrounded views. */
2321 if (!realloc_lines(view, view->lines + lines))
2322 goto alloc_error;
2324 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
2325 size_t linelen = strlen(line);
2327 if (linelen)
2328 line[linelen - 1] = 0;
2330 if (opt_iconv != ICONV_NONE) {
2331 ICONV_CONST char *inbuf = line;
2332 size_t inlen = linelen;
2334 char *outbuf = out_buffer;
2335 size_t outlen = sizeof(out_buffer);
2337 size_t ret;
2339 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
2340 if (ret != (size_t) -1) {
2341 line = out_buffer;
2342 linelen = strlen(out_buffer);
2343 }
2344 }
2346 if (!view->ops->read(view, line))
2347 goto alloc_error;
2349 if (lines-- == 1)
2350 break;
2351 }
2353 {
2354 int digits;
2356 lines = view->lines;
2357 for (digits = 0; lines; digits++)
2358 lines /= 10;
2360 /* Keep the displayed view in sync with line number scaling. */
2361 if (digits != view->digits) {
2362 view->digits = digits;
2363 redraw_from = 0;
2364 }
2365 }
2367 if (!view_is_displayed(view))
2368 goto check_pipe;
2370 if (view == VIEW(REQ_VIEW_TREE)) {
2371 /* Clear the view and redraw everything since the tree sorting
2372 * might have rearranged things. */
2373 redraw_view(view);
2375 } else if (redraw_from >= 0) {
2376 /* If this is an incremental update, redraw the previous line
2377 * since for commits some members could have changed when
2378 * loading the main view. */
2379 if (redraw_from > 0)
2380 redraw_from--;
2382 /* Since revision graph visualization requires knowledge
2383 * about the parent commit, it causes a further one-off
2384 * needed to be redrawn for incremental updates. */
2385 if (redraw_from > 0 && opt_rev_graph)
2386 redraw_from--;
2388 /* Incrementally draw avoids flickering. */
2389 redraw_view_from(view, redraw_from);
2390 }
2392 if (view == VIEW(REQ_VIEW_BLAME))
2393 redraw_view_dirty(view);
2395 /* Update the title _after_ the redraw so that if the redraw picks up a
2396 * commit reference in view->ref it'll be available here. */
2397 update_view_title(view);
2399 check_pipe:
2400 if (ferror(view->pipe) && errno != 0) {
2401 report("Failed to read: %s", strerror(errno));
2402 end_update(view, TRUE);
2404 } else if (feof(view->pipe)) {
2405 report("");
2406 end_update(view, FALSE);
2407 }
2409 return TRUE;
2411 alloc_error:
2412 report("Allocation failure");
2413 end_update(view, TRUE);
2414 return FALSE;
2415 }
2417 static struct line *
2418 add_line_data(struct view *view, void *data, enum line_type type)
2419 {
2420 struct line *line = &view->line[view->lines++];
2422 memset(line, 0, sizeof(*line));
2423 line->type = type;
2424 line->data = data;
2426 return line;
2427 }
2429 static struct line *
2430 add_line_text(struct view *view, char *data, enum line_type type)
2431 {
2432 if (data)
2433 data = strdup(data);
2435 return data ? add_line_data(view, data, type) : NULL;
2436 }
2439 /*
2440 * View opening
2441 */
2443 enum open_flags {
2444 OPEN_DEFAULT = 0, /* Use default view switching. */
2445 OPEN_SPLIT = 1, /* Split current view. */
2446 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2447 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2448 OPEN_NOMAXIMIZE = 8 /* Do not maximize the current view. */
2449 };
2451 static void
2452 open_view(struct view *prev, enum request request, enum open_flags flags)
2453 {
2454 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2455 bool split = !!(flags & OPEN_SPLIT);
2456 bool reload = !!(flags & OPEN_RELOAD);
2457 bool nomaximize = !!(flags & OPEN_NOMAXIMIZE);
2458 struct view *view = VIEW(request);
2459 int nviews = displayed_views();
2460 struct view *base_view = display[0];
2462 if (view == prev && nviews == 1 && !reload) {
2463 report("Already in %s view", view->name);
2464 return;
2465 }
2467 if (view->git_dir && !opt_git_dir[0]) {
2468 report("The %s view is disabled in pager view", view->name);
2469 return;
2470 }
2472 if (split) {
2473 display[1] = view;
2474 if (!backgrounded)
2475 current_view = 1;
2476 } else if (!nomaximize) {
2477 /* Maximize the current view. */
2478 memset(display, 0, sizeof(display));
2479 current_view = 0;
2480 display[current_view] = view;
2481 }
2483 /* Resize the view when switching between split- and full-screen,
2484 * or when switching between two different full-screen views. */
2485 if (nviews != displayed_views() ||
2486 (nviews == 1 && base_view != display[0]))
2487 resize_display();
2489 if (view->pipe)
2490 end_update(view, TRUE);
2492 if (view->ops->open) {
2493 if (!view->ops->open(view)) {
2494 report("Failed to load %s view", view->name);
2495 return;
2496 }
2498 } else if ((reload || strcmp(view->vid, view->id)) &&
2499 !begin_update(view)) {
2500 report("Failed to load %s view", view->name);
2501 return;
2502 }
2504 if (split && prev->lineno - prev->offset >= prev->height) {
2505 /* Take the title line into account. */
2506 int lines = prev->lineno - prev->offset - prev->height + 1;
2508 /* Scroll the view that was split if the current line is
2509 * outside the new limited view. */
2510 do_scroll_view(prev, lines);
2511 }
2513 if (prev && view != prev) {
2514 if (split && !backgrounded) {
2515 /* "Blur" the previous view. */
2516 update_view_title(prev);
2517 }
2519 view->parent = prev;
2520 }
2522 if (view->pipe && view->lines == 0) {
2523 /* Clear the old view and let the incremental updating refill
2524 * the screen. */
2525 werase(view->win);
2526 report("");
2527 } else {
2528 redraw_view(view);
2529 report("");
2530 }
2532 /* If the view is backgrounded the above calls to report()
2533 * won't redraw the view title. */
2534 if (backgrounded)
2535 update_view_title(view);
2536 }
2538 static bool
2539 run_confirm(const char *cmd, const char *prompt)
2540 {
2541 bool confirmation = prompt_yesno(prompt);
2543 if (confirmation)
2544 system(cmd);
2546 return confirmation;
2547 }
2549 static void
2550 open_external_viewer(const char *cmd)
2551 {
2552 def_prog_mode(); /* save current tty modes */
2553 endwin(); /* restore original tty modes */
2554 system(cmd);
2555 fprintf(stderr, "Press Enter to continue");
2556 getc(stdin);
2557 reset_prog_mode();
2558 redraw_display();
2559 }
2561 static void
2562 open_mergetool(const char *file)
2563 {
2564 char cmd[SIZEOF_STR];
2565 char file_sq[SIZEOF_STR];
2567 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2568 string_format(cmd, "git mergetool %s", file_sq)) {
2569 open_external_viewer(cmd);
2570 }
2571 }
2573 static void
2574 open_editor(bool from_root, const char *file)
2575 {
2576 char cmd[SIZEOF_STR];
2577 char file_sq[SIZEOF_STR];
2578 char *editor;
2579 char *prefix = from_root ? opt_cdup : "";
2581 editor = getenv("GIT_EDITOR");
2582 if (!editor && *opt_editor)
2583 editor = opt_editor;
2584 if (!editor)
2585 editor = getenv("VISUAL");
2586 if (!editor)
2587 editor = getenv("EDITOR");
2588 if (!editor)
2589 editor = "vi";
2591 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2592 string_format(cmd, "%s %s%s", editor, prefix, file_sq)) {
2593 open_external_viewer(cmd);
2594 }
2595 }
2597 static void
2598 open_run_request(enum request request)
2599 {
2600 struct run_request *req = get_run_request(request);
2601 char buf[SIZEOF_STR * 2];
2602 size_t bufpos;
2603 char *cmd;
2605 if (!req) {
2606 report("Unknown run request");
2607 return;
2608 }
2610 bufpos = 0;
2611 cmd = req->cmd;
2613 while (cmd) {
2614 char *next = strstr(cmd, "%(");
2615 int len = next - cmd;
2616 char *value;
2618 if (!next) {
2619 len = strlen(cmd);
2620 value = "";
2622 } else if (!strncmp(next, "%(head)", 7)) {
2623 value = ref_head;
2625 } else if (!strncmp(next, "%(commit)", 9)) {
2626 value = ref_commit;
2628 } else if (!strncmp(next, "%(blob)", 7)) {
2629 value = ref_blob;
2631 } else {
2632 report("Unknown replacement in run request: `%s`", req->cmd);
2633 return;
2634 }
2636 if (!string_format_from(buf, &bufpos, "%.*s%s", len, cmd, value))
2637 return;
2639 if (next)
2640 next = strchr(next, ')') + 1;
2641 cmd = next;
2642 }
2644 open_external_viewer(buf);
2645 }
2647 /*
2648 * User request switch noodle
2649 */
2651 static int
2652 view_driver(struct view *view, enum request request)
2653 {
2654 int i;
2656 if (request == REQ_NONE) {
2657 doupdate();
2658 return TRUE;
2659 }
2661 if (request > REQ_NONE) {
2662 open_run_request(request);
2663 /* FIXME: When all views can refresh always do this. */
2664 if (view == VIEW(REQ_VIEW_STATUS) ||
2665 view == VIEW(REQ_VIEW_MAIN) ||
2666 view == VIEW(REQ_VIEW_STAGE))
2667 request = REQ_REFRESH;
2668 else
2669 return TRUE;
2670 }
2672 if (view && view->lines) {
2673 request = view->ops->request(view, request, &view->line[view->lineno]);
2674 if (request == REQ_NONE)
2675 return TRUE;
2676 }
2678 switch (request) {
2679 case REQ_MOVE_UP:
2680 case REQ_MOVE_DOWN:
2681 case REQ_MOVE_PAGE_UP:
2682 case REQ_MOVE_PAGE_DOWN:
2683 case REQ_MOVE_FIRST_LINE:
2684 case REQ_MOVE_LAST_LINE:
2685 move_view(view, request);
2686 break;
2688 case REQ_SCROLL_LINE_DOWN:
2689 case REQ_SCROLL_LINE_UP:
2690 case REQ_SCROLL_PAGE_DOWN:
2691 case REQ_SCROLL_PAGE_UP:
2692 scroll_view(view, request);
2693 break;
2695 case REQ_VIEW_BLAME:
2696 if (!opt_file[0]) {
2697 report("No file chosen, press %s to open tree view",
2698 get_key(REQ_VIEW_TREE));
2699 break;
2700 }
2701 open_view(view, request, OPEN_DEFAULT);
2702 break;
2704 case REQ_VIEW_BLOB:
2705 if (!ref_blob[0]) {
2706 report("No file chosen, press %s to open tree view",
2707 get_key(REQ_VIEW_TREE));
2708 break;
2709 }
2710 open_view(view, request, OPEN_DEFAULT);
2711 break;
2713 case REQ_VIEW_PAGER:
2714 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2715 report("No pager content, press %s to run command from prompt",
2716 get_key(REQ_PROMPT));
2717 break;
2718 }
2719 open_view(view, request, OPEN_DEFAULT);
2720 break;
2722 case REQ_VIEW_STAGE:
2723 if (!VIEW(REQ_VIEW_STAGE)->lines) {
2724 report("No stage content, press %s to open the status view and choose file",
2725 get_key(REQ_VIEW_STATUS));
2726 break;
2727 }
2728 open_view(view, request, OPEN_DEFAULT);
2729 break;
2731 case REQ_VIEW_STATUS:
2732 if (opt_is_inside_work_tree == FALSE) {
2733 report("The status view requires a working tree");
2734 break;
2735 }
2736 open_view(view, request, OPEN_DEFAULT);
2737 break;
2739 case REQ_VIEW_MAIN:
2740 case REQ_VIEW_DIFF:
2741 case REQ_VIEW_LOG:
2742 case REQ_VIEW_TREE:
2743 case REQ_VIEW_HELP:
2744 open_view(view, request, OPEN_DEFAULT);
2745 break;
2747 case REQ_NEXT:
2748 case REQ_PREVIOUS:
2749 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2751 if ((view == VIEW(REQ_VIEW_DIFF) &&
2752 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2753 (view == VIEW(REQ_VIEW_DIFF) &&
2754 view->parent == VIEW(REQ_VIEW_BLAME)) ||
2755 (view == VIEW(REQ_VIEW_STAGE) &&
2756 view->parent == VIEW(REQ_VIEW_STATUS)) ||
2757 (view == VIEW(REQ_VIEW_BLOB) &&
2758 view->parent == VIEW(REQ_VIEW_TREE))) {
2759 int line;
2761 view = view->parent;
2762 line = view->lineno;
2763 move_view(view, request);
2764 if (view_is_displayed(view))
2765 update_view_title(view);
2766 if (line != view->lineno)
2767 view->ops->request(view, REQ_ENTER,
2768 &view->line[view->lineno]);
2770 } else {
2771 move_view(view, request);
2772 }
2773 break;
2775 case REQ_VIEW_NEXT:
2776 {
2777 int nviews = displayed_views();
2778 int next_view = (current_view + 1) % nviews;
2780 if (next_view == current_view) {
2781 report("Only one view is displayed");
2782 break;
2783 }
2785 current_view = next_view;
2786 /* Blur out the title of the previous view. */
2787 update_view_title(view);
2788 report("");
2789 break;
2790 }
2791 case REQ_REFRESH:
2792 report("Refreshing is not yet supported for the %s view", view->name);
2793 break;
2795 case REQ_MAXIMIZE:
2796 if (displayed_views() == 2)
2797 open_view(view, VIEW_REQ(view), OPEN_DEFAULT);
2798 break;
2800 case REQ_TOGGLE_LINENO:
2801 opt_line_number = !opt_line_number;
2802 redraw_display();
2803 break;
2805 case REQ_TOGGLE_DATE:
2806 opt_date = !opt_date;
2807 redraw_display();
2808 break;
2810 case REQ_TOGGLE_AUTHOR:
2811 opt_author = !opt_author;
2812 redraw_display();
2813 break;
2815 case REQ_TOGGLE_REV_GRAPH:
2816 opt_rev_graph = !opt_rev_graph;
2817 redraw_display();
2818 break;
2820 case REQ_TOGGLE_REFS:
2821 opt_show_refs = !opt_show_refs;
2822 redraw_display();
2823 break;
2825 case REQ_SEARCH:
2826 case REQ_SEARCH_BACK:
2827 search_view(view, request);
2828 break;
2830 case REQ_FIND_NEXT:
2831 case REQ_FIND_PREV:
2832 find_next(view, request);
2833 break;
2835 case REQ_STOP_LOADING:
2836 for (i = 0; i < ARRAY_SIZE(views); i++) {
2837 view = &views[i];
2838 if (view->pipe)
2839 report("Stopped loading the %s view", view->name),
2840 end_update(view, TRUE);
2841 }
2842 break;
2844 case REQ_SHOW_VERSION:
2845 report("tig-%s (built %s)", TIG_VERSION, __DATE__);
2846 return TRUE;
2848 case REQ_SCREEN_RESIZE:
2849 resize_display();
2850 /* Fall-through */
2851 case REQ_SCREEN_REDRAW:
2852 redraw_display();
2853 break;
2855 case REQ_EDIT:
2856 report("Nothing to edit");
2857 break;
2859 case REQ_ENTER:
2860 report("Nothing to enter");
2861 break;
2863 case REQ_VIEW_CLOSE:
2864 /* XXX: Mark closed views by letting view->parent point to the
2865 * view itself. Parents to closed view should never be
2866 * followed. */
2867 if (view->parent &&
2868 view->parent->parent != view->parent) {
2869 memset(display, 0, sizeof(display));
2870 current_view = 0;
2871 display[current_view] = view->parent;
2872 view->parent = view;
2873 resize_display();
2874 redraw_display();
2875 break;
2876 }
2877 /* Fall-through */
2878 case REQ_QUIT:
2879 return FALSE;
2881 default:
2882 /* An unknown key will show most commonly used commands. */
2883 report("Unknown key, press 'h' for help");
2884 return TRUE;
2885 }
2887 return TRUE;
2888 }
2891 /*
2892 * Pager backend
2893 */
2895 static bool
2896 pager_draw(struct view *view, struct line *line, unsigned int lineno)
2897 {
2898 char *text = line->data;
2900 if (opt_line_number && draw_lineno(view, lineno))
2901 return TRUE;
2903 draw_text(view, line->type, text, TRUE);
2904 return TRUE;
2905 }
2907 static bool
2908 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2909 {
2910 char refbuf[SIZEOF_STR];
2911 char *ref = NULL;
2912 FILE *pipe;
2914 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2915 return TRUE;
2917 pipe = popen(refbuf, "r");
2918 if (!pipe)
2919 return TRUE;
2921 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2922 ref = chomp_string(ref);
2923 pclose(pipe);
2925 if (!ref || !*ref)
2926 return TRUE;
2928 /* This is the only fatal call, since it can "corrupt" the buffer. */
2929 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2930 return FALSE;
2932 return TRUE;
2933 }
2935 static void
2936 add_pager_refs(struct view *view, struct line *line)
2937 {
2938 char buf[SIZEOF_STR];
2939 char *commit_id = (char *)line->data + STRING_SIZE("commit ");
2940 struct ref **refs;
2941 size_t bufpos = 0, refpos = 0;
2942 const char *sep = "Refs: ";
2943 bool is_tag = FALSE;
2945 assert(line->type == LINE_COMMIT);
2947 refs = get_refs(commit_id);
2948 if (!refs) {
2949 if (view == VIEW(REQ_VIEW_DIFF))
2950 goto try_add_describe_ref;
2951 return;
2952 }
2954 do {
2955 struct ref *ref = refs[refpos];
2956 char *fmt = ref->tag ? "%s[%s]" :
2957 ref->remote ? "%s<%s>" : "%s%s";
2959 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2960 return;
2961 sep = ", ";
2962 if (ref->tag)
2963 is_tag = TRUE;
2964 } while (refs[refpos++]->next);
2966 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2967 try_add_describe_ref:
2968 /* Add <tag>-g<commit_id> "fake" reference. */
2969 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2970 return;
2971 }
2973 if (bufpos == 0)
2974 return;
2976 if (!realloc_lines(view, view->line_size + 1))
2977 return;
2979 add_line_text(view, buf, LINE_PP_REFS);
2980 }
2982 static bool
2983 pager_read(struct view *view, char *data)
2984 {
2985 struct line *line;
2987 if (!data)
2988 return TRUE;
2990 line = add_line_text(view, data, get_line_type(data));
2991 if (!line)
2992 return FALSE;
2994 if (line->type == LINE_COMMIT &&
2995 (view == VIEW(REQ_VIEW_DIFF) ||
2996 view == VIEW(REQ_VIEW_LOG)))
2997 add_pager_refs(view, line);
2999 return TRUE;
3000 }
3002 static enum request
3003 pager_request(struct view *view, enum request request, struct line *line)
3004 {
3005 int split = 0;
3007 if (request != REQ_ENTER)
3008 return request;
3010 if (line->type == LINE_COMMIT &&
3011 (view == VIEW(REQ_VIEW_LOG) ||
3012 view == VIEW(REQ_VIEW_PAGER))) {
3013 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
3014 split = 1;
3015 }
3017 /* Always scroll the view even if it was split. That way
3018 * you can use Enter to scroll through the log view and
3019 * split open each commit diff. */
3020 scroll_view(view, REQ_SCROLL_LINE_DOWN);
3022 /* FIXME: A minor workaround. Scrolling the view will call report("")
3023 * but if we are scrolling a non-current view this won't properly
3024 * update the view title. */
3025 if (split)
3026 update_view_title(view);
3028 return REQ_NONE;
3029 }
3031 static bool
3032 pager_grep(struct view *view, struct line *line)
3033 {
3034 regmatch_t pmatch;
3035 char *text = line->data;
3037 if (!*text)
3038 return FALSE;
3040 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
3041 return FALSE;
3043 return TRUE;
3044 }
3046 static void
3047 pager_select(struct view *view, struct line *line)
3048 {
3049 if (line->type == LINE_COMMIT) {
3050 char *text = (char *)line->data + STRING_SIZE("commit ");
3052 if (view != VIEW(REQ_VIEW_PAGER))
3053 string_copy_rev(view->ref, text);
3054 string_copy_rev(ref_commit, text);
3055 }
3056 }
3058 static struct view_ops pager_ops = {
3059 "line",
3060 NULL,
3061 pager_read,
3062 pager_draw,
3063 pager_request,
3064 pager_grep,
3065 pager_select,
3066 };
3069 /*
3070 * Help backend
3071 */
3073 static bool
3074 help_open(struct view *view)
3075 {
3076 char buf[BUFSIZ];
3077 int lines = ARRAY_SIZE(req_info) + 2;
3078 int i;
3080 if (view->lines > 0)
3081 return TRUE;
3083 for (i = 0; i < ARRAY_SIZE(req_info); i++)
3084 if (!req_info[i].request)
3085 lines++;
3087 lines += run_requests + 1;
3089 view->line = calloc(lines, sizeof(*view->line));
3090 if (!view->line)
3091 return FALSE;
3093 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
3095 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
3096 char *key;
3098 if (req_info[i].request == REQ_NONE)
3099 continue;
3101 if (!req_info[i].request) {
3102 add_line_text(view, "", LINE_DEFAULT);
3103 add_line_text(view, req_info[i].help, LINE_DEFAULT);
3104 continue;
3105 }
3107 key = get_key(req_info[i].request);
3108 if (!*key)
3109 key = "(no key defined)";
3111 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
3112 continue;
3114 add_line_text(view, buf, LINE_DEFAULT);
3115 }
3117 if (run_requests) {
3118 add_line_text(view, "", LINE_DEFAULT);
3119 add_line_text(view, "External commands:", LINE_DEFAULT);
3120 }
3122 for (i = 0; i < run_requests; i++) {
3123 struct run_request *req = get_run_request(REQ_NONE + i + 1);
3124 char *key;
3126 if (!req)
3127 continue;
3129 key = get_key_name(req->key);
3130 if (!*key)
3131 key = "(no key defined)";
3133 if (!string_format(buf, " %-10s %-14s `%s`",
3134 keymap_table[req->keymap].name,
3135 key, req->cmd))
3136 continue;
3138 add_line_text(view, buf, LINE_DEFAULT);
3139 }
3141 return TRUE;
3142 }
3144 static struct view_ops help_ops = {
3145 "line",
3146 help_open,
3147 NULL,
3148 pager_draw,
3149 pager_request,
3150 pager_grep,
3151 pager_select,
3152 };
3155 /*
3156 * Tree backend
3157 */
3159 struct tree_stack_entry {
3160 struct tree_stack_entry *prev; /* Entry below this in the stack */
3161 unsigned long lineno; /* Line number to restore */
3162 char *name; /* Position of name in opt_path */
3163 };
3165 /* The top of the path stack. */
3166 static struct tree_stack_entry *tree_stack = NULL;
3167 unsigned long tree_lineno = 0;
3169 static void
3170 pop_tree_stack_entry(void)
3171 {
3172 struct tree_stack_entry *entry = tree_stack;
3174 tree_lineno = entry->lineno;
3175 entry->name[0] = 0;
3176 tree_stack = entry->prev;
3177 free(entry);
3178 }
3180 static void
3181 push_tree_stack_entry(char *name, unsigned long lineno)
3182 {
3183 struct tree_stack_entry *entry = calloc(1, sizeof(*entry));
3184 size_t pathlen = strlen(opt_path);
3186 if (!entry)
3187 return;
3189 entry->prev = tree_stack;
3190 entry->name = opt_path + pathlen;
3191 tree_stack = entry;
3193 if (!string_format_from(opt_path, &pathlen, "%s/", name)) {
3194 pop_tree_stack_entry();
3195 return;
3196 }
3198 /* Move the current line to the first tree entry. */
3199 tree_lineno = 1;
3200 entry->lineno = lineno;
3201 }
3203 /* Parse output from git-ls-tree(1):
3204 *
3205 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
3206 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
3207 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
3208 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
3209 */
3211 #define SIZEOF_TREE_ATTR \
3212 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
3214 #define TREE_UP_FORMAT "040000 tree %s\t.."
3216 static int
3217 tree_compare_entry(enum line_type type1, char *name1,
3218 enum line_type type2, char *name2)
3219 {
3220 if (type1 != type2) {
3221 if (type1 == LINE_TREE_DIR)
3222 return -1;
3223 return 1;
3224 }
3226 return strcmp(name1, name2);
3227 }
3229 static char *
3230 tree_path(struct line *line)
3231 {
3232 char *path = line->data;
3234 return path + SIZEOF_TREE_ATTR;
3235 }
3237 static bool
3238 tree_read(struct view *view, char *text)
3239 {
3240 size_t textlen = text ? strlen(text) : 0;
3241 char buf[SIZEOF_STR];
3242 unsigned long pos;
3243 enum line_type type;
3244 bool first_read = view->lines == 0;
3246 if (!text)
3247 return TRUE;
3248 if (textlen <= SIZEOF_TREE_ATTR)
3249 return FALSE;
3251 type = text[STRING_SIZE("100644 ")] == 't'
3252 ? LINE_TREE_DIR : LINE_TREE_FILE;
3254 if (first_read) {
3255 /* Add path info line */
3256 if (!string_format(buf, "Directory path /%s", opt_path) ||
3257 !realloc_lines(view, view->line_size + 1) ||
3258 !add_line_text(view, buf, LINE_DEFAULT))
3259 return FALSE;
3261 /* Insert "link" to parent directory. */
3262 if (*opt_path) {
3263 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
3264 !realloc_lines(view, view->line_size + 1) ||
3265 !add_line_text(view, buf, LINE_TREE_DIR))
3266 return FALSE;
3267 }
3268 }
3270 /* Strip the path part ... */
3271 if (*opt_path) {
3272 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
3273 size_t striplen = strlen(opt_path);
3274 char *path = text + SIZEOF_TREE_ATTR;
3276 if (pathlen > striplen)
3277 memmove(path, path + striplen,
3278 pathlen - striplen + 1);
3279 }
3281 /* Skip "Directory ..." and ".." line. */
3282 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
3283 struct line *line = &view->line[pos];
3284 char *path1 = tree_path(line);
3285 char *path2 = text + SIZEOF_TREE_ATTR;
3286 int cmp = tree_compare_entry(line->type, path1, type, path2);
3288 if (cmp <= 0)
3289 continue;
3291 text = strdup(text);
3292 if (!text)
3293 return FALSE;
3295 if (view->lines > pos)
3296 memmove(&view->line[pos + 1], &view->line[pos],
3297 (view->lines - pos) * sizeof(*line));
3299 line = &view->line[pos];
3300 line->data = text;
3301 line->type = type;
3302 view->lines++;
3303 return TRUE;
3304 }
3306 if (!add_line_text(view, text, type))
3307 return FALSE;
3309 if (tree_lineno > view->lineno) {
3310 view->lineno = tree_lineno;
3311 tree_lineno = 0;
3312 }
3314 return TRUE;
3315 }
3317 static enum request
3318 tree_request(struct view *view, enum request request, struct line *line)
3319 {
3320 enum open_flags flags;
3322 if (request == REQ_VIEW_BLAME) {
3323 char *filename = tree_path(line);
3325 if (line->type == LINE_TREE_DIR) {
3326 report("Cannot show blame for directory %s", opt_path);
3327 return REQ_NONE;
3328 }
3330 string_copy(opt_ref, view->vid);
3331 string_format(opt_file, "%s%s", opt_path, filename);
3332 return request;
3333 }
3334 if (request == REQ_TREE_PARENT) {
3335 if (*opt_path) {
3336 /* fake 'cd ..' */
3337 request = REQ_ENTER;
3338 line = &view->line[1];
3339 } else {
3340 /* quit view if at top of tree */
3341 return REQ_VIEW_CLOSE;
3342 }
3343 }
3344 if (request != REQ_ENTER)
3345 return request;
3347 /* Cleanup the stack if the tree view is at a different tree. */
3348 while (!*opt_path && tree_stack)
3349 pop_tree_stack_entry();
3351 switch (line->type) {
3352 case LINE_TREE_DIR:
3353 /* Depending on whether it is a subdir or parent (updir?) link
3354 * mangle the path buffer. */
3355 if (line == &view->line[1] && *opt_path) {
3356 pop_tree_stack_entry();
3358 } else {
3359 char *basename = tree_path(line);
3361 push_tree_stack_entry(basename, view->lineno);
3362 }
3364 /* Trees and subtrees share the same ID, so they are not not
3365 * unique like blobs. */
3366 flags = OPEN_RELOAD;
3367 request = REQ_VIEW_TREE;
3368 break;
3370 case LINE_TREE_FILE:
3371 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3372 request = REQ_VIEW_BLOB;
3373 break;
3375 default:
3376 return TRUE;
3377 }
3379 open_view(view, request, flags);
3380 if (request == REQ_VIEW_TREE) {
3381 view->lineno = tree_lineno;
3382 }
3384 return REQ_NONE;
3385 }
3387 static void
3388 tree_select(struct view *view, struct line *line)
3389 {
3390 char *text = (char *)line->data + STRING_SIZE("100644 blob ");
3392 if (line->type == LINE_TREE_FILE) {
3393 string_copy_rev(ref_blob, text);
3395 } else if (line->type != LINE_TREE_DIR) {
3396 return;
3397 }
3399 string_copy_rev(view->ref, text);
3400 }
3402 static struct view_ops tree_ops = {
3403 "file",
3404 NULL,
3405 tree_read,
3406 pager_draw,
3407 tree_request,
3408 pager_grep,
3409 tree_select,
3410 };
3412 static bool
3413 blob_read(struct view *view, char *line)
3414 {
3415 if (!line)
3416 return TRUE;
3417 return add_line_text(view, line, LINE_DEFAULT) != NULL;
3418 }
3420 static struct view_ops blob_ops = {
3421 "line",
3422 NULL,
3423 blob_read,
3424 pager_draw,
3425 pager_request,
3426 pager_grep,
3427 pager_select,
3428 };
3430 /*
3431 * Blame backend
3432 *
3433 * Loading the blame view is a two phase job:
3434 *
3435 * 1. File content is read either using opt_file from the
3436 * filesystem or using git-cat-file.
3437 * 2. Then blame information is incrementally added by
3438 * reading output from git-blame.
3439 */
3441 struct blame_commit {
3442 char id[SIZEOF_REV]; /* SHA1 ID. */
3443 char title[128]; /* First line of the commit message. */
3444 char author[75]; /* Author of the commit. */
3445 struct tm time; /* Date from the author ident. */
3446 char filename[128]; /* Name of file. */
3447 };
3449 struct blame {
3450 struct blame_commit *commit;
3451 unsigned int header:1;
3452 char text[1];
3453 };
3455 #define BLAME_CAT_FILE_CMD "git cat-file blob %s:%s"
3456 #define BLAME_INCREMENTAL_CMD "git blame --incremental %s %s"
3458 static bool
3459 blame_open(struct view *view)
3460 {
3461 char path[SIZEOF_STR];
3462 char ref[SIZEOF_STR] = "";
3464 if (sq_quote(path, 0, opt_file) >= sizeof(path))
3465 return FALSE;
3467 if (*opt_ref && sq_quote(ref, 0, opt_ref) >= sizeof(ref))
3468 return FALSE;
3470 if (*opt_ref) {
3471 if (!string_format(view->cmd, BLAME_CAT_FILE_CMD, ref, path))
3472 return FALSE;
3473 } else {
3474 view->pipe = fopen(opt_file, "r");
3475 if (!view->pipe &&
3476 !string_format(view->cmd, BLAME_CAT_FILE_CMD, "HEAD", path))
3477 return FALSE;
3478 }
3480 if (!view->pipe)
3481 view->pipe = popen(view->cmd, "r");
3482 if (!view->pipe)
3483 return FALSE;
3485 if (!string_format(view->cmd, BLAME_INCREMENTAL_CMD, ref, path))
3486 return FALSE;
3488 string_format(view->ref, "%s ...", opt_file);
3489 string_copy_rev(view->vid, opt_file);
3490 set_nonblocking_input(TRUE);
3492 if (view->line) {
3493 int i;
3495 for (i = 0; i < view->lines; i++)
3496 free(view->line[i].data);
3497 free(view->line);
3498 }
3500 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3501 view->offset = view->lines = view->lineno = 0;
3502 view->line = NULL;
3503 view->start_time = time(NULL);
3505 return TRUE;
3506 }
3508 static struct blame_commit *
3509 get_blame_commit(struct view *view, const char *id)
3510 {
3511 size_t i;
3513 for (i = 0; i < view->lines; i++) {
3514 struct blame *blame = view->line[i].data;
3516 if (!blame->commit)
3517 continue;
3519 if (!strncmp(blame->commit->id, id, SIZEOF_REV - 1))
3520 return blame->commit;
3521 }
3523 {
3524 struct blame_commit *commit = calloc(1, sizeof(*commit));
3526 if (commit)
3527 string_ncopy(commit->id, id, SIZEOF_REV);
3528 return commit;
3529 }
3530 }
3532 static bool
3533 parse_number(char **posref, size_t *number, size_t min, size_t max)
3534 {
3535 char *pos = *posref;
3537 *posref = NULL;
3538 pos = strchr(pos + 1, ' ');
3539 if (!pos || !isdigit(pos[1]))
3540 return FALSE;
3541 *number = atoi(pos + 1);
3542 if (*number < min || *number > max)
3543 return FALSE;
3545 *posref = pos;
3546 return TRUE;
3547 }
3549 static struct blame_commit *
3550 parse_blame_commit(struct view *view, char *text, int *blamed)
3551 {
3552 struct blame_commit *commit;
3553 struct blame *blame;
3554 char *pos = text + SIZEOF_REV - 1;
3555 size_t lineno;
3556 size_t group;
3558 if (strlen(text) <= SIZEOF_REV || *pos != ' ')
3559 return NULL;
3561 if (!parse_number(&pos, &lineno, 1, view->lines) ||
3562 !parse_number(&pos, &group, 1, view->lines - lineno + 1))
3563 return NULL;
3565 commit = get_blame_commit(view, text);
3566 if (!commit)
3567 return NULL;
3569 *blamed += group;
3570 while (group--) {
3571 struct line *line = &view->line[lineno + group - 1];
3573 blame = line->data;
3574 blame->commit = commit;
3575 blame->header = !group;
3576 line->dirty = 1;
3577 }
3579 return commit;
3580 }
3582 static bool
3583 blame_read_file(struct view *view, char *line)
3584 {
3585 if (!line) {
3586 FILE *pipe = NULL;
3588 if (view->lines > 0)
3589 pipe = popen(view->cmd, "r");
3590 else if (!view->parent)
3591 die("No blame exist for %s", view->vid);
3592 view->cmd[0] = 0;
3593 if (!pipe) {
3594 report("Failed to load blame data");
3595 return TRUE;
3596 }
3598 fclose(view->pipe);
3599 view->pipe = pipe;
3600 return FALSE;
3602 } else {
3603 size_t linelen = strlen(line);
3604 struct blame *blame = malloc(sizeof(*blame) + linelen);
3606 blame->commit = NULL;
3607 strncpy(blame->text, line, linelen);
3608 blame->text[linelen] = 0;
3609 return add_line_data(view, blame, LINE_BLAME_ID) != NULL;
3610 }
3611 }
3613 static bool
3614 match_blame_header(const char *name, char **line)
3615 {
3616 size_t namelen = strlen(name);
3617 bool matched = !strncmp(name, *line, namelen);
3619 if (matched)
3620 *line += namelen;
3622 return matched;
3623 }
3625 static bool
3626 blame_read(struct view *view, char *line)
3627 {
3628 static struct blame_commit *commit = NULL;
3629 static int blamed = 0;
3630 static time_t author_time;
3632 if (*view->cmd)
3633 return blame_read_file(view, line);
3635 if (!line) {
3636 /* Reset all! */
3637 commit = NULL;
3638 blamed = 0;
3639 string_format(view->ref, "%s", view->vid);
3640 if (view_is_displayed(view)) {
3641 update_view_title(view);
3642 redraw_view_from(view, 0);
3643 }
3644 return TRUE;
3645 }
3647 if (!commit) {
3648 commit = parse_blame_commit(view, line, &blamed);
3649 string_format(view->ref, "%s %2d%%", view->vid,
3650 blamed * 100 / view->lines);
3652 } else if (match_blame_header("author ", &line)) {
3653 string_ncopy(commit->author, line, strlen(line));
3655 } else if (match_blame_header("author-time ", &line)) {
3656 author_time = (time_t) atol(line);
3658 } else if (match_blame_header("author-tz ", &line)) {
3659 long tz;
3661 tz = ('0' - line[1]) * 60 * 60 * 10;
3662 tz += ('0' - line[2]) * 60 * 60;
3663 tz += ('0' - line[3]) * 60;
3664 tz += ('0' - line[4]) * 60;
3666 if (line[0] == '-')
3667 tz = -tz;
3669 author_time -= tz;
3670 gmtime_r(&author_time, &commit->time);
3672 } else if (match_blame_header("summary ", &line)) {
3673 string_ncopy(commit->title, line, strlen(line));
3675 } else if (match_blame_header("filename ", &line)) {
3676 string_ncopy(commit->filename, line, strlen(line));
3677 commit = NULL;
3678 }
3680 return TRUE;
3681 }
3683 static bool
3684 blame_draw(struct view *view, struct line *line, unsigned int lineno)
3685 {
3686 struct blame *blame = line->data;
3687 struct tm *time = NULL;
3688 char *id = NULL, *author = NULL;
3690 if (blame->commit && *blame->commit->filename) {
3691 id = blame->commit->id;
3692 author = blame->commit->author;
3693 time = &blame->commit->time;
3694 }
3696 if (opt_date && draw_date(view, time))
3697 return TRUE;
3699 if (opt_author &&
3700 draw_field(view, LINE_MAIN_AUTHOR, author, opt_author_cols, TRUE))
3701 return TRUE;
3703 if (draw_field(view, LINE_BLAME_ID, id, ID_COLS, FALSE))
3704 return TRUE;
3706 if (draw_lineno(view, lineno))
3707 return TRUE;
3709 draw_text(view, LINE_DEFAULT, blame->text, TRUE);
3710 return TRUE;
3711 }
3713 static enum request
3714 blame_request(struct view *view, enum request request, struct line *line)
3715 {
3716 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3717 struct blame *blame = line->data;
3719 switch (request) {
3720 case REQ_ENTER:
3721 if (!blame->commit) {
3722 report("No commit loaded yet");
3723 break;
3724 }
3726 if (!strcmp(blame->commit->id, NULL_ID)) {
3727 char path[SIZEOF_STR];
3729 if (sq_quote(path, 0, view->vid) >= sizeof(path))
3730 break;
3731 string_format(opt_cmd, "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s 2>/dev/null", path);
3732 }
3734 open_view(view, REQ_VIEW_DIFF, flags);
3735 break;
3737 default:
3738 return request;
3739 }
3741 return REQ_NONE;
3742 }
3744 static bool
3745 blame_grep(struct view *view, struct line *line)
3746 {
3747 struct blame *blame = line->data;
3748 struct blame_commit *commit = blame->commit;
3749 regmatch_t pmatch;
3751 #define MATCH(text, on) \
3752 (on && *text && regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3754 if (commit) {
3755 char buf[DATE_COLS + 1];
3757 if (MATCH(commit->title, 1) ||
3758 MATCH(commit->author, opt_author) ||
3759 MATCH(commit->id, opt_date))
3760 return TRUE;
3762 if (strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time) &&
3763 MATCH(buf, 1))
3764 return TRUE;
3765 }
3767 return MATCH(blame->text, 1);
3769 #undef MATCH
3770 }
3772 static void
3773 blame_select(struct view *view, struct line *line)
3774 {
3775 struct blame *blame = line->data;
3776 struct blame_commit *commit = blame->commit;
3778 if (!commit)
3779 return;
3781 if (!strcmp(commit->id, NULL_ID))
3782 string_ncopy(ref_commit, "HEAD", 4);
3783 else
3784 string_copy_rev(ref_commit, commit->id);
3785 }
3787 static struct view_ops blame_ops = {
3788 "line",
3789 blame_open,
3790 blame_read,
3791 blame_draw,
3792 blame_request,
3793 blame_grep,
3794 blame_select,
3795 };
3797 /*
3798 * Status backend
3799 */
3801 struct status {
3802 char status;
3803 struct {
3804 mode_t mode;
3805 char rev[SIZEOF_REV];
3806 char name[SIZEOF_STR];
3807 } old;
3808 struct {
3809 mode_t mode;
3810 char rev[SIZEOF_REV];
3811 char name[SIZEOF_STR];
3812 } new;
3813 };
3815 static char status_onbranch[SIZEOF_STR];
3816 static struct status stage_status;
3817 static enum line_type stage_line_type;
3818 static size_t stage_chunks;
3819 static int *stage_chunk;
3821 /* This should work even for the "On branch" line. */
3822 static inline bool
3823 status_has_none(struct view *view, struct line *line)
3824 {
3825 return line < view->line + view->lines && !line[1].data;
3826 }
3828 /* Get fields from the diff line:
3829 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
3830 */
3831 static inline bool
3832 status_get_diff(struct status *file, char *buf, size_t bufsize)
3833 {
3834 char *old_mode = buf + 1;
3835 char *new_mode = buf + 8;
3836 char *old_rev = buf + 15;
3837 char *new_rev = buf + 56;
3838 char *status = buf + 97;
3840 if (bufsize < 99 ||
3841 old_mode[-1] != ':' ||
3842 new_mode[-1] != ' ' ||
3843 old_rev[-1] != ' ' ||
3844 new_rev[-1] != ' ' ||
3845 status[-1] != ' ')
3846 return FALSE;
3848 file->status = *status;
3850 string_copy_rev(file->old.rev, old_rev);
3851 string_copy_rev(file->new.rev, new_rev);
3853 file->old.mode = strtoul(old_mode, NULL, 8);
3854 file->new.mode = strtoul(new_mode, NULL, 8);
3856 file->old.name[0] = file->new.name[0] = 0;
3858 return TRUE;
3859 }
3861 static bool
3862 status_run(struct view *view, const char cmd[], char status, enum line_type type)
3863 {
3864 struct status *file = NULL;
3865 struct status *unmerged = NULL;
3866 char buf[SIZEOF_STR * 4];
3867 size_t bufsize = 0;
3868 FILE *pipe;
3870 pipe = popen(cmd, "r");
3871 if (!pipe)
3872 return FALSE;
3874 add_line_data(view, NULL, type);
3876 while (!feof(pipe) && !ferror(pipe)) {
3877 char *sep;
3878 size_t readsize;
3880 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
3881 if (!readsize)
3882 break;
3883 bufsize += readsize;
3885 /* Process while we have NUL chars. */
3886 while ((sep = memchr(buf, 0, bufsize))) {
3887 size_t sepsize = sep - buf + 1;
3889 if (!file) {
3890 if (!realloc_lines(view, view->line_size + 1))
3891 goto error_out;
3893 file = calloc(1, sizeof(*file));
3894 if (!file)
3895 goto error_out;
3897 add_line_data(view, file, type);
3898 }
3900 /* Parse diff info part. */
3901 if (status) {
3902 file->status = status;
3903 if (status == 'A')
3904 string_copy(file->old.rev, NULL_ID);
3906 } else if (!file->status) {
3907 if (!status_get_diff(file, buf, sepsize))
3908 goto error_out;
3910 bufsize -= sepsize;
3911 memmove(buf, sep + 1, bufsize);
3913 sep = memchr(buf, 0, bufsize);
3914 if (!sep)
3915 break;
3916 sepsize = sep - buf + 1;
3918 /* Collapse all 'M'odified entries that
3919 * follow a associated 'U'nmerged entry.
3920 */
3921 if (file->status == 'U') {
3922 unmerged = file;
3924 } else if (unmerged) {
3925 int collapse = !strcmp(buf, unmerged->new.name);
3927 unmerged = NULL;
3928 if (collapse) {
3929 free(file);
3930 view->lines--;
3931 continue;
3932 }
3933 }
3934 }
3936 /* Grab the old name for rename/copy. */
3937 if (!*file->old.name &&
3938 (file->status == 'R' || file->status == 'C')) {
3939 sepsize = sep - buf + 1;
3940 string_ncopy(file->old.name, buf, sepsize);
3941 bufsize -= sepsize;
3942 memmove(buf, sep + 1, bufsize);
3944 sep = memchr(buf, 0, bufsize);
3945 if (!sep)
3946 break;
3947 sepsize = sep - buf + 1;
3948 }
3950 /* git-ls-files just delivers a NUL separated
3951 * list of file names similar to the second half
3952 * of the git-diff-* output. */
3953 string_ncopy(file->new.name, buf, sepsize);
3954 if (!*file->old.name)
3955 string_copy(file->old.name, file->new.name);
3956 bufsize -= sepsize;
3957 memmove(buf, sep + 1, bufsize);
3958 file = NULL;
3959 }
3960 }
3962 if (ferror(pipe)) {
3963 error_out:
3964 pclose(pipe);
3965 return FALSE;
3966 }
3968 if (!view->line[view->lines - 1].data)
3969 add_line_data(view, NULL, LINE_STAT_NONE);
3971 pclose(pipe);
3972 return TRUE;
3973 }
3975 /* Don't show unmerged entries in the staged section. */
3976 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached -M HEAD"
3977 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
3978 #define STATUS_LIST_OTHER_CMD \
3979 "git ls-files -z --others --exclude-standard"
3980 #define STATUS_LIST_NO_HEAD_CMD \
3981 "git ls-files -z --cached --exclude-standard"
3983 #define STATUS_DIFF_INDEX_SHOW_CMD \
3984 "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s %s 2>/dev/null"
3986 #define STATUS_DIFF_FILES_SHOW_CMD \
3987 "git diff-files --root --patch-with-stat -C -M -- %s %s 2>/dev/null"
3989 #define STATUS_DIFF_NO_HEAD_SHOW_CMD \
3990 "git diff --no-color --patch-with-stat /dev/null %s 2>/dev/null"
3992 /* First parse staged info using git-diff-index(1), then parse unstaged
3993 * info using git-diff-files(1), and finally untracked files using
3994 * git-ls-files(1). */
3995 static bool
3996 status_open(struct view *view)
3997 {
3998 unsigned long prev_lineno = view->lineno;
3999 size_t i;
4001 for (i = 0; i < view->lines; i++)
4002 free(view->line[i].data);
4003 free(view->line);
4004 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
4005 view->line = NULL;
4007 if (!realloc_lines(view, view->line_size + 7))
4008 return FALSE;
4010 add_line_data(view, NULL, LINE_STAT_HEAD);
4011 if (opt_no_head)
4012 string_copy(status_onbranch, "Initial commit");
4013 else if (!*opt_head)
4014 string_copy(status_onbranch, "Not currently on any branch");
4015 else if (!string_format(status_onbranch, "On branch %s", opt_head))
4016 return FALSE;
4018 system("git update-index -q --refresh >/dev/null 2>/dev/null");
4020 if (opt_no_head &&
4021 !status_run(view, STATUS_LIST_NO_HEAD_CMD, 'A', LINE_STAT_STAGED))
4022 return FALSE;
4023 else if (!status_run(view, STATUS_DIFF_INDEX_CMD, 0, LINE_STAT_STAGED))
4024 return FALSE;
4026 if (!status_run(view, STATUS_DIFF_FILES_CMD, 0, LINE_STAT_UNSTAGED) ||
4027 !status_run(view, STATUS_LIST_OTHER_CMD, '?', LINE_STAT_UNTRACKED))
4028 return FALSE;
4030 /* If all went well restore the previous line number to stay in
4031 * the context or select a line with something that can be
4032 * updated. */
4033 if (prev_lineno >= view->lines)
4034 prev_lineno = view->lines - 1;
4035 while (prev_lineno < view->lines && !view->line[prev_lineno].data)
4036 prev_lineno++;
4037 while (prev_lineno > 0 && !view->line[prev_lineno].data)
4038 prev_lineno--;
4040 /* If the above fails, always skip the "On branch" line. */
4041 if (prev_lineno < view->lines)
4042 view->lineno = prev_lineno;
4043 else
4044 view->lineno = 1;
4046 if (view->lineno < view->offset)
4047 view->offset = view->lineno;
4048 else if (view->offset + view->height <= view->lineno)
4049 view->offset = view->lineno - view->height + 1;
4051 return TRUE;
4052 }
4054 static bool
4055 status_draw(struct view *view, struct line *line, unsigned int lineno)
4056 {
4057 struct status *status = line->data;
4058 enum line_type type;
4059 char *text;
4061 if (!status) {
4062 switch (line->type) {
4063 case LINE_STAT_STAGED:
4064 type = LINE_STAT_SECTION;
4065 text = "Changes to be committed:";
4066 break;
4068 case LINE_STAT_UNSTAGED:
4069 type = LINE_STAT_SECTION;
4070 text = "Changed but not updated:";
4071 break;
4073 case LINE_STAT_UNTRACKED:
4074 type = LINE_STAT_SECTION;
4075 text = "Untracked files:";
4076 break;
4078 case LINE_STAT_NONE:
4079 type = LINE_DEFAULT;
4080 text = " (no files)";
4081 break;
4083 case LINE_STAT_HEAD:
4084 type = LINE_STAT_HEAD;
4085 text = status_onbranch;
4086 break;
4088 default:
4089 return FALSE;
4090 }
4091 } else {
4092 static char buf[] = { '?', ' ', ' ', ' ', 0 };
4094 buf[0] = status->status;
4095 if (draw_text(view, line->type, buf, TRUE))
4096 return TRUE;
4097 type = LINE_DEFAULT;
4098 text = status->new.name;
4099 }
4101 draw_text(view, type, text, TRUE);
4102 return TRUE;
4103 }
4105 static enum request
4106 status_enter(struct view *view, struct line *line)
4107 {
4108 struct status *status = line->data;
4109 char oldpath[SIZEOF_STR] = "";
4110 char newpath[SIZEOF_STR] = "";
4111 char *info;
4112 size_t cmdsize = 0;
4113 enum open_flags split;
4115 if (line->type == LINE_STAT_NONE ||
4116 (!status && line[1].type == LINE_STAT_NONE)) {
4117 report("No file to diff");
4118 return REQ_NONE;
4119 }
4121 if (status) {
4122 if (sq_quote(oldpath, 0, status->old.name) >= sizeof(oldpath))
4123 return REQ_QUIT;
4124 /* Diffs for unmerged entries are empty when pasing the
4125 * new path, so leave it empty. */
4126 if (status->status != 'U' &&
4127 sq_quote(newpath, 0, status->new.name) >= sizeof(newpath))
4128 return REQ_QUIT;
4129 }
4131 if (opt_cdup[0] &&
4132 line->type != LINE_STAT_UNTRACKED &&
4133 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
4134 return REQ_QUIT;
4136 switch (line->type) {
4137 case LINE_STAT_STAGED:
4138 if (opt_no_head) {
4139 if (!string_format_from(opt_cmd, &cmdsize,
4140 STATUS_DIFF_NO_HEAD_SHOW_CMD,
4141 newpath))
4142 return REQ_QUIT;
4143 } else {
4144 if (!string_format_from(opt_cmd, &cmdsize,
4145 STATUS_DIFF_INDEX_SHOW_CMD,
4146 oldpath, newpath))
4147 return REQ_QUIT;
4148 }
4150 if (status)
4151 info = "Staged changes to %s";
4152 else
4153 info = "Staged changes";
4154 break;
4156 case LINE_STAT_UNSTAGED:
4157 if (!string_format_from(opt_cmd, &cmdsize,
4158 STATUS_DIFF_FILES_SHOW_CMD, oldpath, newpath))
4159 return REQ_QUIT;
4160 if (status)
4161 info = "Unstaged changes to %s";
4162 else
4163 info = "Unstaged changes";
4164 break;
4166 case LINE_STAT_UNTRACKED:
4167 if (opt_pipe)
4168 return REQ_QUIT;
4170 if (!status) {
4171 report("No file to show");
4172 return REQ_NONE;
4173 }
4175 opt_pipe = fopen(status->new.name, "r");
4176 info = "Untracked file %s";
4177 break;
4179 case LINE_STAT_HEAD:
4180 return REQ_NONE;
4182 default:
4183 die("line type %d not handled in switch", line->type);
4184 }
4186 split = view_is_displayed(view) ? OPEN_SPLIT : 0;
4187 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | split);
4188 if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) {
4189 if (status) {
4190 stage_status = *status;
4191 } else {
4192 memset(&stage_status, 0, sizeof(stage_status));
4193 }
4195 stage_line_type = line->type;
4196 stage_chunks = 0;
4197 string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.new.name);
4198 }
4200 return REQ_NONE;
4201 }
4203 static bool
4204 status_exists(struct status *status, enum line_type type)
4205 {
4206 struct view *view = VIEW(REQ_VIEW_STATUS);
4207 struct line *line;
4209 for (line = view->line; line < view->line + view->lines; line++) {
4210 struct status *pos = line->data;
4212 if (line->type == type && pos &&
4213 !strcmp(status->new.name, pos->new.name))
4214 return TRUE;
4215 }
4217 return FALSE;
4218 }
4221 static FILE *
4222 status_update_prepare(enum line_type type)
4223 {
4224 char cmd[SIZEOF_STR];
4225 size_t cmdsize = 0;
4227 if (opt_cdup[0] &&
4228 type != LINE_STAT_UNTRACKED &&
4229 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4230 return NULL;
4232 switch (type) {
4233 case LINE_STAT_STAGED:
4234 string_add(cmd, cmdsize, "git update-index -z --index-info");
4235 break;
4237 case LINE_STAT_UNSTAGED:
4238 case LINE_STAT_UNTRACKED:
4239 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
4240 break;
4242 default:
4243 die("line type %d not handled in switch", type);
4244 }
4246 return popen(cmd, "w");
4247 }
4249 static bool
4250 status_update_write(FILE *pipe, struct status *status, enum line_type type)
4251 {
4252 char buf[SIZEOF_STR];
4253 size_t bufsize = 0;
4254 size_t written = 0;
4256 switch (type) {
4257 case LINE_STAT_STAGED:
4258 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
4259 status->old.mode,
4260 status->old.rev,
4261 status->old.name, 0))
4262 return FALSE;
4263 break;
4265 case LINE_STAT_UNSTAGED:
4266 case LINE_STAT_UNTRACKED:
4267 if (!string_format_from(buf, &bufsize, "%s%c", status->new.name, 0))
4268 return FALSE;
4269 break;
4271 default:
4272 die("line type %d not handled in switch", type);
4273 }
4275 while (!ferror(pipe) && written < bufsize) {
4276 written += fwrite(buf + written, 1, bufsize - written, pipe);
4277 }
4279 return written == bufsize;
4280 }
4282 static bool
4283 status_update_file(struct status *status, enum line_type type)
4284 {
4285 FILE *pipe = status_update_prepare(type);
4286 bool result;
4288 if (!pipe)
4289 return FALSE;
4291 result = status_update_write(pipe, status, type);
4292 pclose(pipe);
4293 return result;
4294 }
4296 static bool
4297 status_update_files(struct view *view, struct line *line)
4298 {
4299 FILE *pipe = status_update_prepare(line->type);
4300 bool result = TRUE;
4301 struct line *pos = view->line + view->lines;
4302 int files = 0;
4303 int file, done;
4305 if (!pipe)
4306 return FALSE;
4308 for (pos = line; pos < view->line + view->lines && pos->data; pos++)
4309 files++;
4311 for (file = 0, done = 0; result && file < files; line++, file++) {
4312 int almost_done = file * 100 / files;
4314 if (almost_done > done) {
4315 done = almost_done;
4316 string_format(view->ref, "updating file %u of %u (%d%% done)",
4317 file, files, done);
4318 update_view_title(view);
4319 }
4320 result = status_update_write(pipe, line->data, line->type);
4321 }
4323 pclose(pipe);
4324 return result;
4325 }
4327 static bool
4328 status_update(struct view *view)
4329 {
4330 struct line *line = &view->line[view->lineno];
4332 assert(view->lines);
4334 if (!line->data) {
4335 /* This should work even for the "On branch" line. */
4336 if (line < view->line + view->lines && !line[1].data) {
4337 report("Nothing to update");
4338 return FALSE;
4339 }
4341 if (!status_update_files(view, line + 1)) {
4342 report("Failed to update file status");
4343 return FALSE;
4344 }
4346 } else if (!status_update_file(line->data, line->type)) {
4347 report("Failed to update file status");
4348 return FALSE;
4349 }
4351 return TRUE;
4352 }
4354 static bool
4355 status_checkout(struct status *status, enum line_type type, bool has_next)
4356 {
4357 if (!status || type != LINE_STAT_UNSTAGED) {
4358 if (has_next) {
4359 report("Nothing to checkout");
4360 } else if (type == LINE_STAT_UNTRACKED) {
4361 report("Cannot checkout untracked files");
4362 } else if (type == LINE_STAT_STAGED) {
4363 report("Cannot checkout staged files");
4364 } else {
4365 report("Cannot checkout multiple files");
4366 }
4367 return FALSE;
4369 } else {
4370 char cmd[SIZEOF_STR];
4371 char file_sq[SIZEOF_STR];
4373 if (sq_quote(file_sq, 0, status->old.name) >= sizeof(file_sq) ||
4374 !string_format(cmd, "git checkout %s%s", opt_cdup, file_sq))
4375 return FALSE;
4377 return run_confirm(cmd, "Are you sure you want to overwrite any changes?");
4378 }
4379 }
4381 static enum request
4382 status_request(struct view *view, enum request request, struct line *line)
4383 {
4384 struct status *status = line->data;
4386 switch (request) {
4387 case REQ_STATUS_UPDATE:
4388 if (!status_update(view))
4389 return REQ_NONE;
4390 break;
4392 case REQ_STATUS_CHECKOUT:
4393 if (!status_checkout(status, line->type, status_has_none(view, line)))
4394 return REQ_NONE;
4395 break;
4397 case REQ_STATUS_MERGE:
4398 if (!status || status->status != 'U') {
4399 report("Merging only possible for files with unmerged status ('U').");
4400 return REQ_NONE;
4401 }
4402 open_mergetool(status->new.name);
4403 break;
4405 case REQ_EDIT:
4406 if (!status)
4407 return request;
4409 open_editor(status->status != '?', status->new.name);
4410 break;
4412 case REQ_VIEW_BLAME:
4413 if (status) {
4414 string_copy(opt_file, status->new.name);
4415 opt_ref[0] = 0;
4416 }
4417 return request;
4419 case REQ_ENTER:
4420 /* After returning the status view has been split to
4421 * show the stage view. No further reloading is
4422 * necessary. */
4423 status_enter(view, line);
4424 return REQ_NONE;
4426 case REQ_REFRESH:
4427 /* Simply reload the view. */
4428 break;
4430 default:
4431 return request;
4432 }
4434 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
4436 return REQ_NONE;
4437 }
4439 static void
4440 status_select(struct view *view, struct line *line)
4441 {
4442 struct status *status = line->data;
4443 char file[SIZEOF_STR] = "all files";
4444 char *text;
4445 char *key;
4447 if (status && !string_format(file, "'%s'", status->new.name))
4448 return;
4450 if (!status && line[1].type == LINE_STAT_NONE)
4451 line++;
4453 switch (line->type) {
4454 case LINE_STAT_STAGED:
4455 text = "Press %s to unstage %s for commit";
4456 break;
4458 case LINE_STAT_UNSTAGED:
4459 text = "Press %s to stage %s for commit";
4460 break;
4462 case LINE_STAT_UNTRACKED:
4463 text = "Press %s to stage %s for addition";
4464 break;
4466 case LINE_STAT_HEAD:
4467 case LINE_STAT_NONE:
4468 text = "Nothing to update";
4469 break;
4471 default:
4472 die("line type %d not handled in switch", line->type);
4473 }
4475 if (status && status->status == 'U') {
4476 text = "Press %s to resolve conflict in %s";
4477 key = get_key(REQ_STATUS_MERGE);
4479 } else {
4480 key = get_key(REQ_STATUS_UPDATE);
4481 }
4483 string_format(view->ref, text, key, file);
4484 }
4486 static bool
4487 status_grep(struct view *view, struct line *line)
4488 {
4489 struct status *status = line->data;
4490 enum { S_STATUS, S_NAME, S_END } state;
4491 char buf[2] = "?";
4492 regmatch_t pmatch;
4494 if (!status)
4495 return FALSE;
4497 for (state = S_STATUS; state < S_END; state++) {
4498 char *text;
4500 switch (state) {
4501 case S_NAME: text = status->new.name; break;
4502 case S_STATUS:
4503 buf[0] = status->status;
4504 text = buf;
4505 break;
4507 default:
4508 return FALSE;
4509 }
4511 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4512 return TRUE;
4513 }
4515 return FALSE;
4516 }
4518 static struct view_ops status_ops = {
4519 "file",
4520 status_open,
4521 NULL,
4522 status_draw,
4523 status_request,
4524 status_grep,
4525 status_select,
4526 };
4529 static bool
4530 stage_diff_line(FILE *pipe, struct line *line)
4531 {
4532 char *buf = line->data;
4533 size_t bufsize = strlen(buf);
4534 size_t written = 0;
4536 while (!ferror(pipe) && written < bufsize) {
4537 written += fwrite(buf + written, 1, bufsize - written, pipe);
4538 }
4540 fputc('\n', pipe);
4542 return written == bufsize;
4543 }
4545 static bool
4546 stage_diff_write(FILE *pipe, struct line *line, struct line *end)
4547 {
4548 while (line < end) {
4549 if (!stage_diff_line(pipe, line++))
4550 return FALSE;
4551 if (line->type == LINE_DIFF_CHUNK ||
4552 line->type == LINE_DIFF_HEADER)
4553 break;
4554 }
4556 return TRUE;
4557 }
4559 static struct line *
4560 stage_diff_find(struct view *view, struct line *line, enum line_type type)
4561 {
4562 for (; view->line < line; line--)
4563 if (line->type == type)
4564 return line;
4566 return NULL;
4567 }
4569 static bool
4570 stage_update_chunk(struct view *view, struct line *chunk)
4571 {
4572 char cmd[SIZEOF_STR];
4573 size_t cmdsize = 0;
4574 struct line *diff_hdr;
4575 FILE *pipe;
4577 diff_hdr = stage_diff_find(view, chunk, LINE_DIFF_HEADER);
4578 if (!diff_hdr)
4579 return FALSE;
4581 if (opt_cdup[0] &&
4582 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4583 return FALSE;
4585 if (!string_format_from(cmd, &cmdsize,
4586 "git apply --whitespace=nowarn --cached %s - && "
4587 "git update-index -q --unmerged --refresh 2>/dev/null",
4588 stage_line_type == LINE_STAT_STAGED ? "-R" : ""))
4589 return FALSE;
4591 pipe = popen(cmd, "w");
4592 if (!pipe)
4593 return FALSE;
4595 if (!stage_diff_write(pipe, diff_hdr, chunk) ||
4596 !stage_diff_write(pipe, chunk, view->line + view->lines))
4597 chunk = NULL;
4599 pclose(pipe);
4601 return chunk ? TRUE : FALSE;
4602 }
4604 static bool
4605 stage_update(struct view *view, struct line *line)
4606 {
4607 struct line *chunk = NULL;
4609 if (!opt_no_head && stage_line_type != LINE_STAT_UNTRACKED)
4610 chunk = stage_diff_find(view, line, LINE_DIFF_CHUNK);
4612 if (chunk) {
4613 if (!stage_update_chunk(view, chunk)) {
4614 report("Failed to apply chunk");
4615 return FALSE;
4616 }
4618 } else if (!stage_status.status) {
4619 view = VIEW(REQ_VIEW_STATUS);
4621 for (line = view->line; line < view->line + view->lines; line++)
4622 if (line->type == stage_line_type)
4623 break;
4625 if (!status_update_files(view, line + 1)) {
4626 report("Failed to update files");
4627 return FALSE;
4628 }
4630 } else if (!status_update_file(&stage_status, stage_line_type)) {
4631 report("Failed to update file");
4632 return FALSE;
4633 }
4635 return TRUE;
4636 }
4638 static void
4639 stage_next(struct view *view, struct line *line)
4640 {
4641 int i;
4643 if (!stage_chunks) {
4644 static size_t alloc = 0;
4645 int *tmp;
4647 for (line = view->line; line < view->line + view->lines; line++) {
4648 if (line->type != LINE_DIFF_CHUNK)
4649 continue;
4651 tmp = realloc_items(stage_chunk, &alloc,
4652 stage_chunks, sizeof(*tmp));
4653 if (!tmp) {
4654 report("Allocation failure");
4655 return;
4656 }
4658 stage_chunk = tmp;
4659 stage_chunk[stage_chunks++] = line - view->line;
4660 }
4661 }
4663 for (i = 0; i < stage_chunks; i++) {
4664 if (stage_chunk[i] > view->lineno) {
4665 do_scroll_view(view, stage_chunk[i] - view->lineno);
4666 report("Chunk %d of %d", i + 1, stage_chunks);
4667 return;
4668 }
4669 }
4671 report("No next chunk found");
4672 }
4674 static enum request
4675 stage_request(struct view *view, enum request request, struct line *line)
4676 {
4677 switch (request) {
4678 case REQ_STATUS_UPDATE:
4679 if (!stage_update(view, line))
4680 return REQ_NONE;
4681 break;
4683 case REQ_STATUS_CHECKOUT:
4684 if (!status_checkout(&stage_status, stage_line_type, FALSE))
4685 return REQ_NONE;
4686 break;
4688 case REQ_STAGE_NEXT:
4689 if (stage_line_type == LINE_STAT_UNTRACKED) {
4690 report("File is untracked; press %s to add",
4691 get_key(REQ_STATUS_UPDATE));
4692 return REQ_NONE;
4693 }
4694 stage_next(view, line);
4695 return REQ_NONE;
4697 case REQ_EDIT:
4698 if (!stage_status.new.name[0])
4699 return request;
4701 open_editor(stage_status.status != '?', stage_status.new.name);
4702 break;
4704 case REQ_REFRESH:
4705 /* Reload everything ... */
4706 break;
4708 case REQ_VIEW_BLAME:
4709 if (stage_status.new.name[0]) {
4710 string_copy(opt_file, stage_status.new.name);
4711 opt_ref[0] = 0;
4712 }
4713 return request;
4715 case REQ_ENTER:
4716 return pager_request(view, request, line);
4718 default:
4719 return request;
4720 }
4722 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD | OPEN_NOMAXIMIZE);
4724 /* Check whether the staged entry still exists, and close the
4725 * stage view if it doesn't. */
4726 if (!status_exists(&stage_status, stage_line_type))
4727 return REQ_VIEW_CLOSE;
4729 if (stage_line_type == LINE_STAT_UNTRACKED)
4730 opt_pipe = fopen(stage_status.new.name, "r");
4731 else
4732 string_copy(opt_cmd, view->cmd);
4733 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | OPEN_NOMAXIMIZE);
4735 return REQ_NONE;
4736 }
4738 static struct view_ops stage_ops = {
4739 "line",
4740 NULL,
4741 pager_read,
4742 pager_draw,
4743 stage_request,
4744 pager_grep,
4745 pager_select,
4746 };
4749 /*
4750 * Revision graph
4751 */
4753 struct commit {
4754 char id[SIZEOF_REV]; /* SHA1 ID. */
4755 char title[128]; /* First line of the commit message. */
4756 char author[75]; /* Author of the commit. */
4757 struct tm time; /* Date from the author ident. */
4758 struct ref **refs; /* Repository references. */
4759 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
4760 size_t graph_size; /* The width of the graph array. */
4761 bool has_parents; /* Rewritten --parents seen. */
4762 };
4764 /* Size of rev graph with no "padding" columns */
4765 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
4767 struct rev_graph {
4768 struct rev_graph *prev, *next, *parents;
4769 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
4770 size_t size;
4771 struct commit *commit;
4772 size_t pos;
4773 unsigned int boundary:1;
4774 };
4776 /* Parents of the commit being visualized. */
4777 static struct rev_graph graph_parents[4];
4779 /* The current stack of revisions on the graph. */
4780 static struct rev_graph graph_stacks[4] = {
4781 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
4782 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
4783 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
4784 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
4785 };
4787 static inline bool
4788 graph_parent_is_merge(struct rev_graph *graph)
4789 {
4790 return graph->parents->size > 1;
4791 }
4793 static inline void
4794 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
4795 {
4796 struct commit *commit = graph->commit;
4798 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
4799 commit->graph[commit->graph_size++] = symbol;
4800 }
4802 static void
4803 clear_rev_graph(struct rev_graph *graph)
4804 {
4805 graph->boundary = 0;
4806 graph->size = graph->pos = 0;
4807 graph->commit = NULL;
4808 memset(graph->parents, 0, sizeof(*graph->parents));
4809 }
4811 static void
4812 done_rev_graph(struct rev_graph *graph)
4813 {
4814 if (graph_parent_is_merge(graph) &&
4815 graph->pos < graph->size - 1 &&
4816 graph->next->size == graph->size + graph->parents->size - 1) {
4817 size_t i = graph->pos + graph->parents->size - 1;
4819 graph->commit->graph_size = i * 2;
4820 while (i < graph->next->size - 1) {
4821 append_to_rev_graph(graph, ' ');
4822 append_to_rev_graph(graph, '\\');
4823 i++;
4824 }
4825 }
4827 clear_rev_graph(graph);
4828 }
4830 static void
4831 push_rev_graph(struct rev_graph *graph, char *parent)
4832 {
4833 int i;
4835 /* "Collapse" duplicate parents lines.
4836 *
4837 * FIXME: This needs to also update update the drawn graph but
4838 * for now it just serves as a method for pruning graph lines. */
4839 for (i = 0; i < graph->size; i++)
4840 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
4841 return;
4843 if (graph->size < SIZEOF_REVITEMS) {
4844 string_copy_rev(graph->rev[graph->size++], parent);
4845 }
4846 }
4848 static chtype
4849 get_rev_graph_symbol(struct rev_graph *graph)
4850 {
4851 chtype symbol;
4853 if (graph->boundary)
4854 symbol = REVGRAPH_BOUND;
4855 else if (graph->parents->size == 0)
4856 symbol = REVGRAPH_INIT;
4857 else if (graph_parent_is_merge(graph))
4858 symbol = REVGRAPH_MERGE;
4859 else if (graph->pos >= graph->size)
4860 symbol = REVGRAPH_BRANCH;
4861 else
4862 symbol = REVGRAPH_COMMIT;
4864 return symbol;
4865 }
4867 static void
4868 draw_rev_graph(struct rev_graph *graph)
4869 {
4870 struct rev_filler {
4871 chtype separator, line;
4872 };
4873 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
4874 static struct rev_filler fillers[] = {
4875 { ' ', '|' },
4876 { '`', '.' },
4877 { '\'', ' ' },
4878 { '/', ' ' },
4879 };
4880 chtype symbol = get_rev_graph_symbol(graph);
4881 struct rev_filler *filler;
4882 size_t i;
4884 if (opt_line_graphics)
4885 fillers[DEFAULT].line = line_graphics[LINE_GRAPHIC_VLINE];
4887 filler = &fillers[DEFAULT];
4889 for (i = 0; i < graph->pos; i++) {
4890 append_to_rev_graph(graph, filler->line);
4891 if (graph_parent_is_merge(graph->prev) &&
4892 graph->prev->pos == i)
4893 filler = &fillers[RSHARP];
4895 append_to_rev_graph(graph, filler->separator);
4896 }
4898 /* Place the symbol for this revision. */
4899 append_to_rev_graph(graph, symbol);
4901 if (graph->prev->size > graph->size)
4902 filler = &fillers[RDIAG];
4903 else
4904 filler = &fillers[DEFAULT];
4906 i++;
4908 for (; i < graph->size; i++) {
4909 append_to_rev_graph(graph, filler->separator);
4910 append_to_rev_graph(graph, filler->line);
4911 if (graph_parent_is_merge(graph->prev) &&
4912 i < graph->prev->pos + graph->parents->size)
4913 filler = &fillers[RSHARP];
4914 if (graph->prev->size > graph->size)
4915 filler = &fillers[LDIAG];
4916 }
4918 if (graph->prev->size > graph->size) {
4919 append_to_rev_graph(graph, filler->separator);
4920 if (filler->line != ' ')
4921 append_to_rev_graph(graph, filler->line);
4922 }
4923 }
4925 /* Prepare the next rev graph */
4926 static void
4927 prepare_rev_graph(struct rev_graph *graph)
4928 {
4929 size_t i;
4931 /* First, traverse all lines of revisions up to the active one. */
4932 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
4933 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
4934 break;
4936 push_rev_graph(graph->next, graph->rev[graph->pos]);
4937 }
4939 /* Interleave the new revision parent(s). */
4940 for (i = 0; !graph->boundary && i < graph->parents->size; i++)
4941 push_rev_graph(graph->next, graph->parents->rev[i]);
4943 /* Lastly, put any remaining revisions. */
4944 for (i = graph->pos + 1; i < graph->size; i++)
4945 push_rev_graph(graph->next, graph->rev[i]);
4946 }
4948 static void
4949 update_rev_graph(struct rev_graph *graph)
4950 {
4951 /* If this is the finalizing update ... */
4952 if (graph->commit)
4953 prepare_rev_graph(graph);
4955 /* Graph visualization needs a one rev look-ahead,
4956 * so the first update doesn't visualize anything. */
4957 if (!graph->prev->commit)
4958 return;
4960 draw_rev_graph(graph->prev);
4961 done_rev_graph(graph->prev->prev);
4962 }
4965 /*
4966 * Main view backend
4967 */
4969 static int load_refs(void);
4971 static bool
4972 main_draw(struct view *view, struct line *line, unsigned int lineno)
4973 {
4974 struct commit *commit = line->data;
4976 if (!*commit->author)
4977 return FALSE;
4979 if (opt_date && draw_date(view, &commit->time))
4980 return TRUE;
4982 if (opt_author &&
4983 draw_field(view, LINE_MAIN_AUTHOR, commit->author, opt_author_cols, TRUE))
4984 return TRUE;
4986 if (opt_rev_graph && commit->graph_size &&
4987 draw_graphic(view, LINE_MAIN_REVGRAPH, commit->graph, commit->graph_size))
4988 return TRUE;
4990 if (opt_show_refs && commit->refs) {
4991 size_t i = 0;
4993 do {
4994 enum line_type type;
4996 if (commit->refs[i]->head)
4997 type = LINE_MAIN_HEAD;
4998 else if (commit->refs[i]->ltag)
4999 type = LINE_MAIN_LOCAL_TAG;
5000 else if (commit->refs[i]->tag)
5001 type = LINE_MAIN_TAG;
5002 else if (commit->refs[i]->tracked)
5003 type = LINE_MAIN_TRACKED;
5004 else if (commit->refs[i]->remote)
5005 type = LINE_MAIN_REMOTE;
5006 else
5007 type = LINE_MAIN_REF;
5009 if (draw_text(view, type, "[", TRUE) ||
5010 draw_text(view, type, commit->refs[i]->name, TRUE) ||
5011 draw_text(view, type, "]", TRUE))
5012 return TRUE;
5014 if (draw_text(view, LINE_DEFAULT, " ", TRUE))
5015 return TRUE;
5016 } while (commit->refs[i++]->next);
5017 }
5019 draw_text(view, LINE_DEFAULT, commit->title, TRUE);
5020 return TRUE;
5021 }
5023 /* Reads git log --pretty=raw output and parses it into the commit struct. */
5024 static bool
5025 main_read(struct view *view, char *line)
5026 {
5027 static struct rev_graph *graph = graph_stacks;
5028 enum line_type type;
5029 struct commit *commit;
5031 if (!line) {
5032 int i;
5034 if (!view->lines && !view->parent)
5035 die("No revisions match the given arguments.");
5036 if (view->lines > 0) {
5037 commit = view->line[view->lines - 1].data;
5038 if (!*commit->author) {
5039 view->lines--;
5040 free(commit);
5041 graph->commit = NULL;
5042 }
5043 }
5044 update_rev_graph(graph);
5046 for (i = 0; i < ARRAY_SIZE(graph_stacks); i++)
5047 clear_rev_graph(&graph_stacks[i]);
5048 return TRUE;
5049 }
5051 type = get_line_type(line);
5052 if (type == LINE_COMMIT) {
5053 commit = calloc(1, sizeof(struct commit));
5054 if (!commit)
5055 return FALSE;
5057 line += STRING_SIZE("commit ");
5058 if (*line == '-') {
5059 graph->boundary = 1;
5060 line++;
5061 }
5063 string_copy_rev(commit->id, line);
5064 commit->refs = get_refs(commit->id);
5065 graph->commit = commit;
5066 add_line_data(view, commit, LINE_MAIN_COMMIT);
5068 while ((line = strchr(line, ' '))) {
5069 line++;
5070 push_rev_graph(graph->parents, line);
5071 commit->has_parents = TRUE;
5072 }
5073 return TRUE;
5074 }
5076 if (!view->lines)
5077 return TRUE;
5078 commit = view->line[view->lines - 1].data;
5080 switch (type) {
5081 case LINE_PARENT:
5082 if (commit->has_parents)
5083 break;
5084 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
5085 break;
5087 case LINE_AUTHOR:
5088 {
5089 /* Parse author lines where the name may be empty:
5090 * author <email@address.tld> 1138474660 +0100
5091 */
5092 char *ident = line + STRING_SIZE("author ");
5093 char *nameend = strchr(ident, '<');
5094 char *emailend = strchr(ident, '>');
5096 if (!nameend || !emailend)
5097 break;
5099 update_rev_graph(graph);
5100 graph = graph->next;
5102 *nameend = *emailend = 0;
5103 ident = chomp_string(ident);
5104 if (!*ident) {
5105 ident = chomp_string(nameend + 1);
5106 if (!*ident)
5107 ident = "Unknown";
5108 }
5110 string_ncopy(commit->author, ident, strlen(ident));
5112 /* Parse epoch and timezone */
5113 if (emailend[1] == ' ') {
5114 char *secs = emailend + 2;
5115 char *zone = strchr(secs, ' ');
5116 time_t time = (time_t) atol(secs);
5118 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
5119 long tz;
5121 zone++;
5122 tz = ('0' - zone[1]) * 60 * 60 * 10;
5123 tz += ('0' - zone[2]) * 60 * 60;
5124 tz += ('0' - zone[3]) * 60;
5125 tz += ('0' - zone[4]) * 60;
5127 if (zone[0] == '-')
5128 tz = -tz;
5130 time -= tz;
5131 }
5133 gmtime_r(&time, &commit->time);
5134 }
5135 break;
5136 }
5137 default:
5138 /* Fill in the commit title if it has not already been set. */
5139 if (commit->title[0])
5140 break;
5142 /* Require titles to start with a non-space character at the
5143 * offset used by git log. */
5144 if (strncmp(line, " ", 4))
5145 break;
5146 line += 4;
5147 /* Well, if the title starts with a whitespace character,
5148 * try to be forgiving. Otherwise we end up with no title. */
5149 while (isspace(*line))
5150 line++;
5151 if (*line == '\0')
5152 break;
5153 /* FIXME: More graceful handling of titles; append "..." to
5154 * shortened titles, etc. */
5156 string_ncopy(commit->title, line, strlen(line));
5157 }
5159 return TRUE;
5160 }
5162 static enum request
5163 main_request(struct view *view, enum request request, struct line *line)
5164 {
5165 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
5167 switch (request) {
5168 case REQ_ENTER:
5169 open_view(view, REQ_VIEW_DIFF, flags);
5170 break;
5171 case REQ_REFRESH:
5172 load_refs();
5173 string_copy(opt_cmd, view->cmd);
5174 open_view(view, REQ_VIEW_MAIN, OPEN_RELOAD);
5175 break;
5176 default:
5177 return request;
5178 }
5180 return REQ_NONE;
5181 }
5183 static bool
5184 grep_refs(struct ref **refs, regex_t *regex)
5185 {
5186 regmatch_t pmatch;
5187 size_t i = 0;
5189 if (!refs)
5190 return FALSE;
5191 do {
5192 if (regexec(regex, refs[i]->name, 1, &pmatch, 0) != REG_NOMATCH)
5193 return TRUE;
5194 } while (refs[i++]->next);
5196 return FALSE;
5197 }
5199 static bool
5200 main_grep(struct view *view, struct line *line)
5201 {
5202 struct commit *commit = line->data;
5203 enum { S_TITLE, S_AUTHOR, S_DATE, S_REFS, S_END } state;
5204 char buf[DATE_COLS + 1];
5205 regmatch_t pmatch;
5207 for (state = S_TITLE; state < S_END; state++) {
5208 char *text;
5210 switch (state) {
5211 case S_TITLE: text = commit->title; break;
5212 case S_AUTHOR:
5213 if (!opt_author)
5214 continue;
5215 text = commit->author;
5216 break;
5217 case S_DATE:
5218 if (!opt_date)
5219 continue;
5220 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
5221 continue;
5222 text = buf;
5223 break;
5224 case S_REFS:
5225 if (!opt_show_refs)
5226 continue;
5227 if (grep_refs(commit->refs, view->regex) == TRUE)
5228 return TRUE;
5229 continue;
5230 default:
5231 return FALSE;
5232 }
5234 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
5235 return TRUE;
5236 }
5238 return FALSE;
5239 }
5241 static void
5242 main_select(struct view *view, struct line *line)
5243 {
5244 struct commit *commit = line->data;
5246 string_copy_rev(view->ref, commit->id);
5247 string_copy_rev(ref_commit, view->ref);
5248 }
5250 static struct view_ops main_ops = {
5251 "commit",
5252 NULL,
5253 main_read,
5254 main_draw,
5255 main_request,
5256 main_grep,
5257 main_select,
5258 };
5261 /*
5262 * Unicode / UTF-8 handling
5263 *
5264 * NOTE: Much of the following code for dealing with unicode is derived from
5265 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
5266 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
5267 */
5269 /* I've (over)annotated a lot of code snippets because I am not entirely
5270 * confident that the approach taken by this small UTF-8 interface is correct.
5271 * --jonas */
5273 static inline int
5274 unicode_width(unsigned long c)
5275 {
5276 if (c >= 0x1100 &&
5277 (c <= 0x115f /* Hangul Jamo */
5278 || c == 0x2329
5279 || c == 0x232a
5280 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
5281 /* CJK ... Yi */
5282 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
5283 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
5284 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
5285 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
5286 || (c >= 0xffe0 && c <= 0xffe6)
5287 || (c >= 0x20000 && c <= 0x2fffd)
5288 || (c >= 0x30000 && c <= 0x3fffd)))
5289 return 2;
5291 if (c == '\t')
5292 return opt_tab_size;
5294 return 1;
5295 }
5297 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
5298 * Illegal bytes are set one. */
5299 static const unsigned char utf8_bytes[256] = {
5300 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,
5301 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,
5302 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,
5303 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,
5304 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,
5305 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,
5306 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,
5307 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,
5308 };
5310 /* Decode UTF-8 multi-byte representation into a unicode character. */
5311 static inline unsigned long
5312 utf8_to_unicode(const char *string, size_t length)
5313 {
5314 unsigned long unicode;
5316 switch (length) {
5317 case 1:
5318 unicode = string[0];
5319 break;
5320 case 2:
5321 unicode = (string[0] & 0x1f) << 6;
5322 unicode += (string[1] & 0x3f);
5323 break;
5324 case 3:
5325 unicode = (string[0] & 0x0f) << 12;
5326 unicode += ((string[1] & 0x3f) << 6);
5327 unicode += (string[2] & 0x3f);
5328 break;
5329 case 4:
5330 unicode = (string[0] & 0x0f) << 18;
5331 unicode += ((string[1] & 0x3f) << 12);
5332 unicode += ((string[2] & 0x3f) << 6);
5333 unicode += (string[3] & 0x3f);
5334 break;
5335 case 5:
5336 unicode = (string[0] & 0x0f) << 24;
5337 unicode += ((string[1] & 0x3f) << 18);
5338 unicode += ((string[2] & 0x3f) << 12);
5339 unicode += ((string[3] & 0x3f) << 6);
5340 unicode += (string[4] & 0x3f);
5341 break;
5342 case 6:
5343 unicode = (string[0] & 0x01) << 30;
5344 unicode += ((string[1] & 0x3f) << 24);
5345 unicode += ((string[2] & 0x3f) << 18);
5346 unicode += ((string[3] & 0x3f) << 12);
5347 unicode += ((string[4] & 0x3f) << 6);
5348 unicode += (string[5] & 0x3f);
5349 break;
5350 default:
5351 die("Invalid unicode length");
5352 }
5354 /* Invalid characters could return the special 0xfffd value but NUL
5355 * should be just as good. */
5356 return unicode > 0xffff ? 0 : unicode;
5357 }
5359 /* Calculates how much of string can be shown within the given maximum width
5360 * and sets trimmed parameter to non-zero value if all of string could not be
5361 * shown. If the reserve flag is TRUE, it will reserve at least one
5362 * trailing character, which can be useful when drawing a delimiter.
5363 *
5364 * Returns the number of bytes to output from string to satisfy max_width. */
5365 static size_t
5366 utf8_length(const char *string, int *width, size_t max_width, int *trimmed, bool reserve)
5367 {
5368 const char *start = string;
5369 const char *end = strchr(string, '\0');
5370 unsigned char last_bytes = 0;
5371 size_t last_ucwidth = 0;
5373 *width = 0;
5374 *trimmed = 0;
5376 while (string < end) {
5377 int c = *(unsigned char *) string;
5378 unsigned char bytes = utf8_bytes[c];
5379 size_t ucwidth;
5380 unsigned long unicode;
5382 if (string + bytes > end)
5383 break;
5385 /* Change representation to figure out whether
5386 * it is a single- or double-width character. */
5388 unicode = utf8_to_unicode(string, bytes);
5389 /* FIXME: Graceful handling of invalid unicode character. */
5390 if (!unicode)
5391 break;
5393 ucwidth = unicode_width(unicode);
5394 *width += ucwidth;
5395 if (*width > max_width) {
5396 *trimmed = 1;
5397 *width -= ucwidth;
5398 if (reserve && *width == max_width) {
5399 string -= last_bytes;
5400 *width -= last_ucwidth;
5401 }
5402 break;
5403 }
5405 string += bytes;
5406 last_bytes = bytes;
5407 last_ucwidth = ucwidth;
5408 }
5410 return string - start;
5411 }
5414 /*
5415 * Status management
5416 */
5418 /* Whether or not the curses interface has been initialized. */
5419 static bool cursed = FALSE;
5421 /* The status window is used for polling keystrokes. */
5422 static WINDOW *status_win;
5424 static bool status_empty = TRUE;
5426 /* Update status and title window. */
5427 static void
5428 report(const char *msg, ...)
5429 {
5430 struct view *view = display[current_view];
5432 if (input_mode)
5433 return;
5435 if (!view) {
5436 char buf[SIZEOF_STR];
5437 va_list args;
5439 va_start(args, msg);
5440 if (vsnprintf(buf, sizeof(buf), msg, args) >= sizeof(buf)) {
5441 buf[sizeof(buf) - 1] = 0;
5442 buf[sizeof(buf) - 2] = '.';
5443 buf[sizeof(buf) - 3] = '.';
5444 buf[sizeof(buf) - 4] = '.';
5445 }
5446 va_end(args);
5447 die("%s", buf);
5448 }
5450 if (!status_empty || *msg) {
5451 va_list args;
5453 va_start(args, msg);
5455 wmove(status_win, 0, 0);
5456 if (*msg) {
5457 vwprintw(status_win, msg, args);
5458 status_empty = FALSE;
5459 } else {
5460 status_empty = TRUE;
5461 }
5462 wclrtoeol(status_win);
5463 wrefresh(status_win);
5465 va_end(args);
5466 }
5468 update_view_title(view);
5469 update_display_cursor(view);
5470 }
5472 /* Controls when nodelay should be in effect when polling user input. */
5473 static void
5474 set_nonblocking_input(bool loading)
5475 {
5476 static unsigned int loading_views;
5478 if ((loading == FALSE && loading_views-- == 1) ||
5479 (loading == TRUE && loading_views++ == 0))
5480 nodelay(status_win, loading);
5481 }
5483 static void
5484 init_display(void)
5485 {
5486 int x, y;
5488 /* Initialize the curses library */
5489 if (isatty(STDIN_FILENO)) {
5490 cursed = !!initscr();
5491 } else {
5492 /* Leave stdin and stdout alone when acting as a pager. */
5493 FILE *io = fopen("/dev/tty", "r+");
5495 if (!io)
5496 die("Failed to open /dev/tty");
5497 cursed = !!newterm(NULL, io, io);
5498 }
5500 if (!cursed)
5501 die("Failed to initialize curses");
5503 nonl(); /* Tell curses not to do NL->CR/NL on output */
5504 cbreak(); /* Take input chars one at a time, no wait for \n */
5505 noecho(); /* Don't echo input */
5506 leaveok(stdscr, TRUE);
5508 if (has_colors())
5509 init_colors();
5511 getmaxyx(stdscr, y, x);
5512 status_win = newwin(1, 0, y - 1, 0);
5513 if (!status_win)
5514 die("Failed to create status window");
5516 /* Enable keyboard mapping */
5517 keypad(status_win, TRUE);
5518 wbkgdset(status_win, get_line_attr(LINE_STATUS));
5520 TABSIZE = opt_tab_size;
5521 if (opt_line_graphics) {
5522 line_graphics[LINE_GRAPHIC_VLINE] = ACS_VLINE;
5523 }
5524 }
5526 static bool
5527 prompt_yesno(const char *prompt)
5528 {
5529 enum { WAIT, STOP, CANCEL } status = WAIT;
5530 bool answer = FALSE;
5532 while (status == WAIT) {
5533 struct view *view;
5534 int i, key;
5536 input_mode = TRUE;
5538 foreach_view (view, i)
5539 update_view(view);
5541 input_mode = FALSE;
5543 mvwprintw(status_win, 0, 0, "%s [Yy]/[Nn]", prompt);
5544 wclrtoeol(status_win);
5546 /* Refresh, accept single keystroke of input */
5547 key = wgetch(status_win);
5548 switch (key) {
5549 case ERR:
5550 break;
5552 case 'y':
5553 case 'Y':
5554 answer = TRUE;
5555 status = STOP;
5556 break;
5558 case KEY_ESC:
5559 case KEY_RETURN:
5560 case KEY_ENTER:
5561 case KEY_BACKSPACE:
5562 case 'n':
5563 case 'N':
5564 case '\n':
5565 default:
5566 answer = FALSE;
5567 status = CANCEL;
5568 }
5569 }
5571 /* Clear the status window */
5572 status_empty = FALSE;
5573 report("");
5575 return answer;
5576 }
5578 static char *
5579 read_prompt(const char *prompt)
5580 {
5581 enum { READING, STOP, CANCEL } status = READING;
5582 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
5583 int pos = 0;
5585 while (status == READING) {
5586 struct view *view;
5587 int i, key;
5589 input_mode = TRUE;
5591 foreach_view (view, i)
5592 update_view(view);
5594 input_mode = FALSE;
5596 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
5597 wclrtoeol(status_win);
5599 /* Refresh, accept single keystroke of input */
5600 key = wgetch(status_win);
5601 switch (key) {
5602 case KEY_RETURN:
5603 case KEY_ENTER:
5604 case '\n':
5605 status = pos ? STOP : CANCEL;
5606 break;
5608 case KEY_BACKSPACE:
5609 if (pos > 0)
5610 pos--;
5611 else
5612 status = CANCEL;
5613 break;
5615 case KEY_ESC:
5616 status = CANCEL;
5617 break;
5619 case ERR:
5620 break;
5622 default:
5623 if (pos >= sizeof(buf)) {
5624 report("Input string too long");
5625 return NULL;
5626 }
5628 if (isprint(key))
5629 buf[pos++] = (char) key;
5630 }
5631 }
5633 /* Clear the status window */
5634 status_empty = FALSE;
5635 report("");
5637 if (status == CANCEL)
5638 return NULL;
5640 buf[pos++] = 0;
5642 return buf;
5643 }
5645 /*
5646 * Repository references
5647 */
5649 static struct ref *refs = NULL;
5650 static size_t refs_alloc = 0;
5651 static size_t refs_size = 0;
5653 /* Id <-> ref store */
5654 static struct ref ***id_refs = NULL;
5655 static size_t id_refs_alloc = 0;
5656 static size_t id_refs_size = 0;
5658 static struct ref **
5659 get_refs(char *id)
5660 {
5661 struct ref ***tmp_id_refs;
5662 struct ref **ref_list = NULL;
5663 size_t ref_list_alloc = 0;
5664 size_t ref_list_size = 0;
5665 size_t i;
5667 for (i = 0; i < id_refs_size; i++)
5668 if (!strcmp(id, id_refs[i][0]->id))
5669 return id_refs[i];
5671 tmp_id_refs = realloc_items(id_refs, &id_refs_alloc, id_refs_size + 1,
5672 sizeof(*id_refs));
5673 if (!tmp_id_refs)
5674 return NULL;
5676 id_refs = tmp_id_refs;
5678 for (i = 0; i < refs_size; i++) {
5679 struct ref **tmp;
5681 if (strcmp(id, refs[i].id))
5682 continue;
5684 tmp = realloc_items(ref_list, &ref_list_alloc,
5685 ref_list_size + 1, sizeof(*ref_list));
5686 if (!tmp) {
5687 if (ref_list)
5688 free(ref_list);
5689 return NULL;
5690 }
5692 ref_list = tmp;
5693 if (ref_list_size > 0)
5694 ref_list[ref_list_size - 1]->next = 1;
5695 ref_list[ref_list_size] = &refs[i];
5697 /* XXX: The properties of the commit chains ensures that we can
5698 * safely modify the shared ref. The repo references will
5699 * always be similar for the same id. */
5700 ref_list[ref_list_size]->next = 0;
5701 ref_list_size++;
5702 }
5704 if (ref_list)
5705 id_refs[id_refs_size++] = ref_list;
5707 return ref_list;
5708 }
5710 static int
5711 read_ref(char *id, size_t idlen, char *name, size_t namelen)
5712 {
5713 struct ref *ref;
5714 bool tag = FALSE;
5715 bool ltag = FALSE;
5716 bool remote = FALSE;
5717 bool tracked = FALSE;
5718 bool check_replace = FALSE;
5719 bool head = FALSE;
5721 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
5722 if (!strcmp(name + namelen - 3, "^{}")) {
5723 namelen -= 3;
5724 name[namelen] = 0;
5725 if (refs_size > 0 && refs[refs_size - 1].ltag == TRUE)
5726 check_replace = TRUE;
5727 } else {
5728 ltag = TRUE;
5729 }
5731 tag = TRUE;
5732 namelen -= STRING_SIZE("refs/tags/");
5733 name += STRING_SIZE("refs/tags/");
5735 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
5736 remote = TRUE;
5737 namelen -= STRING_SIZE("refs/remotes/");
5738 name += STRING_SIZE("refs/remotes/");
5739 tracked = !strcmp(opt_remote, name);
5741 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5742 namelen -= STRING_SIZE("refs/heads/");
5743 name += STRING_SIZE("refs/heads/");
5744 head = !strncmp(opt_head, name, namelen);
5746 } else if (!strcmp(name, "HEAD")) {
5747 opt_no_head = FALSE;
5748 return OK;
5749 }
5751 if (check_replace && !strcmp(name, refs[refs_size - 1].name)) {
5752 /* it's an annotated tag, replace the previous sha1 with the
5753 * resolved commit id; relies on the fact git-ls-remote lists
5754 * the commit id of an annotated tag right beofre the commit id
5755 * it points to. */
5756 refs[refs_size - 1].ltag = ltag;
5757 string_copy_rev(refs[refs_size - 1].id, id);
5759 return OK;
5760 }
5761 refs = realloc_items(refs, &refs_alloc, refs_size + 1, sizeof(*refs));
5762 if (!refs)
5763 return ERR;
5765 ref = &refs[refs_size++];
5766 ref->name = malloc(namelen + 1);
5767 if (!ref->name)
5768 return ERR;
5770 strncpy(ref->name, name, namelen);
5771 ref->name[namelen] = 0;
5772 ref->head = head;
5773 ref->tag = tag;
5774 ref->ltag = ltag;
5775 ref->remote = remote;
5776 ref->tracked = tracked;
5777 string_copy_rev(ref->id, id);
5779 return OK;
5780 }
5782 static int
5783 load_refs(void)
5784 {
5785 const char *cmd_env = getenv("TIG_LS_REMOTE");
5786 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
5788 if (!*opt_git_dir)
5789 return OK;
5791 while (refs_size > 0)
5792 free(refs[--refs_size].name);
5793 while (id_refs_size > 0)
5794 free(id_refs[--id_refs_size]);
5796 return read_properties(popen(cmd, "r"), "\t", read_ref);
5797 }
5799 static int
5800 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
5801 {
5802 if (!strcmp(name, "i18n.commitencoding"))
5803 string_ncopy(opt_encoding, value, valuelen);
5805 if (!strcmp(name, "core.editor"))
5806 string_ncopy(opt_editor, value, valuelen);
5808 /* branch.<head>.remote */
5809 if (*opt_head &&
5810 !strncmp(name, "branch.", 7) &&
5811 !strncmp(name + 7, opt_head, strlen(opt_head)) &&
5812 !strcmp(name + 7 + strlen(opt_head), ".remote"))
5813 string_ncopy(opt_remote, value, valuelen);
5815 if (*opt_head && *opt_remote &&
5816 !strncmp(name, "branch.", 7) &&
5817 !strncmp(name + 7, opt_head, strlen(opt_head)) &&
5818 !strcmp(name + 7 + strlen(opt_head), ".merge")) {
5819 size_t from = strlen(opt_remote);
5821 if (!strncmp(value, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5822 value += STRING_SIZE("refs/heads/");
5823 valuelen -= STRING_SIZE("refs/heads/");
5824 }
5826 if (!string_format_from(opt_remote, &from, "/%s", value))
5827 opt_remote[0] = 0;
5828 }
5830 return OK;
5831 }
5833 static int
5834 load_git_config(void)
5835 {
5836 return read_properties(popen("git " GIT_CONFIG " --list", "r"),
5837 "=", read_repo_config_option);
5838 }
5840 static int
5841 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
5842 {
5843 if (!opt_git_dir[0]) {
5844 string_ncopy(opt_git_dir, name, namelen);
5846 } else if (opt_is_inside_work_tree == -1) {
5847 /* This can be 3 different values depending on the
5848 * version of git being used. If git-rev-parse does not
5849 * understand --is-inside-work-tree it will simply echo
5850 * the option else either "true" or "false" is printed.
5851 * Default to true for the unknown case. */
5852 opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
5854 } else if (opt_cdup[0] == ' ') {
5855 string_ncopy(opt_cdup, name, namelen);
5856 } else {
5857 if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5858 namelen -= STRING_SIZE("refs/heads/");
5859 name += STRING_SIZE("refs/heads/");
5860 string_ncopy(opt_head, name, namelen);
5861 }
5862 }
5864 return OK;
5865 }
5867 static int
5868 load_repo_info(void)
5869 {
5870 int result;
5871 FILE *pipe = popen("(git rev-parse --git-dir --is-inside-work-tree "
5872 " --show-cdup; git symbolic-ref HEAD) 2>/dev/null", "r");
5874 /* XXX: The line outputted by "--show-cdup" can be empty so
5875 * initialize it to something invalid to make it possible to
5876 * detect whether it has been set or not. */
5877 opt_cdup[0] = ' ';
5879 result = read_properties(pipe, "=", read_repo_info);
5880 if (opt_cdup[0] == ' ')
5881 opt_cdup[0] = 0;
5883 return result;
5884 }
5886 static int
5887 read_properties(FILE *pipe, const char *separators,
5888 int (*read_property)(char *, size_t, char *, size_t))
5889 {
5890 char buffer[BUFSIZ];
5891 char *name;
5892 int state = OK;
5894 if (!pipe)
5895 return ERR;
5897 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
5898 char *value;
5899 size_t namelen;
5900 size_t valuelen;
5902 name = chomp_string(name);
5903 namelen = strcspn(name, separators);
5905 if (name[namelen]) {
5906 name[namelen] = 0;
5907 value = chomp_string(name + namelen + 1);
5908 valuelen = strlen(value);
5910 } else {
5911 value = "";
5912 valuelen = 0;
5913 }
5915 state = read_property(name, namelen, value, valuelen);
5916 }
5918 if (state != ERR && ferror(pipe))
5919 state = ERR;
5921 pclose(pipe);
5923 return state;
5924 }
5927 /*
5928 * Main
5929 */
5931 static void __NORETURN
5932 quit(int sig)
5933 {
5934 /* XXX: Restore tty modes and let the OS cleanup the rest! */
5935 if (cursed)
5936 endwin();
5937 exit(0);
5938 }
5940 static void __NORETURN
5941 die(const char *err, ...)
5942 {
5943 va_list args;
5945 endwin();
5947 va_start(args, err);
5948 fputs("tig: ", stderr);
5949 vfprintf(stderr, err, args);
5950 fputs("\n", stderr);
5951 va_end(args);
5953 exit(1);
5954 }
5956 static void
5957 warn(const char *msg, ...)
5958 {
5959 va_list args;
5961 va_start(args, msg);
5962 fputs("tig warning: ", stderr);
5963 vfprintf(stderr, msg, args);
5964 fputs("\n", stderr);
5965 va_end(args);
5966 }
5968 int
5969 main(int argc, char *argv[])
5970 {
5971 struct view *view;
5972 enum request request;
5973 size_t i;
5975 signal(SIGINT, quit);
5977 if (setlocale(LC_ALL, "")) {
5978 char *codeset = nl_langinfo(CODESET);
5980 string_ncopy(opt_codeset, codeset, strlen(codeset));
5981 }
5983 if (load_repo_info() == ERR)
5984 die("Failed to load repo info.");
5986 if (load_options() == ERR)
5987 die("Failed to load user config.");
5989 if (load_git_config() == ERR)
5990 die("Failed to load repo config.");
5992 request = parse_options(argc, argv);
5993 if (request == REQ_NONE)
5994 return 0;
5996 /* Require a git repository unless when running in pager mode. */
5997 if (!opt_git_dir[0] && request != REQ_VIEW_PAGER)
5998 die("Not a git repository");
6000 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
6001 opt_utf8 = FALSE;
6003 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
6004 opt_iconv = iconv_open(opt_codeset, opt_encoding);
6005 if (opt_iconv == ICONV_NONE)
6006 die("Failed to initialize character set conversion");
6007 }
6009 if (load_refs() == ERR)
6010 die("Failed to load refs.");
6012 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
6013 view->cmd_env = getenv(view->cmd_env);
6015 init_display();
6017 while (view_driver(display[current_view], request)) {
6018 int key;
6019 int i;
6021 foreach_view (view, i)
6022 update_view(view);
6024 /* Refresh, accept single keystroke of input */
6025 key = wgetch(status_win);
6027 /* wgetch() with nodelay() enabled returns ERR when there's no
6028 * input. */
6029 if (key == ERR) {
6030 request = REQ_NONE;
6031 continue;
6032 }
6034 request = get_keybinding(display[current_view]->keymap, key);
6036 /* Some low-level request handling. This keeps access to
6037 * status_win restricted. */
6038 switch (request) {
6039 case REQ_PROMPT:
6040 {
6041 char *cmd = read_prompt(":");
6043 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
6044 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
6045 request = REQ_VIEW_DIFF;
6046 } else {
6047 request = REQ_VIEW_PAGER;
6048 }
6050 /* Always reload^Wrerun commands from the prompt. */
6051 open_view(view, request, OPEN_RELOAD);
6052 }
6054 request = REQ_NONE;
6055 break;
6056 }
6057 case REQ_SEARCH:
6058 case REQ_SEARCH_BACK:
6059 {
6060 const char *prompt = request == REQ_SEARCH
6061 ? "/" : "?";
6062 char *search = read_prompt(prompt);
6064 if (search)
6065 string_ncopy(opt_search, search, strlen(search));
6066 else
6067 request = REQ_NONE;
6068 break;
6069 }
6070 case REQ_SCREEN_RESIZE:
6071 {
6072 int height, width;
6074 getmaxyx(stdscr, height, width);
6076 /* Resize the status view and let the view driver take
6077 * care of resizing the displayed views. */
6078 wresize(status_win, 1, width);
6079 mvwin(status_win, height - 1, 0);
6080 wrefresh(status_win);
6081 break;
6082 }
6083 default:
6084 break;
6085 }
6086 }
6088 quit(0);
6090 return 0;
6091 }