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

From:
Stefan Sperling <stsp@stsp.name>
Subject:
back up old commits after rebase and histedit
To:
gameoftrees@openbsd.org
Date:
Sun, 21 Mar 2021 15:21:38 +0100

Download raw body.

Thread
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?

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);
+
+	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"
 }