8e06c1ac5031bbb19cb3e0275e91e46c71652976
1 /* Copyright (c) 2006-2008 Jonas Fonseca <fonseca@diku.dk>
2 *
3 * This program is free software; you can redistribute it and/or
4 * modify it under the terms of the GNU General Public License as
5 * published by the Free Software Foundation; either version 2 of
6 * the License, or (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 */
14 #ifdef HAVE_CONFIG_H
15 #include "config.h"
16 #endif
18 #ifndef TIG_VERSION
19 #define TIG_VERSION "unknown-version"
20 #endif
22 #ifndef DEBUG
23 #define NDEBUG
24 #endif
26 #include <assert.h>
27 #include <errno.h>
28 #include <ctype.h>
29 #include <signal.h>
30 #include <stdarg.h>
31 #include <stdio.h>
32 #include <stdlib.h>
33 #include <string.h>
34 #include <sys/types.h>
35 #include <sys/stat.h>
36 #include <unistd.h>
37 #include <time.h>
39 #include <regex.h>
41 #include <locale.h>
42 #include <langinfo.h>
43 #include <iconv.h>
45 /* ncurses(3): Must be defined to have extended wide-character functions. */
46 #define _XOPEN_SOURCE_EXTENDED
48 #ifdef HAVE_NCURSESW_NCURSES_H
49 #include <ncursesw/ncurses.h>
50 #else
51 #ifdef HAVE_NCURSES_NCURSES_H
52 #include <ncurses/ncurses.h>
53 #else
54 #include <ncurses.h>
55 #endif
56 #endif
58 #if __GNUC__ >= 3
59 #define __NORETURN __attribute__((__noreturn__))
60 #else
61 #define __NORETURN
62 #endif
64 static void __NORETURN die(const char *err, ...);
65 static void warn(const char *msg, ...);
66 static void report(const char *msg, ...);
67 static int read_properties(FILE *pipe, const char *separators, int (*read)(char *, size_t, char *, size_t));
68 static void set_nonblocking_input(bool loading);
69 static size_t utf8_length(const char *string, int *width, size_t max_width, int *trimmed, bool reserve);
71 #define ABS(x) ((x) >= 0 ? (x) : -(x))
72 #define MIN(x, y) ((x) < (y) ? (x) : (y))
74 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
75 #define STRING_SIZE(x) (sizeof(x) - 1)
77 #define SIZEOF_STR 1024 /* Default string size. */
78 #define SIZEOF_REF 256 /* Size of symbolic or SHA1 ID. */
79 #define SIZEOF_REV 41 /* Holds a SHA-1 and an ending NUL */
81 /* Revision graph */
83 #define REVGRAPH_INIT 'I'
84 #define REVGRAPH_MERGE 'M'
85 #define REVGRAPH_BRANCH '+'
86 #define REVGRAPH_COMMIT '*'
87 #define REVGRAPH_BOUND '^'
89 #define SIZEOF_REVGRAPH 19 /* Size of revision ancestry graphics. */
91 /* This color name can be used to refer to the default term colors. */
92 #define COLOR_DEFAULT (-1)
94 #define ICONV_NONE ((iconv_t) -1)
95 #ifndef ICONV_CONST
96 #define ICONV_CONST /* nothing */
97 #endif
99 /* The format and size of the date column in the main view. */
100 #define DATE_FORMAT "%Y-%m-%d %H:%M"
101 #define DATE_COLS STRING_SIZE("2006-04-29 14:21 ")
103 #define AUTHOR_COLS 20
104 #define ID_COLS 8
106 /* The default interval between line numbers. */
107 #define NUMBER_INTERVAL 5
109 #define TAB_SIZE 8
111 #define SCALE_SPLIT_VIEW(height) ((height) * 2 / 3)
113 #define NULL_ID "0000000000000000000000000000000000000000"
115 #ifndef GIT_CONFIG
116 #define GIT_CONFIG "git config"
117 #endif
119 #define TIG_LS_REMOTE \
120 "git ls-remote . 2>/dev/null"
122 #define TIG_DIFF_CMD \
123 "git show --pretty=fuller --no-color --root --patch-with-stat --find-copies-harder -C %s 2>/dev/null"
125 #define TIG_LOG_CMD \
126 "git log --no-color --cc --stat -n100 %s 2>/dev/null"
128 #define TIG_MAIN_CMD \
129 "git log --no-color --topo-order --parents --boundary --pretty=raw %s 2>/dev/null"
131 #define TIG_TREE_CMD \
132 "git ls-tree %s %s"
134 #define TIG_BLOB_CMD \
135 "git cat-file blob %s"
137 /* XXX: Needs to be defined to the empty string. */
138 #define TIG_HELP_CMD ""
139 #define TIG_PAGER_CMD ""
140 #define TIG_STATUS_CMD ""
141 #define TIG_STAGE_CMD ""
142 #define TIG_BLAME_CMD ""
144 /* Some ascii-shorthands fitted into the ncurses namespace. */
145 #define KEY_TAB '\t'
146 #define KEY_RETURN '\r'
147 #define KEY_ESC 27
150 struct ref {
151 char *name; /* Ref name; tag or head names are shortened. */
152 char id[SIZEOF_REV]; /* Commit SHA1 ID */
153 unsigned int head:1; /* Is it the current HEAD? */
154 unsigned int tag:1; /* Is it a tag? */
155 unsigned int ltag:1; /* If so, is the tag local? */
156 unsigned int remote:1; /* Is it a remote ref? */
157 unsigned int tracked:1; /* Is it the remote for the current HEAD? */
158 unsigned int next:1; /* For ref lists: are there more refs? */
159 };
161 static struct ref **get_refs(char *id);
163 struct int_map {
164 const char *name;
165 int namelen;
166 int value;
167 };
169 static int
170 set_from_int_map(struct int_map *map, size_t map_size,
171 int *value, const char *name, int namelen)
172 {
174 int i;
176 for (i = 0; i < map_size; i++)
177 if (namelen == map[i].namelen &&
178 !strncasecmp(name, map[i].name, namelen)) {
179 *value = map[i].value;
180 return OK;
181 }
183 return ERR;
184 }
187 /*
188 * String helpers
189 */
191 static inline void
192 string_ncopy_do(char *dst, size_t dstlen, const char *src, size_t srclen)
193 {
194 if (srclen > dstlen - 1)
195 srclen = dstlen - 1;
197 strncpy(dst, src, srclen);
198 dst[srclen] = 0;
199 }
201 /* Shorthands for safely copying into a fixed buffer. */
203 #define string_copy(dst, src) \
204 string_ncopy_do(dst, sizeof(dst), src, sizeof(src))
206 #define string_ncopy(dst, src, srclen) \
207 string_ncopy_do(dst, sizeof(dst), src, srclen)
209 #define string_copy_rev(dst, src) \
210 string_ncopy_do(dst, SIZEOF_REV, src, SIZEOF_REV - 1)
212 #define string_add(dst, from, src) \
213 string_ncopy_do(dst + (from), sizeof(dst) - (from), src, sizeof(src))
215 static char *
216 chomp_string(char *name)
217 {
218 int namelen;
220 while (isspace(*name))
221 name++;
223 namelen = strlen(name) - 1;
224 while (namelen > 0 && isspace(name[namelen]))
225 name[namelen--] = 0;
227 return name;
228 }
230 static bool
231 string_nformat(char *buf, size_t bufsize, size_t *bufpos, const char *fmt, ...)
232 {
233 va_list args;
234 size_t pos = bufpos ? *bufpos : 0;
236 va_start(args, fmt);
237 pos += vsnprintf(buf + pos, bufsize - pos, fmt, args);
238 va_end(args);
240 if (bufpos)
241 *bufpos = pos;
243 return pos >= bufsize ? FALSE : TRUE;
244 }
246 #define string_format(buf, fmt, args...) \
247 string_nformat(buf, sizeof(buf), NULL, fmt, args)
249 #define string_format_from(buf, from, fmt, args...) \
250 string_nformat(buf, sizeof(buf), from, fmt, args)
252 static int
253 string_enum_compare(const char *str1, const char *str2, int len)
254 {
255 size_t i;
257 #define string_enum_sep(x) ((x) == '-' || (x) == '_' || (x) == '.')
259 /* Diff-Header == DIFF_HEADER */
260 for (i = 0; i < len; i++) {
261 if (toupper(str1[i]) == toupper(str2[i]))
262 continue;
264 if (string_enum_sep(str1[i]) &&
265 string_enum_sep(str2[i]))
266 continue;
268 return str1[i] - str2[i];
269 }
271 return 0;
272 }
274 /* Shell quoting
275 *
276 * NOTE: The following is a slightly modified copy of the git project's shell
277 * quoting routines found in the quote.c file.
278 *
279 * Help to copy the thing properly quoted for the shell safety. any single
280 * quote is replaced with '\'', any exclamation point is replaced with '\!',
281 * and the whole thing is enclosed in a
282 *
283 * E.g.
284 * original sq_quote result
285 * name ==> name ==> 'name'
286 * a b ==> a b ==> 'a b'
287 * a'b ==> a'\''b ==> 'a'\''b'
288 * a!b ==> a'\!'b ==> 'a'\!'b'
289 */
291 static size_t
292 sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
293 {
294 char c;
296 #define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0)
298 BUFPUT('\'');
299 while ((c = *src++)) {
300 if (c == '\'' || c == '!') {
301 BUFPUT('\'');
302 BUFPUT('\\');
303 BUFPUT(c);
304 BUFPUT('\'');
305 } else {
306 BUFPUT(c);
307 }
308 }
309 BUFPUT('\'');
311 if (bufsize < SIZEOF_STR)
312 buf[bufsize] = 0;
314 return bufsize;
315 }
318 /*
319 * User requests
320 */
322 #define REQ_INFO \
323 /* XXX: Keep the view request first and in sync with views[]. */ \
324 REQ_GROUP("View switching") \
325 REQ_(VIEW_MAIN, "Show main view"), \
326 REQ_(VIEW_DIFF, "Show diff view"), \
327 REQ_(VIEW_LOG, "Show log view"), \
328 REQ_(VIEW_TREE, "Show tree view"), \
329 REQ_(VIEW_BLOB, "Show blob view"), \
330 REQ_(VIEW_BLAME, "Show blame view"), \
331 REQ_(VIEW_HELP, "Show help page"), \
332 REQ_(VIEW_PAGER, "Show pager view"), \
333 REQ_(VIEW_STATUS, "Show status view"), \
334 REQ_(VIEW_STAGE, "Show stage view"), \
335 \
336 REQ_GROUP("View manipulation") \
337 REQ_(ENTER, "Enter current line and scroll"), \
338 REQ_(NEXT, "Move to next"), \
339 REQ_(PREVIOUS, "Move to previous"), \
340 REQ_(VIEW_NEXT, "Move focus to next view"), \
341 REQ_(REFRESH, "Reload and refresh"), \
342 REQ_(MAXIMIZE, "Maximize the current view"), \
343 REQ_(VIEW_CLOSE, "Close the current view"), \
344 REQ_(QUIT, "Close all views and quit"), \
345 \
346 REQ_GROUP("Cursor navigation") \
347 REQ_(MOVE_UP, "Move cursor one line up"), \
348 REQ_(MOVE_DOWN, "Move cursor one line down"), \
349 REQ_(MOVE_PAGE_DOWN, "Move cursor one page down"), \
350 REQ_(MOVE_PAGE_UP, "Move cursor one page up"), \
351 REQ_(MOVE_FIRST_LINE, "Move cursor to first line"), \
352 REQ_(MOVE_LAST_LINE, "Move cursor to last line"), \
353 \
354 REQ_GROUP("Scrolling") \
355 REQ_(SCROLL_LINE_UP, "Scroll one line up"), \
356 REQ_(SCROLL_LINE_DOWN, "Scroll one line down"), \
357 REQ_(SCROLL_PAGE_UP, "Scroll one page up"), \
358 REQ_(SCROLL_PAGE_DOWN, "Scroll one page down"), \
359 \
360 REQ_GROUP("Searching") \
361 REQ_(SEARCH, "Search the view"), \
362 REQ_(SEARCH_BACK, "Search backwards in the view"), \
363 REQ_(FIND_NEXT, "Find next search match"), \
364 REQ_(FIND_PREV, "Find previous search match"), \
365 \
366 REQ_GROUP("Misc") \
367 REQ_(PROMPT, "Bring up the prompt"), \
368 REQ_(SCREEN_REDRAW, "Redraw the screen"), \
369 REQ_(SCREEN_RESIZE, "Resize the screen"), \
370 REQ_(SHOW_VERSION, "Show version information"), \
371 REQ_(STOP_LOADING, "Stop all loading views"), \
372 REQ_(TOGGLE_LINENO, "Toggle line numbers"), \
373 REQ_(TOGGLE_DATE, "Toggle date display"), \
374 REQ_(TOGGLE_AUTHOR, "Toggle author display"), \
375 REQ_(TOGGLE_REV_GRAPH, "Toggle revision graph visualization"), \
376 REQ_(TOGGLE_REFS, "Toggle reference display (tags/branches)"), \
377 REQ_(STATUS_UPDATE, "Update file status"), \
378 REQ_(STATUS_MERGE, "Merge file using external tool"), \
379 REQ_(TREE_PARENT, "Switch to parent directory in tree view"), \
380 REQ_(EDIT, "Open in editor"), \
381 REQ_(NONE, "Do nothing")
384 /* User action requests. */
385 enum request {
386 #define REQ_GROUP(help)
387 #define REQ_(req, help) REQ_##req
389 /* Offset all requests to avoid conflicts with ncurses getch values. */
390 REQ_OFFSET = KEY_MAX + 1,
391 REQ_INFO
393 #undef REQ_GROUP
394 #undef REQ_
395 };
397 struct request_info {
398 enum request request;
399 char *name;
400 int namelen;
401 char *help;
402 };
404 static struct request_info req_info[] = {
405 #define REQ_GROUP(help) { 0, NULL, 0, (help) },
406 #define REQ_(req, help) { REQ_##req, (#req), STRING_SIZE(#req), (help) }
407 REQ_INFO
408 #undef REQ_GROUP
409 #undef REQ_
410 };
412 static enum request
413 get_request(const char *name)
414 {
415 int namelen = strlen(name);
416 int i;
418 for (i = 0; i < ARRAY_SIZE(req_info); i++)
419 if (req_info[i].namelen == namelen &&
420 !string_enum_compare(req_info[i].name, name, namelen))
421 return req_info[i].request;
423 return REQ_NONE;
424 }
427 /*
428 * Options
429 */
431 static const char usage[] =
432 "tig " TIG_VERSION " (" __DATE__ ")\n"
433 "\n"
434 "Usage: tig [options] [revs] [--] [paths]\n"
435 " or: tig show [options] [revs] [--] [paths]\n"
436 " or: tig blame [rev] path\n"
437 " or: tig status\n"
438 " or: tig < [git command output]\n"
439 "\n"
440 "Options:\n"
441 " -v, --version Show version and exit\n"
442 " -h, --help Show help message and exit";
444 /* Option and state variables. */
445 static bool opt_date = TRUE;
446 static bool opt_author = TRUE;
447 static bool opt_line_number = FALSE;
448 static bool opt_line_graphics = TRUE;
449 static bool opt_rev_graph = FALSE;
450 static bool opt_show_refs = TRUE;
451 static int opt_num_interval = NUMBER_INTERVAL;
452 static int opt_tab_size = TAB_SIZE;
453 static enum request opt_request = REQ_VIEW_MAIN;
454 static char opt_cmd[SIZEOF_STR] = "";
455 static char opt_path[SIZEOF_STR] = "";
456 static char opt_file[SIZEOF_STR] = "";
457 static char opt_ref[SIZEOF_REF] = "";
458 static char opt_head[SIZEOF_REF] = "";
459 static char opt_remote[SIZEOF_REF] = "";
460 static bool opt_no_head = TRUE;
461 static FILE *opt_pipe = NULL;
462 static char opt_encoding[20] = "UTF-8";
463 static bool opt_utf8 = TRUE;
464 static char opt_codeset[20] = "UTF-8";
465 static iconv_t opt_iconv = ICONV_NONE;
466 static char opt_search[SIZEOF_STR] = "";
467 static char opt_cdup[SIZEOF_STR] = "";
468 static char opt_git_dir[SIZEOF_STR] = "";
469 static signed char opt_is_inside_work_tree = -1; /* set to TRUE or FALSE */
470 static char opt_editor[SIZEOF_STR] = "";
472 static bool
473 parse_options(int argc, char *argv[])
474 {
475 size_t buf_size;
476 char *subcommand;
477 bool seen_dashdash = FALSE;
478 int i;
480 if (!isatty(STDIN_FILENO)) {
481 opt_request = REQ_VIEW_PAGER;
482 opt_pipe = stdin;
483 return TRUE;
484 }
486 if (argc <= 1)
487 return TRUE;
489 subcommand = argv[1];
490 if (!strcmp(subcommand, "status") || !strcmp(subcommand, "-S")) {
491 opt_request = REQ_VIEW_STATUS;
492 if (!strcmp(subcommand, "-S"))
493 warn("`-S' has been deprecated; use `tig status' instead");
494 if (argc > 2)
495 warn("ignoring arguments after `%s'", subcommand);
496 return TRUE;
498 } else if (!strcmp(subcommand, "blame")) {
499 opt_request = REQ_VIEW_BLAME;
500 if (argc <= 2 || argc > 4)
501 die("invalid number of options to blame\n\n%s", usage);
503 i = 2;
504 if (argc == 4) {
505 string_ncopy(opt_ref, argv[i], strlen(argv[i]));
506 i++;
507 }
509 string_ncopy(opt_file, argv[i], strlen(argv[i]));
510 return TRUE;
512 } else if (!strcmp(subcommand, "show")) {
513 opt_request = REQ_VIEW_DIFF;
515 } else if (!strcmp(subcommand, "log") || !strcmp(subcommand, "diff")) {
516 opt_request = subcommand[0] == 'l'
517 ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
518 warn("`tig %s' has been deprecated", subcommand);
520 } else {
521 subcommand = NULL;
522 }
524 if (!subcommand)
525 /* XXX: This is vulnerable to the user overriding
526 * options required for the main view parser. */
527 string_copy(opt_cmd, "git log --no-color --pretty=raw --boundary --parents");
528 else
529 string_format(opt_cmd, "git %s", subcommand);
531 buf_size = strlen(opt_cmd);
533 for (i = 1 + !!subcommand; i < argc; i++) {
534 char *opt = argv[i];
536 if (seen_dashdash || !strcmp(opt, "--")) {
537 seen_dashdash = TRUE;
539 } else if (!strcmp(opt, "-v") || !strcmp(opt, "--version")) {
540 printf("tig version %s\n", TIG_VERSION);
541 return FALSE;
543 } else if (!strcmp(opt, "-h") || !strcmp(opt, "--help")) {
544 printf("%s\n", usage);
545 return FALSE;
546 }
548 opt_cmd[buf_size++] = ' ';
549 buf_size = sq_quote(opt_cmd, buf_size, opt);
550 if (buf_size >= sizeof(opt_cmd))
551 die("command too long");
552 }
554 opt_cmd[buf_size] = 0;
556 return TRUE;
557 }
560 /*
561 * Line-oriented content detection.
562 */
564 #define LINE_INFO \
565 LINE(DIFF_HEADER, "diff --git ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
566 LINE(DIFF_CHUNK, "@@", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
567 LINE(DIFF_ADD, "+", COLOR_GREEN, COLOR_DEFAULT, 0), \
568 LINE(DIFF_DEL, "-", COLOR_RED, COLOR_DEFAULT, 0), \
569 LINE(DIFF_INDEX, "index ", COLOR_BLUE, COLOR_DEFAULT, 0), \
570 LINE(DIFF_OLDMODE, "old file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
571 LINE(DIFF_NEWMODE, "new file mode ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
572 LINE(DIFF_COPY_FROM, "copy from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
573 LINE(DIFF_COPY_TO, "copy to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
574 LINE(DIFF_RENAME_FROM, "rename from", COLOR_YELLOW, COLOR_DEFAULT, 0), \
575 LINE(DIFF_RENAME_TO, "rename to", COLOR_YELLOW, COLOR_DEFAULT, 0), \
576 LINE(DIFF_SIMILARITY, "similarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
577 LINE(DIFF_DISSIMILARITY,"dissimilarity ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
578 LINE(DIFF_TREE, "diff-tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
579 LINE(PP_AUTHOR, "Author: ", COLOR_CYAN, COLOR_DEFAULT, 0), \
580 LINE(PP_COMMIT, "Commit: ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
581 LINE(PP_MERGE, "Merge: ", COLOR_BLUE, COLOR_DEFAULT, 0), \
582 LINE(PP_DATE, "Date: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
583 LINE(PP_ADATE, "AuthorDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
584 LINE(PP_CDATE, "CommitDate: ", COLOR_YELLOW, COLOR_DEFAULT, 0), \
585 LINE(PP_REFS, "Refs: ", COLOR_RED, COLOR_DEFAULT, 0), \
586 LINE(COMMIT, "commit ", COLOR_GREEN, COLOR_DEFAULT, 0), \
587 LINE(PARENT, "parent ", COLOR_BLUE, COLOR_DEFAULT, 0), \
588 LINE(TREE, "tree ", COLOR_BLUE, COLOR_DEFAULT, 0), \
589 LINE(AUTHOR, "author ", COLOR_CYAN, COLOR_DEFAULT, 0), \
590 LINE(COMMITTER, "committer ", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
591 LINE(SIGNOFF, " Signed-off-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
592 LINE(ACKED, " Acked-by", COLOR_YELLOW, COLOR_DEFAULT, 0), \
593 LINE(DEFAULT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
594 LINE(CURSOR, "", COLOR_WHITE, COLOR_GREEN, A_BOLD), \
595 LINE(STATUS, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
596 LINE(DELIMITER, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
597 LINE(DATE, "", COLOR_BLUE, COLOR_DEFAULT, 0), \
598 LINE(LINE_NUMBER, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
599 LINE(TITLE_BLUR, "", COLOR_WHITE, COLOR_BLUE, 0), \
600 LINE(TITLE_FOCUS, "", COLOR_WHITE, COLOR_BLUE, A_BOLD), \
601 LINE(MAIN_AUTHOR, "", COLOR_GREEN, COLOR_DEFAULT, 0), \
602 LINE(MAIN_COMMIT, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
603 LINE(MAIN_TAG, "", COLOR_MAGENTA, COLOR_DEFAULT, A_BOLD), \
604 LINE(MAIN_LOCAL_TAG,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
605 LINE(MAIN_REMOTE, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \
606 LINE(MAIN_TRACKED, "", COLOR_YELLOW, COLOR_DEFAULT, A_BOLD), \
607 LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
608 LINE(MAIN_HEAD, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
609 LINE(MAIN_REVGRAPH,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
610 LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
611 LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
612 LINE(STAT_HEAD, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \
613 LINE(STAT_SECTION, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
614 LINE(STAT_NONE, "", COLOR_DEFAULT, COLOR_DEFAULT, 0), \
615 LINE(STAT_STAGED, "", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
616 LINE(STAT_UNSTAGED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
617 LINE(STAT_UNTRACKED,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
618 LINE(BLAME_ID, "", COLOR_MAGENTA, COLOR_DEFAULT, 0)
620 enum line_type {
621 #define LINE(type, line, fg, bg, attr) \
622 LINE_##type
623 LINE_INFO,
624 LINE_NONE
625 #undef LINE
626 };
628 struct line_info {
629 const char *name; /* Option name. */
630 int namelen; /* Size of option name. */
631 const char *line; /* The start of line to match. */
632 int linelen; /* Size of string to match. */
633 int fg, bg, attr; /* Color and text attributes for the lines. */
634 };
636 static struct line_info line_info[] = {
637 #define LINE(type, line, fg, bg, attr) \
638 { #type, STRING_SIZE(#type), (line), STRING_SIZE(line), (fg), (bg), (attr) }
639 LINE_INFO
640 #undef LINE
641 };
643 static enum line_type
644 get_line_type(char *line)
645 {
646 int linelen = strlen(line);
647 enum line_type type;
649 for (type = 0; type < ARRAY_SIZE(line_info); type++)
650 /* Case insensitive search matches Signed-off-by lines better. */
651 if (linelen >= line_info[type].linelen &&
652 !strncasecmp(line_info[type].line, line, line_info[type].linelen))
653 return type;
655 return LINE_DEFAULT;
656 }
658 static inline int
659 get_line_attr(enum line_type type)
660 {
661 assert(type < ARRAY_SIZE(line_info));
662 return COLOR_PAIR(type) | line_info[type].attr;
663 }
665 static struct line_info *
666 get_line_info(char *name)
667 {
668 size_t namelen = strlen(name);
669 enum line_type type;
671 for (type = 0; type < ARRAY_SIZE(line_info); type++)
672 if (namelen == line_info[type].namelen &&
673 !string_enum_compare(line_info[type].name, name, namelen))
674 return &line_info[type];
676 return NULL;
677 }
679 static void
680 init_colors(void)
681 {
682 int default_bg = line_info[LINE_DEFAULT].bg;
683 int default_fg = line_info[LINE_DEFAULT].fg;
684 enum line_type type;
686 start_color();
688 if (assume_default_colors(default_fg, default_bg) == ERR) {
689 default_bg = COLOR_BLACK;
690 default_fg = COLOR_WHITE;
691 }
693 for (type = 0; type < ARRAY_SIZE(line_info); type++) {
694 struct line_info *info = &line_info[type];
695 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
696 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
698 init_pair(type, fg, bg);
699 }
700 }
702 struct line {
703 enum line_type type;
705 /* State flags */
706 unsigned int selected:1;
707 unsigned int dirty:1;
709 void *data; /* User data */
710 };
713 /*
714 * Keys
715 */
717 struct keybinding {
718 int alias;
719 enum request request;
720 struct keybinding *next;
721 };
723 static struct keybinding default_keybindings[] = {
724 /* View switching */
725 { 'm', REQ_VIEW_MAIN },
726 { 'd', REQ_VIEW_DIFF },
727 { 'l', REQ_VIEW_LOG },
728 { 't', REQ_VIEW_TREE },
729 { 'f', REQ_VIEW_BLOB },
730 { 'B', REQ_VIEW_BLAME },
731 { 'p', REQ_VIEW_PAGER },
732 { 'h', REQ_VIEW_HELP },
733 { 'S', REQ_VIEW_STATUS },
734 { 'c', REQ_VIEW_STAGE },
736 /* View manipulation */
737 { 'q', REQ_VIEW_CLOSE },
738 { KEY_TAB, REQ_VIEW_NEXT },
739 { KEY_RETURN, REQ_ENTER },
740 { KEY_UP, REQ_PREVIOUS },
741 { KEY_DOWN, REQ_NEXT },
742 { 'R', REQ_REFRESH },
743 { KEY_F(5), REQ_REFRESH },
744 { 'O', REQ_MAXIMIZE },
746 /* Cursor navigation */
747 { 'k', REQ_MOVE_UP },
748 { 'j', REQ_MOVE_DOWN },
749 { KEY_HOME, REQ_MOVE_FIRST_LINE },
750 { KEY_END, REQ_MOVE_LAST_LINE },
751 { KEY_NPAGE, REQ_MOVE_PAGE_DOWN },
752 { ' ', REQ_MOVE_PAGE_DOWN },
753 { KEY_PPAGE, REQ_MOVE_PAGE_UP },
754 { 'b', REQ_MOVE_PAGE_UP },
755 { '-', REQ_MOVE_PAGE_UP },
757 /* Scrolling */
758 { KEY_IC, REQ_SCROLL_LINE_UP },
759 { KEY_DC, REQ_SCROLL_LINE_DOWN },
760 { 'w', REQ_SCROLL_PAGE_UP },
761 { 's', REQ_SCROLL_PAGE_DOWN },
763 /* Searching */
764 { '/', REQ_SEARCH },
765 { '?', REQ_SEARCH_BACK },
766 { 'n', REQ_FIND_NEXT },
767 { 'N', REQ_FIND_PREV },
769 /* Misc */
770 { 'Q', REQ_QUIT },
771 { 'z', REQ_STOP_LOADING },
772 { 'v', REQ_SHOW_VERSION },
773 { 'r', REQ_SCREEN_REDRAW },
774 { '.', REQ_TOGGLE_LINENO },
775 { 'D', REQ_TOGGLE_DATE },
776 { 'A', REQ_TOGGLE_AUTHOR },
777 { 'g', REQ_TOGGLE_REV_GRAPH },
778 { 'F', REQ_TOGGLE_REFS },
779 { ':', REQ_PROMPT },
780 { 'u', REQ_STATUS_UPDATE },
781 { 'M', REQ_STATUS_MERGE },
782 { ',', REQ_TREE_PARENT },
783 { 'e', REQ_EDIT },
785 /* Using the ncurses SIGWINCH handler. */
786 { KEY_RESIZE, REQ_SCREEN_RESIZE },
787 };
789 #define KEYMAP_INFO \
790 KEYMAP_(GENERIC), \
791 KEYMAP_(MAIN), \
792 KEYMAP_(DIFF), \
793 KEYMAP_(LOG), \
794 KEYMAP_(TREE), \
795 KEYMAP_(BLOB), \
796 KEYMAP_(BLAME), \
797 KEYMAP_(PAGER), \
798 KEYMAP_(HELP), \
799 KEYMAP_(STATUS), \
800 KEYMAP_(STAGE)
802 enum keymap {
803 #define KEYMAP_(name) KEYMAP_##name
804 KEYMAP_INFO
805 #undef KEYMAP_
806 };
808 static struct int_map keymap_table[] = {
809 #define KEYMAP_(name) { #name, STRING_SIZE(#name), KEYMAP_##name }
810 KEYMAP_INFO
811 #undef KEYMAP_
812 };
814 #define set_keymap(map, name) \
815 set_from_int_map(keymap_table, ARRAY_SIZE(keymap_table), map, name, strlen(name))
817 static struct keybinding *keybindings[ARRAY_SIZE(keymap_table)];
819 static void
820 add_keybinding(enum keymap keymap, enum request request, int key)
821 {
822 struct keybinding *keybinding;
824 keybinding = calloc(1, sizeof(*keybinding));
825 if (!keybinding)
826 die("Failed to allocate keybinding");
828 keybinding->alias = key;
829 keybinding->request = request;
830 keybinding->next = keybindings[keymap];
831 keybindings[keymap] = keybinding;
832 }
834 /* Looks for a key binding first in the given map, then in the generic map, and
835 * lastly in the default keybindings. */
836 static enum request
837 get_keybinding(enum keymap keymap, int key)
838 {
839 struct keybinding *kbd;
840 int i;
842 for (kbd = keybindings[keymap]; kbd; kbd = kbd->next)
843 if (kbd->alias == key)
844 return kbd->request;
846 for (kbd = keybindings[KEYMAP_GENERIC]; kbd; kbd = kbd->next)
847 if (kbd->alias == key)
848 return kbd->request;
850 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++)
851 if (default_keybindings[i].alias == key)
852 return default_keybindings[i].request;
854 return (enum request) key;
855 }
858 struct key {
859 char *name;
860 int value;
861 };
863 static struct key key_table[] = {
864 { "Enter", KEY_RETURN },
865 { "Space", ' ' },
866 { "Backspace", KEY_BACKSPACE },
867 { "Tab", KEY_TAB },
868 { "Escape", KEY_ESC },
869 { "Left", KEY_LEFT },
870 { "Right", KEY_RIGHT },
871 { "Up", KEY_UP },
872 { "Down", KEY_DOWN },
873 { "Insert", KEY_IC },
874 { "Delete", KEY_DC },
875 { "Hash", '#' },
876 { "Home", KEY_HOME },
877 { "End", KEY_END },
878 { "PageUp", KEY_PPAGE },
879 { "PageDown", KEY_NPAGE },
880 { "F1", KEY_F(1) },
881 { "F2", KEY_F(2) },
882 { "F3", KEY_F(3) },
883 { "F4", KEY_F(4) },
884 { "F5", KEY_F(5) },
885 { "F6", KEY_F(6) },
886 { "F7", KEY_F(7) },
887 { "F8", KEY_F(8) },
888 { "F9", KEY_F(9) },
889 { "F10", KEY_F(10) },
890 { "F11", KEY_F(11) },
891 { "F12", KEY_F(12) },
892 };
894 static int
895 get_key_value(const char *name)
896 {
897 int i;
899 for (i = 0; i < ARRAY_SIZE(key_table); i++)
900 if (!strcasecmp(key_table[i].name, name))
901 return key_table[i].value;
903 if (strlen(name) == 1 && isprint(*name))
904 return (int) *name;
906 return ERR;
907 }
909 static char *
910 get_key_name(int key_value)
911 {
912 static char key_char[] = "'X'";
913 char *seq = NULL;
914 int key;
916 for (key = 0; key < ARRAY_SIZE(key_table); key++)
917 if (key_table[key].value == key_value)
918 seq = key_table[key].name;
920 if (seq == NULL &&
921 key_value < 127 &&
922 isprint(key_value)) {
923 key_char[1] = (char) key_value;
924 seq = key_char;
925 }
927 return seq ? seq : "'?'";
928 }
930 static char *
931 get_key(enum request request)
932 {
933 static char buf[BUFSIZ];
934 size_t pos = 0;
935 char *sep = "";
936 int i;
938 buf[pos] = 0;
940 for (i = 0; i < ARRAY_SIZE(default_keybindings); i++) {
941 struct keybinding *keybinding = &default_keybindings[i];
943 if (keybinding->request != request)
944 continue;
946 if (!string_format_from(buf, &pos, "%s%s", sep,
947 get_key_name(keybinding->alias)))
948 return "Too many keybindings!";
949 sep = ", ";
950 }
952 return buf;
953 }
955 struct run_request {
956 enum keymap keymap;
957 int key;
958 char cmd[SIZEOF_STR];
959 };
961 static struct run_request *run_request;
962 static size_t run_requests;
964 static enum request
965 add_run_request(enum keymap keymap, int key, int argc, char **argv)
966 {
967 struct run_request *req;
968 char cmd[SIZEOF_STR];
969 size_t bufpos;
971 for (bufpos = 0; argc > 0; argc--, argv++)
972 if (!string_format_from(cmd, &bufpos, "%s ", *argv))
973 return REQ_NONE;
975 req = realloc(run_request, (run_requests + 1) * sizeof(*run_request));
976 if (!req)
977 return REQ_NONE;
979 run_request = req;
980 req = &run_request[run_requests++];
981 string_copy(req->cmd, cmd);
982 req->keymap = keymap;
983 req->key = key;
985 return REQ_NONE + run_requests;
986 }
988 static struct run_request *
989 get_run_request(enum request request)
990 {
991 if (request <= REQ_NONE)
992 return NULL;
993 return &run_request[request - REQ_NONE - 1];
994 }
996 static void
997 add_builtin_run_requests(void)
998 {
999 struct {
1000 enum keymap keymap;
1001 int key;
1002 char *argv[1];
1003 } reqs[] = {
1004 { KEYMAP_MAIN, 'C', { "git cherry-pick %(commit)" } },
1005 { KEYMAP_GENERIC, 'G', { "git gc" } },
1006 };
1007 int i;
1009 for (i = 0; i < ARRAY_SIZE(reqs); i++) {
1010 enum request req;
1012 req = add_run_request(reqs[i].keymap, reqs[i].key, 1, reqs[i].argv);
1013 if (req != REQ_NONE)
1014 add_keybinding(reqs[i].keymap, req, reqs[i].key);
1015 }
1016 }
1018 /*
1019 * User config file handling.
1020 */
1022 static struct int_map color_map[] = {
1023 #define COLOR_MAP(name) { #name, STRING_SIZE(#name), COLOR_##name }
1024 COLOR_MAP(DEFAULT),
1025 COLOR_MAP(BLACK),
1026 COLOR_MAP(BLUE),
1027 COLOR_MAP(CYAN),
1028 COLOR_MAP(GREEN),
1029 COLOR_MAP(MAGENTA),
1030 COLOR_MAP(RED),
1031 COLOR_MAP(WHITE),
1032 COLOR_MAP(YELLOW),
1033 };
1035 #define set_color(color, name) \
1036 set_from_int_map(color_map, ARRAY_SIZE(color_map), color, name, strlen(name))
1038 static struct int_map attr_map[] = {
1039 #define ATTR_MAP(name) { #name, STRING_SIZE(#name), A_##name }
1040 ATTR_MAP(NORMAL),
1041 ATTR_MAP(BLINK),
1042 ATTR_MAP(BOLD),
1043 ATTR_MAP(DIM),
1044 ATTR_MAP(REVERSE),
1045 ATTR_MAP(STANDOUT),
1046 ATTR_MAP(UNDERLINE),
1047 };
1049 #define set_attribute(attr, name) \
1050 set_from_int_map(attr_map, ARRAY_SIZE(attr_map), attr, name, strlen(name))
1052 static int config_lineno;
1053 static bool config_errors;
1054 static char *config_msg;
1056 /* Wants: object fgcolor bgcolor [attr] */
1057 static int
1058 option_color_command(int argc, char *argv[])
1059 {
1060 struct line_info *info;
1062 if (argc != 3 && argc != 4) {
1063 config_msg = "Wrong number of arguments given to color command";
1064 return ERR;
1065 }
1067 info = get_line_info(argv[0]);
1068 if (!info) {
1069 if (!string_enum_compare(argv[0], "main-delim", strlen("main-delim"))) {
1070 info = get_line_info("delimiter");
1072 } else if (!string_enum_compare(argv[0], "main-date", strlen("main-date"))) {
1073 info = get_line_info("date");
1075 } else {
1076 config_msg = "Unknown color name";
1077 return ERR;
1078 }
1079 }
1081 if (set_color(&info->fg, argv[1]) == ERR ||
1082 set_color(&info->bg, argv[2]) == ERR) {
1083 config_msg = "Unknown color";
1084 return ERR;
1085 }
1087 if (argc == 4 && set_attribute(&info->attr, argv[3]) == ERR) {
1088 config_msg = "Unknown attribute";
1089 return ERR;
1090 }
1092 return OK;
1093 }
1095 static bool parse_bool(const char *s)
1096 {
1097 return (!strcmp(s, "1") || !strcmp(s, "true") ||
1098 !strcmp(s, "yes")) ? TRUE : FALSE;
1099 }
1101 /* Wants: name = value */
1102 static int
1103 option_set_command(int argc, char *argv[])
1104 {
1105 if (argc != 3) {
1106 config_msg = "Wrong number of arguments given to set command";
1107 return ERR;
1108 }
1110 if (strcmp(argv[1], "=")) {
1111 config_msg = "No value assigned";
1112 return ERR;
1113 }
1115 if (!strcmp(argv[0], "show-author")) {
1116 opt_author = parse_bool(argv[2]);
1117 return OK;
1118 }
1120 if (!strcmp(argv[0], "show-date")) {
1121 opt_date = parse_bool(argv[2]);
1122 return OK;
1123 }
1125 if (!strcmp(argv[0], "show-rev-graph")) {
1126 opt_rev_graph = parse_bool(argv[2]);
1127 return OK;
1128 }
1130 if (!strcmp(argv[0], "show-refs")) {
1131 opt_show_refs = parse_bool(argv[2]);
1132 return OK;
1133 }
1135 if (!strcmp(argv[0], "show-line-numbers")) {
1136 opt_line_number = parse_bool(argv[2]);
1137 return OK;
1138 }
1140 if (!strcmp(argv[0], "line-graphics")) {
1141 opt_line_graphics = parse_bool(argv[2]);
1142 return OK;
1143 }
1145 if (!strcmp(argv[0], "line-number-interval")) {
1146 opt_num_interval = atoi(argv[2]);
1147 return OK;
1148 }
1150 if (!strcmp(argv[0], "tab-size")) {
1151 opt_tab_size = atoi(argv[2]);
1152 return OK;
1153 }
1155 if (!strcmp(argv[0], "commit-encoding")) {
1156 char *arg = argv[2];
1157 int delimiter = *arg;
1158 int i;
1160 switch (delimiter) {
1161 case '"':
1162 case '\'':
1163 for (arg++, i = 0; arg[i]; i++)
1164 if (arg[i] == delimiter) {
1165 arg[i] = 0;
1166 break;
1167 }
1168 default:
1169 string_ncopy(opt_encoding, arg, strlen(arg));
1170 return OK;
1171 }
1172 }
1174 config_msg = "Unknown variable name";
1175 return ERR;
1176 }
1178 /* Wants: mode request key */
1179 static int
1180 option_bind_command(int argc, char *argv[])
1181 {
1182 enum request request;
1183 int keymap;
1184 int key;
1186 if (argc < 3) {
1187 config_msg = "Wrong number of arguments given to bind command";
1188 return ERR;
1189 }
1191 if (set_keymap(&keymap, argv[0]) == ERR) {
1192 config_msg = "Unknown key map";
1193 return ERR;
1194 }
1196 key = get_key_value(argv[1]);
1197 if (key == ERR) {
1198 config_msg = "Unknown key";
1199 return ERR;
1200 }
1202 request = get_request(argv[2]);
1203 if (request == REQ_NONE) {
1204 const char *obsolete[] = { "cherry-pick" };
1205 size_t namelen = strlen(argv[2]);
1206 int i;
1208 for (i = 0; i < ARRAY_SIZE(obsolete); i++) {
1209 if (namelen == strlen(obsolete[i]) &&
1210 !string_enum_compare(obsolete[i], argv[2], namelen)) {
1211 config_msg = "Obsolete request name";
1212 return ERR;
1213 }
1214 }
1215 }
1216 if (request == REQ_NONE && *argv[2]++ == '!')
1217 request = add_run_request(keymap, key, argc - 2, argv + 2);
1218 if (request == REQ_NONE) {
1219 config_msg = "Unknown request name";
1220 return ERR;
1221 }
1223 add_keybinding(keymap, request, key);
1225 return OK;
1226 }
1228 static int
1229 set_option(char *opt, char *value)
1230 {
1231 char *argv[16];
1232 int valuelen;
1233 int argc = 0;
1235 /* Tokenize */
1236 while (argc < ARRAY_SIZE(argv) && (valuelen = strcspn(value, " \t"))) {
1237 argv[argc++] = value;
1238 value += valuelen;
1240 /* Nothing more to tokenize or last available token. */
1241 if (!*value || argc >= ARRAY_SIZE(argv))
1242 break;
1244 *value++ = 0;
1245 while (isspace(*value))
1246 value++;
1247 }
1249 if (!strcmp(opt, "color"))
1250 return option_color_command(argc, argv);
1252 if (!strcmp(opt, "set"))
1253 return option_set_command(argc, argv);
1255 if (!strcmp(opt, "bind"))
1256 return option_bind_command(argc, argv);
1258 config_msg = "Unknown option command";
1259 return ERR;
1260 }
1262 static int
1263 read_option(char *opt, size_t optlen, char *value, size_t valuelen)
1264 {
1265 int status = OK;
1267 config_lineno++;
1268 config_msg = "Internal error";
1270 /* Check for comment markers, since read_properties() will
1271 * only ensure opt and value are split at first " \t". */
1272 optlen = strcspn(opt, "#");
1273 if (optlen == 0)
1274 return OK;
1276 if (opt[optlen] != 0) {
1277 config_msg = "No option value";
1278 status = ERR;
1280 } else {
1281 /* Look for comment endings in the value. */
1282 size_t len = strcspn(value, "#");
1284 if (len < valuelen) {
1285 valuelen = len;
1286 value[valuelen] = 0;
1287 }
1289 status = set_option(opt, value);
1290 }
1292 if (status == ERR) {
1293 fprintf(stderr, "Error on line %d, near '%.*s': %s\n",
1294 config_lineno, (int) optlen, opt, config_msg);
1295 config_errors = TRUE;
1296 }
1298 /* Always keep going if errors are encountered. */
1299 return OK;
1300 }
1302 static void
1303 load_option_file(const char *path)
1304 {
1305 FILE *file;
1307 /* It's ok that the file doesn't exist. */
1308 file = fopen(path, "r");
1309 if (!file)
1310 return;
1312 config_lineno = 0;
1313 config_errors = FALSE;
1315 if (read_properties(file, " \t", read_option) == ERR ||
1316 config_errors == TRUE)
1317 fprintf(stderr, "Errors while loading %s.\n", path);
1318 }
1320 static int
1321 load_options(void)
1322 {
1323 char *home = getenv("HOME");
1324 char *tigrc_user = getenv("TIGRC_USER");
1325 char *tigrc_system = getenv("TIGRC_SYSTEM");
1326 char buf[SIZEOF_STR];
1328 add_builtin_run_requests();
1330 if (!tigrc_system) {
1331 if (!string_format(buf, "%s/tigrc", SYSCONFDIR))
1332 return ERR;
1333 tigrc_system = buf;
1334 }
1335 load_option_file(tigrc_system);
1337 if (!tigrc_user) {
1338 if (!home || !string_format(buf, "%s/.tigrc", home))
1339 return ERR;
1340 tigrc_user = buf;
1341 }
1342 load_option_file(tigrc_user);
1344 return OK;
1345 }
1348 /*
1349 * The viewer
1350 */
1352 struct view;
1353 struct view_ops;
1355 /* The display array of active views and the index of the current view. */
1356 static struct view *display[2];
1357 static unsigned int current_view;
1359 /* Reading from the prompt? */
1360 static bool input_mode = FALSE;
1362 #define foreach_displayed_view(view, i) \
1363 for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
1365 #define displayed_views() (display[1] != NULL ? 2 : 1)
1367 /* Current head and commit ID */
1368 static char ref_blob[SIZEOF_REF] = "";
1369 static char ref_commit[SIZEOF_REF] = "HEAD";
1370 static char ref_head[SIZEOF_REF] = "HEAD";
1372 struct view {
1373 const char *name; /* View name */
1374 const char *cmd_fmt; /* Default command line format */
1375 const char *cmd_env; /* Command line set via environment */
1376 const char *id; /* Points to either of ref_{head,commit,blob} */
1378 struct view_ops *ops; /* View operations */
1380 enum keymap keymap; /* What keymap does this view have */
1381 bool git_dir; /* Whether the view requires a git directory. */
1383 char cmd[SIZEOF_STR]; /* Command buffer */
1384 char ref[SIZEOF_REF]; /* Hovered commit reference */
1385 char vid[SIZEOF_REF]; /* View ID. Set to id member when updating. */
1387 int height, width; /* The width and height of the main window */
1388 WINDOW *win; /* The main window */
1389 WINDOW *title; /* The title window living below the main window */
1391 /* Navigation */
1392 unsigned long offset; /* Offset of the window top */
1393 unsigned long lineno; /* Current line number */
1395 /* Searching */
1396 char grep[SIZEOF_STR]; /* Search string */
1397 regex_t *regex; /* Pre-compiled regex */
1399 /* If non-NULL, points to the view that opened this view. If this view
1400 * is closed tig will switch back to the parent view. */
1401 struct view *parent;
1403 /* Buffering */
1404 size_t lines; /* Total number of lines */
1405 struct line *line; /* Line index */
1406 size_t line_alloc; /* Total number of allocated lines */
1407 size_t line_size; /* Total number of used lines */
1408 unsigned int digits; /* Number of digits in the lines member. */
1410 /* Drawing */
1411 struct line *curline; /* Line currently being drawn. */
1412 enum line_type curtype; /* Attribute currently used for drawing. */
1413 unsigned long col; /* Column when drawing. */
1415 /* Loading */
1416 FILE *pipe;
1417 time_t start_time;
1418 };
1420 struct view_ops {
1421 /* What type of content being displayed. Used in the title bar. */
1422 const char *type;
1423 /* Open and reads in all view content. */
1424 bool (*open)(struct view *view);
1425 /* Read one line; updates view->line. */
1426 bool (*read)(struct view *view, char *data);
1427 /* Draw one line; @lineno must be < view->height. */
1428 bool (*draw)(struct view *view, struct line *line, unsigned int lineno);
1429 /* Depending on view handle a special requests. */
1430 enum request (*request)(struct view *view, enum request request, struct line *line);
1431 /* Search for regex in a line. */
1432 bool (*grep)(struct view *view, struct line *line);
1433 /* Select line */
1434 void (*select)(struct view *view, struct line *line);
1435 };
1437 static struct view_ops pager_ops;
1438 static struct view_ops main_ops;
1439 static struct view_ops tree_ops;
1440 static struct view_ops blob_ops;
1441 static struct view_ops blame_ops;
1442 static struct view_ops help_ops;
1443 static struct view_ops status_ops;
1444 static struct view_ops stage_ops;
1446 #define VIEW_STR(name, cmd, env, ref, ops, map, git) \
1447 { name, cmd, #env, ref, ops, map, git }
1449 #define VIEW_(id, name, ops, git, ref) \
1450 VIEW_STR(name, TIG_##id##_CMD, TIG_##id##_CMD, ref, ops, KEYMAP_##id, git)
1453 static struct view views[] = {
1454 VIEW_(MAIN, "main", &main_ops, TRUE, ref_head),
1455 VIEW_(DIFF, "diff", &pager_ops, TRUE, ref_commit),
1456 VIEW_(LOG, "log", &pager_ops, TRUE, ref_head),
1457 VIEW_(TREE, "tree", &tree_ops, TRUE, ref_commit),
1458 VIEW_(BLOB, "blob", &blob_ops, TRUE, ref_blob),
1459 VIEW_(BLAME, "blame", &blame_ops, TRUE, ref_commit),
1460 VIEW_(HELP, "help", &help_ops, FALSE, ""),
1461 VIEW_(PAGER, "pager", &pager_ops, FALSE, "stdin"),
1462 VIEW_(STATUS, "status", &status_ops, TRUE, ""),
1463 VIEW_(STAGE, "stage", &stage_ops, TRUE, ""),
1464 };
1466 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
1467 #define VIEW_REQ(view) ((view) - views + REQ_OFFSET + 1)
1469 #define foreach_view(view, i) \
1470 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
1472 #define view_is_displayed(view) \
1473 (view == display[0] || view == display[1])
1476 enum line_graphic {
1477 LINE_GRAPHIC_VLINE
1478 };
1480 static int line_graphics[] = {
1481 /* LINE_GRAPHIC_VLINE: */ '|'
1482 };
1484 static inline void
1485 set_view_attr(struct view *view, enum line_type type)
1486 {
1487 if (!view->curline->selected && view->curtype != type) {
1488 wattrset(view->win, get_line_attr(type));
1489 wchgat(view->win, -1, 0, type, NULL);
1490 view->curtype = type;
1491 }
1492 }
1494 static int
1495 draw_chars(struct view *view, enum line_type type, const char *string,
1496 int max_len, bool use_tilde)
1497 {
1498 int len = 0;
1499 int col = 0;
1500 int trimmed = FALSE;
1502 if (max_len <= 0)
1503 return 0;
1505 if (opt_utf8) {
1506 len = utf8_length(string, &col, max_len, &trimmed, use_tilde);
1507 } else {
1508 col = len = strlen(string);
1509 if (len > max_len) {
1510 if (use_tilde) {
1511 max_len -= 1;
1512 }
1513 col = len = max_len;
1514 trimmed = TRUE;
1515 }
1516 }
1518 set_view_attr(view, type);
1519 waddnstr(view->win, string, len);
1520 if (trimmed && use_tilde) {
1521 set_view_attr(view, LINE_DELIMITER);
1522 waddch(view->win, '~');
1523 col++;
1524 }
1526 return col;
1527 }
1529 static int
1530 draw_space(struct view *view, enum line_type type, int max, int spaces)
1531 {
1532 static char space[] = " ";
1533 int col = 0;
1535 spaces = MIN(max, spaces);
1537 while (spaces > 0) {
1538 int len = MIN(spaces, sizeof(space) - 1);
1540 col += draw_chars(view, type, space, spaces, FALSE);
1541 spaces -= len;
1542 }
1544 return col;
1545 }
1547 static bool
1548 draw_lineno(struct view *view, unsigned int lineno)
1549 {
1550 char number[10];
1551 int digits3 = view->digits < 3 ? 3 : view->digits;
1552 int max_number = MIN(digits3, STRING_SIZE(number));
1553 int max = view->width - view->col;
1554 int col;
1556 if (max < max_number)
1557 max_number = max;
1559 lineno += view->offset + 1;
1560 if (lineno == 1 || (lineno % opt_num_interval) == 0) {
1561 static char fmt[] = "%1ld";
1563 if (view->digits <= 9)
1564 fmt[1] = '0' + digits3;
1566 if (!string_format(number, fmt, lineno))
1567 number[0] = 0;
1568 col = draw_chars(view, LINE_LINE_NUMBER, number, max_number, TRUE);
1569 } else {
1570 col = draw_space(view, LINE_LINE_NUMBER, max_number, max_number);
1571 }
1573 if (col < max) {
1574 set_view_attr(view, LINE_DEFAULT);
1575 waddch(view->win, line_graphics[LINE_GRAPHIC_VLINE]);
1576 col++;
1577 }
1579 if (col < max)
1580 col += draw_space(view, LINE_DEFAULT, max - col, 1);
1581 view->col += col;
1583 return view->width - view->col <= 0;
1584 }
1586 static bool
1587 draw_text(struct view *view, enum line_type type, const char *string, bool trim)
1588 {
1589 view->col += draw_chars(view, type, string, view->width - view->col, trim);
1590 return view->width - view->col <= 0;
1591 }
1593 static bool
1594 draw_graphic(struct view *view, enum line_type type, chtype graphic[], size_t size)
1595 {
1596 int max = view->width - view->col;
1597 int i;
1599 if (max < size)
1600 size = max;
1602 set_view_attr(view, type);
1603 /* Using waddch() instead of waddnstr() ensures that
1604 * they'll be rendered correctly for the cursor line. */
1605 for (i = 0; i < size; i++)
1606 waddch(view->win, graphic[i]);
1608 view->col += size;
1609 if (size < max) {
1610 waddch(view->win, ' ');
1611 view->col++;
1612 }
1614 return view->width - view->col <= 0;
1615 }
1617 static bool
1618 draw_field(struct view *view, enum line_type type, char *text, int len, bool trim)
1619 {
1620 int max = MIN(view->width - view->col, len);
1621 int col;
1623 if (text)
1624 col = draw_chars(view, type, text, max - 1, trim);
1625 else
1626 col = draw_space(view, type, max - 1, max - 1);
1628 view->col += col + draw_space(view, LINE_DEFAULT, max - col, max - col);
1629 return view->width - view->col <= 0;
1630 }
1632 static bool
1633 draw_date(struct view *view, struct tm *time)
1634 {
1635 char buf[DATE_COLS];
1636 char *date;
1637 int timelen = 0;
1639 if (time)
1640 timelen = strftime(buf, sizeof(buf), DATE_FORMAT, time);
1641 date = timelen ? buf : NULL;
1643 return draw_field(view, LINE_DATE, date, DATE_COLS, FALSE);
1644 }
1646 static bool
1647 draw_view_line(struct view *view, unsigned int lineno)
1648 {
1649 struct line *line;
1650 bool selected = (view->offset + lineno == view->lineno);
1651 bool draw_ok;
1653 assert(view_is_displayed(view));
1655 if (view->offset + lineno >= view->lines)
1656 return FALSE;
1658 line = &view->line[view->offset + lineno];
1660 wmove(view->win, lineno, 0);
1661 view->col = 0;
1662 view->curline = line;
1663 view->curtype = LINE_NONE;
1664 line->selected = FALSE;
1666 if (selected) {
1667 set_view_attr(view, LINE_CURSOR);
1668 line->selected = TRUE;
1669 view->ops->select(view, line);
1670 } else if (line->selected) {
1671 wclrtoeol(view->win);
1672 }
1674 scrollok(view->win, FALSE);
1675 draw_ok = view->ops->draw(view, line, lineno);
1676 scrollok(view->win, TRUE);
1678 return draw_ok;
1679 }
1681 static void
1682 redraw_view_dirty(struct view *view)
1683 {
1684 bool dirty = FALSE;
1685 int lineno;
1687 for (lineno = 0; lineno < view->height; lineno++) {
1688 struct line *line = &view->line[view->offset + lineno];
1690 if (!line->dirty)
1691 continue;
1692 line->dirty = 0;
1693 dirty = TRUE;
1694 if (!draw_view_line(view, lineno))
1695 break;
1696 }
1698 if (!dirty)
1699 return;
1700 redrawwin(view->win);
1701 if (input_mode)
1702 wnoutrefresh(view->win);
1703 else
1704 wrefresh(view->win);
1705 }
1707 static void
1708 redraw_view_from(struct view *view, int lineno)
1709 {
1710 assert(0 <= lineno && lineno < view->height);
1712 for (; lineno < view->height; lineno++) {
1713 if (!draw_view_line(view, lineno))
1714 break;
1715 }
1717 redrawwin(view->win);
1718 if (input_mode)
1719 wnoutrefresh(view->win);
1720 else
1721 wrefresh(view->win);
1722 }
1724 static void
1725 redraw_view(struct view *view)
1726 {
1727 wclear(view->win);
1728 redraw_view_from(view, 0);
1729 }
1732 static void
1733 update_view_title(struct view *view)
1734 {
1735 char buf[SIZEOF_STR];
1736 char state[SIZEOF_STR];
1737 size_t bufpos = 0, statelen = 0;
1739 assert(view_is_displayed(view));
1741 if (view != VIEW(REQ_VIEW_STATUS) && (view->lines || view->pipe)) {
1742 unsigned int view_lines = view->offset + view->height;
1743 unsigned int lines = view->lines
1744 ? MIN(view_lines, view->lines) * 100 / view->lines
1745 : 0;
1747 string_format_from(state, &statelen, "- %s %d of %d (%d%%)",
1748 view->ops->type,
1749 view->lineno + 1,
1750 view->lines,
1751 lines);
1753 if (view->pipe) {
1754 time_t secs = time(NULL) - view->start_time;
1756 /* Three git seconds are a long time ... */
1757 if (secs > 2)
1758 string_format_from(state, &statelen, " %lds", secs);
1759 }
1760 }
1762 string_format_from(buf, &bufpos, "[%s]", view->name);
1763 if (*view->ref && bufpos < view->width) {
1764 size_t refsize = strlen(view->ref);
1765 size_t minsize = bufpos + 1 + /* abbrev= */ 7 + 1 + statelen;
1767 if (minsize < view->width)
1768 refsize = view->width - minsize + 7;
1769 string_format_from(buf, &bufpos, " %.*s", (int) refsize, view->ref);
1770 }
1772 if (statelen && bufpos < view->width) {
1773 string_format_from(buf, &bufpos, " %s", state);
1774 }
1776 if (view == display[current_view])
1777 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
1778 else
1779 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
1781 mvwaddnstr(view->title, 0, 0, buf, bufpos);
1782 wclrtoeol(view->title);
1783 wmove(view->title, 0, view->width - 1);
1785 if (input_mode)
1786 wnoutrefresh(view->title);
1787 else
1788 wrefresh(view->title);
1789 }
1791 static void
1792 resize_display(void)
1793 {
1794 int offset, i;
1795 struct view *base = display[0];
1796 struct view *view = display[1] ? display[1] : display[0];
1798 /* Setup window dimensions */
1800 getmaxyx(stdscr, base->height, base->width);
1802 /* Make room for the status window. */
1803 base->height -= 1;
1805 if (view != base) {
1806 /* Horizontal split. */
1807 view->width = base->width;
1808 view->height = SCALE_SPLIT_VIEW(base->height);
1809 base->height -= view->height;
1811 /* Make room for the title bar. */
1812 view->height -= 1;
1813 }
1815 /* Make room for the title bar. */
1816 base->height -= 1;
1818 offset = 0;
1820 foreach_displayed_view (view, i) {
1821 if (!view->win) {
1822 view->win = newwin(view->height, 0, offset, 0);
1823 if (!view->win)
1824 die("Failed to create %s view", view->name);
1826 scrollok(view->win, TRUE);
1828 view->title = newwin(1, 0, offset + view->height, 0);
1829 if (!view->title)
1830 die("Failed to create title window");
1832 } else {
1833 wresize(view->win, view->height, view->width);
1834 mvwin(view->win, offset, 0);
1835 mvwin(view->title, offset + view->height, 0);
1836 }
1838 offset += view->height + 1;
1839 }
1840 }
1842 static void
1843 redraw_display(void)
1844 {
1845 struct view *view;
1846 int i;
1848 foreach_displayed_view (view, i) {
1849 redraw_view(view);
1850 update_view_title(view);
1851 }
1852 }
1854 static void
1855 update_display_cursor(struct view *view)
1856 {
1857 /* Move the cursor to the right-most column of the cursor line.
1858 *
1859 * XXX: This could turn out to be a bit expensive, but it ensures that
1860 * the cursor does not jump around. */
1861 if (view->lines) {
1862 wmove(view->win, view->lineno - view->offset, view->width - 1);
1863 wrefresh(view->win);
1864 }
1865 }
1867 /*
1868 * Navigation
1869 */
1871 /* Scrolling backend */
1872 static void
1873 do_scroll_view(struct view *view, int lines)
1874 {
1875 bool redraw_current_line = FALSE;
1877 /* The rendering expects the new offset. */
1878 view->offset += lines;
1880 assert(0 <= view->offset && view->offset < view->lines);
1881 assert(lines);
1883 /* Move current line into the view. */
1884 if (view->lineno < view->offset) {
1885 view->lineno = view->offset;
1886 redraw_current_line = TRUE;
1887 } else if (view->lineno >= view->offset + view->height) {
1888 view->lineno = view->offset + view->height - 1;
1889 redraw_current_line = TRUE;
1890 }
1892 assert(view->offset <= view->lineno && view->lineno < view->lines);
1894 /* Redraw the whole screen if scrolling is pointless. */
1895 if (view->height < ABS(lines)) {
1896 redraw_view(view);
1898 } else {
1899 int line = lines > 0 ? view->height - lines : 0;
1900 int end = line + ABS(lines);
1902 wscrl(view->win, lines);
1904 for (; line < end; line++) {
1905 if (!draw_view_line(view, line))
1906 break;
1907 }
1909 if (redraw_current_line)
1910 draw_view_line(view, view->lineno - view->offset);
1911 }
1913 redrawwin(view->win);
1914 wrefresh(view->win);
1915 report("");
1916 }
1918 /* Scroll frontend */
1919 static void
1920 scroll_view(struct view *view, enum request request)
1921 {
1922 int lines = 1;
1924 assert(view_is_displayed(view));
1926 switch (request) {
1927 case REQ_SCROLL_PAGE_DOWN:
1928 lines = view->height;
1929 case REQ_SCROLL_LINE_DOWN:
1930 if (view->offset + lines > view->lines)
1931 lines = view->lines - view->offset;
1933 if (lines == 0 || view->offset + view->height >= view->lines) {
1934 report("Cannot scroll beyond the last line");
1935 return;
1936 }
1937 break;
1939 case REQ_SCROLL_PAGE_UP:
1940 lines = view->height;
1941 case REQ_SCROLL_LINE_UP:
1942 if (lines > view->offset)
1943 lines = view->offset;
1945 if (lines == 0) {
1946 report("Cannot scroll beyond the first line");
1947 return;
1948 }
1950 lines = -lines;
1951 break;
1953 default:
1954 die("request %d not handled in switch", request);
1955 }
1957 do_scroll_view(view, lines);
1958 }
1960 /* Cursor moving */
1961 static void
1962 move_view(struct view *view, enum request request)
1963 {
1964 int scroll_steps = 0;
1965 int steps;
1967 switch (request) {
1968 case REQ_MOVE_FIRST_LINE:
1969 steps = -view->lineno;
1970 break;
1972 case REQ_MOVE_LAST_LINE:
1973 steps = view->lines - view->lineno - 1;
1974 break;
1976 case REQ_MOVE_PAGE_UP:
1977 steps = view->height > view->lineno
1978 ? -view->lineno : -view->height;
1979 break;
1981 case REQ_MOVE_PAGE_DOWN:
1982 steps = view->lineno + view->height >= view->lines
1983 ? view->lines - view->lineno - 1 : view->height;
1984 break;
1986 case REQ_MOVE_UP:
1987 steps = -1;
1988 break;
1990 case REQ_MOVE_DOWN:
1991 steps = 1;
1992 break;
1994 default:
1995 die("request %d not handled in switch", request);
1996 }
1998 if (steps <= 0 && view->lineno == 0) {
1999 report("Cannot move beyond the first line");
2000 return;
2002 } else if (steps >= 0 && view->lineno + 1 >= view->lines) {
2003 report("Cannot move beyond the last line");
2004 return;
2005 }
2007 /* Move the current line */
2008 view->lineno += steps;
2009 assert(0 <= view->lineno && view->lineno < view->lines);
2011 /* Check whether the view needs to be scrolled */
2012 if (view->lineno < view->offset ||
2013 view->lineno >= view->offset + view->height) {
2014 scroll_steps = steps;
2015 if (steps < 0 && -steps > view->offset) {
2016 scroll_steps = -view->offset;
2018 } else if (steps > 0) {
2019 if (view->lineno == view->lines - 1 &&
2020 view->lines > view->height) {
2021 scroll_steps = view->lines - view->offset - 1;
2022 if (scroll_steps >= view->height)
2023 scroll_steps -= view->height - 1;
2024 }
2025 }
2026 }
2028 if (!view_is_displayed(view)) {
2029 view->offset += scroll_steps;
2030 assert(0 <= view->offset && view->offset < view->lines);
2031 view->ops->select(view, &view->line[view->lineno]);
2032 return;
2033 }
2035 /* Repaint the old "current" line if we be scrolling */
2036 if (ABS(steps) < view->height)
2037 draw_view_line(view, view->lineno - steps - view->offset);
2039 if (scroll_steps) {
2040 do_scroll_view(view, scroll_steps);
2041 return;
2042 }
2044 /* Draw the current line */
2045 draw_view_line(view, view->lineno - view->offset);
2047 redrawwin(view->win);
2048 wrefresh(view->win);
2049 report("");
2050 }
2053 /*
2054 * Searching
2055 */
2057 static void search_view(struct view *view, enum request request);
2059 static bool
2060 find_next_line(struct view *view, unsigned long lineno, struct line *line)
2061 {
2062 assert(view_is_displayed(view));
2064 if (!view->ops->grep(view, line))
2065 return FALSE;
2067 if (lineno - view->offset >= view->height) {
2068 view->offset = lineno;
2069 view->lineno = lineno;
2070 redraw_view(view);
2072 } else {
2073 unsigned long old_lineno = view->lineno - view->offset;
2075 view->lineno = lineno;
2076 draw_view_line(view, old_lineno);
2078 draw_view_line(view, view->lineno - view->offset);
2079 redrawwin(view->win);
2080 wrefresh(view->win);
2081 }
2083 report("Line %ld matches '%s'", lineno + 1, view->grep);
2084 return TRUE;
2085 }
2087 static void
2088 find_next(struct view *view, enum request request)
2089 {
2090 unsigned long lineno = view->lineno;
2091 int direction;
2093 if (!*view->grep) {
2094 if (!*opt_search)
2095 report("No previous search");
2096 else
2097 search_view(view, request);
2098 return;
2099 }
2101 switch (request) {
2102 case REQ_SEARCH:
2103 case REQ_FIND_NEXT:
2104 direction = 1;
2105 break;
2107 case REQ_SEARCH_BACK:
2108 case REQ_FIND_PREV:
2109 direction = -1;
2110 break;
2112 default:
2113 return;
2114 }
2116 if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
2117 lineno += direction;
2119 /* Note, lineno is unsigned long so will wrap around in which case it
2120 * will become bigger than view->lines. */
2121 for (; lineno < view->lines; lineno += direction) {
2122 struct line *line = &view->line[lineno];
2124 if (find_next_line(view, lineno, line))
2125 return;
2126 }
2128 report("No match found for '%s'", view->grep);
2129 }
2131 static void
2132 search_view(struct view *view, enum request request)
2133 {
2134 int regex_err;
2136 if (view->regex) {
2137 regfree(view->regex);
2138 *view->grep = 0;
2139 } else {
2140 view->regex = calloc(1, sizeof(*view->regex));
2141 if (!view->regex)
2142 return;
2143 }
2145 regex_err = regcomp(view->regex, opt_search, REG_EXTENDED);
2146 if (regex_err != 0) {
2147 char buf[SIZEOF_STR] = "unknown error";
2149 regerror(regex_err, view->regex, buf, sizeof(buf));
2150 report("Search failed: %s", buf);
2151 return;
2152 }
2154 string_copy(view->grep, opt_search);
2156 find_next(view, request);
2157 }
2159 /*
2160 * Incremental updating
2161 */
2163 static void
2164 end_update(struct view *view)
2165 {
2166 if (!view->pipe)
2167 return;
2168 set_nonblocking_input(FALSE);
2169 if (view->pipe == stdin)
2170 fclose(view->pipe);
2171 else
2172 pclose(view->pipe);
2173 view->pipe = NULL;
2174 }
2176 static bool
2177 begin_update(struct view *view)
2178 {
2179 if (view->pipe)
2180 end_update(view);
2182 if (opt_cmd[0]) {
2183 string_copy(view->cmd, opt_cmd);
2184 opt_cmd[0] = 0;
2185 /* When running random commands, initially show the
2186 * command in the title. However, it maybe later be
2187 * overwritten if a commit line is selected. */
2188 if (view == VIEW(REQ_VIEW_PAGER))
2189 string_copy(view->ref, view->cmd);
2190 else
2191 view->ref[0] = 0;
2193 } else if (view == VIEW(REQ_VIEW_TREE)) {
2194 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2195 char path[SIZEOF_STR];
2197 if (strcmp(view->vid, view->id))
2198 opt_path[0] = path[0] = 0;
2199 else if (sq_quote(path, 0, opt_path) >= sizeof(path))
2200 return FALSE;
2202 if (!string_format(view->cmd, format, view->id, path))
2203 return FALSE;
2205 } else {
2206 const char *format = view->cmd_env ? view->cmd_env : view->cmd_fmt;
2207 const char *id = view->id;
2209 if (!string_format(view->cmd, format, id, id, id, id, id))
2210 return FALSE;
2212 /* Put the current ref_* value to the view title ref
2213 * member. This is needed by the blob view. Most other
2214 * views sets it automatically after loading because the
2215 * first line is a commit line. */
2216 string_copy_rev(view->ref, view->id);
2217 }
2219 /* Special case for the pager view. */
2220 if (opt_pipe) {
2221 view->pipe = opt_pipe;
2222 opt_pipe = NULL;
2223 } else {
2224 view->pipe = popen(view->cmd, "r");
2225 }
2227 if (!view->pipe)
2228 return FALSE;
2230 set_nonblocking_input(TRUE);
2232 view->offset = 0;
2233 view->lines = 0;
2234 view->lineno = 0;
2235 string_copy_rev(view->vid, view->id);
2237 if (view->line) {
2238 int i;
2240 for (i = 0; i < view->lines; i++)
2241 if (view->line[i].data)
2242 free(view->line[i].data);
2244 free(view->line);
2245 view->line = NULL;
2246 }
2248 view->start_time = time(NULL);
2250 return TRUE;
2251 }
2253 #define ITEM_CHUNK_SIZE 256
2254 static void *
2255 realloc_items(void *mem, size_t *size, size_t new_size, size_t item_size)
2256 {
2257 size_t num_chunks = *size / ITEM_CHUNK_SIZE;
2258 size_t num_chunks_new = (new_size + ITEM_CHUNK_SIZE - 1) / ITEM_CHUNK_SIZE;
2260 if (mem == NULL || num_chunks != num_chunks_new) {
2261 *size = num_chunks_new * ITEM_CHUNK_SIZE;
2262 mem = realloc(mem, *size * item_size);
2263 }
2265 return mem;
2266 }
2268 static struct line *
2269 realloc_lines(struct view *view, size_t line_size)
2270 {
2271 size_t alloc = view->line_alloc;
2272 struct line *tmp = realloc_items(view->line, &alloc, line_size,
2273 sizeof(*view->line));
2275 if (!tmp)
2276 return NULL;
2278 view->line = tmp;
2279 view->line_alloc = alloc;
2280 view->line_size = line_size;
2281 return view->line;
2282 }
2284 static bool
2285 update_view(struct view *view)
2286 {
2287 char in_buffer[BUFSIZ];
2288 char out_buffer[BUFSIZ * 2];
2289 char *line;
2290 /* The number of lines to read. If too low it will cause too much
2291 * redrawing (and possible flickering), if too high responsiveness
2292 * will suffer. */
2293 unsigned long lines = view->height;
2294 int redraw_from = -1;
2296 if (!view->pipe)
2297 return TRUE;
2299 /* Only redraw if lines are visible. */
2300 if (view->offset + view->height >= view->lines)
2301 redraw_from = view->lines - view->offset;
2303 /* FIXME: This is probably not perfect for backgrounded views. */
2304 if (!realloc_lines(view, view->lines + lines))
2305 goto alloc_error;
2307 while ((line = fgets(in_buffer, sizeof(in_buffer), view->pipe))) {
2308 size_t linelen = strlen(line);
2310 if (linelen)
2311 line[linelen - 1] = 0;
2313 if (opt_iconv != ICONV_NONE) {
2314 ICONV_CONST char *inbuf = line;
2315 size_t inlen = linelen;
2317 char *outbuf = out_buffer;
2318 size_t outlen = sizeof(out_buffer);
2320 size_t ret;
2322 ret = iconv(opt_iconv, &inbuf, &inlen, &outbuf, &outlen);
2323 if (ret != (size_t) -1) {
2324 line = out_buffer;
2325 linelen = strlen(out_buffer);
2326 }
2327 }
2329 if (!view->ops->read(view, line))
2330 goto alloc_error;
2332 if (lines-- == 1)
2333 break;
2334 }
2336 {
2337 int digits;
2339 lines = view->lines;
2340 for (digits = 0; lines; digits++)
2341 lines /= 10;
2343 /* Keep the displayed view in sync with line number scaling. */
2344 if (digits != view->digits) {
2345 view->digits = digits;
2346 redraw_from = 0;
2347 }
2348 }
2350 if (!view_is_displayed(view))
2351 goto check_pipe;
2353 if (view == VIEW(REQ_VIEW_TREE)) {
2354 /* Clear the view and redraw everything since the tree sorting
2355 * might have rearranged things. */
2356 redraw_view(view);
2358 } else if (redraw_from >= 0) {
2359 /* If this is an incremental update, redraw the previous line
2360 * since for commits some members could have changed when
2361 * loading the main view. */
2362 if (redraw_from > 0)
2363 redraw_from--;
2365 /* Since revision graph visualization requires knowledge
2366 * about the parent commit, it causes a further one-off
2367 * needed to be redrawn for incremental updates. */
2368 if (redraw_from > 0 && opt_rev_graph)
2369 redraw_from--;
2371 /* Incrementally draw avoids flickering. */
2372 redraw_view_from(view, redraw_from);
2373 }
2375 if (view == VIEW(REQ_VIEW_BLAME))
2376 redraw_view_dirty(view);
2378 /* Update the title _after_ the redraw so that if the redraw picks up a
2379 * commit reference in view->ref it'll be available here. */
2380 update_view_title(view);
2382 check_pipe:
2383 if (ferror(view->pipe)) {
2384 report("Failed to read: %s", strerror(errno));
2385 goto end;
2387 } else if (feof(view->pipe)) {
2388 report("");
2389 goto end;
2390 }
2392 return TRUE;
2394 alloc_error:
2395 report("Allocation failure");
2397 end:
2398 if (view->ops->read(view, NULL))
2399 end_update(view);
2400 return FALSE;
2401 }
2403 static struct line *
2404 add_line_data(struct view *view, void *data, enum line_type type)
2405 {
2406 struct line *line = &view->line[view->lines++];
2408 memset(line, 0, sizeof(*line));
2409 line->type = type;
2410 line->data = data;
2412 return line;
2413 }
2415 static struct line *
2416 add_line_text(struct view *view, char *data, enum line_type type)
2417 {
2418 if (data)
2419 data = strdup(data);
2421 return data ? add_line_data(view, data, type) : NULL;
2422 }
2425 /*
2426 * View opening
2427 */
2429 enum open_flags {
2430 OPEN_DEFAULT = 0, /* Use default view switching. */
2431 OPEN_SPLIT = 1, /* Split current view. */
2432 OPEN_BACKGROUNDED = 2, /* Backgrounded. */
2433 OPEN_RELOAD = 4, /* Reload view even if it is the current. */
2434 OPEN_NOMAXIMIZE = 8 /* Do not maximize the current view. */
2435 };
2437 static void
2438 open_view(struct view *prev, enum request request, enum open_flags flags)
2439 {
2440 bool backgrounded = !!(flags & OPEN_BACKGROUNDED);
2441 bool split = !!(flags & OPEN_SPLIT);
2442 bool reload = !!(flags & OPEN_RELOAD);
2443 bool nomaximize = !!(flags & OPEN_NOMAXIMIZE);
2444 struct view *view = VIEW(request);
2445 int nviews = displayed_views();
2446 struct view *base_view = display[0];
2448 if (view == prev && nviews == 1 && !reload) {
2449 report("Already in %s view", view->name);
2450 return;
2451 }
2453 if (view->git_dir && !opt_git_dir[0]) {
2454 report("The %s view is disabled in pager view", view->name);
2455 return;
2456 }
2458 if (split) {
2459 display[1] = view;
2460 if (!backgrounded)
2461 current_view = 1;
2462 } else if (!nomaximize) {
2463 /* Maximize the current view. */
2464 memset(display, 0, sizeof(display));
2465 current_view = 0;
2466 display[current_view] = view;
2467 }
2469 /* Resize the view when switching between split- and full-screen,
2470 * or when switching between two different full-screen views. */
2471 if (nviews != displayed_views() ||
2472 (nviews == 1 && base_view != display[0]))
2473 resize_display();
2475 if (view->ops->open) {
2476 if (!view->ops->open(view)) {
2477 report("Failed to load %s view", view->name);
2478 return;
2479 }
2481 } else if ((reload || strcmp(view->vid, view->id)) &&
2482 !begin_update(view)) {
2483 report("Failed to load %s view", view->name);
2484 return;
2485 }
2487 if (split && prev->lineno - prev->offset >= prev->height) {
2488 /* Take the title line into account. */
2489 int lines = prev->lineno - prev->offset - prev->height + 1;
2491 /* Scroll the view that was split if the current line is
2492 * outside the new limited view. */
2493 do_scroll_view(prev, lines);
2494 }
2496 if (prev && view != prev) {
2497 if (split && !backgrounded) {
2498 /* "Blur" the previous view. */
2499 update_view_title(prev);
2500 }
2502 view->parent = prev;
2503 }
2505 if (view->pipe && view->lines == 0) {
2506 /* Clear the old view and let the incremental updating refill
2507 * the screen. */
2508 werase(view->win);
2509 report("");
2510 } else {
2511 redraw_view(view);
2512 report("");
2513 }
2515 /* If the view is backgrounded the above calls to report()
2516 * won't redraw the view title. */
2517 if (backgrounded)
2518 update_view_title(view);
2519 }
2521 static void
2522 open_external_viewer(const char *cmd)
2523 {
2524 def_prog_mode(); /* save current tty modes */
2525 endwin(); /* restore original tty modes */
2526 system(cmd);
2527 fprintf(stderr, "Press Enter to continue");
2528 getc(stdin);
2529 reset_prog_mode();
2530 redraw_display();
2531 }
2533 static void
2534 open_mergetool(const char *file)
2535 {
2536 char cmd[SIZEOF_STR];
2537 char file_sq[SIZEOF_STR];
2539 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2540 string_format(cmd, "git mergetool %s", file_sq)) {
2541 open_external_viewer(cmd);
2542 }
2543 }
2545 static void
2546 open_editor(bool from_root, const char *file)
2547 {
2548 char cmd[SIZEOF_STR];
2549 char file_sq[SIZEOF_STR];
2550 char *editor;
2551 char *prefix = from_root ? opt_cdup : "";
2553 editor = getenv("GIT_EDITOR");
2554 if (!editor && *opt_editor)
2555 editor = opt_editor;
2556 if (!editor)
2557 editor = getenv("VISUAL");
2558 if (!editor)
2559 editor = getenv("EDITOR");
2560 if (!editor)
2561 editor = "vi";
2563 if (sq_quote(file_sq, 0, file) < sizeof(file_sq) &&
2564 string_format(cmd, "%s %s%s", editor, prefix, file_sq)) {
2565 open_external_viewer(cmd);
2566 }
2567 }
2569 static void
2570 open_run_request(enum request request)
2571 {
2572 struct run_request *req = get_run_request(request);
2573 char buf[SIZEOF_STR * 2];
2574 size_t bufpos;
2575 char *cmd;
2577 if (!req) {
2578 report("Unknown run request");
2579 return;
2580 }
2582 bufpos = 0;
2583 cmd = req->cmd;
2585 while (cmd) {
2586 char *next = strstr(cmd, "%(");
2587 int len = next - cmd;
2588 char *value;
2590 if (!next) {
2591 len = strlen(cmd);
2592 value = "";
2594 } else if (!strncmp(next, "%(head)", 7)) {
2595 value = ref_head;
2597 } else if (!strncmp(next, "%(commit)", 9)) {
2598 value = ref_commit;
2600 } else if (!strncmp(next, "%(blob)", 7)) {
2601 value = ref_blob;
2603 } else {
2604 report("Unknown replacement in run request: `%s`", req->cmd);
2605 return;
2606 }
2608 if (!string_format_from(buf, &bufpos, "%.*s%s", len, cmd, value))
2609 return;
2611 if (next)
2612 next = strchr(next, ')') + 1;
2613 cmd = next;
2614 }
2616 open_external_viewer(buf);
2617 }
2619 /*
2620 * User request switch noodle
2621 */
2623 static int
2624 view_driver(struct view *view, enum request request)
2625 {
2626 int i;
2628 if (request == REQ_NONE) {
2629 doupdate();
2630 return TRUE;
2631 }
2633 if (request > REQ_NONE) {
2634 open_run_request(request);
2635 /* FIXME: When all views can refresh always do this. */
2636 if (view == VIEW(REQ_VIEW_STATUS) ||
2637 view == VIEW(REQ_VIEW_STAGE))
2638 request = REQ_REFRESH;
2639 else
2640 return TRUE;
2641 }
2643 if (view && view->lines) {
2644 request = view->ops->request(view, request, &view->line[view->lineno]);
2645 if (request == REQ_NONE)
2646 return TRUE;
2647 }
2649 switch (request) {
2650 case REQ_MOVE_UP:
2651 case REQ_MOVE_DOWN:
2652 case REQ_MOVE_PAGE_UP:
2653 case REQ_MOVE_PAGE_DOWN:
2654 case REQ_MOVE_FIRST_LINE:
2655 case REQ_MOVE_LAST_LINE:
2656 move_view(view, request);
2657 break;
2659 case REQ_SCROLL_LINE_DOWN:
2660 case REQ_SCROLL_LINE_UP:
2661 case REQ_SCROLL_PAGE_DOWN:
2662 case REQ_SCROLL_PAGE_UP:
2663 scroll_view(view, request);
2664 break;
2666 case REQ_VIEW_BLAME:
2667 if (!opt_file[0]) {
2668 report("No file chosen, press %s to open tree view",
2669 get_key(REQ_VIEW_TREE));
2670 break;
2671 }
2672 open_view(view, request, OPEN_DEFAULT);
2673 break;
2675 case REQ_VIEW_BLOB:
2676 if (!ref_blob[0]) {
2677 report("No file chosen, press %s to open tree view",
2678 get_key(REQ_VIEW_TREE));
2679 break;
2680 }
2681 open_view(view, request, OPEN_DEFAULT);
2682 break;
2684 case REQ_VIEW_PAGER:
2685 if (!opt_pipe && !VIEW(REQ_VIEW_PAGER)->lines) {
2686 report("No pager content, press %s to run command from prompt",
2687 get_key(REQ_PROMPT));
2688 break;
2689 }
2690 open_view(view, request, OPEN_DEFAULT);
2691 break;
2693 case REQ_VIEW_STAGE:
2694 if (!VIEW(REQ_VIEW_STAGE)->lines) {
2695 report("No stage content, press %s to open the status view and choose file",
2696 get_key(REQ_VIEW_STATUS));
2697 break;
2698 }
2699 open_view(view, request, OPEN_DEFAULT);
2700 break;
2702 case REQ_VIEW_STATUS:
2703 if (opt_is_inside_work_tree == FALSE) {
2704 report("The status view requires a working tree");
2705 break;
2706 }
2707 open_view(view, request, OPEN_DEFAULT);
2708 break;
2710 case REQ_VIEW_MAIN:
2711 case REQ_VIEW_DIFF:
2712 case REQ_VIEW_LOG:
2713 case REQ_VIEW_TREE:
2714 case REQ_VIEW_HELP:
2715 open_view(view, request, OPEN_DEFAULT);
2716 break;
2718 case REQ_NEXT:
2719 case REQ_PREVIOUS:
2720 request = request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
2722 if ((view == VIEW(REQ_VIEW_DIFF) &&
2723 view->parent == VIEW(REQ_VIEW_MAIN)) ||
2724 (view == VIEW(REQ_VIEW_DIFF) &&
2725 view->parent == VIEW(REQ_VIEW_BLAME)) ||
2726 (view == VIEW(REQ_VIEW_STAGE) &&
2727 view->parent == VIEW(REQ_VIEW_STATUS)) ||
2728 (view == VIEW(REQ_VIEW_BLOB) &&
2729 view->parent == VIEW(REQ_VIEW_TREE))) {
2730 int line;
2732 view = view->parent;
2733 line = view->lineno;
2734 move_view(view, request);
2735 if (view_is_displayed(view))
2736 update_view_title(view);
2737 if (line != view->lineno)
2738 view->ops->request(view, REQ_ENTER,
2739 &view->line[view->lineno]);
2741 } else {
2742 move_view(view, request);
2743 }
2744 break;
2746 case REQ_VIEW_NEXT:
2747 {
2748 int nviews = displayed_views();
2749 int next_view = (current_view + 1) % nviews;
2751 if (next_view == current_view) {
2752 report("Only one view is displayed");
2753 break;
2754 }
2756 current_view = next_view;
2757 /* Blur out the title of the previous view. */
2758 update_view_title(view);
2759 report("");
2760 break;
2761 }
2762 case REQ_REFRESH:
2763 report("Refreshing is not yet supported for the %s view", view->name);
2764 break;
2766 case REQ_MAXIMIZE:
2767 if (displayed_views() == 2)
2768 open_view(view, VIEW_REQ(view), OPEN_DEFAULT);
2769 break;
2771 case REQ_TOGGLE_LINENO:
2772 opt_line_number = !opt_line_number;
2773 redraw_display();
2774 break;
2776 case REQ_TOGGLE_DATE:
2777 opt_date = !opt_date;
2778 redraw_display();
2779 break;
2781 case REQ_TOGGLE_AUTHOR:
2782 opt_author = !opt_author;
2783 redraw_display();
2784 break;
2786 case REQ_TOGGLE_REV_GRAPH:
2787 opt_rev_graph = !opt_rev_graph;
2788 redraw_display();
2789 break;
2791 case REQ_TOGGLE_REFS:
2792 opt_show_refs = !opt_show_refs;
2793 redraw_display();
2794 break;
2796 case REQ_PROMPT:
2797 /* Always reload^Wrerun commands from the prompt. */
2798 open_view(view, opt_request, OPEN_RELOAD);
2799 break;
2801 case REQ_SEARCH:
2802 case REQ_SEARCH_BACK:
2803 search_view(view, request);
2804 break;
2806 case REQ_FIND_NEXT:
2807 case REQ_FIND_PREV:
2808 find_next(view, request);
2809 break;
2811 case REQ_STOP_LOADING:
2812 for (i = 0; i < ARRAY_SIZE(views); i++) {
2813 view = &views[i];
2814 if (view->pipe)
2815 report("Stopped loading the %s view", view->name),
2816 end_update(view);
2817 }
2818 break;
2820 case REQ_SHOW_VERSION:
2821 report("tig-%s (built %s)", TIG_VERSION, __DATE__);
2822 return TRUE;
2824 case REQ_SCREEN_RESIZE:
2825 resize_display();
2826 /* Fall-through */
2827 case REQ_SCREEN_REDRAW:
2828 redraw_display();
2829 break;
2831 case REQ_EDIT:
2832 report("Nothing to edit");
2833 break;
2836 case REQ_ENTER:
2837 report("Nothing to enter");
2838 break;
2841 case REQ_VIEW_CLOSE:
2842 /* XXX: Mark closed views by letting view->parent point to the
2843 * view itself. Parents to closed view should never be
2844 * followed. */
2845 if (view->parent &&
2846 view->parent->parent != view->parent) {
2847 memset(display, 0, sizeof(display));
2848 current_view = 0;
2849 display[current_view] = view->parent;
2850 view->parent = view;
2851 resize_display();
2852 redraw_display();
2853 break;
2854 }
2855 /* Fall-through */
2856 case REQ_QUIT:
2857 return FALSE;
2859 default:
2860 /* An unknown key will show most commonly used commands. */
2861 report("Unknown key, press 'h' for help");
2862 return TRUE;
2863 }
2865 return TRUE;
2866 }
2869 /*
2870 * Pager backend
2871 */
2873 static bool
2874 pager_draw(struct view *view, struct line *line, unsigned int lineno)
2875 {
2876 char *text = line->data;
2878 if (opt_line_number && draw_lineno(view, lineno))
2879 return TRUE;
2881 draw_text(view, line->type, text, TRUE);
2882 return TRUE;
2883 }
2885 static bool
2886 add_describe_ref(char *buf, size_t *bufpos, char *commit_id, const char *sep)
2887 {
2888 char refbuf[SIZEOF_STR];
2889 char *ref = NULL;
2890 FILE *pipe;
2892 if (!string_format(refbuf, "git describe %s 2>/dev/null", commit_id))
2893 return TRUE;
2895 pipe = popen(refbuf, "r");
2896 if (!pipe)
2897 return TRUE;
2899 if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
2900 ref = chomp_string(ref);
2901 pclose(pipe);
2903 if (!ref || !*ref)
2904 return TRUE;
2906 /* This is the only fatal call, since it can "corrupt" the buffer. */
2907 if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
2908 return FALSE;
2910 return TRUE;
2911 }
2913 static void
2914 add_pager_refs(struct view *view, struct line *line)
2915 {
2916 char buf[SIZEOF_STR];
2917 char *commit_id = (char *)line->data + STRING_SIZE("commit ");
2918 struct ref **refs;
2919 size_t bufpos = 0, refpos = 0;
2920 const char *sep = "Refs: ";
2921 bool is_tag = FALSE;
2923 assert(line->type == LINE_COMMIT);
2925 refs = get_refs(commit_id);
2926 if (!refs) {
2927 if (view == VIEW(REQ_VIEW_DIFF))
2928 goto try_add_describe_ref;
2929 return;
2930 }
2932 do {
2933 struct ref *ref = refs[refpos];
2934 char *fmt = ref->tag ? "%s[%s]" :
2935 ref->remote ? "%s<%s>" : "%s%s";
2937 if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
2938 return;
2939 sep = ", ";
2940 if (ref->tag)
2941 is_tag = TRUE;
2942 } while (refs[refpos++]->next);
2944 if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
2945 try_add_describe_ref:
2946 /* Add <tag>-g<commit_id> "fake" reference. */
2947 if (!add_describe_ref(buf, &bufpos, commit_id, sep))
2948 return;
2949 }
2951 if (bufpos == 0)
2952 return;
2954 if (!realloc_lines(view, view->line_size + 1))
2955 return;
2957 add_line_text(view, buf, LINE_PP_REFS);
2958 }
2960 static bool
2961 pager_read(struct view *view, char *data)
2962 {
2963 struct line *line;
2965 if (!data)
2966 return TRUE;
2968 line = add_line_text(view, data, get_line_type(data));
2969 if (!line)
2970 return FALSE;
2972 if (line->type == LINE_COMMIT &&
2973 (view == VIEW(REQ_VIEW_DIFF) ||
2974 view == VIEW(REQ_VIEW_LOG)))
2975 add_pager_refs(view, line);
2977 return TRUE;
2978 }
2980 static enum request
2981 pager_request(struct view *view, enum request request, struct line *line)
2982 {
2983 int split = 0;
2985 if (request != REQ_ENTER)
2986 return request;
2988 if (line->type == LINE_COMMIT &&
2989 (view == VIEW(REQ_VIEW_LOG) ||
2990 view == VIEW(REQ_VIEW_PAGER))) {
2991 open_view(view, REQ_VIEW_DIFF, OPEN_SPLIT);
2992 split = 1;
2993 }
2995 /* Always scroll the view even if it was split. That way
2996 * you can use Enter to scroll through the log view and
2997 * split open each commit diff. */
2998 scroll_view(view, REQ_SCROLL_LINE_DOWN);
3000 /* FIXME: A minor workaround. Scrolling the view will call report("")
3001 * but if we are scrolling a non-current view this won't properly
3002 * update the view title. */
3003 if (split)
3004 update_view_title(view);
3006 return REQ_NONE;
3007 }
3009 static bool
3010 pager_grep(struct view *view, struct line *line)
3011 {
3012 regmatch_t pmatch;
3013 char *text = line->data;
3015 if (!*text)
3016 return FALSE;
3018 if (regexec(view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
3019 return FALSE;
3021 return TRUE;
3022 }
3024 static void
3025 pager_select(struct view *view, struct line *line)
3026 {
3027 if (line->type == LINE_COMMIT) {
3028 char *text = (char *)line->data + STRING_SIZE("commit ");
3030 if (view != VIEW(REQ_VIEW_PAGER))
3031 string_copy_rev(view->ref, text);
3032 string_copy_rev(ref_commit, text);
3033 }
3034 }
3036 static struct view_ops pager_ops = {
3037 "line",
3038 NULL,
3039 pager_read,
3040 pager_draw,
3041 pager_request,
3042 pager_grep,
3043 pager_select,
3044 };
3047 /*
3048 * Help backend
3049 */
3051 static bool
3052 help_open(struct view *view)
3053 {
3054 char buf[BUFSIZ];
3055 int lines = ARRAY_SIZE(req_info) + 2;
3056 int i;
3058 if (view->lines > 0)
3059 return TRUE;
3061 for (i = 0; i < ARRAY_SIZE(req_info); i++)
3062 if (!req_info[i].request)
3063 lines++;
3065 lines += run_requests + 1;
3067 view->line = calloc(lines, sizeof(*view->line));
3068 if (!view->line)
3069 return FALSE;
3071 add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
3073 for (i = 0; i < ARRAY_SIZE(req_info); i++) {
3074 char *key;
3076 if (req_info[i].request == REQ_NONE)
3077 continue;
3079 if (!req_info[i].request) {
3080 add_line_text(view, "", LINE_DEFAULT);
3081 add_line_text(view, req_info[i].help, LINE_DEFAULT);
3082 continue;
3083 }
3085 key = get_key(req_info[i].request);
3086 if (!*key)
3087 key = "(no key defined)";
3089 if (!string_format(buf, " %-25s %s", key, req_info[i].help))
3090 continue;
3092 add_line_text(view, buf, LINE_DEFAULT);
3093 }
3095 if (run_requests) {
3096 add_line_text(view, "", LINE_DEFAULT);
3097 add_line_text(view, "External commands:", LINE_DEFAULT);
3098 }
3100 for (i = 0; i < run_requests; i++) {
3101 struct run_request *req = get_run_request(REQ_NONE + i + 1);
3102 char *key;
3104 if (!req)
3105 continue;
3107 key = get_key_name(req->key);
3108 if (!*key)
3109 key = "(no key defined)";
3111 if (!string_format(buf, " %-10s %-14s `%s`",
3112 keymap_table[req->keymap].name,
3113 key, req->cmd))
3114 continue;
3116 add_line_text(view, buf, LINE_DEFAULT);
3117 }
3119 return TRUE;
3120 }
3122 static struct view_ops help_ops = {
3123 "line",
3124 help_open,
3125 NULL,
3126 pager_draw,
3127 pager_request,
3128 pager_grep,
3129 pager_select,
3130 };
3133 /*
3134 * Tree backend
3135 */
3137 struct tree_stack_entry {
3138 struct tree_stack_entry *prev; /* Entry below this in the stack */
3139 unsigned long lineno; /* Line number to restore */
3140 char *name; /* Position of name in opt_path */
3141 };
3143 /* The top of the path stack. */
3144 static struct tree_stack_entry *tree_stack = NULL;
3145 unsigned long tree_lineno = 0;
3147 static void
3148 pop_tree_stack_entry(void)
3149 {
3150 struct tree_stack_entry *entry = tree_stack;
3152 tree_lineno = entry->lineno;
3153 entry->name[0] = 0;
3154 tree_stack = entry->prev;
3155 free(entry);
3156 }
3158 static void
3159 push_tree_stack_entry(char *name, unsigned long lineno)
3160 {
3161 struct tree_stack_entry *entry = calloc(1, sizeof(*entry));
3162 size_t pathlen = strlen(opt_path);
3164 if (!entry)
3165 return;
3167 entry->prev = tree_stack;
3168 entry->name = opt_path + pathlen;
3169 tree_stack = entry;
3171 if (!string_format_from(opt_path, &pathlen, "%s/", name)) {
3172 pop_tree_stack_entry();
3173 return;
3174 }
3176 /* Move the current line to the first tree entry. */
3177 tree_lineno = 1;
3178 entry->lineno = lineno;
3179 }
3181 /* Parse output from git-ls-tree(1):
3182 *
3183 * 100644 blob fb0e31ea6cc679b7379631188190e975f5789c26 Makefile
3184 * 100644 blob 5304ca4260aaddaee6498f9630e7d471b8591ea6 README
3185 * 100644 blob f931e1d229c3e185caad4449bf5b66ed72462657 tig.c
3186 * 100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38 web.conf
3187 */
3189 #define SIZEOF_TREE_ATTR \
3190 STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
3192 #define TREE_UP_FORMAT "040000 tree %s\t.."
3194 static int
3195 tree_compare_entry(enum line_type type1, char *name1,
3196 enum line_type type2, char *name2)
3197 {
3198 if (type1 != type2) {
3199 if (type1 == LINE_TREE_DIR)
3200 return -1;
3201 return 1;
3202 }
3204 return strcmp(name1, name2);
3205 }
3207 static char *
3208 tree_path(struct line *line)
3209 {
3210 char *path = line->data;
3212 return path + SIZEOF_TREE_ATTR;
3213 }
3215 static bool
3216 tree_read(struct view *view, char *text)
3217 {
3218 size_t textlen = text ? strlen(text) : 0;
3219 char buf[SIZEOF_STR];
3220 unsigned long pos;
3221 enum line_type type;
3222 bool first_read = view->lines == 0;
3224 if (!text)
3225 return TRUE;
3226 if (textlen <= SIZEOF_TREE_ATTR)
3227 return FALSE;
3229 type = text[STRING_SIZE("100644 ")] == 't'
3230 ? LINE_TREE_DIR : LINE_TREE_FILE;
3232 if (first_read) {
3233 /* Add path info line */
3234 if (!string_format(buf, "Directory path /%s", opt_path) ||
3235 !realloc_lines(view, view->line_size + 1) ||
3236 !add_line_text(view, buf, LINE_DEFAULT))
3237 return FALSE;
3239 /* Insert "link" to parent directory. */
3240 if (*opt_path) {
3241 if (!string_format(buf, TREE_UP_FORMAT, view->ref) ||
3242 !realloc_lines(view, view->line_size + 1) ||
3243 !add_line_text(view, buf, LINE_TREE_DIR))
3244 return FALSE;
3245 }
3246 }
3248 /* Strip the path part ... */
3249 if (*opt_path) {
3250 size_t pathlen = textlen - SIZEOF_TREE_ATTR;
3251 size_t striplen = strlen(opt_path);
3252 char *path = text + SIZEOF_TREE_ATTR;
3254 if (pathlen > striplen)
3255 memmove(path, path + striplen,
3256 pathlen - striplen + 1);
3257 }
3259 /* Skip "Directory ..." and ".." line. */
3260 for (pos = 1 + !!*opt_path; pos < view->lines; pos++) {
3261 struct line *line = &view->line[pos];
3262 char *path1 = tree_path(line);
3263 char *path2 = text + SIZEOF_TREE_ATTR;
3264 int cmp = tree_compare_entry(line->type, path1, type, path2);
3266 if (cmp <= 0)
3267 continue;
3269 text = strdup(text);
3270 if (!text)
3271 return FALSE;
3273 if (view->lines > pos)
3274 memmove(&view->line[pos + 1], &view->line[pos],
3275 (view->lines - pos) * sizeof(*line));
3277 line = &view->line[pos];
3278 line->data = text;
3279 line->type = type;
3280 view->lines++;
3281 return TRUE;
3282 }
3284 if (!add_line_text(view, text, type))
3285 return FALSE;
3287 if (tree_lineno > view->lineno) {
3288 view->lineno = tree_lineno;
3289 tree_lineno = 0;
3290 }
3292 return TRUE;
3293 }
3295 static enum request
3296 tree_request(struct view *view, enum request request, struct line *line)
3297 {
3298 enum open_flags flags;
3300 if (request == REQ_VIEW_BLAME) {
3301 char *filename = tree_path(line);
3303 if (line->type == LINE_TREE_DIR) {
3304 report("Cannot show blame for directory %s", opt_path);
3305 return REQ_NONE;
3306 }
3308 string_copy(opt_ref, view->vid);
3309 string_format(opt_file, "%s%s", opt_path, filename);
3310 return request;
3311 }
3312 if (request == REQ_TREE_PARENT) {
3313 if (*opt_path) {
3314 /* fake 'cd ..' */
3315 request = REQ_ENTER;
3316 line = &view->line[1];
3317 } else {
3318 /* quit view if at top of tree */
3319 return REQ_VIEW_CLOSE;
3320 }
3321 }
3322 if (request != REQ_ENTER)
3323 return request;
3325 /* Cleanup the stack if the tree view is at a different tree. */
3326 while (!*opt_path && tree_stack)
3327 pop_tree_stack_entry();
3329 switch (line->type) {
3330 case LINE_TREE_DIR:
3331 /* Depending on whether it is a subdir or parent (updir?) link
3332 * mangle the path buffer. */
3333 if (line == &view->line[1] && *opt_path) {
3334 pop_tree_stack_entry();
3336 } else {
3337 char *basename = tree_path(line);
3339 push_tree_stack_entry(basename, view->lineno);
3340 }
3342 /* Trees and subtrees share the same ID, so they are not not
3343 * unique like blobs. */
3344 flags = OPEN_RELOAD;
3345 request = REQ_VIEW_TREE;
3346 break;
3348 case LINE_TREE_FILE:
3349 flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3350 request = REQ_VIEW_BLOB;
3351 break;
3353 default:
3354 return TRUE;
3355 }
3357 open_view(view, request, flags);
3358 if (request == REQ_VIEW_TREE) {
3359 view->lineno = tree_lineno;
3360 }
3362 return REQ_NONE;
3363 }
3365 static void
3366 tree_select(struct view *view, struct line *line)
3367 {
3368 char *text = (char *)line->data + STRING_SIZE("100644 blob ");
3370 if (line->type == LINE_TREE_FILE) {
3371 string_copy_rev(ref_blob, text);
3373 } else if (line->type != LINE_TREE_DIR) {
3374 return;
3375 }
3377 string_copy_rev(view->ref, text);
3378 }
3380 static struct view_ops tree_ops = {
3381 "file",
3382 NULL,
3383 tree_read,
3384 pager_draw,
3385 tree_request,
3386 pager_grep,
3387 tree_select,
3388 };
3390 static bool
3391 blob_read(struct view *view, char *line)
3392 {
3393 if (!line)
3394 return TRUE;
3395 return add_line_text(view, line, LINE_DEFAULT) != NULL;
3396 }
3398 static struct view_ops blob_ops = {
3399 "line",
3400 NULL,
3401 blob_read,
3402 pager_draw,
3403 pager_request,
3404 pager_grep,
3405 pager_select,
3406 };
3408 /*
3409 * Blame backend
3410 *
3411 * Loading the blame view is a two phase job:
3412 *
3413 * 1. File content is read either using opt_file from the
3414 * filesystem or using git-cat-file.
3415 * 2. Then blame information is incrementally added by
3416 * reading output from git-blame.
3417 */
3419 struct blame_commit {
3420 char id[SIZEOF_REV]; /* SHA1 ID. */
3421 char title[128]; /* First line of the commit message. */
3422 char author[75]; /* Author of the commit. */
3423 struct tm time; /* Date from the author ident. */
3424 char filename[128]; /* Name of file. */
3425 };
3427 struct blame {
3428 struct blame_commit *commit;
3429 unsigned int header:1;
3430 char text[1];
3431 };
3433 #define BLAME_CAT_FILE_CMD "git cat-file blob %s:%s"
3434 #define BLAME_INCREMENTAL_CMD "git blame --incremental %s %s"
3436 static bool
3437 blame_open(struct view *view)
3438 {
3439 char path[SIZEOF_STR];
3440 char ref[SIZEOF_STR] = "";
3442 if (sq_quote(path, 0, opt_file) >= sizeof(path))
3443 return FALSE;
3445 if (*opt_ref && sq_quote(ref, 0, opt_ref) >= sizeof(ref))
3446 return FALSE;
3448 if (*opt_ref) {
3449 if (!string_format(view->cmd, BLAME_CAT_FILE_CMD, ref, path))
3450 return FALSE;
3451 } else {
3452 view->pipe = fopen(opt_file, "r");
3453 if (!view->pipe &&
3454 !string_format(view->cmd, BLAME_CAT_FILE_CMD, "HEAD", path))
3455 return FALSE;
3456 }
3458 if (!view->pipe)
3459 view->pipe = popen(view->cmd, "r");
3460 if (!view->pipe)
3461 return FALSE;
3463 if (!string_format(view->cmd, BLAME_INCREMENTAL_CMD, ref, path))
3464 return FALSE;
3466 string_format(view->ref, "%s ...", opt_file);
3467 string_copy_rev(view->vid, opt_file);
3468 set_nonblocking_input(TRUE);
3470 if (view->line) {
3471 int i;
3473 for (i = 0; i < view->lines; i++)
3474 free(view->line[i].data);
3475 free(view->line);
3476 }
3478 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3479 view->offset = view->lines = view->lineno = 0;
3480 view->line = NULL;
3481 view->start_time = time(NULL);
3483 return TRUE;
3484 }
3486 static struct blame_commit *
3487 get_blame_commit(struct view *view, const char *id)
3488 {
3489 size_t i;
3491 for (i = 0; i < view->lines; i++) {
3492 struct blame *blame = view->line[i].data;
3494 if (!blame->commit)
3495 continue;
3497 if (!strncmp(blame->commit->id, id, SIZEOF_REV - 1))
3498 return blame->commit;
3499 }
3501 {
3502 struct blame_commit *commit = calloc(1, sizeof(*commit));
3504 if (commit)
3505 string_ncopy(commit->id, id, SIZEOF_REV);
3506 return commit;
3507 }
3508 }
3510 static bool
3511 parse_number(char **posref, size_t *number, size_t min, size_t max)
3512 {
3513 char *pos = *posref;
3515 *posref = NULL;
3516 pos = strchr(pos + 1, ' ');
3517 if (!pos || !isdigit(pos[1]))
3518 return FALSE;
3519 *number = atoi(pos + 1);
3520 if (*number < min || *number > max)
3521 return FALSE;
3523 *posref = pos;
3524 return TRUE;
3525 }
3527 static struct blame_commit *
3528 parse_blame_commit(struct view *view, char *text, int *blamed)
3529 {
3530 struct blame_commit *commit;
3531 struct blame *blame;
3532 char *pos = text + SIZEOF_REV - 1;
3533 size_t lineno;
3534 size_t group;
3536 if (strlen(text) <= SIZEOF_REV || *pos != ' ')
3537 return NULL;
3539 if (!parse_number(&pos, &lineno, 1, view->lines) ||
3540 !parse_number(&pos, &group, 1, view->lines - lineno + 1))
3541 return NULL;
3543 commit = get_blame_commit(view, text);
3544 if (!commit)
3545 return NULL;
3547 *blamed += group;
3548 while (group--) {
3549 struct line *line = &view->line[lineno + group - 1];
3551 blame = line->data;
3552 blame->commit = commit;
3553 blame->header = !group;
3554 line->dirty = 1;
3555 }
3557 return commit;
3558 }
3560 static bool
3561 blame_read_file(struct view *view, char *line)
3562 {
3563 if (!line) {
3564 FILE *pipe = NULL;
3566 if (view->lines > 0)
3567 pipe = popen(view->cmd, "r");
3568 else if (!view->parent)
3569 die("No blame exist for %s", view->vid);
3570 view->cmd[0] = 0;
3571 if (!pipe) {
3572 report("Failed to load blame data");
3573 return TRUE;
3574 }
3576 fclose(view->pipe);
3577 view->pipe = pipe;
3578 return FALSE;
3580 } else {
3581 size_t linelen = strlen(line);
3582 struct blame *blame = malloc(sizeof(*blame) + linelen);
3584 if (!line)
3585 return FALSE;
3587 blame->commit = NULL;
3588 strncpy(blame->text, line, linelen);
3589 blame->text[linelen] = 0;
3590 return add_line_data(view, blame, LINE_BLAME_ID) != NULL;
3591 }
3592 }
3594 static bool
3595 match_blame_header(const char *name, char **line)
3596 {
3597 size_t namelen = strlen(name);
3598 bool matched = !strncmp(name, *line, namelen);
3600 if (matched)
3601 *line += namelen;
3603 return matched;
3604 }
3606 static bool
3607 blame_read(struct view *view, char *line)
3608 {
3609 static struct blame_commit *commit = NULL;
3610 static int blamed = 0;
3611 static time_t author_time;
3613 if (*view->cmd)
3614 return blame_read_file(view, line);
3616 if (!line) {
3617 /* Reset all! */
3618 commit = NULL;
3619 blamed = 0;
3620 string_format(view->ref, "%s", view->vid);
3621 if (view_is_displayed(view)) {
3622 update_view_title(view);
3623 redraw_view_from(view, 0);
3624 }
3625 return TRUE;
3626 }
3628 if (!commit) {
3629 commit = parse_blame_commit(view, line, &blamed);
3630 string_format(view->ref, "%s %2d%%", view->vid,
3631 blamed * 100 / view->lines);
3633 } else if (match_blame_header("author ", &line)) {
3634 string_ncopy(commit->author, line, strlen(line));
3636 } else if (match_blame_header("author-time ", &line)) {
3637 author_time = (time_t) atol(line);
3639 } else if (match_blame_header("author-tz ", &line)) {
3640 long tz;
3642 tz = ('0' - line[1]) * 60 * 60 * 10;
3643 tz += ('0' - line[2]) * 60 * 60;
3644 tz += ('0' - line[3]) * 60;
3645 tz += ('0' - line[4]) * 60;
3647 if (line[0] == '-')
3648 tz = -tz;
3650 author_time -= tz;
3651 gmtime_r(&author_time, &commit->time);
3653 } else if (match_blame_header("summary ", &line)) {
3654 string_ncopy(commit->title, line, strlen(line));
3656 } else if (match_blame_header("filename ", &line)) {
3657 string_ncopy(commit->filename, line, strlen(line));
3658 commit = NULL;
3659 }
3661 return TRUE;
3662 }
3664 static bool
3665 blame_draw(struct view *view, struct line *line, unsigned int lineno)
3666 {
3667 struct blame *blame = line->data;
3668 struct tm *time = NULL;
3669 char *id = NULL, *author = NULL;
3671 if (blame->commit && *blame->commit->filename) {
3672 id = blame->commit->id;
3673 author = blame->commit->author;
3674 time = &blame->commit->time;
3675 }
3677 if (opt_date && draw_date(view, time))
3678 return TRUE;
3680 if (opt_author &&
3681 draw_field(view, LINE_MAIN_AUTHOR, author, AUTHOR_COLS, TRUE))
3682 return TRUE;
3684 if (draw_field(view, LINE_BLAME_ID, id, ID_COLS, FALSE))
3685 return TRUE;
3687 if (draw_lineno(view, lineno))
3688 return TRUE;
3690 draw_text(view, LINE_DEFAULT, blame->text, TRUE);
3691 return TRUE;
3692 }
3694 static enum request
3695 blame_request(struct view *view, enum request request, struct line *line)
3696 {
3697 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
3698 struct blame *blame = line->data;
3700 switch (request) {
3701 case REQ_ENTER:
3702 if (!blame->commit) {
3703 report("No commit loaded yet");
3704 break;
3705 }
3707 if (!strcmp(blame->commit->id, NULL_ID)) {
3708 char path[SIZEOF_STR];
3710 if (sq_quote(path, 0, view->vid) >= sizeof(path))
3711 break;
3712 string_format(opt_cmd, "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s 2>/dev/null", path);
3713 }
3715 open_view(view, REQ_VIEW_DIFF, flags);
3716 break;
3718 default:
3719 return request;
3720 }
3722 return REQ_NONE;
3723 }
3725 static bool
3726 blame_grep(struct view *view, struct line *line)
3727 {
3728 struct blame *blame = line->data;
3729 struct blame_commit *commit = blame->commit;
3730 regmatch_t pmatch;
3732 #define MATCH(text, on) \
3733 (on && *text && regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
3735 if (commit) {
3736 char buf[DATE_COLS + 1];
3738 if (MATCH(commit->title, 1) ||
3739 MATCH(commit->author, opt_author) ||
3740 MATCH(commit->id, opt_date))
3741 return TRUE;
3743 if (strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time) &&
3744 MATCH(buf, 1))
3745 return TRUE;
3746 }
3748 return MATCH(blame->text, 1);
3750 #undef MATCH
3751 }
3753 static void
3754 blame_select(struct view *view, struct line *line)
3755 {
3756 struct blame *blame = line->data;
3757 struct blame_commit *commit = blame->commit;
3759 if (!commit)
3760 return;
3762 if (!strcmp(commit->id, NULL_ID))
3763 string_ncopy(ref_commit, "HEAD", 4);
3764 else
3765 string_copy_rev(ref_commit, commit->id);
3766 }
3768 static struct view_ops blame_ops = {
3769 "line",
3770 blame_open,
3771 blame_read,
3772 blame_draw,
3773 blame_request,
3774 blame_grep,
3775 blame_select,
3776 };
3778 /*
3779 * Status backend
3780 */
3782 struct status {
3783 char status;
3784 struct {
3785 mode_t mode;
3786 char rev[SIZEOF_REV];
3787 char name[SIZEOF_STR];
3788 } old;
3789 struct {
3790 mode_t mode;
3791 char rev[SIZEOF_REV];
3792 char name[SIZEOF_STR];
3793 } new;
3794 };
3796 static char status_onbranch[SIZEOF_STR];
3797 static struct status stage_status;
3798 static enum line_type stage_line_type;
3800 /* Get fields from the diff line:
3801 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
3802 */
3803 static inline bool
3804 status_get_diff(struct status *file, char *buf, size_t bufsize)
3805 {
3806 char *old_mode = buf + 1;
3807 char *new_mode = buf + 8;
3808 char *old_rev = buf + 15;
3809 char *new_rev = buf + 56;
3810 char *status = buf + 97;
3812 if (bufsize < 99 ||
3813 old_mode[-1] != ':' ||
3814 new_mode[-1] != ' ' ||
3815 old_rev[-1] != ' ' ||
3816 new_rev[-1] != ' ' ||
3817 status[-1] != ' ')
3818 return FALSE;
3820 file->status = *status;
3822 string_copy_rev(file->old.rev, old_rev);
3823 string_copy_rev(file->new.rev, new_rev);
3825 file->old.mode = strtoul(old_mode, NULL, 8);
3826 file->new.mode = strtoul(new_mode, NULL, 8);
3828 file->old.name[0] = file->new.name[0] = 0;
3830 return TRUE;
3831 }
3833 static bool
3834 status_run(struct view *view, const char cmd[], char status, enum line_type type)
3835 {
3836 struct status *file = NULL;
3837 struct status *unmerged = NULL;
3838 char buf[SIZEOF_STR * 4];
3839 size_t bufsize = 0;
3840 FILE *pipe;
3842 pipe = popen(cmd, "r");
3843 if (!pipe)
3844 return FALSE;
3846 add_line_data(view, NULL, type);
3848 while (!feof(pipe) && !ferror(pipe)) {
3849 char *sep;
3850 size_t readsize;
3852 readsize = fread(buf + bufsize, 1, sizeof(buf) - bufsize, pipe);
3853 if (!readsize)
3854 break;
3855 bufsize += readsize;
3857 /* Process while we have NUL chars. */
3858 while ((sep = memchr(buf, 0, bufsize))) {
3859 size_t sepsize = sep - buf + 1;
3861 if (!file) {
3862 if (!realloc_lines(view, view->line_size + 1))
3863 goto error_out;
3865 file = calloc(1, sizeof(*file));
3866 if (!file)
3867 goto error_out;
3869 add_line_data(view, file, type);
3870 }
3872 /* Parse diff info part. */
3873 if (status) {
3874 file->status = status;
3875 if (status == 'A')
3876 string_copy(file->old.rev, NULL_ID);
3878 } else if (!file->status) {
3879 if (!status_get_diff(file, buf, sepsize))
3880 goto error_out;
3882 bufsize -= sepsize;
3883 memmove(buf, sep + 1, bufsize);
3885 sep = memchr(buf, 0, bufsize);
3886 if (!sep)
3887 break;
3888 sepsize = sep - buf + 1;
3890 /* Collapse all 'M'odified entries that
3891 * follow a associated 'U'nmerged entry.
3892 */
3893 if (file->status == 'U') {
3894 unmerged = file;
3896 } else if (unmerged) {
3897 int collapse = !strcmp(buf, unmerged->new.name);
3899 unmerged = NULL;
3900 if (collapse) {
3901 free(file);
3902 view->lines--;
3903 continue;
3904 }
3905 }
3906 }
3908 /* Grab the old name for rename/copy. */
3909 if (!*file->old.name &&
3910 (file->status == 'R' || file->status == 'C')) {
3911 sepsize = sep - buf + 1;
3912 string_ncopy(file->old.name, buf, sepsize);
3913 bufsize -= sepsize;
3914 memmove(buf, sep + 1, bufsize);
3916 sep = memchr(buf, 0, bufsize);
3917 if (!sep)
3918 break;
3919 sepsize = sep - buf + 1;
3920 }
3922 /* git-ls-files just delivers a NUL separated
3923 * list of file names similar to the second half
3924 * of the git-diff-* output. */
3925 string_ncopy(file->new.name, buf, sepsize);
3926 if (!*file->old.name)
3927 string_copy(file->old.name, file->new.name);
3928 bufsize -= sepsize;
3929 memmove(buf, sep + 1, bufsize);
3930 file = NULL;
3931 }
3932 }
3934 if (ferror(pipe)) {
3935 error_out:
3936 pclose(pipe);
3937 return FALSE;
3938 }
3940 if (!view->line[view->lines - 1].data)
3941 add_line_data(view, NULL, LINE_STAT_NONE);
3943 pclose(pipe);
3944 return TRUE;
3945 }
3947 /* Don't show unmerged entries in the staged section. */
3948 #define STATUS_DIFF_INDEX_CMD "git diff-index -z --diff-filter=ACDMRTXB --cached -M HEAD"
3949 #define STATUS_DIFF_FILES_CMD "git diff-files -z"
3950 #define STATUS_LIST_OTHER_CMD \
3951 "git ls-files -z --others --exclude-per-directory=.gitignore"
3952 #define STATUS_LIST_NO_HEAD_CMD \
3953 "git ls-files -z --cached --exclude-per-directory=.gitignore"
3955 #define STATUS_DIFF_INDEX_SHOW_CMD \
3956 "git diff-index --root --patch-with-stat -C -M --cached HEAD -- %s %s 2>/dev/null"
3958 #define STATUS_DIFF_FILES_SHOW_CMD \
3959 "git diff-files --root --patch-with-stat -C -M -- %s %s 2>/dev/null"
3961 #define STATUS_DIFF_NO_HEAD_SHOW_CMD \
3962 "git diff --no-color --patch-with-stat /dev/null %s 2>/dev/null"
3964 /* First parse staged info using git-diff-index(1), then parse unstaged
3965 * info using git-diff-files(1), and finally untracked files using
3966 * git-ls-files(1). */
3967 static bool
3968 status_open(struct view *view)
3969 {
3970 struct stat statbuf;
3971 char exclude[SIZEOF_STR];
3972 char indexcmd[SIZEOF_STR] = STATUS_DIFF_INDEX_CMD;
3973 char othercmd[SIZEOF_STR] = STATUS_LIST_OTHER_CMD;
3974 unsigned long prev_lineno = view->lineno;
3975 char indexstatus = 0;
3976 size_t i;
3978 for (i = 0; i < view->lines; i++)
3979 free(view->line[i].data);
3980 free(view->line);
3981 view->lines = view->line_alloc = view->line_size = view->lineno = 0;
3982 view->line = NULL;
3984 if (!realloc_lines(view, view->line_size + 7))
3985 return FALSE;
3987 add_line_data(view, NULL, LINE_STAT_HEAD);
3988 if (opt_no_head)
3989 string_copy(status_onbranch, "Initial commit");
3990 else if (!*opt_head)
3991 string_copy(status_onbranch, "Not currently on any branch");
3992 else if (!string_format(status_onbranch, "On branch %s", opt_head))
3993 return FALSE;
3995 if (opt_no_head) {
3996 string_copy(indexcmd, STATUS_LIST_NO_HEAD_CMD);
3997 indexstatus = 'A';
3998 }
4000 if (!string_format(exclude, "%s/info/exclude", opt_git_dir))
4001 return FALSE;
4003 if (stat(exclude, &statbuf) >= 0) {
4004 size_t cmdsize = strlen(othercmd);
4006 if (!string_format_from(othercmd, &cmdsize, " %s", "--exclude-from=") ||
4007 sq_quote(othercmd, cmdsize, exclude) >= sizeof(othercmd))
4008 return FALSE;
4010 cmdsize = strlen(indexcmd);
4011 if (opt_no_head &&
4012 (!string_format_from(indexcmd, &cmdsize, " %s", "--exclude-from=") ||
4013 sq_quote(indexcmd, cmdsize, exclude) >= sizeof(indexcmd)))
4014 return FALSE;
4015 }
4017 system("git update-index -q --refresh >/dev/null 2>/dev/null");
4019 if (!status_run(view, indexcmd, indexstatus, LINE_STAT_STAGED) ||
4020 !status_run(view, STATUS_DIFF_FILES_CMD, 0, LINE_STAT_UNSTAGED) ||
4021 !status_run(view, othercmd, '?', LINE_STAT_UNTRACKED))
4022 return FALSE;
4024 /* If all went well restore the previous line number to stay in
4025 * the context or select a line with something that can be
4026 * updated. */
4027 if (prev_lineno >= view->lines)
4028 prev_lineno = view->lines - 1;
4029 while (prev_lineno < view->lines && !view->line[prev_lineno].data)
4030 prev_lineno++;
4031 while (prev_lineno > 0 && !view->line[prev_lineno].data)
4032 prev_lineno--;
4034 /* If the above fails, always skip the "On branch" line. */
4035 if (prev_lineno < view->lines)
4036 view->lineno = prev_lineno;
4037 else
4038 view->lineno = 1;
4040 if (view->lineno < view->offset)
4041 view->offset = view->lineno;
4042 else if (view->offset + view->height <= view->lineno)
4043 view->offset = view->lineno - view->height + 1;
4045 return TRUE;
4046 }
4048 static bool
4049 status_draw(struct view *view, struct line *line, unsigned int lineno)
4050 {
4051 struct status *status = line->data;
4052 enum line_type type;
4053 char *text;
4055 if (!status) {
4056 switch (line->type) {
4057 case LINE_STAT_STAGED:
4058 type = LINE_STAT_SECTION;
4059 text = "Changes to be committed:";
4060 break;
4062 case LINE_STAT_UNSTAGED:
4063 type = LINE_STAT_SECTION;
4064 text = "Changed but not updated:";
4065 break;
4067 case LINE_STAT_UNTRACKED:
4068 type = LINE_STAT_SECTION;
4069 text = "Untracked files:";
4070 break;
4072 case LINE_STAT_NONE:
4073 type = LINE_DEFAULT;
4074 text = " (no files)";
4075 break;
4077 case LINE_STAT_HEAD:
4078 type = LINE_STAT_HEAD;
4079 text = status_onbranch;
4080 break;
4082 default:
4083 return FALSE;
4084 }
4085 } else {
4086 static char buf[] = { '?', ' ', ' ', ' ', 0 };
4088 buf[0] = status->status;
4089 if (draw_text(view, line->type, buf, TRUE))
4090 return TRUE;
4091 type = LINE_DEFAULT;
4092 text = status->new.name;
4093 }
4095 draw_text(view, type, text, TRUE);
4096 return TRUE;
4097 }
4099 static enum request
4100 status_enter(struct view *view, struct line *line)
4101 {
4102 struct status *status = line->data;
4103 char oldpath[SIZEOF_STR] = "";
4104 char newpath[SIZEOF_STR] = "";
4105 char *info;
4106 size_t cmdsize = 0;
4107 enum open_flags split;
4109 if (line->type == LINE_STAT_NONE ||
4110 (!status && line[1].type == LINE_STAT_NONE)) {
4111 report("No file to diff");
4112 return REQ_NONE;
4113 }
4115 if (status) {
4116 if (sq_quote(oldpath, 0, status->old.name) >= sizeof(oldpath))
4117 return REQ_QUIT;
4118 /* Diffs for unmerged entries are empty when pasing the
4119 * new path, so leave it empty. */
4120 if (status->status != 'U' &&
4121 sq_quote(newpath, 0, status->new.name) >= sizeof(newpath))
4122 return REQ_QUIT;
4123 }
4125 if (opt_cdup[0] &&
4126 line->type != LINE_STAT_UNTRACKED &&
4127 !string_format_from(opt_cmd, &cmdsize, "cd %s;", opt_cdup))
4128 return REQ_QUIT;
4130 switch (line->type) {
4131 case LINE_STAT_STAGED:
4132 if (opt_no_head) {
4133 if (!string_format_from(opt_cmd, &cmdsize,
4134 STATUS_DIFF_NO_HEAD_SHOW_CMD,
4135 newpath))
4136 return REQ_QUIT;
4137 } else {
4138 if (!string_format_from(opt_cmd, &cmdsize,
4139 STATUS_DIFF_INDEX_SHOW_CMD,
4140 oldpath, newpath))
4141 return REQ_QUIT;
4142 }
4144 if (status)
4145 info = "Staged changes to %s";
4146 else
4147 info = "Staged changes";
4148 break;
4150 case LINE_STAT_UNSTAGED:
4151 if (!string_format_from(opt_cmd, &cmdsize,
4152 STATUS_DIFF_FILES_SHOW_CMD, oldpath, newpath))
4153 return REQ_QUIT;
4154 if (status)
4155 info = "Unstaged changes to %s";
4156 else
4157 info = "Unstaged changes";
4158 break;
4160 case LINE_STAT_UNTRACKED:
4161 if (opt_pipe)
4162 return REQ_QUIT;
4164 if (!status) {
4165 report("No file to show");
4166 return REQ_NONE;
4167 }
4169 opt_pipe = fopen(status->new.name, "r");
4170 info = "Untracked file %s";
4171 break;
4173 case LINE_STAT_HEAD:
4174 return REQ_NONE;
4176 default:
4177 die("line type %d not handled in switch", line->type);
4178 }
4180 split = view_is_displayed(view) ? OPEN_SPLIT : 0;
4181 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | split);
4182 if (view_is_displayed(VIEW(REQ_VIEW_STAGE))) {
4183 if (status) {
4184 stage_status = *status;
4185 } else {
4186 memset(&stage_status, 0, sizeof(stage_status));
4187 }
4189 stage_line_type = line->type;
4190 string_format(VIEW(REQ_VIEW_STAGE)->ref, info, stage_status.new.name);
4191 }
4193 return REQ_NONE;
4194 }
4196 static bool
4197 status_exists(struct status *status, enum line_type type)
4198 {
4199 struct view *view = VIEW(REQ_VIEW_STATUS);
4200 struct line *line;
4202 for (line = view->line; line < view->line + view->lines; line++) {
4203 struct status *pos = line->data;
4205 if (line->type == type && pos &&
4206 !strcmp(status->new.name, pos->new.name))
4207 return TRUE;
4208 }
4210 return FALSE;
4211 }
4214 static FILE *
4215 status_update_prepare(enum line_type type)
4216 {
4217 char cmd[SIZEOF_STR];
4218 size_t cmdsize = 0;
4220 if (opt_cdup[0] &&
4221 type != LINE_STAT_UNTRACKED &&
4222 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4223 return NULL;
4225 switch (type) {
4226 case LINE_STAT_STAGED:
4227 string_add(cmd, cmdsize, "git update-index -z --index-info");
4228 break;
4230 case LINE_STAT_UNSTAGED:
4231 case LINE_STAT_UNTRACKED:
4232 string_add(cmd, cmdsize, "git update-index -z --add --remove --stdin");
4233 break;
4235 default:
4236 die("line type %d not handled in switch", type);
4237 }
4239 return popen(cmd, "w");
4240 }
4242 static bool
4243 status_update_write(FILE *pipe, struct status *status, enum line_type type)
4244 {
4245 char buf[SIZEOF_STR];
4246 size_t bufsize = 0;
4247 size_t written = 0;
4249 switch (type) {
4250 case LINE_STAT_STAGED:
4251 if (!string_format_from(buf, &bufsize, "%06o %s\t%s%c",
4252 status->old.mode,
4253 status->old.rev,
4254 status->old.name, 0))
4255 return FALSE;
4256 break;
4258 case LINE_STAT_UNSTAGED:
4259 case LINE_STAT_UNTRACKED:
4260 if (!string_format_from(buf, &bufsize, "%s%c", status->new.name, 0))
4261 return FALSE;
4262 break;
4264 default:
4265 die("line type %d not handled in switch", type);
4266 }
4268 while (!ferror(pipe) && written < bufsize) {
4269 written += fwrite(buf + written, 1, bufsize - written, pipe);
4270 }
4272 return written == bufsize;
4273 }
4275 static bool
4276 status_update_file(struct status *status, enum line_type type)
4277 {
4278 FILE *pipe = status_update_prepare(type);
4279 bool result;
4281 if (!pipe)
4282 return FALSE;
4284 result = status_update_write(pipe, status, type);
4285 pclose(pipe);
4286 return result;
4287 }
4289 static bool
4290 status_update_files(struct view *view, struct line *line)
4291 {
4292 FILE *pipe = status_update_prepare(line->type);
4293 bool result = TRUE;
4294 struct line *pos = view->line + view->lines;
4295 int files = 0;
4296 int file, done;
4298 if (!pipe)
4299 return FALSE;
4301 for (pos = line; pos < view->line + view->lines && pos->data; pos++)
4302 files++;
4304 for (file = 0, done = 0; result && file < files; line++, file++) {
4305 int almost_done = file * 100 / files;
4307 if (almost_done > done) {
4308 done = almost_done;
4309 string_format(view->ref, "updating file %u of %u (%d%% done)",
4310 file, files, done);
4311 update_view_title(view);
4312 }
4313 result = status_update_write(pipe, line->data, line->type);
4314 }
4316 pclose(pipe);
4317 return result;
4318 }
4320 static bool
4321 status_update(struct view *view)
4322 {
4323 struct line *line = &view->line[view->lineno];
4325 assert(view->lines);
4327 if (!line->data) {
4328 /* This should work even for the "On branch" line. */
4329 if (line < view->line + view->lines && !line[1].data) {
4330 report("Nothing to update");
4331 return FALSE;
4332 }
4334 if (!status_update_files(view, line + 1)) {
4335 report("Failed to update file status");
4336 return FALSE;
4337 }
4339 } else if (!status_update_file(line->data, line->type)) {
4340 report("Failed to update file status");
4341 return FALSE;
4342 }
4344 return TRUE;
4345 }
4347 static enum request
4348 status_request(struct view *view, enum request request, struct line *line)
4349 {
4350 struct status *status = line->data;
4352 switch (request) {
4353 case REQ_STATUS_UPDATE:
4354 if (!status_update(view))
4355 return REQ_NONE;
4356 break;
4358 case REQ_STATUS_MERGE:
4359 if (!status || status->status != 'U') {
4360 report("Merging only possible for files with unmerged status ('U').");
4361 return REQ_NONE;
4362 }
4363 open_mergetool(status->new.name);
4364 break;
4366 case REQ_EDIT:
4367 if (!status)
4368 return request;
4370 open_editor(status->status != '?', status->new.name);
4371 break;
4373 case REQ_VIEW_BLAME:
4374 if (status) {
4375 string_copy(opt_file, status->new.name);
4376 opt_ref[0] = 0;
4377 }
4378 return request;
4380 case REQ_ENTER:
4381 /* After returning the status view has been split to
4382 * show the stage view. No further reloading is
4383 * necessary. */
4384 status_enter(view, line);
4385 return REQ_NONE;
4387 case REQ_REFRESH:
4388 /* Simply reload the view. */
4389 break;
4391 default:
4392 return request;
4393 }
4395 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD);
4397 return REQ_NONE;
4398 }
4400 static void
4401 status_select(struct view *view, struct line *line)
4402 {
4403 struct status *status = line->data;
4404 char file[SIZEOF_STR] = "all files";
4405 char *text;
4406 char *key;
4408 if (status && !string_format(file, "'%s'", status->new.name))
4409 return;
4411 if (!status && line[1].type == LINE_STAT_NONE)
4412 line++;
4414 switch (line->type) {
4415 case LINE_STAT_STAGED:
4416 text = "Press %s to unstage %s for commit";
4417 break;
4419 case LINE_STAT_UNSTAGED:
4420 text = "Press %s to stage %s for commit";
4421 break;
4423 case LINE_STAT_UNTRACKED:
4424 text = "Press %s to stage %s for addition";
4425 break;
4427 case LINE_STAT_HEAD:
4428 case LINE_STAT_NONE:
4429 text = "Nothing to update";
4430 break;
4432 default:
4433 die("line type %d not handled in switch", line->type);
4434 }
4436 if (status && status->status == 'U') {
4437 text = "Press %s to resolve conflict in %s";
4438 key = get_key(REQ_STATUS_MERGE);
4440 } else {
4441 key = get_key(REQ_STATUS_UPDATE);
4442 }
4444 string_format(view->ref, text, key, file);
4445 }
4447 static bool
4448 status_grep(struct view *view, struct line *line)
4449 {
4450 struct status *status = line->data;
4451 enum { S_STATUS, S_NAME, S_END } state;
4452 char buf[2] = "?";
4453 regmatch_t pmatch;
4455 if (!status)
4456 return FALSE;
4458 for (state = S_STATUS; state < S_END; state++) {
4459 char *text;
4461 switch (state) {
4462 case S_NAME: text = status->new.name; break;
4463 case S_STATUS:
4464 buf[0] = status->status;
4465 text = buf;
4466 break;
4468 default:
4469 return FALSE;
4470 }
4472 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
4473 return TRUE;
4474 }
4476 return FALSE;
4477 }
4479 static struct view_ops status_ops = {
4480 "file",
4481 status_open,
4482 NULL,
4483 status_draw,
4484 status_request,
4485 status_grep,
4486 status_select,
4487 };
4490 static bool
4491 stage_diff_line(FILE *pipe, struct line *line)
4492 {
4493 char *buf = line->data;
4494 size_t bufsize = strlen(buf);
4495 size_t written = 0;
4497 while (!ferror(pipe) && written < bufsize) {
4498 written += fwrite(buf + written, 1, bufsize - written, pipe);
4499 }
4501 fputc('\n', pipe);
4503 return written == bufsize;
4504 }
4506 static bool
4507 stage_diff_write(FILE *pipe, struct line *line, struct line *end)
4508 {
4509 while (line < end) {
4510 if (!stage_diff_line(pipe, line++))
4511 return FALSE;
4512 if (line->type == LINE_DIFF_CHUNK ||
4513 line->type == LINE_DIFF_HEADER)
4514 break;
4515 }
4517 return TRUE;
4518 }
4520 static struct line *
4521 stage_diff_find(struct view *view, struct line *line, enum line_type type)
4522 {
4523 for (; view->line < line; line--)
4524 if (line->type == type)
4525 return line;
4527 return NULL;
4528 }
4530 static bool
4531 stage_update_chunk(struct view *view, struct line *chunk)
4532 {
4533 char cmd[SIZEOF_STR];
4534 size_t cmdsize = 0;
4535 struct line *diff_hdr;
4536 FILE *pipe;
4538 diff_hdr = stage_diff_find(view, chunk, LINE_DIFF_HEADER);
4539 if (!diff_hdr)
4540 return FALSE;
4542 if (opt_cdup[0] &&
4543 !string_format_from(cmd, &cmdsize, "cd %s;", opt_cdup))
4544 return FALSE;
4546 if (!string_format_from(cmd, &cmdsize,
4547 "git apply --whitespace=nowarn --cached %s - && "
4548 "git update-index -q --unmerged --refresh 2>/dev/null",
4549 stage_line_type == LINE_STAT_STAGED ? "-R" : ""))
4550 return FALSE;
4552 pipe = popen(cmd, "w");
4553 if (!pipe)
4554 return FALSE;
4556 if (!stage_diff_write(pipe, diff_hdr, chunk) ||
4557 !stage_diff_write(pipe, chunk, view->line + view->lines))
4558 chunk = NULL;
4560 pclose(pipe);
4562 return chunk ? TRUE : FALSE;
4563 }
4565 static bool
4566 stage_update(struct view *view, struct line *line)
4567 {
4568 struct line *chunk = NULL;
4570 if (!opt_no_head && stage_line_type != LINE_STAT_UNTRACKED)
4571 chunk = stage_diff_find(view, line, LINE_DIFF_CHUNK);
4573 if (chunk) {
4574 if (!stage_update_chunk(view, chunk)) {
4575 report("Failed to apply chunk");
4576 return FALSE;
4577 }
4579 } else if (!stage_status.status) {
4580 view = VIEW(REQ_VIEW_STATUS);
4582 for (line = view->line; line < view->line + view->lines; line++)
4583 if (line->type == stage_line_type)
4584 break;
4586 if (!status_update_files(view, line + 1)) {
4587 report("Failed to update files");
4588 return FALSE;
4589 }
4591 } else if (!status_update_file(&stage_status, stage_line_type)) {
4592 report("Failed to update file");
4593 return FALSE;
4594 }
4596 return TRUE;
4597 }
4599 static enum request
4600 stage_request(struct view *view, enum request request, struct line *line)
4601 {
4602 switch (request) {
4603 case REQ_STATUS_UPDATE:
4604 if (!stage_update(view, line))
4605 return REQ_NONE;
4606 break;
4608 case REQ_EDIT:
4609 if (!stage_status.new.name[0])
4610 return request;
4612 open_editor(stage_status.status != '?', stage_status.new.name);
4613 break;
4615 case REQ_REFRESH:
4616 /* Reload everything ... */
4617 break;
4619 case REQ_VIEW_BLAME:
4620 if (stage_status.new.name[0]) {
4621 string_copy(opt_file, stage_status.new.name);
4622 opt_ref[0] = 0;
4623 }
4624 return request;
4626 case REQ_ENTER:
4627 return pager_request(view, request, line);
4629 default:
4630 return request;
4631 }
4633 open_view(view, REQ_VIEW_STATUS, OPEN_RELOAD | OPEN_NOMAXIMIZE);
4635 /* Check whether the staged entry still exists, and close the
4636 * stage view if it doesn't. */
4637 if (!status_exists(&stage_status, stage_line_type))
4638 return REQ_VIEW_CLOSE;
4640 if (stage_line_type == LINE_STAT_UNTRACKED)
4641 opt_pipe = fopen(stage_status.new.name, "r");
4642 else
4643 string_copy(opt_cmd, view->cmd);
4644 open_view(view, REQ_VIEW_STAGE, OPEN_RELOAD | OPEN_NOMAXIMIZE);
4646 return REQ_NONE;
4647 }
4649 static struct view_ops stage_ops = {
4650 "line",
4651 NULL,
4652 pager_read,
4653 pager_draw,
4654 stage_request,
4655 pager_grep,
4656 pager_select,
4657 };
4660 /*
4661 * Revision graph
4662 */
4664 struct commit {
4665 char id[SIZEOF_REV]; /* SHA1 ID. */
4666 char title[128]; /* First line of the commit message. */
4667 char author[75]; /* Author of the commit. */
4668 struct tm time; /* Date from the author ident. */
4669 struct ref **refs; /* Repository references. */
4670 chtype graph[SIZEOF_REVGRAPH]; /* Ancestry chain graphics. */
4671 size_t graph_size; /* The width of the graph array. */
4672 bool has_parents; /* Rewritten --parents seen. */
4673 };
4675 /* Size of rev graph with no "padding" columns */
4676 #define SIZEOF_REVITEMS (SIZEOF_REVGRAPH - (SIZEOF_REVGRAPH / 2))
4678 struct rev_graph {
4679 struct rev_graph *prev, *next, *parents;
4680 char rev[SIZEOF_REVITEMS][SIZEOF_REV];
4681 size_t size;
4682 struct commit *commit;
4683 size_t pos;
4684 unsigned int boundary:1;
4685 };
4687 /* Parents of the commit being visualized. */
4688 static struct rev_graph graph_parents[4];
4690 /* The current stack of revisions on the graph. */
4691 static struct rev_graph graph_stacks[4] = {
4692 { &graph_stacks[3], &graph_stacks[1], &graph_parents[0] },
4693 { &graph_stacks[0], &graph_stacks[2], &graph_parents[1] },
4694 { &graph_stacks[1], &graph_stacks[3], &graph_parents[2] },
4695 { &graph_stacks[2], &graph_stacks[0], &graph_parents[3] },
4696 };
4698 static inline bool
4699 graph_parent_is_merge(struct rev_graph *graph)
4700 {
4701 return graph->parents->size > 1;
4702 }
4704 static inline void
4705 append_to_rev_graph(struct rev_graph *graph, chtype symbol)
4706 {
4707 struct commit *commit = graph->commit;
4709 if (commit->graph_size < ARRAY_SIZE(commit->graph) - 1)
4710 commit->graph[commit->graph_size++] = symbol;
4711 }
4713 static void
4714 done_rev_graph(struct rev_graph *graph)
4715 {
4716 if (graph_parent_is_merge(graph) &&
4717 graph->pos < graph->size - 1 &&
4718 graph->next->size == graph->size + graph->parents->size - 1) {
4719 size_t i = graph->pos + graph->parents->size - 1;
4721 graph->commit->graph_size = i * 2;
4722 while (i < graph->next->size - 1) {
4723 append_to_rev_graph(graph, ' ');
4724 append_to_rev_graph(graph, '\\');
4725 i++;
4726 }
4727 }
4729 graph->size = graph->pos = 0;
4730 graph->commit = NULL;
4731 memset(graph->parents, 0, sizeof(*graph->parents));
4732 }
4734 static void
4735 push_rev_graph(struct rev_graph *graph, char *parent)
4736 {
4737 int i;
4739 /* "Collapse" duplicate parents lines.
4740 *
4741 * FIXME: This needs to also update update the drawn graph but
4742 * for now it just serves as a method for pruning graph lines. */
4743 for (i = 0; i < graph->size; i++)
4744 if (!strncmp(graph->rev[i], parent, SIZEOF_REV))
4745 return;
4747 if (graph->size < SIZEOF_REVITEMS) {
4748 string_copy_rev(graph->rev[graph->size++], parent);
4749 }
4750 }
4752 static chtype
4753 get_rev_graph_symbol(struct rev_graph *graph)
4754 {
4755 chtype symbol;
4757 if (graph->boundary)
4758 symbol = REVGRAPH_BOUND;
4759 else if (graph->parents->size == 0)
4760 symbol = REVGRAPH_INIT;
4761 else if (graph_parent_is_merge(graph))
4762 symbol = REVGRAPH_MERGE;
4763 else if (graph->pos >= graph->size)
4764 symbol = REVGRAPH_BRANCH;
4765 else
4766 symbol = REVGRAPH_COMMIT;
4768 return symbol;
4769 }
4771 static void
4772 draw_rev_graph(struct rev_graph *graph)
4773 {
4774 struct rev_filler {
4775 chtype separator, line;
4776 };
4777 enum { DEFAULT, RSHARP, RDIAG, LDIAG };
4778 static struct rev_filler fillers[] = {
4779 { ' ', '|' },
4780 { '`', '.' },
4781 { '\'', ' ' },
4782 { '/', ' ' },
4783 };
4784 chtype symbol = get_rev_graph_symbol(graph);
4785 struct rev_filler *filler;
4786 size_t i;
4788 if (opt_line_graphics)
4789 fillers[DEFAULT].line = line_graphics[LINE_GRAPHIC_VLINE];
4791 filler = &fillers[DEFAULT];
4793 for (i = 0; i < graph->pos; i++) {
4794 append_to_rev_graph(graph, filler->line);
4795 if (graph_parent_is_merge(graph->prev) &&
4796 graph->prev->pos == i)
4797 filler = &fillers[RSHARP];
4799 append_to_rev_graph(graph, filler->separator);
4800 }
4802 /* Place the symbol for this revision. */
4803 append_to_rev_graph(graph, symbol);
4805 if (graph->prev->size > graph->size)
4806 filler = &fillers[RDIAG];
4807 else
4808 filler = &fillers[DEFAULT];
4810 i++;
4812 for (; i < graph->size; i++) {
4813 append_to_rev_graph(graph, filler->separator);
4814 append_to_rev_graph(graph, filler->line);
4815 if (graph_parent_is_merge(graph->prev) &&
4816 i < graph->prev->pos + graph->parents->size)
4817 filler = &fillers[RSHARP];
4818 if (graph->prev->size > graph->size)
4819 filler = &fillers[LDIAG];
4820 }
4822 if (graph->prev->size > graph->size) {
4823 append_to_rev_graph(graph, filler->separator);
4824 if (filler->line != ' ')
4825 append_to_rev_graph(graph, filler->line);
4826 }
4827 }
4829 /* Prepare the next rev graph */
4830 static void
4831 prepare_rev_graph(struct rev_graph *graph)
4832 {
4833 size_t i;
4835 /* First, traverse all lines of revisions up to the active one. */
4836 for (graph->pos = 0; graph->pos < graph->size; graph->pos++) {
4837 if (!strcmp(graph->rev[graph->pos], graph->commit->id))
4838 break;
4840 push_rev_graph(graph->next, graph->rev[graph->pos]);
4841 }
4843 /* Interleave the new revision parent(s). */
4844 for (i = 0; !graph->boundary && i < graph->parents->size; i++)
4845 push_rev_graph(graph->next, graph->parents->rev[i]);
4847 /* Lastly, put any remaining revisions. */
4848 for (i = graph->pos + 1; i < graph->size; i++)
4849 push_rev_graph(graph->next, graph->rev[i]);
4850 }
4852 static void
4853 update_rev_graph(struct rev_graph *graph)
4854 {
4855 /* If this is the finalizing update ... */
4856 if (graph->commit)
4857 prepare_rev_graph(graph);
4859 /* Graph visualization needs a one rev look-ahead,
4860 * so the first update doesn't visualize anything. */
4861 if (!graph->prev->commit)
4862 return;
4864 draw_rev_graph(graph->prev);
4865 done_rev_graph(graph->prev->prev);
4866 }
4869 /*
4870 * Main view backend
4871 */
4873 static bool
4874 main_draw(struct view *view, struct line *line, unsigned int lineno)
4875 {
4876 struct commit *commit = line->data;
4878 if (!*commit->author)
4879 return FALSE;
4881 if (opt_date && draw_date(view, &commit->time))
4882 return TRUE;
4884 if (opt_author &&
4885 draw_field(view, LINE_MAIN_AUTHOR, commit->author, AUTHOR_COLS, TRUE))
4886 return TRUE;
4888 if (opt_rev_graph && commit->graph_size &&
4889 draw_graphic(view, LINE_MAIN_REVGRAPH, commit->graph, commit->graph_size))
4890 return TRUE;
4892 if (opt_show_refs && commit->refs) {
4893 size_t i = 0;
4895 do {
4896 enum line_type type;
4898 if (commit->refs[i]->head)
4899 type = LINE_MAIN_HEAD;
4900 else if (commit->refs[i]->ltag)
4901 type = LINE_MAIN_LOCAL_TAG;
4902 else if (commit->refs[i]->tag)
4903 type = LINE_MAIN_TAG;
4904 else if (commit->refs[i]->tracked)
4905 type = LINE_MAIN_TRACKED;
4906 else if (commit->refs[i]->remote)
4907 type = LINE_MAIN_REMOTE;
4908 else
4909 type = LINE_MAIN_REF;
4911 if (draw_text(view, type, "[", TRUE) ||
4912 draw_text(view, type, commit->refs[i]->name, TRUE) ||
4913 draw_text(view, type, "]", TRUE))
4914 return TRUE;
4916 if (draw_text(view, LINE_DEFAULT, " ", TRUE))
4917 return TRUE;
4918 } while (commit->refs[i++]->next);
4919 }
4921 draw_text(view, LINE_DEFAULT, commit->title, TRUE);
4922 return TRUE;
4923 }
4925 /* Reads git log --pretty=raw output and parses it into the commit struct. */
4926 static bool
4927 main_read(struct view *view, char *line)
4928 {
4929 static struct rev_graph *graph = graph_stacks;
4930 enum line_type type;
4931 struct commit *commit;
4933 if (!line) {
4934 if (!view->lines && !view->parent)
4935 die("No revisions match the given arguments.");
4936 update_rev_graph(graph);
4937 return TRUE;
4938 }
4940 type = get_line_type(line);
4941 if (type == LINE_COMMIT) {
4942 commit = calloc(1, sizeof(struct commit));
4943 if (!commit)
4944 return FALSE;
4946 line += STRING_SIZE("commit ");
4947 if (*line == '-') {
4948 graph->boundary = 1;
4949 line++;
4950 }
4952 string_copy_rev(commit->id, line);
4953 commit->refs = get_refs(commit->id);
4954 graph->commit = commit;
4955 add_line_data(view, commit, LINE_MAIN_COMMIT);
4957 while ((line = strchr(line, ' '))) {
4958 line++;
4959 push_rev_graph(graph->parents, line);
4960 commit->has_parents = TRUE;
4961 }
4962 return TRUE;
4963 }
4965 if (!view->lines)
4966 return TRUE;
4967 commit = view->line[view->lines - 1].data;
4969 switch (type) {
4970 case LINE_PARENT:
4971 if (commit->has_parents)
4972 break;
4973 push_rev_graph(graph->parents, line + STRING_SIZE("parent "));
4974 break;
4976 case LINE_AUTHOR:
4977 {
4978 /* Parse author lines where the name may be empty:
4979 * author <email@address.tld> 1138474660 +0100
4980 */
4981 char *ident = line + STRING_SIZE("author ");
4982 char *nameend = strchr(ident, '<');
4983 char *emailend = strchr(ident, '>');
4985 if (!nameend || !emailend)
4986 break;
4988 update_rev_graph(graph);
4989 graph = graph->next;
4991 *nameend = *emailend = 0;
4992 ident = chomp_string(ident);
4993 if (!*ident) {
4994 ident = chomp_string(nameend + 1);
4995 if (!*ident)
4996 ident = "Unknown";
4997 }
4999 string_ncopy(commit->author, ident, strlen(ident));
5001 /* Parse epoch and timezone */
5002 if (emailend[1] == ' ') {
5003 char *secs = emailend + 2;
5004 char *zone = strchr(secs, ' ');
5005 time_t time = (time_t) atol(secs);
5007 if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
5008 long tz;
5010 zone++;
5011 tz = ('0' - zone[1]) * 60 * 60 * 10;
5012 tz += ('0' - zone[2]) * 60 * 60;
5013 tz += ('0' - zone[3]) * 60;
5014 tz += ('0' - zone[4]) * 60;
5016 if (zone[0] == '-')
5017 tz = -tz;
5019 time -= tz;
5020 }
5022 gmtime_r(&time, &commit->time);
5023 }
5024 break;
5025 }
5026 default:
5027 /* Fill in the commit title if it has not already been set. */
5028 if (commit->title[0])
5029 break;
5031 /* Require titles to start with a non-space character at the
5032 * offset used by git log. */
5033 if (strncmp(line, " ", 4))
5034 break;
5035 line += 4;
5036 /* Well, if the title starts with a whitespace character,
5037 * try to be forgiving. Otherwise we end up with no title. */
5038 while (isspace(*line))
5039 line++;
5040 if (*line == '\0')
5041 break;
5042 /* FIXME: More graceful handling of titles; append "..." to
5043 * shortened titles, etc. */
5045 string_ncopy(commit->title, line, strlen(line));
5046 }
5048 return TRUE;
5049 }
5051 static enum request
5052 main_request(struct view *view, enum request request, struct line *line)
5053 {
5054 enum open_flags flags = display[0] == view ? OPEN_SPLIT : OPEN_DEFAULT;
5056 if (request == REQ_ENTER)
5057 open_view(view, REQ_VIEW_DIFF, flags);
5058 else
5059 return request;
5061 return REQ_NONE;
5062 }
5064 static bool
5065 grep_refs(struct ref **refs, regex_t *regex)
5066 {
5067 regmatch_t pmatch;
5068 size_t i = 0;
5070 if (!refs)
5071 return FALSE;
5072 do {
5073 if (regexec(regex, refs[i]->name, 1, &pmatch, 0) != REG_NOMATCH)
5074 return TRUE;
5075 } while (refs[i++]->next);
5077 return FALSE;
5078 }
5080 static bool
5081 main_grep(struct view *view, struct line *line)
5082 {
5083 struct commit *commit = line->data;
5084 enum { S_TITLE, S_AUTHOR, S_DATE, S_REFS, S_END } state;
5085 char buf[DATE_COLS + 1];
5086 regmatch_t pmatch;
5088 for (state = S_TITLE; state < S_END; state++) {
5089 char *text;
5091 switch (state) {
5092 case S_TITLE: text = commit->title; break;
5093 case S_AUTHOR:
5094 if (!opt_author)
5095 continue;
5096 text = commit->author;
5097 break;
5098 case S_DATE:
5099 if (!opt_date)
5100 continue;
5101 if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
5102 continue;
5103 text = buf;
5104 break;
5105 case S_REFS:
5106 if (!opt_show_refs)
5107 continue;
5108 if (grep_refs(commit->refs, view->regex) == TRUE)
5109 return TRUE;
5110 continue;
5111 default:
5112 return FALSE;
5113 }
5115 if (regexec(view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
5116 return TRUE;
5117 }
5119 return FALSE;
5120 }
5122 static void
5123 main_select(struct view *view, struct line *line)
5124 {
5125 struct commit *commit = line->data;
5127 string_copy_rev(view->ref, commit->id);
5128 string_copy_rev(ref_commit, view->ref);
5129 }
5131 static struct view_ops main_ops = {
5132 "commit",
5133 NULL,
5134 main_read,
5135 main_draw,
5136 main_request,
5137 main_grep,
5138 main_select,
5139 };
5142 /*
5143 * Unicode / UTF-8 handling
5144 *
5145 * NOTE: Much of the following code for dealing with unicode is derived from
5146 * ELinks' UTF-8 code developed by Scrool <scroolik@gmail.com>. Origin file is
5147 * src/intl/charset.c from the utf8 branch commit elinks-0.11.0-g31f2c28.
5148 */
5150 /* I've (over)annotated a lot of code snippets because I am not entirely
5151 * confident that the approach taken by this small UTF-8 interface is correct.
5152 * --jonas */
5154 static inline int
5155 unicode_width(unsigned long c)
5156 {
5157 if (c >= 0x1100 &&
5158 (c <= 0x115f /* Hangul Jamo */
5159 || c == 0x2329
5160 || c == 0x232a
5161 || (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f)
5162 /* CJK ... Yi */
5163 || (c >= 0xac00 && c <= 0xd7a3) /* Hangul Syllables */
5164 || (c >= 0xf900 && c <= 0xfaff) /* CJK Compatibility Ideographs */
5165 || (c >= 0xfe30 && c <= 0xfe6f) /* CJK Compatibility Forms */
5166 || (c >= 0xff00 && c <= 0xff60) /* Fullwidth Forms */
5167 || (c >= 0xffe0 && c <= 0xffe6)
5168 || (c >= 0x20000 && c <= 0x2fffd)
5169 || (c >= 0x30000 && c <= 0x3fffd)))
5170 return 2;
5172 if (c == '\t')
5173 return opt_tab_size;
5175 return 1;
5176 }
5178 /* Number of bytes used for encoding a UTF-8 character indexed by first byte.
5179 * Illegal bytes are set one. */
5180 static const unsigned char utf8_bytes[256] = {
5181 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,
5182 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,
5183 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,
5184 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,
5185 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,
5186 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,
5187 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,
5188 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,
5189 };
5191 /* Decode UTF-8 multi-byte representation into a unicode character. */
5192 static inline unsigned long
5193 utf8_to_unicode(const char *string, size_t length)
5194 {
5195 unsigned long unicode;
5197 switch (length) {
5198 case 1:
5199 unicode = string[0];
5200 break;
5201 case 2:
5202 unicode = (string[0] & 0x1f) << 6;
5203 unicode += (string[1] & 0x3f);
5204 break;
5205 case 3:
5206 unicode = (string[0] & 0x0f) << 12;
5207 unicode += ((string[1] & 0x3f) << 6);
5208 unicode += (string[2] & 0x3f);
5209 break;
5210 case 4:
5211 unicode = (string[0] & 0x0f) << 18;
5212 unicode += ((string[1] & 0x3f) << 12);
5213 unicode += ((string[2] & 0x3f) << 6);
5214 unicode += (string[3] & 0x3f);
5215 break;
5216 case 5:
5217 unicode = (string[0] & 0x0f) << 24;
5218 unicode += ((string[1] & 0x3f) << 18);
5219 unicode += ((string[2] & 0x3f) << 12);
5220 unicode += ((string[3] & 0x3f) << 6);
5221 unicode += (string[4] & 0x3f);
5222 break;
5223 case 6:
5224 unicode = (string[0] & 0x01) << 30;
5225 unicode += ((string[1] & 0x3f) << 24);
5226 unicode += ((string[2] & 0x3f) << 18);
5227 unicode += ((string[3] & 0x3f) << 12);
5228 unicode += ((string[4] & 0x3f) << 6);
5229 unicode += (string[5] & 0x3f);
5230 break;
5231 default:
5232 die("Invalid unicode length");
5233 }
5235 /* Invalid characters could return the special 0xfffd value but NUL
5236 * should be just as good. */
5237 return unicode > 0xffff ? 0 : unicode;
5238 }
5240 /* Calculates how much of string can be shown within the given maximum width
5241 * and sets trimmed parameter to non-zero value if all of string could not be
5242 * shown. If the reserve flag is TRUE, it will reserve at least one
5243 * trailing character, which can be useful when drawing a delimiter.
5244 *
5245 * Returns the number of bytes to output from string to satisfy max_width. */
5246 static size_t
5247 utf8_length(const char *string, int *width, size_t max_width, int *trimmed, bool reserve)
5248 {
5249 const char *start = string;
5250 const char *end = strchr(string, '\0');
5251 unsigned char last_bytes = 0;
5252 size_t last_ucwidth = 0;
5254 *width = 0;
5255 *trimmed = 0;
5257 while (string < end) {
5258 int c = *(unsigned char *) string;
5259 unsigned char bytes = utf8_bytes[c];
5260 size_t ucwidth;
5261 unsigned long unicode;
5263 if (string + bytes > end)
5264 break;
5266 /* Change representation to figure out whether
5267 * it is a single- or double-width character. */
5269 unicode = utf8_to_unicode(string, bytes);
5270 /* FIXME: Graceful handling of invalid unicode character. */
5271 if (!unicode)
5272 break;
5274 ucwidth = unicode_width(unicode);
5275 *width += ucwidth;
5276 if (*width > max_width) {
5277 *trimmed = 1;
5278 *width -= ucwidth;
5279 if (reserve && *width == max_width) {
5280 string -= last_bytes;
5281 *width -= last_ucwidth;
5282 }
5283 break;
5284 }
5286 string += bytes;
5287 last_bytes = bytes;
5288 last_ucwidth = ucwidth;
5289 }
5291 return string - start;
5292 }
5295 /*
5296 * Status management
5297 */
5299 /* Whether or not the curses interface has been initialized. */
5300 static bool cursed = FALSE;
5302 /* The status window is used for polling keystrokes. */
5303 static WINDOW *status_win;
5305 static bool status_empty = TRUE;
5307 /* Update status and title window. */
5308 static void
5309 report(const char *msg, ...)
5310 {
5311 struct view *view = display[current_view];
5313 if (input_mode)
5314 return;
5316 if (!view) {
5317 char buf[SIZEOF_STR];
5318 va_list args;
5320 va_start(args, msg);
5321 if (vsnprintf(buf, sizeof(buf), msg, args) >= sizeof(buf)) {
5322 buf[sizeof(buf) - 1] = 0;
5323 buf[sizeof(buf) - 2] = '.';
5324 buf[sizeof(buf) - 3] = '.';
5325 buf[sizeof(buf) - 4] = '.';
5326 }
5327 va_end(args);
5328 die("%s", buf);
5329 }
5331 if (!status_empty || *msg) {
5332 va_list args;
5334 va_start(args, msg);
5336 wmove(status_win, 0, 0);
5337 if (*msg) {
5338 vwprintw(status_win, msg, args);
5339 status_empty = FALSE;
5340 } else {
5341 status_empty = TRUE;
5342 }
5343 wclrtoeol(status_win);
5344 wrefresh(status_win);
5346 va_end(args);
5347 }
5349 update_view_title(view);
5350 update_display_cursor(view);
5351 }
5353 /* Controls when nodelay should be in effect when polling user input. */
5354 static void
5355 set_nonblocking_input(bool loading)
5356 {
5357 static unsigned int loading_views;
5359 if ((loading == FALSE && loading_views-- == 1) ||
5360 (loading == TRUE && loading_views++ == 0))
5361 nodelay(status_win, loading);
5362 }
5364 static void
5365 init_display(void)
5366 {
5367 int x, y;
5369 /* Initialize the curses library */
5370 if (isatty(STDIN_FILENO)) {
5371 cursed = !!initscr();
5372 } else {
5373 /* Leave stdin and stdout alone when acting as a pager. */
5374 FILE *io = fopen("/dev/tty", "r+");
5376 if (!io)
5377 die("Failed to open /dev/tty");
5378 cursed = !!newterm(NULL, io, io);
5379 }
5381 if (!cursed)
5382 die("Failed to initialize curses");
5384 nonl(); /* Tell curses not to do NL->CR/NL on output */
5385 cbreak(); /* Take input chars one at a time, no wait for \n */
5386 noecho(); /* Don't echo input */
5387 leaveok(stdscr, TRUE);
5389 if (has_colors())
5390 init_colors();
5392 getmaxyx(stdscr, y, x);
5393 status_win = newwin(1, 0, y - 1, 0);
5394 if (!status_win)
5395 die("Failed to create status window");
5397 /* Enable keyboard mapping */
5398 keypad(status_win, TRUE);
5399 wbkgdset(status_win, get_line_attr(LINE_STATUS));
5401 TABSIZE = opt_tab_size;
5402 if (opt_line_graphics) {
5403 line_graphics[LINE_GRAPHIC_VLINE] = ACS_VLINE;
5404 }
5405 }
5407 static char *
5408 read_prompt(const char *prompt)
5409 {
5410 enum { READING, STOP, CANCEL } status = READING;
5411 static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
5412 int pos = 0;
5414 while (status == READING) {
5415 struct view *view;
5416 int i, key;
5418 input_mode = TRUE;
5420 foreach_view (view, i)
5421 update_view(view);
5423 input_mode = FALSE;
5425 mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
5426 wclrtoeol(status_win);
5428 /* Refresh, accept single keystroke of input */
5429 key = wgetch(status_win);
5430 switch (key) {
5431 case KEY_RETURN:
5432 case KEY_ENTER:
5433 case '\n':
5434 status = pos ? STOP : CANCEL;
5435 break;
5437 case KEY_BACKSPACE:
5438 if (pos > 0)
5439 pos--;
5440 else
5441 status = CANCEL;
5442 break;
5444 case KEY_ESC:
5445 status = CANCEL;
5446 break;
5448 case ERR:
5449 break;
5451 default:
5452 if (pos >= sizeof(buf)) {
5453 report("Input string too long");
5454 return NULL;
5455 }
5457 if (isprint(key))
5458 buf[pos++] = (char) key;
5459 }
5460 }
5462 /* Clear the status window */
5463 status_empty = FALSE;
5464 report("");
5466 if (status == CANCEL)
5467 return NULL;
5469 buf[pos++] = 0;
5471 return buf;
5472 }
5474 /*
5475 * Repository references
5476 */
5478 static struct ref *refs = NULL;
5479 static size_t refs_alloc = 0;
5480 static size_t refs_size = 0;
5482 /* Id <-> ref store */
5483 static struct ref ***id_refs = NULL;
5484 static size_t id_refs_alloc = 0;
5485 static size_t id_refs_size = 0;
5487 static struct ref **
5488 get_refs(char *id)
5489 {
5490 struct ref ***tmp_id_refs;
5491 struct ref **ref_list = NULL;
5492 size_t ref_list_alloc = 0;
5493 size_t ref_list_size = 0;
5494 size_t i;
5496 for (i = 0; i < id_refs_size; i++)
5497 if (!strcmp(id, id_refs[i][0]->id))
5498 return id_refs[i];
5500 tmp_id_refs = realloc_items(id_refs, &id_refs_alloc, id_refs_size + 1,
5501 sizeof(*id_refs));
5502 if (!tmp_id_refs)
5503 return NULL;
5505 id_refs = tmp_id_refs;
5507 for (i = 0; i < refs_size; i++) {
5508 struct ref **tmp;
5510 if (strcmp(id, refs[i].id))
5511 continue;
5513 tmp = realloc_items(ref_list, &ref_list_alloc,
5514 ref_list_size + 1, sizeof(*ref_list));
5515 if (!tmp) {
5516 if (ref_list)
5517 free(ref_list);
5518 return NULL;
5519 }
5521 ref_list = tmp;
5522 if (ref_list_size > 0)
5523 ref_list[ref_list_size - 1]->next = 1;
5524 ref_list[ref_list_size] = &refs[i];
5526 /* XXX: The properties of the commit chains ensures that we can
5527 * safely modify the shared ref. The repo references will
5528 * always be similar for the same id. */
5529 ref_list[ref_list_size]->next = 0;
5530 ref_list_size++;
5531 }
5533 if (ref_list)
5534 id_refs[id_refs_size++] = ref_list;
5536 return ref_list;
5537 }
5539 static int
5540 read_ref(char *id, size_t idlen, char *name, size_t namelen)
5541 {
5542 struct ref *ref;
5543 bool tag = FALSE;
5544 bool ltag = FALSE;
5545 bool remote = FALSE;
5546 bool tracked = FALSE;
5547 bool check_replace = FALSE;
5548 bool head = FALSE;
5550 if (!strncmp(name, "refs/tags/", STRING_SIZE("refs/tags/"))) {
5551 if (!strcmp(name + namelen - 3, "^{}")) {
5552 namelen -= 3;
5553 name[namelen] = 0;
5554 if (refs_size > 0 && refs[refs_size - 1].ltag == TRUE)
5555 check_replace = TRUE;
5556 } else {
5557 ltag = TRUE;
5558 }
5560 tag = TRUE;
5561 namelen -= STRING_SIZE("refs/tags/");
5562 name += STRING_SIZE("refs/tags/");
5564 } else if (!strncmp(name, "refs/remotes/", STRING_SIZE("refs/remotes/"))) {
5565 remote = TRUE;
5566 namelen -= STRING_SIZE("refs/remotes/");
5567 name += STRING_SIZE("refs/remotes/");
5568 tracked = !strcmp(opt_remote, name);
5570 } else if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5571 namelen -= STRING_SIZE("refs/heads/");
5572 name += STRING_SIZE("refs/heads/");
5573 head = !strncmp(opt_head, name, namelen);
5575 } else if (!strcmp(name, "HEAD")) {
5576 opt_no_head = FALSE;
5577 return OK;
5578 }
5580 if (check_replace && !strcmp(name, refs[refs_size - 1].name)) {
5581 /* it's an annotated tag, replace the previous sha1 with the
5582 * resolved commit id; relies on the fact git-ls-remote lists
5583 * the commit id of an annotated tag right beofre the commit id
5584 * it points to. */
5585 refs[refs_size - 1].ltag = ltag;
5586 string_copy_rev(refs[refs_size - 1].id, id);
5588 return OK;
5589 }
5590 refs = realloc_items(refs, &refs_alloc, refs_size + 1, sizeof(*refs));
5591 if (!refs)
5592 return ERR;
5594 ref = &refs[refs_size++];
5595 ref->name = malloc(namelen + 1);
5596 if (!ref->name)
5597 return ERR;
5599 strncpy(ref->name, name, namelen);
5600 ref->name[namelen] = 0;
5601 ref->head = head;
5602 ref->tag = tag;
5603 ref->ltag = ltag;
5604 ref->remote = remote;
5605 ref->tracked = tracked;
5606 string_copy_rev(ref->id, id);
5608 return OK;
5609 }
5611 static int
5612 load_refs(void)
5613 {
5614 const char *cmd_env = getenv("TIG_LS_REMOTE");
5615 const char *cmd = cmd_env && *cmd_env ? cmd_env : TIG_LS_REMOTE;
5617 return read_properties(popen(cmd, "r"), "\t", read_ref);
5618 }
5620 static int
5621 read_repo_config_option(char *name, size_t namelen, char *value, size_t valuelen)
5622 {
5623 if (!strcmp(name, "i18n.commitencoding"))
5624 string_ncopy(opt_encoding, value, valuelen);
5626 if (!strcmp(name, "core.editor"))
5627 string_ncopy(opt_editor, value, valuelen);
5629 /* branch.<head>.remote */
5630 if (*opt_head &&
5631 !strncmp(name, "branch.", 7) &&
5632 !strncmp(name + 7, opt_head, strlen(opt_head)) &&
5633 !strcmp(name + 7 + strlen(opt_head), ".remote"))
5634 string_ncopy(opt_remote, value, valuelen);
5636 if (*opt_head && *opt_remote &&
5637 !strncmp(name, "branch.", 7) &&
5638 !strncmp(name + 7, opt_head, strlen(opt_head)) &&
5639 !strcmp(name + 7 + strlen(opt_head), ".merge")) {
5640 size_t from = strlen(opt_remote);
5642 if (!strncmp(value, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5643 value += STRING_SIZE("refs/heads/");
5644 valuelen -= STRING_SIZE("refs/heads/");
5645 }
5647 if (!string_format_from(opt_remote, &from, "/%s", value))
5648 opt_remote[0] = 0;
5649 }
5651 return OK;
5652 }
5654 static int
5655 load_git_config(void)
5656 {
5657 return read_properties(popen(GIT_CONFIG " --list", "r"),
5658 "=", read_repo_config_option);
5659 }
5661 static int
5662 read_repo_info(char *name, size_t namelen, char *value, size_t valuelen)
5663 {
5664 if (!opt_git_dir[0]) {
5665 string_ncopy(opt_git_dir, name, namelen);
5667 } else if (opt_is_inside_work_tree == -1) {
5668 /* This can be 3 different values depending on the
5669 * version of git being used. If git-rev-parse does not
5670 * understand --is-inside-work-tree it will simply echo
5671 * the option else either "true" or "false" is printed.
5672 * Default to true for the unknown case. */
5673 opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
5675 } else if (opt_cdup[0] == ' ') {
5676 string_ncopy(opt_cdup, name, namelen);
5677 } else {
5678 if (!strncmp(name, "refs/heads/", STRING_SIZE("refs/heads/"))) {
5679 namelen -= STRING_SIZE("refs/heads/");
5680 name += STRING_SIZE("refs/heads/");
5681 string_ncopy(opt_head, name, namelen);
5682 }
5683 }
5685 return OK;
5686 }
5688 static int
5689 load_repo_info(void)
5690 {
5691 int result;
5692 FILE *pipe = popen("(git rev-parse --git-dir --is-inside-work-tree "
5693 " --show-cdup; git symbolic-ref HEAD) 2>/dev/null", "r");
5695 /* XXX: The line outputted by "--show-cdup" can be empty so
5696 * initialize it to something invalid to make it possible to
5697 * detect whether it has been set or not. */
5698 opt_cdup[0] = ' ';
5700 result = read_properties(pipe, "=", read_repo_info);
5701 if (opt_cdup[0] == ' ')
5702 opt_cdup[0] = 0;
5704 return result;
5705 }
5707 static int
5708 read_properties(FILE *pipe, const char *separators,
5709 int (*read_property)(char *, size_t, char *, size_t))
5710 {
5711 char buffer[BUFSIZ];
5712 char *name;
5713 int state = OK;
5715 if (!pipe)
5716 return ERR;
5718 while (state == OK && (name = fgets(buffer, sizeof(buffer), pipe))) {
5719 char *value;
5720 size_t namelen;
5721 size_t valuelen;
5723 name = chomp_string(name);
5724 namelen = strcspn(name, separators);
5726 if (name[namelen]) {
5727 name[namelen] = 0;
5728 value = chomp_string(name + namelen + 1);
5729 valuelen = strlen(value);
5731 } else {
5732 value = "";
5733 valuelen = 0;
5734 }
5736 state = read_property(name, namelen, value, valuelen);
5737 }
5739 if (state != ERR && ferror(pipe))
5740 state = ERR;
5742 pclose(pipe);
5744 return state;
5745 }
5748 /*
5749 * Main
5750 */
5752 static void __NORETURN
5753 quit(int sig)
5754 {
5755 /* XXX: Restore tty modes and let the OS cleanup the rest! */
5756 if (cursed)
5757 endwin();
5758 exit(0);
5759 }
5761 static void __NORETURN
5762 die(const char *err, ...)
5763 {
5764 va_list args;
5766 endwin();
5768 va_start(args, err);
5769 fputs("tig: ", stderr);
5770 vfprintf(stderr, err, args);
5771 fputs("\n", stderr);
5772 va_end(args);
5774 exit(1);
5775 }
5777 static void
5778 warn(const char *msg, ...)
5779 {
5780 va_list args;
5782 va_start(args, msg);
5783 fputs("tig warning: ", stderr);
5784 vfprintf(stderr, msg, args);
5785 fputs("\n", stderr);
5786 va_end(args);
5787 }
5789 int
5790 main(int argc, char *argv[])
5791 {
5792 struct view *view;
5793 enum request request;
5794 size_t i;
5796 signal(SIGINT, quit);
5798 if (setlocale(LC_ALL, "")) {
5799 char *codeset = nl_langinfo(CODESET);
5801 string_ncopy(opt_codeset, codeset, strlen(codeset));
5802 }
5804 if (load_repo_info() == ERR)
5805 die("Failed to load repo info.");
5807 if (load_options() == ERR)
5808 die("Failed to load user config.");
5810 if (load_git_config() == ERR)
5811 die("Failed to load repo config.");
5813 if (!parse_options(argc, argv))
5814 return 0;
5816 /* Require a git repository unless when running in pager mode. */
5817 if (!opt_git_dir[0] && opt_request != REQ_VIEW_PAGER)
5818 die("Not a git repository");
5820 if (*opt_encoding && strcasecmp(opt_encoding, "UTF-8"))
5821 opt_utf8 = FALSE;
5823 if (*opt_codeset && strcmp(opt_codeset, opt_encoding)) {
5824 opt_iconv = iconv_open(opt_codeset, opt_encoding);
5825 if (opt_iconv == ICONV_NONE)
5826 die("Failed to initialize character set conversion");
5827 }
5829 if (*opt_git_dir && load_refs() == ERR)
5830 die("Failed to load refs.");
5832 for (i = 0; i < ARRAY_SIZE(views) && (view = &views[i]); i++)
5833 view->cmd_env = getenv(view->cmd_env);
5835 request = opt_request;
5837 init_display();
5839 while (view_driver(display[current_view], request)) {
5840 int key;
5841 int i;
5843 foreach_view (view, i)
5844 update_view(view);
5846 /* Refresh, accept single keystroke of input */
5847 key = wgetch(status_win);
5849 /* wgetch() with nodelay() enabled returns ERR when there's no
5850 * input. */
5851 if (key == ERR) {
5852 request = REQ_NONE;
5853 continue;
5854 }
5856 request = get_keybinding(display[current_view]->keymap, key);
5858 /* Some low-level request handling. This keeps access to
5859 * status_win restricted. */
5860 switch (request) {
5861 case REQ_PROMPT:
5862 {
5863 char *cmd = read_prompt(":");
5865 if (cmd && string_format(opt_cmd, "git %s", cmd)) {
5866 if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
5867 opt_request = REQ_VIEW_DIFF;
5868 } else {
5869 opt_request = REQ_VIEW_PAGER;
5870 }
5871 break;
5872 }
5874 request = REQ_NONE;
5875 break;
5876 }
5877 case REQ_SEARCH:
5878 case REQ_SEARCH_BACK:
5879 {
5880 const char *prompt = request == REQ_SEARCH
5881 ? "/" : "?";
5882 char *search = read_prompt(prompt);
5884 if (search)
5885 string_ncopy(opt_search, search, strlen(search));
5886 else
5887 request = REQ_NONE;
5888 break;
5889 }
5890 case REQ_SCREEN_RESIZE:
5891 {
5892 int height, width;
5894 getmaxyx(stdscr, height, width);
5896 /* Resize the status view and let the view driver take
5897 * care of resizing the displayed views. */
5898 wresize(status_win, 1, width);
5899 mvwin(status_win, height - 1, 0);
5900 wrefresh(status_win);
5901 break;
5902 }
5903 default:
5904 break;
5905 }
5906 }
5908 quit(0);
5910 return 0;
5911 }