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

From:
Mark Jamsek <mark@jamsek.com>
Subject:
Re: got backout/cherrypick metadata (for commit log messages)
To:
Game of Trees <gameoftrees@openbsd.org>
Date:
Fri, 27 Jan 2023 22:50:26 +1100

Download raw body.

Thread
On 23-01-26 08:34PM, Mark Jamsek wrote:
> On 23-01-26 09:50AM, Stefan Sperling wrote:
> > On Thu, Jan 26, 2023 at 06:04:43PM +1100, Mark Jamsek wrote:
> > > As a quick recap, when the user runs 'got {backout,cherrypick} commit',
> > > we create a "logmsg" reference pointing to the tree's base commit. What
> > > it points at is largely irrelevant for now, but the reference path takes
> > > the form:
> > > 
> > >   refs/got/worktree/cherrypick-${WORKTREE_UUID}-$cherry-picked-commit-id}
> > >   refs/got/worktree/backout-${WORKTREE_UUID}-${backed-out-commit-id}
> > > 
> > > As such, when the user next runs 'got commit', we parse the log
> > > message(s) from the ${backed-out,cherry-picked}-commit-id refs with
> > > which we prepopulate the editor. This way, if the user invokes N 'got
> > > {bo,cy}' commands, all N log messages will appear in the editor. This is
> > > the primary reason I think this idea of stsp's is better than the first.
> > > The other being what op observed: no change is required from the user;
> > > just cherrypick and commit!
> > 
> > I think the manual page should talk about this at a higher level
> > of abstraction.
> > 
> > Users won't know/care whether cherrypick commands created any references.
> > What matters to users is that Got will try to remember log messages of
> > cherrypicked commits and append any relevant log messages to the commit
> > message template.
> > 
> > We could say something like:
> > [[[
> > .It Fl l
> > Display a list of commit log messages recorded by cherrypick operations,
> > represented by references in the "refs/got/worktree/" reference namespace.
> > ]]]
> > 
> > Similar to how documentation of -X options for rebase/histedit just talks
> > about "backups" rather than references, and how the corresponding -l option
> > mentions how this is implemented, but otherwise keeps the high-level concepts
> > in focus.
> 
> Good idea! I'll make this change in the next revision. Thanks for the
> feedback and also for making this change very straightforward :)

New diff below with your improvements to the documentation plus regress.

Just in case you've already reviewed the first diff, the only changes--
besides the addition to regress/cmdline--are to the options in got.1 as
per your suggestion (I think I used your wording verbatim), and
a copypasta mistake in got.c:process_logmsg_refs(), specifically this
line:

	err = got_ref_list(&refs, repo, "refs/got/worktree",
	    got_ref_cmp_by_name, repo);

was changed to:

	err = got_ref_list(&refs, repo, NULL, got_ref_cmp_by_name, repo);

Also, to aid review, the backout regress is quite literally a copypasta
of the cherrypick followed by s/cherrypick/backout because the interface
coverage is identical with these tests. I want to add more, but the diff
is becoming a bit outrageous :)

Besides which, some wider testing would also be nice, and I think this
is a good enough start.  But if not, please just let me know and I'll
resubmit once I've covered the actual commit side too. Thanks!

diffstat /home/mark/src/got
 M  got/got.1                      |   90+   4-
 M  got/got.c                      |  688+  22-
 M  include/got_worktree.h         |   15+   0-
 M  lib/worktree.c                 |    7+   0-
 M  regress/cmdline/backout.sh     |  279+   0-
 M  regress/cmdline/cherrypick.sh  |  279+   0-

6 files changed, 1358 insertions(+), 26 deletions(-)

diff /home/mark/src/got
commit - 711bec03cb52c156a80885f2ee8ad7fce27dd57e
path + /home/mark/src/got
blob - ccdc977d9180a5385e27d7658840d68175702374
file + got/got.1
--- got/got.1
+++ got/got.1
@@ -1970,7 +1970,11 @@ The maximum is 3.
 The maximum is 3.
 .El
 .Tg cy
-.It Cm cherrypick Ar commit
+.It Xo
+.Cm cherrypick
+.Op Fl lX
+.Ar commit
+.Xc
 .Dl Pq alias: Cm cy
 Merge changes from a single
 .Ar commit
@@ -2001,7 +2005,8 @@ committed with
 .Cm got cherrypick
 commands,
 committed with
-.Cm got commit ,
+.Cm got commit
+where the log message of the cherrypicked commit will appear in the editor,
 or discarded again with
 .Cm got revert .
 .Pp
@@ -2012,8 +2017,50 @@ conflicts must be resolved first.
 .Cm got update .
 If any relevant files already contain merge conflicts, these
 conflicts must be resolved first.
+.Pp
+The options for
+.Nm
+.Cm cherrypick
+are as follows:
+.Bl -tag -width Ds
+.It Fl l
+Display a list of commit log messages recorded by cherrypick operations,
+represented by references in the
+.Dq refs/got/worktree
+reference namespace.
+If a
+.Ar commit
+is specified, only show the log message of the specified commit.
+.Pp
+If invoked in a work tree, only log messages recorded by cherrypick operations
+in the current work tree will be displayed.
+Otherwise, all commit log messages will be displayed irrespective of the
+work tree in which they were created.
+This option cannot be used with
+.Cm X .
+.It Fl X
+Delete log messages created by previous cherrypick operations, represented by
+references in the
+.Dq refs/got/worktree
+reference namespace.
+If a
+.Ar commit
+is specified, only delete the log message of the specified commit.
+.Pp
+If invoked in a work tree, only log messages recorded by cherrypick operations
+in the current work tree will be deleted.
+Otherwise, all commit log messages will be deleted irrespective of the
+work tree in which they were created.
+This option cannot be used with
+.Cm l .
+.El
+.Pp
 .Tg bo
-.It Cm backout Ar commit
+.It Xo
+.Cm backout
+.Op Fl lX
+.Ar commit
+.Xc
 .Dl Pq alias: Cm bo
 Reverse-merge changes from a single
 .Ar commit
@@ -2044,7 +2091,8 @@ committed with
 .Cm got backout
 commands,
 committed with
-.Cm got commit ,
+.Cm got commit
+where the log message of the backed-out commit will appear in the editor,
 or discarded again with
 .Cm got revert .
 .Pp
@@ -2055,6 +2103,44 @@ conflicts must be resolved first.
 .Cm got update .
 If any relevant files already contain merge conflicts, these
 conflicts must be resolved first.
+.Pp
+The options for
+.Nm
+.Cm backout
+are as follows:
+.Bl -tag -width Ds
+.It Fl l
+Display a list of commit log messages recorded by backout operations,
+represented by references in the
+.Dq refs/got/worktree
+reference namespace.
+If a
+.Ar commit
+is specified, only show the log message of the specified commit.
+.Pp
+If invoked in a work tree, only log messages recorded by backout operations
+in the current work tree will be displayed.
+Otherwise, all commit log messages will be displayed irrespective of the
+work tree in which they were created.
+This option cannot be used with
+.Cm X .
+.It Fl X
+Delete log messages created by previous backout operations, represented by
+references in the
+.Dq refs/got/worktree
+reference namespace.
+If a
+.Ar commit
+is specified, only delete the log message of the specified commit.
+.Pp
+If invoked in a work tree, only log messages recorded by backout operations
+in the current work tree will be deleted.
+Otherwise, all commit log messages will be deleted irrespective of the
+work tree in which they were created.
+This option cannot be used with
+.Cm l .
+.El
+.Pp
 .Tg rb
 .It Xo
 .Cm rebase
blob - 50fb5ec76cf7c16aa5ecf3017aa935c3f4c6cffa
file + got/got.c
--- got/got.c
+++ got/got.c
@@ -8370,7 +8370,179 @@ static const struct got_error *
 	return NULL;
 }
 
+/*
+ * Shortcut work tree status callback to determine if the set of
+ * paths scanned has at least one versioned path that is modified.
+ * Set arg and return GOT_ERR_FILE_MODIFIED as soon as a path is
+ * passed with a status that is neither unchanged nor unversioned.
+ */
 static const struct got_error *
+worktree_has_changed_path(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 *has_changes = arg;
+
+	if (status == staged_status && (status == GOT_STATUS_DELETE))
+		status = GOT_STATUS_NO_CHANGE;
+
+	if (!(status == GOT_STATUS_NO_CHANGE ||
+	    status == GOT_STATUS_UNVERSIONED) ||
+	    staged_status != GOT_STATUS_NO_CHANGE) {
+		*has_changes = 1;
+		return got_error(GOT_ERR_FILE_MODIFIED);
+	}
+
+	return NULL;
+}
+
+/*
+ * Check that the changeset of the commit identified by id is
+ * comprised of at least one path that is modified in the work tree.
+ */
+static const struct got_error *
+commit_path_changed_in_worktree(int *add_logmsg, struct got_object_id *id,
+    struct got_worktree *worktree, struct got_repository *repo)
+{
+	const struct got_error		*err;
+	struct got_pathlist_head	 paths;
+	struct got_commit_object	*commit = NULL, *pcommit = NULL;
+	struct got_tree_object		*tree = NULL, *ptree = NULL;
+	struct got_object_qid		*pid;
+
+	TAILQ_INIT(&paths);
+
+	err = got_object_open_as_commit(&commit, repo, id);
+	if (err)
+		goto done;
+
+	pid = STAILQ_FIRST(got_object_commit_get_parent_ids(commit));
+
+	err = got_object_open_as_commit(&pcommit, repo, &pid->id);
+	if (err)
+		goto done;
+
+	err = got_object_open_as_tree(&tree, repo,
+	    got_object_commit_get_tree_id(commit));
+	if (err)
+		goto done;
+
+	err = got_object_open_as_tree(&ptree, repo,
+	    got_object_commit_get_tree_id(pcommit));
+	if (err)
+		goto done;
+
+	err = got_diff_tree(ptree, tree, NULL, NULL, -1, -1, "", "", repo,
+	    got_diff_tree_collect_changed_paths, &paths, 0);
+	if (err)
+		goto done;
+
+	err = got_worktree_status(worktree, &paths, repo, 0,
+	    worktree_has_changed_path, add_logmsg, check_cancelled, NULL);
+	if (err && err->code == GOT_ERR_FILE_MODIFIED) {
+		/*
+		 * At least one changed path in the referenced commit is
+		 * modified in the work tree, that's all we need to know!
+		 */
+		err = NULL;
+	}
+
+done:
+	got_pathlist_free(&paths, GOT_PATHLIST_FREE_ALL);
+	if (commit)
+		got_object_commit_close(commit);
+	if (pcommit)
+		got_object_commit_close(pcommit);
+	if (tree)
+		got_object_tree_close(tree);
+	if (ptree)
+		got_object_tree_close(ptree);
+	return err;
+}
+
+/*
+ * Remove any "logmsg" reference comprised entirely of paths that have
+ * been reverted in this work tree. If any path in the logmsg ref changeset
+ * remains in a changed state in the worktree, do not remove the reference.
+ */
+static const struct got_error *
+rm_logmsg_ref(struct got_worktree *worktree, struct got_repository *repo)
+{
+	const struct got_error		*err;
+	struct got_reflist_head		 refs;
+	struct got_reflist_entry	*re;
+	struct got_commit_object	*commit = NULL;
+	struct got_object_id		*commit_id = NULL;
+	char				*uuidstr = NULL;
+
+	TAILQ_INIT(&refs);
+
+	err = got_worktree_get_uuid(&uuidstr, worktree);
+	if (err)
+		goto done;
+
+	err = got_ref_list(&refs, repo, "refs/got/worktree",
+	    got_ref_cmp_by_name, repo);
+	if (err)
+		goto done;
+
+	TAILQ_FOREACH(re, &refs, entry) {
+		const char	*refname;
+		int		 has_changes = 0;
+
+		refname = got_ref_get_name(re->ref);
+
+		if (!strncmp(refname, GOT_WORKTREE_CHERRYPICK_REF_PREFIX,
+		    GOT_WORKTREE_CHERRYPICK_REF_PREFIX_LEN))
+			refname += GOT_WORKTREE_CHERRYPICK_REF_PREFIX_LEN + 1;
+		else if (!strncmp(refname, GOT_WORKTREE_BACKOUT_REF_PREFIX,
+		    GOT_WORKTREE_BACKOUT_REF_PREFIX_LEN))
+			refname += GOT_WORKTREE_BACKOUT_REF_PREFIX_LEN + 1;
+		else
+			continue;
+
+		if (strncmp(refname, uuidstr, GOT_WORKTREE_UUID_STRLEN) == 0)
+			refname += GOT_WORKTREE_UUID_STRLEN + 1; /* skip '-' */
+		else
+			continue;
+
+		err = got_repo_match_object_id(&commit_id, NULL, refname,
+		    GOT_OBJ_TYPE_COMMIT, NULL, repo);
+		if (err)
+			goto done;
+
+		err = got_object_open_as_commit(&commit, repo, commit_id);
+		if (err)
+			goto done;
+
+		err = commit_path_changed_in_worktree(&has_changes, commit_id,
+		    worktree, repo);
+		if (err)
+			goto done;
+
+		if (!has_changes) {
+			err = got_ref_delete(re->ref, repo);
+			if (err)
+				goto done;
+		}
+
+		got_object_commit_close(commit);
+		commit = NULL;
+		free(commit_id);
+		commit_id = NULL;
+	}
+
+done:
+	free(uuidstr);
+	free(commit_id);
+	got_ref_list_free(&refs);
+	if (commit)
+		got_object_commit_close(commit);
+	return err;
+}
+
+static const struct got_error *
 cmd_revert(int argc, char *argv[])
 {
 	const struct got_error *error = NULL;
@@ -8447,7 +8619,11 @@ cmd_revert(int argc, char *argv[])
 			goto done;
 		}
 	}
-	error = apply_unveil(got_repo_get_path(repo), 1,
+
+	/*
+	 * XXX "c" perm needed on repo dir to delete merge references.
+	 */
+	error = apply_unveil(got_repo_get_path(repo), 0,
 	    got_worktree_get_root_path(worktree));
 	if (error)
 		goto done;
@@ -8489,6 +8665,8 @@ done:
 	cpa.action = "revert";
 	error = got_worktree_revert(worktree, &paths, revert_progress, NULL,
 	    pflag ? choose_patch : NULL, &cpa, repo);
+
+	error = rm_logmsg_ref(worktree, repo);
 done:
 	if (patch_script_file && fclose(patch_script_file) == EOF &&
 	    error == NULL)
@@ -8670,6 +8848,132 @@ cmd_commit(int argc, char *argv[])
 }
 
 static const struct got_error *
+cat_logmsg(FILE *f, struct got_commit_object *commit, const char *idstr,
+    const char *type, int has_content)
+{
+	const struct got_error	*err = NULL;
+	char			*logmsg = NULL;
+
+	err = got_object_commit_get_logmsg(&logmsg, commit);
+	if (err)
+		return err;
+
+	if (fprintf(f, "%s# log message of %s commit %s:%s",
+	    has_content ? "\n" : "", type, idstr, logmsg) < 0)
+		err = got_ferror(f, GOT_ERR_IO);
+
+	free(logmsg);
+	return err;
+}
+
+/*
+ * Lookup "logmsg" references of backed-out and cherrypicked commits
+ * belonging to the current work tree. If found, and the worktree has
+ * at least one modified file that was changed in the referenced commit,
+ * add its log message to *logmsg. Add all refs found to matched_refs
+ * to be scheduled for removal on successful commit.
+ */
+static const struct got_error *
+lookup_logmsg_ref(char **logmsg, struct got_reflist_head *matched_refs,
+    struct got_worktree *worktree, struct got_repository *repo)
+{
+	const struct got_error		*err;
+	struct got_commit_object	*commit = NULL;
+	struct got_object_id		*id = NULL;
+	struct got_reflist_head		 refs;
+	struct got_reflist_entry	*re, *re_match;
+	FILE				*f = NULL;
+	char				*uuidstr = NULL;
+	int				 added_logmsg = 0;
+
+	TAILQ_INIT(&refs);
+
+	err = got_opentemp_named(logmsg, &f, "got-commit-logmsg", "");
+	if (err)
+		goto done;
+
+	err = got_worktree_get_uuid(&uuidstr, worktree);
+	if (err)
+		goto done;
+
+	err = got_ref_list(&refs, repo, "refs/got/worktree",
+	    got_ref_cmp_by_name, repo);
+	if (err)
+		goto done;
+
+	TAILQ_FOREACH(re, &refs, entry) {
+		const char	*refname, *type;
+		int		 add_logmsg = 0;
+
+		refname = got_ref_get_name(re->ref);
+
+		if (strncmp(refname, GOT_WORKTREE_CHERRYPICK_REF_PREFIX,
+		    GOT_WORKTREE_CHERRYPICK_REF_PREFIX_LEN) == 0) {
+			refname += GOT_WORKTREE_CHERRYPICK_REF_PREFIX_LEN + 1;
+			type = "cherrypicked";
+		} else if (strncmp(refname, GOT_WORKTREE_BACKOUT_REF_PREFIX,
+		    GOT_WORKTREE_BACKOUT_REF_PREFIX_LEN) == 0) {
+			refname += GOT_WORKTREE_BACKOUT_REF_PREFIX_LEN + 1;
+			type = "backed-out";
+		} else
+			continue;
+
+		if (strncmp(refname, uuidstr, GOT_WORKTREE_UUID_STRLEN) == 0)
+			refname += GOT_WORKTREE_UUID_STRLEN + 1; /* skip '-' */
+		else
+			continue;
+
+		err = got_repo_match_object_id(&id, NULL, refname,
+		    GOT_OBJ_TYPE_COMMIT, NULL, repo);
+		if (err)
+			goto done;
+
+		err = got_object_open_as_commit(&commit, repo, id);
+		if (err)
+			goto done;
+
+		err = commit_path_changed_in_worktree(&add_logmsg, id,
+		    worktree, repo);
+		if (err)
+			goto done;
+
+		if (add_logmsg) {
+			err = cat_logmsg(f, commit, refname, type,
+			    added_logmsg);
+			if (err)
+				goto done;
+			if (!added_logmsg)
+				++added_logmsg;
+		}
+
+		err = got_reflist_entry_dup(&re_match, re);
+		if (err)
+			goto done;
+		TAILQ_INSERT_HEAD(matched_refs, re_match, entry);
+
+		got_object_commit_close(commit);
+		commit = NULL;
+		free(id);
+		id = NULL;
+	}
+
+done:
+	free(id);
+	free(uuidstr);
+	got_ref_list_free(&refs);
+	if (commit)
+		got_object_commit_close(commit);
+	if (f && fclose(f) == EOF && err == NULL)
+		err = got_error_from_errno("fclose");
+	if (!added_logmsg) {
+		if (*logmsg && unlink(*logmsg) != 0 && err == NULL)
+			err = got_error_from_errno2("unlink", *logmsg);
+		*logmsg = NULL;
+	}
+	return err;
+}
+
+static const struct got_error *
 cmd_commit(int argc, char *argv[])
 {
 	const struct got_error *error = NULL;
@@ -8686,8 +8990,11 @@ cmd_commit(int argc, char *argv[])
 	int allow_bad_symlinks = 0, non_interactive = 0, merge_in_progress = 0;
 	int show_diff = 1;
 	struct got_pathlist_head paths;
+	struct got_reflist_head refs;
+	struct got_reflist_entry *re;
 	int *pack_fds = NULL;
 
+	TAILQ_INIT(&refs);
 	TAILQ_INIT(&paths);
 	cl_arg.logmsg_path = NULL;
 
@@ -8809,6 +9116,13 @@ cmd_commit(int argc, char *argv[])
 	if (error)
 		goto done;
 
+	if (prepared_logmsg == NULL) {
+		error = lookup_logmsg_ref(&prepared_logmsg, &refs,
+		    worktree, repo);
+		if (error)
+			goto done;
+	}
+
 	cl_arg.editor = editor;
 	cl_arg.cmdline_log = logmsg;
 	cl_arg.prepared_log = prepared_logmsg;
@@ -8837,6 +9151,13 @@ done:
 	if (error)
 		goto done;
 	printf("Created commit %s\n", id_str);
+
+	TAILQ_FOREACH(re, &refs, entry) {
+		error = got_ref_delete(re->ref, repo);
+		if (error)
+			goto done;
+	}
+
 done:
 	if (preserve_logmsg) {
 		fprintf(stderr, "%s: log message preserved in %s\n",
@@ -8858,6 +9179,7 @@ done:
 		if (error == NULL)
 			error = pack_err;
 	}
+	got_ref_list_free(&refs);
 	got_pathlist_free(&paths, GOT_PATHLIST_FREE_PATH);
 	free(cwd);
 	free(id_str);
@@ -9437,10 +9759,221 @@ __dead static void
 	return error;
 }
 
+/*
+ * Print and if delete is set delete all ref_prefix references.
+ * If wanted_ref is not NULL, only print or delete this reference.
+ */
+static const struct got_error *
+process_logmsg_refs(const char *ref_prefix, size_t prefix_len,
+    const char *wanted_ref, int delete, struct got_worktree *worktree,
+    struct got_repository *repo)
+{
+	const struct got_error			*err;
+	struct got_pathlist_head		 paths;
+	struct got_reflist_head			 refs;
+	struct got_reflist_entry		*re;
+	struct got_reflist_object_id_map	*refs_idmap = NULL;
+	struct got_commit_object		*commit = NULL;
+	struct got_object_id			*id = NULL;
+	char					*uuidstr = NULL;
+	int					 found = 0;
+
+	TAILQ_INIT(&refs);
+	TAILQ_INIT(&paths);
+
+	err = got_ref_list(&refs, repo, NULL, got_ref_cmp_by_name, repo);
+	if (err)
+		goto done;
+
+	err = got_reflist_object_id_map_create(&refs_idmap, &refs, repo);
+	if (err)
+		goto done;
+
+	if (worktree != NULL) {
+		err = got_worktree_get_uuid(&uuidstr, worktree);
+		if (err)
+			goto done;
+	}
+
+	if (wanted_ref) {
+		if (strncmp(wanted_ref, "refs/heads/", 11) == 0)
+			wanted_ref += 11;
+	}
+
+	TAILQ_FOREACH(re, &refs, entry) {
+		const char *refname;
+
+		refname = got_ref_get_name(re->ref);
+
+		err = check_cancelled(NULL);
+		if (err)
+			goto done;
+
+		if (strncmp(refname, ref_prefix, prefix_len) == 0)
+			refname += prefix_len + 1;  /* skip '-' delimiter */
+		else
+			continue;
+
+		if (worktree == NULL || strncmp(refname, uuidstr,
+		    GOT_WORKTREE_UUID_STRLEN) == 0)
+			refname += GOT_WORKTREE_UUID_STRLEN + 1; /* skip '-' */
+		else
+			continue;
+
+		err = got_repo_match_object_id(&id, NULL, refname,
+		    GOT_OBJ_TYPE_COMMIT, NULL, repo);
+		if (err)
+			goto done;
+
+		err = got_object_open_as_commit(&commit, repo, id);
+		if (err)
+			goto done;
+
+		if (wanted_ref)
+			found = strncmp(wanted_ref, refname,
+			    strlen(wanted_ref)) == 0;
+		if (wanted_ref && !found) {
+			struct got_reflist_head	*ci_refs;
+
+			ci_refs = got_reflist_object_id_map_lookup(refs_idmap,
+			    id);
+
+			if (ci_refs) {
+				char		*refs_str = NULL;
+				char const	*r = NULL;
+
+				err = build_refs_str(&refs_str, ci_refs, id,
+				    repo, 1);
+				if (err)
+					goto done;
+
+				r = refs_str;
+				while (r) {
+					if (strncmp(r, wanted_ref,
+					    strlen(wanted_ref)) == 0) {
+						found = 1;
+						break;
+					}
+					r = strchr(r, ' ');
+					if (r)
+						++r;
+				}
+				free(refs_str);
+			}
+		}
+
+		if (wanted_ref == NULL || found) {
+			if (delete) {
+				err = got_ref_delete(re->ref, repo);
+				if (err)
+					goto done;
+				printf("deleted: ");
+				err = print_commit_oneline(commit, id, repo,
+				    refs_idmap);
+			} else {
+				/*
+				 * Print paths modified by commit to help
+				 * associate commits with worktree changes.
+				 */
+				err = get_changed_paths(&paths, commit,
+				    repo, NULL);
+				if (err)
+					goto done;
+
+				err = print_commit(commit, id, repo, NULL,
+				    &paths, NULL, 0, 0, refs_idmap, NULL);
+				got_pathlist_free(&paths,
+				    GOT_PATHLIST_FREE_ALL);
+			}
+			if (err || found)
+				goto done;
+		}
+
+		got_object_commit_close(commit);
+		commit = NULL;
+		free(id);
+		id = NULL;
+	}
+
+	if (wanted_ref != NULL && !found)
+		err = got_error_fmt(GOT_ERR_NOT_REF, "%s", wanted_ref);
+
+done:
+	free(id);
+	free(uuidstr);
+	got_ref_list_free(&refs);
+	got_pathlist_free(&paths, GOT_PATHLIST_FREE_ALL);
+	if (refs_idmap)
+		got_reflist_object_id_map_free(refs_idmap);
+	if (commit)
+		got_object_commit_close(commit);
+	return err;
+}
+
+/*
+ * Create new temp "logmsg" ref of the backed-out or cherrypicked commit
+ * identified by id for log messages to prepopulate the editor on commit.
+ */
+static const struct got_error *
+logmsg_ref(struct got_object_id *id, const char *prefix,
+    struct got_worktree *worktree, struct got_repository *repo)
+{
+	const struct got_error	*err = NULL;
+	char			*idstr, *ref = NULL, *refname = NULL;
+	int			 histedit_in_progress;
+	int			 rebase_in_progress, merge_in_progress;
+
+	/*
+	 * Silenty refuse to create merge reference if any histedit, merge,
+	 * or rebase operation is in progress.
+	 */
+	err = got_worktree_histedit_in_progress(&histedit_in_progress,
+	    worktree);
+	if (err)
+		return err;
+	if (histedit_in_progress)
+		return NULL;
+
+	err = got_worktree_rebase_in_progress(&rebase_in_progress, worktree);
+	if (err)
+		return err;
+	if (rebase_in_progress)
+		return NULL;
+
+	err = got_worktree_merge_in_progress(&merge_in_progress, worktree,
+	    repo);
+	if (err)
+		return err;
+	if (merge_in_progress)
+		return NULL;
+
+	err = got_object_id_str(&idstr, id);
+	if (err)
+		return err;
+
+	err = got_worktree_get_logmsg_ref_name(&refname, worktree, prefix);
+	if (err)
+		goto done;
+
+	if (asprintf(&ref, "%s-%s", refname, idstr) == -1) {
+		err = got_error_from_errno("asprintf");
+		goto done;
+	}
+
+	err = create_ref(ref, got_worktree_get_base_commit_id(worktree),
+	    -1, repo);
+done:
+	free(ref);
+	free(idstr);
+	free(refname);
+	return err;
+}
+
 __dead static void
 usage_cherrypick(void)
 {
-	fprintf(stderr, "usage: %s cherrypick commit-id\n", getprogname());
+	fprintf(stderr, "usage: %s cherrypick [-lX] [commit-id]\n",
+	    getprogname());
 	exit(1);
 }
 
@@ -9454,12 +9987,18 @@ cmd_cherrypick(int argc, char *argv[])
 	struct got_object_id *commit_id = NULL;
 	struct got_commit_object *commit = NULL;
 	struct got_object_qid *pid;
-	int ch;
+	int ch, list_refs = 0, remove_refs = 0;
 	struct got_update_progress_arg upa;
 	int *pack_fds = NULL;
 
-	while ((ch = getopt(argc, argv, "")) != -1) {
+	while ((ch = getopt(argc, argv, "lX")) != -1) {
 		switch (ch) {
+		case 'l':
+			list_refs = 1;
+			break;
+		case 'X':
+			remove_refs = 1;
+			break;
 		default:
 			usage_cherrypick();
 			/* NOTREACHED */
@@ -9474,8 +10013,13 @@ cmd_cherrypick(int argc, char *argv[])
 	    "unveil", NULL) == -1)
 		err(1, "pledge");
 #endif
-	if (argc != 1)
+	if (list_refs || remove_refs) {
+		if (argc != 0 && argc != 1)
+			usage_cherrypick();
+	} else if (argc != 1)
 		usage_cherrypick();
+	if (list_refs && remove_refs)
+		option_conflict('l', 'X');
 
 	cwd = getcwd(NULL, 0);
 	if (cwd == NULL) {
@@ -9489,22 +10033,35 @@ cmd_cherrypick(int argc, char *argv[])
 
 	error = got_worktree_open(&worktree, cwd);
 	if (error) {
-		if (error->code == GOT_ERR_NOT_WORKTREE)
-			error = wrap_not_worktree_error(error, "cherrypick",
-			    cwd);
-		goto done;
+		if (list_refs || remove_refs) {
+			if (error->code != GOT_ERR_NOT_WORKTREE)
+				goto done;
+		} else {
+			if (error->code == GOT_ERR_NOT_WORKTREE)
+				error = wrap_not_worktree_error(error,
+				    "cherrypick", cwd);
+			goto done;
+		}
 	}
 
-	error = got_repo_open(&repo, got_worktree_get_repo_path(worktree),
+	error = got_repo_open(&repo,
+	    worktree ? got_worktree_get_repo_path(worktree) : cwd,
 	    NULL, pack_fds);
 	if (error != NULL)
 		goto done;
 
 	error = apply_unveil(got_repo_get_path(repo), 0,
-	    got_worktree_get_root_path(worktree));
+	    worktree ? got_worktree_get_root_path(worktree) : NULL);
 	if (error)
 		goto done;
 
+	if (list_refs || remove_refs) {
+		error = process_logmsg_refs(GOT_WORKTREE_CHERRYPICK_REF_PREFIX,
+		    GOT_WORKTREE_CHERRYPICK_REF_PREFIX_LEN,
+		    argc == 1 ? argv[0] : NULL, remove_refs, worktree, repo);
+		goto done;
+	}
+
 	error = got_repo_match_object_id(&commit_id, NULL, argv[0],
 	    GOT_OBJ_TYPE_COMMIT, NULL, repo);
 	if (error)
@@ -9524,8 +10081,13 @@ cmd_cherrypick(int argc, char *argv[])
 	if (error != NULL)
 		goto done;
 
-	if (upa.did_something)
+	if (upa.did_something) {
+		error = logmsg_ref(commit_id,
+		    GOT_WORKTREE_CHERRYPICK_REF_PREFIX, worktree, repo);
+		if (error)
+			goto done;
 		printf("Merged commit %s\n", commit_id_str);
+	}
 	print_merge_progress_stats(&upa);
 done:
 	if (commit)
@@ -9551,7 +10113,7 @@ usage_backout(void)
 __dead static void
 usage_backout(void)
 {
-	fprintf(stderr, "usage: %s backout commit-id\n", getprogname());
+	fprintf(stderr, "usage: %s backout [-lX] [commit-id]\n", getprogname());
 	exit(1);
 }
 
@@ -9565,12 +10127,18 @@ cmd_backout(int argc, char *argv[])
 	struct got_object_id *commit_id = NULL;
 	struct got_commit_object *commit = NULL;
 	struct got_object_qid *pid;
-	int ch;
+	int ch, list_refs = 0, remove_refs = 0;
 	struct got_update_progress_arg upa;
 	int *pack_fds = NULL;
 
-	while ((ch = getopt(argc, argv, "")) != -1) {
+	while ((ch = getopt(argc, argv, "lX")) != -1) {
 		switch (ch) {
+		case 'l':
+			list_refs = 1;
+			break;
+		case 'X':
+			remove_refs = 1;
+			break;
 		default:
 			usage_backout();
 			/* NOTREACHED */
@@ -9585,8 +10153,13 @@ cmd_backout(int argc, char *argv[])
 	    "unveil", NULL) == -1)
 		err(1, "pledge");
 #endif
-	if (argc != 1)
+	if (list_refs || remove_refs) {
+		if (argc != 0 && argc != 1)
+			usage_backout();
+	} else if (argc != 1)
 		usage_backout();
+	if (list_refs && remove_refs)
+		option_conflict('l', 'X');
 
 	cwd = getcwd(NULL, 0);
 	if (cwd == NULL) {
@@ -9600,21 +10173,35 @@ cmd_backout(int argc, char *argv[])
 
 	error = got_worktree_open(&worktree, cwd);
 	if (error) {
-		if (error->code == GOT_ERR_NOT_WORKTREE)
-			error = wrap_not_worktree_error(error, "backout", cwd);
-		goto done;
+		if (list_refs || remove_refs) {
+			if (error->code != GOT_ERR_NOT_WORKTREE)
+				goto done;
+		} else {
+			if (error->code == GOT_ERR_NOT_WORKTREE)
+				error = wrap_not_worktree_error(error,
+				    "backout", cwd);
+			goto done;
+		}
 	}
 
-	error = got_repo_open(&repo, got_worktree_get_repo_path(worktree),
+	error = got_repo_open(&repo,
+	    worktree ? got_worktree_get_repo_path(worktree) : cwd,
 	    NULL, pack_fds);
 	if (error != NULL)
 		goto done;
 
 	error = apply_unveil(got_repo_get_path(repo), 0,
-	    got_worktree_get_root_path(worktree));
+	    worktree ? got_worktree_get_root_path(worktree) : NULL);
 	if (error)
 		goto done;
 
+	if (list_refs || remove_refs) {
+		error = process_logmsg_refs(GOT_WORKTREE_BACKOUT_REF_PREFIX,
+		    GOT_WORKTREE_BACKOUT_REF_PREFIX_LEN,
+		    argc == 1 ? argv[0] : NULL, remove_refs, worktree, repo);
+		goto done;
+	}
+
 	error = got_repo_match_object_id(&commit_id, NULL, argv[0],
 	    GOT_OBJ_TYPE_COMMIT, NULL, repo);
 	if (error)
@@ -9638,8 +10225,13 @@ cmd_backout(int argc, char *argv[])
 	if (error != NULL)
 		goto done;
 
-	if (upa.did_something)
+	if (upa.did_something) {
+		error = logmsg_ref(commit_id, GOT_WORKTREE_BACKOUT_REF_PREFIX,
+		    worktree, repo);
+		if (error)
+			goto done;
 		printf("Backed out commit %s\n", commit_id_str);
+	}
 	print_merge_progress_stats(&upa);
 done:
 	if (commit)
@@ -10100,6 +10692,62 @@ process_backup_refs(const char *backup_ref_prefix,
 }
 
 static const struct got_error *
+worktree_has_logmsg_ref(const char *caller, struct got_worktree *worktree,
+    struct got_repository *repo)
+{
+	const struct got_error		*err;
+	struct got_reflist_head		 refs;
+	struct got_reflist_entry	*re;
+	char				*uuidstr = NULL;
+	static char			 msg[160];
+
+	TAILQ_INIT(&refs);
+
+	err = got_worktree_get_uuid(&uuidstr, worktree);
+	if (err)
+		goto done;
+
+	err = got_ref_list(&refs, repo, "refs/got/worktree",
+	    got_ref_cmp_by_name, repo);
+	if (err)
+		goto done;
+
+	TAILQ_FOREACH(re, &refs, entry) {
+		const char *cmd, *refname, *type;
+
+		refname = got_ref_get_name(re->ref);
+
+		if (strncmp(refname, GOT_WORKTREE_CHERRYPICK_REF_PREFIX,
+		    GOT_WORKTREE_CHERRYPICK_REF_PREFIX_LEN) == 0) {
+			refname += GOT_WORKTREE_CHERRYPICK_REF_PREFIX_LEN + 1;
+			cmd = "cherrypick";
+			type = "cherrypicked";
+		} else if (strncmp(refname, GOT_WORKTREE_BACKOUT_REF_PREFIX,
+		    GOT_WORKTREE_BACKOUT_REF_PREFIX_LEN) == 0) {
+			refname += GOT_WORKTREE_BACKOUT_REF_PREFIX_LEN + 1;
+			cmd = "backout";
+			type = "backed-out";
+		} else
+			continue;
+
+		if (strncmp(refname, uuidstr, GOT_WORKTREE_UUID_STRLEN) != 0)
+			continue;
+
+		snprintf(msg, sizeof(msg),
+		    "work tree has references created by %s commits which "
+		    "must be removed with 'got %s -X' before running the %s "
+		    "command", type, cmd, caller);
+		err = got_error_msg(GOT_ERR_WORKTREE_META, msg);
+		goto done;
+	}
+
+done:
+	free(uuidstr);
+	got_ref_list_free(&refs);
+	return err;
+}
+
+static const struct got_error *
 process_backup_refs(const char *backup_ref_prefix,
     const char *wanted_branch_name,
     int delete, struct got_repository *repo)
@@ -10340,6 +10988,12 @@ cmd_rebase(int argc, char *argv[])
 	if (error != NULL)
 		goto done;
 
+	if (worktree != NULL && !list_backups && !delete_backups) {
+		error = worktree_has_logmsg_ref("rebase", worktree, repo);
+		if (error)
+			goto done;
+	}
+
 	error = get_author(&committer, repo, worktree);
 	if (error && error->code != GOT_ERR_COMMIT_NO_AUTHOR)
 		goto done;
@@ -11677,6 +12331,12 @@ cmd_histedit(int argc, char *argv[])
 	if (error != NULL)
 		goto done;
 
+	if (worktree != NULL && !list_backups && !delete_backups) {
+		error = worktree_has_logmsg_ref("histedit", worktree, repo);
+		if (error)
+			goto done;
+	}
+
 	error = got_worktree_rebase_in_progress(&rebase_in_progress, worktree);
 	if (error)
 		goto done;
@@ -12310,6 +12970,12 @@ cmd_merge(int argc, char *argv[])
 	if (error != NULL)
 		goto done;
 
+	if (worktree != NULL) {
+		error = worktree_has_logmsg_ref("merge", worktree, repo);
+		if (error)
+			goto done;
+	}
+
 	error = apply_unveil(got_repo_get_path(repo), 0,
 	    worktree ? got_worktree_get_root_path(worktree) : NULL);
 	if (error)
blob - d3c26b9a044948f5b93e8f2f6adf673b8dd7e599
file + include/got_worktree.h
--- include/got_worktree.h
+++ include/got_worktree.h
@@ -87,6 +87,21 @@ const struct got_error *got_worktree_match_path_prefix
     struct got_worktree *, const char *);
 
 /*
+ * Prefix for references pointing at base commit of backout/cherrypick commits.
+ * Reference path takes the form: PREFIX-WORKTREE_UUID-COMMIT_ID
+ */
+#define GOT_WORKTREE_CHERRYPICK_REF_PREFIX	"refs/got/worktree/cherrypick"
+#define GOT_WORKTREE_BACKOUT_REF_PREFIX		"refs/got/worktree/backout"
+
+#define GOT_WORKTREE_CHERRYPICK_REF_PREFIX_LEN		\
+	sizeof(GOT_WORKTREE_CHERRYPICK_REF_PREFIX) - 1
+#define GOT_WORKTREE_BACKOUT_REF_PREFIX_LEN		\
+	sizeof(GOT_WORKTREE_BACKOUT_REF_PREFIX) - 1
+#define GOT_WORKTREE_UUID_STRLEN	36
+
+const struct got_error *got_worktree_get_logmsg_ref_name(char **,
+    struct got_worktree *, const char *);
+/*
  * Get the name of a work tree's HEAD reference.
  */
 const char *got_worktree_get_head_ref_name(struct got_worktree *);
blob - febf807fc9a1e5b9c2cdab208adc8217a492980e
file + lib/worktree.c
--- lib/worktree.c
+++ lib/worktree.c
@@ -2242,6 +2242,13 @@ got_worktree_get_base_ref_name(char **refname, struct 
 }
 
 const struct got_error *
+got_worktree_get_logmsg_ref_name(char **refname, struct got_worktree *worktree,
+    const char *prefix)
+{
+	return get_ref_name(refname, worktree, prefix);
+}
+
+const struct got_error *
 got_worktree_get_base_ref_name(char **refname, struct got_worktree *worktree)
 {
 	return get_ref_name(refname, worktree, GOT_WORKTREE_BASE_REF_PREFIX);
blob - 02acb5b14f9b116de16f3ff0fe380929aada9a65
file + regress/cmdline/backout.sh
--- regress/cmdline/backout.sh
+++ regress/cmdline/backout.sh
@@ -242,8 +242,287 @@ test_parseargs "$@"
 	test_done "$testroot" 0
 }
 
+test_backout_logmsg_ref() {
+	local testroot=`test_init backout_logmsg_ref`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/repo && git checkout -q -b newbranch)
+
+	echo "modified delta on branch" > $testroot/repo/gamma/delta
+	echo "modified alpha on branch" > $testroot/repo/alpha
+	(cd $testroot/repo && git rm -q beta)
+	echo "new file on branch" > $testroot/repo/epsilon/new
+	(cd $testroot/repo && git add epsilon/new)
+
+	git_commit $testroot/repo -m "commit changes on newbranch"
+	local commit_time=`git_show_author_time $testroot/repo`
+	local branch_rev=`git_show_head $testroot/repo`
+
+	echo "modified new file on branch" > $testroot/repo/epsilon/new
+
+	git_commit $testroot/repo -m "commit modified new file on newbranch"
+	local commit_time2=`git_show_author_time $testroot/repo`
+	local branch_rev2=`git_show_head $testroot/repo`
+
+	(cd $testroot/wt && got backout $branch_rev > /dev/null)
+	(cd $testroot/wt && got backout $branch_rev2 > /dev/null)
+
+	# show all backout log message refs in the work tree
+	local sep="-----------------------------------------------"
+	local logmsg="commit changes on newbranch"
+	local changeset=" M  alpha\n D  beta\n A  epsilon/new\n M  gamma/delta"
+	local logmsg2="commit modified new file on newbranch"
+	local changeset2=" M  epsilon/new"
+	local date=`date -u -r $commit_time +"%a %b %e %X %Y UTC"`
+	local date2=`date -u -r $commit_time2 +"%a %b %e %X %Y UTC"`
+	local ymd=`date -u -r $commit_time +"%F"`
+	local short_id=$(printf '%.7s' $branch_rev)
+	local ymd2=`date -u -r $commit_time2 +"%F"`
+	local short_id2="newbranch"
+	local sorted=$(printf "$branch_rev\n$branch_rev2" | sort)
+
+	for r in $sorted; do
+		echo $sep >> $testroot/stdout.expected
+		if [ $r == $branch_rev ]; then
+			echo "commit $r" >> $testroot/stdout.expected
+			echo "from: $GOT_AUTHOR" >> $testroot/stdout.expected
+			echo "date: $date" >> $testroot/stdout.expected
+			printf " \n $logmsg\n \n" >> $testroot/stdout.expected
+			printf "$changeset\n\n" >> $testroot/stdout.expected
+
+			# for forthcoming wt 'backout -X' test
+			echo "deleted: $ymd $short_id $logmsg" >> \
+			    $testroot/stdout.wt_deleted
+		else
+			echo "commit $r (newbranch)" \
+			    >> $testroot/stdout.expected
+			echo "from: $GOT_AUTHOR" >> $testroot/stdout.expected
+			echo "date: $date2" >> $testroot/stdout.expected
+			printf " \n $logmsg2\n \n" >> $testroot/stdout.expected
+			printf "$changeset2\n\n" >> $testroot/stdout.expected
+
+			# for forthcoming wt 'backout -X' test
+			echo "deleted: $ymd2 $short_id2 $logmsg2" >> \
+			    $testroot/stdout.wt_deleted
+		fi
+	done
+
+	(cd $testroot/wt && got backout -l > $testroot/stdout)
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# only show log message ref of the specified commit id
+	echo $sep > $testroot/stdout.expected
+	echo "commit $branch_rev" >> $testroot/stdout.expected
+	echo "from: $GOT_AUTHOR" >> $testroot/stdout.expected
+	echo "date: $date" >> $testroot/stdout.expected
+	printf " \n $logmsg\n \n" >> $testroot/stdout.expected
+	printf "$changeset\n\n" >> $testroot/stdout.expected
+
+	(cd $testroot/wt && got backout -l $branch_rev > $testroot/stdout)
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# only show log message ref of the specified symref
+	echo $sep > $testroot/stdout.expected
+	echo "commit $branch_rev2 (newbranch)" >> $testroot/stdout.expected
+	echo "from: $GOT_AUTHOR" >> $testroot/stdout.expected
+	echo "date: $date2" >> $testroot/stdout.expected
+	printf " \n $logmsg2\n \n" >> $testroot/stdout.expected
+	printf "$changeset2\n\n" >> $testroot/stdout.expected
+
+	(cd $testroot/wt && got backout -l "newbranch" > $testroot/stdout)
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# create a second work tree with backed-out commits and ensure
+	# bo -l within the new work tree only shows the refs it created
+	got checkout $testroot/repo $testroot/wt2 > /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/repo && git checkout -q -b newbranch2)
+
+	echo "modified delta on branch2" > $testroot/repo/gamma/delta
+	echo "modified alpha on branch2" > $testroot/repo/alpha
+	echo "new file on branch2" > $testroot/repo/epsilon/new2
+	(cd $testroot/repo && git add epsilon/new2)
+
+	git_commit $testroot/repo -m "commit changes on newbranch2"
+	local b2_commit_time=`git_show_author_time $testroot/repo`
+	local branch2_rev=`git_show_head $testroot/repo`
+
+	echo "modified file new2 on branch2" > $testroot/repo/epsilon/new2
+
+	git_commit $testroot/repo -m "commit modified file new2 on newbranch2"
+	local b2_commit_time2=`git_show_author_time $testroot/repo`
+	local branch2_rev2=`git_show_head $testroot/repo`
+
+	(cd $testroot/wt2 && got backout $branch2_rev > /dev/null)
+	(cd $testroot/wt2 && got backout $branch2_rev2 > /dev/null)
+
+	local b2_logmsg="commit changes on newbranch2"
+	local b2_changeset=" M  alpha\n A  epsilon/new2\n M  gamma/delta"
+	local b2_logmsg2="commit modified file new2 on newbranch2"
+	local b2_changeset2=" M  epsilon/new2"
+	date=`date -u -r $b2_commit_time +"%a %b %e %X %Y UTC"`
+	date2=`date -u -r $b2_commit_time2 +"%a %b %e %X %Y UTC"`
+	sorted=$(printf "$branch2_rev\n$branch2_rev2" | sort)
+
+	echo -n > $testroot/stdout.expected
+	for r in $sorted; do
+		echo $sep >> $testroot/stdout.expected
+		if [ $r == $branch2_rev ]; then
+			echo "commit $r" >> $testroot/stdout.expected
+			echo "from: $GOT_AUTHOR" >> $testroot/stdout.expected
+			echo "date: $date" >> $testroot/stdout.expected
+			printf " \n $b2_logmsg\n \n" >> \
+			    $testroot/stdout.expected
+			printf "$b2_changeset\n\n" >> \
+			    $testroot/stdout.expected
+		else
+			echo "commit $r (newbranch2)" \
+			    >> $testroot/stdout.expected
+			echo "from: $GOT_AUTHOR" >> $testroot/stdout.expected
+			echo "date: $date2" >> $testroot/stdout.expected
+			printf " \n $b2_logmsg2\n \n" >> \
+			    $testroot/stdout.expected
+			printf "$b2_changeset2\n\n" >> \
+			    $testroot/stdout.expected
+		fi
+	done
+
+	(cd $testroot/wt2 && got backout -l > $testroot/stdout)
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# ensure both wt and wt2 logmsg refs can be retrieved from the repo
+	sorted=`printf \
+	    "$branch_rev\n$branch_rev2\n$branch2_rev\n$branch2_rev2" | sort`
+
+	echo -n > $testroot/stdout.expected
+	for r in $sorted; do
+		echo "commit $r" >> $testroot/stdout.expected
+	done
+
+	(cd $testroot/repo && got backout -l | grep ^commit | \
+	    sort | cut -f1,2 -d' ' > $testroot/stdout)
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# delete logmsg ref of the specified commit in work tree 2
+	ymd=`date -u -r $b2_commit_time +"%F"`
+	short_id=$(printf '%.7s' $branch2_rev)
+
+	echo "deleted: $ymd $short_id $b2_logmsg" > $testroot/stdout.expected
+	(cd $testroot/wt2 && got backout -X $branch2_rev > $testroot/stdout)
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# delete all logmsg refs in work tree 1
+	(cd $testroot && mv stdout.wt_deleted stdout.expected)
+	(cd $testroot/wt && got backout -X > $testroot/stdout)
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# confirm all work tree 1 refs were deleted
+	echo -n > $testroot/stdout.expected
+	(cd $testroot/wt && got backout -l > $testroot/stdout)
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# make sure the remaining ref in work tree 2 was not also deleted
+	echo $sep > $testroot/stdout.expected
+	echo "commit $branch2_rev2 (newbranch2)" >> $testroot/stdout.expected
+	echo "from: $GOT_AUTHOR" >> $testroot/stdout.expected
+	echo "date: $date2" >> $testroot/stdout.expected
+	printf " \n $b2_logmsg2\n \n" >> $testroot/stdout.expected
+	printf "$b2_changeset2\n\n" >> $testroot/stdout.expected
+
+	(cd $testroot/wt2 && got backout -l > $testroot/stdout)
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# ensure we can delete work tree refs from the repository dir
+	ymd=`date -u -r $b2_commit_time2 +"%F"`
+	echo "deleted: $ymd newbranch2 $b2_logmsg2" > $testroot/stdout.expected
+	(cd $testroot/repo && got backout -X > $testroot/stdout)
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+	fi
+
+	test_done "$testroot" "$ret"
+}
+
 test_parseargs "$@"
 run_test test_backout_basic
 run_test test_backout_edits_for_file_since_deleted
 run_test test_backout_next_commit
 run_test test_backout_umask
+run_test test_backout_logmsg_ref
blob - 464cd2159b38557b4fa61c2ac402ba344ba16d43
file + regress/cmdline/cherrypick.sh
--- regress/cmdline/cherrypick.sh
+++ regress/cmdline/cherrypick.sh
@@ -1725,6 +1725,284 @@ test_parseargs "$@"
 	test_done "$testroot" 0
 }
 
+test_cherrypick_logmsg_ref() {
+	local testroot=`test_init cherrypick_logmsg_ref`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/repo && git checkout -q -b newbranch)
+
+	echo "modified delta on branch" > $testroot/repo/gamma/delta
+	echo "modified alpha on branch" > $testroot/repo/alpha
+	(cd $testroot/repo && git rm -q beta)
+	echo "new file on branch" > $testroot/repo/epsilon/new
+	(cd $testroot/repo && git add epsilon/new)
+
+	git_commit $testroot/repo -m "commit changes on newbranch"
+	local commit_time=`git_show_author_time $testroot/repo`
+	local branch_rev=`git_show_head $testroot/repo`
+
+	echo "modified new file on branch" > $testroot/repo/epsilon/new
+
+	git_commit $testroot/repo -m "commit modified new file on newbranch"
+	local commit_time2=`git_show_author_time $testroot/repo`
+	local branch_rev2=`git_show_head $testroot/repo`
+
+	(cd $testroot/wt && got cherrypick $branch_rev > /dev/null)
+	(cd $testroot/wt && got cherrypick $branch_rev2 > /dev/null)
+
+	# show all log message refs in the work tree
+	local sep="-----------------------------------------------"
+	local logmsg="commit changes on newbranch"
+	local changeset=" M  alpha\n D  beta\n A  epsilon/new\n M  gamma/delta"
+	local logmsg2="commit modified new file on newbranch"
+	local changeset2=" M  epsilon/new"
+	local date=`date -u -r $commit_time +"%a %b %e %X %Y UTC"`
+	local date2=`date -u -r $commit_time2 +"%a %b %e %X %Y UTC"`
+	local ymd=`date -u -r $commit_time +"%F"`
+	local short_id=$(printf '%.7s' $branch_rev)
+	local ymd2=`date -u -r $commit_time2 +"%F"`
+	local short_id2="newbranch"
+	local sorted=$(printf "$branch_rev\n$branch_rev2" | sort)
+
+	for r in $sorted; do
+		echo $sep >> $testroot/stdout.expected
+		if [ $r == $branch_rev ]; then
+			echo "commit $r" >> $testroot/stdout.expected
+			echo "from: $GOT_AUTHOR" >> $testroot/stdout.expected
+			echo "date: $date" >> $testroot/stdout.expected
+			printf " \n $logmsg\n \n" >> $testroot/stdout.expected
+			printf "$changeset\n\n" >> $testroot/stdout.expected
+
+			# for forthcoming wt 'cherrypick -X' test
+			echo "deleted: $ymd $short_id $logmsg" >> \
+			    $testroot/stdout.wt_deleted
+		else
+			echo "commit $r (newbranch)" \
+			    >> $testroot/stdout.expected
+			echo "from: $GOT_AUTHOR" >> $testroot/stdout.expected
+			echo "date: $date2" >> $testroot/stdout.expected
+			printf " \n $logmsg2\n \n" >> $testroot/stdout.expected
+			printf "$changeset2\n\n" >> $testroot/stdout.expected
+
+			# for forthcoming wt 'cherrypick -X' test
+			echo "deleted: $ymd2 $short_id2 $logmsg2" >> \
+			    $testroot/stdout.wt_deleted
+		fi
+	done
+
+	(cd $testroot/wt && got cherrypick -l > $testroot/stdout)
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# only show log message ref of the specified commit id
+	echo $sep > $testroot/stdout.expected
+	echo "commit $branch_rev" >> $testroot/stdout.expected
+	echo "from: $GOT_AUTHOR" >> $testroot/stdout.expected
+	echo "date: $date" >> $testroot/stdout.expected
+	printf " \n $logmsg\n \n" >> $testroot/stdout.expected
+	printf "$changeset\n\n" >> $testroot/stdout.expected
+
+	(cd $testroot/wt && got cherrypick -l $branch_rev > $testroot/stdout)
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# only show log message ref of the specified symref
+	echo $sep > $testroot/stdout.expected
+	echo "commit $branch_rev2 (newbranch)" >> $testroot/stdout.expected
+	echo "from: $GOT_AUTHOR" >> $testroot/stdout.expected
+	echo "date: $date2" >> $testroot/stdout.expected
+	printf " \n $logmsg2\n \n" >> $testroot/stdout.expected
+	printf "$changeset2\n\n" >> $testroot/stdout.expected
+
+	(cd $testroot/wt && got cherrypick -l "newbranch" > $testroot/stdout)
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# create a second work tree with cherrypicked commits and ensure
+	# cy -l within the new work tree only shows the refs it created
+	got checkout $testroot/repo $testroot/wt2 > /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/repo && git checkout -q -b newbranch2)
+
+	echo "modified delta on branch2" > $testroot/repo/gamma/delta
+	echo "modified alpha on branch2" > $testroot/repo/alpha
+	echo "new file on branch2" > $testroot/repo/epsilon/new2
+	(cd $testroot/repo && git add epsilon/new2)
+
+	git_commit $testroot/repo -m "commit changes on newbranch2"
+	local b2_commit_time=`git_show_author_time $testroot/repo`
+	local branch2_rev=`git_show_head $testroot/repo`
+
+	echo "modified file new2 on branch2" > $testroot/repo/epsilon/new2
+
+	git_commit $testroot/repo -m "commit modified file new2 on newbranch2"
+	local b2_commit_time2=`git_show_author_time $testroot/repo`
+	local branch2_rev2=`git_show_head $testroot/repo`
+
+	(cd $testroot/wt2 && got cherrypick $branch2_rev > /dev/null)
+	(cd $testroot/wt2 && got cherrypick $branch2_rev2 > /dev/null)
+
+	local b2_logmsg="commit changes on newbranch2"
+	local b2_changeset=" M  alpha\n A  epsilon/new2\n M  gamma/delta"
+	local b2_logmsg2="commit modified file new2 on newbranch2"
+	local b2_changeset2=" M  epsilon/new2"
+	date=`date -u -r $b2_commit_time +"%a %b %e %X %Y UTC"`
+	date2=`date -u -r $b2_commit_time2 +"%a %b %e %X %Y UTC"`
+	sorted=$(printf "$branch2_rev\n$branch2_rev2" | sort)
+
+	echo -n > $testroot/stdout.expected
+	for r in $sorted; do
+		echo $sep >> $testroot/stdout.expected
+		if [ $r == $branch2_rev ]; then
+			echo "commit $r" >> $testroot/stdout.expected
+			echo "from: $GOT_AUTHOR" >> $testroot/stdout.expected
+			echo "date: $date" >> $testroot/stdout.expected
+			printf " \n $b2_logmsg\n \n" >> \
+			    $testroot/stdout.expected
+			printf "$b2_changeset\n\n" >> \
+			    $testroot/stdout.expected
+		else
+			echo "commit $r (newbranch2)" \
+			    >> $testroot/stdout.expected
+			echo "from: $GOT_AUTHOR" >> $testroot/stdout.expected
+			echo "date: $date2" >> $testroot/stdout.expected
+			printf " \n $b2_logmsg2\n \n" >> \
+			    $testroot/stdout.expected
+			printf "$b2_changeset2\n\n" >> \
+			    $testroot/stdout.expected
+		fi
+	done
+
+	(cd $testroot/wt2 && got cherrypick -l > $testroot/stdout)
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# ensure both wt and wt2 logmsg refs can be retrieved from the repo
+	sorted=`printf \
+	    "$branch_rev\n$branch_rev2\n$branch2_rev\n$branch2_rev2" | sort`
+
+	echo -n > $testroot/stdout.expected
+	for r in $sorted; do
+		echo "commit $r" >> $testroot/stdout.expected
+	done
+
+	(cd $testroot/repo && got cherrypick -l | grep ^commit | \
+	    sort | cut -f1,2 -d' ' > $testroot/stdout)
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# delete logmsg ref of the specified commit in work tree 2
+	ymd=`date -u -r $b2_commit_time +"%F"`
+	short_id=$(printf '%.7s' $branch2_rev)
+
+	echo "deleted: $ymd $short_id $b2_logmsg" > $testroot/stdout.expected
+	(cd $testroot/wt2 && got cherrypick -X $branch2_rev > $testroot/stdout)
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# delete all logmsg refs in work tree 1
+	(cd $testroot && mv stdout.wt_deleted stdout.expected)
+	(cd $testroot/wt && got cherrypick -X > $testroot/stdout)
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# confirm all work tree 1 refs were deleted
+	echo -n > $testroot/stdout.expected
+	(cd $testroot/wt && got cherrypick -l > $testroot/stdout)
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# make sure the remaining ref in work tree 2 was not also deleted
+	echo $sep > $testroot/stdout.expected
+	echo "commit $branch2_rev2 (newbranch2)" >> $testroot/stdout.expected
+	echo "from: $GOT_AUTHOR" >> $testroot/stdout.expected
+	echo "date: $date2" >> $testroot/stdout.expected
+	printf " \n $b2_logmsg2\n \n" >> $testroot/stdout.expected
+	printf "$b2_changeset2\n\n" >> $testroot/stdout.expected
+
+	(cd $testroot/wt2 && got cherrypick -l > $testroot/stdout)
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# ensure we can delete work tree refs from the repository dir
+	ymd=`date -u -r $b2_commit_time2 +"%F"`
+	echo "deleted: $ymd newbranch2 $b2_logmsg2" > $testroot/stdout.expected
+	(cd $testroot/repo && got cherrypick -X > $testroot/stdout)
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+	fi
+
+	test_done "$testroot" "$ret"
+}
+
 test_parseargs "$@"
 run_test test_cherrypick_basic
 run_test test_cherrypick_root_commit
@@ -1743,3 +2021,4 @@ run_test test_cherrypick_umask
 run_test test_cherrypick_dot_on_a_line_by_itself
 run_test test_cherrypick_binary_file
 run_test test_cherrypick_umask
+run_test test_cherrypick_logmsg_ref

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