Commit Diff


commit - 601aba22ab7ec8a86055e5fca4fcef11882c3ba4
commit + e600f1246e15fff13251ba9d299d74a24ae579c2
blob - a8b88374d28eb0bf77bcda9f79b0a80b31e5c227
blob + 6b06d43ab24d2750a66f3657513869ecb77d13f8
--- got/got.1
+++ got/got.1
@@ -1428,7 +1428,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.
@@ -1469,6 +1469,16 @@ Once rebasing has completed successfully, the temporar
 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.
+Removal of these references makes objects which become unreachable via
+any reference subject to removal by Git's garbage collector.
 .Pp
 While rebasing commits, show the status of each affected file,
 using the following status codes:
@@ -1532,11 +1542,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
@@ -1602,6 +1645,16 @@ 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.
+Removal of these references makes objects which become unreachable via
+any reference subject to removal by Git's garbage collector.
+.Pp
 While merging commits, show the status of each affected file,
 using the following status codes:
 .Bl -column YXZ description
@@ -1688,6 +1741,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 - 5a81fddc99d43b6d16f047c9a6ade37e0058f4e6
blob + 72b046e72813d5b96fe84dcbe2363d5afb0fc7f9
--- 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 *
@@ -7763,9 +7770,241 @@ collect_commits(struct got_object_id_queue *commits,
 done:
 	got_commit_graph_close(graph);
 	return err;
+}
+
+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,9 +9264,31 @@ 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),
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 + 9246dbdca16b2907e56bbebc9f597bb7e3e91b10
--- 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 *);
+
+/* References pointing at pre-rebase commit backups. */
+#define GOT_WORKTREE_REBASE_BACKUP_REF_PREFIX "refs/got/backup/rebase"
+
+/* References 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
@@ -735,6 +735,48 @@ done:
 		got_object_tag_close(tag1);
 	if (tag2)
 		got_object_tag_close(tag2);
+	if (commit1)
+		got_object_commit_close(commit1);
+	if (commit2)
+		got_object_commit_close(commit2);
+	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)
blob - e81fcf0f255ee6dd35a283cf1c758066903935f0
blob + 3d7fd16f3bc469ada987b984a5a539da754992ca
--- lib/worktree.c
+++ lib/worktree.c
@@ -6451,6 +6451,50 @@ done:
 	free(new_base_branch_ref_name);
 	free(branch_ref_name);
 	free(commit_ref_name);
+	return err;
+}
+
+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;
 }
 
@@ -6458,7 +6502,7 @@ 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;
@@ -6467,6 +6511,13 @@ got_worktree_rebase_complete(struct got_worktree *work
 	err = got_ref_resolve(&new_head_commit_id, repo, tmp_branch);
 	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)
@@ -6951,6 +7002,11 @@ got_worktree_histedit_complete(struct got_worktree *wo
 
 	err = got_ref_open(&resolved, repo,
 	    got_ref_get_symref_target(edited_branch), 0);
+	if (err)
+		goto done;
+
+	err = create_backup_ref(GOT_WORKTREE_HISTEDIT_BACKUP_REF_PREFIX,
+	    resolved, new_head_commit_id, repo);
 	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`
@@ -139,6 +142,31 @@ test_histedit_no_op() {
 	(cd $testroot/wt && got update > $testroot/stdout)
 
 	echo 'Already up-to-date' > $testroot/stdout.expected
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	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
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"
 }