"GOT", but the "O" is a cute, smiling pufferfish. Index | Thread | Search

From:
Mark Jamsek <mark@jamsek.com>
Subject:
Re: implement support for work tree diffs in tog
To:
Mark Jamsek <mark@jamsek.com>
Cc:
Stefan Sperling <stsp@stsp.name>, gameoftrees@openbsd.org
Date:
Sat, 07 Dec 2024 14:55:40 +1100

Download raw body.

Thread
Mark Jamsek <mark@jamsek.com> wrote:
> Stefan Sperling <stsp@stsp.name> 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 <mark@jamsek.dev>
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 <ctype.h>
 #include <errno.h>
+#include <fcntl.h>
 #define _XOPEN_SOURCE_EXTENDED /* for ncurses wide-character functions */
 #include <curses.h>
 #include <panel.h>
@@ -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 <email>", 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 <https://bsdbox.org>
GPG: F2FF 13DE 6A06 C471 CA80  E6E2 2930 DC66 86EE CF68