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);
70 static bool prompt_yesno(const char *prompt);
71 static int load_refs(void);
73 #define ABS(x) ((x) >= 0 ? (x) : -(x))
74 #define MIN(x, y) ((x) < (y) ? (x) : (y))
76 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
77 #define STRING_SIZE(x) (sizeof(x) - 1)
79 #define SIZEOF_STR 1024 /* Default string size. */
80 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
81 #define SIZEOF_REV 41 /* Holds a SHA-1 and an ending NUL. */
82 #define SIZEOF_ARG 32 /* Default argument array size. */
84 /* Revision graph */
86 #define REVGRAPH_INIT 'I'
87 #define REVGRAPH_MERGE 'M'
88 #define REVGRAPH_BRANCH '+'
89 #define REVGRAPH_COMMIT '*'
90 #define REVGRAPH_BOUND '^'
92 #define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
94 /* This color name can be used to refer to the default term colors. */
95 #define COLOR_DEFAULT (-1)
97 #define ICONV_NONE ((iconv_t) -1)
98 #ifndef ICONV_CONST
99 #define ICONV_CONST /* nothing */
100 #endif
102 /* The format and size of the date column in the main view. */
103 #define DATE_FORMAT "%Y-%m-%d %H:%M"
104 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
106 #define AUTHOR_COLS 20
107 #define ID_COLS 8
109 /* The default interval between line numbers. */
110 #define NUMBER_INTERVAL 5
112 #define TAB_SIZE 8
114 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
116 #define NULL_ID "0000000000000000000000000000000000000000"
118 #ifndef GIT_CONFIG
119 #define GIT_CONFIG "config"
120 #endif
122 #define TIG_LS_REMOTE \
123 "git ls-remote . 2>/dev/null"
125 #define TIG_DIFF_CMD \
126 "git show --pretty=fuller --no-color --root --patch-with-stat --find-copies-harder -C %s 2>/dev/null"
128 #define TIG_LOG_CMD \
129 "git log --no-color --cc --stat -n100 %s 2>/dev/null"
131 #define TIG_MAIN_BASE \
132 "git log --no-color --pretty=raw --parents --topo-order"
134 #define TIG_MAIN_CMD \
135 TIG_MAIN_BASE " %s 2>/dev/null"
137 #define TIG_TREE_CMD \
138 "git ls-tree %s %s"
140 #define TIG_BLOB_CMD \
141 "git cat-file blob %s"
143 /* XXX: Needs to be defined to the empty string. */
144 #define TIG_HELP_CMD ""
145 #define TIG_PAGER_CMD ""
146 #define TIG_STATUS_CMD ""
147 #define TIG_STAGE_CMD ""
148 #define TIG_BLAME_CMD ""
150 /* Some ascii-shorthands fitted into the ncurses namespace. */
151 #define KEY_TAB '\t'
152 #define KEY_RETURN '\r'
153 #define KEY_ESC 27
156 struct ref {
157 char *name; /* Ref name; tag or head names are shortened. */
158 char id[SIZEOF_REV]; /* Commit SHA1 ID */
159 unsigned int head:1; /* Is it the current HEAD? */
160 unsigned int tag:1; /* Is it a tag? */
161 unsigned int ltag:1; /* If so, is the tag local? */
162 unsigned int remote:1; /* Is it a remote ref? */
163 unsigned int tracked:1; /* Is it the remote for the current HEAD? */
164 unsigned int next:1; /* For ref lists: are there more refs? */
165 };
167 static struct ref **get_refs(const char *id);
169 struct int_map {
170 const char *name;
171 int namelen;
172 int value;
173 };
175 static int
176 set_from_int_map(struct int_map *map, size_t map_size,
177 int *value, const char *name, int namelen)
178 {
180 int i;
182 for (i = 0; i < map_size; i++)
183 if (namelen == map[i].namelen &&
184 !strncasecmp(name, map[i].name, namelen)) {
185 *value = map[i].value;
186 return OK;
187 }
189 return ERR;
190 }
193 /*
194 * String helpers
195 */
197 static inline void
198 string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
199 {
200 if (srclen > dstlen - 1)
201 srclen = dstlen - 1;
203 strncpy(dst, src, srclen);
204 dst[srclen] = 0;
205 }
207 /* Shorthands for safely copying into a fixed buffer. */
209 #define string_copy(dst, src) \
210 string_ncopy_do(dst, sizeof(dst), src, sizeof(src))
212 #define string_ncopy(dst, src, srclen) \
213 string_ncopy_do(dst, sizeof(dst), src, srclen)
215 #define string_copy_rev(dst, src) \
216 string_ncopy_do(dst, SIZEOF_REV, src, SIZEOF_REV - 1)
218 #define string_add(dst, from, src) \
219 string_ncopy_do(dst + (from), sizeof(dst) - (from), src, sizeof(src))
221 static char *
222 chomp_string(char *name)
223 {
224 int namelen;
226 while (isspace(*name))
227 name++;
229 namelen = strlen(name) - 1;
230 while (namelen > 0 && isspace(name[namelen]))
231 name[namelen--] = 0;
233 return name;
234 }
236 static bool
237 string_nformat(char *buf, size_t bufsize, size_t *bufpos, const char *fmt, ...)
238 {
239 va_list args;
240 size_t pos = bufpos ? *bufpos : 0;
242 va_start(args, fmt);
243 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
244 va_end(args);
246 if (bufpos)
247 *bufpos = pos;
249 return pos >= bufsize ? FALSE : TRUE;
250 }
252 #define string_format(buf, fmt, args...) \
253 string_nformat(buf, sizeof(buf), NULL, fmt, args)
255 #define string_format_from(buf, from, fmt, args...) \
256 string_nformat(buf, sizeof(buf), from, fmt, args)
258 static int
259 string_enum_compare(const char *str1, const char *str2, int len)
260 {
261 size_t i;
263 #define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
265 /* Diff-Header == DIFF_HEADER */
266 for (i = 0; i < len; i++) {
267 if (toupper(str1[i]) == toupper(str2[i]))
268 continue;
270 if (string_enum_sep(str1[i]) &&
271 string_enum_sep(str2[i]))
272 continue;
274 return str1[i] - str2[i];
275 }
277 return 0;
278 }
280 #define prefixcmp(str1, str2) \
281 strncmp(str1, str2, STRING_SIZE(str2))
283 /* Shell quoting
284 *
285 * NOTE: The following is a slightly modified copy of the git project's shell
286 * quoting routines found in the quote.c file.
287 *
288 * Help to copy the thing properly quoted for the shell safety. any single
289 * quote is replaced with '\'', any exclamation point is replaced with '\!',
290 * and the whole thing is enclosed in a
291 *
292 * E.g.
293 * original sq_quote result
294 * name ==> name ==> 'name'
295 * a b ==> a b ==> 'a b'
296 * a'b ==> a'\''b ==> 'a'\''b'
297 * a!b ==> a'\!'b ==> 'a'\!'b'
298 */
300 static size_t
301 sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
302 {
303 char c;
305 #define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0)
307 BUFPUT('\'');
308 while ((c = *src++)) {
309 if (c == '\'' || c == '!') {
310 BUFPUT('\'');
311 BUFPUT('\\');
312 BUFPUT(c);
313 BUFPUT('\'');
314 } else {
315 BUFPUT(c);
316 }
317 }
318 BUFPUT('\'');
320 if (bufsize < SIZEOF_STR)
321 buf[bufsize] = 0;
323 return bufsize;
324 }
327 /*
328 * User requests
329 */
331 #define REQ_INFO \
332 /* XXX: Keep the view request first and in sync with views[]. */ \
333 REQ_GROUP("View switching") \
334 REQ_(VIEW_MAIN, "Show main view"), \
335 REQ_(VIEW_DIFF, "Show diff view"), \
336 REQ_(VIEW_LOG, "Show log view"), \
337 REQ_(VIEW_TREE, "Show tree view"), \
338 REQ_(VIEW_BLOB, "Show blob view"), \
339 REQ_(VIEW_BLAME, "Show blame view"), \
340 REQ_(VIEW_HELP, "Show help page"), \
341 REQ_(VIEW_PAGER, "Show pager view"), \
342 REQ_(VIEW_STATUS, "Show status view"), \
343 REQ_(VIEW_STAGE, "Show stage view"), \
344 \
345 REQ_GROUP("View manipulation") \
346 REQ_(ENTER, "Enter current line and scroll"), \
347 REQ_(NEXT, "Move to next"), \
348 REQ_(PREVIOUS, "Move to previous"), \
349 REQ_(VIEW_NEXT, "Move focus to next view"), \
350 REQ_(REFRESH, "Reload and refresh"), \
351 REQ_(MAXIMIZE, "Maximize the current view"), \
352 REQ_(VIEW_CLOSE, "Close the current view"), \
353 REQ_(QUIT, "Close all views and quit"), \
354 \
355 REQ_GROUP("View specific requests") \
356 REQ_(STATUS_UPDATE, "Update file status"), \
357 REQ_(STATUS_REVERT, "Revert file changes"), \
358 REQ_(STATUS_MERGE, "Merge file using external tool"), \
359 REQ_(STAGE_NEXT, "Find next chunk to stage"), \
360 REQ_(TREE_PARENT, "Switch to parent directory in tree view"), \
361 \
362 REQ_GROUP("Cursor navigation") \
363 REQ_(MOVE_UP, "Move cursor one line up"), \
364 REQ_(MOVE_DOWN, "Move cursor one line down"), \
365 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
366 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
367 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
368 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
369 \
370 REQ_GROUP("Scrolling") \
371 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
372 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
373 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
374 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
375 \
376 REQ_GROUP("Searching") \
377 REQ_(SEARCH, "Search the view"), \
378 REQ_(SEARCH_BACK, "Search backwards in the view"), \
379 REQ_(FIND_NEXT, "Find next search match"), \
380 REQ_(FIND_PREV, "Find previous search match"), \
381 \
382 REQ_GROUP("Option manipulation") \
383 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
384 REQ_(TOGGLE_DATE, "Toggle date display"), \
385 REQ_(TOGGLE_AUTHOR, "Toggle author display"), \
386 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"), \
387 REQ_(TOGGLE_REFS, "Toggle reference display (tags/branches)"), \
388 \
389 REQ_GROUP("Misc") \
390 REQ_(PROMPT, "Bring up the prompt"), \
391 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
392 REQ_(SCREEN_RESIZE, "Resize the screen"), \
393 REQ_(SHOW_VERSION, "Show version information"), \
394 REQ_(STOP_LOADING, "Stop all loading views"), \
395 REQ_(EDIT, "Open in editor"), \
396 REQ_(NONE, "Do nothing")
399 /* User action requests. */
400 enum request {
401 #define REQ_GROUP(help)
402 #define REQ_(req, help) REQ_##req
404 /* Offset all requests to avoid conflicts with ncurses getch values. */
405 REQ_OFFSET = KEY_MAX + 1,
406 REQ_INFO
408 #undef REQ_GROUP
409 #undef REQ_
410 };
412 struct request_info {
413 enum request request;
414 const char *name;
415 int namelen;
416 const char *help;
417 };
419 static struct request_info req_info[] = {
420 #define REQ_GROUP(help) { 0, NULL, 0, (help) },
421 #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
422 REQ_INFO
423 #undef REQ_GROUP
424 #undef REQ_
425 };
427 static enum request
428 get_request(const char *name)
429 {
430 int namelen = strlen(name);
431 int i;
433 for (i = 0; i < ARRAY_SIZE(req_info); i++)
434 if (req_info[i].namelen == namelen &&
435 !string_enum_compare(req_info[i].name, name, namelen))
436 return req_info[i].request;
438 return REQ_NONE;
439 }
442 /*
443 * Options
444 */
446 static const char usage[] =
447 "tig " TIG_VERSION " (" __DATE__ ")\n"
448 "\n"
449 "Usage: tig [options] [revs] [--] [paths]\n"
450 " or: tig show [options] [revs] [--] [paths]\n"
451 " or: tig blame [rev] path\n"
452 " or: tig status\n"
453 " or: tig < [git command output]\n"
454 "\n"
455 "Options:\n"
456 " -v, --version Show version and exit\n"
457 " -h, --help Show help message and exit";
459 /* Option and state variables. */
460 static bool opt_date = TRUE;
461 static bool opt_author = TRUE;
462 static bool opt_line_number = FALSE;
463 static bool opt_line_graphics = TRUE;
464 static bool opt_rev_graph = FALSE;
465 static bool opt_show_refs = TRUE;
466 static int opt_num_interval = NUMBER_INTERVAL;
467 static int opt_tab_size = TAB_SIZE;
468 static int opt_author_cols = AUTHOR_COLS-1;
469 static char opt_cmd[SIZEOF_STR] = "";
470 static char opt_path[SIZEOF_STR] = "";
471 static char opt_file[SIZEOF_STR] = "";
472 static char opt_ref[SIZEOF_REF] = "";
473 static char opt_head[SIZEOF_REF] = "";
474 static char opt_remote[SIZEOF_REF] = "";
475 static bool opt_no_head = TRUE;
476 static FILE *opt_pipe = NULL;
477 static char opt_encoding[20] = "UTF-8";
478 static bool opt_utf8 = TRUE;
479 static char opt_codeset[20] = "UTF-8";
480 static iconv_t opt_iconv = ICONV_NONE;
481 static char opt_search[SIZEOF_STR] = "";
482 static char opt_cdup[SIZEOF_STR] = "";
483 static char opt_git_dir[SIZEOF_STR] = "";
484 static signed char opt_is_inside_work_tree = -1; /* set to TRUE or FALSE */
485 static char opt_editor[SIZEOF_STR] = "";
487 static enum request
488 parse_options(int argc, const char *argv[])
489 {
490 enum request request = REQ_VIEW_MAIN;
491 size_t buf_size;
492 const char *subcommand;
493 bool seen_dashdash = FALSE;
494 int i;
496 if (!isatty(STDIN_FILENO)) {
497 opt_pipe = stdin;
498 return REQ_VIEW_PAGER;
499 }
501 if (argc <= 1)
502 return REQ_VIEW_MAIN;
504 subcommand = argv[1];
505 if (!strcmp(subcommand, "status") || !strcmp(subcommand, "-S")) {
506 if (!strcmp(subcommand, "-S"))
507 warn("`-S' has been deprecated; use `tig status' instead");
508 if (argc > 2)
509 warn("ignoring arguments after `%s'", subcommand);
510 return REQ_VIEW_STATUS;
512 } else if (!strcmp(subcommand, "blame")) {
513 if (argc <= 2 || argc > 4)
514 die("invalid number of options to blame\n\n%s", usage);
516 i = 2;
517 if (argc == 4) {
518 string_ncopy(opt_ref, argv[i], strlen(argv[i]));
519 i++;
520 }
522 string_ncopy(opt_file, argv[i], strlen(argv[i]));
523 return REQ_VIEW_BLAME;
525 } else if (!strcmp(subcommand, "show")) {
526 request = REQ_VIEW_DIFF;
528 } else if (!strcmp(subcommand, "log") || !strcmp(subcommand, "diff")) {
529 request = subcommand[0] == 'l' ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
530 warn("`tig %s' has been deprecated", subcommand);
532 } else {
533 subcommand = NULL;
534 }
536 if (!subcommand)
537 /* XXX: This is vulnerable to the user overriding
538 * options required for the main view parser. */
539 string_copy(opt_cmd, TIG_MAIN_BASE);
540 else
541 string_format(opt_cmd, "git %s", subcommand);
543 buf_size = strlen(opt_cmd);
545 for (i = 1 + !!subcommand; i < argc; i++) {
546 const char *opt = argv[i];
548 if (seen_dashdash || !strcmp(opt, "--")) {
549 seen_dashdash = TRUE;
551 } else if (!strcmp(opt, "-v") || !strcmp(opt, "--version")) {
552 printf("tig version %s\n", TIG_VERSION);
553 return REQ_NONE;
555 } else if (!strcmp(opt, "-h") || !strcmp(opt, "--help")) {
556 printf("%s\n", usage);
557 return REQ_NONE;
558 }
560 opt_cmd[buf_size++] = ' ';
561 buf_size = sq_quote(opt_cmd, buf_size, opt);
562 if (buf_size >= sizeof(opt_cmd))
563 die("command too long");
564 }
566 opt_cmd[buf_size] = 0;
568 return request;
569 }
572 /*
573 * Line-oriented content detection.
574 */
576 #define LINE_INFO \
577 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
578 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
579 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
580 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
581 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
582 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
583 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
584 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
585 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
586 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
587 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
588 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
589 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
590 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
591 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
592 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
593 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
594 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
595 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
596 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
597 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
598 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
599 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
600 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
601 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
602 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
603 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
604 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
605 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
606 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
607 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
608 LINE(DELIMITER, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
609 LINE(DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
610 LINE(LINE_NUMBER, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
611 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
612 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
613 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
614 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
615 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
616 LINE(MAIN_LOCAL_TAG,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
617 LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \
618 LINE(MAIN_TRACKED, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
619 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
620 LINE(MAIN_HEAD, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
621 LINE(MAIN_REVGRAPH,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
622 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
623 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
624 LINE(STAT_HEAD, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \
625 LINE(STAT_SECTION, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
626 LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
627 LINE(STAT_STAGED, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
628 LINE(STAT_UNSTAGED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
629 LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
630 LINE(BLAME_ID, "", COLOR_MAGENTA, COLOR_DEFAULT, 0)
632 enum line_type {
633 #define LINE(type, line, fg, bg, attr) \
634 LINE_##type
635 LINE_INFO,
636 LINE_NONE
637 #undef LINE
638 };
640 struct line_info {
641 const char *name; /* Option name. */
642 int namelen; /* Size of option name. */
643 const char *line; /* The start of line to match. */
644 int linelen; /* Size of string to match. */
645 int fg, bg, attr; /* Color and text attributes for the lines. */
646 };
648 static struct line_info line_info[] = {
649 #define LINE(type, line, fg, bg, attr) \
650 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
651 LINE_INFO
652 #undef LINE
653 };
655 static enum line_type
656 get_line_type(const char *line)
657 {
658 int linelen = strlen(line);
659 enum line_type type;
661 for (type = 0; type < ARRAY_SIZE(line_info); type++)
662 /* Case insensitive search matches Signed-off-by lines better. */
663 if (linelen >= line_info[type].linelen &&
664 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
665 return type;
667 return LINE_DEFAULT;
668 }
670 static inline int
671 get_line_attr(enum line_type type)
672 {
673 assert(type < ARRAY_SIZE(line_info));
674 return COLOR_PAIR(type) | line_info[type].attr;
675 }
677 static struct line_info *
678 get_line_info(const char *name)
679 {
680 size_t namelen = strlen(name);
681 enum line_type type;
683 for (type = 0; type < ARRAY_SIZE(line_info); type++)
684 if (namelen == line_info[type].namelen &&
685 !string_enum_compare(line_info[type].name, name, namelen))
686 return &line_info[type];
688 return NULL;
689 }
691 static void
692 init_colors(void)
693 {
694 int default_bg = line_info[LINE_DEFAULT].bg;
695 int default_fg = line_info[LINE_DEFAULT].fg;
696 enum line_type type;
698 start_color();
700 if (assume_default_colors(default_fg, default_bg) == ERR) {
701 default_bg = COLOR_BLACK;
702 default_fg = COLOR_WHITE;
703 }
705 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
706 struct line_info *info = &line_info[type];
707 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
708 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
710 init_pair(type, fg, bg);
711 }
712 }
714 struct line {
715 enum line_type type;
717 /* State flags */
718 unsigned int selected:1;
719 unsigned int dirty:1;
721 void *data; /* User data */
722 };
725 /*
726 * Keys
727 */
729 struct keybinding {
730 int alias;
731 enum request request;
732 struct keybinding *next;
733 };
735 static struct keybinding default_keybindings[] = {
736 /* View switching */
737 { 'm', REQ_VIEW_MAIN },
738 { 'd', REQ_VIEW_DIFF },
739 { 'l', REQ_VIEW_LOG },
740 { 't', REQ_VIEW_TREE },
741 { 'f', REQ_VIEW_BLOB },
742 { 'B', REQ_VIEW_BLAME },
743 { 'p', REQ_VIEW_PAGER },
744 { 'h', REQ_VIEW_HELP },
745 { 'S', REQ_VIEW_STATUS },
746 { 'c', REQ_VIEW_STAGE },
748 /* View manipulation */
749 { 'q', REQ_VIEW_CLOSE },
750 { KEY_TAB, REQ_VIEW_NEXT },
751 { KEY_RETURN, REQ_ENTER },
752 { KEY_UP, REQ_PREVIOUS },
753 { KEY_DOWN, REQ_NEXT },
754 { 'R', REQ_REFRESH },
755 { KEY_F(5), REQ_REFRESH },
756 { 'O', REQ_MAXIMIZE },
758 /* Cursor navigation */
759 { 'k', REQ_MOVE_UP },
760 { 'j', REQ_MOVE_DOWN },
761 { KEY_HOME, REQ_MOVE_FIRST_LINE },
762 { KEY_END, REQ_MOVE_LAST_LINE },
763 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
764 { ' ', REQ_MOVE_PAGE_DOWN },
765 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
766 { 'b', REQ_MOVE_PAGE_UP },
767 { '-', REQ_MOVE_PAGE_UP },
769 /* Scrolling */
770 { KEY_IC, REQ_SCROLL_LINE_UP },
771 { KEY_DC, REQ_SCROLL_LINE_DOWN },
772 { 'w', REQ_SCROLL_PAGE_UP },
773 { 's', REQ_SCROLL_PAGE_DOWN },
775 /* Searching */
776 { '/', REQ_SEARCH },
777 { '?', REQ_SEARCH_BACK },
778 { 'n', REQ_FIND_NEXT },
779 { 'N', REQ_FIND_PREV },
781 /* Misc */
782 { 'Q', REQ_QUIT },
783 { 'z', REQ_STOP_LOADING },
784 { 'v', REQ_SHOW_VERSION },
785 { 'r', REQ_SCREEN_REDRAW },
786 { '.', REQ_TOGGLE_LINENO },
787 { 'D', REQ_TOGGLE_DATE },
788 { 'A', REQ_TOGGLE_AUTHOR },
789 { 'g', REQ_TOGGLE_REV_GRAPH },
790 { 'F', REQ_TOGGLE_REFS },
791 { ':', REQ_PROMPT },
792 { 'u', REQ_STATUS_UPDATE },
793 { '!', REQ_STATUS_REVERT },
794 { 'M', REQ_STATUS_MERGE },
795 { '@', REQ_STAGE_NEXT },
796 { ',', REQ_TREE_PARENT },
797 { 'e', REQ_EDIT },
799 /* Using the ncurses SIGWINCH handler. */
800 { KEY_RESIZE, REQ_SCREEN_RESIZE },
801 };
803 #define KEYMAP_INFO \
804 KEYMAP_(GENERIC), \
805 KEYMAP_(MAIN), \
806 KEYMAP_(DIFF), \
807 KEYMAP_(LOG), \
808 KEYMAP_(TREE), \
809 KEYMAP_(BLOB), \
810 KEYMAP_(BLAME), \
811 KEYMAP_(PAGER), \
812 KEYMAP_(HELP), \
813 KEYMAP_(STATUS), \
814 KEYMAP_(STAGE)
816 enum keymap {
817 #define KEYMAP_(name) KEYMAP_##name
818 KEYMAP_INFO
819 #undef KEYMAP_
820 };
822 static struct int_map keymap_table[] = {
823 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
824 KEYMAP_INFO
825 #undef KEYMAP_
826 };
828 #define set_keymap(map, name) \
829 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
831 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
833 static void
834 add_keybinding(enum keymap keymap, enum request request, int key)
835 {
836 struct keybinding *keybinding;
838 keybinding = calloc(1, sizeof(*keybinding));
839 if (!keybinding)
840 die("Failed to allocate keybinding");
842 keybinding->alias = key;
843 keybinding->request = request;
844 keybinding->next = keybindings[keymap];
845 keybindings[keymap] = keybinding;
846 }
848 /* Looks for a key binding first in the given map, then in the generic map, and
849 * lastly in the default keybindings. */
850 static enum request
851 get_keybinding(enum keymap keymap, int key)
852 {
853 struct keybinding *kbd;
854 int i;
856 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
857 if (kbd->alias == key)
858 return kbd->request;
860 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
861 if (kbd->alias == key)
862 return kbd->request;
864 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
865 if (default_keybindings[i].alias == key)
866 return default_keybindings[i].request;
868 return (enum request) key;
869 }
872 struct key {
873 const char *name;
874 int value;
875 };
877 static struct key key_table[] = {
878 { "Enter", KEY_RETURN },
879 { "Space", ' ' },
880 { "Backspace", KEY_BACKSPACE },
881 { "Tab", KEY_TAB },
882 { "Escape", KEY_ESC },
883 { "Left", KEY_LEFT },
884 { "Right", KEY_RIGHT },
885 { "Up", KEY_UP },
886 { "Down", KEY_DOWN },
887 { "Insert", KEY_IC },
888 { "Delete", KEY_DC },
889 { "Hash", '#' },
890 { "Home", KEY_HOME },
891 { "End", KEY_END },
892 { "PageUp", KEY_PPAGE },
893 { "PageDown", KEY_NPAGE },
894 { "F1", KEY_F(1) },
895 { "F2", KEY_F(2) },
896 { "F3", KEY_F(3) },
897 { "F4", KEY_F(4) },
898 { "F5", KEY_F(5) },
899 { "F6", KEY_F(6) },
900 { "F7", KEY_F(7) },
901 { "F8", KEY_F(8) },
902 { "F9", KEY_F(9) },
903 { "F10", KEY_F(10) },
904 { "F11", KEY_F(11) },
905 { "F12", KEY_F(12) },
906 };
908 static int
909 get_key_value(const char *name)
910 {
911 int i;
913 for (i = 0; i < ARRAY_SIZE(key_table); i++)
914 if (!strcasecmp(key_table[i].name, name))
915 return key_table[i].value;
917 if (strlen(name) == 1 && isprint(*name))
918 return (int) *name;
920 return ERR;
921 }
923 static const char *
924 get_key_name(int key_value)
925 {
926 static char key_char[] = "'X'";
927 const char *seq = NULL;
928 int key;
930 for (key = 0; key < ARRAY_SIZE(key_table); key++)
931 if (key_table[key].value == key_value)
932 seq = key_table[key].name;
934 if (seq == NULL &&
935 key_value < 127 &&
936 isprint(key_value)) {
937 key_char[1] = (char) key_value;
938 seq = key_char;
939 }
941 return seq ? seq : "(no key)";
942 }
944 static const char *
945 get_key(enum request request)
946 {
947 static char buf[BUFSIZ];
948 size_t pos = 0;
949 char *sep = "";
950 int i;
952 buf[pos] = 0;
954 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
955 struct keybinding *keybinding = &default_keybindings[i];
957 if (keybinding->request != request)
958 continue;
960 if (!string_format_from(buf, &pos, "%s%s", sep,
961 get_key_name(keybinding->alias)))
962 return "Too many keybindings!";
963 sep = ", ";
964 }
966 return buf;
967 }
969 struct run_request {
970 enum keymap keymap;
971 int key;
972 char cmd[SIZEOF_STR];
973 };
975 static struct run_request *run_request;
976 static size_t run_requests;
978 static enum request
979 add_run_request(enum keymap keymap, int key, int argc, const char **argv)
980 {
981 struct run_request *req;
982 char cmd[SIZEOF_STR];
983 size_t bufpos;
985 for (bufpos = 0; argc > 0; argc--, argv++)
986 if (!string_format_from(cmd, &bufpos, "%s ", *argv))
987 return REQ_NONE;
989 req = realloc(run_request, (run_requests + 1) * sizeof(*run_request));
990 if (!req)
991 return REQ_NONE;
993 run_request = req;
994 req = &run_request[run_requests++];
995 string_copy(req->cmd, cmd);
996 req->keymap = keymap;
997 req->key = key;
999 return REQ_NONE + run_requests;
1000 }
1002 static struct run_request *
1003 get_run_request(enum request request)
1004 {
1005 if (request <= REQ_NONE)
1006 return NULL;
1007 return &run_request[request - REQ_NONE - 1];
1008 }
1010 static void
1011 add_builtin_run_requests(void)
1012 {
1013 struct {
1014 enum keymap keymap;
1015 int key;
1016 const char *argv[1];
1017 } reqs[] = {
1018 { KEYMAP_MAIN, 'C', { "git cherry-pick %(commit)" } },
1019 { KEYMAP_GENERIC, 'G', { "git gc" } },
1020 };
1021 int i;
1023 for (i = 0; i < ARRAY_SIZE(reqs); i++) {
1024 enum request req;
1026 req = add_run_request(reqs[i].keymap, reqs[i].key, 1, reqs[i].argv);
1027 if (req != REQ_NONE)
1028 add_keybinding(reqs[i].keymap, req, reqs[i].key);
1029 }
1030 }
1032 /*
1033 * User config file handling.
1034 */
1036 static struct int_map color_map[] = {
1037 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
1038 COLOR_MAP(DEFAULT),
1039 COLOR_MAP(BLACK),
1040 COLOR_MAP(BLUE),
1041 COLOR_MAP(CYAN),
1042 COLOR_MAP(GREEN),
1043 COLOR_MAP(MAGENTA),
1044 COLOR_MAP(RED),
1045 COLOR_MAP(WHITE),
1046 COLOR_MAP(YELLOW),
1047 };
1049 #define set_color(color, name) \
1050 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
1052 static struct int_map attr_map[] = {
1053 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
1054 ATTR_MAP(NORMAL),
1055 ATTR_MAP(BLINK),
1056 ATTR_MAP(BOLD),
1057 ATTR_MAP(DIM),
1058 ATTR_MAP(REVERSE),
1059 ATTR_MAP(STANDOUT),
1060 ATTR_MAP(UNDERLINE),
1061 };
1063 #define set_attribute(attr, name) \
1064 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
1066 static int config_lineno;
1067 static bool config_errors;
1068 static const char *config_msg;
1070 /* Wants: object fgcolor bgcolor [attr] */
1071 static int
1072 option_color_command(int argc, const char *argv[])
1073 {
1074 struct line_info *info;
1076 if (argc != 3 && argc != 4) {
1077 config_msg = "Wrong number of arguments given to color command";
1078 return ERR;
1079 }
1081 info = get_line_info(argv[0]);
1082 if (!info) {
1083 if (!string_enum_compare(argv[0], "main-delim", strlen("main-delim"))) {
1084 info = get_line_info("delimiter");
1086 } else if (!string_enum_compare(argv[0], "main-date", strlen("main-date"))) {
1087 info = get_line_info("date");
1089 } else {
1090 config_msg = "Unknown color name";
1091 return ERR;
1092 }
1093 }
1095 if (set_color(&info->fg, argv[1]) == ERR ||
1096 set_color(&info->bg, argv[2]) == ERR) {
1097 config_msg = "Unknown color";
1098 return ERR;
1099 }
1101 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
1102 config_msg = "Unknown attribute";
1103 return ERR;
1104 }
1106 return OK;
1107 }
1109 static bool parse_bool(const char *s)
1110 {
1111 return (!strcmp(s, "1") || !strcmp(s, "true") ||
1112 !strcmp(s, "yes")) ? TRUE : FALSE;
1113 }
1115 static int
1116 parse_int(const char *s, int default_value, int min, int max)
1117 {
1118 int value = atoi(s);
1120 return (value < min || value > max) ? default_value : value;
1121 }
1123 /* Wants: name = value */
1124 static int
1125 option_set_command(int argc, const char *argv[])
1126 {
1127 if (argc != 3) {
1128 config_msg = "Wrong number of arguments given to set command";
1129 return ERR;
1130 }
1132 if (strcmp(argv[1], "=")) {
1133 config_msg = "No value assigned";
1134 return ERR;
1135 }
1137 if (!strcmp(argv[0], "show-author")) {
1138 opt_author = parse_bool(argv[2]);
1139 return OK;
1140 }
1142 if (!strcmp(argv[0], "show-date")) {
1143 opt_date = parse_bool(argv[2]);
1144 return OK;
1145 }
1147 if (!strcmp(argv[0], "show-rev-graph")) {
1148 opt_rev_graph = parse_bool(argv[2]);
1149 return OK;
1150 }
1152 if (!strcmp(argv[0], "show-refs")) {
1153 opt_show_refs = parse_bool(argv[2]);
1154 return OK;
1155 }
1157 if (!strcmp(argv[0], "show-line-numbers")) {
1158 opt_line_number = parse_bool(argv[2]);
1159 return OK;
1160 }
1162 if (!strcmp(argv[0], "line-graphics")) {
1163 opt_line_graphics = parse_bool(argv[2]);
1164 return OK;
1165 }
1167 if (!strcmp(argv[0], "line-number-interval")) {
1168 opt_num_interval = parse_int(argv[2], opt_num_interval, 1, 1024);
1169 return OK;
1170 }
1172 if (!strcmp(argv[0], "author-width")) {
1173 opt_author_cols = parse_int(argv[2], opt_author_cols, 0, 1024);
1174 return OK;
1175 }
1177 if (!strcmp(argv[0], "tab-size")) {
1178 opt_tab_size = parse_int(argv[2], opt_tab_size, 1, 1024);
1179 return OK;
1180 }
1182 if (!strcmp(argv[0], "commit-encoding")) {
1183 const char *arg = argv[2];
1184 int arglen = strlen(arg);
1186 switch (arg[0]) {
1187 case '"':
1188 case '\'':
1189 if (arglen == 1 || arg[arglen - 1] != arg[0]) {
1190 config_msg = "Unmatched quotation";
1191 return ERR;
1192 }
1193 arg += 1; arglen -= 2;
1194 default:
1195 string_ncopy(opt_encoding, arg, strlen(arg));
1196 return OK;
1197 }
1198 }
1200 config_msg = "Unknown variable name";
1201 return ERR;
1202 }
1204 /* Wants: mode request key */
1205 static int
1206 option_bind_command(int argc, const char *argv[])
1207 {
1208 enum request request;
1209 int keymap;
1210 int key;
1212 if (argc < 3) {
1213 config_msg = "Wrong number of arguments given to bind command";
1214 return ERR;
1215 }
1217 if (set_keymap(&keymap, argv[0]) == ERR) {
1218 config_msg = "Unknown key map";
1219 return ERR;
1220 }
1222 key = get_key_value(argv[1]);
1223 if (key == ERR) {
1224 config_msg = "Unknown key";
1225 return ERR;
1226 }
1228 request = get_request(argv[2]);
1229 if (request == REQ_NONE) {
1230 const char *obsolete[] = { "cherry-pick" };
1231 size_t namelen = strlen(argv[2]);
1232 int i;
1234 for (i = 0; i < ARRAY_SIZE(obsolete); i++) {
1235 if (namelen == strlen(obsolete[i]) &&
1236 !string_enum_compare(obsolete[i], argv[2], namelen)) {
1237 config_msg = "Obsolete request name";
1238 return ERR;
1239 }
1240 }
1241 }
1242 if (request == REQ_NONE && *argv[2]++ == '!')
1243 request = add_run_request(keymap, key, argc - 2, argv + 2);
1244 if (request == REQ_NONE) {
1245 config_msg = "Unknown request name";
1246 return ERR;
1247 }
1249 add_keybinding(keymap, request, key);
1251 return OK;
1252 }
1254 static int
1255 set_option(const char *opt, char *value)
1256 {
1257 const char *argv[SIZEOF_ARG];
1258 int valuelen;
1259 int argc = 0;
1261 /* Tokenize */
1262 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1263 argv[argc++] = value;
1264 value += valuelen;
1266 /* Nothing more to tokenize or last available token. */
1267 if (!*value || argc >= ARRAY_SIZE(argv))
1268 break;
1270 *value++ = 0;
1271 while (isspace(*value))
1272 value++;
1273 }
1275 if (!strcmp(opt, "color"))
1276 return option_color_command(argc, argv);
1278 if (!strcmp(opt, "set"))
1279 return option_set_command(argc, argv);
1281 if (!strcmp(opt, "bind"))
1282 return option_bind_command(argc, argv);
1284 config_msg = "Unknown option command";
1285 return ERR;
1286 }
1288 static int
1289 read_option(char *opt, size_t optlen, char *value, size_t valuelen)
1290 {
1291 int status = OK;
1293 config_lineno++;
1294 config_msg = "Internal error";
1296 /* Check for comment markers, since read_properties() will
1297 * only ensure opt and value are split at first " \t". */
1298 optlen = strcspn(opt, "#");
1299 if (optlen == 0)
1300 return OK;
1302 if (opt[optlen] != 0) {
1303 config_msg = "No option value";
1304 status = ERR;
1306 } else {
1307 /* Look for comment endings in the value. */
1308 size_t len = strcspn(value, "#");
1310 if (len < valuelen) {
1311 valuelen = len;
1312 value[valuelen] = 0;
1313 }
1315 status = set_option(opt, value);
1316 }
1318 if (status == ERR) {
1319 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1320 config_lineno, (int) optlen, opt, config_msg);
1321 config_errors = TRUE;
1322 }
1324 /* Always keep going if errors are encountered. */
1325 return OK;
1326 }
1328 static void
1329 load_option_file(const char *path)
1330 {
1331 FILE *file;
1333 /* It's ok that the file doesn't exist. */
1334 file = fopen(path, "r");
1335 if (!file)
1336 return;
1338 config_lineno = 0;
1339 config_errors = FALSE;
1341 if (read_properties(file, " \t", read_option) == ERR ||
1342 config_errors == TRUE)
1343 fprintf(stderr, "Errors while loading %s.\n", path);
1344 }
1346 static int
1347 load_options(void)
1348 {
1349 const char *home = getenv("HOME");
1350 const char *tigrc_user = getenv("TIGRC_USER");
1351 const char *tigrc_system = getenv("TIGRC_SYSTEM");
1352 char buf[SIZEOF_STR];
1354 add_builtin_run_requests();
1356 if (!tigrc_system) {
1357 if (!string_format(buf, "%s/tigrc", SYSCONFDIR))
1358 return ERR;
1359 tigrc_system = buf;
1360 }
1361 load_option_file(tigrc_system);
1363 if (!tigrc_user) {
1364 if (!home || !string_format(buf, "%s/.tigrc", home))
1365 return ERR;
1366 tigrc_user = buf;
1367 }
1368 load_option_file(tigrc_user);
1370 return OK;
1371 }
1374 /*
1375 * The viewer
1376 */
1378 struct view;
1379 struct view_ops;
1381 /* The display array of active views and the index of the current view. */
1382 static struct view *display[2];
1383 static unsigned int current_view;
1385 /* Reading from the prompt? */
1386 static bool input_mode = FALSE;
1388 #define foreach_displayed_view(view, i) \
1389 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1391 #define displayed_views() (display[1] != NULL ? 2 : 1)
1393 /* Current head and commit ID */
1394 static char ref_blob[SIZEOF_REF] = "";
1395 static char ref_commit[SIZEOF_REF] = "HEAD";
1396 static char ref_head[SIZEOF_REF] = "HEAD";
1398 struct view {
1399 const char *name; /* View name */
1400 const char *cmd_fmt; /* Default command line format */
1401 const char *cmd_env; /* Command line set via environment */
1402 const char *id; /* Points to either of ref_{head,commit,blob} */
1404 struct view_ops *ops; /* View operations */
1406 enum keymap keymap; /* What keymap does this view have */
1407 bool git_dir; /* Whether the view requires a git directory. */
1409 char cmd[SIZEOF_STR]; /* Command buffer */
1410 char ref[SIZEOF_REF]; /* Hovered commit reference */
1411 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1413 int height, width; /* The width and height of the main window */
1414 WINDOW *win; /* The main window */
1415 WINDOW *title; /* The title window living below the main window */
1417 /* Navigation */
1418 unsigned long offset; /* Offset of the window top */
1419 unsigned long lineno; /* Current line number */
1421 /* Searching */
1422 char grep[SIZEOF_STR]; /* Search string */
1423 regex_t *regex; /* Pre-compiled regex */
1425 /* If non-NULL, points to the view that opened this view. If this view
1426 * is closed tig will switch back to the parent view. */
1427 struct view *parent;
1429 /* Buffering */
1430 size_t lines; /* Total number of lines */
1431 struct line *line; /* Line index */
1432 size_t line_alloc; /* Total number of allocated lines */
1433 size_t line_size; /* Total number of used lines */
1434 unsigned int digits; /* Number of digits in the lines member. */
1436 /* Drawing */
1437 struct line *curline; /* Line currently being drawn. */
1438 enum line_type curtype; /* Attribute currently used for drawing. */
1439 unsigned long col; /* Column when drawing. */
1441 /* Loading */
1442 FILE *pipe;
1443 time_t start_time;
1444 };
1446 struct view_ops {
1447 /* What type of content being displayed. Used in the title bar. */
1448 const char *type;
1449 /* Open and reads in all view content. */
1450 bool (*open)(struct view *view);
1451 /* Read one line; updates view->line. */
1452 bool (*read)(struct view *view, char *data);
1453 /* Draw one line; @lineno must be < view->height. */
1454 bool (*draw)(struct view *view, struct line *line, unsigned int lineno);
1455 /* Depending on view handle a special requests. */
1456 enum request (*request)(struct view *view, enum request request, struct line *line);
1457 /* Search for regex in a line. */
1458 bool (*grep)(struct view *view, struct line *line);
1459 /* Select line */
1460 void (*select)(struct view *view, struct line *line);
1461 };
1463 static struct view_ops blame_ops;
1464 static struct view_ops blob_ops;
1465 static struct view_ops help_ops;
1466 static struct view_ops log_ops;
1467 static struct view_ops main_ops;
1468 static struct view_ops pager_ops;
1469 static struct view_ops stage_ops;
1470 static struct view_ops status_ops;
1471 static struct view_ops tree_ops;
1473 #define VIEW_STR(name, cmd, env, ref, ops, map, git) \
1474 { name, cmd, #env, ref, ops, map, git }
1476 #define VIEW_(id, name, ops, git, ref) \
1477 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id, git)
1480 static struct view views[] = {
1481 VIEW_(MAIN, "main", &main_ops, TRUE, ref_head),
1482 VIEW_(DIFF, "diff", &pager_ops, TRUE, ref_commit),
1483 VIEW_(LOG, "log", &log_ops, TRUE, ref_head),
1484 VIEW_(TREE, "tree", &tree_ops, TRUE, ref_commit),
1485 VIEW_(BLOB, "blob", &blob_ops, TRUE, ref_blob),
1486 VIEW_(BLAME, "blame", &blame_ops, TRUE, ref_commit),
1487 VIEW_(HELP, "help", &help_ops, FALSE, ""),
1488 VIEW_(PAGER, "pager", &pager_ops, FALSE, "stdin"),
1489 VIEW_(STATUS, "status", &status_ops, TRUE, ""),
1490 VIEW_(STAGE, "stage", &stage_ops, TRUE, ""),
1491 };
1493 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1494 #define VIEW_REQ(view) ((view) - views + REQ_OFFSET + 1)
1496 #define foreach_view(view, i) \
1497 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1499 #define view_is_displayed(view) \
1500 (view == display[0] || view == display[1])
1503 enum line_graphic {
1504 LINE_GRAPHIC_VLINE
1505 };
1507 static int line_graphics[] = {
1508 /* LINE_GRAPHIC_VLINE: */ '|'
1509 };
1511 static inline void
1512 set_view_attr(struct view *view, enum line_type type)
1513 {
1514 if (!view->curline->selected && view->curtype != type) {
1515 wattrset(view->win, get_line_attr(type));
1516 wchgat(view->win, -1, 0, type, NULL);
1517 view->curtype = type;
1518 }
1519 }
1521 static int
1522 draw_chars(struct view *view, enum line_type type, const char *string,
1523 int max_len, bool use_tilde)
1524 {
1525 int len = 0;
1526 int col = 0;
1527 int trimmed = FALSE;
1529 if (max_len <= 0)
1530 return 0;
1532 if (opt_utf8) {
1533 len = utf8_length(string, &col, max_len, &trimmed, use_tilde);
1534 } else {
1535 col = len = strlen(string);
1536 if (len > max_len) {
1537 if (use_tilde) {
1538 max_len -= 1;
1539 }
1540 col = len = max_len;
1541 trimmed = TRUE;
1542 }
1543 }
1545 set_view_attr(view, type);
1546 waddnstr(view->win, string, len);
1547 if (trimmed && use_tilde) {
1548 set_view_attr(view, LINE_DELIMITER);
1549 waddch(view->win, '~');
1550 col++;
1551 }
1553 return col;
1554 }
1556 static int
1557 draw_space(struct view *view, enum line_type type, int max, int spaces)
1558 {
1559 static char space[] = " ";
1560 int col = 0;
1562 spaces = MIN(max, spaces);
1564 while (spaces > 0) {
1565 int len = MIN(spaces, sizeof(space) - 1);
1567 col += draw_chars(view, type, space, spaces, FALSE);
1568 spaces -= len;
1569 }
1571 return col;
1572 }
1574 static bool
1575 draw_lineno(struct view *view, unsigned int lineno)
1576 {
1577 char number[10];
1578 int digits3 = view->digits < 3 ? 3 : view->digits;
1579 int max_number = MIN(digits3, STRING_SIZE(number));
1580 int max = view->width - view->col;
1581 int col;
1583 if (max < max_number)
1584 max_number = max;
1586 lineno += view->offset + 1;
1587 if (lineno == 1 || (lineno % opt_num_interval) == 0) {
1588 static char fmt[] = "%1ld";
1590 if (view->digits <= 9)
1591 fmt[1] = '0' + digits3;
1593 if (!string_format(number, fmt, lineno))
1594 number[0] = 0;
1595 col = draw_chars(view, LINE_LINE_NUMBER, number, max_number, TRUE);
1596 } else {
1597 col = draw_space(view, LINE_LINE_NUMBER, max_number, max_number);
1598 }
1600 if (col < max) {
1601 set_view_attr(view, LINE_DEFAULT);
1602 waddch(view->win, line_graphics[LINE_GRAPHIC_VLINE]);
1603 col++;
1604 }
1606 if (col < max)
1607 col += draw_space(view, LINE_DEFAULT, max - col, 1);
1608 view->col += col;
1610 return view->width - view->col <= 0;
1611 }
1613 static bool
1614 draw_text(struct view *view, enum line_type type, const char *string, bool trim)
1615 {
1616 view->col += draw_chars(view, type, string, view->width - view->col, trim);
1617 return view->width - view->col <= 0;
1618 }
1620 static bool
1621 draw_graphic(struct view *view, enum line_type type, chtype graphic[], size_t size)
1622 {
1623 int max = view->width - view->col;
1624 int i;
1626 if (max < size)
1627 size = max;
1629 set_view_attr(view, type);
1630 /* Using waddch() instead of waddnstr() ensures that
1631 * they'll be rendered correctly for the cursor line. */
1632 for (i = 0; i < size; i++)
1633 waddch(view->win, graphic[i]);
1635 view->col += size;
1636 if (size < max) {
1637 waddch(view->win, ' ');
1638 view->col++;
1639 }
1641 return view->width - view->col <= 0;
1642 }
1644 static bool
1645 draw_field(struct view *view, enum line_type type, const char *text, int len, bool trim)
1646 {
1647 int max = MIN(view->width - view->col, len);
1648 int col;
1650 if (text)
1651 col = draw_chars(view, type, text, max - 1, trim);
1652 else
1653 col = draw_space(view, type, max - 1, max - 1);
1655 view->col += col + draw_space(view, LINE_DEFAULT, max - col, max - col);
1656 return view->width - view->col <= 0;
1657 }
1659 static bool
1660 draw_date(struct view *view, struct tm *time)
1661 {
1662 char buf[DATE_COLS];
1663 char *date;
1664 int timelen = 0;
1666 if (time)
1667 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, time);
1668 date = timelen ? buf : NULL;
1670 return draw_field(view, LINE_DATE, date, DATE_COLS, FALSE);
1671 }
1673 static bool
1674 draw_view_line(struct view *view, unsigned int lineno)
1675 {
1676 struct line *line;
1677 bool selected = (view->offset + lineno == view->lineno);
1678 bool draw_ok;
1680 assert(view_is_displayed(view));
1682 if (view->offset + lineno >= view->lines)
1683 return FALSE;
1685 line = &view->line[view->offset + lineno];
1687 wmove(view->win, lineno, 0);
1688 view->col = 0;
1689 view->curline = line;
1690 view->curtype = LINE_NONE;
1691 line->selected = FALSE;
1693 if (selected) {
1694 set_view_attr(view, LINE_CURSOR);
1695 line->selected = TRUE;
1696 view->ops->select(view, line);
1697 } else if (line->selected) {
1698 wclrtoeol(view->win);
1699 }
1701 scrollok(view->win, FALSE);
1702 draw_ok = view->ops->draw(view, line, lineno);
1703 scrollok(view->win, TRUE);
1705 return draw_ok;
1706 }
1708 static void
1709 redraw_view_dirty(struct view *view)
1710 {
1711 bool dirty = FALSE;
1712 int lineno;
1714 for (lineno = 0; lineno < view->height; lineno++) {
1715 struct line *line = &view->line[view->offset + lineno];
1717 if (!line->dirty)
1718 continue;
1719 line->dirty = 0;
1720 dirty = TRUE;
1721 if (!draw_view_line(view, lineno))
1722 break;
1723 }
1725 if (!dirty)
1726 return;
1727 redrawwin(view->win);
1728 if (input_mode)
1729 wnoutrefresh(view->win);
1730 else
1731 wrefresh(view->win);
1732 }
1734 static void
1735 redraw_view_from(struct view *view, int lineno)
1736 {
1737 assert(0 <= lineno && lineno < view->height);
1739 for (; lineno < view->height; lineno++) {
1740 if (!draw_view_line(view, lineno))
1741 break;
1742 }
1744 redrawwin(view->win);
1745 if (input_mode)
1746 wnoutrefresh(view->win);
1747 else
1748 wrefresh(view->win);
1749 }
1751 static void
1752 redraw_view(struct view *view)
1753 {
1754 wclear(view->win);
1755 redraw_view_from(view, 0);
1756 }
1759 static void
1760 update_view_title(struct view *view)
1761 {
1762 char buf[SIZEOF_STR];
1763 char state[SIZEOF_STR];
1764 size_t bufpos = 0, statelen = 0;
1766 assert(view_is_displayed(view));
1768 if (view != VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) {
1769 unsigned int view_lines = view->offset + view->height;
1770 unsigned int lines = view->lines
1771 ? MIN(view_lines, view->lines) * 100 / view->lines
1772 : 0;
1774 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1775 view->ops->type,
1776 view->lineno + 1,
1777 view->lines,
1778 lines);
1780 if (view->pipe) {
1781 time_t secs = time(NULL) - view->start_time;
1783 /* Three git seconds are a long time ... */
1784 if (secs > 2)
1785 string_format_from(state, &statelen, " %lds", secs);
1786 }
1787 }
1789 string_format_from(buf, &bufpos, "[%s]", view->name);
1790 if (*view->ref && bufpos < view->width) {
1791 size_t refsize = strlen(view->ref);
1792 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1794 if (minsize < view->width)
1795 refsize = view->width - minsize + 7;
1796 string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
1797 }
1799 if (statelen && bufpos < view->width) {
1800 string_format_from(buf, &bufpos, " %s", state);
1801 }
1803 if (view == display[current_view])
1804 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1805 else
1806 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1808 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1809 wclrtoeol(view->title);
1810 wmove(view->title, 0, view->width - 1);
1812 if (input_mode)
1813 wnoutrefresh(view->title);
1814 else
1815 wrefresh(view->title);
1816 }
1818 static void
1819 resize_display(void)
1820 {
1821 int offset, i;
1822 struct view *base = display[0];
1823 struct view *view = display[1] ? display[1] : display[0];
1825 /* Setup window dimensions */
1827 getmaxyx(stdscr, base->height, base->width);
1829 /* Make room for the status window. */
1830 base->height -= 1;
1832 if (view != base) {
1833 /* Horizontal split. */
1834 view->width = base->width;
1835 view->height = SCALE_SPLIT_VIEW(base->height);
1836 base->height -= view->height;
1838 /* Make room for the title bar. */
1839 view->height -= 1;
1840 }
1842 /* Make room for the title bar. */
1843 base->height -= 1;
1845 offset = 0;
1847 foreach_displayed_view (view, i) {
1848 if (!view->win) {
1849 view->win = newwin(view->height, 0, offset, 0);
1850 if (!view->win)
1851 die("Failed to create %s view", view->name);
1853 scrollok(view->win, TRUE);
1855 view->title = newwin(1, 0, offset + view->height, 0);
1856 if (!view->title)
1857 die("Failed to create title window");
1859 } else {
1860 wresize(view->win, view->height, view->width);
1861 mvwin(view->win, offset, 0);
1862 mvwin(view->title, offset + view->height, 0);
1863 }
1865 offset += view->height + 1;
1866 }
1867 }
1869 static void
1870 redraw_display(void)
1871 {
1872 struct view *view;
1873 int i;
1875 foreach_displayed_view (view, i) {
1876 redraw_view(view);
1877 update_view_title(view);
1878 }
1879 }
1881 static void
1882 update_display_cursor(struct view *view)
1883 {
1884 /* Move the cursor to the right-most column of the cursor line.
1885 *
1886 * XXX: This could turn out to be a bit expensive, but it ensures that
1887 * the cursor does not jump around. */
1888 if (view->lines) {
1889 wmove(view->win, view->lineno - view->offset, view->width - 1);
1890 wrefresh(view->win);
1891 }
1892 }
1894 /*
1895 * Navigation
1896 */
1898 /* Scrolling backend */
1899 static void
1900 do_scroll_view(struct view *view, int lines)
1901 {
1902 bool redraw_current_line = FALSE;
1904 /* The rendering expects the new offset. */
1905 view->offset += lines;
1907 assert(0 <= view->offset && view->offset < view->lines);
1908 assert(lines);
1910 /* Move current line into the view. */
1911 if (view->lineno < view->offset) {
1912 view->lineno = view->offset;
1913 redraw_current_line = TRUE;
1914 } else if (view->lineno >= view->offset + view->height) {
1915 view->lineno = view->offset + view->height - 1;
1916 redraw_current_line = TRUE;
1917 }
1919 assert(view->offset <= view->lineno && view->lineno < view->lines);
1921 /* Redraw the whole screen if scrolling is pointless. */
1922 if (view->height < ABS(lines)) {
1923 redraw_view(view);
1925 } else {
1926 int line = lines > 0 ? view->height - lines : 0;
1927 int end = line + ABS(lines);
1929 wscrl(view->win, lines);
1931 for (; line < end; line++) {
1932 if (!draw_view_line(view, line))
1933 break;
1934 }
1936 if (redraw_current_line)
1937 draw_view_line(view, view->lineno - view->offset);
1938 }
1940 redrawwin(view->win);
1941 wrefresh(view->win);
1942 report("");
1943 }
1945 /* Scroll frontend */
1946 static void
1947 scroll_view(struct view *view, enum request request)
1948 {
1949 int lines = 1;
1951 assert(view_is_displayed(view));
1953 switch (request) {
1954 case REQ_SCROLL_PAGE_DOWN:
1955 lines = view->height;
1956 case REQ_SCROLL_LINE_DOWN:
1957 if (view->offset + lines > view->lines)
1958 lines = view->lines - view->offset;
1960 if (lines == 0 || view->offset + view->height >= view->lines) {
1961 report("Cannot scroll beyond the last line");
1962 return;
1963 }
1964 break;
1966 case REQ_SCROLL_PAGE_UP:
1967 lines = view->height;
1968 case REQ_SCROLL_LINE_UP:
1969 if (lines > view->offset)
1970 lines = view->offset;
1972 if (lines == 0) {
1973 report("Cannot scroll beyond the first line");
1974 return;
1975 }
1977 lines = -lines;
1978 break;
1980 default:
1981 die("request %d not handled in switch", request);
1982 }
1984 do_scroll_view(view, lines);
1985 }
1987 /* Cursor moving */
1988 static void
1989 move_view(struct view *view, enum request request)
1990 {
1991 int scroll_steps = 0;
1992 int steps;
1994 switch (request) {
1995 case REQ_MOVE_FIRST_LINE:
1996 steps = -view->lineno;
1997 break;
1999 case REQ_MOVE_LAST_LINE:
2000 steps = view->lines - view->lineno - 1;
2001 break;
2003 case REQ_MOVE_PAGE_UP:
2004 steps = view->height > view->lineno
2005 ? -view->lineno : -view->height;
2006 break;
2008 case REQ_MOVE_PAGE_DOWN:
2009 steps = view->lineno + view->height >= view->lines
2010 ? view->lines - view->lineno - 1 : view->height;
2011 break;
2013 case REQ_MOVE_UP:
2014 steps = -1;
2015 break;
2017 case REQ_MOVE_DOWN:
2018 steps = 1;
2019 break;
2021 default:
2022 die("request %d not handled in switch", request);
2023 }
2025 if (steps <= 0 && view->lineno == 0) {
2026 report("Cannot move beyond the first line");
2027 return;
2029 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
2030 report("Cannot move beyond the last line");
2031 return;
2032 }
2034 /* Move the current line */
2035 view->lineno += steps;
2036 assert(0 <= view->lineno && view->lineno < view->lines);
2038 /* Check whether the view needs to be scrolled */
2039 if (view->lineno < view->offset ||
2040 view->lineno >= view->offset + view->height) {
2041 scroll_steps = steps;
2042 if (steps < 0 && -steps > view->offset) {
2043 scroll_steps = -view->offset;
2045 } else if (steps > 0) {
2046 if (view->lineno == view->lines - 1 &&
2047 view->lines > view->height) {
2048 scroll_steps = view->lines - view->offset - 1;
2049 if (scroll_steps >= view->height)
2050 scroll_steps -= view->height - 1;
2051 }
2052 }
2053 }
2055 if (!view_is_displayed(view)) {
2056 view->offset += scroll_steps;
2057 assert(0 <= view->offset && view->offset < view->lines);
2058 view->ops->select(view, &view->line[view->lineno]);
2059 return;
2060 }
2062 /* Repaint the old "current" line if we be scrolling */
2063 if (ABS(steps) < view->height)
2064 draw_view_line(view, view->lineno - steps - view->offset);
2066 if (scroll_steps) {
2067 do_scroll_view(view, scroll_steps);
2068 return;
2069 }
2071 /* Draw the current line */
2072 draw_view_line(view, view->lineno - view->offset);
2074 redrawwin(view->win);
2075 wrefresh(view->win);
2076 report("");
2077 }
2080 /*
2081 * Searching
2082 */
2084 static void search_view(struct view *view, enum request request);
2086 static bool
2087 find_next_line(struct view *view, unsigned long lineno, struct line *line)
2088 {
2089 assert(view_is_displayed(view));
2091 if (!view->ops->grep(view, line))
2092 return FALSE;
2094 if (lineno - view->offset >= view->height) {
2095 view->offset = lineno;
2096 view->lineno = lineno;
2097 redraw_view(view);
2099 } else {
2100 unsigned long old_lineno = view->lineno - view->offset;
2102 view->lineno = lineno;
2103 draw_view_line(view, old_lineno);
2105 draw_view_line(view, view->lineno - view->offset);
2106 redrawwin(view->win);
2107 wrefresh(view->win);
2108 }
2110 report("Line %ld matches '%s'", lineno + 1, view->grep);
2111 return TRUE;
2112 }
2114 static void
2115 find_next(struct view *view, enum request request)
2116 {
2117 unsigned long lineno = view->lineno;
2118 int direction;
2120 if (!*view->grep) {
2121 if (!*opt_search)
2122 report("No previous search");
2123 else
2124 search_view(view, request);
2125 return;
2126 }
2128 switch (request) {
2129 case REQ_SEARCH:
2130 case REQ_FIND_NEXT:
2131 direction = 1;
2132 break;
2134 case REQ_SEARCH_BACK:
2135 case REQ_FIND_PREV:
2136 direction = -1;
2137 break;
2139 default:
2140 return;
2141 }
2143 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
2144 lineno += direction;
2146 /* Note, lineno is unsigned long so will wrap around in which case it
2147 * will become bigger than view->lines. */
2148 for (; lineno < view->lines; lineno += direction) {
2149 struct line *line = &view->line[lineno];
2151 if (find_next_line(view, lineno, line))
2152 return;
2153 }
2155 report("No match found for '%s'", view->grep);
2156 }
2158 static void
2159 search_view(struct view *view, enum request request)
2160 {
2161 int regex_err;
2163 if (view->regex) {
2164 regfree(view->regex);
2165 *view->grep = 0;
2166 } else {
2167 view->regex = calloc(1, sizeof(*view->regex));
2168 if (!view->regex)
2169 return;
2170 }
2172 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
2173 if (regex_err != 0) {
2174 char buf[SIZEOF_STR] = "unknown error";
2176 regerror(regex_err, view->regex, buf, sizeof(buf));
2177 report("Search failed: %s", buf);
2178 return;
2179 }
2181 string_copy(view->grep, opt_search);
2183 find_next(view, request);
2184 }
2186 /*
2187 * Incremental updating
2188 */
2190 static void
2191 reset_view(struct view *view)
2192 {
2193 int i;
2195 for (i = 0; i < view->lines; i++)
2196 free(view->line[i].data);
2197 free(view->line);
2199 view->line = NULL;
2200 view->offset = 0;
2201 view->lines = 0;
2202 view->lineno = 0;
2203 view->line_size = 0;
2204 view->line_alloc = 0;
2205 view->vid[0] = 0;
2206 }
2208 static void
2209 end_update(struct view *view, bool force)
2210 {
2211 if (!view->pipe)
2212 return;
2213 while (!view->ops->read(view, NULL))
2214 if (!force)
2215 return;
2216 set_nonblocking_input(FALSE);
2217 if (view->pipe == stdin)
2218 fclose(view->pipe);
2219 else
2220 pclose(view->pipe);
2221 view->pipe = NULL;
2222 }
2224 static bool
2225 begin_update(struct view *view, bool refresh)
2226 {
2227 if (opt_cmd[0]) {
2228 string_copy(view->cmd, opt_cmd);
2229 opt_cmd[0] = 0;
2230 /* When running random commands, initially show the
2231 * command in the title. However, it maybe later be
2232 * overwritten if a commit line is selected. */
2233 if (view == VIEW(REQ_VIEW_PAGER))
2234 string_copy(view->ref, view->cmd);
2235 else
2236 view->ref[0] = 0;
2238 } else if (view == VIEW(REQ_VIEW_TREE)) {
2239 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2240 char path[SIZEOF_STR];
2242 if (strcmp(view->vid, view->id))
2243 opt_path[0] = path[0] = 0;
2244 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
2245 return FALSE;
2247 if (!string_format(view->cmd, format, view->id, path))
2248 return FALSE;
2250 } else if (!refresh) {
2251 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2252 const char *id = view->id;
2254 if (!string_format(view->cmd, format, id, id, id, id, id))
2255 return FALSE;
2257 /* Put the current ref_* value to the view title ref
2258 * member. This is needed by the blob view. Most other
2259 * views sets it automatically after loading because the
2260 * first line is a commit line. */
2261 string_copy_rev(view->ref, view->id);
2262 }
2264 /* Special case for the pager view. */
2265 if (opt_pipe) {
2266 view->pipe = opt_pipe;
2267 opt_pipe = NULL;
2268 } else {
2269 view->pipe = popen(view->cmd, "r");
2270 }
2272 if (!view->pipe)
2273 return FALSE;
2275 set_nonblocking_input(TRUE);
2276 reset_view(view);
2277 string_copy_rev(view->vid, view->id);
2279 view->start_time = time(NULL);
2281 return TRUE;
2282 }
2284 #define ITEM_CHUNK_SIZE 256
2285 static void *
2286 realloc_items(void *mem, size_t *size, size_t new_size, size_t item_size)
2287 {
2288 size_t num_chunks = *size / ITEM_CHUNK_SIZE;
2289 size_t num_chunks_new = (new_size + ITEM_CHUNK_SIZE - 1) / ITEM_CHUNK_SIZE;
2291 if (mem == NULL || num_chunks != num_chunks_new) {
2292 *size = num_chunks_new * ITEM_CHUNK_SIZE;
2293 mem = realloc(mem, *size * item_size);
2294 }
2296 return mem;
2297 }
2299 static struct line *
2300 realloc_lines(struct view *view, size_t line_size)
2301 {
2302 size_t alloc = view->line_alloc;
2303 struct line *tmp = realloc_items(view->line, &alloc, line_size,
2304 sizeof(*view->line));
2306 if (!tmp)
2307 return NULL;
2309 view->line = tmp;
2310 view->line_alloc = alloc;
2311 view->line_size = line_size;
2312 return view->line;
2313 }
2315 static bool
2316 update_view(struct view *view)
2317 {
2318 char in_buffer[BUFSIZ];
2319 char out_buffer[BUFSIZ * 2];
2320 char *line;
2321 /* The number of lines to read. If too low it will cause too much
2322 * redrawing (and possible flickering), if too high responsiveness
2323 * will suffer. */
2324 unsigned long lines = view->height;
2325 int redraw_from = -1;
2327 if (!view->pipe)
2328 return TRUE;
2330 /* Only redraw if lines are visible. */
2331 if (view->offset + view->height >= view->lines)
2332 redraw_from = view->lines - view->offset;
2334 /* FIXME: This is probably not perfect for backgrounded views. */
2335 if (!realloc_lines(view, view->lines + lines))
2336 goto alloc_error;
2338 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
2339 size_t linelen = strlen(line);
2341 if (linelen)
2342 line[linelen - 1] = 0;
2344 if (opt_iconv != ICONV_NONE) {
2345 ICONV_CONST char *inbuf = line;
2346 size_t inlen = linelen;
2348 char *outbuf = out_buffer;
2349 size_t outlen = sizeof(out_buffer);
2351 size_t ret;
2353 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
2354 if (ret != (size_t) -1) {
2355 line = out_buffer;
2356 linelen = strlen(out_buffer);
2357 }
2358 }
2360 if (!view->ops->read(view, line))
2361 goto alloc_error;
2363 if (lines-- == 1)
2364 break;
2365 }
2367 {
2368 int digits;
2370 lines = view->lines;
2371 for (digits = 0; lines; digits++)
2372 lines /= 10;
2374 /* Keep the displayed view in sync with line number scaling. */
2375 if (digits != view->digits) {
2376 view->digits = digits;
2377 redraw_from = 0;
2378 }
2379 }
2381 if (ferror(view->pipe) && errno != 0) {
2382 report("Failed to read: %s", strerror(errno));
2383 end_update(view, TRUE);
2385 } else if (feof(view->pipe)) {
2386 report("");
2387 end_update(view, FALSE);
2388 }
2390 if (view == VIEW(REQ_VIEW_TREE)) {
2391 /* Clear the view and redraw everything since the tree sorting
2392 * might have rearranged things. */
2393 redraw_view(view);
2395 } else if (redraw_from >= 0) {
2396 /* If this is an incremental update, redraw the previous line
2397 * since for commits some members could have changed when
2398 * loading the main view. */
2399 if (redraw_from > 0)
2400 redraw_from--;
2402 /* Since revision graph visualization requires knowledge
2403 * about the parent commit, it causes a further one-off
2404 * needed to be redrawn for incremental updates. */
2405 if (redraw_from > 0 && opt_rev_graph)
2406 redraw_from--;
2408 /* Incrementally draw avoids flickering. */
2409 redraw_view_from(view, redraw_from);
2410 }
2412 if (view == VIEW(REQ_VIEW_BLAME))
2413 redraw_view_dirty(view);
2415 /* Update the title _after_ the redraw so that if the redraw picks up a
2416 * commit reference in view->ref it'll be available here. */
2417 update_view_title(view);
2418 return TRUE;
2420 alloc_error:
2421 report("Allocation failure");
2422 end_update(view, TRUE);
2423 return FALSE;
2424 }
2426 static struct line *
2427 add_line_data(struct view *view, void *data, enum line_type type)
2428 {
2429 struct line *line = &view->line[view->lines++];
2431 memset(line, 0, sizeof(*line));
2432 line->type = type;
2433 line->data = data;
2435 return line;
2436 }
2438 static struct line *
2439 add_line_text(struct view *view, const char *text, enum line_type type)
2440 {
2441 char *data = text ? strdup(text) : NULL;
2443 return data ? add_line_data(view, data, type) : NULL;
2444 }
2447 /*
2448 * View opening
2449 */
2451 enum open_flags {
2452 OPEN_DEFAULT = 0, /* Use default view switching. */
2453 OPEN_SPLIT = 1, /* Split current view. */
2454 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2455 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2456 OPEN_NOMAXIMIZE = 8, /* Do not maximize the current view. */
2457 OPEN_REFRESH = 16, /* Refresh view using previous command. */
2458 };
2460 static void
2461 open_view(struct view *prev, enum request request, enum open_flags flags)
2462 {
2463 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2464 bool split = !!(flags & OPEN_SPLIT);
2465 bool reload = !!(flags & (OPEN_RELOAD | OPEN_REFRESH));
2466 bool nomaximize = !!(flags & (OPEN_NOMAXIMIZE | OPEN_REFRESH));
2467 struct view *view = VIEW(request);
2468 int nviews = displayed_views();
2469 struct view *base_view = display[0];
2471 if (view == prev && nviews == 1 && !reload) {
2472 report("Already in %s view", view->name);
2473 return;
2474 }
2476 if (view->git_dir && !opt_git_dir[0]) {
2477 report("The %s view is disabled in pager view", view->name);
2478 return;
2479 }
2481 if (split) {
2482 display[1] = view;
2483 if (!backgrounded)
2484 current_view = 1;
2485 } else if (!nomaximize) {
2486 /* Maximize the current view. */
2487 memset(display, 0, sizeof(display));
2488 current_view = 0;
2489 display[current_view] = view;
2490 }
2492 /* Resize the view when switching between split- and full-screen,
2493 * or when switching between two different full-screen views. */
2494 if (nviews != displayed_views() ||
2495 (nviews == 1 && base_view != display[0]))
2496 resize_display();
2498 if (view->pipe)
2499 end_update(view, TRUE);
2501 if (view->ops->open) {
2502 if (!view->ops->open(view)) {
2503 report("Failed to load %s view", view->name);
2504 return;
2505 }
2507 } else if ((reload || strcmp(view->vid, view->id)) &&
2508 !begin_update(view, flags & OPEN_REFRESH)) {
2509 report("Failed to load %s view", view->name);
2510 return;
2511 }
2513 if (split && prev->lineno - prev->offset >= prev->height) {
2514 /* Take the title line into account. */
2515 int lines = prev->lineno - prev->offset - prev->height + 1;
2517 /* Scroll the view that was split if the current line is
2518 * outside the new limited view. */
2519 do_scroll_view(prev, lines);
2520 }
2522 if (prev && view != prev) {
2523 if (split && !backgrounded) {
2524 /* "Blur" the previous view. */
2525 update_view_title(prev);
2526 }
2528 view->parent = prev;
2529 }
2531 if (view->pipe && view->lines == 0) {
2532 /* Clear the old view and let the incremental updating refill
2533 * the screen. */
2534 werase(view->win);
2535 report("");
2536 } else if (view_is_displayed(view)) {
2537 redraw_view(view);
2538 report("");
2539 }
2541 /* If the view is backgrounded the above calls to report()
2542 * won't redraw the view title. */
2543 if (backgrounded)
2544 update_view_title(view);
2545 }
2547 static bool
2548 run_confirm(const char *cmd, const char *prompt)
2549 {
2550 bool confirmation = prompt_yesno(prompt);
2552 if (confirmation)
2553 system(cmd);
2555 return confirmation;
2556 }
2558 static void
2559 open_external_viewer(const char *cmd)
2560 {
2561 def_prog_mode(); /* save current tty modes */
2562 endwin(); /* restore original tty modes */
2563 system(cmd);
2564 fprintf(stderr, "Press Enter to continue");
2565 getc(stdin);
2566 reset_prog_mode();
2567 redraw_display();
2568 }
2570 static void
2571 open_mergetool(const char *file)
2572 {
2573 char cmd[SIZEOF_STR];
2574 char file_sq[SIZEOF_STR];
2576 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2577 string_format(cmd, "git mergetool %s", file_sq)) {
2578 open_external_viewer(cmd);
2579 }
2580 }
2582 static void
2583 open_editor(bool from_root, const char *file)
2584 {
2585 char cmd[SIZEOF_STR];
2586 char file_sq[SIZEOF_STR];
2587 const char *editor;
2588 char *prefix = from_root ? opt_cdup : "";
2590 editor = getenv("GIT_EDITOR");
2591 if (!editor && *opt_editor)
2592 editor = opt_editor;
2593 if (!editor)
2594 editor = getenv("VISUAL");
2595 if (!editor)
2596 editor = getenv("EDITOR");
2597 if (!editor)
2598 editor = "vi";
2600 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2601 string_format(cmd, "%s %s%s", editor, prefix, file_sq)) {
2602 open_external_viewer(cmd);
2603 }
2604 }
2606 static void
2607 open_run_request(enum request request)
2608 {
2609 struct run_request *req = get_run_request(request);
2610 char buf[SIZEOF_STR * 2];
2611 size_t bufpos;
2612 char *cmd;
2614 if (!req) {
2615 report("Unknown run request");
2616 return;
2617 }
2619 bufpos = 0;
2620 cmd = req->cmd;
2622 while (cmd) {
2623 char *next = strstr(cmd, "%(");
2624 int len = next - cmd;
2625 char *value;
2627 if (!next) {
2628 len = strlen(cmd);
2629 value = "";
2631 } else if (!strncmp(next, "%(head)", 7)) {
2632 value = ref_head;
2634 } else if (!strncmp(next, "%(commit)", 9)) {
2635 value = ref_commit;
2637 } else if (!strncmp(next, "%(blob)", 7)) {
2638 value = ref_blob;
2640 } else {
2641 report("Unknown replacement in run request: `%s`", req->cmd);
2642 return;
2643 }
2645 if (!string_format_from(buf, &bufpos, "%.*s%s", len, cmd, value))
2646 return;
2648 if (next)
2649 next = strchr(next, ')') + 1;
2650 cmd = next;
2651 }
2653 open_external_viewer(buf);
2654 }
2656 /*
2657 * User request switch noodle
2658 */
2660 static int
2661 view_driver(struct view *view, enum request request)
2662 {
2663 int i;
2665 if (request == REQ_NONE) {
2666 doupdate();
2667 return TRUE;
2668 }
2670 if (request > REQ_NONE) {
2671 open_run_request(request);
2672 /* FIXME: When all views can refresh always do this. */
2673 if (view == VIEW(REQ_VIEW_STATUS) ||
2674 view == VIEW(REQ_VIEW_MAIN) ||
2675 view == VIEW(REQ_VIEW_LOG) ||
2676 view == VIEW(REQ_VIEW_STAGE))
2677 request = REQ_REFRESH;
2678 else
2679 return TRUE;
2680 }
2682 if (view && view->lines) {
2683 request = view->ops->request(view, request, &view->line[view->lineno]);
2684 if (request == REQ_NONE)
2685 return TRUE;
2686 }
2688 switch (request) {
2689 case REQ_MOVE_UP:
2690 case REQ_MOVE_DOWN:
2691 case REQ_MOVE_PAGE_UP:
2692 case REQ_MOVE_PAGE_DOWN:
2693 case REQ_MOVE_FIRST_LINE:
2694 case REQ_MOVE_LAST_LINE:
2695 move_view(view, request);
2696 break;
2698 case REQ_SCROLL_LINE_DOWN:
2699 case REQ_SCROLL_LINE_UP:
2700 case REQ_SCROLL_PAGE_DOWN:
2701 case REQ_SCROLL_PAGE_UP:
2702 scroll_view(view, request);
2703 break;
2705 case REQ_VIEW_BLAME:
2706 if (!opt_file[0]) {
2707 report("No file chosen, press %s to open tree view",
2708 get_key(REQ_VIEW_TREE));
2709 break;
2710 }
2711 open_view(view, request, OPEN_DEFAULT);
2712 break;
2714 case REQ_VIEW_BLOB:
2715 if (!ref_blob[0]) {
2716 report("No file chosen, press %s to open tree view",
2717 get_key(REQ_VIEW_TREE));
2718 break;
2719 }
2720 open_view(view, request, OPEN_DEFAULT);
2721 break;
2723 case REQ_VIEW_PAGER:
2724 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2725 report("No pager content, press %s to run command from prompt",
2726 get_key(REQ_PROMPT));
2727 break;
2728 }
2729 open_view(view, request, OPEN_DEFAULT);
2730 break;
2732 case REQ_VIEW_STAGE:
2733 if (!VIEW(REQ_VIEW_STAGE)->lines) {
2734 report("No stage content, press %s to open the status view and choose file",
2735 get_key(REQ_VIEW_STATUS));
2736 break;
2737 }
2738 open_view(view, request, OPEN_DEFAULT);
2739 break;
2741 case REQ_VIEW_STATUS:
2742 if (opt_is_inside_work_tree == FALSE) {
2743 report("The status view requires a working tree");
2744 break;
2745 }
2746 open_view(view, request, OPEN_DEFAULT);
2747 break;
2749 case REQ_VIEW_MAIN:
2750 case REQ_VIEW_DIFF:
2751 case REQ_VIEW_LOG:
2752 case REQ_VIEW_TREE:
2753 case REQ_VIEW_HELP:
2754 open_view(view, request, OPEN_DEFAULT);
2755 break;
2757 case REQ_NEXT:
2758 case REQ_PREVIOUS:
2759 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2761 if ((view == VIEW(REQ_VIEW_DIFF) &&
2762 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2763 (view == VIEW(REQ_VIEW_DIFF) &&
2764 view->parent == VIEW(REQ_VIEW_BLAME)) ||
2765 (view == VIEW(REQ_VIEW_STAGE) &&
2766 view->parent == VIEW(REQ_VIEW_STATUS)) ||
2767 (view == VIEW(REQ_VIEW_BLOB) &&
2768 view->parent == VIEW(REQ_VIEW_TREE))) {
2769 int line;
2771 view = view->parent;
2772 line = view->lineno;
2773 move_view(view, request);
2774 if (view_is_displayed(view))
2775 update_view_title(view);
2776 if (line != view->lineno)
2777 view->ops->request(view, REQ_ENTER,
2778 &view->line[view->lineno]);
2780 } else {
2781 move_view(view, request);
2782 }
2783 break;
2785 case REQ_VIEW_NEXT:
2786 {
2787 int nviews = displayed_views();
2788 int next_view = (current_view + 1) % nviews;
2790 if (next_view == current_view) {
2791 report("Only one view is displayed");
2792 break;
2793 }
2795 current_view = next_view;
2796 /* Blur out the title of the previous view. */
2797 update_view_title(view);
2798 report("");
2799 break;
2800 }
2801 case REQ_REFRESH:
2802 report("Refreshing is not yet supported for the %s view", view->name);
2803 break;
2805 case REQ_MAXIMIZE:
2806 if (displayed_views() == 2)
2807 open_view(view, VIEW_REQ(view), OPEN_DEFAULT);
2808 break;
2810 case REQ_TOGGLE_LINENO:
2811 opt_line_number = !opt_line_number;
2812 redraw_display();
2813 break;
2815 case REQ_TOGGLE_DATE:
2816 opt_date = !opt_date;
2817 redraw_display();
2818 break;
2820 case REQ_TOGGLE_AUTHOR:
2821 opt_author = !opt_author;
2822 redraw_display();
2823 break;
2825 case REQ_TOGGLE_REV_GRAPH:
2826 opt_rev_graph = !opt_rev_graph;
2827 redraw_display();
2828 break;
2830 case REQ_TOGGLE_REFS:
2831 opt_show_refs = !opt_show_refs;
2832 redraw_display();
2833 break;
2835 case REQ_SEARCH:
2836 case REQ_SEARCH_BACK:
2837 search_view(view, request);
2838 break;
2840 case REQ_FIND_NEXT:
2841 case REQ_FIND_PREV:
2842 find_next(view, request);
2843 break;
2845 case REQ_STOP_LOADING:
2846 for (i = 0; i < ARRAY_SIZE(views); i++) {
2847 view = &views[i];
2848 if (view->pipe)
2849 report("Stopped loading the %s view", view->name),
2850 end_update(view, TRUE);
2851 }
2852 break;
2854 case REQ_SHOW_VERSION:
2855 report("tig-%s (built %s)", TIG_VERSION, __DATE__);
2856 return TRUE;
2858 case REQ_SCREEN_RESIZE:
2859 resize_display();
2860 /* Fall-through */
2861 case REQ_SCREEN_REDRAW:
2862 redraw_display();
2863 break;
2865 case REQ_EDIT:
2866 report("Nothing to edit");
2867 break;
2869 case REQ_ENTER:
2870 report("Nothing to enter");
2871 break;
2873 case REQ_VIEW_CLOSE:
2874 /* XXX: Mark closed views by letting view->parent point to the
2875 * view itself. Parents to closed view should never be
2876 * followed. */
2877 if (view->parent &&
2878 view->parent->parent != view->parent) {
2879 memset(display, 0, sizeof(display));
2880 current_view = 0;
2881 display[current_view] = view->parent;
2882 view->parent = view;
2883 resize_display();
2884 redraw_display();
2885 report("");
2886 break;
2887 }
2888 /* Fall-through */
2889 case REQ_QUIT:
2890 return FALSE;
2892 default:
2893 report("Unknown key, press 'h' for help");
2894 return TRUE;
2895 }
2897 return TRUE;
2898 }
2901 /*
2902 * Pager backend
2903 */
2905 static bool
2906 pager_draw(struct view *view, struct line *line, unsigned int lineno)
2907 {
2908 char *text = line->data;
2910 if (opt_line_number && draw_lineno(view, lineno))
2911 return TRUE;
2913 draw_text(view, line->type, text, TRUE);
2914 return TRUE;
2915 }
2917 static bool
2918 add_describe_ref(char *buf, size_t *bufpos, const char *commit_id, const char *sep)
2919 {
2920 char refbuf[SIZEOF_STR];
2921 char *ref = NULL;
2922 FILE *pipe;
2924 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2925 return TRUE;
2927 pipe = popen(refbuf, "r");
2928 if (!pipe)
2929 return TRUE;
2931 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2932 ref = chomp_string(ref);
2933 pclose(pipe);
2935 if (!ref || !*ref)
2936 return TRUE;
2938 /* This is the only fatal call, since it can "corrupt" the buffer. */
2939 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2940 return FALSE;
2942 return TRUE;
2943 }
2945 static void
2946 add_pager_refs(struct view *view, struct line *line)
2947 {
2948 char buf[SIZEOF_STR];
2949 char *commit_id = (char *)line->data + STRING_SIZE("commit ");
2950 struct ref **refs;
2951 size_t bufpos = 0, refpos = 0;
2952 const char *sep = "Refs: ";
2953 bool is_tag = FALSE;
2955 assert(line->type == LINE_COMMIT);
2957 refs = get_refs(commit_id);
2958 if (!refs) {
2959 if (view == VIEW(REQ_VIEW_DIFF))
2960 goto try_add_describe_ref;
2961 return;
2962 }
2964 do {
2965 struct ref *ref = refs[refpos];
2966 const char *fmt = ref->tag ? "%s[%s]" :
2967 ref->remote ? "%s<%s>" : "%s%s";
2969 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2970 return;
2971 sep = ", ";
2972 if (ref->tag)
2973 is_tag = TRUE;
2974 } while (refs[refpos++]->next);
2976 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2977 try_add_describe_ref:
2978 /* Add <tag>-g<commit_id> "fake" reference. */
2979 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2980 return;
2981 }
2983 if (bufpos == 0)
2984 return;
2986 if (!realloc_lines(view, view->line_size + 1))
2987 return;
2989 add_line_text(view, buf, LINE_PP_REFS);
2990 }
2992 static bool
2993 pager_read(struct view *view, char *data)
2994 {
2995 struct line *line;
2997 if (!data)
2998 return TRUE;
3000 line = add_line_text(view, data, get_line_type(data));
3001 if (!line)
3002 return FALSE;
3004 if (line->type == LINE_COMMIT &&
3005 (view == VIEW(REQ_VIEW_DIFF) ||
3006 view == VIEW(REQ_VIEW_LOG)))
3007 add_pager_refs(view, line);
3009 return TRUE;
3010 }
3012 static enum request
3013 pager_request(struct view *view, enum request request, struct line *line)
3014 {
3015 int split = 0;
3017 if (request != REQ_ENTER)
3018 return request;
3020 if (line->type == LINE_COMMIT &&
3021 (view == VIEW(REQ_VIEW_LOG) ||
3022 view == VIEW(REQ_VIEW_PAGER))) {
3023 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
3024 split = 1;
3025 }
3027 /* Always scroll the view even if it was split. That way
3028 * you can use Enter to scroll through the log view and
3029 * split open each commit diff. */
3030 scroll_view(view, REQ_SCROLL_LINE_DOWN);
3032 /* FIXME: A minor workaround. Scrolling the view will call report("")
3033 * but if we are scrolling a non-current view this won't properly
3034 * update the view title. */
3035 if (split)
3036 update_view_title(view);
3038 return REQ_NONE;
3039 }
3041 static bool
3042 pager_grep(struct view *view, struct line *line)
3043 {
3044 regmatch_t pmatch;
3045 char *text = line->data;
3047 if (!*text)
3048 return FALSE;
3050 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
3051 return FALSE;
3053 return TRUE;
3054 }
3056 static void
3057 pager_select(struct view *view, struct line *line)
3058 {
3059 if (line->type == LINE_COMMIT) {
3060 char *text = (char *)line->data + STRING_SIZE("commit ");
3062 if (view != VIEW(REQ_VIEW_PAGER))
3063 string_copy_rev(view->ref, text);
3064 string_copy_rev(ref_commit, text);
3065 }
3066 }
3068 static struct view_ops pager_ops = {
3069 "line",
3070 NULL,
3071 pager_read,
3072 pager_draw,
3073 pager_request,
3074 pager_grep,
3075 pager_select,
3076 };
3078 static enum request
3079 log_request(struct view *view, enum request request, struct line *line)
3080 {
3081 switch (request) {
3082 case REQ_REFRESH:
3083 load_refs();
3084 open_view(view, REQ_VIEW_LOG, OPEN_REFRESH);
3085 return REQ_NONE;
3086 default:
3087 return pager_request(view, request, line);
3088 }
3089 }
3091 static struct view_ops log_ops = {
3092 "line",
3093 NULL,
3094 pager_read,
3095 pager_draw,
3096 log_request,
3097 pager_grep,
3098 pager_select,
3099 };
3102 /*
3103 * Help backend
3104 */
3106 static bool
3107 help_open(struct view *view)
3108 {
3109 char buf[BUFSIZ];
3110 int lines = ARRAY_SIZE(req_info) + 2;
3111 int i;
3113 if (view->lines > 0)
3114 return TRUE;
3116 for (i = 0; i < ARRAY_SIZE(req_info); i++)
3117 if (!req_info[i].request)
3118 lines++;
3120 lines += run_requests + 1;
3122 view->line = calloc(lines, sizeof(*view->line));
3123 if (!view->line)
3124 return FALSE;
3126 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
3128 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
3129 const char *key;
3131 if (req_info[i].request == REQ_NONE)
3132 continue;
3134 if (!req_info[i].request) {
3135 add_line_text(view, "", LINE_DEFAULT);
3136 add_line_text(view, req_info[i].help, LINE_DEFAULT);
3137 continue;
3138 }
3140 key = get_key(req_info[i].request);
3141 if (!*key)
3142 key = "(no key defined)";
3144 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
3145 continue;
3147 add_line_text(view, buf, LINE_DEFAULT);
3148 }
3150 if (run_requests) {
3151 add_line_text(view, "", LINE_DEFAULT);
3152 add_line_text(view, "External commands:", LINE_DEFAULT);
3153 }
3155 for (i = 0; i < run_requests; i++) {
3156 struct run_request *req = get_run_request(REQ_NONE + i + 1);
3157 const char *key;
3159 if (!req)
3160 continue;
3162 key = get_key_name(req->key);
3163 if (!*key)
3164 key = "(no key defined)";
3166 if (!string_format(buf, " %-10s %-14s `%s`",
3167 keymap_table[req->keymap].name,
3168 key, req->cmd))
3169 continue;
3171 add_line_text(view, buf, LINE_DEFAULT);
3172 }
3174 return TRUE;
3175 }
3177 static struct view_ops help_ops = {
3178 "line",
3179 help_open,
3180 NULL,
3181 pager_draw,
3182 pager_request,
3183 pager_grep,
3184 pager_select,
3185 };
3188 /*
3189 * Tree backend
3190 */
3192 struct tree_stack_entry {
3193 struct tree_stack_entry *prev; /* Entry below this in the stack */
3194 unsigned long lineno; /* Line number to restore */
3195 char *name; /* Position of name in opt_path */
3196 };
3198 /* The top of the path stack. */
3199 static struct tree_stack_entry *tree_stack = NULL;
3200 unsigned long tree_lineno = 0;
3202 static void
3203 pop_tree_stack_entry(void)
3204 {
3205 struct tree_stack_entry *entry = tree_stack;
3207 tree_lineno = entry->lineno;
3208 entry->name[0] = 0;
3209 tree_stack = entry->prev;
3210 free(entry);
3211 }
3213 static void
3214 push_tree_stack_entry(const char *name, unsigned long lineno)
3215 {
3216 struct tree_stack_entry *entry = calloc(1, sizeof(*entry));
3217 size_t pathlen = strlen(opt_path);
3219 if (!entry)
3220 return;
3222 entry->prev = tree_stack;
3223 entry->name = opt_path + pathlen;
3224 tree_stack = entry;
3226 if (!string_format_from(opt_path, &pathlen, "%s/", name)) {
3227 pop_tree_stack_entry();
3228 return;
3229 }
3231 /* Move the current line to the first tree entry. */
3232 tree_lineno = 1;
3233 entry->lineno = lineno;
3234 }
3236 /* Parse output from git-ls-tree(1):
3237 *
3238 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
3239 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
3240 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
3241 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
3242 */
3244 #define SIZEOF_TREE_ATTR \
3245 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
3247 #define TREE_UP_FORMAT "040000 tree %s\t.."
3249 static int
3250 tree_compare_entry(enum line_type type1, const char *name1,
3251 enum line_type type2, const char *name2)
3252 {
3253 if (type1 != type2) {
3254 if (type1 == LINE_TREE_DIR)
3255 return -1;
3256 return 1;
3257 }
3259 return strcmp(name1, name2);
3260 }
3262 static const char *
3263 tree_path(struct line *line)
3264 {
3265 const char *path = line->data;
3267 return path + SIZEOF_TREE_ATTR;
3268 }
3270 static bool
3271 tree_read(struct view *view, char *text)
3272 {
3273 size_t textlen = text ? strlen(text) : 0;
3274 char buf[SIZEOF_STR];
3275 unsigned long pos;
3276 enum line_type type;
3277 bool first_read = view->lines == 0;
3279 if (!text)
3280 return TRUE;
3281 if (textlen <= SIZEOF_TREE_ATTR)
3282 return FALSE;
3284 type = text[STRING_SIZE("100644 ")] == 't'
3285 ? LINE_TREE_DIR : LINE_TREE_FILE;
3287 if (first_read) {
3288 /* Add path info line */
3289 if (!string_format(buf, "Directory path /%s", opt_path) ||
3290 !realloc_lines(view, view->line_size + 1) ||
3291 !add_line_text(view, buf, LINE_DEFAULT))
3292 return FALSE;
3294 /* Insert "link" to parent directory. */
3295 if (*opt_path) {
3296 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
3297 !realloc_lines(view, view->line_size + 1) ||
3298 !add_line_text(view, buf, LINE_TREE_DIR))
3299 return FALSE;
3300 }
3301 }
3303 /* Strip the path part ... */
3304 if (*opt_path) {
3305 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
3306 size_t striplen = strlen(opt_path);
3307 char *path = text + SIZEOF_TREE_ATTR;
3309 if (pathlen > striplen)
3310 memmove(path, path + striplen,
3311 pathlen - striplen + 1);
3312 }
3314 /* Skip "Directory ..." and ".." line. */
3315 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
3316 struct line *line = &view->line[pos];
3317 const char *path1 = tree_path(line);
3318 char *path2 = text + SIZEOF_TREE_ATTR;
3319 int cmp = tree_compare_entry(line->type, path1, type, path2);
3321 if (cmp <= 0)
3322 continue;
3324 text = strdup(text);
3325 if (!text)
3326 return FALSE;
3328 if (view->lines > pos)
3329 memmove(&view->line[pos + 1], &view->line[pos],
3330 (view->lines - pos) * sizeof(*line));
3332 line = &view->line[pos];
3333 line->data = text;
3334 line->type = type;
3335 view->lines++;
3336 return TRUE;
3337 }
3339 if (!add_line_text(view, text, type))
3340 return FALSE;
3342 if (tree_lineno > view->lineno) {
3343 view->lineno = tree_lineno;
3344 tree_lineno = 0;
3345 }
3347 return TRUE;
3348 }
3350 static enum request
3351 tree_request(struct view *view, enum request request, struct line *line)
3352 {
3353 enum open_flags flags;
3355 if (request == REQ_VIEW_BLAME) {
3356 const char *filename = tree_path(line);
3358 if (line->type == LINE_TREE_DIR) {
3359 report("Cannot show blame for directory %s", opt_path);
3360 return REQ_NONE;
3361 }
3363 string_copy(opt_ref, view->vid);
3364 string_format(opt_file, "%s%s", opt_path, filename);
3365 return request;
3366 }
3367 if (request == REQ_TREE_PARENT) {
3368 if (*opt_path) {
3369 /* fake 'cd ..' */
3370 request = REQ_ENTER;
3371 line = &view->line[1];
3372 } else {
3373 /* quit view if at top of tree */
3374 return REQ_VIEW_CLOSE;
3375 }
3376 }
3377 if (request != REQ_ENTER)
3378 return request;
3380 /* Cleanup the stack if the tree view is at a different tree. */
3381 while (!*opt_path && tree_stack)
3382 pop_tree_stack_entry();
3384 switch (line->type) {
3385 case LINE_TREE_DIR:
3386 /* Depending on whether it is a subdir or parent (updir?) link
3387 * mangle the path buffer. */
3388 if (line == &view->line[1] && *opt_path) {
3389 pop_tree_stack_entry();
3391 } else {
3392 const char *basename = tree_path(line);
3394 push_tree_stack_entry(basename, view->lineno);
3395 }
3397 /* Trees and subtrees share the same ID, so they are not not
3398 * unique like blobs. */
3399 flags = OPEN_RELOAD;
3400 request = REQ_VIEW_TREE;
3401 break;
3403 case LINE_TREE_FILE:
3404 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3405 request = REQ_VIEW_BLOB;
3406 break;
3408 default:
3409 return TRUE;
3410 }
3412 open_view(view, request, flags);
3413 if (request == REQ_VIEW_TREE) {
3414 view->lineno = tree_lineno;
3415 }
3417 return REQ_NONE;
3418 }
3420 static void
3421 tree_select(struct view *view, struct line *line)
3422 {
3423 char *text = (char *)line->data + STRING_SIZE("100644 blob ");
3425 if (line->type == LINE_TREE_FILE) {
3426 string_copy_rev(ref_blob, text);
3428 } else if (line->type != LINE_TREE_DIR) {
3429 return;
3430 }
3432 string_copy_rev(view->ref, text);
3433 }
3435 static struct view_ops tree_ops = {
3436 "file",
3437 NULL,
3438 tree_read,
3439 pager_draw,
3440 tree_request,
3441 pager_grep,
3442 tree_select,
3443 };
3445 static bool
3446 blob_read(struct view *view, char *line)
3447 {
3448 if (!line)
3449 return TRUE;
3450 return add_line_text(view, line, LINE_DEFAULT) != NULL;
3451 }
3453 static struct view_ops blob_ops = {
3454 "line",
3455 NULL,
3456 blob_read,
3457 pager_draw,
3458 pager_request,
3459 pager_grep,
3460 pager_select,
3461 };
3463 /*
3464 * Blame backend
3465 *
3466 * Loading the blame view is a two phase job:
3467 *
3468 * 1. File content is read either using opt_file from the
3469 * filesystem or using git-cat-file.
3470 * 2. Then blame information is incrementally added by
3471 * reading output from git-blame.
3472 */
3474 struct blame_commit {
3475 char id[SIZEOF_REV]; /* SHA1 ID. */
3476 char title[128]; /* First line of the commit message. */
3477 char author[75]; /* Author of the commit. */
3478 struct tm time; /* Date from the author ident. */
3479 char filename[128]; /* Name of file. */
3480 };
3482 struct blame {
3483 struct blame_commit *commit;
3484 unsigned int header:1;
3485 char text[1];
3486 };
3488 #define BLAME_CAT_FILE_CMD "git cat-file blob %s:%s"
3489 #define BLAME_INCREMENTAL_CMD "git blame --incremental %s -- %s"
3491 static bool
3492 blame_open(struct view *view)
3493 {
3494 char path[SIZEOF_STR];
3495 char ref[SIZEOF_STR] = "";
3497 if (sq_quote(path, 0, opt_file) >= sizeof(path))
3498 return FALSE;
3500 if (*opt_ref && sq_quote(ref, 0, opt_ref) >= sizeof(ref))
3501 return FALSE;
3503 if (*opt_ref) {
3504 if (!string_format(view->cmd, BLAME_CAT_FILE_CMD, ref, path))
3505 return FALSE;
3506 } else {
3507 view->pipe = fopen(opt_file, "r");
3508 if (!view->pipe &&
3509 !string_format(view->cmd, BLAME_CAT_FILE_CMD, "HEAD", path))
3510 return FALSE;
3511 }
3513 if (!view->pipe)
3514 view->pipe = popen(view->cmd, "r");
3515 if (!view->pipe)
3516 return FALSE;
3518 if (!string_format(view->cmd, BLAME_INCREMENTAL_CMD, ref, path))
3519 return FALSE;
3521 reset_view(view);
3522 string_format(view->ref, "%s ...", opt_file);
3523 string_copy_rev(view->vid, opt_file);
3524 set_nonblocking_input(TRUE);
3525 view->start_time = time(NULL);
3527 return TRUE;
3528 }
3530 static struct blame_commit *
3531 get_blame_commit(struct view *view, const char *id)
3532 {
3533 size_t i;
3535 for (i = 0; i < view->lines; i++) {
3536 struct blame *blame = view->line[i].data;
3538 if (!blame->commit)
3539 continue;
3541 if (!strncmp(blame->commit->id, id, SIZEOF_REV - 1))
3542 return blame->commit;
3543 }
3545 {
3546 struct blame_commit *commit = calloc(1, sizeof(*commit));
3548 if (commit)
3549 string_ncopy(commit->id, id, SIZEOF_REV);
3550 return commit;
3551 }
3552 }
3554 static bool
3555 parse_number(const char **posref, size_t *number, size_t min, size_t max)
3556 {
3557 const char *pos = *posref;
3559 *posref = NULL;
3560 pos = strchr(pos + 1, ' ');
3561 if (!pos || !isdigit(pos[1]))
3562 return FALSE;
3563 *number = atoi(pos + 1);
3564 if (*number < min || *number > max)
3565 return FALSE;
3567 *posref = pos;
3568 return TRUE;
3569 }
3571 static struct blame_commit *
3572 parse_blame_commit(struct view *view, const char *text, int *blamed)
3573 {
3574 struct blame_commit *commit;
3575 struct blame *blame;
3576 const char *pos = text + SIZEOF_REV - 1;
3577 size_t lineno;
3578 size_t group;
3580 if (strlen(text) <= SIZEOF_REV || *pos != ' ')
3581 return NULL;
3583 if (!parse_number(&pos, &lineno, 1, view->lines) ||
3584 !parse_number(&pos, &group, 1, view->lines - lineno + 1))
3585 return NULL;
3587 commit = get_blame_commit(view, text);
3588 if (!commit)
3589 return NULL;
3591 *blamed += group;
3592 while (group--) {
3593 struct line *line = &view->line[lineno + group - 1];
3595 blame = line->data;
3596 blame->commit = commit;
3597 blame->header = !group;
3598 line->dirty = 1;
3599 }
3601 return commit;
3602 }
3604 static bool
3605 blame_read_file(struct view *view, const char *line)
3606 {
3607 if (!line) {
3608 FILE *pipe = NULL;
3610 if (view->lines > 0)
3611 pipe = popen(view->cmd, "r");
3612 else if (!view->parent)
3613 die("No blame exist for %s", view->vid);
3614 view->cmd[0] = 0;
3615 if (!pipe) {
3616 report("Failed to load blame data");
3617 return TRUE;
3618 }
3620 fclose(view->pipe);
3621 view->pipe = pipe;
3622 return FALSE;
3624 } else {
3625 size_t linelen = strlen(line);
3626 struct blame *blame = malloc(sizeof(*blame) + linelen);
3628 blame->commit = NULL;
3629 strncpy(blame->text, line, linelen);
3630 blame->text[linelen] = 0;
3631 return add_line_data(view, blame, LINE_BLAME_ID) != NULL;
3632 }
3633 }
3635 static bool
3636 match_blame_header(const char *name, char **line)
3637 {
3638 size_t namelen = strlen(name);
3639 bool matched = !strncmp(name, *line, namelen);
3641 if (matched)
3642 *line += namelen;
3644 return matched;
3645 }
3647 static bool
3648 blame_read(struct view *view, char *line)
3649 {
3650 static struct blame_commit *commit = NULL;
3651 static int blamed = 0;
3652 static time_t author_time;
3654 if (*view->cmd)
3655 return blame_read_file(view, line);
3657 if (!line) {
3658 /* Reset all! */
3659 commit = NULL;
3660 blamed = 0;
3661 string_format(view->ref, "%s", view->vid);
3662 if (view_is_displayed(view)) {
3663 update_view_title(view);
3664 redraw_view_from(view, 0);
3665 }
3666 return TRUE;
3667 }
3669 if (!commit) {
3670 commit = parse_blame_commit(view, line, &blamed);
3671 string_format(view->ref, "%s %2d%%", view->vid,
3672 blamed * 100 / view->lines);
3674 } else if (match_blame_header("author ", &line)) {
3675 string_ncopy(commit->author, line, strlen(line));
3677 } else if (match_blame_header("author-time ", &line)) {
3678 author_time = (time_t) atol(line);
3680 } else if (match_blame_header("author-tz ", &line)) {
3681 long tz;
3683 tz = ('0' - line[1]) * 60 * 60 * 10;
3684 tz += ('0' - line[2]) * 60 * 60;
3685 tz += ('0' - line[3]) * 60;
3686 tz += ('0' - line[4]) * 60;
3688 if (line[0] == '-')
3689 tz = -tz;
3691 author_time -= tz;
3692 gmtime_r(&author_time, &commit->time);
3694 } else if (match_blame_header("summary ", &line)) {
3695 string_ncopy(commit->title, line, strlen(line));
3697 } else if (match_blame_header("filename ", &line)) {
3698 string_ncopy(commit->filename, line, strlen(line));
3699 commit = NULL;
3700 }
3702 return TRUE;
3703 }
3705 static bool
3706 blame_draw(struct view *view, struct line *line, unsigned int lineno)
3707 {
3708 struct blame *blame = line->data;
3709 struct tm *time = NULL;
3710 const char *id = NULL, *author = NULL;
3712 if (blame->commit && *blame->commit->filename) {
3713 id = blame->commit->id;
3714 author = blame->commit->author;
3715 time = &blame->commit->time;
3716 }
3718 if (opt_date && draw_date(view, time))
3719 return TRUE;
3721 if (opt_author &&
3722 draw_field(view, LINE_MAIN_AUTHOR, author, opt_author_cols, TRUE))
3723 return TRUE;
3725 if (draw_field(view, LINE_BLAME_ID, id, ID_COLS, FALSE))
3726 return TRUE;
3728 if (draw_lineno(view, lineno))
3729 return TRUE;
3731 draw_text(view, LINE_DEFAULT, blame->text, TRUE);
3732 return TRUE;
3733 }
3735 static enum request
3736 blame_request(struct view *view, enum request request, struct line *line)
3737 {
3738 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3739 struct blame *blame = line->data;
3741 switch (request) {
3742 case REQ_ENTER:
3743 if (!blame->commit) {
3744 report("No commit loaded yet");
3745 break;
3746 }
3748 if (!strcmp(blame->commit->id, NULL_ID)) {
3749 char path[SIZEOF_STR];
3751 if (sq_quote(path, 0, view->vid) >= sizeof(path))
3752 break;
3753 string_format(opt_cmd, "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s 2>/dev/null", path);
3754 }
3756 open_view(view, REQ_VIEW_DIFF, flags);
3757 break;
3759 default:
3760 return request;
3761 }
3763 return REQ_NONE;
3764 }
3766 static bool
3767 blame_grep(struct view *view, struct line *line)
3768 {
3769 struct blame *blame = line->data;
3770 struct blame_commit *commit = blame->commit;
3771 regmatch_t pmatch;
3773 #define MATCH(text, on) \
3774 (on && *text && regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3776 if (commit) {
3777 char buf[DATE_COLS + 1];
3779 if (MATCH(commit->title, 1) ||
3780 MATCH(commit->author, opt_author) ||
3781 MATCH(commit->id, opt_date))
3782 return TRUE;
3784 if (strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time) &&
3785 MATCH(buf, 1))
3786 return TRUE;
3787 }
3789 return MATCH(blame->text, 1);
3791 #undef MATCH
3792 }
3794 static void
3795 blame_select(struct view *view, struct line *line)
3796 {
3797 struct blame *blame = line->data;
3798 struct blame_commit *commit = blame->commit;
3800 if (!commit)
3801 return;
3803 if (!strcmp(commit->id, NULL_ID))
3804 string_ncopy(ref_commit, "HEAD", 4);
3805 else
3806 string_copy_rev(ref_commit, commit->id);
3807 }
3809 static struct view_ops blame_ops = {
3810 "line",
3811 blame_open,
3812 blame_read,
3813 blame_draw,
3814 blame_request,
3815 blame_grep,
3816 blame_select,
3817 };
3819 /*
3820 * Status backend
3821 */
3823 struct status {
3824 char status;
3825 struct {
3826 mode_t mode;
3827 char rev[SIZEOF_REV];
3828 char name[SIZEOF_STR];
3829 } old;
3830 struct {
3831 mode_t mode;
3832 char rev[SIZEOF_REV];
3833 char name[SIZEOF_STR];
3834 } new;
3835 };
3837 static char status_onbranch[SIZEOF_STR];
3838 static struct status stage_status;
3839 static enum line_type stage_line_type;
3840 static size_t stage_chunks;
3841 static int *stage_chunk;
3843 /* This should work even for the "On branch" line. */
3844 static inline bool
3845 status_has_none(struct view *view, struct line *line)
3846 {
3847 return line < view->line + view->lines && !line[1].data;
3848 }
3850 /* Get fields from the diff line:
3851 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
3852 */
3853 static inline bool
3854 status_get_diff(struct status *file, const char *buf, size_t bufsize)
3855 {
3856 const char *old_mode = buf + 1;
3857 const char *new_mode = buf + 8;
3858 const char *old_rev = buf + 15;
3859 const char *new_rev = buf + 56;
3860 const char *status = buf + 97;
3862 if (bufsize < 99 ||
3863 old_mode[-1] != ':' ||
3864 new_mode[-1] != ' ' ||
3865 old_rev[-1] != ' ' ||
3866 new_rev[-1] != ' ' ||
3867 status[-1] != ' ')
3868 return FALSE;
3870 file->status = *status;
3872 string_copy_rev(file->old.rev, old_rev);
3873 string_copy_rev(file->new.rev, new_rev);
3875 file->old.mode = strtoul(old_mode, NULL, 8);
3876 file->new.mode = strtoul(new_mode, NULL, 8);
3878 file->old.name[0] = file->new.name[0] = 0;
3880 return TRUE;
3881 }
3883 static bool
3884 status_run(struct view *view, const char cmd[], char status, enum line_type type)
3885 {
3886 struct status *file = NULL;
3887 struct status *unmerged = NULL;
3888 char buf[SIZEOF_STR * 4];
3889 size_t bufsize = 0;
3890 FILE *pipe;
3892 pipe = popen(cmd, "r");
3893 if (!pipe)
3894 return FALSE;
3896 add_line_data(view, NULL, type);
3898 while (!feof(pipe) && !ferror(pipe)) {
3899 char *sep;
3900 size_t readsize;
3902 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
3903 if (!readsize)
3904 break;
3905 bufsize += readsize;
3907 /* Process while we have NUL chars. */
3908 while ((sep = memchr(buf, 0, bufsize))) {
3909 size_t sepsize = sep - buf + 1;
3911 if (!file) {
3912 if (!realloc_lines(view, view->line_size + 1))
3913 goto error_out;
3915 file = calloc(1, sizeof(*file));
3916 if (!file)
3917 goto error_out;
3919 add_line_data(view, file, type);
3920 }
3922 /* Parse diff info part. */
3923 if (status) {
3924 file->status = status;
3925 if (status == 'A')
3926 string_copy(file->old.rev, NULL_ID);
3928 } else if (!file->status) {
3929 if (!status_get_diff(file, buf, sepsize))
3930 goto error_out;
3932 bufsize -= sepsize;
3933 memmove(buf, sep + 1, bufsize);
3935 sep = memchr(buf, 0, bufsize);
3936 if (!sep)
3937 break;
3938 sepsize = sep - buf + 1;
3940 /* Collapse all 'M'odified entries that
3941 * follow a associated 'U'nmerged entry.
3942 */
3943 if (file->status == 'U') {
3944 unmerged = file;
3946 } else if (unmerged) {
3947 int collapse = !strcmp(buf, unmerged->new.name);
3949 unmerged = NULL;
3950 if (collapse) {
3951 free(file);
3952 view->lines--;
3953 continue;
3954 }
3955 }
3956 }
3958 /* Grab the old name for rename/copy. */
3959 if (!*file->old.name &&
3960 (file->status == 'R' || file->status == 'C')) {
3961 sepsize = sep - buf + 1;
3962 string_ncopy(file->old.name, buf, sepsize);
3963 bufsize -= sepsize;
3964 memmove(buf, sep + 1, bufsize);
3966 sep = memchr(buf, 0, bufsize);
3967 if (!sep)
3968 break;
3969 sepsize = sep - buf + 1;
3970 }
3972 /* git-ls-files just delivers a NUL separated
3973 * list of file names similar to the second half
3974 * of the git-diff-* output. */
3975 string_ncopy(file->new.name, buf, sepsize);
3976 if (!*file->old.name)
3977 string_copy(file->old.name, file->new.name);
3978 bufsize -= sepsize;
3979 memmove(buf, sep + 1, bufsize);
3980 file = NULL;
3981 }
3982 }
3984 if (ferror(pipe)) {
3985 error_out:
3986 pclose(pipe);
3987 return FALSE;
3988 }
3990 if (!view->line[view->lines - 1].data)
3991 add_line_data(view, NULL, LINE_STAT_NONE);
3993 pclose(pipe);
3994 return TRUE;
3995 }
3997 /* Don't show unmerged entries in the staged section. */
3998 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached -M HEAD"
3999 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
4000 #define STATUS_LIST_OTHER_CMD \
4001 "git ls-files -z --others --exclude-standard"
4002 #define STATUS_LIST_NO_HEAD_CMD \
4003 "git ls-files -z --cached --exclude-standard"
4005 #define STATUS_DIFF_INDEX_SHOW_CMD \
4006 "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s %s 2>/dev/null"
4008 #define STATUS_DIFF_FILES_SHOW_CMD \
4009 "git diff-files --root --patch-with-stat -C -M -- %s %s 2>/dev/null"
4011 #define STATUS_DIFF_NO_HEAD_SHOW_CMD \
4012 "git diff --no-color --patch-with-stat /dev/null %s 2>/dev/null"
4014 /* First parse staged info using git-diff-index(1), then parse unstaged
4015 * info using git-diff-files(1), and finally untracked files using
4016 * git-ls-files(1). */
4017 static bool
4018 status_open(struct view *view)
4019 {
4020 unsigned long prev_lineno = view->lineno;
4022 reset_view(view);
4024 if (!realloc_lines(view, view->line_size + 7))
4025 return FALSE;
4027 add_line_data(view, NULL, LINE_STAT_HEAD);
4028 if (opt_no_head)
4029 string_copy(status_onbranch, "Initial commit");
4030 else if (!*opt_head)
4031 string_copy(status_onbranch, "Not currently on any branch");
4032 else if (!string_format(status_onbranch, "On branch %s", opt_head))
4033 return FALSE;
4035 system("git update-index -q --refresh >/dev/null 2>/dev/null");
4037 if (opt_no_head) {
4038 if (!status_run(view, STATUS_LIST_NO_HEAD_CMD, 'A', LINE_STAT_STAGED))
4039 return FALSE;
4040 } else if (!status_run(view, STATUS_DIFF_INDEX_CMD, 0, LINE_STAT_STAGED)) {
4041 return FALSE;
4042 }
4044 if (!status_run(view, STATUS_DIFF_FILES_CMD, 0, LINE_STAT_UNSTAGED) ||
4045 !status_run(view, STATUS_LIST_OTHER_CMD, '?', LINE_STAT_UNTRACKED))
4046 return FALSE;
4048 /* If all went well restore the previous line number to stay in
4049 * the context or select a line with something that can be
4050 * updated. */
4051 if (prev_lineno >= view->lines)
4052 prev_lineno = view->lines - 1;
4053 while (prev_lineno < view->lines && !view->line[prev_lineno].data)
4054 prev_lineno++;
4055 while (prev_lineno > 0 && !view->line[prev_lineno].data)
4056 prev_lineno--;
4058 /* If the above fails, always skip the "On branch" line. */
4059 if (prev_lineno < view->lines)
4060 view->lineno = prev_lineno;
4061 else
4062 view->lineno = 1;
4064 if (view->lineno < view->offset)
4065 view->offset = view->lineno;
4066 else if (view->offset + view->height <= view->lineno)
4067 view->offset = view->lineno - view->height + 1;
4069 return TRUE;
4070 }
4072 static bool
4073 status_draw(struct view *view, struct line *line, unsigned int lineno)
4074 {
4075 struct status *status = line->data;
4076 enum line_type type;
4077 const char *text;
4079 if (!status) {
4080 switch (line->type) {
4081 case LINE_STAT_STAGED:
4082 type = LINE_STAT_SECTION;
4083 text = "Changes to be committed:";
4084 break;
4086 case LINE_STAT_UNSTAGED:
4087 type = LINE_STAT_SECTION;
4088 text = "Changed but not updated:";
4089 break;
4091 case LINE_STAT_UNTRACKED:
4092 type = LINE_STAT_SECTION;
4093 text = "Untracked files:";
4094 break;
4096 case LINE_STAT_NONE:
4097 type = LINE_DEFAULT;
4098 text = " (no files)";
4099 break;
4101 case LINE_STAT_HEAD:
4102 type = LINE_STAT_HEAD;
4103 text = status_onbranch;
4104 break;
4106 default:
4107 return FALSE;
4108 }
4109 } else {
4110 static char buf[] = { '?', ' ', ' ', ' ', 0 };
4112 buf[0] = status->status;
4113 if (draw_text(view, line->type, buf, TRUE))
4114 return TRUE;
4115 type = LINE_DEFAULT;
4116 text = status->new.name;
4117 }
4119 draw_text(view, type, text, TRUE);
4120 return TRUE;
4121 }
4123 static enum request
4124 status_enter(struct view *view, struct line *line)
4125 {
4126 struct status *status = line->data;
4127 char oldpath[SIZEOF_STR] = "";
4128 char newpath[SIZEOF_STR] = "";
4129 const char *info;
4130 size_t cmdsize = 0;
4131 enum open_flags split;
4133 if (line->type == LINE_STAT_NONE ||
4134 (!status && line[1].type == LINE_STAT_NONE)) {
4135 report("No file to diff");
4136 return REQ_NONE;
4137 }
4139 if (status) {
4140 if (sq_quote(oldpath, 0, status->old.name) >= sizeof(oldpath))
4141 return REQ_QUIT;
4142 /* Diffs for unmerged entries are empty when pasing the
4143 * new path, so leave it empty. */
4144 if (status->status != 'U' &&
4145 sq_quote(newpath, 0, status->new.name) >= sizeof(newpath))
4146 return REQ_QUIT;
4147 }
4149 if (opt_cdup[0] &&
4150 line->type != LINE_STAT_UNTRACKED &&
4151 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
4152 return REQ_QUIT;
4154 switch (line->type) {
4155 case LINE_STAT_STAGED:
4156 if (opt_no_head) {
4157 if (!string_format_from(opt_cmd, &cmdsize,
4158 STATUS_DIFF_NO_HEAD_SHOW_CMD,
4159 newpath))
4160 return REQ_QUIT;
4161 } else {
4162 if (!string_format_from(opt_cmd, &cmdsize,
4163 STATUS_DIFF_INDEX_SHOW_CMD,
4164 oldpath, newpath))
4165 return REQ_QUIT;
4166 }
4168 if (status)
4169 info = "Staged changes to %s";
4170 else
4171 info = "Staged changes";
4172 break;
4174 case LINE_STAT_UNSTAGED:
4175 if (!string_format_from(opt_cmd, &cmdsize,
4176 STATUS_DIFF_FILES_SHOW_CMD, oldpath, newpath))
4177 return REQ_QUIT;
4178 if (status)
4179 info = "Unstaged changes to %s";
4180 else
4181 info = "Unstaged changes";
4182 break;
4184 case LINE_STAT_UNTRACKED:
4185 if (opt_pipe)
4186 return REQ_QUIT;
4188 if (!status) {
4189 report("No file to show");
4190 return REQ_NONE;
4191 }
4193 opt_pipe = fopen(status->new.name, "r");
4194 info = "Untracked file %s";
4195 break;
4197 case LINE_STAT_HEAD:
4198 return REQ_NONE;
4200 default:
4201 die("line type %d not handled in switch", line->type);
4202 }
4204 split = view_is_displayed(view) ? OPEN_SPLIT : 0;
4205 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | split);
4206 if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) {
4207 if (status) {
4208 stage_status = *status;
4209 } else {
4210 memset(&stage_status, 0, sizeof(stage_status));
4211 }
4213 stage_line_type = line->type;
4214 stage_chunks = 0;
4215 string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.new.name);
4216 }
4218 return REQ_NONE;
4219 }
4221 static bool
4222 status_exists(struct status *status, enum line_type type)
4223 {
4224 struct view *view = VIEW(REQ_VIEW_STATUS);
4225 struct line *line;
4227 for (line = view->line; line < view->line + view->lines; line++) {
4228 struct status *pos = line->data;
4230 if (line->type == type && pos &&
4231 !strcmp(status->new.name, pos->new.name))
4232 return TRUE;
4233 }
4235 return FALSE;
4236 }
4239 static FILE *
4240 status_update_prepare(enum line_type type)
4241 {
4242 char cmd[SIZEOF_STR];
4243 size_t cmdsize = 0;
4245 if (opt_cdup[0] &&
4246 type != LINE_STAT_UNTRACKED &&
4247 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4248 return NULL;
4250 switch (type) {
4251 case LINE_STAT_STAGED:
4252 string_add(cmd, cmdsize, "git update-index -z --index-info");
4253 break;
4255 case LINE_STAT_UNSTAGED:
4256 case LINE_STAT_UNTRACKED:
4257 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
4258 break;
4260 default:
4261 die("line type %d not handled in switch", type);
4262 }
4264 return popen(cmd, "w");
4265 }
4267 static bool
4268 status_update_write(FILE *pipe, struct status *status, enum line_type type)
4269 {
4270 char buf[SIZEOF_STR];
4271 size_t bufsize = 0;
4272 size_t written = 0;
4274 switch (type) {
4275 case LINE_STAT_STAGED:
4276 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
4277 status->old.mode,
4278 status->old.rev,
4279 status->old.name, 0))
4280 return FALSE;
4281 break;
4283 case LINE_STAT_UNSTAGED:
4284 case LINE_STAT_UNTRACKED:
4285 if (!string_format_from(buf, &bufsize, "%s%c", status->new.name, 0))
4286 return FALSE;
4287 break;
4289 default:
4290 die("line type %d not handled in switch", type);
4291 }
4293 while (!ferror(pipe) && written < bufsize) {
4294 written += fwrite(buf + written, 1, bufsize - written, pipe);
4295 }
4297 return written == bufsize;
4298 }
4300 static bool
4301 status_update_file(struct status *status, enum line_type type)
4302 {
4303 FILE *pipe = status_update_prepare(type);
4304 bool result;
4306 if (!pipe)
4307 return FALSE;
4309 result = status_update_write(pipe, status, type);
4310 pclose(pipe);
4311 return result;
4312 }
4314 static bool
4315 status_update_files(struct view *view, struct line *line)
4316 {
4317 FILE *pipe = status_update_prepare(line->type);
4318 bool result = TRUE;
4319 struct line *pos = view->line + view->lines;
4320 int files = 0;
4321 int file, done;
4323 if (!pipe)
4324 return FALSE;
4326 for (pos = line; pos < view->line + view->lines && pos->data; pos++)
4327 files++;
4329 for (file = 0, done = 0; result && file < files; line++, file++) {
4330 int almost_done = file * 100 / files;
4332 if (almost_done > done) {
4333 done = almost_done;
4334 string_format(view->ref, "updating file %u of %u (%d%% done)",
4335 file, files, done);
4336 update_view_title(view);
4337 }
4338 result = status_update_write(pipe, line->data, line->type);
4339 }
4341 pclose(pipe);
4342 return result;
4343 }
4345 static bool
4346 status_update(struct view *view)
4347 {
4348 struct line *line = &view->line[view->lineno];
4350 assert(view->lines);
4352 if (!line->data) {
4353 /* This should work even for the "On branch" line. */
4354 if (line < view->line + view->lines && !line[1].data) {
4355 report("Nothing to update");
4356 return FALSE;
4357 }
4359 if (!status_update_files(view, line + 1)) {
4360 report("Failed to update file status");
4361 return FALSE;
4362 }
4364 } else if (!status_update_file(line->data, line->type)) {
4365 report("Failed to update file status");
4366 return FALSE;
4367 }
4369 return TRUE;
4370 }
4372 static bool
4373 status_revert(struct status *status, enum line_type type, bool has_none)
4374 {
4375 if (!status || type != LINE_STAT_UNSTAGED) {
4376 if (type == LINE_STAT_STAGED) {
4377 report("Cannot revert changes to staged files");
4378 } else if (type == LINE_STAT_UNTRACKED) {
4379 report("Cannot revert changes to untracked files");
4380 } else if (has_none) {
4381 report("Nothing to revert");
4382 } else {
4383 report("Cannot revert changes to multiple files");
4384 }
4385 return FALSE;
4387 } else {
4388 char cmd[SIZEOF_STR];
4389 char file_sq[SIZEOF_STR];
4391 if (sq_quote(file_sq, 0, status->old.name) >= sizeof(file_sq) ||
4392 !string_format(cmd, "git checkout -- %s%s", opt_cdup, file_sq))
4393 return FALSE;
4395 return run_confirm(cmd, "Are you sure you want to overwrite any changes?");
4396 }
4397 }
4399 static enum request
4400 status_request(struct view *view, enum request request, struct line *line)
4401 {
4402 struct status *status = line->data;
4404 switch (request) {
4405 case REQ_STATUS_UPDATE:
4406 if (!status_update(view))
4407 return REQ_NONE;
4408 break;
4410 case REQ_STATUS_REVERT:
4411 if (!status_revert(status, line->type, status_has_none(view, line)))
4412 return REQ_NONE;
4413 break;
4415 case REQ_STATUS_MERGE:
4416 if (!status || status->status != 'U') {
4417 report("Merging only possible for files with unmerged status ('U').");
4418 return REQ_NONE;
4419 }
4420 open_mergetool(status->new.name);
4421 break;
4423 case REQ_EDIT:
4424 if (!status)
4425 return request;
4427 open_editor(status->status != '?', status->new.name);
4428 break;
4430 case REQ_VIEW_BLAME:
4431 if (status) {
4432 string_copy(opt_file, status->new.name);
4433 opt_ref[0] = 0;
4434 }
4435 return request;
4437 case REQ_ENTER:
4438 /* After returning the status view has been split to
4439 * show the stage view. No further reloading is
4440 * necessary. */
4441 status_enter(view, line);
4442 return REQ_NONE;
4444 case REQ_REFRESH:
4445 /* Simply reload the view. */
4446 break;
4448 default:
4449 return request;
4450 }
4452 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
4454 return REQ_NONE;
4455 }
4457 static void
4458 status_select(struct view *view, struct line *line)
4459 {
4460 struct status *status = line->data;
4461 char file[SIZEOF_STR] = "all files";
4462 const char *text;
4463 const char *key;
4465 if (status && !string_format(file, "'%s'", status->new.name))
4466 return;
4468 if (!status && line[1].type == LINE_STAT_NONE)
4469 line++;
4471 switch (line->type) {
4472 case LINE_STAT_STAGED:
4473 text = "Press %s to unstage %s for commit";
4474 break;
4476 case LINE_STAT_UNSTAGED:
4477 text = "Press %s to stage %s for commit";
4478 break;
4480 case LINE_STAT_UNTRACKED:
4481 text = "Press %s to stage %s for addition";
4482 break;
4484 case LINE_STAT_HEAD:
4485 case LINE_STAT_NONE:
4486 text = "Nothing to update";
4487 break;
4489 default:
4490 die("line type %d not handled in switch", line->type);
4491 }
4493 if (status && status->status == 'U') {
4494 text = "Press %s to resolve conflict in %s";
4495 key = get_key(REQ_STATUS_MERGE);
4497 } else {
4498 key = get_key(REQ_STATUS_UPDATE);
4499 }
4501 string_format(view->ref, text, key, file);
4502 }
4504 static bool
4505 status_grep(struct view *view, struct line *line)
4506 {
4507 struct status *status = line->data;
4508 enum { S_STATUS, S_NAME, S_END } state;
4509 char buf[2] = "?";
4510 regmatch_t pmatch;
4512 if (!status)
4513 return FALSE;
4515 for (state = S_STATUS; state < S_END; state++) {
4516 const char *text;
4518 switch (state) {
4519 case S_NAME: text = status->new.name; break;
4520 case S_STATUS:
4521 buf[0] = status->status;
4522 text = buf;
4523 break;
4525 default:
4526 return FALSE;
4527 }
4529 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4530 return TRUE;
4531 }
4533 return FALSE;
4534 }
4536 static struct view_ops status_ops = {
4537 "file",
4538 status_open,
4539 NULL,
4540 status_draw,
4541 status_request,
4542 status_grep,
4543 status_select,
4544 };
4547 static bool
4548 stage_diff_line(FILE *pipe, struct line *line)
4549 {
4550 const char *buf = line->data;
4551 size_t bufsize = strlen(buf);
4552 size_t written = 0;
4554 while (!ferror(pipe) && written < bufsize) {
4555 written += fwrite(buf + written, 1, bufsize - written, pipe);
4556 }
4558 fputc('\n', pipe);
4560 return written == bufsize;
4561 }
4563 static bool
4564 stage_diff_write(FILE *pipe, struct line *line, struct line *end)
4565 {
4566 while (line < end) {
4567 if (!stage_diff_line(pipe, line++))
4568 return FALSE;
4569 if (line->type == LINE_DIFF_CHUNK ||
4570 line->type == LINE_DIFF_HEADER)
4571 break;
4572 }
4574 return TRUE;
4575 }
4577 static struct line *
4578 stage_diff_find(struct view *view, struct line *line, enum line_type type)
4579 {
4580 for (; view->line < line; line--)
4581 if (line->type == type)
4582 return line;
4584 return NULL;
4585 }
4587 static bool
4588 stage_apply_chunk(struct view *view, struct line *chunk, bool revert)
4589 {
4590 char cmd[SIZEOF_STR];
4591 size_t cmdsize = 0;
4592 struct line *diff_hdr;
4593 FILE *pipe;
4595 diff_hdr = stage_diff_find(view, chunk, LINE_DIFF_HEADER);
4596 if (!diff_hdr)
4597 return FALSE;
4599 if (opt_cdup[0] &&
4600 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4601 return FALSE;
4603 if (!string_format_from(cmd, &cmdsize,
4604 "git apply --whitespace=nowarn %s %s - && "
4605 "git update-index -q --unmerged --refresh 2>/dev/null",
4606 revert ? "" : "--cached",
4607 revert || stage_line_type == LINE_STAT_STAGED ? "-R" : ""))
4608 return FALSE;
4610 pipe = popen(cmd, "w");
4611 if (!pipe)
4612 return FALSE;
4614 if (!stage_diff_write(pipe, diff_hdr, chunk) ||
4615 !stage_diff_write(pipe, chunk, view->line + view->lines))
4616 chunk = NULL;
4618 pclose(pipe);
4620 return chunk ? TRUE : FALSE;
4621 }
4623 static bool
4624 stage_update(struct view *view, struct line *line)
4625 {
4626 struct line *chunk = NULL;
4628 if (!opt_no_head && stage_line_type != LINE_STAT_UNTRACKED)
4629 chunk = stage_diff_find(view, line, LINE_DIFF_CHUNK);
4631 if (chunk) {
4632 if (!stage_apply_chunk(view, chunk, FALSE)) {
4633 report("Failed to apply chunk");
4634 return FALSE;
4635 }
4637 } else if (!stage_status.status) {
4638 view = VIEW(REQ_VIEW_STATUS);
4640 for (line = view->line; line < view->line + view->lines; line++)
4641 if (line->type == stage_line_type)
4642 break;
4644 if (!status_update_files(view, line + 1)) {
4645 report("Failed to update files");
4646 return FALSE;
4647 }
4649 } else if (!status_update_file(&stage_status, stage_line_type)) {
4650 report("Failed to update file");
4651 return FALSE;
4652 }
4654 return TRUE;
4655 }
4657 static bool
4658 stage_revert(struct view *view, struct line *line)
4659 {
4660 struct line *chunk = NULL;
4662 if (!opt_no_head && stage_line_type == LINE_STAT_UNSTAGED)
4663 chunk = stage_diff_find(view, line, LINE_DIFF_CHUNK);
4665 if (chunk) {
4666 if (!prompt_yesno("Are you sure you want to revert changes?"))
4667 return FALSE;
4669 if (!stage_apply_chunk(view, chunk, TRUE)) {
4670 report("Failed to revert chunk");
4671 return FALSE;
4672 }
4673 return TRUE;
4675 } else {
4676 return status_revert(stage_status.status ? &stage_status : NULL,
4677 stage_line_type, FALSE);
4678 }
4679 }
4682 static void
4683 stage_next(struct view *view, struct line *line)
4684 {
4685 int i;
4687 if (!stage_chunks) {
4688 static size_t alloc = 0;
4689 int *tmp;
4691 for (line = view->line; line < view->line + view->lines; line++) {
4692 if (line->type != LINE_DIFF_CHUNK)
4693 continue;
4695 tmp = realloc_items(stage_chunk, &alloc,
4696 stage_chunks, sizeof(*tmp));
4697 if (!tmp) {
4698 report("Allocation failure");
4699 return;
4700 }
4702 stage_chunk = tmp;
4703 stage_chunk[stage_chunks++] = line - view->line;
4704 }
4705 }
4707 for (i = 0; i < stage_chunks; i++) {
4708 if (stage_chunk[i] > view->lineno) {
4709 do_scroll_view(view, stage_chunk[i] - view->lineno);
4710 report("Chunk %d of %d", i + 1, stage_chunks);
4711 return;
4712 }
4713 }
4715 report("No next chunk found");
4716 }
4718 static enum request
4719 stage_request(struct view *view, enum request request, struct line *line)
4720 {
4721 switch (request) {
4722 case REQ_STATUS_UPDATE:
4723 if (!stage_update(view, line))
4724 return REQ_NONE;
4725 break;
4727 case REQ_STATUS_REVERT:
4728 if (!stage_revert(view, line))
4729 return REQ_NONE;
4730 break;
4732 case REQ_STAGE_NEXT:
4733 if (stage_line_type == LINE_STAT_UNTRACKED) {
4734 report("File is untracked; press %s to add",
4735 get_key(REQ_STATUS_UPDATE));
4736 return REQ_NONE;
4737 }
4738 stage_next(view, line);
4739 return REQ_NONE;
4741 case REQ_EDIT:
4742 if (!stage_status.new.name[0])
4743 return request;
4745 open_editor(stage_status.status != '?', stage_status.new.name);
4746 break;
4748 case REQ_REFRESH:
4749 /* Reload everything ... */
4750 break;
4752 case REQ_VIEW_BLAME:
4753 if (stage_status.new.name[0]) {
4754 string_copy(opt_file, stage_status.new.name);
4755 opt_ref[0] = 0;
4756 }
4757 return request;
4759 case REQ_ENTER:
4760 return pager_request(view, request, line);
4762 default:
4763 return request;
4764 }
4766 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD | OPEN_NOMAXIMIZE);
4768 /* Check whether the staged entry still exists, and close the
4769 * stage view if it doesn't. */
4770 if (!status_exists(&stage_status, stage_line_type))
4771 return REQ_VIEW_CLOSE;
4773 if (stage_line_type == LINE_STAT_UNTRACKED)
4774 opt_pipe = fopen(stage_status.new.name, "r");
4775 open_view(view, REQ_VIEW_STAGE, OPEN_REFRESH);
4777 return REQ_NONE;
4778 }
4780 static struct view_ops stage_ops = {
4781 "line",
4782 NULL,
4783 pager_read,
4784 pager_draw,
4785 stage_request,
4786 pager_grep,
4787 pager_select,
4788 };
4791 /*
4792 * Revision graph
4793 */
4795 struct commit {
4796 char id[SIZEOF_REV]; /* SHA1 ID. */
4797 char title[128]; /* First line of the commit message. */
4798 char author[75]; /* Author of the commit. */
4799 struct tm time; /* Date from the author ident. */
4800 struct ref **refs; /* Repository references. */
4801 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
4802 size_t graph_size; /* The width of the graph array. */
4803 bool has_parents; /* Rewritten --parents seen. */
4804 };
4806 /* Size of rev graph with no "padding" columns */
4807 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
4809 struct rev_graph {
4810 struct rev_graph *prev, *next, *parents;
4811 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
4812 size_t size;
4813 struct commit *commit;
4814 size_t pos;
4815 unsigned int boundary:1;
4816 };
4818 /* Parents of the commit being visualized. */
4819 static struct rev_graph graph_parents[4];
4821 /* The current stack of revisions on the graph. */
4822 static struct rev_graph graph_stacks[4] = {
4823 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
4824 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
4825 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
4826 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
4827 };
4829 static inline bool
4830 graph_parent_is_merge(struct rev_graph *graph)
4831 {
4832 return graph->parents->size > 1;
4833 }
4835 static inline void
4836 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
4837 {
4838 struct commit *commit = graph->commit;
4840 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
4841 commit->graph[commit->graph_size++] = symbol;
4842 }
4844 static void
4845 clear_rev_graph(struct rev_graph *graph)
4846 {
4847 graph->boundary = 0;
4848 graph->size = graph->pos = 0;
4849 graph->commit = NULL;
4850 memset(graph->parents, 0, sizeof(*graph->parents));
4851 }
4853 static void
4854 done_rev_graph(struct rev_graph *graph)
4855 {
4856 if (graph_parent_is_merge(graph) &&
4857 graph->pos < graph->size - 1 &&
4858 graph->next->size == graph->size + graph->parents->size - 1) {
4859 size_t i = graph->pos + graph->parents->size - 1;
4861 graph->commit->graph_size = i * 2;
4862 while (i < graph->next->size - 1) {
4863 append_to_rev_graph(graph, ' ');
4864 append_to_rev_graph(graph, '\\');
4865 i++;
4866 }
4867 }
4869 clear_rev_graph(graph);
4870 }
4872 static void
4873 push_rev_graph(struct rev_graph *graph, const char *parent)
4874 {
4875 int i;
4877 /* "Collapse" duplicate parents lines.
4878 *
4879 * FIXME: This needs to also update update the drawn graph but
4880 * for now it just serves as a method for pruning graph lines. */
4881 for (i = 0; i < graph->size; i++)
4882 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
4883 return;
4885 if (graph->size < SIZEOF_REVITEMS) {
4886 string_copy_rev(graph->rev[graph->size++], parent);
4887 }
4888 }
4890 static chtype
4891 get_rev_graph_symbol(struct rev_graph *graph)
4892 {
4893 chtype symbol;
4895 if (graph->boundary)
4896 symbol = REVGRAPH_BOUND;
4897 else if (graph->parents->size == 0)
4898 symbol = REVGRAPH_INIT;
4899 else if (graph_parent_is_merge(graph))
4900 symbol = REVGRAPH_MERGE;
4901 else if (graph->pos >= graph->size)
4902 symbol = REVGRAPH_BRANCH;
4903 else
4904 symbol = REVGRAPH_COMMIT;
4906 return symbol;
4907 }
4909 static void
4910 draw_rev_graph(struct rev_graph *graph)
4911 {
4912 struct rev_filler {
4913 chtype separator, line;
4914 };
4915 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
4916 static struct rev_filler fillers[] = {
4917 { ' ', '|' },
4918 { '`', '.' },
4919 { '\'', ' ' },
4920 { '/', ' ' },
4921 };
4922 chtype symbol = get_rev_graph_symbol(graph);
4923 struct rev_filler *filler;
4924 size_t i;
4926 if (opt_line_graphics)
4927 fillers[DEFAULT].line = line_graphics[LINE_GRAPHIC_VLINE];
4929 filler = &fillers[DEFAULT];
4931 for (i = 0; i < graph->pos; i++) {
4932 append_to_rev_graph(graph, filler->line);
4933 if (graph_parent_is_merge(graph->prev) &&
4934 graph->prev->pos == i)
4935 filler = &fillers[RSHARP];
4937 append_to_rev_graph(graph, filler->separator);
4938 }
4940 /* Place the symbol for this revision. */
4941 append_to_rev_graph(graph, symbol);
4943 if (graph->prev->size > graph->size)
4944 filler = &fillers[RDIAG];
4945 else
4946 filler = &fillers[DEFAULT];
4948 i++;
4950 for (; i < graph->size; i++) {
4951 append_to_rev_graph(graph, filler->separator);
4952 append_to_rev_graph(graph, filler->line);
4953 if (graph_parent_is_merge(graph->prev) &&
4954 i < graph->prev->pos + graph->parents->size)
4955 filler = &fillers[RSHARP];
4956 if (graph->prev->size > graph->size)
4957 filler = &fillers[LDIAG];
4958 }
4960 if (graph->prev->size > graph->size) {
4961 append_to_rev_graph(graph, filler->separator);
4962 if (filler->line != ' ')
4963 append_to_rev_graph(graph, filler->line);
4964 }
4965 }
4967 /* Prepare the next rev graph */
4968 static void
4969 prepare_rev_graph(struct rev_graph *graph)
4970 {
4971 size_t i;
4973 /* First, traverse all lines of revisions up to the active one. */
4974 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
4975 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
4976 break;
4978 push_rev_graph(graph->next, graph->rev[graph->pos]);
4979 }
4981 /* Interleave the new revision parent(s). */
4982 for (i = 0; !graph->boundary && i < graph->parents->size; i++)
4983 push_rev_graph(graph->next, graph->parents->rev[i]);
4985 /* Lastly, put any remaining revisions. */
4986 for (i = graph->pos + 1; i < graph->size; i++)
4987 push_rev_graph(graph->next, graph->rev[i]);
4988 }
4990 static void
4991 update_rev_graph(struct rev_graph *graph)
4992 {
4993 /* If this is the finalizing update ... */
4994 if (graph->commit)
4995 prepare_rev_graph(graph);
4997 /* Graph visualization needs a one rev look-ahead,
4998 * so the first update doesn't visualize anything. */
4999 if (!graph->prev->commit)
5000 return;
5002 draw_rev_graph(graph->prev);
5003 done_rev_graph(graph->prev->prev);
5004 }
5007 /*
5008 * Main view backend
5009 */
5011 static bool
5012 main_draw(struct view *view, struct line *line, unsigned int lineno)
5013 {
5014 struct commit *commit = line->data;
5016 if (!*commit->author)
5017 return FALSE;
5019 if (opt_date && draw_date(view, &commit->time))
5020 return TRUE;
5022 if (opt_author &&
5023 draw_field(view, LINE_MAIN_AUTHOR, commit->author, opt_author_cols, TRUE))
5024 return TRUE;
5026 if (opt_rev_graph && commit->graph_size &&
5027 draw_graphic(view, LINE_MAIN_REVGRAPH, commit->graph, commit->graph_size))
5028 return TRUE;
5030 if (opt_show_refs && commit->refs) {
5031 size_t i = 0;
5033 do {
5034 enum line_type type;
5036 if (commit->refs[i]->head)
5037 type = LINE_MAIN_HEAD;
5038 else if (commit->refs[i]->ltag)
5039 type = LINE_MAIN_LOCAL_TAG;
5040 else if (commit->refs[i]->tag)
5041 type = LINE_MAIN_TAG;
5042 else if (commit->refs[i]->tracked)
5043 type = LINE_MAIN_TRACKED;
5044 else if (commit->refs[i]->remote)
5045 type = LINE_MAIN_REMOTE;
5046 else
5047 type = LINE_MAIN_REF;
5049 if (draw_text(view, type, "[", TRUE) ||
5050 draw_text(view, type, commit->refs[i]->name, TRUE) ||
5051 draw_text(view, type, "]", TRUE))
5052 return TRUE;
5054 if (draw_text(view, LINE_DEFAULT, " ", TRUE))
5055 return TRUE;
5056 } while (commit->refs[i++]->next);
5057 }
5059 draw_text(view, LINE_DEFAULT, commit->title, TRUE);
5060 return TRUE;
5061 }
5063 /* Reads git log --pretty=raw output and parses it into the commit struct. */
5064 static bool
5065 main_read(struct view *view, char *line)
5066 {
5067 static struct rev_graph *graph = graph_stacks;
5068 enum line_type type;
5069 struct commit *commit;
5071 if (!line) {
5072 int i;
5074 if (!view->lines && !view->parent)
5075 die("No revisions match the given arguments.");
5076 if (view->lines > 0) {
5077 commit = view->line[view->lines - 1].data;
5078 if (!*commit->author) {
5079 view->lines--;
5080 free(commit);
5081 graph->commit = NULL;
5082 }
5083 }
5084 update_rev_graph(graph);
5086 for (i = 0; i < ARRAY_SIZE(graph_stacks); i++)
5087 clear_rev_graph(&graph_stacks[i]);
5088 return TRUE;
5089 }
5091 type = get_line_type(line);
5092 if (type == LINE_COMMIT) {
5093 commit = calloc(1, sizeof(struct commit));
5094 if (!commit)
5095 return FALSE;
5097 line += STRING_SIZE("commit ");
5098 if (*line == '-') {
5099 graph->boundary = 1;
5100 line++;
5101 }
5103 string_copy_rev(commit->id, line);
5104 commit->refs = get_refs(commit->id);
5105 graph->commit = commit;
5106 add_line_data(view, commit, LINE_MAIN_COMMIT);
5108 while ((line = strchr(line, ' '))) {
5109 line++;
5110 push_rev_graph(graph->parents, line);
5111 commit->has_parents = TRUE;
5112 }
5113 return TRUE;
5114 }
5116 if (!view->lines)
5117 return TRUE;
5118 commit = view->line[view->lines - 1].data;
5120 switch (type) {
5121 case LINE_PARENT:
5122 if (commit->has_parents)
5123 break;
5124 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
5125 break;
5127 case LINE_AUTHOR:
5128 {
5129 /* Parse author lines where the name may be empty:
5130 * author <email@address.tld> 1138474660 +0100
5131 */
5132 char *ident = line + STRING_SIZE("author ");
5133 char *nameend = strchr(ident, '<');
5134 char *emailend = strchr(ident, '>');
5136 if (!nameend || !emailend)
5137 break;
5139 update_rev_graph(graph);
5140 graph = graph->next;
5142 *nameend = *emailend = 0;
5143 ident = chomp_string(ident);
5144 if (!*ident) {
5145 ident = chomp_string(nameend + 1);
5146 if (!*ident)
5147 ident = "Unknown";
5148 }
5150 string_ncopy(commit->author, ident, strlen(ident));
5152 /* Parse epoch and timezone */
5153 if (emailend[1] == ' ') {
5154 char *secs = emailend + 2;
5155 char *zone = strchr(secs, ' ');
5156 time_t time = (time_t) atol(secs);
5158 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
5159 long tz;
5161 zone++;
5162 tz = ('0' - zone[1]) * 60 * 60 * 10;
5163 tz += ('0' - zone[2]) * 60 * 60;
5164 tz += ('0' - zone[3]) * 60;
5165 tz += ('0' - zone[4]) * 60;
5167 if (zone[0] == '-')
5168 tz = -tz;
5170 time -= tz;
5171 }
5173 gmtime_r(&time, &commit->time);
5174 }
5175 break;
5176 }
5177 default:
5178 /* Fill in the commit title if it has not already been set. */
5179 if (commit->title[0])
5180 break;
5182 /* Require titles to start with a non-space character at the
5183 * offset used by git log. */
5184 if (strncmp(line, " ", 4))
5185 break;
5186 line += 4;
5187 /* Well, if the title starts with a whitespace character,
5188 * try to be forgiving. Otherwise we end up with no title. */
5189 while (isspace(*line))
5190 line++;
5191 if (*line == '\0')
5192 break;
5193 /* FIXME: More graceful handling of titles; append "..." to
5194 * shortened titles, etc. */
5196 string_ncopy(commit->title, line, strlen(line));
5197 }
5199 return TRUE;
5200 }
5202 static enum request
5203 main_request(struct view *view, enum request request, struct line *line)
5204 {
5205 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
5207 switch (request) {
5208 case REQ_ENTER:
5209 open_view(view, REQ_VIEW_DIFF, flags);
5210 break;
5211 case REQ_REFRESH:
5212 load_refs();
5213 open_view(view, REQ_VIEW_MAIN, OPEN_REFRESH);
5214 break;
5215 default:
5216 return request;
5217 }
5219 return REQ_NONE;
5220 }
5222 static bool
5223 grep_refs(struct ref **refs, regex_t *regex)
5224 {
5225 regmatch_t pmatch;
5226 size_t i = 0;
5228 if (!refs)
5229 return FALSE;
5230 do {
5231 if (regexec(regex, refs[i]->name, 1, &pmatch, 0) != REG_NOMATCH)
5232 return TRUE;
5233 } while (refs[i++]->next);
5235 return FALSE;
5236 }
5238 static bool
5239 main_grep(struct view *view, struct line *line)
5240 {
5241 struct commit *commit = line->data;
5242 enum { S_TITLE, S_AUTHOR, S_DATE, S_REFS, S_END } state;
5243 char buf[DATE_COLS + 1];
5244 regmatch_t pmatch;
5246 for (state = S_TITLE; state < S_END; state++) {
5247 char *text;
5249 switch (state) {
5250 case S_TITLE: text = commit->title; break;
5251 case S_AUTHOR:
5252 if (!opt_author)
5253 continue;
5254 text = commit->author;
5255 break;
5256 case S_DATE:
5257 if (!opt_date)
5258 continue;
5259 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
5260 continue;
5261 text = buf;
5262 break;
5263 case S_REFS:
5264 if (!opt_show_refs)
5265 continue;
5266 if (grep_refs(commit->refs, view->regex) == TRUE)
5267 return TRUE;
5268 continue;
5269 default:
5270 return FALSE;
5271 }
5273 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
5274 return TRUE;
5275 }
5277 return FALSE;
5278 }
5280 static void
5281 main_select(struct view *view, struct line *line)
5282 {
5283 struct commit *commit = line->data;
5285 string_copy_rev(view->ref, commit->id);
5286 string_copy_rev(ref_commit, view->ref);
5287 }
5289 static struct view_ops main_ops = {
5290 "commit",
5291 NULL,
5292 main_read,
5293 main_draw,
5294 main_request,
5295 main_grep,
5296 main_select,
5297 };
5300 /*
5301 * Unicode / UTF-8 handling
5302 *
5303 * NOTE: Much of the following code for dealing with unicode is derived from
5304 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
5305 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
5306 */
5308 /* I've (over)annotated a lot of code snippets because I am not entirely
5309 * confident that the approach taken by this small UTF-8 interface is correct.
5310 * --jonas */
5312 static inline int
5313 unicode_width(unsigned long c)
5314 {
5315 if (c >= 0x1100 &&
5316 (c <= 0x115f /* Hangul Jamo */
5317 || c == 0x2329
5318 || c == 0x232a
5319 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
5320 /* CJK ... Yi */
5321 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
5322 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
5323 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
5324 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
5325 || (c >= 0xffe0 && c <= 0xffe6)
5326 || (c >= 0x20000 && c <= 0x2fffd)
5327 || (c >= 0x30000 && c <= 0x3fffd)))
5328 return 2;
5330 if (c == '\t')
5331 return opt_tab_size;
5333 return 1;
5334 }
5336 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
5337 * Illegal bytes are set one. */
5338 static const unsigned char utf8_bytes[256] = {
5339 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,
5340 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,
5341 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,
5342 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,
5343 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,
5344 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,
5345 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,
5346 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,
5347 };
5349 /* Decode UTF-8 multi-byte representation into a unicode character. */
5350 static inline unsigned long
5351 utf8_to_unicode(const char *string, size_t length)
5352 {
5353 unsigned long unicode;
5355 switch (length) {
5356 case 1:
5357 unicode = string[0];
5358 break;
5359 case 2:
5360 unicode = (string[0] & 0x1f) << 6;
5361 unicode += (string[1] & 0x3f);
5362 break;
5363 case 3:
5364 unicode = (string[0] & 0x0f) << 12;
5365 unicode += ((string[1] & 0x3f) << 6);
5366 unicode += (string[2] & 0x3f);
5367 break;
5368 case 4:
5369 unicode = (string[0] & 0x0f) << 18;
5370 unicode += ((string[1] & 0x3f) << 12);
5371 unicode += ((string[2] & 0x3f) << 6);
5372 unicode += (string[3] & 0x3f);
5373 break;
5374 case 5:
5375 unicode = (string[0] & 0x0f) << 24;
5376 unicode += ((string[1] & 0x3f) << 18);
5377 unicode += ((string[2] & 0x3f) << 12);
5378 unicode += ((string[3] & 0x3f) << 6);
5379 unicode += (string[4] & 0x3f);
5380 break;
5381 case 6:
5382 unicode = (string[0] & 0x01) << 30;
5383 unicode += ((string[1] & 0x3f) << 24);
5384 unicode += ((string[2] & 0x3f) << 18);
5385 unicode += ((string[3] & 0x3f) << 12);
5386 unicode += ((string[4] & 0x3f) << 6);
5387 unicode += (string[5] & 0x3f);
5388 break;
5389 default:
5390 die("Invalid unicode length");
5391 }
5393 /* Invalid characters could return the special 0xfffd value but NUL
5394 * should be just as good. */
5395 return unicode > 0xffff ? 0 : unicode;
5396 }
5398 /* Calculates how much of string can be shown within the given maximum width
5399 * and sets trimmed parameter to non-zero value if all of string could not be
5400 * shown. If the reserve flag is TRUE, it will reserve at least one
5401 * trailing character, which can be useful when drawing a delimiter.
5402 *
5403 * Returns the number of bytes to output from string to satisfy max_width. */
5404 static size_t
5405 utf8_length(const char *string, int *width, size_t max_width, int *trimmed, bool reserve)
5406 {
5407 const char *start = string;
5408 const char *end = strchr(string, '\0');
5409 unsigned char last_bytes = 0;
5410 size_t last_ucwidth = 0;
5412 *width = 0;
5413 *trimmed = 0;
5415 while (string < end) {
5416 int c = *(unsigned char *) string;
5417 unsigned char bytes = utf8_bytes[c];
5418 size_t ucwidth;
5419 unsigned long unicode;
5421 if (string + bytes > end)
5422 break;
5424 /* Change representation to figure out whether
5425 * it is a single- or double-width character. */
5427 unicode = utf8_to_unicode(string, bytes);
5428 /* FIXME: Graceful handling of invalid unicode character. */
5429 if (!unicode)
5430 break;
5432 ucwidth = unicode_width(unicode);
5433 *width += ucwidth;
5434 if (*width > max_width) {
5435 *trimmed = 1;
5436 *width -= ucwidth;
5437 if (reserve && *width == max_width) {
5438 string -= last_bytes;
5439 *width -= last_ucwidth;
5440 }
5441 break;
5442 }
5444 string += bytes;
5445 last_bytes = bytes;
5446 last_ucwidth = ucwidth;
5447 }
5449 return string - start;
5450 }
5453 /*
5454 * Status management
5455 */
5457 /* Whether or not the curses interface has been initialized. */
5458 static bool cursed = FALSE;
5460 /* The status window is used for polling keystrokes. */
5461 static WINDOW *status_win;
5463 static bool status_empty = TRUE;
5465 /* Update status and title window. */
5466 static void
5467 report(const char *msg, ...)
5468 {
5469 struct view *view = display[current_view];
5471 if (input_mode)
5472 return;
5474 if (!view) {
5475 char buf[SIZEOF_STR];
5476 va_list args;
5478 va_start(args, msg);
5479 if (vsnprintf(buf, sizeof(buf), msg, args) >= sizeof(buf)) {
5480 buf[sizeof(buf) - 1] = 0;
5481 buf[sizeof(buf) - 2] = '.';
5482 buf[sizeof(buf) - 3] = '.';
5483 buf[sizeof(buf) - 4] = '.';
5484 }
5485 va_end(args);
5486 die("%s", buf);
5487 }
5489 if (!status_empty || *msg) {
5490 va_list args;
5492 va_start(args, msg);
5494 wmove(status_win, 0, 0);
5495 if (*msg) {
5496 vwprintw(status_win, msg, args);
5497 status_empty = FALSE;
5498 } else {
5499 status_empty = TRUE;
5500 }
5501 wclrtoeol(status_win);
5502 wrefresh(status_win);
5504 va_end(args);
5505 }
5507 update_view_title(view);
5508 update_display_cursor(view);
5509 }
5511 /* Controls when nodelay should be in effect when polling user input. */
5512 static void
5513 set_nonblocking_input(bool loading)
5514 {
5515 static unsigned int loading_views;
5517 if ((loading == FALSE && loading_views-- == 1) ||
5518 (loading == TRUE && loading_views++ == 0))
5519 nodelay(status_win, loading);
5520 }
5522 static void
5523 init_display(void)
5524 {
5525 int x, y;
5527 /* Initialize the curses library */
5528 if (isatty(STDIN_FILENO)) {
5529 cursed = !!initscr();
5530 } else {
5531 /* Leave stdin and stdout alone when acting as a pager. */
5532 FILE *io = fopen("/dev/tty", "r+");
5534 if (!io)
5535 die("Failed to open /dev/tty");
5536 cursed = !!newterm(NULL, io, io);
5537 }
5539 if (!cursed)
5540 die("Failed to initialize curses");
5542 nonl(); /* Tell curses not to do NL->CR/NL on output */
5543 cbreak(); /* Take input chars one at a time, no wait for \n */
5544 noecho(); /* Don't echo input */
5545 leaveok(stdscr, TRUE);
5547 if (has_colors())
5548 init_colors();
5550 getmaxyx(stdscr, y, x);
5551 status_win = newwin(1, 0, y - 1, 0);
5552 if (!status_win)
5553 die("Failed to create status window");
5555 /* Enable keyboard mapping */
5556 keypad(status_win, TRUE);
5557 wbkgdset(status_win, get_line_attr(LINE_STATUS));
5559 TABSIZE = opt_tab_size;
5560 if (opt_line_graphics) {
5561 line_graphics[LINE_GRAPHIC_VLINE] = ACS_VLINE;
5562 }
5563 }
5565 static bool
5566 prompt_yesno(const char *prompt)
5567 {
5568 enum { WAIT, STOP, CANCEL } status = WAIT;
5569 bool answer = FALSE;
5571 while (status == WAIT) {
5572 struct view *view;
5573 int i, key;
5575 input_mode = TRUE;
5577 foreach_view (view, i)
5578 update_view(view);
5580 input_mode = FALSE;
5582 mvwprintw(status_win, 0, 0, "%s [Yy]/[Nn]", prompt);
5583 wclrtoeol(status_win);
5585 /* Refresh, accept single keystroke of input */
5586 key = wgetch(status_win);
5587 switch (key) {
5588 case ERR:
5589 break;
5591 case 'y':
5592 case 'Y':
5593 answer = TRUE;
5594 status = STOP;
5595 break;
5597 case KEY_ESC:
5598 case KEY_RETURN:
5599 case KEY_ENTER:
5600 case KEY_BACKSPACE:
5601 case 'n':
5602 case 'N':
5603 case '\n':
5604 default:
5605 answer = FALSE;
5606 status = CANCEL;
5607 }
5608 }
5610 /* Clear the status window */
5611 status_empty = FALSE;
5612 report("");
5614 return answer;
5615 }
5617 static char *
5618 read_prompt(const char *prompt)
5619 {
5620 enum { READING, STOP, CANCEL } status = READING;
5621 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
5622 int pos = 0;
5624 while (status == READING) {
5625 struct view *view;
5626 int i, key;
5628 input_mode = TRUE;
5630 foreach_view (view, i)
5631 update_view(view);
5633 input_mode = FALSE;
5635 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
5636 wclrtoeol(status_win);
5638 /* Refresh, accept single keystroke of input */
5639 key = wgetch(status_win);
5640 switch (key) {
5641 case KEY_RETURN:
5642 case KEY_ENTER:
5643 case '\n':
5644 status = pos ? STOP : CANCEL;
5645 break;
5647 case KEY_BACKSPACE:
5648 if (pos > 0)
5649 pos--;
5650 else
5651 status = CANCEL;
5652 break;
5654 case KEY_ESC:
5655 status = CANCEL;
5656 break;
5658 case ERR:
5659 break;
5661 default:
5662 if (pos >= sizeof(buf)) {
5663 report("Input string too long");
5664 return NULL;
5665 }
5667 if (isprint(key))
5668 buf[pos++] = (char) key;
5669 }
5670 }
5672 /* Clear the status window */
5673 status_empty = FALSE;
5674 report("");
5676 if (status == CANCEL)
5677 return NULL;
5679 buf[pos++] = 0;
5681 return buf;
5682 }
5684 /*
5685 * Repository references
5686 */
5688 static struct ref *refs = NULL;
5689 static size_t refs_alloc = 0;
5690 static size_t refs_size = 0;
5692 /* Id <-> ref store */
5693 static struct ref ***id_refs = NULL;
5694 static size_t id_refs_alloc = 0;
5695 static size_t id_refs_size = 0;
5697 static int
5698 compare_refs(const void *ref1_, const void *ref2_)
5699 {
5700 const struct ref *ref1 = *(const struct ref **)ref1_;
5701 const struct ref *ref2 = *(const struct ref **)ref2_;
5703 if (ref1->tag != ref2->tag)
5704 return ref2->tag - ref1->tag;
5705 if (ref1->ltag != ref2->ltag)
5706 return ref2->ltag - ref2->ltag;
5707 if (ref1->head != ref2->head)
5708 return ref2->head - ref1->head;
5709 if (ref1->tracked != ref2->tracked)
5710 return ref2->tracked - ref1->tracked;
5711 if (ref1->remote != ref2->remote)
5712 return ref2->remote - ref1->remote;
5713 return strcmp(ref1->name, ref2->name);
5714 }
5716 static struct ref **
5717 get_refs(const char *id)
5718 {
5719 struct ref ***tmp_id_refs;
5720 struct ref **ref_list = NULL;
5721 size_t ref_list_alloc = 0;
5722 size_t ref_list_size = 0;
5723 size_t i;
5725 for (i = 0; i < id_refs_size; i++)
5726 if (!strcmp(id, id_refs[i][0]->id))
5727 return id_refs[i];
5729 tmp_id_refs = realloc_items(id_refs, &id_refs_alloc, id_refs_size + 1,
5730 sizeof(*id_refs));
5731 if (!tmp_id_refs)
5732 return NULL;
5734 id_refs = tmp_id_refs;
5736 for (i = 0; i < refs_size; i++) {
5737 struct ref **tmp;
5739 if (strcmp(id, refs[i].id))
5740 continue;
5742 tmp = realloc_items(ref_list, &ref_list_alloc,
5743 ref_list_size + 1, sizeof(*ref_list));
5744 if (!tmp) {
5745 if (ref_list)
5746 free(ref_list);
5747 return NULL;
5748 }
5750 ref_list = tmp;
5751 ref_list[ref_list_size] = &refs[i];
5752 /* XXX: The properties of the commit chains ensures that we can
5753 * safely modify the shared ref. The repo references will
5754 * always be similar for the same id. */
5755 ref_list[ref_list_size]->next = 1;
5757 ref_list_size++;
5758 }
5760 if (ref_list) {
5761 qsort(ref_list, ref_list_size, sizeof(*ref_list), compare_refs);
5762 ref_list[ref_list_size - 1]->next = 0;
5763 id_refs[id_refs_size++] = ref_list;
5764 }
5766 return ref_list;
5767 }
5769 static int
5770 read_ref(char *id, size_t idlen, char *name, size_t namelen)
5771 {
5772 struct ref *ref;
5773 bool tag = FALSE;
5774 bool ltag = FALSE;
5775 bool remote = FALSE;
5776 bool tracked = FALSE;
5777 bool check_replace = FALSE;
5778 bool head = FALSE;
5780 if (!prefixcmp(name, "refs/tags/")) {
5781 if (!strcmp(name + namelen - 3, "^{}")) {
5782 namelen -= 3;
5783 name[namelen] = 0;
5784 if (refs_size > 0 && refs[refs_size - 1].ltag == TRUE)
5785 check_replace = TRUE;
5786 } else {
5787 ltag = TRUE;
5788 }
5790 tag = TRUE;
5791 namelen -= STRING_SIZE("refs/tags/");
5792 name += STRING_SIZE("refs/tags/");
5794 } else if (!prefixcmp(name, "refs/remotes/")) {
5795 remote = TRUE;
5796 namelen -= STRING_SIZE("refs/remotes/");
5797 name += STRING_SIZE("refs/remotes/");
5798 tracked = !strcmp(opt_remote, name);
5800 } else if (!prefixcmp(name, "refs/heads/")) {
5801 namelen -= STRING_SIZE("refs/heads/");
5802 name += STRING_SIZE("refs/heads/");
5803 head = !strncmp(opt_head, name, namelen);
5805 } else if (!strcmp(name, "HEAD")) {
5806 opt_no_head = FALSE;
5807 return OK;
5808 }
5810 if (check_replace && !strcmp(name, refs[refs_size - 1].name)) {
5811 /* it's an annotated tag, replace the previous sha1 with the
5812 * resolved commit id; relies on the fact git-ls-remote lists
5813 * the commit id of an annotated tag right before the commit id
5814 * it points to. */
5815 refs[refs_size - 1].ltag = ltag;
5816 string_copy_rev(refs[refs_size - 1].id, id);
5818 return OK;
5819 }
5820 refs = realloc_items(refs, &refs_alloc, refs_size + 1, sizeof(*refs));
5821 if (!refs)
5822 return ERR;
5824 ref = &refs[refs_size++];
5825 ref->name = malloc(namelen + 1);
5826 if (!ref->name)
5827 return ERR;
5829 strncpy(ref->name, name, namelen);
5830 ref->name[namelen] = 0;
5831 ref->head = head;
5832 ref->tag = tag;
5833 ref->ltag = ltag;
5834 ref->remote = remote;
5835 ref->tracked = tracked;
5836 string_copy_rev(ref->id, id);
5838 return OK;
5839 }
5841 static int
5842 load_refs(void)
5843 {
5844 const char *cmd_env = getenv("TIG_LS_REMOTE");
5845 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
5847 if (!*opt_git_dir)
5848 return OK;
5850 while (refs_size > 0)
5851 free(refs[--refs_size].name);
5852 while (id_refs_size > 0)
5853 free(id_refs[--id_refs_size]);
5855 return read_properties(popen(cmd, "r"), "\t", read_ref);
5856 }
5858 static int
5859 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
5860 {
5861 if (!strcmp(name, "i18n.commitencoding"))
5862 string_ncopy(opt_encoding, value, valuelen);
5864 if (!strcmp(name, "core.editor"))
5865 string_ncopy(opt_editor, value, valuelen);
5867 /* branch.<head>.remote */
5868 if (*opt_head &&
5869 !strncmp(name, "branch.", 7) &&
5870 !strncmp(name + 7, opt_head, strlen(opt_head)) &&
5871 !strcmp(name + 7 + strlen(opt_head), ".remote"))
5872 string_ncopy(opt_remote, value, valuelen);
5874 if (*opt_head && *opt_remote &&
5875 !strncmp(name, "branch.", 7) &&
5876 !strncmp(name + 7, opt_head, strlen(opt_head)) &&
5877 !strcmp(name + 7 + strlen(opt_head), ".merge")) {
5878 size_t from = strlen(opt_remote);
5880 if (!prefixcmp(value, "refs/heads/")) {
5881 value += STRING_SIZE("refs/heads/");
5882 valuelen -= STRING_SIZE("refs/heads/");
5883 }
5885 if (!string_format_from(opt_remote, &from, "/%s", value))
5886 opt_remote[0] = 0;
5887 }
5889 return OK;
5890 }
5892 static int
5893 load_git_config(void)
5894 {
5895 return read_properties(popen("git " GIT_CONFIG " --list", "r"),
5896 "=", read_repo_config_option);
5897 }
5899 static int
5900 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
5901 {
5902 if (!opt_git_dir[0]) {
5903 string_ncopy(opt_git_dir, name, namelen);
5905 } else if (opt_is_inside_work_tree == -1) {
5906 /* This can be 3 different values depending on the
5907 * version of git being used. If git-rev-parse does not
5908 * understand --is-inside-work-tree it will simply echo
5909 * the option else either "true" or "false" is printed.
5910 * Default to true for the unknown case. */
5911 opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
5913 } else if (opt_cdup[0] == ' ') {
5914 string_ncopy(opt_cdup, name, namelen);
5915 } else {
5916 if (!prefixcmp(name, "refs/heads/")) {
5917 namelen -= STRING_SIZE("refs/heads/");
5918 name += STRING_SIZE("refs/heads/");
5919 string_ncopy(opt_head, name, namelen);
5920 }
5921 }
5923 return OK;
5924 }
5926 static int
5927 load_repo_info(void)
5928 {
5929 int result;
5930 FILE *pipe = popen("(git rev-parse --git-dir --is-inside-work-tree "
5931 " --show-cdup; git symbolic-ref HEAD) 2>/dev/null", "r");
5933 /* XXX: The line outputted by "--show-cdup" can be empty so
5934 * initialize it to something invalid to make it possible to
5935 * detect whether it has been set or not. */
5936 opt_cdup[0] = ' ';
5938 result = read_properties(pipe, "=", read_repo_info);
5939 if (opt_cdup[0] == ' ')
5940 opt_cdup[0] = 0;
5942 return result;
5943 }
5945 static int
5946 read_properties(FILE *pipe, const char *separators,
5947 int (*read_property)(char *, size_t, char *, size_t))
5948 {
5949 char buffer[BUFSIZ];
5950 char *name;
5951 int state = OK;
5953 if (!pipe)
5954 return ERR;
5956 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
5957 char *value;
5958 size_t namelen;
5959 size_t valuelen;
5961 name = chomp_string(name);
5962 namelen = strcspn(name, separators);
5964 if (name[namelen]) {
5965 name[namelen] = 0;
5966 value = chomp_string(name + namelen + 1);
5967 valuelen = strlen(value);
5969 } else {
5970 value = "";
5971 valuelen = 0;
5972 }
5974 state = read_property(name, namelen, value, valuelen);
5975 }
5977 if (state != ERR && ferror(pipe))
5978 state = ERR;
5980 pclose(pipe);
5982 return state;
5983 }
5986 /*
5987 * Main
5988 */
5990 static void __NORETURN
5991 quit(int sig)
5992 {
5993 /* XXX: Restore tty modes and let the OS cleanup the rest! */
5994 if (cursed)
5995 endwin();
5996 exit(0);
5997 }
5999 static void __NORETURN
6000 die(const char *err, ...)
6001 {
6002 va_list args;
6004 endwin();
6006 va_start(args, err);
6007 fputs("tig: ", stderr);
6008 vfprintf(stderr, err, args);
6009 fputs("\n", stderr);
6010 va_end(args);
6012 exit(1);
6013 }
6015 static void
6016 warn(const char *msg, ...)
6017 {
6018 va_list args;
6020 va_start(args, msg);
6021 fputs("tig warning: ", stderr);
6022 vfprintf(stderr, msg, args);
6023 fputs("\n", stderr);
6024 va_end(args);
6025 }
6027 int
6028 main(int argc, const char *argv[])
6029 {
6030 struct view *view;
6031 enum request request;
6032 size_t i;
6034 signal(SIGINT, quit);
6036 if (setlocale(LC_ALL, "")) {
6037 char *codeset = nl_langinfo(CODESET);
6039 string_ncopy(opt_codeset, codeset, strlen(codeset));
6040 }
6042 if (load_repo_info() == ERR)
6043 die("Failed to load repo info.");
6045 if (load_options() == ERR)
6046 die("Failed to load user config.");
6048 if (load_git_config() == ERR)
6049 die("Failed to load repo config.");
6051 request = parse_options(argc, argv);
6052 if (request == REQ_NONE)
6053 return 0;
6055 /* Require a git repository unless when running in pager mode. */
6056 if (!opt_git_dir[0] && request != REQ_VIEW_PAGER)
6057 die("Not a git repository");
6059 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
6060 opt_utf8 = FALSE;
6062 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
6063 opt_iconv = iconv_open(opt_codeset, opt_encoding);
6064 if (opt_iconv == ICONV_NONE)
6065 die("Failed to initialize character set conversion");
6066 }
6068 if (load_refs() == ERR)
6069 die("Failed to load refs.");
6071 foreach_view (view, i)
6072 view->cmd_env = getenv(view->cmd_env);
6074 init_display();
6076 while (view_driver(display[current_view], request)) {
6077 int key;
6078 int i;
6080 foreach_view (view, i)
6081 update_view(view);
6082 view = display[current_view];
6084 /* Refresh, accept single keystroke of input */
6085 key = wgetch(status_win);
6087 /* wgetch() with nodelay() enabled returns ERR when there's no
6088 * input. */
6089 if (key == ERR) {
6090 request = REQ_NONE;
6091 continue;
6092 }
6094 request = get_keybinding(view->keymap, key);
6096 /* Some low-level request handling. This keeps access to
6097 * status_win restricted. */
6098 switch (request) {
6099 case REQ_PROMPT:
6100 {
6101 char *cmd = read_prompt(":");
6103 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
6104 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
6105 request = REQ_VIEW_DIFF;
6106 } else {
6107 request = REQ_VIEW_PAGER;
6108 }
6110 /* Always reload^Wrerun commands from the prompt. */
6111 open_view(view, request, OPEN_RELOAD);
6112 }
6114 request = REQ_NONE;
6115 break;
6116 }
6117 case REQ_SEARCH:
6118 case REQ_SEARCH_BACK:
6119 {
6120 const char *prompt = request == REQ_SEARCH ? "/" : "?";
6121 char *search = read_prompt(prompt);
6123 if (search)
6124 string_ncopy(opt_search, search, strlen(search));
6125 else
6126 request = REQ_NONE;
6127 break;
6128 }
6129 case REQ_SCREEN_RESIZE:
6130 {
6131 int height, width;
6133 getmaxyx(stdscr, height, width);
6135 /* Resize the status view and let the view driver take
6136 * care of resizing the displayed views. */
6137 wresize(status_win, 1, width);
6138 mvwin(status_win, height - 1, 0);
6139 wrefresh(status_win);
6140 break;
6141 }
6142 default:
6143 break;
6144 }
6145 }
6147 quit(0);
6149 return 0;
6150 }