commit 818c750100809b9b2d2c638d39f1427a66929fce from: Stefan Sperling date: Thu Jul 11 01:52:30 2019 UTC initial 'got rebase' implementation commit - 2b496619daecc1f25b1bc0c53e01685030dc2c74 commit + 818c750100809b9b2d2c638d39f1427a66929fce blob - 892d74adc7e2c036bf693521b13f8399543ee673 blob + 0bcca9eaa8ffbc8bd3c160351ddde972552e16ff --- got/got.1 +++ got/got.1 @@ -529,7 +529,73 @@ conflicts must be resolved first. .It Cm bo Short alias for .Cm backout . +.It Cm rebase Ar branch +Rebase commits on the specified +.Ar branch +onto the tip of the current branch of the work tree. +The +.Ar branch +must share common ancestry with the work tree's current branch. +Rebasing begins with the first descendent of the youngest common +ancestor commit of +.Ar branch +and the work tree's current branch, and stops once the tip commit +of +.Ar branch +has been reached. +.Pp +Rebased commits are accumulated on a temporary branch and represent +the same changes and log messages as their counterparts on the original +.Ar branch , +but with different commit IDs. +Once rebasing has completed successfully, the temporary branch becomes +the new version of +.Ar branch +and the work tree is automatically switched to it. +.Pp +While rebasing commits, show the status of each affected file, +using the following status codes: +.Bl -column YXZ description +.It G Ta file was merged +.It C Ta file was merged and conflicts occurred during merge +.It ! Ta changes destined for a missing file were not merged +.It D Ta file was deleted +.It d Ta file's deletion was obstructed by local modifications +.It A Ta new file was added +.It ~ Ta changes destined for a non-regular file were not merged .El +.Pp +If merge conflicts occur, the rebase operation will be interrupted and +may be continued once conflicts have been resolved. +Alternatively, the rebase operation may be aborted which will leave +.Ar branch +unmodified and the work tree switched back to its original branch. +.Pp +.Cm got rebase +will refuse to run if certain preconditions are not met. +If the work tree contains multiple base commits it must first be updated +to a single base commit with +.Cm got update . +If the work tree contains local changes, these changes must be committed +or reverted first. +.Pp +Some +.Nm +commands may refuse to run while a rebase operation is in progress. +.Pp +The options for +.Cm got rebase +are as follows: +.Bl -tag -width Ds +.It Fl a +Abort an interrupted rebase operation. +.It Fl c +Continue an interrupted current rebase operation. +.El +.It Cm rb +Short alias for +.Cm rebase . +.El .Sh ENVIRONMENT .Bl -tag -width GOT_AUTHOR .It Ev GOT_AUTHOR @@ -619,20 +685,9 @@ Rebase the branch on top of the new head commit of the .Dq master branch. -This step currently requires -.Xr git 1 : .Pp -.Dl $ git clone /var/git/src.git ~/src-git-wt -.Dl $ cd ~/src-git-wt -.Dl $ git checkout unified-buffer-cache -.Dl $ git rebase master -.Dl $ git push -f -.Pp -Update the work tree to the newly rebased -.Dq unified-buffer-cache -branch: -.Pp -.Dl $ got update -b unified-buffer-cache +.Dl $ got update -b master +.Dl $ got rebase unified-buffer-cache .Sh SEE ALSO .Xr git-repository 5 .Xr got-worktree 5 blob - 9ab824b4938b0e7ee27eb4d390a76c74e7f61c9f blob + e6bdb5cd9d11bf52149587239d83c4a3b11b0613 --- got/got.c +++ got/got.c @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -90,6 +91,7 @@ __dead static void usage_revert(void); __dead static void usage_commit(void); __dead static void usage_cherrypick(void); __dead static void usage_backout(void); +__dead static void usage_rebase(void); static const struct got_error* cmd_init(int, char *[]); static const struct got_error* cmd_checkout(int, char *[]); @@ -107,6 +109,7 @@ static const struct got_error* cmd_revert(int, char * static const struct got_error* cmd_commit(int, char *[]); static const struct got_error* cmd_cherrypick(int, char *[]); static const struct got_error* cmd_backout(int, char *[]); +static const struct got_error* cmd_rebase(int, char *[]); static struct got_cmd got_commands[] = { { "init", cmd_init, usage_init, "" }, @@ -125,6 +128,7 @@ static struct got_cmd got_commands[] = { { "commit", cmd_commit, usage_commit, "ci" }, { "cherrypick", cmd_cherrypick, usage_cherrypick, "cy" }, { "backout", cmd_backout, usage_backout, "bo" }, + { "rebase", cmd_rebase, usage_rebase, "rb" }, }; static void @@ -3176,6 +3180,364 @@ done: free(commit_id_str); if (head_ref) got_ref_close(head_ref); + if (worktree) + got_worktree_close(worktree); + if (repo) + got_repo_close(repo); + return error; +} + +__dead static void +usage_rebase(void) +{ + fprintf(stderr, "usage: %s rebase [-a] [-c] | branch\n", getprogname()); + exit(1); +} + +static const struct got_error * +show_rebase_progress(struct got_commit_object *commit, + struct got_object_id *old_id, struct got_object_id *new_id) +{ + const struct got_error *err; + char *old_id_str = NULL, *new_id_str = NULL; + char *logmsg0 = NULL, *logmsg, *nl; + size_t len; + + err = got_object_id_str(&old_id_str, old_id); + if (err) + goto done; + + err = got_object_id_str(&new_id_str, new_id); + if (err) + goto done; + + logmsg0 = strdup(got_object_commit_get_logmsg(commit)); + if (logmsg0 == NULL) { + err = got_error_from_errno("strdup"); + goto done; + } + logmsg = logmsg0; + + while (isspace(logmsg[0])) + logmsg++; + + old_id_str[12] = '\0'; + new_id_str[12] = '\0'; + len = strlen(logmsg); + if (len > 42) + len = 42; + logmsg[len] = '\0'; + nl = strchr(logmsg, '\n'); + if (nl) + *nl = '\0'; + printf("%s -> %s: %s\n", old_id_str, new_id_str, logmsg); +done: + free(old_id_str); + free(new_id_str); + free(logmsg0); + return err; +} + +static void +rebase_progress(void *arg, unsigned char status, const char *path) +{ + unsigned char *rebase_status = arg; + + while (path[0] == '/') + path++; + printf("%c %s\n", status, path); + + if (*rebase_status == GOT_STATUS_CONFLICT) + return; + if (status == GOT_STATUS_CONFLICT || status == GOT_STATUS_MERGE) + *rebase_status = status; +} + +static const struct got_error * +rebase_complete(struct got_worktree *worktree, struct got_reference *branch, + struct got_reference *new_base_branch, struct got_reference *tmp_branch, + struct got_repository *repo) +{ + printf("Switching work tree to %s\n", got_ref_get_name(branch)); + return got_worktree_rebase_complete(worktree, + new_base_branch, tmp_branch, branch, repo); +} + +static const struct got_error * +cmd_rebase(int argc, char *argv[]) +{ + const struct got_error *error = NULL; + struct got_worktree *worktree = NULL; + struct got_repository *repo = NULL; + char *cwd = NULL; + struct got_reference *branch = NULL; + struct got_reference *new_base_branch = NULL, *tmp_branch = NULL; + struct got_object_id *commit_id = NULL, *parent_id = NULL; + struct got_object_id *resume_commit_id = NULL; + struct got_object_id *branch_head_commit_id = NULL, *yca_id = NULL; + struct got_commit_graph *graph = NULL; + struct got_commit_object *commit = NULL; + int ch, rebase_in_progress = 0, abort_rebase = 0, continue_rebase = 0; + unsigned char rebase_status = GOT_STATUS_NO_CHANGE; + struct got_object_id_queue commits; + const struct got_object_id_queue *parent_ids; + struct got_object_qid *qid, *pid; + + SIMPLEQ_INIT(&commits); + + while ((ch = getopt(argc, argv, "ac")) != -1) { + switch (ch) { + case 'a': + abort_rebase = 1; + break; + case 'c': + continue_rebase = 1; + break; + default: + usage_rebase(); + /* NOTREACHED */ + } + } + + argc -= optind; + argv += optind; + + 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) { + error = got_error_from_errno("getcwd"); + goto done; + } + error = got_worktree_open(&worktree, cwd); + if (error) + goto done; + + + error = got_repo_open(&repo, got_worktree_get_repo_path(worktree)); + if (error != NULL) + goto done; + + error = apply_unveil(got_repo_get_path(repo), 0, + got_worktree_get_root_path(worktree), 0); + if (error) + goto done; + + error = got_worktree_rebase_in_progress(&rebase_in_progress, worktree); + if (error) + goto done; + + if (rebase_in_progress && abort_rebase) { + int did_something; + error = got_worktree_rebase_continue(&resume_commit_id, + &new_base_branch, &tmp_branch, &branch, worktree, repo); + if (error) + goto done; + printf("Switching work tree to %s\n", + got_ref_get_symref_target(new_base_branch)); + error = got_worktree_rebase_abort(worktree, repo, + new_base_branch, update_progress, &did_something); + if (error) + goto done; + printf("Rebase of %s aborted\n", got_ref_get_name(branch)); + goto done; /* nothing else to do */ + } else if (abort_rebase) { + error = got_error(GOT_ERR_NOT_REBASING); + goto done; + } + + if (continue_rebase) { + struct got_object_id *new_commit_id; + + error = got_worktree_rebase_continue(&resume_commit_id, + &new_base_branch, &tmp_branch, &branch, worktree, repo); + if (error) + goto done; + error = got_commit_graph_find_youngest_common_ancestor(&yca_id, + got_worktree_get_base_commit_id(worktree), + resume_commit_id, repo); + if (error) + goto done; + if (yca_id == NULL) { + error = got_error_msg(GOT_ERR_ANCESTRY, + "cannot determine common ancestor commit for " + "continued rebase operation"); + goto done; + } + error = got_object_open_as_commit(&commit, repo, + resume_commit_id); + if (error) + goto done; + + error = got_worktree_rebase_commit(&new_commit_id, worktree, + tmp_branch, commit, resume_commit_id, repo); + if (error) + goto done; + error = show_rebase_progress(commit, resume_commit_id, + new_commit_id); + free(new_commit_id); + free(resume_commit_id); + + resume_commit_id = got_object_id_dup(SIMPLEQ_FIRST( + got_object_commit_get_parent_ids(commit))->id); + if (resume_commit_id == NULL) { + error = got_error_from_errno("got_object_id_dup"); + goto done; + } + got_object_commit_close(commit); + commit = NULL; + commit_id = resume_commit_id; + if (got_object_id_cmp(resume_commit_id, yca_id) == 0) { + error = rebase_complete(worktree, branch, + new_base_branch, tmp_branch, repo); + /* YCA has been reached; we are done. */ + goto done; + } + } else { + error = got_ref_open(&branch, repo, argv[0], 0); + if (error != NULL) + goto done; + + error = check_same_branch( + got_worktree_get_base_commit_id(worktree), branch, repo); + if (error) { + if (error->code != GOT_ERR_ANCESTRY) + goto done; + error = NULL; + } else { + error = got_error_msg(GOT_ERR_SAME_BRANCH, + "specified branch resolves to a commit which " + "is already contained in work tree's branch"); + goto done; + } + + error = got_ref_resolve(&branch_head_commit_id, repo, branch); + if (error) + goto done; + error = got_commit_graph_find_youngest_common_ancestor(&yca_id, + got_worktree_get_base_commit_id(worktree), + branch_head_commit_id, repo); + if (error) + goto done; + if (yca_id == NULL) { + error = got_error_msg(GOT_ERR_ANCESTRY, + "specified branch shares no common ancestry " + "with work tree's branch"); + goto done; + } + + error = got_worktree_rebase_prepare(&new_base_branch, + &tmp_branch, worktree, branch, repo); + if (error) + goto done; + commit_id = branch_head_commit_id; + } + + error = got_object_open_as_commit(&commit, repo, commit_id); + if (error) + goto done; + + error = got_commit_graph_open(&graph, commit_id, "/", 1, repo); + if (error) + goto done; + parent_ids = got_object_commit_get_parent_ids(commit); + pid = SIMPLEQ_FIRST(parent_ids); + error = got_commit_graph_iter_start(graph, pid->id, repo); + got_object_commit_close(commit); + commit = NULL; + if (error) + goto done; + while (got_object_id_cmp(commit_id, yca_id) != 0) { + error = got_commit_graph_iter_next(&parent_id, graph); + if (error) { + if (error->code == GOT_ERR_ITER_COMPLETED) { + error = got_error_msg(GOT_ERR_ANCESTRY, + "ran out of commits to rebase before " + "youngest common ancestor commit has " + "been reached?!?"); + goto done; + } else if (error->code != GOT_ERR_ITER_NEED_MORE) + goto done; + error = got_commit_graph_fetch_commits(graph, 1, repo); + if (error) + goto done; + } else { + error = got_object_qid_alloc(&qid, commit_id); + if (error) + goto done; + SIMPLEQ_INSERT_HEAD(&commits, qid, entry); + commit_id = parent_id; + } + } + + if (SIMPLEQ_EMPTY(&commits)) { + error = got_error(GOT_ERR_EMPTY_REBASE); + goto done; + } + + pid = NULL; + SIMPLEQ_FOREACH(qid, &commits, entry) { + struct got_object_id *new_commit_id; + + commit_id = qid->id; + parent_id = pid ? pid->id : yca_id; + pid = qid; + + error = got_worktree_rebase_merge_files(worktree, parent_id, + commit_id, repo, rebase_progress, &rebase_status, + check_cancelled, NULL); + if (error) + goto done; + + if (rebase_status == GOT_STATUS_CONFLICT) + break; + + error = got_object_open_as_commit(&commit, repo, commit_id); + if (error) + goto done; + error = got_worktree_rebase_commit(&new_commit_id, worktree, + tmp_branch, commit, commit_id, repo); + if (error) + goto done; + error = show_rebase_progress(commit, commit_id, new_commit_id); + free(new_commit_id); + got_object_commit_close(commit); + commit = NULL; + if (error) + goto done; + + } + + if (rebase_status == GOT_STATUS_CONFLICT) { + error = got_worktree_rebase_postpone(worktree); + if (error) + goto done; + error = got_error_msg(GOT_ERR_CONFLICTS, + "conflicts must be resolved before rebase can be resumed"); + } else + error = rebase_complete(worktree, branch, new_base_branch, + tmp_branch, repo); +done: + got_object_id_queue_free(&commits); + free(branch_head_commit_id); + free(resume_commit_id); + free(yca_id); + if (graph) + got_commit_graph_close(graph); + if (commit) + got_object_commit_close(commit); + if (branch) + got_ref_close(branch); + if (new_base_branch) + got_ref_close(new_base_branch); + if (tmp_branch) + got_ref_close(tmp_branch); if (worktree) got_worktree_close(worktree); if (repo) blob - 83b7c99d663d7a35621aa740a8b3f4da13281612 blob + 441dea9a556adbeec96868da881ff0e40f1f605d --- include/got_error.h +++ include/got_error.h @@ -97,6 +97,10 @@ #define GOT_ERR_MIXED_COMMITS 81 #define GOT_ERR_CONFLICTS 82 #define GOT_ERR_BRANCH_EXISTS 83 +#define GOT_ERR_MODIFIED 84 +#define GOT_ERR_NOT_REBASING 85 +#define GOT_ERR_EMPTY_REBASE 86 +#define GOT_ERR_REBASE_COMMITID 87 static const struct got_error { int code; @@ -189,6 +193,11 @@ static const struct got_error { { GOT_ERR_CONFLICTS, "work tree contains conflicted files; these " "conflicts must be resolved first" }, { GOT_ERR_BRANCH_EXISTS,"specified branch already exists" }, + { GOT_ERR_MODIFIED, "work tree contains local changes; these " + "changes must be committed or reverted first" }, + { GOT_ERR_NOT_REBASING, "rebase operation not in progress" }, + { GOT_ERR_EMPTY_REBASE, "no commits to rebase" }, + { GOT_ERR_REBASE_COMMITID,"rebase commit ID mismatch" }, }; /* blob - 2dd6e8cfae54c6bc3ebc6d27f919785066d57529 blob + c61b327efec88778ab8c25f0964c86989d6075bb --- include/got_worktree.h +++ include/got_worktree.h @@ -16,6 +16,7 @@ struct got_worktree; struct got_commitable; +struct got_commit_object; /* status codes */ #define GOT_STATUS_NO_CHANGE ' ' @@ -208,3 +209,66 @@ const char *got_commitable_get_path(struct got_commita /* Get the status of a commitable worktree item. */ unsigned int got_commitable_get_status(struct got_commitable *); + +/* + * Prepare for rebasing a branch onto the work tree's current branch. + * This function creates references to a temporary branch, the branch + * being rebased, and the work tree's current branch, under the + * "got/worktree/rebase/" namespace. These references are used to + * keep track of rebase operation state and are used as input and/or + * output arguments with other rebase-related functions. + */ +const struct got_error *got_worktree_rebase_prepare(struct got_reference **, + struct got_reference **, struct got_worktree *, struct got_reference *, + struct got_repository *); + +/* + * Continue an interrupted rebase operation. + * This function returns existing references created when rebase was prepared, + * and the ID of the commit currently being rebased. This should be called + * before either resuming or aborting a rebase operation. + */ +const struct got_error *got_worktree_rebase_continue(struct got_object_id **, + struct got_reference **, struct got_reference **, struct got_reference **, + struct got_worktree *, struct got_repository *); + +/* Check whether a, potentially interrupted, rebase operation is in progress. */ +const struct got_error *got_worktree_rebase_in_progress(int *, + struct got_worktree *); + +/* + * Merge changes from the commit currently being rebased into the work tree. + * Report affected files, including merge conflicts, via the specified + * progress callback. + */ +const struct got_error *got_worktree_rebase_merge_files( + struct got_worktree *, struct got_object_id *, struct got_object_id *, + struct got_repository *, got_worktree_checkout_cb, void *, + got_worktree_cancel_cb, void *); + +/* + * Commit merged rebased changes to a temporary branch and return the + * ID of the newly created commit. + */ +const struct got_error *got_worktree_rebase_commit(struct got_object_id **, + struct got_worktree *, struct got_reference *, struct got_commit_object *, + struct got_object_id *, struct got_repository *); + +/* Postpone the rebase operation. Should be called after a merge conflict. */ +const struct got_error *got_worktree_rebase_postpone(struct got_worktree *); + +/* + * Complete the current rebase operation. This should be called once all + * commits have been rebased successfully. + */ +const struct got_error *got_worktree_rebase_complete(struct got_worktree *, + struct got_reference *, struct got_reference *, struct got_reference *, + struct got_repository *); + +/* + * Abort the current rebase operation. + * Report reverted files via the specified progress callback. + */ +const struct got_error *got_worktree_rebase_abort(struct got_worktree *, + struct got_repository *, struct got_reference *, + got_worktree_checkout_cb, void *); blob - df3ae1a5f75b418b82f1861704326ed50c7b09b7 blob + 05aa9d3b001104a9b0383a1b83f76f77924f6916 --- lib/commit_graph.c +++ lib/commit_graph.c @@ -473,7 +473,8 @@ fetch_commits_from_open_branches(int *nfetched, int i, ntips; *nfetched = 0; - *changed_id = NULL; + if (changed_id) + *changed_id = NULL; ntips = got_object_idset_num_elements(graph->open_branches); if (ntips == 0) @@ -517,7 +518,7 @@ fetch_commits_from_open_branches(int *nfetched, commit, repo); if (err) break; - if (changed && *changed_id == NULL) + if (changed && changed_id && *changed_id == NULL) *changed_id = commit_id; } done: @@ -578,8 +579,16 @@ got_commit_graph_iter_start(struct got_commit_graph *g int changed; start_node = got_object_idset_get(graph->node_ids, id); - if (start_node == NULL) - return got_error_no_obj(id); + while (start_node == NULL) { + int ncommits; + err = fetch_commits_from_open_branches(&ncommits, NULL, graph, + repo); + if (err) + return err; + if (ncommits == 0) + return got_error_no_obj(id); + start_node = got_object_idset_get(graph->node_ids, id); + } err = got_object_open_as_commit(&commit, repo, &start_node->id); if (err) blob - 9809abfda0e41da7cba9bee90a6e329ed1c7f01f blob + 317d0fdf6a3b2bf43047f3743caed1b085199df0 --- lib/got_lib_worktree.h +++ lib/got_lib_worktree.h @@ -65,3 +65,15 @@ struct got_commitable { const struct got_error *got_worktree_get_base_ref_name(char **, struct got_worktree *worktree); + +/* Temporary branch which accumulates commits during a rebase operation. */ +#define GOT_WORKTREE_REBASE_TMP_REF_PREFIX "refs/got/worktree/rebase/tmp" + +/* Symbolic reference pointing at the name of the new base branch. */ +#define GOT_WORKTREE_NEWBASE_REF_PREFIX "refs/got/worktree/rebase/newbase" + +/* Symbolic reference pointing at the name of the branch being rebased. */ +#define GOT_WORKTREE_REBASE_BRANCH_REF_PREFIX "refs/got/worktree/rebase/branch" + +/* Reference pointing at the ID of the current commit being rebased. */ +#define GOT_WORKTREE_REBASE_COMMIT_REF_PREFIX "refs/got/worktree/rebase/commit" blob - 6c06691116548c4c4bf246510416f6568ebfdb71 blob + 4d551f4bea130a721ce5deddee5d882ebf873ec1 --- lib/worktree.c +++ lib/worktree.c @@ -725,6 +725,7 @@ static const struct got_error * merge_blob(int *local_changes_subsumed, struct got_worktree *worktree, struct got_blob_object *blob_orig, const char *ondisk_path, const char *path, uint16_t st_mode, struct got_blob_object *blob_deriv, + struct got_object_id *deriv_base_commit_id, struct got_repository *repo, got_worktree_checkout_cb progress_cb, void *progress_arg) { @@ -734,7 +735,7 @@ merge_blob(int *local_changes_subsumed, struct got_wor char *blob_deriv_path = NULL, *blob_orig_path = NULL; char *merged_path = NULL, *base_path = NULL; char *id_str = NULL; - char *label1 = NULL; + char *label_deriv = NULL; int overlapcnt = 0; char *parent; @@ -789,16 +790,16 @@ merge_blob(int *local_changes_subsumed, struct got_wor */ } - err = got_object_id_str(&id_str, worktree->base_commit_id); + err = got_object_id_str(&id_str, deriv_base_commit_id); if (err) goto done; - if (asprintf(&label1, "commit %s", id_str) == -1) { + if (asprintf(&label_deriv, "commit %s", id_str) == -1) { err = got_error_from_errno("asprintf"); goto done; } err = got_merge_diff3(&overlapcnt, merged_fd, blob_deriv_path, - blob_orig_path, ondisk_path, label1, path); + blob_orig_path, ondisk_path, label_deriv, path); if (err) goto done; @@ -848,7 +849,7 @@ done: free(blob_orig_path); } free(id_str); - free(label1); + free(label_deriv); return err; } @@ -1183,7 +1184,8 @@ update_blob(struct got_worktree *worktree, goto done; } err = merge_blob(&update_timestamps, worktree, blob2, - ondisk_path, path, sb.st_mode, blob, repo, + ondisk_path, path, sb.st_mode, blob, + worktree->base_commit_id, repo, progress_cb, progress_arg); if (blob2) got_object_blob_close(blob2); @@ -1348,8 +1350,8 @@ diff_new(void *arg, struct got_tree_entry *te, const c return err; } -const struct got_error * -got_worktree_get_base_ref_name(char **refname, struct got_worktree *worktree) +static const struct got_error * +get_ref_name(char **refname, struct got_worktree *worktree, const char *prefix) { const struct got_error *err = NULL; char *uuidstr = NULL; @@ -1361,7 +1363,7 @@ got_worktree_get_base_ref_name(char **refname, struct if (uuid_status != uuid_s_ok) return got_error_uuid(uuid_status); - if (asprintf(refname, "%s-%s", GOT_WORKTREE_BASE_REF_PREFIX, uuidstr) + if (asprintf(refname, "%s-%s", prefix, uuidstr) == -1) { err = got_error_from_errno("asprintf"); *refname = NULL; @@ -1370,6 +1372,40 @@ got_worktree_get_base_ref_name(char **refname, struct return err; } +const struct got_error * +got_worktree_get_base_ref_name(char **refname, struct got_worktree *worktree) +{ + return get_ref_name(refname, worktree, GOT_WORKTREE_BASE_REF_PREFIX); +} + +static const struct got_error * +get_rebase_tmp_ref_name(char **refname, struct got_worktree *worktree) +{ + return get_ref_name(refname, worktree, + GOT_WORKTREE_REBASE_TMP_REF_PREFIX); +} + +static const struct got_error * +get_newbase_symref_name(char **refname, struct got_worktree *worktree) +{ + return get_ref_name(refname, worktree, GOT_WORKTREE_NEWBASE_REF_PREFIX); +} + +static const struct got_error * +get_rebase_branch_symref_name(char **refname, struct got_worktree *worktree) +{ + return get_ref_name(refname, worktree, + GOT_WORKTREE_REBASE_BRANCH_REF_PREFIX); +} + +static const struct got_error * +get_rebase_commit_ref_name(char **refname, struct got_worktree *worktree) +{ + return get_ref_name(refname, worktree, + GOT_WORKTREE_REBASE_COMMIT_REF_PREFIX); +} + + /* * Prevent Git's garbage collector from deleting our base commit by * setting a reference to our base commit's ID. @@ -1660,6 +1696,7 @@ struct merge_file_cb_arg { void *progress_arg; got_worktree_cancel_cb cancel_cb; void *cancel_arg; + struct got_object_id *commit_id2; }; static const struct got_error * @@ -1706,7 +1743,7 @@ merge_file_cb(void *arg, struct got_blob_object *blob1 } err = merge_blob(&local_changes_subsumed, a->worktree, blob1, - ondisk_path, path2, sb.st_mode, blob2, repo, + ondisk_path, path2, sb.st_mode, blob2, a->commit_id2, repo, a->progress_cb, a->progress_arg); } else if (blob1) { ie = got_fileindex_entry_get(a->fileindex, path1); @@ -1772,7 +1809,8 @@ merge_file_cb(void *arg, struct got_blob_object *blob1 goto done; } err = merge_blob(&local_changes_subsumed, a->worktree, - NULL, ondisk_path, path2, sb.st_mode, blob2, repo, + NULL, ondisk_path, path2, sb.st_mode, blob2, + a->commit_id2, repo, a->progress_cb, a->progress_arg); if (status == GOT_STATUS_DELETE) { err = update_blob_fileindex_entry(a->worktree, @@ -1839,35 +1877,18 @@ check_merge_ok(void *arg, struct got_fileindex_entry * return NULL; } -const struct got_error * -got_worktree_merge_files(struct got_worktree *worktree, - struct got_object_id *commit_id1, struct got_object_id *commit_id2, - struct got_repository *repo, got_worktree_checkout_cb progress_cb, - void *progress_arg, got_worktree_cancel_cb cancel_cb, void *cancel_arg) +static const struct got_error * +merge_files(struct got_worktree *worktree, struct got_fileindex *fileindex, + const char *fileindex_path, struct got_object_id *commit_id1, + struct got_object_id *commit_id2, struct got_repository *repo, + got_worktree_checkout_cb progress_cb, void *progress_arg, + got_worktree_cancel_cb cancel_cb, void *cancel_arg) { - const struct got_error *err = NULL, *sync_err, *unlockerr; + const struct got_error *err = NULL, *sync_err; struct got_object_id *tree_id1 = NULL, *tree_id2 = NULL; struct got_tree_object *tree1 = NULL, *tree2 = NULL; struct merge_file_cb_arg arg; - char *fileindex_path = NULL; - struct got_fileindex *fileindex = NULL; - struct check_merge_ok_arg mok_arg; - - err = lock_worktree(worktree, LOCK_EX); - if (err) - return err; - - err = open_fileindex(&fileindex, &fileindex_path, worktree); - if (err) - goto done; - mok_arg.worktree = worktree; - mok_arg.repo = repo; - err = got_fileindex_for_each_entry_safe(fileindex, check_merge_ok, - &mok_arg); - if (err) - goto done; - if (commit_id1) { err = got_object_id_by_path(&tree_id1, repo, commit_id1, worktree->path_prefix); @@ -1894,17 +1915,51 @@ got_worktree_merge_files(struct got_worktree *worktree arg.progress_arg = progress_arg; arg.cancel_cb = cancel_cb; arg.cancel_arg = cancel_arg; + arg.commit_id2 = commit_id2; err = got_diff_tree(tree1, tree2, "", "", repo, merge_file_cb, &arg); sync_err = sync_fileindex(fileindex, fileindex_path); if (sync_err && err == NULL) err = sync_err; done: - got_fileindex_free(fileindex); if (tree1) got_object_tree_close(tree1); if (tree2) got_object_tree_close(tree2); + return err; +} +const struct got_error * +got_worktree_merge_files(struct got_worktree *worktree, + struct got_object_id *commit_id1, struct got_object_id *commit_id2, + struct got_repository *repo, got_worktree_checkout_cb progress_cb, + void *progress_arg, got_worktree_cancel_cb cancel_cb, void *cancel_arg) +{ + const struct got_error *err, *unlockerr; + char *fileindex_path = NULL; + struct got_fileindex *fileindex = NULL; + struct check_merge_ok_arg mok_arg; + + err = lock_worktree(worktree, LOCK_EX); + if (err) + return err; + + err = open_fileindex(&fileindex, &fileindex_path, worktree); + if (err) + goto done; + + mok_arg.worktree = worktree; + mok_arg.repo = repo; + err = got_fileindex_for_each_entry_safe(fileindex, check_merge_ok, + &mok_arg); + if (err) + goto done; + + err = merge_files(worktree, fileindex, fileindex_path, commit_id1, + commit_id2, repo, progress_cb, progress_arg, cancel_cb, cancel_arg); +done: + if (fileindex) + got_fileindex_free(fileindex); + free(fileindex_path); unlockerr = lock_worktree(worktree, LOCK_SH); if (unlockerr && err == NULL) err = unlockerr; @@ -3399,4 +3454,525 @@ unsigned int got_commitable_get_status(struct got_commitable *ct) { return ct->status; +} + +struct check_rebase_ok_arg { + struct got_worktree *worktree; + struct got_repository *repo; + int rebase_in_progress; +}; + +static const struct got_error * +check_rebase_ok(void *arg, struct got_fileindex_entry *ie) +{ + const struct got_error *err = NULL; + struct check_rebase_ok_arg *a = arg; + unsigned char status; + struct stat sb; + char *ondisk_path; + + if (!a->rebase_in_progress) { + /* Reject rebase of a work tree with mixed base commits. */ + if (memcmp(ie->commit_sha1, a->worktree->base_commit_id->sha1, + SHA1_DIGEST_LENGTH)) + return got_error(GOT_ERR_MIXED_COMMITS); + } + + if (asprintf(&ondisk_path, "%s/%s", a->worktree->root_path, ie->path) + == -1) + return got_error_from_errno("asprintf"); + + /* Reject rebase of a work tree with modified or conflicted files. */ + err = get_file_status(&status, &sb, ie, ondisk_path, a->repo); + free(ondisk_path); + if (err) + return err; + + if (a->rebase_in_progress) { + if (status == GOT_STATUS_CONFLICT) + return got_error(GOT_ERR_CONFLICTS); + } else if (status != GOT_STATUS_NO_CHANGE) + return got_error(GOT_ERR_MODIFIED); + + return NULL; +} + +const struct got_error * +got_worktree_rebase_prepare(struct got_reference **new_base_branch_ref, + struct got_reference **tmp_branch, struct got_worktree *worktree, + struct got_reference *branch, struct got_repository *repo) +{ + const struct got_error *err = NULL; + char *tmp_branch_name = NULL, *new_base_branch_ref_name = NULL; + char *branch_ref_name = NULL; + struct got_fileindex *fileindex = NULL; + char *fileindex_path = NULL; + struct check_rebase_ok_arg ok_arg; + struct got_reference *wt_branch = NULL, *branch_ref = NULL; + + *new_base_branch_ref = NULL; + *tmp_branch = NULL; + + err = lock_worktree(worktree, LOCK_EX); + if (err) + return err; + + err = open_fileindex(&fileindex, &fileindex_path, worktree); + if (err) + goto done; + + ok_arg.worktree = worktree; + ok_arg.repo = repo; + ok_arg.rebase_in_progress = 0; + err = got_fileindex_for_each_entry_safe(fileindex, check_rebase_ok, + &ok_arg); + if (err) + goto done; + + err = get_rebase_tmp_ref_name(&tmp_branch_name, worktree); + if (err) + goto done; + + err = get_newbase_symref_name(&new_base_branch_ref_name, worktree); + if (err) + goto done; + + err = get_rebase_branch_symref_name(&branch_ref_name, worktree); + if (err) + goto done; + + err = got_ref_open(&wt_branch, repo, worktree->head_ref_name, + 0); + if (err) + goto done; + + err = got_ref_alloc_symref(new_base_branch_ref, + new_base_branch_ref_name, wt_branch); + if (err) + goto done; + err = got_ref_write(*new_base_branch_ref, repo); + if (err) + goto done; + + /* TODO Lock original branch's ref while rebasing? */ + + err = got_ref_alloc_symref(&branch_ref, branch_ref_name, branch); + if (err) + goto done; + + err = got_ref_write(branch_ref, repo); + if (err) + goto done; + + err = got_ref_alloc(tmp_branch, tmp_branch_name, + worktree->base_commit_id); + if (err) + goto done; + err = got_ref_write(*tmp_branch, repo); + if (err) + goto done; + + err = got_worktree_set_head_ref(worktree, *tmp_branch); + if (err) + goto done; +done: + free(fileindex_path); + if (fileindex) + got_fileindex_free(fileindex); + free(tmp_branch_name); + free(new_base_branch_ref_name); + free(branch_ref_name); + if (branch_ref) + got_ref_close(branch_ref); + if (wt_branch) + got_ref_close(wt_branch); + if (err) { + if (*new_base_branch_ref) { + got_ref_close(*new_base_branch_ref); + *new_base_branch_ref = NULL; + } + if (*tmp_branch) { + got_ref_close(*tmp_branch); + *tmp_branch = NULL; + } + lock_worktree(worktree, LOCK_SH); + } + return err; +} + +const struct got_error * +got_worktree_rebase_continue(struct got_object_id **commit_id, + struct got_reference **new_base_branch, struct got_reference **tmp_branch, + struct got_reference **branch, struct got_worktree *worktree, + struct got_repository *repo) +{ + const struct got_error *err; + char *commit_ref_name = NULL, *new_base_branch_ref_name = NULL; + char *tmp_branch_name = NULL, *branch_ref_name = NULL; + struct got_reference *commit_ref = NULL, *branch_ref = NULL; + + *commit_id = NULL; + + err = get_rebase_tmp_ref_name(&tmp_branch_name, worktree); + if (err) + return err; + + err = get_rebase_branch_symref_name(&branch_ref_name, worktree); + if (err) + goto done; + + err = get_rebase_commit_ref_name(&commit_ref_name, worktree); + if (err) + goto done; + + err = get_newbase_symref_name(&new_base_branch_ref_name, worktree); + if (err) + goto done; + + err = got_ref_open(&branch_ref, repo, branch_ref_name, 0); + if (err) + goto done; + + err = got_ref_open(branch, repo, + got_ref_get_symref_target(branch_ref), 0); + if (err) + goto done; + + err = got_ref_open(&commit_ref, repo, commit_ref_name, 0); + if (err) + goto done; + + err = got_ref_resolve(commit_id, repo, commit_ref); + if (err) + goto done; + + err = got_ref_open(new_base_branch, repo, + new_base_branch_ref_name, 0); + if (err) + goto done; + + err = got_ref_open(tmp_branch, repo, tmp_branch_name, 0); + if (err) + goto done; +done: + free(commit_ref_name); + free(branch_ref_name); + if (commit_ref) + got_ref_close(commit_ref); + if (branch_ref) + got_ref_close(branch_ref); + if (err) { + free(*commit_id); + *commit_id = NULL; + if (*tmp_branch) { + got_ref_close(*tmp_branch); + *tmp_branch = NULL; + } + if (*new_base_branch) { + got_ref_close(*new_base_branch); + *new_base_branch = NULL; + } + if (*branch) { + got_ref_close(*branch); + *branch = NULL; + } + } + return err; } + +const struct got_error * +got_worktree_rebase_in_progress(int *in_progress, struct got_worktree *worktree) +{ + const struct got_error *err; + char *tmp_branch_name = NULL; + + err = get_rebase_tmp_ref_name(&tmp_branch_name, worktree); + if (err) + return err; + + *in_progress = (strcmp(tmp_branch_name, worktree->head_ref_name) == 0); + free(tmp_branch_name); + return NULL; +} + +static const struct got_error * +collect_rebase_commit_msg(struct got_pathlist_head *commitable_paths, + char **logmsg, void *arg) +{ + struct got_commit_object *commit = arg; + + *logmsg = strdup(got_object_commit_get_logmsg(commit)); + if (*logmsg == NULL) + return got_error_from_errno("strdup"); + + return NULL; +} + +static const struct got_error * +rebase_status(void *arg, unsigned char status, const char *path, + struct got_object_id *blob_id, struct got_object_id *commit_id) +{ + return NULL; +} + +const struct got_error * +got_worktree_rebase_merge_files(struct got_worktree *worktree, + struct got_object_id *parent_commit_id, struct got_object_id *commit_id, + struct got_repository *repo, got_worktree_checkout_cb progress_cb, + void *progress_arg, got_worktree_cancel_cb cancel_cb, void *cancel_arg) +{ + const struct got_error *err; + struct got_fileindex *fileindex; + char *fileindex_path, *commit_ref_name = NULL; + struct got_reference *commit_ref = NULL; + + /* Work tree is locked/unlocked during rebase prepartion/teardown. */ + + err = open_fileindex(&fileindex, &fileindex_path, worktree); + if (err) + return err; + + err = get_rebase_commit_ref_name(&commit_ref_name, worktree); + if (err) + goto done; + err = got_ref_open(&commit_ref, repo, commit_ref_name, 0); + if (err) { + if (err->code != GOT_ERR_NOT_REF) + goto done; + err = got_ref_alloc(&commit_ref, commit_ref_name, commit_id); + if (err) + goto done; + err = got_ref_write(commit_ref, repo); + if (err) + goto done; + } else { + struct got_object_id *stored_id; + int cmp; + + err = got_ref_resolve(&stored_id, repo, commit_ref); + if (err) + goto done; + cmp = got_object_id_cmp(commit_id, stored_id); + free(stored_id); + if (cmp != 0) { + err = got_error(GOT_ERR_REBASE_COMMITID); + goto done; + } + } + + err = merge_files(worktree, fileindex, fileindex_path, + parent_commit_id, commit_id, repo, progress_cb, progress_arg, + cancel_cb, cancel_arg); +done: + got_fileindex_free(fileindex); + free(fileindex_path); + if (commit_ref) + got_ref_close(commit_ref); + return err; +} + +const struct got_error * +got_worktree_rebase_commit(struct got_object_id **new_commit_id, + struct got_worktree *worktree, struct got_reference *tmp_branch, + struct got_commit_object *orig_commit, + struct got_object_id *orig_commit_id, struct got_repository *repo) +{ + const struct got_error *err; + char *commit_ref_name = NULL; + struct got_reference *commit_ref = NULL; + struct got_object_id *commit_id = NULL; + + /* Work tree is locked/unlocked during rebase prepartion/teardown. */ + + err = get_rebase_commit_ref_name(&commit_ref_name, worktree); + if (err) + goto done; + err = got_ref_open(&commit_ref, repo, commit_ref_name, 0); + if (err) + goto done; + err = got_ref_resolve(&commit_id, repo, commit_ref); + if (err) + goto done; + if (got_object_id_cmp(commit_id, orig_commit_id) != 0) { + err = got_error(GOT_ERR_REBASE_COMMITID); + goto done; + } + + err = got_worktree_commit(new_commit_id, worktree, NULL, + got_object_commit_get_author(orig_commit), + got_object_commit_get_committer(orig_commit), + collect_rebase_commit_msg, orig_commit, + rebase_status, NULL, repo); + if (err) + goto done; + + err = got_ref_delete(commit_ref, repo); + if (err) + goto done; + + err = got_ref_change_ref(tmp_branch, *new_commit_id); +done: + free(commit_ref_name); + if (commit_ref) + got_ref_close(commit_ref); + if (err) { + free(*new_commit_id); + *new_commit_id = NULL; + } + return err; +} + +const struct got_error * +got_worktree_rebase_postpone(struct got_worktree *worktree) +{ + return lock_worktree(worktree, LOCK_SH); +} + +const struct got_error * +got_worktree_rebase_complete(struct got_worktree *worktree, + struct got_reference *new_base_branch, struct got_reference *tmp_branch, + struct got_reference *rebased_branch, + struct got_repository *repo) +{ + const struct got_error *err, *unlockerr; + struct got_object_id *new_head_commit_id = NULL; + + err = got_ref_resolve(&new_head_commit_id, repo, tmp_branch); + if (err) + return err; + + err = got_ref_change_ref(rebased_branch, new_head_commit_id); + if (err) + goto done; + + err = got_ref_write(rebased_branch, repo); + if (err) + goto done; + + err = got_worktree_set_head_ref(worktree, rebased_branch); + if (err) + goto done; + + err = got_ref_delete(tmp_branch, repo); + if (err) + goto done; + + err = got_ref_delete(new_base_branch, repo); + if (err) + goto done; +done: + free(new_head_commit_id); + unlockerr = lock_worktree(worktree, LOCK_SH); + if (unlockerr && err == NULL) + err = unlockerr; + return err; +} + +struct collect_revertible_paths_arg { + struct got_pathlist_head *revertible_paths; + struct got_worktree *worktree; +}; + +static const struct got_error * +collect_revertible_paths(void *arg, unsigned char status, const char *relpath, + struct got_object_id *blob_id, struct got_object_id *commit_id) +{ + struct collect_revertible_paths_arg *a = arg; + const struct got_error *err = NULL; + struct got_pathlist_entry *new = NULL; + char *path = NULL; + + if (status != GOT_STATUS_ADD && + status != GOT_STATUS_DELETE && + status != GOT_STATUS_MODIFY && + status != GOT_STATUS_CONFLICT && + status != GOT_STATUS_MISSING) + return NULL; + + if (asprintf(&path, "%s/%s", a->worktree->root_path, relpath) == -1) + return got_error_from_errno("asprintf"); + + err = got_pathlist_insert(&new, a->revertible_paths, path, NULL); + if (err || new == NULL) + free(path); + return err; +} + +const struct got_error * +got_worktree_rebase_abort(struct got_worktree *worktree, + struct got_repository *repo, struct got_reference *new_base_branch, + got_worktree_checkout_cb progress_cb, void *progress_arg) +{ + const struct got_error *err, *unlockerr; + struct got_reference *resolved = NULL; + struct got_object_id *commit_id = NULL; + struct got_fileindex *fileindex = NULL; + char *fileindex_path = NULL; + struct got_pathlist_head revertible_paths; + struct got_pathlist_entry *pe; + struct collect_revertible_paths_arg crp_arg; + + TAILQ_INIT(&revertible_paths); + + err = lock_worktree(worktree, LOCK_EX); + if (err) + return err; + + err = open_fileindex(&fileindex, &fileindex_path, worktree); + if (err) + goto done; + + err = got_ref_open(&resolved, repo, + got_ref_get_symref_target(new_base_branch), 0); + if (err) + goto done; + + err = got_worktree_set_head_ref(worktree, resolved); + if (err) + goto done; + + /* + * XXX commits to the base branch could have happened while + * we were busy rebasing; should we store the original commit ID + * when rebase begins and read it back here? + */ + err = got_ref_resolve(&commit_id, repo, resolved); + if (err) + goto done; + + err = got_worktree_set_base_commit_id(worktree, repo, commit_id); + if (err) + goto done; + + err = got_worktree_checkout_files(worktree, "", repo, progress_cb, + progress_arg, NULL, NULL); + if (err) + goto done; + + crp_arg.revertible_paths = &revertible_paths; + crp_arg.worktree = worktree; + err = got_worktree_status(worktree, "", repo, + collect_revertible_paths, &crp_arg, NULL, NULL); + if (err) + goto done; + + TAILQ_FOREACH(pe, &revertible_paths, entry) { + err = revert_file(worktree, fileindex, pe->path, + progress_cb, progress_arg, repo); + if (err) + break; + } +done: + got_ref_close(resolved); + free(commit_id); + got_fileindex_free(fileindex); + free(fileindex_path); + TAILQ_FOREACH(pe, &revertible_paths, entry) + free((char *)pe->path); + got_pathlist_free(&revertible_paths); + + unlockerr = lock_worktree(worktree, LOCK_SH); + if (unlockerr && err == NULL) + err = unlockerr; + return err; +} blob - 55653a3b4b9d82d357392bf627327bc0efc63549 blob + cc53c4052b7de555d29b4d516d502908cf15873f --- regress/cmdline/Makefile +++ regress/cmdline/Makefile @@ -1,4 +1,5 @@ -REGRESS_TARGETS=checkout update status log add rm diff commit cherrypick backout +REGRESS_TARGETS=checkout update status log add rm diff commit \ + cherrypick backout rebase NOOBJ=Yes checkout: @@ -31,4 +32,7 @@ cherrypick: backout: ./backout.sh +rebase: + ./rebase.sh + .include blob - 4135c2b665cb9eaecfe1d9ed4d76cd51551b0d01 blob + 05565fef26b7848ff5d3ce99c04bb7ddf94780e2 --- regress/cmdline/common.sh +++ regress/cmdline/common.sh @@ -44,12 +44,32 @@ function git_show_head (cd $repo && git show --no-patch --pretty='format:%H') } +function git_show_parent_commit +{ + local repo="$1" + (cd $repo && git show --no-patch --pretty='format:%P') +} + function git_show_tree { local repo="$1" (cd $repo && git show --no-patch --pretty='format:%T') } +function trim_obj_id +{ + let trimcount=$1 + id=$2 + + pat="" + while [ trimcount -gt 0 ]; do + pat="[0-9a-f]$pat" + let trimcount-- + done + + echo ${id%$pat} +} + function git_commit_tree { local repo="$1" blob - /dev/null blob + ca79164bd6cb0593ca6f129a8ab8026fb8532b83 (mode 755) --- /dev/null +++ regress/cmdline/rebase.sh @@ -0,0 +1,393 @@ +#!/bin/sh +# +# Copyright (c) 2019 Stefan Sperling +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +. ./common.sh + +function test_rebase_basic { + local testroot=`test_init rebase_basic` + + (cd $testroot/repo && git checkout -q -b newbranch) + echo "modified delta on branch" > $testroot/repo/gamma/delta + git_commit $testroot/repo -m "committing to delta on newbranch" + + echo "modified alpha on branch" > $testroot/repo/alpha + (cd $testroot/repo && git rm -q beta) + echo "new file on branch" > $testroot/repo/epsilon/new + (cd $testroot/repo && git add epsilon/new) + git_commit $testroot/repo -m "committing more changes on newbranch" + + local orig_commit1=`git_show_parent_commit $testroot/repo` + local orig_commit2=`git_show_head $testroot/repo` + + (cd $testroot/repo && git checkout -q master) + echo "modified zeta on master" > $testroot/repo/epsilon/zeta + git_commit $testroot/repo -m "committing to zeta on master" + local master_commit=`git_show_head $testroot/repo` + + got checkout $testroot/repo $testroot/wt > /dev/null + ret="$?" + if [ "$ret" != "0" ]; then + test_done "$testroot" "$ret" + return 1 + fi + + (cd $testroot/wt && got rebase newbranch > $testroot/stdout) + + (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 short_orig_commit1=`trim_obj_id 28 $orig_commit1` + local short_orig_commit2=`trim_obj_id 28 $orig_commit2` + local short_new_commit1=`trim_obj_id 28 $new_commit1` + local short_new_commit2=`trim_obj_id 28 $new_commit2` + + echo "G gamma/delta" >> $testroot/stdout.expected + echo -n "$short_orig_commit1 -> $short_new_commit1" \ + >> $testroot/stdout.expected + echo ": committing to delta on newbranch" >> $testroot/stdout.expected + echo "G alpha" >> $testroot/stdout.expected + echo "D beta" >> $testroot/stdout.expected + echo "A epsilon/new" >> $testroot/stdout.expected + echo -n "$short_orig_commit2 -> $short_new_commit2" \ + >> $testroot/stdout.expected + echo ": committing more changes on newbranch" \ + >> $testroot/stdout.expected + echo "Switching work tree to refs/heads/newbranch" \ + >> $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" + return 1 + fi + + echo "modified delta on branch" > $testroot/content.expected + cat $testroot/wt/gamma/delta > $testroot/content + cmp -s $testroot/content.expected $testroot/content + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/content.expected $testroot/content + test_done "$testroot" "$ret" + return 1 + fi + + echo "modified alpha on branch" > $testroot/content.expected + cat $testroot/wt/alpha > $testroot/content + cmp -s $testroot/content.expected $testroot/content + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/content.expected $testroot/content + test_done "$testroot" "$ret" + return 1 + fi + + if [ -e $testroot/wt/beta ]; then + echo "removed file beta still exists on disk" >&2 + test_done "$testroot" "1" + return 1 + fi + + echo "new file on branch" > $testroot/content.expected + cat $testroot/wt/epsilon/new > $testroot/content + cmp -s $testroot/content.expected $testroot/content + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/content.expected $testroot/content + test_done "$testroot" "$ret" + return 1 + fi + + (cd $testroot/wt && got status > $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 + test_done "$testroot" "$ret" + return 1 + fi + + (cd $testroot/wt && got log -l3 | grep ^commit > $testroot/stdout) + echo "commit $new_commit2 (newbranch)" > $testroot/stdout.expected + echo "commit $new_commit1" >> $testroot/stdout.expected + echo "commit $master_commit (master)" >> $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" +} + +function test_rebase_ancestry_check { + local testroot=`test_init rebase_ancestry_check` + + got checkout $testroot/repo $testroot/wt > /dev/null + ret="$?" + if [ "$ret" != "0" ]; then + test_done "$testroot" "$ret" + return 1 + fi + + (cd $testroot/repo && git checkout -q -b newbranch) + echo "modified delta on branch" > $testroot/repo/gamma/delta + git_commit $testroot/repo -m "committing to delta on newbranch" + + (cd $testroot/wt && got rebase newbranch > $testroot/stdout \ + 2> $testroot/stderr) + + echo -n > $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" + return 1 + fi + + echo -n "got: specified branch resolves to a commit " \ + > $testroot/stderr.expected + echo "which is already contained in work tree's branch" \ + >> $testroot/stderr.expected + cmp -s $testroot/stderr.expected $testroot/stderr + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stderr.expected $testroot/stderr + fi + test_done "$testroot" "$ret" +} + +function test_rebase_continue { + local testroot=`test_init rebase_continue` + + (cd $testroot/repo && git checkout -q -b newbranch) + echo "modified alpha on branch" > $testroot/repo/alpha + git_commit $testroot/repo -m "committing to alpha on newbranch" + local orig_commit1=`git_show_head $testroot/repo` + + (cd $testroot/repo && git checkout -q master) + echo "modified alpha on master" > $testroot/repo/alpha + git_commit $testroot/repo -m "committing to alpha on master" + local master_commit=`git_show_head $testroot/repo` + + got checkout $testroot/repo $testroot/wt > /dev/null + ret="$?" + if [ "$ret" != "0" ]; then + test_done "$testroot" "$ret" + return 1 + fi + + (cd $testroot/wt && got rebase newbranch > $testroot/stdout \ + 2> $testroot/stderr) + + echo "C alpha" > $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" + return 1 + fi + + echo "got: conflicts must be resolved before rebase can be resumed" \ + > $testroot/stderr.expected + cmp -s $testroot/stderr.expected $testroot/stderr + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stderr.expected $testroot/stderr + test_done "$testroot" "$ret" + return 1 + fi + + echo "<<<<<<< commit $orig_commit1" > $testroot/content.expected + echo "modified alpha on branch" >> $testroot/content.expected + echo "=======" >> $testroot/content.expected + echo "modified alpha on master" >> $testroot/content.expected + echo '>>>>>>> alpha' >> $testroot/content.expected + cat $testroot/wt/alpha > $testroot/content + cmp -s $testroot/content.expected $testroot/content + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/content.expected $testroot/content + test_done "$testroot" "$ret" + return 1 + fi + + (cd $testroot/wt && got status > $testroot/stdout) + + echo "C alpha" > $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" + return 1 + fi + + # resolve the conflict + echo "modified alpha on branch and master" > $testroot/wt/alpha + + (cd $testroot/wt && got rebase -c > $testroot/stdout) + + (cd $testroot/repo && git checkout -q newbranch) + local new_commit1=`git_show_head $testroot/repo` + + local short_orig_commit1=`trim_obj_id 28 $orig_commit1` + local short_new_commit1=`trim_obj_id 28 $new_commit1` + + echo -n "$short_orig_commit1 -> $short_new_commit1" \ + > $testroot/stdout.expected + echo ": committing to alpha on newbranch" >> $testroot/stdout.expected + echo "Switching work tree to refs/heads/newbranch" \ + >> $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" + return 1 + fi + + + (cd $testroot/wt && got log -l2 | grep ^commit > $testroot/stdout) + echo "commit $new_commit1 (newbranch)" > $testroot/stdout.expected + echo "commit $master_commit (master)" >> $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" +} + +function test_rebase_abort { + local testroot=`test_init rebase_abort` + + local init_commit=`git_show_head $testroot/repo` + + (cd $testroot/repo && git checkout -q -b newbranch) + echo "modified alpha on branch" > $testroot/repo/alpha + git_commit $testroot/repo -m "committing to alpha on newbranch" + local orig_commit1=`git_show_head $testroot/repo` + + (cd $testroot/repo && git checkout -q master) + echo "modified alpha on master" > $testroot/repo/alpha + git_commit $testroot/repo -m "committing to alpha on master" + local master_commit=`git_show_head $testroot/repo` + + got checkout $testroot/repo $testroot/wt > /dev/null + ret="$?" + if [ "$ret" != "0" ]; then + test_done "$testroot" "$ret" + return 1 + fi + + (cd $testroot/wt && got rebase newbranch > $testroot/stdout \ + 2> $testroot/stderr) + + echo "C alpha" > $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" + return 1 + fi + + echo "got: conflicts must be resolved before rebase can be resumed" \ + > $testroot/stderr.expected + cmp -s $testroot/stderr.expected $testroot/stderr + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stderr.expected $testroot/stderr + test_done "$testroot" "$ret" + return 1 + fi + + echo "<<<<<<< commit $orig_commit1" > $testroot/content.expected + echo "modified alpha on branch" >> $testroot/content.expected + echo "=======" >> $testroot/content.expected + echo "modified alpha on master" >> $testroot/content.expected + echo '>>>>>>> alpha' >> $testroot/content.expected + cat $testroot/wt/alpha > $testroot/content + cmp -s $testroot/content.expected $testroot/content + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/content.expected $testroot/content + test_done "$testroot" "$ret" + return 1 + fi + + (cd $testroot/wt && got status > $testroot/stdout) + + echo "C alpha" > $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" + return 1 + fi + + (cd $testroot/wt && got rebase -a > $testroot/stdout) + + (cd $testroot/repo && git checkout -q newbranch) + + echo "Switching work tree to refs/heads/master" \ + > $testroot/stdout.expected + echo 'R alpha' >> $testroot/stdout.expected + echo "Rebase of refs/heads/newbranch aborted" \ + >> $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" + return 1 + fi + + echo "modified alpha on master" > $testroot/content.expected + cat $testroot/wt/alpha > $testroot/content + cmp -s $testroot/content.expected $testroot/content + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/content.expected $testroot/content + test_done "$testroot" "$ret" + return 1 + fi + + (cd $testroot/wt && got log -l3 -c newbranch \ + | grep ^commit > $testroot/stdout) + echo "commit $orig_commit1 (newbranch)" > $testroot/stdout.expected + echo "commit $init_commit" >> $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" +} + +run_test test_rebase_basic +run_test test_rebase_ancestry_check +run_test test_rebase_continue +run_test test_rebase_abort