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