From: Mark Jamsek Subject: Re: got backout/cherrypick metadata (for commit log messages) To: Game of Trees Date: Fri, 27 Jan 2023 22:50:26 +1100 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 GPG: F2FF 13DE 6A06 C471 CA80 E6E2 2930 DC66 86EE CF68