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 <curses.h>
20 #include <panel.h>
21 #include <locale.h>
22 #include <stdlib.h>
23 #include <getopt.h>
24 #include <string.h>
25 #include <err.h>
26 #include <unistd.h>
28 #include "got_error.h"
29 #include "got_object.h"
30 #include "got_reference.h"
31 #include "got_repository.h"
32 #include "got_diff.h"
34 #ifndef nitems
35 #define nitems(_a) (sizeof((_a)) / sizeof((_a)[0]))
36 #endif
38 enum tog_view_id {
39 TOG_VIEW_LOG,
40 TOG_VIEW_DIFF,
41 TOG_VIEW_BLAME,
42 };
44 struct tog_cmd {
45 const char *name;
46 const struct got_error *(*cmd_main)(int, char *[]);
47 void (*cmd_usage)(void);
48 enum tog_view_id view;
49 const char *descr;
50 };
52 __dead void usage(void);
53 __dead void usage_log(void);
54 __dead void usage_diff(void);
55 __dead void usage_blame(void);
57 const struct got_error* cmd_log(int, char *[]);
58 const struct got_error* cmd_diff(int, char *[]);
59 const struct got_error* cmd_blame(int, char *[]);
61 struct tog_cmd tog_commands[] = {
62 { "log", cmd_log, usage_log, TOG_VIEW_LOG,
63 "show repository history" },
64 { "diff", cmd_diff, usage_diff, TOG_VIEW_DIFF,
65 "compare files and directories" },
66 { "blame", cmd_blame, usage_blame, TOG_VIEW_BLAME,
67 "show line-by-line file history" },
68 };
70 /* globals */
71 WINDOW *tog_main_win;
72 PANEL *tog_main_panel;
73 static struct tog_log_view {
74 WINDOW *window;
75 PANEL *panel;
76 } tog_log_view;
78 __dead void
79 usage_log(void)
80 {
81 endwin();
82 fprintf(stderr, "usage: %s log [-c commit] [repository-path]\n",
83 getprogname());
84 exit(1);
85 }
87 static const struct got_error *
88 draw_commit(struct got_commit_object *commit, struct got_object_id *id,
89 struct got_repository *repo)
90 {
91 const struct got_error *err = NULL;
92 char *logmsg0 = NULL, *logmsg = NULL;
93 char *author0 = NULL, *author = NULL;
94 char *newline, *smallerthan;
95 char *line = NULL;
96 char *id_str = NULL;
97 size_t len;
99 err = got_object_id_str(&id_str, id);
100 if (err)
101 return err;
102 logmsg0 = strdup(commit->logmsg);
103 if (logmsg0 == NULL) {
104 err = got_error_from_errno();
105 goto done;
107 logmsg = logmsg0;
108 while (*logmsg == '\n')
109 logmsg++;
110 newline = strchr(logmsg, '\n');
111 if (newline)
112 *newline = '\0';
114 author0 = strdup(commit->author);
115 if (author0 == NULL) {
116 err = got_error_from_errno();
117 goto done;
119 author = author0;
120 smallerthan = strchr(author, '<');
121 if (smallerthan)
122 *smallerthan = '\0';
123 else {
124 char *at = strchr(author, '@');
125 if (at)
126 *at = '\0';
129 if (asprintf(&line, "%.8s %.20s %s", id_str, author, logmsg) == -1) {
130 err = got_error_from_errno();
131 goto done;
134 waddstr(tog_log_view.window, line);
135 len = strlen(line);
136 while (len < COLS - 1) {
137 waddch(tog_log_view.window, ' ');
138 len++;
140 waddch(tog_log_view.window, '\n');
141 done:
142 free(logmsg0);
143 free(author0);
144 free(line);
145 free(id_str);
146 return err;
148 struct commit_queue_entry {
149 TAILQ_ENTRY(commit_queue_entry) entry;
150 struct got_object_id *id;
151 struct got_commit_object *commit;
152 };
154 static const struct got_error *
155 draw_commits(struct got_object *root_obj, struct got_object_id *root_id,
156 struct got_repository *repo, int selected)
158 const struct got_error *err;
159 struct got_commit_object *root_commit;
160 TAILQ_HEAD(, commit_queue_entry) commits;
161 struct commit_queue_entry *entry;
162 int ncommits = 0;
164 TAILQ_INIT(&commits);
166 err = got_object_commit_open(&root_commit, repo, root_obj);
167 if (err)
168 return err;
170 entry = calloc(1, sizeof(*entry));
171 if (entry == NULL)
172 return got_error_from_errno();
173 entry->id = got_object_id_dup(root_id);
174 if (entry->id == NULL) {
175 err = got_error_from_errno();
176 free(entry);
177 return err;
179 entry->commit = root_commit;
180 TAILQ_INSERT_HEAD(&commits, entry, entry);
182 wclear(tog_log_view.window);
184 while (!TAILQ_EMPTY(&commits) && ncommits < LINES) {
185 struct got_parent_id *pid;
186 struct got_object *obj;
187 struct got_commit_object *pcommit;
188 struct commit_queue_entry *pentry;
190 entry = TAILQ_FIRST(&commits);
192 if (ncommits == selected)
193 wstandout(tog_log_view.window);
194 err = draw_commit(entry->commit, entry->id, repo);
195 if (ncommits == selected)
196 wstandend(tog_log_view.window);
197 if (err)
198 break;
199 ncommits++;
201 if (entry->commit->nparents == 0)
202 break;
204 /* Follow the first parent (TODO: handle merge commits). */
205 pid = SIMPLEQ_FIRST(&entry->commit->parent_ids);
206 err = got_object_open(&obj, repo, pid->id);
207 if (err)
208 break;
209 if (got_object_get_type(obj) != GOT_OBJ_TYPE_COMMIT) {
210 err = got_error(GOT_ERR_OBJ_TYPE);
211 break;
214 err = got_object_commit_open(&pcommit, repo, obj);
215 got_object_close(obj);
216 if (err)
217 break;
219 pentry = calloc(1, sizeof(*pentry));
220 if (pentry == NULL) {
221 err = got_error_from_errno();
222 got_object_commit_close(pcommit);
223 break;
225 pentry->id = got_object_id_dup(pid->id);
226 if (pentry->id == NULL) {
227 err = got_error_from_errno();
228 got_object_commit_close(pcommit);
229 break;
231 pentry->commit = pcommit;
232 TAILQ_INSERT_TAIL(&commits, pentry, entry);
234 TAILQ_REMOVE(&commits, entry, entry);
235 got_object_commit_close(entry->commit);
236 free(entry->id);
237 free(entry);
240 while (!TAILQ_EMPTY(&commits)) {
241 entry = TAILQ_FIRST(&commits);
242 TAILQ_REMOVE(&commits, entry, entry);
243 got_object_commit_close(entry->commit);
244 free(entry->id);
245 free(entry);
248 update_panels();
249 doupdate();
250 return err;
254 static const struct got_error *
255 show_log_view(struct got_object_id *start_id, struct got_repository *repo)
257 const struct got_error *err = NULL;
258 struct got_object *obj;
259 int ch, done = 0, selected = 0;
260 struct got_object_id *id = start_id;
262 if (tog_log_view.window == NULL) {
263 tog_log_view.window = newwin(0, 0, 0, 0);
264 if (tog_log_view.window == NULL)
265 return got_error_from_errno();
266 keypad(tog_log_view.window, TRUE);
268 if (tog_log_view.panel == NULL) {
269 tog_log_view.panel = new_panel(tog_log_view.window);
270 if (tog_log_view.panel == NULL)
271 return got_error_from_errno();
274 err = got_object_open(&obj, repo, id);
275 if (err)
276 return err;
277 if (got_object_get_type(obj) != GOT_OBJ_TYPE_COMMIT) {
278 err = got_error(GOT_ERR_OBJ_TYPE);
279 goto done;
282 do {
283 err = draw_commits(obj, id, repo, selected);
284 if (err)
285 return err;
287 nodelay(stdscr, FALSE);
288 ch = wgetch(tog_log_view.window);
289 switch (ch) {
290 case 'q':
291 done = 1;
292 break;
293 case 'k':
294 case KEY_UP:
295 if (selected > 0)
296 selected--;
297 break;
298 case 'j':
299 case KEY_DOWN:
300 if (selected < LINES - 1)
301 selected++;
302 break;
303 default:
304 break;
306 nodelay(stdscr, TRUE);
307 } while (!done);
308 done:
309 got_object_close(obj);
310 return err;
313 const struct got_error *
314 cmd_log(int argc, char *argv[])
316 const struct got_error *error;
317 struct got_repository *repo;
318 struct got_object_id *id = NULL;
319 char *repo_path = NULL;
320 char *start_commit = NULL;
321 int ch;
323 #ifndef PROFILE
324 if (pledge("stdio rpath wpath cpath flock proc tty", NULL) == -1)
325 err(1, "pledge");
326 #endif
328 while ((ch = getopt(argc, argv, "c:")) != -1) {
329 switch (ch) {
330 case 'c':
331 start_commit = optarg;
332 break;
333 default:
334 usage();
335 /* NOTREACHED */
339 argc -= optind;
340 argv += optind;
342 if (argc == 0) {
343 repo_path = getcwd(NULL, 0);
344 if (repo_path == NULL)
345 return got_error_from_errno();
346 } else if (argc == 1) {
347 repo_path = realpath(argv[0], NULL);
348 if (repo_path == NULL)
349 return got_error_from_errno();
350 } else
351 usage_log();
353 error = got_repo_open(&repo, repo_path);
354 free(repo_path);
355 if (error != NULL)
356 return error;
358 if (start_commit == NULL) {
359 struct got_reference *head_ref;
360 error = got_ref_open(&head_ref, repo, GOT_REF_HEAD);
361 if (error != NULL)
362 return error;
363 error = got_ref_resolve(&id, repo, head_ref);
364 got_ref_close(head_ref);
365 if (error != NULL)
366 return error;
367 } else {
368 struct got_object *obj;
369 error = got_object_open_by_id_str(&obj, repo, start_commit);
370 if (error == NULL) {
371 id = got_object_get_id(obj);
372 if (id == NULL)
373 error = got_error_from_errno();
376 if (error != NULL)
377 return error;
378 error = show_log_view(id, repo);
379 free(id);
380 got_repo_close(repo);
381 return error;
384 __dead void
385 usage_diff(void)
387 endwin();
388 fprintf(stderr, "usage: %s diff [repository-path] object1 object2\n",
389 getprogname());
390 exit(1);
393 const struct got_error *
394 cmd_diff(int argc, char *argv[])
396 return got_error(GOT_ERR_NOT_IMPL);
399 __dead void
400 usage_blame(void)
402 endwin();
403 fprintf(stderr, "usage: %s blame [repository-path] blob-object\n",
404 getprogname());
405 exit(1);
408 const struct got_error *
409 cmd_blame(int argc, char *argv[])
411 return got_error(GOT_ERR_NOT_IMPL);
414 static const struct got_error *
415 init_curses(void)
417 initscr();
418 cbreak();
419 noecho();
420 nonl();
421 intrflush(stdscr, FALSE);
422 keypad(stdscr, TRUE);
424 tog_main_win = newwin(0, 0, 0, 0);
425 if (tog_main_win == NULL)
426 return got_error_from_errno();
427 tog_main_panel = new_panel(tog_main_win);
428 if (tog_main_panel == NULL)
429 return got_error_from_errno();
431 return NULL;
434 __dead void
435 usage(void)
437 int i;
439 fprintf(stderr, "usage: %s [-h] [command] [arg ...]\n\n"
440 "Available commands:\n", getprogname());
441 for (i = 0; i < nitems(tog_commands); i++) {
442 struct tog_cmd *cmd = &tog_commands[i];
443 fprintf(stderr, " %s: %s\n", cmd->name, cmd->descr);
445 exit(1);
448 static char **
449 make_argv(const char *arg0, const char *arg1)
451 char **argv;
452 int argc = (arg1 == NULL ? 1 : 2);
454 argv = calloc(argc, sizeof(char *));
455 if (argv == NULL)
456 err(1, "calloc");
457 argv[0] = strdup(arg0);
458 if (argv[0] == NULL)
459 err(1, "calloc");
460 if (arg1) {
461 argv[1] = strdup(arg1);
462 if (argv[1] == NULL)
463 err(1, "calloc");
466 return argv;
469 int
470 main(int argc, char *argv[])
472 const struct got_error *error = NULL;
473 struct tog_cmd *cmd = NULL;
474 int ch, hflag = 0;
475 char **cmd_argv = NULL;
477 setlocale(LC_ALL, "");
479 while ((ch = getopt(argc, argv, "h")) != -1) {
480 switch (ch) {
481 case 'h':
482 hflag = 1;
483 break;
484 default:
485 usage();
486 /* NOTREACHED */
490 argc -= optind;
491 argv += optind;
492 optind = 0;
493 optreset = 1;
495 if (argc == 0) {
496 /* Build an argument vector which runs a default command. */
497 cmd = &tog_commands[0];
498 cmd_argv = make_argv(cmd->name, NULL);
499 argc = 1;
500 } else {
501 int i;
503 /* Did the user specific a command? */
504 for (i = 0; i < nitems(tog_commands); i++) {
505 if (strncmp(tog_commands[i].name, argv[0],
506 strlen(argv[0])) == 0) {
507 cmd = &tog_commands[i];
508 if (hflag)
509 tog_commands[i].cmd_usage();
510 break;
513 if (cmd == NULL) {
514 /* Did the user specify a repository? */
515 char *repo_path = realpath(argv[0], NULL);
516 if (repo_path) {
517 struct got_repository *repo;
518 error = got_repo_open(&repo, repo_path);
519 if (error == NULL)
520 got_repo_close(repo);
521 } else
522 error = got_error(GOT_ERR_NOT_GIT_REPO);
523 if (error) {
524 free(repo_path);
525 fprintf(stderr, "%s: unknown command '%s'\n",
526 getprogname(), argv[0]);
527 return 1;
529 cmd = &tog_commands[0];
530 cmd_argv = make_argv(cmd->name, repo_path);
531 argc = 2;
532 free(repo_path);
536 error = init_curses();
537 if (error) {
538 fprintf(stderr, "cannot initialize ncurses: %s\n", error->msg);
539 return 1;
542 error = cmd->cmd_main(argc, cmd_argv ? cmd_argv : argv);
543 if (error)
544 goto done;
545 done:
546 endwin();
547 free(cmd_argv);
548 if (error)
549 fprintf(stderr, "%s: %s\n", getprogname(), error->msg);
550 return 0;