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

From:
Mark Jamsek <mark@jamsek.com>
Subject:
tog: diff local worktree changes
To:
Game of Trees <gameoftrees@openbsd.org>
Date:
Fri, 26 Aug 2022 01:42:46 +1000

Download raw body.

Thread
The below diff enables diffing local changes in the work tree. I've
just copied the `got diff` interface. I think this is good because it
makes the UI consistent, which makes sense imo. Now we can view changes
in the work tree with:

  tog diff [path ...]

Also like `got diff`, -c can be used up to two times with optional path
arguments, or two positional object arguments can be specified, in which
case path arguments are invalid. And we have added the -s switch to `tog
diff` now to diff staged changes. So we've only added to the tog diff
interface--not broken the existing syntax.

There's some copypasta, and maybe things like parsing paths from argv
and symlink targets could end up in the lib somewhere, but that can be
discussed if you think it makes sense. I thought this would come in
handy next week--I like the advantages tog provides like increasing
context--so I wanted to get this done quick before flying out later
today.

I'm off to zzz now and most likely won't be able to hack again till in
transit or layover so no rush :)

btw there are two simple typo bug fixes included in this diff that I'm
sending to the list separately.

diff refs/heads/main refs/heads/dev/togdiff
commit - 9e0116c7f0bcd2dee2c9dd0f27ac65e8ea30ae53
commit + ed43f994c97b24257d53db48c40569dc85f66617
blob - 892b622a43e9f228674e275339385e0cebd26615
blob + cb28cb5d99a1907baa446a38a9c01570fbe5793a
--- got/got.c
+++ got/got.c
@@ -4864,9 +4864,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.st_size, path, GOT_DIFF_ALGORITHM_PATIENCE,
-	    a->diff_context, a->ignore_whitespace, a->force_text_diff, stdout);
+	err = got_diff_blob_file(NULL, NULL, blob1, a->f1, size1, label1,
+	    f2 ? f2 : a->f2, f2_exists, sb.st_size, path,
+	    GOT_DIFF_ALGORITHM_PATIENCE, a->diff_context, a->ignore_whitespace,
+	    a->force_text_diff, stdout);
 done:
 	if (fd1 != -1 && close(fd1) == -1 && err == NULL)
 		err = got_error_from_errno("close");
blob - bc4bc871ee90bf32c4fd2bd3b2b452f29d5822a0
blob + c1c6321d78fe3711ca0858d9a112e59f742acbef
--- include/got_diff.h
+++ include/got_diff.h
@@ -73,9 +73,9 @@ const struct got_error *got_diff_blob(struct got_diff_
  * The number of context lines to show in the diff must be specified as well.
  * Whitespace differences may optionally be ignored.
  */
-const struct got_error *got_diff_blob_file(struct got_blob_object *, FILE *,
-    off_t, const char *, FILE *, int, size_t, const char *,
-    enum got_diff_algorithm, int, int, int, 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, size_t,
+    const char *, enum got_diff_algorithm, int, int, int, FILE *);
 
 /*
  * A callback function invoked to handle the differences between two blobs
blob - ec271a03249db4f54f213d8fd71a18c6220153ef
blob + 239a8f333e5f43089b7c7591f530da15d10c755c
--- lib/diff.c
+++ lib/diff.c
@@ -219,17 +219,27 @@ 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, size_t size2, const char *label2,
-    enum got_diff_algorithm diff_algo, int diff_context, int ignore_whitespace,
-    int force_text_diff, 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,
+    size_t size2, const char *label2, enum got_diff_algorithm diff_algo,
+    int diff_context, int ignore_whitespace, int force_text_diff, FILE *outfile)
 {
 	const struct got_error *err = NULL, *free_err;
 	char hex1[SHA1_DIGEST_STRING_LENGTH];
 	const char *idstr1 = NULL;
 	struct got_diffreg_result *result = NULL;
+	off_t outoff = 0;
+	int n;
 
+	if (lines && *lines && *nlines > 0)
+		outoff = (*lines)[*nlines - 1].offset;
+	else if (lines) {
+		err = add_line_metadata(lines, nlines, 0, GOT_DIFF_LINE_NONE);
+		if (err)
+			goto done;
+	}
+
 	if (resultp)
 		*resultp = NULL;
 
@@ -239,9 +249,31 @@ diff_blob_file(struct got_diffreg_result **resultp,
 		idstr1 = "/dev/null";
 
 	if (outfile) {
-		fprintf(outfile, "blob - %s\n", label1 ? label1 : idstr1);
-		fprintf(outfile, "file + %s\n",
+		n = fprintf(outfile, "blob - %s\n", label1 ? label1 : idstr1);
+		if (n < 0) {
+			err = got_error_from_errno("fprintf");
+			goto done;
+		}
+		outoff += n;
+		if (lines) {
+			err = add_line_metadata(lines, nlines, outoff,
+			    GOT_DIFF_LINE_BLOB_MIN);
+			if (err)
+				goto done;
+		}
+		n = fprintf(outfile, "file + %s\n",
 		    f2_exists ? label2 : "/dev/null");
+		if (n < 0) {
+			err = got_error_from_errno("fprintf");
+			goto done;
+		}
+		outoff += n;
+		if (lines) {
+			err = add_line_metadata(lines, nlines, outoff,
+			    GOT_DIFF_LINE_BLOB_PLUS);
+			if (err)
+				goto done;
+		}
 	}
 
 	err = got_diffreg(&result, f1, f2, diff_algo, ignore_whitespace,
@@ -250,7 +282,7 @@ diff_blob_file(struct got_diffreg_result **resultp,
 		goto done;
 
 	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,
@@ -271,14 +303,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, size_t size2,
-    const char *label2, enum got_diff_algorithm diff_algo, int diff_context,
+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, size_t size2, const char *label2,
+    enum got_diff_algorithm diff_algo, int diff_context,
     int ignore_whitespace, int force_text_diff, FILE *outfile)
 {
-	return diff_blob_file(NULL, blob1, f1, size1, label1, f2, f2_exists,
-	    size2, label2, diff_algo, diff_context, ignore_whitespace,
-	    force_text_diff, outfile );
+	return diff_blob_file(lines, nlines, NULL, blob1, f1, size1, label1,
+	    f2, f2_exists, size2, label2, diff_algo, diff_context,
+	    ignore_whitespace, force_text_diff, outfile);
 }
 
 static const struct got_error *
blob - cbd5233dd61d780f245c5bcb24d95b030a5a7676
blob + ef8017042ef90e7bc8bb30efa7ece087fdfcea31
--- tog/tog.1
+++ tog/tog.1
@@ -251,8 +251,15 @@ If this directory is a
 .Xr got 1
 work tree, use the repository path associated with this work tree.
 .El
-.It Cm diff Oo Fl aw Oc Oo Fl C Ar number Oc Oo Fl r Ar repository-path Oc Ar object1 Ar object2
-Display the differences between two objects in the repository.
+.It Cm diff Oo Fl asw Oc Oo Fl C Ar number Oc Oo Fl c Ar commit Oc Oo Fl r Ar repository-path Oc Op Ar object1 Ar object2 | Ar path ...
+When 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, or an object
 ID SHA1 hash, and display differences between the corresponding objects.
 Both objects must be of the same type (blobs, trees, or commits).
@@ -355,6 +362,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 SHA1 hash or an existing reference
+or tag name which will be resolved to a commit ID.
+An abbreviated hash argument will be expanded to a full SHA1 hash
+automatically, provided the abbreviation is unique.
+.Pp
+If the
+.Fl c
+option is used, all non-option arguments will be 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
@@ -362,6 +388,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 - 6ec8b5f0c155fafe55ee2aab0653d259876e6254
blob + 0d5838ec7268890a58d0f45563a34f1e2683638d
--- 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>
@@ -313,6 +314,23 @@ 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;
+	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;
@@ -325,7 +343,11 @@ 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_worktree *worktree;
+	struct got_pathlist_head *paths;
 	struct got_diff_line *lines;
 	size_t nlines;
 	int matched_line;
@@ -570,8 +592,8 @@ 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 *);
+    const char *, const char *, int, int, int, int, int, struct tog_view *,
+    struct got_repository *, struct got_worktree *, 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);
@@ -2553,7 +2575,7 @@ open_diff_view_for_commit(struct tog_view **new_view, 
 
 	parent_id = STAILQ_FIRST(got_object_commit_get_parent_ids(commit));
 	err = open_diff_view(diff_view, parent_id ? &parent_id->id : NULL,
-	    commit_id, NULL, NULL, 3, 0, 0, log_view, repo);
+	    commit_id, NULL, NULL, 3, 0, 0, 0, 0, log_view, repo, NULL, NULL);
 	if (err == NULL)
 		*new_view = diff_view;
 	return err;
@@ -3746,8 +3768,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);
 }
 
@@ -4335,7 +4358,311 @@ done:
 	return err;
 }
 
+/*
+ * 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 *
+diff_worktree(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)
+{
+	struct diff_worktree_arg	*a = arg;
+	struct got_blob_object		*blob1 = NULL;
+	const struct got_error		*err = 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)
+		return got_error_from_errno("got_opentemp_truncate");
+	err = got_opentemp_truncate(a->f2);
+	if (err)
+		return got_error_from_errno("got_opentemp_truncate");
+
+	if (!a->header_shown) {
+		n = fprintf(a->outfile, "commit - %s\n", a->id_str);
+		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)
+			return err;
+
+		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)
+			return err;
+		a->header_shown = 1;
+	}
+
+	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) {
+			err = got_error_from_errno("got_opentempfd");
+			goto done;
+		}
+		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->repo, a->outfile);
+		goto done;
+	}
+
+	fd1 = got_opentempfd();
+	if (fd1 == -1) {
+		err = got_error_from_errno("got_opentempfd");
+		goto done;
+	}
+
+	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)
+			goto done;
+		err = got_object_id_str(&id_str, staged_blob_id);
+		if (err)
+			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)
+			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)
+					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)
+					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) {
+		err = got_object_blob_dump_to_file(&size1, NULL, NULL, a->f1,
+		    blob1);
+		if (err)
+			goto done;
+	}
+
+	err = got_diff_blob_file(a->lines, a->nlines, blob1, a->f1, size1,
+	    label1, f2 ? f2 : a->f2, f2_exists, sb.st_size, path, tog_diff_algo,
+	    a->diff_context, a->ignore_whitespace, a->force_text_diff,
+	    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)
+		got_object_blob_close(blob1);
+	if (f2 && fclose(f2) == EOF && err == NULL)
+		err = got_error_from_errno("fclose");
+	free(abspath);
+	return err;
+}
+
+static const struct got_error *
+tog_diff_worktree(struct tog_diff_view_state *s)
+{
+	struct diff_worktree_arg	 arg;
+	const struct got_error		*err = NULL;
+	char				*id_str;
+	int				 n;
+
+	err = got_object_id_str(&id_str,
+	    got_worktree_get_base_commit_id(s->worktree));
+	if (err)
+		return err;
+	err = got_repo_match_object_id(&s->id1, NULL, id_str, GOT_OBJ_TYPE_ANY,
+	    &tog_refs, s->repo);
+	if (err)
+		goto done;
+
+	arg.id_str = id_str;
+	arg.diff_algo = tog_diff_algo;
+	arg.repo = s->repo;
+	arg.worktree = s->worktree;
+	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 = &s->lines;
+	arg.nlines = &s->nlines;
+	arg.f1 = s->f1;
+	arg.f2 = s->f2;
+	arg.outfile = s->f;
+
+	err = add_line_metadata(&s->lines, &s->nlines, 0, GOT_DIFF_LINE_NONE);
+	if (err)
+		goto done;
+
+	err = got_worktree_status(s->worktree, s->paths, s->repo, 0,
+	    diff_worktree, &arg, NULL, NULL);
+	if (err == NULL && s->nlines == 1) {
+		n = fprintf(s->f, "no %schanges\n",
+		    s->diff_staged ? "staged " : "local ");
+		if (n < 0) {
+			err = got_error_from_errno("fprintf");
+			goto done;
+		}
+		err = add_line_metadata(&s->lines, &s->nlines, n,
+		    GOT_DIFF_LINE_META);
+	}
+done:
+	free(id_str);
+	return err;
+}
+static const struct got_error *
 create_diff(struct tog_diff_view_state *s)
 {
 	const struct got_error *err = NULL;
@@ -4359,6 +4686,11 @@ create_diff(struct tog_diff_view_state *s)
 	}
 	s->f = f;
 
+	if (s->diff_worktree) {
+		err = tog_diff_worktree(s);
+		goto done;
+	}
+
 	if (s->id1)
 		err = got_object_get_type(&obj_type, s->repo, s->id1);
 	else
@@ -4375,9 +4707,9 @@ create_diff(struct tog_diff_view_state *s)
 		break;
 	case GOT_OBJ_TYPE_TREE:
 		err = got_diff_objects_as_trees(&s->lines, &s->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, s->repo, s->f);
+		    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, s->repo, s->f);
 		break;
 	case GOT_OBJ_TYPE_COMMIT: {
 		const struct got_object_id_queue *parent_ids;
@@ -4411,7 +4743,7 @@ create_diff(struct tog_diff_view_state *s)
 		got_object_commit_close(commit2);
 
 		err = got_diff_objects_as_commits(&s->lines, &s->nlines,
-		    s->f1, s->f2, s->fd1, s->fd2, s->id1, s->id2, NULL,
+		    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, s->repo, s->f);
 		break;
@@ -4551,7 +4883,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, struct tog_view *parent_view,
+    struct got_repository *repo, struct got_worktree *worktree,
+    struct got_pathlist_head *paths)
 {
 	const struct got_error *err;
 	struct tog_diff_view_state *s = &view->state.diff;
@@ -4581,17 +4915,19 @@ open_diff_view(struct tog_view *view, struct got_objec
 	s->label1 = label1;
 	s->label2 = label2;
 
-	if (id1) {
-		s->id1 = got_object_id_dup(id1);
-		if (s->id1 == NULL)
-			return got_error_from_errno("got_object_id_dup");
-	} else
-		s->id1 = NULL;
+	if (!diff_worktree) {
+		if (id1) {
+			s->id1 = got_object_id_dup(id1);
+			if (s->id1 == NULL)
+				return got_error_from_errno("got_object_id_dup");
+		} 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->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();
@@ -4623,8 +4959,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_staged = diff_staged;
+	s->diff_worktree = diff_worktree;
 	s->parent_view = parent_view;
 	s->repo = repo;
+	s->worktree = worktree;
+	s->paths = paths;
 
 	if (has_colors() && getenv("TOG_COLORS") != NULL) {
 		int rc;
@@ -4688,27 +5028,35 @@ show_diff_view(struct tog_view *view)
 	char *id_str1 = NULL, *id_str2, *header;
 	const char *label1, *label2;
 
-	if (s->id1) {
-		err = got_object_id_str(&id_str1, s->id1);
-		if (err)
+	if (s->diff_worktree) {
+		if (asprintf(&header, "diff %s%s", s->diff_staged ? "-s " : "",
+		    got_worktree_get_root_path(s->worktree)) == -1)
+			return got_error_from_errno("asprintf");
+	} else {
+		if (s->id1) {
+			err = got_object_id_str(&id_str1, s->id1);
+			if (err)
+				return err;
+			label1 = s->label1 ? : id_str1;
+		} else
+			label1 = "/dev/null";
+
+		err = got_object_id_str(&id_str2, s->id2);
+		if (err) {
+			free(id_str1);
 			return err;
-		label1 = s->label1 ? : id_str1;
-	} else
-		label1 = "/dev/null";
+		}
+		label2 = s->label2 ? : id_str2;
 
-	err = got_object_id_str(&id_str2, s->id2);
-	if (err)
-		return err;
-	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);
@@ -5020,22 +5368,59 @@ input_diff_view(struct tog_view **new_view, struct tog
 }
 
 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_append(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 || new == NULL /* duplicate */) {
+			free(path);
+			if (err)
+				break;
+		}
+	}
+
+	return err;
+}
+
+static const struct got_error *
 cmd_diff(int argc, char *argv[])
 {
 	const struct got_error *error = NULL;
 	struct got_repository *repo = NULL;
 	struct got_worktree *worktree = NULL;
-	struct got_object_id *id1 = NULL, *id2 = NULL;
+	struct got_pathlist_head paths;
+	struct got_pathlist_entry *pe;
+	struct got_object_id *ids[2] = { NULL, NULL };
+	const char *commit_args[2] = { NULL, NULL };
+	char *labels[2] = { NULL, NULL };
 	char *repo_path = NULL, *cwd = NULL;
-	char *id_str1 = NULL, *id_str2 = NULL;
-	char *label1 = NULL, *label2 = NULL;
+	int type1 = GOT_OBJ_TYPE_ANY, type2 = GOT_OBJ_TYPE_ANY;
 	int diff_context = 3, ignore_whitespace = 0;
-	int ch, force_text_diff = 0;
+	int ch, diff_staged = 0, diff_worktree = 0, force_text_diff = 0;
 	const char *errstr;
 	struct tog_view *view;
 	int *pack_fds = NULL;
+	int i, ncommit_args = 0;
 
-	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;
@@ -5047,6 +5432,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)
@@ -5054,6 +5444,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;
@@ -5066,14 +5459,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;
@@ -5100,41 +5485,216 @@ cmd_diff(int argc, char *argv[])
 	if (error)
 		goto done;
 
+	if (diff_staged && (worktree == NULL || ncommit_args > 0)) {
+		error = got_error_msg(GOT_ERR_NOT_IMPL,
+		    "-s option 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 ? got_worktree_get_root_path(worktree) : NULL);
 	if (error)
 		goto done;
 
-	error = tog_load_refs(repo, 0);
-	if (error)
-		goto done;
+	if (argc == 2 || ncommit_args > 0) {
+		int obj_type = (ncommit_args > 0 ?
+		    GOT_OBJ_TYPE_COMMIT : GOT_OBJ_TYPE_ANY);
 
-	error = got_repo_match_object_id(&id1, &label1, id_str1,
-	    GOT_OBJ_TYPE_ANY, &tog_refs, repo);
-	if (error)
+		error = tog_load_refs(repo, 0);
+		if (error)
+			goto done;
+
+		for (i = 0; i < (ncommit_args > 0 ? ncommit_args : argc); ++i) {
+			const char *arg;
+
+			if (ncommit_args > 0)
+				arg = commit_args[i];
+			else
+				arg = argv[i];
+			error = got_repo_match_object_id(&ids[i], &labels[i],
+			    arg, obj_type, &tog_refs, repo);
+			if (error) {
+				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]) {
+		error = got_error_msg(GOT_ERR_NOT_IMPL,
+		    "-s option can only be used when diffing a work tree");
 		goto done;
+	}
 
-	error = got_repo_match_object_id(&id2, &label2, id_str2,
-	    GOT_OBJ_TYPE_ANY, &tog_refs, repo);
-	if (error)
+	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)
+			goto done;
+
+		diff_worktree = 1;
+	}
+
+	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)
+			goto done;
+
+		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) {
+				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);
+	}
+
+	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;
+	}
 
+	if (ids[0]) {
+		error = got_object_get_type(&type1, repo, ids[0]);
+		if (error)
+			goto done;
+	}
+
+	if (!diff_worktree) {
+		error = got_object_get_type(&type2, repo, ids[1]);
+		if (error)
+			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)
+				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)
+				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 || new == NULL /* duplicate */)
+			free(in_repo_path);
+		if (error)
+			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);
+	error = open_diff_view(view, ids[0], ids[1], labels[0], labels[1],
+	    diff_context, ignore_whitespace, force_text_diff, diff_staged,
+	    diff_worktree, NULL, repo, worktree, &paths);
 	if (error)
 		goto done;
 	error = view_loop(view);
 done:
-	free(label1);
-	free(label2);
+	free(labels[0]);
+	free(labels[1]);
+	free(ids[0]);
+	free(ids[1]);
 	free(repo_path);
 	free(cwd);
+	TAILQ_FOREACH(pe, &paths, entry)
+		free((char *)pe->path);
+	got_pathlist_free(&paths);
 	if (repo) {
 		const struct got_error *close_err = got_repo_close(repo);
 		if (error == NULL)
@@ -6099,7 +6659,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, view, s->repo, NULL, NULL);
 		got_object_commit_close(commit);
 		if (err) {
 			view_close(diff_view);

-- 
Mark Jamsek <fnc.bsdbox.org>
GPG: F2FF 13DE 6A06 C471 CA80  E6E2 2930 DC66 86EE CF68