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

From:
Josh Rickmar <joshrickmar@outlook.com>
Subject:
Re: back up old commits after rebase and histedit
To:
Stefan Sperling <stsp@stsp.name>
Cc:
gameoftrees@openbsd.org
Date:
Sun, 21 Mar 2021 13:07:54 -0400

Download raw body.

Thread
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"
>  }
>  
>