Commit Diff


commit - a3599220a642dcd27bacb8def35d06dd79ca3795
commit + cc8021afc28da386d1cab85b4d74b632b90e0108
blob - cf8d6c0c7dfcb49db87bfb44ed436e7dc2e5a132
blob + f0a19759ff8cd6e32301a3956c93992b0687c7e2
--- got/got.1
+++ got/got.1
@@ -839,7 +839,7 @@ This option has no effect if the specified
 is never traversed.
 .El
 .Tg di
-.It Cm diff Oo Fl a Oc Oo Fl C Ar number Oc Oo Fl r Ar repository-path Oc Oo Fl s Oc Oo Fl P Oc Oo Fl w Oc Op Ar object1 Ar object2 | Ar path ...
+.It Cm diff Oo Fl a Oc Oo Fl c Ar commit Oc Oo Fl C Ar number Oc Oo Fl r Ar repository-path Oc Oo Fl s Oc Oo Fl P Oc Oo Fl w Oc Op Ar object1 Ar object2 | Ar path ...
 .Dl (alias: Cm di )
 When invoked within a work tree without any arguments, display all
 local changes in the work tree.
@@ -866,6 +866,30 @@ are as follows:
 .Bl -tag -width Ds
 .It Fl a
 Treat file contents as ASCII text even if binary data is detected.
+.It Fl c Ar commit
+Show differences between commits in the repository.
+This options 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.
+.Pp
+Cannot be used together with the
+.Fl P
+option.
+.Pp
 .It Fl C Ar number
 Set the number of context lines shown in the diff.
 By default, 3 lines of context are shown.
blob - 99ed42546e8da33259e8178264dda6113761b31d
blob + a3724af547272d52fe04444e3c3bbd5414a69ac5
--- got/got.c
+++ got/got.c
@@ -4276,7 +4276,8 @@ done:
 __dead static void
 usage_diff(void)
 {
-	fprintf(stderr, "usage: %s diff [-a] [-C number] [-r repository-path] "
+	fprintf(stderr, "usage: %s diff [-a] [-c commit1] [-c commit2] "
+	    "[-C number] [-r repository-path] "
 	    "[-s] [-w] [-P] [object1 object2 | path ...]\n", getprogname());
 	exit(1);
 }
@@ -4486,9 +4487,11 @@ cmd_diff(int argc, char *argv[])
 	struct got_repository *repo = NULL;
 	struct got_worktree *worktree = NULL;
 	char *cwd = NULL, *repo_path = NULL;
+	const char *commit_args[2] = { NULL, NULL };
+	int ncommit_args = 0;
 	struct got_object_id *ids[2] = { NULL, NULL };
 	char *labels[2] = { NULL, NULL };
-	int type1, type2;
+	int type1 = GOT_OBJ_TYPE_ANY, type2 = GOT_OBJ_TYPE_ANY;
 	int diff_context = 3, diff_staged = 0, ignore_whitespace = 0, ch, i;
 	int force_text_diff = 0, force_path = 0, rflag = 0;
 	const char *errstr;
@@ -4505,11 +4508,16 @@ cmd_diff(int argc, char *argv[])
 		err(1, "pledge");
 #endif
 
-	while ((ch = getopt(argc, argv, "aC:r:swP")) != -1) {
+	while ((ch = getopt(argc, argv, "ac:C:r:swP")) != -1) {
 		switch (ch) {
 		case 'a':
 			force_text_diff = 1;
 			break;
+		case 'c':
+			if (ncommit_args >= 2)
+				errx(1, "too many -c options used");
+			commit_args[ncommit_args++] = optarg;
+			break;
 		case 'C':
 			diff_context = strtonum(optarg, 0, GOT_DIFF_MAX_CONTEXT,
 			    &errstr);
@@ -4570,48 +4578,84 @@ cmd_diff(int argc, char *argv[])
 		}
 	}
 
-	if (force_path && (rflag || worktree == NULL))
-		errx(1, "-P option can only be used when diffing a work tree");
-
 	error = got_repo_open(&repo, repo_path, NULL);
 	free(repo_path);
 	if (error != NULL)
 		goto done;
+
+	if (rflag || worktree == NULL || ncommit_args > 0) {
+		if (force_path) {
+			error = got_error_msg(GOT_ERR_NOT_IMPL,
+			    "-P option can only be used when diffing "
+			    "a work tree");
+			goto done;
+		}
+		if (diff_staged) {
+			error = got_error_msg(GOT_ERR_NOT_IMPL,
+			    "-s option can only be used when diffing "
+			    "a work tree");
+			goto done;
+		}
+	}
 
 	error = apply_unveil(got_repo_get_path(repo), 1,
 	    worktree ? got_worktree_get_root_path(worktree) : NULL);
 	if (error)
 		goto done;
 
-	if (!force_path && argc == 2) {
+	if ((!force_path && argc == 2) || ncommit_args > 0) {
+		int obj_type = (ncommit_args > 0 ?
+		    GOT_OBJ_TYPE_COMMIT : GOT_OBJ_TYPE_ANY);
 		error = got_ref_list(&refs, repo, NULL, got_ref_cmp_by_name,
 		    NULL);
 		if (error)
 			goto done;
-		for (i = 0; i < argc; i++) {
+		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],
-			    argv[i], GOT_OBJ_TYPE_ANY, &refs, repo);
+			    arg, obj_type, &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 (worktree != NULL && (ids[0] == NULL || ids[1] == NULL)) {
-		error = get_worktree_paths_from_argv(&paths,
-		    argc, argv, worktree);
-		if (error)
-			goto done;
 	}
 
-	if (!TAILQ_EMPTY(&paths)) {
+	if (ncommit_args == 0 && (ids[0] == NULL || ids[1] == NULL)) {
 		struct print_diff_arg arg;
 		char *id_str;
 
+		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;
+
 		error = got_object_id_str(&id_str,
 		    got_worktree_get_base_commit_id(worktree));
 		if (error)
@@ -4631,34 +4675,115 @@ cmd_diff(int argc, char *argv[])
 		goto done;
 	}
 
-	if (ids[0] == NULL || ids[1] == NULL) {
-		if (argc == 2) {
-			error = got_error_fmt(GOT_ERR_NO_OBJ, "%s",
-			    ids[0] ? argv[1] : argv[0]);
+	if (ncommit_args == 1) {
+		struct got_commit_object *commit;
+		error = got_object_open_as_commit(&commit, repo, ids[0]);
+		if (error)
 			goto done;
-		} if (worktree == NULL) {
-			error = got_error(GOT_ERR_NOT_WORKTREE);
-			goto done;
-		} else
-			usage_diff();
-	}
-	if (diff_staged)
-		errx(1, "-s option can't be used when diffing "
-		    "objects in repository");
 
-	error = got_object_get_type(&type1, repo, ids[0]);
-	if (error)
+		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;
+	}
+
 	error = got_object_get_type(&type2, repo, ids[1]);
 	if (error)
 		goto done;
-	if (type1 != type2) {
+	if (type1 != GOT_OBJ_TYPE_ANY && type1 != type2) {
 		error = got_error(GOT_ERR_OBJ_TYPE);
 		goto done;
 	}
+	if (type1 == GOT_OBJ_TYPE_BLOB && argc > 0) {
+		error = got_error_msg(GOT_ERR_OBJ_TYPE,
+		    "path arguments cannot be used when diffing blobs");
+		goto done;
+	}
 
-	switch (type1) {
+	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;
+	}
+
+	switch (type1 == GOT_OBJ_TYPE_ANY ? type2 : type1) {
 	case GOT_OBJ_TYPE_BLOB:
 		error = got_diff_objects_as_blobs(NULL, NULL, ids[0], ids[1],
 		    NULL, NULL, diff_context, ignore_whitespace,
@@ -4666,14 +4791,14 @@ cmd_diff(int argc, char *argv[])
 		break;
 	case GOT_OBJ_TYPE_TREE:
 		error = got_diff_objects_as_trees(NULL, NULL, ids[0], ids[1],
-		    "", "", diff_context, ignore_whitespace, force_text_diff,
-		    repo, stdout);
+		    &paths, "", "", diff_context, ignore_whitespace,
+		    force_text_diff, repo, stdout);
 		break;
 	case GOT_OBJ_TYPE_COMMIT:
 		printf("diff %s %s\n", labels[0], labels[1]);
 		error = got_diff_objects_as_commits(NULL, NULL, ids[0], ids[1],
-		    diff_context, ignore_whitespace, force_text_diff, repo,
-		    stdout);
+		    &paths, diff_context, ignore_whitespace, force_text_diff,
+		    repo, stdout);
 		break;
 	default:
 		error = got_error(GOT_ERR_OBJ_TYPE);
blob - 2df9fbb9372d0538377a14c4ea095f7a7ea90e95
blob + 248a77e5bca676a06e63703fb30b6fd83aef15cd
--- gotweb/gotweb.c
+++ gotweb/gotweb.c
@@ -2886,11 +2886,11 @@ gw_output_diff(struct gw_trans *gw_trans, struct gw_he
 		break;
 	case GOT_OBJ_TYPE_TREE:
 		error = got_diff_objects_as_trees(NULL, NULL, id1, id2,
-		    "", "", 3, 0, 0, gw_trans->repo, f);
+		    NULL, "", "", 3, 0, 0, gw_trans->repo, f);
 		break;
 	case GOT_OBJ_TYPE_COMMIT:
 		error = got_diff_objects_as_commits(NULL, NULL, id1, id2,
-		    3, 0, 0, gw_trans->repo, f);
+		    NULL, 3, 0, 0, gw_trans->repo, f);
 		break;
 	default:
 		error = got_error(GOT_ERR_OBJ_TYPE);
blob - 2e56321573d28e2eeb0cb21dadfcb6110292501c
blob + 47d14eddfbf3c27bb83d461d7330e04389cd0700
--- include/got_diff.h
+++ include/got_diff.h
@@ -142,8 +142,8 @@ const struct got_error *got_diff_objects_as_blobs(off_
  * array of line offsets for, and the number of lines in, the unidiff text.
  */
 const struct got_error *got_diff_objects_as_trees(off_t **, size_t *,
-    struct got_object_id *, struct got_object_id *, char *, char *,
-    int, int, int, struct got_repository *, FILE *);
+    struct got_object_id *, struct got_object_id *, struct got_pathlist_head *,
+    char *, char *, int, int, int, struct got_repository *, FILE *);
 
 /*
  * Diff two objects, assuming both objects are commits.
@@ -153,7 +153,7 @@ const struct got_error *got_diff_objects_as_trees(off_
  * array of line offsets for, and the number of lines in, the unidiff text.
  */
 const struct got_error *got_diff_objects_as_commits(off_t **, size_t *,
-    struct got_object_id *, struct got_object_id *, int, int, int,
-    struct got_repository *, FILE *);
+    struct got_object_id *, struct got_object_id *, struct got_pathlist_head *,
+    int, int, int, struct got_repository *, FILE *);
 
 #define GOT_DIFF_MAX_CONTEXT	64
blob - f0f73917f7b7ea9ee69014ce63be92ae9ad0b705
blob + b6870a5a2324dda8c85c26409b7fd6329fa970ed
--- include/got_object.h
+++ include/got_object.h
@@ -93,6 +93,15 @@ struct got_object_id *got_object_id_dup(struct got_obj
 
 /*
  * Get a newly allocated ID of the object which resides at the specified
+ * path in the specified tree.
+ * The caller should dispose of it with free(3).
+ */
+const struct got_error *got_object_tree_find_path(struct got_object_id **id,
+    mode_t *mode, struct got_repository *repo, struct got_tree_object *tree,
+    const char *path);
+
+/*
+ * Get a newly allocated ID of the object which resides at the specified
  * path in the tree of the specified commit.
  * The caller should dispose of it with free(3).
  */
blob - 92405943c1b236430780ec2eebfbe0a2153f2dd9
blob + 55c41e201b3a3b22ac019685dd47322b2a7150d4
--- lib/diff.c
+++ lib/diff.c
@@ -753,7 +753,134 @@ got_diff_objects_as_blobs(off_t **line_offsets, size_t
 	err = got_diff_blob(line_offsets, nlines, blob1, blob2,
 	    label1, label2, diff_context, ignore_whitespace, force_text_diff,
 	    outfile);
+done:
+	if (blob1)
+		got_object_blob_close(blob1);
+	if (blob2)
+		got_object_blob_close(blob2);
+	return err;
+}
+
+static const struct got_error *
+diff_paths(struct got_tree_object *tree1, struct got_tree_object *tree2,
+    struct got_pathlist_head *paths, struct got_repository *repo,
+    got_diff_blob_cb cb, void *cb_arg)
+{
+	const struct got_error *err = NULL;
+	struct got_pathlist_entry *pe;
+	struct got_object_id *id1 = NULL, *id2 = NULL;
+	struct got_tree_object *subtree1 = NULL, *subtree2 = NULL;
+	struct got_blob_object *blob1 = NULL, *blob2 = NULL;
+
+	TAILQ_FOREACH(pe, paths, entry) {
+		int type1 = GOT_OBJ_TYPE_ANY, type2 = GOT_OBJ_TYPE_ANY;
+		mode_t mode1 = 0, mode2 = 0;
+
+		free(id1);
+		id1 = NULL;
+		free(id2);
+		id2 = NULL;
+		if (subtree1) {
+			got_object_tree_close(subtree1);
+			subtree1 = NULL;
+		}
+		if (subtree2) {
+			got_object_tree_close(subtree2);
+			subtree2 = NULL;
+		}
+		if (blob1) {
+			got_object_blob_close(blob1);
+			blob1 = NULL;
+		}
+		if (blob2) {
+			got_object_blob_close(blob2);
+			blob2 = NULL;
+		}
+
+		err = got_object_tree_find_path(&id1, &mode1, repo, tree1,
+		    pe->path);
+		if (err && err->code != GOT_ERR_NO_TREE_ENTRY)
+			goto done;
+		err = got_object_tree_find_path(&id2, &mode2, repo, tree2,
+		    pe->path);
+		if (err && err->code != GOT_ERR_NO_TREE_ENTRY)
+			goto done;
+		if (id1 == NULL && id2 == NULL) {
+			err = got_error_path(pe->path, GOT_ERR_NO_TREE_ENTRY);
+			goto done;
+		}
+		if (id1) {
+			err = got_object_get_type(&type1, repo, id1);
+			if (err)
+				goto done;
+		}
+		if (id2) {
+			err = got_object_get_type(&type2, repo, id2);
+			if (err)
+				goto done;
+		}
+		if (type1 == GOT_OBJ_TYPE_ANY &&
+		    type2 == GOT_OBJ_TYPE_ANY) {
+			err = got_error_path(pe->path, GOT_ERR_NO_OBJ);
+			goto done;
+		} else if (type1 != GOT_OBJ_TYPE_ANY &&
+		    type2 != GOT_OBJ_TYPE_ANY && type1 != type2) {
+			err = got_error(GOT_ERR_OBJ_TYPE);
+			goto done;
+		}
+
+		if (type1 == GOT_OBJ_TYPE_BLOB ||
+		    type2 == GOT_OBJ_TYPE_BLOB) {
+			if (id1) {
+				err = got_object_open_as_blob(&blob1, repo,
+				    id1, 8192);
+				if (err)
+					goto done;
+			}
+			if (id2) {
+				err = got_object_open_as_blob(&blob2, repo,
+				    id2, 8192);
+				if (err)
+					goto done;
+			}
+			err = cb(cb_arg, blob1, blob2, id1, id2,
+			    id1 ? pe->path : "/dev/null",
+			    id2 ? pe->path : "/dev/null",
+			    mode1, mode2, repo);
+			if (err)
+				goto done;
+		} else if (type1 == GOT_OBJ_TYPE_TREE ||
+		    type2 == GOT_OBJ_TYPE_TREE) {
+			if (id1) {
+				err = got_object_open_as_tree(&subtree1, repo,
+				    id1);
+				if (err)
+					goto done;
+			}
+			if (id2) {
+				err = got_object_open_as_tree(&subtree2, repo,
+				    id2);
+				if (err)
+					goto done;
+			}
+			err = got_diff_tree(subtree1, subtree2,
+			    id1 ? pe->path : "/dev/null",
+			    id2 ? pe->path : "/dev/null",
+			    repo, cb, cb_arg, 1);
+			if (err)
+				goto done;
+		} else {
+			err = got_error(GOT_ERR_OBJ_TYPE);
+			goto done;
+		}
+	}
 done:
+	free(id1);
+	free(id2);
+	if (subtree1)
+		got_object_tree_close(subtree1);
+	if (subtree2)
+		got_object_tree_close(subtree2);
 	if (blob1)
 		got_object_blob_close(blob1);
 	if (blob2)
@@ -764,6 +891,7 @@ done:
 const struct got_error *
 got_diff_objects_as_trees(off_t **line_offsets, size_t *nlines,
     struct got_object_id *id1, struct got_object_id *id2,
+    struct got_pathlist_head *paths,
     char *label1, char *label2, int diff_context, int ignore_whitespace,
     int force_text_diff, struct got_repository *repo, FILE *outfile)
 {
@@ -785,6 +913,7 @@ got_diff_objects_as_trees(off_t **line_offsets, size_t
 		if (err)
 			goto done;
 	}
+
 	arg.diff_context = diff_context;
 	arg.ignore_whitespace = ignore_whitespace;
 	arg.force_text_diff = force_text_diff;
@@ -796,9 +925,13 @@ got_diff_objects_as_trees(off_t **line_offsets, size_t
 		arg.line_offsets = NULL;
 		arg.nlines = 0;
 	}
-	err = got_diff_tree(tree1, tree2, label1, label2, repo,
-	    got_diff_blob_output_unidiff, &arg, 1);
-
+	if (paths == NULL || TAILQ_EMPTY(paths)) {
+		err = got_diff_tree(tree1, tree2, label1, label2, repo,
+		    got_diff_blob_output_unidiff, &arg, 1);
+	} else {
+		err = diff_paths(tree1, tree2, paths, repo,
+		    got_diff_blob_output_unidiff, &arg);
+	}
 	if (want_lineoffsets) {
 		*line_offsets = arg.line_offsets; /* was likely re-allocated */
 		*nlines = arg.nlines;
@@ -814,6 +947,7 @@ done:
 const struct got_error *
 got_diff_objects_as_commits(off_t **line_offsets, size_t *nlines,
     struct got_object_id *id1, struct got_object_id *id2,
+    struct got_pathlist_head *paths,
     int diff_context, int ignore_whitespace, int force_text_diff,
     struct got_repository *repo, FILE *outfile)
 {
@@ -835,8 +969,8 @@ got_diff_objects_as_commits(off_t **line_offsets, size
 
 	err = got_diff_objects_as_trees(line_offsets, nlines,
 	    commit1 ? got_object_commit_get_tree_id(commit1) : NULL,
-	    got_object_commit_get_tree_id(commit2), "", "", diff_context,
-	    ignore_whitespace, force_text_diff, repo, outfile);
+	    got_object_commit_get_tree_id(commit2), paths, "", "",
+	    diff_context, ignore_whitespace, force_text_diff, repo, outfile);
 done:
 	if (commit1)
 		got_object_commit_close(commit1);
blob - edb39eef485f9b575297475dd981f0e81f1f9da3
blob + f9d1969b15f4cb43862b7013f35e7485fea9e8b5
--- lib/object.c
+++ lib/object.c
@@ -1905,39 +1905,24 @@ got_object_tree_find_entry(struct got_tree_object *tre
 }
 
 const struct got_error *
-got_object_id_by_path(struct got_object_id **id, struct got_repository *repo,
-    struct got_object_id *commit_id, const char *path)
+got_object_tree_find_path(struct got_object_id **id, mode_t *mode,
+    struct got_repository *repo, struct got_tree_object *tree,
+    const char *path)
 {
 	const struct got_error *err = NULL;
-	struct got_commit_object *commit = NULL;
-	struct got_tree_object *tree = NULL;
+	struct got_tree_object *subtree = NULL;
 	struct got_tree_entry *te = NULL;
 	const char *seg, *s;
 	size_t seglen;
 
 	*id = NULL;
-
-	err = got_object_open_as_commit(&commit, repo, commit_id);
-	if (err)
-		goto done;
-
-	/* Handle opening of root of commit's tree. */
-	if (got_path_is_root_dir(path)) {
-		*id = got_object_id_dup(commit->tree_id);
-		if (*id == NULL)
-			err = got_error_from_errno("got_object_id_dup");
-		goto done;
-	}
 
-	err = got_object_open_as_tree(&tree, repo, commit->tree_id);
-	if (err)
-		goto done;
-
 	s = path;
 	while (s[0] == '/')
 		s++;
 	seg = s;
 	seglen = 0;
+	subtree = tree;
 	while (*s) {
 		struct got_tree_object *next_tree;
 
@@ -1948,7 +1933,7 @@ got_object_id_by_path(struct got_object_id **id, struc
 				continue;
 		}
 
-		te = find_entry_by_name(tree, seg, seglen);
+		te = find_entry_by_name(subtree, seg, seglen);
 		if (te == NULL) {
 			err = got_error_path(path, GOT_ERR_NO_TREE_ENTRY);
 			goto done;
@@ -1966,8 +1951,9 @@ got_object_id_by_path(struct got_object_id **id, struc
 			te = NULL;
 			if (err)
 				goto done;
-			got_object_tree_close(tree);
-			tree = next_tree;
+			if (subtree != tree)
+				got_object_tree_close(subtree);
+			subtree = next_tree;
 		}
 	}
 
@@ -1975,8 +1961,40 @@ got_object_id_by_path(struct got_object_id **id, struc
 		*id = got_object_id_dup(&te->id);
 		if (*id == NULL)
 			return got_error_from_errno("got_object_id_dup");
+		if (mode)
+			*mode = te->mode;
 	} else
 		err = got_error_path(path, GOT_ERR_NO_TREE_ENTRY);
+done:
+	if (subtree && subtree != tree)
+		got_object_tree_close(subtree);
+	return err;
+}
+const struct got_error *
+got_object_id_by_path(struct got_object_id **id, struct got_repository *repo,
+    struct got_object_id *commit_id, const char *path)
+{
+	const struct got_error *err = NULL;
+	struct got_commit_object *commit = NULL;
+	struct got_tree_object *tree = NULL;
+
+	*id = NULL;
+
+	err = got_object_open_as_commit(&commit, repo, commit_id);
+	if (err)
+		goto done;
+
+	/* Handle opening of root of commit's tree. */
+	if (got_path_is_root_dir(path)) {
+		*id = got_object_id_dup(commit->tree_id);
+		if (*id == NULL)
+			err = got_error_from_errno("got_object_id_dup");
+	} else {
+		err = got_object_open_as_tree(&tree, repo, commit->tree_id);
+		if (err)
+			goto done;
+		err = got_object_tree_find_path(id, NULL, repo, tree, path);
+	}
 done:
 	if (commit)
 		got_object_commit_close(commit);
blob - c41e9a4f8236f9042cd3dc2623784b30c32f98f4
blob + 24e167624a9dff80bfc3ea53ddfcfe092ffd80d6
--- regress/cmdline/diff.sh
+++ regress/cmdline/diff.sh
@@ -970,7 +970,248 @@ test_diff_binary_files() {
 	ret="$?"
 	if [ "$ret" != "0" ]; then
 		diff -a -u $testroot/stdout.expected $testroot/stdout
+	fi
+	test_done "$testroot" "$ret"
+}
+
+test_diff_commits() {
+	local testroot=`test_init diff_commits`
+	local commit_id0=`git_show_head $testroot/repo`
+	alpha_id0=`get_blob_id $testroot/repo "" alpha`
+	beta_id0=`get_blob_id $testroot/repo "" beta`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "modified alpha" > $testroot/wt/alpha
+	(cd $testroot/wt && got rm beta >/dev/null)
+	echo "new file" > $testroot/wt/new
+	(cd $testroot/wt && got add new >/dev/null)
+	(cd $testroot/wt && got commit -m 'committing changes' >/dev/null)
+	local commit_id1=`git_show_head $testroot/repo`
+
+	alpha_id1=`get_blob_id $testroot/repo "" alpha`
+	new_id1=`get_blob_id $testroot/repo "" new`
+
+	echo "diff $commit_id0 refs/heads/master" > $testroot/stdout.expected
+	echo "blob - $alpha_id0" >> $testroot/stdout.expected
+	echo "blob + $alpha_id1" >> $testroot/stdout.expected
+	echo '--- alpha' >> $testroot/stdout.expected
+	echo '+++ alpha' >> $testroot/stdout.expected
+	echo '@@ -1 +1 @@' >> $testroot/stdout.expected
+	echo '-alpha' >> $testroot/stdout.expected
+	echo '+modified alpha' >> $testroot/stdout.expected
+	echo "blob - $beta_id0 (mode 644)" >> $testroot/stdout.expected
+	echo 'blob + /dev/null' >> $testroot/stdout.expected
+	echo '--- beta' >> $testroot/stdout.expected
+	echo '+++ /dev/null' >> $testroot/stdout.expected
+	echo '@@ -1 +0,0 @@' >> $testroot/stdout.expected
+	echo '-beta' >> $testroot/stdout.expected
+	echo 'blob - /dev/null' >> $testroot/stdout.expected
+	echo "blob + $new_id1 (mode 644)" >> $testroot/stdout.expected
+	echo '--- /dev/null' >> $testroot/stdout.expected
+	echo '+++ new' >> $testroot/stdout.expected
+	echo '@@ -0,0 +1 @@' >> $testroot/stdout.expected
+	echo '+new file' >> $testroot/stdout.expected
+
+	(cd $testroot/wt && got diff -c master > $testroot/stdout)
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# same diff with explicit parent commit ID
+	(cd $testroot/wt && got diff -c $commit_id0 -c master \
+		> $testroot/stdout)
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# same diff with commit object IDs
+	echo "diff $commit_id0 $commit_id1" > $testroot/stdout.expected
+	echo "blob - $alpha_id0" >> $testroot/stdout.expected
+	echo "blob + $alpha_id1" >> $testroot/stdout.expected
+	echo '--- alpha' >> $testroot/stdout.expected
+	echo '+++ alpha' >> $testroot/stdout.expected
+	echo '@@ -1 +1 @@' >> $testroot/stdout.expected
+	echo '-alpha' >> $testroot/stdout.expected
+	echo '+modified alpha' >> $testroot/stdout.expected
+	echo "blob - $beta_id0 (mode 644)" >> $testroot/stdout.expected
+	echo 'blob + /dev/null' >> $testroot/stdout.expected
+	echo '--- beta' >> $testroot/stdout.expected
+	echo '+++ /dev/null' >> $testroot/stdout.expected
+	echo '@@ -1 +0,0 @@' >> $testroot/stdout.expected
+	echo '-beta' >> $testroot/stdout.expected
+	echo 'blob - /dev/null' >> $testroot/stdout.expected
+	echo "blob + $new_id1 (mode 644)" >> $testroot/stdout.expected
+	echo '--- /dev/null' >> $testroot/stdout.expected
+	echo '+++ new' >> $testroot/stdout.expected
+	echo '@@ -0,0 +1 @@' >> $testroot/stdout.expected
+	echo '+new file' >> $testroot/stdout.expected
+	(cd $testroot/wt && got diff -c $commit_id0 -c $commit_id1 \
+		> $testroot/stdout)
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# same diff, filtered by paths
+	echo "diff $commit_id0 $commit_id1" > $testroot/stdout.expected
+	echo "blob - $alpha_id0" >> $testroot/stdout.expected
+	echo "blob + $alpha_id1" >> $testroot/stdout.expected
+	echo '--- alpha' >> $testroot/stdout.expected
+	echo '+++ alpha' >> $testroot/stdout.expected
+	echo '@@ -1 +1 @@' >> $testroot/stdout.expected
+	echo '-alpha' >> $testroot/stdout.expected
+	echo '+modified alpha' >> $testroot/stdout.expected
+	(cd $testroot/repo && got diff -c $commit_id0 -c $commit_id1 alpha \
+		> $testroot/stdout)
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+	# same in a work tree
+	(cd $testroot/wt && got diff -c $commit_id0 -c $commit_id1 alpha \
+		> $testroot/stdout)
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
 	fi
+
+	echo "diff $commit_id0 $commit_id1" > $testroot/stdout.expected
+	echo "blob - $beta_id0 (mode 644)" >> $testroot/stdout.expected
+	echo 'blob + /dev/null' >> $testroot/stdout.expected
+	echo '--- beta' >> $testroot/stdout.expected
+	echo '+++ /dev/null' >> $testroot/stdout.expected
+	echo '@@ -1 +0,0 @@' >> $testroot/stdout.expected
+	echo '-beta' >> $testroot/stdout.expected
+	echo 'blob - /dev/null' >> $testroot/stdout.expected
+	echo "blob + $new_id1 (mode 644)" >> $testroot/stdout.expected
+	echo '--- /dev/null' >> $testroot/stdout.expected
+	echo '+++ new' >> $testroot/stdout.expected
+	echo '@@ -0,0 +1 @@' >> $testroot/stdout.expected
+	echo '+new file' >> $testroot/stdout.expected
+	(cd $testroot/repo && got diff -c $commit_id0 -c $commit_id1 \
+		beta new > $testroot/stdout)
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# more than two -c options are not allowed
+	(cd $testroot/repo && got diff -c $commit_id0 -c $commit_id1 -c foo \
+		2> $testroot/stderr)
+	ret="$?"
+	if [ "$ret" == "0" ]; then
+		echo "diff succeeded unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+	echo "got: too many -c options used" > $testroot/stderr.expected
+	cmp -s $testroot/stderr.expected $testroot/stderr
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# use of -c options implies a repository diff; use with -P is an error
+	(cd $testroot/wt && got diff -c $commit_id0 -c $commit_id1 -P foo \
+		2> $testroot/stderr)
+	ret="$?"
+	if [ "$ret" == "0" ]; then
+		echo "diff succeeded unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+	echo "got: -P option can only be used when diffing a work tree" \
+		> $testroot/stderr.expected
+	cmp -s $testroot/stderr.expected $testroot/stderr
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# use of -c options implies a repository diff; use with -s is an error
+	(cd $testroot/wt && got diff -c $commit_id0 -c $commit_id1 -s foo \
+		2> $testroot/stderr)
+	ret="$?"
+	if [ "$ret" == "0" ]; then
+		echo "diff succeeded unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+	echo "got: -s option can only be used when diffing a work tree" \
+		> $testroot/stderr.expected
+	cmp -s $testroot/stderr.expected $testroot/stderr
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# three arguments imply use of path filtering (repository case)
+	(cd $testroot/repo && got diff $commit_id0 $commit_id1 foo \
+		2> $testroot/stderr)
+	ret="$?"
+	if [ "$ret" == "0" ]; then
+		echo "diff succeeded unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+	echo "got: specified paths cannot be resolved: no got work tree found" \
+		> $testroot/stderr.expected
+	cmp -s $testroot/stderr.expected $testroot/stderr
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# three arguments imply use of path filtering (work tree case)
+	(cd $testroot/wt && got diff $commit_id0 $commit_id1 foo \
+		2> $testroot/stderr)
+	ret="$?"
+	if [ "$ret" == "0" ]; then
+		echo "diff succeeded unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+	echo "got: $commit_id0: No such file or directory" \
+		> $testroot/stderr.expected
+	cmp -s $testroot/stderr.expected $testroot/stderr
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+	fi
 	test_done "$testroot" "$ret"
 }
 
@@ -984,3 +1225,4 @@ run_test test_diff_submodule_of_same_repo
 run_test test_diff_symlinks_in_work_tree
 run_test test_diff_symlinks_in_repo
 run_test test_diff_binary_files
+run_test test_diff_commits
blob - 3e9e90f6b382d2b217417f4f229183da2df159b5
blob + 15773adb5cfcef6d2e7a96584702408d8908643d
--- tog/tog.c
+++ tog/tog.c
@@ -3350,7 +3350,7 @@ create_diff(struct tog_diff_view_state *s)
 		break;
 	case GOT_OBJ_TYPE_TREE:
 		err = got_diff_objects_as_trees(&s->line_offsets, &s->nlines,
-		    s->id1, s->id2, "", "", s->diff_context,
+		    s->id1, s->id2, NULL, "", "", s->diff_context,
 		    s->ignore_whitespace, s->force_text_diff, s->repo, s->f);
 		break;
 	case GOT_OBJ_TYPE_COMMIT: {
@@ -3385,7 +3385,7 @@ create_diff(struct tog_diff_view_state *s)
 		got_object_commit_close(commit2);
 
 		err = got_diff_objects_as_commits(&s->line_offsets, &s->nlines,
-		    s->id1, s->id2, s->diff_context, s->ignore_whitespace,
+		    s->id1, s->id2, NULL, s->diff_context, s->ignore_whitespace,
 		    s->force_text_diff, s->repo, s->f);
 		break;
 	}