Add support for git log / git diff options
[tig] / tig.c
1 /* Copyright (c) 2006 Jonas Fonseca <fonseca@diku.dk>
2  * See license info at the bottom. */
3 /**
4  * TIG(1)
5  * ======
6  *
7  * NAME
8  * ----
9  * tig - text-mode interface for git
10  *
11  * SYNOPSIS
12  * --------
13  * [verse]
14  * tig [options]
15  * tig [options] [--] [git log options]
16  * tig [options] log  [git log options]
17  * tig [options] diff [git diff options]
18  * tig [options] <    [git log or git diff output]
19  *
20  * DESCRIPTION
21  * -----------
22  * Browse changes in a git repository.
23  **/
24
25 #ifndef DEBUG
26 #define NDEBUG
27 #endif
28
29 #ifndef VERSION
30 #define VERSION "tig-0.1"
31 #endif
32
33 #include <assert.h>
34 #include <errno.h>
35 #include <ctype.h>
36 #include <signal.h>
37 #include <stdarg.h>
38 #include <stdio.h>
39 #include <stdlib.h>
40 #include <string.h>
41 #include <time.h>
42
43 #include <curses.h>
44
45 static void die(const char *err, ...);
46 static void report(const char *msg, ...);
47 static void set_nonblocking_input(int boolean);
48
49 #define ABS(x)          ((x) >= 0  ? (x) : -(x))
50 #define MIN(x, y)       ((x) < (y) ? (x) :  (y))
51
52 #define ARRAY_SIZE(x)   (sizeof(x) / sizeof(x[0]))
53 #define STRING_SIZE(x)  (sizeof(x) - 1)
54
55 #define SIZEOF_REF      256     /* Size of symbolic or SHA1 ID. */
56 #define SIZEOF_CMD      1024    /* Size of command buffer. */
57
58 /* This color name can be used to refer to the default term colors. */
59 #define COLOR_DEFAULT   (-1)
60
61 /* The format and size of the date column in the main view. */
62 #define DATE_FORMAT     "%Y-%m-%d %H:%M"
63 #define DATE_COLS       STRING_SIZE("2006-04-29 14:21 ")
64
65 /* The default interval between line numbers. */
66 #define NUMBER_INTERVAL 1
67
68 #define SCALE_SPLIT_VIEW(height)        ((height) * 2 / 3)
69
70 /* Some ascii-shorthands that fit into the ncurses namespace. */
71 #define KEY_TAB         '\t'
72 #define KEY_RETURN      '\r'
73 #define KEY_ESC         27
74
75 /* User action requests. */
76 enum request {
77         /* Offset all requests to avoid conflicts with ncurses getch values. */
78         REQ_OFFSET = KEY_MAX + 1,
79
80         /* XXX: Keep the view request first and in sync with views[]. */
81         REQ_VIEW_MAIN,
82         REQ_VIEW_DIFF,
83         REQ_VIEW_LOG,
84         REQ_VIEW_HELP,
85
86         REQ_ENTER,
87         REQ_QUIT,
88         REQ_PROMPT,
89         REQ_SCREEN_REDRAW,
90         REQ_SCREEN_UPDATE,
91         REQ_SHOW_VERSION,
92         REQ_STOP_LOADING,
93         REQ_TOGGLE_LINE_NUMBERS,
94         REQ_VIEW_NEXT,
95
96         REQ_MOVE_UP,
97         REQ_MOVE_DOWN,
98         REQ_MOVE_PAGE_UP,
99         REQ_MOVE_PAGE_DOWN,
100         REQ_MOVE_FIRST_LINE,
101         REQ_MOVE_LAST_LINE,
102
103         REQ_SCROLL_LINE_UP,
104         REQ_SCROLL_LINE_DOWN,
105         REQ_SCROLL_PAGE_UP,
106         REQ_SCROLL_PAGE_DOWN,
107 };
108
109 struct commit {
110         char id[41];            /* SHA1 ID. */
111         char title[75];         /* The first line of the commit message. */
112         char author[75];        /* The author of the commit. */
113         struct tm time;         /* Date from the author ident. */
114 };
115
116 /*
117  * String helpers
118  */
119
120 static inline void
121 string_ncopy(char *dst, char *src, int dstlen)
122 {
123         strncpy(dst, src, dstlen - 1);
124         dst[dstlen - 1] = 0;
125
126 }
127
128 /* Shorthand for safely copying into a fixed buffer. */
129 #define string_copy(dst, src) \
130         string_ncopy(dst, src, sizeof(dst))
131
132 /* Shell quoting
133  *
134  * NOTE: The following is a slightly modified copy of the git project's shell
135  * quoting routines found in the quote.c file.
136  *
137  * Help to copy the thing properly quoted for the shell safety.  any single
138  * quote is replaced with '\'', any exclamation point is replaced with '\!',
139  * and the whole thing is enclosed in a
140  *
141  * E.g.
142  *  original     sq_quote     result
143  *  name     ==> name      ==> 'name'
144  *  a b      ==> a b       ==> 'a b'
145  *  a'b      ==> a'\''b    ==> 'a'\''b'
146  *  a!b      ==> a'\!'b    ==> 'a'\!'b'
147  */
148
149 static size_t
150 sq_quote(char buf[SIZEOF_CMD], size_t bufsize, const char *src)
151 {
152         char c;
153
154 #define BUFPUT(x) ( (bufsize < SIZEOF_CMD) && (buf[bufsize++] = (x)) )
155
156         BUFPUT('\'');
157         while ((c = *src++)) {
158                 if (c == '\'' || c == '!') {
159                         BUFPUT('\'');
160                         BUFPUT('\\');
161                         BUFPUT(c);
162                         BUFPUT('\'');
163                 } else {
164                         BUFPUT(c);
165                 }
166         }
167         BUFPUT('\'');
168
169         return bufsize;
170 }
171
172
173 /**
174  * OPTIONS
175  * -------
176  **/
177
178 static int opt_line_number      = FALSE;
179 static int opt_num_interval     = NUMBER_INTERVAL;
180 static enum request opt_request = REQ_VIEW_MAIN;
181 static char opt_cmd[SIZEOF_CMD] = "";
182
183 char ref_head[SIZEOF_REF]       = "HEAD";
184 char ref_commit[SIZEOF_REF]     = "HEAD";
185
186 /* Returns the index of log or diff command or -1 to exit. */
187 static int
188 parse_options(int argc, char *argv[])
189 {
190         int i;
191
192         for (i = 1; i < argc; i++) {
193                 char *opt = argv[i];
194
195                 /**
196                  * log [options]::
197                  *      git log options.
198                  *
199                  * diff [options]::
200                  *      git diff options.
201                  **/
202                 if (!strcmp(opt, "log") ||
203                     !strcmp(opt, "diff")) {
204                         opt_request = opt[0] == 'l'
205                                     ? REQ_VIEW_LOG : REQ_VIEW_DIFF;
206                         return i;
207                 }
208
209                 /**
210                  * -l::
211                  *      Start up in log view.
212                  **/
213                 if (!strcmp(opt, "-l")) {
214                         opt_request = REQ_VIEW_LOG;
215                         continue;
216                 }
217
218                 /**
219                  * -d::
220                  *      Start up in diff view.
221                  **/
222                 if (!strcmp(opt, "-d")) {
223                         opt_request = REQ_VIEW_DIFF;
224                         continue;
225                 }
226
227                 /**
228                  * -n[INTERVAL], --line-number[=INTERVAL]::
229                  *      Prefix line numbers in log and diff view.
230                  *      Optionally, with interval different than each line.
231                  **/
232                 if (!strncmp(opt, "-n", 2) ||
233                            !strncmp(opt, "--line-number", 13)) {
234                         char *num = opt;
235
236                         if (opt[1] == 'n') {
237                                 num = opt + 2;
238
239                         } else if (opt[STRING_SIZE("--line-number")] == '=') {
240                                 num = opt + STRING_SIZE("--line-number=");
241                         }
242
243                         if (isdigit(*num))
244                                 opt_num_interval = atoi(num);
245
246                         opt_line_number = TRUE;
247                         continue;
248                 }
249
250                 /**
251                  * -v, --version::
252                  *      Show version and exit.
253                  **/
254                 if (!strcmp(opt, "-v") ||
255                            !strcmp(opt, "--version")) {
256                         printf("tig version %s\n", VERSION);
257                         return -1;
258                 }
259
260                 /**
261                  * \--::
262                  *      End of tig options. Useful when specifying commands
263                  *      for the main view. Example:
264                  *
265                  *              $ tig -- --pretty=raw tag-1.0..HEAD
266                  **/
267                 if (!strcmp(opt, "--"))
268                         return i + 1;
269
270                  /* Make stuff like:
271                   *
272                  *      $ tig tag-1.0..HEAD
273                  *
274                  * work.
275                  */
276                 if (opt[0] && opt[0] != '-')
277                         return i;
278
279                 die("unknown command '%s'", opt);
280         }
281
282         return i;
283 }
284
285
286 /**
287  * KEYS
288  * ----
289  **/
290
291 #define HELP "(d)iff, (l)og, (m)ain, (q)uit, (v)ersion, (h)elp"
292
293 struct keymap {
294         int alias;
295         int request;
296 };
297
298 struct keymap keymap[] = {
299         /**
300          * View switching
301          * ~~~~~~~~~~~~~~
302          * d::
303          *      Switch to diff view.
304          * l::
305          *      Switch to log view.
306          * m::
307          *      Switch to main view.
308          * h::
309          *      Show man page.
310          * Return::
311          *      If in main view split the view
312          *      and show the diff in the bottom view.
313          * Tab::
314          *      Switch to next view.
315          **/
316         { 'm',          REQ_VIEW_MAIN },
317         { 'd',          REQ_VIEW_DIFF },
318         { 'l',          REQ_VIEW_LOG },
319         { 'h',          REQ_VIEW_HELP },
320
321         { KEY_TAB,      REQ_VIEW_NEXT },
322         { KEY_RETURN,   REQ_ENTER },
323
324         /**
325          * Cursor navigation
326          * ~~~~~~~~~~~~~~~~~
327          * Up, k::
328          *      Move curser one line up.
329          * Down, j::
330          *      Move cursor one line down.
331          * Page Up::
332          *      Move curser one page up.
333          * Page Down::
334          *      Move cursor one page down.
335          * Home::
336          *      Jump to first line.
337          * End::
338          *      Jump to last line.
339          **/
340         { KEY_UP,       REQ_MOVE_UP },
341         { 'k',          REQ_MOVE_UP },
342         { KEY_DOWN,     REQ_MOVE_DOWN },
343         { 'j',          REQ_MOVE_DOWN },
344         { KEY_HOME,     REQ_MOVE_FIRST_LINE },
345         { KEY_END,      REQ_MOVE_LAST_LINE },
346         { KEY_NPAGE,    REQ_MOVE_PAGE_DOWN },
347         { KEY_PPAGE,    REQ_MOVE_PAGE_UP },
348
349         /**
350          * Scrolling
351          * ~~~~~~~~~
352          * Insert::
353          *      Scroll view one line up.
354          * Delete::
355          *      Scroll view one line down.
356          * w::
357          *      Scroll view one page up.
358          * s::
359          *      Scroll view one page down.
360          **/
361         { KEY_IC,       REQ_SCROLL_LINE_UP },
362         { KEY_DC,       REQ_SCROLL_LINE_DOWN },
363         { 'w',          REQ_SCROLL_PAGE_UP },
364         { 's',          REQ_SCROLL_PAGE_DOWN },
365
366         /**
367          * Misc
368          * ~~~~
369          * q, Escape::
370          *      Quit
371          * r::
372          *      Redraw screen.
373          * z::
374          *      Stop all background loading.
375          * v::
376          *      Show version.
377          * n::
378          *      Toggle line numbers on/off.
379          * ':'::
380          *      Open prompt.
381          **/
382         { KEY_ESC,      REQ_QUIT },
383         { 'q',          REQ_QUIT },
384         { 'z',          REQ_STOP_LOADING },
385         { 'v',          REQ_SHOW_VERSION },
386         { 'r',          REQ_SCREEN_REDRAW },
387         { 'n',          REQ_TOGGLE_LINE_NUMBERS },
388         { ':',          REQ_PROMPT },
389
390         /* wgetch() with nodelay() enabled returns ERR when there's no input. */
391         { ERR,          REQ_SCREEN_UPDATE },
392 };
393
394 static enum request
395 get_request(int key)
396 {
397         int i;
398
399         for (i = 0; i < ARRAY_SIZE(keymap); i++)
400                 if (keymap[i].alias == key)
401                         return keymap[i].request;
402
403         return (enum request) key;
404 }
405
406
407 /*
408  * Line-oriented content detection.
409  */
410
411 #define LINE_INFO \
412 /*   Line type     String to match      Foreground      Background      Attributes
413  *   ---------     ---------------      ----------      ----------      ---------- */ \
414 /* Diff markup */ \
415 LINE(DIFF,         "diff --git ",       COLOR_YELLOW,   COLOR_DEFAULT,  0), \
416 LINE(INDEX,        "index ",            COLOR_BLUE,     COLOR_DEFAULT,  0), \
417 LINE(DIFF_CHUNK,   "@@",                COLOR_MAGENTA,  COLOR_DEFAULT,  0), \
418 LINE(DIFF_ADD,     "+",                 COLOR_GREEN,    COLOR_DEFAULT,  0), \
419 LINE(DIFF_DEL,     "-",                 COLOR_RED,      COLOR_DEFAULT,  0), \
420 LINE(DIFF_OLDMODE, "old mode ",         COLOR_YELLOW,   COLOR_DEFAULT,  0), \
421 LINE(DIFF_NEWMODE, "new mode ",         COLOR_YELLOW,   COLOR_DEFAULT,  0), \
422 LINE(DIFF_COPY,    "copy ",             COLOR_YELLOW,   COLOR_DEFAULT,  0), \
423 LINE(DIFF_RENAME,  "rename ",           COLOR_YELLOW,   COLOR_DEFAULT,  0), \
424 LINE(DIFF_SIM,     "similarity ",       COLOR_YELLOW,   COLOR_DEFAULT,  0), \
425 LINE(DIFF_DISSIM,  "dissimilarity ",    COLOR_YELLOW,   COLOR_DEFAULT,  0), \
426 /* Pretty print commit header */ \
427 LINE(AUTHOR,       "Author: ",          COLOR_CYAN,     COLOR_DEFAULT,  0), \
428 LINE(MERGE,        "Merge: ",           COLOR_BLUE,     COLOR_DEFAULT,  0), \
429 LINE(DATE,         "Date:   ",          COLOR_YELLOW,   COLOR_DEFAULT,  0), \
430 /* Raw commit header */ \
431 LINE(COMMIT,       "commit ",           COLOR_GREEN,    COLOR_DEFAULT,  0), \
432 LINE(PARENT,       "parent ",           COLOR_BLUE,     COLOR_DEFAULT,  0), \
433 LINE(TREE,         "tree ",             COLOR_BLUE,     COLOR_DEFAULT,  0), \
434 LINE(AUTHOR_IDENT, "author ",           COLOR_CYAN,     COLOR_DEFAULT,  0), \
435 LINE(COMMITTER,    "committer ",        COLOR_MAGENTA,  COLOR_DEFAULT,  0), \
436 /* Misc */ \
437 LINE(DIFF_TREE,    "diff-tree ",        COLOR_BLUE,     COLOR_DEFAULT,  0), \
438 LINE(SIGNOFF,      "    Signed-off-by", COLOR_YELLOW,   COLOR_DEFAULT,  0), \
439 /* UI colors */ \
440 LINE(DEFAULT,      "",                  COLOR_DEFAULT,  COLOR_DEFAULT,  A_NORMAL), \
441 LINE(CURSOR,       "",                  COLOR_WHITE,    COLOR_GREEN,    A_BOLD), \
442 LINE(STATUS,       "",                  COLOR_GREEN,    COLOR_DEFAULT,  0), \
443 LINE(TITLE_BLUR,   "",                  COLOR_WHITE,    COLOR_BLUE,     0), \
444 LINE(TITLE_FOCUS,  "",                  COLOR_WHITE,    COLOR_BLUE,     A_BOLD), \
445 LINE(MAIN_DATE,    "",                  COLOR_BLUE,     COLOR_DEFAULT,  0), \
446 LINE(MAIN_AUTHOR,  "",                  COLOR_GREEN,    COLOR_DEFAULT,  0), \
447 LINE(MAIN_COMMIT,  "",                  COLOR_DEFAULT,  COLOR_DEFAULT,  0), \
448 LINE(MAIN_DELIM,   "",                  COLOR_MAGENTA,  COLOR_DEFAULT,  0),
449
450 enum line_type {
451 #define LINE(type, line, fg, bg, attr) \
452         LINE_##type
453         LINE_INFO
454 #undef  LINE
455 };
456
457 struct line_info {
458         char *line;             /* The start of line to match. */
459         int linelen;            /* Size of string to match. */
460         int fg, bg, attr;       /* Color and text attributes for the lines. */
461 };
462
463 static struct line_info line_info[] = {
464 #define LINE(type, line, fg, bg, attr) \
465         { (line), STRING_SIZE(line), (fg), (bg), (attr) }
466         LINE_INFO
467 #undef  LINE
468 };
469
470 static enum line_type
471 get_line_type(char *line)
472 {
473         int linelen = strlen(line);
474         enum line_type type;
475
476         for (type = 0; type < ARRAY_SIZE(line_info); type++)
477                 /* Case insensitive search matches Signed-off-by lines better. */
478                 if (linelen >= line_info[type].linelen &&
479                     !strncasecmp(line_info[type].line, line, line_info[type].linelen))
480                         return type;
481
482         return LINE_DEFAULT;
483 }
484
485 static inline int
486 get_line_attr(enum line_type type)
487 {
488         assert(type < ARRAY_SIZE(line_info));
489         return COLOR_PAIR(type) | line_info[type].attr;
490 }
491
492 static void
493 init_colors(void)
494 {
495         int default_bg = COLOR_BLACK;
496         int default_fg = COLOR_WHITE;
497         enum line_type type;
498
499         start_color();
500
501         if (use_default_colors() != ERR) {
502                 default_bg = -1;
503                 default_fg = -1;
504         }
505
506         for (type = 0; type < ARRAY_SIZE(line_info); type++) {
507                 struct line_info *info = &line_info[type];
508                 int bg = info->bg == COLOR_DEFAULT ? default_bg : info->bg;
509                 int fg = info->fg == COLOR_DEFAULT ? default_fg : info->fg;
510
511                 init_pair(type, fg, bg);
512         }
513 }
514
515
516 /*
517  * Viewer
518  */
519
520 struct view {
521         const char *name;       /* View name */
522         const char *defcmd;     /* Default command line */
523         char *id;               /* Points to either of ref_{head,commit} */
524         size_t objsize;         /* Size of objects in the line index */
525
526         struct view_ops {
527                 bool (*draw)(struct view *view, unsigned int lineno);
528                 bool (*read)(struct view *view, char *line);
529                 bool (*enter)(struct view *view);
530         } *ops;
531
532         char cmd[SIZEOF_CMD];   /* Command buffer */
533         char ref[SIZEOF_REF];   /* Hovered Commit reference */
534         /* The view reference that describes the content of this view. */
535         char vref[SIZEOF_REF];
536
537         WINDOW *win;
538         WINDOW *title;
539         int height, width;
540
541         /* Navigation */
542         unsigned long offset;   /* Offset of the window top */
543         unsigned long lineno;   /* Current line number */
544
545         /* Buffering */
546         unsigned long lines;    /* Total number of lines */
547         void **line;            /* Line index */
548
549         /* Loading */
550         FILE *pipe;
551         time_t start_time;
552 };
553
554 static struct view_ops pager_ops;
555 static struct view_ops main_ops;
556
557 #define DIFF_CMD \
558         "git log --stat -n1 %s ; echo; " \
559         "git diff --find-copies-harder -B -C %s^ %s"
560
561 #define LOG_CMD \
562         "git log --cc --stat -n100 %s"
563
564 #define MAIN_CMD \
565         "git log --stat --pretty=raw %s"
566
567 #define HELP_CMD \
568         "man tig 2> /dev/null"
569
570 static struct view views[] = {
571         { "main",  MAIN_CMD,   ref_head,    sizeof(struct commit), &main_ops },
572         { "diff",  DIFF_CMD,   ref_commit,  sizeof(char),          &pager_ops },
573         { "log",   LOG_CMD,    ref_head,    sizeof(char),          &pager_ops },
574         { "help",  HELP_CMD,   ref_head,    sizeof(char),          &pager_ops },
575 };
576
577 #define VIEW(req) (&views[(req) - REQ_OFFSET - 1])
578
579 /* The display array of active views and the index of the current view. */
580 static struct view *display[2];
581 static unsigned int current_view;
582
583 #define foreach_view(view, i) \
584         for (i = 0; i < ARRAY_SIZE(display) && (view = display[i]); i++)
585
586
587 static void
588 redraw_view_from(struct view *view, int lineno)
589 {
590         assert(0 <= lineno && lineno < view->height);
591
592         for (; lineno < view->height; lineno++) {
593                 if (!view->ops->draw(view, lineno))
594                         break;
595         }
596
597         redrawwin(view->win);
598         wrefresh(view->win);
599 }
600
601 static void
602 redraw_view(struct view *view)
603 {
604         wclear(view->win);
605         redraw_view_from(view, 0);
606 }
607
608 static void
609 resize_display(void)
610 {
611         int offset, i;
612         struct view *base = display[0];
613         struct view *view = display[1] ? display[1] : display[0];
614
615         /* Setup window dimensions */
616
617         getmaxyx(stdscr, base->height, base->width);
618
619         /* Make room for the status window. */
620         base->height -= 1;
621
622         if (view != base) {
623                 /* Horizontal split. */
624                 view->width   = base->width;
625                 view->height  = SCALE_SPLIT_VIEW(base->height);
626                 base->height -= view->height;
627
628                 /* Make room for the title bar. */
629                 view->height -= 1;
630         }
631
632         /* Make room for the title bar. */
633         base->height -= 1;
634
635         offset = 0;
636
637         foreach_view (view, i) {
638                 if (!view->win) {
639                         view->win = newwin(view->height, 0, offset, 0);
640                         if (!view->win)
641                                 die("Failed to create %s view", view->name);
642
643                         scrollok(view->win, TRUE);
644
645                         view->title = newwin(1, 0, offset + view->height, 0);
646                         if (!view->title)
647                                 die("Failed to create title window");
648
649                 } else {
650                         wresize(view->win, view->height, view->width);
651                         mvwin(view->win,   offset, 0);
652                         mvwin(view->title, offset + view->height, 0);
653                         wrefresh(view->win);
654                 }
655
656                 offset += view->height + 1;
657         }
658 }
659
660 static void
661 update_view_title(struct view *view)
662 {
663         if (view == display[current_view])
664                 wbkgdset(view->title, get_line_attr(LINE_TITLE_FOCUS));
665         else
666                 wbkgdset(view->title, get_line_attr(LINE_TITLE_BLUR));
667
668         werase(view->title);
669         wmove(view->title, 0, 0);
670
671         /* [main] ref: 334b506... - commit 6 of 4383 (0%) */
672         wprintw(view->title, "[%s] ref: %s", view->name, view->ref);
673         if (view->lines) {
674                 char *type = view == VIEW(REQ_VIEW_MAIN) ? "commit" : "line";
675
676                 wprintw(view->title, " - %s %d of %d (%d%%)",
677                         type,
678                         view->lineno + 1,
679                         view->lines,
680                         (view->lineno + 1) * 100 / view->lines);
681         }
682
683         wrefresh(view->title);
684 }
685
686 /*
687  * Navigation
688  */
689
690 /* Scrolling backend */
691 static void
692 do_scroll_view(struct view *view, int lines)
693 {
694         /* The rendering expects the new offset. */
695         view->offset += lines;
696
697         assert(0 <= view->offset && view->offset < view->lines);
698         assert(lines);
699
700         /* Redraw the whole screen if scrolling is pointless. */
701         if (view->height < ABS(lines)) {
702                 redraw_view(view);
703
704         } else {
705                 int line = lines > 0 ? view->height - lines : 0;
706                 int end = line + ABS(lines);
707
708                 wscrl(view->win, lines);
709
710                 for (; line < end; line++) {
711                         if (!view->ops->draw(view, line))
712                                 break;
713                 }
714         }
715
716         /* Move current line into the view. */
717         if (view->lineno < view->offset) {
718                 view->lineno = view->offset;
719                 view->ops->draw(view, 0);
720
721         } else if (view->lineno >= view->offset + view->height) {
722                 view->lineno = view->offset + view->height - 1;
723                 view->ops->draw(view, view->lineno - view->offset);
724         }
725
726         assert(view->offset <= view->lineno && view->lineno < view->lines);
727
728         redrawwin(view->win);
729         wrefresh(view->win);
730         report("");
731 }
732
733 /* Scroll frontend */
734 static void
735 scroll_view(struct view *view, enum request request)
736 {
737         int lines = 1;
738
739         switch (request) {
740         case REQ_SCROLL_PAGE_DOWN:
741                 lines = view->height;
742         case REQ_SCROLL_LINE_DOWN:
743                 if (view->offset + lines > view->lines)
744                         lines = view->lines - view->offset;
745
746                 if (lines == 0 || view->offset + view->height >= view->lines) {
747                         report("Already on last line");
748                         return;
749                 }
750                 break;
751
752         case REQ_SCROLL_PAGE_UP:
753                 lines = view->height;
754         case REQ_SCROLL_LINE_UP:
755                 if (lines > view->offset)
756                         lines = view->offset;
757
758                 if (lines == 0) {
759                         report("Already on first line");
760                         return;
761                 }
762
763                 lines = -lines;
764                 break;
765
766         default:
767                 die("request %d not handled in switch", request);
768         }
769
770         do_scroll_view(view, lines);
771 }
772
773 /* Cursor moving */
774 static void
775 move_view(struct view *view, enum request request)
776 {
777         int steps;
778
779         switch (request) {
780         case REQ_MOVE_FIRST_LINE:
781                 steps = -view->lineno;
782                 break;
783
784         case REQ_MOVE_LAST_LINE:
785                 steps = view->lines - view->lineno - 1;
786                 break;
787
788         case REQ_MOVE_PAGE_UP:
789                 steps = view->height > view->lineno
790                       ? -view->lineno : -view->height;
791                 break;
792
793         case REQ_MOVE_PAGE_DOWN:
794                 steps = view->lineno + view->height >= view->lines
795                       ? view->lines - view->lineno - 1 : view->height;
796                 break;
797
798         case REQ_MOVE_UP:
799                 steps = -1;
800                 break;
801
802         case REQ_MOVE_DOWN:
803                 steps = 1;
804                 break;
805
806         default:
807                 die("request %d not handled in switch", request);
808         }
809
810         if (steps <= 0 && view->lineno == 0) {
811                 report("Already on first line");
812                 return;
813
814         } else if (steps >= 0 && view->lineno + 1 == view->lines) {
815                 report("Already on last line");
816                 return;
817         }
818
819         /* Move the current line */
820         view->lineno += steps;
821         assert(0 <= view->lineno && view->lineno < view->lines);
822
823         /* Repaint the old "current" line if we be scrolling */
824         if (ABS(steps) < view->height) {
825                 int prev_lineno = view->lineno - steps - view->offset;
826
827                 wmove(view->win, prev_lineno, 0);
828                 wclrtoeol(view->win);
829                 view->ops->draw(view, prev_lineno);
830         }
831
832         /* Check whether the view needs to be scrolled */
833         if (view->lineno < view->offset ||
834             view->lineno >= view->offset + view->height) {
835                 if (steps < 0 && -steps > view->offset) {
836                         steps = -view->offset;
837
838                 } else if (steps > 0) {
839                         if (view->lineno == view->lines - 1 &&
840                             view->lines > view->height) {
841                                 steps = view->lines - view->offset - 1;
842                                 if (steps >= view->height)
843                                         steps -= view->height - 1;
844                         }
845                 }
846
847                 do_scroll_view(view, steps);
848                 return;
849         }
850
851         /* Draw the current line */
852         view->ops->draw(view, view->lineno - view->offset);
853
854         redrawwin(view->win);
855         wrefresh(view->win);
856         report("");
857 }
858
859
860 /*
861  * Incremental updating
862  */
863
864 static bool
865 begin_update(struct view *view)
866 {
867         char *id = view->id;
868
869         if (opt_cmd[0]) {
870                 string_copy(view->cmd, opt_cmd);
871                 opt_cmd[0] = 0;
872         } else {
873                 if (snprintf(view->cmd, sizeof(view->cmd), view->defcmd,
874                              id, id, id) >= sizeof(view->cmd))
875                         return FALSE;
876         }
877
878         view->pipe = popen(view->cmd, "r");
879         if (!view->pipe)
880                 return FALSE;
881
882         set_nonblocking_input(TRUE);
883
884         view->offset = 0;
885         view->lines  = 0;
886         view->lineno = 0;
887
888         if (view->line) {
889                 int i;
890
891                 for (i = 0; i < view->lines; i++)
892                         if (view->line[i])
893                                 free(view->line[i]);
894
895                 free(view->line);
896                 view->line = NULL;
897         }
898
899         view->start_time = time(NULL);
900
901         return TRUE;
902 }
903
904 static void
905 end_update(struct view *view)
906 {
907         if (!view->pipe)
908                 return;
909         set_nonblocking_input(FALSE);
910         pclose(view->pipe);
911         view->pipe = NULL;
912 }
913
914 static bool
915 update_view(struct view *view)
916 {
917         char buffer[BUFSIZ];
918         char *line;
919         void **tmp;
920         /* The number of lines to read. If too low it will cause too much
921          * redrawing (and possible flickering), if too high responsiveness
922          * will suffer. */
923         int lines = view->height;
924         int redraw_from = -1;
925
926         if (!view->pipe)
927                 return TRUE;
928
929         /* Only redraw if lines are visible. */
930         if (view->offset + view->height >= view->lines)
931                 redraw_from = view->lines - view->offset;
932
933         tmp = realloc(view->line, sizeof(*view->line) * (view->lines + lines));
934         if (!tmp)
935                 goto alloc_error;
936
937         view->line = tmp;
938
939         while ((line = fgets(buffer, sizeof(buffer), view->pipe))) {
940                 int linelen;
941
942                 linelen = strlen(line);
943                 if (linelen)
944                         line[linelen - 1] = 0;
945
946                 if (!view->ops->read(view, line))
947                         goto alloc_error;
948
949                 if (lines-- == 1)
950                         break;
951         }
952
953         /* CPU hogilicious! */
954         update_view_title(view);
955
956         if (redraw_from >= 0) {
957                 /* If this is an incremental update, redraw the previous line
958                  * since for commits some members could have changed when
959                  * loading the main view. */
960                 if (redraw_from > 0)
961                         redraw_from--;
962
963                 /* Incrementally draw avoids flickering. */
964                 redraw_view_from(view, redraw_from);
965         }
966
967         if (ferror(view->pipe)) {
968                 report("Failed to read: %s", strerror(errno));
969                 goto end;
970
971         } else if (feof(view->pipe)) {
972                 time_t secs = time(NULL) - view->start_time;
973
974                 if (view == VIEW(REQ_VIEW_HELP)) {
975                         report("%s", HELP);
976                         goto end;
977                 }
978
979                 report("Loaded %d lines in %ld second%s", view->lines, secs,
980                        secs == 1 ? "" : "s");
981                 goto end;
982         }
983
984         return TRUE;
985
986 alloc_error:
987         report("Allocation failure");
988
989 end:
990         end_update(view);
991         return FALSE;
992 }
993
994 static void
995 switch_view(struct view *prev, enum request request,
996             bool backgrounded, bool split)
997 {
998         struct view *view = VIEW(request);
999         struct view *displayed;
1000         int nviews;
1001
1002         /* Cycle between displayed views and count the views. */
1003         foreach_view (displayed, nviews) {
1004                 if (prev != view &&
1005                     view == displayed &&
1006                     !strcmp(view->id, prev->id)) {
1007                         current_view = nviews;
1008                         /* Blur out the title of the previous view. */
1009                         update_view_title(prev);
1010                         report("Switching to %s view", view->name);
1011                         return;
1012                 }
1013         }
1014
1015         if (view == prev && nviews == 1) {
1016                 report("Already in %s view", view->name);
1017                 return;
1018         }
1019
1020         if (strcmp(view->vref, view->id) &&
1021             !begin_update(view)) {
1022                 report("Failed to load %s view", view->name);
1023                 return;
1024         }
1025
1026         if (split) {
1027                 display[current_view + 1] = view;
1028                 if (!backgrounded)
1029                         current_view++;
1030         } else {
1031                 /* Maximize the current view. */
1032                 memset(display, 0, sizeof(display));
1033                 current_view = 0;
1034                 display[current_view] = view;
1035         }
1036
1037         resize_display();
1038
1039         if (split && prev->lineno - prev->offset > prev->height) {
1040                 /* Take the title line into account. */
1041                 int lines = prev->lineno - prev->height + 1;
1042
1043                 /* Scroll the view that was split if the current line is
1044                  * outside the new limited view. */
1045                 do_scroll_view(prev, lines);
1046         }
1047
1048         if (prev && view != prev) {
1049                 /* "Blur" the previous view. */
1050                 update_view_title(prev);
1051
1052                 /* Continue loading split views in the background. */
1053                 if (!split)
1054                         end_update(prev);
1055         }
1056
1057         if (view->pipe) {
1058                 /* Clear the old view and let the incremental updating refill
1059                  * the screen. */
1060                 wclear(view->win);
1061                 report("Loading...");
1062         } else {
1063                 redraw_view(view);
1064                 report("");
1065         }
1066 }
1067
1068
1069 /*
1070  * User request switch noodle
1071  */
1072
1073 static int
1074 view_driver(struct view *view, enum request request)
1075 {
1076         int i;
1077
1078         switch (request) {
1079         case REQ_MOVE_UP:
1080         case REQ_MOVE_DOWN:
1081         case REQ_MOVE_PAGE_UP:
1082         case REQ_MOVE_PAGE_DOWN:
1083         case REQ_MOVE_FIRST_LINE:
1084         case REQ_MOVE_LAST_LINE:
1085                 move_view(view, request);
1086                 break;
1087
1088         case REQ_SCROLL_LINE_DOWN:
1089         case REQ_SCROLL_LINE_UP:
1090         case REQ_SCROLL_PAGE_DOWN:
1091         case REQ_SCROLL_PAGE_UP:
1092                 scroll_view(view, request);
1093                 break;
1094
1095         case REQ_VIEW_MAIN:
1096         case REQ_VIEW_DIFF:
1097         case REQ_VIEW_LOG:
1098         case REQ_VIEW_HELP:
1099                 switch_view(view, request, FALSE, FALSE);
1100                 break;
1101
1102         case REQ_ENTER:
1103                 return view->ops->enter(view);
1104
1105         case REQ_VIEW_NEXT:
1106         {
1107                 int nviews = display[1] ? 2 : 1;
1108                 int next_view = (current_view + 1) % nviews;
1109
1110                 if (next_view == current_view) {
1111                         report("Only one view is displayed");
1112                         break;
1113                 }
1114
1115                 current_view = next_view;
1116                 /* Blur out the title of the previous view. */
1117                 update_view_title(view);
1118                 report("Switching to %s view", display[current_view]->name);
1119                 break;
1120         }
1121         case REQ_TOGGLE_LINE_NUMBERS:
1122                 opt_line_number = !opt_line_number;
1123                 redraw_view(view);
1124                 break;
1125
1126         case REQ_PROMPT:
1127                 switch_view(view, opt_request, FALSE, FALSE);
1128                 break;
1129
1130         case REQ_STOP_LOADING:
1131                 foreach_view (view, i) {
1132                         if (view->pipe)
1133                                 report("Stopped loaded of %s view", view->name),
1134                         end_update(view);
1135                 }
1136                 break;
1137
1138         case REQ_SHOW_VERSION:
1139                 report("Version: %s", VERSION);
1140                 return TRUE;
1141
1142         case REQ_SCREEN_REDRAW:
1143                 redraw_view(view);
1144                 break;
1145
1146         case REQ_SCREEN_UPDATE:
1147                 doupdate();
1148                 return TRUE;
1149
1150         case REQ_QUIT:
1151                 return FALSE;
1152
1153         default:
1154                 /* An unknown key will show most commonly used commands. */
1155                 report("%s", HELP);
1156                 return TRUE;
1157         }
1158
1159         return TRUE;
1160 }
1161
1162
1163 /*
1164  * View backend handlers
1165  */
1166
1167 static bool
1168 pager_draw(struct view *view, unsigned int lineno)
1169 {
1170         enum line_type type;
1171         char *line;
1172         int linelen;
1173         int attr;
1174
1175         if (view->offset + lineno >= view->lines)
1176                 return FALSE;
1177
1178         line = view->line[view->offset + lineno];
1179         type = get_line_type(line);
1180
1181         if (view->offset + lineno == view->lineno) {
1182                 if (type == LINE_COMMIT) {
1183                         string_copy(view->ref, line + 7);
1184                         string_copy(ref_commit, view->ref);
1185                 }
1186
1187                 type = LINE_CURSOR;
1188         }
1189
1190         attr = get_line_attr(type);
1191         wattrset(view->win, attr);
1192
1193         linelen = strlen(line);
1194         linelen = MIN(linelen, view->width);
1195
1196         if (opt_line_number) {
1197                 unsigned int real_lineno = view->offset + lineno + 1;
1198                 int col = 0;
1199
1200                 if (real_lineno == 1 || (real_lineno % opt_num_interval) == 0)
1201                         mvwprintw(view->win, lineno, 0, "%4d: ", real_lineno);
1202                 else
1203                         mvwaddstr(view->win, lineno, 0, "    : ");
1204
1205                 while (line) {
1206                         if (*line == '\t') {
1207                                 waddnstr(view->win, "        ", 8 - (col % 8));
1208                                 col += 8 - (col % 8);
1209                                 line++;
1210
1211                         } else {
1212                                 char *tab = strchr(line, '\t');
1213
1214                                 if (tab)
1215                                         waddnstr(view->win, line, tab - line);
1216                                 else
1217                                         waddstr(view->win, line);
1218                                 col += tab - line;
1219                                 line = tab;
1220                         }
1221                 }
1222                 waddstr(view->win, line);
1223
1224         } else {
1225 #if 0
1226                 /* NOTE: Code for only highlighting the text on the cursor line.
1227                  * Kept since I've not yet decided whether to highlight the
1228                  * entire line or not. --fonseca */
1229                 /* No empty lines makes cursor drawing and clearing implicit. */
1230                 if (!*line)
1231                         line = " ", linelen = 1;
1232 #endif
1233                 mvwaddnstr(view->win, lineno, 0, line, linelen);
1234         }
1235
1236         /* Paint the rest of the line if it's the cursor line. */
1237         if (type == LINE_CURSOR)
1238                 wchgat(view->win, -1, 0, type, NULL);
1239
1240         return TRUE;
1241 }
1242
1243 static bool
1244 pager_read(struct view *view, char *line)
1245 {
1246         view->line[view->lines] = strdup(line);
1247         if (!view->line[view->lines])
1248                 return FALSE;
1249
1250         view->lines++;
1251         return TRUE;
1252 }
1253
1254 static bool
1255 pager_enter(struct view *view)
1256 {
1257         char *line = view->line[view->lineno];
1258
1259         if (get_line_type(line) == LINE_COMMIT) {
1260                 switch_view(view, REQ_VIEW_DIFF, FALSE, FALSE);
1261         }
1262
1263         return TRUE;
1264 }
1265
1266
1267 static struct view_ops pager_ops = {
1268         pager_draw,
1269         pager_read,
1270         pager_enter,
1271 };
1272
1273 static bool
1274 main_draw(struct view *view, unsigned int lineno)
1275 {
1276         char buf[DATE_COLS + 1];
1277         struct commit *commit;
1278         enum line_type type;
1279         int cols = 0;
1280         size_t timelen;
1281
1282         if (view->offset + lineno >= view->lines)
1283                 return FALSE;
1284
1285         commit = view->line[view->offset + lineno];
1286         if (!*commit->author)
1287                 return FALSE;
1288
1289         if (view->offset + lineno == view->lineno) {
1290                 string_copy(view->ref, commit->id);
1291                 type = LINE_CURSOR;
1292         } else {
1293                 type = LINE_MAIN_COMMIT;
1294         }
1295
1296         wmove(view->win, lineno, cols);
1297         wattrset(view->win, get_line_attr(LINE_MAIN_DATE));
1298
1299         timelen = strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time);
1300         waddnstr(view->win, buf, timelen);
1301         waddstr(view->win, " ");
1302
1303         cols += DATE_COLS;
1304         wmove(view->win, lineno, cols);
1305         wattrset(view->win, get_line_attr(LINE_MAIN_AUTHOR));
1306
1307         if (strlen(commit->author) > 19) {
1308                 waddnstr(view->win, commit->author, 18);
1309                 wattrset(view->win, get_line_attr(LINE_MAIN_DELIM));
1310                 waddch(view->win, '~');
1311         } else {
1312                 waddstr(view->win, commit->author);
1313         }
1314
1315         cols += 20;
1316         wattrset(view->win, A_NORMAL);
1317         mvwaddch(view->win, lineno, cols, ACS_LTEE);
1318         wattrset(view->win, get_line_attr(type));
1319         mvwaddstr(view->win, lineno, cols + 2, commit->title);
1320         wattrset(view->win, A_NORMAL);
1321
1322         return TRUE;
1323 }
1324
1325 /* Reads git log --pretty=raw output and parses it into the commit struct. */
1326 static bool
1327 main_read(struct view *view, char *line)
1328 {
1329         enum line_type type = get_line_type(line);
1330         struct commit *commit;
1331
1332         switch (type) {
1333         case LINE_COMMIT:
1334                 commit = calloc(1, sizeof(struct commit));
1335                 if (!commit)
1336                         return FALSE;
1337
1338                 line += STRING_SIZE("commit ");
1339
1340                 view->line[view->lines++] = commit;
1341                 string_copy(commit->id, line);
1342                 break;
1343
1344         case LINE_AUTHOR_IDENT:
1345         {
1346                 char *ident = line + STRING_SIZE("author ");
1347                 char *end = strchr(ident, '<');
1348
1349                 if (end) {
1350                         for (; end > ident && isspace(end[-1]); end--) ;
1351                         *end = 0;
1352                 }
1353
1354                 commit = view->line[view->lines - 1];
1355                 string_copy(commit->author, ident);
1356
1357                 /* Parse epoch and timezone */
1358                 if (end) {
1359                         char *secs = strchr(end + 1, '>');
1360                         char *zone;
1361                         time_t time;
1362
1363                         if (!secs || secs[1] != ' ')
1364                                 break;
1365
1366                         secs += 2;
1367                         time = (time_t) atol(secs);
1368                         zone = strchr(secs, ' ');
1369                         if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
1370                                 long tz;
1371
1372                                 zone++;
1373                                 tz  = ('0' - zone[1]) * 60 * 60 * 10;
1374                                 tz += ('0' - zone[2]) * 60 * 60;
1375                                 tz += ('0' - zone[3]) * 60;
1376                                 tz += ('0' - zone[4]) * 60;
1377
1378                                 if (zone[0] == '-')
1379                                         tz = -tz;
1380
1381                                 time -= tz;
1382                         }
1383                         gmtime_r(&time, &commit->time);
1384                 }
1385                 break;
1386         }
1387         default:
1388                 /* We should only ever end up here if there has already been a
1389                  * commit line, however, be safe. */
1390                 if (view->lines == 0)
1391                         break;
1392
1393                 /* Fill in the commit title if it has not already been set. */
1394                 commit = view->line[view->lines - 1];
1395                 if (commit->title[0])
1396                         break;
1397
1398                 /* Require titles to start with a non-space character at the
1399                  * offset used by git log. */
1400                 if (strncmp(line, "    ", 4) ||
1401                     isspace(line[5]))
1402                         break;
1403
1404                 string_copy(commit->title, line + 4);
1405         }
1406
1407         return TRUE;
1408 }
1409
1410 static bool
1411 main_enter(struct view *view)
1412 {
1413         switch_view(view, REQ_VIEW_DIFF, TRUE, TRUE);
1414         return TRUE;
1415 }
1416
1417 static struct view_ops main_ops = {
1418         main_draw,
1419         main_read,
1420         main_enter,
1421 };
1422
1423 /*
1424  * Status management
1425  */
1426
1427 /* The status window is used for polling keystrokes. */
1428 static WINDOW *status_win;
1429
1430 /* Update status and title window. */
1431 static void
1432 report(const char *msg, ...)
1433 {
1434         va_list args;
1435
1436         va_start(args, msg);
1437
1438         /* Update the title window first, so the cursor ends up in the status
1439          * window. */
1440         update_view_title(display[current_view]);
1441
1442         werase(status_win);
1443         wmove(status_win, 0, 0);
1444         vwprintw(status_win, msg, args);
1445         wrefresh(status_win);
1446
1447         va_end(args);
1448 }
1449
1450 /* Controls when nodelay should be in effect when polling user input. */
1451 static void
1452 set_nonblocking_input(int loading)
1453 {
1454         /* The number of loading views. */
1455         static unsigned int nloading;
1456
1457         if (loading == TRUE) {
1458                 if (nloading++ == 0)
1459                         nodelay(status_win, TRUE);
1460                 return;
1461         }
1462
1463         if (nloading-- == 1)
1464                 nodelay(status_win, FALSE);
1465 }
1466
1467 static void
1468 init_display(void)
1469 {
1470         int x, y;
1471
1472         initscr();      /* Initialize the curses library */
1473         nonl();         /* Tell curses not to do NL->CR/NL on output */
1474         cbreak();       /* Take input chars one at a time, no wait for \n */
1475         noecho();       /* Don't echo input */
1476         leaveok(stdscr, TRUE);
1477
1478         if (has_colors())
1479                 init_colors();
1480
1481         getmaxyx(stdscr, y, x);
1482         status_win = newwin(1, 0, y - 1, 0);
1483         if (!status_win)
1484                 die("Failed to create status window");
1485
1486         /* Enable keyboard mapping */
1487         keypad(status_win, TRUE);
1488         wbkgdset(status_win, get_line_attr(LINE_STATUS));
1489 }
1490
1491 /*
1492  * Main
1493  */
1494
1495 static void
1496 quit(int sig)
1497 {
1498         if (status_win)
1499                 delwin(status_win);
1500         endwin();
1501
1502         /* FIXME: Shutdown gracefully. */
1503
1504         exit(0);
1505 }
1506
1507 static void die(const char *err, ...)
1508 {
1509         va_list args;
1510
1511         endwin();
1512
1513         va_start(args, err);
1514         fputs("tig: ", stderr);
1515         vfprintf(stderr, err, args);
1516         fputs("\n", stderr);
1517         va_end(args);
1518
1519         exit(1);
1520 }
1521
1522 int
1523 main(int argc, char *argv[])
1524 {
1525         enum request request;
1526         int git_arg;
1527
1528         signal(SIGINT, quit);
1529
1530         git_arg = parse_options(argc, argv);
1531         if (git_arg < 0)
1532                 return 0;
1533
1534         if (git_arg < argc) {
1535                 size_t buf_size;
1536
1537                 /* XXX: This is vulnerable to the user overriding options
1538                  * required for the main view parser. */
1539                 if (opt_request == REQ_VIEW_MAIN)
1540                         string_copy(opt_cmd, "git log --stat --pretty=raw");
1541                 else
1542                         string_copy(opt_cmd, "git");
1543                 buf_size = strlen(opt_cmd);
1544
1545                 while (buf_size < sizeof(opt_cmd) && git_arg < argc) {
1546                         opt_cmd[buf_size++] = ' ';
1547                         buf_size = sq_quote(opt_cmd, buf_size, argv[git_arg++]);
1548                 }
1549
1550                 if (buf_size >= sizeof(opt_cmd))
1551                         die("command too long");
1552
1553                 opt_cmd[buf_size] = 0;
1554         }
1555
1556         request = opt_request;
1557
1558         init_display();
1559
1560         while (view_driver(display[current_view], request)) {
1561                 struct view *view;
1562                 int key;
1563                 int i;
1564
1565                 foreach_view (view, i)
1566                         update_view(view);
1567
1568                 /* Refresh, accept single keystroke of input */
1569                 key = wgetch(status_win);
1570                 request = get_request(key);
1571
1572                 if (request == REQ_PROMPT) {
1573                         nocbreak();
1574                         echo();
1575                         report(":");
1576                         if (wgetnstr(status_win, opt_cmd, sizeof(opt_cmd)) == OK)
1577                                 die("%s", opt_cmd);
1578                         cbreak();       /* Take input chars one at a time, no wait for \n */
1579                         noecho();       /* Don't echo input */
1580                 }
1581         }
1582
1583         quit(0);
1584
1585         return 0;
1586 }
1587
1588 /**
1589  * TODO
1590  * ----
1591  * Features that should be explored.
1592  *
1593  * - Dynamic scaling of line number indentation.
1594  *
1595  * - Internal command line (exmode-inspired) which allows to specify what git
1596  *   log or git diff command to run. Example:
1597  *
1598  *      :log -p
1599  *
1600  * - Terminal resizing support. I am yet to figure out whether catching
1601  *   SIGWINCH is preferred over using ncurses' built-in support for resizing.
1602  *
1603  * - Locale support.
1604  *
1605  * COPYRIGHT
1606  * ---------
1607  * Copyright (c) Jonas Fonseca <fonseca@diku.dk>, 2006
1608  *
1609  * This program is free software; you can redistribute it and/or modify
1610  * it under the terms of the GNU General Public License as published by
1611  * the Free Software Foundation; either version 2 of the License, or
1612  * (at your option) any later version.
1613  *
1614  * SEE ALSO
1615  * --------
1616  * [verse]
1617  * link:http://www.kernel.org/pub/software/scm/git/docs/[git(7)],
1618  * link:http://www.kernel.org/pub/software/scm/cogito/docs/[cogito(7)]
1619  * gitk(1): git repository browser written using tcl/tk,
1620  * gitview(1): git repository browser written using python/gtk.
1621  **/