2a0d793c0bc338aed2f1bc8a6eb50d31e2da3af0
1 /* Copyright (c) 2006-2008 Jonas Fonseca <fonseca@diku.dk>
2 *
3 * This program is free software; you can redistribute it and/or
4 * modify it under the terms of the GNU General Public License as
5 * published by the Free Software Foundation; either version 2 of
6 * the License, or (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 */
14 #ifdef HAVE_CONFIG_H
15 #include "config.h"
16 #endif
18 #ifndef TIG_VERSION
19 #define TIG_VERSION "unknown-version"
20 #endif
22 #ifndef DEBUG
23 #define NDEBUG
24 #endif
26 #include <assert.h>
27 #include <errno.h>
28 #include <ctype.h>
29 #include <signal.h>
30 #include <stdarg.h>
31 #include <stdio.h>
32 #include <stdlib.h>
33 #include <string.h>
34 #include <sys/types.h>
35 #include <sys/stat.h>
36 #include <unistd.h>
37 #include <time.h>
39 #include <regex.h>
41 #include <locale.h>
42 #include <langinfo.h>
43 #include <iconv.h>
45 /* ncurses(3): Must be defined to have extended wide-character functions. */
46 #define _XOPEN_SOURCE_EXTENDED
48 #ifdef HAVE_NCURSESW_NCURSES_H
49 #include <ncursesw/ncurses.h>
50 #else
51 #ifdef HAVE_NCURSES_NCURSES_H
52 #include <ncurses/ncurses.h>
53 #else
54 #include <ncurses.h>
55 #endif
56 #endif
58 #if __GNUC__ >= 3
59 #define __NORETURN __attribute__((__noreturn__))
60 #else
61 #define __NORETURN
62 #endif
64 static void __NORETURN die(const char *err, ...);
65 static void warn(const char *msg, ...);
66 static void report(const char *msg, ...);
67 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, size_t, char *, size_t));
68 static void set_nonblocking_input(bool loading);
69 static size_t utf8_length(const char *string, int *width, size_t max_width, int *trimmed, bool reserve);
71 #define ABS(x) ((x) >= 0 ? (x) : -(x))
72 #define MIN(x, y) ((x) < (y) ? (x) : (y))
74 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
75 #define STRING_SIZE(x) (sizeof(x) - 1)
77 #define SIZEOF_STR 1024 /* Default string size. */
78 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
79 #define SIZEOF_REV 41 /* Holds a SHA-1 and an ending NUL */
81 /* Revision graph */
83 #define REVGRAPH_INIT 'I'
84 #define REVGRAPH_MERGE 'M'
85 #define REVGRAPH_BRANCH '+'
86 #define REVGRAPH_COMMIT '*'
87 #define REVGRAPH_BOUND '^'
89 #define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
91 /* This color name can be used to refer to the default term colors. */
92 #define COLOR_DEFAULT (-1)
94 #define ICONV_NONE ((iconv_t) -1)
95 #ifndef ICONV_CONST
96 #define ICONV_CONST /* nothing */
97 #endif
99 /* The format and size of the date column in the main view. */
100 #define DATE_FORMAT "%Y-%m-%d %H:%M"
101 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
103 #define AUTHOR_COLS 20
104 #define ID_COLS 8
106 /* The default interval between line numbers. */
107 #define NUMBER_INTERVAL 5
109 #define TAB_SIZE 8
111 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
113 #define NULL_ID "0000000000000000000000000000000000000000"
115 #ifndef GIT_CONFIG
116 #define GIT_CONFIG "git config"
117 #endif
119 #define TIG_LS_REMOTE \
120 "git ls-remote . 2>/dev/null"
122 #define TIG_DIFF_CMD \
123 "git show --pretty=fuller --no-color --root --patch-with-stat --find-copies-harder -C %s 2>/dev/null"
125 #define TIG_LOG_CMD \
126 "git log --no-color --cc --stat -n100 %s 2>/dev/null"
128 #define TIG_MAIN_CMD \
129 "git log --no-color --topo-order --parents --boundary --pretty=raw %s 2>/dev/null"
131 #define TIG_TREE_CMD \
132 "git ls-tree %s %s"
134 #define TIG_BLOB_CMD \
135 "git cat-file blob %s"
137 /* XXX: Needs to be defined to the empty string. */
138 #define TIG_HELP_CMD ""
139 #define TIG_PAGER_CMD ""
140 #define TIG_STATUS_CMD ""
141 #define TIG_STAGE_CMD ""
142 #define TIG_BLAME_CMD ""
144 /* Some ascii-shorthands fitted into the ncurses namespace. */
145 #define KEY_TAB '\t'
146 #define KEY_RETURN '\r'
147 #define KEY_ESC 27
150 struct ref {
151 char *name; /* Ref name; tag or head names are shortened. */
152 char id[SIZEOF_REV]; /* Commit SHA1 ID */
153 unsigned int head:1; /* Is it the current HEAD? */
154 unsigned int tag:1; /* Is it a tag? */
155 unsigned int ltag:1; /* If so, is the tag local? */
156 unsigned int remote:1; /* Is it a remote ref? */
157 unsigned int tracked:1; /* Is it the remote for the current HEAD? */
158 unsigned int next:1; /* For ref lists: are there more refs? */
159 };
161 static struct ref **get_refs(char *id);
163 struct int_map {
164 const char *name;
165 int namelen;
166 int value;
167 };
169 static int
170 set_from_int_map(struct int_map *map, size_t map_size,
171 int *value, const char *name, int namelen)
172 {
174 int i;
176 for (i = 0; i < map_size; i++)
177 if (namelen == map[i].namelen &&
178 !strncasecmp(name, map[i].name, namelen)) {
179 *value = map[i].value;
180 return OK;
181 }
183 return ERR;
184 }
187 /*
188 * String helpers
189 */
191 static inline void
192 string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
193 {
194 if (srclen > dstlen - 1)
195 srclen = dstlen - 1;
197 strncpy(dst, src, srclen);
198 dst[srclen] = 0;
199 }
201 /* Shorthands for safely copying into a fixed buffer. */
203 #define string_copy(dst, src) \
204 string_ncopy_do(dst, sizeof(dst), src, sizeof(src))
206 #define string_ncopy(dst, src, srclen) \
207 string_ncopy_do(dst, sizeof(dst), src, srclen)
209 #define string_copy_rev(dst, src) \
210 string_ncopy_do(dst, SIZEOF_REV, src, SIZEOF_REV - 1)
212 #define string_add(dst, from, src) \
213 string_ncopy_do(dst + (from), sizeof(dst) - (from), src, sizeof(src))
215 static char *
216 chomp_string(char *name)
217 {
218 int namelen;
220 while (isspace(*name))
221 name++;
223 namelen = strlen(name) - 1;
224 while (namelen > 0 && isspace(name[namelen]))
225 name[namelen--] = 0;
227 return name;
228 }
230 static bool
231 string_nformat(char *buf, size_t bufsize, size_t *bufpos, const char *fmt, ...)
232 {
233 va_list args;
234 size_t pos = bufpos ? *bufpos : 0;
236 va_start(args, fmt);
237 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
238 va_end(args);
240 if (bufpos)
241 *bufpos = pos;
243 return pos >= bufsize ? FALSE : TRUE;
244 }
246 #define string_format(buf, fmt, args...) \
247 string_nformat(buf, sizeof(buf), NULL, fmt, args)
249 #define string_format_from(buf, from, fmt, args...) \
250 string_nformat(buf, sizeof(buf), from, fmt, args)
252 static int
253 string_enum_compare(const char *str1, const char *str2, int len)
254 {
255 size_t i;
257 #define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
259 /* Diff-Header == DIFF_HEADER */
260 for (i = 0; i < len; i++) {
261 if (toupper(str1[i]) == toupper(str2[i]))
262 continue;
264 if (string_enum_sep(str1[i]) &&
265 string_enum_sep(str2[i]))
266 continue;
268 return str1[i] - str2[i];
269 }
271 return 0;
272 }
274 /* Shell quoting
275 *
276 * NOTE: The following is a slightly modified copy of the git project's shell
277 * quoting routines found in the quote.c file.
278 *
279 * Help to copy the thing properly quoted for the shell safety. any single
280 * quote is replaced with '\'', any exclamation point is replaced with '\!',
281 * and the whole thing is enclosed in a
282 *
283 * E.g.
284 * original sq_quote result
285 * name ==> name ==> 'name'
286 * a b ==> a b ==> 'a b'
287 * a'b ==> a'\''b ==> 'a'\''b'
288 * a!b ==> a'\!'b ==> 'a'\!'b'
289 */
291 static size_t
292 sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
293 {
294 char c;
296 #define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0)
298 BUFPUT('\'');
299 while ((c = *src++)) {
300 if (c == '\'' || c == '!') {
301 BUFPUT('\'');
302 BUFPUT('\\');
303 BUFPUT(c);
304 BUFPUT('\'');
305 } else {
306 BUFPUT(c);
307 }
308 }
309 BUFPUT('\'');
311 if (bufsize < SIZEOF_STR)
312 buf[bufsize] = 0;
314 return bufsize;
315 }
318 /*
319 * User requests
320 */
322 #define REQ_INFO \
323 /* XXX: Keep the view request first and in sync with views[]. */ \
324 REQ_GROUP("View switching") \
325 REQ_(VIEW_MAIN, "Show main view"), \
326 REQ_(VIEW_DIFF, "Show diff view"), \
327 REQ_(VIEW_LOG, "Show log view"), \
328 REQ_(VIEW_TREE, "Show tree view"), \
329 REQ_(VIEW_BLOB, "Show blob view"), \
330 REQ_(VIEW_BLAME, "Show blame view"), \
331 REQ_(VIEW_HELP, "Show help page"), \
332 REQ_(VIEW_PAGER, "Show pager view"), \
333 REQ_(VIEW_STATUS, "Show status view"), \
334 REQ_(VIEW_STAGE, "Show stage view"), \
335 \
336 REQ_GROUP("View manipulation") \
337 REQ_(ENTER, "Enter current line and scroll"), \
338 REQ_(NEXT, "Move to next"), \
339 REQ_(PREVIOUS, "Move to previous"), \
340 REQ_(VIEW_NEXT, "Move focus to next view"), \
341 REQ_(REFRESH, "Reload and refresh"), \
342 REQ_(MAXIMIZE, "Maximize the current view"), \
343 REQ_(VIEW_CLOSE, "Close the current view"), \
344 REQ_(QUIT, "Close all views and quit"), \
345 \
346 REQ_GROUP("Cursor navigation") \
347 REQ_(MOVE_UP, "Move cursor one line up"), \
348 REQ_(MOVE_DOWN, "Move cursor one line down"), \
349 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
350 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
351 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
352 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
353 \
354 REQ_GROUP("Scrolling") \
355 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
356 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
357 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
358 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
359 \
360 REQ_GROUP("Searching") \
361 REQ_(SEARCH, "Search the view"), \
362 REQ_(SEARCH_BACK, "Search backwards in the view"), \
363 REQ_(FIND_NEXT, "Find next search match"), \
364 REQ_(FIND_PREV, "Find previous search match"), \
365 \
366 REQ_GROUP("Misc") \
367 REQ_(PROMPT, "Bring up the prompt"), \
368 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
369 REQ_(SCREEN_RESIZE, "Resize the screen"), \
370 REQ_(SHOW_VERSION, "Show version information"), \
371 REQ_(STOP_LOADING, "Stop all loading views"), \
372 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
373 REQ_(TOGGLE_DATE, "Toggle date display"), \
374 REQ_(TOGGLE_AUTHOR, "Toggle author display"), \
375 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"), \
376 REQ_(TOGGLE_REFS, "Toggle reference display (tags/branches)"), \
377 REQ_(STATUS_UPDATE, "Update file status"), \
378 REQ_(STATUS_MERGE, "Merge file using external tool"), \
379 REQ_(STAGE_NEXT, "Find next chunk to stage"), \
380 REQ_(TREE_PARENT, "Switch to parent directory in tree view"), \
381 REQ_(EDIT, "Open in editor"), \
382 REQ_(NONE, "Do nothing")
385 /* User action requests. */
386 enum request {
387 #define REQ_GROUP(help)
388 #define REQ_(req, help) REQ_##req
390 /* Offset all requests to avoid conflicts with ncurses getch values. */
391 REQ_OFFSET = KEY_MAX + 1,
392 REQ_INFO
394 #undef REQ_GROUP
395 #undef REQ_
396 };
398 struct request_info {
399 enum request request;
400 char *name;
401 int namelen;
402 char *help;
403 };
405 static struct request_info req_info[] = {
406 #define REQ_GROUP(help) { 0, NULL, 0, (help) },
407 #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
408 REQ_INFO
409 #undef REQ_GROUP
410 #undef REQ_
411 };
413 static enum request
414 get_request(const char *name)
415 {
416 int namelen = strlen(name);
417 int i;
419 for (i = 0; i < ARRAY_SIZE(req_info); i++)
420 if (req_info[i].namelen == namelen &&
421 !string_enum_compare(req_info[i].name, name, namelen))
422 return req_info[i].request;
424 return REQ_NONE;
425 }
428 /*
429 * Options
430 */
432 static const char usage[] =
433 "tig " TIG_VERSION " (" __DATE__ ")\n"
434 "\n"
435 "Usage: tig [options] [revs] [--] [paths]\n"
436 " or: tig show [options] [revs] [--] [paths]\n"
437 " or: tig blame [rev] path\n"
438 " or: tig status\n"
439 " or: tig < [git command output]\n"
440 "\n"
441 "Options:\n"
442 " -v, --version Show version and exit\n"
443 " -h, --help Show help message and exit";
445 /* Option and state variables. */
446 static bool opt_date = TRUE;
447 static bool opt_author = TRUE;
448 static bool opt_line_number = FALSE;
449 static bool opt_line_graphics = TRUE;
450 static bool opt_rev_graph = FALSE;
451 static bool opt_show_refs = TRUE;
452 static int opt_num_interval = NUMBER_INTERVAL;
453 static int opt_tab_size = TAB_SIZE;
454 static enum request opt_request = REQ_VIEW_MAIN;
455 static char opt_cmd[SIZEOF_STR] = "";
456 static char opt_path[SIZEOF_STR] = "";
457 static char opt_file[SIZEOF_STR] = "";
458 static char opt_ref[SIZEOF_REF] = "";
459 static char opt_head[SIZEOF_REF] = "";
460 static char opt_remote[SIZEOF_REF] = "";
461 static bool opt_no_head = TRUE;
462 static FILE *opt_pipe = NULL;
463 static char opt_encoding[20] = "UTF-8";
464 static bool opt_utf8 = TRUE;
465 static char opt_codeset[20] = "UTF-8";
466 static iconv_t opt_iconv = ICONV_NONE;
467 static char opt_search[SIZEOF_STR] = "";
468 static char opt_cdup[SIZEOF_STR] = "";
469 static char opt_git_dir[SIZEOF_STR] = "";
470 static signed char opt_is_inside_work_tree = -1; /* set to TRUE or FALSE */
471 static char opt_editor[SIZEOF_STR] = "";
473 static bool
474 parse_options(int argc, char *argv[])
475 {
476 size_t buf_size;
477 char *subcommand;
478 bool seen_dashdash = FALSE;
479 int i;
481 if (!isatty(STDIN_FILENO)) {
482 opt_request = REQ_VIEW_PAGER;
483 opt_pipe = stdin;
484 return TRUE;
485 }
487 if (argc <= 1)
488 return TRUE;
490 subcommand = argv[1];
491 if (!strcmp(subcommand, "status") || !strcmp(subcommand, "-S")) {
492 opt_request = REQ_VIEW_STATUS;
493 if (!strcmp(subcommand, "-S"))
494 warn("`-S' has been deprecated; use `tig status' instead");
495 if (argc > 2)
496 warn("ignoring arguments after `%s'", subcommand);
497 return TRUE;
499 } else if (!strcmp(subcommand, "blame")) {
500 opt_request = REQ_VIEW_BLAME;
501 if (argc <= 2 || argc > 4)
502 die("invalid number of options to blame\n\n%s", usage);
504 i = 2;
505 if (argc == 4) {
506 string_ncopy(opt_ref, argv[i], strlen(argv[i]));
507 i++;
508 }
510 string_ncopy(opt_file, argv[i], strlen(argv[i]));
511 return TRUE;
513 } else if (!strcmp(subcommand, "show")) {
514 opt_request = REQ_VIEW_DIFF;
516 } else if (!strcmp(subcommand, "log") || !strcmp(subcommand, "diff")) {
517 opt_request = subcommand[0] == 'l'
518 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
519 warn("`tig %s' has been deprecated", subcommand);
521 } else {
522 subcommand = NULL;
523 }
525 if (!subcommand)
526 /* XXX: This is vulnerable to the user overriding
527 * options required for the main view parser. */
528 string_copy(opt_cmd, "git log --no-color --pretty=raw --boundary --parents");
529 else
530 string_format(opt_cmd, "git %s", subcommand);
532 buf_size = strlen(opt_cmd);
534 for (i = 1 + !!subcommand; i < argc; i++) {
535 char *opt = argv[i];
537 if (seen_dashdash || !strcmp(opt, "--")) {
538 seen_dashdash = TRUE;
540 } else if (!strcmp(opt, "-v") || !strcmp(opt, "--version")) {
541 printf("tig version %s\n", TIG_VERSION);
542 return FALSE;
544 } else if (!strcmp(opt, "-h") || !strcmp(opt, "--help")) {
545 printf("%s\n", usage);
546 return FALSE;
547 }
549 opt_cmd[buf_size++] = ' ';
550 buf_size = sq_quote(opt_cmd, buf_size, opt);
551 if (buf_size >= sizeof(opt_cmd))
552 die("command too long");
553 }
555 opt_cmd[buf_size] = 0;
557 return TRUE;
558 }
561 /*
562 * Line-oriented content detection.
563 */
565 #define LINE_INFO \
566 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
567 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
568 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
569 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
570 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
571 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
572 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
573 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
574 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
575 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
576 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
577 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
578 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
579 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
580 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
581 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
582 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
583 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
584 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
585 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
586 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
587 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
588 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
589 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
590 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
591 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
592 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
593 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
594 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
595 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
596 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
597 LINE(DELIMITER, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
598 LINE(DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
599 LINE(LINE_NUMBER, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
600 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
601 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
602 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
603 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
604 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
605 LINE(MAIN_LOCAL_TAG,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
606 LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \
607 LINE(MAIN_TRACKED, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
608 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
609 LINE(MAIN_HEAD, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
610 LINE(MAIN_REVGRAPH,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
611 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
612 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
613 LINE(STAT_HEAD, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \
614 LINE(STAT_SECTION, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
615 LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
616 LINE(STAT_STAGED, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
617 LINE(STAT_UNSTAGED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
618 LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
619 LINE(BLAME_ID, "", COLOR_MAGENTA, COLOR_DEFAULT, 0)
621 enum line_type {
622 #define LINE(type, line, fg, bg, attr) \
623 LINE_##type
624 LINE_INFO,
625 LINE_NONE
626 #undef LINE
627 };
629 struct line_info {
630 const char *name; /* Option name. */
631 int namelen; /* Size of option name. */
632 const char *line; /* The start of line to match. */
633 int linelen; /* Size of string to match. */
634 int fg, bg, attr; /* Color and text attributes for the lines. */
635 };
637 static struct line_info line_info[] = {
638 #define LINE(type, line, fg, bg, attr) \
639 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
640 LINE_INFO
641 #undef LINE
642 };
644 static enum line_type
645 get_line_type(char *line)
646 {
647 int linelen = strlen(line);
648 enum line_type type;
650 for (type = 0; type < ARRAY_SIZE(line_info); type++)
651 /* Case insensitive search matches Signed-off-by lines better. */
652 if (linelen >= line_info[type].linelen &&
653 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
654 return type;
656 return LINE_DEFAULT;
657 }
659 static inline int
660 get_line_attr(enum line_type type)
661 {
662 assert(type < ARRAY_SIZE(line_info));
663 return COLOR_PAIR(type) | line_info[type].attr;
664 }
666 static struct line_info *
667 get_line_info(char *name)
668 {
669 size_t namelen = strlen(name);
670 enum line_type type;
672 for (type = 0; type < ARRAY_SIZE(line_info); type++)
673 if (namelen == line_info[type].namelen &&
674 !string_enum_compare(line_info[type].name, name, namelen))
675 return &line_info[type];
677 return NULL;
678 }
680 static void
681 init_colors(void)
682 {
683 int default_bg = line_info[LINE_DEFAULT].bg;
684 int default_fg = line_info[LINE_DEFAULT].fg;
685 enum line_type type;
687 start_color();
689 if (assume_default_colors(default_fg, default_bg) == ERR) {
690 default_bg = COLOR_BLACK;
691 default_fg = COLOR_WHITE;
692 }
694 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
695 struct line_info *info = &line_info[type];
696 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
697 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
699 init_pair(type, fg, bg);
700 }
701 }
703 struct line {
704 enum line_type type;
706 /* State flags */
707 unsigned int selected:1;
708 unsigned int dirty:1;
710 void *data; /* User data */
711 };
714 /*
715 * Keys
716 */
718 struct keybinding {
719 int alias;
720 enum request request;
721 struct keybinding *next;
722 };
724 static struct keybinding default_keybindings[] = {
725 /* View switching */
726 { 'm', REQ_VIEW_MAIN },
727 { 'd', REQ_VIEW_DIFF },
728 { 'l', REQ_VIEW_LOG },
729 { 't', REQ_VIEW_TREE },
730 { 'f', REQ_VIEW_BLOB },
731 { 'B', REQ_VIEW_BLAME },
732 { 'p', REQ_VIEW_PAGER },
733 { 'h', REQ_VIEW_HELP },
734 { 'S', REQ_VIEW_STATUS },
735 { 'c', REQ_VIEW_STAGE },
737 /* View manipulation */
738 { 'q', REQ_VIEW_CLOSE },
739 { KEY_TAB, REQ_VIEW_NEXT },
740 { KEY_RETURN, REQ_ENTER },
741 { KEY_UP, REQ_PREVIOUS },
742 { KEY_DOWN, REQ_NEXT },
743 { 'R', REQ_REFRESH },
744 { KEY_F(5), REQ_REFRESH },
745 { 'O', REQ_MAXIMIZE },
747 /* Cursor navigation */
748 { 'k', REQ_MOVE_UP },
749 { 'j', REQ_MOVE_DOWN },
750 { KEY_HOME, REQ_MOVE_FIRST_LINE },
751 { KEY_END, REQ_MOVE_LAST_LINE },
752 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
753 { ' ', REQ_MOVE_PAGE_DOWN },
754 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
755 { 'b', REQ_MOVE_PAGE_UP },
756 { '-', REQ_MOVE_PAGE_UP },
758 /* Scrolling */
759 { KEY_IC, REQ_SCROLL_LINE_UP },
760 { KEY_DC, REQ_SCROLL_LINE_DOWN },
761 { 'w', REQ_SCROLL_PAGE_UP },
762 { 's', REQ_SCROLL_PAGE_DOWN },
764 /* Searching */
765 { '/', REQ_SEARCH },
766 { '?', REQ_SEARCH_BACK },
767 { 'n', REQ_FIND_NEXT },
768 { 'N', REQ_FIND_PREV },
770 /* Misc */
771 { 'Q', REQ_QUIT },
772 { 'z', REQ_STOP_LOADING },
773 { 'v', REQ_SHOW_VERSION },
774 { 'r', REQ_SCREEN_REDRAW },
775 { '.', REQ_TOGGLE_LINENO },
776 { 'D', REQ_TOGGLE_DATE },
777 { 'A', REQ_TOGGLE_AUTHOR },
778 { 'g', REQ_TOGGLE_REV_GRAPH },
779 { 'F', REQ_TOGGLE_REFS },
780 { ':', REQ_PROMPT },
781 { 'u', REQ_STATUS_UPDATE },
782 { 'M', REQ_STATUS_MERGE },
783 { '@', REQ_STAGE_NEXT },
784 { ',', REQ_TREE_PARENT },
785 { 'e', REQ_EDIT },
787 /* Using the ncurses SIGWINCH handler. */
788 { KEY_RESIZE, REQ_SCREEN_RESIZE },
789 };
791 #define KEYMAP_INFO \
792 KEYMAP_(GENERIC), \
793 KEYMAP_(MAIN), \
794 KEYMAP_(DIFF), \
795 KEYMAP_(LOG), \
796 KEYMAP_(TREE), \
797 KEYMAP_(BLOB), \
798 KEYMAP_(BLAME), \
799 KEYMAP_(PAGER), \
800 KEYMAP_(HELP), \
801 KEYMAP_(STATUS), \
802 KEYMAP_(STAGE)
804 enum keymap {
805 #define KEYMAP_(name) KEYMAP_##name
806 KEYMAP_INFO
807 #undef KEYMAP_
808 };
810 static struct int_map keymap_table[] = {
811 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
812 KEYMAP_INFO
813 #undef KEYMAP_
814 };
816 #define set_keymap(map, name) \
817 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
819 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
821 static void
822 add_keybinding(enum keymap keymap, enum request request, int key)
823 {
824 struct keybinding *keybinding;
826 keybinding = calloc(1, sizeof(*keybinding));
827 if (!keybinding)
828 die("Failed to allocate keybinding");
830 keybinding->alias = key;
831 keybinding->request = request;
832 keybinding->next = keybindings[keymap];
833 keybindings[keymap] = keybinding;
834 }
836 /* Looks for a key binding first in the given map, then in the generic map, and
837 * lastly in the default keybindings. */
838 static enum request
839 get_keybinding(enum keymap keymap, int key)
840 {
841 struct keybinding *kbd;
842 int i;
844 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
845 if (kbd->alias == key)
846 return kbd->request;
848 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
849 if (kbd->alias == key)
850 return kbd->request;
852 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
853 if (default_keybindings[i].alias == key)
854 return default_keybindings[i].request;
856 return (enum request) key;
857 }
860 struct key {
861 char *name;
862 int value;
863 };
865 static struct key key_table[] = {
866 { "Enter", KEY_RETURN },
867 { "Space", ' ' },
868 { "Backspace", KEY_BACKSPACE },
869 { "Tab", KEY_TAB },
870 { "Escape", KEY_ESC },
871 { "Left", KEY_LEFT },
872 { "Right", KEY_RIGHT },
873 { "Up", KEY_UP },
874 { "Down", KEY_DOWN },
875 { "Insert", KEY_IC },
876 { "Delete", KEY_DC },
877 { "Hash", '#' },
878 { "Home", KEY_HOME },
879 { "End", KEY_END },
880 { "PageUp", KEY_PPAGE },
881 { "PageDown", KEY_NPAGE },
882 { "F1", KEY_F(1) },
883 { "F2", KEY_F(2) },
884 { "F3", KEY_F(3) },
885 { "F4", KEY_F(4) },
886 { "F5", KEY_F(5) },
887 { "F6", KEY_F(6) },
888 { "F7", KEY_F(7) },
889 { "F8", KEY_F(8) },
890 { "F9", KEY_F(9) },
891 { "F10", KEY_F(10) },
892 { "F11", KEY_F(11) },
893 { "F12", KEY_F(12) },
894 };
896 static int
897 get_key_value(const char *name)
898 {
899 int i;
901 for (i = 0; i < ARRAY_SIZE(key_table); i++)
902 if (!strcasecmp(key_table[i].name, name))
903 return key_table[i].value;
905 if (strlen(name) == 1 && isprint(*name))
906 return (int) *name;
908 return ERR;
909 }
911 static char *
912 get_key_name(int key_value)
913 {
914 static char key_char[] = "'X'";
915 char *seq = NULL;
916 int key;
918 for (key = 0; key < ARRAY_SIZE(key_table); key++)
919 if (key_table[key].value == key_value)
920 seq = key_table[key].name;
922 if (seq == NULL &&
923 key_value < 127 &&
924 isprint(key_value)) {
925 key_char[1] = (char) key_value;
926 seq = key_char;
927 }
929 return seq ? seq : "'?'";
930 }
932 static char *
933 get_key(enum request request)
934 {
935 static char buf[BUFSIZ];
936 size_t pos = 0;
937 char *sep = "";
938 int i;
940 buf[pos] = 0;
942 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
943 struct keybinding *keybinding = &default_keybindings[i];
945 if (keybinding->request != request)
946 continue;
948 if (!string_format_from(buf, &pos, "%s%s", sep,
949 get_key_name(keybinding->alias)))
950 return "Too many keybindings!";
951 sep = ", ";
952 }
954 return buf;
955 }
957 struct run_request {
958 enum keymap keymap;
959 int key;
960 char cmd[SIZEOF_STR];
961 };
963 static struct run_request *run_request;
964 static size_t run_requests;
966 static enum request
967 add_run_request(enum keymap keymap, int key, int argc, char **argv)
968 {
969 struct run_request *req;
970 char cmd[SIZEOF_STR];
971 size_t bufpos;
973 for (bufpos = 0; argc > 0; argc--, argv++)
974 if (!string_format_from(cmd, &bufpos, "%s ", *argv))
975 return REQ_NONE;
977 req = realloc(run_request, (run_requests + 1) * sizeof(*run_request));
978 if (!req)
979 return REQ_NONE;
981 run_request = req;
982 req = &run_request[run_requests++];
983 string_copy(req->cmd, cmd);
984 req->keymap = keymap;
985 req->key = key;
987 return REQ_NONE + run_requests;
988 }
990 static struct run_request *
991 get_run_request(enum request request)
992 {
993 if (request <= REQ_NONE)
994 return NULL;
995 return &run_request[request - REQ_NONE - 1];
996 }
998 static void
999 add_builtin_run_requests(void)
1000 {
1001 struct {
1002 enum keymap keymap;
1003 int key;
1004 char *argv[1];
1005 } reqs[] = {
1006 { KEYMAP_MAIN, 'C', { "git cherry-pick %(commit)" } },
1007 { KEYMAP_GENERIC, 'G', { "git gc" } },
1008 };
1009 int i;
1011 for (i = 0; i < ARRAY_SIZE(reqs); i++) {
1012 enum request req;
1014 req = add_run_request(reqs[i].keymap, reqs[i].key, 1, reqs[i].argv);
1015 if (req != REQ_NONE)
1016 add_keybinding(reqs[i].keymap, req, reqs[i].key);
1017 }
1018 }
1020 /*
1021 * User config file handling.
1022 */
1024 static struct int_map color_map[] = {
1025 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
1026 COLOR_MAP(DEFAULT),
1027 COLOR_MAP(BLACK),
1028 COLOR_MAP(BLUE),
1029 COLOR_MAP(CYAN),
1030 COLOR_MAP(GREEN),
1031 COLOR_MAP(MAGENTA),
1032 COLOR_MAP(RED),
1033 COLOR_MAP(WHITE),
1034 COLOR_MAP(YELLOW),
1035 };
1037 #define set_color(color, name) \
1038 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
1040 static struct int_map attr_map[] = {
1041 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
1042 ATTR_MAP(NORMAL),
1043 ATTR_MAP(BLINK),
1044 ATTR_MAP(BOLD),
1045 ATTR_MAP(DIM),
1046 ATTR_MAP(REVERSE),
1047 ATTR_MAP(STANDOUT),
1048 ATTR_MAP(UNDERLINE),
1049 };
1051 #define set_attribute(attr, name) \
1052 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
1054 static int config_lineno;
1055 static bool config_errors;
1056 static char *config_msg;
1058 /* Wants: object fgcolor bgcolor [attr] */
1059 static int
1060 option_color_command(int argc, char *argv[])
1061 {
1062 struct line_info *info;
1064 if (argc != 3 && argc != 4) {
1065 config_msg = "Wrong number of arguments given to color command";
1066 return ERR;
1067 }
1069 info = get_line_info(argv[0]);
1070 if (!info) {
1071 if (!string_enum_compare(argv[0], "main-delim", strlen("main-delim"))) {
1072 info = get_line_info("delimiter");
1074 } else if (!string_enum_compare(argv[0], "main-date", strlen("main-date"))) {
1075 info = get_line_info("date");
1077 } else {
1078 config_msg = "Unknown color name";
1079 return ERR;
1080 }
1081 }
1083 if (set_color(&info->fg, argv[1]) == ERR ||
1084 set_color(&info->bg, argv[2]) == ERR) {
1085 config_msg = "Unknown color";
1086 return ERR;
1087 }
1089 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
1090 config_msg = "Unknown attribute";
1091 return ERR;
1092 }
1094 return OK;
1095 }
1097 static bool parse_bool(const char *s)
1098 {
1099 return (!strcmp(s, "1") || !strcmp(s, "true") ||
1100 !strcmp(s, "yes")) ? TRUE : FALSE;
1101 }
1103 /* Wants: name = value */
1104 static int
1105 option_set_command(int argc, char *argv[])
1106 {
1107 if (argc != 3) {
1108 config_msg = "Wrong number of arguments given to set command";
1109 return ERR;
1110 }
1112 if (strcmp(argv[1], "=")) {
1113 config_msg = "No value assigned";
1114 return ERR;
1115 }
1117 if (!strcmp(argv[0], "show-author")) {
1118 opt_author = parse_bool(argv[2]);
1119 return OK;
1120 }
1122 if (!strcmp(argv[0], "show-date")) {
1123 opt_date = parse_bool(argv[2]);
1124 return OK;
1125 }
1127 if (!strcmp(argv[0], "show-rev-graph")) {
1128 opt_rev_graph = parse_bool(argv[2]);
1129 return OK;
1130 }
1132 if (!strcmp(argv[0], "show-refs")) {
1133 opt_show_refs = parse_bool(argv[2]);
1134 return OK;
1135 }
1137 if (!strcmp(argv[0], "show-line-numbers")) {
1138 opt_line_number = parse_bool(argv[2]);
1139 return OK;
1140 }
1142 if (!strcmp(argv[0], "line-graphics")) {
1143 opt_line_graphics = parse_bool(argv[2]);
1144 return OK;
1145 }
1147 if (!strcmp(argv[0], "line-number-interval")) {
1148 opt_num_interval = atoi(argv[2]);
1149 return OK;
1150 }
1152 if (!strcmp(argv[0], "tab-size")) {
1153 opt_tab_size = atoi(argv[2]);
1154 return OK;
1155 }
1157 if (!strcmp(argv[0], "commit-encoding")) {
1158 char *arg = argv[2];
1159 int delimiter = *arg;
1160 int i;
1162 switch (delimiter) {
1163 case '"':
1164 case '\'':
1165 for (arg++, i = 0; arg[i]; i++)
1166 if (arg[i] == delimiter) {
1167 arg[i] = 0;
1168 break;
1169 }
1170 default:
1171 string_ncopy(opt_encoding, arg, strlen(arg));
1172 return OK;
1173 }
1174 }
1176 config_msg = "Unknown variable name";
1177 return ERR;
1178 }
1180 /* Wants: mode request key */
1181 static int
1182 option_bind_command(int argc, char *argv[])
1183 {
1184 enum request request;
1185 int keymap;
1186 int key;
1188 if (argc < 3) {
1189 config_msg = "Wrong number of arguments given to bind command";
1190 return ERR;
1191 }
1193 if (set_keymap(&keymap, argv[0]) == ERR) {
1194 config_msg = "Unknown key map";
1195 return ERR;
1196 }
1198 key = get_key_value(argv[1]);
1199 if (key == ERR) {
1200 config_msg = "Unknown key";
1201 return ERR;
1202 }
1204 request = get_request(argv[2]);
1205 if (request == REQ_NONE) {
1206 const char *obsolete[] = { "cherry-pick" };
1207 size_t namelen = strlen(argv[2]);
1208 int i;
1210 for (i = 0; i < ARRAY_SIZE(obsolete); i++) {
1211 if (namelen == strlen(obsolete[i]) &&
1212 !string_enum_compare(obsolete[i], argv[2], namelen)) {
1213 config_msg = "Obsolete request name";
1214 return ERR;
1215 }
1216 }
1217 }
1218 if (request == REQ_NONE && *argv[2]++ == '!')
1219 request = add_run_request(keymap, key, argc - 2, argv + 2);
1220 if (request == REQ_NONE) {
1221 config_msg = "Unknown request name";
1222 return ERR;
1223 }
1225 add_keybinding(keymap, request, key);
1227 return OK;
1228 }
1230 static int
1231 set_option(char *opt, char *value)
1232 {
1233 char *argv[16];
1234 int valuelen;
1235 int argc = 0;
1237 /* Tokenize */
1238 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1239 argv[argc++] = value;
1240 value += valuelen;
1242 /* Nothing more to tokenize or last available token. */
1243 if (!*value || argc >= ARRAY_SIZE(argv))
1244 break;
1246 *value++ = 0;
1247 while (isspace(*value))
1248 value++;
1249 }
1251 if (!strcmp(opt, "color"))
1252 return option_color_command(argc, argv);
1254 if (!strcmp(opt, "set"))
1255 return option_set_command(argc, argv);
1257 if (!strcmp(opt, "bind"))
1258 return option_bind_command(argc, argv);
1260 config_msg = "Unknown option command";
1261 return ERR;
1262 }
1264 static int
1265 read_option(char *opt, size_t optlen, char *value, size_t valuelen)
1266 {
1267 int status = OK;
1269 config_lineno++;
1270 config_msg = "Internal error";
1272 /* Check for comment markers, since read_properties() will
1273 * only ensure opt and value are split at first " \t". */
1274 optlen = strcspn(opt, "#");
1275 if (optlen == 0)
1276 return OK;
1278 if (opt[optlen] != 0) {
1279 config_msg = "No option value";
1280 status = ERR;
1282 } else {
1283 /* Look for comment endings in the value. */
1284 size_t len = strcspn(value, "#");
1286 if (len < valuelen) {
1287 valuelen = len;
1288 value[valuelen] = 0;
1289 }
1291 status = set_option(opt, value);
1292 }
1294 if (status == ERR) {
1295 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1296 config_lineno, (int) optlen, opt, config_msg);
1297 config_errors = TRUE;
1298 }
1300 /* Always keep going if errors are encountered. */
1301 return OK;
1302 }
1304 static void
1305 load_option_file(const char *path)
1306 {
1307 FILE *file;
1309 /* It's ok that the file doesn't exist. */
1310 file = fopen(path, "r");
1311 if (!file)
1312 return;
1314 config_lineno = 0;
1315 config_errors = FALSE;
1317 if (read_properties(file, " \t", read_option) == ERR ||
1318 config_errors == TRUE)
1319 fprintf(stderr, "Errors while loading %s.\n", path);
1320 }
1322 static int
1323 load_options(void)
1324 {
1325 char *home = getenv("HOME");
1326 char *tigrc_user = getenv("TIGRC_USER");
1327 char *tigrc_system = getenv("TIGRC_SYSTEM");
1328 char buf[SIZEOF_STR];
1330 add_builtin_run_requests();
1332 if (!tigrc_system) {
1333 if (!string_format(buf, "%s/tigrc", SYSCONFDIR))
1334 return ERR;
1335 tigrc_system = buf;
1336 }
1337 load_option_file(tigrc_system);
1339 if (!tigrc_user) {
1340 if (!home || !string_format(buf, "%s/.tigrc", home))
1341 return ERR;
1342 tigrc_user = buf;
1343 }
1344 load_option_file(tigrc_user);
1346 return OK;
1347 }
1350 /*
1351 * The viewer
1352 */
1354 struct view;
1355 struct view_ops;
1357 /* The display array of active views and the index of the current view. */
1358 static struct view *display[2];
1359 static unsigned int current_view;
1361 /* Reading from the prompt? */
1362 static bool input_mode = FALSE;
1364 #define foreach_displayed_view(view, i) \
1365 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1367 #define displayed_views() (display[1] != NULL ? 2 : 1)
1369 /* Current head and commit ID */
1370 static char ref_blob[SIZEOF_REF] = "";
1371 static char ref_commit[SIZEOF_REF] = "HEAD";
1372 static char ref_head[SIZEOF_REF] = "HEAD";
1374 struct view {
1375 const char *name; /* View name */
1376 const char *cmd_fmt; /* Default command line format */
1377 const char *cmd_env; /* Command line set via environment */
1378 const char *id; /* Points to either of ref_{head,commit,blob} */
1380 struct view_ops *ops; /* View operations */
1382 enum keymap keymap; /* What keymap does this view have */
1383 bool git_dir; /* Whether the view requires a git directory. */
1385 char cmd[SIZEOF_STR]; /* Command buffer */
1386 char ref[SIZEOF_REF]; /* Hovered commit reference */
1387 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1389 int height, width; /* The width and height of the main window */
1390 WINDOW *win; /* The main window */
1391 WINDOW *title; /* The title window living below the main window */
1393 /* Navigation */
1394 unsigned long offset; /* Offset of the window top */
1395 unsigned long lineno; /* Current line number */
1397 /* Searching */
1398 char grep[SIZEOF_STR]; /* Search string */
1399 regex_t *regex; /* Pre-compiled regex */
1401 /* If non-NULL, points to the view that opened this view. If this view
1402 * is closed tig will switch back to the parent view. */
1403 struct view *parent;
1405 /* Buffering */
1406 size_t lines; /* Total number of lines */
1407 struct line *line; /* Line index */
1408 size_t line_alloc; /* Total number of allocated lines */
1409 size_t line_size; /* Total number of used lines */
1410 unsigned int digits; /* Number of digits in the lines member. */
1412 /* Drawing */
1413 struct line *curline; /* Line currently being drawn. */
1414 enum line_type curtype; /* Attribute currently used for drawing. */
1415 unsigned long col; /* Column when drawing. */
1417 /* Loading */
1418 FILE *pipe;
1419 time_t start_time;
1420 };
1422 struct view_ops {
1423 /* What type of content being displayed. Used in the title bar. */
1424 const char *type;
1425 /* Open and reads in all view content. */
1426 bool (*open)(struct view *view);
1427 /* Read one line; updates view->line. */
1428 bool (*read)(struct view *view, char *data);
1429 /* Draw one line; @lineno must be < view->height. */
1430 bool (*draw)(struct view *view, struct line *line, unsigned int lineno);
1431 /* Depending on view handle a special requests. */
1432 enum request (*request)(struct view *view, enum request request, struct line *line);
1433 /* Search for regex in a line. */
1434 bool (*grep)(struct view *view, struct line *line);
1435 /* Select line */
1436 void (*select)(struct view *view, struct line *line);
1437 };
1439 static struct view_ops pager_ops;
1440 static struct view_ops main_ops;
1441 static struct view_ops tree_ops;
1442 static struct view_ops blob_ops;
1443 static struct view_ops blame_ops;
1444 static struct view_ops help_ops;
1445 static struct view_ops status_ops;
1446 static struct view_ops stage_ops;
1448 #define VIEW_STR(name, cmd, env, ref, ops, map, git) \
1449 { name, cmd, #env, ref, ops, map, git }
1451 #define VIEW_(id, name, ops, git, ref) \
1452 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id, git)
1455 static struct view views[] = {
1456 VIEW_(MAIN, "main", &main_ops, TRUE, ref_head),
1457 VIEW_(DIFF, "diff", &pager_ops, TRUE, ref_commit),
1458 VIEW_(LOG, "log", &pager_ops, TRUE, ref_head),
1459 VIEW_(TREE, "tree", &tree_ops, TRUE, ref_commit),
1460 VIEW_(BLOB, "blob", &blob_ops, TRUE, ref_blob),
1461 VIEW_(BLAME, "blame", &blame_ops, TRUE, ref_commit),
1462 VIEW_(HELP, "help", &help_ops, FALSE, ""),
1463 VIEW_(PAGER, "pager", &pager_ops, FALSE, "stdin"),
1464 VIEW_(STATUS, "status", &status_ops, TRUE, ""),
1465 VIEW_(STAGE, "stage", &stage_ops, TRUE, ""),
1466 };
1468 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1469 #define VIEW_REQ(view) ((view) - views + REQ_OFFSET + 1)
1471 #define foreach_view(view, i) \
1472 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1474 #define view_is_displayed(view) \
1475 (view == display[0] || view == display[1])
1478 enum line_graphic {
1479 LINE_GRAPHIC_VLINE
1480 };
1482 static int line_graphics[] = {
1483 /* LINE_GRAPHIC_VLINE: */ '|'
1484 };
1486 static inline void
1487 set_view_attr(struct view *view, enum line_type type)
1488 {
1489 if (!view->curline->selected && view->curtype != type) {
1490 wattrset(view->win, get_line_attr(type));
1491 wchgat(view->win, -1, 0, type, NULL);
1492 view->curtype = type;
1493 }
1494 }
1496 static int
1497 draw_chars(struct view *view, enum line_type type, const char *string,
1498 int max_len, bool use_tilde)
1499 {
1500 int len = 0;
1501 int col = 0;
1502 int trimmed = FALSE;
1504 if (max_len <= 0)
1505 return 0;
1507 if (opt_utf8) {
1508 len = utf8_length(string, &col, max_len, &trimmed, use_tilde);
1509 } else {
1510 col = len = strlen(string);
1511 if (len > max_len) {
1512 if (use_tilde) {
1513 max_len -= 1;
1514 }
1515 col = len = max_len;
1516 trimmed = TRUE;
1517 }
1518 }
1520 set_view_attr(view, type);
1521 waddnstr(view->win, string, len);
1522 if (trimmed && use_tilde) {
1523 set_view_attr(view, LINE_DELIMITER);
1524 waddch(view->win, '~');
1525 col++;
1526 }
1528 return col;
1529 }
1531 static int
1532 draw_space(struct view *view, enum line_type type, int max, int spaces)
1533 {
1534 static char space[] = " ";
1535 int col = 0;
1537 spaces = MIN(max, spaces);
1539 while (spaces > 0) {
1540 int len = MIN(spaces, sizeof(space) - 1);
1542 col += draw_chars(view, type, space, spaces, FALSE);
1543 spaces -= len;
1544 }
1546 return col;
1547 }
1549 static bool
1550 draw_lineno(struct view *view, unsigned int lineno)
1551 {
1552 char number[10];
1553 int digits3 = view->digits < 3 ? 3 : view->digits;
1554 int max_number = MIN(digits3, STRING_SIZE(number));
1555 int max = view->width - view->col;
1556 int col;
1558 if (max < max_number)
1559 max_number = max;
1561 lineno += view->offset + 1;
1562 if (lineno == 1 || (lineno % opt_num_interval) == 0) {
1563 static char fmt[] = "%1ld";
1565 if (view->digits <= 9)
1566 fmt[1] = '0' + digits3;
1568 if (!string_format(number, fmt, lineno))
1569 number[0] = 0;
1570 col = draw_chars(view, LINE_LINE_NUMBER, number, max_number, TRUE);
1571 } else {
1572 col = draw_space(view, LINE_LINE_NUMBER, max_number, max_number);
1573 }
1575 if (col < max) {
1576 set_view_attr(view, LINE_DEFAULT);
1577 waddch(view->win, line_graphics[LINE_GRAPHIC_VLINE]);
1578 col++;
1579 }
1581 if (col < max)
1582 col += draw_space(view, LINE_DEFAULT, max - col, 1);
1583 view->col += col;
1585 return view->width - view->col <= 0;
1586 }
1588 static bool
1589 draw_text(struct view *view, enum line_type type, const char *string, bool trim)
1590 {
1591 view->col += draw_chars(view, type, string, view->width - view->col, trim);
1592 return view->width - view->col <= 0;
1593 }
1595 static bool
1596 draw_graphic(struct view *view, enum line_type type, chtype graphic[], size_t size)
1597 {
1598 int max = view->width - view->col;
1599 int i;
1601 if (max < size)
1602 size = max;
1604 set_view_attr(view, type);
1605 /* Using waddch() instead of waddnstr() ensures that
1606 * they'll be rendered correctly for the cursor line. */
1607 for (i = 0; i < size; i++)
1608 waddch(view->win, graphic[i]);
1610 view->col += size;
1611 if (size < max) {
1612 waddch(view->win, ' ');
1613 view->col++;
1614 }
1616 return view->width - view->col <= 0;
1617 }
1619 static bool
1620 draw_field(struct view *view, enum line_type type, char *text, int len, bool trim)
1621 {
1622 int max = MIN(view->width - view->col, len);
1623 int col;
1625 if (text)
1626 col = draw_chars(view, type, text, max - 1, trim);
1627 else
1628 col = draw_space(view, type, max - 1, max - 1);
1630 view->col += col + draw_space(view, LINE_DEFAULT, max - col, max - col);
1631 return view->width - view->col <= 0;
1632 }
1634 static bool
1635 draw_date(struct view *view, struct tm *time)
1636 {
1637 char buf[DATE_COLS];
1638 char *date;
1639 int timelen = 0;
1641 if (time)
1642 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, time);
1643 date = timelen ? buf : NULL;
1645 return draw_field(view, LINE_DATE, date, DATE_COLS, FALSE);
1646 }
1648 static bool
1649 draw_view_line(struct view *view, unsigned int lineno)
1650 {
1651 struct line *line;
1652 bool selected = (view->offset + lineno == view->lineno);
1653 bool draw_ok;
1655 assert(view_is_displayed(view));
1657 if (view->offset + lineno >= view->lines)
1658 return FALSE;
1660 line = &view->line[view->offset + lineno];
1662 wmove(view->win, lineno, 0);
1663 view->col = 0;
1664 view->curline = line;
1665 view->curtype = LINE_NONE;
1666 line->selected = FALSE;
1668 if (selected) {
1669 set_view_attr(view, LINE_CURSOR);
1670 line->selected = TRUE;
1671 view->ops->select(view, line);
1672 } else if (line->selected) {
1673 wclrtoeol(view->win);
1674 }
1676 scrollok(view->win, FALSE);
1677 draw_ok = view->ops->draw(view, line, lineno);
1678 scrollok(view->win, TRUE);
1680 return draw_ok;
1681 }
1683 static void
1684 redraw_view_dirty(struct view *view)
1685 {
1686 bool dirty = FALSE;
1687 int lineno;
1689 for (lineno = 0; lineno < view->height; lineno++) {
1690 struct line *line = &view->line[view->offset + lineno];
1692 if (!line->dirty)
1693 continue;
1694 line->dirty = 0;
1695 dirty = TRUE;
1696 if (!draw_view_line(view, lineno))
1697 break;
1698 }
1700 if (!dirty)
1701 return;
1702 redrawwin(view->win);
1703 if (input_mode)
1704 wnoutrefresh(view->win);
1705 else
1706 wrefresh(view->win);
1707 }
1709 static void
1710 redraw_view_from(struct view *view, int lineno)
1711 {
1712 assert(0 <= lineno && lineno < view->height);
1714 for (; lineno < view->height; lineno++) {
1715 if (!draw_view_line(view, lineno))
1716 break;
1717 }
1719 redrawwin(view->win);
1720 if (input_mode)
1721 wnoutrefresh(view->win);
1722 else
1723 wrefresh(view->win);
1724 }
1726 static void
1727 redraw_view(struct view *view)
1728 {
1729 wclear(view->win);
1730 redraw_view_from(view, 0);
1731 }
1734 static void
1735 update_view_title(struct view *view)
1736 {
1737 char buf[SIZEOF_STR];
1738 char state[SIZEOF_STR];
1739 size_t bufpos = 0, statelen = 0;
1741 assert(view_is_displayed(view));
1743 if (view != VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) {
1744 unsigned int view_lines = view->offset + view->height;
1745 unsigned int lines = view->lines
1746 ? MIN(view_lines, view->lines) * 100 / view->lines
1747 : 0;
1749 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1750 view->ops->type,
1751 view->lineno + 1,
1752 view->lines,
1753 lines);
1755 if (view->pipe) {
1756 time_t secs = time(NULL) - view->start_time;
1758 /* Three git seconds are a long time ... */
1759 if (secs > 2)
1760 string_format_from(state, &statelen, " %lds", secs);
1761 }
1762 }
1764 string_format_from(buf, &bufpos, "[%s]", view->name);
1765 if (*view->ref && bufpos < view->width) {
1766 size_t refsize = strlen(view->ref);
1767 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1769 if (minsize < view->width)
1770 refsize = view->width - minsize + 7;
1771 string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
1772 }
1774 if (statelen && bufpos < view->width) {
1775 string_format_from(buf, &bufpos, " %s", state);
1776 }
1778 if (view == display[current_view])
1779 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1780 else
1781 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1783 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1784 wclrtoeol(view->title);
1785 wmove(view->title, 0, view->width - 1);
1787 if (input_mode)
1788 wnoutrefresh(view->title);
1789 else
1790 wrefresh(view->title);
1791 }
1793 static void
1794 resize_display(void)
1795 {
1796 int offset, i;
1797 struct view *base = display[0];
1798 struct view *view = display[1] ? display[1] : display[0];
1800 /* Setup window dimensions */
1802 getmaxyx(stdscr, base->height, base->width);
1804 /* Make room for the status window. */
1805 base->height -= 1;
1807 if (view != base) {
1808 /* Horizontal split. */
1809 view->width = base->width;
1810 view->height = SCALE_SPLIT_VIEW(base->height);
1811 base->height -= view->height;
1813 /* Make room for the title bar. */
1814 view->height -= 1;
1815 }
1817 /* Make room for the title bar. */
1818 base->height -= 1;
1820 offset = 0;
1822 foreach_displayed_view (view, i) {
1823 if (!view->win) {
1824 view->win = newwin(view->height, 0, offset, 0);
1825 if (!view->win)
1826 die("Failed to create %s view", view->name);
1828 scrollok(view->win, TRUE);
1830 view->title = newwin(1, 0, offset + view->height, 0);
1831 if (!view->title)
1832 die("Failed to create title window");
1834 } else {
1835 wresize(view->win, view->height, view->width);
1836 mvwin(view->win, offset, 0);
1837 mvwin(view->title, offset + view->height, 0);
1838 }
1840 offset += view->height + 1;
1841 }
1842 }
1844 static void
1845 redraw_display(void)
1846 {
1847 struct view *view;
1848 int i;
1850 foreach_displayed_view (view, i) {
1851 redraw_view(view);
1852 update_view_title(view);
1853 }
1854 }
1856 static void
1857 update_display_cursor(struct view *view)
1858 {
1859 /* Move the cursor to the right-most column of the cursor line.
1860 *
1861 * XXX: This could turn out to be a bit expensive, but it ensures that
1862 * the cursor does not jump around. */
1863 if (view->lines) {
1864 wmove(view->win, view->lineno - view->offset, view->width - 1);
1865 wrefresh(view->win);
1866 }
1867 }
1869 /*
1870 * Navigation
1871 */
1873 /* Scrolling backend */
1874 static void
1875 do_scroll_view(struct view *view, int lines)
1876 {
1877 bool redraw_current_line = FALSE;
1879 /* The rendering expects the new offset. */
1880 view->offset += lines;
1882 assert(0 <= view->offset && view->offset < view->lines);
1883 assert(lines);
1885 /* Move current line into the view. */
1886 if (view->lineno < view->offset) {
1887 view->lineno = view->offset;
1888 redraw_current_line = TRUE;
1889 } else if (view->lineno >= view->offset + view->height) {
1890 view->lineno = view->offset + view->height - 1;
1891 redraw_current_line = TRUE;
1892 }
1894 assert(view->offset <= view->lineno && view->lineno < view->lines);
1896 /* Redraw the whole screen if scrolling is pointless. */
1897 if (view->height < ABS(lines)) {
1898 redraw_view(view);
1900 } else {
1901 int line = lines > 0 ? view->height - lines : 0;
1902 int end = line + ABS(lines);
1904 wscrl(view->win, lines);
1906 for (; line < end; line++) {
1907 if (!draw_view_line(view, line))
1908 break;
1909 }
1911 if (redraw_current_line)
1912 draw_view_line(view, view->lineno - view->offset);
1913 }
1915 redrawwin(view->win);
1916 wrefresh(view->win);
1917 report("");
1918 }
1920 /* Scroll frontend */
1921 static void
1922 scroll_view(struct view *view, enum request request)
1923 {
1924 int lines = 1;
1926 assert(view_is_displayed(view));
1928 switch (request) {
1929 case REQ_SCROLL_PAGE_DOWN:
1930 lines = view->height;
1931 case REQ_SCROLL_LINE_DOWN:
1932 if (view->offset + lines > view->lines)
1933 lines = view->lines - view->offset;
1935 if (lines == 0 || view->offset + view->height >= view->lines) {
1936 report("Cannot scroll beyond the last line");
1937 return;
1938 }
1939 break;
1941 case REQ_SCROLL_PAGE_UP:
1942 lines = view->height;
1943 case REQ_SCROLL_LINE_UP:
1944 if (lines > view->offset)
1945 lines = view->offset;
1947 if (lines == 0) {
1948 report("Cannot scroll beyond the first line");
1949 return;
1950 }
1952 lines = -lines;
1953 break;
1955 default:
1956 die("request %d not handled in switch", request);
1957 }
1959 do_scroll_view(view, lines);
1960 }
1962 /* Cursor moving */
1963 static void
1964 move_view(struct view *view, enum request request)
1965 {
1966 int scroll_steps = 0;
1967 int steps;
1969 switch (request) {
1970 case REQ_MOVE_FIRST_LINE:
1971 steps = -view->lineno;
1972 break;
1974 case REQ_MOVE_LAST_LINE:
1975 steps = view->lines - view->lineno - 1;
1976 break;
1978 case REQ_MOVE_PAGE_UP:
1979 steps = view->height > view->lineno
1980 ? -view->lineno : -view->height;
1981 break;
1983 case REQ_MOVE_PAGE_DOWN:
1984 steps = view->lineno + view->height >= view->lines
1985 ? view->lines - view->lineno - 1 : view->height;
1986 break;
1988 case REQ_MOVE_UP:
1989 steps = -1;
1990 break;
1992 case REQ_MOVE_DOWN:
1993 steps = 1;
1994 break;
1996 default:
1997 die("request %d not handled in switch", request);
1998 }
2000 if (steps <= 0 && view->lineno == 0) {
2001 report("Cannot move beyond the first line");
2002 return;
2004 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
2005 report("Cannot move beyond the last line");
2006 return;
2007 }
2009 /* Move the current line */
2010 view->lineno += steps;
2011 assert(0 <= view->lineno && view->lineno < view->lines);
2013 /* Check whether the view needs to be scrolled */
2014 if (view->lineno < view->offset ||
2015 view->lineno >= view->offset + view->height) {
2016 scroll_steps = steps;
2017 if (steps < 0 && -steps > view->offset) {
2018 scroll_steps = -view->offset;
2020 } else if (steps > 0) {
2021 if (view->lineno == view->lines - 1 &&
2022 view->lines > view->height) {
2023 scroll_steps = view->lines - view->offset - 1;
2024 if (scroll_steps >= view->height)
2025 scroll_steps -= view->height - 1;
2026 }
2027 }
2028 }
2030 if (!view_is_displayed(view)) {
2031 view->offset += scroll_steps;
2032 assert(0 <= view->offset && view->offset < view->lines);
2033 view->ops->select(view, &view->line[view->lineno]);
2034 return;
2035 }
2037 /* Repaint the old "current" line if we be scrolling */
2038 if (ABS(steps) < view->height)
2039 draw_view_line(view, view->lineno - steps - view->offset);
2041 if (scroll_steps) {
2042 do_scroll_view(view, scroll_steps);
2043 return;
2044 }
2046 /* Draw the current line */
2047 draw_view_line(view, view->lineno - view->offset);
2049 redrawwin(view->win);
2050 wrefresh(view->win);
2051 report("");
2052 }
2055 /*
2056 * Searching
2057 */
2059 static void search_view(struct view *view, enum request request);
2061 static bool
2062 find_next_line(struct view *view, unsigned long lineno, struct line *line)
2063 {
2064 assert(view_is_displayed(view));
2066 if (!view->ops->grep(view, line))
2067 return FALSE;
2069 if (lineno - view->offset >= view->height) {
2070 view->offset = lineno;
2071 view->lineno = lineno;
2072 redraw_view(view);
2074 } else {
2075 unsigned long old_lineno = view->lineno - view->offset;
2077 view->lineno = lineno;
2078 draw_view_line(view, old_lineno);
2080 draw_view_line(view, view->lineno - view->offset);
2081 redrawwin(view->win);
2082 wrefresh(view->win);
2083 }
2085 report("Line %ld matches '%s'", lineno + 1, view->grep);
2086 return TRUE;
2087 }
2089 static void
2090 find_next(struct view *view, enum request request)
2091 {
2092 unsigned long lineno = view->lineno;
2093 int direction;
2095 if (!*view->grep) {
2096 if (!*opt_search)
2097 report("No previous search");
2098 else
2099 search_view(view, request);
2100 return;
2101 }
2103 switch (request) {
2104 case REQ_SEARCH:
2105 case REQ_FIND_NEXT:
2106 direction = 1;
2107 break;
2109 case REQ_SEARCH_BACK:
2110 case REQ_FIND_PREV:
2111 direction = -1;
2112 break;
2114 default:
2115 return;
2116 }
2118 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
2119 lineno += direction;
2121 /* Note, lineno is unsigned long so will wrap around in which case it
2122 * will become bigger than view->lines. */
2123 for (; lineno < view->lines; lineno += direction) {
2124 struct line *line = &view->line[lineno];
2126 if (find_next_line(view, lineno, line))
2127 return;
2128 }
2130 report("No match found for '%s'", view->grep);
2131 }
2133 static void
2134 search_view(struct view *view, enum request request)
2135 {
2136 int regex_err;
2138 if (view->regex) {
2139 regfree(view->regex);
2140 *view->grep = 0;
2141 } else {
2142 view->regex = calloc(1, sizeof(*view->regex));
2143 if (!view->regex)
2144 return;
2145 }
2147 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
2148 if (regex_err != 0) {
2149 char buf[SIZEOF_STR] = "unknown error";
2151 regerror(regex_err, view->regex, buf, sizeof(buf));
2152 report("Search failed: %s", buf);
2153 return;
2154 }
2156 string_copy(view->grep, opt_search);
2158 find_next(view, request);
2159 }
2161 /*
2162 * Incremental updating
2163 */
2165 static void
2166 end_update(struct view *view)
2167 {
2168 if (!view->pipe)
2169 return;
2170 set_nonblocking_input(FALSE);
2171 if (view->pipe == stdin)
2172 fclose(view->pipe);
2173 else
2174 pclose(view->pipe);
2175 view->pipe = NULL;
2176 }
2178 static bool
2179 begin_update(struct view *view)
2180 {
2181 if (view->pipe)
2182 end_update(view);
2184 if (opt_cmd[0]) {
2185 string_copy(view->cmd, opt_cmd);
2186 opt_cmd[0] = 0;
2187 /* When running random commands, initially show the
2188 * command in the title. However, it maybe later be
2189 * overwritten if a commit line is selected. */
2190 if (view == VIEW(REQ_VIEW_PAGER))
2191 string_copy(view->ref, view->cmd);
2192 else
2193 view->ref[0] = 0;
2195 } else if (view == VIEW(REQ_VIEW_TREE)) {
2196 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2197 char path[SIZEOF_STR];
2199 if (strcmp(view->vid, view->id))
2200 opt_path[0] = path[0] = 0;
2201 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
2202 return FALSE;
2204 if (!string_format(view->cmd, format, view->id, path))
2205 return FALSE;
2207 } else {
2208 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2209 const char *id = view->id;
2211 if (!string_format(view->cmd, format, id, id, id, id, id))
2212 return FALSE;
2214 /* Put the current ref_* value to the view title ref
2215 * member. This is needed by the blob view. Most other
2216 * views sets it automatically after loading because the
2217 * first line is a commit line. */
2218 string_copy_rev(view->ref, view->id);
2219 }
2221 /* Special case for the pager view. */
2222 if (opt_pipe) {
2223 view->pipe = opt_pipe;
2224 opt_pipe = NULL;
2225 } else {
2226 view->pipe = popen(view->cmd, "r");
2227 }
2229 if (!view->pipe)
2230 return FALSE;
2232 set_nonblocking_input(TRUE);
2234 view->offset = 0;
2235 view->lines = 0;
2236 view->lineno = 0;
2237 string_copy_rev(view->vid, view->id);
2239 if (view->line) {
2240 int i;
2242 for (i = 0; i < view->lines; i++)
2243 if (view->line[i].data)
2244 free(view->line[i].data);
2246 free(view->line);
2247 view->line = NULL;
2248 }
2250 view->start_time = time(NULL);
2252 return TRUE;
2253 }
2255 #define ITEM_CHUNK_SIZE 256
2256 static void *
2257 realloc_items(void *mem, size_t *size, size_t new_size, size_t item_size)
2258 {
2259 size_t num_chunks = *size / ITEM_CHUNK_SIZE;
2260 size_t num_chunks_new = (new_size + ITEM_CHUNK_SIZE - 1) / ITEM_CHUNK_SIZE;
2262 if (mem == NULL || num_chunks != num_chunks_new) {
2263 *size = num_chunks_new * ITEM_CHUNK_SIZE;
2264 mem = realloc(mem, *size * item_size);
2265 }
2267 return mem;
2268 }
2270 static struct line *
2271 realloc_lines(struct view *view, size_t line_size)
2272 {
2273 size_t alloc = view->line_alloc;
2274 struct line *tmp = realloc_items(view->line, &alloc, line_size,
2275 sizeof(*view->line));
2277 if (!tmp)
2278 return NULL;
2280 view->line = tmp;
2281 view->line_alloc = alloc;
2282 view->line_size = line_size;
2283 return view->line;
2284 }
2286 static bool
2287 update_view(struct view *view)
2288 {
2289 char in_buffer[BUFSIZ];
2290 char out_buffer[BUFSIZ * 2];
2291 char *line;
2292 /* The number of lines to read. If too low it will cause too much
2293 * redrawing (and possible flickering), if too high responsiveness
2294 * will suffer. */
2295 unsigned long lines = view->height;
2296 int redraw_from = -1;
2298 if (!view->pipe)
2299 return TRUE;
2301 /* Only redraw if lines are visible. */
2302 if (view->offset + view->height >= view->lines)
2303 redraw_from = view->lines - view->offset;
2305 /* FIXME: This is probably not perfect for backgrounded views. */
2306 if (!realloc_lines(view, view->lines + lines))
2307 goto alloc_error;
2309 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
2310 size_t linelen = strlen(line);
2312 if (linelen)
2313 line[linelen - 1] = 0;
2315 if (opt_iconv != ICONV_NONE) {
2316 ICONV_CONST char *inbuf = line;
2317 size_t inlen = linelen;
2319 char *outbuf = out_buffer;
2320 size_t outlen = sizeof(out_buffer);
2322 size_t ret;
2324 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
2325 if (ret != (size_t) -1) {
2326 line = out_buffer;
2327 linelen = strlen(out_buffer);
2328 }
2329 }
2331 if (!view->ops->read(view, line))
2332 goto alloc_error;
2334 if (lines-- == 1)
2335 break;
2336 }
2338 {
2339 int digits;
2341 lines = view->lines;
2342 for (digits = 0; lines; digits++)
2343 lines /= 10;
2345 /* Keep the displayed view in sync with line number scaling. */
2346 if (digits != view->digits) {
2347 view->digits = digits;
2348 redraw_from = 0;
2349 }
2350 }
2352 if (!view_is_displayed(view))
2353 goto check_pipe;
2355 if (view == VIEW(REQ_VIEW_TREE)) {
2356 /* Clear the view and redraw everything since the tree sorting
2357 * might have rearranged things. */
2358 redraw_view(view);
2360 } else if (redraw_from >= 0) {
2361 /* If this is an incremental update, redraw the previous line
2362 * since for commits some members could have changed when
2363 * loading the main view. */
2364 if (redraw_from > 0)
2365 redraw_from--;
2367 /* Since revision graph visualization requires knowledge
2368 * about the parent commit, it causes a further one-off
2369 * needed to be redrawn for incremental updates. */
2370 if (redraw_from > 0 && opt_rev_graph)
2371 redraw_from--;
2373 /* Incrementally draw avoids flickering. */
2374 redraw_view_from(view, redraw_from);
2375 }
2377 if (view == VIEW(REQ_VIEW_BLAME))
2378 redraw_view_dirty(view);
2380 /* Update the title _after_ the redraw so that if the redraw picks up a
2381 * commit reference in view->ref it'll be available here. */
2382 update_view_title(view);
2384 check_pipe:
2385 if (ferror(view->pipe)) {
2386 report("Failed to read: %s", strerror(errno));
2387 goto end;
2389 } else if (feof(view->pipe)) {
2390 report("");
2391 goto end;
2392 }
2394 return TRUE;
2396 alloc_error:
2397 report("Allocation failure");
2399 end:
2400 if (view->ops->read(view, NULL))
2401 end_update(view);
2402 return FALSE;
2403 }
2405 static struct line *
2406 add_line_data(struct view *view, void *data, enum line_type type)
2407 {
2408 struct line *line = &view->line[view->lines++];
2410 memset(line, 0, sizeof(*line));
2411 line->type = type;
2412 line->data = data;
2414 return line;
2415 }
2417 static struct line *
2418 add_line_text(struct view *view, char *data, enum line_type type)
2419 {
2420 if (data)
2421 data = strdup(data);
2423 return data ? add_line_data(view, data, type) : NULL;
2424 }
2427 /*
2428 * View opening
2429 */
2431 enum open_flags {
2432 OPEN_DEFAULT = 0, /* Use default view switching. */
2433 OPEN_SPLIT = 1, /* Split current view. */
2434 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2435 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2436 OPEN_NOMAXIMIZE = 8 /* Do not maximize the current view. */
2437 };
2439 static void
2440 open_view(struct view *prev, enum request request, enum open_flags flags)
2441 {
2442 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2443 bool split = !!(flags & OPEN_SPLIT);
2444 bool reload = !!(flags & OPEN_RELOAD);
2445 bool nomaximize = !!(flags & OPEN_NOMAXIMIZE);
2446 struct view *view = VIEW(request);
2447 int nviews = displayed_views();
2448 struct view *base_view = display[0];
2450 if (view == prev && nviews == 1 && !reload) {
2451 report("Already in %s view", view->name);
2452 return;
2453 }
2455 if (view->git_dir && !opt_git_dir[0]) {
2456 report("The %s view is disabled in pager view", view->name);
2457 return;
2458 }
2460 if (split) {
2461 display[1] = view;
2462 if (!backgrounded)
2463 current_view = 1;
2464 } else if (!nomaximize) {
2465 /* Maximize the current view. */
2466 memset(display, 0, sizeof(display));
2467 current_view = 0;
2468 display[current_view] = view;
2469 }
2471 /* Resize the view when switching between split- and full-screen,
2472 * or when switching between two different full-screen views. */
2473 if (nviews != displayed_views() ||
2474 (nviews == 1 && base_view != display[0]))
2475 resize_display();
2477 if (view->ops->open) {
2478 if (!view->ops->open(view)) {
2479 report("Failed to load %s view", view->name);
2480 return;
2481 }
2483 } else if ((reload || strcmp(view->vid, view->id)) &&
2484 !begin_update(view)) {
2485 report("Failed to load %s view", view->name);
2486 return;
2487 }
2489 if (split && prev->lineno - prev->offset >= prev->height) {
2490 /* Take the title line into account. */
2491 int lines = prev->lineno - prev->offset - prev->height + 1;
2493 /* Scroll the view that was split if the current line is
2494 * outside the new limited view. */
2495 do_scroll_view(prev, lines);
2496 }
2498 if (prev && view != prev) {
2499 if (split && !backgrounded) {
2500 /* "Blur" the previous view. */
2501 update_view_title(prev);
2502 }
2504 view->parent = prev;
2505 }
2507 if (view->pipe && view->lines == 0) {
2508 /* Clear the old view and let the incremental updating refill
2509 * the screen. */
2510 werase(view->win);
2511 report("");
2512 } else {
2513 redraw_view(view);
2514 report("");
2515 }
2517 /* If the view is backgrounded the above calls to report()
2518 * won't redraw the view title. */
2519 if (backgrounded)
2520 update_view_title(view);
2521 }
2523 static void
2524 open_external_viewer(const char *cmd)
2525 {
2526 def_prog_mode(); /* save current tty modes */
2527 endwin(); /* restore original tty modes */
2528 system(cmd);
2529 fprintf(stderr, "Press Enter to continue");
2530 getc(stdin);
2531 reset_prog_mode();
2532 redraw_display();
2533 }
2535 static void
2536 open_mergetool(const char *file)
2537 {
2538 char cmd[SIZEOF_STR];
2539 char file_sq[SIZEOF_STR];
2541 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2542 string_format(cmd, "git mergetool %s", file_sq)) {
2543 open_external_viewer(cmd);
2544 }
2545 }
2547 static void
2548 open_editor(bool from_root, const char *file)
2549 {
2550 char cmd[SIZEOF_STR];
2551 char file_sq[SIZEOF_STR];
2552 char *editor;
2553 char *prefix = from_root ? opt_cdup : "";
2555 editor = getenv("GIT_EDITOR");
2556 if (!editor && *opt_editor)
2557 editor = opt_editor;
2558 if (!editor)
2559 editor = getenv("VISUAL");
2560 if (!editor)
2561 editor = getenv("EDITOR");
2562 if (!editor)
2563 editor = "vi";
2565 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2566 string_format(cmd, "%s %s%s", editor, prefix, file_sq)) {
2567 open_external_viewer(cmd);
2568 }
2569 }
2571 static void
2572 open_run_request(enum request request)
2573 {
2574 struct run_request *req = get_run_request(request);
2575 char buf[SIZEOF_STR * 2];
2576 size_t bufpos;
2577 char *cmd;
2579 if (!req) {
2580 report("Unknown run request");
2581 return;
2582 }
2584 bufpos = 0;
2585 cmd = req->cmd;
2587 while (cmd) {
2588 char *next = strstr(cmd, "%(");
2589 int len = next - cmd;
2590 char *value;
2592 if (!next) {
2593 len = strlen(cmd);
2594 value = "";
2596 } else if (!strncmp(next, "%(head)", 7)) {
2597 value = ref_head;
2599 } else if (!strncmp(next, "%(commit)", 9)) {
2600 value = ref_commit;
2602 } else if (!strncmp(next, "%(blob)", 7)) {
2603 value = ref_blob;
2605 } else {
2606 report("Unknown replacement in run request: `%s`", req->cmd);
2607 return;
2608 }
2610 if (!string_format_from(buf, &bufpos, "%.*s%s", len, cmd, value))
2611 return;
2613 if (next)
2614 next = strchr(next, ')') + 1;
2615 cmd = next;
2616 }
2618 open_external_viewer(buf);
2619 }
2621 /*
2622 * User request switch noodle
2623 */
2625 static int
2626 view_driver(struct view *view, enum request request)
2627 {
2628 int i;
2630 if (request == REQ_NONE) {
2631 doupdate();
2632 return TRUE;
2633 }
2635 if (request > REQ_NONE) {
2636 open_run_request(request);
2637 /* FIXME: When all views can refresh always do this. */
2638 if (view == VIEW(REQ_VIEW_STATUS) ||
2639 view == VIEW(REQ_VIEW_STAGE))
2640 request = REQ_REFRESH;
2641 else
2642 return TRUE;
2643 }
2645 if (view && view->lines) {
2646 request = view->ops->request(view, request, &view->line[view->lineno]);
2647 if (request == REQ_NONE)
2648 return TRUE;
2649 }
2651 switch (request) {
2652 case REQ_MOVE_UP:
2653 case REQ_MOVE_DOWN:
2654 case REQ_MOVE_PAGE_UP:
2655 case REQ_MOVE_PAGE_DOWN:
2656 case REQ_MOVE_FIRST_LINE:
2657 case REQ_MOVE_LAST_LINE:
2658 move_view(view, request);
2659 break;
2661 case REQ_SCROLL_LINE_DOWN:
2662 case REQ_SCROLL_LINE_UP:
2663 case REQ_SCROLL_PAGE_DOWN:
2664 case REQ_SCROLL_PAGE_UP:
2665 scroll_view(view, request);
2666 break;
2668 case REQ_VIEW_BLAME:
2669 if (!opt_file[0]) {
2670 report("No file chosen, press %s to open tree view",
2671 get_key(REQ_VIEW_TREE));
2672 break;
2673 }
2674 open_view(view, request, OPEN_DEFAULT);
2675 break;
2677 case REQ_VIEW_BLOB:
2678 if (!ref_blob[0]) {
2679 report("No file chosen, press %s to open tree view",
2680 get_key(REQ_VIEW_TREE));
2681 break;
2682 }
2683 open_view(view, request, OPEN_DEFAULT);
2684 break;
2686 case REQ_VIEW_PAGER:
2687 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2688 report("No pager content, press %s to run command from prompt",
2689 get_key(REQ_PROMPT));
2690 break;
2691 }
2692 open_view(view, request, OPEN_DEFAULT);
2693 break;
2695 case REQ_VIEW_STAGE:
2696 if (!VIEW(REQ_VIEW_STAGE)->lines) {
2697 report("No stage content, press %s to open the status view and choose file",
2698 get_key(REQ_VIEW_STATUS));
2699 break;
2700 }
2701 open_view(view, request, OPEN_DEFAULT);
2702 break;
2704 case REQ_VIEW_STATUS:
2705 if (opt_is_inside_work_tree == FALSE) {
2706 report("The status view requires a working tree");
2707 break;
2708 }
2709 open_view(view, request, OPEN_DEFAULT);
2710 break;
2712 case REQ_VIEW_MAIN:
2713 case REQ_VIEW_DIFF:
2714 case REQ_VIEW_LOG:
2715 case REQ_VIEW_TREE:
2716 case REQ_VIEW_HELP:
2717 open_view(view, request, OPEN_DEFAULT);
2718 break;
2720 case REQ_NEXT:
2721 case REQ_PREVIOUS:
2722 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2724 if ((view == VIEW(REQ_VIEW_DIFF) &&
2725 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2726 (view == VIEW(REQ_VIEW_DIFF) &&
2727 view->parent == VIEW(REQ_VIEW_BLAME)) ||
2728 (view == VIEW(REQ_VIEW_STAGE) &&
2729 view->parent == VIEW(REQ_VIEW_STATUS)) ||
2730 (view == VIEW(REQ_VIEW_BLOB) &&
2731 view->parent == VIEW(REQ_VIEW_TREE))) {
2732 int line;
2734 view = view->parent;
2735 line = view->lineno;
2736 move_view(view, request);
2737 if (view_is_displayed(view))
2738 update_view_title(view);
2739 if (line != view->lineno)
2740 view->ops->request(view, REQ_ENTER,
2741 &view->line[view->lineno]);
2743 } else {
2744 move_view(view, request);
2745 }
2746 break;
2748 case REQ_VIEW_NEXT:
2749 {
2750 int nviews = displayed_views();
2751 int next_view = (current_view + 1) % nviews;
2753 if (next_view == current_view) {
2754 report("Only one view is displayed");
2755 break;
2756 }
2758 current_view = next_view;
2759 /* Blur out the title of the previous view. */
2760 update_view_title(view);
2761 report("");
2762 break;
2763 }
2764 case REQ_REFRESH:
2765 report("Refreshing is not yet supported for the %s view", view->name);
2766 break;
2768 case REQ_MAXIMIZE:
2769 if (displayed_views() == 2)
2770 open_view(view, VIEW_REQ(view), OPEN_DEFAULT);
2771 break;
2773 case REQ_TOGGLE_LINENO:
2774 opt_line_number = !opt_line_number;
2775 redraw_display();
2776 break;
2778 case REQ_TOGGLE_DATE:
2779 opt_date = !opt_date;
2780 redraw_display();
2781 break;
2783 case REQ_TOGGLE_AUTHOR:
2784 opt_author = !opt_author;
2785 redraw_display();
2786 break;
2788 case REQ_TOGGLE_REV_GRAPH:
2789 opt_rev_graph = !opt_rev_graph;
2790 redraw_display();
2791 break;
2793 case REQ_TOGGLE_REFS:
2794 opt_show_refs = !opt_show_refs;
2795 redraw_display();
2796 break;
2798 case REQ_PROMPT:
2799 /* Always reload^Wrerun commands from the prompt. */
2800 open_view(view, opt_request, OPEN_RELOAD);
2801 break;
2803 case REQ_SEARCH:
2804 case REQ_SEARCH_BACK:
2805 search_view(view, request);
2806 break;
2808 case REQ_FIND_NEXT:
2809 case REQ_FIND_PREV:
2810 find_next(view, request);
2811 break;
2813 case REQ_STOP_LOADING:
2814 for (i = 0; i < ARRAY_SIZE(views); i++) {
2815 view = &views[i];
2816 if (view->pipe)
2817 report("Stopped loading the %s view", view->name),
2818 end_update(view);
2819 }
2820 break;
2822 case REQ_SHOW_VERSION:
2823 report("tig-%s (built %s)", TIG_VERSION, __DATE__);
2824 return TRUE;
2826 case REQ_SCREEN_RESIZE:
2827 resize_display();
2828 /* Fall-through */
2829 case REQ_SCREEN_REDRAW:
2830 redraw_display();
2831 break;
2833 case REQ_EDIT:
2834 report("Nothing to edit");
2835 break;
2838 case REQ_ENTER:
2839 report("Nothing to enter");
2840 break;
2843 case REQ_VIEW_CLOSE:
2844 /* XXX: Mark closed views by letting view->parent point to the
2845 * view itself. Parents to closed view should never be
2846 * followed. */
2847 if (view->parent &&
2848 view->parent->parent != view->parent) {
2849 memset(display, 0, sizeof(display));
2850 current_view = 0;
2851 display[current_view] = view->parent;
2852 view->parent = view;
2853 resize_display();
2854 redraw_display();
2855 break;
2856 }
2857 /* Fall-through */
2858 case REQ_QUIT:
2859 return FALSE;
2861 default:
2862 /* An unknown key will show most commonly used commands. */
2863 report("Unknown key, press 'h' for help");
2864 return TRUE;
2865 }
2867 return TRUE;
2868 }
2871 /*
2872 * Pager backend
2873 */
2875 static bool
2876 pager_draw(struct view *view, struct line *line, unsigned int lineno)
2877 {
2878 char *text = line->data;
2880 if (opt_line_number && draw_lineno(view, lineno))
2881 return TRUE;
2883 draw_text(view, line->type, text, TRUE);
2884 return TRUE;
2885 }
2887 static bool
2888 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2889 {
2890 char refbuf[SIZEOF_STR];
2891 char *ref = NULL;
2892 FILE *pipe;
2894 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2895 return TRUE;
2897 pipe = popen(refbuf, "r");
2898 if (!pipe)
2899 return TRUE;
2901 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2902 ref = chomp_string(ref);
2903 pclose(pipe);
2905 if (!ref || !*ref)
2906 return TRUE;
2908 /* This is the only fatal call, since it can "corrupt" the buffer. */
2909 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2910 return FALSE;
2912 return TRUE;
2913 }
2915 static void
2916 add_pager_refs(struct view *view, struct line *line)
2917 {
2918 char buf[SIZEOF_STR];
2919 char *commit_id = (char *)line->data + STRING_SIZE("commit ");
2920 struct ref **refs;
2921 size_t bufpos = 0, refpos = 0;
2922 const char *sep = "Refs: ";
2923 bool is_tag = FALSE;
2925 assert(line->type == LINE_COMMIT);
2927 refs = get_refs(commit_id);
2928 if (!refs) {
2929 if (view == VIEW(REQ_VIEW_DIFF))
2930 goto try_add_describe_ref;
2931 return;
2932 }
2934 do {
2935 struct ref *ref = refs[refpos];
2936 char *fmt = ref->tag ? "%s[%s]" :
2937 ref->remote ? "%s<%s>" : "%s%s";
2939 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2940 return;
2941 sep = ", ";
2942 if (ref->tag)
2943 is_tag = TRUE;
2944 } while (refs[refpos++]->next);
2946 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2947 try_add_describe_ref:
2948 /* Add <tag>-g<commit_id> "fake" reference. */
2949 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2950 return;
2951 }
2953 if (bufpos == 0)
2954 return;
2956 if (!realloc_lines(view, view->line_size + 1))
2957 return;
2959 add_line_text(view, buf, LINE_PP_REFS);
2960 }
2962 static bool
2963 pager_read(struct view *view, char *data)
2964 {
2965 struct line *line;
2967 if (!data)
2968 return TRUE;
2970 line = add_line_text(view, data, get_line_type(data));
2971 if (!line)
2972 return FALSE;
2974 if (line->type == LINE_COMMIT &&
2975 (view == VIEW(REQ_VIEW_DIFF) ||
2976 view == VIEW(REQ_VIEW_LOG)))
2977 add_pager_refs(view, line);
2979 return TRUE;
2980 }
2982 static enum request
2983 pager_request(struct view *view, enum request request, struct line *line)
2984 {
2985 int split = 0;
2987 if (request != REQ_ENTER)
2988 return request;
2990 if (line->type == LINE_COMMIT &&
2991 (view == VIEW(REQ_VIEW_LOG) ||
2992 view == VIEW(REQ_VIEW_PAGER))) {
2993 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2994 split = 1;
2995 }
2997 /* Always scroll the view even if it was split. That way
2998 * you can use Enter to scroll through the log view and
2999 * split open each commit diff. */
3000 scroll_view(view, REQ_SCROLL_LINE_DOWN);
3002 /* FIXME: A minor workaround. Scrolling the view will call report("")
3003 * but if we are scrolling a non-current view this won't properly
3004 * update the view title. */
3005 if (split)
3006 update_view_title(view);
3008 return REQ_NONE;
3009 }
3011 static bool
3012 pager_grep(struct view *view, struct line *line)
3013 {
3014 regmatch_t pmatch;
3015 char *text = line->data;
3017 if (!*text)
3018 return FALSE;
3020 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
3021 return FALSE;
3023 return TRUE;
3024 }
3026 static void
3027 pager_select(struct view *view, struct line *line)
3028 {
3029 if (line->type == LINE_COMMIT) {
3030 char *text = (char *)line->data + STRING_SIZE("commit ");
3032 if (view != VIEW(REQ_VIEW_PAGER))
3033 string_copy_rev(view->ref, text);
3034 string_copy_rev(ref_commit, text);
3035 }
3036 }
3038 static struct view_ops pager_ops = {
3039 "line",
3040 NULL,
3041 pager_read,
3042 pager_draw,
3043 pager_request,
3044 pager_grep,
3045 pager_select,
3046 };
3049 /*
3050 * Help backend
3051 */
3053 static bool
3054 help_open(struct view *view)
3055 {
3056 char buf[BUFSIZ];
3057 int lines = ARRAY_SIZE(req_info) + 2;
3058 int i;
3060 if (view->lines > 0)
3061 return TRUE;
3063 for (i = 0; i < ARRAY_SIZE(req_info); i++)
3064 if (!req_info[i].request)
3065 lines++;
3067 lines += run_requests + 1;
3069 view->line = calloc(lines, sizeof(*view->line));
3070 if (!view->line)
3071 return FALSE;
3073 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
3075 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
3076 char *key;
3078 if (req_info[i].request == REQ_NONE)
3079 continue;
3081 if (!req_info[i].request) {
3082 add_line_text(view, "", LINE_DEFAULT);
3083 add_line_text(view, req_info[i].help, LINE_DEFAULT);
3084 continue;
3085 }
3087 key = get_key(req_info[i].request);
3088 if (!*key)
3089 key = "(no key defined)";
3091 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
3092 continue;
3094 add_line_text(view, buf, LINE_DEFAULT);
3095 }
3097 if (run_requests) {
3098 add_line_text(view, "", LINE_DEFAULT);
3099 add_line_text(view, "External commands:", LINE_DEFAULT);
3100 }
3102 for (i = 0; i < run_requests; i++) {
3103 struct run_request *req = get_run_request(REQ_NONE + i + 1);
3104 char *key;
3106 if (!req)
3107 continue;
3109 key = get_key_name(req->key);
3110 if (!*key)
3111 key = "(no key defined)";
3113 if (!string_format(buf, " %-10s %-14s `%s`",
3114 keymap_table[req->keymap].name,
3115 key, req->cmd))
3116 continue;
3118 add_line_text(view, buf, LINE_DEFAULT);
3119 }
3121 return TRUE;
3122 }
3124 static struct view_ops help_ops = {
3125 "line",
3126 help_open,
3127 NULL,
3128 pager_draw,
3129 pager_request,
3130 pager_grep,
3131 pager_select,
3132 };
3135 /*
3136 * Tree backend
3137 */
3139 struct tree_stack_entry {
3140 struct tree_stack_entry *prev; /* Entry below this in the stack */
3141 unsigned long lineno; /* Line number to restore */
3142 char *name; /* Position of name in opt_path */
3143 };
3145 /* The top of the path stack. */
3146 static struct tree_stack_entry *tree_stack = NULL;
3147 unsigned long tree_lineno = 0;
3149 static void
3150 pop_tree_stack_entry(void)
3151 {
3152 struct tree_stack_entry *entry = tree_stack;
3154 tree_lineno = entry->lineno;
3155 entry->name[0] = 0;
3156 tree_stack = entry->prev;
3157 free(entry);
3158 }
3160 static void
3161 push_tree_stack_entry(char *name, unsigned long lineno)
3162 {
3163 struct tree_stack_entry *entry = calloc(1, sizeof(*entry));
3164 size_t pathlen = strlen(opt_path);
3166 if (!entry)
3167 return;
3169 entry->prev = tree_stack;
3170 entry->name = opt_path + pathlen;
3171 tree_stack = entry;
3173 if (!string_format_from(opt_path, &pathlen, "%s/", name)) {
3174 pop_tree_stack_entry();
3175 return;
3176 }
3178 /* Move the current line to the first tree entry. */
3179 tree_lineno = 1;
3180 entry->lineno = lineno;
3181 }
3183 /* Parse output from git-ls-tree(1):
3184 *
3185 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
3186 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
3187 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
3188 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
3189 */
3191 #define SIZEOF_TREE_ATTR \
3192 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
3194 #define TREE_UP_FORMAT "040000 tree %s\t.."
3196 static int
3197 tree_compare_entry(enum line_type type1, char *name1,
3198 enum line_type type2, char *name2)
3199 {
3200 if (type1 != type2) {
3201 if (type1 == LINE_TREE_DIR)
3202 return -1;
3203 return 1;
3204 }
3206 return strcmp(name1, name2);
3207 }
3209 static char *
3210 tree_path(struct line *line)
3211 {
3212 char *path = line->data;
3214 return path + SIZEOF_TREE_ATTR;
3215 }
3217 static bool
3218 tree_read(struct view *view, char *text)
3219 {
3220 size_t textlen = text ? strlen(text) : 0;
3221 char buf[SIZEOF_STR];
3222 unsigned long pos;
3223 enum line_type type;
3224 bool first_read = view->lines == 0;
3226 if (!text)
3227 return TRUE;
3228 if (textlen <= SIZEOF_TREE_ATTR)
3229 return FALSE;
3231 type = text[STRING_SIZE("100644 ")] == 't'
3232 ? LINE_TREE_DIR : LINE_TREE_FILE;
3234 if (first_read) {
3235 /* Add path info line */
3236 if (!string_format(buf, "Directory path /%s", opt_path) ||
3237 !realloc_lines(view, view->line_size + 1) ||
3238 !add_line_text(view, buf, LINE_DEFAULT))
3239 return FALSE;
3241 /* Insert "link" to parent directory. */
3242 if (*opt_path) {
3243 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
3244 !realloc_lines(view, view->line_size + 1) ||
3245 !add_line_text(view, buf, LINE_TREE_DIR))
3246 return FALSE;
3247 }
3248 }
3250 /* Strip the path part ... */
3251 if (*opt_path) {
3252 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
3253 size_t striplen = strlen(opt_path);
3254 char *path = text + SIZEOF_TREE_ATTR;
3256 if (pathlen > striplen)
3257 memmove(path, path + striplen,
3258 pathlen - striplen + 1);
3259 }
3261 /* Skip "Directory ..." and ".." line. */
3262 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
3263 struct line *line = &view->line[pos];
3264 char *path1 = tree_path(line);
3265 char *path2 = text + SIZEOF_TREE_ATTR;
3266 int cmp = tree_compare_entry(line->type, path1, type, path2);
3268 if (cmp <= 0)
3269 continue;
3271 text = strdup(text);
3272 if (!text)
3273 return FALSE;
3275 if (view->lines > pos)
3276 memmove(&view->line[pos + 1], &view->line[pos],
3277 (view->lines - pos) * sizeof(*line));
3279 line = &view->line[pos];
3280 line->data = text;
3281 line->type = type;
3282 view->lines++;
3283 return TRUE;
3284 }
3286 if (!add_line_text(view, text, type))
3287 return FALSE;
3289 if (tree_lineno > view->lineno) {
3290 view->lineno = tree_lineno;
3291 tree_lineno = 0;
3292 }
3294 return TRUE;
3295 }
3297 static enum request
3298 tree_request(struct view *view, enum request request, struct line *line)
3299 {
3300 enum open_flags flags;
3302 if (request == REQ_VIEW_BLAME) {
3303 char *filename = tree_path(line);
3305 if (line->type == LINE_TREE_DIR) {
3306 report("Cannot show blame for directory %s", opt_path);
3307 return REQ_NONE;
3308 }
3310 string_copy(opt_ref, view->vid);
3311 string_format(opt_file, "%s%s", opt_path, filename);
3312 return request;
3313 }
3314 if (request == REQ_TREE_PARENT) {
3315 if (*opt_path) {
3316 /* fake 'cd ..' */
3317 request = REQ_ENTER;
3318 line = &view->line[1];
3319 } else {
3320 /* quit view if at top of tree */
3321 return REQ_VIEW_CLOSE;
3322 }
3323 }
3324 if (request != REQ_ENTER)
3325 return request;
3327 /* Cleanup the stack if the tree view is at a different tree. */
3328 while (!*opt_path && tree_stack)
3329 pop_tree_stack_entry();
3331 switch (line->type) {
3332 case LINE_TREE_DIR:
3333 /* Depending on whether it is a subdir or parent (updir?) link
3334 * mangle the path buffer. */
3335 if (line == &view->line[1] && *opt_path) {
3336 pop_tree_stack_entry();
3338 } else {
3339 char *basename = tree_path(line);
3341 push_tree_stack_entry(basename, view->lineno);
3342 }
3344 /* Trees and subtrees share the same ID, so they are not not
3345 * unique like blobs. */
3346 flags = OPEN_RELOAD;
3347 request = REQ_VIEW_TREE;
3348 break;
3350 case LINE_TREE_FILE:
3351 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3352 request = REQ_VIEW_BLOB;
3353 break;
3355 default:
3356 return TRUE;
3357 }
3359 open_view(view, request, flags);
3360 if (request == REQ_VIEW_TREE) {
3361 view->lineno = tree_lineno;
3362 }
3364 return REQ_NONE;
3365 }
3367 static void
3368 tree_select(struct view *view, struct line *line)
3369 {
3370 char *text = (char *)line->data + STRING_SIZE("100644 blob ");
3372 if (line->type == LINE_TREE_FILE) {
3373 string_copy_rev(ref_blob, text);
3375 } else if (line->type != LINE_TREE_DIR) {
3376 return;
3377 }
3379 string_copy_rev(view->ref, text);
3380 }
3382 static struct view_ops tree_ops = {
3383 "file",
3384 NULL,
3385 tree_read,
3386 pager_draw,
3387 tree_request,
3388 pager_grep,
3389 tree_select,
3390 };
3392 static bool
3393 blob_read(struct view *view, char *line)
3394 {
3395 if (!line)
3396 return TRUE;
3397 return add_line_text(view, line, LINE_DEFAULT) != NULL;
3398 }
3400 static struct view_ops blob_ops = {
3401 "line",
3402 NULL,
3403 blob_read,
3404 pager_draw,
3405 pager_request,
3406 pager_grep,
3407 pager_select,
3408 };
3410 /*
3411 * Blame backend
3412 *
3413 * Loading the blame view is a two phase job:
3414 *
3415 * 1. File content is read either using opt_file from the
3416 * filesystem or using git-cat-file.
3417 * 2. Then blame information is incrementally added by
3418 * reading output from git-blame.
3419 */
3421 struct blame_commit {
3422 char id[SIZEOF_REV]; /* SHA1 ID. */
3423 char title[128]; /* First line of the commit message. */
3424 char author[75]; /* Author of the commit. */
3425 struct tm time; /* Date from the author ident. */
3426 char filename[128]; /* Name of file. */
3427 };
3429 struct blame {
3430 struct blame_commit *commit;
3431 unsigned int header:1;
3432 char text[1];
3433 };
3435 #define BLAME_CAT_FILE_CMD "git cat-file blob %s:%s"
3436 #define BLAME_INCREMENTAL_CMD "git blame --incremental %s %s"
3438 static bool
3439 blame_open(struct view *view)
3440 {
3441 char path[SIZEOF_STR];
3442 char ref[SIZEOF_STR] = "";
3444 if (sq_quote(path, 0, opt_file) >= sizeof(path))
3445 return FALSE;
3447 if (*opt_ref && sq_quote(ref, 0, opt_ref) >= sizeof(ref))
3448 return FALSE;
3450 if (*opt_ref) {
3451 if (!string_format(view->cmd, BLAME_CAT_FILE_CMD, ref, path))
3452 return FALSE;
3453 } else {
3454 view->pipe = fopen(opt_file, "r");
3455 if (!view->pipe &&
3456 !string_format(view->cmd, BLAME_CAT_FILE_CMD, "HEAD", path))
3457 return FALSE;
3458 }
3460 if (!view->pipe)
3461 view->pipe = popen(view->cmd, "r");
3462 if (!view->pipe)
3463 return FALSE;
3465 if (!string_format(view->cmd, BLAME_INCREMENTAL_CMD, ref, path))
3466 return FALSE;
3468 string_format(view->ref, "%s ...", opt_file);
3469 string_copy_rev(view->vid, opt_file);
3470 set_nonblocking_input(TRUE);
3472 if (view->line) {
3473 int i;
3475 for (i = 0; i < view->lines; i++)
3476 free(view->line[i].data);
3477 free(view->line);
3478 }
3480 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3481 view->offset = view->lines = view->lineno = 0;
3482 view->line = NULL;
3483 view->start_time = time(NULL);
3485 return TRUE;
3486 }
3488 static struct blame_commit *
3489 get_blame_commit(struct view *view, const char *id)
3490 {
3491 size_t i;
3493 for (i = 0; i < view->lines; i++) {
3494 struct blame *blame = view->line[i].data;
3496 if (!blame->commit)
3497 continue;
3499 if (!strncmp(blame->commit->id, id, SIZEOF_REV - 1))
3500 return blame->commit;
3501 }
3503 {
3504 struct blame_commit *commit = calloc(1, sizeof(*commit));
3506 if (commit)
3507 string_ncopy(commit->id, id, SIZEOF_REV);
3508 return commit;
3509 }
3510 }
3512 static bool
3513 parse_number(char **posref, size_t *number, size_t min, size_t max)
3514 {
3515 char *pos = *posref;
3517 *posref = NULL;
3518 pos = strchr(pos + 1, ' ');
3519 if (!pos || !isdigit(pos[1]))
3520 return FALSE;
3521 *number = atoi(pos + 1);
3522 if (*number < min || *number > max)
3523 return FALSE;
3525 *posref = pos;
3526 return TRUE;
3527 }
3529 static struct blame_commit *
3530 parse_blame_commit(struct view *view, char *text, int *blamed)
3531 {
3532 struct blame_commit *commit;
3533 struct blame *blame;
3534 char *pos = text + SIZEOF_REV - 1;
3535 size_t lineno;
3536 size_t group;
3538 if (strlen(text) <= SIZEOF_REV || *pos != ' ')
3539 return NULL;
3541 if (!parse_number(&pos, &lineno, 1, view->lines) ||
3542 !parse_number(&pos, &group, 1, view->lines - lineno + 1))
3543 return NULL;
3545 commit = get_blame_commit(view, text);
3546 if (!commit)
3547 return NULL;
3549 *blamed += group;
3550 while (group--) {
3551 struct line *line = &view->line[lineno + group - 1];
3553 blame = line->data;
3554 blame->commit = commit;
3555 blame->header = !group;
3556 line->dirty = 1;
3557 }
3559 return commit;
3560 }
3562 static bool
3563 blame_read_file(struct view *view, char *line)
3564 {
3565 if (!line) {
3566 FILE *pipe = NULL;
3568 if (view->lines > 0)
3569 pipe = popen(view->cmd, "r");
3570 else if (!view->parent)
3571 die("No blame exist for %s", view->vid);
3572 view->cmd[0] = 0;
3573 if (!pipe) {
3574 report("Failed to load blame data");
3575 return TRUE;
3576 }
3578 fclose(view->pipe);
3579 view->pipe = pipe;
3580 return FALSE;
3582 } else {
3583 size_t linelen = strlen(line);
3584 struct blame *blame = malloc(sizeof(*blame) + linelen);
3586 if (!line)
3587 return FALSE;
3589 blame->commit = NULL;
3590 strncpy(blame->text, line, linelen);
3591 blame->text[linelen] = 0;
3592 return add_line_data(view, blame, LINE_BLAME_ID) != NULL;
3593 }
3594 }
3596 static bool
3597 match_blame_header(const char *name, char **line)
3598 {
3599 size_t namelen = strlen(name);
3600 bool matched = !strncmp(name, *line, namelen);
3602 if (matched)
3603 *line += namelen;
3605 return matched;
3606 }
3608 static bool
3609 blame_read(struct view *view, char *line)
3610 {
3611 static struct blame_commit *commit = NULL;
3612 static int blamed = 0;
3613 static time_t author_time;
3615 if (*view->cmd)
3616 return blame_read_file(view, line);
3618 if (!line) {
3619 /* Reset all! */
3620 commit = NULL;
3621 blamed = 0;
3622 string_format(view->ref, "%s", view->vid);
3623 if (view_is_displayed(view)) {
3624 update_view_title(view);
3625 redraw_view_from(view, 0);
3626 }
3627 return TRUE;
3628 }
3630 if (!commit) {
3631 commit = parse_blame_commit(view, line, &blamed);
3632 string_format(view->ref, "%s %2d%%", view->vid,
3633 blamed * 100 / view->lines);
3635 } else if (match_blame_header("author ", &line)) {
3636 string_ncopy(commit->author, line, strlen(line));
3638 } else if (match_blame_header("author-time ", &line)) {
3639 author_time = (time_t) atol(line);
3641 } else if (match_blame_header("author-tz ", &line)) {
3642 long tz;
3644 tz = ('0' - line[1]) * 60 * 60 * 10;
3645 tz += ('0' - line[2]) * 60 * 60;
3646 tz += ('0' - line[3]) * 60;
3647 tz += ('0' - line[4]) * 60;
3649 if (line[0] == '-')
3650 tz = -tz;
3652 author_time -= tz;
3653 gmtime_r(&author_time, &commit->time);
3655 } else if (match_blame_header("summary ", &line)) {
3656 string_ncopy(commit->title, line, strlen(line));
3658 } else if (match_blame_header("filename ", &line)) {
3659 string_ncopy(commit->filename, line, strlen(line));
3660 commit = NULL;
3661 }
3663 return TRUE;
3664 }
3666 static bool
3667 blame_draw(struct view *view, struct line *line, unsigned int lineno)
3668 {
3669 struct blame *blame = line->data;
3670 struct tm *time = NULL;
3671 char *id = NULL, *author = NULL;
3673 if (blame->commit && *blame->commit->filename) {
3674 id = blame->commit->id;
3675 author = blame->commit->author;
3676 time = &blame->commit->time;
3677 }
3679 if (opt_date && draw_date(view, time))
3680 return TRUE;
3682 if (opt_author &&
3683 draw_field(view, LINE_MAIN_AUTHOR, author, AUTHOR_COLS, TRUE))
3684 return TRUE;
3686 if (draw_field(view, LINE_BLAME_ID, id, ID_COLS, FALSE))
3687 return TRUE;
3689 if (draw_lineno(view, lineno))
3690 return TRUE;
3692 draw_text(view, LINE_DEFAULT, blame->text, TRUE);
3693 return TRUE;
3694 }
3696 static enum request
3697 blame_request(struct view *view, enum request request, struct line *line)
3698 {
3699 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3700 struct blame *blame = line->data;
3702 switch (request) {
3703 case REQ_ENTER:
3704 if (!blame->commit) {
3705 report("No commit loaded yet");
3706 break;
3707 }
3709 if (!strcmp(blame->commit->id, NULL_ID)) {
3710 char path[SIZEOF_STR];
3712 if (sq_quote(path, 0, view->vid) >= sizeof(path))
3713 break;
3714 string_format(opt_cmd, "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s 2>/dev/null", path);
3715 }
3717 open_view(view, REQ_VIEW_DIFF, flags);
3718 break;
3720 default:
3721 return request;
3722 }
3724 return REQ_NONE;
3725 }
3727 static bool
3728 blame_grep(struct view *view, struct line *line)
3729 {
3730 struct blame *blame = line->data;
3731 struct blame_commit *commit = blame->commit;
3732 regmatch_t pmatch;
3734 #define MATCH(text, on) \
3735 (on && *text && regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3737 if (commit) {
3738 char buf[DATE_COLS + 1];
3740 if (MATCH(commit->title, 1) ||
3741 MATCH(commit->author, opt_author) ||
3742 MATCH(commit->id, opt_date))
3743 return TRUE;
3745 if (strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time) &&
3746 MATCH(buf, 1))
3747 return TRUE;
3748 }
3750 return MATCH(blame->text, 1);
3752 #undef MATCH
3753 }
3755 static void
3756 blame_select(struct view *view, struct line *line)
3757 {
3758 struct blame *blame = line->data;
3759 struct blame_commit *commit = blame->commit;
3761 if (!commit)
3762 return;
3764 if (!strcmp(commit->id, NULL_ID))
3765 string_ncopy(ref_commit, "HEAD", 4);
3766 else
3767 string_copy_rev(ref_commit, commit->id);
3768 }
3770 static struct view_ops blame_ops = {
3771 "line",
3772 blame_open,
3773 blame_read,
3774 blame_draw,
3775 blame_request,
3776 blame_grep,
3777 blame_select,
3778 };
3780 /*
3781 * Status backend
3782 */
3784 struct status {
3785 char status;
3786 struct {
3787 mode_t mode;
3788 char rev[SIZEOF_REV];
3789 char name[SIZEOF_STR];
3790 } old;
3791 struct {
3792 mode_t mode;
3793 char rev[SIZEOF_REV];
3794 char name[SIZEOF_STR];
3795 } new;
3796 };
3798 static char status_onbranch[SIZEOF_STR];
3799 static struct status stage_status;
3800 static enum line_type stage_line_type;
3801 static size_t stage_chunks;
3802 static int *stage_chunk;
3804 /* Get fields from the diff line:
3805 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
3806 */
3807 static inline bool
3808 status_get_diff(struct status *file, char *buf, size_t bufsize)
3809 {
3810 char *old_mode = buf + 1;
3811 char *new_mode = buf + 8;
3812 char *old_rev = buf + 15;
3813 char *new_rev = buf + 56;
3814 char *status = buf + 97;
3816 if (bufsize < 99 ||
3817 old_mode[-1] != ':' ||
3818 new_mode[-1] != ' ' ||
3819 old_rev[-1] != ' ' ||
3820 new_rev[-1] != ' ' ||
3821 status[-1] != ' ')
3822 return FALSE;
3824 file->status = *status;
3826 string_copy_rev(file->old.rev, old_rev);
3827 string_copy_rev(file->new.rev, new_rev);
3829 file->old.mode = strtoul(old_mode, NULL, 8);
3830 file->new.mode = strtoul(new_mode, NULL, 8);
3832 file->old.name[0] = file->new.name[0] = 0;
3834 return TRUE;
3835 }
3837 static bool
3838 status_run(struct view *view, const char cmd[], char status, enum line_type type)
3839 {
3840 struct status *file = NULL;
3841 struct status *unmerged = NULL;
3842 char buf[SIZEOF_STR * 4];
3843 size_t bufsize = 0;
3844 FILE *pipe;
3846 pipe = popen(cmd, "r");
3847 if (!pipe)
3848 return FALSE;
3850 add_line_data(view, NULL, type);
3852 while (!feof(pipe) && !ferror(pipe)) {
3853 char *sep;
3854 size_t readsize;
3856 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
3857 if (!readsize)
3858 break;
3859 bufsize += readsize;
3861 /* Process while we have NUL chars. */
3862 while ((sep = memchr(buf, 0, bufsize))) {
3863 size_t sepsize = sep - buf + 1;
3865 if (!file) {
3866 if (!realloc_lines(view, view->line_size + 1))
3867 goto error_out;
3869 file = calloc(1, sizeof(*file));
3870 if (!file)
3871 goto error_out;
3873 add_line_data(view, file, type);
3874 }
3876 /* Parse diff info part. */
3877 if (status) {
3878 file->status = status;
3879 if (status == 'A')
3880 string_copy(file->old.rev, NULL_ID);
3882 } else if (!file->status) {
3883 if (!status_get_diff(file, buf, sepsize))
3884 goto error_out;
3886 bufsize -= sepsize;
3887 memmove(buf, sep + 1, bufsize);
3889 sep = memchr(buf, 0, bufsize);
3890 if (!sep)
3891 break;
3892 sepsize = sep - buf + 1;
3894 /* Collapse all 'M'odified entries that
3895 * follow a associated 'U'nmerged entry.
3896 */
3897 if (file->status == 'U') {
3898 unmerged = file;
3900 } else if (unmerged) {
3901 int collapse = !strcmp(buf, unmerged->new.name);
3903 unmerged = NULL;
3904 if (collapse) {
3905 free(file);
3906 view->lines--;
3907 continue;
3908 }
3909 }
3910 }
3912 /* Grab the old name for rename/copy. */
3913 if (!*file->old.name &&
3914 (file->status == 'R' || file->status == 'C')) {
3915 sepsize = sep - buf + 1;
3916 string_ncopy(file->old.name, buf, sepsize);
3917 bufsize -= sepsize;
3918 memmove(buf, sep + 1, bufsize);
3920 sep = memchr(buf, 0, bufsize);
3921 if (!sep)
3922 break;
3923 sepsize = sep - buf + 1;
3924 }
3926 /* git-ls-files just delivers a NUL separated
3927 * list of file names similar to the second half
3928 * of the git-diff-* output. */
3929 string_ncopy(file->new.name, buf, sepsize);
3930 if (!*file->old.name)
3931 string_copy(file->old.name, file->new.name);
3932 bufsize -= sepsize;
3933 memmove(buf, sep + 1, bufsize);
3934 file = NULL;
3935 }
3936 }
3938 if (ferror(pipe)) {
3939 error_out:
3940 pclose(pipe);
3941 return FALSE;
3942 }
3944 if (!view->line[view->lines - 1].data)
3945 add_line_data(view, NULL, LINE_STAT_NONE);
3947 pclose(pipe);
3948 return TRUE;
3949 }
3951 /* Don't show unmerged entries in the staged section. */
3952 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached -M HEAD"
3953 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
3954 #define STATUS_LIST_OTHER_CMD \
3955 "git ls-files -z --others --exclude-per-directory=.gitignore"
3956 #define STATUS_LIST_NO_HEAD_CMD \
3957 "git ls-files -z --cached --exclude-per-directory=.gitignore"
3959 #define STATUS_DIFF_INDEX_SHOW_CMD \
3960 "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s %s 2>/dev/null"
3962 #define STATUS_DIFF_FILES_SHOW_CMD \
3963 "git diff-files --root --patch-with-stat -C -M -- %s %s 2>/dev/null"
3965 #define STATUS_DIFF_NO_HEAD_SHOW_CMD \
3966 "git diff --no-color --patch-with-stat /dev/null %s 2>/dev/null"
3968 /* First parse staged info using git-diff-index(1), then parse unstaged
3969 * info using git-diff-files(1), and finally untracked files using
3970 * git-ls-files(1). */
3971 static bool
3972 status_open(struct view *view)
3973 {
3974 struct stat statbuf;
3975 char exclude[SIZEOF_STR];
3976 char indexcmd[SIZEOF_STR] = STATUS_DIFF_INDEX_CMD;
3977 char othercmd[SIZEOF_STR] = STATUS_LIST_OTHER_CMD;
3978 unsigned long prev_lineno = view->lineno;
3979 char indexstatus = 0;
3980 size_t i;
3982 for (i = 0; i < view->lines; i++)
3983 free(view->line[i].data);
3984 free(view->line);
3985 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3986 view->line = NULL;
3988 if (!realloc_lines(view, view->line_size + 7))
3989 return FALSE;
3991 add_line_data(view, NULL, LINE_STAT_HEAD);
3992 if (opt_no_head)
3993 string_copy(status_onbranch, "Initial commit");
3994 else if (!*opt_head)
3995 string_copy(status_onbranch, "Not currently on any branch");
3996 else if (!string_format(status_onbranch, "On branch %s", opt_head))
3997 return FALSE;
3999 if (opt_no_head) {
4000 string_copy(indexcmd, STATUS_LIST_NO_HEAD_CMD);
4001 indexstatus = 'A';
4002 }
4004 if (!string_format(exclude, "%s/info/exclude", opt_git_dir))
4005 return FALSE;
4007 if (stat(exclude, &statbuf) >= 0) {
4008 size_t cmdsize = strlen(othercmd);
4010 if (!string_format_from(othercmd, &cmdsize, " %s", "--exclude-from=") ||
4011 sq_quote(othercmd, cmdsize, exclude) >= sizeof(othercmd))
4012 return FALSE;
4014 cmdsize = strlen(indexcmd);
4015 if (opt_no_head &&
4016 (!string_format_from(indexcmd, &cmdsize, " %s", "--exclude-from=") ||
4017 sq_quote(indexcmd, cmdsize, exclude) >= sizeof(indexcmd)))
4018 return FALSE;
4019 }
4021 system("git update-index -q --refresh >/dev/null 2>/dev/null");
4023 if (!status_run(view, indexcmd, indexstatus, LINE_STAT_STAGED) ||
4024 !status_run(view, STATUS_DIFF_FILES_CMD, 0, LINE_STAT_UNSTAGED) ||
4025 !status_run(view, othercmd, '?', LINE_STAT_UNTRACKED))
4026 return FALSE;
4028 /* If all went well restore the previous line number to stay in
4029 * the context or select a line with something that can be
4030 * updated. */
4031 if (prev_lineno >= view->lines)
4032 prev_lineno = view->lines - 1;
4033 while (prev_lineno < view->lines && !view->line[prev_lineno].data)
4034 prev_lineno++;
4035 while (prev_lineno > 0 && !view->line[prev_lineno].data)
4036 prev_lineno--;
4038 /* If the above fails, always skip the "On branch" line. */
4039 if (prev_lineno < view->lines)
4040 view->lineno = prev_lineno;
4041 else
4042 view->lineno = 1;
4044 if (view->lineno < view->offset)
4045 view->offset = view->lineno;
4046 else if (view->offset + view->height <= view->lineno)
4047 view->offset = view->lineno - view->height + 1;
4049 return TRUE;
4050 }
4052 static bool
4053 status_draw(struct view *view, struct line *line, unsigned int lineno)
4054 {
4055 struct status *status = line->data;
4056 enum line_type type;
4057 char *text;
4059 if (!status) {
4060 switch (line->type) {
4061 case LINE_STAT_STAGED:
4062 type = LINE_STAT_SECTION;
4063 text = "Changes to be committed:";
4064 break;
4066 case LINE_STAT_UNSTAGED:
4067 type = LINE_STAT_SECTION;
4068 text = "Changed but not updated:";
4069 break;
4071 case LINE_STAT_UNTRACKED:
4072 type = LINE_STAT_SECTION;
4073 text = "Untracked files:";
4074 break;
4076 case LINE_STAT_NONE:
4077 type = LINE_DEFAULT;
4078 text = " (no files)";
4079 break;
4081 case LINE_STAT_HEAD:
4082 type = LINE_STAT_HEAD;
4083 text = status_onbranch;
4084 break;
4086 default:
4087 return FALSE;
4088 }
4089 } else {
4090 static char buf[] = { '?', ' ', ' ', ' ', 0 };
4092 buf[0] = status->status;
4093 if (draw_text(view, line->type, buf, TRUE))
4094 return TRUE;
4095 type = LINE_DEFAULT;
4096 text = status->new.name;
4097 }
4099 draw_text(view, type, text, TRUE);
4100 return TRUE;
4101 }
4103 static enum request
4104 status_enter(struct view *view, struct line *line)
4105 {
4106 struct status *status = line->data;
4107 char oldpath[SIZEOF_STR] = "";
4108 char newpath[SIZEOF_STR] = "";
4109 char *info;
4110 size_t cmdsize = 0;
4111 enum open_flags split;
4113 if (line->type == LINE_STAT_NONE ||
4114 (!status && line[1].type == LINE_STAT_NONE)) {
4115 report("No file to diff");
4116 return REQ_NONE;
4117 }
4119 if (status) {
4120 if (sq_quote(oldpath, 0, status->old.name) >= sizeof(oldpath))
4121 return REQ_QUIT;
4122 /* Diffs for unmerged entries are empty when pasing the
4123 * new path, so leave it empty. */
4124 if (status->status != 'U' &&
4125 sq_quote(newpath, 0, status->new.name) >= sizeof(newpath))
4126 return REQ_QUIT;
4127 }
4129 if (opt_cdup[0] &&
4130 line->type != LINE_STAT_UNTRACKED &&
4131 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
4132 return REQ_QUIT;
4134 switch (line->type) {
4135 case LINE_STAT_STAGED:
4136 if (opt_no_head) {
4137 if (!string_format_from(opt_cmd, &cmdsize,
4138 STATUS_DIFF_NO_HEAD_SHOW_CMD,
4139 newpath))
4140 return REQ_QUIT;
4141 } else {
4142 if (!string_format_from(opt_cmd, &cmdsize,
4143 STATUS_DIFF_INDEX_SHOW_CMD,
4144 oldpath, newpath))
4145 return REQ_QUIT;
4146 }
4148 if (status)
4149 info = "Staged changes to %s";
4150 else
4151 info = "Staged changes";
4152 break;
4154 case LINE_STAT_UNSTAGED:
4155 if (!string_format_from(opt_cmd, &cmdsize,
4156 STATUS_DIFF_FILES_SHOW_CMD, oldpath, newpath))
4157 return REQ_QUIT;
4158 if (status)
4159 info = "Unstaged changes to %s";
4160 else
4161 info = "Unstaged changes";
4162 break;
4164 case LINE_STAT_UNTRACKED:
4165 if (opt_pipe)
4166 return REQ_QUIT;
4168 if (!status) {
4169 report("No file to show");
4170 return REQ_NONE;
4171 }
4173 opt_pipe = fopen(status->new.name, "r");
4174 info = "Untracked file %s";
4175 break;
4177 case LINE_STAT_HEAD:
4178 return REQ_NONE;
4180 default:
4181 die("line type %d not handled in switch", line->type);
4182 }
4184 split = view_is_displayed(view) ? OPEN_SPLIT : 0;
4185 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | split);
4186 if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) {
4187 if (status) {
4188 stage_status = *status;
4189 } else {
4190 memset(&stage_status, 0, sizeof(stage_status));
4191 }
4193 stage_line_type = line->type;
4194 stage_chunks = 0;
4195 string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.new.name);
4196 }
4198 return REQ_NONE;
4199 }
4201 static bool
4202 status_exists(struct status *status, enum line_type type)
4203 {
4204 struct view *view = VIEW(REQ_VIEW_STATUS);
4205 struct line *line;
4207 for (line = view->line; line < view->line + view->lines; line++) {
4208 struct status *pos = line->data;
4210 if (line->type == type && pos &&
4211 !strcmp(status->new.name, pos->new.name))
4212 return TRUE;
4213 }
4215 return FALSE;
4216 }
4219 static FILE *
4220 status_update_prepare(enum line_type type)
4221 {
4222 char cmd[SIZEOF_STR];
4223 size_t cmdsize = 0;
4225 if (opt_cdup[0] &&
4226 type != LINE_STAT_UNTRACKED &&
4227 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4228 return NULL;
4230 switch (type) {
4231 case LINE_STAT_STAGED:
4232 string_add(cmd, cmdsize, "git update-index -z --index-info");
4233 break;
4235 case LINE_STAT_UNSTAGED:
4236 case LINE_STAT_UNTRACKED:
4237 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
4238 break;
4240 default:
4241 die("line type %d not handled in switch", type);
4242 }
4244 return popen(cmd, "w");
4245 }
4247 static bool
4248 status_update_write(FILE *pipe, struct status *status, enum line_type type)
4249 {
4250 char buf[SIZEOF_STR];
4251 size_t bufsize = 0;
4252 size_t written = 0;
4254 switch (type) {
4255 case LINE_STAT_STAGED:
4256 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
4257 status->old.mode,
4258 status->old.rev,
4259 status->old.name, 0))
4260 return FALSE;
4261 break;
4263 case LINE_STAT_UNSTAGED:
4264 case LINE_STAT_UNTRACKED:
4265 if (!string_format_from(buf, &bufsize, "%s%c", status->new.name, 0))
4266 return FALSE;
4267 break;
4269 default:
4270 die("line type %d not handled in switch", type);
4271 }
4273 while (!ferror(pipe) && written < bufsize) {
4274 written += fwrite(buf + written, 1, bufsize - written, pipe);
4275 }
4277 return written == bufsize;
4278 }
4280 static bool
4281 status_update_file(struct status *status, enum line_type type)
4282 {
4283 FILE *pipe = status_update_prepare(type);
4284 bool result;
4286 if (!pipe)
4287 return FALSE;
4289 result = status_update_write(pipe, status, type);
4290 pclose(pipe);
4291 return result;
4292 }
4294 static bool
4295 status_update_files(struct view *view, struct line *line)
4296 {
4297 FILE *pipe = status_update_prepare(line->type);
4298 bool result = TRUE;
4299 struct line *pos = view->line + view->lines;
4300 int files = 0;
4301 int file, done;
4303 if (!pipe)
4304 return FALSE;
4306 for (pos = line; pos < view->line + view->lines && pos->data; pos++)
4307 files++;
4309 for (file = 0, done = 0; result && file < files; line++, file++) {
4310 int almost_done = file * 100 / files;
4312 if (almost_done > done) {
4313 done = almost_done;
4314 string_format(view->ref, "updating file %u of %u (%d%% done)",
4315 file, files, done);
4316 update_view_title(view);
4317 }
4318 result = status_update_write(pipe, line->data, line->type);
4319 }
4321 pclose(pipe);
4322 return result;
4323 }
4325 static bool
4326 status_update(struct view *view)
4327 {
4328 struct line *line = &view->line[view->lineno];
4330 assert(view->lines);
4332 if (!line->data) {
4333 /* This should work even for the "On branch" line. */
4334 if (line < view->line + view->lines && !line[1].data) {
4335 report("Nothing to update");
4336 return FALSE;
4337 }
4339 if (!status_update_files(view, line + 1)) {
4340 report("Failed to update file status");
4341 return FALSE;
4342 }
4344 } else if (!status_update_file(line->data, line->type)) {
4345 report("Failed to update file status");
4346 return FALSE;
4347 }
4349 return TRUE;
4350 }
4352 static enum request
4353 status_request(struct view *view, enum request request, struct line *line)
4354 {
4355 struct status *status = line->data;
4357 switch (request) {
4358 case REQ_STATUS_UPDATE:
4359 if (!status_update(view))
4360 return REQ_NONE;
4361 break;
4363 case REQ_STATUS_MERGE:
4364 if (!status || status->status != 'U') {
4365 report("Merging only possible for files with unmerged status ('U').");
4366 return REQ_NONE;
4367 }
4368 open_mergetool(status->new.name);
4369 break;
4371 case REQ_EDIT:
4372 if (!status)
4373 return request;
4375 open_editor(status->status != '?', status->new.name);
4376 break;
4378 case REQ_VIEW_BLAME:
4379 if (status) {
4380 string_copy(opt_file, status->new.name);
4381 opt_ref[0] = 0;
4382 }
4383 return request;
4385 case REQ_ENTER:
4386 /* After returning the status view has been split to
4387 * show the stage view. No further reloading is
4388 * necessary. */
4389 status_enter(view, line);
4390 return REQ_NONE;
4392 case REQ_REFRESH:
4393 /* Simply reload the view. */
4394 break;
4396 default:
4397 return request;
4398 }
4400 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
4402 return REQ_NONE;
4403 }
4405 static void
4406 status_select(struct view *view, struct line *line)
4407 {
4408 struct status *status = line->data;
4409 char file[SIZEOF_STR] = "all files";
4410 char *text;
4411 char *key;
4413 if (status && !string_format(file, "'%s'", status->new.name))
4414 return;
4416 if (!status && line[1].type == LINE_STAT_NONE)
4417 line++;
4419 switch (line->type) {
4420 case LINE_STAT_STAGED:
4421 text = "Press %s to unstage %s for commit";
4422 break;
4424 case LINE_STAT_UNSTAGED:
4425 text = "Press %s to stage %s for commit";
4426 break;
4428 case LINE_STAT_UNTRACKED:
4429 text = "Press %s to stage %s for addition";
4430 break;
4432 case LINE_STAT_HEAD:
4433 case LINE_STAT_NONE:
4434 text = "Nothing to update";
4435 break;
4437 default:
4438 die("line type %d not handled in switch", line->type);
4439 }
4441 if (status && status->status == 'U') {
4442 text = "Press %s to resolve conflict in %s";
4443 key = get_key(REQ_STATUS_MERGE);
4445 } else {
4446 key = get_key(REQ_STATUS_UPDATE);
4447 }
4449 string_format(view->ref, text, key, file);
4450 }
4452 static bool
4453 status_grep(struct view *view, struct line *line)
4454 {
4455 struct status *status = line->data;
4456 enum { S_STATUS, S_NAME, S_END } state;
4457 char buf[2] = "?";
4458 regmatch_t pmatch;
4460 if (!status)
4461 return FALSE;
4463 for (state = S_STATUS; state < S_END; state++) {
4464 char *text;
4466 switch (state) {
4467 case S_NAME: text = status->new.name; break;
4468 case S_STATUS:
4469 buf[0] = status->status;
4470 text = buf;
4471 break;
4473 default:
4474 return FALSE;
4475 }
4477 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4478 return TRUE;
4479 }
4481 return FALSE;
4482 }
4484 static struct view_ops status_ops = {
4485 "file",
4486 status_open,
4487 NULL,
4488 status_draw,
4489 status_request,
4490 status_grep,
4491 status_select,
4492 };
4495 static bool
4496 stage_diff_line(FILE *pipe, struct line *line)
4497 {
4498 char *buf = line->data;
4499 size_t bufsize = strlen(buf);
4500 size_t written = 0;
4502 while (!ferror(pipe) && written < bufsize) {
4503 written += fwrite(buf + written, 1, bufsize - written, pipe);
4504 }
4506 fputc('\n', pipe);
4508 return written == bufsize;
4509 }
4511 static bool
4512 stage_diff_write(FILE *pipe, struct line *line, struct line *end)
4513 {
4514 while (line < end) {
4515 if (!stage_diff_line(pipe, line++))
4516 return FALSE;
4517 if (line->type == LINE_DIFF_CHUNK ||
4518 line->type == LINE_DIFF_HEADER)
4519 break;
4520 }
4522 return TRUE;
4523 }
4525 static struct line *
4526 stage_diff_find(struct view *view, struct line *line, enum line_type type)
4527 {
4528 for (; view->line < line; line--)
4529 if (line->type == type)
4530 return line;
4532 return NULL;
4533 }
4535 static bool
4536 stage_update_chunk(struct view *view, struct line *chunk)
4537 {
4538 char cmd[SIZEOF_STR];
4539 size_t cmdsize = 0;
4540 struct line *diff_hdr;
4541 FILE *pipe;
4543 diff_hdr = stage_diff_find(view, chunk, LINE_DIFF_HEADER);
4544 if (!diff_hdr)
4545 return FALSE;
4547 if (opt_cdup[0] &&
4548 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4549 return FALSE;
4551 if (!string_format_from(cmd, &cmdsize,
4552 "git apply --whitespace=nowarn --cached %s - && "
4553 "git update-index -q --unmerged --refresh 2>/dev/null",
4554 stage_line_type == LINE_STAT_STAGED ? "-R" : ""))
4555 return FALSE;
4557 pipe = popen(cmd, "w");
4558 if (!pipe)
4559 return FALSE;
4561 if (!stage_diff_write(pipe, diff_hdr, chunk) ||
4562 !stage_diff_write(pipe, chunk, view->line + view->lines))
4563 chunk = NULL;
4565 pclose(pipe);
4567 return chunk ? TRUE : FALSE;
4568 }
4570 static bool
4571 stage_update(struct view *view, struct line *line)
4572 {
4573 struct line *chunk = NULL;
4575 if (!opt_no_head && stage_line_type != LINE_STAT_UNTRACKED)
4576 chunk = stage_diff_find(view, line, LINE_DIFF_CHUNK);
4578 if (chunk) {
4579 if (!stage_update_chunk(view, chunk)) {
4580 report("Failed to apply chunk");
4581 return FALSE;
4582 }
4584 } else if (!stage_status.status) {
4585 view = VIEW(REQ_VIEW_STATUS);
4587 for (line = view->line; line < view->line + view->lines; line++)
4588 if (line->type == stage_line_type)
4589 break;
4591 if (!status_update_files(view, line + 1)) {
4592 report("Failed to update files");
4593 return FALSE;
4594 }
4596 } else if (!status_update_file(&stage_status, stage_line_type)) {
4597 report("Failed to update file");
4598 return FALSE;
4599 }
4601 return TRUE;
4602 }
4604 static void
4605 stage_next(struct view *view, struct line *line)
4606 {
4607 int i;
4609 if (!stage_chunks) {
4610 static size_t alloc = 0;
4611 int *tmp;
4613 for (line = view->line; line < view->line + view->lines; line++) {
4614 if (line->type != LINE_DIFF_CHUNK)
4615 continue;
4617 tmp = realloc_items(stage_chunk, &alloc,
4618 stage_chunks, sizeof(*tmp));
4619 if (!tmp) {
4620 report("Allocation failure");
4621 return;
4622 }
4624 stage_chunk = tmp;
4625 stage_chunk[stage_chunks++] = line - view->line;
4626 }
4627 }
4629 for (i = 0; i < stage_chunks; i++) {
4630 if (stage_chunk[i] > view->lineno) {
4631 do_scroll_view(view, stage_chunk[i] - view->lineno);
4632 report("Chunk %d of %d", i + 1, stage_chunks);
4633 return;
4634 }
4635 }
4637 report("No next chunk found");
4638 }
4640 static enum request
4641 stage_request(struct view *view, enum request request, struct line *line)
4642 {
4643 switch (request) {
4644 case REQ_STATUS_UPDATE:
4645 if (!stage_update(view, line))
4646 return REQ_NONE;
4647 break;
4649 case REQ_STAGE_NEXT:
4650 if (stage_line_type == LINE_STAT_UNTRACKED) {
4651 report("File is untracked; press %s to add",
4652 get_key(REQ_STATUS_UPDATE));
4653 return REQ_NONE;
4654 }
4655 stage_next(view, line);
4656 return REQ_NONE;
4658 case REQ_EDIT:
4659 if (!stage_status.new.name[0])
4660 return request;
4662 open_editor(stage_status.status != '?', stage_status.new.name);
4663 break;
4665 case REQ_REFRESH:
4666 /* Reload everything ... */
4667 break;
4669 case REQ_VIEW_BLAME:
4670 if (stage_status.new.name[0]) {
4671 string_copy(opt_file, stage_status.new.name);
4672 opt_ref[0] = 0;
4673 }
4674 return request;
4676 case REQ_ENTER:
4677 return pager_request(view, request, line);
4679 default:
4680 return request;
4681 }
4683 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD | OPEN_NOMAXIMIZE);
4685 /* Check whether the staged entry still exists, and close the
4686 * stage view if it doesn't. */
4687 if (!status_exists(&stage_status, stage_line_type))
4688 return REQ_VIEW_CLOSE;
4690 if (stage_line_type == LINE_STAT_UNTRACKED)
4691 opt_pipe = fopen(stage_status.new.name, "r");
4692 else
4693 string_copy(opt_cmd, view->cmd);
4694 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | OPEN_NOMAXIMIZE);
4696 return REQ_NONE;
4697 }
4699 static struct view_ops stage_ops = {
4700 "line",
4701 NULL,
4702 pager_read,
4703 pager_draw,
4704 stage_request,
4705 pager_grep,
4706 pager_select,
4707 };
4710 /*
4711 * Revision graph
4712 */
4714 struct commit {
4715 char id[SIZEOF_REV]; /* SHA1 ID. */
4716 char title[128]; /* First line of the commit message. */
4717 char author[75]; /* Author of the commit. */
4718 struct tm time; /* Date from the author ident. */
4719 struct ref **refs; /* Repository references. */
4720 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
4721 size_t graph_size; /* The width of the graph array. */
4722 bool has_parents; /* Rewritten --parents seen. */
4723 };
4725 /* Size of rev graph with no "padding" columns */
4726 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
4728 struct rev_graph {
4729 struct rev_graph *prev, *next, *parents;
4730 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
4731 size_t size;
4732 struct commit *commit;
4733 size_t pos;
4734 unsigned int boundary:1;
4735 };
4737 /* Parents of the commit being visualized. */
4738 static struct rev_graph graph_parents[4];
4740 /* The current stack of revisions on the graph. */
4741 static struct rev_graph graph_stacks[4] = {
4742 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
4743 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
4744 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
4745 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
4746 };
4748 static inline bool
4749 graph_parent_is_merge(struct rev_graph *graph)
4750 {
4751 return graph->parents->size > 1;
4752 }
4754 static inline void
4755 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
4756 {
4757 struct commit *commit = graph->commit;
4759 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
4760 commit->graph[commit->graph_size++] = symbol;
4761 }
4763 static void
4764 done_rev_graph(struct rev_graph *graph)
4765 {
4766 if (graph_parent_is_merge(graph) &&
4767 graph->pos < graph->size - 1 &&
4768 graph->next->size == graph->size + graph->parents->size - 1) {
4769 size_t i = graph->pos + graph->parents->size - 1;
4771 graph->commit->graph_size = i * 2;
4772 while (i < graph->next->size - 1) {
4773 append_to_rev_graph(graph, ' ');
4774 append_to_rev_graph(graph, '\\');
4775 i++;
4776 }
4777 }
4779 graph->size = graph->pos = 0;
4780 graph->commit = NULL;
4781 memset(graph->parents, 0, sizeof(*graph->parents));
4782 }
4784 static void
4785 push_rev_graph(struct rev_graph *graph, char *parent)
4786 {
4787 int i;
4789 /* "Collapse" duplicate parents lines.
4790 *
4791 * FIXME: This needs to also update update the drawn graph but
4792 * for now it just serves as a method for pruning graph lines. */
4793 for (i = 0; i < graph->size; i++)
4794 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
4795 return;
4797 if (graph->size < SIZEOF_REVITEMS) {
4798 string_copy_rev(graph->rev[graph->size++], parent);
4799 }
4800 }
4802 static chtype
4803 get_rev_graph_symbol(struct rev_graph *graph)
4804 {
4805 chtype symbol;
4807 if (graph->boundary)
4808 symbol = REVGRAPH_BOUND;
4809 else if (graph->parents->size == 0)
4810 symbol = REVGRAPH_INIT;
4811 else if (graph_parent_is_merge(graph))
4812 symbol = REVGRAPH_MERGE;
4813 else if (graph->pos >= graph->size)
4814 symbol = REVGRAPH_BRANCH;
4815 else
4816 symbol = REVGRAPH_COMMIT;
4818 return symbol;
4819 }
4821 static void
4822 draw_rev_graph(struct rev_graph *graph)
4823 {
4824 struct rev_filler {
4825 chtype separator, line;
4826 };
4827 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
4828 static struct rev_filler fillers[] = {
4829 { ' ', '|' },
4830 { '`', '.' },
4831 { '\'', ' ' },
4832 { '/', ' ' },
4833 };
4834 chtype symbol = get_rev_graph_symbol(graph);
4835 struct rev_filler *filler;
4836 size_t i;
4838 if (opt_line_graphics)
4839 fillers[DEFAULT].line = line_graphics[LINE_GRAPHIC_VLINE];
4841 filler = &fillers[DEFAULT];
4843 for (i = 0; i < graph->pos; i++) {
4844 append_to_rev_graph(graph, filler->line);
4845 if (graph_parent_is_merge(graph->prev) &&
4846 graph->prev->pos == i)
4847 filler = &fillers[RSHARP];
4849 append_to_rev_graph(graph, filler->separator);
4850 }
4852 /* Place the symbol for this revision. */
4853 append_to_rev_graph(graph, symbol);
4855 if (graph->prev->size > graph->size)
4856 filler = &fillers[RDIAG];
4857 else
4858 filler = &fillers[DEFAULT];
4860 i++;
4862 for (; i < graph->size; i++) {
4863 append_to_rev_graph(graph, filler->separator);
4864 append_to_rev_graph(graph, filler->line);
4865 if (graph_parent_is_merge(graph->prev) &&
4866 i < graph->prev->pos + graph->parents->size)
4867 filler = &fillers[RSHARP];
4868 if (graph->prev->size > graph->size)
4869 filler = &fillers[LDIAG];
4870 }
4872 if (graph->prev->size > graph->size) {
4873 append_to_rev_graph(graph, filler->separator);
4874 if (filler->line != ' ')
4875 append_to_rev_graph(graph, filler->line);
4876 }
4877 }
4879 /* Prepare the next rev graph */
4880 static void
4881 prepare_rev_graph(struct rev_graph *graph)
4882 {
4883 size_t i;
4885 /* First, traverse all lines of revisions up to the active one. */
4886 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
4887 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
4888 break;
4890 push_rev_graph(graph->next, graph->rev[graph->pos]);
4891 }
4893 /* Interleave the new revision parent(s). */
4894 for (i = 0; !graph->boundary && i < graph->parents->size; i++)
4895 push_rev_graph(graph->next, graph->parents->rev[i]);
4897 /* Lastly, put any remaining revisions. */
4898 for (i = graph->pos + 1; i < graph->size; i++)
4899 push_rev_graph(graph->next, graph->rev[i]);
4900 }
4902 static void
4903 update_rev_graph(struct rev_graph *graph)
4904 {
4905 /* If this is the finalizing update ... */
4906 if (graph->commit)
4907 prepare_rev_graph(graph);
4909 /* Graph visualization needs a one rev look-ahead,
4910 * so the first update doesn't visualize anything. */
4911 if (!graph->prev->commit)
4912 return;
4914 draw_rev_graph(graph->prev);
4915 done_rev_graph(graph->prev->prev);
4916 }
4919 /*
4920 * Main view backend
4921 */
4923 static bool
4924 main_draw(struct view *view, struct line *line, unsigned int lineno)
4925 {
4926 struct commit *commit = line->data;
4928 if (!*commit->author)
4929 return FALSE;
4931 if (opt_date && draw_date(view, &commit->time))
4932 return TRUE;
4934 if (opt_author &&
4935 draw_field(view, LINE_MAIN_AUTHOR, commit->author, AUTHOR_COLS, TRUE))
4936 return TRUE;
4938 if (opt_rev_graph && commit->graph_size &&
4939 draw_graphic(view, LINE_MAIN_REVGRAPH, commit->graph, commit->graph_size))
4940 return TRUE;
4942 if (opt_show_refs && commit->refs) {
4943 size_t i = 0;
4945 do {
4946 enum line_type type;
4948 if (commit->refs[i]->head)
4949 type = LINE_MAIN_HEAD;
4950 else if (commit->refs[i]->ltag)
4951 type = LINE_MAIN_LOCAL_TAG;
4952 else if (commit->refs[i]->tag)
4953 type = LINE_MAIN_TAG;
4954 else if (commit->refs[i]->tracked)
4955 type = LINE_MAIN_TRACKED;
4956 else if (commit->refs[i]->remote)
4957 type = LINE_MAIN_REMOTE;
4958 else
4959 type = LINE_MAIN_REF;
4961 if (draw_text(view, type, "[", TRUE) ||
4962 draw_text(view, type, commit->refs[i]->name, TRUE) ||
4963 draw_text(view, type, "]", TRUE))
4964 return TRUE;
4966 if (draw_text(view, LINE_DEFAULT, " ", TRUE))
4967 return TRUE;
4968 } while (commit->refs[i++]->next);
4969 }
4971 draw_text(view, LINE_DEFAULT, commit->title, TRUE);
4972 return TRUE;
4973 }
4975 /* Reads git log --pretty=raw output and parses it into the commit struct. */
4976 static bool
4977 main_read(struct view *view, char *line)
4978 {
4979 static struct rev_graph *graph = graph_stacks;
4980 enum line_type type;
4981 struct commit *commit;
4983 if (!line) {
4984 if (!view->lines && !view->parent)
4985 die("No revisions match the given arguments.");
4986 update_rev_graph(graph);
4987 return TRUE;
4988 }
4990 type = get_line_type(line);
4991 if (type == LINE_COMMIT) {
4992 commit = calloc(1, sizeof(struct commit));
4993 if (!commit)
4994 return FALSE;
4996 line += STRING_SIZE("commit ");
4997 if (*line == '-') {
4998 graph->boundary = 1;
4999 line++;
5000 }
5002 string_copy_rev(commit->id, line);
5003 commit->refs = get_refs(commit->id);
5004 graph->commit = commit;
5005 add_line_data(view, commit, LINE_MAIN_COMMIT);
5007 while ((line = strchr(line, ' '))) {
5008 line++;
5009 push_rev_graph(graph->parents, line);
5010 commit->has_parents = TRUE;
5011 }
5012 return TRUE;
5013 }
5015 if (!view->lines)
5016 return TRUE;
5017 commit = view->line[view->lines - 1].data;
5019 switch (type) {
5020 case LINE_PARENT:
5021 if (commit->has_parents)
5022 break;
5023 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
5024 break;
5026 case LINE_AUTHOR:
5027 {
5028 /* Parse author lines where the name may be empty:
5029 * author <email@address.tld> 1138474660 +0100
5030 */
5031 char *ident = line + STRING_SIZE("author ");
5032 char *nameend = strchr(ident, '<');
5033 char *emailend = strchr(ident, '>');
5035 if (!nameend || !emailend)
5036 break;
5038 update_rev_graph(graph);
5039 graph = graph->next;
5041 *nameend = *emailend = 0;
5042 ident = chomp_string(ident);
5043 if (!*ident) {
5044 ident = chomp_string(nameend + 1);
5045 if (!*ident)
5046 ident = "Unknown";
5047 }
5049 string_ncopy(commit->author, ident, strlen(ident));
5051 /* Parse epoch and timezone */
5052 if (emailend[1] == ' ') {
5053 char *secs = emailend + 2;
5054 char *zone = strchr(secs, ' ');
5055 time_t time = (time_t) atol(secs);
5057 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
5058 long tz;
5060 zone++;
5061 tz = ('0' - zone[1]) * 60 * 60 * 10;
5062 tz += ('0' - zone[2]) * 60 * 60;
5063 tz += ('0' - zone[3]) * 60;
5064 tz += ('0' - zone[4]) * 60;
5066 if (zone[0] == '-')
5067 tz = -tz;
5069 time -= tz;
5070 }
5072 gmtime_r(&time, &commit->time);
5073 }
5074 break;
5075 }
5076 default:
5077 /* Fill in the commit title if it has not already been set. */
5078 if (commit->title[0])
5079 break;
5081 /* Require titles to start with a non-space character at the
5082 * offset used by git log. */
5083 if (strncmp(line, " ", 4))
5084 break;
5085 line += 4;
5086 /* Well, if the title starts with a whitespace character,
5087 * try to be forgiving. Otherwise we end up with no title. */
5088 while (isspace(*line))
5089 line++;
5090 if (*line == '\0')
5091 break;
5092 /* FIXME: More graceful handling of titles; append "..." to
5093 * shortened titles, etc. */
5095 string_ncopy(commit->title, line, strlen(line));
5096 }
5098 return TRUE;
5099 }
5101 static enum request
5102 main_request(struct view *view, enum request request, struct line *line)
5103 {
5104 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
5106 if (request == REQ_ENTER)
5107 open_view(view, REQ_VIEW_DIFF, flags);
5108 else
5109 return request;
5111 return REQ_NONE;
5112 }
5114 static bool
5115 grep_refs(struct ref **refs, regex_t *regex)
5116 {
5117 regmatch_t pmatch;
5118 size_t i = 0;
5120 if (!refs)
5121 return FALSE;
5122 do {
5123 if (regexec(regex, refs[i]->name, 1, &pmatch, 0) != REG_NOMATCH)
5124 return TRUE;
5125 } while (refs[i++]->next);
5127 return FALSE;
5128 }
5130 static bool
5131 main_grep(struct view *view, struct line *line)
5132 {
5133 struct commit *commit = line->data;
5134 enum { S_TITLE, S_AUTHOR, S_DATE, S_REFS, S_END } state;
5135 char buf[DATE_COLS + 1];
5136 regmatch_t pmatch;
5138 for (state = S_TITLE; state < S_END; state++) {
5139 char *text;
5141 switch (state) {
5142 case S_TITLE: text = commit->title; break;
5143 case S_AUTHOR:
5144 if (!opt_author)
5145 continue;
5146 text = commit->author;
5147 break;
5148 case S_DATE:
5149 if (!opt_date)
5150 continue;
5151 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
5152 continue;
5153 text = buf;
5154 break;
5155 case S_REFS:
5156 if (!opt_show_refs)
5157 continue;
5158 if (grep_refs(commit->refs, view->regex) == TRUE)
5159 return TRUE;
5160 continue;
5161 default:
5162 return FALSE;
5163 }
5165 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
5166 return TRUE;
5167 }
5169 return FALSE;
5170 }
5172 static void
5173 main_select(struct view *view, struct line *line)
5174 {
5175 struct commit *commit = line->data;
5177 string_copy_rev(view->ref, commit->id);
5178 string_copy_rev(ref_commit, view->ref);
5179 }
5181 static struct view_ops main_ops = {
5182 "commit",
5183 NULL,
5184 main_read,
5185 main_draw,
5186 main_request,
5187 main_grep,
5188 main_select,
5189 };
5192 /*
5193 * Unicode / UTF-8 handling
5194 *
5195 * NOTE: Much of the following code for dealing with unicode is derived from
5196 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
5197 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
5198 */
5200 /* I've (over)annotated a lot of code snippets because I am not entirely
5201 * confident that the approach taken by this small UTF-8 interface is correct.
5202 * --jonas */
5204 static inline int
5205 unicode_width(unsigned long c)
5206 {
5207 if (c >= 0x1100 &&
5208 (c <= 0x115f /* Hangul Jamo */
5209 || c == 0x2329
5210 || c == 0x232a
5211 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
5212 /* CJK ... Yi */
5213 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
5214 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
5215 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
5216 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
5217 || (c >= 0xffe0 && c <= 0xffe6)
5218 || (c >= 0x20000 && c <= 0x2fffd)
5219 || (c >= 0x30000 && c <= 0x3fffd)))
5220 return 2;
5222 if (c == '\t')
5223 return opt_tab_size;
5225 return 1;
5226 }
5228 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
5229 * Illegal bytes are set one. */
5230 static const unsigned char utf8_bytes[256] = {
5231 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,
5232 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,
5233 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,
5234 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,
5235 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,
5236 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,
5237 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,
5238 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,
5239 };
5241 /* Decode UTF-8 multi-byte representation into a unicode character. */
5242 static inline unsigned long
5243 utf8_to_unicode(const char *string, size_t length)
5244 {
5245 unsigned long unicode;
5247 switch (length) {
5248 case 1:
5249 unicode = string[0];
5250 break;
5251 case 2:
5252 unicode = (string[0] & 0x1f) << 6;
5253 unicode += (string[1] & 0x3f);
5254 break;
5255 case 3:
5256 unicode = (string[0] & 0x0f) << 12;
5257 unicode += ((string[1] & 0x3f) << 6);
5258 unicode += (string[2] & 0x3f);
5259 break;
5260 case 4:
5261 unicode = (string[0] & 0x0f) << 18;
5262 unicode += ((string[1] & 0x3f) << 12);
5263 unicode += ((string[2] & 0x3f) << 6);
5264 unicode += (string[3] & 0x3f);
5265 break;
5266 case 5:
5267 unicode = (string[0] & 0x0f) << 24;
5268 unicode += ((string[1] & 0x3f) << 18);
5269 unicode += ((string[2] & 0x3f) << 12);
5270 unicode += ((string[3] & 0x3f) << 6);
5271 unicode += (string[4] & 0x3f);
5272 break;
5273 case 6:
5274 unicode = (string[0] & 0x01) << 30;
5275 unicode += ((string[1] & 0x3f) << 24);
5276 unicode += ((string[2] & 0x3f) << 18);
5277 unicode += ((string[3] & 0x3f) << 12);
5278 unicode += ((string[4] & 0x3f) << 6);
5279 unicode += (string[5] & 0x3f);
5280 break;
5281 default:
5282 die("Invalid unicode length");
5283 }
5285 /* Invalid characters could return the special 0xfffd value but NUL
5286 * should be just as good. */
5287 return unicode > 0xffff ? 0 : unicode;
5288 }
5290 /* Calculates how much of string can be shown within the given maximum width
5291 * and sets trimmed parameter to non-zero value if all of string could not be
5292 * shown. If the reserve flag is TRUE, it will reserve at least one
5293 * trailing character, which can be useful when drawing a delimiter.
5294 *
5295 * Returns the number of bytes to output from string to satisfy max_width. */
5296 static size_t
5297 utf8_length(const char *string, int *width, size_t max_width, int *trimmed, bool reserve)
5298 {
5299 const char *start = string;
5300 const char *end = strchr(string, '\0');
5301 unsigned char last_bytes = 0;
5302 size_t last_ucwidth = 0;
5304 *width = 0;
5305 *trimmed = 0;
5307 while (string < end) {
5308 int c = *(unsigned char *) string;
5309 unsigned char bytes = utf8_bytes[c];
5310 size_t ucwidth;
5311 unsigned long unicode;
5313 if (string + bytes > end)
5314 break;
5316 /* Change representation to figure out whether
5317 * it is a single- or double-width character. */
5319 unicode = utf8_to_unicode(string, bytes);
5320 /* FIXME: Graceful handling of invalid unicode character. */
5321 if (!unicode)
5322 break;
5324 ucwidth = unicode_width(unicode);
5325 *width += ucwidth;
5326 if (*width > max_width) {
5327 *trimmed = 1;
5328 *width -= ucwidth;
5329 if (reserve && *width == max_width) {
5330 string -= last_bytes;
5331 *width -= last_ucwidth;
5332 }
5333 break;
5334 }
5336 string += bytes;
5337 last_bytes = bytes;
5338 last_ucwidth = ucwidth;
5339 }
5341 return string - start;
5342 }
5345 /*
5346 * Status management
5347 */
5349 /* Whether or not the curses interface has been initialized. */
5350 static bool cursed = FALSE;
5352 /* The status window is used for polling keystrokes. */
5353 static WINDOW *status_win;
5355 static bool status_empty = TRUE;
5357 /* Update status and title window. */
5358 static void
5359 report(const char *msg, ...)
5360 {
5361 struct view *view = display[current_view];
5363 if (input_mode)
5364 return;
5366 if (!view) {
5367 char buf[SIZEOF_STR];
5368 va_list args;
5370 va_start(args, msg);
5371 if (vsnprintf(buf, sizeof(buf), msg, args) >= sizeof(buf)) {
5372 buf[sizeof(buf) - 1] = 0;
5373 buf[sizeof(buf) - 2] = '.';
5374 buf[sizeof(buf) - 3] = '.';
5375 buf[sizeof(buf) - 4] = '.';
5376 }
5377 va_end(args);
5378 die("%s", buf);
5379 }
5381 if (!status_empty || *msg) {
5382 va_list args;
5384 va_start(args, msg);
5386 wmove(status_win, 0, 0);
5387 if (*msg) {
5388 vwprintw(status_win, msg, args);
5389 status_empty = FALSE;
5390 } else {
5391 status_empty = TRUE;
5392 }
5393 wclrtoeol(status_win);
5394 wrefresh(status_win);
5396 va_end(args);
5397 }
5399 update_view_title(view);
5400 update_display_cursor(view);
5401 }
5403 /* Controls when nodelay should be in effect when polling user input. */
5404 static void
5405 set_nonblocking_input(bool loading)
5406 {
5407 static unsigned int loading_views;
5409 if ((loading == FALSE && loading_views-- == 1) ||
5410 (loading == TRUE && loading_views++ == 0))
5411 nodelay(status_win, loading);
5412 }
5414 static void
5415 init_display(void)
5416 {
5417 int x, y;
5419 /* Initialize the curses library */
5420 if (isatty(STDIN_FILENO)) {
5421 cursed = !!initscr();
5422 } else {
5423 /* Leave stdin and stdout alone when acting as a pager. */
5424 FILE *io = fopen("/dev/tty", "r+");
5426 if (!io)
5427 die("Failed to open /dev/tty");
5428 cursed = !!newterm(NULL, io, io);
5429 }
5431 if (!cursed)
5432 die("Failed to initialize curses");
5434 nonl(); /* Tell curses not to do NL->CR/NL on output */
5435 cbreak(); /* Take input chars one at a time, no wait for \n */
5436 noecho(); /* Don't echo input */
5437 leaveok(stdscr, TRUE);
5439 if (has_colors())
5440 init_colors();
5442 getmaxyx(stdscr, y, x);
5443 status_win = newwin(1, 0, y - 1, 0);
5444 if (!status_win)
5445 die("Failed to create status window");
5447 /* Enable keyboard mapping */
5448 keypad(status_win, TRUE);
5449 wbkgdset(status_win, get_line_attr(LINE_STATUS));
5451 TABSIZE = opt_tab_size;
5452 if (opt_line_graphics) {
5453 line_graphics[LINE_GRAPHIC_VLINE] = ACS_VLINE;
5454 }
5455 }
5457 static char *
5458 read_prompt(const char *prompt)
5459 {
5460 enum { READING, STOP, CANCEL } status = READING;
5461 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
5462 int pos = 0;
5464 while (status == READING) {
5465 struct view *view;
5466 int i, key;
5468 input_mode = TRUE;
5470 foreach_view (view, i)
5471 update_view(view);
5473 input_mode = FALSE;
5475 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
5476 wclrtoeol(status_win);
5478 /* Refresh, accept single keystroke of input */
5479 key = wgetch(status_win);
5480 switch (key) {
5481 case KEY_RETURN:
5482 case KEY_ENTER:
5483 case '\n':
5484 status = pos ? STOP : CANCEL;
5485 break;
5487 case KEY_BACKSPACE:
5488 if (pos > 0)
5489 pos--;
5490 else
5491 status = CANCEL;
5492 break;
5494 case KEY_ESC:
5495 status = CANCEL;
5496 break;
5498 case ERR:
5499 break;
5501 default:
5502 if (pos >= sizeof(buf)) {
5503 report("Input string too long");
5504 return NULL;
5505 }
5507 if (isprint(key))
5508 buf[pos++] = (char) key;
5509 }
5510 }
5512 /* Clear the status window */
5513 status_empty = FALSE;
5514 report("");
5516 if (status == CANCEL)
5517 return NULL;
5519 buf[pos++] = 0;
5521 return buf;
5522 }
5524 /*
5525 * Repository references
5526 */
5528 static struct ref *refs = NULL;
5529 static size_t refs_alloc = 0;
5530 static size_t refs_size = 0;
5532 /* Id <-> ref store */
5533 static struct ref ***id_refs = NULL;
5534 static size_t id_refs_alloc = 0;
5535 static size_t id_refs_size = 0;
5537 static struct ref **
5538 get_refs(char *id)
5539 {
5540 struct ref ***tmp_id_refs;
5541 struct ref **ref_list = NULL;
5542 size_t ref_list_alloc = 0;
5543 size_t ref_list_size = 0;
5544 size_t i;
5546 for (i = 0; i < id_refs_size; i++)
5547 if (!strcmp(id, id_refs[i][0]->id))
5548 return id_refs[i];
5550 tmp_id_refs = realloc_items(id_refs, &id_refs_alloc, id_refs_size + 1,
5551 sizeof(*id_refs));
5552 if (!tmp_id_refs)
5553 return NULL;
5555 id_refs = tmp_id_refs;
5557 for (i = 0; i < refs_size; i++) {
5558 struct ref **tmp;
5560 if (strcmp(id, refs[i].id))
5561 continue;
5563 tmp = realloc_items(ref_list, &ref_list_alloc,
5564 ref_list_size + 1, sizeof(*ref_list));
5565 if (!tmp) {
5566 if (ref_list)
5567 free(ref_list);
5568 return NULL;
5569 }
5571 ref_list = tmp;
5572 if (ref_list_size > 0)
5573 ref_list[ref_list_size - 1]->next = 1;
5574 ref_list[ref_list_size] = &refs[i];
5576 /* XXX: The properties of the commit chains ensures that we can
5577 * safely modify the shared ref. The repo references will
5578 * always be similar for the same id. */
5579 ref_list[ref_list_size]->next = 0;
5580 ref_list_size++;
5581 }
5583 if (ref_list)
5584 id_refs[id_refs_size++] = ref_list;
5586 return ref_list;
5587 }
5589 static int
5590 read_ref(char *id, size_t idlen, char *name, size_t namelen)
5591 {
5592 struct ref *ref;
5593 bool tag = FALSE;
5594 bool ltag = FALSE;
5595 bool remote = FALSE;
5596 bool tracked = FALSE;
5597 bool check_replace = FALSE;
5598 bool head = FALSE;
5600 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
5601 if (!strcmp(name + namelen - 3, "^{}")) {
5602 namelen -= 3;
5603 name[namelen] = 0;
5604 if (refs_size > 0 && refs[refs_size - 1].ltag == TRUE)
5605 check_replace = TRUE;
5606 } else {
5607 ltag = TRUE;
5608 }
5610 tag = TRUE;
5611 namelen -= STRING_SIZE("refs/tags/");
5612 name += STRING_SIZE("refs/tags/");
5614 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
5615 remote = TRUE;
5616 namelen -= STRING_SIZE("refs/remotes/");
5617 name += STRING_SIZE("refs/remotes/");
5618 tracked = !strcmp(opt_remote, name);
5620 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5621 namelen -= STRING_SIZE("refs/heads/");
5622 name += STRING_SIZE("refs/heads/");
5623 head = !strncmp(opt_head, name, namelen);
5625 } else if (!strcmp(name, "HEAD")) {
5626 opt_no_head = FALSE;
5627 return OK;
5628 }
5630 if (check_replace && !strcmp(name, refs[refs_size - 1].name)) {
5631 /* it's an annotated tag, replace the previous sha1 with the
5632 * resolved commit id; relies on the fact git-ls-remote lists
5633 * the commit id of an annotated tag right beofre the commit id
5634 * it points to. */
5635 refs[refs_size - 1].ltag = ltag;
5636 string_copy_rev(refs[refs_size - 1].id, id);
5638 return OK;
5639 }
5640 refs = realloc_items(refs, &refs_alloc, refs_size + 1, sizeof(*refs));
5641 if (!refs)
5642 return ERR;
5644 ref = &refs[refs_size++];
5645 ref->name = malloc(namelen + 1);
5646 if (!ref->name)
5647 return ERR;
5649 strncpy(ref->name, name, namelen);
5650 ref->name[namelen] = 0;
5651 ref->head = head;
5652 ref->tag = tag;
5653 ref->ltag = ltag;
5654 ref->remote = remote;
5655 ref->tracked = tracked;
5656 string_copy_rev(ref->id, id);
5658 return OK;
5659 }
5661 static int
5662 load_refs(void)
5663 {
5664 const char *cmd_env = getenv("TIG_LS_REMOTE");
5665 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
5667 return read_properties(popen(cmd, "r"), "\t", read_ref);
5668 }
5670 static int
5671 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
5672 {
5673 if (!strcmp(name, "i18n.commitencoding"))
5674 string_ncopy(opt_encoding, value, valuelen);
5676 if (!strcmp(name, "core.editor"))
5677 string_ncopy(opt_editor, value, valuelen);
5679 /* branch.<head>.remote */
5680 if (*opt_head &&
5681 !strncmp(name, "branch.", 7) &&
5682 !strncmp(name + 7, opt_head, strlen(opt_head)) &&
5683 !strcmp(name + 7 + strlen(opt_head), ".remote"))
5684 string_ncopy(opt_remote, value, valuelen);
5686 if (*opt_head && *opt_remote &&
5687 !strncmp(name, "branch.", 7) &&
5688 !strncmp(name + 7, opt_head, strlen(opt_head)) &&
5689 !strcmp(name + 7 + strlen(opt_head), ".merge")) {
5690 size_t from = strlen(opt_remote);
5692 if (!strncmp(value, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5693 value += STRING_SIZE("refs/heads/");
5694 valuelen -= STRING_SIZE("refs/heads/");
5695 }
5697 if (!string_format_from(opt_remote, &from, "/%s", value))
5698 opt_remote[0] = 0;
5699 }
5701 return OK;
5702 }
5704 static int
5705 load_git_config(void)
5706 {
5707 return read_properties(popen(GIT_CONFIG " --list", "r"),
5708 "=", read_repo_config_option);
5709 }
5711 static int
5712 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
5713 {
5714 if (!opt_git_dir[0]) {
5715 string_ncopy(opt_git_dir, name, namelen);
5717 } else if (opt_is_inside_work_tree == -1) {
5718 /* This can be 3 different values depending on the
5719 * version of git being used. If git-rev-parse does not
5720 * understand --is-inside-work-tree it will simply echo
5721 * the option else either "true" or "false" is printed.
5722 * Default to true for the unknown case. */
5723 opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
5725 } else if (opt_cdup[0] == ' ') {
5726 string_ncopy(opt_cdup, name, namelen);
5727 } else {
5728 if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5729 namelen -= STRING_SIZE("refs/heads/");
5730 name += STRING_SIZE("refs/heads/");
5731 string_ncopy(opt_head, name, namelen);
5732 }
5733 }
5735 return OK;
5736 }
5738 static int
5739 load_repo_info(void)
5740 {
5741 int result;
5742 FILE *pipe = popen("(git rev-parse --git-dir --is-inside-work-tree "
5743 " --show-cdup; git symbolic-ref HEAD) 2>/dev/null", "r");
5745 /* XXX: The line outputted by "--show-cdup" can be empty so
5746 * initialize it to something invalid to make it possible to
5747 * detect whether it has been set or not. */
5748 opt_cdup[0] = ' ';
5750 result = read_properties(pipe, "=", read_repo_info);
5751 if (opt_cdup[0] == ' ')
5752 opt_cdup[0] = 0;
5754 return result;
5755 }
5757 static int
5758 read_properties(FILE *pipe, const char *separators,
5759 int (*read_property)(char *, size_t, char *, size_t))
5760 {
5761 char buffer[BUFSIZ];
5762 char *name;
5763 int state = OK;
5765 if (!pipe)
5766 return ERR;
5768 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
5769 char *value;
5770 size_t namelen;
5771 size_t valuelen;
5773 name = chomp_string(name);
5774 namelen = strcspn(name, separators);
5776 if (name[namelen]) {
5777 name[namelen] = 0;
5778 value = chomp_string(name + namelen + 1);
5779 valuelen = strlen(value);
5781 } else {
5782 value = "";
5783 valuelen = 0;
5784 }
5786 state = read_property(name, namelen, value, valuelen);
5787 }
5789 if (state != ERR && ferror(pipe))
5790 state = ERR;
5792 pclose(pipe);
5794 return state;
5795 }
5798 /*
5799 * Main
5800 */
5802 static void __NORETURN
5803 quit(int sig)
5804 {
5805 /* XXX: Restore tty modes and let the OS cleanup the rest! */
5806 if (cursed)
5807 endwin();
5808 exit(0);
5809 }
5811 static void __NORETURN
5812 die(const char *err, ...)
5813 {
5814 va_list args;
5816 endwin();
5818 va_start(args, err);
5819 fputs("tig: ", stderr);
5820 vfprintf(stderr, err, args);
5821 fputs("\n", stderr);
5822 va_end(args);
5824 exit(1);
5825 }
5827 static void
5828 warn(const char *msg, ...)
5829 {
5830 va_list args;
5832 va_start(args, msg);
5833 fputs("tig warning: ", stderr);
5834 vfprintf(stderr, msg, args);
5835 fputs("\n", stderr);
5836 va_end(args);
5837 }
5839 int
5840 main(int argc, char *argv[])
5841 {
5842 struct view *view;
5843 enum request request;
5844 size_t i;
5846 signal(SIGINT, quit);
5848 if (setlocale(LC_ALL, "")) {
5849 char *codeset = nl_langinfo(CODESET);
5851 string_ncopy(opt_codeset, codeset, strlen(codeset));
5852 }
5854 if (load_repo_info() == ERR)
5855 die("Failed to load repo info.");
5857 if (load_options() == ERR)
5858 die("Failed to load user config.");
5860 if (load_git_config() == ERR)
5861 die("Failed to load repo config.");
5863 if (!parse_options(argc, argv))
5864 return 0;
5866 /* Require a git repository unless when running in pager mode. */
5867 if (!opt_git_dir[0] && opt_request != REQ_VIEW_PAGER)
5868 die("Not a git repository");
5870 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
5871 opt_utf8 = FALSE;
5873 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
5874 opt_iconv = iconv_open(opt_codeset, opt_encoding);
5875 if (opt_iconv == ICONV_NONE)
5876 die("Failed to initialize character set conversion");
5877 }
5879 if (*opt_git_dir && load_refs() == ERR)
5880 die("Failed to load refs.");
5882 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
5883 view->cmd_env = getenv(view->cmd_env);
5885 request = opt_request;
5887 init_display();
5889 while (view_driver(display[current_view], request)) {
5890 int key;
5891 int i;
5893 foreach_view (view, i)
5894 update_view(view);
5896 /* Refresh, accept single keystroke of input */
5897 key = wgetch(status_win);
5899 /* wgetch() with nodelay() enabled returns ERR when there's no
5900 * input. */
5901 if (key == ERR) {
5902 request = REQ_NONE;
5903 continue;
5904 }
5906 request = get_keybinding(display[current_view]->keymap, key);
5908 /* Some low-level request handling. This keeps access to
5909 * status_win restricted. */
5910 switch (request) {
5911 case REQ_PROMPT:
5912 {
5913 char *cmd = read_prompt(":");
5915 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
5916 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
5917 opt_request = REQ_VIEW_DIFF;
5918 } else {
5919 opt_request = REQ_VIEW_PAGER;
5920 }
5921 break;
5922 }
5924 request = REQ_NONE;
5925 break;
5926 }
5927 case REQ_SEARCH:
5928 case REQ_SEARCH_BACK:
5929 {
5930 const char *prompt = request == REQ_SEARCH
5931 ? "/" : "?";
5932 char *search = read_prompt(prompt);
5934 if (search)
5935 string_ncopy(opt_search, search, strlen(search));
5936 else
5937 request = REQ_NONE;
5938 break;
5939 }
5940 case REQ_SCREEN_RESIZE:
5941 {
5942 int height, width;
5944 getmaxyx(stdscr, height, width);
5946 /* Resize the status view and let the view driver take
5947 * care of resizing the displayed views. */
5948 wresize(status_win, 1, width);
5949 mvwin(status_win, height - 1, 0);
5950 wrefresh(status_win);
5951 break;
5952 }
5953 default:
5954 break;
5955 }
5956 }
5958 quit(0);
5960 return 0;
5961 }