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