1 /* Copyright (c) 2006-2008 Jonas Fonseca <fonseca@diku.dk>
2 *
3 * This program is free software; you can redistribute it and/or
4 * modify it under the terms of the GNU General Public License as
5 * published by the Free Software Foundation; either version 2 of
6 * the License, or (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 */
14 #ifdef HAVE_CONFIG_H
15 #include "config.h"
16 #endif
18 #ifndef TIG_VERSION
19 #define TIG_VERSION "unknown-version"
20 #endif
22 #ifndef DEBUG
23 #define NDEBUG
24 #endif
26 #include <assert.h>
27 #include <errno.h>
28 #include <ctype.h>
29 #include <signal.h>
30 #include <stdarg.h>
31 #include <stdio.h>
32 #include <stdlib.h>
33 #include <string.h>
34 #include <sys/types.h>
35 #include <sys/stat.h>
36 #include <unistd.h>
37 #include <time.h>
39 #include <regex.h>
41 #include <locale.h>
42 #include <langinfo.h>
43 #include <iconv.h>
45 /* ncurses(3): Must be defined to have extended wide-character functions. */
46 #define _XOPEN_SOURCE_EXTENDED
48 #include <curses.h>
50 #if __GNUC__ >= 3
51 #define __NORETURN __attribute__((__noreturn__))
52 #else
53 #define __NORETURN
54 #endif
56 static void __NORETURN die(const char *err, ...);
57 static void warn(const char *msg, ...);
58 static void report(const char *msg, ...);
59 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, size_t, char *, size_t));
60 static void set_nonblocking_input(bool loading);
61 static size_t utf8_length(const char *string, size_t max_width, int *trimmed, bool reserve);
63 #define ABS(x) ((x) >= 0 ? (x) : -(x))
64 #define MIN(x, y) ((x) < (y) ? (x) : (y))
66 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
67 #define STRING_SIZE(x) (sizeof(x) - 1)
69 #define SIZEOF_STR 1024 /* Default string size. */
70 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
71 #define SIZEOF_REV 41 /* Holds a SHA-1 and an ending NUL */
73 /* Revision graph */
75 #define REVGRAPH_INIT 'I'
76 #define REVGRAPH_MERGE 'M'
77 #define REVGRAPH_BRANCH '+'
78 #define REVGRAPH_COMMIT '*'
79 #define REVGRAPH_BOUND '^'
80 #define REVGRAPH_LINE '|'
82 #define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
84 /* This color name can be used to refer to the default term colors. */
85 #define COLOR_DEFAULT (-1)
87 #define ICONV_NONE ((iconv_t) -1)
88 #ifndef ICONV_CONST
89 #define ICONV_CONST /* nothing */
90 #endif
92 /* The format and size of the date column in the main view. */
93 #define DATE_FORMAT "%Y-%m-%d %H:%M"
94 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
96 #define AUTHOR_COLS 20
97 #define ID_COLS 8
99 /* The default interval between line numbers. */
100 #define NUMBER_INTERVAL 5
102 #define TABSIZE 8
104 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
106 #define NULL_ID "0000000000000000000000000000000000000000"
108 #ifndef GIT_CONFIG
109 #define GIT_CONFIG "git config"
110 #endif
112 #define TIG_LS_REMOTE \
113 "git ls-remote $(git rev-parse --git-dir) 2>/dev/null"
115 #define TIG_DIFF_CMD \
116 "git show --pretty=fuller --no-color --root --patch-with-stat --find-copies-harder -C %s 2>/dev/null"
118 #define TIG_LOG_CMD \
119 "git log --no-color --cc --stat -n100 %s 2>/dev/null"
121 #define TIG_MAIN_CMD \
122 "git log --no-color --topo-order --parents --boundary --pretty=raw %s 2>/dev/null"
124 #define TIG_TREE_CMD \
125 "git ls-tree %s %s"
127 #define TIG_BLOB_CMD \
128 "git cat-file blob %s"
130 /* XXX: Needs to be defined to the empty string. */
131 #define TIG_HELP_CMD ""
132 #define TIG_PAGER_CMD ""
133 #define TIG_STATUS_CMD ""
134 #define TIG_STAGE_CMD ""
135 #define TIG_BLAME_CMD ""
137 /* Some ascii-shorthands fitted into the ncurses namespace. */
138 #define KEY_TAB '\t'
139 #define KEY_RETURN '\r'
140 #define KEY_ESC 27
143 struct ref {
144 char *name; /* Ref name; tag or head names are shortened. */
145 char id[SIZEOF_REV]; /* Commit SHA1 ID */
146 unsigned int tag:1; /* Is it a tag? */
147 unsigned int ltag:1; /* If so, is the tag local? */
148 unsigned int remote:1; /* Is it a remote ref? */
149 unsigned int next:1; /* For ref lists: are there more refs? */
150 unsigned int head:1; /* Is it the current HEAD? */
151 };
153 static struct ref **get_refs(char *id);
155 struct int_map {
156 const char *name;
157 int namelen;
158 int value;
159 };
161 static int
162 set_from_int_map(struct int_map *map, size_t map_size,
163 int *value, const char *name, int namelen)
164 {
166 int i;
168 for (i = 0; i < map_size; i++)
169 if (namelen == map[i].namelen &&
170 !strncasecmp(name, map[i].name, namelen)) {
171 *value = map[i].value;
172 return OK;
173 }
175 return ERR;
176 }
179 /*
180 * String helpers
181 */
183 static inline void
184 string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
185 {
186 if (srclen > dstlen - 1)
187 srclen = dstlen - 1;
189 strncpy(dst, src, srclen);
190 dst[srclen] = 0;
191 }
193 /* Shorthands for safely copying into a fixed buffer. */
195 #define string_copy(dst, src) \
196 string_ncopy_do(dst, sizeof(dst), src, sizeof(src))
198 #define string_ncopy(dst, src, srclen) \
199 string_ncopy_do(dst, sizeof(dst), src, srclen)
201 #define string_copy_rev(dst, src) \
202 string_ncopy_do(dst, SIZEOF_REV, src, SIZEOF_REV - 1)
204 #define string_add(dst, from, src) \
205 string_ncopy_do(dst + (from), sizeof(dst) - (from), src, sizeof(src))
207 static char *
208 chomp_string(char *name)
209 {
210 int namelen;
212 while (isspace(*name))
213 name++;
215 namelen = strlen(name) - 1;
216 while (namelen > 0 && isspace(name[namelen]))
217 name[namelen--] = 0;
219 return name;
220 }
222 static bool
223 string_nformat(char *buf, size_t bufsize, size_t *bufpos, const char *fmt, ...)
224 {
225 va_list args;
226 size_t pos = bufpos ? *bufpos : 0;
228 va_start(args, fmt);
229 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
230 va_end(args);
232 if (bufpos)
233 *bufpos = pos;
235 return pos >= bufsize ? FALSE : TRUE;
236 }
238 #define string_format(buf, fmt, args...) \
239 string_nformat(buf, sizeof(buf), NULL, fmt, args)
241 #define string_format_from(buf, from, fmt, args...) \
242 string_nformat(buf, sizeof(buf), from, fmt, args)
244 static int
245 string_enum_compare(const char *str1, const char *str2, int len)
246 {
247 size_t i;
249 #define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
251 /* Diff-Header == DIFF_HEADER */
252 for (i = 0; i < len; i++) {
253 if (toupper(str1[i]) == toupper(str2[i]))
254 continue;
256 if (string_enum_sep(str1[i]) &&
257 string_enum_sep(str2[i]))
258 continue;
260 return str1[i] - str2[i];
261 }
263 return 0;
264 }
266 /* Shell quoting
267 *
268 * NOTE: The following is a slightly modified copy of the git project's shell
269 * quoting routines found in the quote.c file.
270 *
271 * Help to copy the thing properly quoted for the shell safety. any single
272 * quote is replaced with '\'', any exclamation point is replaced with '\!',
273 * and the whole thing is enclosed in a
274 *
275 * E.g.
276 * original sq_quote result
277 * name ==> name ==> 'name'
278 * a b ==> a b ==> 'a b'
279 * a'b ==> a'\''b ==> 'a'\''b'
280 * a!b ==> a'\!'b ==> 'a'\!'b'
281 */
283 static size_t
284 sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
285 {
286 char c;
288 #define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0)
290 BUFPUT('\'');
291 while ((c = *src++)) {
292 if (c == '\'' || c == '!') {
293 BUFPUT('\'');
294 BUFPUT('\\');
295 BUFPUT(c);
296 BUFPUT('\'');
297 } else {
298 BUFPUT(c);
299 }
300 }
301 BUFPUT('\'');
303 if (bufsize < SIZEOF_STR)
304 buf[bufsize] = 0;
306 return bufsize;
307 }
310 /*
311 * User requests
312 */
314 #define REQ_INFO \
315 /* XXX: Keep the view request first and in sync with views[]. */ \
316 REQ_GROUP("View switching") \
317 REQ_(VIEW_MAIN, "Show main view"), \
318 REQ_(VIEW_DIFF, "Show diff view"), \
319 REQ_(VIEW_LOG, "Show log view"), \
320 REQ_(VIEW_TREE, "Show tree view"), \
321 REQ_(VIEW_BLOB, "Show blob view"), \
322 REQ_(VIEW_BLAME, "Show blame view"), \
323 REQ_(VIEW_HELP, "Show help page"), \
324 REQ_(VIEW_PAGER, "Show pager view"), \
325 REQ_(VIEW_STATUS, "Show status view"), \
326 REQ_(VIEW_STAGE, "Show stage view"), \
327 \
328 REQ_GROUP("View manipulation") \
329 REQ_(ENTER, "Enter current line and scroll"), \
330 REQ_(NEXT, "Move to next"), \
331 REQ_(PREVIOUS, "Move to previous"), \
332 REQ_(VIEW_NEXT, "Move focus to next view"), \
333 REQ_(REFRESH, "Reload and refresh"), \
334 REQ_(MAXIMIZE, "Maximize the current view"), \
335 REQ_(VIEW_CLOSE, "Close the current view"), \
336 REQ_(QUIT, "Close all views and quit"), \
337 \
338 REQ_GROUP("Cursor navigation") \
339 REQ_(MOVE_UP, "Move cursor one line up"), \
340 REQ_(MOVE_DOWN, "Move cursor one line down"), \
341 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
342 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
343 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
344 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
345 \
346 REQ_GROUP("Scrolling") \
347 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
348 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
349 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
350 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
351 \
352 REQ_GROUP("Searching") \
353 REQ_(SEARCH, "Search the view"), \
354 REQ_(SEARCH_BACK, "Search backwards in the view"), \
355 REQ_(FIND_NEXT, "Find next search match"), \
356 REQ_(FIND_PREV, "Find previous search match"), \
357 \
358 REQ_GROUP("Misc") \
359 REQ_(PROMPT, "Bring up the prompt"), \
360 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
361 REQ_(SCREEN_RESIZE, "Resize the screen"), \
362 REQ_(SHOW_VERSION, "Show version information"), \
363 REQ_(STOP_LOADING, "Stop all loading views"), \
364 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
365 REQ_(TOGGLE_DATE, "Toggle date display"), \
366 REQ_(TOGGLE_AUTHOR, "Toggle author display"), \
367 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"), \
368 REQ_(TOGGLE_REFS, "Toggle reference display (tags/branches)"), \
369 REQ_(STATUS_UPDATE, "Update file status"), \
370 REQ_(STATUS_MERGE, "Merge file using external tool"), \
371 REQ_(TREE_PARENT, "Switch to parent directory in tree view"), \
372 REQ_(EDIT, "Open in editor"), \
373 REQ_(NONE, "Do nothing")
376 /* User action requests. */
377 enum request {
378 #define REQ_GROUP(help)
379 #define REQ_(req, help) REQ_##req
381 /* Offset all requests to avoid conflicts with ncurses getch values. */
382 REQ_OFFSET = KEY_MAX + 1,
383 REQ_INFO
385 #undef REQ_GROUP
386 #undef REQ_
387 };
389 struct request_info {
390 enum request request;
391 char *name;
392 int namelen;
393 char *help;
394 };
396 static struct request_info req_info[] = {
397 #define REQ_GROUP(help) { 0, NULL, 0, (help) },
398 #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
399 REQ_INFO
400 #undef REQ_GROUP
401 #undef REQ_
402 };
404 static enum request
405 get_request(const char *name)
406 {
407 int namelen = strlen(name);
408 int i;
410 for (i = 0; i < ARRAY_SIZE(req_info); i++)
411 if (req_info[i].namelen == namelen &&
412 !string_enum_compare(req_info[i].name, name, namelen))
413 return req_info[i].request;
415 return REQ_NONE;
416 }
419 /*
420 * Options
421 */
423 static const char usage[] =
424 "tig " TIG_VERSION " (" __DATE__ ")\n"
425 "\n"
426 "Usage: tig [options] [revs] [--] [paths]\n"
427 " or: tig show [options] [revs] [--] [paths]\n"
428 " or: tig blame [rev] path\n"
429 " or: tig status\n"
430 " or: tig < [git command output]\n"
431 "\n"
432 "Options:\n"
433 " -v, --version Show version and exit\n"
434 " -h, --help Show help message and exit";
436 /* Option and state variables. */
437 static bool opt_date = TRUE;
438 static bool opt_author = TRUE;
439 static bool opt_line_number = FALSE;
440 static bool opt_rev_graph = FALSE;
441 static bool opt_show_refs = TRUE;
442 static int opt_num_interval = NUMBER_INTERVAL;
443 static int opt_tab_size = TABSIZE;
444 static enum request opt_request = REQ_VIEW_MAIN;
445 static char opt_cmd[SIZEOF_STR] = "";
446 static char opt_path[SIZEOF_STR] = "";
447 static char opt_file[SIZEOF_STR] = "";
448 static char opt_ref[SIZEOF_REF] = "";
449 static char opt_head[SIZEOF_REF] = "";
450 static bool opt_no_head = TRUE;
451 static FILE *opt_pipe = NULL;
452 static char opt_encoding[20] = "UTF-8";
453 static bool opt_utf8 = TRUE;
454 static char opt_codeset[20] = "UTF-8";
455 static iconv_t opt_iconv = ICONV_NONE;
456 static char opt_search[SIZEOF_STR] = "";
457 static char opt_cdup[SIZEOF_STR] = "";
458 static char opt_git_dir[SIZEOF_STR] = "";
459 static signed char opt_is_inside_work_tree = -1; /* set to TRUE or FALSE */
460 static char opt_editor[SIZEOF_STR] = "";
462 static bool
463 parse_options(int argc, char *argv[])
464 {
465 size_t buf_size;
466 char *subcommand;
467 bool seen_dashdash = FALSE;
468 int i;
470 if (!isatty(STDIN_FILENO)) {
471 opt_request = REQ_VIEW_PAGER;
472 opt_pipe = stdin;
473 return TRUE;
474 }
476 if (argc <= 1)
477 return TRUE;
479 subcommand = argv[1];
480 if (!strcmp(subcommand, "status") || !strcmp(subcommand, "-S")) {
481 opt_request = REQ_VIEW_STATUS;
482 if (!strcmp(subcommand, "-S"))
483 warn("`-S' has been deprecated; use `tig status' instead");
484 if (argc > 2)
485 warn("ignoring arguments after `%s'", subcommand);
486 return TRUE;
488 } else if (!strcmp(subcommand, "blame")) {
489 opt_request = REQ_VIEW_BLAME;
490 if (argc <= 2 || argc > 4)
491 die("invalid number of options to blame\n\n%s", usage);
493 i = 2;
494 if (argc == 4) {
495 string_ncopy(opt_ref, argv[i], strlen(argv[i]));
496 i++;
497 }
499 string_ncopy(opt_file, argv[i], strlen(argv[i]));
500 return TRUE;
502 } else if (!strcmp(subcommand, "show")) {
503 opt_request = REQ_VIEW_DIFF;
505 } else if (!strcmp(subcommand, "log") || !strcmp(subcommand, "diff")) {
506 opt_request = subcommand[0] == 'l'
507 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
508 warn("`tig %s' has been deprecated", subcommand);
510 } else {
511 subcommand = NULL;
512 }
514 if (!subcommand)
515 /* XXX: This is vulnerable to the user overriding
516 * options required for the main view parser. */
517 string_copy(opt_cmd, "git log --no-color --pretty=raw --boundary --parents");
518 else
519 string_format(opt_cmd, "git %s", subcommand);
521 buf_size = strlen(opt_cmd);
523 for (i = 1 + !!subcommand; i < argc; i++) {
524 char *opt = argv[i];
526 if (seen_dashdash || !strcmp(opt, "--")) {
527 seen_dashdash = TRUE;
529 } else if (!strcmp(opt, "-v") || !strcmp(opt, "--version")) {
530 printf("tig version %s\n", TIG_VERSION);
531 return FALSE;
533 } else if (!strcmp(opt, "-h") || !strcmp(opt, "--help")) {
534 printf("%s\n", usage);
535 return FALSE;
536 }
538 opt_cmd[buf_size++] = ' ';
539 buf_size = sq_quote(opt_cmd, buf_size, opt);
540 if (buf_size >= sizeof(opt_cmd))
541 die("command too long");
542 }
544 opt_cmd[buf_size] = 0;
546 return TRUE;
547 }
550 /*
551 * Line-oriented content detection.
552 */
554 #define LINE_INFO \
555 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
556 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
557 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
558 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
559 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
560 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
561 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
562 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
563 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
564 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
565 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
566 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
567 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
568 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
569 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
570 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
571 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
572 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
573 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
574 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
575 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
576 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
577 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
578 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
579 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
580 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
581 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
582 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
583 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
584 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
585 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
586 LINE(DELIMITER, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
587 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
588 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
589 LINE(MAIN_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
590 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
591 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
592 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
593 LINE(MAIN_LOCAL_TAG,"", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
594 LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
595 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
596 LINE(MAIN_HEAD, "", COLOR_RED, COLOR_DEFAULT, A_BOLD), \
597 LINE(MAIN_REVGRAPH,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
598 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
599 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
600 LINE(STAT_HEAD, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \
601 LINE(STAT_SECTION, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
602 LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
603 LINE(STAT_STAGED, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
604 LINE(STAT_UNSTAGED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
605 LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
606 LINE(BLAME_DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
607 LINE(BLAME_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
608 LINE(BLAME_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
609 LINE(BLAME_ID, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
610 LINE(BLAME_LINENO, "", COLOR_CYAN, COLOR_DEFAULT, 0)
612 enum line_type {
613 #define LINE(type, line, fg, bg, attr) \
614 LINE_##type
615 LINE_INFO
616 #undef LINE
617 };
619 struct line_info {
620 const char *name; /* Option name. */
621 int namelen; /* Size of option name. */
622 const char *line; /* The start of line to match. */
623 int linelen; /* Size of string to match. */
624 int fg, bg, attr; /* Color and text attributes for the lines. */
625 };
627 static struct line_info line_info[] = {
628 #define LINE(type, line, fg, bg, attr) \
629 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
630 LINE_INFO
631 #undef LINE
632 };
634 static enum line_type
635 get_line_type(char *line)
636 {
637 int linelen = strlen(line);
638 enum line_type type;
640 for (type = 0; type < ARRAY_SIZE(line_info); type++)
641 /* Case insensitive search matches Signed-off-by lines better. */
642 if (linelen >= line_info[type].linelen &&
643 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
644 return type;
646 return LINE_DEFAULT;
647 }
649 static inline int
650 get_line_attr(enum line_type type)
651 {
652 assert(type < ARRAY_SIZE(line_info));
653 return COLOR_PAIR(type) | line_info[type].attr;
654 }
656 static struct line_info *
657 get_line_info(char *name)
658 {
659 size_t namelen = strlen(name);
660 enum line_type type;
662 for (type = 0; type < ARRAY_SIZE(line_info); type++)
663 if (namelen == line_info[type].namelen &&
664 !string_enum_compare(line_info[type].name, name, namelen))
665 return &line_info[type];
667 return NULL;
668 }
670 static void
671 init_colors(void)
672 {
673 int default_bg = line_info[LINE_DEFAULT].bg;
674 int default_fg = line_info[LINE_DEFAULT].fg;
675 enum line_type type;
677 start_color();
679 if (assume_default_colors(default_fg, default_bg) == ERR) {
680 default_bg = COLOR_BLACK;
681 default_fg = COLOR_WHITE;
682 }
684 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
685 struct line_info *info = &line_info[type];
686 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
687 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
689 init_pair(type, fg, bg);
690 }
691 }
693 struct line {
694 enum line_type type;
696 /* State flags */
697 unsigned int selected:1;
698 unsigned int dirty:1;
700 void *data; /* User data */
701 };
704 /*
705 * Keys
706 */
708 struct keybinding {
709 int alias;
710 enum request request;
711 struct keybinding *next;
712 };
714 static struct keybinding default_keybindings[] = {
715 /* View switching */
716 { 'm', REQ_VIEW_MAIN },
717 { 'd', REQ_VIEW_DIFF },
718 { 'l', REQ_VIEW_LOG },
719 { 't', REQ_VIEW_TREE },
720 { 'f', REQ_VIEW_BLOB },
721 { 'B', REQ_VIEW_BLAME },
722 { 'p', REQ_VIEW_PAGER },
723 { 'h', REQ_VIEW_HELP },
724 { 'S', REQ_VIEW_STATUS },
725 { 'c', REQ_VIEW_STAGE },
727 /* View manipulation */
728 { 'q', REQ_VIEW_CLOSE },
729 { KEY_TAB, REQ_VIEW_NEXT },
730 { KEY_RETURN, REQ_ENTER },
731 { KEY_UP, REQ_PREVIOUS },
732 { KEY_DOWN, REQ_NEXT },
733 { 'R', REQ_REFRESH },
734 { 'M', REQ_MAXIMIZE },
736 /* Cursor navigation */
737 { 'k', REQ_MOVE_UP },
738 { 'j', REQ_MOVE_DOWN },
739 { KEY_HOME, REQ_MOVE_FIRST_LINE },
740 { KEY_END, REQ_MOVE_LAST_LINE },
741 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
742 { ' ', REQ_MOVE_PAGE_DOWN },
743 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
744 { 'b', REQ_MOVE_PAGE_UP },
745 { '-', REQ_MOVE_PAGE_UP },
747 /* Scrolling */
748 { KEY_IC, REQ_SCROLL_LINE_UP },
749 { KEY_DC, REQ_SCROLL_LINE_DOWN },
750 { 'w', REQ_SCROLL_PAGE_UP },
751 { 's', REQ_SCROLL_PAGE_DOWN },
753 /* Searching */
754 { '/', REQ_SEARCH },
755 { '?', REQ_SEARCH_BACK },
756 { 'n', REQ_FIND_NEXT },
757 { 'N', REQ_FIND_PREV },
759 /* Misc */
760 { 'Q', REQ_QUIT },
761 { 'z', REQ_STOP_LOADING },
762 { 'v', REQ_SHOW_VERSION },
763 { 'r', REQ_SCREEN_REDRAW },
764 { '.', REQ_TOGGLE_LINENO },
765 { 'D', REQ_TOGGLE_DATE },
766 { 'A', REQ_TOGGLE_AUTHOR },
767 { 'g', REQ_TOGGLE_REV_GRAPH },
768 { 'F', REQ_TOGGLE_REFS },
769 { ':', REQ_PROMPT },
770 { 'u', REQ_STATUS_UPDATE },
771 { 'M', REQ_STATUS_MERGE },
772 { ',', REQ_TREE_PARENT },
773 { 'e', REQ_EDIT },
775 /* Using the ncurses SIGWINCH handler. */
776 { KEY_RESIZE, REQ_SCREEN_RESIZE },
777 };
779 #define KEYMAP_INFO \
780 KEYMAP_(GENERIC), \
781 KEYMAP_(MAIN), \
782 KEYMAP_(DIFF), \
783 KEYMAP_(LOG), \
784 KEYMAP_(TREE), \
785 KEYMAP_(BLOB), \
786 KEYMAP_(BLAME), \
787 KEYMAP_(PAGER), \
788 KEYMAP_(HELP), \
789 KEYMAP_(STATUS), \
790 KEYMAP_(STAGE)
792 enum keymap {
793 #define KEYMAP_(name) KEYMAP_##name
794 KEYMAP_INFO
795 #undef KEYMAP_
796 };
798 static struct int_map keymap_table[] = {
799 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
800 KEYMAP_INFO
801 #undef KEYMAP_
802 };
804 #define set_keymap(map, name) \
805 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
807 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
809 static void
810 add_keybinding(enum keymap keymap, enum request request, int key)
811 {
812 struct keybinding *keybinding;
814 keybinding = calloc(1, sizeof(*keybinding));
815 if (!keybinding)
816 die("Failed to allocate keybinding");
818 keybinding->alias = key;
819 keybinding->request = request;
820 keybinding->next = keybindings[keymap];
821 keybindings[keymap] = keybinding;
822 }
824 /* Looks for a key binding first in the given map, then in the generic map, and
825 * lastly in the default keybindings. */
826 static enum request
827 get_keybinding(enum keymap keymap, int key)
828 {
829 struct keybinding *kbd;
830 int i;
832 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
833 if (kbd->alias == key)
834 return kbd->request;
836 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
837 if (kbd->alias == key)
838 return kbd->request;
840 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
841 if (default_keybindings[i].alias == key)
842 return default_keybindings[i].request;
844 return (enum request) key;
845 }
848 struct key {
849 char *name;
850 int value;
851 };
853 static struct key key_table[] = {
854 { "Enter", KEY_RETURN },
855 { "Space", ' ' },
856 { "Backspace", KEY_BACKSPACE },
857 { "Tab", KEY_TAB },
858 { "Escape", KEY_ESC },
859 { "Left", KEY_LEFT },
860 { "Right", KEY_RIGHT },
861 { "Up", KEY_UP },
862 { "Down", KEY_DOWN },
863 { "Insert", KEY_IC },
864 { "Delete", KEY_DC },
865 { "Hash", '#' },
866 { "Home", KEY_HOME },
867 { "End", KEY_END },
868 { "PageUp", KEY_PPAGE },
869 { "PageDown", KEY_NPAGE },
870 { "F1", KEY_F(1) },
871 { "F2", KEY_F(2) },
872 { "F3", KEY_F(3) },
873 { "F4", KEY_F(4) },
874 { "F5", KEY_F(5) },
875 { "F6", KEY_F(6) },
876 { "F7", KEY_F(7) },
877 { "F8", KEY_F(8) },
878 { "F9", KEY_F(9) },
879 { "F10", KEY_F(10) },
880 { "F11", KEY_F(11) },
881 { "F12", KEY_F(12) },
882 };
884 static int
885 get_key_value(const char *name)
886 {
887 int i;
889 for (i = 0; i < ARRAY_SIZE(key_table); i++)
890 if (!strcasecmp(key_table[i].name, name))
891 return key_table[i].value;
893 if (strlen(name) == 1 && isprint(*name))
894 return (int) *name;
896 return ERR;
897 }
899 static char *
900 get_key_name(int key_value)
901 {
902 static char key_char[] = "'X'";
903 char *seq = NULL;
904 int key;
906 for (key = 0; key < ARRAY_SIZE(key_table); key++)
907 if (key_table[key].value == key_value)
908 seq = key_table[key].name;
910 if (seq == NULL &&
911 key_value < 127 &&
912 isprint(key_value)) {
913 key_char[1] = (char) key_value;
914 seq = key_char;
915 }
917 return seq ? seq : "'?'";
918 }
920 static char *
921 get_key(enum request request)
922 {
923 static char buf[BUFSIZ];
924 size_t pos = 0;
925 char *sep = "";
926 int i;
928 buf[pos] = 0;
930 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
931 struct keybinding *keybinding = &default_keybindings[i];
933 if (keybinding->request != request)
934 continue;
936 if (!string_format_from(buf, &pos, "%s%s", sep,
937 get_key_name(keybinding->alias)))
938 return "Too many keybindings!";
939 sep = ", ";
940 }
942 return buf;
943 }
945 struct run_request {
946 enum keymap keymap;
947 int key;
948 char cmd[SIZEOF_STR];
949 };
951 static struct run_request *run_request;
952 static size_t run_requests;
954 static enum request
955 add_run_request(enum keymap keymap, int key, int argc, char **argv)
956 {
957 struct run_request *tmp;
958 struct run_request req = { keymap, key };
959 size_t bufpos;
961 for (bufpos = 0; argc > 0; argc--, argv++)
962 if (!string_format_from(req.cmd, &bufpos, "%s ", *argv))
963 return REQ_NONE;
965 req.cmd[bufpos - 1] = 0;
967 tmp = realloc(run_request, (run_requests + 1) * sizeof(*run_request));
968 if (!tmp)
969 return REQ_NONE;
971 run_request = tmp;
972 run_request[run_requests++] = req;
974 return REQ_NONE + run_requests;
975 }
977 static struct run_request *
978 get_run_request(enum request request)
979 {
980 if (request <= REQ_NONE)
981 return NULL;
982 return &run_request[request - REQ_NONE - 1];
983 }
985 static void
986 add_builtin_run_requests(void)
987 {
988 struct {
989 enum keymap keymap;
990 int key;
991 char *argv[1];
992 } reqs[] = {
993 { KEYMAP_MAIN, 'C', { "git cherry-pick %(commit)" } },
994 { KEYMAP_GENERIC, 'G', { "git gc" } },
995 };
996 int i;
998 for (i = 0; i < ARRAY_SIZE(reqs); i++) {
999 enum request req;
1001 req = add_run_request(reqs[i].keymap, reqs[i].key, 1, reqs[i].argv);
1002 if (req != REQ_NONE)
1003 add_keybinding(reqs[i].keymap, req, reqs[i].key);
1004 }
1005 }
1007 /*
1008 * User config file handling.
1009 */
1011 static struct int_map color_map[] = {
1012 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
1013 COLOR_MAP(DEFAULT),
1014 COLOR_MAP(BLACK),
1015 COLOR_MAP(BLUE),
1016 COLOR_MAP(CYAN),
1017 COLOR_MAP(GREEN),
1018 COLOR_MAP(MAGENTA),
1019 COLOR_MAP(RED),
1020 COLOR_MAP(WHITE),
1021 COLOR_MAP(YELLOW),
1022 };
1024 #define set_color(color, name) \
1025 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
1027 static struct int_map attr_map[] = {
1028 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
1029 ATTR_MAP(NORMAL),
1030 ATTR_MAP(BLINK),
1031 ATTR_MAP(BOLD),
1032 ATTR_MAP(DIM),
1033 ATTR_MAP(REVERSE),
1034 ATTR_MAP(STANDOUT),
1035 ATTR_MAP(UNDERLINE),
1036 };
1038 #define set_attribute(attr, name) \
1039 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
1041 static int config_lineno;
1042 static bool config_errors;
1043 static char *config_msg;
1045 /* Wants: object fgcolor bgcolor [attr] */
1046 static int
1047 option_color_command(int argc, char *argv[])
1048 {
1049 struct line_info *info;
1051 if (argc != 3 && argc != 4) {
1052 config_msg = "Wrong number of arguments given to color command";
1053 return ERR;
1054 }
1056 info = get_line_info(argv[0]);
1057 if (!info) {
1058 if (!string_enum_compare(argv[0], "main-delim", strlen("main-delim"))) {
1059 info = get_line_info("delimiter");
1061 } else {
1062 config_msg = "Unknown color name";
1063 return ERR;
1064 }
1065 }
1067 if (set_color(&info->fg, argv[1]) == ERR ||
1068 set_color(&info->bg, argv[2]) == ERR) {
1069 config_msg = "Unknown color";
1070 return ERR;
1071 }
1073 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
1074 config_msg = "Unknown attribute";
1075 return ERR;
1076 }
1078 return OK;
1079 }
1081 static bool parse_bool(const char *s)
1082 {
1083 return (!strcmp(s, "1") || !strcmp(s, "true") ||
1084 !strcmp(s, "yes")) ? TRUE : FALSE;
1085 }
1087 /* Wants: name = value */
1088 static int
1089 option_set_command(int argc, char *argv[])
1090 {
1091 if (argc != 3) {
1092 config_msg = "Wrong number of arguments given to set command";
1093 return ERR;
1094 }
1096 if (strcmp(argv[1], "=")) {
1097 config_msg = "No value assigned";
1098 return ERR;
1099 }
1101 if (!strcmp(argv[0], "show-author")) {
1102 opt_author = parse_bool(argv[2]);
1103 return OK;
1104 }
1106 if (!strcmp(argv[0], "show-date")) {
1107 opt_date = parse_bool(argv[2]);
1108 return OK;
1109 }
1111 if (!strcmp(argv[0], "show-rev-graph")) {
1112 opt_rev_graph = parse_bool(argv[2]);
1113 return OK;
1114 }
1116 if (!strcmp(argv[0], "show-refs")) {
1117 opt_show_refs = parse_bool(argv[2]);
1118 return OK;
1119 }
1121 if (!strcmp(argv[0], "show-line-numbers")) {
1122 opt_line_number = parse_bool(argv[2]);
1123 return OK;
1124 }
1126 if (!strcmp(argv[0], "line-number-interval")) {
1127 opt_num_interval = atoi(argv[2]);
1128 return OK;
1129 }
1131 if (!strcmp(argv[0], "tab-size")) {
1132 opt_tab_size = atoi(argv[2]);
1133 return OK;
1134 }
1136 if (!strcmp(argv[0], "commit-encoding")) {
1137 char *arg = argv[2];
1138 int delimiter = *arg;
1139 int i;
1141 switch (delimiter) {
1142 case '"':
1143 case '\'':
1144 for (arg++, i = 0; arg[i]; i++)
1145 if (arg[i] == delimiter) {
1146 arg[i] = 0;
1147 break;
1148 }
1149 default:
1150 string_ncopy(opt_encoding, arg, strlen(arg));
1151 return OK;
1152 }
1153 }
1155 config_msg = "Unknown variable name";
1156 return ERR;
1157 }
1159 /* Wants: mode request key */
1160 static int
1161 option_bind_command(int argc, char *argv[])
1162 {
1163 enum request request;
1164 int keymap;
1165 int key;
1167 if (argc < 3) {
1168 config_msg = "Wrong number of arguments given to bind command";
1169 return ERR;
1170 }
1172 if (set_keymap(&keymap, argv[0]) == ERR) {
1173 config_msg = "Unknown key map";
1174 return ERR;
1175 }
1177 key = get_key_value(argv[1]);
1178 if (key == ERR) {
1179 config_msg = "Unknown key";
1180 return ERR;
1181 }
1183 request = get_request(argv[2]);
1184 if (request == REQ_NONE) {
1185 const char *obsolete[] = { "cherry-pick" };
1186 size_t namelen = strlen(argv[2]);
1187 int i;
1189 for (i = 0; i < ARRAY_SIZE(obsolete); i++) {
1190 if (namelen == strlen(obsolete[i]) &&
1191 !string_enum_compare(obsolete[i], argv[2], namelen)) {
1192 config_msg = "Obsolete request name";
1193 return ERR;
1194 }
1195 }
1196 }
1197 if (request == REQ_NONE && *argv[2]++ == '!')
1198 request = add_run_request(keymap, key, argc - 2, argv + 2);
1199 if (request == REQ_NONE) {
1200 config_msg = "Unknown request name";
1201 return ERR;
1202 }
1204 add_keybinding(keymap, request, key);
1206 return OK;
1207 }
1209 static int
1210 set_option(char *opt, char *value)
1211 {
1212 char *argv[16];
1213 int valuelen;
1214 int argc = 0;
1216 /* Tokenize */
1217 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1218 argv[argc++] = value;
1219 value += valuelen;
1221 /* Nothing more to tokenize or last available token. */
1222 if (!*value || argc >= ARRAY_SIZE(argv))
1223 break;
1225 *value++ = 0;
1226 while (isspace(*value))
1227 value++;
1228 }
1230 if (!strcmp(opt, "color"))
1231 return option_color_command(argc, argv);
1233 if (!strcmp(opt, "set"))
1234 return option_set_command(argc, argv);
1236 if (!strcmp(opt, "bind"))
1237 return option_bind_command(argc, argv);
1239 config_msg = "Unknown option command";
1240 return ERR;
1241 }
1243 static int
1244 read_option(char *opt, size_t optlen, char *value, size_t valuelen)
1245 {
1246 int status = OK;
1248 config_lineno++;
1249 config_msg = "Internal error";
1251 /* Check for comment markers, since read_properties() will
1252 * only ensure opt and value are split at first " \t". */
1253 optlen = strcspn(opt, "#");
1254 if (optlen == 0)
1255 return OK;
1257 if (opt[optlen] != 0) {
1258 config_msg = "No option value";
1259 status = ERR;
1261 } else {
1262 /* Look for comment endings in the value. */
1263 size_t len = strcspn(value, "#");
1265 if (len < valuelen) {
1266 valuelen = len;
1267 value[valuelen] = 0;
1268 }
1270 status = set_option(opt, value);
1271 }
1273 if (status == ERR) {
1274 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1275 config_lineno, (int) optlen, opt, config_msg);
1276 config_errors = TRUE;
1277 }
1279 /* Always keep going if errors are encountered. */
1280 return OK;
1281 }
1283 static void
1284 load_option_file(const char *path)
1285 {
1286 FILE *file;
1288 /* It's ok that the file doesn't exist. */
1289 file = fopen(path, "r");
1290 if (!file)
1291 return;
1293 config_lineno = 0;
1294 config_errors = FALSE;
1296 if (read_properties(file, " \t", read_option) == ERR ||
1297 config_errors == TRUE)
1298 fprintf(stderr, "Errors while loading %s.\n", path);
1299 }
1301 static int
1302 load_options(void)
1303 {
1304 char *home = getenv("HOME");
1305 char *tigrc_user = getenv("TIGRC_USER");
1306 char *tigrc_system = getenv("TIGRC_SYSTEM");
1307 char buf[SIZEOF_STR];
1309 add_builtin_run_requests();
1311 if (!tigrc_system) {
1312 if (!string_format(buf, "%s/tigrc", SYSCONFDIR))
1313 return ERR;
1314 tigrc_system = buf;
1315 }
1316 load_option_file(tigrc_system);
1318 if (!tigrc_user) {
1319 if (!home || !string_format(buf, "%s/.tigrc", home))
1320 return ERR;
1321 tigrc_user = buf;
1322 }
1323 load_option_file(tigrc_user);
1325 return OK;
1326 }
1329 /*
1330 * The viewer
1331 */
1333 struct view;
1334 struct view_ops;
1336 /* The display array of active views and the index of the current view. */
1337 static struct view *display[2];
1338 static unsigned int current_view;
1340 /* Reading from the prompt? */
1341 static bool input_mode = FALSE;
1343 #define foreach_displayed_view(view, i) \
1344 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1346 #define displayed_views() (display[1] != NULL ? 2 : 1)
1348 /* Current head and commit ID */
1349 static char ref_blob[SIZEOF_REF] = "";
1350 static char ref_commit[SIZEOF_REF] = "HEAD";
1351 static char ref_head[SIZEOF_REF] = "HEAD";
1353 struct view {
1354 const char *name; /* View name */
1355 const char *cmd_fmt; /* Default command line format */
1356 const char *cmd_env; /* Command line set via environment */
1357 const char *id; /* Points to either of ref_{head,commit,blob} */
1359 struct view_ops *ops; /* View operations */
1361 enum keymap keymap; /* What keymap does this view have */
1362 bool git_dir; /* Whether the view requires a git directory. */
1364 char cmd[SIZEOF_STR]; /* Command buffer */
1365 char ref[SIZEOF_REF]; /* Hovered commit reference */
1366 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1368 int height, width; /* The width and height of the main window */
1369 WINDOW *win; /* The main window */
1370 WINDOW *title; /* The title window living below the main window */
1372 /* Navigation */
1373 unsigned long offset; /* Offset of the window top */
1374 unsigned long lineno; /* Current line number */
1376 /* Searching */
1377 char grep[SIZEOF_STR]; /* Search string */
1378 regex_t *regex; /* Pre-compiled regex */
1380 /* If non-NULL, points to the view that opened this view. If this view
1381 * is closed tig will switch back to the parent view. */
1382 struct view *parent;
1384 /* Buffering */
1385 size_t lines; /* Total number of lines */
1386 struct line *line; /* Line index */
1387 size_t line_alloc; /* Total number of allocated lines */
1388 size_t line_size; /* Total number of used lines */
1389 unsigned int digits; /* Number of digits in the lines member. */
1391 /* Loading */
1392 FILE *pipe;
1393 time_t start_time;
1394 };
1396 struct view_ops {
1397 /* What type of content being displayed. Used in the title bar. */
1398 const char *type;
1399 /* Open and reads in all view content. */
1400 bool (*open)(struct view *view);
1401 /* Read one line; updates view->line. */
1402 bool (*read)(struct view *view, char *data);
1403 /* Draw one line; @lineno must be < view->height. */
1404 bool (*draw)(struct view *view, struct line *line, unsigned int lineno, bool selected);
1405 /* Depending on view handle a special requests. */
1406 enum request (*request)(struct view *view, enum request request, struct line *line);
1407 /* Search for regex in a line. */
1408 bool (*grep)(struct view *view, struct line *line);
1409 /* Select line */
1410 void (*select)(struct view *view, struct line *line);
1411 };
1413 static struct view_ops pager_ops;
1414 static struct view_ops main_ops;
1415 static struct view_ops tree_ops;
1416 static struct view_ops blob_ops;
1417 static struct view_ops blame_ops;
1418 static struct view_ops help_ops;
1419 static struct view_ops status_ops;
1420 static struct view_ops stage_ops;
1422 #define VIEW_STR(name, cmd, env, ref, ops, map, git) \
1423 { name, cmd, #env, ref, ops, map, git }
1425 #define VIEW_(id, name, ops, git, ref) \
1426 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id, git)
1429 static struct view views[] = {
1430 VIEW_(MAIN, "main", &main_ops, TRUE, ref_head),
1431 VIEW_(DIFF, "diff", &pager_ops, TRUE, ref_commit),
1432 VIEW_(LOG, "log", &pager_ops, TRUE, ref_head),
1433 VIEW_(TREE, "tree", &tree_ops, TRUE, ref_commit),
1434 VIEW_(BLOB, "blob", &blob_ops, TRUE, ref_blob),
1435 VIEW_(BLAME, "blame", &blame_ops, TRUE, ref_commit),
1436 VIEW_(HELP, "help", &help_ops, FALSE, ""),
1437 VIEW_(PAGER, "pager", &pager_ops, FALSE, "stdin"),
1438 VIEW_(STATUS, "status", &status_ops, TRUE, ""),
1439 VIEW_(STAGE, "stage", &stage_ops, TRUE, ""),
1440 };
1442 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1443 #define VIEW_REQ(view) ((view) - views + REQ_OFFSET + 1)
1445 #define foreach_view(view, i) \
1446 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1448 #define view_is_displayed(view) \
1449 (view == display[0] || view == display[1])
1451 static int
1452 draw_text(struct view *view, const char *string, int max_len,
1453 bool use_tilde, bool selected)
1454 {
1455 int len = 0;
1456 int trimmed = FALSE;
1458 if (max_len <= 0)
1459 return 0;
1461 if (opt_utf8) {
1462 len = utf8_length(string, max_len, &trimmed, use_tilde);
1463 } else {
1464 len = strlen(string);
1465 if (len > max_len) {
1466 if (use_tilde) {
1467 max_len -= 1;
1468 }
1469 len = max_len;
1470 trimmed = TRUE;
1471 }
1472 }
1474 waddnstr(view->win, string, len);
1475 if (trimmed && use_tilde) {
1476 if (!selected)
1477 wattrset(view->win, get_line_attr(LINE_DELIMITER));
1478 waddch(view->win, '~');
1479 len++;
1480 }
1482 return len;
1483 }
1485 static bool
1486 draw_view_line(struct view *view, unsigned int lineno)
1487 {
1488 struct line *line;
1489 bool selected = (view->offset + lineno == view->lineno);
1490 bool draw_ok;
1492 assert(view_is_displayed(view));
1494 if (view->offset + lineno >= view->lines)
1495 return FALSE;
1497 line = &view->line[view->offset + lineno];
1499 if (selected) {
1500 line->selected = TRUE;
1501 view->ops->select(view, line);
1502 } else if (line->selected) {
1503 line->selected = FALSE;
1504 wmove(view->win, lineno, 0);
1505 wclrtoeol(view->win);
1506 }
1508 scrollok(view->win, FALSE);
1509 draw_ok = view->ops->draw(view, line, lineno, selected);
1510 scrollok(view->win, TRUE);
1512 return draw_ok;
1513 }
1515 static void
1516 redraw_view_dirty(struct view *view)
1517 {
1518 bool dirty = FALSE;
1519 int lineno;
1521 for (lineno = 0; lineno < view->height; lineno++) {
1522 struct line *line = &view->line[view->offset + lineno];
1524 if (!line->dirty)
1525 continue;
1526 line->dirty = 0;
1527 dirty = TRUE;
1528 if (!draw_view_line(view, lineno))
1529 break;
1530 }
1532 if (!dirty)
1533 return;
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_from(struct view *view, int lineno)
1543 {
1544 assert(0 <= lineno && lineno < view->height);
1546 for (; lineno < view->height; lineno++) {
1547 if (!draw_view_line(view, lineno))
1548 break;
1549 }
1551 redrawwin(view->win);
1552 if (input_mode)
1553 wnoutrefresh(view->win);
1554 else
1555 wrefresh(view->win);
1556 }
1558 static void
1559 redraw_view(struct view *view)
1560 {
1561 wclear(view->win);
1562 redraw_view_from(view, 0);
1563 }
1566 static void
1567 update_view_title(struct view *view)
1568 {
1569 char buf[SIZEOF_STR];
1570 char state[SIZEOF_STR];
1571 size_t bufpos = 0, statelen = 0;
1573 assert(view_is_displayed(view));
1575 if (view != VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) {
1576 unsigned int view_lines = view->offset + view->height;
1577 unsigned int lines = view->lines
1578 ? MIN(view_lines, view->lines) * 100 / view->lines
1579 : 0;
1581 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1582 view->ops->type,
1583 view->lineno + 1,
1584 view->lines,
1585 lines);
1587 if (view->pipe) {
1588 time_t secs = time(NULL) - view->start_time;
1590 /* Three git seconds are a long time ... */
1591 if (secs > 2)
1592 string_format_from(state, &statelen, " %lds", secs);
1593 }
1594 }
1596 string_format_from(buf, &bufpos, "[%s]", view->name);
1597 if (*view->ref && bufpos < view->width) {
1598 size_t refsize = strlen(view->ref);
1599 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1601 if (minsize < view->width)
1602 refsize = view->width - minsize + 7;
1603 string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
1604 }
1606 if (statelen && bufpos < view->width) {
1607 string_format_from(buf, &bufpos, " %s", state);
1608 }
1610 if (view == display[current_view])
1611 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1612 else
1613 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1615 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1616 wclrtoeol(view->title);
1617 wmove(view->title, 0, view->width - 1);
1619 if (input_mode)
1620 wnoutrefresh(view->title);
1621 else
1622 wrefresh(view->title);
1623 }
1625 static void
1626 resize_display(void)
1627 {
1628 int offset, i;
1629 struct view *base = display[0];
1630 struct view *view = display[1] ? display[1] : display[0];
1632 /* Setup window dimensions */
1634 getmaxyx(stdscr, base->height, base->width);
1636 /* Make room for the status window. */
1637 base->height -= 1;
1639 if (view != base) {
1640 /* Horizontal split. */
1641 view->width = base->width;
1642 view->height = SCALE_SPLIT_VIEW(base->height);
1643 base->height -= view->height;
1645 /* Make room for the title bar. */
1646 view->height -= 1;
1647 }
1649 /* Make room for the title bar. */
1650 base->height -= 1;
1652 offset = 0;
1654 foreach_displayed_view (view, i) {
1655 if (!view->win) {
1656 view->win = newwin(view->height, 0, offset, 0);
1657 if (!view->win)
1658 die("Failed to create %s view", view->name);
1660 scrollok(view->win, TRUE);
1662 view->title = newwin(1, 0, offset + view->height, 0);
1663 if (!view->title)
1664 die("Failed to create title window");
1666 } else {
1667 wresize(view->win, view->height, view->width);
1668 mvwin(view->win, offset, 0);
1669 mvwin(view->title, offset + view->height, 0);
1670 }
1672 offset += view->height + 1;
1673 }
1674 }
1676 static void
1677 redraw_display(void)
1678 {
1679 struct view *view;
1680 int i;
1682 foreach_displayed_view (view, i) {
1683 redraw_view(view);
1684 update_view_title(view);
1685 }
1686 }
1688 static void
1689 update_display_cursor(struct view *view)
1690 {
1691 /* Move the cursor to the right-most column of the cursor line.
1692 *
1693 * XXX: This could turn out to be a bit expensive, but it ensures that
1694 * the cursor does not jump around. */
1695 if (view->lines) {
1696 wmove(view->win, view->lineno - view->offset, view->width - 1);
1697 wrefresh(view->win);
1698 }
1699 }
1701 /*
1702 * Navigation
1703 */
1705 /* Scrolling backend */
1706 static void
1707 do_scroll_view(struct view *view, int lines)
1708 {
1709 bool redraw_current_line = FALSE;
1711 /* The rendering expects the new offset. */
1712 view->offset += lines;
1714 assert(0 <= view->offset && view->offset < view->lines);
1715 assert(lines);
1717 /* Move current line into the view. */
1718 if (view->lineno < view->offset) {
1719 view->lineno = view->offset;
1720 redraw_current_line = TRUE;
1721 } else if (view->lineno >= view->offset + view->height) {
1722 view->lineno = view->offset + view->height - 1;
1723 redraw_current_line = TRUE;
1724 }
1726 assert(view->offset <= view->lineno && view->lineno < view->lines);
1728 /* Redraw the whole screen if scrolling is pointless. */
1729 if (view->height < ABS(lines)) {
1730 redraw_view(view);
1732 } else {
1733 int line = lines > 0 ? view->height - lines : 0;
1734 int end = line + ABS(lines);
1736 wscrl(view->win, lines);
1738 for (; line < end; line++) {
1739 if (!draw_view_line(view, line))
1740 break;
1741 }
1743 if (redraw_current_line)
1744 draw_view_line(view, view->lineno - view->offset);
1745 }
1747 redrawwin(view->win);
1748 wrefresh(view->win);
1749 report("");
1750 }
1752 /* Scroll frontend */
1753 static void
1754 scroll_view(struct view *view, enum request request)
1755 {
1756 int lines = 1;
1758 assert(view_is_displayed(view));
1760 switch (request) {
1761 case REQ_SCROLL_PAGE_DOWN:
1762 lines = view->height;
1763 case REQ_SCROLL_LINE_DOWN:
1764 if (view->offset + lines > view->lines)
1765 lines = view->lines - view->offset;
1767 if (lines == 0 || view->offset + view->height >= view->lines) {
1768 report("Cannot scroll beyond the last line");
1769 return;
1770 }
1771 break;
1773 case REQ_SCROLL_PAGE_UP:
1774 lines = view->height;
1775 case REQ_SCROLL_LINE_UP:
1776 if (lines > view->offset)
1777 lines = view->offset;
1779 if (lines == 0) {
1780 report("Cannot scroll beyond the first line");
1781 return;
1782 }
1784 lines = -lines;
1785 break;
1787 default:
1788 die("request %d not handled in switch", request);
1789 }
1791 do_scroll_view(view, lines);
1792 }
1794 /* Cursor moving */
1795 static void
1796 move_view(struct view *view, enum request request)
1797 {
1798 int scroll_steps = 0;
1799 int steps;
1801 switch (request) {
1802 case REQ_MOVE_FIRST_LINE:
1803 steps = -view->lineno;
1804 break;
1806 case REQ_MOVE_LAST_LINE:
1807 steps = view->lines - view->lineno - 1;
1808 break;
1810 case REQ_MOVE_PAGE_UP:
1811 steps = view->height > view->lineno
1812 ? -view->lineno : -view->height;
1813 break;
1815 case REQ_MOVE_PAGE_DOWN:
1816 steps = view->lineno + view->height >= view->lines
1817 ? view->lines - view->lineno - 1 : view->height;
1818 break;
1820 case REQ_MOVE_UP:
1821 steps = -1;
1822 break;
1824 case REQ_MOVE_DOWN:
1825 steps = 1;
1826 break;
1828 default:
1829 die("request %d not handled in switch", request);
1830 }
1832 if (steps <= 0 && view->lineno == 0) {
1833 report("Cannot move beyond the first line");
1834 return;
1836 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
1837 report("Cannot move beyond the last line");
1838 return;
1839 }
1841 /* Move the current line */
1842 view->lineno += steps;
1843 assert(0 <= view->lineno && view->lineno < view->lines);
1845 /* Check whether the view needs to be scrolled */
1846 if (view->lineno < view->offset ||
1847 view->lineno >= view->offset + view->height) {
1848 scroll_steps = steps;
1849 if (steps < 0 && -steps > view->offset) {
1850 scroll_steps = -view->offset;
1852 } else if (steps > 0) {
1853 if (view->lineno == view->lines - 1 &&
1854 view->lines > view->height) {
1855 scroll_steps = view->lines - view->offset - 1;
1856 if (scroll_steps >= view->height)
1857 scroll_steps -= view->height - 1;
1858 }
1859 }
1860 }
1862 if (!view_is_displayed(view)) {
1863 view->offset += scroll_steps;
1864 assert(0 <= view->offset && view->offset < view->lines);
1865 view->ops->select(view, &view->line[view->lineno]);
1866 return;
1867 }
1869 /* Repaint the old "current" line if we be scrolling */
1870 if (ABS(steps) < view->height)
1871 draw_view_line(view, view->lineno - steps - view->offset);
1873 if (scroll_steps) {
1874 do_scroll_view(view, scroll_steps);
1875 return;
1876 }
1878 /* Draw the current line */
1879 draw_view_line(view, view->lineno - view->offset);
1881 redrawwin(view->win);
1882 wrefresh(view->win);
1883 report("");
1884 }
1887 /*
1888 * Searching
1889 */
1891 static void search_view(struct view *view, enum request request);
1893 static bool
1894 find_next_line(struct view *view, unsigned long lineno, struct line *line)
1895 {
1896 assert(view_is_displayed(view));
1898 if (!view->ops->grep(view, line))
1899 return FALSE;
1901 if (lineno - view->offset >= view->height) {
1902 view->offset = lineno;
1903 view->lineno = lineno;
1904 redraw_view(view);
1906 } else {
1907 unsigned long old_lineno = view->lineno - view->offset;
1909 view->lineno = lineno;
1910 draw_view_line(view, old_lineno);
1912 draw_view_line(view, view->lineno - view->offset);
1913 redrawwin(view->win);
1914 wrefresh(view->win);
1915 }
1917 report("Line %ld matches '%s'", lineno + 1, view->grep);
1918 return TRUE;
1919 }
1921 static void
1922 find_next(struct view *view, enum request request)
1923 {
1924 unsigned long lineno = view->lineno;
1925 int direction;
1927 if (!*view->grep) {
1928 if (!*opt_search)
1929 report("No previous search");
1930 else
1931 search_view(view, request);
1932 return;
1933 }
1935 switch (request) {
1936 case REQ_SEARCH:
1937 case REQ_FIND_NEXT:
1938 direction = 1;
1939 break;
1941 case REQ_SEARCH_BACK:
1942 case REQ_FIND_PREV:
1943 direction = -1;
1944 break;
1946 default:
1947 return;
1948 }
1950 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
1951 lineno += direction;
1953 /* Note, lineno is unsigned long so will wrap around in which case it
1954 * will become bigger than view->lines. */
1955 for (; lineno < view->lines; lineno += direction) {
1956 struct line *line = &view->line[lineno];
1958 if (find_next_line(view, lineno, line))
1959 return;
1960 }
1962 report("No match found for '%s'", view->grep);
1963 }
1965 static void
1966 search_view(struct view *view, enum request request)
1967 {
1968 int regex_err;
1970 if (view->regex) {
1971 regfree(view->regex);
1972 *view->grep = 0;
1973 } else {
1974 view->regex = calloc(1, sizeof(*view->regex));
1975 if (!view->regex)
1976 return;
1977 }
1979 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
1980 if (regex_err != 0) {
1981 char buf[SIZEOF_STR] = "unknown error";
1983 regerror(regex_err, view->regex, buf, sizeof(buf));
1984 report("Search failed: %s", buf);
1985 return;
1986 }
1988 string_copy(view->grep, opt_search);
1990 find_next(view, request);
1991 }
1993 /*
1994 * Incremental updating
1995 */
1997 static void
1998 end_update(struct view *view)
1999 {
2000 if (!view->pipe)
2001 return;
2002 set_nonblocking_input(FALSE);
2003 if (view->pipe == stdin)
2004 fclose(view->pipe);
2005 else
2006 pclose(view->pipe);
2007 view->pipe = NULL;
2008 }
2010 static bool
2011 begin_update(struct view *view)
2012 {
2013 if (view->pipe)
2014 end_update(view);
2016 if (opt_cmd[0]) {
2017 string_copy(view->cmd, opt_cmd);
2018 opt_cmd[0] = 0;
2019 /* When running random commands, initially show the
2020 * command in the title. However, it maybe later be
2021 * overwritten if a commit line is selected. */
2022 if (view == VIEW(REQ_VIEW_PAGER))
2023 string_copy(view->ref, view->cmd);
2024 else
2025 view->ref[0] = 0;
2027 } else if (view == VIEW(REQ_VIEW_TREE)) {
2028 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2029 char path[SIZEOF_STR];
2031 if (strcmp(view->vid, view->id))
2032 opt_path[0] = path[0] = 0;
2033 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
2034 return FALSE;
2036 if (!string_format(view->cmd, format, view->id, path))
2037 return FALSE;
2039 } else {
2040 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2041 const char *id = view->id;
2043 if (!string_format(view->cmd, format, id, id, id, id, id))
2044 return FALSE;
2046 /* Put the current ref_* value to the view title ref
2047 * member. This is needed by the blob view. Most other
2048 * views sets it automatically after loading because the
2049 * first line is a commit line. */
2050 string_copy_rev(view->ref, view->id);
2051 }
2053 /* Special case for the pager view. */
2054 if (opt_pipe) {
2055 view->pipe = opt_pipe;
2056 opt_pipe = NULL;
2057 } else {
2058 view->pipe = popen(view->cmd, "r");
2059 }
2061 if (!view->pipe)
2062 return FALSE;
2064 set_nonblocking_input(TRUE);
2066 view->offset = 0;
2067 view->lines = 0;
2068 view->lineno = 0;
2069 string_copy_rev(view->vid, view->id);
2071 if (view->line) {
2072 int i;
2074 for (i = 0; i < view->lines; i++)
2075 if (view->line[i].data)
2076 free(view->line[i].data);
2078 free(view->line);
2079 view->line = NULL;
2080 }
2082 view->start_time = time(NULL);
2084 return TRUE;
2085 }
2087 #define ITEM_CHUNK_SIZE 256
2088 static void *
2089 realloc_items(void *mem, size_t *size, size_t new_size, size_t item_size)
2090 {
2091 size_t num_chunks = *size / ITEM_CHUNK_SIZE;
2092 size_t num_chunks_new = (new_size + ITEM_CHUNK_SIZE - 1) / ITEM_CHUNK_SIZE;
2094 if (mem == NULL || num_chunks != num_chunks_new) {
2095 *size = num_chunks_new * ITEM_CHUNK_SIZE;
2096 mem = realloc(mem, *size * item_size);
2097 }
2099 return mem;
2100 }
2102 static struct line *
2103 realloc_lines(struct view *view, size_t line_size)
2104 {
2105 size_t alloc = view->line_alloc;
2106 struct line *tmp = realloc_items(view->line, &alloc, line_size,
2107 sizeof(*view->line));
2109 if (!tmp)
2110 return NULL;
2112 view->line = tmp;
2113 view->line_alloc = alloc;
2114 view->line_size = line_size;
2115 return view->line;
2116 }
2118 static bool
2119 update_view(struct view *view)
2120 {
2121 char in_buffer[BUFSIZ];
2122 char out_buffer[BUFSIZ * 2];
2123 char *line;
2124 /* The number of lines to read. If too low it will cause too much
2125 * redrawing (and possible flickering), if too high responsiveness
2126 * will suffer. */
2127 unsigned long lines = view->height;
2128 int redraw_from = -1;
2130 if (!view->pipe)
2131 return TRUE;
2133 /* Only redraw if lines are visible. */
2134 if (view->offset + view->height >= view->lines)
2135 redraw_from = view->lines - view->offset;
2137 /* FIXME: This is probably not perfect for backgrounded views. */
2138 if (!realloc_lines(view, view->lines + lines))
2139 goto alloc_error;
2141 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
2142 size_t linelen = strlen(line);
2144 if (linelen)
2145 line[linelen - 1] = 0;
2147 if (opt_iconv != ICONV_NONE) {
2148 ICONV_CONST char *inbuf = line;
2149 size_t inlen = linelen;
2151 char *outbuf = out_buffer;
2152 size_t outlen = sizeof(out_buffer);
2154 size_t ret;
2156 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
2157 if (ret != (size_t) -1) {
2158 line = out_buffer;
2159 linelen = strlen(out_buffer);
2160 }
2161 }
2163 if (!view->ops->read(view, line))
2164 goto alloc_error;
2166 if (lines-- == 1)
2167 break;
2168 }
2170 {
2171 int digits;
2173 lines = view->lines;
2174 for (digits = 0; lines; digits++)
2175 lines /= 10;
2177 /* Keep the displayed view in sync with line number scaling. */
2178 if (digits != view->digits) {
2179 view->digits = digits;
2180 redraw_from = 0;
2181 }
2182 }
2184 if (!view_is_displayed(view))
2185 goto check_pipe;
2187 if (view == VIEW(REQ_VIEW_TREE)) {
2188 /* Clear the view and redraw everything since the tree sorting
2189 * might have rearranged things. */
2190 redraw_view(view);
2192 } else if (redraw_from >= 0) {
2193 /* If this is an incremental update, redraw the previous line
2194 * since for commits some members could have changed when
2195 * loading the main view. */
2196 if (redraw_from > 0)
2197 redraw_from--;
2199 /* Since revision graph visualization requires knowledge
2200 * about the parent commit, it causes a further one-off
2201 * needed to be redrawn for incremental updates. */
2202 if (redraw_from > 0 && opt_rev_graph)
2203 redraw_from--;
2205 /* Incrementally draw avoids flickering. */
2206 redraw_view_from(view, redraw_from);
2207 }
2209 if (view == VIEW(REQ_VIEW_BLAME))
2210 redraw_view_dirty(view);
2212 /* Update the title _after_ the redraw so that if the redraw picks up a
2213 * commit reference in view->ref it'll be available here. */
2214 update_view_title(view);
2216 check_pipe:
2217 if (ferror(view->pipe)) {
2218 report("Failed to read: %s", strerror(errno));
2219 goto end;
2221 } else if (feof(view->pipe)) {
2222 report("");
2223 goto end;
2224 }
2226 return TRUE;
2228 alloc_error:
2229 report("Allocation failure");
2231 end:
2232 if (view->ops->read(view, NULL))
2233 end_update(view);
2234 return FALSE;
2235 }
2237 static struct line *
2238 add_line_data(struct view *view, void *data, enum line_type type)
2239 {
2240 struct line *line = &view->line[view->lines++];
2242 memset(line, 0, sizeof(*line));
2243 line->type = type;
2244 line->data = data;
2246 return line;
2247 }
2249 static struct line *
2250 add_line_text(struct view *view, char *data, enum line_type type)
2251 {
2252 if (data)
2253 data = strdup(data);
2255 return data ? add_line_data(view, data, type) : NULL;
2256 }
2259 /*
2260 * View opening
2261 */
2263 enum open_flags {
2264 OPEN_DEFAULT = 0, /* Use default view switching. */
2265 OPEN_SPLIT = 1, /* Split current view. */
2266 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2267 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2268 };
2270 static void
2271 open_view(struct view *prev, enum request request, enum open_flags flags)
2272 {
2273 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2274 bool split = !!(flags & OPEN_SPLIT);
2275 bool reload = !!(flags & OPEN_RELOAD);
2276 struct view *view = VIEW(request);
2277 int nviews = displayed_views();
2278 struct view *base_view = display[0];
2280 if (view == prev && nviews == 1 && !reload) {
2281 report("Already in %s view", view->name);
2282 return;
2283 }
2285 if (view->git_dir && !opt_git_dir[0]) {
2286 report("The %s view is disabled in pager view", view->name);
2287 return;
2288 }
2290 if (split) {
2291 display[1] = view;
2292 if (!backgrounded)
2293 current_view = 1;
2294 } else {
2295 /* Maximize the current view. */
2296 memset(display, 0, sizeof(display));
2297 current_view = 0;
2298 display[current_view] = view;
2299 }
2301 /* Resize the view when switching between split- and full-screen,
2302 * or when switching between two different full-screen views. */
2303 if (nviews != displayed_views() ||
2304 (nviews == 1 && base_view != display[0]))
2305 resize_display();
2307 if (view->ops->open) {
2308 if (!view->ops->open(view)) {
2309 report("Failed to load %s view", view->name);
2310 return;
2311 }
2313 } else if ((reload || strcmp(view->vid, view->id)) &&
2314 !begin_update(view)) {
2315 report("Failed to load %s view", view->name);
2316 return;
2317 }
2319 if (split && prev->lineno - prev->offset >= prev->height) {
2320 /* Take the title line into account. */
2321 int lines = prev->lineno - prev->offset - prev->height + 1;
2323 /* Scroll the view that was split if the current line is
2324 * outside the new limited view. */
2325 do_scroll_view(prev, lines);
2326 }
2328 if (prev && view != prev) {
2329 if (split && !backgrounded) {
2330 /* "Blur" the previous view. */
2331 update_view_title(prev);
2332 }
2334 view->parent = prev;
2335 }
2337 if (view->pipe && view->lines == 0) {
2338 /* Clear the old view and let the incremental updating refill
2339 * the screen. */
2340 wclear(view->win);
2341 report("");
2342 } else {
2343 redraw_view(view);
2344 report("");
2345 }
2347 /* If the view is backgrounded the above calls to report()
2348 * won't redraw the view title. */
2349 if (backgrounded)
2350 update_view_title(view);
2351 }
2353 static void
2354 open_external_viewer(const char *cmd)
2355 {
2356 def_prog_mode(); /* save current tty modes */
2357 endwin(); /* restore original tty modes */
2358 system(cmd);
2359 fprintf(stderr, "Press Enter to continue");
2360 getc(stdin);
2361 reset_prog_mode();
2362 redraw_display();
2363 }
2365 static void
2366 open_mergetool(const char *file)
2367 {
2368 char cmd[SIZEOF_STR];
2369 char file_sq[SIZEOF_STR];
2371 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2372 string_format(cmd, "git mergetool %s", file_sq)) {
2373 open_external_viewer(cmd);
2374 }
2375 }
2377 static void
2378 open_editor(bool from_root, const char *file)
2379 {
2380 char cmd[SIZEOF_STR];
2381 char file_sq[SIZEOF_STR];
2382 char *editor;
2383 char *prefix = from_root ? opt_cdup : "";
2385 editor = getenv("GIT_EDITOR");
2386 if (!editor && *opt_editor)
2387 editor = opt_editor;
2388 if (!editor)
2389 editor = getenv("VISUAL");
2390 if (!editor)
2391 editor = getenv("EDITOR");
2392 if (!editor)
2393 editor = "vi";
2395 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2396 string_format(cmd, "%s %s%s", editor, prefix, file_sq)) {
2397 open_external_viewer(cmd);
2398 }
2399 }
2401 static void
2402 open_run_request(enum request request)
2403 {
2404 struct run_request *req = get_run_request(request);
2405 char buf[SIZEOF_STR * 2];
2406 size_t bufpos;
2407 char *cmd;
2409 if (!req) {
2410 report("Unknown run request");
2411 return;
2412 }
2414 bufpos = 0;
2415 cmd = req->cmd;
2417 while (cmd) {
2418 char *next = strstr(cmd, "%(");
2419 int len = next - cmd;
2420 char *value;
2422 if (!next) {
2423 len = strlen(cmd);
2424 value = "";
2426 } else if (!strncmp(next, "%(head)", 7)) {
2427 value = ref_head;
2429 } else if (!strncmp(next, "%(commit)", 9)) {
2430 value = ref_commit;
2432 } else if (!strncmp(next, "%(blob)", 7)) {
2433 value = ref_blob;
2435 } else {
2436 report("Unknown replacement in run request: `%s`", req->cmd);
2437 return;
2438 }
2440 if (!string_format_from(buf, &bufpos, "%.*s%s", len, cmd, value))
2441 return;
2443 if (next)
2444 next = strchr(next, ')') + 1;
2445 cmd = next;
2446 }
2448 open_external_viewer(buf);
2449 }
2451 /*
2452 * User request switch noodle
2453 */
2455 static int
2456 view_driver(struct view *view, enum request request)
2457 {
2458 int i;
2460 if (request == REQ_NONE) {
2461 doupdate();
2462 return TRUE;
2463 }
2465 if (request > REQ_NONE) {
2466 open_run_request(request);
2467 return TRUE;
2468 }
2470 if (view && view->lines) {
2471 request = view->ops->request(view, request, &view->line[view->lineno]);
2472 if (request == REQ_NONE)
2473 return TRUE;
2474 }
2476 switch (request) {
2477 case REQ_MOVE_UP:
2478 case REQ_MOVE_DOWN:
2479 case REQ_MOVE_PAGE_UP:
2480 case REQ_MOVE_PAGE_DOWN:
2481 case REQ_MOVE_FIRST_LINE:
2482 case REQ_MOVE_LAST_LINE:
2483 move_view(view, request);
2484 break;
2486 case REQ_SCROLL_LINE_DOWN:
2487 case REQ_SCROLL_LINE_UP:
2488 case REQ_SCROLL_PAGE_DOWN:
2489 case REQ_SCROLL_PAGE_UP:
2490 scroll_view(view, request);
2491 break;
2493 case REQ_VIEW_BLAME:
2494 if (!opt_file[0]) {
2495 report("No file chosen, press %s to open tree view",
2496 get_key(REQ_VIEW_TREE));
2497 break;
2498 }
2499 open_view(view, request, OPEN_DEFAULT);
2500 break;
2502 case REQ_VIEW_BLOB:
2503 if (!ref_blob[0]) {
2504 report("No file chosen, press %s to open tree view",
2505 get_key(REQ_VIEW_TREE));
2506 break;
2507 }
2508 open_view(view, request, OPEN_DEFAULT);
2509 break;
2511 case REQ_VIEW_PAGER:
2512 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2513 report("No pager content, press %s to run command from prompt",
2514 get_key(REQ_PROMPT));
2515 break;
2516 }
2517 open_view(view, request, OPEN_DEFAULT);
2518 break;
2520 case REQ_VIEW_STAGE:
2521 if (!VIEW(REQ_VIEW_STAGE)->lines) {
2522 report("No stage content, press %s to open the status view and choose file",
2523 get_key(REQ_VIEW_STATUS));
2524 break;
2525 }
2526 open_view(view, request, OPEN_DEFAULT);
2527 break;
2529 case REQ_VIEW_STATUS:
2530 if (opt_is_inside_work_tree == FALSE) {
2531 report("The status view requires a working tree");
2532 break;
2533 }
2534 open_view(view, request, OPEN_DEFAULT);
2535 break;
2537 case REQ_VIEW_MAIN:
2538 case REQ_VIEW_DIFF:
2539 case REQ_VIEW_LOG:
2540 case REQ_VIEW_TREE:
2541 case REQ_VIEW_HELP:
2542 open_view(view, request, OPEN_DEFAULT);
2543 break;
2545 case REQ_NEXT:
2546 case REQ_PREVIOUS:
2547 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2549 if ((view == VIEW(REQ_VIEW_DIFF) &&
2550 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2551 (view == VIEW(REQ_VIEW_DIFF) &&
2552 view->parent == VIEW(REQ_VIEW_BLAME)) ||
2553 (view == VIEW(REQ_VIEW_STAGE) &&
2554 view->parent == VIEW(REQ_VIEW_STATUS)) ||
2555 (view == VIEW(REQ_VIEW_BLOB) &&
2556 view->parent == VIEW(REQ_VIEW_TREE))) {
2557 int line;
2559 view = view->parent;
2560 line = view->lineno;
2561 move_view(view, request);
2562 if (view_is_displayed(view))
2563 update_view_title(view);
2564 if (line != view->lineno)
2565 view->ops->request(view, REQ_ENTER,
2566 &view->line[view->lineno]);
2568 } else {
2569 move_view(view, request);
2570 }
2571 break;
2573 case REQ_VIEW_NEXT:
2574 {
2575 int nviews = displayed_views();
2576 int next_view = (current_view + 1) % nviews;
2578 if (next_view == current_view) {
2579 report("Only one view is displayed");
2580 break;
2581 }
2583 current_view = next_view;
2584 /* Blur out the title of the previous view. */
2585 update_view_title(view);
2586 report("");
2587 break;
2588 }
2589 case REQ_REFRESH:
2590 report("Refreshing is not yet supported for the %s view", view->name);
2591 break;
2593 case REQ_MAXIMIZE:
2594 if (displayed_views() == 2)
2595 open_view(view, VIEW_REQ(view), OPEN_DEFAULT);
2596 break;
2598 case REQ_TOGGLE_LINENO:
2599 opt_line_number = !opt_line_number;
2600 redraw_display();
2601 break;
2603 case REQ_TOGGLE_DATE:
2604 opt_date = !opt_date;
2605 redraw_display();
2606 break;
2608 case REQ_TOGGLE_AUTHOR:
2609 opt_author = !opt_author;
2610 redraw_display();
2611 break;
2613 case REQ_TOGGLE_REV_GRAPH:
2614 opt_rev_graph = !opt_rev_graph;
2615 redraw_display();
2616 break;
2618 case REQ_TOGGLE_REFS:
2619 opt_show_refs = !opt_show_refs;
2620 redraw_display();
2621 break;
2623 case REQ_PROMPT:
2624 /* Always reload^Wrerun commands from the prompt. */
2625 open_view(view, opt_request, OPEN_RELOAD);
2626 break;
2628 case REQ_SEARCH:
2629 case REQ_SEARCH_BACK:
2630 search_view(view, request);
2631 break;
2633 case REQ_FIND_NEXT:
2634 case REQ_FIND_PREV:
2635 find_next(view, request);
2636 break;
2638 case REQ_STOP_LOADING:
2639 for (i = 0; i < ARRAY_SIZE(views); i++) {
2640 view = &views[i];
2641 if (view->pipe)
2642 report("Stopped loading the %s view", view->name),
2643 end_update(view);
2644 }
2645 break;
2647 case REQ_SHOW_VERSION:
2648 report("tig-%s (built %s)", TIG_VERSION, __DATE__);
2649 return TRUE;
2651 case REQ_SCREEN_RESIZE:
2652 resize_display();
2653 /* Fall-through */
2654 case REQ_SCREEN_REDRAW:
2655 redraw_display();
2656 break;
2658 case REQ_EDIT:
2659 report("Nothing to edit");
2660 break;
2663 case REQ_ENTER:
2664 report("Nothing to enter");
2665 break;
2668 case REQ_VIEW_CLOSE:
2669 /* XXX: Mark closed views by letting view->parent point to the
2670 * view itself. Parents to closed view should never be
2671 * followed. */
2672 if (view->parent &&
2673 view->parent->parent != view->parent) {
2674 memset(display, 0, sizeof(display));
2675 current_view = 0;
2676 display[current_view] = view->parent;
2677 view->parent = view;
2678 resize_display();
2679 redraw_display();
2680 break;
2681 }
2682 /* Fall-through */
2683 case REQ_QUIT:
2684 return FALSE;
2686 default:
2687 /* An unknown key will show most commonly used commands. */
2688 report("Unknown key, press 'h' for help");
2689 return TRUE;
2690 }
2692 return TRUE;
2693 }
2696 /*
2697 * Pager backend
2698 */
2700 static bool
2701 pager_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
2702 {
2703 char *text = line->data;
2704 enum line_type type = line->type;
2705 int attr;
2707 wmove(view->win, lineno, 0);
2709 if (selected) {
2710 type = LINE_CURSOR;
2711 wchgat(view->win, -1, 0, type, NULL);
2712 }
2714 attr = get_line_attr(type);
2715 wattrset(view->win, attr);
2717 if (opt_line_number || opt_tab_size < TABSIZE) {
2718 static char spaces[] = " ";
2719 int col_offset = 0, col = 0;
2721 if (opt_line_number) {
2722 unsigned long real_lineno = view->offset + lineno + 1;
2724 if (real_lineno == 1 ||
2725 (real_lineno % opt_num_interval) == 0) {
2726 wprintw(view->win, "%.*d", view->digits, real_lineno);
2728 } else {
2729 waddnstr(view->win, spaces,
2730 MIN(view->digits, STRING_SIZE(spaces)));
2731 }
2732 waddstr(view->win, ": ");
2733 col_offset = view->digits + 2;
2734 }
2736 while (text && col_offset + col < view->width) {
2737 int cols_max = view->width - col_offset - col;
2738 char *pos = text;
2739 int cols;
2741 if (*text == '\t') {
2742 text++;
2743 assert(sizeof(spaces) > TABSIZE);
2744 pos = spaces;
2745 cols = opt_tab_size - (col % opt_tab_size);
2747 } else {
2748 text = strchr(text, '\t');
2749 cols = line ? text - pos : strlen(pos);
2750 }
2752 waddnstr(view->win, pos, MIN(cols, cols_max));
2753 col += cols;
2754 }
2756 } else {
2757 draw_text(view, text, view->width, TRUE, selected);
2758 }
2760 return TRUE;
2761 }
2763 static bool
2764 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2765 {
2766 char refbuf[SIZEOF_STR];
2767 char *ref = NULL;
2768 FILE *pipe;
2770 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2771 return TRUE;
2773 pipe = popen(refbuf, "r");
2774 if (!pipe)
2775 return TRUE;
2777 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2778 ref = chomp_string(ref);
2779 pclose(pipe);
2781 if (!ref || !*ref)
2782 return TRUE;
2784 /* This is the only fatal call, since it can "corrupt" the buffer. */
2785 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2786 return FALSE;
2788 return TRUE;
2789 }
2791 static void
2792 add_pager_refs(struct view *view, struct line *line)
2793 {
2794 char buf[SIZEOF_STR];
2795 char *commit_id = (char *)line->data + STRING_SIZE("commit ");
2796 struct ref **refs;
2797 size_t bufpos = 0, refpos = 0;
2798 const char *sep = "Refs: ";
2799 bool is_tag = FALSE;
2801 assert(line->type == LINE_COMMIT);
2803 refs = get_refs(commit_id);
2804 if (!refs) {
2805 if (view == VIEW(REQ_VIEW_DIFF))
2806 goto try_add_describe_ref;
2807 return;
2808 }
2810 do {
2811 struct ref *ref = refs[refpos];
2812 char *fmt = ref->tag ? "%s[%s]" :
2813 ref->remote ? "%s<%s>" : "%s%s";
2815 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2816 return;
2817 sep = ", ";
2818 if (ref->tag)
2819 is_tag = TRUE;
2820 } while (refs[refpos++]->next);
2822 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2823 try_add_describe_ref:
2824 /* Add <tag>-g<commit_id> "fake" reference. */
2825 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2826 return;
2827 }
2829 if (bufpos == 0)
2830 return;
2832 if (!realloc_lines(view, view->line_size + 1))
2833 return;
2835 add_line_text(view, buf, LINE_PP_REFS);
2836 }
2838 static bool
2839 pager_read(struct view *view, char *data)
2840 {
2841 struct line *line;
2843 if (!data)
2844 return TRUE;
2846 line = add_line_text(view, data, get_line_type(data));
2847 if (!line)
2848 return FALSE;
2850 if (line->type == LINE_COMMIT &&
2851 (view == VIEW(REQ_VIEW_DIFF) ||
2852 view == VIEW(REQ_VIEW_LOG)))
2853 add_pager_refs(view, line);
2855 return TRUE;
2856 }
2858 static enum request
2859 pager_request(struct view *view, enum request request, struct line *line)
2860 {
2861 int split = 0;
2863 if (request != REQ_ENTER)
2864 return request;
2866 if (line->type == LINE_COMMIT &&
2867 (view == VIEW(REQ_VIEW_LOG) ||
2868 view == VIEW(REQ_VIEW_PAGER))) {
2869 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2870 split = 1;
2871 }
2873 /* Always scroll the view even if it was split. That way
2874 * you can use Enter to scroll through the log view and
2875 * split open each commit diff. */
2876 scroll_view(view, REQ_SCROLL_LINE_DOWN);
2878 /* FIXME: A minor workaround. Scrolling the view will call report("")
2879 * but if we are scrolling a non-current view this won't properly
2880 * update the view title. */
2881 if (split)
2882 update_view_title(view);
2884 return REQ_NONE;
2885 }
2887 static bool
2888 pager_grep(struct view *view, struct line *line)
2889 {
2890 regmatch_t pmatch;
2891 char *text = line->data;
2893 if (!*text)
2894 return FALSE;
2896 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
2897 return FALSE;
2899 return TRUE;
2900 }
2902 static void
2903 pager_select(struct view *view, struct line *line)
2904 {
2905 if (line->type == LINE_COMMIT) {
2906 char *text = (char *)line->data + STRING_SIZE("commit ");
2908 if (view != VIEW(REQ_VIEW_PAGER))
2909 string_copy_rev(view->ref, text);
2910 string_copy_rev(ref_commit, text);
2911 }
2912 }
2914 static struct view_ops pager_ops = {
2915 "line",
2916 NULL,
2917 pager_read,
2918 pager_draw,
2919 pager_request,
2920 pager_grep,
2921 pager_select,
2922 };
2925 /*
2926 * Help backend
2927 */
2929 static bool
2930 help_open(struct view *view)
2931 {
2932 char buf[BUFSIZ];
2933 int lines = ARRAY_SIZE(req_info) + 2;
2934 int i;
2936 if (view->lines > 0)
2937 return TRUE;
2939 for (i = 0; i < ARRAY_SIZE(req_info); i++)
2940 if (!req_info[i].request)
2941 lines++;
2943 lines += run_requests + 1;
2945 view->line = calloc(lines, sizeof(*view->line));
2946 if (!view->line)
2947 return FALSE;
2949 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
2951 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
2952 char *key;
2954 if (req_info[i].request == REQ_NONE)
2955 continue;
2957 if (!req_info[i].request) {
2958 add_line_text(view, "", LINE_DEFAULT);
2959 add_line_text(view, req_info[i].help, LINE_DEFAULT);
2960 continue;
2961 }
2963 key = get_key(req_info[i].request);
2964 if (!*key)
2965 key = "(no key defined)";
2967 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
2968 continue;
2970 add_line_text(view, buf, LINE_DEFAULT);
2971 }
2973 if (run_requests) {
2974 add_line_text(view, "", LINE_DEFAULT);
2975 add_line_text(view, "External commands:", LINE_DEFAULT);
2976 }
2978 for (i = 0; i < run_requests; i++) {
2979 struct run_request *req = get_run_request(REQ_NONE + i + 1);
2980 char *key;
2982 if (!req)
2983 continue;
2985 key = get_key_name(req->key);
2986 if (!*key)
2987 key = "(no key defined)";
2989 if (!string_format(buf, " %-10s %-14s `%s`",
2990 keymap_table[req->keymap].name,
2991 key, req->cmd))
2992 continue;
2994 add_line_text(view, buf, LINE_DEFAULT);
2995 }
2997 return TRUE;
2998 }
3000 static struct view_ops help_ops = {
3001 "line",
3002 help_open,
3003 NULL,
3004 pager_draw,
3005 pager_request,
3006 pager_grep,
3007 pager_select,
3008 };
3011 /*
3012 * Tree backend
3013 */
3015 struct tree_stack_entry {
3016 struct tree_stack_entry *prev; /* Entry below this in the stack */
3017 unsigned long lineno; /* Line number to restore */
3018 char *name; /* Position of name in opt_path */
3019 };
3021 /* The top of the path stack. */
3022 static struct tree_stack_entry *tree_stack = NULL;
3023 unsigned long tree_lineno = 0;
3025 static void
3026 pop_tree_stack_entry(void)
3027 {
3028 struct tree_stack_entry *entry = tree_stack;
3030 tree_lineno = entry->lineno;
3031 entry->name[0] = 0;
3032 tree_stack = entry->prev;
3033 free(entry);
3034 }
3036 static void
3037 push_tree_stack_entry(char *name, unsigned long lineno)
3038 {
3039 struct tree_stack_entry *entry = calloc(1, sizeof(*entry));
3040 size_t pathlen = strlen(opt_path);
3042 if (!entry)
3043 return;
3045 entry->prev = tree_stack;
3046 entry->name = opt_path + pathlen;
3047 tree_stack = entry;
3049 if (!string_format_from(opt_path, &pathlen, "%s/", name)) {
3050 pop_tree_stack_entry();
3051 return;
3052 }
3054 /* Move the current line to the first tree entry. */
3055 tree_lineno = 1;
3056 entry->lineno = lineno;
3057 }
3059 /* Parse output from git-ls-tree(1):
3060 *
3061 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
3062 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
3063 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
3064 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
3065 */
3067 #define SIZEOF_TREE_ATTR \
3068 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
3070 #define TREE_UP_FORMAT "040000 tree %s\t.."
3072 static int
3073 tree_compare_entry(enum line_type type1, char *name1,
3074 enum line_type type2, char *name2)
3075 {
3076 if (type1 != type2) {
3077 if (type1 == LINE_TREE_DIR)
3078 return -1;
3079 return 1;
3080 }
3082 return strcmp(name1, name2);
3083 }
3085 static char *
3086 tree_path(struct line *line)
3087 {
3088 char *path = line->data;
3090 return path + SIZEOF_TREE_ATTR;
3091 }
3093 static bool
3094 tree_read(struct view *view, char *text)
3095 {
3096 size_t textlen = text ? strlen(text) : 0;
3097 char buf[SIZEOF_STR];
3098 unsigned long pos;
3099 enum line_type type;
3100 bool first_read = view->lines == 0;
3102 if (!text)
3103 return TRUE;
3104 if (textlen <= SIZEOF_TREE_ATTR)
3105 return FALSE;
3107 type = text[STRING_SIZE("100644 ")] == 't'
3108 ? LINE_TREE_DIR : LINE_TREE_FILE;
3110 if (first_read) {
3111 /* Add path info line */
3112 if (!string_format(buf, "Directory path /%s", opt_path) ||
3113 !realloc_lines(view, view->line_size + 1) ||
3114 !add_line_text(view, buf, LINE_DEFAULT))
3115 return FALSE;
3117 /* Insert "link" to parent directory. */
3118 if (*opt_path) {
3119 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
3120 !realloc_lines(view, view->line_size + 1) ||
3121 !add_line_text(view, buf, LINE_TREE_DIR))
3122 return FALSE;
3123 }
3124 }
3126 /* Strip the path part ... */
3127 if (*opt_path) {
3128 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
3129 size_t striplen = strlen(opt_path);
3130 char *path = text + SIZEOF_TREE_ATTR;
3132 if (pathlen > striplen)
3133 memmove(path, path + striplen,
3134 pathlen - striplen + 1);
3135 }
3137 /* Skip "Directory ..." and ".." line. */
3138 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
3139 struct line *line = &view->line[pos];
3140 char *path1 = tree_path(line);
3141 char *path2 = text + SIZEOF_TREE_ATTR;
3142 int cmp = tree_compare_entry(line->type, path1, type, path2);
3144 if (cmp <= 0)
3145 continue;
3147 text = strdup(text);
3148 if (!text)
3149 return FALSE;
3151 if (view->lines > pos)
3152 memmove(&view->line[pos + 1], &view->line[pos],
3153 (view->lines - pos) * sizeof(*line));
3155 line = &view->line[pos];
3156 line->data = text;
3157 line->type = type;
3158 view->lines++;
3159 return TRUE;
3160 }
3162 if (!add_line_text(view, text, type))
3163 return FALSE;
3165 if (tree_lineno > view->lineno) {
3166 view->lineno = tree_lineno;
3167 tree_lineno = 0;
3168 }
3170 return TRUE;
3171 }
3173 static enum request
3174 tree_request(struct view *view, enum request request, struct line *line)
3175 {
3176 enum open_flags flags;
3178 if (request == REQ_VIEW_BLAME) {
3179 char *filename = tree_path(line);
3181 if (line->type == LINE_TREE_DIR) {
3182 report("Cannot show blame for directory %s", opt_path);
3183 return REQ_NONE;
3184 }
3186 string_copy(opt_ref, view->vid);
3187 string_format(opt_file, "%s%s", opt_path, filename);
3188 return request;
3189 }
3190 if (request == REQ_TREE_PARENT) {
3191 if (*opt_path) {
3192 /* fake 'cd ..' */
3193 request = REQ_ENTER;
3194 line = &view->line[1];
3195 } else {
3196 /* quit view if at top of tree */
3197 return REQ_VIEW_CLOSE;
3198 }
3199 }
3200 if (request != REQ_ENTER)
3201 return request;
3203 /* Cleanup the stack if the tree view is at a different tree. */
3204 while (!*opt_path && tree_stack)
3205 pop_tree_stack_entry();
3207 switch (line->type) {
3208 case LINE_TREE_DIR:
3209 /* Depending on whether it is a subdir or parent (updir?) link
3210 * mangle the path buffer. */
3211 if (line == &view->line[1] && *opt_path) {
3212 pop_tree_stack_entry();
3214 } else {
3215 char *basename = tree_path(line);
3217 push_tree_stack_entry(basename, view->lineno);
3218 }
3220 /* Trees and subtrees share the same ID, so they are not not
3221 * unique like blobs. */
3222 flags = OPEN_RELOAD;
3223 request = REQ_VIEW_TREE;
3224 break;
3226 case LINE_TREE_FILE:
3227 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3228 request = REQ_VIEW_BLOB;
3229 break;
3231 default:
3232 return TRUE;
3233 }
3235 open_view(view, request, flags);
3236 if (request == REQ_VIEW_TREE) {
3237 view->lineno = tree_lineno;
3238 }
3240 return REQ_NONE;
3241 }
3243 static void
3244 tree_select(struct view *view, struct line *line)
3245 {
3246 char *text = (char *)line->data + STRING_SIZE("100644 blob ");
3248 if (line->type == LINE_TREE_FILE) {
3249 string_copy_rev(ref_blob, text);
3251 } else if (line->type != LINE_TREE_DIR) {
3252 return;
3253 }
3255 string_copy_rev(view->ref, text);
3256 }
3258 static struct view_ops tree_ops = {
3259 "file",
3260 NULL,
3261 tree_read,
3262 pager_draw,
3263 tree_request,
3264 pager_grep,
3265 tree_select,
3266 };
3268 static bool
3269 blob_read(struct view *view, char *line)
3270 {
3271 if (!line)
3272 return TRUE;
3273 return add_line_text(view, line, LINE_DEFAULT) != NULL;
3274 }
3276 static struct view_ops blob_ops = {
3277 "line",
3278 NULL,
3279 blob_read,
3280 pager_draw,
3281 pager_request,
3282 pager_grep,
3283 pager_select,
3284 };
3286 /*
3287 * Blame backend
3288 *
3289 * Loading the blame view is a two phase job:
3290 *
3291 * 1. File content is read either using opt_file from the
3292 * filesystem or using git-cat-file.
3293 * 2. Then blame information is incrementally added by
3294 * reading output from git-blame.
3295 */
3297 struct blame_commit {
3298 char id[SIZEOF_REV]; /* SHA1 ID. */
3299 char title[128]; /* First line of the commit message. */
3300 char author[75]; /* Author of the commit. */
3301 struct tm time; /* Date from the author ident. */
3302 char filename[128]; /* Name of file. */
3303 };
3305 struct blame {
3306 struct blame_commit *commit;
3307 unsigned int header:1;
3308 char text[1];
3309 };
3311 #define BLAME_CAT_FILE_CMD "git cat-file blob %s:%s"
3312 #define BLAME_INCREMENTAL_CMD "git blame --incremental %s %s"
3314 static bool
3315 blame_open(struct view *view)
3316 {
3317 char path[SIZEOF_STR];
3318 char ref[SIZEOF_STR] = "";
3320 if (sq_quote(path, 0, opt_file) >= sizeof(path))
3321 return FALSE;
3323 if (*opt_ref && sq_quote(ref, 0, opt_ref) >= sizeof(ref))
3324 return FALSE;
3326 if (*opt_ref) {
3327 if (!string_format(view->cmd, BLAME_CAT_FILE_CMD, ref, path))
3328 return FALSE;
3329 } else {
3330 view->pipe = fopen(opt_file, "r");
3331 if (!view->pipe &&
3332 !string_format(view->cmd, BLAME_CAT_FILE_CMD, "HEAD", path))
3333 return FALSE;
3334 }
3336 if (!view->pipe)
3337 view->pipe = popen(view->cmd, "r");
3338 if (!view->pipe)
3339 return FALSE;
3341 if (!string_format(view->cmd, BLAME_INCREMENTAL_CMD, ref, path))
3342 return FALSE;
3344 string_format(view->ref, "%s ...", opt_file);
3345 string_copy_rev(view->vid, opt_file);
3346 set_nonblocking_input(TRUE);
3348 if (view->line) {
3349 int i;
3351 for (i = 0; i < view->lines; i++)
3352 free(view->line[i].data);
3353 free(view->line);
3354 }
3356 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3357 view->offset = view->lines = view->lineno = 0;
3358 view->line = NULL;
3359 view->start_time = time(NULL);
3361 return TRUE;
3362 }
3364 static struct blame_commit *
3365 get_blame_commit(struct view *view, const char *id)
3366 {
3367 size_t i;
3369 for (i = 0; i < view->lines; i++) {
3370 struct blame *blame = view->line[i].data;
3372 if (!blame->commit)
3373 continue;
3375 if (!strncmp(blame->commit->id, id, SIZEOF_REV - 1))
3376 return blame->commit;
3377 }
3379 {
3380 struct blame_commit *commit = calloc(1, sizeof(*commit));
3382 if (commit)
3383 string_ncopy(commit->id, id, SIZEOF_REV);
3384 return commit;
3385 }
3386 }
3388 static bool
3389 parse_number(char **posref, size_t *number, size_t min, size_t max)
3390 {
3391 char *pos = *posref;
3393 *posref = NULL;
3394 pos = strchr(pos + 1, ' ');
3395 if (!pos || !isdigit(pos[1]))
3396 return FALSE;
3397 *number = atoi(pos + 1);
3398 if (*number < min || *number > max)
3399 return FALSE;
3401 *posref = pos;
3402 return TRUE;
3403 }
3405 static struct blame_commit *
3406 parse_blame_commit(struct view *view, char *text, int *blamed)
3407 {
3408 struct blame_commit *commit;
3409 struct blame *blame;
3410 char *pos = text + SIZEOF_REV - 1;
3411 size_t lineno;
3412 size_t group;
3414 if (strlen(text) <= SIZEOF_REV || *pos != ' ')
3415 return NULL;
3417 if (!parse_number(&pos, &lineno, 1, view->lines) ||
3418 !parse_number(&pos, &group, 1, view->lines - lineno + 1))
3419 return NULL;
3421 commit = get_blame_commit(view, text);
3422 if (!commit)
3423 return NULL;
3425 *blamed += group;
3426 while (group--) {
3427 struct line *line = &view->line[lineno + group - 1];
3429 blame = line->data;
3430 blame->commit = commit;
3431 blame->header = !group;
3432 line->dirty = 1;
3433 }
3435 return commit;
3436 }
3438 static bool
3439 blame_read_file(struct view *view, char *line)
3440 {
3441 if (!line) {
3442 FILE *pipe = NULL;
3444 if (view->lines > 0)
3445 pipe = popen(view->cmd, "r");
3446 view->cmd[0] = 0;
3447 if (!pipe) {
3448 report("Failed to load blame data");
3449 return TRUE;
3450 }
3452 fclose(view->pipe);
3453 view->pipe = pipe;
3454 return FALSE;
3456 } else {
3457 size_t linelen = strlen(line);
3458 struct blame *blame = malloc(sizeof(*blame) + linelen);
3460 if (!line)
3461 return FALSE;
3463 blame->commit = NULL;
3464 strncpy(blame->text, line, linelen);
3465 blame->text[linelen] = 0;
3466 return add_line_data(view, blame, LINE_BLAME_COMMIT) != NULL;
3467 }
3468 }
3470 static bool
3471 match_blame_header(const char *name, char **line)
3472 {
3473 size_t namelen = strlen(name);
3474 bool matched = !strncmp(name, *line, namelen);
3476 if (matched)
3477 *line += namelen;
3479 return matched;
3480 }
3482 static bool
3483 blame_read(struct view *view, char *line)
3484 {
3485 static struct blame_commit *commit = NULL;
3486 static int blamed = 0;
3487 static time_t author_time;
3489 if (*view->cmd)
3490 return blame_read_file(view, line);
3492 if (!line) {
3493 /* Reset all! */
3494 commit = NULL;
3495 blamed = 0;
3496 string_format(view->ref, "%s", view->vid);
3497 if (view_is_displayed(view)) {
3498 update_view_title(view);
3499 redraw_view_from(view, 0);
3500 }
3501 return TRUE;
3502 }
3504 if (!commit) {
3505 commit = parse_blame_commit(view, line, &blamed);
3506 string_format(view->ref, "%s %2d%%", view->vid,
3507 blamed * 100 / view->lines);
3509 } else if (match_blame_header("author ", &line)) {
3510 string_ncopy(commit->author, line, strlen(line));
3512 } else if (match_blame_header("author-time ", &line)) {
3513 author_time = (time_t) atol(line);
3515 } else if (match_blame_header("author-tz ", &line)) {
3516 long tz;
3518 tz = ('0' - line[1]) * 60 * 60 * 10;
3519 tz += ('0' - line[2]) * 60 * 60;
3520 tz += ('0' - line[3]) * 60;
3521 tz += ('0' - line[4]) * 60;
3523 if (line[0] == '-')
3524 tz = -tz;
3526 author_time -= tz;
3527 gmtime_r(&author_time, &commit->time);
3529 } else if (match_blame_header("summary ", &line)) {
3530 string_ncopy(commit->title, line, strlen(line));
3532 } else if (match_blame_header("filename ", &line)) {
3533 string_ncopy(commit->filename, line, strlen(line));
3534 commit = NULL;
3535 }
3537 return TRUE;
3538 }
3540 static bool
3541 blame_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
3542 {
3543 struct blame *blame = line->data;
3544 int col = 0;
3546 wmove(view->win, lineno, 0);
3548 if (selected) {
3549 wattrset(view->win, get_line_attr(LINE_CURSOR));
3550 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
3551 } else {
3552 wattrset(view->win, A_NORMAL);
3553 }
3555 if (opt_date) {
3556 int n;
3558 if (!selected)
3559 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
3560 if (blame->commit) {
3561 char buf[DATE_COLS + 1];
3562 int timelen;
3564 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &blame->commit->time);
3565 n = draw_text(view, buf, view->width - col, FALSE, selected);
3566 draw_text(view, " ", view->width - col - n, FALSE, selected);
3567 }
3569 col += DATE_COLS;
3570 wmove(view->win, lineno, col);
3571 if (col >= view->width)
3572 return TRUE;
3573 }
3575 if (opt_author) {
3576 int max = MIN(AUTHOR_COLS - 1, view->width - col);
3578 if (!selected)
3579 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
3580 if (blame->commit)
3581 draw_text(view, blame->commit->author, max, TRUE, selected);
3582 col += AUTHOR_COLS;
3583 if (col >= view->width)
3584 return TRUE;
3585 wmove(view->win, lineno, col);
3586 }
3588 {
3589 int max = MIN(ID_COLS - 1, view->width - col);
3591 if (!selected)
3592 wattrset(view->win, get_line_attr(LINE_BLAME_ID));
3593 if (blame->commit)
3594 draw_text(view, blame->commit->id, max, FALSE, -1);
3595 col += ID_COLS;
3596 if (col >= view->width)
3597 return TRUE;
3598 wmove(view->win, lineno, col);
3599 }
3601 {
3602 unsigned long real_lineno = view->offset + lineno + 1;
3603 char number[10] = " ";
3604 int max = MIN(view->digits, STRING_SIZE(number));
3605 bool showtrimmed = FALSE;
3607 if (real_lineno == 1 ||
3608 (real_lineno % opt_num_interval) == 0) {
3609 char fmt[] = "%1ld";
3611 if (view->digits <= 9)
3612 fmt[1] = '0' + view->digits;
3614 if (!string_format(number, fmt, real_lineno))
3615 number[0] = 0;
3616 showtrimmed = TRUE;
3617 }
3619 if (max > view->width - col)
3620 max = view->width - col;
3621 if (!selected)
3622 wattrset(view->win, get_line_attr(LINE_BLAME_LINENO));
3623 col += draw_text(view, number, max, showtrimmed, selected);
3624 if (col >= view->width)
3625 return TRUE;
3626 }
3628 if (!selected)
3629 wattrset(view->win, A_NORMAL);
3631 if (col >= view->width)
3632 return TRUE;
3633 waddch(view->win, ACS_VLINE);
3634 col++;
3635 if (col >= view->width)
3636 return TRUE;
3637 waddch(view->win, ' ');
3638 col++;
3639 col += draw_text(view, blame->text, view->width - col, TRUE, selected);
3641 return TRUE;
3642 }
3644 static enum request
3645 blame_request(struct view *view, enum request request, struct line *line)
3646 {
3647 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3648 struct blame *blame = line->data;
3650 switch (request) {
3651 case REQ_ENTER:
3652 if (!blame->commit) {
3653 report("No commit loaded yet");
3654 break;
3655 }
3657 if (!strcmp(blame->commit->id, NULL_ID)) {
3658 char path[SIZEOF_STR];
3660 if (sq_quote(path, 0, view->vid) >= sizeof(path))
3661 break;
3662 string_format(opt_cmd, "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s 2>/dev/null", path);
3663 }
3665 open_view(view, REQ_VIEW_DIFF, flags);
3666 break;
3668 default:
3669 return request;
3670 }
3672 return REQ_NONE;
3673 }
3675 static bool
3676 blame_grep(struct view *view, struct line *line)
3677 {
3678 struct blame *blame = line->data;
3679 struct blame_commit *commit = blame->commit;
3680 regmatch_t pmatch;
3682 #define MATCH(text) \
3683 (*text && regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3685 if (commit) {
3686 char buf[DATE_COLS + 1];
3688 if (MATCH(commit->title) ||
3689 MATCH(commit->author) ||
3690 MATCH(commit->id))
3691 return TRUE;
3693 if (strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time) &&
3694 MATCH(buf))
3695 return TRUE;
3696 }
3698 return MATCH(blame->text);
3700 #undef MATCH
3701 }
3703 static void
3704 blame_select(struct view *view, struct line *line)
3705 {
3706 struct blame *blame = line->data;
3707 struct blame_commit *commit = blame->commit;
3709 if (!commit)
3710 return;
3712 if (!strcmp(commit->id, NULL_ID))
3713 string_ncopy(ref_commit, "HEAD", 4);
3714 else
3715 string_copy_rev(ref_commit, commit->id);
3716 }
3718 static struct view_ops blame_ops = {
3719 "line",
3720 blame_open,
3721 blame_read,
3722 blame_draw,
3723 blame_request,
3724 blame_grep,
3725 blame_select,
3726 };
3728 /*
3729 * Status backend
3730 */
3732 struct status {
3733 char status;
3734 struct {
3735 mode_t mode;
3736 char rev[SIZEOF_REV];
3737 char name[SIZEOF_STR];
3738 } old;
3739 struct {
3740 mode_t mode;
3741 char rev[SIZEOF_REV];
3742 char name[SIZEOF_STR];
3743 } new;
3744 };
3746 static char status_onbranch[SIZEOF_STR];
3747 static struct status stage_status;
3748 static enum line_type stage_line_type;
3750 /* Get fields from the diff line:
3751 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
3752 */
3753 static inline bool
3754 status_get_diff(struct status *file, char *buf, size_t bufsize)
3755 {
3756 char *old_mode = buf + 1;
3757 char *new_mode = buf + 8;
3758 char *old_rev = buf + 15;
3759 char *new_rev = buf + 56;
3760 char *status = buf + 97;
3762 if (bufsize < 99 ||
3763 old_mode[-1] != ':' ||
3764 new_mode[-1] != ' ' ||
3765 old_rev[-1] != ' ' ||
3766 new_rev[-1] != ' ' ||
3767 status[-1] != ' ')
3768 return FALSE;
3770 file->status = *status;
3772 string_copy_rev(file->old.rev, old_rev);
3773 string_copy_rev(file->new.rev, new_rev);
3775 file->old.mode = strtoul(old_mode, NULL, 8);
3776 file->new.mode = strtoul(new_mode, NULL, 8);
3778 file->old.name[0] = file->new.name[0] = 0;
3780 return TRUE;
3781 }
3783 static bool
3784 status_run(struct view *view, const char cmd[], char status, enum line_type type)
3785 {
3786 struct status *file = NULL;
3787 struct status *unmerged = NULL;
3788 char buf[SIZEOF_STR * 4];
3789 size_t bufsize = 0;
3790 FILE *pipe;
3792 pipe = popen(cmd, "r");
3793 if (!pipe)
3794 return FALSE;
3796 add_line_data(view, NULL, type);
3798 while (!feof(pipe) && !ferror(pipe)) {
3799 char *sep;
3800 size_t readsize;
3802 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
3803 if (!readsize)
3804 break;
3805 bufsize += readsize;
3807 /* Process while we have NUL chars. */
3808 while ((sep = memchr(buf, 0, bufsize))) {
3809 size_t sepsize = sep - buf + 1;
3811 if (!file) {
3812 if (!realloc_lines(view, view->line_size + 1))
3813 goto error_out;
3815 file = calloc(1, sizeof(*file));
3816 if (!file)
3817 goto error_out;
3819 add_line_data(view, file, type);
3820 }
3822 /* Parse diff info part. */
3823 if (status) {
3824 file->status = status;
3825 if (status == 'A')
3826 string_copy(file->old.rev, NULL_ID);
3828 } else if (!file->status) {
3829 if (!status_get_diff(file, buf, sepsize))
3830 goto error_out;
3832 bufsize -= sepsize;
3833 memmove(buf, sep + 1, bufsize);
3835 sep = memchr(buf, 0, bufsize);
3836 if (!sep)
3837 break;
3838 sepsize = sep - buf + 1;
3840 /* Collapse all 'M'odified entries that
3841 * follow a associated 'U'nmerged entry.
3842 */
3843 if (file->status == 'U') {
3844 unmerged = file;
3846 } else if (unmerged) {
3847 int collapse = !strcmp(buf, unmerged->new.name);
3849 unmerged = NULL;
3850 if (collapse) {
3851 free(file);
3852 view->lines--;
3853 continue;
3854 }
3855 }
3856 }
3858 /* Grab the old name for rename/copy. */
3859 if (!*file->old.name &&
3860 (file->status == 'R' || file->status == 'C')) {
3861 sepsize = sep - buf + 1;
3862 string_ncopy(file->old.name, buf, sepsize);
3863 bufsize -= sepsize;
3864 memmove(buf, sep + 1, bufsize);
3866 sep = memchr(buf, 0, bufsize);
3867 if (!sep)
3868 break;
3869 sepsize = sep - buf + 1;
3870 }
3872 /* git-ls-files just delivers a NUL separated
3873 * list of file names similar to the second half
3874 * of the git-diff-* output. */
3875 string_ncopy(file->new.name, buf, sepsize);
3876 if (!*file->old.name)
3877 string_copy(file->old.name, file->new.name);
3878 bufsize -= sepsize;
3879 memmove(buf, sep + 1, bufsize);
3880 file = NULL;
3881 }
3882 }
3884 if (ferror(pipe)) {
3885 error_out:
3886 pclose(pipe);
3887 return FALSE;
3888 }
3890 if (!view->line[view->lines - 1].data)
3891 add_line_data(view, NULL, LINE_STAT_NONE);
3893 pclose(pipe);
3894 return TRUE;
3895 }
3897 /* Don't show unmerged entries in the staged section. */
3898 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached -M HEAD"
3899 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
3900 #define STATUS_LIST_OTHER_CMD \
3901 "git ls-files -z --others --exclude-per-directory=.gitignore"
3902 #define STATUS_LIST_NO_HEAD_CMD \
3903 "git ls-files -z --cached --exclude-per-directory=.gitignore"
3905 #define STATUS_DIFF_INDEX_SHOW_CMD \
3906 "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s %s 2>/dev/null"
3908 #define STATUS_DIFF_FILES_SHOW_CMD \
3909 "git diff-files --root --patch-with-stat -C -M -- %s %s 2>/dev/null"
3911 #define STATUS_DIFF_NO_HEAD_SHOW_CMD \
3912 "git diff --no-color --patch-with-stat /dev/null %s 2>/dev/null"
3914 /* First parse staged info using git-diff-index(1), then parse unstaged
3915 * info using git-diff-files(1), and finally untracked files using
3916 * git-ls-files(1). */
3917 static bool
3918 status_open(struct view *view)
3919 {
3920 struct stat statbuf;
3921 char exclude[SIZEOF_STR];
3922 char indexcmd[SIZEOF_STR] = STATUS_DIFF_INDEX_CMD;
3923 char othercmd[SIZEOF_STR] = STATUS_LIST_OTHER_CMD;
3924 unsigned long prev_lineno = view->lineno;
3925 char indexstatus = 0;
3926 size_t i;
3928 for (i = 0; i < view->lines; i++)
3929 free(view->line[i].data);
3930 free(view->line);
3931 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3932 view->line = NULL;
3934 if (!realloc_lines(view, view->line_size + 7))
3935 return FALSE;
3937 add_line_data(view, NULL, LINE_STAT_HEAD);
3938 if (opt_no_head)
3939 string_copy(status_onbranch, "Initial commit");
3940 else if (!*opt_head)
3941 string_copy(status_onbranch, "Not currently on any branch");
3942 else if (!string_format(status_onbranch, "On branch %s", opt_head))
3943 return FALSE;
3945 if (opt_no_head) {
3946 string_copy(indexcmd, STATUS_LIST_NO_HEAD_CMD);
3947 indexstatus = 'A';
3948 }
3950 if (!string_format(exclude, "%s/info/exclude", opt_git_dir))
3951 return FALSE;
3953 if (stat(exclude, &statbuf) >= 0) {
3954 size_t cmdsize = strlen(othercmd);
3956 if (!string_format_from(othercmd, &cmdsize, " %s", "--exclude-from=") ||
3957 sq_quote(othercmd, cmdsize, exclude) >= sizeof(othercmd))
3958 return FALSE;
3960 cmdsize = strlen(indexcmd);
3961 if (opt_no_head &&
3962 (!string_format_from(indexcmd, &cmdsize, " %s", "--exclude-from=") ||
3963 sq_quote(indexcmd, cmdsize, exclude) >= sizeof(indexcmd)))
3964 return FALSE;
3965 }
3967 system("git update-index -q --refresh");
3969 if (!status_run(view, indexcmd, indexstatus, LINE_STAT_STAGED) ||
3970 !status_run(view, STATUS_DIFF_FILES_CMD, 0, LINE_STAT_UNSTAGED) ||
3971 !status_run(view, othercmd, '?', LINE_STAT_UNTRACKED))
3972 return FALSE;
3974 /* If all went well restore the previous line number to stay in
3975 * the context or select a line with something that can be
3976 * updated. */
3977 if (prev_lineno >= view->lines)
3978 prev_lineno = view->lines - 1;
3979 while (prev_lineno < view->lines && !view->line[prev_lineno].data)
3980 prev_lineno++;
3981 while (prev_lineno > 0 && !view->line[prev_lineno].data)
3982 prev_lineno--;
3984 /* If the above fails, always skip the "On branch" line. */
3985 if (prev_lineno < view->lines)
3986 view->lineno = prev_lineno;
3987 else
3988 view->lineno = 1;
3990 if (view->lineno < view->offset)
3991 view->offset = view->lineno;
3992 else if (view->offset + view->height <= view->lineno)
3993 view->offset = view->lineno - view->height + 1;
3995 return TRUE;
3996 }
3998 static bool
3999 status_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
4000 {
4001 struct status *status = line->data;
4003 wmove(view->win, lineno, 0);
4005 if (selected) {
4006 wattrset(view->win, get_line_attr(LINE_CURSOR));
4007 wchgat(view->win, -1, 0, LINE_CURSOR, NULL);
4009 } else if (line->type == LINE_STAT_HEAD) {
4010 wattrset(view->win, get_line_attr(LINE_STAT_HEAD));
4011 wchgat(view->win, -1, 0, LINE_STAT_HEAD, NULL);
4013 } else if (!status && line->type != LINE_STAT_NONE) {
4014 wattrset(view->win, get_line_attr(LINE_STAT_SECTION));
4015 wchgat(view->win, -1, 0, LINE_STAT_SECTION, NULL);
4017 } else {
4018 wattrset(view->win, get_line_attr(line->type));
4019 }
4021 if (!status) {
4022 char *text;
4024 switch (line->type) {
4025 case LINE_STAT_STAGED:
4026 text = "Changes to be committed:";
4027 break;
4029 case LINE_STAT_UNSTAGED:
4030 text = "Changed but not updated:";
4031 break;
4033 case LINE_STAT_UNTRACKED:
4034 text = "Untracked files:";
4035 break;
4037 case LINE_STAT_NONE:
4038 text = " (no files)";
4039 break;
4041 case LINE_STAT_HEAD:
4042 text = status_onbranch;
4043 break;
4045 default:
4046 return FALSE;
4047 }
4049 draw_text(view, text, view->width, TRUE, selected);
4050 return TRUE;
4051 }
4053 waddch(view->win, status->status);
4054 if (!selected)
4055 wattrset(view->win, A_NORMAL);
4056 wmove(view->win, lineno, 4);
4057 if (view->width < 5)
4058 return TRUE;
4060 draw_text(view, status->new.name, view->width - 5, TRUE, selected);
4061 return TRUE;
4062 }
4064 static enum request
4065 status_enter(struct view *view, struct line *line)
4066 {
4067 struct status *status = line->data;
4068 char oldpath[SIZEOF_STR] = "";
4069 char newpath[SIZEOF_STR] = "";
4070 char *info;
4071 size_t cmdsize = 0;
4073 if (line->type == LINE_STAT_NONE ||
4074 (!status && line[1].type == LINE_STAT_NONE)) {
4075 report("No file to diff");
4076 return REQ_NONE;
4077 }
4079 if (status) {
4080 if (sq_quote(oldpath, 0, status->old.name) >= sizeof(oldpath))
4081 return REQ_QUIT;
4082 /* Diffs for unmerged entries are empty when pasing the
4083 * new path, so leave it empty. */
4084 if (status->status != 'U' &&
4085 sq_quote(newpath, 0, status->new.name) >= sizeof(newpath))
4086 return REQ_QUIT;
4087 }
4089 if (opt_cdup[0] &&
4090 line->type != LINE_STAT_UNTRACKED &&
4091 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
4092 return REQ_QUIT;
4094 switch (line->type) {
4095 case LINE_STAT_STAGED:
4096 if (opt_no_head) {
4097 if (!string_format_from(opt_cmd, &cmdsize,
4098 STATUS_DIFF_NO_HEAD_SHOW_CMD,
4099 newpath))
4100 return REQ_QUIT;
4101 } else {
4102 if (!string_format_from(opt_cmd, &cmdsize,
4103 STATUS_DIFF_INDEX_SHOW_CMD,
4104 oldpath, newpath))
4105 return REQ_QUIT;
4106 }
4108 if (status)
4109 info = "Staged changes to %s";
4110 else
4111 info = "Staged changes";
4112 break;
4114 case LINE_STAT_UNSTAGED:
4115 if (!string_format_from(opt_cmd, &cmdsize,
4116 STATUS_DIFF_FILES_SHOW_CMD, oldpath, newpath))
4117 return REQ_QUIT;
4118 if (status)
4119 info = "Unstaged changes to %s";
4120 else
4121 info = "Unstaged changes";
4122 break;
4124 case LINE_STAT_UNTRACKED:
4125 if (opt_pipe)
4126 return REQ_QUIT;
4128 if (!status) {
4129 report("No file to show");
4130 return REQ_NONE;
4131 }
4133 opt_pipe = fopen(status->new.name, "r");
4134 info = "Untracked file %s";
4135 break;
4137 case LINE_STAT_HEAD:
4138 return REQ_NONE;
4140 default:
4141 die("line type %d not handled in switch", line->type);
4142 }
4144 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | OPEN_SPLIT);
4145 if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) {
4146 if (status) {
4147 stage_status = *status;
4148 } else {
4149 memset(&stage_status, 0, sizeof(stage_status));
4150 }
4152 stage_line_type = line->type;
4153 string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.new.name);
4154 }
4156 return REQ_NONE;
4157 }
4160 static FILE *
4161 status_update_prepare(enum line_type type)
4162 {
4163 char cmd[SIZEOF_STR];
4164 size_t cmdsize = 0;
4166 if (opt_cdup[0] &&
4167 type != LINE_STAT_UNTRACKED &&
4168 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4169 return NULL;
4171 switch (type) {
4172 case LINE_STAT_STAGED:
4173 string_add(cmd, cmdsize, "git update-index -z --index-info");
4174 break;
4176 case LINE_STAT_UNSTAGED:
4177 case LINE_STAT_UNTRACKED:
4178 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
4179 break;
4181 default:
4182 die("line type %d not handled in switch", type);
4183 }
4185 return popen(cmd, "w");
4186 }
4188 static bool
4189 status_update_write(FILE *pipe, struct status *status, enum line_type type)
4190 {
4191 char buf[SIZEOF_STR];
4192 size_t bufsize = 0;
4193 size_t written = 0;
4195 switch (type) {
4196 case LINE_STAT_STAGED:
4197 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
4198 status->old.mode,
4199 status->old.rev,
4200 status->old.name, 0))
4201 return FALSE;
4202 break;
4204 case LINE_STAT_UNSTAGED:
4205 case LINE_STAT_UNTRACKED:
4206 if (!string_format_from(buf, &bufsize, "%s%c", status->new.name, 0))
4207 return FALSE;
4208 break;
4210 default:
4211 die("line type %d not handled in switch", type);
4212 }
4214 while (!ferror(pipe) && written < bufsize) {
4215 written += fwrite(buf + written, 1, bufsize - written, pipe);
4216 }
4218 return written == bufsize;
4219 }
4221 static bool
4222 status_update_file(struct status *status, enum line_type type)
4223 {
4224 FILE *pipe = status_update_prepare(type);
4225 bool result;
4227 if (!pipe)
4228 return FALSE;
4230 result = status_update_write(pipe, status, type);
4231 pclose(pipe);
4232 return result;
4233 }
4235 static bool
4236 status_update_files(struct view *view, struct line *line)
4237 {
4238 FILE *pipe = status_update_prepare(line->type);
4239 bool result = TRUE;
4240 struct line *pos = view->line + view->lines;
4241 int files = 0;
4242 int file, done;
4244 if (!pipe)
4245 return FALSE;
4247 for (pos = line; pos < view->line + view->lines && pos->data; pos++)
4248 files++;
4250 for (file = 0, done = 0; result && file < files; line++, file++) {
4251 int almost_done = file * 100 / files;
4253 if (almost_done > done) {
4254 done = almost_done;
4255 string_format(view->ref, "updating file %u of %u (%d%% done)",
4256 file, files, done);
4257 update_view_title(view);
4258 }
4259 result = status_update_write(pipe, line->data, line->type);
4260 }
4262 pclose(pipe);
4263 return result;
4264 }
4266 static bool
4267 status_update(struct view *view)
4268 {
4269 struct line *line = &view->line[view->lineno];
4271 assert(view->lines);
4273 if (!line->data) {
4274 /* This should work even for the "On branch" line. */
4275 if (line < view->line + view->lines && !line[1].data) {
4276 report("Nothing to update");
4277 return FALSE;
4278 }
4280 if (!status_update_files(view, line + 1))
4281 report("Failed to update file status");
4283 } else if (!status_update_file(line->data, line->type)) {
4284 report("Failed to update file status");
4285 }
4287 return TRUE;
4288 }
4290 static enum request
4291 status_request(struct view *view, enum request request, struct line *line)
4292 {
4293 struct status *status = line->data;
4295 switch (request) {
4296 case REQ_STATUS_UPDATE:
4297 if (!status_update(view))
4298 return REQ_NONE;
4299 break;
4301 case REQ_STATUS_MERGE:
4302 if (!status || status->status != 'U') {
4303 report("Merging only possible for files with unmerged status ('U').");
4304 return REQ_NONE;
4305 }
4306 open_mergetool(status->new.name);
4307 break;
4309 case REQ_EDIT:
4310 if (!status)
4311 return request;
4313 open_editor(status->status != '?', status->new.name);
4314 break;
4316 case REQ_VIEW_BLAME:
4317 if (status) {
4318 string_copy(opt_file, status->new.name);
4319 opt_ref[0] = 0;
4320 }
4321 return request;
4323 case REQ_ENTER:
4324 /* After returning the status view has been split to
4325 * show the stage view. No further reloading is
4326 * necessary. */
4327 status_enter(view, line);
4328 return REQ_NONE;
4330 case REQ_REFRESH:
4331 /* Simply reload the view. */
4332 break;
4334 default:
4335 return request;
4336 }
4338 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
4340 return REQ_NONE;
4341 }
4343 static void
4344 status_select(struct view *view, struct line *line)
4345 {
4346 struct status *status = line->data;
4347 char file[SIZEOF_STR] = "all files";
4348 char *text;
4349 char *key;
4351 if (status && !string_format(file, "'%s'", status->new.name))
4352 return;
4354 if (!status && line[1].type == LINE_STAT_NONE)
4355 line++;
4357 switch (line->type) {
4358 case LINE_STAT_STAGED:
4359 text = "Press %s to unstage %s for commit";
4360 break;
4362 case LINE_STAT_UNSTAGED:
4363 text = "Press %s to stage %s for commit";
4364 break;
4366 case LINE_STAT_UNTRACKED:
4367 text = "Press %s to stage %s for addition";
4368 break;
4370 case LINE_STAT_HEAD:
4371 case LINE_STAT_NONE:
4372 text = "Nothing to update";
4373 break;
4375 default:
4376 die("line type %d not handled in switch", line->type);
4377 }
4379 if (status && status->status == 'U') {
4380 text = "Press %s to resolve conflict in %s";
4381 key = get_key(REQ_STATUS_MERGE);
4383 } else {
4384 key = get_key(REQ_STATUS_UPDATE);
4385 }
4387 string_format(view->ref, text, key, file);
4388 }
4390 static bool
4391 status_grep(struct view *view, struct line *line)
4392 {
4393 struct status *status = line->data;
4394 enum { S_STATUS, S_NAME, S_END } state;
4395 char buf[2] = "?";
4396 regmatch_t pmatch;
4398 if (!status)
4399 return FALSE;
4401 for (state = S_STATUS; state < S_END; state++) {
4402 char *text;
4404 switch (state) {
4405 case S_NAME: text = status->new.name; break;
4406 case S_STATUS:
4407 buf[0] = status->status;
4408 text = buf;
4409 break;
4411 default:
4412 return FALSE;
4413 }
4415 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4416 return TRUE;
4417 }
4419 return FALSE;
4420 }
4422 static struct view_ops status_ops = {
4423 "file",
4424 status_open,
4425 NULL,
4426 status_draw,
4427 status_request,
4428 status_grep,
4429 status_select,
4430 };
4433 static bool
4434 stage_diff_line(FILE *pipe, struct line *line)
4435 {
4436 char *buf = line->data;
4437 size_t bufsize = strlen(buf);
4438 size_t written = 0;
4440 while (!ferror(pipe) && written < bufsize) {
4441 written += fwrite(buf + written, 1, bufsize - written, pipe);
4442 }
4444 fputc('\n', pipe);
4446 return written == bufsize;
4447 }
4449 static struct line *
4450 stage_diff_hdr(struct view *view, struct line *line)
4451 {
4452 int diff_hdr_dir = line->type == LINE_DIFF_CHUNK ? -1 : 1;
4453 struct line *diff_hdr;
4455 if (line->type == LINE_DIFF_CHUNK)
4456 diff_hdr = line - 1;
4457 else
4458 diff_hdr = view->line + 1;
4460 while (diff_hdr > view->line && diff_hdr < view->line + view->lines) {
4461 if (diff_hdr->type == LINE_DIFF_HEADER)
4462 return diff_hdr;
4464 diff_hdr += diff_hdr_dir;
4465 }
4467 return NULL;
4468 }
4470 static bool
4471 stage_update_chunk(struct view *view, struct line *line)
4472 {
4473 char cmd[SIZEOF_STR];
4474 size_t cmdsize = 0;
4475 struct line *diff_hdr, *diff_chunk, *diff_end;
4476 FILE *pipe;
4478 diff_hdr = stage_diff_hdr(view, line);
4479 if (!diff_hdr)
4480 return FALSE;
4482 if (opt_cdup[0] &&
4483 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4484 return FALSE;
4486 if (!string_format_from(cmd, &cmdsize,
4487 "git apply --whitespace=nowarn --cached %s - && "
4488 "git update-index -q --unmerged --refresh 2>/dev/null",
4489 stage_line_type == LINE_STAT_STAGED ? "-R" : ""))
4490 return FALSE;
4492 pipe = popen(cmd, "w");
4493 if (!pipe)
4494 return FALSE;
4496 diff_end = view->line + view->lines;
4497 if (line->type != LINE_DIFF_CHUNK) {
4498 diff_chunk = diff_hdr;
4500 } else {
4501 for (diff_chunk = line + 1; diff_chunk < diff_end; diff_chunk++)
4502 if (diff_chunk->type == LINE_DIFF_CHUNK ||
4503 diff_chunk->type == LINE_DIFF_HEADER)
4504 diff_end = diff_chunk;
4506 diff_chunk = line;
4508 while (diff_hdr->type != LINE_DIFF_CHUNK) {
4509 switch (diff_hdr->type) {
4510 case LINE_DIFF_HEADER:
4511 case LINE_DIFF_INDEX:
4512 case LINE_DIFF_ADD:
4513 case LINE_DIFF_DEL:
4514 break;
4516 default:
4517 diff_hdr++;
4518 continue;
4519 }
4521 if (!stage_diff_line(pipe, diff_hdr++)) {
4522 pclose(pipe);
4523 return FALSE;
4524 }
4525 }
4526 }
4528 while (diff_chunk < diff_end && stage_diff_line(pipe, diff_chunk))
4529 diff_chunk++;
4531 pclose(pipe);
4533 if (diff_chunk != diff_end)
4534 return FALSE;
4536 return TRUE;
4537 }
4539 static void
4540 stage_update(struct view *view, struct line *line)
4541 {
4542 if (!opt_no_head && stage_line_type != LINE_STAT_UNTRACKED &&
4543 (line->type == LINE_DIFF_CHUNK || !stage_status.status)) {
4544 if (!stage_update_chunk(view, line)) {
4545 report("Failed to apply chunk");
4546 return;
4547 }
4549 } else if (!status_update_file(&stage_status, stage_line_type)) {
4550 report("Failed to update file");
4551 return;
4552 }
4554 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
4556 view = VIEW(REQ_VIEW_STATUS);
4557 if (view_is_displayed(view))
4558 status_enter(view, &view->line[view->lineno]);
4559 }
4561 static enum request
4562 stage_request(struct view *view, enum request request, struct line *line)
4563 {
4564 switch (request) {
4565 case REQ_STATUS_UPDATE:
4566 stage_update(view, line);
4567 break;
4569 case REQ_EDIT:
4570 if (!stage_status.new.name[0])
4571 return request;
4573 open_editor(stage_status.status != '?', stage_status.new.name);
4574 break;
4576 case REQ_VIEW_BLAME:
4577 if (stage_status.new.name[0]) {
4578 string_copy(opt_file, stage_status.new.name);
4579 opt_ref[0] = 0;
4580 }
4581 return request;
4583 case REQ_ENTER:
4584 pager_request(view, request, line);
4585 break;
4587 default:
4588 return request;
4589 }
4591 return REQ_NONE;
4592 }
4594 static struct view_ops stage_ops = {
4595 "line",
4596 NULL,
4597 pager_read,
4598 pager_draw,
4599 stage_request,
4600 pager_grep,
4601 pager_select,
4602 };
4605 /*
4606 * Revision graph
4607 */
4609 struct commit {
4610 char id[SIZEOF_REV]; /* SHA1 ID. */
4611 char title[128]; /* First line of the commit message. */
4612 char author[75]; /* Author of the commit. */
4613 struct tm time; /* Date from the author ident. */
4614 struct ref **refs; /* Repository references. */
4615 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
4616 size_t graph_size; /* The width of the graph array. */
4617 bool has_parents; /* Rewritten --parents seen. */
4618 };
4620 /* Size of rev graph with no "padding" columns */
4621 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
4623 struct rev_graph {
4624 struct rev_graph *prev, *next, *parents;
4625 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
4626 size_t size;
4627 struct commit *commit;
4628 size_t pos;
4629 unsigned int boundary:1;
4630 };
4632 /* Parents of the commit being visualized. */
4633 static struct rev_graph graph_parents[4];
4635 /* The current stack of revisions on the graph. */
4636 static struct rev_graph graph_stacks[4] = {
4637 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
4638 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
4639 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
4640 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
4641 };
4643 static inline bool
4644 graph_parent_is_merge(struct rev_graph *graph)
4645 {
4646 return graph->parents->size > 1;
4647 }
4649 static inline void
4650 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
4651 {
4652 struct commit *commit = graph->commit;
4654 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
4655 commit->graph[commit->graph_size++] = symbol;
4656 }
4658 static void
4659 done_rev_graph(struct rev_graph *graph)
4660 {
4661 if (graph_parent_is_merge(graph) &&
4662 graph->pos < graph->size - 1 &&
4663 graph->next->size == graph->size + graph->parents->size - 1) {
4664 size_t i = graph->pos + graph->parents->size - 1;
4666 graph->commit->graph_size = i * 2;
4667 while (i < graph->next->size - 1) {
4668 append_to_rev_graph(graph, ' ');
4669 append_to_rev_graph(graph, '\\');
4670 i++;
4671 }
4672 }
4674 graph->size = graph->pos = 0;
4675 graph->commit = NULL;
4676 memset(graph->parents, 0, sizeof(*graph->parents));
4677 }
4679 static void
4680 push_rev_graph(struct rev_graph *graph, char *parent)
4681 {
4682 int i;
4684 /* "Collapse" duplicate parents lines.
4685 *
4686 * FIXME: This needs to also update update the drawn graph but
4687 * for now it just serves as a method for pruning graph lines. */
4688 for (i = 0; i < graph->size; i++)
4689 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
4690 return;
4692 if (graph->size < SIZEOF_REVITEMS) {
4693 string_copy_rev(graph->rev[graph->size++], parent);
4694 }
4695 }
4697 static chtype
4698 get_rev_graph_symbol(struct rev_graph *graph)
4699 {
4700 chtype symbol;
4702 if (graph->boundary)
4703 symbol = REVGRAPH_BOUND;
4704 else if (graph->parents->size == 0)
4705 symbol = REVGRAPH_INIT;
4706 else if (graph_parent_is_merge(graph))
4707 symbol = REVGRAPH_MERGE;
4708 else if (graph->pos >= graph->size)
4709 symbol = REVGRAPH_BRANCH;
4710 else
4711 symbol = REVGRAPH_COMMIT;
4713 return symbol;
4714 }
4716 static void
4717 draw_rev_graph(struct rev_graph *graph)
4718 {
4719 struct rev_filler {
4720 chtype separator, line;
4721 };
4722 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
4723 static struct rev_filler fillers[] = {
4724 { ' ', REVGRAPH_LINE },
4725 { '`', '.' },
4726 { '\'', ' ' },
4727 { '/', ' ' },
4728 };
4729 chtype symbol = get_rev_graph_symbol(graph);
4730 struct rev_filler *filler;
4731 size_t i;
4733 filler = &fillers[DEFAULT];
4735 for (i = 0; i < graph->pos; i++) {
4736 append_to_rev_graph(graph, filler->line);
4737 if (graph_parent_is_merge(graph->prev) &&
4738 graph->prev->pos == i)
4739 filler = &fillers[RSHARP];
4741 append_to_rev_graph(graph, filler->separator);
4742 }
4744 /* Place the symbol for this revision. */
4745 append_to_rev_graph(graph, symbol);
4747 if (graph->prev->size > graph->size)
4748 filler = &fillers[RDIAG];
4749 else
4750 filler = &fillers[DEFAULT];
4752 i++;
4754 for (; i < graph->size; i++) {
4755 append_to_rev_graph(graph, filler->separator);
4756 append_to_rev_graph(graph, filler->line);
4757 if (graph_parent_is_merge(graph->prev) &&
4758 i < graph->prev->pos + graph->parents->size)
4759 filler = &fillers[RSHARP];
4760 if (graph->prev->size > graph->size)
4761 filler = &fillers[LDIAG];
4762 }
4764 if (graph->prev->size > graph->size) {
4765 append_to_rev_graph(graph, filler->separator);
4766 if (filler->line != ' ')
4767 append_to_rev_graph(graph, filler->line);
4768 }
4769 }
4771 /* Prepare the next rev graph */
4772 static void
4773 prepare_rev_graph(struct rev_graph *graph)
4774 {
4775 size_t i;
4777 /* First, traverse all lines of revisions up to the active one. */
4778 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
4779 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
4780 break;
4782 push_rev_graph(graph->next, graph->rev[graph->pos]);
4783 }
4785 /* Interleave the new revision parent(s). */
4786 for (i = 0; !graph->boundary && i < graph->parents->size; i++)
4787 push_rev_graph(graph->next, graph->parents->rev[i]);
4789 /* Lastly, put any remaining revisions. */
4790 for (i = graph->pos + 1; i < graph->size; i++)
4791 push_rev_graph(graph->next, graph->rev[i]);
4792 }
4794 static void
4795 update_rev_graph(struct rev_graph *graph)
4796 {
4797 /* If this is the finalizing update ... */
4798 if (graph->commit)
4799 prepare_rev_graph(graph);
4801 /* Graph visualization needs a one rev look-ahead,
4802 * so the first update doesn't visualize anything. */
4803 if (!graph->prev->commit)
4804 return;
4806 draw_rev_graph(graph->prev);
4807 done_rev_graph(graph->prev->prev);
4808 }
4811 /*
4812 * Main view backend
4813 */
4815 static bool
4816 main_draw(struct view *view, struct line *line, unsigned int lineno, bool selected)
4817 {
4818 char buf[DATE_COLS + 1];
4819 struct commit *commit = line->data;
4820 enum line_type type;
4821 int col = 0;
4822 size_t timelen;
4823 int space;
4825 if (!*commit->author)
4826 return FALSE;
4828 space = view->width;
4829 wmove(view->win, lineno, col);
4831 if (selected) {
4832 type = LINE_CURSOR;
4833 wattrset(view->win, get_line_attr(type));
4834 wchgat(view->win, -1, 0, type, NULL);
4835 } else {
4836 type = LINE_MAIN_COMMIT;
4837 wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
4838 }
4840 if (opt_date) {
4841 int n;
4843 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
4844 n = draw_text(view, buf, view->width - col, FALSE, selected);
4845 draw_text(view, " ", view->width - col - n, FALSE, selected);
4847 col += DATE_COLS;
4848 wmove(view->win, lineno, col);
4849 if (col >= view->width)
4850 return TRUE;
4851 }
4852 if (type != LINE_CURSOR)
4853 wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
4855 if (opt_author) {
4856 int max_len;
4858 max_len = view->width - col;
4859 if (max_len > AUTHOR_COLS - 1)
4860 max_len = AUTHOR_COLS - 1;
4861 draw_text(view, commit->author, max_len, TRUE, selected);
4862 col += AUTHOR_COLS;
4863 if (col >= view->width)
4864 return TRUE;
4865 }
4867 if (opt_rev_graph && commit->graph_size) {
4868 size_t graph_size = view->width - col;
4869 size_t i;
4871 if (type != LINE_CURSOR)
4872 wattrset(view->win, get_line_attr(LINE_MAIN_REVGRAPH));
4873 wmove(view->win, lineno, col);
4874 if (graph_size > commit->graph_size)
4875 graph_size = commit->graph_size;
4876 /* Using waddch() instead of waddnstr() ensures that
4877 * they'll be rendered correctly for the cursor line. */
4878 for (i = 0; i < graph_size; i++)
4879 waddch(view->win, commit->graph[i]);
4881 col += commit->graph_size + 1;
4882 if (col >= view->width)
4883 return TRUE;
4884 waddch(view->win, ' ');
4885 }
4886 if (type != LINE_CURSOR)
4887 wattrset(view->win, A_NORMAL);
4889 wmove(view->win, lineno, col);
4891 if (opt_show_refs && commit->refs) {
4892 size_t i = 0;
4894 do {
4895 if (type == LINE_CURSOR)
4896 ;
4897 else if (commit->refs[i]->head)
4898 wattrset(view->win, get_line_attr(LINE_MAIN_HEAD));
4899 else if (commit->refs[i]->ltag)
4900 wattrset(view->win, get_line_attr(LINE_MAIN_LOCAL_TAG));
4901 else if (commit->refs[i]->tag)
4902 wattrset(view->win, get_line_attr(LINE_MAIN_TAG));
4903 else if (commit->refs[i]->remote)
4904 wattrset(view->win, get_line_attr(LINE_MAIN_REMOTE));
4905 else
4906 wattrset(view->win, get_line_attr(LINE_MAIN_REF));
4908 col += draw_text(view, "[", view->width - col, TRUE, selected);
4909 col += draw_text(view, commit->refs[i]->name, view->width - col,
4910 TRUE, selected);
4911 col += draw_text(view, "]", view->width - col, TRUE, selected);
4912 if (type != LINE_CURSOR)
4913 wattrset(view->win, A_NORMAL);
4914 col += draw_text(view, " ", view->width - col, TRUE, selected);
4915 if (col >= view->width)
4916 return TRUE;
4917 } while (commit->refs[i++]->next);
4918 }
4920 if (type != LINE_CURSOR)
4921 wattrset(view->win, get_line_attr(type));
4923 draw_text(view, commit->title, view->width - col, TRUE, selected);
4924 return TRUE;
4925 }
4927 /* Reads git log --pretty=raw output and parses it into the commit struct. */
4928 static bool
4929 main_read(struct view *view, char *line)
4930 {
4931 static struct rev_graph *graph = graph_stacks;
4932 enum line_type type;
4933 struct commit *commit;
4935 if (!line) {
4936 update_rev_graph(graph);
4937 return TRUE;
4938 }
4940 type = get_line_type(line);
4941 if (type == LINE_COMMIT) {
4942 commit = calloc(1, sizeof(struct commit));
4943 if (!commit)
4944 return FALSE;
4946 line += STRING_SIZE("commit ");
4947 if (*line == '-') {
4948 graph->boundary = 1;
4949 line++;
4950 }
4952 string_copy_rev(commit->id, line);
4953 commit->refs = get_refs(commit->id);
4954 graph->commit = commit;
4955 add_line_data(view, commit, LINE_MAIN_COMMIT);
4957 while ((line = strchr(line, ' '))) {
4958 line++;
4959 push_rev_graph(graph->parents, line);
4960 commit->has_parents = TRUE;
4961 }
4962 return TRUE;
4963 }
4965 if (!view->lines)
4966 return TRUE;
4967 commit = view->line[view->lines - 1].data;
4969 switch (type) {
4970 case LINE_PARENT:
4971 if (commit->has_parents)
4972 break;
4973 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
4974 break;
4976 case LINE_AUTHOR:
4977 {
4978 /* Parse author lines where the name may be empty:
4979 * author <email@address.tld> 1138474660 +0100
4980 */
4981 char *ident = line + STRING_SIZE("author ");
4982 char *nameend = strchr(ident, '<');
4983 char *emailend = strchr(ident, '>');
4985 if (!nameend || !emailend)
4986 break;
4988 update_rev_graph(graph);
4989 graph = graph->next;
4991 *nameend = *emailend = 0;
4992 ident = chomp_string(ident);
4993 if (!*ident) {
4994 ident = chomp_string(nameend + 1);
4995 if (!*ident)
4996 ident = "Unknown";
4997 }
4999 string_ncopy(commit->author, ident, strlen(ident));
5001 /* Parse epoch and timezone */
5002 if (emailend[1] == ' ') {
5003 char *secs = emailend + 2;
5004 char *zone = strchr(secs, ' ');
5005 time_t time = (time_t) atol(secs);
5007 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
5008 long tz;
5010 zone++;
5011 tz = ('0' - zone[1]) * 60 * 60 * 10;
5012 tz += ('0' - zone[2]) * 60 * 60;
5013 tz += ('0' - zone[3]) * 60;
5014 tz += ('0' - zone[4]) * 60;
5016 if (zone[0] == '-')
5017 tz = -tz;
5019 time -= tz;
5020 }
5022 gmtime_r(&time, &commit->time);
5023 }
5024 break;
5025 }
5026 default:
5027 /* Fill in the commit title if it has not already been set. */
5028 if (commit->title[0])
5029 break;
5031 /* Require titles to start with a non-space character at the
5032 * offset used by git log. */
5033 if (strncmp(line, " ", 4))
5034 break;
5035 line += 4;
5036 /* Well, if the title starts with a whitespace character,
5037 * try to be forgiving. Otherwise we end up with no title. */
5038 while (isspace(*line))
5039 line++;
5040 if (*line == '\0')
5041 break;
5042 /* FIXME: More graceful handling of titles; append "..." to
5043 * shortened titles, etc. */
5045 string_ncopy(commit->title, line, strlen(line));
5046 }
5048 return TRUE;
5049 }
5051 static enum request
5052 main_request(struct view *view, enum request request, struct line *line)
5053 {
5054 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
5056 if (request == REQ_ENTER)
5057 open_view(view, REQ_VIEW_DIFF, flags);
5058 else
5059 return request;
5061 return REQ_NONE;
5062 }
5064 static bool
5065 main_grep(struct view *view, struct line *line)
5066 {
5067 struct commit *commit = line->data;
5068 enum { S_TITLE, S_AUTHOR, S_DATE, S_END } state;
5069 char buf[DATE_COLS + 1];
5070 regmatch_t pmatch;
5072 for (state = S_TITLE; state < S_END; state++) {
5073 char *text;
5075 switch (state) {
5076 case S_TITLE: text = commit->title; break;
5077 case S_AUTHOR: text = commit->author; break;
5078 case S_DATE:
5079 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
5080 continue;
5081 text = buf;
5082 break;
5084 default:
5085 return FALSE;
5086 }
5088 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
5089 return TRUE;
5090 }
5092 return FALSE;
5093 }
5095 static void
5096 main_select(struct view *view, struct line *line)
5097 {
5098 struct commit *commit = line->data;
5100 string_copy_rev(view->ref, commit->id);
5101 string_copy_rev(ref_commit, view->ref);
5102 }
5104 static struct view_ops main_ops = {
5105 "commit",
5106 NULL,
5107 main_read,
5108 main_draw,
5109 main_request,
5110 main_grep,
5111 main_select,
5112 };
5115 /*
5116 * Unicode / UTF-8 handling
5117 *
5118 * NOTE: Much of the following code for dealing with unicode is derived from
5119 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
5120 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
5121 */
5123 /* I've (over)annotated a lot of code snippets because I am not entirely
5124 * confident that the approach taken by this small UTF-8 interface is correct.
5125 * --jonas */
5127 static inline int
5128 unicode_width(unsigned long c)
5129 {
5130 if (c >= 0x1100 &&
5131 (c <= 0x115f /* Hangul Jamo */
5132 || c == 0x2329
5133 || c == 0x232a
5134 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
5135 /* CJK ... Yi */
5136 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
5137 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
5138 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
5139 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
5140 || (c >= 0xffe0 && c <= 0xffe6)
5141 || (c >= 0x20000 && c <= 0x2fffd)
5142 || (c >= 0x30000 && c <= 0x3fffd)))
5143 return 2;
5145 if (c == '\t')
5146 return opt_tab_size;
5148 return 1;
5149 }
5151 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
5152 * Illegal bytes are set one. */
5153 static const unsigned char utf8_bytes[256] = {
5154 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,
5155 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,
5156 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,
5157 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,
5158 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,
5159 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,
5160 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,
5161 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,
5162 };
5164 /* Decode UTF-8 multi-byte representation into a unicode character. */
5165 static inline unsigned long
5166 utf8_to_unicode(const char *string, size_t length)
5167 {
5168 unsigned long unicode;
5170 switch (length) {
5171 case 1:
5172 unicode = string[0];
5173 break;
5174 case 2:
5175 unicode = (string[0] & 0x1f) << 6;
5176 unicode += (string[1] & 0x3f);
5177 break;
5178 case 3:
5179 unicode = (string[0] & 0x0f) << 12;
5180 unicode += ((string[1] & 0x3f) << 6);
5181 unicode += (string[2] & 0x3f);
5182 break;
5183 case 4:
5184 unicode = (string[0] & 0x0f) << 18;
5185 unicode += ((string[1] & 0x3f) << 12);
5186 unicode += ((string[2] & 0x3f) << 6);
5187 unicode += (string[3] & 0x3f);
5188 break;
5189 case 5:
5190 unicode = (string[0] & 0x0f) << 24;
5191 unicode += ((string[1] & 0x3f) << 18);
5192 unicode += ((string[2] & 0x3f) << 12);
5193 unicode += ((string[3] & 0x3f) << 6);
5194 unicode += (string[4] & 0x3f);
5195 break;
5196 case 6:
5197 unicode = (string[0] & 0x01) << 30;
5198 unicode += ((string[1] & 0x3f) << 24);
5199 unicode += ((string[2] & 0x3f) << 18);
5200 unicode += ((string[3] & 0x3f) << 12);
5201 unicode += ((string[4] & 0x3f) << 6);
5202 unicode += (string[5] & 0x3f);
5203 break;
5204 default:
5205 die("Invalid unicode length");
5206 }
5208 /* Invalid characters could return the special 0xfffd value but NUL
5209 * should be just as good. */
5210 return unicode > 0xffff ? 0 : unicode;
5211 }
5213 /* Calculates how much of string can be shown within the given maximum width
5214 * and sets trimmed parameter to non-zero value if all of string could not be
5215 * shown. If the reserve flag is TRUE, it will reserve at least one
5216 * trailing character, which can be useful when drawing a delimiter.
5217 *
5218 * Returns the number of bytes to output from string to satisfy max_width. */
5219 static size_t
5220 utf8_length(const char *string, size_t max_width, int *trimmed, bool reserve)
5221 {
5222 const char *start = string;
5223 const char *end = strchr(string, '\0');
5224 unsigned char last_bytes = 0;
5225 size_t width = 0;
5227 *trimmed = 0;
5229 while (string < end) {
5230 int c = *(unsigned char *) string;
5231 unsigned char bytes = utf8_bytes[c];
5232 size_t ucwidth;
5233 unsigned long unicode;
5235 if (string + bytes > end)
5236 break;
5238 /* Change representation to figure out whether
5239 * it is a single- or double-width character. */
5241 unicode = utf8_to_unicode(string, bytes);
5242 /* FIXME: Graceful handling of invalid unicode character. */
5243 if (!unicode)
5244 break;
5246 ucwidth = unicode_width(unicode);
5247 width += ucwidth;
5248 if (width > max_width) {
5249 *trimmed = 1;
5250 if (reserve && width - ucwidth == max_width) {
5251 string -= last_bytes;
5252 }
5253 break;
5254 }
5256 string += bytes;
5257 last_bytes = bytes;
5258 }
5260 return string - start;
5261 }
5264 /*
5265 * Status management
5266 */
5268 /* Whether or not the curses interface has been initialized. */
5269 static bool cursed = FALSE;
5271 /* The status window is used for polling keystrokes. */
5272 static WINDOW *status_win;
5274 static bool status_empty = TRUE;
5276 /* Update status and title window. */
5277 static void
5278 report(const char *msg, ...)
5279 {
5280 struct view *view = display[current_view];
5282 if (input_mode)
5283 return;
5285 if (!view) {
5286 char buf[SIZEOF_STR];
5287 va_list args;
5289 va_start(args, msg);
5290 if (vsnprintf(buf, sizeof(buf), msg, args) >= sizeof(buf)) {
5291 buf[sizeof(buf) - 1] = 0;
5292 buf[sizeof(buf) - 2] = '.';
5293 buf[sizeof(buf) - 3] = '.';
5294 buf[sizeof(buf) - 4] = '.';
5295 }
5296 va_end(args);
5297 die("%s", buf);
5298 }
5300 if (!status_empty || *msg) {
5301 va_list args;
5303 va_start(args, msg);
5305 wmove(status_win, 0, 0);
5306 if (*msg) {
5307 vwprintw(status_win, msg, args);
5308 status_empty = FALSE;
5309 } else {
5310 status_empty = TRUE;
5311 }
5312 wclrtoeol(status_win);
5313 wrefresh(status_win);
5315 va_end(args);
5316 }
5318 update_view_title(view);
5319 update_display_cursor(view);
5320 }
5322 /* Controls when nodelay should be in effect when polling user input. */
5323 static void
5324 set_nonblocking_input(bool loading)
5325 {
5326 static unsigned int loading_views;
5328 if ((loading == FALSE && loading_views-- == 1) ||
5329 (loading == TRUE && loading_views++ == 0))
5330 nodelay(status_win, loading);
5331 }
5333 static void
5334 init_display(void)
5335 {
5336 int x, y;
5338 /* Initialize the curses library */
5339 if (isatty(STDIN_FILENO)) {
5340 cursed = !!initscr();
5341 } else {
5342 /* Leave stdin and stdout alone when acting as a pager. */
5343 FILE *io = fopen("/dev/tty", "r+");
5345 if (!io)
5346 die("Failed to open /dev/tty");
5347 cursed = !!newterm(NULL, io, io);
5348 }
5350 if (!cursed)
5351 die("Failed to initialize curses");
5353 nonl(); /* Tell curses not to do NL->CR/NL on output */
5354 cbreak(); /* Take input chars one at a time, no wait for \n */
5355 noecho(); /* Don't echo input */
5356 leaveok(stdscr, TRUE);
5358 if (has_colors())
5359 init_colors();
5361 getmaxyx(stdscr, y, x);
5362 status_win = newwin(1, 0, y - 1, 0);
5363 if (!status_win)
5364 die("Failed to create status window");
5366 /* Enable keyboard mapping */
5367 keypad(status_win, TRUE);
5368 wbkgdset(status_win, get_line_attr(LINE_STATUS));
5369 }
5371 static char *
5372 read_prompt(const char *prompt)
5373 {
5374 enum { READING, STOP, CANCEL } status = READING;
5375 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
5376 int pos = 0;
5378 while (status == READING) {
5379 struct view *view;
5380 int i, key;
5382 input_mode = TRUE;
5384 foreach_view (view, i)
5385 update_view(view);
5387 input_mode = FALSE;
5389 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
5390 wclrtoeol(status_win);
5392 /* Refresh, accept single keystroke of input */
5393 key = wgetch(status_win);
5394 switch (key) {
5395 case KEY_RETURN:
5396 case KEY_ENTER:
5397 case '\n':
5398 status = pos ? STOP : CANCEL;
5399 break;
5401 case KEY_BACKSPACE:
5402 if (pos > 0)
5403 pos--;
5404 else
5405 status = CANCEL;
5406 break;
5408 case KEY_ESC:
5409 status = CANCEL;
5410 break;
5412 case ERR:
5413 break;
5415 default:
5416 if (pos >= sizeof(buf)) {
5417 report("Input string too long");
5418 return NULL;
5419 }
5421 if (isprint(key))
5422 buf[pos++] = (char) key;
5423 }
5424 }
5426 /* Clear the status window */
5427 status_empty = FALSE;
5428 report("");
5430 if (status == CANCEL)
5431 return NULL;
5433 buf[pos++] = 0;
5435 return buf;
5436 }
5438 /*
5439 * Repository references
5440 */
5442 static struct ref *refs = NULL;
5443 static size_t refs_alloc = 0;
5444 static size_t refs_size = 0;
5446 /* Id <-> ref store */
5447 static struct ref ***id_refs = NULL;
5448 static size_t id_refs_alloc = 0;
5449 static size_t id_refs_size = 0;
5451 static struct ref **
5452 get_refs(char *id)
5453 {
5454 struct ref ***tmp_id_refs;
5455 struct ref **ref_list = NULL;
5456 size_t ref_list_alloc = 0;
5457 size_t ref_list_size = 0;
5458 size_t i;
5460 for (i = 0; i < id_refs_size; i++)
5461 if (!strcmp(id, id_refs[i][0]->id))
5462 return id_refs[i];
5464 tmp_id_refs = realloc_items(id_refs, &id_refs_alloc, id_refs_size + 1,
5465 sizeof(*id_refs));
5466 if (!tmp_id_refs)
5467 return NULL;
5469 id_refs = tmp_id_refs;
5471 for (i = 0; i < refs_size; i++) {
5472 struct ref **tmp;
5474 if (strcmp(id, refs[i].id))
5475 continue;
5477 tmp = realloc_items(ref_list, &ref_list_alloc,
5478 ref_list_size + 1, sizeof(*ref_list));
5479 if (!tmp) {
5480 if (ref_list)
5481 free(ref_list);
5482 return NULL;
5483 }
5485 ref_list = tmp;
5486 if (ref_list_size > 0)
5487 ref_list[ref_list_size - 1]->next = 1;
5488 ref_list[ref_list_size] = &refs[i];
5490 /* XXX: The properties of the commit chains ensures that we can
5491 * safely modify the shared ref. The repo references will
5492 * always be similar for the same id. */
5493 ref_list[ref_list_size]->next = 0;
5494 ref_list_size++;
5495 }
5497 if (ref_list)
5498 id_refs[id_refs_size++] = ref_list;
5500 return ref_list;
5501 }
5503 static int
5504 read_ref(char *id, size_t idlen, char *name, size_t namelen)
5505 {
5506 struct ref *ref;
5507 bool tag = FALSE;
5508 bool ltag = FALSE;
5509 bool remote = FALSE;
5510 bool check_replace = FALSE;
5511 bool head = FALSE;
5513 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
5514 if (!strcmp(name + namelen - 3, "^{}")) {
5515 namelen -= 3;
5516 name[namelen] = 0;
5517 if (refs_size > 0 && refs[refs_size - 1].ltag == TRUE)
5518 check_replace = TRUE;
5519 } else {
5520 ltag = TRUE;
5521 }
5523 tag = TRUE;
5524 namelen -= STRING_SIZE("refs/tags/");
5525 name += STRING_SIZE("refs/tags/");
5527 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
5528 remote = TRUE;
5529 namelen -= STRING_SIZE("refs/remotes/");
5530 name += STRING_SIZE("refs/remotes/");
5532 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5533 namelen -= STRING_SIZE("refs/heads/");
5534 name += STRING_SIZE("refs/heads/");
5535 head = !strncmp(opt_head, name, namelen);
5537 } else if (!strcmp(name, "HEAD")) {
5538 opt_no_head = FALSE;
5539 return OK;
5540 }
5542 if (check_replace && !strcmp(name, refs[refs_size - 1].name)) {
5543 /* it's an annotated tag, replace the previous sha1 with the
5544 * resolved commit id; relies on the fact git-ls-remote lists
5545 * the commit id of an annotated tag right beofre the commit id
5546 * it points to. */
5547 refs[refs_size - 1].ltag = ltag;
5548 string_copy_rev(refs[refs_size - 1].id, id);
5550 return OK;
5551 }
5552 refs = realloc_items(refs, &refs_alloc, refs_size + 1, sizeof(*refs));
5553 if (!refs)
5554 return ERR;
5556 ref = &refs[refs_size++];
5557 ref->name = malloc(namelen + 1);
5558 if (!ref->name)
5559 return ERR;
5561 strncpy(ref->name, name, namelen);
5562 ref->name[namelen] = 0;
5563 ref->tag = tag;
5564 ref->ltag = ltag;
5565 ref->remote = remote;
5566 ref->head = head;
5567 string_copy_rev(ref->id, id);
5569 return OK;
5570 }
5572 static int
5573 load_refs(void)
5574 {
5575 const char *cmd_env = getenv("TIG_LS_REMOTE");
5576 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
5578 return read_properties(popen(cmd, "r"), "\t", read_ref);
5579 }
5581 static int
5582 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
5583 {
5584 if (!strcmp(name, "i18n.commitencoding"))
5585 string_ncopy(opt_encoding, value, valuelen);
5587 if (!strcmp(name, "core.editor"))
5588 string_ncopy(opt_editor, value, valuelen);
5590 return OK;
5591 }
5593 static int
5594 load_repo_config(void)
5595 {
5596 return read_properties(popen(GIT_CONFIG " --list", "r"),
5597 "=", read_repo_config_option);
5598 }
5600 static int
5601 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
5602 {
5603 if (!opt_git_dir[0]) {
5604 string_ncopy(opt_git_dir, name, namelen);
5606 } else if (opt_is_inside_work_tree == -1) {
5607 /* This can be 3 different values depending on the
5608 * version of git being used. If git-rev-parse does not
5609 * understand --is-inside-work-tree it will simply echo
5610 * the option else either "true" or "false" is printed.
5611 * Default to true for the unknown case. */
5612 opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
5614 } else if (opt_cdup[0] == ' ') {
5615 string_ncopy(opt_cdup, name, namelen);
5616 } else {
5617 if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5618 namelen -= STRING_SIZE("refs/heads/");
5619 name += STRING_SIZE("refs/heads/");
5620 string_ncopy(opt_head, name, namelen);
5621 }
5622 }
5624 return OK;
5625 }
5627 static int
5628 load_repo_info(void)
5629 {
5630 int result;
5631 FILE *pipe = popen("git rev-parse --git-dir --is-inside-work-tree "
5632 " --show-cdup --symbolic-full-name HEAD 2>/dev/null", "r");
5634 /* XXX: The line outputted by "--show-cdup" can be empty so
5635 * initialize it to something invalid to make it possible to
5636 * detect whether it has been set or not. */
5637 opt_cdup[0] = ' ';
5639 result = read_properties(pipe, "=", read_repo_info);
5640 if (opt_cdup[0] == ' ')
5641 opt_cdup[0] = 0;
5643 return result;
5644 }
5646 static int
5647 read_properties(FILE *pipe, const char *separators,
5648 int (*read_property)(char *, size_t, char *, size_t))
5649 {
5650 char buffer[BUFSIZ];
5651 char *name;
5652 int state = OK;
5654 if (!pipe)
5655 return ERR;
5657 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
5658 char *value;
5659 size_t namelen;
5660 size_t valuelen;
5662 name = chomp_string(name);
5663 namelen = strcspn(name, separators);
5665 if (name[namelen]) {
5666 name[namelen] = 0;
5667 value = chomp_string(name + namelen + 1);
5668 valuelen = strlen(value);
5670 } else {
5671 value = "";
5672 valuelen = 0;
5673 }
5675 state = read_property(name, namelen, value, valuelen);
5676 }
5678 if (state != ERR && ferror(pipe))
5679 state = ERR;
5681 pclose(pipe);
5683 return state;
5684 }
5687 /*
5688 * Main
5689 */
5691 static void __NORETURN
5692 quit(int sig)
5693 {
5694 /* XXX: Restore tty modes and let the OS cleanup the rest! */
5695 if (cursed)
5696 endwin();
5697 exit(0);
5698 }
5700 static void __NORETURN
5701 die(const char *err, ...)
5702 {
5703 va_list args;
5705 endwin();
5707 va_start(args, err);
5708 fputs("tig: ", stderr);
5709 vfprintf(stderr, err, args);
5710 fputs("\n", stderr);
5711 va_end(args);
5713 exit(1);
5714 }
5716 static void
5717 warn(const char *msg, ...)
5718 {
5719 va_list args;
5721 va_start(args, msg);
5722 fputs("tig warning: ", stderr);
5723 vfprintf(stderr, msg, args);
5724 fputs("\n", stderr);
5725 va_end(args);
5726 }
5728 int
5729 main(int argc, char *argv[])
5730 {
5731 struct view *view;
5732 enum request request;
5733 size_t i;
5735 signal(SIGINT, quit);
5737 if (setlocale(LC_ALL, "")) {
5738 char *codeset = nl_langinfo(CODESET);
5740 string_ncopy(opt_codeset, codeset, strlen(codeset));
5741 }
5743 if (load_repo_info() == ERR)
5744 die("Failed to load repo info.");
5746 if (load_options() == ERR)
5747 die("Failed to load user config.");
5749 /* Load the repo config file so options can be overwritten from
5750 * the command line. */
5751 if (load_repo_config() == ERR)
5752 die("Failed to load repo config.");
5754 if (!parse_options(argc, argv))
5755 return 0;
5757 /* Require a git repository unless when running in pager mode. */
5758 if (!opt_git_dir[0] && opt_request != REQ_VIEW_PAGER)
5759 die("Not a git repository");
5761 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
5762 opt_utf8 = FALSE;
5764 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
5765 opt_iconv = iconv_open(opt_codeset, opt_encoding);
5766 if (opt_iconv == ICONV_NONE)
5767 die("Failed to initialize character set conversion");
5768 }
5770 if (load_refs() == ERR)
5771 die("Failed to load refs.");
5773 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
5774 view->cmd_env = getenv(view->cmd_env);
5776 request = opt_request;
5778 init_display();
5780 while (view_driver(display[current_view], request)) {
5781 int key;
5782 int i;
5784 foreach_view (view, i)
5785 update_view(view);
5787 /* Refresh, accept single keystroke of input */
5788 key = wgetch(status_win);
5790 /* wgetch() with nodelay() enabled returns ERR when there's no
5791 * input. */
5792 if (key == ERR) {
5793 request = REQ_NONE;
5794 continue;
5795 }
5797 request = get_keybinding(display[current_view]->keymap, key);
5799 /* Some low-level request handling. This keeps access to
5800 * status_win restricted. */
5801 switch (request) {
5802 case REQ_PROMPT:
5803 {
5804 char *cmd = read_prompt(":");
5806 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
5807 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
5808 opt_request = REQ_VIEW_DIFF;
5809 } else {
5810 opt_request = REQ_VIEW_PAGER;
5811 }
5812 break;
5813 }
5815 request = REQ_NONE;
5816 break;
5817 }
5818 case REQ_SEARCH:
5819 case REQ_SEARCH_BACK:
5820 {
5821 const char *prompt = request == REQ_SEARCH
5822 ? "/" : "?";
5823 char *search = read_prompt(prompt);
5825 if (search)
5826 string_ncopy(opt_search, search, strlen(search));
5827 else
5828 request = REQ_NONE;
5829 break;
5830 }
5831 case REQ_SCREEN_RESIZE:
5832 {
5833 int height, width;
5835 getmaxyx(stdscr, height, width);
5837 /* Resize the status view and let the view driver take
5838 * care of resizing the displayed views. */
5839 wresize(status_win, 1, width);
5840 mvwin(status_win, height - 1, 0);
5841 wrefresh(status_win);
5842 break;
5843 }
5844 default:
5845 break;
5846 }
5847 }
5849 quit(0);
5851 return 0;
5852 }