Download raw body.
back up old commits after rebase and histedit
On Sun, Mar 21, 2021 at 03:21:38PM +0100, Stefan Sperling wrote:
> At present, if a user wants to access old commits after rebasing them
> or editing them, they will have to figure out the old commit hash somehow.
> There's the possibility of setting a backup reference before running a
> rebase or histedit operation. However, in many cases people won't do this,
> which makes it difficult to undo the rebase/histedit operation if needed.
>
> Git keeps a 'reflog' for this purpose, but this is quite general and also
> serves other use cases. I would like to provide a solution for keeping
> old commits reachable out of the box, and nothing more.
>
> My proposed solution is to create backup references automatically.
>
> These references live in the "refs/got/backup/" namespace and look like:
>
> refs/got/backup/histedit/main/abcdef... -> 59d1e4a0...
> refs/got/backup/histedit/main/123456... -> 8703c7ce...
>
> Where 'main' was the edited branch, abcdef and 123456 were the result of
> a histedit operation, and 59d1e4a0 and 8703c7ce were the pre-histedit
> branch tip commits.
>
> Both rebase and histedit gain a new -l option which list past rebase or
> histedit operations based on this information. This command does a history
> walk to figure out the common ancestor between the two commits, which is
> the lower bound to pass to commands like 'got log -x' (see example below).
>
> The -l output looks the same for both rebase and histedit.
> For example, after running 'got histedit -f' two times on my local refbackup
> branch which implements this feature, the output looked like this, with
> commits ordered by timestamp (newest first):
> [[[
> $ got histedit -l
> -----------------------------------------------
> commit cbc80094f89af6e2d4a2b90b688015e0f0f5e4d2 (formerly refbackup)
> from: Stefan Sperling <stsp@stsp.name>
> date: Sun Mar 21 14:07:32 2021 UTC
>
> explain how to restore old branches
>
> has become commit 9d8ee1b0ae2b906e1834372c1e8629ef630990da (refbackup)
> 2021-03-21 stsp back up old commits after 'got rebase' and 'got histedit'
> history forked at 59d1e4a0a9c19debc27746357d97084b59a76db8 (main, origin/main)
> 2021-03-10 stsp implement raw object data access; this will be required for packing
> -----------------------------------------------
> commit a510f1abc7a4a48559e0ba8f662ceb545cf8a7e8 (formerly refbackup)
> from: Stefan Sperling <stsp@stsp.name>
> date: Sun Mar 21 13:40:41 2021 UTC
>
> show former branch name next to backup commits
>
> has become commit 91b8e8b3da028dd5af3318117ebb99b1f38d7784
> 2021-03-21 stsp back up old commits after 'got rebase' and 'got histedit'
> history forked at 59d1e4a0a9c19debc27746357d97084b59a76db8 (main, origin/main)
> 2021-03-10 stsp implement raw object data access; this will be required for packing
> $
> ]]]
>
> Each version of history can easily be viewed separately:
>
> The newer version which had most commits already folded into one:
> [[[
> $ got log -x main -c cbc80094f
> -----------------------------------------------
> commit cbc80094f89af6e2d4a2b90b688015e0f0f5e4d2
> from: Stefan Sperling <stsp@stsp.name>
> date: Sun Mar 21 14:07:32 2021 UTC
>
> explain how to restore old branches
>
> -----------------------------------------------
> commit 91b8e8b3da028dd5af3318117ebb99b1f38d7784
> from: Stefan Sperling <stsp@stsp.name>
> date: Sun Mar 21 13:44:36 2021 UTC
>
> back up old commits after 'got rebase' and 'got histedit'
>
> -----------------------------------------------
> commit 59d1e4a0a9c19debc27746357d97084b59a76db8 (main, origin/main)
> from: Stefan Sperling <stsp@stsp.name>
> date: Wed Mar 10 22:49:22 2021 UTC
>
> implement raw object data access; this will be required for packing
> ]]]
>
> The original version with several individual commits:
> [[[
> $ got log -x main -c a510f1abc
> -----------------------------------------------
> commit a510f1abc7a4a48559e0ba8f662ceb545cf8a7e8
> from: Stefan Sperling <stsp@stsp.name>
> date: Sun Mar 21 13:40:41 2021 UTC
>
> show former branch name next to backup commits
>
> -----------------------------------------------
> commit c7d3d9f65904dd111de00468cfac439b85d762b0
> from: Stefan Sperling <stsp@stsp.name>
> date: Sun Mar 21 11:42:14 2021 UTC
>
> test got histedit -l
>
> -----------------------------------------------
> commit 6981cd52afcc47a8f6d07e70da2253d9b90fb179
> from: Stefan Sperling <stsp@stsp.name>
> date: Sun Mar 21 11:34:12 2021 UTC
>
> test got rebase -l
>
> -----------------------------------------------
> commit c616658a38255db2db14dcfdb97c13e56da3e6bc
> from: Stefan Sperling <stsp@stsp.name>
> date: Sun Mar 21 11:08:02 2021 UTC
>
> do not backup after forward-only rebase
>
> -----------------------------------------------
> commit 4f9e80dce230446e0d2108055a28a1f847ffa709 (noel/refbackup)
> from: Stefan Sperling <stsp@stsp.name>
> date: Sun Mar 21 10:34:41 2021 UTC
>
> sort backed up commit list by commit timestamp in descending order
>
> -----------------------------------------------
> commit a188e9298e22e09bf7980db571b25757c7207b4d
> from: Stefan Sperling <stsp@stsp.name>
> date: Sun Mar 21 10:15:55 2021 UTC
>
> remove -n option; update docs
>
> -----------------------------------------------
> commit 8bd4ff27447e2862db7c2ed2147ac1225345e8a3
> from: Stefan Sperling <stsp@stsp.name>
> date: Sun Mar 21 09:54:18 2021 UTC
>
> display yca commit
>
> -----------------------------------------------
> commit 37907c76d936203d10f34efcb032a43eb63a41b8
> from: Stefan Sperling <stsp@stsp.name>
> date: Sun Mar 21 09:11:42 2021 UTC
>
> more output tweaks
>
> -----------------------------------------------
> commit be816fdb718335cf3fcc66966eebba27aa14bb48
> from: Stefan Sperling <stsp@stsp.name>
> date: Sun Mar 21 08:53:05 2021 UTC
>
> tweak display format
>
> -----------------------------------------------
> commit ed8b70d49729329ae68ae7a2d7702019ab4b2b89
> from: Stefan Sperling <stsp@stsp.name>
> date: Sat Mar 20 23:43:11 2021 UTC
>
> simplify wording
>
> -----------------------------------------------
> commit 45f94350a614782851b5cb52791d6b1998d10290
> from: Stefan Sperling <stsp@stsp.name>
> date: Sat Mar 20 23:42:50 2021 UTC
>
> doc for histedit
>
> -----------------------------------------------
> commit 95432e243d84cb9e2b567bea1b1a4e42923dbe47
> from: Stefan Sperling <stsp@stsp.name>
> date: Sat Mar 20 23:38:13 2021 UTC
>
> add reference backup support to 'got histedit'
>
> -----------------------------------------------
> commit 4d52569cbe0253d3ef092175378cc34d390462e8
> from: Stefan Sperling <stsp@stsp.name>
> date: Sat Mar 20 23:12:31 2021 UTC
>
> implement reference backup and listing for 'got rebase'
>
> -----------------------------------------------
> commit 59d1e4a0a9c19debc27746357d97084b59a76db8 (main, origin/main)
> from: Stefan Sperling <stsp@stsp.name>
> date: Wed Mar 10 22:49:22 2021 UTC
>
> implement raw object data access; this will be required for packing
>
> $
> ]]]
>
> 'got fetch' will not copy references in "refs/got" from remote servers,
> and neither would a future 'got push' send them. This means backups will
> always stay on the local machine which is desirable because we need an
> easy way to delete them when they are no longer needed.
>
> Today, individual backups can be deleted with 'got ref -d' followed by
> 'git gc' to purge the now unreferenced objects. For the future I envision
> a new 'gotadmin cleanup' command. This would delete any unreferenced objects
> and could optionally remove references in "refs/got/worktree" and in
> "refs/got/backup" beforehand. Old backups could then be deleted in bulk.
>
> Any concerns?
I like the feature and it works well under my testing. Have one
inline comment on the patch:
>
> diff 9271e2e9348b1ac096d2f48ede73ee75ddc346a9 655ca3222601c350dcaae151a8ef7ef8ba48d736
> blob - 22cc9a1eba46d729c6f6890233b894a7d846f716
> blob + 1b140eaa791af5528134ff5ff1bbcf2ecee0a348
> --- got/got.1
> +++ got/got.1
> @@ -1425,7 +1425,7 @@ conflicts must be resolved first.
> .It Cm bo
> Short alias for
> .Cm backout .
> -.It Cm rebase Oo Fl a Oc Oo Fl c Oc Op Ar branch
> +.It Cm rebase Oo Fl a Oc Oo Fl c Oc Oo Fl l Oc Op Ar branch
> Rebase commits on the specified
> .Ar branch
> onto the tip of the current branch of the work tree.
> @@ -1467,6 +1467,14 @@ the new version of the specified
> .Ar branch
> and the work tree is automatically switched to it.
> .Pp
> +Old commits in their pre-rebase state are automatically backed up in the
> +.Dq refs/got/backup/rebase
> +reference namespace.
> +As long as these references are not removed older versions of rebased
> +commits will remain in the repository and can be viewed with the
> +.Cm got rebase -l
> +command.
> +.Pp
> While rebasing commits, show the status of each affected file,
> using the following status codes:
> .Bl -column YXZ description
> @@ -1529,11 +1537,44 @@ If this option is used, no other command-line argument
> .It Fl c
> Continue an interrupted rebase operation.
> If this option is used, no other command-line arguments are allowed.
> +.It Fl l
> +Show a list of past rebase operations, represented by references in the
> +.Dq refs/got/backup/rebase
> +reference namespace.
> +.Pp
> +Display the author, date, and log message of each backed up commit,
> +the object ID of the corresponding post-rebase commit, and
> +the object ID of their common ancestor commit.
> +Given these object IDs,
> +the
> +.Cm got log
> +command with the
> +.Fl c
> +and
> +.Fl x
> +options can be used to examine the history of either version of the branch,
> +and the
> +.Cm got branch
> +command with the
> +.Fl c
> +option can be used to create a new branch from a pre-rebase state if desired.
> +.Pp
> +If a
> +.Ar branch
> +is specified, only show commits which at some point in time represented this
> +branch.
> +Otherwise, list all backed up commits for any branches.
> +.Pp
> +If this option is used,
> +.Cm got rebase
> +does not require a work tree.
> +None of the other options can be used together with
> +.Fl l .
> .El
> .It Cm rb
> Short alias for
> .Cm rebase .
> -.It Cm histedit Oo Fl a Oc Oo Fl c Oc Oo Fl f Oc Oo Fl F Ar histedit-script Oc Oo Fl m Oc
> +.It Cm histedit Oo Fl a Oc Oo Fl c Oc Oo Fl f Oc Oo Fl F Ar histedit-script Oc Oo Fl m Oc Oo Fl l Oc Op Ar branch
> Edit commit history between the work tree's current base commit and
> the tip commit of the work tree's current branch.
> .Pp
> @@ -1599,6 +1640,14 @@ Once history editing has completed successfully, the t
> the new version of the work tree's branch and the work tree is automatically
> switched to it.
> .Pp
> +Old commits in their pre-histedit state are automatically backed up in the
> +.Dq refs/got/backup/histedit
> +reference namespace.
> +As long as these references are not removed older versions of edited
> +commits will remain in the repository and can be viewed with the
> +.Cm got histedit -l
> +command.
> +.Pp
> While merging commits, show the status of each affected file,
> using the following status codes:
> .Bl -column YXZ description
> @@ -1685,6 +1734,39 @@ The
> .Fl m
> option can only be used when starting a new histedit operation.
> If this option is used, no other command-line arguments are allowed.
> +.It Fl l
> +Show a list of past histedit operations, represented by references in the
> +.Dq refs/got/backup/histedit
> +reference namespace.
> +.Pp
> +Display the author, date, and log message of each backed up commit,
> +the object ID of the corresponding post-histedit commit, and
> +the object ID of their common ancestor commit.
> +Given these object IDs,
> +the
> +.Cm got log
> +command with the
> +.Fl c
> +and
> +.Fl x
> +options can be used to examine the history of either version of the branch,
> +and the
> +.Cm got branch
> +command with the
> +.Fl c
> +option can be used to create a new branch from a pre-histedit state if desired.
> +.Pp
> +If a
> +.Ar branch
> +is specified, only show commits which at some point in time represented this
> +branch.
> +Otherwise, list all backed up commits for any branches.
> +.Pp
> +If this option is used,
> +.Cm got histedit
> +does not require a work tree.
> +None of the other options can be used together with
> +.Fl l .
> .El
> .It Cm he
> Short alias for
> blob - 9bcd279e5446ef8a890025dd5e3e16b3206fc66a
> blob + 1cb7c78b6064bd34328d1c6e908842214ef4bc0c
> --- got/got.c
> +++ got/got.c
> @@ -3601,7 +3601,8 @@ static const struct got_error *
> print_commit(struct got_commit_object *commit, struct got_object_id *id,
> struct got_repository *repo, const char *path,
> struct got_pathlist_head *changed_paths, int show_patch,
> - int diff_context, struct got_reflist_object_id_map *refs_idmap)
> + int diff_context, struct got_reflist_object_id_map *refs_idmap,
> + const char *custom_refs_str)
> {
> const struct got_error *err = NULL;
> char *id_str, *datestr, *logmsg0, *logmsg, *line;
> @@ -3609,22 +3610,27 @@ print_commit(struct got_commit_object *commit, struct
> time_t committer_time;
> const char *author, *committer;
> char *refs_str = NULL;
> - struct got_reflist_head *refs;
>
> err = got_object_id_str(&id_str, id);
> if (err)
> return err;
>
> - refs = got_reflist_object_id_map_lookup(refs_idmap, id);
> - if (refs) {
> - err = build_refs_str(&refs_str, refs, id, repo);
> - if (err)
> - goto done;
> + if (custom_refs_str == NULL) {
> + struct got_reflist_head *refs;
> + refs = got_reflist_object_id_map_lookup(refs_idmap, id);
> + if (refs) {
> + err = build_refs_str(&refs_str, refs, id, repo);
> + if (err)
> + goto done;
> + }
> }
>
> printf(GOT_COMMIT_SEP_STR);
> - printf("commit %s%s%s%s\n", id_str, refs_str ? " (" : "",
> - refs_str ? refs_str : "", refs_str ? ")" : "");
> + if (custom_refs_str)
> + printf("commit %s (%s)\n", id_str, custom_refs_str);
> + else
> + printf("commit %s%s%s%s\n", id_str, refs_str ? " (" : "",
> + refs_str ? refs_str : "", refs_str ? ")" : "");
> free(id_str);
> id_str = NULL;
> free(refs_str);
> @@ -3773,7 +3779,7 @@ print_commits(struct got_object_id *root_id, struct go
> } else {
> err = print_commit(commit, id, repo, path,
> show_changed_paths ? &changed_paths : NULL,
> - show_patch, diff_context, refs_idmap);
> + show_patch, diff_context, refs_idmap, NULL);
> got_object_commit_close(commit);
> if (err)
> break;
> @@ -3801,7 +3807,7 @@ print_commits(struct got_object_id *root_id, struct go
> }
> err = print_commit(commit, qid->id, repo, path,
> show_changed_paths ? &changed_paths : NULL,
> - show_patch, diff_context, refs_idmap);
> + show_patch, diff_context, refs_idmap, NULL);
> got_object_commit_close(commit);
> if (err)
> break;
> @@ -7495,7 +7501,7 @@ done:
> __dead static void
> usage_rebase(void)
> {
> - fprintf(stderr, "usage: %s rebase [-a] | [-c] | branch\n",
> + fprintf(stderr, "usage: %s rebase [-a] [-c] [-l] [branch]\n",
> getprogname());
> exit(1);
> }
> @@ -7608,11 +7614,12 @@ done:
> static const struct got_error *
> rebase_complete(struct got_worktree *worktree, struct got_fileindex *fileindex,
> struct got_reference *branch, struct got_reference *new_base_branch,
> - struct got_reference *tmp_branch, struct got_repository *repo)
> + struct got_reference *tmp_branch, struct got_repository *repo,
> + int create_backup)
> {
> printf("Switching work tree to %s\n", got_ref_get_name(branch));
> return got_worktree_rebase_complete(worktree, fileindex,
> - new_base_branch, tmp_branch, branch, repo);
> + new_base_branch, tmp_branch, branch, repo, create_backup);
> }
>
> static const struct got_error *
> @@ -7766,6 +7773,238 @@ done:
> }
>
> static const struct got_error *
> +get_commit_brief_str(char **brief_str, struct got_commit_object *commit)
> +{
> + const struct got_error *err = NULL;
> + time_t committer_time;
> + struct tm tm;
> + char datebuf[11]; /* YYYY-MM-DD + NUL */
> + char *author0 = NULL, *author, *smallerthan;
> + char *logmsg0 = NULL, *logmsg, *newline;
> +
> + committer_time = got_object_commit_get_committer_time(commit);
> + if (localtime_r(&committer_time, &tm) == NULL)
> + return got_error_from_errno("localtime_r");
> + if (strftime(datebuf, sizeof(datebuf), "%G-%m-%d", &tm)
> + >= sizeof(datebuf))
> + return got_error(GOT_ERR_NO_SPACE);
strftime returns 0 if the buffer is not large enough, and never more
than sizeof(datebuf)-1.
Looks like this incorrect check already used in several places through
the repo.
> +
> + author0 = strdup(got_object_commit_get_author(commit));
> + if (author0 == NULL)
> + return got_error_from_errno("strdup");
> + author = author0;
> + smallerthan = strchr(author, '<');
> + if (smallerthan && smallerthan[1] != '\0')
> + author = smallerthan + 1;
> + author[strcspn(author, "@>")] = '\0';
> +
> + err = got_object_commit_get_logmsg(&logmsg0, commit);
> + if (err)
> + goto done;
> + logmsg = logmsg0;
> + while (*logmsg == '\n')
> + logmsg++;
> + newline = strchr(logmsg, '\n');
> + if (newline)
> + *newline = '\0';
> +
> + if (asprintf(brief_str, "%s %s %s",
> + datebuf, author, logmsg) == -1)
> + err = got_error_from_errno("asprintf");
> +done:
> + free(author0);
> + free(logmsg0);
> + return err;
> +}
> +
> +static const struct got_error *
> +print_backup_ref(const char *branch_name, const char *new_id_str,
> + struct got_object_id *old_commit_id, struct got_commit_object *old_commit,
> + struct got_reflist_object_id_map *refs_idmap,
> + struct got_repository *repo)
> +{
> + const struct got_error *err = NULL;
> + struct got_reflist_head *refs;
> + char *refs_str = NULL;
> + struct got_object_id *new_commit_id = NULL;
> + struct got_commit_object *new_commit = NULL;
> + char *new_commit_brief_str = NULL;
> + struct got_object_id *yca_id = NULL;
> + struct got_commit_object *yca_commit = NULL;
> + char *yca_id_str = NULL, *yca_brief_str = NULL;
> + char *custom_refs_str;
> +
> + if (asprintf(&custom_refs_str, "formerly %s", branch_name) == -1)
> + return got_error_from_errno("asprintf");
> +
> + err = print_commit(old_commit, old_commit_id, repo, NULL, NULL,
> + 0, 0, refs_idmap, custom_refs_str);
> + if (err)
> + goto done;
> +
> + err = got_object_resolve_id_str(&new_commit_id, repo, new_id_str);
> + if (err)
> + goto done;
> +
> + refs = got_reflist_object_id_map_lookup(refs_idmap, new_commit_id);
> + if (refs) {
> + err = build_refs_str(&refs_str, refs, new_commit_id, repo);
> + if (err)
> + goto done;
> + }
> +
> + err = got_object_open_as_commit(&new_commit, repo, new_commit_id);
> + if (err)
> + goto done;
> +
> + err = get_commit_brief_str(&new_commit_brief_str, new_commit);
> + if (err)
> + goto done;
> +
> + err = got_commit_graph_find_youngest_common_ancestor(&yca_id,
> + old_commit_id, new_commit_id, repo, check_cancelled, NULL);
> + if (err)
> + goto done;
> +
> + printf("has become commit %s%s%s%s\n %s\n", new_id_str,
> + refs_str ? " (" : "", refs_str ? refs_str : "",
> + refs_str ? ")" : "", new_commit_brief_str);
> + if (yca_id && got_object_id_cmp(yca_id, new_commit_id) != 0 &&
> + got_object_id_cmp(yca_id, old_commit_id) != 0) {
> + free(refs_str);
> + refs_str = NULL;
> +
> + err = got_object_open_as_commit(&yca_commit, repo, yca_id);
> + if (err)
> + goto done;
> +
> + err = get_commit_brief_str(&yca_brief_str, yca_commit);
> + if (err)
> + goto done;
> +
> + err = got_object_id_str(&yca_id_str, yca_id);
> + if (err)
> + goto done;
> +
> + refs = got_reflist_object_id_map_lookup(refs_idmap, yca_id);
> + if (refs) {
> + err = build_refs_str(&refs_str, refs, yca_id, repo);
> + if (err)
> + goto done;
> + }
> + printf("history forked at %s%s%s%s\n %s\n",
> + yca_id_str,
> + refs_str ? " (" : "", refs_str ? refs_str : "",
> + refs_str ? ")" : "", yca_brief_str);
> + }
> +done:
> + free(custom_refs_str);
> + free(new_commit_id);
> + free(refs_str);
> + free(yca_id);
> + free(yca_id_str);
> + free(yca_brief_str);
> + if (new_commit)
> + got_object_commit_close(new_commit);
> + if (yca_commit)
> + got_object_commit_close(yca_commit);
> +
> + return NULL;
> +}
> +
> +static const struct got_error *
> +list_backup_refs(const char *backup_ref_prefix, const char *wanted_branch_name,
> + struct got_repository *repo)
> +{
> + const struct got_error *err;
> + struct got_reflist_head refs, backup_refs;
> + struct got_reflist_entry *re;
> + const size_t backup_ref_prefix_len = strlen(backup_ref_prefix);
> + struct got_object_id *old_commit_id = NULL;
> + char *branch_name = NULL;
> + struct got_commit_object *old_commit = NULL;
> + struct got_reflist_object_id_map *refs_idmap = NULL;
> +
> + TAILQ_INIT(&refs);
> + TAILQ_INIT(&backup_refs);
> +
> + err = got_ref_list(&refs, repo, NULL, got_ref_cmp_by_name, NULL);
> + if (err)
> + return err;
> +
> + err = got_reflist_object_id_map_create(&refs_idmap, &refs, repo);
> + if (err)
> + goto done;
> +
> + if (wanted_branch_name) {
> + if (strncmp(wanted_branch_name, "refs/heads/", 11) == 0)
> + wanted_branch_name += 11;
> + }
> +
> + err = got_ref_list(&backup_refs, repo, backup_ref_prefix,
> + got_ref_cmp_by_commit_timestamp_descending, repo);
> + if (err)
> + goto done;
> +
> + TAILQ_FOREACH(re, &backup_refs, entry) {
> + const char *refname = got_ref_get_name(re->ref);
> + char *slash;
> +
> + err = got_ref_resolve(&old_commit_id, repo, re->ref);
> + if (err)
> + break;
> +
> + err = got_object_open_as_commit(&old_commit, repo,
> + old_commit_id);
> + if (err)
> + break;
> +
> + if (strncmp(backup_ref_prefix, refname,
> + backup_ref_prefix_len) == 0)
> + refname += backup_ref_prefix_len;
> +
> + while (refname[0] == '/')
> + refname++;
> +
> + branch_name = strdup(refname);
> + if (branch_name == NULL) {
> + err = got_error_from_errno("strdup");
> + break;
> + }
> + slash = strrchr(branch_name, '/');
> + if (slash) {
> + *slash = '\0';
> + refname += strlen(branch_name) + 1;
> + }
> +
> + if (wanted_branch_name == NULL ||
> + strcmp(wanted_branch_name, branch_name) == 0) {
> + err = print_backup_ref(branch_name, refname,
> + old_commit_id, old_commit, refs_idmap, repo);
> + if (err)
> + break;
> + }
> +
> + free(old_commit_id);
> + old_commit_id = NULL;
> + free(branch_name);
> + branch_name = NULL;
> + got_object_commit_close(old_commit);
> + old_commit = NULL;
> + }
> +done:
> + if (refs_idmap)
> + got_reflist_object_id_map_free(refs_idmap);
> + got_ref_list_free(&refs);
> + got_ref_list_free(&backup_refs);
> + free(old_commit_id);
> + free(branch_name);
> + if (old_commit)
> + got_object_commit_close(old_commit);
> + return err;
> +}
> +
> +static const struct got_error *
> cmd_rebase(int argc, char *argv[])
> {
> const struct got_error *error = NULL;
> @@ -7780,7 +8019,7 @@ cmd_rebase(int argc, char *argv[])
> struct got_object_id *branch_head_commit_id = NULL, *yca_id = NULL;
> struct got_commit_object *commit = NULL;
> int ch, rebase_in_progress = 0, abort_rebase = 0, continue_rebase = 0;
> - int histedit_in_progress = 0;
> + int histedit_in_progress = 0, create_backup = 1, list_backups = 0;
> unsigned char rebase_status = GOT_STATUS_NO_CHANGE;
> struct got_object_id_queue commits;
> struct got_pathlist_head merged_paths;
> @@ -7790,7 +8029,7 @@ cmd_rebase(int argc, char *argv[])
> SIMPLEQ_INIT(&commits);
> TAILQ_INIT(&merged_paths);
>
> - while ((ch = getopt(argc, argv, "ac")) != -1) {
> + while ((ch = getopt(argc, argv, "acl")) != -1) {
> switch (ch) {
> case 'a':
> abort_rebase = 1;
> @@ -7798,6 +8037,9 @@ cmd_rebase(int argc, char *argv[])
> case 'c':
> continue_rebase = 1;
> break;
> + case 'l':
> + list_backups = 1;
> + break;
> default:
> usage_rebase();
> /* NOTREACHED */
> @@ -7812,13 +8054,22 @@ cmd_rebase(int argc, char *argv[])
> "unveil", NULL) == -1)
> err(1, "pledge");
> #endif
> - if (abort_rebase && continue_rebase)
> - usage_rebase();
> - else if (abort_rebase || continue_rebase) {
> - if (argc != 0)
> + if (list_backups) {
> + if (abort_rebase)
> + option_conflict('l', 'a');
> + if (continue_rebase)
> + option_conflict('l', 'c');
> + if (argc != 0 && argc != 1)
> usage_rebase();
> - } else if (argc != 1)
> - usage_rebase();
> + } else {
> + if (abort_rebase && continue_rebase)
> + usage_rebase();
> + else if (abort_rebase || continue_rebase) {
> + if (argc != 0)
> + usage_rebase();
> + } else if (argc != 1)
> + usage_rebase();
> + }
>
> cwd = getcwd(NULL, 0);
> if (cwd == NULL) {
> @@ -7827,21 +8078,33 @@ cmd_rebase(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, "rebase", cwd);
> - goto done;
> + if (list_backups) {
> + if (error->code != GOT_ERR_NOT_WORKTREE)
> + goto done;
> + } else {
> + if (error->code == GOT_ERR_NOT_WORKTREE)
> + error = wrap_not_worktree_error(error,
> + "rebase", cwd);
> + goto done;
> + }
> }
>
> - error = got_repo_open(&repo, got_worktree_get_repo_path(worktree),
> - NULL);
> + error = got_repo_open(&repo,
> + worktree ? got_worktree_get_repo_path(worktree) : cwd, NULL);
> 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_backups) {
> + error = list_backup_refs(GOT_WORKTREE_REBASE_BACKUP_REF_PREFIX,
> + argc == 1 ? argv[0] : NULL, repo);
> + goto done; /* nothing else to do */
> + }
> +
> error = got_worktree_histedit_in_progress(&histedit_in_progress,
> worktree);
> if (error)
> @@ -7979,7 +8242,8 @@ cmd_rebase(int argc, char *argv[])
> if (SIMPLEQ_EMPTY(&commits)) {
> if (continue_rebase) {
> error = rebase_complete(worktree, fileindex,
> - branch, new_base_branch, tmp_branch, repo);
> + branch, new_base_branch, tmp_branch, repo,
> + create_backup);
> goto done;
> } else {
> /* Fast-forward the reference of the branch. */
> @@ -7997,6 +8261,8 @@ cmd_rebase(int argc, char *argv[])
> new_head_commit_id);
> if (error)
> goto done;
> + /* No backup needed since objects did not change. */
> + create_backup = 0;
> }
> }
>
> @@ -8042,7 +8308,7 @@ cmd_rebase(int argc, char *argv[])
> "conflicts must be resolved before rebasing can continue");
> } else
> error = rebase_complete(worktree, fileindex, branch,
> - new_base_branch, tmp_branch, repo);
> + new_base_branch, tmp_branch, repo, create_backup);
> done:
> got_object_id_queue_free(&commits);
> free(branch_head_commit_id);
> @@ -8066,8 +8332,8 @@ done:
> __dead static void
> usage_histedit(void)
> {
> - fprintf(stderr, "usage: %s histedit [-a] [-c] [-f] [-F histedit-script] [-m]\n",
> - getprogname());
> + fprintf(stderr, "usage: %s histedit [-a] [-c] [-f] "
> + "[-F histedit-script] [-m] [-l] [branch]\n", getprogname());
> exit(1);
> }
>
> @@ -8901,6 +9167,7 @@ cmd_histedit(int argc, char *argv[])
> struct got_update_progress_arg upa;
> int edit_in_progress = 0, abort_edit = 0, continue_edit = 0;
> int edit_logmsg_only = 0, fold_only = 0;
> + int list_backups = 0;
> const char *edit_script_path = NULL;
> unsigned char rebase_status = GOT_STATUS_NO_CHANGE;
> struct got_object_id_queue commits;
> @@ -8915,7 +9182,7 @@ cmd_histedit(int argc, char *argv[])
> TAILQ_INIT(&merged_paths);
> memset(&upa, 0, sizeof(upa));
>
> - while ((ch = getopt(argc, argv, "acfF:m")) != -1) {
> + while ((ch = getopt(argc, argv, "acfF:ml")) != -1) {
> switch (ch) {
> case 'a':
> abort_edit = 1;
> @@ -8932,6 +9199,9 @@ cmd_histedit(int argc, char *argv[])
> case 'm':
> edit_logmsg_only = 1;
> break;
> + case 'l':
> + list_backups = 1;
> + break;
> default:
> usage_histedit();
> /* NOTREACHED */
> @@ -8962,7 +9232,20 @@ cmd_histedit(int argc, char *argv[])
> option_conflict('f', 'm');
> if (edit_script_path && fold_only)
> option_conflict('F', 'f');
> - if (argc != 0)
> + if (list_backups) {
> + if (abort_edit)
> + option_conflict('l', 'a');
> + if (continue_edit)
> + option_conflict('l', 'c');
> + if (edit_script_path)
> + option_conflict('l', 'F');
> + if (edit_logmsg_only)
> + option_conflict('l', 'm');
> + if (fold_only)
> + option_conflict('l', 'f');
> + if (argc != 0 && argc != 1)
> + usage_histedit();
> + } else if (argc != 0)
> usage_histedit();
>
> /*
> @@ -8981,11 +9264,33 @@ cmd_histedit(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, "histedit", cwd);
> - goto done;
> + if (list_backups) {
> + if (error->code != GOT_ERR_NOT_WORKTREE)
> + goto done;
> + } else {
> + if (error->code == GOT_ERR_NOT_WORKTREE)
> + error = wrap_not_worktree_error(error,
> + "histedit", cwd);
> + goto done;
> + }
> }
>
> + if (list_backups) {
> + error = got_repo_open(&repo,
> + worktree ? got_worktree_get_repo_path(worktree) : cwd,
> + NULL);
> + if (error != NULL)
> + goto done;
> + error = apply_unveil(got_repo_get_path(repo), 0,
> + worktree ? got_worktree_get_root_path(worktree) : NULL);
> + if (error)
> + goto done;
> + error = list_backup_refs(
> + GOT_WORKTREE_HISTEDIT_BACKUP_REF_PREFIX,
> + argc == 1 ? argv[0] : NULL, repo);
> + goto done; /* nothing else to do */
> + }
> +
> error = got_repo_open(&repo, got_worktree_get_repo_path(worktree),
> NULL);
> if (error != NULL)
> blob - 1c93ac41afa54d36bb8a41c39bec6058ab4d6204
> blob + e330768de849dc0fa39914378bac64b6f8a09c01
> --- include/got_reference.h
> +++ include/got_reference.h
> @@ -99,6 +99,13 @@ const struct got_error *got_ref_cmp_tags(void *, int *
> struct got_reference *, struct got_reference *);
>
> /*
> + * An implementation of got_ref_cmp_cb which compares commit timestamps.
> + * Requires a struct got_repository * as the void * argument.
> + */
> +const struct got_error *got_ref_cmp_by_commit_timestamp_descending(void *,
> + int *, struct got_reference *, struct got_reference *);
> +
> +/*
> * Append all known references to a caller-provided ref list head.
> * Optionally limit references returned to those within a given
> * reference namespace. Sort the list with the provided reference comparison
> blob - 24fddd52e81d96ff7b1800a61de1f80319326a3e
> blob + bdec91ab8835c58cccb0f5d069d42737a22bec05
> --- include/got_worktree.h
> +++ include/got_worktree.h
> @@ -314,10 +314,12 @@ const struct got_error *got_worktree_rebase_postpone(s
> /*
> * Complete the current rebase operation. This should be called once all
> * commits have been rebased successfully.
> + * The create_backup parameter controls whether the rebased branch will
> + * be backed up via a reference in refs/got/backup/rebase/.
> */
> const struct got_error *got_worktree_rebase_complete(struct got_worktree *,
> struct got_fileindex *, struct got_reference *, struct got_reference *,
> - struct got_reference *, struct got_repository *);
> + struct got_reference *, struct got_repository *, int create_backup);
>
> /*
> * Abort the current rebase operation.
> @@ -469,3 +471,9 @@ typedef const struct got_error *(*got_worktree_path_in
> const struct got_error *
> got_worktree_path_info(struct got_worktree *, struct got_pathlist_head *,
> got_worktree_path_info_cb, void *, got_cancel_cb , void *);
> +
> +/* Rereferences pointing at pre-rebase commit backups. */
> +#define GOT_WORKTREE_REBASE_BACKUP_REF_PREFIX "refs/got/backup/rebase"
> +
> +/* Rereferences pointing at pre-histedit commit backups. */
> +#define GOT_WORKTREE_HISTEDIT_BACKUP_REF_PREFIX "refs/got/backup/histedit"
> blob - 71840ebb38db39cca7f0629d64ca474b0077e335
> blob + 4b530db4089bee13c3808f740ec124254aa41fca
> --- lib/reference.c
> +++ lib/reference.c
> @@ -742,6 +742,48 @@ done:
> return err;
> }
>
> +const struct got_error *
> +got_ref_cmp_by_commit_timestamp_descending(void *arg, int *cmp,
> + struct got_reference *ref1, struct got_reference *ref2)
> +{
> + const struct got_error *err;
> + struct got_repository *repo = arg;
> + struct got_object_id *id1, *id2 = NULL;
> + struct got_commit_object *commit1 = NULL, *commit2 = NULL;
> + time_t time1, time2;
> +
> + *cmp = 0;
> +
> + err = got_ref_resolve(&id1, repo, ref1);
> + if (err)
> + return err;
> + err = got_ref_resolve(&id2, repo, ref2);
> + if (err)
> + goto done;
> +
> + err = got_object_open_as_commit(&commit1, repo, id1);
> + if (err)
> + goto done;
> + err = got_object_open_as_commit(&commit2, repo, id2);
> + if (err)
> + goto done;
> +
> + time1 = got_object_commit_get_committer_time(commit1);
> + time2 = got_object_commit_get_committer_time(commit2);
> + if (time1 < time2)
> + *cmp = 1;
> + else if (time2 < time1)
> + *cmp = -1;
> +done:
> + free(id1);
> + free(id2);
> + if (commit1)
> + got_object_commit_close(commit1);
> + if (commit2)
> + got_object_commit_close(commit2);
> + return err;
> +}
> +
> static const struct got_error *
> insert_ref(struct got_reflist_entry **newp, struct got_reflist_head *refs,
> struct got_reference *ref, struct got_repository *repo,
> blob - e81fcf0f255ee6dd35a283cf1c758066903935f0
> blob + 3d7fd16f3bc469ada987b984a5a539da754992ca
> --- lib/worktree.c
> +++ lib/worktree.c
> @@ -6455,10 +6455,54 @@ done:
> }
>
> const struct got_error *
> +create_backup_ref(const char *backup_ref_prefix, struct got_reference *branch,
> + struct got_object_id *new_commit_id, struct got_repository *repo)
> +{
> + const struct got_error *err;
> + struct got_reference *ref = NULL;
> + struct got_object_id *old_commit_id = NULL;
> + const char *branch_name = NULL;
> + char *new_id_str = NULL;
> + char *refname = NULL;
> +
> + branch_name = got_ref_get_name(branch);
> + if (strncmp(branch_name, "refs/heads/", 11) != 0)
> + return got_error(GOT_ERR_BAD_REF_NAME); /* should not happen */
> + branch_name += 11;
> +
> + err = got_object_id_str(&new_id_str, new_commit_id);
> + if (err)
> + return err;
> +
> + if (asprintf(&refname, "%s/%s/%s", backup_ref_prefix, branch_name,
> + new_id_str) == -1) {
> + err = got_error_from_errno("asprintf");
> + goto done;
> + }
> +
> + err = got_ref_resolve(&old_commit_id, repo, branch);
> + if (err)
> + goto done;
> +
> + err = got_ref_alloc(&ref, refname, old_commit_id);
> + if (err)
> + goto done;
> +
> + err = got_ref_write(ref, repo);
> +done:
> + free(new_id_str);
> + free(refname);
> + free(old_commit_id);
> + if (ref)
> + got_ref_close(ref);
> + return err;
> +}
> +
> +const struct got_error *
> got_worktree_rebase_complete(struct got_worktree *worktree,
> struct got_fileindex *fileindex, struct got_reference *new_base_branch,
> struct got_reference *tmp_branch, struct got_reference *rebased_branch,
> - struct got_repository *repo)
> + struct got_repository *repo, int create_backup)
> {
> const struct got_error *err, *unlockerr, *sync_err;
> struct got_object_id *new_head_commit_id = NULL;
> @@ -6468,6 +6512,13 @@ got_worktree_rebase_complete(struct got_worktree *work
> if (err)
> return err;
>
> + if (create_backup) {
> + err = create_backup_ref(GOT_WORKTREE_REBASE_BACKUP_REF_PREFIX,
> + rebased_branch, new_head_commit_id, repo);
> + if (err)
> + goto done;
> + }
> +
> err = got_ref_change_ref(rebased_branch, new_head_commit_id);
> if (err)
> goto done;
> @@ -6954,6 +7005,11 @@ got_worktree_histedit_complete(struct got_worktree *wo
> if (err)
> goto done;
>
> + err = create_backup_ref(GOT_WORKTREE_HISTEDIT_BACKUP_REF_PREFIX,
> + resolved, new_head_commit_id, repo);
> + if (err)
> + goto done;
> +
> err = got_ref_change_ref(resolved, new_head_commit_id);
> if (err)
> goto done;
> blob - 4eb63f995b4b84ea70ff445e9e93e6389910486d
> blob + fbd40bcace0448588f375c648b957b069821ab5f
> --- regress/cmdline/common.sh
> +++ regress/cmdline/common.sh
> @@ -20,6 +20,7 @@ export GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"
> export GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"
> export GOT_AUTHOR="$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>"
> export GOT_AUTHOR_8="flan_hac"
> +export GOT_AUTHOR_11="flan_hacker"
> export GOT_LOG_DEFAULT_LIMIT=0
> export GOT_TEST_ROOT="/tmp"
>
> blob - 82d727d0ed29a44abebc77cbd4f0c415c5828142
> blob + b6f5b85dd337c44468c9ff11b1cf6976705b46fb
> --- regress/cmdline/histedit.sh
> +++ regress/cmdline/histedit.sh
> @@ -20,6 +20,7 @@ test_histedit_no_op() {
> local testroot=`test_init histedit_no_op`
>
> local orig_commit=`git_show_head $testroot/repo`
> + local orig_author_time=`git_show_author_time $testroot/repo`
>
> echo "modified alpha on master" > $testroot/repo/alpha
> (cd $testroot/repo && git rm -q beta)
> @@ -31,6 +32,7 @@ test_histedit_no_op() {
> echo "modified zeta on master" > $testroot/repo/epsilon/zeta
> git_commit $testroot/repo -m "committing to zeta on master"
> local old_commit2=`git_show_head $testroot/repo`
> + local old_author_time2=`git_show_author_time $testroot/repo`
>
> got diff -r $testroot/repo $orig_commit $old_commit2 \
> > $testroot/diff.expected
> @@ -50,6 +52,7 @@ test_histedit_no_op() {
>
> local new_commit1=`git_show_parent_commit $testroot/repo`
> local new_commit2=`git_show_head $testroot/repo`
> + local new_author_time2=`git_show_author_time $testroot/repo`
>
> local short_old_commit1=`trim_obj_id 28 $old_commit1`
> local short_old_commit2=`trim_obj_id 28 $old_commit2`
> @@ -143,7 +146,32 @@ test_histedit_no_op() {
> ret="$?"
> if [ "$ret" != "0" ]; then
> diff -u $testroot/stdout.expected $testroot/stdout
> + test_done "$testroot" "$ret"
> fi
> +
> + # We should have a backup of old commits
> + (cd $testroot/repo && got histedit -l > $testroot/stdout)
> + d_orig2=`env TZ=UTC date -r $old_author_time2 +"%a %b %e %X %Y UTC"`
> + d_new2=`env TZ=UTC date -r $new_author_time2 +"%G-%m-%d"`
> + d_orig=`env TZ=UTC date -r $orig_author_time +"%G-%m-%d"`
> + cat > $testroot/stdout.expected <<EOF
> +-----------------------------------------------
> +commit $old_commit2 (formerly master)
> +from: $GOT_AUTHOR
> +date: $d_orig2
> +
> + committing to zeta on master
> +
> +has become commit $new_commit2 (master)
> + $d_new2 $GOT_AUTHOR_11 committing to zeta on master
> +history forked at $orig_commit
> + $d_orig $GOT_AUTHOR_11 adding the test tree
> +EOF
> + cmp -s $testroot/stdout.expected $testroot/stdout
> + ret="$?"
> + if [ "$ret" != "0" ]; then
> + diff -u $testroot/stdout.expected $testroot/stdout
> + fi
> test_done "$testroot" "$ret"
> }
>
> blob - 5bf2ae44883819420fe82ae946ad7f2f328b84b7
> blob + f972bece510f4dafdcb5218eae89fbaa86daef0c
> --- regress/cmdline/rebase.sh
> +++ regress/cmdline/rebase.sh
> @@ -18,6 +18,8 @@
>
> test_rebase_basic() {
> local testroot=`test_init rebase_basic`
> + local commit0=`git_show_head $testroot/repo`
> + local commit0_author_time=`git_show_author_time $testroot/repo`
>
> (cd $testroot/repo && git checkout -q -b newbranch)
> echo "modified delta on branch" > $testroot/repo/gamma/delta
> @@ -31,6 +33,7 @@ test_rebase_basic() {
>
> local orig_commit1=`git_show_parent_commit $testroot/repo`
> local orig_commit2=`git_show_head $testroot/repo`
> + local orig_author_time2=`git_show_author_time $testroot/repo`
>
> (cd $testroot/repo && git checkout -q master)
> echo "modified zeta on master" > $testroot/repo/epsilon/zeta
> @@ -49,6 +52,7 @@ test_rebase_basic() {
> (cd $testroot/repo && git checkout -q newbranch)
> local new_commit1=`git_show_parent_commit $testroot/repo`
> local new_commit2=`git_show_head $testroot/repo`
> + local new_author_time2=`git_show_author_time $testroot/repo`
>
> local short_orig_commit1=`trim_obj_id 28 $orig_commit1`
> local short_orig_commit2=`trim_obj_id 28 $orig_commit2`
> @@ -143,7 +147,32 @@ test_rebase_basic() {
> ret="$?"
> if [ "$ret" != "0" ]; then
> diff -u $testroot/stdout.expected $testroot/stdout
> + test_done "$testroot" "$ret"
> fi
> +
> + # We should have a backup of old commits
> + (cd $testroot/repo && got rebase -l > $testroot/stdout)
> + d_orig2=`env TZ=UTC date -r $orig_author_time2 +"%a %b %e %X %Y UTC"`
> + d_new2=`env TZ=UTC date -r $new_author_time2 +"%G-%m-%d"`
> + d_0=`env TZ=UTC date -r $commit0_author_time +"%G-%m-%d"`
> + cat > $testroot/stdout.expected <<EOF
> +-----------------------------------------------
> +commit $orig_commit2 (formerly newbranch)
> +from: $GOT_AUTHOR
> +date: $d_orig2
> +
> + committing more changes on newbranch
> +
> +has become commit $new_commit2 (newbranch)
> + $d_new2 $GOT_AUTHOR_11 committing more changes on newbranch
> +history forked at $commit0
> + $d_0 $GOT_AUTHOR_11 adding the test tree
> +EOF
> + cmp -s $testroot/stdout.expected $testroot/stdout
> + ret="$?"
> + if [ "$ret" != "0" ]; then
> + diff -u $testroot/stdout.expected $testroot/stdout
> + fi
> test_done "$testroot" "$ret"
> }
>
> @@ -890,7 +919,17 @@ test_rebase_forward() {
> ret="$?"
> if [ "$ret" != "0" ]; then
> diff -u $testroot/stdout.expected $testroot/stdout
> + test_done "$testroot" "$ret"
> fi
> +
> + # Forward-only rebase operations should not be backed up
> + (cd $testroot/repo && got rebase -l > $testroot/stdout)
> + echo -n > $testroot/stdout.expected
> + cmp -s $testroot/stdout.expected $testroot/stdout
> + ret="$?"
> + if [ "$ret" != "0" ]; then
> + diff -u $testroot/stdout.expected $testroot/stdout
> + fi
> test_done "$testroot" "$ret"
> }
>
>
back up old commits after rebase and histedit