Commit | Line | Data |
---|---|---|
b801d8b2 JF |
1 | /** |
2 | * TIG(1) | |
3 | * ====== | |
4 | * | |
5 | * NAME | |
6 | * ---- | |
7 | * tig - text-mode interface for git | |
8 | * | |
9 | * SYNOPSIS | |
10 | * -------- | |
11 | * [verse] | |
12 | * tig | |
13 | * tig log [git log options] | |
14 | * tig diff [git diff options] | |
15 | * tig < [git log or git diff output] | |
16 | * | |
17 | * DESCRIPTION | |
18 | * ----------- | |
19 | * Browse changes in a git repository. | |
20 | * | |
21 | * OPTIONS | |
22 | * ------- | |
23 | * | |
24 | * None. | |
25 | * | |
26 | **/ | |
27 | ||
28 | #define DEBUG | |
29 | ||
30 | #ifndef DEBUG | |
31 | #define NDEBUG | |
32 | #endif | |
33 | ||
34 | #include <stdarg.h> | |
35 | #include <stdlib.h> | |
36 | #include <stdio.h> | |
37 | #include <string.h> | |
38 | #include <signal.h> | |
39 | #include <assert.h> | |
40 | ||
41 | #include <curses.h> | |
42 | #include <form.h> | |
43 | ||
44 | static void die(const char *err, ...); | |
45 | static void report(const char *msg, ...); | |
46 | ||
47 | #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0])) | |
48 | ||
49 | #define KEY_ESC 27 | |
50 | #define KEY_TAB 9 | |
51 | ||
52 | #define REQ_OFFSET (MAX_COMMAND + 1) | |
53 | ||
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 | ||
59 | #define REQ_QUIT (REQ_OFFSET + 11) | |
60 | #define REQ_VERSION (REQ_OFFSET + 12) | |
61 | #define REQ_STOP (REQ_OFFSET + 13) | |
62 | #define REQ_UPDATE (REQ_OFFSET + 14) | |
63 | #define REQ_REDRAW (REQ_OFFSET + 15) | |
64 | ||
65 | ||
66 | /** | |
67 | * KEYS | |
68 | * ---- | |
69 | * | |
70 | * d:: | |
71 | * diff | |
72 | * l:: | |
73 | * log | |
74 | * q:: | |
75 | * quit | |
76 | * r:: | |
77 | * redraw screen | |
78 | * s:: | |
79 | * stop all background loading | |
80 | * j:: | |
81 | * down | |
82 | * k:: | |
83 | * up | |
84 | * h, ?:: | |
85 | * help | |
86 | * v:: | |
87 | * version | |
88 | * | |
89 | **/ | |
90 | ||
91 | #define HELP "(d)iff, (l)og, (m)ain, (q)uit, (v)ersion, (h)elp" | |
92 | ||
93 | struct keymap { | |
94 | int alias; | |
95 | int request; | |
96 | }; | |
97 | ||
98 | struct keymap keymap[] = { | |
99 | { KEY_UP, REQ_PREV_LINE }, | |
100 | { 'k', REQ_PREV_LINE }, | |
101 | { KEY_DOWN, REQ_NEXT_LINE }, | |
102 | { 'j', REQ_NEXT_LINE }, | |
103 | { KEY_NPAGE, REQ_NEXT_PAGE }, | |
104 | { KEY_PPAGE, REQ_PREV_PAGE }, | |
105 | ||
106 | { 'd', REQ_DIFF }, | |
107 | { 'l', REQ_LOG }, | |
108 | { 'm', REQ_MAIN }, | |
109 | ||
110 | /* No input from wgetch() with nodelay() enabled. */ | |
111 | { ERR, REQ_UPDATE }, | |
112 | ||
113 | { KEY_ESC, REQ_QUIT }, | |
114 | { 'q', REQ_QUIT }, | |
115 | { 's', REQ_STOP }, | |
116 | { 'v', REQ_VERSION }, | |
117 | { 'r', REQ_REDRAW }, | |
118 | }; | |
119 | ||
120 | static int | |
121 | get_request(int request) | |
122 | { | |
123 | int i; | |
124 | ||
125 | for (i = 0; i < ARRAY_SIZE(keymap); i++) | |
126 | if (keymap[i].alias == request) | |
127 | return keymap[i].request; | |
128 | ||
129 | return request; | |
130 | } | |
131 | ||
132 | ||
133 | /* | |
134 | * Viewer | |
135 | */ | |
136 | ||
137 | struct view { | |
138 | char *name; | |
139 | char *cmd; | |
140 | ||
141 | /* Rendering */ | |
142 | int (*render)(struct view *, int); | |
143 | WINDOW *win; | |
144 | ||
145 | /* Navigation */ | |
146 | unsigned long offset; /* Offset of the window top */ | |
147 | unsigned long lineno; /* Current line number */ | |
148 | ||
149 | /* Buffering */ | |
150 | unsigned long lines; /* Total number of lines */ | |
151 | char **line; /* Line index */ | |
152 | ||
153 | /* Loading */ | |
154 | FILE *pipe; | |
155 | }; | |
156 | ||
157 | static int default_renderer(struct view *view, int lineno); | |
158 | ||
159 | #define DIFF_CMD \ | |
160 | "git log --stat -n1 %s ; echo; " \ | |
161 | "git diff --find-copies-harder -B -C %s^ %s" | |
162 | ||
163 | #define LOG_CMD \ | |
164 | "git log --stat -n100 %s" | |
165 | ||
166 | /* The status window at the bottom. Used for polling keystrokes. */ | |
167 | static WINDOW *status_win; | |
168 | ||
169 | static struct view views[] = { | |
170 | { "diff", DIFF_CMD, default_renderer }, | |
171 | { "log", LOG_CMD, default_renderer }, | |
172 | { "main", NULL }, | |
173 | }; | |
174 | ||
175 | static struct view *display[ARRAY_SIZE(views)]; | |
176 | static unsigned int current_view; | |
177 | static unsigned int nloading; | |
178 | ||
179 | #define foreach_view(view, i) \ | |
180 | for (i = 0; i < sizeof(display) && (view = display[i]); i++) | |
181 | ||
182 | static void | |
183 | redraw_view(struct view *view) | |
184 | { | |
185 | int lineno; | |
186 | int lines, cols; | |
187 | ||
188 | wclear(view->win); | |
189 | wmove(view->win, 0, 0); | |
190 | ||
191 | getmaxyx(view->win, lines, cols); | |
192 | ||
193 | for (lineno = 0; lineno < lines; lineno++) { | |
194 | view->render(view, lineno); | |
195 | } | |
196 | ||
197 | redrawwin(view->win); | |
198 | wrefresh(view->win); | |
199 | } | |
200 | ||
201 | /* FIXME: Fix percentage. */ | |
202 | static void | |
203 | report_position(struct view *view, int all) | |
204 | { | |
205 | report(all ? "line %d of %d (%d%%) viewing from %d" | |
206 | : "line %d of %d", | |
207 | view->lineno + 1, | |
208 | view->lines, | |
209 | view->lines ? view->offset * 100 / view->lines : 0, | |
210 | view->offset); | |
211 | } | |
212 | ||
213 | static void | |
214 | scroll_view(struct view *view, int request) | |
215 | { | |
216 | int x, y, lines = 1; | |
217 | enum { BACKWARD = -1, FORWARD = 1 } direction = FORWARD; | |
218 | ||
219 | getmaxyx(view->win, y, x); | |
220 | ||
221 | switch (request) { | |
222 | case REQ_NEXT_PAGE: | |
223 | lines = y; | |
224 | case REQ_NEXT_LINE: | |
225 | if (view->offset + lines > view->lines) | |
226 | lines = view->lines - view->offset - 1; | |
227 | ||
228 | if (lines == 0 || view->offset + y >= view->lines) { | |
229 | report("already at last line"); | |
230 | return; | |
231 | } | |
232 | break; | |
233 | ||
234 | case REQ_PREV_PAGE: | |
235 | lines = y; | |
236 | case REQ_PREV_LINE: | |
237 | if (lines > view->offset) | |
238 | lines = view->offset; | |
239 | ||
240 | if (lines == 0) { | |
241 | report("already at first line"); | |
242 | return; | |
243 | } | |
244 | ||
245 | direction = BACKWARD; | |
246 | break; | |
247 | ||
248 | default: | |
249 | lines = 0; | |
250 | } | |
251 | ||
252 | report("off=%d lines=%d lineno=%d move=%d", view->offset, view->lines, view->lineno, lines * direction); | |
253 | ||
254 | /* The rendering expects the new offset. */ | |
255 | view->offset += lines * direction; | |
256 | ||
257 | /* Move current line into the view. */ | |
258 | if (view->lineno < view->offset) | |
259 | view->lineno = view->offset; | |
260 | if (view->lineno > view->offset + y) | |
261 | view->lineno = view->offset + y; | |
262 | ||
263 | assert(0 <= view->offset && view->offset < view->lines); | |
264 | //assert(0 <= view->offset + lines && view->offset + lines < view->lines); | |
265 | assert(view->offset <= view->lineno && view->lineno <= view->lines); | |
266 | ||
267 | if (lines) { | |
268 | int from = direction == FORWARD ? y - lines : 0; | |
269 | int to = from + lines; | |
270 | ||
271 | wscrl(view->win, lines * direction); | |
272 | ||
273 | for (; from < to; from++) { | |
274 | if (!view->render(view, from)) | |
275 | break; | |
276 | } | |
277 | } | |
278 | ||
279 | redrawwin(view->win); | |
280 | wrefresh(view->win); | |
281 | ||
282 | report_position(view, lines); | |
283 | } | |
284 | ||
285 | static void | |
286 | resize_view(struct view *view) | |
287 | { | |
288 | int lines, cols; | |
289 | ||
290 | getmaxyx(stdscr, lines, cols); | |
291 | ||
292 | if (view->win) { | |
293 | mvwin(view->win, 0, 0); | |
294 | wresize(view->win, lines - 1, cols); | |
295 | ||
296 | } else { | |
297 | view->win = newwin(lines - 1, 0, 0, 0); | |
298 | if (!view->win) { | |
299 | report("Failed to create %s view", view->name); | |
300 | return; | |
301 | } | |
302 | scrollok(view->win, TRUE); | |
303 | } | |
304 | } | |
305 | ||
306 | ||
307 | static bool | |
308 | begin_update(struct view *view) | |
309 | { | |
310 | char buf[1024]; | |
311 | ||
312 | if (view->cmd) { | |
313 | if (snprintf(buf, sizeof(buf), view->cmd, "HEAD", "HEAD", "HEAD") < sizeof(buf)) | |
314 | view->pipe = popen(buf, "r"); | |
315 | ||
316 | if (!view->pipe) | |
317 | return FALSE; | |
318 | ||
319 | if (nloading++ == 0) | |
320 | nodelay(status_win, TRUE); | |
321 | } | |
322 | ||
323 | display[current_view] = view; | |
324 | ||
325 | view->offset = 0; | |
326 | view->lines = 0; | |
327 | view->lineno = 0; | |
328 | ||
329 | return TRUE; | |
330 | } | |
331 | ||
332 | static void | |
333 | end_update(struct view *view) | |
334 | { | |
335 | wattrset(view->win, A_NORMAL); | |
336 | pclose(view->pipe); | |
337 | view->pipe = NULL; | |
338 | ||
339 | if (nloading-- == 1) | |
340 | nodelay(status_win, FALSE); | |
341 | } | |
342 | ||
343 | static int | |
344 | update_view(struct view *view) | |
345 | { | |
346 | char buffer[BUFSIZ]; | |
347 | char *line; | |
348 | int lines, cols; | |
349 | char **tmp; | |
350 | int redraw; | |
351 | ||
352 | if (!view->pipe) | |
353 | return TRUE; | |
354 | ||
355 | getmaxyx(view->win, lines, cols); | |
356 | ||
357 | redraw = !view->line; | |
358 | ||
359 | tmp = realloc(view->line, sizeof(*view->line) * (view->lines + lines)); | |
360 | if (!tmp) | |
361 | goto alloc_error; | |
362 | ||
363 | view->line = tmp; | |
364 | ||
365 | while ((line = fgets(buffer, sizeof(buffer), view->pipe))) { | |
366 | int linelen; | |
367 | ||
368 | if (!lines--) | |
369 | break; | |
370 | ||
371 | linelen = strlen(line); | |
372 | if (linelen) | |
373 | line[linelen - 1] = 0; | |
374 | ||
375 | view->line[view->lines] = strdup(line); | |
376 | if (!view->line[view->lines]) | |
377 | goto alloc_error; | |
378 | view->lines++; | |
379 | } | |
380 | ||
381 | if (redraw) | |
382 | redraw_view(view); | |
383 | ||
384 | if (ferror(view->pipe)) { | |
385 | report("Failed to read %s", view->cmd); | |
386 | goto end; | |
387 | ||
388 | } else if (feof(view->pipe)) { | |
389 | report_position(view, 0); | |
390 | goto end; | |
391 | } | |
392 | ||
393 | return TRUE; | |
394 | ||
395 | alloc_error: | |
396 | report("Allocation failure"); | |
397 | ||
398 | end: | |
399 | end_update(view); | |
400 | return FALSE; | |
401 | } | |
402 | ||
403 | ||
404 | static struct view * | |
405 | switch_view(struct view *prev, int request) | |
406 | { | |
407 | struct view *view = &views[request - REQ_OFFSET]; | |
408 | struct view *displayed; | |
409 | int i; | |
410 | ||
411 | if (view == prev) { | |
412 | foreach_view (displayed, i) ; | |
413 | ||
414 | if (i == 1) | |
415 | report("Already in %s view", view->name); | |
416 | else | |
417 | report("FIXME: Maximize"); | |
418 | ||
419 | return view; | |
420 | ||
421 | } else { | |
422 | foreach_view (displayed, i) { | |
423 | if (view == displayed) { | |
424 | current_view = i; | |
425 | report("New current view"); | |
426 | return view; | |
427 | } | |
428 | } | |
429 | } | |
430 | ||
431 | if (!view->win) | |
432 | resize_view(view); | |
433 | ||
434 | /* Reload */ | |
435 | ||
436 | if (view->line) { | |
437 | for (i = 0; i < view->lines; i++) | |
438 | if (view->line[i]) | |
439 | free(view->line[i]); | |
440 | ||
441 | free(view->line); | |
442 | view->line = NULL; | |
443 | } | |
444 | ||
445 | if (prev && prev->pipe) | |
446 | end_update(prev); | |
447 | ||
448 | if (begin_update(view)) { | |
449 | if (!view->cmd) | |
450 | report("%s", HELP); | |
451 | else | |
452 | report("loading..."); | |
453 | } | |
454 | ||
455 | return view; | |
456 | } | |
457 | ||
458 | ||
459 | /* Process a keystroke */ | |
460 | static int | |
461 | view_driver(struct view *view, int key) | |
462 | { | |
463 | int request = get_request(key); | |
464 | int i; | |
465 | ||
466 | switch (request) { | |
467 | case REQ_NEXT_LINE: | |
468 | case REQ_NEXT_PAGE: | |
469 | case REQ_PREV_LINE: | |
470 | case REQ_PREV_PAGE: | |
471 | if (view) | |
472 | scroll_view(view, request); | |
473 | break; | |
474 | ||
475 | case REQ_MAIN: | |
476 | case REQ_LOG: | |
477 | case REQ_DIFF: | |
478 | view = switch_view(view, request); | |
479 | break; | |
480 | ||
481 | case REQ_REDRAW: | |
482 | redraw_view(view); | |
483 | break; | |
484 | ||
485 | case REQ_STOP: | |
486 | foreach_view (view, i) { | |
487 | if (view->pipe) { | |
488 | end_update(view); | |
489 | scroll_view(view, 0); | |
490 | } | |
491 | } | |
492 | break; | |
493 | ||
494 | case REQ_VERSION: | |
495 | report("version %s", VERSION); | |
496 | return TRUE; | |
497 | ||
498 | case REQ_UPDATE: | |
499 | doupdate(); | |
500 | return TRUE; | |
501 | ||
502 | case REQ_QUIT: | |
503 | return FALSE; | |
504 | ||
505 | default: | |
506 | report(HELP); | |
507 | return TRUE; | |
508 | } | |
509 | ||
510 | return TRUE; | |
511 | } | |
512 | ||
513 | ||
514 | /* | |
515 | * Rendering | |
516 | */ | |
517 | ||
518 | #define ATTR(line, attr) { (line), sizeof(line) - 1, (attr) } | |
519 | ||
520 | struct attr { | |
521 | char *line; | |
522 | int linelen; | |
523 | int attr; | |
524 | }; | |
525 | ||
526 | static struct attr attrs[] = { | |
527 | ATTR("commit ", COLOR_PAIR(COLOR_GREEN)), | |
528 | ATTR("Author: ", COLOR_PAIR(COLOR_CYAN)), | |
529 | ATTR("Date: ", COLOR_PAIR(COLOR_YELLOW)), | |
530 | ATTR("diff --git ", COLOR_PAIR(COLOR_YELLOW)), | |
531 | ATTR("diff-tree ", COLOR_PAIR(COLOR_BLUE)), | |
532 | ATTR("index ", COLOR_PAIR(COLOR_BLUE)), | |
533 | ATTR("-", COLOR_PAIR(COLOR_RED)), | |
534 | ATTR("+", COLOR_PAIR(COLOR_GREEN)), | |
535 | ATTR("@", COLOR_PAIR(COLOR_MAGENTA)), | |
536 | }; | |
537 | ||
538 | static int | |
539 | default_renderer(struct view *view, int lineno) | |
540 | { | |
541 | char *line; | |
542 | int linelen; | |
543 | int attr = A_NORMAL; | |
544 | int i; | |
545 | ||
546 | line = view->line[view->offset + lineno]; | |
547 | if (!line) return FALSE; | |
548 | ||
549 | linelen = strlen(line); | |
550 | ||
551 | for (i = 0; i < ARRAY_SIZE(attrs); i++) { | |
552 | if (linelen < attrs[i].linelen | |
553 | || strncmp(attrs[i].line, line, attrs[i].linelen)) | |
554 | continue; | |
555 | ||
556 | attr = attrs[i].attr; | |
557 | break; | |
558 | } | |
559 | ||
560 | wattrset(view->win, attr); | |
561 | mvwprintw(view->win, lineno, 0, "%4d: %s", view->offset + lineno, line); | |
562 | ||
563 | return TRUE; | |
564 | } | |
565 | ||
566 | /* | |
567 | * Main | |
568 | */ | |
569 | ||
570 | static void | |
571 | quit(int sig) | |
572 | { | |
573 | if (status_win) | |
574 | delwin(status_win); | |
575 | endwin(); | |
576 | ||
577 | /* FIXME: Shutdown gracefully. */ | |
578 | ||
579 | exit(0); | |
580 | } | |
581 | ||
582 | static void die(const char *err, ...) | |
583 | { | |
584 | va_list args; | |
585 | ||
586 | endwin(); | |
587 | ||
588 | va_start(args, err); | |
589 | fputs("tig: ", stderr); | |
590 | vfprintf(stderr, err, args); | |
591 | fputs("\n", stderr); | |
592 | va_end(args); | |
593 | ||
594 | exit(1); | |
595 | } | |
596 | ||
597 | static void | |
598 | report(const char *msg, ...) | |
599 | { | |
600 | va_list args; | |
601 | ||
602 | va_start(args, msg); | |
603 | ||
604 | werase(status_win); | |
605 | wmove(status_win, 0, 0); | |
606 | ||
607 | if (display[current_view]) | |
608 | wprintw(status_win, "%4s: ", display[current_view]->name); | |
609 | ||
610 | vwprintw(status_win, msg, args); | |
611 | wrefresh(status_win); | |
612 | ||
613 | va_end(args); | |
614 | } | |
615 | ||
616 | static void | |
617 | init_colors(void) | |
618 | { | |
619 | int bg = COLOR_BLACK; | |
620 | ||
621 | start_color(); | |
622 | ||
623 | if (use_default_colors() != ERR) | |
624 | bg = -1; | |
625 | ||
626 | init_pair(COLOR_BLACK, COLOR_BLACK, bg); | |
627 | init_pair(COLOR_GREEN, COLOR_GREEN, bg); | |
628 | init_pair(COLOR_RED, COLOR_RED, bg); | |
629 | init_pair(COLOR_CYAN, COLOR_CYAN, bg); | |
630 | init_pair(COLOR_WHITE, COLOR_WHITE, bg); | |
631 | init_pair(COLOR_MAGENTA, COLOR_MAGENTA, bg); | |
632 | init_pair(COLOR_BLUE, COLOR_BLUE, bg); | |
633 | init_pair(COLOR_YELLOW, COLOR_YELLOW, bg); | |
634 | } | |
635 | ||
636 | int | |
637 | main(int argc, char *argv[]) | |
638 | { | |
639 | int request = REQ_MAIN; | |
640 | int x, y; | |
641 | ||
642 | signal(SIGINT, quit); | |
643 | ||
644 | initscr(); /* initialize the curses library */ | |
645 | nonl(); /* tell curses not to do NL->CR/NL on output */ | |
646 | cbreak(); /* take input chars one at a time, no wait for \n */ | |
647 | noecho(); /* don't echo input */ | |
648 | leaveok(stdscr, TRUE); | |
649 | /* curs_set(0); */ | |
650 | ||
651 | if (has_colors()) | |
652 | init_colors(); | |
653 | ||
654 | getmaxyx(stdscr, y, x); | |
655 | status_win = newwin(1, 0, y - 1, 0); | |
656 | if (!status_win) | |
657 | die("Failed to create status window"); | |
658 | ||
659 | /* Enable keyboard mapping */ | |
660 | keypad(status_win, TRUE); | |
661 | wattrset(status_win, COLOR_PAIR(COLOR_GREEN)); | |
662 | ||
663 | while (view_driver(display[current_view], request)) { | |
664 | struct view *view; | |
665 | int i; | |
666 | ||
667 | foreach_view (view, i) { | |
668 | if (view->pipe) { | |
669 | update_view(view); | |
670 | } | |
671 | } | |
672 | ||
673 | /* Refresh, accept single keystroke of input */ | |
674 | request = wgetch(status_win); | |
675 | if (request == KEY_RESIZE) { | |
676 | int lines, cols; | |
677 | ||
678 | getmaxyx(stdscr, lines, cols); | |
679 | mvwin(status_win, lines - 1, 0); | |
680 | wresize(status_win, 1, cols - 1); | |
681 | } | |
682 | } | |
683 | ||
684 | quit(0); | |
685 | ||
686 | return 0; | |
687 | } | |
688 | ||
689 | /** | |
690 | * COPYRIGHT | |
691 | * --------- | |
692 | * Copyright (c) Jonas Fonseca, 2006 | |
693 | * | |
694 | * This program is free software; you can redistribute it and/or modify | |
695 | * it under the terms of the GNU General Public License as published by | |
696 | * the Free Software Foundation; either version 2 of the License, or | |
697 | * (at your option) any later version. | |
698 | * | |
699 | * SEE ALSO | |
700 | * -------- | |
701 | * link:http://www.kernel.org/pub/software/scm/git/docs/[git(7)], | |
702 | * link:http://www.kernel.org/pub/software/scm/cogito/docs/[cogito(7)] | |
703 | **/ |