Blob


1 /*
2 * Copyright (c) 2018 Stefan Sperling <stsp@openbsd.org>
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 *
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
17 #include <sys/queue.h>
19 #include <errno.h>
20 #define _XOPEN_SOURCE_EXTENDED
21 #include <curses.h>
22 #undef _XOPEN_SOURCE_EXTENDED
23 #include <panel.h>
24 #include <locale.h>
25 #include <stdlib.h>
26 #include <stdio.h>
27 #include <getopt.h>
28 #include <string.h>
29 #include <err.h>
30 #include <unistd.h>
31 #include <util.h>
32 #include <limits.h>
33 #include <wchar.h>
35 #include "got_error.h"
36 #include "got_object.h"
37 #include "got_reference.h"
38 #include "got_repository.h"
39 #include "got_diff.h"
40 #include "got_opentemp.h"
41 #include "got_commit_graph.h"
43 #ifndef MIN
44 #define MIN(_a,_b) ((_a) < (_b) ? (_a) : (_b))
45 #endif
47 #ifndef nitems
48 #define nitems(_a) (sizeof((_a)) / sizeof((_a)[0]))
49 #endif
51 struct tog_cmd {
52 const char *name;
53 const struct got_error *(*cmd_main)(int, char *[]);
54 void (*cmd_usage)(void);
55 const char *descr;
56 };
58 __dead static void usage(void);
59 __dead static void usage_log(void);
60 __dead static void usage_diff(void);
61 __dead static void usage_blame(void);
63 static const struct got_error* cmd_log(int, char *[]);
64 static const struct got_error* cmd_diff(int, char *[]);
65 static const struct got_error* cmd_blame(int, char *[]);
67 static struct tog_cmd tog_commands[] = {
68 { "log", cmd_log, usage_log,
69 "show repository history" },
70 { "diff", cmd_diff, usage_diff,
71 "compare files and directories" },
72 { "blame", cmd_blame, usage_blame,
73 "show line-by-line file history" },
74 };
76 static struct tog_view {
77 WINDOW *window;
78 PANEL *panel;
79 } tog_log_view, tog_diff_view;
81 static const struct got_error *
82 show_diff_view(struct got_object *, struct got_object *,
83 struct got_repository *);
84 static const struct got_error *
85 show_log_view(struct got_object_id *, struct got_repository *);
87 __dead static void
88 usage_log(void)
89 {
90 endwin();
91 fprintf(stderr, "usage: %s log [-c commit] [repository-path]\n",
92 getprogname());
93 exit(1);
94 }
96 /* Create newly allocated wide-character string equivalent to a byte string. */
97 static const struct got_error *
98 mbs2ws(wchar_t **ws, size_t *wlen, const char *s)
99 {
100 const struct got_error *err = NULL;
102 *ws = NULL;
103 *wlen = mbstowcs(NULL, s, 0);
104 if (*wlen == (size_t)-1)
105 return got_error_from_errno();
107 *ws = calloc(*wlen + 1, sizeof(*ws));
108 if (*ws == NULL)
109 return got_error_from_errno();
111 if (mbstowcs(*ws, s, *wlen) != *wlen)
112 err = got_error_from_errno();
114 if (err) {
115 free(*ws);
116 *ws = NULL;
117 *wlen = 0;
119 return err;
122 /* Format a line for display, ensuring that it won't overflow a width limit. */
123 static const struct got_error *
124 format_line(wchar_t **wlinep, int *widthp, char *line, int wlimit)
126 const struct got_error *err = NULL;
127 int cols = 0;
128 wchar_t *wline = NULL;
129 size_t wlen;
130 int i;
132 *wlinep = NULL;
134 err = mbs2ws(&wline, &wlen, line);
135 if (err)
136 return err;
138 i = 0;
139 while (i < wlen && cols <= wlimit) {
140 int width = wcwidth(wline[i]);
141 switch (width) {
142 case 0:
143 break;
144 case 1:
145 case 2:
146 cols += width;
147 break;
148 case -1:
149 if (wline[i] == L'\t')
150 cols += TABSIZE;
151 break;
152 default:
153 err = got_error_from_errno();
154 goto done;
156 if (cols <= COLS) {
157 i++;
158 if (widthp)
159 *widthp = cols;
162 wline[i] = L'\0';
163 done:
164 if (err)
165 free(wline);
166 else
167 *wlinep = wline;
168 return err;
171 static const struct got_error *
172 draw_commit(struct got_commit_object *commit, struct got_object_id *id)
174 const struct got_error *err = NULL;
175 char *logmsg0 = NULL, *logmsg = NULL;
176 char *author0 = NULL, *author = NULL;
177 wchar_t *wlogmsg = NULL, *wauthor = NULL;
178 int author_width, logmsg_width;
179 char *newline, *smallerthan;
180 char *line = NULL;
181 char *id_str = NULL;
182 size_t id_len;
183 int col, limit;
184 static const size_t id_display_cols = 8;
185 static const size_t author_display_cols = 16;
186 const int avail = COLS;
188 err = got_object_id_str(&id_str, id);
189 if (err)
190 return err;
191 id_len = strlen(id_str);
192 if (avail < id_display_cols) {
193 limit = MIN(id_len, avail);
194 waddnstr(tog_log_view.window, id_str, limit);
195 } else {
196 limit = MIN(id_display_cols, id_len);
197 waddnstr(tog_log_view.window, id_str, limit);
199 col = limit + 1;
200 while (col <= avail && col < id_display_cols + 2) {
201 waddch(tog_log_view.window, ' ');
202 col++;
204 if (col > avail)
205 goto done;
207 author0 = strdup(commit->author);
208 if (author0 == NULL) {
209 err = got_error_from_errno();
210 goto done;
212 author = author0;
213 smallerthan = strchr(author, '<');
214 if (smallerthan)
215 *smallerthan = '\0';
216 else {
217 char *at = strchr(author, '@');
218 if (at)
219 *at = '\0';
221 limit = MIN(avail, author_display_cols);
222 err = format_line(&wauthor, &author_width, author, limit);
223 if (err)
224 goto done;
225 waddwstr(tog_log_view.window, wauthor);
226 col += author_width;
227 while (col <= avail && author_width < author_display_cols + 1) {
228 waddch(tog_log_view.window, ' ');
229 col++;
230 author_width++;
232 if (col > avail)
233 goto done;
235 logmsg0 = strdup(commit->logmsg);
236 if (logmsg0 == NULL) {
237 err = got_error_from_errno();
238 goto done;
240 logmsg = logmsg0;
241 while (*logmsg == '\n')
242 logmsg++;
243 newline = strchr(logmsg, '\n');
244 if (newline)
245 *newline = '\0';
246 limit = avail - col;
247 err = format_line(&wlogmsg, &logmsg_width, logmsg, limit);
248 if (err)
249 goto done;
250 waddwstr(tog_log_view.window, wlogmsg);
251 col += logmsg_width;
252 while (col <= avail) {
253 waddch(tog_log_view.window, ' ');
254 col++;
256 done:
257 free(logmsg0);
258 free(wlogmsg);
259 free(author0);
260 free(wauthor);
261 free(line);
262 free(id_str);
263 return err;
266 struct commit_queue_entry {
267 TAILQ_ENTRY(commit_queue_entry) entry;
268 struct got_object_id *id;
269 struct got_commit_object *commit;
270 };
271 TAILQ_HEAD(commit_queue, commit_queue_entry);
273 static struct commit_queue_entry *
274 alloc_commit_queue_entry(struct got_commit_object *commit,
275 struct got_object_id *id)
277 struct commit_queue_entry *entry;
279 entry = calloc(1, sizeof(*entry));
280 if (entry == NULL)
281 return NULL;
283 entry->id = id;
284 entry->commit = commit;
285 return entry;
288 static void
289 pop_commit(struct commit_queue *commits)
291 struct commit_queue_entry *entry;
293 entry = TAILQ_FIRST(commits);
294 TAILQ_REMOVE(commits, entry, entry);
295 got_object_commit_close(entry->commit);
296 /* Don't free entry->id! It is owned by the commit graph. */
297 free(entry);
300 static void
301 free_commits(struct commit_queue *commits)
303 while (!TAILQ_EMPTY(commits))
304 pop_commit(commits);
307 static const struct got_error *
308 queue_commits(struct got_commit_graph *graph, struct commit_queue *commits,
309 struct got_object_id *start_id, struct got_repository *repo)
311 const struct got_error *err = NULL;
312 struct got_object_id *id;
313 struct commit_queue_entry *entry;
315 err = got_commit_graph_iter_start(graph, start_id);
316 if (err)
317 return err;
319 entry = TAILQ_LAST(commits, commit_queue);
320 if (entry && got_object_id_cmp(entry->id, start_id) == 0) {
321 int nfetched;
323 /* Start ID's commit is already on the queue; skip over it. */
324 err = got_commit_graph_iter_next(&id, graph);
325 if (err && err->code != GOT_ERR_ITER_NEED_MORE)
326 return err;
328 err = got_commit_graph_fetch_commits(&nfetched, graph, 1, repo);
329 if (err)
330 return err;
333 while (1) {
334 struct got_commit_object *commit;
336 err = got_commit_graph_iter_next(&id, graph);
337 if (err) {
338 if (err->code == GOT_ERR_ITER_NEED_MORE)
339 err = NULL;
340 break;
343 err = got_object_open_as_commit(&commit, repo, id);
344 if (err)
345 break;
347 entry = alloc_commit_queue_entry(commit, id);
348 if (entry == NULL) {
349 err = got_error_from_errno();
350 break;
353 TAILQ_INSERT_TAIL(commits, entry, entry);
356 return err;
359 static const struct got_error *
360 fetch_next_commit(struct commit_queue_entry **pentry,
361 struct commit_queue_entry *entry, struct commit_queue *commits,
362 struct got_commit_graph *graph, struct got_repository *repo)
364 const struct got_error *err = NULL;
365 struct got_object_qid *qid;
367 *pentry = NULL;
369 /* Populate commit graph with entry's parent commits. */
370 SIMPLEQ_FOREACH(qid, &entry->commit->parent_ids, entry) {
371 int nfetched;
372 err = got_commit_graph_fetch_commits_up_to(&nfetched,
373 graph, qid->id, repo);
374 if (err)
375 return err;
378 /* Append outstanding commits to queue in graph sort order. */
379 err = queue_commits(graph, commits, entry->id, repo);
380 if (err) {
381 if (err->code == GOT_ERR_ITER_COMPLETED)
382 err = NULL;
383 return err;
386 /* Next entry to display should now be available. */
387 *pentry = TAILQ_NEXT(entry, entry);
388 if (*pentry == NULL)
389 return got_error(GOT_ERR_NO_OBJ);
391 return NULL;
394 static const struct got_error *
395 get_head_commit_id(struct got_object_id **head_id, struct got_repository *repo)
397 const struct got_error *err = NULL;
398 struct got_reference *head_ref;
400 *head_id = NULL;
402 err = got_ref_open(&head_ref, repo, GOT_REF_HEAD);
403 if (err)
404 return err;
406 err = got_ref_resolve(head_id, repo, head_ref);
407 got_ref_close(head_ref);
408 if (err) {
409 *head_id = NULL;
410 return err;
413 return NULL;
416 static const struct got_error *
417 draw_commits(struct commit_queue_entry **last, struct commit_queue_entry **selected,
418 struct commit_queue_entry *first, int selected_idx, int limit)
420 const struct got_error *err = NULL;
421 struct commit_queue_entry *entry;
422 int ncommits = 0;
424 werase(tog_log_view.window);
426 entry = first;
427 *last = first;
428 while (entry) {
429 if (ncommits == limit)
430 break;
431 if (ncommits == selected_idx) {
432 wstandout(tog_log_view.window);
433 *selected = entry;
435 err = draw_commit(entry->commit, entry->id);
436 if (ncommits == selected_idx)
437 wstandend(tog_log_view.window);
438 if (err)
439 break;
440 ncommits++;
441 *last = entry;
442 entry = TAILQ_NEXT(entry, entry);
445 update_panels();
446 doupdate();
448 return err;
451 static void
452 scroll_up(struct commit_queue_entry **first_displayed_entry, int maxscroll,
453 struct commit_queue *commits)
455 struct commit_queue_entry *entry;
456 int nscrolled = 0;
458 entry = TAILQ_FIRST(commits);
459 if (*first_displayed_entry == entry)
460 return;
462 entry = *first_displayed_entry;
463 while (entry && nscrolled < maxscroll) {
464 entry = TAILQ_PREV(entry, commit_queue, entry);
465 if (entry) {
466 *first_displayed_entry = entry;
467 nscrolled++;
472 static const struct got_error *
473 scroll_down(struct commit_queue_entry **first_displayed_entry, int maxscroll,
474 struct commit_queue_entry *last_displayed_entry,
475 struct commit_queue *commits, struct got_commit_graph *graph,
476 struct got_repository *repo)
478 const struct got_error *err = NULL;
479 struct commit_queue_entry *pentry;
480 int nscrolled = 0;
482 do {
483 pentry = TAILQ_NEXT(last_displayed_entry, entry);
484 if (pentry == NULL) {
485 err = fetch_next_commit(&pentry, last_displayed_entry,
486 commits, graph, repo);
487 if (err || pentry == NULL)
488 break;
490 last_displayed_entry = pentry;
492 pentry = TAILQ_NEXT(*first_displayed_entry, entry);
493 if (pentry == NULL)
494 break;
495 *first_displayed_entry = pentry;
496 } while (++nscrolled < maxscroll);
498 return err;
501 static int
502 num_parents(struct commit_queue_entry *entry)
504 int nparents = 0;
506 while (entry) {
507 entry = TAILQ_NEXT(entry, entry);
508 nparents++;
511 return nparents;
514 static const struct got_error *
515 show_commit(struct commit_queue_entry *entry, struct got_repository *repo)
517 const struct got_error *err;
518 struct got_object *obj1 = NULL, *obj2 = NULL;
519 struct got_object_qid *parent_id;
521 err = got_object_open(&obj2, repo, entry->id);
522 if (err)
523 return err;
525 parent_id = SIMPLEQ_FIRST(&entry->commit->parent_ids);
526 if (parent_id) {
527 err = got_object_open(&obj1, repo, parent_id->id);
528 if (err)
529 goto done;
532 err = show_diff_view(obj1, obj2, repo);
533 done:
534 if (obj1)
535 got_object_close(obj1);
536 if (obj2)
537 got_object_close(obj2);
538 return err;
541 static const struct got_error *
542 show_log_view(struct got_object_id *start_id, struct got_repository *repo)
544 const struct got_error *err = NULL;
545 struct got_object_id *head_id = NULL;
546 int ch, done = 0, selected = 0, nparents, nfetched;
547 struct got_commit_graph *graph;
548 struct commit_queue commits;
549 struct commit_queue_entry *entry = NULL;
550 struct commit_queue_entry *first_displayed_entry = NULL;
551 struct commit_queue_entry *last_displayed_entry = NULL;
552 struct commit_queue_entry *selected_entry = NULL;
554 if (tog_log_view.window == NULL) {
555 tog_log_view.window = newwin(0, 0, 0, 0);
556 if (tog_log_view.window == NULL)
557 return got_error_from_errno();
558 keypad(tog_log_view.window, TRUE);
560 if (tog_log_view.panel == NULL) {
561 tog_log_view.panel = new_panel(tog_log_view.window);
562 if (tog_log_view.panel == NULL)
563 return got_error_from_errno();
564 } else
565 show_panel(tog_log_view.panel);
567 err = get_head_commit_id(&head_id, repo);
568 if (err)
569 return err;
571 TAILQ_INIT(&commits);
573 err = got_commit_graph_open(&graph, head_id, repo);
574 if (err)
575 goto done;
577 /* Populate commit graph with a sufficient number of commits. */
578 err = got_commit_graph_fetch_commits_up_to(&nfetched, graph, start_id,
579 repo);
580 if (err)
581 goto done;
582 err = got_commit_graph_fetch_commits(&nfetched, graph, LINES, repo);
583 if (err)
584 goto done;
586 /*
587 * Open the initial batch of commits, sorted in commit graph order.
588 * We keep all commits open throughout the lifetime of the log view
589 * in order to avoid having to re-fetch commits from disk while
590 * updating the display.
591 */
592 err = queue_commits(graph, &commits, head_id, repo);
593 if (err && err->code != GOT_ERR_ITER_COMPLETED)
594 goto done;
596 /* Find entry corresponding to the first commit to display. */
597 TAILQ_FOREACH(entry, &commits, entry) {
598 if (got_object_id_cmp(entry->id, start_id) == 0) {
599 first_displayed_entry = entry;
600 break;
603 if (first_displayed_entry == NULL) {
604 err = got_error(GOT_ERR_NO_OBJ);
605 goto done;
608 while (!done) {
609 err = draw_commits(&last_displayed_entry, &selected_entry,
610 first_displayed_entry, selected, LINES);
611 if (err)
612 goto done;
614 nodelay(stdscr, FALSE);
615 ch = wgetch(tog_log_view.window);
616 nodelay(stdscr, TRUE);
617 switch (ch) {
618 case ERR:
619 if (errno) {
620 err = got_error_from_errno();
621 goto done;
623 break;
624 case 'q':
625 done = 1;
626 break;
627 case 'k':
628 case KEY_UP:
629 if (selected > 0)
630 selected--;
631 if (selected > 0)
632 break;
633 scroll_up(&first_displayed_entry, 1, &commits);
634 break;
635 case KEY_PPAGE:
636 if (TAILQ_FIRST(&commits) ==
637 first_displayed_entry) {
638 selected = 0;
639 break;
641 scroll_up(&first_displayed_entry, LINES,
642 &commits);
643 break;
644 case 'j':
645 case KEY_DOWN:
646 nparents = num_parents(first_displayed_entry);
647 if (selected < LINES - 1 &&
648 selected < nparents - 1) {
649 selected++;
650 break;
652 err = scroll_down(&first_displayed_entry, 1,
653 last_displayed_entry, &commits, graph,
654 repo);
655 if (err)
656 goto done;
657 break;
658 case KEY_NPAGE:
659 err = scroll_down(&first_displayed_entry, LINES,
660 last_displayed_entry, &commits, graph,
661 repo);
662 if (err)
663 goto done;
664 if (last_displayed_entry->commit->nparents > 0)
665 break;
666 /* can't scroll any further; move cursor down */
667 nparents = num_parents(first_displayed_entry);
668 if (selected < LINES - 1 ||
669 selected < nparents - 1)
670 selected = MIN(LINES - 1, nparents - 1);
671 break;
672 case KEY_RESIZE:
673 if (selected > LINES)
674 selected = LINES - 1;
675 break;
676 case KEY_ENTER:
677 case '\r':
678 err = show_commit(selected_entry, repo);
679 if (err)
680 break;
681 show_panel(tog_log_view.panel);
682 break;
683 default:
684 break;
687 done:
688 free(head_id);
689 if (graph)
690 got_commit_graph_close(graph);
691 free_commits(&commits);
692 return err;
695 static const struct got_error *
696 cmd_log(int argc, char *argv[])
698 const struct got_error *error;
699 struct got_repository *repo;
700 struct got_object_id *start_id = NULL;
701 char *repo_path = NULL;
702 char *start_commit = NULL;
703 int ch;
705 #ifndef PROFILE
706 if (pledge("stdio rpath wpath cpath flock proc tty", NULL) == -1)
707 err(1, "pledge");
708 #endif
710 while ((ch = getopt(argc, argv, "c:")) != -1) {
711 switch (ch) {
712 case 'c':
713 start_commit = optarg;
714 break;
715 default:
716 usage();
717 /* NOTREACHED */
721 argc -= optind;
722 argv += optind;
724 if (argc == 0) {
725 repo_path = getcwd(NULL, 0);
726 if (repo_path == NULL)
727 return got_error_from_errno();
728 } else if (argc == 1) {
729 repo_path = realpath(argv[0], NULL);
730 if (repo_path == NULL)
731 return got_error_from_errno();
732 } else
733 usage_log();
735 error = got_repo_open(&repo, repo_path);
736 free(repo_path);
737 if (error != NULL)
738 return error;
740 if (start_commit == NULL) {
741 error = get_head_commit_id(&start_id, repo);
742 if (error != NULL)
743 return error;
744 } else {
745 struct got_object *obj;
746 error = got_object_open_by_id_str(&obj, repo, start_commit);
747 if (error == NULL) {
748 start_id = got_object_get_id(obj);
749 if (start_id == NULL)
750 error = got_error_from_errno();
753 if (error != NULL)
754 return error;
755 error = show_log_view(start_id, repo);
756 free(start_id);
757 got_repo_close(repo);
758 return error;
761 __dead static void
762 usage_diff(void)
764 endwin();
765 fprintf(stderr, "usage: %s diff [repository-path] object1 object2\n",
766 getprogname());
767 exit(1);
770 static char *
771 parse_next_line(FILE *f, size_t *len)
773 char *line;
774 size_t linelen;
775 size_t lineno;
776 const char delim[3] = { '\0', '\0', '\0'};
778 line = fparseln(f, &linelen, &lineno, delim, 0);
779 if (len)
780 *len = linelen;
781 return line;
784 static const struct got_error *
785 draw_diff(FILE *f, int *first_displayed_line, int *last_displayed_line,
786 int *eof, int max_lines)
788 const struct got_error *err;
789 int nlines = 0, nprinted = 0;
790 char *line;
791 size_t len;
792 wchar_t *wline;
793 int width;
795 rewind(f);
796 werase(tog_diff_view.window);
798 *eof = 0;
799 while (nprinted < max_lines) {
800 line = parse_next_line(f, &len);
801 if (line == NULL) {
802 *eof = 1;
803 break;
805 if (++nlines < *first_displayed_line) {
806 free(line);
807 continue;
810 err = format_line(&wline, &width, line, COLS);
811 if (err) {
812 free(line);
813 return err;
815 waddwstr(tog_diff_view.window, wline);
816 if (width < COLS)
817 waddch(tog_diff_view.window, '\n');
818 if (++nprinted == 1)
819 *first_displayed_line = nlines;
820 free(line);
822 *last_displayed_line = nlines;
824 update_panels();
825 doupdate();
827 return NULL;
830 static const struct got_error *
831 show_diff_view(struct got_object *obj1, struct got_object *obj2,
832 struct got_repository *repo)
834 const struct got_error *err;
835 FILE *f;
836 int ch, done = 0, first_displayed_line = 1, last_displayed_line = LINES;
837 int eof, i;
839 if (obj1 != NULL && obj2 != NULL &&
840 got_object_get_type(obj1) != got_object_get_type(obj2))
841 return got_error(GOT_ERR_OBJ_TYPE);
843 f = got_opentemp();
844 if (f == NULL)
845 return got_error_from_errno();
847 switch (got_object_get_type(obj1 ? obj1 : obj2)) {
848 case GOT_OBJ_TYPE_BLOB:
849 err = got_diff_objects_as_blobs(obj1, obj2, repo, f);
850 break;
851 case GOT_OBJ_TYPE_TREE:
852 err = got_diff_objects_as_trees(obj1, obj2, repo, f);
853 break;
854 case GOT_OBJ_TYPE_COMMIT:
855 err = got_diff_objects_as_commits(obj1, obj2, repo, f);
856 break;
857 default:
858 return got_error(GOT_ERR_OBJ_TYPE);
861 fflush(f);
863 if (tog_diff_view.window == NULL) {
864 tog_diff_view.window = newwin(0, 0, 0, 0);
865 if (tog_diff_view.window == NULL)
866 return got_error_from_errno();
867 keypad(tog_diff_view.window, TRUE);
869 if (tog_diff_view.panel == NULL) {
870 tog_diff_view.panel = new_panel(tog_diff_view.window);
871 if (tog_diff_view.panel == NULL)
872 return got_error_from_errno();
873 } else
874 show_panel(tog_diff_view.panel);
876 while (!done) {
877 err = draw_diff(f, &first_displayed_line, &last_displayed_line,
878 &eof, LINES);
879 if (err)
880 break;
881 nodelay(stdscr, FALSE);
882 ch = wgetch(tog_diff_view.window);
883 nodelay(stdscr, TRUE);
884 switch (ch) {
885 case 'q':
886 done = 1;
887 break;
888 case 'k':
889 case KEY_UP:
890 case KEY_BACKSPACE:
891 if (first_displayed_line > 1)
892 first_displayed_line--;
893 break;
894 case KEY_PPAGE:
895 i = 0;
896 while (i++ < LINES - 1 &&
897 first_displayed_line > 1)
898 first_displayed_line--;
899 break;
900 case 'j':
901 case KEY_DOWN:
902 case KEY_ENTER:
903 case '\r':
904 if (!eof)
905 first_displayed_line++;
906 break;
907 case KEY_NPAGE:
908 case ' ':
909 i = 0;
910 while (!eof && i++ < LINES - 1) {
911 char *line = parse_next_line(f, NULL);
912 first_displayed_line++;
913 if (line == NULL)
914 break;
916 break;
917 default:
918 break;
921 fclose(f);
922 return err;
925 static const struct got_error *
926 cmd_diff(int argc, char *argv[])
928 const struct got_error *error = NULL;
929 struct got_repository *repo = NULL;
930 struct got_object *obj1 = NULL, *obj2 = NULL;
931 char *repo_path = NULL;
932 char *obj_id_str1 = NULL, *obj_id_str2 = NULL;
933 int ch;
935 #ifndef PROFILE
936 if (pledge("stdio rpath wpath cpath flock proc tty", NULL) == -1)
937 err(1, "pledge");
938 #endif
940 while ((ch = getopt(argc, argv, "")) != -1) {
941 switch (ch) {
942 default:
943 usage();
944 /* NOTREACHED */
948 argc -= optind;
949 argv += optind;
951 if (argc == 0) {
952 usage_diff(); /* TODO show local worktree changes */
953 } else if (argc == 2) {
954 repo_path = getcwd(NULL, 0);
955 if (repo_path == NULL)
956 return got_error_from_errno();
957 obj_id_str1 = argv[0];
958 obj_id_str2 = argv[1];
959 } else if (argc == 3) {
960 repo_path = realpath(argv[0], NULL);
961 if (repo_path == NULL)
962 return got_error_from_errno();
963 obj_id_str1 = argv[1];
964 obj_id_str2 = argv[2];
965 } else
966 usage_diff();
968 error = got_repo_open(&repo, repo_path);
969 free(repo_path);
970 if (error)
971 goto done;
973 error = got_object_open_by_id_str(&obj1, repo, obj_id_str1);
974 if (error)
975 goto done;
977 error = got_object_open_by_id_str(&obj2, repo, obj_id_str2);
978 if (error)
979 goto done;
981 error = show_diff_view(obj1, obj2, repo);
982 done:
983 got_repo_close(repo);
984 if (obj1)
985 got_object_close(obj1);
986 if (obj2)
987 got_object_close(obj2);
988 return error;
991 __dead static void
992 usage_blame(void)
994 endwin();
995 fprintf(stderr, "usage: %s blame [repository-path] blob-object\n",
996 getprogname());
997 exit(1);
1000 static const struct got_error *
1001 cmd_blame(int argc, char *argv[])
1003 return got_error(GOT_ERR_NOT_IMPL);
1006 static void
1007 init_curses(void)
1009 initscr();
1010 cbreak();
1011 noecho();
1012 nonl();
1013 intrflush(stdscr, FALSE);
1014 keypad(stdscr, TRUE);
1015 curs_set(0);
1018 __dead static void
1019 usage(void)
1021 int i;
1023 fprintf(stderr, "usage: %s [-h] [command] [arg ...]\n\n"
1024 "Available commands:\n", getprogname());
1025 for (i = 0; i < nitems(tog_commands); i++) {
1026 struct tog_cmd *cmd = &tog_commands[i];
1027 fprintf(stderr, " %s: %s\n", cmd->name, cmd->descr);
1029 exit(1);
1032 static char **
1033 make_argv(const char *arg0, const char *arg1)
1035 char **argv;
1036 int argc = (arg1 == NULL ? 1 : 2);
1038 argv = calloc(argc, sizeof(char *));
1039 if (argv == NULL)
1040 err(1, "calloc");
1041 argv[0] = strdup(arg0);
1042 if (argv[0] == NULL)
1043 err(1, "calloc");
1044 if (arg1) {
1045 argv[1] = strdup(arg1);
1046 if (argv[1] == NULL)
1047 err(1, "calloc");
1050 return argv;
1053 int
1054 main(int argc, char *argv[])
1056 const struct got_error *error = NULL;
1057 struct tog_cmd *cmd = NULL;
1058 int ch, hflag = 0;
1059 char **cmd_argv = NULL;
1061 setlocale(LC_ALL, "");
1063 while ((ch = getopt(argc, argv, "h")) != -1) {
1064 switch (ch) {
1065 case 'h':
1066 hflag = 1;
1067 break;
1068 default:
1069 usage();
1070 /* NOTREACHED */
1074 argc -= optind;
1075 argv += optind;
1076 optind = 0;
1077 optreset = 1;
1079 if (argc == 0) {
1080 /* Build an argument vector which runs a default command. */
1081 cmd = &tog_commands[0];
1082 cmd_argv = make_argv(cmd->name, NULL);
1083 argc = 1;
1084 } else {
1085 int i;
1087 /* Did the user specific a command? */
1088 for (i = 0; i < nitems(tog_commands); i++) {
1089 if (strncmp(tog_commands[i].name, argv[0],
1090 strlen(argv[0])) == 0) {
1091 cmd = &tog_commands[i];
1092 if (hflag)
1093 tog_commands[i].cmd_usage();
1094 break;
1097 if (cmd == NULL) {
1098 /* Did the user specify a repository? */
1099 char *repo_path = realpath(argv[0], NULL);
1100 if (repo_path) {
1101 struct got_repository *repo;
1102 error = got_repo_open(&repo, repo_path);
1103 if (error == NULL)
1104 got_repo_close(repo);
1105 } else
1106 error = got_error_from_errno();
1107 if (error) {
1108 fprintf(stderr, "%s: '%s' is neither a known "
1109 "command nor a path to a repository\n",
1110 getprogname(), argv[0]);
1111 free(repo_path);
1112 return 1;
1114 cmd = &tog_commands[0];
1115 cmd_argv = make_argv(cmd->name, repo_path);
1116 argc = 2;
1117 free(repo_path);
1121 init_curses();
1123 error = cmd->cmd_main(argc, cmd_argv ? cmd_argv : argv);
1124 if (error)
1125 goto done;
1126 done:
1127 endwin();
1128 free(cmd_argv);
1129 if (error)
1130 fprintf(stderr, "%s: %s\n", getprogname(), error->msg);
1131 return 0;