7 * tig - text-mode interface for git
13 * tig [options] log [git log options]
14 * tig [options] diff [git diff options]
15 * tig [options] < [git log or git diff output]
19 * Browse changes in a git repository.
28 #define VERSION "tig-0.1"
43 static void die(const char *err
, ...);
44 static void report(const char *msg
, ...);
46 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
52 #define REQ_OFFSET (MAX_COMMAND + 1)
54 /* Requests for switching between the different views. */
55 #define REQ_DIFF (REQ_OFFSET + 0)
56 #define REQ_LOG (REQ_OFFSET + 1)
57 #define REQ_MAIN (REQ_OFFSET + 2)
58 #define REQ_VIEWS (REQ_OFFSET + 3)
60 #define REQ_QUIT (REQ_OFFSET + 11)
61 #define REQ_VERSION (REQ_OFFSET + 12)
62 #define REQ_STOP (REQ_OFFSET + 13)
63 #define REQ_UPDATE (REQ_OFFSET + 14)
64 #define REQ_REDRAW (REQ_OFFSET + 15)
65 #define REQ_FIRST_LINE (REQ_OFFSET + 16)
66 #define REQ_LAST_LINE (REQ_OFFSET + 17)
67 #define REQ_LINE_NUMBER (REQ_OFFSET + 18)
69 #define SIZEOF_VIEWS (REQ_VIEWS - REQ_OFFSET)
70 #define SIZEOF_ID 1024
72 #define COLOR_TRANSP (-1)
80 static int opt_line_number
;
81 static int opt_request
= REQ_MAIN
;
83 char head_id
[SIZEOF_ID
] = "HEAD";
84 char commit_id
[SIZEOF_ID
] = "HEAD";
87 /* Returns the index of log or diff command or -1 to exit. */
89 parse_options(int argc
, char *argv
[])
93 for (i
= 1; i
< argc
; i
++) {
100 if (!strcmp(opt
, "log")) {
101 opt_request
= REQ_LOG
;
108 } else if (!strcmp(opt
, "diff")) {
109 opt_request
= REQ_DIFF
;
114 * Start up in log view.
116 } else if (!strcmp(opt
, "-l")) {
117 opt_request
= REQ_LOG
;
121 * Start up in diff view.
123 } else if (!strcmp(opt
, "-d")) {
124 opt_request
= REQ_DIFF
;
127 * -n, --line-number::
128 * Prefix line numbers in log and diff view.
130 } else if (!strcmp(opt
, "-n") ||
131 !strcmp(opt
, "--line-number")) {
136 * Show version and exit.
138 } else if (!strcmp(opt
, "-v") ||
139 !strcmp(opt
, "--version")) {
140 printf("tig version %s\n", VERSION
);
145 * Commit reference, symbolic or raw SHA1 ID.
147 } else if (opt
[0] && opt
[0] != '-') {
148 strncpy(head_id
, opt
, SIZEOF_ID
);
149 strncpy(commit_id
, opt
, SIZEOF_ID
);
152 die("Unknown command: '%s'", opt
);
161 * Line-oriented content detection.
205 #define LINE(type, line, fg, bg, attr) \
206 { LINE_##type, (line), sizeof(line) - 1, (fg), (bg), (attr) }
208 static struct line_info line_info
[] = {
210 LINE(DIFF
, "diff --git ", COLOR_YELLOW
, COLOR_TRANSP
, 0),
211 LINE(INDEX
, "index ", COLOR_BLUE
, COLOR_TRANSP
, 0),
212 LINE(DIFF_CHUNK
, "@@", COLOR_MAGENTA
, COLOR_TRANSP
, 0),
213 LINE(DIFF_ADD
, "+", COLOR_GREEN
, COLOR_TRANSP
, 0),
214 LINE(DIFF_DEL
, "-", COLOR_RED
, COLOR_TRANSP
, 0),
215 LINE(DIFF_OLDMODE
, "old mode ", COLOR_YELLOW
, COLOR_TRANSP
, 0),
216 LINE(DIFF_NEWMODE
, "new mode ", COLOR_YELLOW
, COLOR_TRANSP
, 0),
217 LINE(DIFF_COPY
, "copy ", COLOR_YELLOW
, COLOR_TRANSP
, 0),
218 LINE(DIFF_RENAME
, "rename ", COLOR_YELLOW
, COLOR_TRANSP
, 0),
219 LINE(DIFF_SIM
, "similarity ", COLOR_YELLOW
, COLOR_TRANSP
, 0),
220 LINE(DIFF_DISSIM
, "dissimilarity ", COLOR_YELLOW
, COLOR_TRANSP
, 0),
222 /* Pretty print commit header */
223 LINE(AUTHOR
, "Author: ", COLOR_CYAN
, COLOR_TRANSP
, 0),
224 LINE(MERGE
, "Merge: ", COLOR_BLUE
, COLOR_TRANSP
, 0),
225 LINE(DATE
, "Date: ", COLOR_YELLOW
, COLOR_TRANSP
, 0),
227 /* Raw commit header */
228 LINE(COMMIT
, "commit ", COLOR_GREEN
, COLOR_TRANSP
, 0),
229 LINE(PARENT
, "parent ", COLOR_BLUE
, COLOR_TRANSP
, 0),
230 LINE(TREE
, "tree ", COLOR_BLUE
, COLOR_TRANSP
, 0),
231 LINE(AUTHOR_IDENT
, "author ", COLOR_CYAN
, COLOR_TRANSP
, 0),
232 LINE(COMMITTER
, "committer ", COLOR_MAGENTA
, COLOR_TRANSP
, 0),
234 LINE(DIFF_TREE
, "diff-tree ", COLOR_BLUE
, COLOR_TRANSP
, 0),
237 LINE(DEFAULT
, "", COLOR_TRANSP
, COLOR_TRANSP
, A_NORMAL
),
238 LINE(CURSOR
, "", COLOR_WHITE
, COLOR_GREEN
, A_BOLD
),
239 LINE(STATUS
, "", COLOR_GREEN
, COLOR_TRANSP
, 0),
240 LINE(TITLE
, "", COLOR_YELLOW
, COLOR_BLUE
, A_BOLD
),
241 LINE(MAIN_DATE
, "", COLOR_BLUE
, COLOR_TRANSP
, 0),
242 LINE(MAIN_AUTHOR
, "", COLOR_GREEN
, COLOR_TRANSP
, 0),
243 LINE(MAIN_COMMIT
, "", COLOR_TRANSP
, COLOR_TRANSP
, 0),
244 LINE(MAIN_DELIM
, "", COLOR_MAGENTA
, COLOR_TRANSP
, 0),
247 static struct line_info
*
248 get_line_info(char *line
)
250 int linelen
= strlen(line
);
253 for (i
= 0; i
< ARRAY_SIZE(line_info
); i
++) {
254 if (linelen
< line_info
[i
].linelen
255 || strncmp(line_info
[i
].line
, line
, line_info
[i
].linelen
))
258 return &line_info
[i
];
264 static enum line_type
265 get_line_type(char *line
)
267 struct line_info
*info
= get_line_info(line
);
269 return info ? info
->type
: LINE_DEFAULT
;
273 get_line_attr(enum line_type type
)
277 for (i
= 0; i
< ARRAY_SIZE(line_info
); i
++)
278 if (line_info
[i
].type
== type
)
279 return COLOR_PAIR(line_info
[i
].type
) | line_info
[i
].attr
;
287 int transparent_bg
= COLOR_BLACK
;
288 int transparent_fg
= COLOR_WHITE
;
293 if (use_default_colors() != ERR
) {
298 for (i
= 0; i
< ARRAY_SIZE(line_info
); i
++) {
299 struct line_info
*info
= &line_info
[i
];
300 int bg
= info
->bg
== COLOR_TRANSP ? transparent_bg
: info
->bg
;
301 int fg
= info
->fg
== COLOR_TRANSP ? transparent_fg
: info
->fg
;
303 init_pair(info
->type
, fg
, bg
);
321 * stop all background loading
333 #define HELP "(d)iff, (l)og, (m)ain, (q)uit, (v)ersion, (h)elp"
340 struct keymap keymap
[] = {
341 /* Cursor navigation */
342 { KEY_UP
, REQ_PREV_LINE
},
343 { 'k', REQ_PREV_LINE
},
344 { KEY_DOWN
, REQ_NEXT_LINE
},
345 { 'j', REQ_NEXT_LINE
},
346 { KEY_HOME
, REQ_FIRST_LINE
},
347 { KEY_END
, REQ_LAST_LINE
},
348 { KEY_NPAGE
, REQ_NEXT_PAGE
},
349 { KEY_PPAGE
, REQ_PREV_PAGE
},
352 { KEY_IC
, REQ_SCR_BLINE
}, /* scroll field backward a line */
353 { KEY_DC
, REQ_SCR_FLINE
}, /* scroll field forward a line */
354 { 's', REQ_SCR_FPAGE
}, /* scroll field forward a page */
355 { 'w', REQ_SCR_BPAGE
}, /* scroll field backward a page */
361 { 'n', REQ_LINE_NUMBER
},
363 /* No input from wgetch() with nodelay() enabled. */
366 { KEY_ESC
, REQ_QUIT
},
369 { 'v', REQ_VERSION
},
374 get_request(int request
)
378 for (i
= 0; i
< ARRAY_SIZE(keymap
); i
++)
379 if (keymap
[i
].alias
== request
)
380 return keymap
[i
].request
;
397 int (*read
)(struct view
*, char *);
398 int (*draw
)(struct view
*, unsigned int);
399 size_t objsize
; /* Size of objects in the line index */
405 unsigned long offset
; /* Offset of the window top */
406 unsigned long lineno
; /* Current line number */
409 unsigned long lines
; /* Total number of lines */
410 void **line
; /* Line index */
423 static int pager_draw(struct view
*view
, unsigned int lineno
);
424 static int pager_read(struct view
*view
, char *line
);
426 static int main_draw(struct view
*view
, unsigned int lineno
);
427 static int main_read(struct view
*view
, char *line
);
430 "git log --stat -n1 %s ; echo; " \
431 "git diff --find-copies-harder -B -C %s^ %s"
434 "git log --stat -n100 %s"
437 "git log --stat --pretty=raw %s"
439 /* The status window at the bottom. Used for polling keystrokes. */
440 static WINDOW
*status_win
;
442 static WINDOW
*title_win
;
444 static unsigned int current_view
;
445 static unsigned int nloading
;
447 static struct view views
[];
448 static struct view
*display
[];
450 static struct view views
[] = {
451 { "diff", DIFF_CMD
, commit_id
, pager_read
, pager_draw
, sizeof(char) },
452 { "log", LOG_CMD
, head_id
, pager_read
, pager_draw
, sizeof(char) },
453 { "main", MAIN_CMD
, head_id
, main_read
, main_draw
, sizeof(struct commit
) },
456 static struct view
*display
[ARRAY_SIZE(views
)];
459 #define foreach_view(view, i) \
460 for (i = 0; i < sizeof(display) && (view = display[i]); i++)
463 redraw_view(struct view
*view
)
468 wmove(view
->win
, 0, 0);
470 for (lineno
= 0; lineno
< view
->height
; lineno
++) {
471 if (!view
->draw(view
, lineno
))
475 redrawwin(view
->win
);
480 resize_view(struct view
*view
)
484 getmaxyx(stdscr
, lines
, cols
);
487 mvwin(view
->win
, 0, 0);
488 wresize(view
->win
, lines
- 2, cols
);
491 view
->win
= newwin(lines
- 2, 0, 0, 0);
493 report("failed to create %s view", view
->name
);
496 scrollok(view
->win
, TRUE
);
499 getmaxyx(view
->win
, view
->height
, view
->width
);
502 /* FIXME: Fix percentage. */
504 report_position(struct view
*view
, int all
)
506 report(all ?
"line %d of %d (%d%%) viewing from %d"
510 view
->lines ? view
->offset
* 100 / view
->lines
: 0,
516 move_view(struct view
*view
, int lines
)
518 /* The rendering expects the new offset. */
519 view
->offset
+= lines
;
521 assert(0 <= view
->offset
&& view
->offset
< view
->lines
);
524 if (view
->height
< (lines
> 0 ? lines
: -lines
)) {
528 int line
= lines
> 0 ? view
->height
- lines
: 0;
529 int end
= line
+ (lines
> 0 ? lines
: -lines
);
531 wscrl(view
->win
, lines
);
533 for (; line
< end
; line
++) {
534 if (!view
->draw(view
, line
))
539 /* Move current line into the view. */
540 if (view
->lineno
< view
->offset
) {
541 view
->lineno
= view
->offset
;
544 } else if (view
->lineno
>= view
->offset
+ view
->height
) {
545 view
->lineno
= view
->offset
+ view
->height
- 1;
546 view
->draw(view
, view
->lineno
- view
->offset
);
549 assert(view
->offset
<= view
->lineno
&& view
->lineno
<= view
->lines
);
551 redrawwin(view
->win
);
554 report_position(view
, lines
);
558 scroll_view(struct view
*view
, int request
)
564 lines
= view
->height
;
566 if (view
->offset
+ lines
> view
->lines
)
567 lines
= view
->lines
- view
->offset
;
569 if (lines
== 0 || view
->offset
+ view
->height
>= view
->lines
) {
570 report("already at last line");
576 lines
= view
->height
;
578 if (lines
> view
->offset
)
579 lines
= view
->offset
;
582 report("already at first line");
590 move_view(view
, lines
);
594 navigate_view(struct view
*view
, int request
)
600 steps
= -view
->lineno
;
604 steps
= view
->lines
- view
->lineno
- 1;
608 steps
= view
->height
> view
->lineno
609 ?
-view
->lineno
: -view
->height
;
613 steps
= view
->lineno
+ view
->height
>= view
->lines
614 ? view
->lines
- view
->lineno
- 1 : view
->height
;
626 if (steps
< 0 && view
->lineno
== 0) {
627 report("already at first line");
630 } else if (steps
> 0 && view
->lineno
+ 1 == view
->lines
) {
631 report("already at last line");
635 view
->lineno
+= steps
;
636 view
->draw(view
, view
->lineno
- steps
- view
->offset
);
638 if (view
->lineno
< view
->offset
||
639 view
->lineno
>= view
->offset
+ view
->height
) {
640 if (steps
< 0 && -steps
> view
->offset
) {
641 steps
= -view
->offset
;
643 } else if (steps
> 0) {
644 if (view
->lineno
== view
->lines
- 1 &&
645 view
->lines
> view
->height
) {
646 steps
= view
->lines
- view
->offset
- 1;
647 if (steps
>= view
->height
)
648 steps
-= view
->height
- 1;
652 move_view(view
, steps
);
656 /* Draw the cursor line */
657 view
->draw(view
, view
->lineno
- view
->offset
);
659 redrawwin(view
->win
);
662 report_position(view
, view
->height
);
668 begin_update(struct view
*view
)
675 if (snprintf(buf
, sizeof(buf
), view
->cmd
, id
, id
, id
) < sizeof(buf
))
676 view
->pipe
= popen(buf
, "r");
682 nodelay(status_win
, TRUE
);
685 display
[current_view
] = view
;
695 end_update(struct view
*view
)
697 wattrset(view
->win
, A_NORMAL
);
702 nodelay(status_win
, FALSE
);
706 update_view(struct view
*view
)
712 int lines
= view
->height
;
717 /* Only redraw after the first reading session. */
718 /* FIXME: ... and possibly more. */
719 redraw
= view
->height
> view
->lines
;
721 tmp
= realloc(view
->line
, sizeof(*view
->line
) * (view
->lines
+ lines
));
727 while ((line
= fgets(buffer
, sizeof(buffer
), view
->pipe
))) {
730 linelen
= strlen(line
);
732 line
[linelen
- 1] = 0;
734 if (!view
->read(view
, line
))
744 if (ferror(view
->pipe
)) {
745 report("failed to read %s", view
->cmd
);
748 } else if (feof(view
->pipe
)) {
749 report_position(view
, 0);
756 report("allocation failure");
765 switch_view(struct view
*prev
, int request
)
767 struct view
*view
= &views
[request
- REQ_OFFSET
];
768 struct view
*displayed
;
772 foreach_view (displayed
, i
) ;
775 report("already in %s view", view
->name
);
777 report("FIXME: Maximize");
782 foreach_view (displayed
, i
) {
783 if (view
== displayed
) {
785 report("new current view");
797 for (i
= 0; i
< view
->lines
; i
++)
805 if (prev
&& prev
->pipe
)
808 if (begin_update(view
)) {
812 report("loading...");
819 /* Process a keystroke */
821 view_driver(struct view
*view
, int key
)
823 int request
= get_request(key
);
834 navigate_view(view
, request
);
842 scroll_view(view
, request
);
848 view
= switch_view(view
, request
);
851 case REQ_LINE_NUMBER
:
852 opt_line_number
= !opt_line_number
;
861 foreach_view (view
, i
) {
864 scroll_view(view
, 0);
870 report("version %s", VERSION
);
894 pager_draw(struct view
*view
, unsigned int lineno
)
900 if (view
->offset
+ lineno
>= view
->lines
)
903 line
= view
->line
[view
->offset
+ lineno
];
904 type
= get_line_type(line
);
906 if (view
->offset
+ lineno
== view
->lineno
) {
907 if (type
== LINE_COMMIT
)
908 strncpy(commit_id
, line
+ 7, SIZEOF_ID
);
912 attr
= get_line_attr(type
);
913 wattrset(view
->win
, attr
);
915 if (opt_line_number
) {
916 mvwprintw(view
->win
, lineno
, 0, "%4d: ", view
->offset
+ lineno
+ 1);
919 waddstr(view
->win
, " ");
922 char *tab
= strchr(line
, '\t');
925 waddnstr(view
->win
, line
, tab
- line
);
927 waddstr(view
->win
, line
);
931 waddstr(view
->win
, line
);
934 /* No empty lines makes cursor drawing and clearing implicit. */
937 mvwaddstr(view
->win
, lineno
, 0, line
);
944 pager_read(struct view
*view
, char *line
)
946 view
->line
[view
->lines
] = strdup(line
);
947 if (!view
->line
[view
->lines
])
955 main_draw(struct view
*view
, unsigned int lineno
)
958 struct commit
*commit
;
963 if (view
->offset
+ lineno
>= view
->lines
)
966 commit
= view
->line
[view
->offset
+ lineno
];
967 if (!commit
) return FALSE
;
969 if (view
->offset
+ lineno
== view
->lineno
) {
970 strncpy(commit_id
, commit
->id
, SIZEOF_ID
);
973 type
= LINE_MAIN_COMMIT
;
976 wmove(view
->win
, lineno
, cols
);
977 wattrset(view
->win
, get_line_attr(LINE_MAIN_DATE
));
979 timelen
= strftime(buf
, sizeof(buf
), "%Y-%m-%d %H:%M:%S ", &commit
->time
);
980 waddnstr(view
->win
, buf
, timelen
);
983 wmove(view
->win
, lineno
, cols
);
984 wattrset(view
->win
, get_line_attr(LINE_MAIN_AUTHOR
));
986 if (strlen(commit
->author
) > 19) {
987 waddnstr(view
->win
, commit
->author
, 18);
988 wattrset(view
->win
, get_line_attr(LINE_MAIN_DELIM
));
989 waddch(view
->win
, '~');
991 waddstr(view
->win
, commit
->author
);
995 wattrset(view
->win
, A_NORMAL
);
996 mvwaddch(view
->win
, lineno
, cols
, ACS_LTEE
);
997 wattrset(view
->win
, get_line_attr(type
));
998 mvwaddstr(view
->win
, lineno
, cols
+ 2, commit
->title
);
999 wattrset(view
->win
, A_NORMAL
);
1005 main_read(struct view
*view
, char *line
)
1007 enum line_type type
= get_line_type(line
);
1008 struct commit
*commit
;
1012 commit
= calloc(1, sizeof(struct commit
));
1016 line
+= sizeof("commit ") - 1;
1018 view
->line
[view
->lines
++] = commit
;
1019 strncpy(commit
->id
, line
, sizeof(commit
->id
));
1022 case LINE_AUTHOR_IDENT
:
1024 char *ident
= line
+ sizeof("author ") - 1;
1025 char *end
= strchr(ident
, '<');
1028 for (; end
> ident
&& isspace(end
[-1]); end
--) ;
1032 commit
= view
->line
[view
->lines
- 1];
1033 strncpy(commit
->author
, ident
, sizeof(commit
->author
));
1036 char *secs
= strchr(end
+ 1, '>');
1040 if (!secs
|| secs
[1] != ' ')
1044 time
= (time_t) atol(secs
);
1045 zone
= strchr(secs
, ' ');
1046 if (zone
&& strlen(zone
) == sizeof(" +0700") - 1) {
1050 tz
= ('0' - zone
[1]) * 60 * 60 * 10;
1051 tz
+= ('0' - zone
[2]) * 60 * 60;
1052 tz
+= ('0' - zone
[3]) * 60;
1053 tz
+= ('0' - zone
[4]) * 60;
1060 gmtime_r(&time
, &commit
->time
);
1065 commit
= view
->line
[view
->lines
- 1];
1066 if (!commit
->title
[0] &&
1067 !strncmp(line
, " ", 4) &&
1069 strncpy(commit
->title
, line
+ 4, sizeof(commit
->title
));
1089 /* FIXME: Shutdown gracefully. */
1094 static void die(const char *err
, ...)
1100 va_start(args
, err
);
1101 fputs("tig: ", stderr
);
1102 vfprintf(stderr
, err
, args
);
1103 fputs("\n", stderr
);
1110 report(const char *msg
, ...)
1115 wmove(title_win
, 0, 0);
1116 wprintw(title_win
, "commit %s", commit_id
);
1117 wrefresh(title_win
);
1119 va_start(args
, msg
);
1122 wmove(status_win
, 0, 0);
1125 if (display
[current_view
])
1126 wprintw(status_win
, "%s %4s: ", commit_id
, display
[current_view
]->name
);
1128 vwprintw(status_win
, msg
, args
);
1129 wrefresh(status_win
);
1135 main(int argc
, char *argv
[])
1141 signal(SIGINT
, quit
);
1143 git_cmd
= parse_options(argc
, argv
);
1147 request
= opt_request
;
1149 initscr(); /* initialize the curses library */
1150 nonl(); /* tell curses not to do NL->CR/NL on output */
1151 cbreak(); /* take input chars one at a time, no wait for \n */
1152 noecho(); /* don't echo input */
1153 leaveok(stdscr
, TRUE
);
1159 getmaxyx(stdscr
, y
, x
);
1160 status_win
= newwin(1, 0, y
- 1, 0);
1162 die("Failed to create status window");
1164 title_win
= newwin(1, 0, y
- 2, 0);
1166 die("Failed to create title window");
1168 /* Enable keyboard mapping */
1169 keypad(status_win
, TRUE
);
1170 wbkgdset(status_win
, get_line_attr(LINE_STATUS
));
1171 wbkgdset(title_win
, get_line_attr(LINE_TITLE
));
1173 while (view_driver(display
[current_view
], request
)) {
1177 foreach_view (view
, i
) {
1183 /* Refresh, accept single keystroke of input */
1184 request
= wgetch(status_win
);
1185 if (request
== KEY_RESIZE
) {
1188 getmaxyx(stdscr
, lines
, cols
);
1190 mvwin(status_win
, lines
- 1, 0);
1191 wresize(status_win
, 1, cols
- 1);
1193 mvwin(title_win
, lines
- 2, 0);
1194 wresize(title_win
, 1, cols
- 1);
1206 * Copyright (c) Jonas Fonseca, 2006
1208 * This program is free software; you can redistribute it and/or modify
1209 * it under the terms of the GNU General Public License as published by
1210 * the Free Software Foundation; either version 2 of the License, or
1211 * (at your option) any later version.
1215 * link:http://www.kernel.org/pub/software/scm/git/docs/[git(7)],
1216 * link:http://www.kernel.org/pub/software/scm/cogito/docs/[cogito(7)]