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 TAB_SIZE 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_line_graphics = TRUE;
442 static bool opt_rev_graph = FALSE;
443 static bool opt_show_refs = TRUE;
444 static int opt_num_interval = NUMBER_INTERVAL;
445 static int opt_tab_size = TAB_SIZE;
446 static enum request opt_request = REQ_VIEW_MAIN;
447 static char opt_cmd[SIZEOF_STR] = "";
448 static char opt_path[SIZEOF_STR] = "";
449 static char opt_file[SIZEOF_STR] = "";
450 static char opt_ref[SIZEOF_REF] = "";
451 static char opt_head[SIZEOF_REF] = "";
452 static char opt_remote[SIZEOF_REF] = "";
453 static bool opt_no_head = TRUE;
454 static FILE *opt_pipe = NULL;
455 static char opt_encoding[20] = "UTF-8";
456 static bool opt_utf8 = TRUE;
457 static char opt_codeset[20] = "UTF-8";
458 static iconv_t opt_iconv = ICONV_NONE;
459 static char opt_search[SIZEOF_STR] = "";
460 static char opt_cdup[SIZEOF_STR] = "";
461 static char opt_git_dir[SIZEOF_STR] = "";
462 static signed char opt_is_inside_work_tree = -1; /* set to TRUE or FALSE */
463 static char opt_editor[SIZEOF_STR] = "";
465 static bool
466 parse_options(int argc, char *argv[])
467 {
468 size_t buf_size;
469 char *subcommand;
470 bool seen_dashdash = FALSE;
471 int i;
473 if (!isatty(STDIN_FILENO)) {
474 opt_request = REQ_VIEW_PAGER;
475 opt_pipe = stdin;
476 return TRUE;
477 }
479 if (argc <= 1)
480 return TRUE;
482 subcommand = argv[1];
483 if (!strcmp(subcommand, "status") || !strcmp(subcommand, "-S")) {
484 opt_request = REQ_VIEW_STATUS;
485 if (!strcmp(subcommand, "-S"))
486 warn("`-S' has been deprecated; use `tig status' instead");
487 if (argc > 2)
488 warn("ignoring arguments after `%s'", subcommand);
489 return TRUE;
491 } else if (!strcmp(subcommand, "blame")) {
492 opt_request = REQ_VIEW_BLAME;
493 if (argc <= 2 || argc > 4)
494 die("invalid number of options to blame\n\n%s", usage);
496 i = 2;
497 if (argc == 4) {
498 string_ncopy(opt_ref, argv[i], strlen(argv[i]));
499 i++;
500 }
502 string_ncopy(opt_file, argv[i], strlen(argv[i]));
503 return TRUE;
505 } else if (!strcmp(subcommand, "show")) {
506 opt_request = REQ_VIEW_DIFF;
508 } else if (!strcmp(subcommand, "log") || !strcmp(subcommand, "diff")) {
509 opt_request = subcommand[0] == 'l'
510 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
511 warn("`tig %s' has been deprecated", subcommand);
513 } else {
514 subcommand = NULL;
515 }
517 if (!subcommand)
518 /* XXX: This is vulnerable to the user overriding
519 * options required for the main view parser. */
520 string_copy(opt_cmd, "git log --no-color --pretty=raw --boundary --parents");
521 else
522 string_format(opt_cmd, "git %s", subcommand);
524 buf_size = strlen(opt_cmd);
526 for (i = 1 + !!subcommand; i < argc; i++) {
527 char *opt = argv[i];
529 if (seen_dashdash || !strcmp(opt, "--")) {
530 seen_dashdash = TRUE;
532 } else if (!strcmp(opt, "-v") || !strcmp(opt, "--version")) {
533 printf("tig version %s\n", TIG_VERSION);
534 return FALSE;
536 } else if (!strcmp(opt, "-h") || !strcmp(opt, "--help")) {
537 printf("%s\n", usage);
538 return FALSE;
539 }
541 opt_cmd[buf_size++] = ' ';
542 buf_size = sq_quote(opt_cmd, buf_size, opt);
543 if (buf_size >= sizeof(opt_cmd))
544 die("command too long");
545 }
547 opt_cmd[buf_size] = 0;
549 return TRUE;
550 }
553 /*
554 * Line-oriented content detection.
555 */
557 #define LINE_INFO \
558 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
559 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
560 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
561 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
562 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
563 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
564 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
565 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
566 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
567 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
568 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
569 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
570 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
571 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
572 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
573 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
574 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
575 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
576 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
577 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
578 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
579 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
580 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
581 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
582 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
583 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
584 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
585 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
586 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
587 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
588 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
589 LINE(DELIMITER, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
590 LINE(DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
591 LINE(LINE_NUMBER, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
592 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
593 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
594 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
595 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
596 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
597 LINE(MAIN_LOCAL_TAG,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
598 LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \
599 LINE(MAIN_TRACKED, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
600 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
601 LINE(MAIN_HEAD, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
602 LINE(MAIN_REVGRAPH,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
603 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
604 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
605 LINE(STAT_HEAD, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \
606 LINE(STAT_SECTION, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
607 LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
608 LINE(STAT_STAGED, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
609 LINE(STAT_UNSTAGED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
610 LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
611 LINE(BLAME_ID, "", COLOR_MAGENTA, COLOR_DEFAULT, 0)
613 enum line_type {
614 #define LINE(type, line, fg, bg, attr) \
615 LINE_##type
616 LINE_INFO
617 #undef LINE
618 };
620 struct line_info {
621 const char *name; /* Option name. */
622 int namelen; /* Size of option name. */
623 const char *line; /* The start of line to match. */
624 int linelen; /* Size of string to match. */
625 int fg, bg, attr; /* Color and text attributes for the lines. */
626 };
628 static struct line_info line_info[] = {
629 #define LINE(type, line, fg, bg, attr) \
630 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
631 LINE_INFO
632 #undef LINE
633 };
635 static enum line_type
636 get_line_type(char *line)
637 {
638 int linelen = strlen(line);
639 enum line_type type;
641 for (type = 0; type < ARRAY_SIZE(line_info); type++)
642 /* Case insensitive search matches Signed-off-by lines better. */
643 if (linelen >= line_info[type].linelen &&
644 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
645 return type;
647 return LINE_DEFAULT;
648 }
650 static inline int
651 get_line_attr(enum line_type type)
652 {
653 assert(type < ARRAY_SIZE(line_info));
654 return COLOR_PAIR(type) | line_info[type].attr;
655 }
657 static struct line_info *
658 get_line_info(char *name)
659 {
660 size_t namelen = strlen(name);
661 enum line_type type;
663 for (type = 0; type < ARRAY_SIZE(line_info); type++)
664 if (namelen == line_info[type].namelen &&
665 !string_enum_compare(line_info[type].name, name, namelen))
666 return &line_info[type];
668 return NULL;
669 }
671 static void
672 init_colors(void)
673 {
674 int default_bg = line_info[LINE_DEFAULT].bg;
675 int default_fg = line_info[LINE_DEFAULT].fg;
676 enum line_type type;
678 start_color();
680 if (assume_default_colors(default_fg, default_bg) == ERR) {
681 default_bg = COLOR_BLACK;
682 default_fg = COLOR_WHITE;
683 }
685 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
686 struct line_info *info = &line_info[type];
687 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
688 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
690 init_pair(type, fg, bg);
691 }
692 }
694 struct line {
695 enum line_type type;
697 /* State flags */
698 unsigned int selected:1;
699 unsigned int dirty:1;
701 void *data; /* User data */
702 };
705 /*
706 * Keys
707 */
709 struct keybinding {
710 int alias;
711 enum request request;
712 struct keybinding *next;
713 };
715 static struct keybinding default_keybindings[] = {
716 /* View switching */
717 { 'm', REQ_VIEW_MAIN },
718 { 'd', REQ_VIEW_DIFF },
719 { 'l', REQ_VIEW_LOG },
720 { 't', REQ_VIEW_TREE },
721 { 'f', REQ_VIEW_BLOB },
722 { 'B', REQ_VIEW_BLAME },
723 { 'p', REQ_VIEW_PAGER },
724 { 'h', REQ_VIEW_HELP },
725 { 'S', REQ_VIEW_STATUS },
726 { 'c', REQ_VIEW_STAGE },
728 /* View manipulation */
729 { 'q', REQ_VIEW_CLOSE },
730 { KEY_TAB, REQ_VIEW_NEXT },
731 { KEY_RETURN, REQ_ENTER },
732 { KEY_UP, REQ_PREVIOUS },
733 { KEY_DOWN, REQ_NEXT },
734 { 'R', REQ_REFRESH },
735 { KEY_F(5), REQ_REFRESH },
736 { 'O', REQ_MAXIMIZE },
738 /* Cursor navigation */
739 { 'k', REQ_MOVE_UP },
740 { 'j', REQ_MOVE_DOWN },
741 { KEY_HOME, REQ_MOVE_FIRST_LINE },
742 { KEY_END, REQ_MOVE_LAST_LINE },
743 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
744 { ' ', REQ_MOVE_PAGE_DOWN },
745 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
746 { 'b', REQ_MOVE_PAGE_UP },
747 { '-', REQ_MOVE_PAGE_UP },
749 /* Scrolling */
750 { KEY_IC, REQ_SCROLL_LINE_UP },
751 { KEY_DC, REQ_SCROLL_LINE_DOWN },
752 { 'w', REQ_SCROLL_PAGE_UP },
753 { 's', REQ_SCROLL_PAGE_DOWN },
755 /* Searching */
756 { '/', REQ_SEARCH },
757 { '?', REQ_SEARCH_BACK },
758 { 'n', REQ_FIND_NEXT },
759 { 'N', REQ_FIND_PREV },
761 /* Misc */
762 { 'Q', REQ_QUIT },
763 { 'z', REQ_STOP_LOADING },
764 { 'v', REQ_SHOW_VERSION },
765 { 'r', REQ_SCREEN_REDRAW },
766 { '.', REQ_TOGGLE_LINENO },
767 { 'D', REQ_TOGGLE_DATE },
768 { 'A', REQ_TOGGLE_AUTHOR },
769 { 'g', REQ_TOGGLE_REV_GRAPH },
770 { 'F', REQ_TOGGLE_REFS },
771 { ':', REQ_PROMPT },
772 { 'u', REQ_STATUS_UPDATE },
773 { 'M', REQ_STATUS_MERGE },
774 { ',', REQ_TREE_PARENT },
775 { 'e', REQ_EDIT },
777 /* Using the ncurses SIGWINCH handler. */
778 { KEY_RESIZE, REQ_SCREEN_RESIZE },
779 };
781 #define KEYMAP_INFO \
782 KEYMAP_(GENERIC), \
783 KEYMAP_(MAIN), \
784 KEYMAP_(DIFF), \
785 KEYMAP_(LOG), \
786 KEYMAP_(TREE), \
787 KEYMAP_(BLOB), \
788 KEYMAP_(BLAME), \
789 KEYMAP_(PAGER), \
790 KEYMAP_(HELP), \
791 KEYMAP_(STATUS), \
792 KEYMAP_(STAGE)
794 enum keymap {
795 #define KEYMAP_(name) KEYMAP_##name
796 KEYMAP_INFO
797 #undef KEYMAP_
798 };
800 static struct int_map keymap_table[] = {
801 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
802 KEYMAP_INFO
803 #undef KEYMAP_
804 };
806 #define set_keymap(map, name) \
807 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
809 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
811 static void
812 add_keybinding(enum keymap keymap, enum request request, int key)
813 {
814 struct keybinding *keybinding;
816 keybinding = calloc(1, sizeof(*keybinding));
817 if (!keybinding)
818 die("Failed to allocate keybinding");
820 keybinding->alias = key;
821 keybinding->request = request;
822 keybinding->next = keybindings[keymap];
823 keybindings[keymap] = keybinding;
824 }
826 /* Looks for a key binding first in the given map, then in the generic map, and
827 * lastly in the default keybindings. */
828 static enum request
829 get_keybinding(enum keymap keymap, int key)
830 {
831 struct keybinding *kbd;
832 int i;
834 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
835 if (kbd->alias == key)
836 return kbd->request;
838 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
839 if (kbd->alias == key)
840 return kbd->request;
842 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
843 if (default_keybindings[i].alias == key)
844 return default_keybindings[i].request;
846 return (enum request) key;
847 }
850 struct key {
851 char *name;
852 int value;
853 };
855 static struct key key_table[] = {
856 { "Enter", KEY_RETURN },
857 { "Space", ' ' },
858 { "Backspace", KEY_BACKSPACE },
859 { "Tab", KEY_TAB },
860 { "Escape", KEY_ESC },
861 { "Left", KEY_LEFT },
862 { "Right", KEY_RIGHT },
863 { "Up", KEY_UP },
864 { "Down", KEY_DOWN },
865 { "Insert", KEY_IC },
866 { "Delete", KEY_DC },
867 { "Hash", '#' },
868 { "Home", KEY_HOME },
869 { "End", KEY_END },
870 { "PageUp", KEY_PPAGE },
871 { "PageDown", KEY_NPAGE },
872 { "F1", KEY_F(1) },
873 { "F2", KEY_F(2) },
874 { "F3", KEY_F(3) },
875 { "F4", KEY_F(4) },
876 { "F5", KEY_F(5) },
877 { "F6", KEY_F(6) },
878 { "F7", KEY_F(7) },
879 { "F8", KEY_F(8) },
880 { "F9", KEY_F(9) },
881 { "F10", KEY_F(10) },
882 { "F11", KEY_F(11) },
883 { "F12", KEY_F(12) },
884 };
886 static int
887 get_key_value(const char *name)
888 {
889 int i;
891 for (i = 0; i < ARRAY_SIZE(key_table); i++)
892 if (!strcasecmp(key_table[i].name, name))
893 return key_table[i].value;
895 if (strlen(name) == 1 && isprint(*name))
896 return (int) *name;
898 return ERR;
899 }
901 static char *
902 get_key_name(int key_value)
903 {
904 static char key_char[] = "'X'";
905 char *seq = NULL;
906 int key;
908 for (key = 0; key < ARRAY_SIZE(key_table); key++)
909 if (key_table[key].value == key_value)
910 seq = key_table[key].name;
912 if (seq == NULL &&
913 key_value < 127 &&
914 isprint(key_value)) {
915 key_char[1] = (char) key_value;
916 seq = key_char;
917 }
919 return seq ? seq : "'?'";
920 }
922 static char *
923 get_key(enum request request)
924 {
925 static char buf[BUFSIZ];
926 size_t pos = 0;
927 char *sep = "";
928 int i;
930 buf[pos] = 0;
932 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
933 struct keybinding *keybinding = &default_keybindings[i];
935 if (keybinding->request != request)
936 continue;
938 if (!string_format_from(buf, &pos, "%s%s", sep,
939 get_key_name(keybinding->alias)))
940 return "Too many keybindings!";
941 sep = ", ";
942 }
944 return buf;
945 }
947 struct run_request {
948 enum keymap keymap;
949 int key;
950 char cmd[SIZEOF_STR];
951 };
953 static struct run_request *run_request;
954 static size_t run_requests;
956 static enum request
957 add_run_request(enum keymap keymap, int key, int argc, char **argv)
958 {
959 struct run_request *tmp;
960 struct run_request req = { keymap, key };
961 size_t bufpos;
963 for (bufpos = 0; argc > 0; argc--, argv++)
964 if (!string_format_from(req.cmd, &bufpos, "%s ", *argv))
965 return REQ_NONE;
967 req.cmd[bufpos - 1] = 0;
969 tmp = realloc(run_request, (run_requests + 1) * sizeof(*run_request));
970 if (!tmp)
971 return REQ_NONE;
973 run_request = tmp;
974 run_request[run_requests++] = req;
976 return REQ_NONE + run_requests;
977 }
979 static struct run_request *
980 get_run_request(enum request request)
981 {
982 if (request <= REQ_NONE)
983 return NULL;
984 return &run_request[request - REQ_NONE - 1];
985 }
987 static void
988 add_builtin_run_requests(void)
989 {
990 struct {
991 enum keymap keymap;
992 int key;
993 char *argv[1];
994 } reqs[] = {
995 { KEYMAP_MAIN, 'C', { "git cherry-pick %(commit)" } },
996 { KEYMAP_GENERIC, 'G', { "git gc" } },
997 };
998 int i;
1000 for (i = 0; i < ARRAY_SIZE(reqs); i++) {
1001 enum request req;
1003 req = add_run_request(reqs[i].keymap, reqs[i].key, 1, reqs[i].argv);
1004 if (req != REQ_NONE)
1005 add_keybinding(reqs[i].keymap, req, reqs[i].key);
1006 }
1007 }
1009 /*
1010 * User config file handling.
1011 */
1013 static struct int_map color_map[] = {
1014 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
1015 COLOR_MAP(DEFAULT),
1016 COLOR_MAP(BLACK),
1017 COLOR_MAP(BLUE),
1018 COLOR_MAP(CYAN),
1019 COLOR_MAP(GREEN),
1020 COLOR_MAP(MAGENTA),
1021 COLOR_MAP(RED),
1022 COLOR_MAP(WHITE),
1023 COLOR_MAP(YELLOW),
1024 };
1026 #define set_color(color, name) \
1027 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
1029 static struct int_map attr_map[] = {
1030 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
1031 ATTR_MAP(NORMAL),
1032 ATTR_MAP(BLINK),
1033 ATTR_MAP(BOLD),
1034 ATTR_MAP(DIM),
1035 ATTR_MAP(REVERSE),
1036 ATTR_MAP(STANDOUT),
1037 ATTR_MAP(UNDERLINE),
1038 };
1040 #define set_attribute(attr, name) \
1041 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
1043 static int config_lineno;
1044 static bool config_errors;
1045 static char *config_msg;
1047 /* Wants: object fgcolor bgcolor [attr] */
1048 static int
1049 option_color_command(int argc, char *argv[])
1050 {
1051 struct line_info *info;
1053 if (argc != 3 && argc != 4) {
1054 config_msg = "Wrong number of arguments given to color command";
1055 return ERR;
1056 }
1058 info = get_line_info(argv[0]);
1059 if (!info) {
1060 if (!string_enum_compare(argv[0], "main-delim", strlen("main-delim"))) {
1061 info = get_line_info("delimiter");
1063 } else if (!string_enum_compare(argv[0], "main-date", strlen("main-date"))) {
1064 info = get_line_info("date");
1066 } else {
1067 config_msg = "Unknown color name";
1068 return ERR;
1069 }
1070 }
1072 if (set_color(&info->fg, argv[1]) == ERR ||
1073 set_color(&info->bg, argv[2]) == ERR) {
1074 config_msg = "Unknown color";
1075 return ERR;
1076 }
1078 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
1079 config_msg = "Unknown attribute";
1080 return ERR;
1081 }
1083 return OK;
1084 }
1086 static bool parse_bool(const char *s)
1087 {
1088 return (!strcmp(s, "1") || !strcmp(s, "true") ||
1089 !strcmp(s, "yes")) ? TRUE : FALSE;
1090 }
1092 /* Wants: name = value */
1093 static int
1094 option_set_command(int argc, char *argv[])
1095 {
1096 if (argc != 3) {
1097 config_msg = "Wrong number of arguments given to set command";
1098 return ERR;
1099 }
1101 if (strcmp(argv[1], "=")) {
1102 config_msg = "No value assigned";
1103 return ERR;
1104 }
1106 if (!strcmp(argv[0], "show-author")) {
1107 opt_author = parse_bool(argv[2]);
1108 return OK;
1109 }
1111 if (!strcmp(argv[0], "show-date")) {
1112 opt_date = parse_bool(argv[2]);
1113 return OK;
1114 }
1116 if (!strcmp(argv[0], "show-rev-graph")) {
1117 opt_rev_graph = parse_bool(argv[2]);
1118 return OK;
1119 }
1121 if (!strcmp(argv[0], "show-refs")) {
1122 opt_show_refs = parse_bool(argv[2]);
1123 return OK;
1124 }
1126 if (!strcmp(argv[0], "show-line-numbers")) {
1127 opt_line_number = parse_bool(argv[2]);
1128 return OK;
1129 }
1131 if (!strcmp(argv[0], "line-graphics")) {
1132 opt_line_graphics = parse_bool(argv[2]);
1133 return OK;
1134 }
1136 if (!strcmp(argv[0], "line-number-interval")) {
1137 opt_num_interval = atoi(argv[2]);
1138 return OK;
1139 }
1141 if (!strcmp(argv[0], "tab-size")) {
1142 opt_tab_size = atoi(argv[2]);
1143 return OK;
1144 }
1146 if (!strcmp(argv[0], "commit-encoding")) {
1147 char *arg = argv[2];
1148 int delimiter = *arg;
1149 int i;
1151 switch (delimiter) {
1152 case '"':
1153 case '\'':
1154 for (arg++, i = 0; arg[i]; i++)
1155 if (arg[i] == delimiter) {
1156 arg[i] = 0;
1157 break;
1158 }
1159 default:
1160 string_ncopy(opt_encoding, arg, strlen(arg));
1161 return OK;
1162 }
1163 }
1165 config_msg = "Unknown variable name";
1166 return ERR;
1167 }
1169 /* Wants: mode request key */
1170 static int
1171 option_bind_command(int argc, char *argv[])
1172 {
1173 enum request request;
1174 int keymap;
1175 int key;
1177 if (argc < 3) {
1178 config_msg = "Wrong number of arguments given to bind command";
1179 return ERR;
1180 }
1182 if (set_keymap(&keymap, argv[0]) == ERR) {
1183 config_msg = "Unknown key map";
1184 return ERR;
1185 }
1187 key = get_key_value(argv[1]);
1188 if (key == ERR) {
1189 config_msg = "Unknown key";
1190 return ERR;
1191 }
1193 request = get_request(argv[2]);
1194 if (request == REQ_NONE) {
1195 const char *obsolete[] = { "cherry-pick" };
1196 size_t namelen = strlen(argv[2]);
1197 int i;
1199 for (i = 0; i < ARRAY_SIZE(obsolete); i++) {
1200 if (namelen == strlen(obsolete[i]) &&
1201 !string_enum_compare(obsolete[i], argv[2], namelen)) {
1202 config_msg = "Obsolete request name";
1203 return ERR;
1204 }
1205 }
1206 }
1207 if (request == REQ_NONE && *argv[2]++ == '!')
1208 request = add_run_request(keymap, key, argc - 2, argv + 2);
1209 if (request == REQ_NONE) {
1210 config_msg = "Unknown request name";
1211 return ERR;
1212 }
1214 add_keybinding(keymap, request, key);
1216 return OK;
1217 }
1219 static int
1220 set_option(char *opt, char *value)
1221 {
1222 char *argv[16];
1223 int valuelen;
1224 int argc = 0;
1226 /* Tokenize */
1227 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1228 argv[argc++] = value;
1229 value += valuelen;
1231 /* Nothing more to tokenize or last available token. */
1232 if (!*value || argc >= ARRAY_SIZE(argv))
1233 break;
1235 *value++ = 0;
1236 while (isspace(*value))
1237 value++;
1238 }
1240 if (!strcmp(opt, "color"))
1241 return option_color_command(argc, argv);
1243 if (!strcmp(opt, "set"))
1244 return option_set_command(argc, argv);
1246 if (!strcmp(opt, "bind"))
1247 return option_bind_command(argc, argv);
1249 config_msg = "Unknown option command";
1250 return ERR;
1251 }
1253 static int
1254 read_option(char *opt, size_t optlen, char *value, size_t valuelen)
1255 {
1256 int status = OK;
1258 config_lineno++;
1259 config_msg = "Internal error";
1261 /* Check for comment markers, since read_properties() will
1262 * only ensure opt and value are split at first " \t". */
1263 optlen = strcspn(opt, "#");
1264 if (optlen == 0)
1265 return OK;
1267 if (opt[optlen] != 0) {
1268 config_msg = "No option value";
1269 status = ERR;
1271 } else {
1272 /* Look for comment endings in the value. */
1273 size_t len = strcspn(value, "#");
1275 if (len < valuelen) {
1276 valuelen = len;
1277 value[valuelen] = 0;
1278 }
1280 status = set_option(opt, value);
1281 }
1283 if (status == ERR) {
1284 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1285 config_lineno, (int) optlen, opt, config_msg);
1286 config_errors = TRUE;
1287 }
1289 /* Always keep going if errors are encountered. */
1290 return OK;
1291 }
1293 static void
1294 load_option_file(const char *path)
1295 {
1296 FILE *file;
1298 /* It's ok that the file doesn't exist. */
1299 file = fopen(path, "r");
1300 if (!file)
1301 return;
1303 config_lineno = 0;
1304 config_errors = FALSE;
1306 if (read_properties(file, " \t", read_option) == ERR ||
1307 config_errors == TRUE)
1308 fprintf(stderr, "Errors while loading %s.\n", path);
1309 }
1311 static int
1312 load_options(void)
1313 {
1314 char *home = getenv("HOME");
1315 char *tigrc_user = getenv("TIGRC_USER");
1316 char *tigrc_system = getenv("TIGRC_SYSTEM");
1317 char buf[SIZEOF_STR];
1319 add_builtin_run_requests();
1321 if (!tigrc_system) {
1322 if (!string_format(buf, "%s/tigrc", SYSCONFDIR))
1323 return ERR;
1324 tigrc_system = buf;
1325 }
1326 load_option_file(tigrc_system);
1328 if (!tigrc_user) {
1329 if (!home || !string_format(buf, "%s/.tigrc", home))
1330 return ERR;
1331 tigrc_user = buf;
1332 }
1333 load_option_file(tigrc_user);
1335 return OK;
1336 }
1339 /*
1340 * The viewer
1341 */
1343 struct view;
1344 struct view_ops;
1346 /* The display array of active views and the index of the current view. */
1347 static struct view *display[2];
1348 static unsigned int current_view;
1350 /* Reading from the prompt? */
1351 static bool input_mode = FALSE;
1353 #define foreach_displayed_view(view, i) \
1354 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1356 #define displayed_views() (display[1] != NULL ? 2 : 1)
1358 /* Current head and commit ID */
1359 static char ref_blob[SIZEOF_REF] = "";
1360 static char ref_commit[SIZEOF_REF] = "HEAD";
1361 static char ref_head[SIZEOF_REF] = "HEAD";
1363 struct view {
1364 const char *name; /* View name */
1365 const char *cmd_fmt; /* Default command line format */
1366 const char *cmd_env; /* Command line set via environment */
1367 const char *id; /* Points to either of ref_{head,commit,blob} */
1369 struct view_ops *ops; /* View operations */
1371 enum keymap keymap; /* What keymap does this view have */
1372 bool git_dir; /* Whether the view requires a git directory. */
1374 char cmd[SIZEOF_STR]; /* Command buffer */
1375 char ref[SIZEOF_REF]; /* Hovered commit reference */
1376 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1378 int height, width; /* The width and height of the main window */
1379 WINDOW *win; /* The main window */
1380 WINDOW *title; /* The title window living below the main window */
1382 /* Navigation */
1383 unsigned long offset; /* Offset of the window top */
1384 unsigned long lineno; /* Current line number */
1386 /* Searching */
1387 char grep[SIZEOF_STR]; /* Search string */
1388 regex_t *regex; /* Pre-compiled regex */
1390 /* If non-NULL, points to the view that opened this view. If this view
1391 * is closed tig will switch back to the parent view. */
1392 struct view *parent;
1394 /* Buffering */
1395 size_t lines; /* Total number of lines */
1396 struct line *line; /* Line index */
1397 size_t line_alloc; /* Total number of allocated lines */
1398 size_t line_size; /* Total number of used lines */
1399 unsigned int digits; /* Number of digits in the lines member. */
1401 /* Loading */
1402 FILE *pipe;
1403 time_t start_time;
1404 };
1406 struct view_ops {
1407 /* What type of content being displayed. Used in the title bar. */
1408 const char *type;
1409 /* Open and reads in all view content. */
1410 bool (*open)(struct view *view);
1411 /* Read one line; updates view->line. */
1412 bool (*read)(struct view *view, char *data);
1413 /* Draw one line; @lineno must be < view->height. */
1414 bool (*draw)(struct view *view, struct line *line, unsigned int lineno, bool selected);
1415 /* Depending on view handle a special requests. */
1416 enum request (*request)(struct view *view, enum request request, struct line *line);
1417 /* Search for regex in a line. */
1418 bool (*grep)(struct view *view, struct line *line);
1419 /* Select line */
1420 void (*select)(struct view *view, struct line *line);
1421 };
1423 static struct view_ops pager_ops;
1424 static struct view_ops main_ops;
1425 static struct view_ops tree_ops;
1426 static struct view_ops blob_ops;
1427 static struct view_ops blame_ops;
1428 static struct view_ops help_ops;
1429 static struct view_ops status_ops;
1430 static struct view_ops stage_ops;
1432 #define VIEW_STR(name, cmd, env, ref, ops, map, git) \
1433 { name, cmd, #env, ref, ops, map, git }
1435 #define VIEW_(id, name, ops, git, ref) \
1436 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id, git)
1439 static struct view views[] = {
1440 VIEW_(MAIN, "main", &main_ops, TRUE, ref_head),
1441 VIEW_(DIFF, "diff", &pager_ops, TRUE, ref_commit),
1442 VIEW_(LOG, "log", &pager_ops, TRUE, ref_head),
1443 VIEW_(TREE, "tree", &tree_ops, TRUE, ref_commit),
1444 VIEW_(BLOB, "blob", &blob_ops, TRUE, ref_blob),
1445 VIEW_(BLAME, "blame", &blame_ops, TRUE, ref_commit),
1446 VIEW_(HELP, "help", &help_ops, FALSE, ""),
1447 VIEW_(PAGER, "pager", &pager_ops, FALSE, "stdin"),
1448 VIEW_(STATUS, "status", &status_ops, TRUE, ""),
1449 VIEW_(STAGE, "stage", &stage_ops, TRUE, ""),
1450 };
1452 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1453 #define VIEW_REQ(view) ((view) - views + REQ_OFFSET + 1)
1455 #define foreach_view(view, i) \
1456 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1458 #define view_is_displayed(view) \
1459 (view == display[0] || view == display[1])
1461 static int
1462 draw_text(struct view *view, const char *string, int max_len,
1463 bool use_tilde, bool selected)
1464 {
1465 int len = 0;
1466 int trimmed = FALSE;
1468 if (max_len <= 0)
1469 return 0;
1471 if (opt_utf8) {
1472 len = utf8_length(string, max_len, &trimmed, use_tilde);
1473 } else {
1474 len = strlen(string);
1475 if (len > max_len) {
1476 if (use_tilde) {
1477 max_len -= 1;
1478 }
1479 len = max_len;
1480 trimmed = TRUE;
1481 }
1482 }
1484 waddnstr(view->win, string, len);
1485 if (trimmed && use_tilde) {
1486 if (!selected)
1487 wattrset(view->win, get_line_attr(LINE_DELIMITER));
1488 waddch(view->win, '~');
1489 len++;
1490 }
1492 return len;
1493 }
1495 static int
1496 draw_lineno(struct view *view, unsigned int lineno, int max, bool selected)
1497 {
1498 static char fmt[] = "%1ld";
1499 char number[10] = " ";
1500 int digits3 = view->digits < 3 ? 3 : view->digits;
1501 int max_number = MIN(digits3, STRING_SIZE(number));
1502 bool showtrimmed = FALSE;
1503 int col;
1505 lineno += view->offset + 1;
1506 if (lineno == 1 || (lineno % opt_num_interval) == 0) {
1507 if (view->digits <= 9)
1508 fmt[1] = '0' + digits3;
1510 if (!string_format(number, fmt, lineno))
1511 number[0] = 0;
1512 showtrimmed = TRUE;
1513 }
1515 if (max < max_number)
1516 max_number = max;
1518 if (!selected)
1519 wattrset(view->win, get_line_attr(LINE_LINE_NUMBER));
1520 col = draw_text(view, number, max_number, showtrimmed, selected);
1521 if (col < max) {
1522 if (!selected)
1523 wattrset(view->win, A_NORMAL);
1524 waddch(view->win, opt_line_graphics ? ACS_VLINE : '|');
1525 col++;
1526 }
1527 if (col < max) {
1528 waddch(view->win, ' ');
1529 col++;
1530 }
1532 return col;
1533 }
1535 static int
1536 draw_date(struct view *view, struct tm *time, int max, bool selected)
1537 {
1538 char buf[DATE_COLS];
1539 int col;
1540 int timelen = 0;
1542 if (max > DATE_COLS)
1543 max = DATE_COLS;
1544 if (time)
1545 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, time);
1546 if (!timelen) {
1547 memset(buf, ' ', sizeof(buf) - 1);
1548 buf[sizeof(buf) - 1] = 0;
1549 }
1551 if (!selected)
1552 wattrset(view->win, get_line_attr(LINE_DATE));
1553 col = draw_text(view, buf, max, FALSE, selected);
1554 if (col < max) {
1555 if (!selected)
1556 wattrset(view->win, get_line_attr(LINE_DEFAULT));
1557 waddch(view->win, ' ');
1558 col++;
1559 }
1561 return col;
1562 }
1564 static bool
1565 draw_view_line(struct view *view, unsigned int lineno)
1566 {
1567 struct line *line;
1568 bool selected = (view->offset + lineno == view->lineno);
1569 bool draw_ok;
1571 assert(view_is_displayed(view));
1573 if (view->offset + lineno >= view->lines)
1574 return FALSE;
1576 line = &view->line[view->offset + lineno];
1578 wmove(view->win, lineno, 0);
1580 if (selected) {
1581 line->selected = TRUE;
1582 view->ops->select(view, line);
1583 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
1584 wattrset(view->win, get_line_attr(LINE_CURSOR));
1585 } else if (line->selected) {
1586 line->selected = FALSE;
1587 wclrtoeol(view->win);
1588 }
1590 scrollok(view->win, FALSE);
1591 draw_ok = view->ops->draw(view, line, lineno, selected);
1592 scrollok(view->win, TRUE);
1594 return draw_ok;
1595 }
1597 static void
1598 redraw_view_dirty(struct view *view)
1599 {
1600 bool dirty = FALSE;
1601 int lineno;
1603 for (lineno = 0; lineno < view->height; lineno++) {
1604 struct line *line = &view->line[view->offset + lineno];
1606 if (!line->dirty)
1607 continue;
1608 line->dirty = 0;
1609 dirty = TRUE;
1610 if (!draw_view_line(view, lineno))
1611 break;
1612 }
1614 if (!dirty)
1615 return;
1616 redrawwin(view->win);
1617 if (input_mode)
1618 wnoutrefresh(view->win);
1619 else
1620 wrefresh(view->win);
1621 }
1623 static void
1624 redraw_view_from(struct view *view, int lineno)
1625 {
1626 assert(0 <= lineno && lineno < view->height);
1628 for (; lineno < view->height; lineno++) {
1629 if (!draw_view_line(view, lineno))
1630 break;
1631 }
1633 redrawwin(view->win);
1634 if (input_mode)
1635 wnoutrefresh(view->win);
1636 else
1637 wrefresh(view->win);
1638 }
1640 static void
1641 redraw_view(struct view *view)
1642 {
1643 wclear(view->win);
1644 redraw_view_from(view, 0);
1645 }
1648 static void
1649 update_view_title(struct view *view)
1650 {
1651 char buf[SIZEOF_STR];
1652 char state[SIZEOF_STR];
1653 size_t bufpos = 0, statelen = 0;
1655 assert(view_is_displayed(view));
1657 if (view != VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) {
1658 unsigned int view_lines = view->offset + view->height;
1659 unsigned int lines = view->lines
1660 ? MIN(view_lines, view->lines) * 100 / view->lines
1661 : 0;
1663 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1664 view->ops->type,
1665 view->lineno + 1,
1666 view->lines,
1667 lines);
1669 if (view->pipe) {
1670 time_t secs = time(NULL) - view->start_time;
1672 /* Three git seconds are a long time ... */
1673 if (secs > 2)
1674 string_format_from(state, &statelen, " %lds", secs);
1675 }
1676 }
1678 string_format_from(buf, &bufpos, "[%s]", view->name);
1679 if (*view->ref && bufpos < view->width) {
1680 size_t refsize = strlen(view->ref);
1681 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1683 if (minsize < view->width)
1684 refsize = view->width - minsize + 7;
1685 string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
1686 }
1688 if (statelen && bufpos < view->width) {
1689 string_format_from(buf, &bufpos, " %s", state);
1690 }
1692 if (view == display[current_view])
1693 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1694 else
1695 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1697 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1698 wclrtoeol(view->title);
1699 wmove(view->title, 0, view->width - 1);
1701 if (input_mode)
1702 wnoutrefresh(view->title);
1703 else
1704 wrefresh(view->title);
1705 }
1707 static void
1708 resize_display(void)
1709 {
1710 int offset, i;
1711 struct view *base = display[0];
1712 struct view *view = display[1] ? display[1] : display[0];
1714 /* Setup window dimensions */
1716 getmaxyx(stdscr, base->height, base->width);
1718 /* Make room for the status window. */
1719 base->height -= 1;
1721 if (view != base) {
1722 /* Horizontal split. */
1723 view->width = base->width;
1724 view->height = SCALE_SPLIT_VIEW(base->height);
1725 base->height -= view->height;
1727 /* Make room for the title bar. */
1728 view->height -= 1;
1729 }
1731 /* Make room for the title bar. */
1732 base->height -= 1;
1734 offset = 0;
1736 foreach_displayed_view (view, i) {
1737 if (!view->win) {
1738 view->win = newwin(view->height, 0, offset, 0);
1739 if (!view->win)
1740 die("Failed to create %s view", view->name);
1742 scrollok(view->win, TRUE);
1744 view->title = newwin(1, 0, offset + view->height, 0);
1745 if (!view->title)
1746 die("Failed to create title window");
1748 } else {
1749 wresize(view->win, view->height, view->width);
1750 mvwin(view->win, offset, 0);
1751 mvwin(view->title, offset + view->height, 0);
1752 }
1754 offset += view->height + 1;
1755 }
1756 }
1758 static void
1759 redraw_display(void)
1760 {
1761 struct view *view;
1762 int i;
1764 foreach_displayed_view (view, i) {
1765 redraw_view(view);
1766 update_view_title(view);
1767 }
1768 }
1770 static void
1771 update_display_cursor(struct view *view)
1772 {
1773 /* Move the cursor to the right-most column of the cursor line.
1774 *
1775 * XXX: This could turn out to be a bit expensive, but it ensures that
1776 * the cursor does not jump around. */
1777 if (view->lines) {
1778 wmove(view->win, view->lineno - view->offset, view->width - 1);
1779 wrefresh(view->win);
1780 }
1781 }
1783 /*
1784 * Navigation
1785 */
1787 /* Scrolling backend */
1788 static void
1789 do_scroll_view(struct view *view, int lines)
1790 {
1791 bool redraw_current_line = FALSE;
1793 /* The rendering expects the new offset. */
1794 view->offset += lines;
1796 assert(0 <= view->offset && view->offset < view->lines);
1797 assert(lines);
1799 /* Move current line into the view. */
1800 if (view->lineno < view->offset) {
1801 view->lineno = view->offset;
1802 redraw_current_line = TRUE;
1803 } else if (view->lineno >= view->offset + view->height) {
1804 view->lineno = view->offset + view->height - 1;
1805 redraw_current_line = TRUE;
1806 }
1808 assert(view->offset <= view->lineno && view->lineno < view->lines);
1810 /* Redraw the whole screen if scrolling is pointless. */
1811 if (view->height < ABS(lines)) {
1812 redraw_view(view);
1814 } else {
1815 int line = lines > 0 ? view->height - lines : 0;
1816 int end = line + ABS(lines);
1818 wscrl(view->win, lines);
1820 for (; line < end; line++) {
1821 if (!draw_view_line(view, line))
1822 break;
1823 }
1825 if (redraw_current_line)
1826 draw_view_line(view, view->lineno - view->offset);
1827 }
1829 redrawwin(view->win);
1830 wrefresh(view->win);
1831 report("");
1832 }
1834 /* Scroll frontend */
1835 static void
1836 scroll_view(struct view *view, enum request request)
1837 {
1838 int lines = 1;
1840 assert(view_is_displayed(view));
1842 switch (request) {
1843 case REQ_SCROLL_PAGE_DOWN:
1844 lines = view->height;
1845 case REQ_SCROLL_LINE_DOWN:
1846 if (view->offset + lines > view->lines)
1847 lines = view->lines - view->offset;
1849 if (lines == 0 || view->offset + view->height >= view->lines) {
1850 report("Cannot scroll beyond the last line");
1851 return;
1852 }
1853 break;
1855 case REQ_SCROLL_PAGE_UP:
1856 lines = view->height;
1857 case REQ_SCROLL_LINE_UP:
1858 if (lines > view->offset)
1859 lines = view->offset;
1861 if (lines == 0) {
1862 report("Cannot scroll beyond the first line");
1863 return;
1864 }
1866 lines = -lines;
1867 break;
1869 default:
1870 die("request %d not handled in switch", request);
1871 }
1873 do_scroll_view(view, lines);
1874 }
1876 /* Cursor moving */
1877 static void
1878 move_view(struct view *view, enum request request)
1879 {
1880 int scroll_steps = 0;
1881 int steps;
1883 switch (request) {
1884 case REQ_MOVE_FIRST_LINE:
1885 steps = -view->lineno;
1886 break;
1888 case REQ_MOVE_LAST_LINE:
1889 steps = view->lines - view->lineno - 1;
1890 break;
1892 case REQ_MOVE_PAGE_UP:
1893 steps = view->height > view->lineno
1894 ? -view->lineno : -view->height;
1895 break;
1897 case REQ_MOVE_PAGE_DOWN:
1898 steps = view->lineno + view->height >= view->lines
1899 ? view->lines - view->lineno - 1 : view->height;
1900 break;
1902 case REQ_MOVE_UP:
1903 steps = -1;
1904 break;
1906 case REQ_MOVE_DOWN:
1907 steps = 1;
1908 break;
1910 default:
1911 die("request %d not handled in switch", request);
1912 }
1914 if (steps <= 0 && view->lineno == 0) {
1915 report("Cannot move beyond the first line");
1916 return;
1918 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1919 report("Cannot move beyond the last line");
1920 return;
1921 }
1923 /* Move the current line */
1924 view->lineno += steps;
1925 assert(0 <= view->lineno && view->lineno < view->lines);
1927 /* Check whether the view needs to be scrolled */
1928 if (view->lineno < view->offset ||
1929 view->lineno >= view->offset + view->height) {
1930 scroll_steps = steps;
1931 if (steps < 0 && -steps > view->offset) {
1932 scroll_steps = -view->offset;
1934 } else if (steps > 0) {
1935 if (view->lineno == view->lines - 1 &&
1936 view->lines > view->height) {
1937 scroll_steps = view->lines - view->offset - 1;
1938 if (scroll_steps >= view->height)
1939 scroll_steps -= view->height - 1;
1940 }
1941 }
1942 }
1944 if (!view_is_displayed(view)) {
1945 view->offset += scroll_steps;
1946 assert(0 <= view->offset && view->offset < view->lines);
1947 view->ops->select(view, &view->line[view->lineno]);
1948 return;
1949 }
1951 /* Repaint the old "current" line if we be scrolling */
1952 if (ABS(steps) < view->height)
1953 draw_view_line(view, view->lineno - steps - view->offset);
1955 if (scroll_steps) {
1956 do_scroll_view(view, scroll_steps);
1957 return;
1958 }
1960 /* Draw the current line */
1961 draw_view_line(view, view->lineno - view->offset);
1963 redrawwin(view->win);
1964 wrefresh(view->win);
1965 report("");
1966 }
1969 /*
1970 * Searching
1971 */
1973 static void search_view(struct view *view, enum request request);
1975 static bool
1976 find_next_line(struct view *view, unsigned long lineno, struct line *line)
1977 {
1978 assert(view_is_displayed(view));
1980 if (!view->ops->grep(view, line))
1981 return FALSE;
1983 if (lineno - view->offset >= view->height) {
1984 view->offset = lineno;
1985 view->lineno = lineno;
1986 redraw_view(view);
1988 } else {
1989 unsigned long old_lineno = view->lineno - view->offset;
1991 view->lineno = lineno;
1992 draw_view_line(view, old_lineno);
1994 draw_view_line(view, view->lineno - view->offset);
1995 redrawwin(view->win);
1996 wrefresh(view->win);
1997 }
1999 report("Line %ld matches '%s'", lineno + 1, view->grep);
2000 return TRUE;
2001 }
2003 static void
2004 find_next(struct view *view, enum request request)
2005 {
2006 unsigned long lineno = view->lineno;
2007 int direction;
2009 if (!*view->grep) {
2010 if (!*opt_search)
2011 report("No previous search");
2012 else
2013 search_view(view, request);
2014 return;
2015 }
2017 switch (request) {
2018 case REQ_SEARCH:
2019 case REQ_FIND_NEXT:
2020 direction = 1;
2021 break;
2023 case REQ_SEARCH_BACK:
2024 case REQ_FIND_PREV:
2025 direction = -1;
2026 break;
2028 default:
2029 return;
2030 }
2032 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
2033 lineno += direction;
2035 /* Note, lineno is unsigned long so will wrap around in which case it
2036 * will become bigger than view->lines. */
2037 for (; lineno < view->lines; lineno += direction) {
2038 struct line *line = &view->line[lineno];
2040 if (find_next_line(view, lineno, line))
2041 return;
2042 }
2044 report("No match found for '%s'", view->grep);
2045 }
2047 static void
2048 search_view(struct view *view, enum request request)
2049 {
2050 int regex_err;
2052 if (view->regex) {
2053 regfree(view->regex);
2054 *view->grep = 0;
2055 } else {
2056 view->regex = calloc(1, sizeof(*view->regex));
2057 if (!view->regex)
2058 return;
2059 }
2061 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
2062 if (regex_err != 0) {
2063 char buf[SIZEOF_STR] = "unknown error";
2065 regerror(regex_err, view->regex, buf, sizeof(buf));
2066 report("Search failed: %s", buf);
2067 return;
2068 }
2070 string_copy(view->grep, opt_search);
2072 find_next(view, request);
2073 }
2075 /*
2076 * Incremental updating
2077 */
2079 static void
2080 end_update(struct view *view)
2081 {
2082 if (!view->pipe)
2083 return;
2084 set_nonblocking_input(FALSE);
2085 if (view->pipe == stdin)
2086 fclose(view->pipe);
2087 else
2088 pclose(view->pipe);
2089 view->pipe = NULL;
2090 }
2092 static bool
2093 begin_update(struct view *view)
2094 {
2095 if (view->pipe)
2096 end_update(view);
2098 if (opt_cmd[0]) {
2099 string_copy(view->cmd, opt_cmd);
2100 opt_cmd[0] = 0;
2101 /* When running random commands, initially show the
2102 * command in the title. However, it maybe later be
2103 * overwritten if a commit line is selected. */
2104 if (view == VIEW(REQ_VIEW_PAGER))
2105 string_copy(view->ref, view->cmd);
2106 else
2107 view->ref[0] = 0;
2109 } else if (view == VIEW(REQ_VIEW_TREE)) {
2110 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2111 char path[SIZEOF_STR];
2113 if (strcmp(view->vid, view->id))
2114 opt_path[0] = path[0] = 0;
2115 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
2116 return FALSE;
2118 if (!string_format(view->cmd, format, view->id, path))
2119 return FALSE;
2121 } else {
2122 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2123 const char *id = view->id;
2125 if (!string_format(view->cmd, format, id, id, id, id, id))
2126 return FALSE;
2128 /* Put the current ref_* value to the view title ref
2129 * member. This is needed by the blob view. Most other
2130 * views sets it automatically after loading because the
2131 * first line is a commit line. */
2132 string_copy_rev(view->ref, view->id);
2133 }
2135 /* Special case for the pager view. */
2136 if (opt_pipe) {
2137 view->pipe = opt_pipe;
2138 opt_pipe = NULL;
2139 } else {
2140 view->pipe = popen(view->cmd, "r");
2141 }
2143 if (!view->pipe)
2144 return FALSE;
2146 set_nonblocking_input(TRUE);
2148 view->offset = 0;
2149 view->lines = 0;
2150 view->lineno = 0;
2151 string_copy_rev(view->vid, view->id);
2153 if (view->line) {
2154 int i;
2156 for (i = 0; i < view->lines; i++)
2157 if (view->line[i].data)
2158 free(view->line[i].data);
2160 free(view->line);
2161 view->line = NULL;
2162 }
2164 view->start_time = time(NULL);
2166 return TRUE;
2167 }
2169 #define ITEM_CHUNK_SIZE 256
2170 static void *
2171 realloc_items(void *mem, size_t *size, size_t new_size, size_t item_size)
2172 {
2173 size_t num_chunks = *size / ITEM_CHUNK_SIZE;
2174 size_t num_chunks_new = (new_size + ITEM_CHUNK_SIZE - 1) / ITEM_CHUNK_SIZE;
2176 if (mem == NULL || num_chunks != num_chunks_new) {
2177 *size = num_chunks_new * ITEM_CHUNK_SIZE;
2178 mem = realloc(mem, *size * item_size);
2179 }
2181 return mem;
2182 }
2184 static struct line *
2185 realloc_lines(struct view *view, size_t line_size)
2186 {
2187 size_t alloc = view->line_alloc;
2188 struct line *tmp = realloc_items(view->line, &alloc, line_size,
2189 sizeof(*view->line));
2191 if (!tmp)
2192 return NULL;
2194 view->line = tmp;
2195 view->line_alloc = alloc;
2196 view->line_size = line_size;
2197 return view->line;
2198 }
2200 static bool
2201 update_view(struct view *view)
2202 {
2203 char in_buffer[BUFSIZ];
2204 char out_buffer[BUFSIZ * 2];
2205 char *line;
2206 /* The number of lines to read. If too low it will cause too much
2207 * redrawing (and possible flickering), if too high responsiveness
2208 * will suffer. */
2209 unsigned long lines = view->height;
2210 int redraw_from = -1;
2212 if (!view->pipe)
2213 return TRUE;
2215 /* Only redraw if lines are visible. */
2216 if (view->offset + view->height >= view->lines)
2217 redraw_from = view->lines - view->offset;
2219 /* FIXME: This is probably not perfect for backgrounded views. */
2220 if (!realloc_lines(view, view->lines + lines))
2221 goto alloc_error;
2223 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
2224 size_t linelen = strlen(line);
2226 if (linelen)
2227 line[linelen - 1] = 0;
2229 if (opt_iconv != ICONV_NONE) {
2230 ICONV_CONST char *inbuf = line;
2231 size_t inlen = linelen;
2233 char *outbuf = out_buffer;
2234 size_t outlen = sizeof(out_buffer);
2236 size_t ret;
2238 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
2239 if (ret != (size_t) -1) {
2240 line = out_buffer;
2241 linelen = strlen(out_buffer);
2242 }
2243 }
2245 if (!view->ops->read(view, line))
2246 goto alloc_error;
2248 if (lines-- == 1)
2249 break;
2250 }
2252 {
2253 int digits;
2255 lines = view->lines;
2256 for (digits = 0; lines; digits++)
2257 lines /= 10;
2259 /* Keep the displayed view in sync with line number scaling. */
2260 if (digits != view->digits) {
2261 view->digits = digits;
2262 redraw_from = 0;
2263 }
2264 }
2266 if (!view_is_displayed(view))
2267 goto check_pipe;
2269 if (view == VIEW(REQ_VIEW_TREE)) {
2270 /* Clear the view and redraw everything since the tree sorting
2271 * might have rearranged things. */
2272 redraw_view(view);
2274 } else if (redraw_from >= 0) {
2275 /* If this is an incremental update, redraw the previous line
2276 * since for commits some members could have changed when
2277 * loading the main view. */
2278 if (redraw_from > 0)
2279 redraw_from--;
2281 /* Since revision graph visualization requires knowledge
2282 * about the parent commit, it causes a further one-off
2283 * needed to be redrawn for incremental updates. */
2284 if (redraw_from > 0 && opt_rev_graph)
2285 redraw_from--;
2287 /* Incrementally draw avoids flickering. */
2288 redraw_view_from(view, redraw_from);
2289 }
2291 if (view == VIEW(REQ_VIEW_BLAME))
2292 redraw_view_dirty(view);
2294 /* Update the title _after_ the redraw so that if the redraw picks up a
2295 * commit reference in view->ref it'll be available here. */
2296 update_view_title(view);
2298 check_pipe:
2299 if (ferror(view->pipe)) {
2300 report("Failed to read: %s", strerror(errno));
2301 goto end;
2303 } else if (feof(view->pipe)) {
2304 report("");
2305 goto end;
2306 }
2308 return TRUE;
2310 alloc_error:
2311 report("Allocation failure");
2313 end:
2314 if (view->ops->read(view, NULL))
2315 end_update(view);
2316 return FALSE;
2317 }
2319 static struct line *
2320 add_line_data(struct view *view, void *data, enum line_type type)
2321 {
2322 struct line *line = &view->line[view->lines++];
2324 memset(line, 0, sizeof(*line));
2325 line->type = type;
2326 line->data = data;
2328 return line;
2329 }
2331 static struct line *
2332 add_line_text(struct view *view, char *data, enum line_type type)
2333 {
2334 if (data)
2335 data = strdup(data);
2337 return data ? add_line_data(view, data, type) : NULL;
2338 }
2341 /*
2342 * View opening
2343 */
2345 enum open_flags {
2346 OPEN_DEFAULT = 0, /* Use default view switching. */
2347 OPEN_SPLIT = 1, /* Split current view. */
2348 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2349 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2350 OPEN_NOMAXIMIZE = 8, /* Do not maximize the current view. */
2351 };
2353 static void
2354 open_view(struct view *prev, enum request request, enum open_flags flags)
2355 {
2356 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2357 bool split = !!(flags & OPEN_SPLIT);
2358 bool reload = !!(flags & OPEN_RELOAD);
2359 bool nomaximize = !!(flags & OPEN_NOMAXIMIZE);
2360 struct view *view = VIEW(request);
2361 int nviews = displayed_views();
2362 struct view *base_view = display[0];
2364 if (view == prev && nviews == 1 && !reload) {
2365 report("Already in %s view", view->name);
2366 return;
2367 }
2369 if (view->git_dir && !opt_git_dir[0]) {
2370 report("The %s view is disabled in pager view", view->name);
2371 return;
2372 }
2374 if (split) {
2375 display[1] = view;
2376 if (!backgrounded)
2377 current_view = 1;
2378 } else if (!nomaximize) {
2379 /* Maximize the current view. */
2380 memset(display, 0, sizeof(display));
2381 current_view = 0;
2382 display[current_view] = view;
2383 }
2385 /* Resize the view when switching between split- and full-screen,
2386 * or when switching between two different full-screen views. */
2387 if (nviews != displayed_views() ||
2388 (nviews == 1 && base_view != display[0]))
2389 resize_display();
2391 if (view->ops->open) {
2392 if (!view->ops->open(view)) {
2393 report("Failed to load %s view", view->name);
2394 return;
2395 }
2397 } else if ((reload || strcmp(view->vid, view->id)) &&
2398 !begin_update(view)) {
2399 report("Failed to load %s view", view->name);
2400 return;
2401 }
2403 if (split && prev->lineno - prev->offset >= prev->height) {
2404 /* Take the title line into account. */
2405 int lines = prev->lineno - prev->offset - prev->height + 1;
2407 /* Scroll the view that was split if the current line is
2408 * outside the new limited view. */
2409 do_scroll_view(prev, lines);
2410 }
2412 if (prev && view != prev) {
2413 if (split && !backgrounded) {
2414 /* "Blur" the previous view. */
2415 update_view_title(prev);
2416 }
2418 view->parent = prev;
2419 }
2421 if (view->pipe && view->lines == 0) {
2422 /* Clear the old view and let the incremental updating refill
2423 * the screen. */
2424 werase(view->win);
2425 report("");
2426 } else {
2427 redraw_view(view);
2428 report("");
2429 }
2431 /* If the view is backgrounded the above calls to report()
2432 * won't redraw the view title. */
2433 if (backgrounded)
2434 update_view_title(view);
2435 }
2437 static void
2438 open_external_viewer(const char *cmd)
2439 {
2440 def_prog_mode(); /* save current tty modes */
2441 endwin(); /* restore original tty modes */
2442 system(cmd);
2443 fprintf(stderr, "Press Enter to continue");
2444 getc(stdin);
2445 reset_prog_mode();
2446 redraw_display();
2447 }
2449 static void
2450 open_mergetool(const char *file)
2451 {
2452 char cmd[SIZEOF_STR];
2453 char file_sq[SIZEOF_STR];
2455 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2456 string_format(cmd, "git mergetool %s", file_sq)) {
2457 open_external_viewer(cmd);
2458 }
2459 }
2461 static void
2462 open_editor(bool from_root, const char *file)
2463 {
2464 char cmd[SIZEOF_STR];
2465 char file_sq[SIZEOF_STR];
2466 char *editor;
2467 char *prefix = from_root ? opt_cdup : "";
2469 editor = getenv("GIT_EDITOR");
2470 if (!editor && *opt_editor)
2471 editor = opt_editor;
2472 if (!editor)
2473 editor = getenv("VISUAL");
2474 if (!editor)
2475 editor = getenv("EDITOR");
2476 if (!editor)
2477 editor = "vi";
2479 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2480 string_format(cmd, "%s %s%s", editor, prefix, file_sq)) {
2481 open_external_viewer(cmd);
2482 }
2483 }
2485 static void
2486 open_run_request(enum request request)
2487 {
2488 struct run_request *req = get_run_request(request);
2489 char buf[SIZEOF_STR * 2];
2490 size_t bufpos;
2491 char *cmd;
2493 if (!req) {
2494 report("Unknown run request");
2495 return;
2496 }
2498 bufpos = 0;
2499 cmd = req->cmd;
2501 while (cmd) {
2502 char *next = strstr(cmd, "%(");
2503 int len = next - cmd;
2504 char *value;
2506 if (!next) {
2507 len = strlen(cmd);
2508 value = "";
2510 } else if (!strncmp(next, "%(head)", 7)) {
2511 value = ref_head;
2513 } else if (!strncmp(next, "%(commit)", 9)) {
2514 value = ref_commit;
2516 } else if (!strncmp(next, "%(blob)", 7)) {
2517 value = ref_blob;
2519 } else {
2520 report("Unknown replacement in run request: `%s`", req->cmd);
2521 return;
2522 }
2524 if (!string_format_from(buf, &bufpos, "%.*s%s", len, cmd, value))
2525 return;
2527 if (next)
2528 next = strchr(next, ')') + 1;
2529 cmd = next;
2530 }
2532 open_external_viewer(buf);
2533 }
2535 /*
2536 * User request switch noodle
2537 */
2539 static int
2540 view_driver(struct view *view, enum request request)
2541 {
2542 int i;
2544 if (request == REQ_NONE) {
2545 doupdate();
2546 return TRUE;
2547 }
2549 if (request > REQ_NONE) {
2550 open_run_request(request);
2551 /* FIXME: When all views can refresh always do this. */
2552 if (view == VIEW(REQ_VIEW_STATUS) ||
2553 view == VIEW(REQ_VIEW_STAGE))
2554 request = REQ_REFRESH;
2555 else
2556 return TRUE;
2557 }
2559 if (view && view->lines) {
2560 request = view->ops->request(view, request, &view->line[view->lineno]);
2561 if (request == REQ_NONE)
2562 return TRUE;
2563 }
2565 switch (request) {
2566 case REQ_MOVE_UP:
2567 case REQ_MOVE_DOWN:
2568 case REQ_MOVE_PAGE_UP:
2569 case REQ_MOVE_PAGE_DOWN:
2570 case REQ_MOVE_FIRST_LINE:
2571 case REQ_MOVE_LAST_LINE:
2572 move_view(view, request);
2573 break;
2575 case REQ_SCROLL_LINE_DOWN:
2576 case REQ_SCROLL_LINE_UP:
2577 case REQ_SCROLL_PAGE_DOWN:
2578 case REQ_SCROLL_PAGE_UP:
2579 scroll_view(view, request);
2580 break;
2582 case REQ_VIEW_BLAME:
2583 if (!opt_file[0]) {
2584 report("No file chosen, press %s to open tree view",
2585 get_key(REQ_VIEW_TREE));
2586 break;
2587 }
2588 open_view(view, request, OPEN_DEFAULT);
2589 break;
2591 case REQ_VIEW_BLOB:
2592 if (!ref_blob[0]) {
2593 report("No file chosen, press %s to open tree view",
2594 get_key(REQ_VIEW_TREE));
2595 break;
2596 }
2597 open_view(view, request, OPEN_DEFAULT);
2598 break;
2600 case REQ_VIEW_PAGER:
2601 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2602 report("No pager content, press %s to run command from prompt",
2603 get_key(REQ_PROMPT));
2604 break;
2605 }
2606 open_view(view, request, OPEN_DEFAULT);
2607 break;
2609 case REQ_VIEW_STAGE:
2610 if (!VIEW(REQ_VIEW_STAGE)->lines) {
2611 report("No stage content, press %s to open the status view and choose file",
2612 get_key(REQ_VIEW_STATUS));
2613 break;
2614 }
2615 open_view(view, request, OPEN_DEFAULT);
2616 break;
2618 case REQ_VIEW_STATUS:
2619 if (opt_is_inside_work_tree == FALSE) {
2620 report("The status view requires a working tree");
2621 break;
2622 }
2623 open_view(view, request, OPEN_DEFAULT);
2624 break;
2626 case REQ_VIEW_MAIN:
2627 case REQ_VIEW_DIFF:
2628 case REQ_VIEW_LOG:
2629 case REQ_VIEW_TREE:
2630 case REQ_VIEW_HELP:
2631 open_view(view, request, OPEN_DEFAULT);
2632 break;
2634 case REQ_NEXT:
2635 case REQ_PREVIOUS:
2636 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2638 if ((view == VIEW(REQ_VIEW_DIFF) &&
2639 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2640 (view == VIEW(REQ_VIEW_DIFF) &&
2641 view->parent == VIEW(REQ_VIEW_BLAME)) ||
2642 (view == VIEW(REQ_VIEW_STAGE) &&
2643 view->parent == VIEW(REQ_VIEW_STATUS)) ||
2644 (view == VIEW(REQ_VIEW_BLOB) &&
2645 view->parent == VIEW(REQ_VIEW_TREE))) {
2646 int line;
2648 view = view->parent;
2649 line = view->lineno;
2650 move_view(view, request);
2651 if (view_is_displayed(view))
2652 update_view_title(view);
2653 if (line != view->lineno)
2654 view->ops->request(view, REQ_ENTER,
2655 &view->line[view->lineno]);
2657 } else {
2658 move_view(view, request);
2659 }
2660 break;
2662 case REQ_VIEW_NEXT:
2663 {
2664 int nviews = displayed_views();
2665 int next_view = (current_view + 1) % nviews;
2667 if (next_view == current_view) {
2668 report("Only one view is displayed");
2669 break;
2670 }
2672 current_view = next_view;
2673 /* Blur out the title of the previous view. */
2674 update_view_title(view);
2675 report("");
2676 break;
2677 }
2678 case REQ_REFRESH:
2679 report("Refreshing is not yet supported for the %s view", view->name);
2680 break;
2682 case REQ_MAXIMIZE:
2683 if (displayed_views() == 2)
2684 open_view(view, VIEW_REQ(view), OPEN_DEFAULT);
2685 break;
2687 case REQ_TOGGLE_LINENO:
2688 opt_line_number = !opt_line_number;
2689 redraw_display();
2690 break;
2692 case REQ_TOGGLE_DATE:
2693 opt_date = !opt_date;
2694 redraw_display();
2695 break;
2697 case REQ_TOGGLE_AUTHOR:
2698 opt_author = !opt_author;
2699 redraw_display();
2700 break;
2702 case REQ_TOGGLE_REV_GRAPH:
2703 opt_rev_graph = !opt_rev_graph;
2704 redraw_display();
2705 break;
2707 case REQ_TOGGLE_REFS:
2708 opt_show_refs = !opt_show_refs;
2709 redraw_display();
2710 break;
2712 case REQ_PROMPT:
2713 /* Always reload^Wrerun commands from the prompt. */
2714 open_view(view, opt_request, OPEN_RELOAD);
2715 break;
2717 case REQ_SEARCH:
2718 case REQ_SEARCH_BACK:
2719 search_view(view, request);
2720 break;
2722 case REQ_FIND_NEXT:
2723 case REQ_FIND_PREV:
2724 find_next(view, request);
2725 break;
2727 case REQ_STOP_LOADING:
2728 for (i = 0; i < ARRAY_SIZE(views); i++) {
2729 view = &views[i];
2730 if (view->pipe)
2731 report("Stopped loading the %s view", view->name),
2732 end_update(view);
2733 }
2734 break;
2736 case REQ_SHOW_VERSION:
2737 report("tig-%s (built %s)", TIG_VERSION, __DATE__);
2738 return TRUE;
2740 case REQ_SCREEN_RESIZE:
2741 resize_display();
2742 /* Fall-through */
2743 case REQ_SCREEN_REDRAW:
2744 redraw_display();
2745 break;
2747 case REQ_EDIT:
2748 report("Nothing to edit");
2749 break;
2752 case REQ_ENTER:
2753 report("Nothing to enter");
2754 break;
2757 case REQ_VIEW_CLOSE:
2758 /* XXX: Mark closed views by letting view->parent point to the
2759 * view itself. Parents to closed view should never be
2760 * followed. */
2761 if (view->parent &&
2762 view->parent->parent != view->parent) {
2763 memset(display, 0, sizeof(display));
2764 current_view = 0;
2765 display[current_view] = view->parent;
2766 view->parent = view;
2767 resize_display();
2768 redraw_display();
2769 break;
2770 }
2771 /* Fall-through */
2772 case REQ_QUIT:
2773 return FALSE;
2775 default:
2776 /* An unknown key will show most commonly used commands. */
2777 report("Unknown key, press 'h' for help");
2778 return TRUE;
2779 }
2781 return TRUE;
2782 }
2785 /*
2786 * Pager backend
2787 */
2789 static bool
2790 pager_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2791 {
2792 char *text = line->data;
2793 int col = 0;
2795 if (opt_line_number) {
2796 col += draw_lineno(view, lineno, view->width, selected);
2797 if (col >= view->width)
2798 return TRUE;
2799 }
2801 if (!selected)
2802 wattrset(view->win, get_line_attr(line->type));
2804 draw_text(view, text, view->width - col, TRUE, selected);
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 else if (!view->parent)
3492 die("No blame exist for %s", view->vid);
3493 view->cmd[0] = 0;
3494 if (!pipe) {
3495 report("Failed to load blame data");
3496 return TRUE;
3497 }
3499 fclose(view->pipe);
3500 view->pipe = pipe;
3501 return FALSE;
3503 } else {
3504 size_t linelen = strlen(line);
3505 struct blame *blame = malloc(sizeof(*blame) + linelen);
3507 if (!line)
3508 return FALSE;
3510 blame->commit = NULL;
3511 strncpy(blame->text, line, linelen);
3512 blame->text[linelen] = 0;
3513 return add_line_data(view, blame, LINE_BLAME_ID) != NULL;
3514 }
3515 }
3517 static bool
3518 match_blame_header(const char *name, char **line)
3519 {
3520 size_t namelen = strlen(name);
3521 bool matched = !strncmp(name, *line, namelen);
3523 if (matched)
3524 *line += namelen;
3526 return matched;
3527 }
3529 static bool
3530 blame_read(struct view *view, char *line)
3531 {
3532 static struct blame_commit *commit = NULL;
3533 static int blamed = 0;
3534 static time_t author_time;
3536 if (*view->cmd)
3537 return blame_read_file(view, line);
3539 if (!line) {
3540 /* Reset all! */
3541 commit = NULL;
3542 blamed = 0;
3543 string_format(view->ref, "%s", view->vid);
3544 if (view_is_displayed(view)) {
3545 update_view_title(view);
3546 redraw_view_from(view, 0);
3547 }
3548 return TRUE;
3549 }
3551 if (!commit) {
3552 commit = parse_blame_commit(view, line, &blamed);
3553 string_format(view->ref, "%s %2d%%", view->vid,
3554 blamed * 100 / view->lines);
3556 } else if (match_blame_header("author ", &line)) {
3557 string_ncopy(commit->author, line, strlen(line));
3559 } else if (match_blame_header("author-time ", &line)) {
3560 author_time = (time_t) atol(line);
3562 } else if (match_blame_header("author-tz ", &line)) {
3563 long tz;
3565 tz = ('0' - line[1]) * 60 * 60 * 10;
3566 tz += ('0' - line[2]) * 60 * 60;
3567 tz += ('0' - line[3]) * 60;
3568 tz += ('0' - line[4]) * 60;
3570 if (line[0] == '-')
3571 tz = -tz;
3573 author_time -= tz;
3574 gmtime_r(&author_time, &commit->time);
3576 } else if (match_blame_header("summary ", &line)) {
3577 string_ncopy(commit->title, line, strlen(line));
3579 } else if (match_blame_header("filename ", &line)) {
3580 string_ncopy(commit->filename, line, strlen(line));
3581 commit = NULL;
3582 }
3584 return TRUE;
3585 }
3587 static bool
3588 blame_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
3589 {
3590 struct blame *blame = line->data;
3591 int col = 0;
3593 if (opt_date) {
3594 struct tm *time = blame->commit && *blame->commit->filename
3595 ? &blame->commit->time : NULL;
3597 col += draw_date(view, time, view->width, selected);
3598 if (col >= view->width)
3599 return TRUE;
3600 }
3602 if (opt_author) {
3603 int max = MIN(AUTHOR_COLS - 1, view->width - col);
3605 if (!selected)
3606 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
3607 if (blame->commit)
3608 draw_text(view, blame->commit->author, max, TRUE, selected);
3609 col += AUTHOR_COLS;
3610 if (col >= view->width)
3611 return TRUE;
3612 wmove(view->win, lineno, col);
3613 }
3615 {
3616 int max = MIN(ID_COLS - 1, view->width - col);
3618 if (!selected)
3619 wattrset(view->win, get_line_attr(LINE_BLAME_ID));
3620 if (blame->commit)
3621 draw_text(view, blame->commit->id, max, FALSE, -1);
3622 col += ID_COLS;
3623 if (col >= view->width)
3624 return TRUE;
3625 wmove(view->win, lineno, col);
3626 }
3628 col += draw_lineno(view, lineno, view->width - col, selected);
3629 if (col >= view->width)
3630 return TRUE;
3632 col += draw_text(view, blame->text, view->width - col, TRUE, selected);
3633 return TRUE;
3634 }
3636 static enum request
3637 blame_request(struct view *view, enum request request, struct line *line)
3638 {
3639 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3640 struct blame *blame = line->data;
3642 switch (request) {
3643 case REQ_ENTER:
3644 if (!blame->commit) {
3645 report("No commit loaded yet");
3646 break;
3647 }
3649 if (!strcmp(blame->commit->id, NULL_ID)) {
3650 char path[SIZEOF_STR];
3652 if (sq_quote(path, 0, view->vid) >= sizeof(path))
3653 break;
3654 string_format(opt_cmd, "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s 2>/dev/null", path);
3655 }
3657 open_view(view, REQ_VIEW_DIFF, flags);
3658 break;
3660 default:
3661 return request;
3662 }
3664 return REQ_NONE;
3665 }
3667 static bool
3668 blame_grep(struct view *view, struct line *line)
3669 {
3670 struct blame *blame = line->data;
3671 struct blame_commit *commit = blame->commit;
3672 regmatch_t pmatch;
3674 #define MATCH(text, on) \
3675 (on && *text && regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3677 if (commit) {
3678 char buf[DATE_COLS + 1];
3680 if (MATCH(commit->title, 1) ||
3681 MATCH(commit->author, opt_author) ||
3682 MATCH(commit->id, opt_date))
3683 return TRUE;
3685 if (strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time) &&
3686 MATCH(buf, 1))
3687 return TRUE;
3688 }
3690 return MATCH(blame->text, 1);
3692 #undef MATCH
3693 }
3695 static void
3696 blame_select(struct view *view, struct line *line)
3697 {
3698 struct blame *blame = line->data;
3699 struct blame_commit *commit = blame->commit;
3701 if (!commit)
3702 return;
3704 if (!strcmp(commit->id, NULL_ID))
3705 string_ncopy(ref_commit, "HEAD", 4);
3706 else
3707 string_copy_rev(ref_commit, commit->id);
3708 }
3710 static struct view_ops blame_ops = {
3711 "line",
3712 blame_open,
3713 blame_read,
3714 blame_draw,
3715 blame_request,
3716 blame_grep,
3717 blame_select,
3718 };
3720 /*
3721 * Status backend
3722 */
3724 struct status {
3725 char status;
3726 struct {
3727 mode_t mode;
3728 char rev[SIZEOF_REV];
3729 char name[SIZEOF_STR];
3730 } old;
3731 struct {
3732 mode_t mode;
3733 char rev[SIZEOF_REV];
3734 char name[SIZEOF_STR];
3735 } new;
3736 };
3738 static char status_onbranch[SIZEOF_STR];
3739 static struct status stage_status;
3740 static enum line_type stage_line_type;
3742 /* Get fields from the diff line:
3743 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
3744 */
3745 static inline bool
3746 status_get_diff(struct status *file, char *buf, size_t bufsize)
3747 {
3748 char *old_mode = buf + 1;
3749 char *new_mode = buf + 8;
3750 char *old_rev = buf + 15;
3751 char *new_rev = buf + 56;
3752 char *status = buf + 97;
3754 if (bufsize < 99 ||
3755 old_mode[-1] != ':' ||
3756 new_mode[-1] != ' ' ||
3757 old_rev[-1] != ' ' ||
3758 new_rev[-1] != ' ' ||
3759 status[-1] != ' ')
3760 return FALSE;
3762 file->status = *status;
3764 string_copy_rev(file->old.rev, old_rev);
3765 string_copy_rev(file->new.rev, new_rev);
3767 file->old.mode = strtoul(old_mode, NULL, 8);
3768 file->new.mode = strtoul(new_mode, NULL, 8);
3770 file->old.name[0] = file->new.name[0] = 0;
3772 return TRUE;
3773 }
3775 static bool
3776 status_run(struct view *view, const char cmd[], char status, enum line_type type)
3777 {
3778 struct status *file = NULL;
3779 struct status *unmerged = NULL;
3780 char buf[SIZEOF_STR * 4];
3781 size_t bufsize = 0;
3782 FILE *pipe;
3784 pipe = popen(cmd, "r");
3785 if (!pipe)
3786 return FALSE;
3788 add_line_data(view, NULL, type);
3790 while (!feof(pipe) && !ferror(pipe)) {
3791 char *sep;
3792 size_t readsize;
3794 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
3795 if (!readsize)
3796 break;
3797 bufsize += readsize;
3799 /* Process while we have NUL chars. */
3800 while ((sep = memchr(buf, 0, bufsize))) {
3801 size_t sepsize = sep - buf + 1;
3803 if (!file) {
3804 if (!realloc_lines(view, view->line_size + 1))
3805 goto error_out;
3807 file = calloc(1, sizeof(*file));
3808 if (!file)
3809 goto error_out;
3811 add_line_data(view, file, type);
3812 }
3814 /* Parse diff info part. */
3815 if (status) {
3816 file->status = status;
3817 if (status == 'A')
3818 string_copy(file->old.rev, NULL_ID);
3820 } else if (!file->status) {
3821 if (!status_get_diff(file, buf, sepsize))
3822 goto error_out;
3824 bufsize -= sepsize;
3825 memmove(buf, sep + 1, bufsize);
3827 sep = memchr(buf, 0, bufsize);
3828 if (!sep)
3829 break;
3830 sepsize = sep - buf + 1;
3832 /* Collapse all 'M'odified entries that
3833 * follow a associated 'U'nmerged entry.
3834 */
3835 if (file->status == 'U') {
3836 unmerged = file;
3838 } else if (unmerged) {
3839 int collapse = !strcmp(buf, unmerged->new.name);
3841 unmerged = NULL;
3842 if (collapse) {
3843 free(file);
3844 view->lines--;
3845 continue;
3846 }
3847 }
3848 }
3850 /* Grab the old name for rename/copy. */
3851 if (!*file->old.name &&
3852 (file->status == 'R' || file->status == 'C')) {
3853 sepsize = sep - buf + 1;
3854 string_ncopy(file->old.name, buf, sepsize);
3855 bufsize -= sepsize;
3856 memmove(buf, sep + 1, bufsize);
3858 sep = memchr(buf, 0, bufsize);
3859 if (!sep)
3860 break;
3861 sepsize = sep - buf + 1;
3862 }
3864 /* git-ls-files just delivers a NUL separated
3865 * list of file names similar to the second half
3866 * of the git-diff-* output. */
3867 string_ncopy(file->new.name, buf, sepsize);
3868 if (!*file->old.name)
3869 string_copy(file->old.name, file->new.name);
3870 bufsize -= sepsize;
3871 memmove(buf, sep + 1, bufsize);
3872 file = NULL;
3873 }
3874 }
3876 if (ferror(pipe)) {
3877 error_out:
3878 pclose(pipe);
3879 return FALSE;
3880 }
3882 if (!view->line[view->lines - 1].data)
3883 add_line_data(view, NULL, LINE_STAT_NONE);
3885 pclose(pipe);
3886 return TRUE;
3887 }
3889 /* Don't show unmerged entries in the staged section. */
3890 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached -M HEAD"
3891 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
3892 #define STATUS_LIST_OTHER_CMD \
3893 "git ls-files -z --others --exclude-per-directory=.gitignore"
3894 #define STATUS_LIST_NO_HEAD_CMD \
3895 "git ls-files -z --cached --exclude-per-directory=.gitignore"
3897 #define STATUS_DIFF_INDEX_SHOW_CMD \
3898 "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s %s 2>/dev/null"
3900 #define STATUS_DIFF_FILES_SHOW_CMD \
3901 "git diff-files --root --patch-with-stat -C -M -- %s %s 2>/dev/null"
3903 #define STATUS_DIFF_NO_HEAD_SHOW_CMD \
3904 "git diff --no-color --patch-with-stat /dev/null %s 2>/dev/null"
3906 /* First parse staged info using git-diff-index(1), then parse unstaged
3907 * info using git-diff-files(1), and finally untracked files using
3908 * git-ls-files(1). */
3909 static bool
3910 status_open(struct view *view)
3911 {
3912 struct stat statbuf;
3913 char exclude[SIZEOF_STR];
3914 char indexcmd[SIZEOF_STR] = STATUS_DIFF_INDEX_CMD;
3915 char othercmd[SIZEOF_STR] = STATUS_LIST_OTHER_CMD;
3916 unsigned long prev_lineno = view->lineno;
3917 char indexstatus = 0;
3918 size_t i;
3920 for (i = 0; i < view->lines; i++)
3921 free(view->line[i].data);
3922 free(view->line);
3923 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3924 view->line = NULL;
3926 if (!realloc_lines(view, view->line_size + 7))
3927 return FALSE;
3929 add_line_data(view, NULL, LINE_STAT_HEAD);
3930 if (opt_no_head)
3931 string_copy(status_onbranch, "Initial commit");
3932 else if (!*opt_head)
3933 string_copy(status_onbranch, "Not currently on any branch");
3934 else if (!string_format(status_onbranch, "On branch %s", opt_head))
3935 return FALSE;
3937 if (opt_no_head) {
3938 string_copy(indexcmd, STATUS_LIST_NO_HEAD_CMD);
3939 indexstatus = 'A';
3940 }
3942 if (!string_format(exclude, "%s/info/exclude", opt_git_dir))
3943 return FALSE;
3945 if (stat(exclude, &statbuf) >= 0) {
3946 size_t cmdsize = strlen(othercmd);
3948 if (!string_format_from(othercmd, &cmdsize, " %s", "--exclude-from=") ||
3949 sq_quote(othercmd, cmdsize, exclude) >= sizeof(othercmd))
3950 return FALSE;
3952 cmdsize = strlen(indexcmd);
3953 if (opt_no_head &&
3954 (!string_format_from(indexcmd, &cmdsize, " %s", "--exclude-from=") ||
3955 sq_quote(indexcmd, cmdsize, exclude) >= sizeof(indexcmd)))
3956 return FALSE;
3957 }
3959 system("git update-index -q --refresh >/dev/null 2>/dev/null");
3961 if (!status_run(view, indexcmd, indexstatus, LINE_STAT_STAGED) ||
3962 !status_run(view, STATUS_DIFF_FILES_CMD, 0, LINE_STAT_UNSTAGED) ||
3963 !status_run(view, othercmd, '?', LINE_STAT_UNTRACKED))
3964 return FALSE;
3966 /* If all went well restore the previous line number to stay in
3967 * the context or select a line with something that can be
3968 * updated. */
3969 if (prev_lineno >= view->lines)
3970 prev_lineno = view->lines - 1;
3971 while (prev_lineno < view->lines && !view->line[prev_lineno].data)
3972 prev_lineno++;
3973 while (prev_lineno > 0 && !view->line[prev_lineno].data)
3974 prev_lineno--;
3976 /* If the above fails, always skip the "On branch" line. */
3977 if (prev_lineno < view->lines)
3978 view->lineno = prev_lineno;
3979 else
3980 view->lineno = 1;
3982 if (view->lineno < view->offset)
3983 view->offset = view->lineno;
3984 else if (view->offset + view->height <= view->lineno)
3985 view->offset = view->lineno - view->height + 1;
3987 return TRUE;
3988 }
3990 static bool
3991 status_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
3992 {
3993 struct status *status = line->data;
3994 char *text;
3995 int col = 0;
3997 if (selected) {
3998 /* No attributes. */
4000 } else if (line->type == LINE_STAT_HEAD) {
4001 wattrset(view->win, get_line_attr(LINE_STAT_HEAD));
4002 wchgat(view->win, -1, 0, LINE_STAT_HEAD, NULL);
4004 } else if (!status && line->type != LINE_STAT_NONE) {
4005 wattrset(view->win, get_line_attr(LINE_STAT_SECTION));
4006 wchgat(view->win, -1, 0, LINE_STAT_SECTION, NULL);
4008 } else {
4009 wattrset(view->win, get_line_attr(line->type));
4010 }
4012 if (!status) {
4013 switch (line->type) {
4014 case LINE_STAT_STAGED:
4015 text = "Changes to be committed:";
4016 break;
4018 case LINE_STAT_UNSTAGED:
4019 text = "Changed but not updated:";
4020 break;
4022 case LINE_STAT_UNTRACKED:
4023 text = "Untracked files:";
4024 break;
4026 case LINE_STAT_NONE:
4027 text = " (no files)";
4028 break;
4030 case LINE_STAT_HEAD:
4031 text = status_onbranch;
4032 break;
4034 default:
4035 return FALSE;
4036 }
4037 } else {
4038 char buf[] = { status->status, ' ', ' ', ' ', 0 };
4040 col += draw_text(view, buf, view->width, TRUE, selected);
4041 if (!selected)
4042 wattrset(view->win, A_NORMAL);
4043 text = status->new.name;
4044 }
4046 draw_text(view, text, view->width - col, TRUE, selected);
4047 return TRUE;
4048 }
4050 static enum request
4051 status_enter(struct view *view, struct line *line)
4052 {
4053 struct status *status = line->data;
4054 char oldpath[SIZEOF_STR] = "";
4055 char newpath[SIZEOF_STR] = "";
4056 char *info;
4057 size_t cmdsize = 0;
4058 enum open_flags split;
4060 if (line->type == LINE_STAT_NONE ||
4061 (!status && line[1].type == LINE_STAT_NONE)) {
4062 report("No file to diff");
4063 return REQ_NONE;
4064 }
4066 if (status) {
4067 if (sq_quote(oldpath, 0, status->old.name) >= sizeof(oldpath))
4068 return REQ_QUIT;
4069 /* Diffs for unmerged entries are empty when pasing the
4070 * new path, so leave it empty. */
4071 if (status->status != 'U' &&
4072 sq_quote(newpath, 0, status->new.name) >= sizeof(newpath))
4073 return REQ_QUIT;
4074 }
4076 if (opt_cdup[0] &&
4077 line->type != LINE_STAT_UNTRACKED &&
4078 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
4079 return REQ_QUIT;
4081 switch (line->type) {
4082 case LINE_STAT_STAGED:
4083 if (opt_no_head) {
4084 if (!string_format_from(opt_cmd, &cmdsize,
4085 STATUS_DIFF_NO_HEAD_SHOW_CMD,
4086 newpath))
4087 return REQ_QUIT;
4088 } else {
4089 if (!string_format_from(opt_cmd, &cmdsize,
4090 STATUS_DIFF_INDEX_SHOW_CMD,
4091 oldpath, newpath))
4092 return REQ_QUIT;
4093 }
4095 if (status)
4096 info = "Staged changes to %s";
4097 else
4098 info = "Staged changes";
4099 break;
4101 case LINE_STAT_UNSTAGED:
4102 if (!string_format_from(opt_cmd, &cmdsize,
4103 STATUS_DIFF_FILES_SHOW_CMD, oldpath, newpath))
4104 return REQ_QUIT;
4105 if (status)
4106 info = "Unstaged changes to %s";
4107 else
4108 info = "Unstaged changes";
4109 break;
4111 case LINE_STAT_UNTRACKED:
4112 if (opt_pipe)
4113 return REQ_QUIT;
4115 if (!status) {
4116 report("No file to show");
4117 return REQ_NONE;
4118 }
4120 opt_pipe = fopen(status->new.name, "r");
4121 info = "Untracked file %s";
4122 break;
4124 case LINE_STAT_HEAD:
4125 return REQ_NONE;
4127 default:
4128 die("line type %d not handled in switch", line->type);
4129 }
4131 split = view_is_displayed(view) ? OPEN_SPLIT : 0;
4132 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | split);
4133 if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) {
4134 if (status) {
4135 stage_status = *status;
4136 } else {
4137 memset(&stage_status, 0, sizeof(stage_status));
4138 }
4140 stage_line_type = line->type;
4141 string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.new.name);
4142 }
4144 return REQ_NONE;
4145 }
4147 static bool
4148 status_exists(struct status *status, enum line_type type)
4149 {
4150 struct view *view = VIEW(REQ_VIEW_STATUS);
4151 struct line *line;
4153 for (line = view->line; line < view->line + view->lines; line++) {
4154 struct status *pos = line->data;
4156 if (line->type == type && pos &&
4157 !strcmp(status->new.name, pos->new.name))
4158 return TRUE;
4159 }
4161 return FALSE;
4162 }
4165 static FILE *
4166 status_update_prepare(enum line_type type)
4167 {
4168 char cmd[SIZEOF_STR];
4169 size_t cmdsize = 0;
4171 if (opt_cdup[0] &&
4172 type != LINE_STAT_UNTRACKED &&
4173 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4174 return NULL;
4176 switch (type) {
4177 case LINE_STAT_STAGED:
4178 string_add(cmd, cmdsize, "git update-index -z --index-info");
4179 break;
4181 case LINE_STAT_UNSTAGED:
4182 case LINE_STAT_UNTRACKED:
4183 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
4184 break;
4186 default:
4187 die("line type %d not handled in switch", type);
4188 }
4190 return popen(cmd, "w");
4191 }
4193 static bool
4194 status_update_write(FILE *pipe, struct status *status, enum line_type type)
4195 {
4196 char buf[SIZEOF_STR];
4197 size_t bufsize = 0;
4198 size_t written = 0;
4200 switch (type) {
4201 case LINE_STAT_STAGED:
4202 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
4203 status->old.mode,
4204 status->old.rev,
4205 status->old.name, 0))
4206 return FALSE;
4207 break;
4209 case LINE_STAT_UNSTAGED:
4210 case LINE_STAT_UNTRACKED:
4211 if (!string_format_from(buf, &bufsize, "%s%c", status->new.name, 0))
4212 return FALSE;
4213 break;
4215 default:
4216 die("line type %d not handled in switch", type);
4217 }
4219 while (!ferror(pipe) && written < bufsize) {
4220 written += fwrite(buf + written, 1, bufsize - written, pipe);
4221 }
4223 return written == bufsize;
4224 }
4226 static bool
4227 status_update_file(struct status *status, enum line_type type)
4228 {
4229 FILE *pipe = status_update_prepare(type);
4230 bool result;
4232 if (!pipe)
4233 return FALSE;
4235 result = status_update_write(pipe, status, type);
4236 pclose(pipe);
4237 return result;
4238 }
4240 static bool
4241 status_update_files(struct view *view, struct line *line)
4242 {
4243 FILE *pipe = status_update_prepare(line->type);
4244 bool result = TRUE;
4245 struct line *pos = view->line + view->lines;
4246 int files = 0;
4247 int file, done;
4249 if (!pipe)
4250 return FALSE;
4252 for (pos = line; pos < view->line + view->lines && pos->data; pos++)
4253 files++;
4255 for (file = 0, done = 0; result && file < files; line++, file++) {
4256 int almost_done = file * 100 / files;
4258 if (almost_done > done) {
4259 done = almost_done;
4260 string_format(view->ref, "updating file %u of %u (%d%% done)",
4261 file, files, done);
4262 update_view_title(view);
4263 }
4264 result = status_update_write(pipe, line->data, line->type);
4265 }
4267 pclose(pipe);
4268 return result;
4269 }
4271 static bool
4272 status_update(struct view *view)
4273 {
4274 struct line *line = &view->line[view->lineno];
4276 assert(view->lines);
4278 if (!line->data) {
4279 /* This should work even for the "On branch" line. */
4280 if (line < view->line + view->lines && !line[1].data) {
4281 report("Nothing to update");
4282 return FALSE;
4283 }
4285 if (!status_update_files(view, line + 1)) {
4286 report("Failed to update file status");
4287 return FALSE;
4288 }
4290 } else if (!status_update_file(line->data, line->type)) {
4291 report("Failed to update file status");
4292 return FALSE;
4293 }
4295 return TRUE;
4296 }
4298 static enum request
4299 status_request(struct view *view, enum request request, struct line *line)
4300 {
4301 struct status *status = line->data;
4303 switch (request) {
4304 case REQ_STATUS_UPDATE:
4305 if (!status_update(view))
4306 return REQ_NONE;
4307 break;
4309 case REQ_STATUS_MERGE:
4310 if (!status || status->status != 'U') {
4311 report("Merging only possible for files with unmerged status ('U').");
4312 return REQ_NONE;
4313 }
4314 open_mergetool(status->new.name);
4315 break;
4317 case REQ_EDIT:
4318 if (!status)
4319 return request;
4321 open_editor(status->status != '?', status->new.name);
4322 break;
4324 case REQ_VIEW_BLAME:
4325 if (status) {
4326 string_copy(opt_file, status->new.name);
4327 opt_ref[0] = 0;
4328 }
4329 return request;
4331 case REQ_ENTER:
4332 /* After returning the status view has been split to
4333 * show the stage view. No further reloading is
4334 * necessary. */
4335 status_enter(view, line);
4336 return REQ_NONE;
4338 case REQ_REFRESH:
4339 /* Simply reload the view. */
4340 break;
4342 default:
4343 return request;
4344 }
4346 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
4348 return REQ_NONE;
4349 }
4351 static void
4352 status_select(struct view *view, struct line *line)
4353 {
4354 struct status *status = line->data;
4355 char file[SIZEOF_STR] = "all files";
4356 char *text;
4357 char *key;
4359 if (status && !string_format(file, "'%s'", status->new.name))
4360 return;
4362 if (!status && line[1].type == LINE_STAT_NONE)
4363 line++;
4365 switch (line->type) {
4366 case LINE_STAT_STAGED:
4367 text = "Press %s to unstage %s for commit";
4368 break;
4370 case LINE_STAT_UNSTAGED:
4371 text = "Press %s to stage %s for commit";
4372 break;
4374 case LINE_STAT_UNTRACKED:
4375 text = "Press %s to stage %s for addition";
4376 break;
4378 case LINE_STAT_HEAD:
4379 case LINE_STAT_NONE:
4380 text = "Nothing to update";
4381 break;
4383 default:
4384 die("line type %d not handled in switch", line->type);
4385 }
4387 if (status && status->status == 'U') {
4388 text = "Press %s to resolve conflict in %s";
4389 key = get_key(REQ_STATUS_MERGE);
4391 } else {
4392 key = get_key(REQ_STATUS_UPDATE);
4393 }
4395 string_format(view->ref, text, key, file);
4396 }
4398 static bool
4399 status_grep(struct view *view, struct line *line)
4400 {
4401 struct status *status = line->data;
4402 enum { S_STATUS, S_NAME, S_END } state;
4403 char buf[2] = "?";
4404 regmatch_t pmatch;
4406 if (!status)
4407 return FALSE;
4409 for (state = S_STATUS; state < S_END; state++) {
4410 char *text;
4412 switch (state) {
4413 case S_NAME: text = status->new.name; break;
4414 case S_STATUS:
4415 buf[0] = status->status;
4416 text = buf;
4417 break;
4419 default:
4420 return FALSE;
4421 }
4423 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4424 return TRUE;
4425 }
4427 return FALSE;
4428 }
4430 static struct view_ops status_ops = {
4431 "file",
4432 status_open,
4433 NULL,
4434 status_draw,
4435 status_request,
4436 status_grep,
4437 status_select,
4438 };
4441 static bool
4442 stage_diff_line(FILE *pipe, struct line *line)
4443 {
4444 char *buf = line->data;
4445 size_t bufsize = strlen(buf);
4446 size_t written = 0;
4448 while (!ferror(pipe) && written < bufsize) {
4449 written += fwrite(buf + written, 1, bufsize - written, pipe);
4450 }
4452 fputc('\n', pipe);
4454 return written == bufsize;
4455 }
4457 static bool
4458 stage_diff_write(FILE *pipe, struct line *line, struct line *end)
4459 {
4460 while (line < end) {
4461 if (!stage_diff_line(pipe, line++))
4462 return FALSE;
4463 if (line->type == LINE_DIFF_CHUNK ||
4464 line->type == LINE_DIFF_HEADER)
4465 break;
4466 }
4468 return TRUE;
4469 }
4471 static struct line *
4472 stage_diff_find(struct view *view, struct line *line, enum line_type type)
4473 {
4474 for (; view->line < line; line--)
4475 if (line->type == type)
4476 return line;
4478 return NULL;
4479 }
4481 static bool
4482 stage_update_chunk(struct view *view, struct line *chunk)
4483 {
4484 char cmd[SIZEOF_STR];
4485 size_t cmdsize = 0;
4486 struct line *diff_hdr;
4487 FILE *pipe;
4489 diff_hdr = stage_diff_find(view, chunk, LINE_DIFF_HEADER);
4490 if (!diff_hdr)
4491 return FALSE;
4493 if (opt_cdup[0] &&
4494 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4495 return FALSE;
4497 if (!string_format_from(cmd, &cmdsize,
4498 "git apply --whitespace=nowarn --cached %s - && "
4499 "git update-index -q --unmerged --refresh 2>/dev/null",
4500 stage_line_type == LINE_STAT_STAGED ? "-R" : ""))
4501 return FALSE;
4503 pipe = popen(cmd, "w");
4504 if (!pipe)
4505 return FALSE;
4507 if (!stage_diff_write(pipe, diff_hdr, chunk) ||
4508 !stage_diff_write(pipe, chunk, view->line + view->lines))
4509 chunk = NULL;
4511 pclose(pipe);
4513 return chunk ? TRUE : FALSE;
4514 }
4516 static bool
4517 stage_update(struct view *view, struct line *line)
4518 {
4519 struct line *chunk = NULL;
4521 if (!opt_no_head && stage_line_type != LINE_STAT_UNTRACKED)
4522 chunk = stage_diff_find(view, line, LINE_DIFF_CHUNK);
4524 if (chunk) {
4525 if (!stage_update_chunk(view, chunk)) {
4526 report("Failed to apply chunk");
4527 return FALSE;
4528 }
4530 } else if (!stage_status.status) {
4531 view = VIEW(REQ_VIEW_STATUS);
4533 for (line = view->line; line < view->line + view->lines; line++)
4534 if (line->type == stage_line_type)
4535 break;
4537 if (!status_update_files(view, line + 1)) {
4538 report("Failed to update files");
4539 return FALSE;
4540 }
4542 } else if (!status_update_file(&stage_status, stage_line_type)) {
4543 report("Failed to update file");
4544 return FALSE;
4545 }
4547 return TRUE;
4548 }
4550 static enum request
4551 stage_request(struct view *view, enum request request, struct line *line)
4552 {
4553 switch (request) {
4554 case REQ_STATUS_UPDATE:
4555 if (!stage_update(view, line))
4556 return REQ_NONE;
4557 break;
4559 case REQ_EDIT:
4560 if (!stage_status.new.name[0])
4561 return request;
4563 open_editor(stage_status.status != '?', stage_status.new.name);
4564 break;
4566 case REQ_REFRESH:
4567 /* Reload everything ... */
4568 break;
4570 case REQ_VIEW_BLAME:
4571 if (stage_status.new.name[0]) {
4572 string_copy(opt_file, stage_status.new.name);
4573 opt_ref[0] = 0;
4574 }
4575 return request;
4577 case REQ_ENTER:
4578 return pager_request(view, request, line);
4580 default:
4581 return request;
4582 }
4584 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD | OPEN_NOMAXIMIZE);
4586 /* Check whether the staged entry still exists, and close the
4587 * stage view if it doesn't. */
4588 if (!status_exists(&stage_status, stage_line_type))
4589 return REQ_VIEW_CLOSE;
4591 if (stage_line_type == LINE_STAT_UNTRACKED)
4592 opt_pipe = fopen(stage_status.new.name, "r");
4593 else
4594 string_copy(opt_cmd, view->cmd);
4595 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | OPEN_NOMAXIMIZE);
4597 return REQ_NONE;
4598 }
4600 static struct view_ops stage_ops = {
4601 "line",
4602 NULL,
4603 pager_read,
4604 pager_draw,
4605 stage_request,
4606 pager_grep,
4607 pager_select,
4608 };
4611 /*
4612 * Revision graph
4613 */
4615 struct commit {
4616 char id[SIZEOF_REV]; /* SHA1 ID. */
4617 char title[128]; /* First line of the commit message. */
4618 char author[75]; /* Author of the commit. */
4619 struct tm time; /* Date from the author ident. */
4620 struct ref **refs; /* Repository references. */
4621 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
4622 size_t graph_size; /* The width of the graph array. */
4623 bool has_parents; /* Rewritten --parents seen. */
4624 };
4626 /* Size of rev graph with no "padding" columns */
4627 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
4629 struct rev_graph {
4630 struct rev_graph *prev, *next, *parents;
4631 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
4632 size_t size;
4633 struct commit *commit;
4634 size_t pos;
4635 unsigned int boundary:1;
4636 };
4638 /* Parents of the commit being visualized. */
4639 static struct rev_graph graph_parents[4];
4641 /* The current stack of revisions on the graph. */
4642 static struct rev_graph graph_stacks[4] = {
4643 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
4644 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
4645 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
4646 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
4647 };
4649 static inline bool
4650 graph_parent_is_merge(struct rev_graph *graph)
4651 {
4652 return graph->parents->size > 1;
4653 }
4655 static inline void
4656 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
4657 {
4658 struct commit *commit = graph->commit;
4660 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
4661 commit->graph[commit->graph_size++] = symbol;
4662 }
4664 static void
4665 done_rev_graph(struct rev_graph *graph)
4666 {
4667 if (graph_parent_is_merge(graph) &&
4668 graph->pos < graph->size - 1 &&
4669 graph->next->size == graph->size + graph->parents->size - 1) {
4670 size_t i = graph->pos + graph->parents->size - 1;
4672 graph->commit->graph_size = i * 2;
4673 while (i < graph->next->size - 1) {
4674 append_to_rev_graph(graph, ' ');
4675 append_to_rev_graph(graph, '\\');
4676 i++;
4677 }
4678 }
4680 graph->size = graph->pos = 0;
4681 graph->commit = NULL;
4682 memset(graph->parents, 0, sizeof(*graph->parents));
4683 }
4685 static void
4686 push_rev_graph(struct rev_graph *graph, char *parent)
4687 {
4688 int i;
4690 /* "Collapse" duplicate parents lines.
4691 *
4692 * FIXME: This needs to also update update the drawn graph but
4693 * for now it just serves as a method for pruning graph lines. */
4694 for (i = 0; i < graph->size; i++)
4695 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
4696 return;
4698 if (graph->size < SIZEOF_REVITEMS) {
4699 string_copy_rev(graph->rev[graph->size++], parent);
4700 }
4701 }
4703 static chtype
4704 get_rev_graph_symbol(struct rev_graph *graph)
4705 {
4706 chtype symbol;
4708 if (graph->boundary)
4709 symbol = REVGRAPH_BOUND;
4710 else if (graph->parents->size == 0)
4711 symbol = REVGRAPH_INIT;
4712 else if (graph_parent_is_merge(graph))
4713 symbol = REVGRAPH_MERGE;
4714 else if (graph->pos >= graph->size)
4715 symbol = REVGRAPH_BRANCH;
4716 else
4717 symbol = REVGRAPH_COMMIT;
4719 return symbol;
4720 }
4722 static void
4723 draw_rev_graph(struct rev_graph *graph)
4724 {
4725 struct rev_filler {
4726 chtype separator, line;
4727 };
4728 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
4729 static struct rev_filler fillers[] = {
4730 { ' ', REVGRAPH_LINE },
4731 { '`', '.' },
4732 { '\'', ' ' },
4733 { '/', ' ' },
4734 };
4735 chtype symbol = get_rev_graph_symbol(graph);
4736 struct rev_filler *filler;
4737 size_t i;
4739 filler = &fillers[DEFAULT];
4741 for (i = 0; i < graph->pos; i++) {
4742 append_to_rev_graph(graph, filler->line);
4743 if (graph_parent_is_merge(graph->prev) &&
4744 graph->prev->pos == i)
4745 filler = &fillers[RSHARP];
4747 append_to_rev_graph(graph, filler->separator);
4748 }
4750 /* Place the symbol for this revision. */
4751 append_to_rev_graph(graph, symbol);
4753 if (graph->prev->size > graph->size)
4754 filler = &fillers[RDIAG];
4755 else
4756 filler = &fillers[DEFAULT];
4758 i++;
4760 for (; i < graph->size; i++) {
4761 append_to_rev_graph(graph, filler->separator);
4762 append_to_rev_graph(graph, filler->line);
4763 if (graph_parent_is_merge(graph->prev) &&
4764 i < graph->prev->pos + graph->parents->size)
4765 filler = &fillers[RSHARP];
4766 if (graph->prev->size > graph->size)
4767 filler = &fillers[LDIAG];
4768 }
4770 if (graph->prev->size > graph->size) {
4771 append_to_rev_graph(graph, filler->separator);
4772 if (filler->line != ' ')
4773 append_to_rev_graph(graph, filler->line);
4774 }
4775 }
4777 /* Prepare the next rev graph */
4778 static void
4779 prepare_rev_graph(struct rev_graph *graph)
4780 {
4781 size_t i;
4783 /* First, traverse all lines of revisions up to the active one. */
4784 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
4785 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
4786 break;
4788 push_rev_graph(graph->next, graph->rev[graph->pos]);
4789 }
4791 /* Interleave the new revision parent(s). */
4792 for (i = 0; !graph->boundary && i < graph->parents->size; i++)
4793 push_rev_graph(graph->next, graph->parents->rev[i]);
4795 /* Lastly, put any remaining revisions. */
4796 for (i = graph->pos + 1; i < graph->size; i++)
4797 push_rev_graph(graph->next, graph->rev[i]);
4798 }
4800 static void
4801 update_rev_graph(struct rev_graph *graph)
4802 {
4803 /* If this is the finalizing update ... */
4804 if (graph->commit)
4805 prepare_rev_graph(graph);
4807 /* Graph visualization needs a one rev look-ahead,
4808 * so the first update doesn't visualize anything. */
4809 if (!graph->prev->commit)
4810 return;
4812 draw_rev_graph(graph->prev);
4813 done_rev_graph(graph->prev->prev);
4814 }
4817 /*
4818 * Main view backend
4819 */
4821 static bool
4822 main_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
4823 {
4824 struct commit *commit = line->data;
4825 enum line_type type;
4826 int col = 0;
4828 if (!*commit->author)
4829 return FALSE;
4831 if (selected) {
4832 type = LINE_CURSOR;
4833 } else {
4834 type = LINE_MAIN_COMMIT;
4835 }
4837 if (opt_date) {
4838 col += draw_date(view, &commit->time, view->width, selected);
4839 if (col >= view->width)
4840 return TRUE;
4841 }
4842 if (type != LINE_CURSOR)
4843 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
4845 if (opt_author) {
4846 int max_len;
4848 max_len = view->width - col;
4849 if (max_len > AUTHOR_COLS - 1)
4850 max_len = AUTHOR_COLS - 1;
4851 draw_text(view, commit->author, max_len, TRUE, selected);
4852 col += AUTHOR_COLS;
4853 if (col >= view->width)
4854 return TRUE;
4855 }
4857 if (opt_rev_graph && commit->graph_size) {
4858 size_t graph_size = view->width - col;
4859 size_t i;
4861 if (type != LINE_CURSOR)
4862 wattrset(view->win, get_line_attr(LINE_MAIN_REVGRAPH));
4863 wmove(view->win, lineno, col);
4864 if (graph_size > commit->graph_size)
4865 graph_size = commit->graph_size;
4866 /* Using waddch() instead of waddnstr() ensures that
4867 * they'll be rendered correctly for the cursor line. */
4868 for (i = 0; i < graph_size; i++)
4869 waddch(view->win, commit->graph[i]);
4871 col += commit->graph_size + 1;
4872 if (col >= view->width)
4873 return TRUE;
4874 waddch(view->win, ' ');
4875 }
4876 if (type != LINE_CURSOR)
4877 wattrset(view->win, A_NORMAL);
4879 wmove(view->win, lineno, col);
4881 if (opt_show_refs && commit->refs) {
4882 size_t i = 0;
4884 do {
4885 if (type == LINE_CURSOR)
4886 ;
4887 else if (commit->refs[i]->head)
4888 wattrset(view->win, get_line_attr(LINE_MAIN_HEAD));
4889 else if (commit->refs[i]->ltag)
4890 wattrset(view->win, get_line_attr(LINE_MAIN_LOCAL_TAG));
4891 else if (commit->refs[i]->tag)
4892 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
4893 else if (commit->refs[i]->tracked)
4894 wattrset(view->win, get_line_attr(LINE_MAIN_TRACKED));
4895 else if (commit->refs[i]->remote)
4896 wattrset(view->win, get_line_attr(LINE_MAIN_REMOTE));
4897 else
4898 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
4900 col += draw_text(view, "[", view->width - col, TRUE, selected);
4901 col += draw_text(view, commit->refs[i]->name, view->width - col,
4902 TRUE, selected);
4903 col += draw_text(view, "]", view->width - col, TRUE, selected);
4904 if (type != LINE_CURSOR)
4905 wattrset(view->win, A_NORMAL);
4906 col += draw_text(view, " ", view->width - col, TRUE, selected);
4907 if (col >= view->width)
4908 return TRUE;
4909 } while (commit->refs[i++]->next);
4910 }
4912 if (type != LINE_CURSOR)
4913 wattrset(view->win, get_line_attr(type));
4915 draw_text(view, commit->title, view->width - col, TRUE, selected);
4916 return TRUE;
4917 }
4919 /* Reads git log --pretty=raw output and parses it into the commit struct. */
4920 static bool
4921 main_read(struct view *view, char *line)
4922 {
4923 static struct rev_graph *graph = graph_stacks;
4924 enum line_type type;
4925 struct commit *commit;
4927 if (!line) {
4928 if (!view->lines && !view->parent)
4929 die("No revisions match the given arguments.");
4930 update_rev_graph(graph);
4931 return TRUE;
4932 }
4934 type = get_line_type(line);
4935 if (type == LINE_COMMIT) {
4936 commit = calloc(1, sizeof(struct commit));
4937 if (!commit)
4938 return FALSE;
4940 line += STRING_SIZE("commit ");
4941 if (*line == '-') {
4942 graph->boundary = 1;
4943 line++;
4944 }
4946 string_copy_rev(commit->id, line);
4947 commit->refs = get_refs(commit->id);
4948 graph->commit = commit;
4949 add_line_data(view, commit, LINE_MAIN_COMMIT);
4951 while ((line = strchr(line, ' '))) {
4952 line++;
4953 push_rev_graph(graph->parents, line);
4954 commit->has_parents = TRUE;
4955 }
4956 return TRUE;
4957 }
4959 if (!view->lines)
4960 return TRUE;
4961 commit = view->line[view->lines - 1].data;
4963 switch (type) {
4964 case LINE_PARENT:
4965 if (commit->has_parents)
4966 break;
4967 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
4968 break;
4970 case LINE_AUTHOR:
4971 {
4972 /* Parse author lines where the name may be empty:
4973 * author <email@address.tld> 1138474660 +0100
4974 */
4975 char *ident = line + STRING_SIZE("author ");
4976 char *nameend = strchr(ident, '<');
4977 char *emailend = strchr(ident, '>');
4979 if (!nameend || !emailend)
4980 break;
4982 update_rev_graph(graph);
4983 graph = graph->next;
4985 *nameend = *emailend = 0;
4986 ident = chomp_string(ident);
4987 if (!*ident) {
4988 ident = chomp_string(nameend + 1);
4989 if (!*ident)
4990 ident = "Unknown";
4991 }
4993 string_ncopy(commit->author, ident, strlen(ident));
4995 /* Parse epoch and timezone */
4996 if (emailend[1] == ' ') {
4997 char *secs = emailend + 2;
4998 char *zone = strchr(secs, ' ');
4999 time_t time = (time_t) atol(secs);
5001 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
5002 long tz;
5004 zone++;
5005 tz = ('0' - zone[1]) * 60 * 60 * 10;
5006 tz += ('0' - zone[2]) * 60 * 60;
5007 tz += ('0' - zone[3]) * 60;
5008 tz += ('0' - zone[4]) * 60;
5010 if (zone[0] == '-')
5011 tz = -tz;
5013 time -= tz;
5014 }
5016 gmtime_r(&time, &commit->time);
5017 }
5018 break;
5019 }
5020 default:
5021 /* Fill in the commit title if it has not already been set. */
5022 if (commit->title[0])
5023 break;
5025 /* Require titles to start with a non-space character at the
5026 * offset used by git log. */
5027 if (strncmp(line, " ", 4))
5028 break;
5029 line += 4;
5030 /* Well, if the title starts with a whitespace character,
5031 * try to be forgiving. Otherwise we end up with no title. */
5032 while (isspace(*line))
5033 line++;
5034 if (*line == '\0')
5035 break;
5036 /* FIXME: More graceful handling of titles; append "..." to
5037 * shortened titles, etc. */
5039 string_ncopy(commit->title, line, strlen(line));
5040 }
5042 return TRUE;
5043 }
5045 static enum request
5046 main_request(struct view *view, enum request request, struct line *line)
5047 {
5048 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
5050 if (request == REQ_ENTER)
5051 open_view(view, REQ_VIEW_DIFF, flags);
5052 else
5053 return request;
5055 return REQ_NONE;
5056 }
5058 static bool
5059 grep_refs(struct ref **refs, regex_t *regex)
5060 {
5061 regmatch_t pmatch;
5062 size_t i = 0;
5064 if (!refs)
5065 return FALSE;
5066 do {
5067 if (regexec(regex, refs[i]->name, 1, &pmatch, 0) != REG_NOMATCH)
5068 return TRUE;
5069 } while (refs[i++]->next);
5071 return FALSE;
5072 }
5074 static bool
5075 main_grep(struct view *view, struct line *line)
5076 {
5077 struct commit *commit = line->data;
5078 enum { S_TITLE, S_AUTHOR, S_DATE, S_REFS, S_END } state;
5079 char buf[DATE_COLS + 1];
5080 regmatch_t pmatch;
5082 for (state = S_TITLE; state < S_END; state++) {
5083 char *text;
5085 switch (state) {
5086 case S_TITLE: text = commit->title; break;
5087 case S_AUTHOR:
5088 if (!opt_author)
5089 continue;
5090 text = commit->author;
5091 break;
5092 case S_DATE:
5093 if (!opt_date)
5094 continue;
5095 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
5096 continue;
5097 text = buf;
5098 break;
5099 case S_REFS:
5100 if (!opt_show_refs)
5101 continue;
5102 if (grep_refs(commit->refs, view->regex) == TRUE)
5103 return TRUE;
5104 continue;
5105 default:
5106 return FALSE;
5107 }
5109 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
5110 return TRUE;
5111 }
5113 return FALSE;
5114 }
5116 static void
5117 main_select(struct view *view, struct line *line)
5118 {
5119 struct commit *commit = line->data;
5121 string_copy_rev(view->ref, commit->id);
5122 string_copy_rev(ref_commit, view->ref);
5123 }
5125 static struct view_ops main_ops = {
5126 "commit",
5127 NULL,
5128 main_read,
5129 main_draw,
5130 main_request,
5131 main_grep,
5132 main_select,
5133 };
5136 /*
5137 * Unicode / UTF-8 handling
5138 *
5139 * NOTE: Much of the following code for dealing with unicode is derived from
5140 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
5141 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
5142 */
5144 /* I've (over)annotated a lot of code snippets because I am not entirely
5145 * confident that the approach taken by this small UTF-8 interface is correct.
5146 * --jonas */
5148 static inline int
5149 unicode_width(unsigned long c)
5150 {
5151 if (c >= 0x1100 &&
5152 (c <= 0x115f /* Hangul Jamo */
5153 || c == 0x2329
5154 || c == 0x232a
5155 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
5156 /* CJK ... Yi */
5157 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
5158 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
5159 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
5160 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
5161 || (c >= 0xffe0 && c <= 0xffe6)
5162 || (c >= 0x20000 && c <= 0x2fffd)
5163 || (c >= 0x30000 && c <= 0x3fffd)))
5164 return 2;
5166 if (c == '\t')
5167 return opt_tab_size;
5169 return 1;
5170 }
5172 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
5173 * Illegal bytes are set one. */
5174 static const unsigned char utf8_bytes[256] = {
5175 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,
5176 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,
5177 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,
5178 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,
5179 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,
5180 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,
5181 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,
5182 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,
5183 };
5185 /* Decode UTF-8 multi-byte representation into a unicode character. */
5186 static inline unsigned long
5187 utf8_to_unicode(const char *string, size_t length)
5188 {
5189 unsigned long unicode;
5191 switch (length) {
5192 case 1:
5193 unicode = string[0];
5194 break;
5195 case 2:
5196 unicode = (string[0] & 0x1f) << 6;
5197 unicode += (string[1] & 0x3f);
5198 break;
5199 case 3:
5200 unicode = (string[0] & 0x0f) << 12;
5201 unicode += ((string[1] & 0x3f) << 6);
5202 unicode += (string[2] & 0x3f);
5203 break;
5204 case 4:
5205 unicode = (string[0] & 0x0f) << 18;
5206 unicode += ((string[1] & 0x3f) << 12);
5207 unicode += ((string[2] & 0x3f) << 6);
5208 unicode += (string[3] & 0x3f);
5209 break;
5210 case 5:
5211 unicode = (string[0] & 0x0f) << 24;
5212 unicode += ((string[1] & 0x3f) << 18);
5213 unicode += ((string[2] & 0x3f) << 12);
5214 unicode += ((string[3] & 0x3f) << 6);
5215 unicode += (string[4] & 0x3f);
5216 break;
5217 case 6:
5218 unicode = (string[0] & 0x01) << 30;
5219 unicode += ((string[1] & 0x3f) << 24);
5220 unicode += ((string[2] & 0x3f) << 18);
5221 unicode += ((string[3] & 0x3f) << 12);
5222 unicode += ((string[4] & 0x3f) << 6);
5223 unicode += (string[5] & 0x3f);
5224 break;
5225 default:
5226 die("Invalid unicode length");
5227 }
5229 /* Invalid characters could return the special 0xfffd value but NUL
5230 * should be just as good. */
5231 return unicode > 0xffff ? 0 : unicode;
5232 }
5234 /* Calculates how much of string can be shown within the given maximum width
5235 * and sets trimmed parameter to non-zero value if all of string could not be
5236 * shown. If the reserve flag is TRUE, it will reserve at least one
5237 * trailing character, which can be useful when drawing a delimiter.
5238 *
5239 * Returns the number of bytes to output from string to satisfy max_width. */
5240 static size_t
5241 utf8_length(const char *string, size_t max_width, int *trimmed, bool reserve)
5242 {
5243 const char *start = string;
5244 const char *end = strchr(string, '\0');
5245 unsigned char last_bytes = 0;
5246 size_t width = 0;
5248 *trimmed = 0;
5250 while (string < end) {
5251 int c = *(unsigned char *) string;
5252 unsigned char bytes = utf8_bytes[c];
5253 size_t ucwidth;
5254 unsigned long unicode;
5256 if (string + bytes > end)
5257 break;
5259 /* Change representation to figure out whether
5260 * it is a single- or double-width character. */
5262 unicode = utf8_to_unicode(string, bytes);
5263 /* FIXME: Graceful handling of invalid unicode character. */
5264 if (!unicode)
5265 break;
5267 ucwidth = unicode_width(unicode);
5268 width += ucwidth;
5269 if (width > max_width) {
5270 *trimmed = 1;
5271 if (reserve && width - ucwidth == max_width) {
5272 string -= last_bytes;
5273 }
5274 break;
5275 }
5277 string += bytes;
5278 last_bytes = bytes;
5279 }
5281 return string - start;
5282 }
5285 /*
5286 * Status management
5287 */
5289 /* Whether or not the curses interface has been initialized. */
5290 static bool cursed = FALSE;
5292 /* The status window is used for polling keystrokes. */
5293 static WINDOW *status_win;
5295 static bool status_empty = TRUE;
5297 /* Update status and title window. */
5298 static void
5299 report(const char *msg, ...)
5300 {
5301 struct view *view = display[current_view];
5303 if (input_mode)
5304 return;
5306 if (!view) {
5307 char buf[SIZEOF_STR];
5308 va_list args;
5310 va_start(args, msg);
5311 if (vsnprintf(buf, sizeof(buf), msg, args) >= sizeof(buf)) {
5312 buf[sizeof(buf) - 1] = 0;
5313 buf[sizeof(buf) - 2] = '.';
5314 buf[sizeof(buf) - 3] = '.';
5315 buf[sizeof(buf) - 4] = '.';
5316 }
5317 va_end(args);
5318 die("%s", buf);
5319 }
5321 if (!status_empty || *msg) {
5322 va_list args;
5324 va_start(args, msg);
5326 wmove(status_win, 0, 0);
5327 if (*msg) {
5328 vwprintw(status_win, msg, args);
5329 status_empty = FALSE;
5330 } else {
5331 status_empty = TRUE;
5332 }
5333 wclrtoeol(status_win);
5334 wrefresh(status_win);
5336 va_end(args);
5337 }
5339 update_view_title(view);
5340 update_display_cursor(view);
5341 }
5343 /* Controls when nodelay should be in effect when polling user input. */
5344 static void
5345 set_nonblocking_input(bool loading)
5346 {
5347 static unsigned int loading_views;
5349 if ((loading == FALSE && loading_views-- == 1) ||
5350 (loading == TRUE && loading_views++ == 0))
5351 nodelay(status_win, loading);
5352 }
5354 static void
5355 init_display(void)
5356 {
5357 int x, y;
5359 /* Initialize the curses library */
5360 if (isatty(STDIN_FILENO)) {
5361 cursed = !!initscr();
5362 } else {
5363 /* Leave stdin and stdout alone when acting as a pager. */
5364 FILE *io = fopen("/dev/tty", "r+");
5366 if (!io)
5367 die("Failed to open /dev/tty");
5368 cursed = !!newterm(NULL, io, io);
5369 }
5371 if (!cursed)
5372 die("Failed to initialize curses");
5374 nonl(); /* Tell curses not to do NL->CR/NL on output */
5375 cbreak(); /* Take input chars one at a time, no wait for \n */
5376 noecho(); /* Don't echo input */
5377 leaveok(stdscr, TRUE);
5379 if (has_colors())
5380 init_colors();
5382 getmaxyx(stdscr, y, x);
5383 status_win = newwin(1, 0, y - 1, 0);
5384 if (!status_win)
5385 die("Failed to create status window");
5387 /* Enable keyboard mapping */
5388 keypad(status_win, TRUE);
5389 wbkgdset(status_win, get_line_attr(LINE_STATUS));
5391 TABSIZE = opt_tab_size;
5392 }
5394 static char *
5395 read_prompt(const char *prompt)
5396 {
5397 enum { READING, STOP, CANCEL } status = READING;
5398 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
5399 int pos = 0;
5401 while (status == READING) {
5402 struct view *view;
5403 int i, key;
5405 input_mode = TRUE;
5407 foreach_view (view, i)
5408 update_view(view);
5410 input_mode = FALSE;
5412 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
5413 wclrtoeol(status_win);
5415 /* Refresh, accept single keystroke of input */
5416 key = wgetch(status_win);
5417 switch (key) {
5418 case KEY_RETURN:
5419 case KEY_ENTER:
5420 case '\n':
5421 status = pos ? STOP : CANCEL;
5422 break;
5424 case KEY_BACKSPACE:
5425 if (pos > 0)
5426 pos--;
5427 else
5428 status = CANCEL;
5429 break;
5431 case KEY_ESC:
5432 status = CANCEL;
5433 break;
5435 case ERR:
5436 break;
5438 default:
5439 if (pos >= sizeof(buf)) {
5440 report("Input string too long");
5441 return NULL;
5442 }
5444 if (isprint(key))
5445 buf[pos++] = (char) key;
5446 }
5447 }
5449 /* Clear the status window */
5450 status_empty = FALSE;
5451 report("");
5453 if (status == CANCEL)
5454 return NULL;
5456 buf[pos++] = 0;
5458 return buf;
5459 }
5461 /*
5462 * Repository references
5463 */
5465 static struct ref *refs = NULL;
5466 static size_t refs_alloc = 0;
5467 static size_t refs_size = 0;
5469 /* Id <-> ref store */
5470 static struct ref ***id_refs = NULL;
5471 static size_t id_refs_alloc = 0;
5472 static size_t id_refs_size = 0;
5474 static struct ref **
5475 get_refs(char *id)
5476 {
5477 struct ref ***tmp_id_refs;
5478 struct ref **ref_list = NULL;
5479 size_t ref_list_alloc = 0;
5480 size_t ref_list_size = 0;
5481 size_t i;
5483 for (i = 0; i < id_refs_size; i++)
5484 if (!strcmp(id, id_refs[i][0]->id))
5485 return id_refs[i];
5487 tmp_id_refs = realloc_items(id_refs, &id_refs_alloc, id_refs_size + 1,
5488 sizeof(*id_refs));
5489 if (!tmp_id_refs)
5490 return NULL;
5492 id_refs = tmp_id_refs;
5494 for (i = 0; i < refs_size; i++) {
5495 struct ref **tmp;
5497 if (strcmp(id, refs[i].id))
5498 continue;
5500 tmp = realloc_items(ref_list, &ref_list_alloc,
5501 ref_list_size + 1, sizeof(*ref_list));
5502 if (!tmp) {
5503 if (ref_list)
5504 free(ref_list);
5505 return NULL;
5506 }
5508 ref_list = tmp;
5509 if (ref_list_size > 0)
5510 ref_list[ref_list_size - 1]->next = 1;
5511 ref_list[ref_list_size] = &refs[i];
5513 /* XXX: The properties of the commit chains ensures that we can
5514 * safely modify the shared ref. The repo references will
5515 * always be similar for the same id. */
5516 ref_list[ref_list_size]->next = 0;
5517 ref_list_size++;
5518 }
5520 if (ref_list)
5521 id_refs[id_refs_size++] = ref_list;
5523 return ref_list;
5524 }
5526 static int
5527 read_ref(char *id, size_t idlen, char *name, size_t namelen)
5528 {
5529 struct ref *ref;
5530 bool tag = FALSE;
5531 bool ltag = FALSE;
5532 bool remote = FALSE;
5533 bool tracked = FALSE;
5534 bool check_replace = FALSE;
5535 bool head = FALSE;
5537 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
5538 if (!strcmp(name + namelen - 3, "^{}")) {
5539 namelen -= 3;
5540 name[namelen] = 0;
5541 if (refs_size > 0 && refs[refs_size - 1].ltag == TRUE)
5542 check_replace = TRUE;
5543 } else {
5544 ltag = TRUE;
5545 }
5547 tag = TRUE;
5548 namelen -= STRING_SIZE("refs/tags/");
5549 name += STRING_SIZE("refs/tags/");
5551 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
5552 remote = TRUE;
5553 namelen -= STRING_SIZE("refs/remotes/");
5554 name += STRING_SIZE("refs/remotes/");
5555 tracked = !strcmp(opt_remote, name);
5557 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5558 namelen -= STRING_SIZE("refs/heads/");
5559 name += STRING_SIZE("refs/heads/");
5560 head = !strncmp(opt_head, name, namelen);
5562 } else if (!strcmp(name, "HEAD")) {
5563 opt_no_head = FALSE;
5564 return OK;
5565 }
5567 if (check_replace && !strcmp(name, refs[refs_size - 1].name)) {
5568 /* it's an annotated tag, replace the previous sha1 with the
5569 * resolved commit id; relies on the fact git-ls-remote lists
5570 * the commit id of an annotated tag right beofre the commit id
5571 * it points to. */
5572 refs[refs_size - 1].ltag = ltag;
5573 string_copy_rev(refs[refs_size - 1].id, id);
5575 return OK;
5576 }
5577 refs = realloc_items(refs, &refs_alloc, refs_size + 1, sizeof(*refs));
5578 if (!refs)
5579 return ERR;
5581 ref = &refs[refs_size++];
5582 ref->name = malloc(namelen + 1);
5583 if (!ref->name)
5584 return ERR;
5586 strncpy(ref->name, name, namelen);
5587 ref->name[namelen] = 0;
5588 ref->head = head;
5589 ref->tag = tag;
5590 ref->ltag = ltag;
5591 ref->remote = remote;
5592 ref->tracked = tracked;
5593 string_copy_rev(ref->id, id);
5595 return OK;
5596 }
5598 static int
5599 load_refs(void)
5600 {
5601 const char *cmd_env = getenv("TIG_LS_REMOTE");
5602 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
5604 return read_properties(popen(cmd, "r"), "\t", read_ref);
5605 }
5607 static int
5608 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
5609 {
5610 if (!strcmp(name, "i18n.commitencoding"))
5611 string_ncopy(opt_encoding, value, valuelen);
5613 if (!strcmp(name, "core.editor"))
5614 string_ncopy(opt_editor, value, valuelen);
5616 /* branch.<head>.remote */
5617 if (*opt_head &&
5618 !strncmp(name, "branch.", 7) &&
5619 !strncmp(name + 7, opt_head, strlen(opt_head)) &&
5620 !strcmp(name + 7 + strlen(opt_head), ".remote"))
5621 string_ncopy(opt_remote, value, valuelen);
5623 if (*opt_head && *opt_remote &&
5624 !strncmp(name, "branch.", 7) &&
5625 !strncmp(name + 7, opt_head, strlen(opt_head)) &&
5626 !strcmp(name + 7 + strlen(opt_head), ".merge")) {
5627 size_t from = strlen(opt_remote);
5629 if (!strncmp(value, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5630 value += STRING_SIZE("refs/heads/");
5631 valuelen -= STRING_SIZE("refs/heads/");
5632 }
5634 if (!string_format_from(opt_remote, &from, "/%s", value))
5635 opt_remote[0] = 0;
5636 }
5638 return OK;
5639 }
5641 static int
5642 load_git_config(void)
5643 {
5644 return read_properties(popen(GIT_CONFIG " --list", "r"),
5645 "=", read_repo_config_option);
5646 }
5648 static int
5649 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
5650 {
5651 if (!opt_git_dir[0]) {
5652 string_ncopy(opt_git_dir, name, namelen);
5654 } else if (opt_is_inside_work_tree == -1) {
5655 /* This can be 3 different values depending on the
5656 * version of git being used. If git-rev-parse does not
5657 * understand --is-inside-work-tree it will simply echo
5658 * the option else either "true" or "false" is printed.
5659 * Default to true for the unknown case. */
5660 opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
5662 } else if (opt_cdup[0] == ' ') {
5663 string_ncopy(opt_cdup, name, namelen);
5664 } else {
5665 if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5666 namelen -= STRING_SIZE("refs/heads/");
5667 name += STRING_SIZE("refs/heads/");
5668 string_ncopy(opt_head, name, namelen);
5669 }
5670 }
5672 return OK;
5673 }
5675 static int
5676 load_repo_info(void)
5677 {
5678 int result;
5679 FILE *pipe = popen("(git rev-parse --git-dir --is-inside-work-tree "
5680 " --show-cdup; git symbolic-ref HEAD) 2>/dev/null", "r");
5682 /* XXX: The line outputted by "--show-cdup" can be empty so
5683 * initialize it to something invalid to make it possible to
5684 * detect whether it has been set or not. */
5685 opt_cdup[0] = ' ';
5687 result = read_properties(pipe, "=", read_repo_info);
5688 if (opt_cdup[0] == ' ')
5689 opt_cdup[0] = 0;
5691 return result;
5692 }
5694 static int
5695 read_properties(FILE *pipe, const char *separators,
5696 int (*read_property)(char *, size_t, char *, size_t))
5697 {
5698 char buffer[BUFSIZ];
5699 char *name;
5700 int state = OK;
5702 if (!pipe)
5703 return ERR;
5705 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
5706 char *value;
5707 size_t namelen;
5708 size_t valuelen;
5710 name = chomp_string(name);
5711 namelen = strcspn(name, separators);
5713 if (name[namelen]) {
5714 name[namelen] = 0;
5715 value = chomp_string(name + namelen + 1);
5716 valuelen = strlen(value);
5718 } else {
5719 value = "";
5720 valuelen = 0;
5721 }
5723 state = read_property(name, namelen, value, valuelen);
5724 }
5726 if (state != ERR && ferror(pipe))
5727 state = ERR;
5729 pclose(pipe);
5731 return state;
5732 }
5735 /*
5736 * Main
5737 */
5739 static void __NORETURN
5740 quit(int sig)
5741 {
5742 /* XXX: Restore tty modes and let the OS cleanup the rest! */
5743 if (cursed)
5744 endwin();
5745 exit(0);
5746 }
5748 static void __NORETURN
5749 die(const char *err, ...)
5750 {
5751 va_list args;
5753 endwin();
5755 va_start(args, err);
5756 fputs("tig: ", stderr);
5757 vfprintf(stderr, err, args);
5758 fputs("\n", stderr);
5759 va_end(args);
5761 exit(1);
5762 }
5764 static void
5765 warn(const char *msg, ...)
5766 {
5767 va_list args;
5769 va_start(args, msg);
5770 fputs("tig warning: ", stderr);
5771 vfprintf(stderr, msg, args);
5772 fputs("\n", stderr);
5773 va_end(args);
5774 }
5776 int
5777 main(int argc, char *argv[])
5778 {
5779 struct view *view;
5780 enum request request;
5781 size_t i;
5783 signal(SIGINT, quit);
5785 if (setlocale(LC_ALL, "")) {
5786 char *codeset = nl_langinfo(CODESET);
5788 string_ncopy(opt_codeset, codeset, strlen(codeset));
5789 }
5791 if (load_repo_info() == ERR)
5792 die("Failed to load repo info.");
5794 if (load_options() == ERR)
5795 die("Failed to load user config.");
5797 if (load_git_config() == ERR)
5798 die("Failed to load repo config.");
5800 if (!parse_options(argc, argv))
5801 return 0;
5803 /* Require a git repository unless when running in pager mode. */
5804 if (!opt_git_dir[0] && opt_request != REQ_VIEW_PAGER)
5805 die("Not a git repository");
5807 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
5808 opt_utf8 = FALSE;
5810 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
5811 opt_iconv = iconv_open(opt_codeset, opt_encoding);
5812 if (opt_iconv == ICONV_NONE)
5813 die("Failed to initialize character set conversion");
5814 }
5816 if (*opt_git_dir && load_refs() == ERR)
5817 die("Failed to load refs.");
5819 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
5820 view->cmd_env = getenv(view->cmd_env);
5822 request = opt_request;
5824 init_display();
5826 while (view_driver(display[current_view], request)) {
5827 int key;
5828 int i;
5830 foreach_view (view, i)
5831 update_view(view);
5833 /* Refresh, accept single keystroke of input */
5834 key = wgetch(status_win);
5836 /* wgetch() with nodelay() enabled returns ERR when there's no
5837 * input. */
5838 if (key == ERR) {
5839 request = REQ_NONE;
5840 continue;
5841 }
5843 request = get_keybinding(display[current_view]->keymap, key);
5845 /* Some low-level request handling. This keeps access to
5846 * status_win restricted. */
5847 switch (request) {
5848 case REQ_PROMPT:
5849 {
5850 char *cmd = read_prompt(":");
5852 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
5853 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
5854 opt_request = REQ_VIEW_DIFF;
5855 } else {
5856 opt_request = REQ_VIEW_PAGER;
5857 }
5858 break;
5859 }
5861 request = REQ_NONE;
5862 break;
5863 }
5864 case REQ_SEARCH:
5865 case REQ_SEARCH_BACK:
5866 {
5867 const char *prompt = request == REQ_SEARCH
5868 ? "/" : "?";
5869 char *search = read_prompt(prompt);
5871 if (search)
5872 string_ncopy(opt_search, search, strlen(search));
5873 else
5874 request = REQ_NONE;
5875 break;
5876 }
5877 case REQ_SCREEN_RESIZE:
5878 {
5879 int height, width;
5881 getmaxyx(stdscr, height, width);
5883 /* Resize the status view and let the view driver take
5884 * care of resizing the displayed views. */
5885 wresize(status_win, 1, width);
5886 mvwin(status_win, height - 1, 0);
5887 wrefresh(status_win);
5888 break;
5889 }
5890 default:
5891 break;
5892 }
5893 }
5895 quit(0);
5897 return 0;
5898 }