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"
43 #define MIN(_a,_b) ((_a) < (_b) ? (_a) : (_b))
47 #define nitems(_a) (sizeof((_a)) / sizeof((_a)[0]))
52 const struct got_error *(*cmd_main)(int, char *[]);
53 void (*cmd_usage)(void);
57 __dead static void usage(void);
58 __dead static void usage_log(void);
59 __dead static void usage_diff(void);
60 __dead static void usage_blame(void);
62 static const struct got_error* cmd_log(int, char *[]);
63 static const struct got_error* cmd_diff(int, char *[]);
64 static const struct got_error* cmd_blame(int, char *[]);
66 static struct tog_cmd tog_commands[] = {
67 { "log", cmd_log, usage_log,
68 "show repository history" },
69 { "diff", cmd_diff, usage_diff,
70 "compare files and directories" },
71 { "blame", cmd_blame, usage_blame,
72 "show line-by-line file history" },
75 static struct tog_view {
78 } tog_log_view, tog_diff_view;
80 static const struct got_error *
81 show_diff_view(struct got_object *, struct got_object *,
82 struct got_repository *);
83 static const struct got_error *
84 show_log_view(struct got_object_id *, struct got_repository *);
90 fprintf(stderr, "usage: %s log [-c commit] [repository-path]\n",
95 /* Create newly allocated wide-character string equivalent to a byte string. */
96 static const struct got_error *
97 mbs2ws(wchar_t **ws, size_t *wlen, const char *s)
99 const struct got_error *err = NULL;
102 *wlen = mbstowcs(NULL, s, 0);
103 if (*wlen == (size_t)-1)
104 return got_error_from_errno();
106 *ws = calloc(*wlen + 1, sizeof(*ws));
108 return got_error_from_errno();
110 if (mbstowcs(*ws, s, *wlen) != *wlen)
111 err = got_error_from_errno();
121 /* Format a line for display, ensuring that it won't overflow a width limit. */
122 static const struct got_error *
123 format_line(wchar_t **wlinep, int *widthp, char *line, int wlimit)
125 const struct got_error *err = NULL;
127 wchar_t *wline = NULL;
133 err = mbs2ws(&wline, &wlen, line);
138 while (i < wlen && cols <= wlimit) {
139 int width = wcwidth(wline[i]);
148 if (wline[i] == L'\t')
152 err = got_error_from_errno();
170 static const struct got_error *
171 draw_commit(struct got_commit_object *commit, struct got_object_id *id)
173 const struct got_error *err = NULL;
174 char *logmsg0 = NULL, *logmsg = NULL;
175 char *author0 = NULL, *author = NULL;
176 wchar_t *wlogmsg = NULL, *wauthor = NULL;
177 int author_width, logmsg_width;
178 char *newline, *smallerthan;
183 static const size_t id_display_cols = 8;
184 static const size_t author_display_cols = 16;
185 const int avail = COLS;
187 err = got_object_id_str(&id_str, id);
190 id_len = strlen(id_str);
191 if (avail < id_display_cols) {
192 limit = MIN(id_len, avail);
193 waddnstr(tog_log_view.window, id_str, limit);
195 limit = MIN(id_display_cols, id_len);
196 waddnstr(tog_log_view.window, id_str, limit);
199 while (col <= avail && col < id_display_cols + 2) {
200 waddch(tog_log_view.window, ' ');
206 author0 = strdup(commit->author);
207 if (author0 == NULL) {
208 err = got_error_from_errno();
212 smallerthan = strchr(author, '<');
216 char *at = strchr(author, '@');
220 limit = MIN(avail, author_display_cols);
221 err = format_line(&wauthor, &author_width, author, limit);
224 waddwstr(tog_log_view.window, wauthor);
226 while (col <= avail && author_width < author_display_cols + 1) {
227 waddch(tog_log_view.window, ' ');
234 logmsg0 = strdup(commit->logmsg);
235 if (logmsg0 == NULL) {
236 err = got_error_from_errno();
240 while (*logmsg == '\n')
242 newline = strchr(logmsg, '\n');
246 err = format_line(&wlogmsg, &logmsg_width, logmsg, limit);
249 waddwstr(tog_log_view.window, wlogmsg);
251 while (col <= avail) {
252 waddch(tog_log_view.window, ' ');
265 struct commit_queue_entry {
266 TAILQ_ENTRY(commit_queue_entry) entry;
267 struct got_object_id *id;
268 struct got_commit_object *commit;
270 TAILQ_HEAD(commit_queue, commit_queue_entry);
272 static struct commit_queue_entry *
273 alloc_commit_queue_entry(struct got_commit_object *commit,
274 struct got_object_id *id)
276 struct commit_queue_entry *entry;
278 entry = calloc(1, sizeof(*entry));
283 entry->commit = commit;
288 pop_commit(struct commit_queue *commits)
290 struct commit_queue_entry *entry;
292 entry = TAILQ_FIRST(commits);
293 TAILQ_REMOVE(commits, entry, entry);
294 got_object_commit_close(entry->commit);
300 free_commits(struct commit_queue *commits)
302 while (!TAILQ_EMPTY(commits))
306 static const struct got_error *
307 fetch_parent_commit(struct commit_queue_entry **pentry,
308 struct commit_queue_entry *entry, struct got_repository *repo)
310 const struct got_error *err = NULL;
311 struct got_object *obj = NULL;
312 struct got_commit_object *commit;
313 struct got_object_id *id;
314 struct got_parent_id *pid;
318 /* Follow the first parent (TODO: handle merge commits). */
319 pid = SIMPLEQ_FIRST(&entry->commit->parent_ids);
322 err = got_object_open(&obj, repo, pid->id);
325 if (got_object_get_type(obj) != GOT_OBJ_TYPE_COMMIT) {
326 err = got_error(GOT_ERR_OBJ_TYPE);
327 got_object_close(obj);
331 err = got_object_commit_open(&commit, repo, obj);
332 got_object_close(obj);
336 id = got_object_id_dup(pid->id);
338 err = got_error_from_errno();
339 got_object_commit_close(commit);
343 *pentry = alloc_commit_queue_entry(commit, id);
344 if (*pentry == NULL) {
345 err = got_error_from_errno();
346 got_object_commit_close(commit);
352 static const struct got_error *
353 get_head_commit_id(struct got_object_id **head_id, struct got_repository *repo)
355 const struct got_error *err = NULL;
356 struct got_reference *head_ref;
360 err = got_ref_open(&head_ref, repo, GOT_REF_HEAD);
364 err = got_ref_resolve(head_id, repo, head_ref);
365 got_ref_close(head_ref);
374 static const struct got_error *
375 prepend_commits(int *ncommits, struct commit_queue *commits,
376 struct got_object_id *first_id, struct got_object_id *last_id,
377 int limit, struct got_repository *repo)
379 const struct got_error *err = NULL;
380 struct got_object *first_obj = NULL, *last_obj = NULL;
381 struct got_commit_object *commit = NULL;
382 struct got_object_id *id = NULL;
383 struct commit_queue_entry *entry, *old_head_entry;
387 err = got_object_open(&first_obj, repo, first_id);
390 if (got_object_get_type(first_obj) != GOT_OBJ_TYPE_COMMIT) {
391 err = got_error(GOT_ERR_OBJ_TYPE);
394 err = got_object_open(&last_obj, repo, last_id);
397 if (got_object_get_type(last_obj) != GOT_OBJ_TYPE_COMMIT) {
398 err = got_error(GOT_ERR_OBJ_TYPE);
402 err = got_object_commit_open(&commit, repo, first_obj);
406 id = got_object_id_dup(first_id);
408 err = got_error_from_errno();
412 entry = alloc_commit_queue_entry(commit, id);
414 return got_error_from_errno();
416 old_head_entry = TAILQ_FIRST(commits);
418 TAILQ_INSERT_BEFORE(old_head_entry, entry, entry);
420 TAILQ_INSERT_HEAD(commits, entry, entry);
425 * Fetch parent commits.
426 * XXX If first and last commit aren't ancestrally related this loop
427 * we will keep iterating until a root commit is encountered.
430 struct commit_queue_entry *pentry;
432 err = fetch_parent_commit(&pentry, entry, repo);
439 * Fill up to old HEAD commit if commit queue was not empty.
440 * We must not leave a gap in history.
442 if (old_head_entry &&
443 got_object_id_cmp(pentry->id, old_head_entry->id) == 0)
446 TAILQ_INSERT_AFTER(commits, entry, pentry, entry);
448 if (*ncommits >= limit)
451 /* Fill up to last requested commit if queue was empty. */
452 if (old_head_entry == NULL &&
453 got_object_id_cmp(pentry->id, last_id) == 0)
461 got_object_close(first_obj);
463 got_object_close(last_obj);
467 static const struct got_error *
468 fetch_commits(struct commit_queue_entry **start_entry,
469 struct got_object_id *start_id, struct commit_queue *commits,
470 int limit, struct got_repository *repo)
472 const struct got_error *err;
473 struct commit_queue_entry *entry;
475 struct got_object_id *head_id = NULL;
479 err = get_head_commit_id(&head_id, repo);
483 /* Prepend HEAD commit and all ancestors up to start commit. */
484 err = prepend_commits(&ncommits, commits, head_id, start_id, limit,
489 if (got_object_id_cmp(head_id, start_id) == 0)
490 *start_entry = TAILQ_FIRST(commits);
492 *start_entry = TAILQ_LAST(commits, commit_queue);
494 if (ncommits >= limit)
497 /* Append more commits from start commit up to the requested limit. */
498 entry = TAILQ_LAST(commits, commit_queue);
499 while (entry && ncommits < limit) {
500 struct commit_queue_entry *pentry;
502 err = fetch_parent_commit(&pentry, entry, repo);
506 TAILQ_INSERT_TAIL(commits, pentry, entry);
516 static const struct got_error *
517 draw_commits(struct commit_queue_entry **last, struct commit_queue_entry **selected,
518 struct commit_queue_entry *first, int selected_idx, int limit)
520 const struct got_error *err = NULL;
521 struct commit_queue_entry *entry;
524 werase(tog_log_view.window);
529 if (ncommits == limit)
531 if (ncommits == selected_idx) {
532 wstandout(tog_log_view.window);
535 err = draw_commit(entry->commit, entry->id);
536 if (ncommits == selected_idx)
537 wstandend(tog_log_view.window);
542 entry = TAILQ_NEXT(entry, entry);
552 scroll_up(struct commit_queue_entry **first_displayed_entry, int maxscroll,
553 struct commit_queue *commits)
555 struct commit_queue_entry *entry;
558 entry = TAILQ_FIRST(commits);
559 if (*first_displayed_entry == entry)
562 entry = *first_displayed_entry;
563 while (entry && nscrolled < maxscroll) {
564 entry = TAILQ_PREV(entry, commit_queue, entry);
566 *first_displayed_entry = entry;
572 static const struct got_error *
573 scroll_down(struct commit_queue_entry **first_displayed_entry, int maxscroll,
574 struct commit_queue_entry *last_displayed_entry,
575 struct commit_queue *commits, struct got_repository *repo)
577 const struct got_error *err = NULL;
578 struct commit_queue_entry *pentry;
582 pentry = TAILQ_NEXT(last_displayed_entry, entry);
583 if (pentry == NULL) {
584 err = fetch_parent_commit(&pentry,
585 last_displayed_entry, repo);
586 if (err || pentry == NULL)
588 TAILQ_INSERT_TAIL(commits, pentry, entry);
590 last_displayed_entry = pentry;
592 pentry = TAILQ_NEXT(*first_displayed_entry, entry);
595 *first_displayed_entry = pentry;
596 } while (++nscrolled < maxscroll);
602 num_parents(struct commit_queue_entry *entry)
607 entry = TAILQ_NEXT(entry, entry);
614 static const struct got_error *
615 show_commit(struct commit_queue_entry *entry, struct got_repository *repo)
617 const struct got_error *err;
618 struct commit_queue_entry *pentry;
619 struct got_object *obj1 = NULL, *obj2 = NULL;
621 err = got_object_open(&obj2, repo, entry->id);
625 pentry = TAILQ_NEXT(entry, entry);
626 if (pentry == NULL) {
627 err = fetch_parent_commit(&pentry, entry, repo);
632 err = got_object_open(&obj1, repo, pentry->id);
637 err = show_diff_view(obj1, obj2, repo);
640 got_object_close(obj1);
642 got_object_close(obj2);
646 static const struct got_error *
647 show_log_view(struct got_object_id *start_id, struct got_repository *repo)
649 const struct got_error *err = NULL;
650 struct got_object_id *id;
651 int ch, done = 0, selected = 0, nparents;
652 struct commit_queue commits;
653 struct commit_queue_entry *first_displayed_entry = NULL;
654 struct commit_queue_entry *last_displayed_entry = NULL;
655 struct commit_queue_entry *selected_entry = NULL;
657 id = got_object_id_dup(start_id);
659 return got_error_from_errno();
661 if (tog_log_view.window == NULL) {
662 tog_log_view.window = newwin(0, 0, 0, 0);
663 if (tog_log_view.window == NULL)
664 return got_error_from_errno();
665 keypad(tog_log_view.window, TRUE);
667 if (tog_log_view.panel == NULL) {
668 tog_log_view.panel = new_panel(tog_log_view.window);
669 if (tog_log_view.panel == NULL)
670 return got_error_from_errno();
672 show_panel(tog_log_view.panel);
674 TAILQ_INIT(&commits);
675 err = fetch_commits(&first_displayed_entry, id, &commits, LINES, repo);
679 err = draw_commits(&last_displayed_entry, &selected_entry,
680 first_displayed_entry, selected, LINES);
684 nodelay(stdscr, FALSE);
685 ch = wgetch(tog_log_view.window);
686 nodelay(stdscr, TRUE);
690 err = got_error_from_errno();
703 scroll_up(&first_displayed_entry, 1, &commits);
706 if (TAILQ_FIRST(&commits) ==
707 first_displayed_entry) {
711 scroll_up(&first_displayed_entry, LINES,
716 nparents = num_parents(first_displayed_entry);
717 if (selected < LINES - 1 &&
718 selected < nparents - 1) {
722 err = scroll_down(&first_displayed_entry, 1,
723 last_displayed_entry, &commits, repo);
728 err = scroll_down(&first_displayed_entry, LINES,
729 last_displayed_entry, &commits, repo);
732 if (last_displayed_entry->commit->nparents > 0)
734 /* can't scroll any further; move cursor down */
735 nparents = num_parents(first_displayed_entry);
736 if (selected < LINES - 1 ||
737 selected < nparents - 1)
738 selected = MIN(LINES - 1, nparents - 1);
741 if (selected > LINES)
742 selected = LINES - 1;
746 err = show_commit(selected_entry, repo);
749 show_panel(tog_log_view.panel);
756 free_commits(&commits);
760 static const struct got_error *
761 cmd_log(int argc, char *argv[])
763 const struct got_error *error;
764 struct got_repository *repo;
765 struct got_object_id *start_id = NULL;
766 char *repo_path = NULL;
767 char *start_commit = NULL;
771 if (pledge("stdio rpath wpath cpath flock proc tty", NULL) == -1)
775 while ((ch = getopt(argc, argv, "c:")) != -1) {
778 start_commit = optarg;
790 repo_path = getcwd(NULL, 0);
791 if (repo_path == NULL)
792 return got_error_from_errno();
793 } else if (argc == 1) {
794 repo_path = realpath(argv[0], NULL);
795 if (repo_path == NULL)
796 return got_error_from_errno();
800 error = got_repo_open(&repo, repo_path);
805 if (start_commit == NULL) {
806 error = get_head_commit_id(&start_id, repo);
810 struct got_object *obj;
811 error = got_object_open_by_id_str(&obj, repo, start_commit);
813 start_id = got_object_get_id(obj);
814 if (start_id == NULL)
815 error = got_error_from_errno();
820 error = show_log_view(start_id, repo);
822 got_repo_close(repo);
830 fprintf(stderr, "usage: %s diff [repository-path] object1 object2\n",
836 parse_next_line(FILE *f, size_t *len)
841 const char delim[3] = { '\0', '\0', '\0'};
843 line = fparseln(f, &linelen, &lineno, delim, 0);
849 static const struct got_error *
850 draw_diff(FILE *f, int *first_displayed_line, int *last_displayed_line,
851 int *eof, int max_lines)
853 const struct got_error *err;
854 int nlines = 0, nprinted = 0;
861 werase(tog_diff_view.window);
864 while (nprinted < max_lines) {
865 line = parse_next_line(f, &len);
870 if (++nlines < *first_displayed_line) {
875 err = format_line(&wline, &width, line, COLS);
880 waddwstr(tog_diff_view.window, wline);
882 waddch(tog_diff_view.window, '\n');
884 *first_displayed_line = nlines;
887 *last_displayed_line = nlines;
895 static const struct got_error *
896 show_diff_view(struct got_object *obj1, struct got_object *obj2,
897 struct got_repository *repo)
899 const struct got_error *err;
901 int ch, done = 0, first_displayed_line = 1, last_displayed_line = LINES;
904 if (obj1 != NULL && obj2 != NULL &&
905 got_object_get_type(obj1) != got_object_get_type(obj2))
906 return got_error(GOT_ERR_OBJ_TYPE);
910 return got_error_from_errno();
912 switch (got_object_get_type(obj1 ? obj1 : obj2)) {
913 case GOT_OBJ_TYPE_BLOB:
914 err = got_diff_objects_as_blobs(obj1, obj2, repo, f);
916 case GOT_OBJ_TYPE_TREE:
917 err = got_diff_objects_as_trees(obj1, obj2, repo, f);
919 case GOT_OBJ_TYPE_COMMIT:
920 err = got_diff_objects_as_commits(obj1, obj2, repo, f);
923 return got_error(GOT_ERR_OBJ_TYPE);
928 if (tog_diff_view.window == NULL) {
929 tog_diff_view.window = newwin(0, 0, 0, 0);
930 if (tog_diff_view.window == NULL)
931 return got_error_from_errno();
932 keypad(tog_diff_view.window, TRUE);
934 if (tog_diff_view.panel == NULL) {
935 tog_diff_view.panel = new_panel(tog_diff_view.window);
936 if (tog_diff_view.panel == NULL)
937 return got_error_from_errno();
939 show_panel(tog_diff_view.panel);
942 err = draw_diff(f, &first_displayed_line, &last_displayed_line,
946 nodelay(stdscr, FALSE);
947 ch = wgetch(tog_diff_view.window);
948 nodelay(stdscr, TRUE);
956 if (first_displayed_line > 1)
957 first_displayed_line--;
961 while (i++ < LINES - 1 &&
962 first_displayed_line > 1)
963 first_displayed_line--;
970 first_displayed_line++;
975 while (!eof && i++ < LINES - 1) {
976 char *line = parse_next_line(f, NULL);
977 first_displayed_line++;
990 static const struct got_error *
991 cmd_diff(int argc, char *argv[])
993 const struct got_error *error = NULL;
994 struct got_repository *repo = NULL;
995 struct got_object *obj1 = NULL, *obj2 = NULL;
996 char *repo_path = NULL;
997 char *obj_id_str1 = NULL, *obj_id_str2 = NULL;
1001 if (pledge("stdio rpath wpath cpath flock proc tty", NULL) == -1)
1005 while ((ch = getopt(argc, argv, "")) != -1) {
1017 usage_diff(); /* TODO show local worktree changes */
1018 } else if (argc == 2) {
1019 repo_path = getcwd(NULL, 0);
1020 if (repo_path == NULL)
1021 return got_error_from_errno();
1022 obj_id_str1 = argv[0];
1023 obj_id_str2 = argv[1];
1024 } else if (argc == 3) {
1025 repo_path = realpath(argv[0], NULL);
1026 if (repo_path == NULL)
1027 return got_error_from_errno();
1028 obj_id_str1 = argv[1];
1029 obj_id_str2 = argv[2];
1033 error = got_repo_open(&repo, repo_path);
1038 error = got_object_open_by_id_str(&obj1, repo, obj_id_str1);
1042 error = got_object_open_by_id_str(&obj2, repo, obj_id_str2);
1046 error = show_diff_view(obj1, obj2, repo);
1048 got_repo_close(repo);
1050 got_object_close(obj1);
1052 got_object_close(obj2);
1060 fprintf(stderr, "usage: %s blame [repository-path] blob-object\n",
1065 static const struct got_error *
1066 cmd_blame(int argc, char *argv[])
1068 return got_error(GOT_ERR_NOT_IMPL);
1078 intrflush(stdscr, FALSE);
1079 keypad(stdscr, TRUE);
1088 fprintf(stderr, "usage: %s [-h] [command] [arg ...]\n\n"
1089 "Available commands:\n", getprogname());
1090 for (i = 0; i < nitems(tog_commands); i++) {
1091 struct tog_cmd *cmd = &tog_commands[i];
1092 fprintf(stderr, " %s: %s\n", cmd->name, cmd->descr);
1098 make_argv(const char *arg0, const char *arg1)
1101 int argc = (arg1 == NULL ? 1 : 2);
1103 argv = calloc(argc, sizeof(char *));
1106 argv[0] = strdup(arg0);
1107 if (argv[0] == NULL)
1110 argv[1] = strdup(arg1);
1111 if (argv[1] == NULL)
1119 main(int argc, char *argv[])
1121 const struct got_error *error = NULL;
1122 struct tog_cmd *cmd = NULL;
1124 char **cmd_argv = NULL;
1126 setlocale(LC_ALL, "");
1128 while ((ch = getopt(argc, argv, "h")) != -1) {
1145 /* Build an argument vector which runs a default command. */
1146 cmd = &tog_commands[0];
1147 cmd_argv = make_argv(cmd->name, NULL);
1152 /* Did the user specific a command? */
1153 for (i = 0; i < nitems(tog_commands); i++) {
1154 if (strncmp(tog_commands[i].name, argv[0],
1155 strlen(argv[0])) == 0) {
1156 cmd = &tog_commands[i];
1158 tog_commands[i].cmd_usage();
1163 /* Did the user specify a repository? */
1164 char *repo_path = realpath(argv[0], NULL);
1166 struct got_repository *repo;
1167 error = got_repo_open(&repo, repo_path);
1169 got_repo_close(repo);
1171 error = got_error_from_errno();
1173 fprintf(stderr, "%s: '%s' is neither a known "
1174 "command nor a path to a repository\n",
1175 getprogname(), argv[0]);
1179 cmd = &tog_commands[0];
1180 cmd_argv = make_argv(cmd->name, repo_path);
1188 error = cmd->cmd_main(argc, cmd_argv ? cmd_argv : argv);
1195 fprintf(stderr, "%s: %s\n", getprogname(), error->msg);