From: Mark Jamsek Subject: Re: implement support for work tree diffs in tog To: Mark Jamsek Cc: Stefan Sperling , gameoftrees@openbsd.org Date: Sat, 07 Dec 2024 14:55:40 +1100 Mark Jamsek wrote: > Stefan Sperling wrote: > > On Thu, Dec 05, 2024 at 05:19:02PM +0100, Stefan Sperling wrote: > > > On Fri, Dec 06, 2024 at 02:32:03AM +1100, Mark Jamsek wrote: > > > > The below diff introduces support for work tree diffs via the tog diff > > > > CLI with `tog diff [-s] [path ...]` and the log view. > > > > > > I am happy with this. I will keep running with it to test it, and > > > see no reason to block this diff from going in. > > Thanks! I'm running it locally, too, and trying to drive tog outside my > usual route to find any breakage. So far, I think this will almost > completely replace my use of `got diff`. So far, so good! I've rebased the diff on top of HEAD with the comment you answered on IRC removed and an id_str char pointer leak in the draw_commit() error path plugged. commit 5bea72cce23b9dda9cdb8468bb97c4a1002fd748 (main) from: Mark Jamsek date: Sat Dec 7 01:32:41 2024 UTC implement tog work tree diff support via log view and CLI Work tree diffs can now be viewed with `tog diff [-s] [path ...]` or by selecting a work tree entry from the log view. If there are unstaged changes, a work tree entry is displayed at the top of the log view; likewise if there are staged changes. If there are both staged and unstaged changes, two work tree entries are displayed. The log view can be refreshed with ^l when new changes are staged, unstaged, or reverted in the work tree, but each time a work tree entry is opened from the log view, or an active work tree diff view is refreshed (e.g., A, [,], w) an up-to-date diff is always generated using on disk changes. M cvg/cvg.c | 4+ 3- M got/got.c | 4+ 3- M include/got_diff.h | 6+ 4- M include/got_error.h | 1+ 0- M lib/diff.c | 48+ 16- M lib/error.c | 1+ 0- M lib/worktree.c | 1+ 1- M lib/worktree_cvg.c | 1+ 1- M tog/tog.1 | 41+ 6- M tog/tog.c | 1669+ 356- 10 files changed, 1776 insertions(+), 390 deletions(-) commit - 7b8d6d970e2b5deb4fcf68868ad7a50c65c110d4 commit + 5bea72cce23b9dda9cdb8468bb97c4a1002fd748 blob - f32badb418f5f48bf31962de1a6ac52d4c04c400 blob + 14f6f69962facd1f76c094458fd23bec1fabb9f1 --- cvg/cvg.c +++ cvg/cvg.c @@ -4440,9 +4440,10 @@ print_diff(void *arg, unsigned char status, unsigned c goto done; } - err = got_diff_blob_file(blob1, a->f1, size1, label1, f2 ? f2 : a->f2, - f2_exists, &sb, path, GOT_DIFF_ALGORITHM_PATIENCE, a->diff_context, - a->ignore_whitespace, a->force_text_diff, a->diffstat, a->outfile); + err = got_diff_blob_file(NULL, NULL, blob1, a->f1, size1, label1, + f2 ? f2 : a->f2, f2_exists, &sb, path, GOT_DIFF_ALGORITHM_PATIENCE, + a->diff_context, a->ignore_whitespace, a->force_text_diff, + a->diffstat, a->outfile); done: if (fd1 != -1 && close(fd1) == -1 && err == NULL) err = got_error_from_errno("close"); blob - 0f45835af753a3a6807444c2dc3b9b7127d0dda6 blob + ed5defc312e6a540e5c6a78407f1fa1d9dce3812 --- got/got.c +++ got/got.c @@ -5267,9 +5267,10 @@ print_diff(void *arg, unsigned char status, unsigned c goto done; } - err = got_diff_blob_file(blob1, a->f1, size1, label1, f2 ? f2 : a->f2, - f2_exists, &sb, path, GOT_DIFF_ALGORITHM_PATIENCE, a->diff_context, - a->ignore_whitespace, a->force_text_diff, a->diffstat, a->outfile); + err = got_diff_blob_file(NULL, NULL, blob1, a->f1, size1, label1, + f2 ? f2 : a->f2, f2_exists, &sb, path, GOT_DIFF_ALGORITHM_PATIENCE, + a->diff_context, a->ignore_whitespace, a->force_text_diff, + a->diffstat, a->outfile); done: if (fd1 != -1 && close(fd1) == -1 && err == NULL) err = got_error_from_errno("close"); blob - 3cdd01eba87b65a91beea485fcc804669fa807f3 blob + f418902034cad556fba11cdc5011da9c70bfcafe --- include/got_diff.h +++ include/got_diff.h @@ -74,11 +74,13 @@ const struct got_error *got_diff_blob(struct got_diff_ * An optional const char * diff header label for the blob may be provided, too. * The number of context lines to show in the diff must be specified as well. * Whitespace differences may optionally be ignored. + * If not NULL, the two initial output arguments will be populated with an + * array of line metadata for, and the number of lines in, the unidiff text. */ -const struct got_error *got_diff_blob_file(struct got_blob_object *, FILE *, - off_t, const char *, FILE *, int, struct stat *, const char *, - enum got_diff_algorithm, int, int, int, struct got_diffstat_cb_arg *, - FILE *); +const struct got_error *got_diff_blob_file(struct got_diff_line **, size_t *, + struct got_blob_object *, FILE *, off_t, const char *, FILE *, int, + struct stat *, const char *, enum got_diff_algorithm, int, int, int, + struct got_diffstat_cb_arg *, FILE *); /* * A callback function invoked to handle the differences between two blobs blob - 4181bc79115b549c541be0a74f00f51030a6135d blob + 088c463d5e35cc902a26ad49935eb7c89eaf3987 --- include/got_error.h +++ include/got_error.h @@ -189,6 +189,7 @@ #define GOT_ERR_BAD_KEYWORD 172 #define GOT_ERR_UNKNOWN_CAPA 173 #define GOT_ERR_REF_DUP_ENTRY 174 +#define GOT_ERR_DIFF_NOCHANGES 175 struct got_error { int code; blob - 7b0f22caf13f3bf87b053cad9bb208b1113649bb blob + 705da341e0ac7d7a3ac8291c8c6ad8d46b365aee --- lib/diff.c +++ lib/diff.c @@ -346,20 +346,33 @@ got_diff_blob(struct got_diff_line **lines, size_t*nli } static const struct got_error * -diff_blob_file(struct got_diffreg_result **resultp, - struct got_blob_object *blob1, FILE *f1, off_t size1, const char *label1, - FILE *f2, int f2_exists, struct stat *sb2, const char *label2, - enum got_diff_algorithm diff_algo, int diff_context, int ignore_whitespace, - int force_text_diff, struct got_diffstat_cb_arg *diffstat, FILE *outfile) +diff_blob_file(struct got_diff_line **lines, size_t *nlines, + struct got_diffreg_result **resultp, struct got_blob_object *blob1, + FILE *f1, off_t size1, const char *label1, FILE *f2, int f2_exists, + struct stat *sb2, const char *label2, enum got_diff_algorithm diff_algo, + int diff_context, int ignore_whitespace, int force_text_diff, + struct got_diffstat_cb_arg *diffstat, FILE *outfile) { const struct got_error *err = NULL, *free_err; char hex1[GOT_OBJECT_ID_HEX_MAXLEN]; const char *idstr1 = NULL; struct got_diffreg_result *result = NULL; + off_t outoff = 0; + int n; if (resultp) *resultp = NULL; + if (lines != NULL && *lines != NULL) { + if (*nlines == 0) { + err = add_line_metadata(lines, nlines, 0, + GOT_DIFF_LINE_NONE); + if (err != NULL) + return err; + } else + outoff = (*lines)[*nlines - 1].offset; + } + if (blob1) idstr1 = got_object_blob_id_str(blob1, hex1, sizeof(hex1)); else @@ -368,6 +381,17 @@ diff_blob_file(struct got_diffreg_result **resultp, if (outfile) { char *mode = NULL; + n = fprintf(outfile, "blob - %s\n", label1 ? label1 : idstr1); + if (n < 0) + return got_error_from_errno("fprintf"); + if (lines != NULL && *lines != NULL) { + outoff += n; + err = add_line_metadata(lines, nlines, outoff, + GOT_DIFF_LINE_BLOB_MIN); + if (err != NULL) + return err; + } + /* display file mode for new added files only */ if (f2_exists && blob1 == NULL) { int mmask = (S_IRWXU | S_IRWXG | S_IRWXO); @@ -378,10 +402,18 @@ diff_blob_file(struct got_diffreg_result **resultp, sb2->st_mode & mmask) == -1) return got_error_from_errno("asprintf"); } - fprintf(outfile, "blob - %s\n", label1 ? label1 : idstr1); - fprintf(outfile, "file + %s%s\n", + n = fprintf(outfile, "file + %s%s\n", f2_exists ? label2 : "/dev/null", mode ? mode : ""); free(mode); + if (n < 0) + return got_error_from_errno("fprintf"); + if (lines != NULL && *lines != NULL) { + outoff += n; + err = add_line_metadata(lines, nlines, outoff, + GOT_DIFF_LINE_BLOB_PLUS); + if (err != NULL) + return err; + } } err = got_diffreg(&result, f1, f2, diff_algo, ignore_whitespace, @@ -398,7 +430,7 @@ diff_blob_file(struct got_diffreg_result **resultp, } if (outfile) { - err = got_diffreg_output(NULL, NULL, result, + err = got_diffreg_output(lines, nlines, result, blob1 != NULL, f2_exists, label2, /* show local file's path, not a blob ID */ label2, GOT_DIFF_OUTPUT_UNIDIFF, @@ -453,15 +485,15 @@ done: } const struct got_error * -got_diff_blob_file(struct got_blob_object *blob1, FILE *f1, off_t size1, - const char *label1, FILE *f2, int f2_exists, struct stat *sb2, - const char *label2, enum got_diff_algorithm diff_algo, int diff_context, - int ignore_whitespace, int force_text_diff, - struct got_diffstat_cb_arg *ds, FILE *outfile) +got_diff_blob_file(struct got_diff_line **lines, size_t *nlines, + struct got_blob_object *blob1, FILE *f1, off_t size1, const char *label1, + FILE *f2, int f2_exists, struct stat *sb2, const char *label2, + enum got_diff_algorithm diff_algo, int diff_context, int ignore_whitespace, + int force_text_diff, struct got_diffstat_cb_arg *ds, FILE *outfile) { - return diff_blob_file(NULL, blob1, f1, size1, label1, f2, f2_exists, - sb2, label2, diff_algo, diff_context, ignore_whitespace, - force_text_diff, ds, outfile); + return diff_blob_file(lines, nlines, NULL, blob1, f1, size1, label1, + f2, f2_exists, sb2, label2, diff_algo, diff_context, + ignore_whitespace, force_text_diff, ds, outfile); } static const struct got_error * blob - 33b9d44eaba31b5cde942038314805a373eab474 blob + 5b9fce8995a9dfb8ae760b894371a7e0a3b50352 --- lib/error.c +++ lib/error.c @@ -240,6 +240,7 @@ static const struct got_error got_errors[] = { { GOT_ERR_BAD_KEYWORD, "invalid commit keyword" }, { GOT_ERR_UNKNOWN_CAPA, "unknown capability" }, { GOT_ERR_REF_DUP_ENTRY, "duplicate reference entry" }, + { GOT_ERR_DIFF_NOCHANGES, "no changes match the requested diff" }, }; static struct got_custom_error { blob - 474f10b7b33e56c98f899f81d1428e5bae6a73db blob + 2bd53728fae9b523ae0019194a5f8af4f6d2ce9a --- lib/worktree.c +++ lib/worktree.c @@ -5677,7 +5677,7 @@ append_ct_diff(struct got_commitable *ct, int *diff_he goto done; } - err = got_diff_blob_file(blob1, f1, size1, label1, + err = got_diff_blob_file(NULL, NULL, blob1, f1, size1, label1, ondisk_file ? ondisk_file : f2, f2_exists, &sb, ct->path, GOT_DIFF_ALGORITHM_PATIENCE, 3, 0, 0, NULL, diff_outfile); done: blob - ff9e14244e355ede244af59ba841dcc1f03dd8fd blob + e385e250cc75d449167dd82181f0654e2dcdf4e5 --- lib/worktree_cvg.c +++ lib/worktree_cvg.c @@ -1474,7 +1474,7 @@ append_ct_diff(struct got_commitable *ct, int *diff_he goto done; } - err = got_diff_blob_file(blob1, f1, size1, label1, + err = got_diff_blob_file(NULL, NULL, blob1, f1, size1, label1, ondisk_file ? ondisk_file : f2, f2_exists, &sb, ct->path, GOT_DIFF_ALGORITHM_PATIENCE, 3, 0, 0, NULL, diff_outfile); done: blob - 6e0e4df78865c58469ef88303ebfa3222886178f blob + 12fb6a296691f5f6e253d1bb7b61e721bb020312 --- tog/tog.1 +++ tog/tog.1 @@ -44,7 +44,7 @@ is specified, or if just a .Ar path is specified. .It Diff view -Displays changes made in a particular commit. +Displays work tree changes or changes made in a particular commit. .It Blame view Displays the line-by-line history of a file. .It Tree view @@ -260,7 +260,7 @@ key is pressed. .It Cm Ctrl+l Reload the .Cm log -view with new commits found in the repository. +view with new commits found in the repository or new work tree changes. .It Cm B Reload the .Cm log @@ -345,13 +345,20 @@ work tree, use the repository path associated with thi .El .It Xo .Cm diff -.Op Fl aw +.Op Fl asw .Op Fl C Ar number +.Op Fl c Ar commit .Op Fl r Ar repository-path -.Ar object1 -.Ar object2 +.Op Ar object1 Ar object2 | Ar path ... .Xc -Display the differences between two objects in the repository. +If invoked within a work tree without any arguments, display all local +changes in the work tree. +If one or more +.Ar path +arguments are specified, only show changes within the specified paths. +.Pp +Alternatively, if two object arguments are specified, display the differences +between the two objects in the repository. Treat each of the two arguments as a reference, a tag name, an object ID, or a keyword and display differences between the corresponding objects. @@ -489,6 +496,25 @@ Treat file contents as ASCII text even if binary data .It Fl C Ar number Set the number of context lines shown in the diff. By default, 3 lines of context are shown. +.It Fl c Ar commit +Show differences between commits in the repository. +This option may be used up to two times. +When used only once, show differences between the specified +.Ar commit +and its first parent commit. +When used twice, show differences between the two specified commits. +.Pp +The expected argument is a commit ID hash, or an existing reference, +tag name, or keyword, which is resolved to a commit ID. +Unique abbreviated hash arguments are automatically expanded to a full hash. +Both objects must be of the same type (i.e., blobs, trees, or commits). +.Pp +If the +.Fl c +option is used, all non-option arguments are interpreted as paths. +If one or more such +.Ar path +arguments are provided, only show differences for the specified paths. .It Fl r Ar repository-path Use the repository at the specified path. If not specified, assume the repository is located at or above the current @@ -496,6 +522,15 @@ working directory. If this directory is a .Xr got 1 work tree, use the repository path associated with this work tree. +.It Fl s +Show changes staged with +.Cm got stage +instead of showing local changes in the work tree. +This option is only valid when +.Cm tog diff +is invoked in a work tree with no +.Fl c +options. .It Fl w Ignore whitespace-only changes. .El blob - d1ed335b36437f59534fbfd1126cc20d0b74507f blob + 16256a8af275d99da1e2a192df7aa03132f49e5b --- tog/tog.c +++ tog/tog.c @@ -20,6 +20,7 @@ #include #include +#include #define _XOPEN_SOURCE_EXTENDED /* for ncurses wide-character functions */ #include #include @@ -47,6 +48,7 @@ #include "got_object.h" #include "got_reference.h" #include "got_repository.h" +#include "got_gotconfig.h" #include "got_diff.h" #include "got_opentemp.h" #include "got_utf8.h" @@ -134,6 +136,7 @@ struct commit_queue_entry { TAILQ_ENTRY(commit_queue_entry) entry; struct got_object_id *id; struct got_commit_object *commit; + int worktree_entry; int idx; }; TAILQ_HEAD(commit_queue_head, commit_queue_entry); @@ -333,9 +336,28 @@ get_color_value(const char *envvar) return default_color_value(envvar); } +struct diff_worktree_arg { + struct got_repository *repo; + struct got_worktree *worktree; + struct got_diff_line **lines; + struct got_diffstat_cb_arg *diffstat; + FILE *outfile; + FILE *f1; + FILE *f2; + const char *id_str; + size_t *nlines; + int diff_context; + int header_shown; + int diff_staged; + int ignore_whitespace; + int force_text_diff; + enum got_diff_algorithm diff_algo; +}; + struct tog_diff_view_state { struct got_object_id *id1, *id2; const char *label1, *label2; + const char *worktree_root; char *action; FILE *f, *f1, *f2; int fd1, fd2; @@ -346,7 +368,10 @@ struct tog_diff_view_state { int diff_context; int ignore_whitespace; int force_text_diff; + int diff_worktree; + int diff_staged; struct got_repository *repo; + struct got_pathlist_head *paths; struct got_diff_line *lines; size_t nlines; int matched_line; @@ -356,6 +381,22 @@ struct tog_diff_view_state { struct tog_view *parent_view; }; +#define TOG_WORKTREE_CHANGES_LOCAL_MSG "work tree changes" +#define TOG_WORKTREE_CHANGES_STAGED_MSG "staged work tree changes" + +#define TOG_WORKTREE_CHANGES_LOCAL (1 << 0) +#define TOG_WORKTREE_CHANGES_STAGED (1 << 1) +#define TOG_WORKTREE_CHANGES_ALL \ + (TOG_WORKTREE_CHANGES_LOCAL | TOG_WORKTREE_CHANGES_STAGED) + +struct tog_worktree_ctx { + char *wt_ref; + char *wt_author; + char *wt_root; + int wt_state; + int active; +}; + pthread_mutex_t tog_mutex = PTHREAD_MUTEX_INITIALIZER; static volatile sig_atomic_t tog_thread_error; @@ -366,6 +407,7 @@ struct tog_log_thread_args { int load_all; struct got_commit_graph *graph; struct commit_queue *real_commits; + struct tog_worktree_ctx wctx; const char *in_repo_path; struct got_object_id *start_id; struct got_repository *repo; @@ -374,7 +416,9 @@ struct tog_log_thread_args { pthread_cond_t log_loaded; sig_atomic_t *quit; struct commit_queue_entry **first_displayed_entry; + struct commit_queue_entry **last_displayed_entry; struct commit_queue_entry **selected_entry; + int *selected; int *searching; int *search_next_done; regex_t *regex; @@ -384,6 +428,8 @@ struct tog_log_thread_args { struct commit_queue *limit_commits; struct got_worktree *worktree; int need_commit_marker; + int need_wt_status; + int *view_nlines; }; struct tog_log_view_state { @@ -578,7 +624,8 @@ struct tog_help_view_state { KEY_("@", "Toggle between displaying author and committer name"), \ KEY_("&", "Open prompt to enter term to limit commits displayed"), \ KEY_("C-g Backspace", "Cancel current search or log operation"), \ - KEY_("C-l", "Reload the log view with new commits in the repository"), \ + KEY_("C-l", "Reload the log view with new repository commits or " \ + "work tree changes"), \ \ KEYMAP_("Diff view", TOG_KEYMAP_DIFF), \ KEY_("K < ,", "Display diff of next line in the file/log entry"), \ @@ -724,9 +771,9 @@ struct tog_view { }; static const struct got_error *open_diff_view(struct tog_view *, - struct got_object_id *, struct got_object_id *, - const char *, const char *, int, int, int, struct tog_view *, - struct got_repository *); + struct got_object_id *, struct got_object_id *, const char *, const char *, + int, int, int, int, int, const char *, struct tog_view *, + struct got_repository *, struct got_pathlist_head *); static const struct got_error *show_diff_view(struct tog_view *); static const struct got_error *input_diff_view(struct tog_view **, struct tog_view *, int); @@ -2469,7 +2516,192 @@ draw_commit_marker(struct tog_view *view, char c) return NULL; } +static void +tog_waddwstr(struct tog_view *view, wchar_t *wstr, int width, + int *col, int color, int toeol) +{ + struct tog_color *tc; + int x; + + x = col != NULL ? *col : getcurx(view->window); + tc = color > 0 ? get_color(&view->state.log.colors, color) : NULL; + + if (tc != NULL) + wattr_on(view->window, COLOR_PAIR(tc->colorpair), NULL); + waddwstr(view->window, wstr); + x += MAX(width, 0); + if (toeol) { + while (x < view->ncols) { + waddch(view->window, ' '); + ++x; + } + } + if (tc != NULL) + wattr_off(view->window, COLOR_PAIR(tc->colorpair), NULL); + if (col != NULL) + *col = x; +} + +static void +tog_waddnstr(struct tog_view *view, const char *str, int limit, int color) +{ + struct tog_color *tc; + + if (limit == 0) + limit = view->ncols - getcurx(view->window); + + tc = get_color(&view->state.log.colors, color); + if (tc != NULL) + wattr_on(view->window, COLOR_PAIR(tc->colorpair), NULL); + waddnstr(view->window, str, limit); + if (tc != NULL) + wattr_off(view->window, COLOR_PAIR(tc->colorpair), NULL); +} + static const struct got_error * +draw_author(struct tog_view *view, char *author, int author_display_cols, + int limit, int *col, int color, int marker_column, + struct commit_queue_entry *entry) +{ + const struct got_error *err; + struct tog_log_view_state *s = &view->state.log; + struct tog_color *tc; + wchar_t *wauthor; + int author_width; + + err = format_author(&wauthor, &author_width, author, limit, *col); + if (err != NULL) + return err; + if ((tc = get_color(&s->colors, color)) != NULL) + wattr_on(view->window, COLOR_PAIR(tc->colorpair), NULL); + waddwstr(view->window, wauthor); + free(wauthor); + + *col += author_width; + while (*col < limit && author_width < author_display_cols + 2) { + if (entry != NULL && s->marked_entry == entry && + author_width == marker_column) { + err = draw_commit_marker(view, '>'); + if (err != NULL) + return err; + } else if (entry != NULL && + tog_base_commit.marker != GOT_WORKTREE_STATE_UNKNOWN && + author_width == marker_column && + entry->idx == tog_base_commit.idx && !s->limit_view) { + err = draw_commit_marker(view, tog_base_commit.marker); + if (err != NULL) + return err; + } else + waddch(view->window, ' '); + ++(*col); + ++(author_width); + } + if (tc != NULL) + wattr_off(view->window, COLOR_PAIR(tc->colorpair), NULL); + + return NULL; +} + +static const struct got_error * +draw_idstr(struct tog_view *view, const char *id_str, int color) +{ + char *str = NULL; + + if (strlen(id_str) > 9 && asprintf(&str, "%.8s ", id_str) == -1) + return got_error_from_errno("asprintf"); + + tog_waddnstr(view, str != NULL ? str : id_str, 0, color); + free(str); + return NULL; +} + +static const struct got_error * +draw_ymd(struct tog_view *view, time_t t, int *limit, int avail, + int date_display_cols) +{ + struct tm tm; + char datebuf[12]; /* YYYY-MM-DD + SPACE + NUL */ + + if (gmtime_r(&t, &tm) == NULL) + return got_error_from_errno("gmtime_r"); + if (strftime(datebuf, sizeof(datebuf), "%F ", &tm) == 0) + return got_error(GOT_ERR_NO_SPACE); + + if (avail <= date_display_cols) + *limit = MIN(sizeof(datebuf) - 1, avail); + else + *limit = MIN(date_display_cols, sizeof(datebuf) - 1); + + tog_waddnstr(view, datebuf, *limit, TOG_COLOR_DATE); + return NULL; +} + +static const struct got_error * +draw_worktree_entry(struct tog_view *view, int wt_entry, + const size_t date_display_cols, int author_display_cols) +{ + const struct got_error *err = NULL; + struct tog_log_view_state *s = &view->state.log; + wchar_t *wmsg = NULL; + char *author, *msg = NULL; + char *base_commit_id = NULL; + const char *p = TOG_WORKTREE_CHANGES_LOCAL_MSG; + int col, limit, scrollx, width; + const int avail = view->ncols; + + err = draw_ymd(view, time(NULL), &col, avail, date_display_cols); + if (err != NULL) + return err; + if (col > avail) + return NULL; + if (avail >= 120) { + err = draw_idstr(view, "........ ", TOG_COLOR_COMMIT); + if (err != NULL) + return err; + col += 9; + if (col > avail) + return NULL; + } + + author = strdup(s->thread_args.wctx.wt_author); + if (author == NULL) + return got_error_from_errno("strdup"); + + err = draw_author(view, author, author_display_cols, avail - col, + &col, TOG_COLOR_AUTHOR, 0, NULL); + if (err != NULL) + goto done; + if (col > avail) + goto done; + + err = got_object_id_str(&base_commit_id, tog_base_commit.id); + if (err != NULL) + goto done; + if (wt_entry & TOG_WORKTREE_CHANGES_STAGED) + p = TOG_WORKTREE_CHANGES_STAGED_MSG; + if (asprintf(&msg, "%s based on [%.10s]", p, base_commit_id) == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + + limit = avail - col; + if (view->child != NULL && !view_is_hsplit_top(view) && limit > 0) + limit--; /* for the border */ + + err = format_line(&wmsg, &width, &scrollx, msg, view->x, limit, col, 1); + if (err != NULL) + goto done; + tog_waddwstr(view, &wmsg[scrollx], width, &col, 0, 1); + +done: + free(msg); + free(wmsg); + free(author); + free(base_commit_id); + return err; +} + +static const struct got_error * draw_commit(struct tog_view *view, struct commit_queue_entry *entry, const size_t date_display_cols, int author_display_cols) { @@ -2477,18 +2709,11 @@ draw_commit(struct tog_view *view, struct commit_queue const struct got_error *err = NULL; struct got_commit_object *commit = entry->commit; struct got_object_id *id = entry->id; - char datebuf[12]; /* YYYY-MM-DD + SPACE + NUL */ - char *refs_str = NULL; - char *logmsg0 = NULL, *logmsg = NULL; - char *author = NULL; - wchar_t *wrefstr = NULL, *wlogmsg = NULL, *wauthor = NULL; - int author_width, refstr_width, logmsg_width; - char *newline, *line = NULL; - int col, limit, scrollx, logmsg_x; + char *author, *newline, *logmsg, *logmsg0 = NULL, *refs_str = NULL; + wchar_t *wrefstr = NULL, *wlogmsg = NULL; + int refstr_width, logmsg_width, col, limit, scrollx, logmsg_x; const int avail = view->ncols, marker_column = author_display_cols + 1; - struct tm tm; time_t committer_time; - struct tog_color *tc; struct got_reflist_head *refs; if (tog_base_commit.id != NULL && tog_base_commit.idx == -1 && @@ -2503,82 +2728,38 @@ draw_commit(struct tog_view *view, struct commit_queue } committer_time = got_object_commit_get_committer_time(commit); - if (gmtime_r(&committer_time, &tm) == NULL) - return got_error_from_errno("gmtime_r"); - if (strftime(datebuf, sizeof(datebuf), "%F ", &tm) == 0) - return got_error(GOT_ERR_NO_SPACE); - - if (avail <= date_display_cols) - limit = MIN(sizeof(datebuf) - 1, avail); - else - limit = MIN(date_display_cols, sizeof(datebuf) - 1); - tc = get_color(&s->colors, TOG_COLOR_DATE); - if (tc) - wattr_on(view->window, - COLOR_PAIR(tc->colorpair), NULL); - waddnstr(view->window, datebuf, limit); - if (tc) - wattr_off(view->window, - COLOR_PAIR(tc->colorpair), NULL); - col = limit; + err = draw_ymd(view, committer_time, &col, avail, date_display_cols); + if (err != NULL) + return err; if (col > avail) - goto done; + return NULL; if (avail >= 120) { char *id_str; + err = got_object_id_str(&id_str, id); if (err) - goto done; - tc = get_color(&s->colors, TOG_COLOR_COMMIT); - if (tc) - wattr_on(view->window, - COLOR_PAIR(tc->colorpair), NULL); - wprintw(view->window, "%.8s ", id_str); - if (tc) - wattr_off(view->window, - COLOR_PAIR(tc->colorpair), NULL); + return err; + err = draw_idstr(view, id_str, TOG_COLOR_COMMIT); free(id_str); + if (err != NULL) + return err; col += 9; if (col > avail) - goto done; + return NULL; } if (s->use_committer) author = strdup(got_object_commit_get_committer(commit)); else author = strdup(got_object_commit_get_author(commit)); - if (author == NULL) { - err = got_error_from_errno("strdup"); + if (author == NULL) + return got_error_from_errno("strdup"); + + err = draw_author(view, author, author_display_cols, + avail - col, &col, TOG_COLOR_AUTHOR, marker_column, entry); + if (err != NULL) goto done; - } - err = format_author(&wauthor, &author_width, author, avail - col, col); - if (err) - goto done; - tc = get_color(&s->colors, TOG_COLOR_AUTHOR); - if (tc) - wattr_on(view->window, - COLOR_PAIR(tc->colorpair), NULL); - waddwstr(view->window, wauthor); - col += author_width; - while (col < avail && author_width < author_display_cols + 2) { - if (s->marked_entry == entry && author_width == marker_column) { - err = draw_commit_marker(view, '>'); - if (err != NULL) - goto done; - } else if (tog_base_commit.marker != GOT_WORKTREE_STATE_UNKNOWN - && author_width == marker_column && - entry->idx == tog_base_commit.idx && !s->limit_view) { - err = draw_commit_marker(view, tog_base_commit.marker); - if (err != NULL) - goto done; - } else - waddch(view->window, ' '); - col++; - author_width++; - } - if (tc) - wattr_off(view->window, - COLOR_PAIR(tc->colorpair), NULL); if (col > avail) goto done; @@ -2613,15 +2794,8 @@ draw_commit(struct tog_view *view, struct commit_queue free(rs); if (err) goto done; - tc = get_color(&s->colors, TOG_COLOR_COMMIT); - if (tc) - wattr_on(view->window, - COLOR_PAIR(tc->colorpair), NULL); - waddwstr(view->window, &wrefstr[scrollx]); - if (tc) - wattr_off(view->window, - COLOR_PAIR(tc->colorpair), NULL); - col += MAX(refstr_width, 0); + tog_waddwstr(view, &wrefstr[scrollx], refstr_width, + &col, TOG_COLOR_COMMIT, 0); if (col > avail) goto done; @@ -2655,20 +2829,14 @@ draw_commit(struct tog_view *view, struct commit_queue limit, col, 1); if (err) goto done; - waddwstr(view->window, &wlogmsg[scrollx]); - col += MAX(logmsg_width, 0); - while (col < avail) { - waddch(view->window, ' '); - col++; - } + tog_waddwstr(view, &wlogmsg[scrollx], logmsg_width, &col, 0, 1); + done: free(logmsg0); free(wlogmsg); free(wrefstr); free(refs_str); free(author); - free(wauthor); - free(line); return err; } @@ -2701,7 +2869,8 @@ pop_commit(struct commit_queue *commits) entry = TAILQ_FIRST(&commits->head); TAILQ_REMOVE(&commits->head, entry, entry); - got_object_commit_close(entry->commit); + if (entry->worktree_entry == 0) + got_object_commit_close(entry->commit); commits->ncommits--; free(entry->id); free(entry); @@ -2864,43 +3033,346 @@ select_commit(struct tog_log_view_state *s) } static const struct got_error * -draw_commits(struct tog_view *view) +valid_author(const char *author) { + const char *email = author; + + /* + * Git' expects the author (or committer) to be in the form + * "name ", which are mostly free form (see the + * "committer" description in git-fast-import(1)). We're only + * doing this to avoid git's object parser breaking on commits + * we create. + */ + + while (*author && *author != '\n' && *author != '<' && *author != '>') + author++; + if (author != email && *author == '<' && *(author - 1) != ' ') + return got_error_fmt(GOT_ERR_COMMIT_BAD_AUTHOR, "%s: space " + "between author name and email required", email); + if (*author++ != '<') + return got_error_fmt(GOT_ERR_COMMIT_NO_EMAIL, "%s", email); + while (*author && *author != '\n' && *author != '<' && *author != '>') + author++; + if (strcmp(author, ">") != 0) + return got_error_fmt(GOT_ERR_COMMIT_NO_EMAIL, "%s", email); + return NULL; +} + +/* lifted from got.c:652 (TODO make lib routine) */ +static const struct got_error * +get_author(char **author, struct got_repository *repo, + struct got_worktree *worktree) +{ const struct got_error *err = NULL; - struct tog_log_view_state *s = &view->state.log; - struct commit_queue_entry *entry = s->selected_entry; - int limit = view->nlines; - int width; - int ncommits, author_cols = 4, refstr_cols; - char *id_str = NULL, *header = NULL, *ncommits_str = NULL; - char *refs_str = NULL; - wchar_t *wline; - struct tog_color *tc; - static const size_t date_display_cols = 12; - struct got_reflist_head *refs; + const char *got_author = NULL, *name, *email; + const struct got_gotconfig *worktree_conf = NULL, *repo_conf = NULL; - if (view_is_hsplit_top(view)) - --limit; /* account for border */ + *author = NULL; - if (s->selected_entry && - !(view->searching && view->search_next_done == 0)) { - err = got_object_id_str(&id_str, s->selected_entry->id); - if (err) - return err; - refs = got_reflist_object_id_map_lookup(tog_refs_idmap, - s->selected_entry->id); - err = build_refs_str(&refs_str, refs, s->selected_entry->id, - s->repo); - if (err) + if (worktree) + worktree_conf = got_worktree_get_gotconfig(worktree); + repo_conf = got_repo_get_gotconfig(repo); + + /* + * Priority of potential author information sources, from most + * significant to least significant: + * 1) work tree's .got/got.conf file + * 2) repository's got.conf file + * 3) repository's git config file + * 4) environment variables + * 5) global git config files (in user's home directory or /etc) + */ + + if (worktree_conf) + got_author = got_gotconfig_get_author(worktree_conf); + if (got_author == NULL) + got_author = got_gotconfig_get_author(repo_conf); + if (got_author == NULL) { + name = got_repo_get_gitconfig_author_name(repo); + email = got_repo_get_gitconfig_author_email(repo); + if (name && email) { + if (asprintf(author, "%s <%s>", name, email) == -1) + return got_error_from_errno("asprintf"); + return NULL; + } + + got_author = getenv("GOT_AUTHOR"); + if (got_author == NULL) { + name = got_repo_get_global_gitconfig_author_name(repo); + email = got_repo_get_global_gitconfig_author_email( + repo); + if (name && email) { + if (asprintf(author, "%s <%s>", name, email) + == -1) + return got_error_from_errno("asprintf"); + return NULL; + } + /* TODO: Look up user in password database? */ + return got_error(GOT_ERR_COMMIT_NO_AUTHOR); + } + } + + *author = strdup(got_author); + if (*author == NULL) + return got_error_from_errno("strdup"); + + err = valid_author(*author); + if (err) { + free(*author); + *author = NULL; + } + return err; +} + +static const struct got_error * +push_worktree_entry(struct tog_log_thread_args *ta, int wt_entry, + struct got_worktree *worktree) +{ + struct commit_queue_entry *e, *entry; + int rc; + + entry = calloc(1, sizeof(*entry)); + if (entry == NULL) + return got_error_from_errno("calloc"); + + entry->idx = 0; + entry->worktree_entry = wt_entry; + + rc = pthread_mutex_lock(&tog_mutex); + if (rc != 0) { + free(entry); + return got_error_set_errno(rc, "pthread_mutex_lock"); + } + + TAILQ_FOREACH(e, &ta->real_commits->head, entry) + ++e->idx; + + TAILQ_INSERT_HEAD(&ta->real_commits->head, entry, entry); + ta->wctx.wt_state |= wt_entry; + ++ta->real_commits->ncommits; + ++tog_base_commit.idx; + + rc = pthread_mutex_unlock(&tog_mutex); + if (rc != 0) + return got_error_set_errno(rc, "pthread_mutex_unlock"); + + return NULL; +} + +static const struct got_error * +check_cancelled(void *arg) +{ + if (tog_sigint_received || tog_sigpipe_received) + return got_error(GOT_ERR_CANCELLED); + return NULL; +} + +static const struct got_error * +check_local_changes(void *arg, unsigned char status, + unsigned char staged_status, const char *path, + struct got_object_id *blob_id, struct got_object_id *staged_blob_id, + struct got_object_id *commit_id, int dirfd, const char *de_name) +{ + int *have_local_changes = arg; + + switch (status) { + case GOT_STATUS_ADD: + case GOT_STATUS_DELETE: + case GOT_STATUS_MODIFY: + case GOT_STATUS_CONFLICT: + *have_local_changes |= TOG_WORKTREE_CHANGES_LOCAL; + default: + break; + } + + switch (staged_status) { + case GOT_STATUS_ADD: + case GOT_STATUS_DELETE: + case GOT_STATUS_MODIFY: + *have_local_changes |= TOG_WORKTREE_CHANGES_STAGED; + default: + break; + } + + return NULL; +} + +static const struct got_error * +tog_worktree_status(struct tog_log_thread_args *ta) +{ + const struct got_error *err, *close_err; + struct tog_worktree_ctx *wctx = &ta->wctx; + struct got_worktree *wt = ta->worktree; + struct got_pathlist_head paths; + char *cwd = NULL; + int wt_state = 0; + + TAILQ_INIT(&paths); + + if (wt == NULL) { + cwd = getcwd(NULL, 0); + if (cwd == NULL) + return got_error_from_errno("getcwd"); + + err = got_worktree_open(&wt, cwd, NULL); + if (err != NULL) { + if (err->code == GOT_ERR_NOT_WORKTREE) { + /* + * Shouldn't happen; this routine should only + * be called if tog is invoked in a worktree. + */ + wctx->active = 0; + err = NULL; + } else if (err->code == GOT_ERR_WORKTREE_BUSY) + err = NULL; /* retry next redraw */ goto done; + } } - if (s->thread_args.commits_needed == 0 && !using_mock_io) - halfdelay(10); /* disable fast refresh */ + err = got_pathlist_insert(NULL, &paths, "", NULL); + if (err != NULL) + goto done; + err = got_worktree_status(wt, &paths, ta->repo, 0, + check_local_changes, &wt_state, check_cancelled, NULL); + if (err != NULL) { + if (err->code != GOT_ERR_CANCELLED) + goto done; + err = NULL; + } + + if (wt_state != 0) { + err = get_author(&wctx->wt_author, ta->repo, wt); + if (err != NULL) + goto done; + + wctx->wt_root = strdup(got_worktree_get_root_path(wt)); + if (wctx->wt_root == NULL) { + err = got_error_from_errno("strdup"); + goto done; + } + + wctx->wt_ref = strdup(got_worktree_get_head_ref_name(wt)); + if (wctx->wt_ref == NULL) { + err = got_error_from_errno("strdup"); + goto done; + } + } + + /* + * Push staged entry first so it's the second log entry + * if there are both staged and unstaged work tree changes. + */ + if (wt_state & TOG_WORKTREE_CHANGES_STAGED && + (wctx->wt_state & TOG_WORKTREE_CHANGES_STAGED) == 0) { + err = push_worktree_entry(ta, TOG_WORKTREE_CHANGES_STAGED, wt); + if (err != NULL) + goto done; + } + if (wt_state & TOG_WORKTREE_CHANGES_LOCAL && + (wctx->wt_state & TOG_WORKTREE_CHANGES_LOCAL) == 0) { + err = push_worktree_entry(ta, TOG_WORKTREE_CHANGES_LOCAL, wt); + if (err != NULL) + goto done; + } + +done: + got_pathlist_free(&paths, GOT_PATHLIST_FREE_NONE); + if (ta->worktree == NULL && wt != NULL) { + close_err = got_worktree_close(wt); + if (close_err != NULL && err == NULL) + err = close_err; + } + free(cwd); + return err; +} + +static const struct got_error * +worktree_headref_str(char **ret, const char *ref) +{ + if (strncmp(ref, "refs/heads/", 11) == 0) + *ret = strdup(ref + 11); + else + *ret = strdup(ref); + if (*ret == NULL) + return got_error_from_errno("strdup"); + + return NULL; +} + +static const struct got_error * +fmtindex(char **index, int *ncommits, int wt_state, + struct commit_queue_entry *entry, int limit_view) +{ + int idx = 0; + + if (!limit_view) { + if (*ncommits > 0 && wt_state & TOG_WORKTREE_CHANGES_LOCAL) + --(*ncommits); + if (*ncommits > 0 && wt_state & TOG_WORKTREE_CHANGES_STAGED) + --(*ncommits); + } + + if (entry != NULL && entry->worktree_entry == 0) { + /* + * Display 1-based index of selected commit entries only. + * If a work tree entry is selected, show an index of 0. + */ + idx = entry->idx; + if (wt_state == 0 || limit_view) + ++idx; + else if (wt_state > TOG_WORKTREE_CHANGES_STAGED) + --idx; + } + if (asprintf(index, " [%d/%d] ", idx, *ncommits) == -1) { + *index = NULL; + return got_error_from_errno("asprintf"); + } + + return NULL; +} + +static const struct got_error * +fmtheader(char **header, int *ncommits, struct commit_queue_entry *entry, + struct tog_view *view) +{ + const struct got_error *err; + struct tog_log_view_state *s = &view->state.log; + struct tog_worktree_ctx *wctx = &s->thread_args.wctx; + struct got_reflist_head *refs; + char *id_str = NULL, *index = NULL; + char *wthdr = NULL, *ncommits_str = NULL; + char *refs_str = NULL; + int wt_entry; + + *header = NULL; + wt_entry = entry != NULL ? entry->worktree_entry : 0; + + if (entry && !(view->searching && view->search_next_done == 0)) { + if (entry->worktree_entry == 0) { + err = got_object_id_str(&id_str, entry->id); + if (err != NULL) + return err; + refs = got_reflist_object_id_map_lookup(tog_refs_idmap, + entry->id); + err = build_refs_str(&refs_str, refs, + entry->id, s->repo); + if (err != NULL) + goto done; + } else { + err = worktree_headref_str(&refs_str, wctx->wt_ref); + if (err != NULL) + return err; + } + } + + err = fmtindex(&index, ncommits, wctx->wt_state, entry, s->limit_view); + if (err != NULL) + goto done; + if (s->thread_args.commits_needed > 0 || s->thread_args.load_all) { - if (asprintf(&ncommits_str, " [%d/%d] %s", - entry ? entry->idx + 1 : 0, s->commits->ncommits, + if (asprintf(&ncommits_str, "%s%s", index, (view->searching && !view->search_next_done) ? "searching..." : "loading...") == -1) { err = got_error_from_errno("asprintf"); @@ -2919,11 +3391,10 @@ draw_commits(struct tog_view *view) search_str = "searching..."; } - if (s->limit_view && s->commits->ncommits == 0) + if (s->limit_view && ncommits == 0) limit_str = "no matches found"; - if (asprintf(&ncommits_str, " [%d/%d] %s %s", - entry ? entry->idx + 1 : 0, s->commits->ncommits, + if (asprintf(&ncommits_str, "%s%s %s", index, search_str ? search_str : (refs_str ? refs_str : ""), limit_str ? limit_str : "") == -1) { err = got_error_from_errno("asprintf"); @@ -2931,67 +3402,107 @@ draw_commits(struct tog_view *view) } } - free(refs_str); - refs_str = NULL; + if (wt_entry != 0) { + const char *t = "", *p = TOG_WORKTREE_CHANGES_LOCAL_MSG; - if (s->in_repo_path && strcmp(s->in_repo_path, "/") != 0) { - if (asprintf(&header, "commit %s %s%s", id_str ? id_str : - "........................................", - s->in_repo_path, ncommits_str) == -1) { + if (wt_entry == TOG_WORKTREE_CHANGES_STAGED) { + p = TOG_WORKTREE_CHANGES_STAGED_MSG; + t = "-s "; + } + if (asprintf(&wthdr, "%s%s (%s)", t, wctx->wt_root, p) == -1) { err = got_error_from_errno("asprintf"); - header = NULL; goto done; } - } else if (asprintf(&header, "commit %s%s", - id_str ? id_str : "........................................", - ncommits_str) == -1) { + } + + if (s->in_repo_path != NULL && strcmp(s->in_repo_path, "/") != 0) { + if (asprintf(header, "%s%s %s%s", + wt_entry == 0 ? "commit " : "diff ", + wt_entry == 0 ? id_str ? id_str : + "........................................" : + wthdr != NULL ? wthdr : "", s->in_repo_path, + ncommits_str) == -1) + err = got_error_from_errno("asprintf"); + } else if (asprintf(header, "%s%s%s", + wt_entry == 0 ? "commit " : "diff ", + wt_entry == 0 ? id_str ? id_str : + "........................................" : + wthdr != NULL ? wthdr : "", ncommits_str) == -1) err = got_error_from_errno("asprintf"); - header = NULL; - goto done; - } + if (err != NULL) + *header = NULL; + +done: + free(wthdr); + free(index); + free(id_str); + free(refs_str); + free(ncommits_str); + return err; +} + +static const struct got_error * +draw_commits(struct tog_view *view) +{ + const struct got_error *err; + struct tog_log_view_state *s = &view->state.log; + struct commit_queue_entry *entry = s->selected_entry; + int width, limit = view->nlines; + int ncommits = s->commits->ncommits, author_cols = 4, refstr_cols; + char *header; + wchar_t *wline; + static const size_t date_display_cols = 12; + + if (view_is_hsplit_top(view)) + --limit; /* account for border */ + + if (s->thread_args.commits_needed == 0 && + s->thread_args.need_wt_status == 0 && + s->thread_args.need_commit_marker == 0 && !using_mock_io) + halfdelay(10); /* disable fast refresh */ + + err = fmtheader(&header, &ncommits, entry, view); + if (err != NULL) + return err; + err = format_line(&wline, &width, NULL, header, 0, view->ncols, 0, 0); + free(header); if (err) - goto done; + return err; werase(view->window); if (view_needs_focus_indication(view)) wstandout(view->window); - tc = get_color(&s->colors, TOG_COLOR_COMMIT); - if (tc) - wattr_on(view->window, COLOR_PAIR(tc->colorpair), NULL); - waddwstr(view->window, wline); - while (width < view->ncols) { - waddch(view->window, ' '); - width++; - } - if (tc) - wattr_off(view->window, COLOR_PAIR(tc->colorpair), NULL); + tog_waddwstr(view, wline, width, NULL, TOG_COLOR_COMMIT, 1); if (view_needs_focus_indication(view)) wstandend(view->window); free(wline); if (limit <= 1) - goto done; + return NULL; /* Grow author column size if necessary, and set view->maxx. */ entry = s->first_displayed_entry; ncommits = 0; view->maxx = 0; while (entry) { + struct got_reflist_head *refs; struct got_commit_object *c = entry->commit; - char *author, *eol, *msg, *msg0; + char *author, *eol, *msg, *msg0, *refs_str; wchar_t *wauthor, *wmsg; int width; + if (ncommits >= limit - 1) break; - if (s->use_committer) + if (entry->worktree_entry != 0) + author = strdup(s->thread_args.wctx.wt_author); + else if (s->use_committer) author = strdup(got_object_commit_get_committer(c)); else author = strdup(got_object_commit_get_author(c)); - if (author == NULL) { - err = got_error_from_errno("strdup"); - goto done; - } + if (author == NULL) + return got_error_from_errno("strdup"); + err = format_author(&wauthor, &width, author, COLS, date_display_cols); if (author_cols < width) @@ -2999,27 +3510,38 @@ draw_commits(struct tog_view *view) free(wauthor); free(author); if (err) - goto done; + return err; + if (entry->worktree_entry != 0) { + if (entry->worktree_entry == TOG_WORKTREE_CHANGES_LOCAL) + width = sizeof(TOG_WORKTREE_CHANGES_LOCAL_MSG); + else + width = sizeof(TOG_WORKTREE_CHANGES_STAGED_MSG); + view->maxx = MAX(view->maxx, width - 1); + entry = TAILQ_NEXT(entry, entry); + ++ncommits; + continue; + } refs = got_reflist_object_id_map_lookup(tog_refs_idmap, entry->id); err = build_refs_str(&refs_str, refs, entry->id, s->repo); if (err) - goto done; + return err; if (refs_str) { wchar_t *ws; + err = format_line(&ws, &width, NULL, refs_str, 0, INT_MAX, date_display_cols + author_cols, 0); free(ws); free(refs_str); refs_str = NULL; if (err) - goto done; + return err; refstr_cols = width + 3; /* account for [ ] + space */ } else refstr_cols = 0; err = got_object_commit_get_logmsg(&msg0, c); if (err) - goto done; + return err; msg = msg0; while (*msg == '\n') ++msg; @@ -3027,11 +3549,11 @@ draw_commits(struct tog_view *view) *eol = '\0'; err = format_line(&wmsg, &width, NULL, msg, 0, INT_MAX, date_display_cols + author_cols + refstr_cols, 0); - if (err) - goto done; - view->maxx = MAX(view->maxx, width + refstr_cols); free(msg0); free(wmsg); + if (err) + return err; + view->maxx = MAX(view->maxx, width + refstr_cols); ncommits++; entry = TAILQ_NEXT(entry, entry); } @@ -3044,23 +3566,23 @@ draw_commits(struct tog_view *view) break; if (ncommits == s->selected) wstandout(view->window); - err = draw_commit(view, entry, date_display_cols, author_cols); + if (entry->worktree_entry == 0) + err = draw_commit(view, entry, + date_display_cols, author_cols); + else + err = draw_worktree_entry(view, entry->worktree_entry, + date_display_cols, author_cols); if (ncommits == s->selected) wstandend(view->window); if (err) - goto done; + return err; ncommits++; s->last_displayed_entry = entry; entry = TAILQ_NEXT(entry, entry); } view_border(view); -done: - free(id_str); - free(refs_str); - free(ncommits_str); - free(header); - return err; + return NULL; } static void @@ -3196,32 +3718,36 @@ log_scroll_down(struct tog_view *view, int maxscroll) static const struct got_error * open_diff_view_for_commit(struct tog_view **new_view, int begin_y, int begin_x, - struct got_commit_object *commit, struct got_object_id *commit_id, - struct tog_view *log_view, struct got_repository *repo) + struct commit_queue_entry *entry, struct tog_view *log_view, + struct got_repository *repo) { const struct got_error *err; struct got_object_qid *p; struct got_object_id *parent_id; struct tog_view *diff_view; struct tog_log_view_state *ls = NULL; + const char *worktree_root = NULL; diff_view = view_open(0, 0, begin_y, begin_x, TOG_VIEW_DIFF); if (diff_view == NULL) return got_error_from_errno("view_open"); - if (log_view != NULL) + if (log_view != NULL) { ls = &log_view->state.log; + worktree_root = ls->thread_args.wctx.wt_root; + } if (ls != NULL && ls->marked_entry != NULL && ls->marked_entry != ls->selected_entry) parent_id = ls->marked_entry->id; - else if ((p = STAILQ_FIRST(got_object_commit_get_parent_ids(commit)))) + else if (entry->worktree_entry == 0 && + (p = STAILQ_FIRST(got_object_commit_get_parent_ids(entry->commit)))) parent_id = &p->id; else parent_id = NULL; - err = open_diff_view(diff_view, parent_id, commit_id, - NULL, NULL, 3, 0, 0, log_view, repo); + err = open_diff_view(diff_view, parent_id, entry->id, NULL, NULL, 3, 0, + 0, 0, entry->worktree_entry, worktree_root, log_view, repo, NULL); if (err == NULL) *new_view = diff_view; return err; @@ -3351,6 +3877,58 @@ browse_commit_tree(struct tog_view **new_view, int beg return tree_view_walk_path(s, entry->commit, path); } +/* + * If work tree entries have been pushed onto the commit queue and the + * first commit entry is still displayed, scroll the view so the new + * work tree entries are visible. If the selection cursor is still on + * the first commit entry, keep the cursor in place such that the first + * work tree entry is selected, otherwise move the selection cursor so + * the currently selected commit stays selected if it remains on screen. + */ +static void +worktree_entries_reveal(struct tog_log_thread_args *a) +{ + struct commit_queue_entry **first = a->first_displayed_entry; + struct commit_queue_entry **select = a->selected_entry; + int *cursor = a->selected; + int wts = a->wctx.wt_state; + +#define select_worktree_entry(_first, _selected) do { \ + *_first = TAILQ_FIRST(&a->real_commits->head); \ + *_selected = *_first; \ +} while (0) + + if (first == NULL) + select_worktree_entry(first, select); + else if (*select == *first) { + if (wts == TOG_WORKTREE_CHANGES_LOCAL && (*first)->idx == 1) + select_worktree_entry(first, select); + else if (wts == TOG_WORKTREE_CHANGES_STAGED && + (*first)->idx == 1) + select_worktree_entry(first, select); + else if (wts & TOG_WORKTREE_CHANGES_ALL && (*first)->idx == 2) + select_worktree_entry(first, select); + } else if (wts & TOG_WORKTREE_CHANGES_ALL && (*first)->idx == 2) { + *first = TAILQ_FIRST(&a->real_commits->head); + if (*cursor + 2 < *a->view_nlines - 1) + (*cursor) += 2; + else if (*cursor + 1 < *a->view_nlines - 1) { + *select = TAILQ_PREV(*select, commit_queue_head, entry); + ++(*cursor); + } else { + *select = TAILQ_PREV(*select, commit_queue_head, entry); + *select = TAILQ_PREV(*select, commit_queue_head, entry); + } + } else if (wts != 0 && (*first)->idx == 1) { + *first = TAILQ_FIRST(&a->real_commits->head); + if (*cursor + 1 < *a->view_nlines - 1) + ++(*cursor); + else + *select = TAILQ_PREV(*select, commit_queue_head, entry); + } +#undef select_worktree_entry +} + static const struct got_error * block_signals_used_by_main_thread(void) { @@ -3452,6 +4030,27 @@ log_thread(void *arg) goto done; } + if (a->commits_needed == 0 && a->need_wt_status) { + errcode = pthread_mutex_unlock(&tog_mutex); + if (errcode) { + err = got_error_set_errno(errcode, + "pthread_mutex_unlock"); + goto done; + } + err = tog_worktree_status(a); + if (err != NULL) + goto done; + errcode = pthread_mutex_lock(&tog_mutex); + if (errcode) { + err = got_error_set_errno(errcode, + "pthread_mutex_lock"); + goto done; + } + if (a->wctx.wt_state != 0) + worktree_entries_reveal(a); + a->need_wt_status = 0; + } + if (a->commits_needed == 0 && a->need_commit_marker && a->worktree) { errcode = pthread_mutex_unlock(&tog_mutex); @@ -3578,6 +4177,23 @@ stop_log_thread(struct tog_log_view_state *s) return err ? err : thread_err; } +static void +worktree_ctx_close(struct tog_log_thread_args *ta) +{ + struct tog_worktree_ctx *wctx = &ta->wctx; + + if (wctx->active) { + free(wctx->wt_author); + wctx->wt_author = NULL; + free(wctx->wt_root); + wctx->wt_root = NULL; + free(wctx->wt_ref); + wctx->wt_ref = NULL; + wctx->wt_state = 0; + ta->need_wt_status = 1; + } +} + static const struct got_error * close_log_view(struct tog_view *view) { @@ -3606,6 +4222,7 @@ close_log_view(struct tog_view *view) s->start_id = NULL; free(s->head_ref_name); s->head_ref_name = NULL; + worktree_ctx_close(&s->thread_args); return err; } @@ -3694,10 +4311,12 @@ limit_log_view(struct tog_view *view) TAILQ_FOREACH(entry, &s->real_commits.head, entry) { int have_match = 0; - err = match_commit(&have_match, entry->id, - entry->commit, &s->limit_regex); - if (err) - return err; + if (entry->worktree_entry == 0) { + err = match_commit(&have_match, entry->id, + entry->commit, &s->limit_regex); + if (err) + return err; + } if (have_match) { struct commit_queue_entry *matched; @@ -3820,14 +4439,16 @@ search_next_log_view(struct tog_view *view) return trigger_log_thread(view, 0); } - err = match_commit(&have_match, entry->id, entry->commit, - &view->regex); - if (err) - break; - if (have_match) { - view->search_next_done = TOG_SEARCH_HAVE_MORE; - s->matched_entry = entry; - break; + if (entry->worktree_entry == 0) { + err = match_commit(&have_match, entry->id, + entry->commit, &view->regex); + if (err) + break; + if (have_match) { + view->search_next_done = TOG_SEARCH_HAVE_MORE; + s->matched_entry = entry; + break; + } } s->search_entry = entry; @@ -3964,6 +4585,7 @@ open_log_view(struct tog_view *view, struct got_object return got_error_set_errno(rc, "pthread_cond_init"); } + s->thread_args.view_nlines = &view->nlines; s->thread_args.commits_needed = view->nlines; s->thread_args.graph = thread_graph; s->thread_args.real_commits = &s->real_commits; @@ -3974,7 +4596,9 @@ open_log_view(struct tog_view *view, struct got_object s->thread_args.log_complete = 0; s->thread_args.quit = &s->quit; s->thread_args.first_displayed_entry = &s->first_displayed_entry; + s->thread_args.last_displayed_entry = &s->last_displayed_entry; s->thread_args.selected_entry = &s->selected_entry; + s->thread_args.selected = &s->selected; s->thread_args.searching = &view->searching; s->thread_args.search_next_done = &view->search_next_done; s->thread_args.regex = &view->regex; @@ -3982,8 +4606,12 @@ open_log_view(struct tog_view *view, struct got_object s->thread_args.limit_regex = &s->limit_regex; s->thread_args.limit_commits = &s->limit_commits; s->thread_args.worktree = worktree; - if (worktree) + if (worktree) { + s->thread_args.wctx.active = 1; + s->thread_args.need_wt_status = 1; s->thread_args.need_commit_marker = 1; + } + done: if (err) { if (view->close == NULL) @@ -4421,10 +5049,13 @@ input_log_view(struct tog_view **new_view, struct tog_ s->thread_args.commits_needed = view->lines; s->matched_entry = NULL; s->search_entry = NULL; + tog_base_commit.idx = -1; + worktree_ctx_close(&s->thread_args); view->offset = 0; break; case 'm': - log_mark_commit(s); + if (s->selected_entry->worktree_entry == 0) + log_mark_commit(s); break; case 'R': view->count = 0; @@ -4764,8 +5395,9 @@ __dead static void usage_diff(void) { endwin(); - fprintf(stderr, "usage: %s diff [-aw] [-C number] [-r repository-path] " - "object1 object2\n", getprogname()); + fprintf(stderr, "usage: %s diff [-asw] [-C number] [-c commit] " + "[-r repository-path] [object1 object2 | path ...]\n", + getprogname()); exit(1); } @@ -5394,15 +6026,508 @@ done: return err; } +static void +evict_worktree_entry(struct tog_log_thread_args *ta, int victim) +{ + struct commit_queue_entry *e, *v = *ta->selected_entry; + + if (victim == 0) + return; /* paranoid check */ + + if (v->worktree_entry != victim) { + TAILQ_FOREACH(v, &ta->real_commits->head, entry) { + if (v->worktree_entry == victim) + break; + } + if (v == NULL) + return; + } + + ta->wctx.wt_state &= ~victim; + + if (*ta->selected_entry == v) + *ta->selected_entry = TAILQ_NEXT(v, entry); + if (*ta->first_displayed_entry == v) + *ta->first_displayed_entry = TAILQ_NEXT(v, entry); + if (*ta->last_displayed_entry == v) + *ta->last_displayed_entry = TAILQ_NEXT(v, entry); + + for (e = TAILQ_NEXT(v, entry); e != NULL; e = TAILQ_NEXT(e, entry)) + --e->idx; + + --tog_base_commit.idx; + --ta->real_commits->ncommits; + + TAILQ_REMOVE(&ta->real_commits->head, v, entry); + free(v); +} + +/* + * Create a file which contains the target path of a symlink so we can feed + * it as content to the diff engine. + */ + static const struct got_error * +get_symlink_target_file(int *fd, int dirfd, const char *de_name, + const char *abspath) +{ + const struct got_error *err = NULL; + char target_path[PATH_MAX]; + ssize_t target_len, outlen; + + *fd = -1; + + if (dirfd != -1) { + target_len = readlinkat(dirfd, de_name, target_path, PATH_MAX); + if (target_len == -1) + return got_error_from_errno2("readlinkat", abspath); + } else { + target_len = readlink(abspath, target_path, PATH_MAX); + if (target_len == -1) + return got_error_from_errno2("readlink", abspath); + } + + *fd = got_opentempfd(); + if (*fd == -1) + return got_error_from_errno("got_opentempfd"); + + outlen = write(*fd, target_path, target_len); + if (outlen == -1) { + err = got_error_from_errno("got_opentempfd"); + goto done; + } + + if (lseek(*fd, 0, SEEK_SET) == -1) { + err = got_error_from_errno2("lseek", abspath); + goto done; + } + +done: + if (err) { + close(*fd); + *fd = -1; + } + return err; +} + static const struct got_error * +emit_base_commit_header(FILE *f, struct got_diff_line **lines, size_t *nlines, + struct got_object_id *commit_id, struct got_worktree *worktree) +{ + const struct got_error *err; + struct got_object_id *base_commit_id; + char *base_commit_idstr; + int n; + + if (worktree == NULL) /* shouldn't happen */ + return got_error(GOT_ERR_NOT_WORKTREE); + + base_commit_id = got_worktree_get_base_commit_id(worktree); + + if (commit_id != NULL) { + if (got_object_id_cmp(commit_id, base_commit_id) != 0) + base_commit_id = commit_id; + } + + err = got_object_id_str(&base_commit_idstr, base_commit_id); + if (err != NULL) + return err; + + if ((n = fprintf(f, "commit - %s\n", base_commit_idstr)) < 0) + err = got_error_from_errno("fprintf"); + free(base_commit_idstr); + if (err != NULL) + return err; + + return add_line_metadata(lines, nlines, + (*lines)[*nlines - 1].offset + n, GOT_DIFF_LINE_META); +} + +static const struct got_error * +tog_worktree_diff(void *arg, unsigned char status, unsigned char staged_status, + const char *path, struct got_object_id *blob_id, + struct got_object_id *staged_blob_id, struct got_object_id *commit_id, + int dirfd, const char *de_name) +{ + const struct got_error *err = NULL; + struct diff_worktree_arg *a = arg; + struct got_blob_object *blob1 = NULL; + struct stat sb; + FILE *f2 = NULL; + char *abspath = NULL, *label1 = NULL; + off_t size1 = 0; + off_t outoff = 0; + int fd = -1, fd1 = -1, fd2 = -1; + int n, f2_exists = 1; + + if (a->diff_staged) { + if (staged_status != GOT_STATUS_MODIFY && + staged_status != GOT_STATUS_ADD && + staged_status != GOT_STATUS_DELETE) + return NULL; + } else { + if (staged_status == GOT_STATUS_DELETE) + return NULL; + if (status == GOT_STATUS_NONEXISTENT) + return got_error_set_errno(ENOENT, path); + if (status != GOT_STATUS_MODIFY && + status != GOT_STATUS_ADD && + status != GOT_STATUS_DELETE && + status != GOT_STATUS_CONFLICT) + return NULL; + } + + err = got_opentemp_truncate(a->f1); + if (err != NULL) + return got_error_from_errno("got_opentemp_truncate"); + err = got_opentemp_truncate(a->f2); + if (err != NULL) + return got_error_from_errno("got_opentemp_truncate"); + + if (!a->header_shown) { + n = fprintf(a->outfile, "path + %s%s\n", + got_worktree_get_root_path(a->worktree), + a->diff_staged ? " (staged changes)" : ""); + if (n < 0) + return got_error_from_errno("fprintf"); + + outoff += n; + err = add_line_metadata(a->lines, a->nlines, outoff, + GOT_DIFF_LINE_META); + if (err != NULL) + return err; + + a->header_shown = 1; + } + + err = emit_base_commit_header(a->outfile, + a->lines, a->nlines, commit_id, a->worktree); + if (err != NULL) + return err; + + if (a->diff_staged) { + const char *label1 = NULL, *label2 = NULL; + + switch (staged_status) { + case GOT_STATUS_MODIFY: + label1 = path; + label2 = path; + break; + case GOT_STATUS_ADD: + label2 = path; + break; + case GOT_STATUS_DELETE: + label1 = path; + break; + default: + return got_error(GOT_ERR_FILE_STATUS); + } + + fd1 = got_opentempfd(); + if (fd1 == -1) + return got_error_from_errno("got_opentempfd"); + + fd2 = got_opentempfd(); + if (fd2 == -1) { + err = got_error_from_errno("got_opentempfd"); + goto done; + } + + err = got_diff_objects_as_blobs(a->lines, a->nlines, + a->f1, a->f2, fd1, fd2, blob_id, staged_blob_id, + label1, label2, a->diff_algo, a->diff_context, + a->ignore_whitespace, a->force_text_diff, + a->diffstat, a->repo, a->outfile); + goto done; + } + + fd1 = got_opentempfd(); + if (fd1 == -1) + return got_error_from_errno("got_opentempfd"); + + if (staged_status == GOT_STATUS_ADD || + staged_status == GOT_STATUS_MODIFY) { + char *id_str; + + err = got_object_open_as_blob(&blob1, + a->repo, staged_blob_id, 8192, fd1); + if (err != NULL) + goto done; + err = got_object_id_str(&id_str, staged_blob_id); + if (err != NULL) + goto done; + if (asprintf(&label1, "%s (staged)", id_str) == -1) { + err = got_error_from_errno("asprintf"); + free(id_str); + goto done; + } + free(id_str); + } else if (status != GOT_STATUS_ADD) { + err = got_object_open_as_blob(&blob1, + a->repo, blob_id, 8192, fd1); + if (err != NULL) + goto done; + } + + if (status != GOT_STATUS_DELETE) { + if (asprintf(&abspath, "%s/%s", + got_worktree_get_root_path(a->worktree), path) == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + + if (dirfd != -1) { + fd = openat(dirfd, de_name, + O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + if (fd == -1) { + if (!got_err_open_nofollow_on_symlink()) { + err = got_error_from_errno2("openat", + abspath); + goto done; + } + err = get_symlink_target_file(&fd, + dirfd, de_name, abspath); + if (err != NULL) + goto done; + } + } else { + fd = open(abspath, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + if (fd == -1) { + if (!got_err_open_nofollow_on_symlink()) { + err = got_error_from_errno2("open", + abspath); + goto done; + } + err = get_symlink_target_file(&fd, + dirfd, de_name, abspath); + if (err != NULL) + goto done; + } + } + if (fstat(fd, &sb) == -1) { + err = got_error_from_errno2("fstat", abspath); + goto done; + } + f2 = fdopen(fd, "r"); + if (f2 == NULL) { + err = got_error_from_errno2("fdopen", abspath); + goto done; + } + fd = -1; + } else { + sb.st_size = 0; + f2_exists = 0; + } + + if (blob1 != NULL) { + err = got_object_blob_dump_to_file(&size1, + NULL, NULL, a->f1, blob1); + if (err != NULL) + goto done; + } + + err = got_diff_blob_file(a->lines, a->nlines, blob1, a->f1, size1, + label1, f2 != NULL ? f2 : a->f2, f2_exists, &sb, path, + tog_diff_algo, a->diff_context, a->ignore_whitespace, + a->force_text_diff, a->diffstat, a->outfile); + +done: + if (fd != -1 && close(fd) == -1 && err == NULL) + err = got_error_from_errno("close"); + if (fd1 != -1 && close(fd1) == -1 && err == NULL) + err = got_error_from_errno("close"); + if (fd2 != -1 && close(fd2) == -1 && err == NULL) + err = got_error_from_errno("close"); + if (blob1 != NULL) + got_object_blob_close(blob1); + if (f2 != NULL && fclose(f2) == EOF && err == NULL) + err = got_error_from_errno("fclose"); + free(abspath); + free(label1); + return err; +} + +static const struct got_error * +tog_diff_worktree(struct tog_diff_view_state *s, FILE *f, + struct got_diff_line **lines, size_t *nlines, + struct got_diffstat_cb_arg *dsa) +{ + const struct got_error *close_err, *err; + struct got_worktree *worktree = NULL; + struct diff_worktree_arg arg; + struct got_pathlist_head pathlist; + char *cwd, *id_str = NULL; + + TAILQ_INIT(&pathlist); + + cwd = getcwd(NULL, 0); + if (cwd == NULL) + return got_error_from_errno("getcwd"); + + err = got_worktree_open(&worktree, cwd, NULL); + if (err != NULL) + goto done; + + err = got_object_id_str(&id_str, + got_worktree_get_base_commit_id(worktree)); + if (err != NULL) + goto done; + + err = got_repo_match_object_id(&s->id1, NULL, id_str, + GOT_OBJ_TYPE_ANY, &tog_refs, s->repo); + if (err != NULL) + goto done; + + arg.id_str = id_str; + arg.diff_algo = tog_diff_algo; + arg.repo = s->repo; + arg.worktree = worktree; + arg.diffstat = dsa; + arg.diff_context = s->diff_context; + arg.diff_staged = s->diff_staged; + arg.ignore_whitespace = s->ignore_whitespace; + arg.force_text_diff = s->force_text_diff; + arg.header_shown = 0; + arg.lines = lines; + arg.nlines = nlines; + arg.f1 = s->f1; + arg.f2 = s->f2; + arg.outfile = f; + + err = add_line_metadata(lines, nlines, 0, GOT_DIFF_LINE_NONE); + if (err != NULL) + goto done; + + if (s->paths == NULL) { + err = got_pathlist_insert(NULL, &pathlist, "", NULL); + if (err != NULL) + goto done; + } + + err = got_worktree_status(worktree, s->paths ? s->paths : &pathlist, + s->repo, 0, tog_worktree_diff, &arg, NULL, NULL); + if (err != NULL) + goto done; + + if (*nlines == 1) { + const char *msg = TOG_WORKTREE_CHANGES_LOCAL_MSG; + int n, victim = TOG_WORKTREE_CHANGES_LOCAL; + + if (s->diff_staged) { + victim = TOG_WORKTREE_CHANGES_STAGED; + msg = TOG_WORKTREE_CHANGES_STAGED_MSG; + } + if ((n = fprintf(f, "no %s\n", msg)) < 0) { + err = got_ferror(f, GOT_ERR_IO); + goto done; + } + err = add_line_metadata(lines, nlines, n, GOT_DIFF_LINE_META); + if (err != NULL) + goto done; + if (s->parent_view && s->parent_view->type == TOG_VIEW_LOG) + evict_worktree_entry( + &s->parent_view->state.log.thread_args, victim); + err = got_error(GOT_ERR_DIFF_NOCHANGES); + } + +done: + free(cwd); + free(id_str); + got_pathlist_free(&pathlist, GOT_PATHLIST_FREE_NONE); + if (worktree != NULL) { + if ((close_err = got_worktree_close(worktree)) != NULL) { + if (err == NULL || err->code == GOT_ERR_DIFF_NOCHANGES) + err = close_err; + } + } + return err; +} + +static const struct got_error * +tog_diff_objects(struct tog_diff_view_state *s, FILE *f, + struct got_diff_line **lines, size_t *nlines, + struct got_diffstat_cb_arg *dsa) +{ + const struct got_error *err; + int obj_type; + + if (s->id1) + err = got_object_get_type(&obj_type, s->repo, s->id1); + else + err = got_object_get_type(&obj_type, s->repo, s->id2); + if (err != NULL) + return err; + + switch (obj_type) { + case GOT_OBJ_TYPE_BLOB: + err = got_diff_objects_as_blobs(lines, nlines, s->f1, s->f2, + s->fd1, s->fd2, s->id1, s->id2, NULL, NULL, tog_diff_algo, + s->diff_context, s->ignore_whitespace, s->force_text_diff, + dsa, s->repo, f); + if (err != NULL) + return err; + break; + case GOT_OBJ_TYPE_TREE: + err = got_diff_objects_as_trees(lines, nlines, + s->f1, s->f2, s->fd1, s->fd2, s->id1, s->id2, + s->paths, "", "", tog_diff_algo, s->diff_context, + s->ignore_whitespace, s->force_text_diff, dsa, s->repo, f); + if (err != NULL) + return err; + break; + case GOT_OBJ_TYPE_COMMIT: { + const struct got_object_id_queue *parent_ids; + struct got_commit_object *commit2; + struct got_object_qid *pid; + struct got_reflist_head *refs; + + err = got_diff_objects_as_commits(lines, nlines, s->f1, s->f2, + s->fd1, s->fd2, s->id1, s->id2, s->paths, tog_diff_algo, + s->diff_context, s->ignore_whitespace, s->force_text_diff, + dsa, s->repo, f); + if (err != NULL) + return err; + + refs = got_reflist_object_id_map_lookup(tog_refs_idmap, s->id2); + /* Show commit info if we're diffing to a parent/root commit. */ + if (s->id1 == NULL) + return write_commit_info(&s->lines, &s->nlines, s->id2, + refs, s->repo, s->ignore_whitespace, + s->force_text_diff, dsa, s->f); + + err = got_object_open_as_commit(&commit2, s->repo, + s->id2); + if (err != NULL) + return err; + + parent_ids = got_object_commit_get_parent_ids(commit2); + STAILQ_FOREACH(pid, parent_ids, entry) { + if (got_object_id_cmp(s->id1, &pid->id) == 0) { + err = write_commit_info(&s->lines, &s->nlines, + s->id2, refs, s->repo, s->ignore_whitespace, + s->force_text_diff, dsa, s->f); + break; + } + } + if (commit2 != NULL) + got_object_commit_close(commit2); + if (err != NULL) + return err; + break; + } + default: + return got_error(GOT_ERR_OBJ_TYPE); + } + + return NULL; +} + +static const struct got_error * create_diff(struct tog_diff_view_state *s) { const struct got_error *err = NULL; FILE *tmp_diff_file = NULL; - int obj_type; struct got_diff_line *lines = NULL; struct got_pathlist_head changed_paths; - struct got_commit_object *commit2 = NULL; struct got_diffstat_cb_arg dsa; size_t nlines = 0; @@ -5445,88 +6570,37 @@ create_diff(struct tog_diff_view_state *s) goto done; } - if (s->id1) - err = got_object_get_type(&obj_type, s->repo, s->id1); - else - err = got_object_get_type(&obj_type, s->repo, s->id2); - if (err) - goto done; + if (s->parent_view != NULL && s->parent_view->type == TOG_VIEW_LOG) { + struct tog_log_view_state *ls = &s->parent_view->state.log; + struct commit_queue_entry *cqe = ls->selected_entry; - switch (obj_type) { - case GOT_OBJ_TYPE_BLOB: - err = got_diff_objects_as_blobs(&lines, &nlines, - s->f1, s->f2, s->fd1, s->fd2, s->id1, s->id2, - NULL, NULL, tog_diff_algo, s->diff_context, - s->ignore_whitespace, s->force_text_diff, &dsa, s->repo, - tmp_diff_file); - if (err != NULL) - goto done; - break; - case GOT_OBJ_TYPE_TREE: - err = got_diff_objects_as_trees(&lines, &nlines, - s->f1, s->f2, s->fd1, s->fd2, s->id1, s->id2, NULL, "", "", - tog_diff_algo, s->diff_context, s->ignore_whitespace, - s->force_text_diff, &dsa, s->repo, tmp_diff_file); - if (err != NULL) - goto done; - break; - case GOT_OBJ_TYPE_COMMIT: { - const struct got_object_id_queue *parent_ids; - struct got_object_qid *pid; - struct got_reflist_head *refs; - - err = got_diff_objects_as_commits(&lines, &nlines, - s->f1, s->f2, s->fd1, s->fd2, s->id1, s->id2, NULL, - tog_diff_algo, s->diff_context, s->ignore_whitespace, - s->force_text_diff, &dsa, s->repo, tmp_diff_file); - if (err) - goto done; - - refs = got_reflist_object_id_map_lookup(tog_refs_idmap, s->id2); - /* Show commit info if we're diffing to a parent/root commit. */ - if (s->id1 == NULL) { - err = write_commit_info(&s->lines, &s->nlines, s->id2, - refs, s->repo, s->ignore_whitespace, - s->force_text_diff, &dsa, s->f); - if (err) - goto done; - } else { - err = got_object_open_as_commit(&commit2, s->repo, - s->id2); - if (err) - goto done; - - parent_ids = got_object_commit_get_parent_ids(commit2); - STAILQ_FOREACH(pid, parent_ids, entry) { - if (got_object_id_cmp(s->id1, &pid->id) == 0) { - err = write_commit_info(&s->lines, - &s->nlines, s->id2, refs, s->repo, - s->ignore_whitespace, - s->force_text_diff, &dsa, s->f); - if (err) - goto done; - break; - } - } + if (cqe->worktree_entry != 0) { + if (cqe->worktree_entry == TOG_WORKTREE_CHANGES_STAGED) + s->diff_staged = 1; + s->diff_worktree = 1; } - break; } - default: - err = got_error(GOT_ERR_OBJ_TYPE); - goto done; + + if (s->diff_worktree) + err = tog_diff_worktree(s, tmp_diff_file, + &lines, &nlines, &dsa); + else + err = tog_diff_objects(s, tmp_diff_file, + &lines, &nlines, &dsa); + if (err != NULL) { + if (err->code != GOT_ERR_DIFF_NOCHANGES) + goto done; + } else { + err = write_diffstat(s->f, &s->lines, &s->nlines, &dsa); + if (err != NULL) + goto done; } - err = write_diffstat(s->f, &s->lines, &s->nlines, &dsa); - if (err != NULL) - goto done; - err = cat_diff(s->f, tmp_diff_file, &s->lines, &s->nlines, lines, nlines); done: free(lines); - if (commit2 != NULL) - got_object_commit_close(commit2); got_pathlist_free(&changed_paths, GOT_PATHLIST_FREE_ALL); if (s->f && fflush(s->f) != 0 && err == NULL) err = got_error_from_errno("fflush"); @@ -5688,7 +6762,9 @@ static const struct got_error * open_diff_view(struct tog_view *view, struct got_object_id *id1, struct got_object_id *id2, const char *label1, const char *label2, int diff_context, int ignore_whitespace, int force_text_diff, - struct tog_view *parent_view, struct got_repository *repo) + int diff_staged, int diff_worktree, const char *worktree_root, + struct tog_view *parent_view, struct got_repository *repo, + struct got_pathlist_head *paths) { const struct got_error *err; struct tog_diff_view_state *s = &view->state.diff; @@ -5713,19 +6789,21 @@ open_diff_view(struct tog_view *view, struct got_objec } } - if (id1) { - s->id1 = got_object_id_dup(id1); - if (s->id1 == NULL) { + if (diff_worktree == 0) { + if (id1) { + s->id1 = got_object_id_dup(id1); + if (s->id1 == NULL) { + err = got_error_from_errno("got_object_id_dup"); + goto done; + } + } else + s->id1 = NULL; + + s->id2 = got_object_id_dup(id2); + if (s->id2 == NULL) { err = got_error_from_errno("got_object_id_dup"); goto done; } - } else - s->id1 = NULL; - - s->id2 = got_object_id_dup(id2); - if (s->id2 == NULL) { - err = got_error_from_errno("got_object_id_dup"); - goto done; } s->f1 = got_opentemp(); @@ -5760,8 +6838,12 @@ open_diff_view(struct tog_view *view, struct got_objec s->diff_context = diff_context; s->ignore_whitespace = ignore_whitespace; s->force_text_diff = force_text_diff; + s->diff_worktree = diff_worktree; + s->diff_staged = diff_staged; s->parent_view = parent_view; + s->paths = paths; s->repo = repo; + s->worktree_root = worktree_root; if (has_colors() && getenv("TOG_COLORS") != NULL && !using_mock_io) { int rc; @@ -5829,30 +6911,38 @@ show_diff_view(struct tog_view *view) { const struct got_error *err; struct tog_diff_view_state *s = &view->state.diff; - char *id_str1 = NULL, *id_str2, *header; - const char *label1, *label2; + char *header; - if (s->id1) { - err = got_object_id_str(&id_str1, s->id1); + if (s->diff_worktree) { + if (asprintf(&header, "diff %s%s", + s->diff_staged ? "-s " : "", s->worktree_root) == -1) + return got_error_from_errno("asprintf"); + } else { + char *id_str2, *id_str1 = NULL; + const char *label1, *label2; + + if (s->id1) { + err = got_object_id_str(&id_str1, s->id1); + if (err) + return err; + label1 = s->label1 ? s->label1 : id_str1; + } else + label1 = "/dev/null"; + + err = got_object_id_str(&id_str2, s->id2); if (err) return err; - label1 = s->label1 ? s->label1 : id_str1; - } else - label1 = "/dev/null"; + label2 = s->label2 ? s->label2 : id_str2; - err = got_object_id_str(&id_str2, s->id2); - if (err) - return err; - label2 = s->label2 ? s->label2 : id_str2; - - if (asprintf(&header, "diff %s %s", label1, label2) == -1) { - err = got_error_from_errno("asprintf"); + if (asprintf(&header, "diff %s %s", label1, label2) == -1) { + err = got_error_from_errno("asprintf"); + free(id_str1); + free(id_str2); + return err; + } free(id_str1); free(id_str2); - return err; } - free(id_str1); - free(id_str2); err = draw_file(view, header); free(header); @@ -5864,9 +6954,10 @@ diff_write_patch(struct tog_view *view) { const struct got_error *err; struct tog_diff_view_state *s = &view->state.diff; + struct got_object_id *id2 = s->id2; FILE *f = NULL; char buf[BUFSIZ], pathbase[PATH_MAX]; - char *idstr2, *idstr1 = NULL, *path = NULL; + char *idstr1, *idstr2 = NULL, *path = NULL; size_t r; off_t pos; int rc; @@ -5887,7 +6978,15 @@ diff_write_patch(struct tog_view *view) if (err != NULL) return err; } - err = got_object_id_str(&idstr2, s->id2); + if (id2 == NULL) { + if (s->diff_worktree == 0 || tog_base_commit.id == NULL) { + /* illegal state that should not be possible */ + err = got_error(GOT_ERR_NOT_WORKTREE); + goto done; + } + id2 = tog_base_commit.id; + } + err = got_object_id_str(&idstr2, id2); if (err != NULL) goto done; @@ -5948,19 +7047,26 @@ set_selected_commit(struct tog_diff_view_state *s, struct got_commit_object *selected_commit; struct got_object_qid *pid; - free(s->id2); - s->id2 = got_object_id_dup(entry->id); - if (s->id2 == NULL) - return got_error_from_errno("got_object_id_dup"); - - err = got_object_open_as_commit(&selected_commit, s->repo, entry->id); - if (err) - return err; - parent_ids = got_object_commit_get_parent_ids(selected_commit); free(s->id1); - pid = STAILQ_FIRST(parent_ids); - s->id1 = pid ? got_object_id_dup(&pid->id) : NULL; - got_object_commit_close(selected_commit); + s->id1 = NULL; + free(s->id2); + s->id2 = NULL; + + if (entry->worktree_entry == 0) { + s->id2 = got_object_id_dup(entry->id); + if (s->id2 == NULL) + return got_error_from_errno("got_object_id_dup"); + + err = got_object_open_as_commit(&selected_commit, + s->repo, entry->id); + if (err) + return err; + parent_ids = got_object_commit_get_parent_ids(selected_commit); + pid = STAILQ_FIRST(parent_ids); + s->id1 = pid ? got_object_id_dup(&pid->id) : NULL; + got_object_commit_close(selected_commit); + } + return NULL; } @@ -6206,6 +7312,9 @@ input_diff_view(struct tog_view **new_view, struct tog err = set_selected_commit(s, ls->selected_entry); if (err) break; + + if (s->worktree_root == NULL) + s->worktree_root = ls->thread_args.wctx.wt_root; } else if (s->parent_view->type == TOG_VIEW_BLAME) { struct tog_blame_view_state *bs; struct got_object_id *id, *prev_id; @@ -6235,6 +7344,8 @@ input_diff_view(struct tog_view **new_view, struct tog if (err) break; } + s->diff_staged = 0; + s->diff_worktree = 0; s->first_displayed_line = 1; s->last_displayed_line = view->nlines; s->matched_line = 0; @@ -6254,24 +7365,58 @@ input_diff_view(struct tog_view **new_view, struct tog return err; } + static const struct got_error * +get_worktree_paths_from_argv(struct got_pathlist_head *paths, int argc, + char *argv[], struct got_worktree *worktree) +{ + const struct got_error *err = NULL; + char *path; + struct got_pathlist_entry *new; + int i; + + if (argc == 0) { + path = strdup(""); + if (path == NULL) + return got_error_from_errno("strdup"); + return got_pathlist_insert(NULL, paths, path, NULL); + } + + for (i = 0; i < argc; i++) { + err = got_worktree_resolve_path(&path, worktree, argv[i]); + if (err) + break; + err = got_pathlist_insert(&new, paths, path, NULL); + if (err != NULL || new == NULL) { + free(path); + if (err != NULL) + break; + } + } + + return err; +} + static const struct got_error * cmd_diff(int argc, char *argv[]) { const struct got_error *error; struct got_repository *repo = NULL; struct got_worktree *worktree = NULL; - struct got_object_id *id1 = NULL, *id2 = NULL; - char *repo_path = NULL, *cwd = NULL; - char *id_str1 = NULL, *id_str2 = NULL; - char *keyword_idstr1 = NULL, *keyword_idstr2 = NULL; - char *label1 = NULL, *label2 = NULL; - int diff_context = 3, ignore_whitespace = 0; - int ch, force_text_diff = 0; + struct got_pathlist_head paths; + struct got_object_id *ids[2] = { NULL, NULL }; + const char *commit_args[2] = { NULL, NULL }; + char *labels[2] = { NULL, NULL }; + char *repo_path = NULL, *worktree_path = NULL, *cwd = NULL; + int type1 = GOT_OBJ_TYPE_ANY, type2 = GOT_OBJ_TYPE_ANY; + int i, ncommit_args = 0, diff_context = 3, ignore_whitespace = 0; + int ch, diff_staged = 0, diff_worktree = 0, force_text_diff = 0; const char *errstr; struct tog_view *view; int *pack_fds = NULL; - while ((ch = getopt(argc, argv, "aC:r:w")) != -1) { + TAILQ_INIT(&paths); + + while ((ch = getopt(argc, argv, "aC:c:r:sw")) != -1) { switch (ch) { case 'a': force_text_diff = 1; @@ -6283,6 +7428,11 @@ cmd_diff(int argc, char *argv[]) errx(1, "number of context lines is %s: %s", errstr, errstr); break; + case 'c': + if (ncommit_args >= 2) + errx(1, "too many -c options used"); + commit_args[ncommit_args++] = optarg; + break; case 'r': repo_path = realpath(optarg, NULL); if (repo_path == NULL) @@ -6290,6 +7440,9 @@ cmd_diff(int argc, char *argv[]) optarg); got_path_strip_trailing_slashes(repo_path); break; + case 's': + diff_staged = 1; + break; case 'w': ignore_whitespace = 1; break; @@ -6302,14 +7455,6 @@ cmd_diff(int argc, char *argv[]) argc -= optind; argv += optind; - if (argc == 0) { - usage_diff(); /* TODO show local worktree changes */ - } else if (argc == 2) { - id_str1 = argv[0]; - id_str2 = argv[1]; - } else - usage_diff(); - error = got_repo_pack_fds_open(&pack_fds); if (error) goto done; @@ -6336,52 +7481,215 @@ cmd_diff(int argc, char *argv[]) if (error) goto done; + if (diff_staged && (worktree == NULL || ncommit_args > 0)) { + error = got_error_msg(GOT_ERR_BAD_OPTION, + "-s can only be used when diffing a work tree"); + goto done; + } + init_curses(); - error = apply_unveil(got_repo_get_path(repo), NULL); + error = apply_unveil(got_repo_get_path(repo), + worktree != NULL ? got_worktree_get_root_path(worktree) : NULL); if (error) goto done; - error = tog_load_refs(repo, 0); - if (error) + if (argc == 2 || ncommit_args > 0) { + int obj_type = (ncommit_args > 0 ? + GOT_OBJ_TYPE_COMMIT : GOT_OBJ_TYPE_ANY); + + error = tog_load_refs(repo, 0); + if (error != NULL) + goto done; + + for (i = 0; i < (ncommit_args > 0 ? ncommit_args : argc); ++i) { + const char *arg; + char *keyword_idstr = NULL; + + if (ncommit_args > 0) + arg = commit_args[i]; + else + arg = argv[i]; + + error = got_keyword_to_idstr(&keyword_idstr, arg, + repo, worktree); + if (error != NULL) + goto done; + if (keyword_idstr != NULL) + arg = keyword_idstr; + + error = got_repo_match_object_id(&ids[i], &labels[i], + arg, obj_type, &tog_refs, repo); + free(keyword_idstr); + if (error != NULL) { + if (error->code != GOT_ERR_NOT_REF && + error->code != GOT_ERR_NO_OBJ) + goto done; + if (ncommit_args > 0) + goto done; + error = NULL; + break; + } + } + } + + if (diff_staged && ids[0] != NULL) { + error = got_error_msg(GOT_ERR_BAD_OPTION, + "-s can only be used when diffing a work tree"); goto done; + } - if (id_str1 != NULL) { - error = got_keyword_to_idstr(&keyword_idstr1, id_str1, - repo, worktree); + if (ncommit_args == 0 && (ids[0] == NULL || ids[1] == NULL)) { + if (worktree == NULL) { + if (argc == 2 && ids[0] == NULL) { + error = got_error_path(argv[0], GOT_ERR_NO_OBJ); + goto done; + } else if (argc == 2 && ids[1] == NULL) { + error = got_error_path(argv[1], GOT_ERR_NO_OBJ); + goto done; + } else if (argc > 0) { + error = got_error_fmt(GOT_ERR_NOT_WORKTREE, + "%s", "specified paths cannot be resolved"); + goto done; + } else { + error = got_error(GOT_ERR_NOT_WORKTREE); + goto done; + } + } + + error = get_worktree_paths_from_argv(&paths, argc, argv, + worktree); if (error != NULL) goto done; - if (keyword_idstr1 != NULL) - id_str1 = keyword_idstr1; + + worktree_path = strdup(got_worktree_get_root_path(worktree)); + if (worktree_path == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + diff_worktree = 1; } - if (id_str2 != NULL) { - error = got_keyword_to_idstr(&keyword_idstr2, id_str2, - repo, worktree); + + if (ncommit_args == 1) { /* diff commit against its first parent */ + struct got_commit_object *commit; + + error = got_object_open_as_commit(&commit, repo, ids[0]); if (error != NULL) goto done; - if (keyword_idstr2 != NULL) - id_str2 = keyword_idstr2; + + labels[1] = labels[0]; + ids[1] = ids[0]; + if (got_object_commit_get_nparents(commit) > 0) { + const struct got_object_id_queue *pids; + struct got_object_qid *pid; + + pids = got_object_commit_get_parent_ids(commit); + pid = STAILQ_FIRST(pids); + ids[0] = got_object_id_dup(&pid->id); + if (ids[0] == NULL) { + error = got_error_from_errno( + "got_object_id_dup"); + got_object_commit_close(commit); + goto done; + } + error = got_object_id_str(&labels[0], ids[0]); + if (error != NULL) { + got_object_commit_close(commit); + goto done; + } + } else { + ids[0] = NULL; + labels[0] = strdup("/dev/null"); + if (labels[0] == NULL) { + error = got_error_from_errno("strdup"); + got_object_commit_close(commit); + goto done; + } + } + + got_object_commit_close(commit); } - error = got_repo_match_object_id(&id1, &label1, id_str1, - GOT_OBJ_TYPE_ANY, &tog_refs, repo); - if (error) + if (ncommit_args == 0 && argc > 2) { + error = got_error_msg(GOT_ERR_BAD_PATH, + "path arguments cannot be used when diffing two objects"); goto done; + } - error = got_repo_match_object_id(&id2, &label2, id_str2, - GOT_OBJ_TYPE_ANY, &tog_refs, repo); - if (error) - goto done; + if (ids[0]) { + error = got_object_get_type(&type1, repo, ids[0]); + if (error != NULL) + goto done; + } + if (diff_worktree == 0) { + error = got_object_get_type(&type2, repo, ids[1]); + if (error != NULL) + goto done; + if (type1 != GOT_OBJ_TYPE_ANY && type1 != type2) { + error = got_error(GOT_ERR_OBJ_TYPE); + goto done; + } + if (type1 == GOT_OBJ_TYPE_BLOB && argc > 2) { + error = got_error_msg(GOT_ERR_OBJ_TYPE, + "path arguments cannot be used when diffing blobs"); + goto done; + } + } + + for (i = 0; ncommit_args > 0 && i < argc; i++) { + char *in_repo_path; + struct got_pathlist_entry *new; + + if (worktree) { + const char *prefix; + char *p; + + error = got_worktree_resolve_path(&p, worktree, + argv[i]); + if (error != NULL) + goto done; + prefix = got_worktree_get_path_prefix(worktree); + while (prefix[0] == '/') + prefix++; + if (asprintf(&in_repo_path, "%s%s%s", prefix, + (p[0] != '\0' && prefix[0] != '\0') ? "/" : "", + p) == -1) { + error = got_error_from_errno("asprintf"); + free(p); + goto done; + } + free(p); + } else { + char *mapped_path, *s; + + error = got_repo_map_path(&mapped_path, repo, argv[i]); + if (error != NULL) + goto done; + s = mapped_path; + while (s[0] == '/') + s++; + in_repo_path = strdup(s); + if (in_repo_path == NULL) { + error = got_error_from_errno("asprintf"); + free(mapped_path); + goto done; + } + free(mapped_path); + + } + error = got_pathlist_insert(&new, &paths, in_repo_path, NULL); + if (error != NULL || new == NULL) + free(in_repo_path); + if (error != NULL) + goto done; + } + view = view_open(0, 0, 0, 0, TOG_VIEW_DIFF); if (view == NULL) { error = got_error_from_errno("view_open"); goto done; } - error = open_diff_view(view, id1, id2, label1, label2, diff_context, - ignore_whitespace, force_text_diff, NULL, repo); - if (error) - goto done; if (worktree) { error = set_tog_base_commit(repo, worktree); @@ -6393,17 +7701,23 @@ cmd_diff(int argc, char *argv[]) worktree = NULL; } + error = open_diff_view(view, ids[0], ids[1], labels[0], labels[1], + diff_context, ignore_whitespace, force_text_diff, diff_staged, + diff_worktree, worktree_path, NULL, repo, &paths); + if (error) + goto done; + error = view_loop(view); done: + got_pathlist_free(&paths, GOT_PATHLIST_FREE_PATH); free(tog_base_commit.id); - free(keyword_idstr1); - free(keyword_idstr2); - free(label1); - free(label2); - free(id1); - free(id2); + free(worktree_path); free(repo_path); + free(labels[0]); + free(labels[1]); + free(ids[0]); + free(ids[1]); free(cwd); if (repo) { const struct got_error *close_err = got_repo_close(repo); @@ -7334,7 +8648,7 @@ input_blame_view(struct tog_view **new_view, struct to } } err = open_diff_view(diff_view, pid ? &pid->id : NULL, - id, NULL, NULL, 3, 0, 0, view, s->repo); + id, NULL, NULL, 3, 0, 0, 0, 0, NULL, view, s->repo, NULL); got_object_commit_close(commit); if (err) break; @@ -9935,8 +11249,7 @@ view_dispatch_request(struct tog_view **new_view, stru struct tog_log_view_state *s = &view->state.log; err = open_diff_view_for_commit(new_view, y, x, - s->selected_entry->commit, s->selected_entry->id, - view, s->repo); + s->selected_entry, view, s->repo); } else return got_error_msg(GOT_ERR_NOT_IMPL, "parent/child view pair not supported"); -- Mark Jamsek GPG: F2FF 13DE 6A06 C471 CA80 E6E2 2930 DC66 86EE CF68