From: Mark Jamsek Subject: implement support for work tree diffs in tog To: gameoftrees@openbsd.org Date: Fri, 06 Dec 2024 02:32:03 +1100 The below diff introduces support for work tree diffs via the tog diff CLI with `tog diff [-s] [path ...]` and the log view. When tog log is opened with work tree changes, a log entry is shown at the top of the first page for both unstaged and staged changes. The log view can be refreshed with ^l (or B) to reload the log with new worktree changes. However, an up-to-date diff is always displayed when selecting a work tree entry. So if tog is open while changes are repeatedly made, there's no need to use ^l; reopening or refreshing (e.g., A, [,], w) a diff view of worktree changes will always show an accurate diff. ^l is only needed if tog is invoked in a clean work tree that subsequently becomes dirty, or if changes are staged/unstaged or reverted during the same tog process. The work tree status is fetched in the log thread so we don't block user input, which is necessary for large repos like src. I want to highlight a couple design decisions. If not interested in such details, please ignore and jump straight to the diff. There are likely other choices that can be tweaked, too, but I think it's best to start with this and continue improving it in-tree. After wider use, further changes will also become more apparent. The index in the log header shows [0/N] when an unstaged or staged work tree entry is selected so the first actual commit entry still starts with [1/N]. The index is useful for determining the modifier in keyword arguments to got commands (e.g., `got up -c:base:-N`). And work tree entries aren't commits so it seems wrong to count them as such. I did try eliding the index when work tree entries are selected, but the header looked a little weird without it, and it hides useful info (such as how many commits are loaded). The author displayed in the log summary line is derived from (in order from highest to lowest priority): the work tree got.conf; repo got.conf; repo git.conf; or the GOT_AUTHOR environment variable. There's a simple algorithm to reveal work tree entries when the work tree status has been fetched: if the first commit is still displayed, scroll the log view up to reveal all work tree entries; and if the first commit entry was selected, assume the user has not moved the cursor, so make the first work tree entry the new selection. But if the cursor has moved off the first entry, assume there has been user navigation, so don't move the cursor out from underneath them. This attempts to provide discoverability without interfering with user input. Lastly, the hash id field in the log summary line is filled with '.' placeholders rather than blanks. This somewhat matches the blame view that shows '........' when commits are still loading and blanks when the commit matches the preceding line. Work tree changes are like a commit in the making so dots seem a better fit than blanks. And look better. regress is still happy, and some manual testing of the grep (/) and limit (&) keymaps, and writing work tree diffs to disk with the (p)atch keymap are showing no breakage. But the diff is large and there is substantial refactoring of the curses rendering bits in particular. There are potentional improvements to this feature that I have in mind, some of which have already been discussed with stsp on IRC, but like the diff, this email is already too long so I'll leave them for later. Relatedly, I'd like to propose remapping the ^l keymap or aliasing it with another keymap as I use ^l to switch panes in tmux, which I'm always in so I'm stuck with B to reload the log view. I think this will compel me to work on automagically reloading the log view when there are new work tree changes next. commit cdf722390eb0ea910337bdec827b5e45610aa53a (main) from: Mark Jamsek date: Thu Dec 5 14:53:48 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 | 1665+ 347- 10 files changed, 1772 insertions(+), 381 deletions(-) commit - 8c5906074ad5756611aa1977542e314d8e3662bb commit + cdf722390eb0ea910337bdec827b5e45610aa53a 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 + 6980025801b6118c3765402425c9d5a9412de44b --- 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,15 @@ 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; + wchar_t *wrefstr = NULL, *wlogmsg = NULL; + int refstr_width, logmsg_width; + char *newline; int 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,40 +2732,20 @@ 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) + return err; + err = draw_idstr(view, id_str, TOG_COLOR_COMMIT); + if (err != NULL) 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); free(id_str); col += 9; if (col > avail) @@ -2551,34 +2760,10 @@ draw_commit(struct tog_view *view, struct commit_queue err = got_error_from_errno("strdup"); goto done; } - err = format_author(&wauthor, &author_width, author, avail - col, col); - if (err) + err = draw_author(view, author, author_display_cols, + avail - col, &col, TOG_COLOR_AUTHOR, marker_column, entry); + if (err != NULL) 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 +2798,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 +2833,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 +2873,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 +3037,347 @@ 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) +{ + /* can we even have a worktree outside of the refs/heads/ namespace? */ + 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 +3396,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 +3407,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 +3515,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 +3554,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 +3571,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 +3723,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 +3882,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 +4035,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 +4182,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 +4227,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 +4316,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 +4444,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 +4590,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 +4601,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 +4611,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 +5054,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 +5400,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 +6031,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 +6575,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 +6767,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 +6794,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 +6843,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 +6916,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 +6959,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 +6983,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 +7052,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 +7317,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 +7349,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 +7370,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 +7433,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 +7445,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 +7460,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 +7486,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 +7706,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 +8653,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 +11254,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