1 /* Copyright (c) 2006-2008 Jonas Fonseca <fonseca@diku.dk>
2 *
3 * This program is free software; you can redistribute it and/or
4 * modify it under the terms of the GNU General Public License as
5 * published by the Free Software Foundation; either version 2 of
6 * the License, or (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 */
14 #ifdef HAVE_CONFIG_H
15 #include "config.h"
16 #endif
18 #ifndef TIG_VERSION
19 #define TIG_VERSION "unknown-version"
20 #endif
22 #ifndef DEBUG
23 #define NDEBUG
24 #endif
26 #include <assert.h>
27 #include <errno.h>
28 #include <ctype.h>
29 #include <signal.h>
30 #include <stdarg.h>
31 #include <stdio.h>
32 #include <stdlib.h>
33 #include <string.h>
34 #include <sys/types.h>
35 #include <sys/stat.h>
36 #include <unistd.h>
37 #include <time.h>
39 #include <regex.h>
41 #include <locale.h>
42 #include <langinfo.h>
43 #include <iconv.h>
45 /* ncurses(3): Must be defined to have extended wide-character functions. */
46 #define _XOPEN_SOURCE_EXTENDED
48 #include <curses.h>
50 #if __GNUC__ >= 3
51 #define __NORETURN __attribute__((__noreturn__))
52 #else
53 #define __NORETURN
54 #endif
56 static void __NORETURN die(const char *err, ...);
57 static void warn(const char *msg, ...);
58 static void report(const char *msg, ...);
59 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, size_t, char *, size_t));
60 static void set_nonblocking_input(bool loading);
61 static size_t utf8_length(const char *string, size_t max_width, int *trimmed, bool reserve);
63 #define ABS(x) ((x) >= 0 ? (x) : -(x))
64 #define MIN(x, y) ((x) < (y) ? (x) : (y))
66 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
67 #define STRING_SIZE(x) (sizeof(x) - 1)
69 #define SIZEOF_STR 1024 /* Default string size. */
70 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
71 #define SIZEOF_REV 41 /* Holds a SHA-1 and an ending NUL */
73 /* Revision graph */
75 #define REVGRAPH_INIT 'I'
76 #define REVGRAPH_MERGE 'M'
77 #define REVGRAPH_BRANCH '+'
78 #define REVGRAPH_COMMIT '*'
79 #define REVGRAPH_BOUND '^'
80 #define REVGRAPH_LINE '|'
82 #define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
84 /* This color name can be used to refer to the default term colors. */
85 #define COLOR_DEFAULT (-1)
87 #define ICONV_NONE ((iconv_t) -1)
88 #ifndef ICONV_CONST
89 #define ICONV_CONST /* nothing */
90 #endif
92 /* The format and size of the date column in the main view. */
93 #define DATE_FORMAT "%Y-%m-%d %H:%M"
94 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
96 #define AUTHOR_COLS 20
97 #define ID_COLS 8
99 /* The default interval between line numbers. */
100 #define NUMBER_INTERVAL 5
102 #define TABSIZE 8
104 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
106 #ifndef GIT_CONFIG
107 #define GIT_CONFIG "git config"
108 #endif
110 #define TIG_LS_REMOTE \
111 "git ls-remote $(git rev-parse --git-dir) 2>/dev/null"
113 #define TIG_DIFF_CMD \
114 "git show --pretty=fuller --no-color --root --patch-with-stat --find-copies-harder -C %s 2>/dev/null"
116 #define TIG_LOG_CMD \
117 "git log --no-color --cc --stat -n100 %s 2>/dev/null"
119 #define TIG_MAIN_CMD \
120 "git log --no-color --topo-order --parents --boundary --pretty=raw %s 2>/dev/null"
122 #define TIG_TREE_CMD \
123 "git ls-tree %s %s"
125 #define TIG_BLOB_CMD \
126 "git cat-file blob %s"
128 /* XXX: Needs to be defined to the empty string. */
129 #define TIG_HELP_CMD ""
130 #define TIG_PAGER_CMD ""
131 #define TIG_STATUS_CMD ""
132 #define TIG_STAGE_CMD ""
133 #define TIG_BLAME_CMD ""
135 /* Some ascii-shorthands fitted into the ncurses namespace. */
136 #define KEY_TAB '\t'
137 #define KEY_RETURN '\r'
138 #define KEY_ESC 27
141 struct ref {
142 char *name; /* Ref name; tag or head names are shortened. */
143 char id[SIZEOF_REV]; /* Commit SHA1 ID */
144 unsigned int tag:1; /* Is it a tag? */
145 unsigned int ltag:1; /* If so, is the tag local? */
146 unsigned int remote:1; /* Is it a remote ref? */
147 unsigned int next:1; /* For ref lists: are there more refs? */
148 };
150 static struct ref **get_refs(char *id);
152 struct int_map {
153 const char *name;
154 int namelen;
155 int value;
156 };
158 static int
159 set_from_int_map(struct int_map *map, size_t map_size,
160 int *value, const char *name, int namelen)
161 {
163 int i;
165 for (i = 0; i < map_size; i++)
166 if (namelen == map[i].namelen &&
167 !strncasecmp(name, map[i].name, namelen)) {
168 *value = map[i].value;
169 return OK;
170 }
172 return ERR;
173 }
176 /*
177 * String helpers
178 */
180 static inline void
181 string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
182 {
183 if (srclen > dstlen - 1)
184 srclen = dstlen - 1;
186 strncpy(dst, src, srclen);
187 dst[srclen] = 0;
188 }
190 /* Shorthands for safely copying into a fixed buffer. */
192 #define string_copy(dst, src) \
193 string_ncopy_do(dst, sizeof(dst), src, sizeof(src))
195 #define string_ncopy(dst, src, srclen) \
196 string_ncopy_do(dst, sizeof(dst), src, srclen)
198 #define string_copy_rev(dst, src) \
199 string_ncopy_do(dst, SIZEOF_REV, src, SIZEOF_REV - 1)
201 #define string_add(dst, from, src) \
202 string_ncopy_do(dst + (from), sizeof(dst) - (from), src, sizeof(src))
204 static char *
205 chomp_string(char *name)
206 {
207 int namelen;
209 while (isspace(*name))
210 name++;
212 namelen = strlen(name) - 1;
213 while (namelen > 0 && isspace(name[namelen]))
214 name[namelen--] = 0;
216 return name;
217 }
219 static bool
220 string_nformat(char *buf, size_t bufsize, size_t *bufpos, const char *fmt, ...)
221 {
222 va_list args;
223 size_t pos = bufpos ? *bufpos : 0;
225 va_start(args, fmt);
226 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
227 va_end(args);
229 if (bufpos)
230 *bufpos = pos;
232 return pos >= bufsize ? FALSE : TRUE;
233 }
235 #define string_format(buf, fmt, args...) \
236 string_nformat(buf, sizeof(buf), NULL, fmt, args)
238 #define string_format_from(buf, from, fmt, args...) \
239 string_nformat(buf, sizeof(buf), from, fmt, args)
241 static int
242 string_enum_compare(const char *str1, const char *str2, int len)
243 {
244 size_t i;
246 #define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
248 /* Diff-Header == DIFF_HEADER */
249 for (i = 0; i < len; i++) {
250 if (toupper(str1[i]) == toupper(str2[i]))
251 continue;
253 if (string_enum_sep(str1[i]) &&
254 string_enum_sep(str2[i]))
255 continue;
257 return str1[i] - str2[i];
258 }
260 return 0;
261 }
263 /* Shell quoting
264 *
265 * NOTE: The following is a slightly modified copy of the git project's shell
266 * quoting routines found in the quote.c file.
267 *
268 * Help to copy the thing properly quoted for the shell safety. any single
269 * quote is replaced with '\'', any exclamation point is replaced with '\!',
270 * and the whole thing is enclosed in a
271 *
272 * E.g.
273 * original sq_quote result
274 * name ==> name ==> 'name'
275 * a b ==> a b ==> 'a b'
276 * a'b ==> a'\''b ==> 'a'\''b'
277 * a!b ==> a'\!'b ==> 'a'\!'b'
278 */
280 static size_t
281 sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
282 {
283 char c;
285 #define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0)
287 BUFPUT('\'');
288 while ((c = *src++)) {
289 if (c == '\'' || c == '!') {
290 BUFPUT('\'');
291 BUFPUT('\\');
292 BUFPUT(c);
293 BUFPUT('\'');
294 } else {
295 BUFPUT(c);
296 }
297 }
298 BUFPUT('\'');
300 if (bufsize < SIZEOF_STR)
301 buf[bufsize] = 0;
303 return bufsize;
304 }
307 /*
308 * User requests
309 */
311 #define REQ_INFO \
312 /* XXX: Keep the view request first and in sync with views[]. */ \
313 REQ_GROUP("View switching") \
314 REQ_(VIEW_MAIN, "Show main view"), \
315 REQ_(VIEW_DIFF, "Show diff view"), \
316 REQ_(VIEW_LOG, "Show log view"), \
317 REQ_(VIEW_TREE, "Show tree view"), \
318 REQ_(VIEW_BLOB, "Show blob view"), \
319 REQ_(VIEW_BLAME, "Show blame view"), \
320 REQ_(VIEW_HELP, "Show help page"), \
321 REQ_(VIEW_PAGER, "Show pager view"), \
322 REQ_(VIEW_STATUS, "Show status view"), \
323 REQ_(VIEW_STAGE, "Show stage view"), \
324 \
325 REQ_GROUP("View manipulation") \
326 REQ_(ENTER, "Enter current line and scroll"), \
327 REQ_(NEXT, "Move to next"), \
328 REQ_(PREVIOUS, "Move to previous"), \
329 REQ_(VIEW_NEXT, "Move focus to next view"), \
330 REQ_(REFRESH, "Reload and refresh"), \
331 REQ_(VIEW_CLOSE, "Close the current view"), \
332 REQ_(QUIT, "Close all views and quit"), \
333 \
334 REQ_GROUP("Cursor navigation") \
335 REQ_(MOVE_UP, "Move cursor one line up"), \
336 REQ_(MOVE_DOWN, "Move cursor one line down"), \
337 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
338 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
339 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
340 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
341 \
342 REQ_GROUP("Scrolling") \
343 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
344 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
345 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
346 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
347 \
348 REQ_GROUP("Searching") \
349 REQ_(SEARCH, "Search the view"), \
350 REQ_(SEARCH_BACK, "Search backwards in the view"), \
351 REQ_(FIND_NEXT, "Find next search match"), \
352 REQ_(FIND_PREV, "Find previous search match"), \
353 \
354 REQ_GROUP("Misc") \
355 REQ_(PROMPT, "Bring up the prompt"), \
356 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
357 REQ_(SCREEN_RESIZE, "Resize the screen"), \
358 REQ_(SHOW_VERSION, "Show version information"), \
359 REQ_(STOP_LOADING, "Stop all loading views"), \
360 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
361 REQ_(TOGGLE_DATE, "Toggle date display"), \
362 REQ_(TOGGLE_AUTHOR, "Toggle author display"), \
363 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"), \
364 REQ_(TOGGLE_REFS, "Toggle reference display (tags/branches)"), \
365 REQ_(STATUS_UPDATE, "Update file status"), \
366 REQ_(STATUS_MERGE, "Merge file using external tool"), \
367 REQ_(TREE_PARENT, "Switch to parent directory in tree view"), \
368 REQ_(EDIT, "Open in editor"), \
369 REQ_(NONE, "Do nothing")
372 /* User action requests. */
373 enum request {
374 #define REQ_GROUP(help)
375 #define REQ_(req, help) REQ_##req
377 /* Offset all requests to avoid conflicts with ncurses getch values. */
378 REQ_OFFSET = KEY_MAX + 1,
379 REQ_INFO
381 #undef REQ_GROUP
382 #undef REQ_
383 };
385 struct request_info {
386 enum request request;
387 char *name;
388 int namelen;
389 char *help;
390 };
392 static struct request_info req_info[] = {
393 #define REQ_GROUP(help) { 0, NULL, 0, (help) },
394 #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
395 REQ_INFO
396 #undef REQ_GROUP
397 #undef REQ_
398 };
400 static enum request
401 get_request(const char *name)
402 {
403 int namelen = strlen(name);
404 int i;
406 for (i = 0; i < ARRAY_SIZE(req_info); i++)
407 if (req_info[i].namelen == namelen &&
408 !string_enum_compare(req_info[i].name, name, namelen))
409 return req_info[i].request;
411 return REQ_NONE;
412 }
415 /*
416 * Options
417 */
419 static const char usage[] =
420 "tig " TIG_VERSION " (" __DATE__ ")\n"
421 "\n"
422 "Usage: tig [options] [revs] [--] [paths]\n"
423 " or: tig show [options] [revs] [--] [paths]\n"
424 " or: tig blame [rev] path\n"
425 " or: tig status\n"
426 " or: tig < [git command output]\n"
427 "\n"
428 "Options:\n"
429 " -v, --version Show version and exit\n"
430 " -h, --help Show help message and exit";
432 /* Option and state variables. */
433 static bool opt_date = TRUE;
434 static bool opt_author = TRUE;
435 static bool opt_line_number = FALSE;
436 static bool opt_rev_graph = FALSE;
437 static bool opt_show_refs = TRUE;
438 static int opt_num_interval = NUMBER_INTERVAL;
439 static int opt_tab_size = TABSIZE;
440 static enum request opt_request = REQ_VIEW_MAIN;
441 static char opt_cmd[SIZEOF_STR] = "";
442 static char opt_path[SIZEOF_STR] = "";
443 static char opt_file[SIZEOF_STR] = "";
444 static char opt_ref[SIZEOF_REF] = "";
445 static FILE *opt_pipe = NULL;
446 static char opt_encoding[20] = "UTF-8";
447 static bool opt_utf8 = TRUE;
448 static char opt_codeset[20] = "UTF-8";
449 static iconv_t opt_iconv = ICONV_NONE;
450 static char opt_search[SIZEOF_STR] = "";
451 static char opt_cdup[SIZEOF_STR] = "";
452 static char opt_git_dir[SIZEOF_STR] = "";
453 static signed char opt_is_inside_work_tree = -1; /* set to TRUE or FALSE */
454 static char opt_editor[SIZEOF_STR] = "";
456 static bool
457 parse_options(int argc, char *argv[])
458 {
459 size_t buf_size;
460 char *subcommand;
461 bool seen_dashdash = FALSE;
462 int i;
464 if (argc <= 1)
465 return TRUE;
467 subcommand = argv[1];
468 if (!strcmp(subcommand, "status") || !strcmp(subcommand, "-S")) {
469 opt_request = REQ_VIEW_STATUS;
470 if (!strcmp(subcommand, "-S"))
471 warn("`-S' has been deprecated; use `tig status' instead");
472 if (argc > 2)
473 warn("ignoring arguments after `%s'", subcommand);
474 return TRUE;
476 } else if (!strcmp(subcommand, "blame")) {
477 opt_request = REQ_VIEW_BLAME;
478 if (argc <= 2 || argc > 4)
479 die("invalid number of options to blame\n\n%s", usage);
481 i = 2;
482 if (argc == 4) {
483 string_ncopy(opt_ref, argv[i], strlen(argv[i]));
484 i++;
485 }
487 string_ncopy(opt_file, argv[i], strlen(argv[i]));
488 return TRUE;
490 } else if (!strcmp(subcommand, "show")) {
491 opt_request = REQ_VIEW_DIFF;
493 } else if (!strcmp(subcommand, "log") || !strcmp(subcommand, "diff")) {
494 opt_request = subcommand[0] == 'l'
495 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
496 warn("`tig %s' has been deprecated", subcommand);
498 } else {
499 subcommand = NULL;
500 }
502 if (!subcommand)
503 /* XXX: This is vulnerable to the user overriding
504 * options required for the main view parser. */
505 string_copy(opt_cmd, "git log --no-color --pretty=raw --boundary --parents");
506 else
507 string_format(opt_cmd, "git %s", subcommand);
509 buf_size = strlen(opt_cmd);
511 for (i = 1 + !!subcommand; i < argc; i++) {
512 char *opt = argv[i];
514 if (seen_dashdash || !strcmp(opt, "--")) {
515 seen_dashdash = TRUE;
517 } else if (!strcmp(opt, "-v") || !strcmp(opt, "--version")) {
518 printf("tig version %s\n", TIG_VERSION);
519 return FALSE;
521 } else if (!strcmp(opt, "-h") || !strcmp(opt, "--help")) {
522 printf(usage);
523 return FALSE;
524 }
526 opt_cmd[buf_size++] = ' ';
527 buf_size = sq_quote(opt_cmd, buf_size, opt);
528 if (buf_size >= sizeof(opt_cmd))
529 die("command too long");
530 }
532 if (!isatty(STDIN_FILENO)) {
533 opt_request = REQ_VIEW_PAGER;
534 opt_pipe = stdin;
535 buf_size = 0;
536 }
538 opt_cmd[buf_size] = 0;
540 return TRUE;
541 }
544 /*
545 * Line-oriented content detection.
546 */
548 #define LINE_INFO \
549 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
550 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
551 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
552 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
553 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
554 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
555 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
556 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
557 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
558 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
559 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
560 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
561 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
562 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
563 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
564 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
565 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
566 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
567 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
568 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
569 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
570 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
571 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
572 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
573 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
574 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
575 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
576 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
577 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
578 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
579 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
580 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
581 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
582 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
583 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
584 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
585 LINE(MAIN_DELIM, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
586 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
587 LINE(MAIN_LOCAL_TAG,"", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
588 LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
589 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
590 LINE(MAIN_REVGRAPH,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
591 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
592 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
593 LINE(STAT_SECTION, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
594 LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
595 LINE(STAT_STAGED, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
596 LINE(STAT_UNSTAGED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
597 LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
598 LINE(BLAME_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
599 LINE(BLAME_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
600 LINE(BLAME_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
601 LINE(BLAME_ID, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
602 LINE(BLAME_LINENO, "", COLOR_CYAN, COLOR_DEFAULT, 0)
604 enum line_type {
605 #define LINE(type, line, fg, bg, attr) \
606 LINE_##type
607 LINE_INFO
608 #undef LINE
609 };
611 struct line_info {
612 const char *name; /* Option name. */
613 int namelen; /* Size of option name. */
614 const char *line; /* The start of line to match. */
615 int linelen; /* Size of string to match. */
616 int fg, bg, attr; /* Color and text attributes for the lines. */
617 };
619 static struct line_info line_info[] = {
620 #define LINE(type, line, fg, bg, attr) \
621 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
622 LINE_INFO
623 #undef LINE
624 };
626 static enum line_type
627 get_line_type(char *line)
628 {
629 int linelen = strlen(line);
630 enum line_type type;
632 for (type = 0; type < ARRAY_SIZE(line_info); type++)
633 /* Case insensitive search matches Signed-off-by lines better. */
634 if (linelen >= line_info[type].linelen &&
635 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
636 return type;
638 return LINE_DEFAULT;
639 }
641 static inline int
642 get_line_attr(enum line_type type)
643 {
644 assert(type < ARRAY_SIZE(line_info));
645 return COLOR_PAIR(type) | line_info[type].attr;
646 }
648 static struct line_info *
649 get_line_info(char *name, int namelen)
650 {
651 enum line_type type;
653 for (type = 0; type < ARRAY_SIZE(line_info); type++)
654 if (namelen == line_info[type].namelen &&
655 !string_enum_compare(line_info[type].name, name, namelen))
656 return &line_info[type];
658 return NULL;
659 }
661 static void
662 init_colors(void)
663 {
664 int default_bg = line_info[LINE_DEFAULT].bg;
665 int default_fg = line_info[LINE_DEFAULT].fg;
666 enum line_type type;
668 start_color();
670 if (assume_default_colors(default_fg, default_bg) == ERR) {
671 default_bg = COLOR_BLACK;
672 default_fg = COLOR_WHITE;
673 }
675 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
676 struct line_info *info = &line_info[type];
677 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
678 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
680 init_pair(type, fg, bg);
681 }
682 }
684 struct line {
685 enum line_type type;
687 /* State flags */
688 unsigned int selected:1;
689 unsigned int dirty:1;
691 void *data; /* User data */
692 };
695 /*
696 * Keys
697 */
699 struct keybinding {
700 int alias;
701 enum request request;
702 struct keybinding *next;
703 };
705 static struct keybinding default_keybindings[] = {
706 /* View switching */
707 { 'm', REQ_VIEW_MAIN },
708 { 'd', REQ_VIEW_DIFF },
709 { 'l', REQ_VIEW_LOG },
710 { 't', REQ_VIEW_TREE },
711 { 'f', REQ_VIEW_BLOB },
712 { 'B', REQ_VIEW_BLAME },
713 { 'p', REQ_VIEW_PAGER },
714 { 'h', REQ_VIEW_HELP },
715 { 'S', REQ_VIEW_STATUS },
716 { 'c', REQ_VIEW_STAGE },
718 /* View manipulation */
719 { 'q', REQ_VIEW_CLOSE },
720 { KEY_TAB, REQ_VIEW_NEXT },
721 { KEY_RETURN, REQ_ENTER },
722 { KEY_UP, REQ_PREVIOUS },
723 { KEY_DOWN, REQ_NEXT },
724 { 'R', REQ_REFRESH },
726 /* Cursor navigation */
727 { 'k', REQ_MOVE_UP },
728 { 'j', REQ_MOVE_DOWN },
729 { KEY_HOME, REQ_MOVE_FIRST_LINE },
730 { KEY_END, REQ_MOVE_LAST_LINE },
731 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
732 { ' ', REQ_MOVE_PAGE_DOWN },
733 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
734 { 'b', REQ_MOVE_PAGE_UP },
735 { '-', REQ_MOVE_PAGE_UP },
737 /* Scrolling */
738 { KEY_IC, REQ_SCROLL_LINE_UP },
739 { KEY_DC, REQ_SCROLL_LINE_DOWN },
740 { 'w', REQ_SCROLL_PAGE_UP },
741 { 's', REQ_SCROLL_PAGE_DOWN },
743 /* Searching */
744 { '/', REQ_SEARCH },
745 { '?', REQ_SEARCH_BACK },
746 { 'n', REQ_FIND_NEXT },
747 { 'N', REQ_FIND_PREV },
749 /* Misc */
750 { 'Q', REQ_QUIT },
751 { 'z', REQ_STOP_LOADING },
752 { 'v', REQ_SHOW_VERSION },
753 { 'r', REQ_SCREEN_REDRAW },
754 { '.', REQ_TOGGLE_LINENO },
755 { 'D', REQ_TOGGLE_DATE },
756 { 'A', REQ_TOGGLE_AUTHOR },
757 { 'g', REQ_TOGGLE_REV_GRAPH },
758 { 'F', REQ_TOGGLE_REFS },
759 { ':', REQ_PROMPT },
760 { 'u', REQ_STATUS_UPDATE },
761 { 'M', REQ_STATUS_MERGE },
762 { ',', REQ_TREE_PARENT },
763 { 'e', REQ_EDIT },
765 /* Using the ncurses SIGWINCH handler. */
766 { KEY_RESIZE, REQ_SCREEN_RESIZE },
767 };
769 #define KEYMAP_INFO \
770 KEYMAP_(GENERIC), \
771 KEYMAP_(MAIN), \
772 KEYMAP_(DIFF), \
773 KEYMAP_(LOG), \
774 KEYMAP_(TREE), \
775 KEYMAP_(BLOB), \
776 KEYMAP_(BLAME), \
777 KEYMAP_(PAGER), \
778 KEYMAP_(HELP), \
779 KEYMAP_(STATUS), \
780 KEYMAP_(STAGE)
782 enum keymap {
783 #define KEYMAP_(name) KEYMAP_##name
784 KEYMAP_INFO
785 #undef KEYMAP_
786 };
788 static struct int_map keymap_table[] = {
789 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
790 KEYMAP_INFO
791 #undef KEYMAP_
792 };
794 #define set_keymap(map, name) \
795 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
797 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
799 static void
800 add_keybinding(enum keymap keymap, enum request request, int key)
801 {
802 struct keybinding *keybinding;
804 keybinding = calloc(1, sizeof(*keybinding));
805 if (!keybinding)
806 die("Failed to allocate keybinding");
808 keybinding->alias = key;
809 keybinding->request = request;
810 keybinding->next = keybindings[keymap];
811 keybindings[keymap] = keybinding;
812 }
814 /* Looks for a key binding first in the given map, then in the generic map, and
815 * lastly in the default keybindings. */
816 static enum request
817 get_keybinding(enum keymap keymap, int key)
818 {
819 struct keybinding *kbd;
820 int i;
822 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
823 if (kbd->alias == key)
824 return kbd->request;
826 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
827 if (kbd->alias == key)
828 return kbd->request;
830 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
831 if (default_keybindings[i].alias == key)
832 return default_keybindings[i].request;
834 return (enum request) key;
835 }
838 struct key {
839 char *name;
840 int value;
841 };
843 static struct key key_table[] = {
844 { "Enter", KEY_RETURN },
845 { "Space", ' ' },
846 { "Backspace", KEY_BACKSPACE },
847 { "Tab", KEY_TAB },
848 { "Escape", KEY_ESC },
849 { "Left", KEY_LEFT },
850 { "Right", KEY_RIGHT },
851 { "Up", KEY_UP },
852 { "Down", KEY_DOWN },
853 { "Insert", KEY_IC },
854 { "Delete", KEY_DC },
855 { "Hash", '#' },
856 { "Home", KEY_HOME },
857 { "End", KEY_END },
858 { "PageUp", KEY_PPAGE },
859 { "PageDown", KEY_NPAGE },
860 { "F1", KEY_F(1) },
861 { "F2", KEY_F(2) },
862 { "F3", KEY_F(3) },
863 { "F4", KEY_F(4) },
864 { "F5", KEY_F(5) },
865 { "F6", KEY_F(6) },
866 { "F7", KEY_F(7) },
867 { "F8", KEY_F(8) },
868 { "F9", KEY_F(9) },
869 { "F10", KEY_F(10) },
870 { "F11", KEY_F(11) },
871 { "F12", KEY_F(12) },
872 };
874 static int
875 get_key_value(const char *name)
876 {
877 int i;
879 for (i = 0; i < ARRAY_SIZE(key_table); i++)
880 if (!strcasecmp(key_table[i].name, name))
881 return key_table[i].value;
883 if (strlen(name) == 1 && isprint(*name))
884 return (int) *name;
886 return ERR;
887 }
889 static char *
890 get_key_name(int key_value)
891 {
892 static char key_char[] = "'X'";
893 char *seq = NULL;
894 int key;
896 for (key = 0; key < ARRAY_SIZE(key_table); key++)
897 if (key_table[key].value == key_value)
898 seq = key_table[key].name;
900 if (seq == NULL &&
901 key_value < 127 &&
902 isprint(key_value)) {
903 key_char[1] = (char) key_value;
904 seq = key_char;
905 }
907 return seq ? seq : "'?'";
908 }
910 static char *
911 get_key(enum request request)
912 {
913 static char buf[BUFSIZ];
914 size_t pos = 0;
915 char *sep = "";
916 int i;
918 buf[pos] = 0;
920 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
921 struct keybinding *keybinding = &default_keybindings[i];
923 if (keybinding->request != request)
924 continue;
926 if (!string_format_from(buf, &pos, "%s%s", sep,
927 get_key_name(keybinding->alias)))
928 return "Too many keybindings!";
929 sep = ", ";
930 }
932 return buf;
933 }
935 struct run_request {
936 enum keymap keymap;
937 int key;
938 char cmd[SIZEOF_STR];
939 };
941 static struct run_request *run_request;
942 static size_t run_requests;
944 static enum request
945 add_run_request(enum keymap keymap, int key, int argc, char **argv)
946 {
947 struct run_request *tmp;
948 struct run_request req = { keymap, key };
949 size_t bufpos;
951 for (bufpos = 0; argc > 0; argc--, argv++)
952 if (!string_format_from(req.cmd, &bufpos, "%s ", *argv))
953 return REQ_NONE;
955 req.cmd[bufpos - 1] = 0;
957 tmp = realloc(run_request, (run_requests + 1) * sizeof(*run_request));
958 if (!tmp)
959 return REQ_NONE;
961 run_request = tmp;
962 run_request[run_requests++] = req;
964 return REQ_NONE + run_requests;
965 }
967 static struct run_request *
968 get_run_request(enum request request)
969 {
970 if (request <= REQ_NONE)
971 return NULL;
972 return &run_request[request - REQ_NONE - 1];
973 }
975 static void
976 add_builtin_run_requests(void)
977 {
978 struct {
979 enum keymap keymap;
980 int key;
981 char *argv[1];
982 } reqs[] = {
983 { KEYMAP_MAIN, 'C', { "git cherry-pick %(commit)" } },
984 { KEYMAP_GENERIC, 'G', { "git gc" } },
985 };
986 int i;
988 for (i = 0; i < ARRAY_SIZE(reqs); i++) {
989 enum request req;
991 req = add_run_request(reqs[i].keymap, reqs[i].key, 1, reqs[i].argv);
992 if (req != REQ_NONE)
993 add_keybinding(reqs[i].keymap, req, reqs[i].key);
994 }
995 }
997 /*
998 * User config file handling.
999 */
1001 static struct int_map color_map[] = {
1002 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
1003 COLOR_MAP(DEFAULT),
1004 COLOR_MAP(BLACK),
1005 COLOR_MAP(BLUE),
1006 COLOR_MAP(CYAN),
1007 COLOR_MAP(GREEN),
1008 COLOR_MAP(MAGENTA),
1009 COLOR_MAP(RED),
1010 COLOR_MAP(WHITE),
1011 COLOR_MAP(YELLOW),
1012 };
1014 #define set_color(color, name) \
1015 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
1017 static struct int_map attr_map[] = {
1018 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
1019 ATTR_MAP(NORMAL),
1020 ATTR_MAP(BLINK),
1021 ATTR_MAP(BOLD),
1022 ATTR_MAP(DIM),
1023 ATTR_MAP(REVERSE),
1024 ATTR_MAP(STANDOUT),
1025 ATTR_MAP(UNDERLINE),
1026 };
1028 #define set_attribute(attr, name) \
1029 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
1031 static int config_lineno;
1032 static bool config_errors;
1033 static char *config_msg;
1035 /* Wants: object fgcolor bgcolor [attr] */
1036 static int
1037 option_color_command(int argc, char *argv[])
1038 {
1039 struct line_info *info;
1041 if (argc != 3 && argc != 4) {
1042 config_msg = "Wrong number of arguments given to color command";
1043 return ERR;
1044 }
1046 info = get_line_info(argv[0], strlen(argv[0]));
1047 if (!info) {
1048 config_msg = "Unknown color name";
1049 return ERR;
1050 }
1052 if (set_color(&info->fg, argv[1]) == ERR ||
1053 set_color(&info->bg, argv[2]) == ERR) {
1054 config_msg = "Unknown color";
1055 return ERR;
1056 }
1058 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
1059 config_msg = "Unknown attribute";
1060 return ERR;
1061 }
1063 return OK;
1064 }
1066 static bool parse_bool(const char *s)
1067 {
1068 return (!strcmp(s, "1") || !strcmp(s, "true") ||
1069 !strcmp(s, "yes")) ? TRUE : FALSE;
1070 }
1072 /* Wants: name = value */
1073 static int
1074 option_set_command(int argc, char *argv[])
1075 {
1076 if (argc != 3) {
1077 config_msg = "Wrong number of arguments given to set command";
1078 return ERR;
1079 }
1081 if (strcmp(argv[1], "=")) {
1082 config_msg = "No value assigned";
1083 return ERR;
1084 }
1086 if (!strcmp(argv[0], "show-author")) {
1087 opt_author = parse_bool(argv[2]);
1088 return OK;
1089 }
1091 if (!strcmp(argv[0], "show-date")) {
1092 opt_date = parse_bool(argv[2]);
1093 return OK;
1094 }
1096 if (!strcmp(argv[0], "show-rev-graph")) {
1097 opt_rev_graph = parse_bool(argv[2]);
1098 return OK;
1099 }
1101 if (!strcmp(argv[0], "show-refs")) {
1102 opt_show_refs = parse_bool(argv[2]);
1103 return OK;
1104 }
1106 if (!strcmp(argv[0], "show-line-numbers")) {
1107 opt_line_number = parse_bool(argv[2]);
1108 return OK;
1109 }
1111 if (!strcmp(argv[0], "line-number-interval")) {
1112 opt_num_interval = atoi(argv[2]);
1113 return OK;
1114 }
1116 if (!strcmp(argv[0], "tab-size")) {
1117 opt_tab_size = atoi(argv[2]);
1118 return OK;
1119 }
1121 if (!strcmp(argv[0], "commit-encoding")) {
1122 char *arg = argv[2];
1123 int delimiter = *arg;
1124 int i;
1126 switch (delimiter) {
1127 case '"':
1128 case '\'':
1129 for (arg++, i = 0; arg[i]; i++)
1130 if (arg[i] == delimiter) {
1131 arg[i] = 0;
1132 break;
1133 }
1134 default:
1135 string_ncopy(opt_encoding, arg, strlen(arg));
1136 return OK;
1137 }
1138 }
1140 config_msg = "Unknown variable name";
1141 return ERR;
1142 }
1144 /* Wants: mode request key */
1145 static int
1146 option_bind_command(int argc, char *argv[])
1147 {
1148 enum request request;
1149 int keymap;
1150 int key;
1152 if (argc < 3) {
1153 config_msg = "Wrong number of arguments given to bind command";
1154 return ERR;
1155 }
1157 if (set_keymap(&keymap, argv[0]) == ERR) {
1158 config_msg = "Unknown key map";
1159 return ERR;
1160 }
1162 key = get_key_value(argv[1]);
1163 if (key == ERR) {
1164 config_msg = "Unknown key";
1165 return ERR;
1166 }
1168 request = get_request(argv[2]);
1169 if (request == REQ_NONE) {
1170 const char *obsolete[] = { "cherry-pick" };
1171 size_t namelen = strlen(argv[2]);
1172 int i;
1174 for (i = 0; i < ARRAY_SIZE(obsolete); i++) {
1175 if (namelen == strlen(obsolete[i]) &&
1176 !string_enum_compare(obsolete[i], argv[2], namelen)) {
1177 config_msg = "Obsolete request name";
1178 return ERR;
1179 }
1180 }
1181 }
1182 if (request == REQ_NONE && *argv[2]++ == '!')
1183 request = add_run_request(keymap, key, argc - 2, argv + 2);
1184 if (request == REQ_NONE) {
1185 config_msg = "Unknown request name";
1186 return ERR;
1187 }
1189 add_keybinding(keymap, request, key);
1191 return OK;
1192 }
1194 static int
1195 set_option(char *opt, char *value)
1196 {
1197 char *argv[16];
1198 int valuelen;
1199 int argc = 0;
1201 /* Tokenize */
1202 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1203 argv[argc++] = value;
1204 value += valuelen;
1206 /* Nothing more to tokenize or last available token. */
1207 if (!*value || argc >= ARRAY_SIZE(argv))
1208 break;
1210 *value++ = 0;
1211 while (isspace(*value))
1212 value++;
1213 }
1215 if (!strcmp(opt, "color"))
1216 return option_color_command(argc, argv);
1218 if (!strcmp(opt, "set"))
1219 return option_set_command(argc, argv);
1221 if (!strcmp(opt, "bind"))
1222 return option_bind_command(argc, argv);
1224 config_msg = "Unknown option command";
1225 return ERR;
1226 }
1228 static int
1229 read_option(char *opt, size_t optlen, char *value, size_t valuelen)
1230 {
1231 int status = OK;
1233 config_lineno++;
1234 config_msg = "Internal error";
1236 /* Check for comment markers, since read_properties() will
1237 * only ensure opt and value are split at first " \t". */
1238 optlen = strcspn(opt, "#");
1239 if (optlen == 0)
1240 return OK;
1242 if (opt[optlen] != 0) {
1243 config_msg = "No option value";
1244 status = ERR;
1246 } else {
1247 /* Look for comment endings in the value. */
1248 size_t len = strcspn(value, "#");
1250 if (len < valuelen) {
1251 valuelen = len;
1252 value[valuelen] = 0;
1253 }
1255 status = set_option(opt, value);
1256 }
1258 if (status == ERR) {
1259 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1260 config_lineno, (int) optlen, opt, config_msg);
1261 config_errors = TRUE;
1262 }
1264 /* Always keep going if errors are encountered. */
1265 return OK;
1266 }
1268 static void
1269 load_option_file(const char *path)
1270 {
1271 FILE *file;
1273 /* It's ok that the file doesn't exist. */
1274 file = fopen(path, "r");
1275 if (!file)
1276 return;
1278 config_lineno = 0;
1279 config_errors = FALSE;
1281 if (read_properties(file, " \t", read_option) == ERR ||
1282 config_errors == TRUE)
1283 fprintf(stderr, "Errors while loading %s.\n", path);
1284 }
1286 static int
1287 load_options(void)
1288 {
1289 char *home = getenv("HOME");
1290 char *tigrc_user = getenv("TIGRC_USER");
1291 char *tigrc_system = getenv("TIGRC_SYSTEM");
1292 char buf[SIZEOF_STR];
1294 add_builtin_run_requests();
1296 if (!tigrc_system) {
1297 if (!string_format(buf, "%s/tigrc", SYSCONFDIR))
1298 return ERR;
1299 tigrc_system = buf;
1300 }
1301 load_option_file(tigrc_system);
1303 if (!tigrc_user) {
1304 if (!home || !string_format(buf, "%s/.tigrc", home))
1305 return ERR;
1306 tigrc_user = buf;
1307 }
1308 load_option_file(tigrc_user);
1310 return OK;
1311 }
1314 /*
1315 * The viewer
1316 */
1318 struct view;
1319 struct view_ops;
1321 /* The display array of active views and the index of the current view. */
1322 static struct view *display[2];
1323 static unsigned int current_view;
1325 /* Reading from the prompt? */
1326 static bool input_mode = FALSE;
1328 #define foreach_displayed_view(view, i) \
1329 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1331 #define displayed_views() (display[1] != NULL ? 2 : 1)
1333 /* Current head and commit ID */
1334 static char ref_blob[SIZEOF_REF] = "";
1335 static char ref_commit[SIZEOF_REF] = "HEAD";
1336 static char ref_head[SIZEOF_REF] = "HEAD";
1338 struct view {
1339 const char *name; /* View name */
1340 const char *cmd_fmt; /* Default command line format */
1341 const char *cmd_env; /* Command line set via environment */
1342 const char *id; /* Points to either of ref_{head,commit,blob} */
1344 struct view_ops *ops; /* View operations */
1346 enum keymap keymap; /* What keymap does this view have */
1348 char cmd[SIZEOF_STR]; /* Command buffer */
1349 char ref[SIZEOF_REF]; /* Hovered commit reference */
1350 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1352 int height, width; /* The width and height of the main window */
1353 WINDOW *win; /* The main window */
1354 WINDOW *title; /* The title window living below the main window */
1356 /* Navigation */
1357 unsigned long offset; /* Offset of the window top */
1358 unsigned long lineno; /* Current line number */
1360 /* Searching */
1361 char grep[SIZEOF_STR]; /* Search string */
1362 regex_t *regex; /* Pre-compiled regex */
1364 /* If non-NULL, points to the view that opened this view. If this view
1365 * is closed tig will switch back to the parent view. */
1366 struct view *parent;
1368 /* Buffering */
1369 size_t lines; /* Total number of lines */
1370 struct line *line; /* Line index */
1371 size_t line_alloc; /* Total number of allocated lines */
1372 size_t line_size; /* Total number of used lines */
1373 unsigned int digits; /* Number of digits in the lines member. */
1375 /* Loading */
1376 FILE *pipe;
1377 time_t start_time;
1378 };
1380 struct view_ops {
1381 /* What type of content being displayed. Used in the title bar. */
1382 const char *type;
1383 /* Open and reads in all view content. */
1384 bool (*open)(struct view *view);
1385 /* Read one line; updates view->line. */
1386 bool (*read)(struct view *view, char *data);
1387 /* Draw one line; @lineno must be < view->height. */
1388 bool (*draw)(struct view *view, struct line *line, unsigned int lineno, bool selected);
1389 /* Depending on view handle a special requests. */
1390 enum request (*request)(struct view *view, enum request request, struct line *line);
1391 /* Search for regex in a line. */
1392 bool (*grep)(struct view *view, struct line *line);
1393 /* Select line */
1394 void (*select)(struct view *view, struct line *line);
1395 };
1397 static struct view_ops pager_ops;
1398 static struct view_ops main_ops;
1399 static struct view_ops tree_ops;
1400 static struct view_ops blob_ops;
1401 static struct view_ops blame_ops;
1402 static struct view_ops help_ops;
1403 static struct view_ops status_ops;
1404 static struct view_ops stage_ops;
1406 #define VIEW_STR(name, cmd, env, ref, ops, map) \
1407 { name, cmd, #env, ref, ops, map}
1409 #define VIEW_(id, name, ops, ref) \
1410 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id)
1413 static struct view views[] = {
1414 VIEW_(MAIN, "main", &main_ops, ref_head),
1415 VIEW_(DIFF, "diff", &pager_ops, ref_commit),
1416 VIEW_(LOG, "log", &pager_ops, ref_head),
1417 VIEW_(TREE, "tree", &tree_ops, ref_commit),
1418 VIEW_(BLOB, "blob", &blob_ops, ref_blob),
1419 VIEW_(BLAME, "blame", &blame_ops, ref_commit),
1420 VIEW_(HELP, "help", &help_ops, ""),
1421 VIEW_(PAGER, "pager", &pager_ops, "stdin"),
1422 VIEW_(STATUS, "status", &status_ops, ""),
1423 VIEW_(STAGE, "stage", &stage_ops, ""),
1424 };
1426 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1428 #define foreach_view(view, i) \
1429 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1431 #define view_is_displayed(view) \
1432 (view == display[0] || view == display[1])
1434 static int
1435 draw_text(struct view *view, const char *string, int max_len,
1436 bool use_tilde, int tilde_attr)
1437 {
1438 int len = 0;
1439 int trimmed = FALSE;
1441 if (max_len <= 0)
1442 return 0;
1444 if (opt_utf8) {
1445 len = utf8_length(string, max_len, &trimmed, use_tilde);
1446 } else {
1447 len = strlen(string);
1448 if (len > max_len) {
1449 if (use_tilde) {
1450 max_len -= 1;
1451 }
1452 len = max_len;
1453 trimmed = TRUE;
1454 }
1455 }
1457 waddnstr(view->win, string, len);
1458 if (trimmed && use_tilde) {
1459 if (tilde_attr != -1)
1460 wattrset(view->win, tilde_attr);
1461 waddch(view->win, '~');
1462 len++;
1463 }
1465 return len;
1466 }
1468 static bool
1469 draw_view_line(struct view *view, unsigned int lineno)
1470 {
1471 struct line *line;
1472 bool selected = (view->offset + lineno == view->lineno);
1473 bool draw_ok;
1475 assert(view_is_displayed(view));
1477 if (view->offset + lineno >= view->lines)
1478 return FALSE;
1480 line = &view->line[view->offset + lineno];
1482 if (selected) {
1483 line->selected = TRUE;
1484 view->ops->select(view, line);
1485 } else if (line->selected) {
1486 line->selected = FALSE;
1487 wmove(view->win, lineno, 0);
1488 wclrtoeol(view->win);
1489 }
1491 scrollok(view->win, FALSE);
1492 draw_ok = view->ops->draw(view, line, lineno, selected);
1493 scrollok(view->win, TRUE);
1495 return draw_ok;
1496 }
1498 static void
1499 redraw_view_dirty(struct view *view)
1500 {
1501 bool dirty = FALSE;
1502 int lineno;
1504 for (lineno = 0; lineno < view->height; lineno++) {
1505 struct line *line = &view->line[view->offset + lineno];
1507 if (!line->dirty)
1508 continue;
1509 line->dirty = 0;
1510 dirty = TRUE;
1511 if (!draw_view_line(view, lineno))
1512 break;
1513 }
1515 if (!dirty)
1516 return;
1517 redrawwin(view->win);
1518 if (input_mode)
1519 wnoutrefresh(view->win);
1520 else
1521 wrefresh(view->win);
1522 }
1524 static void
1525 redraw_view_from(struct view *view, int lineno)
1526 {
1527 assert(0 <= lineno && lineno < view->height);
1529 for (; lineno < view->height; lineno++) {
1530 if (!draw_view_line(view, lineno))
1531 break;
1532 }
1534 redrawwin(view->win);
1535 if (input_mode)
1536 wnoutrefresh(view->win);
1537 else
1538 wrefresh(view->win);
1539 }
1541 static void
1542 redraw_view(struct view *view)
1543 {
1544 wclear(view->win);
1545 redraw_view_from(view, 0);
1546 }
1549 static void
1550 update_view_title(struct view *view)
1551 {
1552 char buf[SIZEOF_STR];
1553 char state[SIZEOF_STR];
1554 size_t bufpos = 0, statelen = 0;
1556 assert(view_is_displayed(view));
1558 if (view != VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) {
1559 unsigned int view_lines = view->offset + view->height;
1560 unsigned int lines = view->lines
1561 ? MIN(view_lines, view->lines) * 100 / view->lines
1562 : 0;
1564 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1565 view->ops->type,
1566 view->lineno + 1,
1567 view->lines,
1568 lines);
1570 if (view->pipe) {
1571 time_t secs = time(NULL) - view->start_time;
1573 /* Three git seconds are a long time ... */
1574 if (secs > 2)
1575 string_format_from(state, &statelen, " %lds", secs);
1576 }
1577 }
1579 string_format_from(buf, &bufpos, "[%s]", view->name);
1580 if (*view->ref && bufpos < view->width) {
1581 size_t refsize = strlen(view->ref);
1582 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1584 if (minsize < view->width)
1585 refsize = view->width - minsize + 7;
1586 string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
1587 }
1589 if (statelen && bufpos < view->width) {
1590 string_format_from(buf, &bufpos, " %s", state);
1591 }
1593 if (view == display[current_view])
1594 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1595 else
1596 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1598 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1599 wclrtoeol(view->title);
1600 wmove(view->title, 0, view->width - 1);
1602 if (input_mode)
1603 wnoutrefresh(view->title);
1604 else
1605 wrefresh(view->title);
1606 }
1608 static void
1609 resize_display(void)
1610 {
1611 int offset, i;
1612 struct view *base = display[0];
1613 struct view *view = display[1] ? display[1] : display[0];
1615 /* Setup window dimensions */
1617 getmaxyx(stdscr, base->height, base->width);
1619 /* Make room for the status window. */
1620 base->height -= 1;
1622 if (view != base) {
1623 /* Horizontal split. */
1624 view->width = base->width;
1625 view->height = SCALE_SPLIT_VIEW(base->height);
1626 base->height -= view->height;
1628 /* Make room for the title bar. */
1629 view->height -= 1;
1630 }
1632 /* Make room for the title bar. */
1633 base->height -= 1;
1635 offset = 0;
1637 foreach_displayed_view (view, i) {
1638 if (!view->win) {
1639 view->win = newwin(view->height, 0, offset, 0);
1640 if (!view->win)
1641 die("Failed to create %s view", view->name);
1643 scrollok(view->win, TRUE);
1645 view->title = newwin(1, 0, offset + view->height, 0);
1646 if (!view->title)
1647 die("Failed to create title window");
1649 } else {
1650 wresize(view->win, view->height, view->width);
1651 mvwin(view->win, offset, 0);
1652 mvwin(view->title, offset + view->height, 0);
1653 }
1655 offset += view->height + 1;
1656 }
1657 }
1659 static void
1660 redraw_display(void)
1661 {
1662 struct view *view;
1663 int i;
1665 foreach_displayed_view (view, i) {
1666 redraw_view(view);
1667 update_view_title(view);
1668 }
1669 }
1671 static void
1672 update_display_cursor(struct view *view)
1673 {
1674 /* Move the cursor to the right-most column of the cursor line.
1675 *
1676 * XXX: This could turn out to be a bit expensive, but it ensures that
1677 * the cursor does not jump around. */
1678 if (view->lines) {
1679 wmove(view->win, view->lineno - view->offset, view->width - 1);
1680 wrefresh(view->win);
1681 }
1682 }
1684 /*
1685 * Navigation
1686 */
1688 /* Scrolling backend */
1689 static void
1690 do_scroll_view(struct view *view, int lines)
1691 {
1692 bool redraw_current_line = FALSE;
1694 /* The rendering expects the new offset. */
1695 view->offset += lines;
1697 assert(0 <= view->offset && view->offset < view->lines);
1698 assert(lines);
1700 /* Move current line into the view. */
1701 if (view->lineno < view->offset) {
1702 view->lineno = view->offset;
1703 redraw_current_line = TRUE;
1704 } else if (view->lineno >= view->offset + view->height) {
1705 view->lineno = view->offset + view->height - 1;
1706 redraw_current_line = TRUE;
1707 }
1709 assert(view->offset <= view->lineno && view->lineno < view->lines);
1711 /* Redraw the whole screen if scrolling is pointless. */
1712 if (view->height < ABS(lines)) {
1713 redraw_view(view);
1715 } else {
1716 int line = lines > 0 ? view->height - lines : 0;
1717 int end = line + ABS(lines);
1719 wscrl(view->win, lines);
1721 for (; line < end; line++) {
1722 if (!draw_view_line(view, line))
1723 break;
1724 }
1726 if (redraw_current_line)
1727 draw_view_line(view, view->lineno - view->offset);
1728 }
1730 redrawwin(view->win);
1731 wrefresh(view->win);
1732 report("");
1733 }
1735 /* Scroll frontend */
1736 static void
1737 scroll_view(struct view *view, enum request request)
1738 {
1739 int lines = 1;
1741 assert(view_is_displayed(view));
1743 switch (request) {
1744 case REQ_SCROLL_PAGE_DOWN:
1745 lines = view->height;
1746 case REQ_SCROLL_LINE_DOWN:
1747 if (view->offset + lines > view->lines)
1748 lines = view->lines - view->offset;
1750 if (lines == 0 || view->offset + view->height >= view->lines) {
1751 report("Cannot scroll beyond the last line");
1752 return;
1753 }
1754 break;
1756 case REQ_SCROLL_PAGE_UP:
1757 lines = view->height;
1758 case REQ_SCROLL_LINE_UP:
1759 if (lines > view->offset)
1760 lines = view->offset;
1762 if (lines == 0) {
1763 report("Cannot scroll beyond the first line");
1764 return;
1765 }
1767 lines = -lines;
1768 break;
1770 default:
1771 die("request %d not handled in switch", request);
1772 }
1774 do_scroll_view(view, lines);
1775 }
1777 /* Cursor moving */
1778 static void
1779 move_view(struct view *view, enum request request)
1780 {
1781 int scroll_steps = 0;
1782 int steps;
1784 switch (request) {
1785 case REQ_MOVE_FIRST_LINE:
1786 steps = -view->lineno;
1787 break;
1789 case REQ_MOVE_LAST_LINE:
1790 steps = view->lines - view->lineno - 1;
1791 break;
1793 case REQ_MOVE_PAGE_UP:
1794 steps = view->height > view->lineno
1795 ? -view->lineno : -view->height;
1796 break;
1798 case REQ_MOVE_PAGE_DOWN:
1799 steps = view->lineno + view->height >= view->lines
1800 ? view->lines - view->lineno - 1 : view->height;
1801 break;
1803 case REQ_MOVE_UP:
1804 steps = -1;
1805 break;
1807 case REQ_MOVE_DOWN:
1808 steps = 1;
1809 break;
1811 default:
1812 die("request %d not handled in switch", request);
1813 }
1815 if (steps <= 0 && view->lineno == 0) {
1816 report("Cannot move beyond the first line");
1817 return;
1819 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1820 report("Cannot move beyond the last line");
1821 return;
1822 }
1824 /* Move the current line */
1825 view->lineno += steps;
1826 assert(0 <= view->lineno && view->lineno < view->lines);
1828 /* Check whether the view needs to be scrolled */
1829 if (view->lineno < view->offset ||
1830 view->lineno >= view->offset + view->height) {
1831 scroll_steps = steps;
1832 if (steps < 0 && -steps > view->offset) {
1833 scroll_steps = -view->offset;
1835 } else if (steps > 0) {
1836 if (view->lineno == view->lines - 1 &&
1837 view->lines > view->height) {
1838 scroll_steps = view->lines - view->offset - 1;
1839 if (scroll_steps >= view->height)
1840 scroll_steps -= view->height - 1;
1841 }
1842 }
1843 }
1845 if (!view_is_displayed(view)) {
1846 view->offset += scroll_steps;
1847 assert(0 <= view->offset && view->offset < view->lines);
1848 view->ops->select(view, &view->line[view->lineno]);
1849 return;
1850 }
1852 /* Repaint the old "current" line if we be scrolling */
1853 if (ABS(steps) < view->height)
1854 draw_view_line(view, view->lineno - steps - view->offset);
1856 if (scroll_steps) {
1857 do_scroll_view(view, scroll_steps);
1858 return;
1859 }
1861 /* Draw the current line */
1862 draw_view_line(view, view->lineno - view->offset);
1864 redrawwin(view->win);
1865 wrefresh(view->win);
1866 report("");
1867 }
1870 /*
1871 * Searching
1872 */
1874 static void search_view(struct view *view, enum request request);
1876 static bool
1877 find_next_line(struct view *view, unsigned long lineno, struct line *line)
1878 {
1879 assert(view_is_displayed(view));
1881 if (!view->ops->grep(view, line))
1882 return FALSE;
1884 if (lineno - view->offset >= view->height) {
1885 view->offset = lineno;
1886 view->lineno = lineno;
1887 redraw_view(view);
1889 } else {
1890 unsigned long old_lineno = view->lineno - view->offset;
1892 view->lineno = lineno;
1893 draw_view_line(view, old_lineno);
1895 draw_view_line(view, view->lineno - view->offset);
1896 redrawwin(view->win);
1897 wrefresh(view->win);
1898 }
1900 report("Line %ld matches '%s'", lineno + 1, view->grep);
1901 return TRUE;
1902 }
1904 static void
1905 find_next(struct view *view, enum request request)
1906 {
1907 unsigned long lineno = view->lineno;
1908 int direction;
1910 if (!*view->grep) {
1911 if (!*opt_search)
1912 report("No previous search");
1913 else
1914 search_view(view, request);
1915 return;
1916 }
1918 switch (request) {
1919 case REQ_SEARCH:
1920 case REQ_FIND_NEXT:
1921 direction = 1;
1922 break;
1924 case REQ_SEARCH_BACK:
1925 case REQ_FIND_PREV:
1926 direction = -1;
1927 break;
1929 default:
1930 return;
1931 }
1933 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
1934 lineno += direction;
1936 /* Note, lineno is unsigned long so will wrap around in which case it
1937 * will become bigger than view->lines. */
1938 for (; lineno < view->lines; lineno += direction) {
1939 struct line *line = &view->line[lineno];
1941 if (find_next_line(view, lineno, line))
1942 return;
1943 }
1945 report("No match found for '%s'", view->grep);
1946 }
1948 static void
1949 search_view(struct view *view, enum request request)
1950 {
1951 int regex_err;
1953 if (view->regex) {
1954 regfree(view->regex);
1955 *view->grep = 0;
1956 } else {
1957 view->regex = calloc(1, sizeof(*view->regex));
1958 if (!view->regex)
1959 return;
1960 }
1962 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
1963 if (regex_err != 0) {
1964 char buf[SIZEOF_STR] = "unknown error";
1966 regerror(regex_err, view->regex, buf, sizeof(buf));
1967 report("Search failed: %s", buf);
1968 return;
1969 }
1971 string_copy(view->grep, opt_search);
1973 find_next(view, request);
1974 }
1976 /*
1977 * Incremental updating
1978 */
1980 static void
1981 end_update(struct view *view)
1982 {
1983 if (!view->pipe)
1984 return;
1985 set_nonblocking_input(FALSE);
1986 if (view->pipe == stdin)
1987 fclose(view->pipe);
1988 else
1989 pclose(view->pipe);
1990 view->pipe = NULL;
1991 }
1993 static bool
1994 begin_update(struct view *view)
1995 {
1996 if (view->pipe)
1997 end_update(view);
1999 if (opt_cmd[0]) {
2000 string_copy(view->cmd, opt_cmd);
2001 opt_cmd[0] = 0;
2002 /* When running random commands, initially show the
2003 * command in the title. However, it maybe later be
2004 * overwritten if a commit line is selected. */
2005 if (view == VIEW(REQ_VIEW_PAGER))
2006 string_copy(view->ref, view->cmd);
2007 else
2008 view->ref[0] = 0;
2010 } else if (view == VIEW(REQ_VIEW_TREE)) {
2011 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2012 char path[SIZEOF_STR];
2014 if (strcmp(view->vid, view->id))
2015 opt_path[0] = path[0] = 0;
2016 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
2017 return FALSE;
2019 if (!string_format(view->cmd, format, view->id, path))
2020 return FALSE;
2022 } else {
2023 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2024 const char *id = view->id;
2026 if (!string_format(view->cmd, format, id, id, id, id, id))
2027 return FALSE;
2029 /* Put the current ref_* value to the view title ref
2030 * member. This is needed by the blob view. Most other
2031 * views sets it automatically after loading because the
2032 * first line is a commit line. */
2033 string_copy_rev(view->ref, view->id);
2034 }
2036 /* Special case for the pager view. */
2037 if (opt_pipe) {
2038 view->pipe = opt_pipe;
2039 opt_pipe = NULL;
2040 } else {
2041 view->pipe = popen(view->cmd, "r");
2042 }
2044 if (!view->pipe)
2045 return FALSE;
2047 set_nonblocking_input(TRUE);
2049 view->offset = 0;
2050 view->lines = 0;
2051 view->lineno = 0;
2052 string_copy_rev(view->vid, view->id);
2054 if (view->line) {
2055 int i;
2057 for (i = 0; i < view->lines; i++)
2058 if (view->line[i].data)
2059 free(view->line[i].data);
2061 free(view->line);
2062 view->line = NULL;
2063 }
2065 view->start_time = time(NULL);
2067 return TRUE;
2068 }
2070 #define ITEM_CHUNK_SIZE 256
2071 static void *
2072 realloc_items(void *mem, size_t *size, size_t new_size, size_t item_size)
2073 {
2074 size_t num_chunks = *size / ITEM_CHUNK_SIZE;
2075 size_t num_chunks_new = (new_size + ITEM_CHUNK_SIZE - 1) / ITEM_CHUNK_SIZE;
2077 if (mem == NULL || num_chunks != num_chunks_new) {
2078 *size = num_chunks_new * ITEM_CHUNK_SIZE;
2079 mem = realloc(mem, *size * item_size);
2080 }
2082 return mem;
2083 }
2085 static struct line *
2086 realloc_lines(struct view *view, size_t line_size)
2087 {
2088 size_t alloc = view->line_alloc;
2089 struct line *tmp = realloc_items(view->line, &alloc, line_size,
2090 sizeof(*view->line));
2092 if (!tmp)
2093 return NULL;
2095 view->line = tmp;
2096 view->line_alloc = alloc;
2097 view->line_size = line_size;
2098 return view->line;
2099 }
2101 static bool
2102 update_view(struct view *view)
2103 {
2104 char in_buffer[BUFSIZ];
2105 char out_buffer[BUFSIZ * 2];
2106 char *line;
2107 /* The number of lines to read. If too low it will cause too much
2108 * redrawing (and possible flickering), if too high responsiveness
2109 * will suffer. */
2110 unsigned long lines = view->height;
2111 int redraw_from = -1;
2113 if (!view->pipe)
2114 return TRUE;
2116 /* Only redraw if lines are visible. */
2117 if (view->offset + view->height >= view->lines)
2118 redraw_from = view->lines - view->offset;
2120 /* FIXME: This is probably not perfect for backgrounded views. */
2121 if (!realloc_lines(view, view->lines + lines))
2122 goto alloc_error;
2124 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
2125 size_t linelen = strlen(line);
2127 if (linelen)
2128 line[linelen - 1] = 0;
2130 if (opt_iconv != ICONV_NONE) {
2131 ICONV_CONST char *inbuf = line;
2132 size_t inlen = linelen;
2134 char *outbuf = out_buffer;
2135 size_t outlen = sizeof(out_buffer);
2137 size_t ret;
2139 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
2140 if (ret != (size_t) -1) {
2141 line = out_buffer;
2142 linelen = strlen(out_buffer);
2143 }
2144 }
2146 if (!view->ops->read(view, line))
2147 goto alloc_error;
2149 if (lines-- == 1)
2150 break;
2151 }
2153 {
2154 int digits;
2156 lines = view->lines;
2157 for (digits = 0; lines; digits++)
2158 lines /= 10;
2160 /* Keep the displayed view in sync with line number scaling. */
2161 if (digits != view->digits) {
2162 view->digits = digits;
2163 redraw_from = 0;
2164 }
2165 }
2167 if (!view_is_displayed(view))
2168 goto check_pipe;
2170 if (view == VIEW(REQ_VIEW_TREE)) {
2171 /* Clear the view and redraw everything since the tree sorting
2172 * might have rearranged things. */
2173 redraw_view(view);
2176 } else if (redraw_from >= 0) {
2177 /* If this is an incremental update, redraw the previous line
2178 * since for commits some members could have changed when
2179 * loading the main view. */
2180 if (redraw_from > 0)
2181 redraw_from--;
2183 /* Since revision graph visualization requires knowledge
2184 * about the parent commit, it causes a further one-off
2185 * needed to be redrawn for incremental updates. */
2186 if (redraw_from > 0 && opt_rev_graph)
2187 redraw_from--;
2189 /* Incrementally draw avoids flickering. */
2190 redraw_view_from(view, redraw_from);
2191 }
2193 if (view == VIEW(REQ_VIEW_BLAME))
2194 redraw_view_dirty(view);
2196 /* Update the title _after_ the redraw so that if the redraw picks up a
2197 * commit reference in view->ref it'll be available here. */
2198 update_view_title(view);
2200 check_pipe:
2201 if (ferror(view->pipe)) {
2202 report("Failed to read: %s", strerror(errno));
2203 goto end;
2205 } else if (feof(view->pipe)) {
2206 report("");
2207 goto end;
2208 }
2210 return TRUE;
2212 alloc_error:
2213 report("Allocation failure");
2215 end:
2216 if (view->ops->read(view, NULL))
2217 end_update(view);
2218 return FALSE;
2219 }
2221 static struct line *
2222 add_line_data(struct view *view, void *data, enum line_type type)
2223 {
2224 struct line *line = &view->line[view->lines++];
2226 memset(line, 0, sizeof(*line));
2227 line->type = type;
2228 line->data = data;
2230 return line;
2231 }
2233 static struct line *
2234 add_line_text(struct view *view, char *data, enum line_type type)
2235 {
2236 if (data)
2237 data = strdup(data);
2239 return data ? add_line_data(view, data, type) : NULL;
2240 }
2243 /*
2244 * View opening
2245 */
2247 enum open_flags {
2248 OPEN_DEFAULT = 0, /* Use default view switching. */
2249 OPEN_SPLIT = 1, /* Split current view. */
2250 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2251 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2252 };
2254 static void
2255 open_view(struct view *prev, enum request request, enum open_flags flags)
2256 {
2257 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2258 bool split = !!(flags & OPEN_SPLIT);
2259 bool reload = !!(flags & OPEN_RELOAD);
2260 struct view *view = VIEW(request);
2261 int nviews = displayed_views();
2262 struct view *base_view = display[0];
2264 if (view == prev && nviews == 1 && !reload) {
2265 report("Already in %s view", view->name);
2266 return;
2267 }
2269 if (view->ops->open) {
2270 if (!view->ops->open(view)) {
2271 report("Failed to load %s view", view->name);
2272 return;
2273 }
2275 } else if ((reload || strcmp(view->vid, view->id)) &&
2276 !begin_update(view)) {
2277 report("Failed to load %s view", view->name);
2278 return;
2279 }
2281 if (split) {
2282 display[1] = view;
2283 if (!backgrounded)
2284 current_view = 1;
2285 } else {
2286 /* Maximize the current view. */
2287 memset(display, 0, sizeof(display));
2288 current_view = 0;
2289 display[current_view] = view;
2290 }
2292 /* Resize the view when switching between split- and full-screen,
2293 * or when switching between two different full-screen views. */
2294 if (nviews != displayed_views() ||
2295 (nviews == 1 && base_view != display[0]))
2296 resize_display();
2298 if (split && prev->lineno - prev->offset >= prev->height) {
2299 /* Take the title line into account. */
2300 int lines = prev->lineno - prev->offset - prev->height + 1;
2302 /* Scroll the view that was split if the current line is
2303 * outside the new limited view. */
2304 do_scroll_view(prev, lines);
2305 }
2307 if (prev && view != prev) {
2308 if (split && !backgrounded) {
2309 /* "Blur" the previous view. */
2310 update_view_title(prev);
2311 }
2313 view->parent = prev;
2314 }
2316 if (view->pipe && view->lines == 0) {
2317 /* Clear the old view and let the incremental updating refill
2318 * the screen. */
2319 wclear(view->win);
2320 report("");
2321 } else {
2322 redraw_view(view);
2323 report("");
2324 }
2326 /* If the view is backgrounded the above calls to report()
2327 * won't redraw the view title. */
2328 if (backgrounded)
2329 update_view_title(view);
2330 }
2332 static void
2333 open_external_viewer(const char *cmd)
2334 {
2335 def_prog_mode(); /* save current tty modes */
2336 endwin(); /* restore original tty modes */
2337 system(cmd);
2338 fprintf(stderr, "Press Enter to continue");
2339 getc(stdin);
2340 reset_prog_mode();
2341 redraw_display();
2342 }
2344 static void
2345 open_mergetool(const char *file)
2346 {
2347 char cmd[SIZEOF_STR];
2348 char file_sq[SIZEOF_STR];
2350 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2351 string_format(cmd, "git mergetool %s", file_sq)) {
2352 open_external_viewer(cmd);
2353 }
2354 }
2356 static void
2357 open_editor(bool from_root, const char *file)
2358 {
2359 char cmd[SIZEOF_STR];
2360 char file_sq[SIZEOF_STR];
2361 char *editor;
2362 char *prefix = from_root ? opt_cdup : "";
2364 editor = getenv("GIT_EDITOR");
2365 if (!editor && *opt_editor)
2366 editor = opt_editor;
2367 if (!editor)
2368 editor = getenv("VISUAL");
2369 if (!editor)
2370 editor = getenv("EDITOR");
2371 if (!editor)
2372 editor = "vi";
2374 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2375 string_format(cmd, "%s %s%s", editor, prefix, file_sq)) {
2376 open_external_viewer(cmd);
2377 }
2378 }
2380 static void
2381 open_run_request(enum request request)
2382 {
2383 struct run_request *req = get_run_request(request);
2384 char buf[SIZEOF_STR * 2];
2385 size_t bufpos;
2386 char *cmd;
2388 if (!req) {
2389 report("Unknown run request");
2390 return;
2391 }
2393 bufpos = 0;
2394 cmd = req->cmd;
2396 while (cmd) {
2397 char *next = strstr(cmd, "%(");
2398 int len = next - cmd;
2399 char *value;
2401 if (!next) {
2402 len = strlen(cmd);
2403 value = "";
2405 } else if (!strncmp(next, "%(head)", 7)) {
2406 value = ref_head;
2408 } else if (!strncmp(next, "%(commit)", 9)) {
2409 value = ref_commit;
2411 } else if (!strncmp(next, "%(blob)", 7)) {
2412 value = ref_blob;
2414 } else {
2415 report("Unknown replacement in run request: `%s`", req->cmd);
2416 return;
2417 }
2419 if (!string_format_from(buf, &bufpos, "%.*s%s", len, cmd, value))
2420 return;
2422 if (next)
2423 next = strchr(next, ')') + 1;
2424 cmd = next;
2425 }
2427 open_external_viewer(buf);
2428 }
2430 /*
2431 * User request switch noodle
2432 */
2434 static int
2435 view_driver(struct view *view, enum request request)
2436 {
2437 int i;
2439 if (request == REQ_NONE) {
2440 doupdate();
2441 return TRUE;
2442 }
2444 if (request > REQ_NONE) {
2445 open_run_request(request);
2446 return TRUE;
2447 }
2449 if (view && view->lines) {
2450 request = view->ops->request(view, request, &view->line[view->lineno]);
2451 if (request == REQ_NONE)
2452 return TRUE;
2453 }
2455 switch (request) {
2456 case REQ_MOVE_UP:
2457 case REQ_MOVE_DOWN:
2458 case REQ_MOVE_PAGE_UP:
2459 case REQ_MOVE_PAGE_DOWN:
2460 case REQ_MOVE_FIRST_LINE:
2461 case REQ_MOVE_LAST_LINE:
2462 move_view(view, request);
2463 break;
2465 case REQ_SCROLL_LINE_DOWN:
2466 case REQ_SCROLL_LINE_UP:
2467 case REQ_SCROLL_PAGE_DOWN:
2468 case REQ_SCROLL_PAGE_UP:
2469 scroll_view(view, request);
2470 break;
2472 case REQ_VIEW_BLAME:
2473 if (!opt_file[0]) {
2474 report("No file chosen, press %s to open tree view",
2475 get_key(REQ_VIEW_TREE));
2476 break;
2477 }
2478 open_view(view, request, OPEN_DEFAULT);
2479 break;
2481 case REQ_VIEW_BLOB:
2482 if (!ref_blob[0]) {
2483 report("No file chosen, press %s to open tree view",
2484 get_key(REQ_VIEW_TREE));
2485 break;
2486 }
2487 open_view(view, request, OPEN_DEFAULT);
2488 break;
2490 case REQ_VIEW_PAGER:
2491 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2492 report("No pager content, press %s to run command from prompt",
2493 get_key(REQ_PROMPT));
2494 break;
2495 }
2496 open_view(view, request, OPEN_DEFAULT);
2497 break;
2499 case REQ_VIEW_STAGE:
2500 if (!VIEW(REQ_VIEW_STAGE)->lines) {
2501 report("No stage content, press %s to open the status view and choose file",
2502 get_key(REQ_VIEW_STATUS));
2503 break;
2504 }
2505 open_view(view, request, OPEN_DEFAULT);
2506 break;
2508 case REQ_VIEW_STATUS:
2509 if (opt_is_inside_work_tree == FALSE) {
2510 report("The status view requires a working tree");
2511 break;
2512 }
2513 open_view(view, request, OPEN_DEFAULT);
2514 break;
2516 case REQ_VIEW_MAIN:
2517 case REQ_VIEW_DIFF:
2518 case REQ_VIEW_LOG:
2519 case REQ_VIEW_TREE:
2520 case REQ_VIEW_HELP:
2521 open_view(view, request, OPEN_DEFAULT);
2522 break;
2524 case REQ_NEXT:
2525 case REQ_PREVIOUS:
2526 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2528 if ((view == VIEW(REQ_VIEW_DIFF) &&
2529 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2530 (view == VIEW(REQ_VIEW_DIFF) &&
2531 view->parent == VIEW(REQ_VIEW_BLAME)) ||
2532 (view == VIEW(REQ_VIEW_STAGE) &&
2533 view->parent == VIEW(REQ_VIEW_STATUS)) ||
2534 (view == VIEW(REQ_VIEW_BLOB) &&
2535 view->parent == VIEW(REQ_VIEW_TREE))) {
2536 int line;
2538 view = view->parent;
2539 line = view->lineno;
2540 move_view(view, request);
2541 if (view_is_displayed(view))
2542 update_view_title(view);
2543 if (line != view->lineno)
2544 view->ops->request(view, REQ_ENTER,
2545 &view->line[view->lineno]);
2547 } else {
2548 move_view(view, request);
2549 }
2550 break;
2552 case REQ_VIEW_NEXT:
2553 {
2554 int nviews = displayed_views();
2555 int next_view = (current_view + 1) % nviews;
2557 if (next_view == current_view) {
2558 report("Only one view is displayed");
2559 break;
2560 }
2562 current_view = next_view;
2563 /* Blur out the title of the previous view. */
2564 update_view_title(view);
2565 report("");
2566 break;
2567 }
2568 case REQ_REFRESH:
2569 report("Refreshing is not yet supported for the %s view", view->name);
2570 break;
2572 case REQ_TOGGLE_LINENO:
2573 opt_line_number = !opt_line_number;
2574 redraw_display();
2575 break;
2577 case REQ_TOGGLE_DATE:
2578 opt_date = !opt_date;
2579 redraw_display();
2580 break;
2582 case REQ_TOGGLE_AUTHOR:
2583 opt_author = !opt_author;
2584 redraw_display();
2585 break;
2587 case REQ_TOGGLE_REV_GRAPH:
2588 opt_rev_graph = !opt_rev_graph;
2589 redraw_display();
2590 break;
2592 case REQ_TOGGLE_REFS:
2593 opt_show_refs = !opt_show_refs;
2594 redraw_display();
2595 break;
2597 case REQ_PROMPT:
2598 /* Always reload^Wrerun commands from the prompt. */
2599 open_view(view, opt_request, OPEN_RELOAD);
2600 break;
2602 case REQ_SEARCH:
2603 case REQ_SEARCH_BACK:
2604 search_view(view, request);
2605 break;
2607 case REQ_FIND_NEXT:
2608 case REQ_FIND_PREV:
2609 find_next(view, request);
2610 break;
2612 case REQ_STOP_LOADING:
2613 for (i = 0; i < ARRAY_SIZE(views); i++) {
2614 view = &views[i];
2615 if (view->pipe)
2616 report("Stopped loading the %s view", view->name),
2617 end_update(view);
2618 }
2619 break;
2621 case REQ_SHOW_VERSION:
2622 report("tig-%s (built %s)", TIG_VERSION, __DATE__);
2623 return TRUE;
2625 case REQ_SCREEN_RESIZE:
2626 resize_display();
2627 /* Fall-through */
2628 case REQ_SCREEN_REDRAW:
2629 redraw_display();
2630 break;
2632 case REQ_EDIT:
2633 report("Nothing to edit");
2634 break;
2637 case REQ_ENTER:
2638 report("Nothing to enter");
2639 break;
2642 case REQ_VIEW_CLOSE:
2643 /* XXX: Mark closed views by letting view->parent point to the
2644 * view itself. Parents to closed view should never be
2645 * followed. */
2646 if (view->parent &&
2647 view->parent->parent != view->parent) {
2648 memset(display, 0, sizeof(display));
2649 current_view = 0;
2650 display[current_view] = view->parent;
2651 view->parent = view;
2652 resize_display();
2653 redraw_display();
2654 break;
2655 }
2656 /* Fall-through */
2657 case REQ_QUIT:
2658 return FALSE;
2660 default:
2661 /* An unknown key will show most commonly used commands. */
2662 report("Unknown key, press 'h' for help");
2663 return TRUE;
2664 }
2666 return TRUE;
2667 }
2670 /*
2671 * Pager backend
2672 */
2674 static bool
2675 pager_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2676 {
2677 char *text = line->data;
2678 enum line_type type = line->type;
2679 int attr;
2681 wmove(view->win, lineno, 0);
2683 if (selected) {
2684 type = LINE_CURSOR;
2685 wchgat(view->win, -1, 0, type, NULL);
2686 }
2688 attr = get_line_attr(type);
2689 wattrset(view->win, attr);
2691 if (opt_line_number || opt_tab_size < TABSIZE) {
2692 static char spaces[] = " ";
2693 int col_offset = 0, col = 0;
2695 if (opt_line_number) {
2696 unsigned long real_lineno = view->offset + lineno + 1;
2698 if (real_lineno == 1 ||
2699 (real_lineno % opt_num_interval) == 0) {
2700 wprintw(view->win, "%.*d", view->digits, real_lineno);
2702 } else {
2703 waddnstr(view->win, spaces,
2704 MIN(view->digits, STRING_SIZE(spaces)));
2705 }
2706 waddstr(view->win, ": ");
2707 col_offset = view->digits + 2;
2708 }
2710 while (text && col_offset + col < view->width) {
2711 int cols_max = view->width - col_offset - col;
2712 char *pos = text;
2713 int cols;
2715 if (*text == '\t') {
2716 text++;
2717 assert(sizeof(spaces) > TABSIZE);
2718 pos = spaces;
2719 cols = opt_tab_size - (col % opt_tab_size);
2721 } else {
2722 text = strchr(text, '\t');
2723 cols = line ? text - pos : strlen(pos);
2724 }
2726 waddnstr(view->win, pos, MIN(cols, cols_max));
2727 col += cols;
2728 }
2730 } else {
2731 int tilde_attr = get_line_attr(LINE_MAIN_DELIM);
2733 draw_text(view, text, view->width, TRUE, tilde_attr);
2734 }
2736 return TRUE;
2737 }
2739 static bool
2740 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2741 {
2742 char refbuf[SIZEOF_STR];
2743 char *ref = NULL;
2744 FILE *pipe;
2746 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2747 return TRUE;
2749 pipe = popen(refbuf, "r");
2750 if (!pipe)
2751 return TRUE;
2753 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2754 ref = chomp_string(ref);
2755 pclose(pipe);
2757 if (!ref || !*ref)
2758 return TRUE;
2760 /* This is the only fatal call, since it can "corrupt" the buffer. */
2761 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2762 return FALSE;
2764 return TRUE;
2765 }
2767 static void
2768 add_pager_refs(struct view *view, struct line *line)
2769 {
2770 char buf[SIZEOF_STR];
2771 char *commit_id = (char *)line->data + STRING_SIZE("commit ");
2772 struct ref **refs;
2773 size_t bufpos = 0, refpos = 0;
2774 const char *sep = "Refs: ";
2775 bool is_tag = FALSE;
2777 assert(line->type == LINE_COMMIT);
2779 refs = get_refs(commit_id);
2780 if (!refs) {
2781 if (view == VIEW(REQ_VIEW_DIFF))
2782 goto try_add_describe_ref;
2783 return;
2784 }
2786 do {
2787 struct ref *ref = refs[refpos];
2788 char *fmt = ref->tag ? "%s[%s]" :
2789 ref->remote ? "%s<%s>" : "%s%s";
2791 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2792 return;
2793 sep = ", ";
2794 if (ref->tag)
2795 is_tag = TRUE;
2796 } while (refs[refpos++]->next);
2798 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2799 try_add_describe_ref:
2800 /* Add <tag>-g<commit_id> "fake" reference. */
2801 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2802 return;
2803 }
2805 if (bufpos == 0)
2806 return;
2808 if (!realloc_lines(view, view->line_size + 1))
2809 return;
2811 add_line_text(view, buf, LINE_PP_REFS);
2812 }
2814 static bool
2815 pager_read(struct view *view, char *data)
2816 {
2817 struct line *line;
2819 if (!data)
2820 return TRUE;
2822 line = add_line_text(view, data, get_line_type(data));
2823 if (!line)
2824 return FALSE;
2826 if (line->type == LINE_COMMIT &&
2827 (view == VIEW(REQ_VIEW_DIFF) ||
2828 view == VIEW(REQ_VIEW_LOG)))
2829 add_pager_refs(view, line);
2831 return TRUE;
2832 }
2834 static enum request
2835 pager_request(struct view *view, enum request request, struct line *line)
2836 {
2837 int split = 0;
2839 if (request != REQ_ENTER)
2840 return request;
2842 if (line->type == LINE_COMMIT &&
2843 (view == VIEW(REQ_VIEW_LOG) ||
2844 view == VIEW(REQ_VIEW_PAGER))) {
2845 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2846 split = 1;
2847 }
2849 /* Always scroll the view even if it was split. That way
2850 * you can use Enter to scroll through the log view and
2851 * split open each commit diff. */
2852 scroll_view(view, REQ_SCROLL_LINE_DOWN);
2854 /* FIXME: A minor workaround. Scrolling the view will call report("")
2855 * but if we are scrolling a non-current view this won't properly
2856 * update the view title. */
2857 if (split)
2858 update_view_title(view);
2860 return REQ_NONE;
2861 }
2863 static bool
2864 pager_grep(struct view *view, struct line *line)
2865 {
2866 regmatch_t pmatch;
2867 char *text = line->data;
2869 if (!*text)
2870 return FALSE;
2872 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
2873 return FALSE;
2875 return TRUE;
2876 }
2878 static void
2879 pager_select(struct view *view, struct line *line)
2880 {
2881 if (line->type == LINE_COMMIT) {
2882 char *text = (char *)line->data + STRING_SIZE("commit ");
2884 if (view != VIEW(REQ_VIEW_PAGER))
2885 string_copy_rev(view->ref, text);
2886 string_copy_rev(ref_commit, text);
2887 }
2888 }
2890 static struct view_ops pager_ops = {
2891 "line",
2892 NULL,
2893 pager_read,
2894 pager_draw,
2895 pager_request,
2896 pager_grep,
2897 pager_select,
2898 };
2901 /*
2902 * Help backend
2903 */
2905 static bool
2906 help_open(struct view *view)
2907 {
2908 char buf[BUFSIZ];
2909 int lines = ARRAY_SIZE(req_info) + 2;
2910 int i;
2912 if (view->lines > 0)
2913 return TRUE;
2915 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2916 if (!req_info[i].request)
2917 lines++;
2919 lines += run_requests + 1;
2921 view->line = calloc(lines, sizeof(*view->line));
2922 if (!view->line)
2923 return FALSE;
2925 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
2927 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2928 char *key;
2930 if (req_info[i].request == REQ_NONE)
2931 continue;
2933 if (!req_info[i].request) {
2934 add_line_text(view, "", LINE_DEFAULT);
2935 add_line_text(view, req_info[i].help, LINE_DEFAULT);
2936 continue;
2937 }
2939 key = get_key(req_info[i].request);
2940 if (!*key)
2941 key = "(no key defined)";
2943 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
2944 continue;
2946 add_line_text(view, buf, LINE_DEFAULT);
2947 }
2949 if (run_requests) {
2950 add_line_text(view, "", LINE_DEFAULT);
2951 add_line_text(view, "External commands:", LINE_DEFAULT);
2952 }
2954 for (i = 0; i < run_requests; i++) {
2955 struct run_request *req = get_run_request(REQ_NONE + i + 1);
2956 char *key;
2958 if (!req)
2959 continue;
2961 key = get_key_name(req->key);
2962 if (!*key)
2963 key = "(no key defined)";
2965 if (!string_format(buf, " %-10s %-14s `%s`",
2966 keymap_table[req->keymap].name,
2967 key, req->cmd))
2968 continue;
2970 add_line_text(view, buf, LINE_DEFAULT);
2971 }
2973 return TRUE;
2974 }
2976 static struct view_ops help_ops = {
2977 "line",
2978 help_open,
2979 NULL,
2980 pager_draw,
2981 pager_request,
2982 pager_grep,
2983 pager_select,
2984 };
2987 /*
2988 * Tree backend
2989 */
2991 struct tree_stack_entry {
2992 struct tree_stack_entry *prev; /* Entry below this in the stack */
2993 unsigned long lineno; /* Line number to restore */
2994 char *name; /* Position of name in opt_path */
2995 };
2997 /* The top of the path stack. */
2998 static struct tree_stack_entry *tree_stack = NULL;
2999 unsigned long tree_lineno = 0;
3001 static void
3002 pop_tree_stack_entry(void)
3003 {
3004 struct tree_stack_entry *entry = tree_stack;
3006 tree_lineno = entry->lineno;
3007 entry->name[0] = 0;
3008 tree_stack = entry->prev;
3009 free(entry);
3010 }
3012 static void
3013 push_tree_stack_entry(char *name, unsigned long lineno)
3014 {
3015 struct tree_stack_entry *entry = calloc(1, sizeof(*entry));
3016 size_t pathlen = strlen(opt_path);
3018 if (!entry)
3019 return;
3021 entry->prev = tree_stack;
3022 entry->name = opt_path + pathlen;
3023 tree_stack = entry;
3025 if (!string_format_from(opt_path, &pathlen, "%s/", name)) {
3026 pop_tree_stack_entry();
3027 return;
3028 }
3030 /* Move the current line to the first tree entry. */
3031 tree_lineno = 1;
3032 entry->lineno = lineno;
3033 }
3035 /* Parse output from git-ls-tree(1):
3036 *
3037 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
3038 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
3039 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
3040 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
3041 */
3043 #define SIZEOF_TREE_ATTR \
3044 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
3046 #define TREE_UP_FORMAT "040000 tree %s\t.."
3048 static int
3049 tree_compare_entry(enum line_type type1, char *name1,
3050 enum line_type type2, char *name2)
3051 {
3052 if (type1 != type2) {
3053 if (type1 == LINE_TREE_DIR)
3054 return -1;
3055 return 1;
3056 }
3058 return strcmp(name1, name2);
3059 }
3061 static char *
3062 tree_path(struct line *line)
3063 {
3064 char *path = line->data;
3066 return path + SIZEOF_TREE_ATTR;
3067 }
3069 static bool
3070 tree_read(struct view *view, char *text)
3071 {
3072 size_t textlen = text ? strlen(text) : 0;
3073 char buf[SIZEOF_STR];
3074 unsigned long pos;
3075 enum line_type type;
3076 bool first_read = view->lines == 0;
3078 if (!text)
3079 return TRUE;
3080 if (textlen <= SIZEOF_TREE_ATTR)
3081 return FALSE;
3083 type = text[STRING_SIZE("100644 ")] == 't'
3084 ? LINE_TREE_DIR : LINE_TREE_FILE;
3086 if (first_read) {
3087 /* Add path info line */
3088 if (!string_format(buf, "Directory path /%s", opt_path) ||
3089 !realloc_lines(view, view->line_size + 1) ||
3090 !add_line_text(view, buf, LINE_DEFAULT))
3091 return FALSE;
3093 /* Insert "link" to parent directory. */
3094 if (*opt_path) {
3095 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
3096 !realloc_lines(view, view->line_size + 1) ||
3097 !add_line_text(view, buf, LINE_TREE_DIR))
3098 return FALSE;
3099 }
3100 }
3102 /* Strip the path part ... */
3103 if (*opt_path) {
3104 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
3105 size_t striplen = strlen(opt_path);
3106 char *path = text + SIZEOF_TREE_ATTR;
3108 if (pathlen > striplen)
3109 memmove(path, path + striplen,
3110 pathlen - striplen + 1);
3111 }
3113 /* Skip "Directory ..." and ".." line. */
3114 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
3115 struct line *line = &view->line[pos];
3116 char *path1 = tree_path(line);
3117 char *path2 = text + SIZEOF_TREE_ATTR;
3118 int cmp = tree_compare_entry(line->type, path1, type, path2);
3120 if (cmp <= 0)
3121 continue;
3123 text = strdup(text);
3124 if (!text)
3125 return FALSE;
3127 if (view->lines > pos)
3128 memmove(&view->line[pos + 1], &view->line[pos],
3129 (view->lines - pos) * sizeof(*line));
3131 line = &view->line[pos];
3132 line->data = text;
3133 line->type = type;
3134 view->lines++;
3135 return TRUE;
3136 }
3138 if (!add_line_text(view, text, type))
3139 return FALSE;
3141 if (tree_lineno > view->lineno) {
3142 view->lineno = tree_lineno;
3143 tree_lineno = 0;
3144 }
3146 return TRUE;
3147 }
3149 static enum request
3150 tree_request(struct view *view, enum request request, struct line *line)
3151 {
3152 enum open_flags flags;
3154 if (request == REQ_VIEW_BLAME) {
3155 char *filename = tree_path(line);
3157 if (line->type == LINE_TREE_DIR) {
3158 report("Cannot show blame for directory %s", opt_path);
3159 return REQ_NONE;
3160 }
3162 string_copy(opt_ref, ref_commit);
3163 string_ncopy(opt_file, filename, strlen(filename));
3164 return request;
3165 }
3166 if (request == REQ_TREE_PARENT) {
3167 if (*opt_path) {
3168 /* fake 'cd ..' */
3169 request = REQ_ENTER;
3170 line = &view->line[1];
3171 } else {
3172 /* quit view if at top of tree */
3173 return REQ_VIEW_CLOSE;
3174 }
3175 }
3176 if (request != REQ_ENTER)
3177 return request;
3179 /* Cleanup the stack if the tree view is at a different tree. */
3180 while (!*opt_path && tree_stack)
3181 pop_tree_stack_entry();
3183 switch (line->type) {
3184 case LINE_TREE_DIR:
3185 /* Depending on whether it is a subdir or parent (updir?) link
3186 * mangle the path buffer. */
3187 if (line == &view->line[1] && *opt_path) {
3188 pop_tree_stack_entry();
3190 } else {
3191 char *basename = tree_path(line);
3193 push_tree_stack_entry(basename, view->lineno);
3194 }
3196 /* Trees and subtrees share the same ID, so they are not not
3197 * unique like blobs. */
3198 flags = OPEN_RELOAD;
3199 request = REQ_VIEW_TREE;
3200 break;
3202 case LINE_TREE_FILE:
3203 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3204 request = REQ_VIEW_BLOB;
3205 break;
3207 default:
3208 return TRUE;
3209 }
3211 open_view(view, request, flags);
3212 if (request == REQ_VIEW_TREE) {
3213 view->lineno = tree_lineno;
3214 }
3216 return REQ_NONE;
3217 }
3219 static void
3220 tree_select(struct view *view, struct line *line)
3221 {
3222 char *text = (char *)line->data + STRING_SIZE("100644 blob ");
3224 if (line->type == LINE_TREE_FILE) {
3225 string_copy_rev(ref_blob, text);
3227 } else if (line->type != LINE_TREE_DIR) {
3228 return;
3229 }
3231 string_copy_rev(view->ref, text);
3232 }
3234 static struct view_ops tree_ops = {
3235 "file",
3236 NULL,
3237 tree_read,
3238 pager_draw,
3239 tree_request,
3240 pager_grep,
3241 tree_select,
3242 };
3244 static bool
3245 blob_read(struct view *view, char *line)
3246 {
3247 if (!line)
3248 return TRUE;
3249 return add_line_text(view, line, LINE_DEFAULT) != NULL;
3250 }
3252 static struct view_ops blob_ops = {
3253 "line",
3254 NULL,
3255 blob_read,
3256 pager_draw,
3257 pager_request,
3258 pager_grep,
3259 pager_select,
3260 };
3262 /*
3263 * Blame backend
3264 *
3265 * Loading the blame view is a two phase job:
3266 *
3267 * 1. File content is read either using opt_file from the
3268 * filesystem or using git-cat-file.
3269 * 2. Then blame information is incrementally added by
3270 * reading output from git-blame.
3271 */
3273 struct blame_commit {
3274 char id[SIZEOF_REV]; /* SHA1 ID. */
3275 char title[128]; /* First line of the commit message. */
3276 char author[75]; /* Author of the commit. */
3277 struct tm time; /* Date from the author ident. */
3278 char filename[128]; /* Name of file. */
3279 };
3281 struct blame {
3282 struct blame_commit *commit;
3283 unsigned int header:1;
3284 char text[1];
3285 };
3287 #define BLAME_CAT_FILE_CMD "git cat-file blob %s:%s"
3288 #define BLAME_INCREMENTAL_CMD "git blame --incremental %s %s"
3290 static bool
3291 blame_open(struct view *view)
3292 {
3293 char path[SIZEOF_STR];
3294 char ref[SIZEOF_STR] = "";
3296 if (sq_quote(path, 0, opt_file) >= sizeof(path))
3297 return FALSE;
3299 if (*opt_ref && sq_quote(ref, 0, opt_ref) >= sizeof(ref))
3300 return FALSE;
3302 if (*opt_ref) {
3303 if (!string_format(view->cmd, BLAME_CAT_FILE_CMD, ref, path))
3304 return FALSE;
3305 } else {
3306 view->pipe = fopen(opt_file, "r");
3307 if (!view->pipe &&
3308 !string_format(view->cmd, BLAME_CAT_FILE_CMD, "HEAD", path))
3309 return FALSE;
3310 }
3312 if (!view->pipe)
3313 view->pipe = popen(view->cmd, "r");
3314 if (!view->pipe)
3315 return FALSE;
3317 if (!string_format(view->cmd, BLAME_INCREMENTAL_CMD, ref, path))
3318 return FALSE;
3320 string_format(view->ref, "%s ...", opt_file);
3321 string_copy_rev(view->vid, opt_file);
3322 set_nonblocking_input(TRUE);
3324 if (view->line) {
3325 int i;
3327 for (i = 0; i < view->lines; i++)
3328 free(view->line[i].data);
3329 free(view->line);
3330 }
3332 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3333 view->offset = view->lines = view->lineno = 0;
3334 view->line = NULL;
3335 view->start_time = time(NULL);
3337 return TRUE;
3338 }
3340 static struct blame_commit *
3341 get_blame_commit(struct view *view, const char *id)
3342 {
3343 size_t i;
3345 for (i = 0; i < view->lines; i++) {
3346 struct blame *blame = view->line[i].data;
3348 if (!blame->commit)
3349 continue;
3351 if (!strncmp(blame->commit->id, id, SIZEOF_REV - 1))
3352 return blame->commit;
3353 }
3355 {
3356 struct blame_commit *commit = calloc(1, sizeof(*commit));
3358 if (commit)
3359 string_ncopy(commit->id, id, SIZEOF_REV);
3360 return commit;
3361 }
3362 }
3364 static bool
3365 parse_number(char **posref, size_t *number, size_t min, size_t max)
3366 {
3367 char *pos = *posref;
3369 *posref = NULL;
3370 pos = strchr(pos + 1, ' ');
3371 if (!pos || !isdigit(pos[1]))
3372 return FALSE;
3373 *number = atoi(pos + 1);
3374 if (*number < min || *number > max)
3375 return FALSE;
3377 *posref = pos;
3378 return TRUE;
3379 }
3381 static struct blame_commit *
3382 parse_blame_commit(struct view *view, char *text, int *blamed)
3383 {
3384 struct blame_commit *commit;
3385 struct blame *blame;
3386 char *pos = text + SIZEOF_REV - 1;
3387 size_t lineno;
3388 size_t group;
3390 if (strlen(text) <= SIZEOF_REV || *pos != ' ')
3391 return NULL;
3393 if (!parse_number(&pos, &lineno, 1, view->lines) ||
3394 !parse_number(&pos, &group, 1, view->lines - lineno + 1))
3395 return NULL;
3397 commit = get_blame_commit(view, text);
3398 if (!commit)
3399 return NULL;
3401 *blamed += group;
3402 while (group--) {
3403 struct line *line = &view->line[lineno + group - 1];
3405 blame = line->data;
3406 blame->commit = commit;
3407 line->dirty = 1;
3408 }
3409 blame->header = 1;
3411 return commit;
3412 }
3414 static bool
3415 blame_read_file(struct view *view, char *line)
3416 {
3417 if (!line) {
3418 FILE *pipe = NULL;
3420 if (view->lines > 0)
3421 pipe = popen(view->cmd, "r");
3422 view->cmd[0] = 0;
3423 if (!pipe) {
3424 report("Failed to load blame data");
3425 return TRUE;
3426 }
3428 fclose(view->pipe);
3429 view->pipe = pipe;
3430 return FALSE;
3432 } else {
3433 size_t linelen = strlen(line);
3434 struct blame *blame = malloc(sizeof(*blame) + linelen);
3436 if (!line)
3437 return FALSE;
3439 blame->commit = NULL;
3440 strncpy(blame->text, line, linelen);
3441 blame->text[linelen] = 0;
3442 return add_line_data(view, blame, LINE_BLAME_COMMIT) != NULL;
3443 }
3444 }
3446 static bool
3447 match_blame_header(const char *name, char **line)
3448 {
3449 size_t namelen = strlen(name);
3450 bool matched = !strncmp(name, *line, namelen);
3452 if (matched)
3453 *line += namelen;
3455 return matched;
3456 }
3458 static bool
3459 blame_read(struct view *view, char *line)
3460 {
3461 static struct blame_commit *commit = NULL;
3462 static int blamed = 0;
3463 static time_t author_time;
3465 if (*view->cmd)
3466 return blame_read_file(view, line);
3468 if (!line) {
3469 /* Reset all! */
3470 commit = NULL;
3471 blamed = 0;
3472 string_format(view->ref, "%s", view->vid);
3473 if (view_is_displayed(view)) {
3474 update_view_title(view);
3475 redraw_view_from(view, 0);
3476 }
3477 return TRUE;
3478 }
3480 if (!commit) {
3481 commit = parse_blame_commit(view, line, &blamed);
3482 string_format(view->ref, "%s %2d%%", view->vid,
3483 blamed * 100 / view->lines);
3485 } else if (match_blame_header("author ", &line)) {
3486 string_ncopy(commit->author, line, strlen(line));
3488 } else if (match_blame_header("author-time ", &line)) {
3489 author_time = (time_t) atol(line);
3491 } else if (match_blame_header("author-tz ", &line)) {
3492 long tz;
3494 tz = ('0' - line[1]) * 60 * 60 * 10;
3495 tz += ('0' - line[2]) * 60 * 60;
3496 tz += ('0' - line[3]) * 60;
3497 tz += ('0' - line[4]) * 60;
3499 if (line[0] == '-')
3500 tz = -tz;
3502 author_time -= tz;
3503 gmtime_r(&author_time, &commit->time);
3505 } else if (match_blame_header("summary ", &line)) {
3506 string_ncopy(commit->title, line, strlen(line));
3508 } else if (match_blame_header("filename ", &line)) {
3509 string_ncopy(commit->filename, line, strlen(line));
3510 commit = NULL;
3511 }
3513 return TRUE;
3514 }
3516 static bool
3517 blame_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
3518 {
3519 int tilde_attr = -1;
3520 struct blame *blame = line->data;
3521 int col = 0;
3523 wmove(view->win, lineno, 0);
3525 if (selected) {
3526 wattrset(view->win, get_line_attr(LINE_CURSOR));
3527 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
3528 } else {
3529 wattrset(view->win, A_NORMAL);
3530 tilde_attr = get_line_attr(LINE_MAIN_DELIM);
3531 }
3533 if (opt_date) {
3534 int n;
3536 if (!selected)
3537 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
3538 if (blame->commit) {
3539 char buf[DATE_COLS + 1];
3540 int timelen;
3542 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &blame->commit->time);
3543 n = draw_text(view, buf, view->width - col, FALSE, tilde_attr);
3544 draw_text(view, " ", view->width - col - n, FALSE, tilde_attr);
3545 }
3547 col += DATE_COLS;
3548 wmove(view->win, lineno, col);
3549 if (col >= view->width)
3550 return TRUE;
3551 }
3553 if (opt_author) {
3554 int max = MIN(AUTHOR_COLS - 1, view->width - col);
3556 if (!selected)
3557 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
3558 if (blame->commit)
3559 draw_text(view, blame->commit->author, max, TRUE, tilde_attr);
3560 col += AUTHOR_COLS;
3561 if (col >= view->width)
3562 return TRUE;
3563 wmove(view->win, lineno, col);
3564 }
3566 {
3567 int max = MIN(ID_COLS - 1, view->width - col);
3569 if (!selected)
3570 wattrset(view->win, get_line_attr(LINE_BLAME_ID));
3571 if (blame->commit)
3572 draw_text(view, blame->commit->id, max, FALSE, -1);
3573 col += ID_COLS;
3574 if (col >= view->width)
3575 return TRUE;
3576 wmove(view->win, lineno, col);
3577 }
3579 {
3580 unsigned long real_lineno = view->offset + lineno + 1;
3581 char number[10] = " ";
3582 int max = MIN(view->digits, STRING_SIZE(number));
3583 bool showtrimmed = FALSE;
3585 if (real_lineno == 1 ||
3586 (real_lineno % opt_num_interval) == 0) {
3587 char fmt[] = "%1ld";
3589 if (view->digits <= 9)
3590 fmt[1] = '0' + view->digits;
3592 if (!string_format(number, fmt, real_lineno))
3593 number[0] = 0;
3594 showtrimmed = TRUE;
3595 }
3597 if (max > view->width - col)
3598 max = view->width - col;
3599 if (!selected)
3600 wattrset(view->win, get_line_attr(LINE_BLAME_LINENO));
3601 col += draw_text(view, number, max, showtrimmed, tilde_attr);
3602 if (col >= view->width)
3603 return TRUE;
3604 }
3606 if (!selected)
3607 wattrset(view->win, A_NORMAL);
3609 if (col >= view->width)
3610 return TRUE;
3611 waddch(view->win, ACS_VLINE);
3612 col++;
3613 if (col >= view->width)
3614 return TRUE;
3615 waddch(view->win, ' ');
3616 col++;
3617 col += draw_text(view, blame->text, view->width - col, TRUE, tilde_attr);
3619 return TRUE;
3620 }
3622 static enum request
3623 blame_request(struct view *view, enum request request, struct line *line)
3624 {
3625 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3626 struct blame *blame = line->data;
3628 switch (request) {
3629 case REQ_ENTER:
3630 if (!blame->commit) {
3631 report("No commit loaded yet");
3632 break;
3633 }
3635 if (!strcmp(blame->commit->id, "0000000000000000000000000000000000000000")) {
3636 char path[SIZEOF_STR];
3638 if (sq_quote(path, 0, view->vid) >= sizeof(path))
3639 break;
3640 string_format(opt_cmd, "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s 2>/dev/null", path);
3641 }
3643 open_view(view, REQ_VIEW_DIFF, flags);
3644 break;
3646 default:
3647 return request;
3648 }
3650 return REQ_NONE;
3651 }
3653 static bool
3654 blame_grep(struct view *view, struct line *line)
3655 {
3656 struct blame *blame = line->data;
3657 struct blame_commit *commit = blame->commit;
3658 regmatch_t pmatch;
3660 #define MATCH(text) \
3661 (*text && regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3663 if (commit) {
3664 char buf[DATE_COLS + 1];
3666 if (MATCH(commit->title) ||
3667 MATCH(commit->author) ||
3668 MATCH(commit->id))
3669 return TRUE;
3671 if (strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time) &&
3672 MATCH(buf))
3673 return TRUE;
3674 }
3676 return MATCH(blame->text);
3678 #undef MATCH
3679 }
3681 static void
3682 blame_select(struct view *view, struct line *line)
3683 {
3684 struct blame *blame = line->data;
3685 struct blame_commit *commit = blame->commit;
3687 if (!commit)
3688 return;
3690 if (!strcmp(commit->id, "0000000000000000000000000000000000000000"))
3691 string_ncopy(ref_commit, "HEAD", 4);
3692 else
3693 string_copy_rev(ref_commit, commit->id);
3694 }
3696 static struct view_ops blame_ops = {
3697 "line",
3698 blame_open,
3699 blame_read,
3700 blame_draw,
3701 blame_request,
3702 blame_grep,
3703 blame_select,
3704 };
3706 /*
3707 * Status backend
3708 */
3710 struct status {
3711 char status;
3712 struct {
3713 mode_t mode;
3714 char rev[SIZEOF_REV];
3715 char name[SIZEOF_STR];
3716 } old;
3717 struct {
3718 mode_t mode;
3719 char rev[SIZEOF_REV];
3720 char name[SIZEOF_STR];
3721 } new;
3722 };
3724 static struct status stage_status;
3725 static enum line_type stage_line_type;
3727 /* Get fields from the diff line:
3728 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
3729 */
3730 static inline bool
3731 status_get_diff(struct status *file, char *buf, size_t bufsize)
3732 {
3733 char *old_mode = buf + 1;
3734 char *new_mode = buf + 8;
3735 char *old_rev = buf + 15;
3736 char *new_rev = buf + 56;
3737 char *status = buf + 97;
3739 if (bufsize < 99 ||
3740 old_mode[-1] != ':' ||
3741 new_mode[-1] != ' ' ||
3742 old_rev[-1] != ' ' ||
3743 new_rev[-1] != ' ' ||
3744 status[-1] != ' ')
3745 return FALSE;
3747 file->status = *status;
3749 string_copy_rev(file->old.rev, old_rev);
3750 string_copy_rev(file->new.rev, new_rev);
3752 file->old.mode = strtoul(old_mode, NULL, 8);
3753 file->new.mode = strtoul(new_mode, NULL, 8);
3755 file->old.name[0] = file->new.name[0] = 0;
3757 return TRUE;
3758 }
3760 static bool
3761 status_run(struct view *view, const char cmd[], bool diff, enum line_type type)
3762 {
3763 struct status *file = NULL;
3764 struct status *unmerged = NULL;
3765 char buf[SIZEOF_STR * 4];
3766 size_t bufsize = 0;
3767 FILE *pipe;
3769 pipe = popen(cmd, "r");
3770 if (!pipe)
3771 return FALSE;
3773 add_line_data(view, NULL, type);
3775 while (!feof(pipe) && !ferror(pipe)) {
3776 char *sep;
3777 size_t readsize;
3779 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
3780 if (!readsize)
3781 break;
3782 bufsize += readsize;
3784 /* Process while we have NUL chars. */
3785 while ((sep = memchr(buf, 0, bufsize))) {
3786 size_t sepsize = sep - buf + 1;
3788 if (!file) {
3789 if (!realloc_lines(view, view->line_size + 1))
3790 goto error_out;
3792 file = calloc(1, sizeof(*file));
3793 if (!file)
3794 goto error_out;
3796 add_line_data(view, file, type);
3797 }
3799 /* Parse diff info part. */
3800 if (!diff) {
3801 file->status = '?';
3803 } else if (!file->status) {
3804 if (!status_get_diff(file, buf, sepsize))
3805 goto error_out;
3807 bufsize -= sepsize;
3808 memmove(buf, sep + 1, bufsize);
3810 sep = memchr(buf, 0, bufsize);
3811 if (!sep)
3812 break;
3813 sepsize = sep - buf + 1;
3815 /* Collapse all 'M'odified entries that
3816 * follow a associated 'U'nmerged entry.
3817 */
3818 if (file->status == 'U') {
3819 unmerged = file;
3821 } else if (unmerged) {
3822 int collapse = !strcmp(buf, unmerged->new.name);
3824 unmerged = NULL;
3825 if (collapse) {
3826 free(file);
3827 view->lines--;
3828 continue;
3829 }
3830 }
3831 }
3833 /* Grab the old name for rename/copy. */
3834 if (!*file->old.name &&
3835 (file->status == 'R' || file->status == 'C')) {
3836 sepsize = sep - buf + 1;
3837 string_ncopy(file->old.name, buf, sepsize);
3838 bufsize -= sepsize;
3839 memmove(buf, sep + 1, bufsize);
3841 sep = memchr(buf, 0, bufsize);
3842 if (!sep)
3843 break;
3844 sepsize = sep - buf + 1;
3845 }
3847 /* git-ls-files just delivers a NUL separated
3848 * list of file names similar to the second half
3849 * of the git-diff-* output. */
3850 string_ncopy(file->new.name, buf, sepsize);
3851 if (!*file->old.name)
3852 string_copy(file->old.name, file->new.name);
3853 bufsize -= sepsize;
3854 memmove(buf, sep + 1, bufsize);
3855 file = NULL;
3856 }
3857 }
3859 if (ferror(pipe)) {
3860 error_out:
3861 pclose(pipe);
3862 return FALSE;
3863 }
3865 if (!view->line[view->lines - 1].data)
3866 add_line_data(view, NULL, LINE_STAT_NONE);
3868 pclose(pipe);
3869 return TRUE;
3870 }
3872 /* Don't show unmerged entries in the staged section. */
3873 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached -M HEAD"
3874 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
3875 #define STATUS_LIST_OTHER_CMD \
3876 "git ls-files -z --others --exclude-per-directory=.gitignore"
3878 #define STATUS_DIFF_INDEX_SHOW_CMD \
3879 "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s %s 2>/dev/null"
3881 #define STATUS_DIFF_FILES_SHOW_CMD \
3882 "git diff-files --root --patch-with-stat -C -M -- %s %s 2>/dev/null"
3884 /* First parse staged info using git-diff-index(1), then parse unstaged
3885 * info using git-diff-files(1), and finally untracked files using
3886 * git-ls-files(1). */
3887 static bool
3888 status_open(struct view *view)
3889 {
3890 struct stat statbuf;
3891 char exclude[SIZEOF_STR];
3892 char cmd[SIZEOF_STR];
3893 unsigned long prev_lineno = view->lineno;
3894 size_t i;
3896 for (i = 0; i < view->lines; i++)
3897 free(view->line[i].data);
3898 free(view->line);
3899 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3900 view->line = NULL;
3902 if (!realloc_lines(view, view->line_size + 6))
3903 return FALSE;
3905 if (!string_format(exclude, "%s/info/exclude", opt_git_dir))
3906 return FALSE;
3908 string_copy(cmd, STATUS_LIST_OTHER_CMD);
3910 if (stat(exclude, &statbuf) >= 0) {
3911 size_t cmdsize = strlen(cmd);
3913 if (!string_format_from(cmd, &cmdsize, " %s", "--exclude-from=") ||
3914 sq_quote(cmd, cmdsize, exclude) >= sizeof(cmd))
3915 return FALSE;
3916 }
3918 system("git update-index -q --refresh");
3920 if (!status_run(view, STATUS_DIFF_INDEX_CMD, TRUE, LINE_STAT_STAGED) ||
3921 !status_run(view, STATUS_DIFF_FILES_CMD, TRUE, LINE_STAT_UNSTAGED) ||
3922 !status_run(view, cmd, FALSE, LINE_STAT_UNTRACKED))
3923 return FALSE;
3925 /* If all went well restore the previous line number to stay in
3926 * the context. */
3927 if (prev_lineno < view->lines)
3928 view->lineno = prev_lineno;
3929 else
3930 view->lineno = view->lines - 1;
3932 return TRUE;
3933 }
3935 static bool
3936 status_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
3937 {
3938 struct status *status = line->data;
3939 int tilde_attr = get_line_attr(LINE_MAIN_DELIM);
3941 wmove(view->win, lineno, 0);
3943 if (selected) {
3944 wattrset(view->win, get_line_attr(LINE_CURSOR));
3945 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
3946 tilde_attr = -1;
3948 } else if (!status && line->type != LINE_STAT_NONE) {
3949 wattrset(view->win, get_line_attr(LINE_STAT_SECTION));
3950 wchgat(view->win, -1, 0, LINE_STAT_SECTION, NULL);
3952 } else {
3953 wattrset(view->win, get_line_attr(line->type));
3954 }
3956 if (!status) {
3957 char *text;
3959 switch (line->type) {
3960 case LINE_STAT_STAGED:
3961 text = "Changes to be committed:";
3962 break;
3964 case LINE_STAT_UNSTAGED:
3965 text = "Changed but not updated:";
3966 break;
3968 case LINE_STAT_UNTRACKED:
3969 text = "Untracked files:";
3970 break;
3972 case LINE_STAT_NONE:
3973 text = " (no files)";
3974 break;
3976 default:
3977 return FALSE;
3978 }
3980 draw_text(view, text, view->width, TRUE, tilde_attr);
3981 return TRUE;
3982 }
3984 waddch(view->win, status->status);
3985 if (!selected)
3986 wattrset(view->win, A_NORMAL);
3987 wmove(view->win, lineno, 4);
3988 if (view->width < 5)
3989 return TRUE;
3991 draw_text(view, status->new.name, view->width - 5, TRUE, tilde_attr);
3992 return TRUE;
3993 }
3995 static enum request
3996 status_enter(struct view *view, struct line *line)
3997 {
3998 struct status *status = line->data;
3999 char oldpath[SIZEOF_STR] = "";
4000 char newpath[SIZEOF_STR] = "";
4001 char *info;
4002 size_t cmdsize = 0;
4004 if (line->type == LINE_STAT_NONE ||
4005 (!status && line[1].type == LINE_STAT_NONE)) {
4006 report("No file to diff");
4007 return REQ_NONE;
4008 }
4010 if (status) {
4011 if (sq_quote(oldpath, 0, status->old.name) >= sizeof(oldpath))
4012 return REQ_QUIT;
4013 /* Diffs for unmerged entries are empty when pasing the
4014 * new path, so leave it empty. */
4015 if (status->status != 'U' &&
4016 sq_quote(newpath, 0, status->new.name) >= sizeof(newpath))
4017 return REQ_QUIT;
4018 }
4020 if (opt_cdup[0] &&
4021 line->type != LINE_STAT_UNTRACKED &&
4022 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
4023 return REQ_QUIT;
4025 switch (line->type) {
4026 case LINE_STAT_STAGED:
4027 if (!string_format_from(opt_cmd, &cmdsize,
4028 STATUS_DIFF_INDEX_SHOW_CMD, oldpath, newpath))
4029 return REQ_QUIT;
4030 if (status)
4031 info = "Staged changes to %s";
4032 else
4033 info = "Staged changes";
4034 break;
4036 case LINE_STAT_UNSTAGED:
4037 if (!string_format_from(opt_cmd, &cmdsize,
4038 STATUS_DIFF_FILES_SHOW_CMD, oldpath, newpath))
4039 return REQ_QUIT;
4040 if (status)
4041 info = "Unstaged changes to %s";
4042 else
4043 info = "Unstaged changes";
4044 break;
4046 case LINE_STAT_UNTRACKED:
4047 if (opt_pipe)
4048 return REQ_QUIT;
4051 if (!status) {
4052 report("No file to show");
4053 return REQ_NONE;
4054 }
4056 opt_pipe = fopen(status->new.name, "r");
4057 info = "Untracked file %s";
4058 break;
4060 default:
4061 die("line type %d not handled in switch", line->type);
4062 }
4064 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | OPEN_SPLIT);
4065 if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) {
4066 if (status) {
4067 stage_status = *status;
4068 } else {
4069 memset(&stage_status, 0, sizeof(stage_status));
4070 }
4072 stage_line_type = line->type;
4073 string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.new.name);
4074 }
4076 return REQ_NONE;
4077 }
4080 static bool
4081 status_update_file(struct view *view, struct status *status, enum line_type type)
4082 {
4083 char cmd[SIZEOF_STR];
4084 char buf[SIZEOF_STR];
4085 size_t cmdsize = 0;
4086 size_t bufsize = 0;
4087 size_t written = 0;
4088 FILE *pipe;
4090 if (opt_cdup[0] &&
4091 type != LINE_STAT_UNTRACKED &&
4092 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4093 return FALSE;
4095 switch (type) {
4096 case LINE_STAT_STAGED:
4097 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
4098 status->old.mode,
4099 status->old.rev,
4100 status->old.name, 0))
4101 return FALSE;
4103 string_add(cmd, cmdsize, "git update-index -z --index-info");
4104 break;
4106 case LINE_STAT_UNSTAGED:
4107 case LINE_STAT_UNTRACKED:
4108 if (!string_format_from(buf, &bufsize, "%s%c", status->new.name, 0))
4109 return FALSE;
4111 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
4112 break;
4114 default:
4115 die("line type %d not handled in switch", type);
4116 }
4118 pipe = popen(cmd, "w");
4119 if (!pipe)
4120 return FALSE;
4122 while (!ferror(pipe) && written < bufsize) {
4123 written += fwrite(buf + written, 1, bufsize - written, pipe);
4124 }
4126 pclose(pipe);
4128 if (written != bufsize)
4129 return FALSE;
4131 return TRUE;
4132 }
4134 static void
4135 status_update(struct view *view)
4136 {
4137 struct line *line = &view->line[view->lineno];
4139 assert(view->lines);
4141 if (!line->data) {
4142 while (++line < view->line + view->lines && line->data) {
4143 if (!status_update_file(view, line->data, line->type))
4144 report("Failed to update file status");
4145 }
4147 if (!line[-1].data) {
4148 report("Nothing to update");
4149 return;
4150 }
4152 } else if (!status_update_file(view, line->data, line->type)) {
4153 report("Failed to update file status");
4154 }
4155 }
4157 static enum request
4158 status_request(struct view *view, enum request request, struct line *line)
4159 {
4160 struct status *status = line->data;
4162 switch (request) {
4163 case REQ_STATUS_UPDATE:
4164 status_update(view);
4165 break;
4167 case REQ_STATUS_MERGE:
4168 if (!status || status->status != 'U') {
4169 report("Merging only possible for files with unmerged status ('U').");
4170 return REQ_NONE;
4171 }
4172 open_mergetool(status->new.name);
4173 break;
4175 case REQ_EDIT:
4176 if (!status)
4177 return request;
4179 open_editor(status->status != '?', status->new.name);
4180 break;
4182 case REQ_VIEW_BLAME:
4183 if (status) {
4184 string_copy(opt_file, status->new.name);
4185 opt_ref[0] = 0;
4186 }
4187 return request;
4189 case REQ_ENTER:
4190 /* After returning the status view has been split to
4191 * show the stage view. No further reloading is
4192 * necessary. */
4193 status_enter(view, line);
4194 return REQ_NONE;
4196 case REQ_REFRESH:
4197 /* Simply reload the view. */
4198 break;
4200 default:
4201 return request;
4202 }
4204 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
4206 return REQ_NONE;
4207 }
4209 static void
4210 status_select(struct view *view, struct line *line)
4211 {
4212 struct status *status = line->data;
4213 char file[SIZEOF_STR] = "all files";
4214 char *text;
4215 char *key;
4217 if (status && !string_format(file, "'%s'", status->new.name))
4218 return;
4220 if (!status && line[1].type == LINE_STAT_NONE)
4221 line++;
4223 switch (line->type) {
4224 case LINE_STAT_STAGED:
4225 text = "Press %s to unstage %s for commit";
4226 break;
4228 case LINE_STAT_UNSTAGED:
4229 text = "Press %s to stage %s for commit";
4230 break;
4232 case LINE_STAT_UNTRACKED:
4233 text = "Press %s to stage %s for addition";
4234 break;
4236 case LINE_STAT_NONE:
4237 text = "Nothing to update";
4238 break;
4240 default:
4241 die("line type %d not handled in switch", line->type);
4242 }
4244 if (status && status->status == 'U') {
4245 text = "Press %s to resolve conflict in %s";
4246 key = get_key(REQ_STATUS_MERGE);
4248 } else {
4249 key = get_key(REQ_STATUS_UPDATE);
4250 }
4252 string_format(view->ref, text, key, file);
4253 }
4255 static bool
4256 status_grep(struct view *view, struct line *line)
4257 {
4258 struct status *status = line->data;
4259 enum { S_STATUS, S_NAME, S_END } state;
4260 char buf[2] = "?";
4261 regmatch_t pmatch;
4263 if (!status)
4264 return FALSE;
4266 for (state = S_STATUS; state < S_END; state++) {
4267 char *text;
4269 switch (state) {
4270 case S_NAME: text = status->new.name; break;
4271 case S_STATUS:
4272 buf[0] = status->status;
4273 text = buf;
4274 break;
4276 default:
4277 return FALSE;
4278 }
4280 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4281 return TRUE;
4282 }
4284 return FALSE;
4285 }
4287 static struct view_ops status_ops = {
4288 "file",
4289 status_open,
4290 NULL,
4291 status_draw,
4292 status_request,
4293 status_grep,
4294 status_select,
4295 };
4298 static bool
4299 stage_diff_line(FILE *pipe, struct line *line)
4300 {
4301 char *buf = line->data;
4302 size_t bufsize = strlen(buf);
4303 size_t written = 0;
4305 while (!ferror(pipe) && written < bufsize) {
4306 written += fwrite(buf + written, 1, bufsize - written, pipe);
4307 }
4309 fputc('\n', pipe);
4311 return written == bufsize;
4312 }
4314 static struct line *
4315 stage_diff_hdr(struct view *view, struct line *line)
4316 {
4317 int diff_hdr_dir = line->type == LINE_DIFF_CHUNK ? -1 : 1;
4318 struct line *diff_hdr;
4320 if (line->type == LINE_DIFF_CHUNK)
4321 diff_hdr = line - 1;
4322 else
4323 diff_hdr = view->line + 1;
4325 while (diff_hdr > view->line && diff_hdr < view->line + view->lines) {
4326 if (diff_hdr->type == LINE_DIFF_HEADER)
4327 return diff_hdr;
4329 diff_hdr += diff_hdr_dir;
4330 }
4332 return NULL;
4333 }
4335 static bool
4336 stage_update_chunk(struct view *view, struct line *line)
4337 {
4338 char cmd[SIZEOF_STR];
4339 size_t cmdsize = 0;
4340 struct line *diff_hdr, *diff_chunk, *diff_end;
4341 FILE *pipe;
4343 diff_hdr = stage_diff_hdr(view, line);
4344 if (!diff_hdr)
4345 return FALSE;
4347 if (opt_cdup[0] &&
4348 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4349 return FALSE;
4351 if (!string_format_from(cmd, &cmdsize,
4352 "git apply --cached %s - && "
4353 "git update-index -q --unmerged --refresh 2>/dev/null",
4354 stage_line_type == LINE_STAT_STAGED ? "-R" : ""))
4355 return FALSE;
4357 pipe = popen(cmd, "w");
4358 if (!pipe)
4359 return FALSE;
4361 diff_end = view->line + view->lines;
4362 if (line->type != LINE_DIFF_CHUNK) {
4363 diff_chunk = diff_hdr;
4365 } else {
4366 for (diff_chunk = line + 1; diff_chunk < diff_end; diff_chunk++)
4367 if (diff_chunk->type == LINE_DIFF_CHUNK ||
4368 diff_chunk->type == LINE_DIFF_HEADER)
4369 diff_end = diff_chunk;
4371 diff_chunk = line;
4373 while (diff_hdr->type != LINE_DIFF_CHUNK) {
4374 switch (diff_hdr->type) {
4375 case LINE_DIFF_HEADER:
4376 case LINE_DIFF_INDEX:
4377 case LINE_DIFF_ADD:
4378 case LINE_DIFF_DEL:
4379 break;
4381 default:
4382 diff_hdr++;
4383 continue;
4384 }
4386 if (!stage_diff_line(pipe, diff_hdr++)) {
4387 pclose(pipe);
4388 return FALSE;
4389 }
4390 }
4391 }
4393 while (diff_chunk < diff_end && stage_diff_line(pipe, diff_chunk))
4394 diff_chunk++;
4396 pclose(pipe);
4398 if (diff_chunk != diff_end)
4399 return FALSE;
4401 return TRUE;
4402 }
4404 static void
4405 stage_update(struct view *view, struct line *line)
4406 {
4407 if (stage_line_type != LINE_STAT_UNTRACKED &&
4408 (line->type == LINE_DIFF_CHUNK || !stage_status.status)) {
4409 if (!stage_update_chunk(view, line)) {
4410 report("Failed to apply chunk");
4411 return;
4412 }
4414 } else if (!status_update_file(view, &stage_status, stage_line_type)) {
4415 report("Failed to update file");
4416 return;
4417 }
4419 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
4421 view = VIEW(REQ_VIEW_STATUS);
4422 if (view_is_displayed(view))
4423 status_enter(view, &view->line[view->lineno]);
4424 }
4426 static enum request
4427 stage_request(struct view *view, enum request request, struct line *line)
4428 {
4429 switch (request) {
4430 case REQ_STATUS_UPDATE:
4431 stage_update(view, line);
4432 break;
4434 case REQ_EDIT:
4435 if (!stage_status.new.name[0])
4436 return request;
4438 open_editor(stage_status.status != '?', stage_status.new.name);
4439 break;
4441 case REQ_VIEW_BLAME:
4442 if (stage_status.new.name[0]) {
4443 string_copy(opt_file, stage_status.new.name);
4444 opt_ref[0] = 0;
4445 }
4446 return request;
4448 case REQ_ENTER:
4449 pager_request(view, request, line);
4450 break;
4452 default:
4453 return request;
4454 }
4456 return REQ_NONE;
4457 }
4459 static struct view_ops stage_ops = {
4460 "line",
4461 NULL,
4462 pager_read,
4463 pager_draw,
4464 stage_request,
4465 pager_grep,
4466 pager_select,
4467 };
4470 /*
4471 * Revision graph
4472 */
4474 struct commit {
4475 char id[SIZEOF_REV]; /* SHA1 ID. */
4476 char title[128]; /* First line of the commit message. */
4477 char author[75]; /* Author of the commit. */
4478 struct tm time; /* Date from the author ident. */
4479 struct ref **refs; /* Repository references. */
4480 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
4481 size_t graph_size; /* The width of the graph array. */
4482 bool has_parents; /* Rewritten --parents seen. */
4483 };
4485 /* Size of rev graph with no "padding" columns */
4486 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
4488 struct rev_graph {
4489 struct rev_graph *prev, *next, *parents;
4490 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
4491 size_t size;
4492 struct commit *commit;
4493 size_t pos;
4494 unsigned int boundary:1;
4495 };
4497 /* Parents of the commit being visualized. */
4498 static struct rev_graph graph_parents[4];
4500 /* The current stack of revisions on the graph. */
4501 static struct rev_graph graph_stacks[4] = {
4502 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
4503 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
4504 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
4505 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
4506 };
4508 static inline bool
4509 graph_parent_is_merge(struct rev_graph *graph)
4510 {
4511 return graph->parents->size > 1;
4512 }
4514 static inline void
4515 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
4516 {
4517 struct commit *commit = graph->commit;
4519 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
4520 commit->graph[commit->graph_size++] = symbol;
4521 }
4523 static void
4524 done_rev_graph(struct rev_graph *graph)
4525 {
4526 if (graph_parent_is_merge(graph) &&
4527 graph->pos < graph->size - 1 &&
4528 graph->next->size == graph->size + graph->parents->size - 1) {
4529 size_t i = graph->pos + graph->parents->size - 1;
4531 graph->commit->graph_size = i * 2;
4532 while (i < graph->next->size - 1) {
4533 append_to_rev_graph(graph, ' ');
4534 append_to_rev_graph(graph, '\\');
4535 i++;
4536 }
4537 }
4539 graph->size = graph->pos = 0;
4540 graph->commit = NULL;
4541 memset(graph->parents, 0, sizeof(*graph->parents));
4542 }
4544 static void
4545 push_rev_graph(struct rev_graph *graph, char *parent)
4546 {
4547 int i;
4549 /* "Collapse" duplicate parents lines.
4550 *
4551 * FIXME: This needs to also update update the drawn graph but
4552 * for now it just serves as a method for pruning graph lines. */
4553 for (i = 0; i < graph->size; i++)
4554 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
4555 return;
4557 if (graph->size < SIZEOF_REVITEMS) {
4558 string_copy_rev(graph->rev[graph->size++], parent);
4559 }
4560 }
4562 static chtype
4563 get_rev_graph_symbol(struct rev_graph *graph)
4564 {
4565 chtype symbol;
4567 if (graph->boundary)
4568 symbol = REVGRAPH_BOUND;
4569 else if (graph->parents->size == 0)
4570 symbol = REVGRAPH_INIT;
4571 else if (graph_parent_is_merge(graph))
4572 symbol = REVGRAPH_MERGE;
4573 else if (graph->pos >= graph->size)
4574 symbol = REVGRAPH_BRANCH;
4575 else
4576 symbol = REVGRAPH_COMMIT;
4578 return symbol;
4579 }
4581 static void
4582 draw_rev_graph(struct rev_graph *graph)
4583 {
4584 struct rev_filler {
4585 chtype separator, line;
4586 };
4587 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
4588 static struct rev_filler fillers[] = {
4589 { ' ', REVGRAPH_LINE },
4590 { '`', '.' },
4591 { '\'', ' ' },
4592 { '/', ' ' },
4593 };
4594 chtype symbol = get_rev_graph_symbol(graph);
4595 struct rev_filler *filler;
4596 size_t i;
4598 filler = &fillers[DEFAULT];
4600 for (i = 0; i < graph->pos; i++) {
4601 append_to_rev_graph(graph, filler->line);
4602 if (graph_parent_is_merge(graph->prev) &&
4603 graph->prev->pos == i)
4604 filler = &fillers[RSHARP];
4606 append_to_rev_graph(graph, filler->separator);
4607 }
4609 /* Place the symbol for this revision. */
4610 append_to_rev_graph(graph, symbol);
4612 if (graph->prev->size > graph->size)
4613 filler = &fillers[RDIAG];
4614 else
4615 filler = &fillers[DEFAULT];
4617 i++;
4619 for (; i < graph->size; i++) {
4620 append_to_rev_graph(graph, filler->separator);
4621 append_to_rev_graph(graph, filler->line);
4622 if (graph_parent_is_merge(graph->prev) &&
4623 i < graph->prev->pos + graph->parents->size)
4624 filler = &fillers[RSHARP];
4625 if (graph->prev->size > graph->size)
4626 filler = &fillers[LDIAG];
4627 }
4629 if (graph->prev->size > graph->size) {
4630 append_to_rev_graph(graph, filler->separator);
4631 if (filler->line != ' ')
4632 append_to_rev_graph(graph, filler->line);
4633 }
4634 }
4636 /* Prepare the next rev graph */
4637 static void
4638 prepare_rev_graph(struct rev_graph *graph)
4639 {
4640 size_t i;
4642 /* First, traverse all lines of revisions up to the active one. */
4643 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
4644 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
4645 break;
4647 push_rev_graph(graph->next, graph->rev[graph->pos]);
4648 }
4650 /* Interleave the new revision parent(s). */
4651 for (i = 0; !graph->boundary && i < graph->parents->size; i++)
4652 push_rev_graph(graph->next, graph->parents->rev[i]);
4654 /* Lastly, put any remaining revisions. */
4655 for (i = graph->pos + 1; i < graph->size; i++)
4656 push_rev_graph(graph->next, graph->rev[i]);
4657 }
4659 static void
4660 update_rev_graph(struct rev_graph *graph)
4661 {
4662 /* If this is the finalizing update ... */
4663 if (graph->commit)
4664 prepare_rev_graph(graph);
4666 /* Graph visualization needs a one rev look-ahead,
4667 * so the first update doesn't visualize anything. */
4668 if (!graph->prev->commit)
4669 return;
4671 draw_rev_graph(graph->prev);
4672 done_rev_graph(graph->prev->prev);
4673 }
4676 /*
4677 * Main view backend
4678 */
4680 static bool
4681 main_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
4682 {
4683 char buf[DATE_COLS + 1];
4684 struct commit *commit = line->data;
4685 enum line_type type;
4686 int col = 0;
4687 size_t timelen;
4688 int tilde_attr;
4689 int space;
4691 if (!*commit->author)
4692 return FALSE;
4694 space = view->width;
4695 wmove(view->win, lineno, col);
4697 if (selected) {
4698 type = LINE_CURSOR;
4699 wattrset(view->win, get_line_attr(type));
4700 wchgat(view->win, -1, 0, type, NULL);
4701 tilde_attr = -1;
4702 } else {
4703 type = LINE_MAIN_COMMIT;
4704 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
4705 tilde_attr = get_line_attr(LINE_MAIN_DELIM);
4706 }
4708 if (opt_date) {
4709 int n;
4711 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
4712 n = draw_text(view, buf, view->width - col, FALSE, tilde_attr);
4713 draw_text(view, " ", view->width - col - n, FALSE, tilde_attr);
4715 col += DATE_COLS;
4716 wmove(view->win, lineno, col);
4717 if (col >= view->width)
4718 return TRUE;
4719 }
4720 if (type != LINE_CURSOR)
4721 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
4723 if (opt_author) {
4724 int max_len;
4726 max_len = view->width - col;
4727 if (max_len > AUTHOR_COLS - 1)
4728 max_len = AUTHOR_COLS - 1;
4729 draw_text(view, commit->author, max_len, TRUE, tilde_attr);
4730 col += AUTHOR_COLS;
4731 if (col >= view->width)
4732 return TRUE;
4733 }
4735 if (opt_rev_graph && commit->graph_size) {
4736 size_t graph_size = view->width - col;
4737 size_t i;
4739 if (type != LINE_CURSOR)
4740 wattrset(view->win, get_line_attr(LINE_MAIN_REVGRAPH));
4741 wmove(view->win, lineno, col);
4742 if (graph_size > commit->graph_size)
4743 graph_size = commit->graph_size;
4744 /* Using waddch() instead of waddnstr() ensures that
4745 * they'll be rendered correctly for the cursor line. */
4746 for (i = 0; i < graph_size; i++)
4747 waddch(view->win, commit->graph[i]);
4749 col += commit->graph_size + 1;
4750 if (col >= view->width)
4751 return TRUE;
4752 waddch(view->win, ' ');
4753 }
4754 if (type != LINE_CURSOR)
4755 wattrset(view->win, A_NORMAL);
4757 wmove(view->win, lineno, col);
4759 if (opt_show_refs && commit->refs) {
4760 size_t i = 0;
4762 do {
4763 if (type == LINE_CURSOR)
4764 ;
4765 else if (commit->refs[i]->ltag)
4766 wattrset(view->win, get_line_attr(LINE_MAIN_LOCAL_TAG));
4767 else if (commit->refs[i]->tag)
4768 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
4769 else if (commit->refs[i]->remote)
4770 wattrset(view->win, get_line_attr(LINE_MAIN_REMOTE));
4771 else
4772 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
4774 col += draw_text(view, "[", view->width - col, TRUE, tilde_attr);
4775 col += draw_text(view, commit->refs[i]->name, view->width - col,
4776 TRUE, tilde_attr);
4777 col += draw_text(view, "]", view->width - col, TRUE, tilde_attr);
4778 if (type != LINE_CURSOR)
4779 wattrset(view->win, A_NORMAL);
4780 col += draw_text(view, " ", view->width - col, TRUE, tilde_attr);
4781 if (col >= view->width)
4782 return TRUE;
4783 } while (commit->refs[i++]->next);
4784 }
4786 if (type != LINE_CURSOR)
4787 wattrset(view->win, get_line_attr(type));
4789 draw_text(view, commit->title, view->width - col, TRUE, tilde_attr);
4790 return TRUE;
4791 }
4793 /* Reads git log --pretty=raw output and parses it into the commit struct. */
4794 static bool
4795 main_read(struct view *view, char *line)
4796 {
4797 static struct rev_graph *graph = graph_stacks;
4798 enum line_type type;
4799 struct commit *commit;
4801 if (!line) {
4802 update_rev_graph(graph);
4803 return TRUE;
4804 }
4806 type = get_line_type(line);
4807 if (type == LINE_COMMIT) {
4808 commit = calloc(1, sizeof(struct commit));
4809 if (!commit)
4810 return FALSE;
4812 line += STRING_SIZE("commit ");
4813 if (*line == '-') {
4814 graph->boundary = 1;
4815 line++;
4816 }
4818 string_copy_rev(commit->id, line);
4819 commit->refs = get_refs(commit->id);
4820 graph->commit = commit;
4821 add_line_data(view, commit, LINE_MAIN_COMMIT);
4823 while ((line = strchr(line, ' '))) {
4824 line++;
4825 push_rev_graph(graph->parents, line);
4826 commit->has_parents = TRUE;
4827 }
4828 return TRUE;
4829 }
4831 if (!view->lines)
4832 return TRUE;
4833 commit = view->line[view->lines - 1].data;
4835 switch (type) {
4836 case LINE_PARENT:
4837 if (commit->has_parents)
4838 break;
4839 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
4840 break;
4842 case LINE_AUTHOR:
4843 {
4844 /* Parse author lines where the name may be empty:
4845 * author <email@address.tld> 1138474660 +0100
4846 */
4847 char *ident = line + STRING_SIZE("author ");
4848 char *nameend = strchr(ident, '<');
4849 char *emailend = strchr(ident, '>');
4851 if (!nameend || !emailend)
4852 break;
4854 update_rev_graph(graph);
4855 graph = graph->next;
4857 *nameend = *emailend = 0;
4858 ident = chomp_string(ident);
4859 if (!*ident) {
4860 ident = chomp_string(nameend + 1);
4861 if (!*ident)
4862 ident = "Unknown";
4863 }
4865 string_ncopy(commit->author, ident, strlen(ident));
4867 /* Parse epoch and timezone */
4868 if (emailend[1] == ' ') {
4869 char *secs = emailend + 2;
4870 char *zone = strchr(secs, ' ');
4871 time_t time = (time_t) atol(secs);
4873 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
4874 long tz;
4876 zone++;
4877 tz = ('0' - zone[1]) * 60 * 60 * 10;
4878 tz += ('0' - zone[2]) * 60 * 60;
4879 tz += ('0' - zone[3]) * 60;
4880 tz += ('0' - zone[4]) * 60;
4882 if (zone[0] == '-')
4883 tz = -tz;
4885 time -= tz;
4886 }
4888 gmtime_r(&time, &commit->time);
4889 }
4890 break;
4891 }
4892 default:
4893 /* Fill in the commit title if it has not already been set. */
4894 if (commit->title[0])
4895 break;
4897 /* Require titles to start with a non-space character at the
4898 * offset used by git log. */
4899 if (strncmp(line, " ", 4))
4900 break;
4901 line += 4;
4902 /* Well, if the title starts with a whitespace character,
4903 * try to be forgiving. Otherwise we end up with no title. */
4904 while (isspace(*line))
4905 line++;
4906 if (*line == '\0')
4907 break;
4908 /* FIXME: More graceful handling of titles; append "..." to
4909 * shortened titles, etc. */
4911 string_ncopy(commit->title, line, strlen(line));
4912 }
4914 return TRUE;
4915 }
4917 static enum request
4918 main_request(struct view *view, enum request request, struct line *line)
4919 {
4920 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
4922 if (request == REQ_ENTER)
4923 open_view(view, REQ_VIEW_DIFF, flags);
4924 else
4925 return request;
4927 return REQ_NONE;
4928 }
4930 static bool
4931 main_grep(struct view *view, struct line *line)
4932 {
4933 struct commit *commit = line->data;
4934 enum { S_TITLE, S_AUTHOR, S_DATE, S_END } state;
4935 char buf[DATE_COLS + 1];
4936 regmatch_t pmatch;
4938 for (state = S_TITLE; state < S_END; state++) {
4939 char *text;
4941 switch (state) {
4942 case S_TITLE: text = commit->title; break;
4943 case S_AUTHOR: text = commit->author; break;
4944 case S_DATE:
4945 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
4946 continue;
4947 text = buf;
4948 break;
4950 default:
4951 return FALSE;
4952 }
4954 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4955 return TRUE;
4956 }
4958 return FALSE;
4959 }
4961 static void
4962 main_select(struct view *view, struct line *line)
4963 {
4964 struct commit *commit = line->data;
4966 string_copy_rev(view->ref, commit->id);
4967 string_copy_rev(ref_commit, view->ref);
4968 }
4970 static struct view_ops main_ops = {
4971 "commit",
4972 NULL,
4973 main_read,
4974 main_draw,
4975 main_request,
4976 main_grep,
4977 main_select,
4978 };
4981 /*
4982 * Unicode / UTF-8 handling
4983 *
4984 * NOTE: Much of the following code for dealing with unicode is derived from
4985 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
4986 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
4987 */
4989 /* I've (over)annotated a lot of code snippets because I am not entirely
4990 * confident that the approach taken by this small UTF-8 interface is correct.
4991 * --jonas */
4993 static inline int
4994 unicode_width(unsigned long c)
4995 {
4996 if (c >= 0x1100 &&
4997 (c <= 0x115f /* Hangul Jamo */
4998 || c == 0x2329
4999 || c == 0x232a
5000 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
5001 /* CJK ... Yi */
5002 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
5003 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
5004 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
5005 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
5006 || (c >= 0xffe0 && c <= 0xffe6)
5007 || (c >= 0x20000 && c <= 0x2fffd)
5008 || (c >= 0x30000 && c <= 0x3fffd)))
5009 return 2;
5011 if (c == '\t')
5012 return opt_tab_size;
5014 return 1;
5015 }
5017 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
5018 * Illegal bytes are set one. */
5019 static const unsigned char utf8_bytes[256] = {
5020 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,
5021 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,
5022 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,
5023 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,
5024 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,
5025 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,
5026 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,
5027 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,
5028 };
5030 /* Decode UTF-8 multi-byte representation into a unicode character. */
5031 static inline unsigned long
5032 utf8_to_unicode(const char *string, size_t length)
5033 {
5034 unsigned long unicode;
5036 switch (length) {
5037 case 1:
5038 unicode = string[0];
5039 break;
5040 case 2:
5041 unicode = (string[0] & 0x1f) << 6;
5042 unicode += (string[1] & 0x3f);
5043 break;
5044 case 3:
5045 unicode = (string[0] & 0x0f) << 12;
5046 unicode += ((string[1] & 0x3f) << 6);
5047 unicode += (string[2] & 0x3f);
5048 break;
5049 case 4:
5050 unicode = (string[0] & 0x0f) << 18;
5051 unicode += ((string[1] & 0x3f) << 12);
5052 unicode += ((string[2] & 0x3f) << 6);
5053 unicode += (string[3] & 0x3f);
5054 break;
5055 case 5:
5056 unicode = (string[0] & 0x0f) << 24;
5057 unicode += ((string[1] & 0x3f) << 18);
5058 unicode += ((string[2] & 0x3f) << 12);
5059 unicode += ((string[3] & 0x3f) << 6);
5060 unicode += (string[4] & 0x3f);
5061 break;
5062 case 6:
5063 unicode = (string[0] & 0x01) << 30;
5064 unicode += ((string[1] & 0x3f) << 24);
5065 unicode += ((string[2] & 0x3f) << 18);
5066 unicode += ((string[3] & 0x3f) << 12);
5067 unicode += ((string[4] & 0x3f) << 6);
5068 unicode += (string[5] & 0x3f);
5069 break;
5070 default:
5071 die("Invalid unicode length");
5072 }
5074 /* Invalid characters could return the special 0xfffd value but NUL
5075 * should be just as good. */
5076 return unicode > 0xffff ? 0 : unicode;
5077 }
5079 /* Calculates how much of string can be shown within the given maximum width
5080 * and sets trimmed parameter to non-zero value if all of string could not be
5081 * shown. If the reserve flag is TRUE, it will reserve at least one
5082 * trailing character, which can be useful when drawing a delimiter.
5083 *
5084 * Returns the number of bytes to output from string to satisfy max_width. */
5085 static size_t
5086 utf8_length(const char *string, size_t max_width, int *trimmed, bool reserve)
5087 {
5088 const char *start = string;
5089 const char *end = strchr(string, '\0');
5090 unsigned char last_bytes = 0;
5091 size_t width = 0;
5093 *trimmed = 0;
5095 while (string < end) {
5096 int c = *(unsigned char *) string;
5097 unsigned char bytes = utf8_bytes[c];
5098 size_t ucwidth;
5099 unsigned long unicode;
5101 if (string + bytes > end)
5102 break;
5104 /* Change representation to figure out whether
5105 * it is a single- or double-width character. */
5107 unicode = utf8_to_unicode(string, bytes);
5108 /* FIXME: Graceful handling of invalid unicode character. */
5109 if (!unicode)
5110 break;
5112 ucwidth = unicode_width(unicode);
5113 width += ucwidth;
5114 if (width > max_width) {
5115 *trimmed = 1;
5116 if (reserve && width - ucwidth == max_width) {
5117 string -= last_bytes;
5118 }
5119 break;
5120 }
5122 string += bytes;
5123 last_bytes = bytes;
5124 }
5126 return string - start;
5127 }
5130 /*
5131 * Status management
5132 */
5134 /* Whether or not the curses interface has been initialized. */
5135 static bool cursed = FALSE;
5137 /* The status window is used for polling keystrokes. */
5138 static WINDOW *status_win;
5140 static bool status_empty = TRUE;
5142 /* Update status and title window. */
5143 static void
5144 report(const char *msg, ...)
5145 {
5146 struct view *view = display[current_view];
5148 if (input_mode)
5149 return;
5151 if (!view) {
5152 char buf[SIZEOF_STR];
5153 va_list args;
5155 va_start(args, msg);
5156 if (vsnprintf(buf, sizeof(buf), msg, args) >= sizeof(buf)) {
5157 buf[sizeof(buf) - 1] = 0;
5158 buf[sizeof(buf) - 2] = '.';
5159 buf[sizeof(buf) - 3] = '.';
5160 buf[sizeof(buf) - 4] = '.';
5161 }
5162 va_end(args);
5163 die("%s", buf);
5164 }
5166 if (!status_empty || *msg) {
5167 va_list args;
5169 va_start(args, msg);
5171 wmove(status_win, 0, 0);
5172 if (*msg) {
5173 vwprintw(status_win, msg, args);
5174 status_empty = FALSE;
5175 } else {
5176 status_empty = TRUE;
5177 }
5178 wclrtoeol(status_win);
5179 wrefresh(status_win);
5181 va_end(args);
5182 }
5184 update_view_title(view);
5185 update_display_cursor(view);
5186 }
5188 /* Controls when nodelay should be in effect when polling user input. */
5189 static void
5190 set_nonblocking_input(bool loading)
5191 {
5192 static unsigned int loading_views;
5194 if ((loading == FALSE && loading_views-- == 1) ||
5195 (loading == TRUE && loading_views++ == 0))
5196 nodelay(status_win, loading);
5197 }
5199 static void
5200 init_display(void)
5201 {
5202 int x, y;
5204 /* Initialize the curses library */
5205 if (isatty(STDIN_FILENO)) {
5206 cursed = !!initscr();
5207 } else {
5208 /* Leave stdin and stdout alone when acting as a pager. */
5209 FILE *io = fopen("/dev/tty", "r+");
5211 if (!io)
5212 die("Failed to open /dev/tty");
5213 cursed = !!newterm(NULL, io, io);
5214 }
5216 if (!cursed)
5217 die("Failed to initialize curses");
5219 nonl(); /* Tell curses not to do NL->CR/NL on output */
5220 cbreak(); /* Take input chars one at a time, no wait for \n */
5221 noecho(); /* Don't echo input */
5222 leaveok(stdscr, TRUE);
5224 if (has_colors())
5225 init_colors();
5227 getmaxyx(stdscr, y, x);
5228 status_win = newwin(1, 0, y - 1, 0);
5229 if (!status_win)
5230 die("Failed to create status window");
5232 /* Enable keyboard mapping */
5233 keypad(status_win, TRUE);
5234 wbkgdset(status_win, get_line_attr(LINE_STATUS));
5235 }
5237 static char *
5238 read_prompt(const char *prompt)
5239 {
5240 enum { READING, STOP, CANCEL } status = READING;
5241 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
5242 int pos = 0;
5244 while (status == READING) {
5245 struct view *view;
5246 int i, key;
5248 input_mode = TRUE;
5250 foreach_view (view, i)
5251 update_view(view);
5253 input_mode = FALSE;
5255 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
5256 wclrtoeol(status_win);
5258 /* Refresh, accept single keystroke of input */
5259 key = wgetch(status_win);
5260 switch (key) {
5261 case KEY_RETURN:
5262 case KEY_ENTER:
5263 case '\n':
5264 status = pos ? STOP : CANCEL;
5265 break;
5267 case KEY_BACKSPACE:
5268 if (pos > 0)
5269 pos--;
5270 else
5271 status = CANCEL;
5272 break;
5274 case KEY_ESC:
5275 status = CANCEL;
5276 break;
5278 case ERR:
5279 break;
5281 default:
5282 if (pos >= sizeof(buf)) {
5283 report("Input string too long");
5284 return NULL;
5285 }
5287 if (isprint(key))
5288 buf[pos++] = (char) key;
5289 }
5290 }
5292 /* Clear the status window */
5293 status_empty = FALSE;
5294 report("");
5296 if (status == CANCEL)
5297 return NULL;
5299 buf[pos++] = 0;
5301 return buf;
5302 }
5304 /*
5305 * Repository references
5306 */
5308 static struct ref *refs = NULL;
5309 static size_t refs_alloc = 0;
5310 static size_t refs_size = 0;
5312 /* Id <-> ref store */
5313 static struct ref ***id_refs = NULL;
5314 static size_t id_refs_alloc = 0;
5315 static size_t id_refs_size = 0;
5317 static struct ref **
5318 get_refs(char *id)
5319 {
5320 struct ref ***tmp_id_refs;
5321 struct ref **ref_list = NULL;
5322 size_t ref_list_alloc = 0;
5323 size_t ref_list_size = 0;
5324 size_t i;
5326 for (i = 0; i < id_refs_size; i++)
5327 if (!strcmp(id, id_refs[i][0]->id))
5328 return id_refs[i];
5330 tmp_id_refs = realloc_items(id_refs, &id_refs_alloc, id_refs_size + 1,
5331 sizeof(*id_refs));
5332 if (!tmp_id_refs)
5333 return NULL;
5335 id_refs = tmp_id_refs;
5337 for (i = 0; i < refs_size; i++) {
5338 struct ref **tmp;
5340 if (strcmp(id, refs[i].id))
5341 continue;
5343 tmp = realloc_items(ref_list, &ref_list_alloc,
5344 ref_list_size + 1, sizeof(*ref_list));
5345 if (!tmp) {
5346 if (ref_list)
5347 free(ref_list);
5348 return NULL;
5349 }
5351 ref_list = tmp;
5352 if (ref_list_size > 0)
5353 ref_list[ref_list_size - 1]->next = 1;
5354 ref_list[ref_list_size] = &refs[i];
5356 /* XXX: The properties of the commit chains ensures that we can
5357 * safely modify the shared ref. The repo references will
5358 * always be similar for the same id. */
5359 ref_list[ref_list_size]->next = 0;
5360 ref_list_size++;
5361 }
5363 if (ref_list)
5364 id_refs[id_refs_size++] = ref_list;
5366 return ref_list;
5367 }
5369 static int
5370 read_ref(char *id, size_t idlen, char *name, size_t namelen)
5371 {
5372 struct ref *ref;
5373 bool tag = FALSE;
5374 bool ltag = FALSE;
5375 bool remote = FALSE;
5376 bool check_replace = FALSE;
5378 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
5379 if (!strcmp(name + namelen - 3, "^{}")) {
5380 namelen -= 3;
5381 name[namelen] = 0;
5382 if (refs_size > 0 && refs[refs_size - 1].ltag == TRUE)
5383 check_replace = TRUE;
5384 } else {
5385 ltag = TRUE;
5386 }
5388 tag = TRUE;
5389 namelen -= STRING_SIZE("refs/tags/");
5390 name += STRING_SIZE("refs/tags/");
5392 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
5393 remote = TRUE;
5394 namelen -= STRING_SIZE("refs/remotes/");
5395 name += STRING_SIZE("refs/remotes/");
5397 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5398 namelen -= STRING_SIZE("refs/heads/");
5399 name += STRING_SIZE("refs/heads/");
5401 } else if (!strcmp(name, "HEAD")) {
5402 return OK;
5403 }
5405 if (check_replace && !strcmp(name, refs[refs_size - 1].name)) {
5406 /* it's an annotated tag, replace the previous sha1 with the
5407 * resolved commit id; relies on the fact git-ls-remote lists
5408 * the commit id of an annotated tag right beofre the commit id
5409 * it points to. */
5410 refs[refs_size - 1].ltag = ltag;
5411 string_copy_rev(refs[refs_size - 1].id, id);
5413 return OK;
5414 }
5415 refs = realloc_items(refs, &refs_alloc, refs_size + 1, sizeof(*refs));
5416 if (!refs)
5417 return ERR;
5419 ref = &refs[refs_size++];
5420 ref->name = malloc(namelen + 1);
5421 if (!ref->name)
5422 return ERR;
5424 strncpy(ref->name, name, namelen);
5425 ref->name[namelen] = 0;
5426 ref->tag = tag;
5427 ref->ltag = ltag;
5428 ref->remote = remote;
5429 string_copy_rev(ref->id, id);
5431 return OK;
5432 }
5434 static int
5435 load_refs(void)
5436 {
5437 const char *cmd_env = getenv("TIG_LS_REMOTE");
5438 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
5440 return read_properties(popen(cmd, "r"), "\t", read_ref);
5441 }
5443 static int
5444 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
5445 {
5446 if (!strcmp(name, "i18n.commitencoding"))
5447 string_ncopy(opt_encoding, value, valuelen);
5449 if (!strcmp(name, "core.editor"))
5450 string_ncopy(opt_editor, value, valuelen);
5452 return OK;
5453 }
5455 static int
5456 load_repo_config(void)
5457 {
5458 return read_properties(popen(GIT_CONFIG " --list", "r"),
5459 "=", read_repo_config_option);
5460 }
5462 static int
5463 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
5464 {
5465 if (!opt_git_dir[0]) {
5466 string_ncopy(opt_git_dir, name, namelen);
5468 } else if (opt_is_inside_work_tree == -1) {
5469 /* This can be 3 different values depending on the
5470 * version of git being used. If git-rev-parse does not
5471 * understand --is-inside-work-tree it will simply echo
5472 * the option else either "true" or "false" is printed.
5473 * Default to true for the unknown case. */
5474 opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
5476 } else {
5477 string_ncopy(opt_cdup, name, namelen);
5478 }
5480 return OK;
5481 }
5483 /* XXX: The line outputted by "--show-cdup" can be empty so the option
5484 * must be the last one! */
5485 static int
5486 load_repo_info(void)
5487 {
5488 return read_properties(popen("git rev-parse --git-dir --is-inside-work-tree --show-cdup 2>/dev/null", "r"),
5489 "=", read_repo_info);
5490 }
5492 static int
5493 read_properties(FILE *pipe, const char *separators,
5494 int (*read_property)(char *, size_t, char *, size_t))
5495 {
5496 char buffer[BUFSIZ];
5497 char *name;
5498 int state = OK;
5500 if (!pipe)
5501 return ERR;
5503 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
5504 char *value;
5505 size_t namelen;
5506 size_t valuelen;
5508 name = chomp_string(name);
5509 namelen = strcspn(name, separators);
5511 if (name[namelen]) {
5512 name[namelen] = 0;
5513 value = chomp_string(name + namelen + 1);
5514 valuelen = strlen(value);
5516 } else {
5517 value = "";
5518 valuelen = 0;
5519 }
5521 state = read_property(name, namelen, value, valuelen);
5522 }
5524 if (state != ERR && ferror(pipe))
5525 state = ERR;
5527 pclose(pipe);
5529 return state;
5530 }
5533 /*
5534 * Main
5535 */
5537 static void __NORETURN
5538 quit(int sig)
5539 {
5540 /* XXX: Restore tty modes and let the OS cleanup the rest! */
5541 if (cursed)
5542 endwin();
5543 exit(0);
5544 }
5546 static void __NORETURN
5547 die(const char *err, ...)
5548 {
5549 va_list args;
5551 endwin();
5553 va_start(args, err);
5554 fputs("tig: ", stderr);
5555 vfprintf(stderr, err, args);
5556 fputs("\n", stderr);
5557 va_end(args);
5559 exit(1);
5560 }
5562 static void
5563 warn(const char *msg, ...)
5564 {
5565 va_list args;
5567 va_start(args, msg);
5568 fputs("tig warning: ", stderr);
5569 vfprintf(stderr, msg, args);
5570 fputs("\n", stderr);
5571 va_end(args);
5572 }
5574 int
5575 main(int argc, char *argv[])
5576 {
5577 struct view *view;
5578 enum request request;
5579 size_t i;
5581 signal(SIGINT, quit);
5583 if (setlocale(LC_ALL, "")) {
5584 char *codeset = nl_langinfo(CODESET);
5586 string_ncopy(opt_codeset, codeset, strlen(codeset));
5587 }
5589 if (load_repo_info() == ERR)
5590 die("Failed to load repo info.");
5592 if (load_options() == ERR)
5593 die("Failed to load user config.");
5595 /* Load the repo config file so options can be overwritten from
5596 * the command line. */
5597 if (load_repo_config() == ERR)
5598 die("Failed to load repo config.");
5600 if (!parse_options(argc, argv))
5601 return 0;
5603 /* Require a git repository unless when running in pager mode. */
5604 if (!opt_git_dir[0])
5605 die("Not a git repository");
5607 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
5608 opt_utf8 = FALSE;
5610 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
5611 opt_iconv = iconv_open(opt_codeset, opt_encoding);
5612 if (opt_iconv == ICONV_NONE)
5613 die("Failed to initialize character set conversion");
5614 }
5616 if (load_refs() == ERR)
5617 die("Failed to load refs.");
5619 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
5620 view->cmd_env = getenv(view->cmd_env);
5622 request = opt_request;
5624 init_display();
5626 while (view_driver(display[current_view], request)) {
5627 int key;
5628 int i;
5630 foreach_view (view, i)
5631 update_view(view);
5633 /* Refresh, accept single keystroke of input */
5634 key = wgetch(status_win);
5636 /* wgetch() with nodelay() enabled returns ERR when there's no
5637 * input. */
5638 if (key == ERR) {
5639 request = REQ_NONE;
5640 continue;
5641 }
5643 request = get_keybinding(display[current_view]->keymap, key);
5645 /* Some low-level request handling. This keeps access to
5646 * status_win restricted. */
5647 switch (request) {
5648 case REQ_PROMPT:
5649 {
5650 char *cmd = read_prompt(":");
5652 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
5653 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
5654 opt_request = REQ_VIEW_DIFF;
5655 } else {
5656 opt_request = REQ_VIEW_PAGER;
5657 }
5658 break;
5659 }
5661 request = REQ_NONE;
5662 break;
5663 }
5664 case REQ_SEARCH:
5665 case REQ_SEARCH_BACK:
5666 {
5667 const char *prompt = request == REQ_SEARCH
5668 ? "/" : "?";
5669 char *search = read_prompt(prompt);
5671 if (search)
5672 string_ncopy(opt_search, search, strlen(search));
5673 else
5674 request = REQ_NONE;
5675 break;
5676 }
5677 case REQ_SCREEN_RESIZE:
5678 {
5679 int height, width;
5681 getmaxyx(stdscr, height, width);
5683 /* Resize the status view and let the view driver take
5684 * care of resizing the displayed views. */
5685 wresize(status_win, 1, width);
5686 mvwin(status_win, height - 1, 0);
5687 wrefresh(status_win);
5688 break;
5689 }
5690 default:
5691 break;
5692 }
5693 }
5695 quit(0);
5697 return 0;
5698 }