commit e600f1246e15fff13251ba9d299d74a24ae579c2 from: Stefan Sperling date: Sun Mar 21 19:09:35 2021 UTC ensure that old commits remain referenced after rebase and histedit Create automatic "backup" references which ensure that objects from the pre-rebase or pre-histedit state remain in the repository. A new -l option for 'got rebase' and 'got histedit' lists old commits. This makes it easier to recover from botched rebase or histedit operations. Removal of such objects currently requires got ref -d and git-gc. This will be made more convenient in the future. testing and ok jrick 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 < $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 < $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" }