2 * Copyright (c) 2018 Stefan Sperling <stsp@openbsd.org>
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.
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.
17 #include <sys/queue.h>
20 #define _XOPEN_SOURCE_EXTENDED
22 #undef _XOPEN_SOURCE_EXTENDED
35 #include "got_error.h"
36 #include "got_object.h"
37 #include "got_reference.h"
38 #include "got_repository.h"
40 #include "got_opentemp.h"
41 #include "got_commit_graph.h"
44 #define MIN(_a,_b) ((_a) < (_b) ? (_a) : (_b))
48 #define nitems(_a) (sizeof((_a)) / sizeof((_a)[0]))
53 const struct got_error *(*cmd_main)(int, char *[]);
54 void (*cmd_usage)(void);
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" },
76 static struct tog_view {
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 *);
91 fprintf(stderr, "usage: %s log [-c commit] [repository-path]\n",
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)
100 const struct got_error *err = 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));
109 return got_error_from_errno();
111 if (mbstowcs(*ws, s, *wlen) != *wlen)
112 err = got_error_from_errno();
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;
128 wchar_t *wline = NULL;
134 err = mbs2ws(&wline, &wlen, line);
139 while (i < wlen && cols <= wlimit) {
140 int width = wcwidth(wline[i]);
149 if (wline[i] == L'\t')
153 err = got_error_from_errno();
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;
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);
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);
196 limit = MIN(id_display_cols, id_len);
197 waddnstr(tog_log_view.window, id_str, limit);
200 while (col <= avail && col < id_display_cols + 2) {
201 waddch(tog_log_view.window, ' ');
207 author0 = strdup(commit->author);
208 if (author0 == NULL) {
209 err = got_error_from_errno();
213 smallerthan = strchr(author, '<');
217 char *at = strchr(author, '@');
221 limit = MIN(avail, author_display_cols);
222 err = format_line(&wauthor, &author_width, author, limit);
225 waddwstr(tog_log_view.window, wauthor);
227 while (col <= avail && author_width < author_display_cols + 1) {
228 waddch(tog_log_view.window, ' ');
235 logmsg0 = strdup(commit->logmsg);
236 if (logmsg0 == NULL) {
237 err = got_error_from_errno();
241 while (*logmsg == '\n')
243 newline = strchr(logmsg, '\n');
247 err = format_line(&wlogmsg, &logmsg_width, logmsg, limit);
250 waddwstr(tog_log_view.window, wlogmsg);
252 while (col <= avail) {
253 waddch(tog_log_view.window, ' ');
266 struct commit_queue_entry {
267 TAILQ_ENTRY(commit_queue_entry) entry;
268 struct got_object_id *id;
269 struct got_commit_object *commit;
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));
284 entry->commit = commit;
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. */
301 free_commits(struct commit_queue *commits)
303 while (!TAILQ_EMPTY(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);
319 entry = TAILQ_LAST(commits, commit_queue);
320 if (entry && got_object_id_cmp(entry->id, start_id) == 0) {
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)
328 err = got_commit_graph_fetch_commits(&nfetched, graph, 1, repo);
334 struct got_commit_object *commit;
336 err = got_commit_graph_iter_next(&id, graph);
338 if (err->code == GOT_ERR_ITER_NEED_MORE)
343 err = got_object_open_as_commit(&commit, repo, id);
347 entry = alloc_commit_queue_entry(commit, id);
349 err = got_error_from_errno();
353 TAILQ_INSERT_TAIL(commits, entry, entry);
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;
369 /* Populate commit graph with entry's parent commits. */
370 SIMPLEQ_FOREACH(qid, &entry->commit->parent_ids, entry) {
372 err = got_commit_graph_fetch_commits_up_to(&nfetched,
373 graph, qid->id, repo);
378 /* Append outstanding commits to queue in graph sort order. */
379 err = queue_commits(graph, commits, entry->id, repo);
381 if (err->code == GOT_ERR_ITER_COMPLETED)
386 /* Next entry to display should now be available. */
387 *pentry = TAILQ_NEXT(entry, entry);
389 return got_error(GOT_ERR_NO_OBJ);
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;
402 err = got_ref_open(&head_ref, repo, GOT_REF_HEAD);
406 err = got_ref_resolve(head_id, repo, head_ref);
407 got_ref_close(head_ref);
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;
424 werase(tog_log_view.window);
429 if (ncommits == limit)
431 if (ncommits == selected_idx) {
432 wstandout(tog_log_view.window);
435 err = draw_commit(entry->commit, entry->id);
436 if (ncommits == selected_idx)
437 wstandend(tog_log_view.window);
442 entry = TAILQ_NEXT(entry, entry);
452 scroll_up(struct commit_queue_entry **first_displayed_entry, int maxscroll,
453 struct commit_queue *commits)
455 struct commit_queue_entry *entry;
458 entry = TAILQ_FIRST(commits);
459 if (*first_displayed_entry == entry)
462 entry = *first_displayed_entry;
463 while (entry && nscrolled < maxscroll) {
464 entry = TAILQ_PREV(entry, commit_queue, entry);
466 *first_displayed_entry = entry;
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;
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)
490 last_displayed_entry = pentry;
492 pentry = TAILQ_NEXT(*first_displayed_entry, entry);
495 *first_displayed_entry = pentry;
496 } while (++nscrolled < maxscroll);
502 num_parents(struct commit_queue_entry *entry)
507 entry = TAILQ_NEXT(entry, entry);
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);
525 parent_id = SIMPLEQ_FIRST(&entry->commit->parent_ids);
527 err = got_object_open(&obj1, repo, parent_id->id);
532 err = show_diff_view(obj1, obj2, repo);
535 got_object_close(obj1);
537 got_object_close(obj2);
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();
565 show_panel(tog_log_view.panel);
567 err = get_head_commit_id(&head_id, repo);
571 TAILQ_INIT(&commits);
573 err = got_commit_graph_open(&graph, head_id, repo);
577 /* Populate commit graph with a sufficient number of commits. */
578 err = got_commit_graph_fetch_commits_up_to(&nfetched, graph, start_id,
582 err = got_commit_graph_fetch_commits(&nfetched, graph, LINES, repo);
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.
592 err = queue_commits(graph, &commits, head_id, repo);
593 if (err && err->code != GOT_ERR_ITER_COMPLETED)
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;
603 if (first_displayed_entry == NULL) {
604 err = got_error(GOT_ERR_NO_OBJ);
609 err = draw_commits(&last_displayed_entry, &selected_entry,
610 first_displayed_entry, selected, LINES);
614 nodelay(stdscr, FALSE);
615 ch = wgetch(tog_log_view.window);
616 nodelay(stdscr, TRUE);
620 err = got_error_from_errno();
633 scroll_up(&first_displayed_entry, 1, &commits);
636 if (TAILQ_FIRST(&commits) ==
637 first_displayed_entry) {
641 scroll_up(&first_displayed_entry, LINES,
646 nparents = num_parents(first_displayed_entry);
647 if (selected < LINES - 1 &&
648 selected < nparents - 1) {
652 err = scroll_down(&first_displayed_entry, 1,
653 last_displayed_entry, &commits, graph,
659 err = scroll_down(&first_displayed_entry, LINES,
660 last_displayed_entry, &commits, graph,
664 if (last_displayed_entry->commit->nparents > 0)
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);
673 if (selected > LINES)
674 selected = LINES - 1;
678 err = show_commit(selected_entry, repo);
681 show_panel(tog_log_view.panel);
690 got_commit_graph_close(graph);
691 free_commits(&commits);
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;
706 if (pledge("stdio rpath wpath cpath flock proc tty", NULL) == -1)
710 while ((ch = getopt(argc, argv, "c:")) != -1) {
713 start_commit = optarg;
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();
735 error = got_repo_open(&repo, repo_path);
740 if (start_commit == NULL) {
741 error = get_head_commit_id(&start_id, repo);
745 struct got_object *obj;
746 error = got_object_open_by_id_str(&obj, repo, start_commit);
748 start_id = got_object_get_id(obj);
749 if (start_id == NULL)
750 error = got_error_from_errno();
755 error = show_log_view(start_id, repo);
757 got_repo_close(repo);
765 fprintf(stderr, "usage: %s diff [repository-path] object1 object2\n",
771 parse_next_line(FILE *f, size_t *len)
776 const char delim[3] = { '\0', '\0', '\0'};
778 line = fparseln(f, &linelen, &lineno, delim, 0);
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;
796 werase(tog_diff_view.window);
799 while (nprinted < max_lines) {
800 line = parse_next_line(f, &len);
805 if (++nlines < *first_displayed_line) {
810 err = format_line(&wline, &width, line, COLS);
815 waddwstr(tog_diff_view.window, wline);
817 waddch(tog_diff_view.window, '\n');
819 *first_displayed_line = nlines;
822 *last_displayed_line = nlines;
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;
836 int ch, done = 0, first_displayed_line = 1, last_displayed_line = LINES;
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);
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);
851 case GOT_OBJ_TYPE_TREE:
852 err = got_diff_objects_as_trees(obj1, obj2, repo, f);
854 case GOT_OBJ_TYPE_COMMIT:
855 err = got_diff_objects_as_commits(obj1, obj2, repo, f);
858 return got_error(GOT_ERR_OBJ_TYPE);
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();
874 show_panel(tog_diff_view.panel);
877 err = draw_diff(f, &first_displayed_line, &last_displayed_line,
881 nodelay(stdscr, FALSE);
882 ch = wgetch(tog_diff_view.window);
883 nodelay(stdscr, TRUE);
891 if (first_displayed_line > 1)
892 first_displayed_line--;
896 while (i++ < LINES - 1 &&
897 first_displayed_line > 1)
898 first_displayed_line--;
905 first_displayed_line++;
910 while (!eof && i++ < LINES - 1) {
911 char *line = parse_next_line(f, NULL);
912 first_displayed_line++;
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;
936 if (pledge("stdio rpath wpath cpath flock proc tty", NULL) == -1)
940 while ((ch = getopt(argc, argv, "")) != -1) {
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];
968 error = got_repo_open(&repo, repo_path);
973 error = got_object_open_by_id_str(&obj1, repo, obj_id_str1);
977 error = got_object_open_by_id_str(&obj2, repo, obj_id_str2);
981 error = show_diff_view(obj1, obj2, repo);
983 got_repo_close(repo);
985 got_object_close(obj1);
987 got_object_close(obj2);
995 fprintf(stderr, "usage: %s blame [repository-path] blob-object\n",
1000 static const struct got_error *
1001 cmd_blame(int argc, char *argv[])
1003 return got_error(GOT_ERR_NOT_IMPL);
1013 intrflush(stdscr, FALSE);
1014 keypad(stdscr, TRUE);
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);
1033 make_argv(const char *arg0, const char *arg1)
1036 int argc = (arg1 == NULL ? 1 : 2);
1038 argv = calloc(argc, sizeof(char *));
1041 argv[0] = strdup(arg0);
1042 if (argv[0] == NULL)
1045 argv[1] = strdup(arg1);
1046 if (argv[1] == NULL)
1054 main(int argc, char *argv[])
1056 const struct got_error *error = NULL;
1057 struct tog_cmd *cmd = NULL;
1059 char **cmd_argv = NULL;
1061 setlocale(LC_ALL, "");
1063 while ((ch = getopt(argc, argv, "h")) != -1) {
1080 /* Build an argument vector which runs a default command. */
1081 cmd = &tog_commands[0];
1082 cmd_argv = make_argv(cmd->name, NULL);
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];
1093 tog_commands[i].cmd_usage();
1098 /* Did the user specify a repository? */
1099 char *repo_path = realpath(argv[0], NULL);
1101 struct got_repository *repo;
1102 error = got_repo_open(&repo, repo_path);
1104 got_repo_close(repo);
1106 error = got_error_from_errno();
1108 fprintf(stderr, "%s: '%s' is neither a known "
1109 "command nor a path to a repository\n",
1110 getprogname(), argv[0]);
1114 cmd = &tog_commands[0];
1115 cmd_argv = make_argv(cmd->name, repo_path);
1123 error = cmd->cmd_main(argc, cmd_argv ? cmd_argv : argv);
1130 fprintf(stderr, "%s: %s\n", getprogname(), error->msg);