commit 0ebf8283cd162a594e725ca2a1fd9f16e6ece6e4 from: Stefan Sperling date: Wed Jul 24 20:00:55 2019 UTC initial 'got histedit' implementation commit - f6794adc6b56432ea2c960ccef0b199d2441d395 commit + 0ebf8283cd162a594e725ca2a1fd9f16e6ece6e4 blob - 99596d6ea0e30d3b6d1eeaa28f571e10d4768f9a blob + 59507d1863a1ad983bf5f3e0811d54e3c511f8f5 --- got/got.1 +++ got/got.1 @@ -679,7 +679,101 @@ If this option is used, no further command-line argume .It Cm rb Short alias for .Cm rebase . +.It Cm histedit [ Fl a ] [ Fl c] [ Fl F Ar histedit-script ] +Edit commit history between the work tree's current base commit and +the tip commit of the work tree's current branch. +.Pp +Editing of commit history is controlled via a +.Ar histedit script +which can be edited interactively or passed on the command line. +The format of the histedit script is line-based. +Each line in the script begins with a command name, followed by +whitespace and an argument. +For most commands, the expected argument is a commit ID SHA1 hash. +Any remaining text on the line is ignored. +Lines which begin with the +.Sq # +character are ignored entirely. +.Pp +The available commands are as follows: +.Bl -column YXZ pick-commit +.It pick Ar commit Ta Use the specified commit as it is. +.It edit Ar commit Ta Use the specfified commit but once changes have been +merged into the work tree interrupt the histedit operation for amending. +.It fold Ar commit Ta Combine the specified commit with the next commit +listed further below that will be used. +.It drop Ar commit Ta Remove this commit from the edited history. +.It mesg Ar log-message Ta Use the specified single-line log message for +the commit on the previous line. +If the log message argument is left empty, open an editor where a new +log message can be written. +.El +.Pp +Every commit in the history being edited must be mentioned in histedit script. +Lines may be re-ordered to change the order of commits in the edited history. +.Pp +Edited commits are accumulated on a temporary branch. +Once history editing has completed successfully, the temporary branch becomes +the new version of the work tree's base branch and the work tree is +automatically switched to it. +.Pp +While merging 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 histedit operation is interrupted and may +be continued once conflicts have been resolved. +Alternatively, the histedit operation may be aborted which will leave +the work tree switched back to its original branch. +.Pp +If a merge conflict is resolved in a way which renders the merged +change into a no-op change, the corresponding commit will be elided +when the histedit operation continues. +.Pp +.Cm got histedit +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 first be +committed with +.Cm got commit +or reverted with +.Cm got revert . +If the edited history contains changes to files outside of the work tree's +path prefix, the work tree cannot be used to edit the history of this branch. +.Pp +The +.Cm got update +and +.Cm got commit +commands will refuse to run while a histedit operation is in progress. +Other commands which manipulate the work tree may be used for +conflict resolution purposes. +.Pp +The options for +.Cm got histedit +are as follows: +.Bl -tag -width Ds +.It Fl a +Abort an interrupted histedit operation. +If this option is used, no further command-line arguments are allowed. +.It Fl c +Continue an interrupted histedit operation. +If this option is used, no further command-line arguments are allowed. .El +.It Cm he +Short alias for +.Cm histedit . +.El .Sh ENVIRONMENT .Bl -tag -width GOT_AUTHOR .It Ev GOT_AUTHOR @@ -797,6 +891,14 @@ The patch can be mailed out for review and applied to .Pp .Dl $ got diff master unified-buffer-cache > /tmp/ubc.diff .Pp +Edit the entire commit history of the +.Dq unified-buffer-cache +branch: +.Pp +.Dl $ got update -b unified-buffer-cache +.Dl $ got update -c master +.Dl $ got histedit +.Pp .Sh SEE ALSO .Xr tog 1 , .Xr git-repository 5 , blob - 7d190467b8b7bb62264d404054183db585d304c5 blob + 3dd118c77e4b297b156ed437c7928b5f83cc302b --- got/got.c +++ got/got.c @@ -93,6 +93,7 @@ __dead static void usage_commit(void); __dead static void usage_cherrypick(void); __dead static void usage_backout(void); __dead static void usage_rebase(void); +__dead static void usage_histedit(void); static const struct got_error* cmd_init(int, char *[]); static const struct got_error* cmd_import(int, char *[]); @@ -112,6 +113,7 @@ 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 const struct got_error* cmd_histedit(int, char *[]); static struct got_cmd got_commands[] = { { "init", cmd_init, usage_init, "" }, @@ -132,6 +134,7 @@ static struct got_cmd got_commands[] = { { "cherrypick", cmd_cherrypick, usage_cherrypick, "cy" }, { "backout", cmd_backout, usage_backout, "bo" }, { "rebase", cmd_rebase, usage_rebase, "rb" }, + { "histedit", cmd_histedit, usage_histedit, "he" }, }; static void @@ -997,6 +1000,27 @@ switch_head_ref(struct got_reference *head_ref, got_worktree_get_head_ref_name(worktree)); printf("Switching work tree from %s to %s\n", base_id_str, got_worktree_get_head_ref_name(worktree)); + return NULL; +} + +static const struct got_error * +check_rebase_or_histedit_in_progress(struct got_worktree *worktree) +{ + const struct got_error *err; + int in_progress; + + err = got_worktree_rebase_in_progress(&in_progress, worktree); + if (err) + return err; + if (in_progress) + return got_error(GOT_ERR_REBASING); + + err = got_worktree_histedit_in_progress(&in_progress, worktree); + if (err) + return err; + if (in_progress) + return got_error(GOT_ERR_HISTEDIT_BUSY); + return NULL; } @@ -1011,7 +1035,7 @@ cmd_update(int argc, char *argv[]) char *commit_id_str = NULL; const char *branch_name = NULL; struct got_reference *head_ref = NULL; - int ch, did_something = 0, rebase_in_progress; + int ch, did_something = 0; while ((ch = getopt(argc, argv, "b:c:")) != -1) { switch (ch) { @@ -1046,13 +1070,9 @@ cmd_update(int argc, char *argv[]) if (error) goto done; - error = got_worktree_rebase_in_progress(&rebase_in_progress, worktree); + error = check_rebase_or_histedit_in_progress(worktree); if (error) - goto done; - if (rebase_in_progress) { - error = got_error(GOT_ERR_REBASING); goto done; - } if (argc == 0) { path = strdup(""); @@ -3139,7 +3159,7 @@ cmd_commit(int argc, char *argv[]) const char *got_author = getenv("GOT_AUTHOR"); struct collect_commit_logmsg_arg cl_arg; char *editor = NULL; - int ch, rebase_in_progress; + int ch; while ((ch = getopt(argc, argv, "m:")) != -1) { switch (ch) { @@ -3185,13 +3205,9 @@ cmd_commit(int argc, char *argv[]) if (error) goto done; - error = got_worktree_rebase_in_progress(&rebase_in_progress, worktree); + error = check_rebase_or_histedit_in_progress(worktree); if (error) - goto done; - if (rebase_in_progress) { - error = got_error(GOT_ERR_REBASING); goto done; - } error = got_repo_open(&repo, got_worktree_get_repo_path(worktree)); if (error != NULL) @@ -3481,6 +3497,39 @@ usage_rebase(void) fprintf(stderr, "usage: %s rebase [-a] | [-c] | branch\n", getprogname()); exit(1); +} + +void +trim_logmsg(char *logmsg, int limit) +{ + char *nl; + size_t len; + + len = strlen(logmsg); + if (len > limit) + len = limit; + logmsg[len] = '\0'; + nl = strchr(logmsg, '\n'); + if (nl) + *nl = '\0'; +} + +static const struct got_error * +get_short_logmsg(char **logmsg, int limit, struct got_commit_object *commit) +{ + const char *logmsg0 = NULL; + + logmsg0 = got_object_commit_get_logmsg(commit); + + while (isspace((unsigned char)logmsg0[0])) + logmsg0++; + + *logmsg = strdup(logmsg0); + if (*logmsg == NULL) + return got_error_from_errno("strdup"); + + trim_logmsg(*logmsg, limit); + return NULL; } static const struct got_error * @@ -3488,9 +3537,7 @@ 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; + char *old_id_str = NULL, *new_id_str = NULL, *logmsg = NULL; err = got_object_id_str(&old_id_str, old_id); if (err) @@ -3502,32 +3549,19 @@ show_rebase_progress(struct got_commit_object *commit, 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((unsigned char)logmsg[0])) - logmsg++; - old_id_str[12] = '\0'; if (new_id_str) new_id_str[12] = '\0'; - len = strlen(logmsg); - if (len > 42) - len = 42; - logmsg[len] = '\0'; - nl = strchr(logmsg, '\n'); - if (nl) - *nl = '\0'; + + err = get_short_logmsg(&logmsg, 42, commit); + if (err) + goto done; + printf("%s -> %s: %s\n", old_id_str, new_id_str ? new_id_str : "no-op change", logmsg); done: free(old_id_str); free(new_id_str); - free(logmsg0); return err; } @@ -3560,7 +3594,7 @@ rebase_complete(struct got_worktree *worktree, struct static const struct got_error * rebase_commit(struct got_pathlist_head *merged_paths, struct got_worktree *worktree, struct got_reference *tmp_branch, - struct got_object_id *commit_id, struct got_repository *repo) + struct got_object_id *commit_id, struct got_repository *repo) { const struct got_error *error; struct got_commit_object *commit; @@ -3934,4 +3968,991 @@ done: if (repo) got_repo_close(repo); return error; +} + +__dead static void +usage_histedit(void) +{ + fprintf(stderr, "usage: %s histedit [-a] [-c] [-F path]\n", + getprogname()); + exit(1); +} + +#define GOT_HISTEDIT_PICK 'p' +#define GOT_HISTEDIT_EDIT 'e' +#define GOT_HISTEDIT_FOLD 'f' +#define GOT_HISTEDIT_DROP 'd' +#define GOT_HISTEDIT_MESG 'm' + +static struct got_histedit_cmd { + unsigned char code; + const char *name; + const char *desc; +} got_histedit_cmds[] = { + { GOT_HISTEDIT_PICK, "pick", "use commit" }, + { GOT_HISTEDIT_EDIT, "edit", "use commit but stop for amending" }, + { GOT_HISTEDIT_FOLD, "fold", "combine with commit below" }, + { GOT_HISTEDIT_DROP, "drop", "remove commit from history" }, + { GOT_HISTEDIT_MESG, "mesg", + "single-line log message for commit above (open editor if empty)" }, +}; + +struct got_histedit_list_entry { + TAILQ_ENTRY(got_histedit_list_entry) entry; + struct got_object_id *commit_id; + const struct got_histedit_cmd *cmd; + char *logmsg; +}; +TAILQ_HEAD(got_histedit_list, got_histedit_list_entry); + +static const struct got_error * +histedit_write_commit(struct got_object_id *commit_id, const char *cmdname, + FILE *f, struct got_repository *repo) +{ + const struct got_error *err = NULL; + char *logmsg = NULL, *id_str = NULL; + struct got_commit_object *commit = NULL; + size_t n; + + err = got_object_open_as_commit(&commit, repo, commit_id); + if (err) + goto done; + + err = get_short_logmsg(&logmsg, 34, commit); + if (err) + goto done; + + err = got_object_id_str(&id_str, commit_id); + if (err) + goto done; + + n = fprintf(f, "%s %s %s\n", cmdname, id_str, logmsg); + if (n < 0) + err = got_ferror(f, GOT_ERR_IO); +done: + if (commit) + got_object_commit_close(commit); + free(id_str); + free(logmsg); + return err; +} + +static const struct got_error * +histedit_write_commit_list(struct got_object_id_queue *commits, FILE *f, + struct got_repository *repo) +{ + const struct got_error *err = NULL; + struct got_object_qid *qid; + + if (SIMPLEQ_EMPTY(commits)) + return got_error(GOT_ERR_EMPTY_HISTEDIT); + + SIMPLEQ_FOREACH(qid, commits, entry) { + err = histedit_write_commit(qid->id, got_histedit_cmds[0].name, + f, repo); + if (err) + break; + } + + return err; +} + +static const struct got_error * +write_cmd_list(FILE *f) +{ + const struct got_error *err = NULL; + int n, i; + + n = fprintf(f, "# Available histedit commands:\n"); + if (n < 0) + return got_ferror(f, GOT_ERR_IO); + + for (i = 0; i < nitems(got_histedit_cmds); i++) { + struct got_histedit_cmd *cmd = &got_histedit_cmds[i]; + n = fprintf(f, "# %s (%c): %s\n", cmd->name, cmd->code, + cmd->desc); + if (n < 0) { + err = got_ferror(f, GOT_ERR_IO); + break; + } + } + n = fprintf(f, "# Commits will be processed in order from top to " + "bottom of this file.\n"); + if (n < 0) + return got_ferror(f, GOT_ERR_IO); + return err; +} + +static const struct got_error * +histedit_syntax_error(int lineno) +{ + static char msg[42]; + int ret; + + ret = snprintf(msg, sizeof(msg), "histedit syntax error on line %d", + lineno); + if (ret == -1 || ret >= sizeof(msg)) + return got_error(GOT_ERR_HISTEDIT_SYNTAX); + + return got_error_msg(GOT_ERR_HISTEDIT_SYNTAX, msg); +} + +static const struct got_error * +append_folded_commit_msg(char **new_msg, struct got_histedit_list_entry *hle, + char *logmsg, struct got_repository *repo) +{ + const struct got_error *err; + struct got_commit_object *folded_commit = NULL; + char *id_str; + + err = got_object_id_str(&id_str, hle->commit_id); + if (err) + return err; + + err = got_object_open_as_commit(&folded_commit, repo, hle->commit_id); + if (err) + goto done; + + if (asprintf(new_msg, "%s%s# log message of folded commit %s: %s", + logmsg ? logmsg : "", logmsg ? "\n" : "", id_str, + got_object_commit_get_logmsg(folded_commit)) == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } +done: + if (folded_commit) + got_object_commit_close(folded_commit); + free(id_str); + return err; +} + +static struct got_histedit_list_entry * +get_folded_commits(struct got_histedit_list_entry *hle) +{ + struct got_histedit_list_entry *prev, *folded = NULL; + + prev = TAILQ_PREV(hle, got_histedit_list, entry); + while (prev && prev->cmd->code == GOT_HISTEDIT_FOLD) { + folded = prev; + prev = TAILQ_PREV(prev, got_histedit_list, entry); + } + + return folded; +} + +static const struct got_error * +histedit_edit_logmsg(struct got_histedit_list_entry *hle, + struct got_repository *repo) +{ + char *logmsg_path = NULL, *id_str = NULL; + char *logmsg = NULL, *new_msg = NULL, *editor = NULL; + const struct got_error *err = NULL; + struct got_commit_object *commit = NULL; + int fd; + struct got_histedit_list_entry *folded = NULL; + + err = got_object_open_as_commit(&commit, repo, hle->commit_id); + if (err) + return err; + + folded = get_folded_commits(hle); + if (folded) { + while (folded != hle) { + err = append_folded_commit_msg(&new_msg, folded, + logmsg, repo); + if (err) + goto done; + free(logmsg); + logmsg = new_msg; + folded = TAILQ_NEXT(folded, entry); + } + } + + err = got_object_id_str(&id_str, hle->commit_id); + if (err) + goto done; + if (asprintf(&new_msg, + "%s\n# original log message of commit %s: %s", + logmsg ? logmsg : "", id_str, + got_object_commit_get_logmsg(commit)) == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + free(logmsg); + logmsg = new_msg; + + err = got_object_id_str(&id_str, hle->commit_id); + if (err) + goto done; + + err = got_opentemp_named_fd(&logmsg_path, &fd, "/tmp/got-logmsg"); + if (err) + goto done; + + dprintf(fd, logmsg); + close(fd); + + err = get_editor(&editor); + if (err) + goto done; + + err = edit_logmsg(&hle->logmsg, editor, logmsg_path, logmsg); + if (err) { + if (err->code != GOT_ERR_COMMIT_MSG_EMPTY) + goto done; + err = NULL; + hle->logmsg = strdup(got_object_commit_get_logmsg(commit)); + if (hle->logmsg == NULL) + err = got_error_from_errno("strdup"); + } +done: + if (logmsg_path && unlink(logmsg_path) != 0 && err == NULL) + err = got_error_from_errno2("unlink", logmsg_path); + free(logmsg_path); + free(logmsg); + free(editor); + if (commit) + got_object_commit_close(commit); + return err; } + +static const struct got_error * +histedit_parse_list(struct got_histedit_list *histedit_cmds, + FILE *f, struct got_repository *repo) +{ + const struct got_error *err = NULL; + char *line = NULL, *p, *end; + size_t size; + ssize_t len; + int lineno = 0, i; + const struct got_histedit_cmd *cmd; + struct got_object_id *commit_id = NULL; + struct got_histedit_list_entry *hle = NULL; + + for (;;) { + len = getline(&line, &size, f); + if (len == -1) { + const struct got_error *getline_err; + if (feof(f)) + break; + getline_err = got_error_from_errno("getline"); + err = got_ferror(f, getline_err->code); + break; + } + lineno++; + p = line; + while (isspace((unsigned char)p[0])) + p++; + if (p[0] == '#' || p[0] == '\0') { + free(line); + line = NULL; + continue; + } + cmd = NULL; + for (i = 0; i < nitems(got_histedit_cmds); i++) { + cmd = &got_histedit_cmds[i]; + if (strncmp(cmd->name, p, strlen(cmd->name)) == 0 && + isspace((unsigned char)p[strlen(cmd->name)])) { + p += strlen(cmd->name); + break; + } + if (p[0] == cmd->code && isspace((unsigned char)p[1])) { + p++; + break; + } + } + if (cmd == NULL) { + err = histedit_syntax_error(lineno); + break; + } + while (isspace((unsigned char)p[0])) + p++; + if (cmd->code == GOT_HISTEDIT_MESG) { + if (hle == NULL || hle->logmsg != NULL) { + err = got_error(GOT_ERR_HISTEDIT_CMD); + break; + } + if (p[0] == '\0') { + err = histedit_edit_logmsg(hle, repo); + if (err) + break; + } else { + hle->logmsg = strdup(p); + if (hle->logmsg == NULL) { + err = got_error_from_errno("strdup"); + break; + } + } + free(line); + line = NULL; + continue; + } else { + end = p; + while (end[0] && !isspace((unsigned char)end[0])) + end++; + *end = '\0'; + + err = got_object_resolve_id_str(&commit_id, repo, p); + if (err) { + /* override error code */ + err = histedit_syntax_error(lineno); + break; + } + } + hle = malloc(sizeof(*hle)); + if (hle == NULL) { + err = got_error_from_errno("malloc"); + break; + } + hle->cmd = cmd; + hle->commit_id = commit_id; + hle->logmsg = NULL; + commit_id = NULL; + free(line); + line = NULL; + TAILQ_INSERT_TAIL(histedit_cmds, hle, entry); + } + + free(line); + free(commit_id); + return err; +} + +static const struct got_error * +histedit_run_editor(struct got_histedit_list *histedit_cmds, + const char *path, struct got_repository *repo) +{ + const struct got_error *err = NULL; + char *editor; + FILE *f = NULL; + + err = get_editor(&editor); + if (err) + return err; + + if (spawn_editor(editor, path) == -1) { + err = got_error_from_errno("failed spawning editor"); + goto done; + } + + f = fopen(path, "r"); + if (f == NULL) { + err = got_error_from_errno("fopen"); + goto done; + } + err = histedit_parse_list(histedit_cmds, f, repo); +done: + if (f && fclose(f) != 0 && err == NULL) + err = got_error_from_errno("fclose"); + free(editor); + return err; +} + +static const struct got_error * +histedit_edit_list_retry(struct got_histedit_list *, const char *, + struct got_object_id_queue *, const char *, struct got_repository *); + +static const struct got_error * +histedit_edit_script(struct got_histedit_list *histedit_cmds, + struct got_object_id_queue *commits, struct got_repository *repo) +{ + const struct got_error *err; + FILE *f = NULL; + char *path = NULL; + + err = got_opentemp_named(&path, &f, "got-histedit"); + if (err) + return err; + + err = write_cmd_list(f); + if (err) + goto done; + + err = histedit_write_commit_list(commits, f, repo); + if (err) + goto done; + + if (fclose(f) != 0) { + err = got_error_from_errno("fclose"); + goto done; + } + f = NULL; + + err = histedit_run_editor(histedit_cmds, path, repo); + if (err) { + const char *errmsg = err->msg; + if (err->code != GOT_ERR_HISTEDIT_SYNTAX) + goto done; + err = histedit_edit_list_retry(histedit_cmds, errmsg, + commits, path, repo); + } +done: + if (f && fclose(f) != 0 && err == NULL) + err = got_error_from_errno("fclose"); + if (path && unlink(path) != 0 && err == NULL) + err = got_error_from_errno2("unlink", path); + free(path); + return err; +} + +static const struct got_error * +histedit_save_list(struct got_histedit_list *histedit_cmds, + struct got_worktree *worktree, struct got_repository *repo) +{ + const struct got_error *err = NULL; + char *path = NULL; + FILE *f = NULL; + struct got_histedit_list_entry *hle; + struct got_commit_object *commit = NULL; + + err = got_worktree_get_histedit_list_path(&path, worktree); + if (err) + return err; + + f = fopen(path, "w"); + if (f == NULL) { + err = got_error_from_errno2("fopen", path); + goto done; + } + TAILQ_FOREACH(hle, histedit_cmds, entry) { + err = histedit_write_commit(hle->commit_id, hle->cmd->name, f, + repo); + if (err) + break; + + if (hle->logmsg) { + int n = fprintf(f, "%c %s\n", + GOT_HISTEDIT_MESG, hle->logmsg); + if (n < 0) { + err = got_ferror(f, GOT_ERR_IO); + break; + } + } + } +done: + if (f && fclose(f) != 0 && err == NULL) + err = got_error_from_errno("fclose"); + free(path); + if (commit) + got_object_commit_close(commit); + return err; +} + +static const struct got_error * +histedit_load_list(struct got_histedit_list *histedit_cmds, + const char *path, struct got_repository *repo) +{ + const struct got_error *err = NULL; + FILE *f = NULL; + + f = fopen(path, "r"); + if (f == NULL) { + err = got_error_from_errno2("fopen", path); + goto done; + } + + err = histedit_parse_list(histedit_cmds, f, repo); +done: + if (f && fclose(f) != 0 && err == NULL) + err = got_error_from_errno("fclose"); + return err; +} + +static const struct got_error * +histedit_edit_list_retry(struct got_histedit_list *histedit_cmds, + const char *errmsg, struct got_object_id_queue *commits, + const char *path, struct got_repository *repo) +{ + const struct got_error *err = NULL; + int resp = ' '; + + while (resp != 'c' && resp != 'r' && resp != 'q') { + printf("%s: %s\n(c)ontinue editing, (r)estart editing, " + "or (a)bort: ", getprogname(), errmsg); + resp = getchar(); + switch (resp) { + case 'c': + err = histedit_run_editor(histedit_cmds, path, repo); + break; + case 'r': + err = histedit_edit_script(histedit_cmds, + commits, repo); + break; + case 'a': + err = got_error(GOT_ERR_HISTEDIT_CANCEL); + break; + default: + printf("invalid response '%c'\n", resp); + break; + } + } + + return err; +} + +static const struct got_error * +histedit_complete(struct got_worktree *worktree, + struct got_reference *tmp_branch, struct got_reference *branch, + struct got_repository *repo) +{ + printf("Switching work tree to %s\n", + got_ref_get_symref_target(branch)); + return got_worktree_histedit_complete(worktree, tmp_branch, branch, + repo); +} + +static const struct got_error * +show_histedit_progress(struct got_commit_object *commit, + struct got_histedit_list_entry *hle, struct got_object_id *new_id) +{ + const struct got_error *err; + char *old_id_str = NULL, *new_id_str = NULL, *logmsg = NULL; + + err = got_object_id_str(&old_id_str, hle->commit_id); + if (err) + goto done; + + if (new_id) { + err = got_object_id_str(&new_id_str, new_id); + if (err) + goto done; + } + + old_id_str[12] = '\0'; + if (new_id_str) + new_id_str[12] = '\0'; + + if (hle->logmsg) { + logmsg = strdup(hle->logmsg); + if (logmsg == NULL) { + err = got_error_from_errno("strdup"); + goto done; + } + trim_logmsg(logmsg, 42); + } else { + err = get_short_logmsg(&logmsg, 42, commit); + if (err) + goto done; + } + + switch (hle->cmd->code) { + case GOT_HISTEDIT_PICK: + case GOT_HISTEDIT_EDIT: + printf("%s -> %s: %s\n", old_id_str, + new_id_str ? new_id_str : "no-op change", logmsg); + break; + case GOT_HISTEDIT_DROP: + case GOT_HISTEDIT_FOLD: + printf("%s -> %s commit: %s\n", old_id_str, hle->cmd->name, + logmsg); + break; + default: + break; + } + +done: + free(old_id_str); + free(new_id_str); + return err; +} + +static const struct got_error * +histedit_commit(struct got_pathlist_head *merged_paths, + struct got_worktree *worktree, struct got_reference *tmp_branch, + struct got_histedit_list_entry *hle, struct got_repository *repo) +{ + const struct got_error *err; + struct got_commit_object *commit; + struct got_object_id *new_commit_id; + + if ((hle->cmd->code == GOT_HISTEDIT_EDIT || get_folded_commits(hle)) + && hle->logmsg == NULL) { + err = histedit_edit_logmsg(hle, repo); + if (err) + return err; + } + + err = got_object_open_as_commit(&commit, repo, hle->commit_id); + if (err) + return err; + + err = got_worktree_histedit_commit(&new_commit_id, merged_paths, + worktree, tmp_branch, commit, hle->commit_id, hle->logmsg, repo); + if (err) { + if (err->code != GOT_ERR_COMMIT_NO_CHANGES) + goto done; + err = show_histedit_progress(commit, hle, NULL); + } else { + err = show_histedit_progress(commit, hle, new_commit_id); + free(new_commit_id); + } +done: + got_object_commit_close(commit); + return err; +} + +static const struct got_error * +histedit_skip_commit(struct got_histedit_list_entry *hle, + struct got_worktree *worktree, struct got_repository *repo) +{ + const struct got_error *error; + struct got_commit_object *commit; + + error = got_worktree_histedit_skip_commit(worktree, hle->commit_id, + repo); + if (error) + return error; + + error = got_object_open_as_commit(&commit, repo, hle->commit_id); + if (error) + return error; + + error = show_histedit_progress(commit, hle, NULL); + got_object_commit_close(commit); + return error; +} + +static const struct got_error * +histedit_check_script(struct got_histedit_list *histedit_cmds, + struct got_object_id_queue *commits, struct got_repository *repo) +{ + const struct got_error *err = NULL; + struct got_object_qid *qid; + struct got_histedit_list_entry *hle; + static char msg[80]; + char *id_str; + + if (TAILQ_EMPTY(histedit_cmds)) + return got_error(GOT_ERR_EMPTY_HISTEDIT); + + SIMPLEQ_FOREACH(qid, commits, entry) { + TAILQ_FOREACH(hle, histedit_cmds, entry) { + if (got_object_id_cmp(qid->id, hle->commit_id) == 0) + break; + } + if (hle == NULL) { + err = got_object_id_str(&id_str, qid->id); + if (err) + return err; + snprintf(msg, sizeof(msg), + "commit %s missing from histedit script", id_str); + free(id_str); + return got_error_msg(GOT_ERR_HISTEDIT_CMD, msg); + } + } + + if (hle->cmd->code == GOT_HISTEDIT_FOLD) + return got_error_msg(GOT_ERR_HISTEDIT_CMD, + "last commit in histedit script cannot be folded"); + + return NULL; +} + +static const struct got_error * +cmd_histedit(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 *tmp_branch = NULL; + struct got_object_id *resume_commit_id = NULL; + struct got_object_id *base_commit_id = NULL; + struct got_object_id *head_commit_id = NULL; + struct got_commit_object *commit = NULL; + int ch, rebase_in_progress = 0; + int edit_in_progress = 0, abort_edit = 0, continue_edit = 0; + const char *edit_script_path = NULL; + unsigned char rebase_status = GOT_STATUS_NO_CHANGE; + struct got_object_id_queue commits; + struct got_pathlist_head merged_paths; + const struct got_object_id_queue *parent_ids; + struct got_object_qid *pid; + struct got_histedit_list histedit_cmds; + struct got_histedit_list_entry *hle; + + SIMPLEQ_INIT(&commits); + TAILQ_INIT(&histedit_cmds); + TAILQ_INIT(&merged_paths); + + while ((ch = getopt(argc, argv, "acF:")) != -1) { + switch (ch) { + case 'a': + abort_edit = 1; + break; + case 'c': + continue_edit = 1; + break; + case 'F': + edit_script_path = optarg; + break; + default: + usage_histedit(); + /* NOTREACHED */ + } + } + + argc -= optind; + argv += optind; + +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath fattr flock proc exec sendfd " + "unveil", NULL) == -1) + err(1, "pledge"); +#endif + if (abort_edit && continue_edit) + usage_histedit(); + if (argc != 0) + usage_histedit(); + + /* + * This command cannot apply unveil(2) in all cases because the + * user may choose to run an editor to edit the histedit script + * and to edit individual commit log messages. + * unveil(2) traverses exec(2); if an editor is used we have to + * apply unveil after edit script and log messages have been written. + * XXX TODO: Make use of unveil(2) where possible. + */ + + 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 = got_worktree_rebase_in_progress(&rebase_in_progress, worktree); + if (error) + goto done; + if (rebase_in_progress) { + error = got_error(GOT_ERR_REBASING); + goto done; + } + + error = got_worktree_histedit_in_progress(&edit_in_progress, worktree); + if (error) + goto done; + + if (edit_in_progress && abort_edit) { + int did_something; + error = got_worktree_histedit_continue(&resume_commit_id, + &tmp_branch, &branch, &base_commit_id, worktree, repo); + if (error) + goto done; + printf("Switching work tree to %s\n", + got_ref_get_symref_target(branch)); + error = got_worktree_histedit_abort(worktree, repo, + branch, base_commit_id, update_progress, &did_something); + if (error) + goto done; + printf("Histedit of %s aborted\n", + got_ref_get_symref_target(branch)); + goto done; /* nothing else to do */ + } else if (abort_edit) { + error = got_error(GOT_ERR_NOT_HISTEDIT); + goto done; + } + + if (continue_edit) { + char *path; + + if (!edit_in_progress) { + error = got_error(GOT_ERR_NOT_HISTEDIT); + goto done; + } + + error = got_worktree_get_histedit_list_path(&path, worktree); + if (error) + goto done; + + error = histedit_load_list(&histedit_cmds, path, repo); + free(path); + if (error) + goto done; + + error = got_worktree_histedit_continue(&resume_commit_id, + &tmp_branch, &branch, &base_commit_id, worktree, repo); + if (error) + goto done; + + error = got_ref_resolve(&head_commit_id, repo, branch); + if (error) + goto done; + + error = got_object_open_as_commit(&commit, repo, + head_commit_id); + if (error) + goto done; + parent_ids = got_object_commit_get_parent_ids(commit); + pid = SIMPLEQ_FIRST(parent_ids); + error = collect_commits_to_rebase(&commits, head_commit_id, + pid->id, base_commit_id, + got_worktree_get_path_prefix(worktree), repo); + got_object_commit_close(commit); + commit = NULL; + if (error) + goto done; + } else { + if (edit_in_progress) { + error = got_error(GOT_ERR_HISTEDIT_BUSY); + goto done; + } + + error = got_ref_open(&branch, repo, + got_worktree_get_head_ref_name(worktree), 0); + if (error != NULL) + goto done; + + error = got_ref_resolve(&head_commit_id, repo, branch); + if (error) + goto done; + + error = got_object_open_as_commit(&commit, repo, + head_commit_id); + if (error) + goto done; + parent_ids = got_object_commit_get_parent_ids(commit); + pid = SIMPLEQ_FIRST(parent_ids); + error = collect_commits_to_rebase(&commits, head_commit_id, + pid->id, got_worktree_get_base_commit_id(worktree), + got_worktree_get_path_prefix(worktree), repo); + got_object_commit_close(commit); + commit = NULL; + if (error) + goto done; + + if (edit_script_path) { + error = histedit_load_list(&histedit_cmds, + edit_script_path, repo); + if (error) + goto done; + } else { + error = histedit_edit_script(&histedit_cmds, &commits, + repo); + if (error) + goto done; + + } + + error = histedit_save_list(&histedit_cmds, worktree, + repo); + if (error) + goto done; + + error = got_worktree_histedit_prepare(&tmp_branch, &branch, + &base_commit_id, worktree, repo); + if (error) + goto done; + + } + + error = histedit_check_script(&histedit_cmds, &commits, repo); + if (error) + goto done; + + TAILQ_FOREACH(hle, &histedit_cmds, entry) { + if (resume_commit_id) { + if (got_object_id_cmp(hle->commit_id, + resume_commit_id) != 0) + continue; + + resume_commit_id = NULL; + if (hle->cmd->code == GOT_HISTEDIT_DROP || + hle->cmd->code == GOT_HISTEDIT_FOLD) { + error = histedit_skip_commit(hle, worktree, + repo); + } else { + error = histedit_commit(NULL, worktree, + tmp_branch, hle, repo); + } + if (error) + goto done; + continue; + } + + if (hle->cmd->code == GOT_HISTEDIT_DROP) { + error = histedit_skip_commit(hle, worktree, repo); + if (error) + goto done; + continue; + } + + error = got_object_open_as_commit(&commit, repo, + hle->commit_id); + if (error) + goto done; + parent_ids = got_object_commit_get_parent_ids(commit); + pid = SIMPLEQ_FIRST(parent_ids); + + error = got_worktree_histedit_merge_files(&merged_paths, + worktree, pid->id, hle->commit_id, repo, rebase_progress, + &rebase_status, check_cancelled, NULL); + if (error) + goto done; + got_object_commit_close(commit); + commit = NULL; + + if (rebase_status == GOT_STATUS_CONFLICT) { + got_worktree_rebase_pathlist_free(&merged_paths); + break; + } + + if (hle->cmd->code == GOT_HISTEDIT_EDIT) { + char *id_str; + error = got_object_id_str(&id_str, hle->commit_id); + if (error) + goto done; + printf("Stopping histedit for amending commit %s\n", + id_str); + free(id_str); + got_worktree_rebase_pathlist_free(&merged_paths); + error = got_worktree_histedit_postpone(worktree); + goto done; + } + + if (hle->cmd->code == GOT_HISTEDIT_FOLD) { + error = histedit_skip_commit(hle, worktree, repo); + if (error) + goto done; + continue; + } + + error = histedit_commit(&merged_paths, worktree, tmp_branch, + hle, repo); + got_worktree_rebase_pathlist_free(&merged_paths); + if (error) + goto done; + } + + if (rebase_status == GOT_STATUS_CONFLICT) { + error = got_worktree_histedit_postpone(worktree); + if (error) + goto done; + error = got_error_msg(GOT_ERR_CONFLICTS, + "conflicts must be resolved before rebasing can continue"); + } else + error = histedit_complete(worktree, tmp_branch, branch, repo); +done: + got_object_id_queue_free(&commits); + free(head_commit_id); + free(base_commit_id); + free(resume_commit_id); + if (commit) + got_object_commit_close(commit); + if (branch) + got_ref_close(branch); + if (tmp_branch) + got_ref_close(tmp_branch); + if (worktree) + got_worktree_close(worktree); + if (repo) + got_repo_close(repo); + return error; +} blob - 48ede3e31fd5acad44260af32cff4957fb97fdad blob + 01c340bddf058f3296064d6b7a860341ce2eeeb2 --- include/got_error.h +++ include/got_error.h @@ -103,6 +103,14 @@ #define GOT_ERR_REBASE_COMMITID 87 #define GOT_ERR_REBASING 88 #define GOT_ERR_REBASE_PATH 89 +#define GOT_ERR_NOT_HISTEDIT 90 +#define GOT_ERR_EMPTY_HISTEDIT 91 +#define GOT_ERR_NO_HISTEDIT_CMD 92 +#define GOT_ERR_HISTEDIT_SYNTAX 93 +#define GOT_ERR_HISTEDIT_CANCEL 94 +#define GOT_ERR_HISTEDIT_COMMITID 95 +#define GOT_ERR_HISTEDIT_BUSY 96 +#define GOT_ERR_HISTEDIT_CMD 97 static const struct got_error { int code; @@ -204,6 +212,15 @@ static const struct got_error { "work tree and must be continued or aborted first" }, { GOT_ERR_REBASE_PATH, "cannot rebase branch which contains " "changes outside of this work tree's path prefix" }, + { GOT_ERR_NOT_HISTEDIT, "histedit operation not in progress" }, + { GOT_ERR_EMPTY_HISTEDIT,"no commits to edit" }, + { GOT_ERR_NO_HISTEDIT_CMD,"no histedit commands provided" }, + { GOT_ERR_HISTEDIT_SYNTAX,"syntax error in histedit command list" }, + { GOT_ERR_HISTEDIT_CANCEL,"histedit operation cancelled" }, + { GOT_ERR_HISTEDIT_COMMITID,"histedit commit ID mismatch" }, + { GOT_ERR_HISTEDIT_BUSY,"histedit operation is in progress in this " + "work tree and must be continued or aborted first" }, + { GOT_ERR_HISTEDIT_CMD, "bad histedit command" }, }; /* blob - 4581e987c69013fec31a525f414b1596bf5ce460 blob + 6e6d2f53d8d2d5dd4b9b32075806433ee2a4cb49 --- include/got_worktree.h +++ include/got_worktree.h @@ -281,4 +281,86 @@ const struct got_error *got_worktree_rebase_complete(s */ const struct got_error *got_worktree_rebase_abort(struct got_worktree *, struct got_repository *, struct got_reference *, + got_worktree_checkout_cb, void *); + +/* + * Prepare for editing the history of the work tree's current branch. + * This function creates references to a temporary branch, and the + * work tree's current branch, under the "got/worktree/histedit/" namespace. + * These references are used to keep track of histedit operation state and + * are used as input and/or output arguments with other histedit-related + * functions. + */ +const struct got_error *got_worktree_histedit_prepare(struct got_reference **, + struct got_reference **, struct got_object_id **, struct got_worktree *, + struct got_repository *); + +/* + * Continue an interrupted histedit operation. + * This function returns existing references created when histedit was + * prepared and the ID of the commit currently being edited. + * It should be called before resuming or aborting a histedit operation. + */ +const struct got_error *got_worktree_histedit_continue(struct got_object_id **, + struct got_reference **, struct got_reference **, struct got_object_id **, + struct got_worktree *, struct got_repository *); + +/* Check whether a histedit operation is in progress. */ +const struct got_error *got_worktree_histedit_in_progress(int *, + struct got_worktree *); + +/* + * Merge changes from the commit currently being edited into the work tree. + * Report affected files, including merge conflicts, via the specified + * progress callback. Also populate a list of affected paths which should + * be passed to got_worktree_histedit_commit() after a conflict-free merge. + * This list must be initialized with TAILQ_INIT() and disposed of with + * got_worktree_rebase_pathlist_free(). + */ +const struct got_error *got_worktree_histedit_merge_files( + struct got_pathlist_head *, 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 changes merged by got_worktree_histedit_merge_files() to a temporary + * branch and return the ID of the newly created commit. An optional list of + * merged paths can be provided; otherwise this function will perform a status + * crawl across the entire work tree to find paths to commit. + * An optional log message can be provided which will be used instead of the + * commit's original message. + */ +const struct got_error *got_worktree_histedit_commit(struct got_object_id **, + struct got_pathlist_head *, struct got_worktree *, + struct got_reference *, struct got_commit_object *, + struct got_object_id *, const char *, struct got_repository *); + +/* + * Record the specified commit as skipped during histedit. + * This should be called for commits which get dropped or get folded into + * a subsequent commit. + */ +const struct got_error *got_worktree_histedit_skip_commit(struct got_worktree *, + struct got_object_id *, struct got_repository *); + +/* Postpone the histedit operation. */ +const struct got_error *got_worktree_histedit_postpone(struct got_worktree *); + +/* + * Complete the current histedit operation. This should be called once all + * commits have been edited successfully. + */ +const struct got_error *got_worktree_histedit_complete(struct got_worktree *, + struct got_reference *, struct got_reference *, struct got_repository *); + +/* + * Abort the current histedit operation. + * Report reverted files via the specified progress callback. + */ +const struct got_error *got_worktree_histedit_abort(struct got_worktree *, + struct got_repository *, struct got_reference *, struct got_object_id *, got_worktree_checkout_cb, void *); + +/* Get the path to this work tree's histedit command list file. */ +const struct got_error *got_worktree_get_histedit_list_path(char **, + struct got_worktree *); blob - 317d0fdf6a3b2bf43047f3743caed1b085199df0 blob + 2a0626057ea68d7928c4de3678836630f02cca24 --- lib/got_lib_worktree.h +++ lib/got_lib_worktree.h @@ -57,6 +57,7 @@ struct got_commitable { #define GOT_WORKTREE_LOCK "lock" #define GOT_WORKTREE_FORMAT "format" #define GOT_WORKTREE_UUID "uuid" +#define GOT_WORKTREE_HISTEDIT_LIST "histedit-list" #define GOT_WORKTREE_FORMAT_VERSION 1 #define GOT_WORKTREE_INVALID_COMMIT_ID GOT_SHA1_STRING_ZERO @@ -77,3 +78,18 @@ const struct got_error *got_worktree_get_base_ref_name /* Reference pointing at the ID of the current commit being rebased. */ #define GOT_WORKTREE_REBASE_COMMIT_REF_PREFIX "refs/got/worktree/rebase/commit" + +/* Temporary branch which accumulates commits during a histedit operation. */ +#define GOT_WORKTREE_HISTEDIT_TMP_REF_PREFIX "refs/got/worktree/histedit/tmp" + +/* Symbolic reference pointing at the name of the branch being edited. */ +#define GOT_WORKTREE_HISTEDIT_BRANCH_REF_PREFIX \ + "refs/got/worktree/histedit/branch" + +/* Reference pointing at the ID of the work tree's pre-edit base commit. */ +#define GOT_WORKTREE_HISTEDIT_BASE_COMMIT_REF_PREFIX \ + "refs/got/worktree/histedit/base-commit" + +/* Reference pointing at the ID of the current commit being edited. */ +#define GOT_WORKTREE_HISTEDIT_COMMIT_REF_PREFIX \ + "refs/got/worktree/histedit/commit" blob - 7a096570fcf9fc2d50fbd56c6a4a29ad6b170e54 blob + cdff663f385ece6d4196b6e18a30d7ddcb7ab947 --- lib/worktree.c +++ lib/worktree.c @@ -1438,9 +1438,47 @@ get_rebase_commit_ref_name(char **refname, struct got_ { return get_ref_name(refname, worktree, GOT_WORKTREE_REBASE_COMMIT_REF_PREFIX); +} + +static const struct got_error * +get_histedit_tmp_ref_name(char **refname, struct got_worktree *worktree) +{ + return get_ref_name(refname, worktree, + GOT_WORKTREE_HISTEDIT_TMP_REF_PREFIX); +} + +static const struct got_error * +get_histedit_branch_symref_name(char **refname, struct got_worktree *worktree) +{ + return get_ref_name(refname, worktree, + GOT_WORKTREE_HISTEDIT_BRANCH_REF_PREFIX); +} + +static const struct got_error * +get_histedit_base_commit_ref_name(char **refname, struct got_worktree *worktree) +{ + return get_ref_name(refname, worktree, + GOT_WORKTREE_HISTEDIT_BASE_COMMIT_REF_PREFIX); } +static const struct got_error * +get_histedit_commit_ref_name(char **refname, struct got_worktree *worktree) +{ + return get_ref_name(refname, worktree, + GOT_WORKTREE_HISTEDIT_COMMIT_REF_PREFIX); +} +const struct got_error * +got_worktree_get_histedit_list_path(char **path, struct got_worktree *worktree) +{ + if (asprintf(path, "%s/%s/%s", worktree->root_path, + GOT_WORKTREE_GOT_DIR, GOT_WORKTREE_HISTEDIT_LIST) == -1) { + *path = NULL; + return got_error_from_errno("asprintf"); + } + return NULL; +} + /* * Prevent Git's garbage collector from deleting our base commit by * setting a reference to our base commit's ID. @@ -3761,12 +3799,7 @@ 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"); - + *logmsg = arg; return NULL; } @@ -3822,28 +3855,13 @@ got_worktree_rebase_pathlist_free(struct got_pathlist_ got_pathlist_free(merged_paths); } -const struct got_error * -got_worktree_rebase_merge_files(struct got_pathlist_head *merged_paths, - 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) +static const struct got_error * +store_commit_id(const char *commit_ref_name, struct got_object_id *commit_id, + struct got_repository *repo) { const struct got_error *err; - struct got_fileindex *fileindex; - char *fileindex_path, *commit_ref_name = NULL; struct got_reference *commit_ref = NULL; - struct collect_merged_paths_arg cmp_arg; - /* Work tree is locked/unlocked during rebase preparation/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) @@ -3868,14 +3886,37 @@ got_worktree_rebase_merge_files(struct got_pathlist_he goto done; } } +done: + if (commit_ref) + got_ref_close(commit_ref); + return err; +} + +static const struct got_error * +rebase_merge_files(struct got_pathlist_head *merged_paths, + const char *commit_ref_name, 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; + struct got_reference *commit_ref = NULL; + struct collect_merged_paths_arg cmp_arg; + /* Work tree is locked/unlocked during rebase preparation/teardown. */ + + err = open_fileindex(&fileindex, &fileindex_path, worktree); + if (err) + return err; + cmp_arg.progress_cb = progress_cb; cmp_arg.progress_arg = progress_arg; cmp_arg.merged_paths = merged_paths; err = merge_files(worktree, fileindex, fileindex_path, parent_commit_id, commit_id, repo, collect_merged_paths, &cmp_arg, cancel_cb, cancel_arg); -done: got_fileindex_free(fileindex); free(fileindex_path); if (commit_ref) @@ -3884,39 +3925,77 @@ done: } const struct got_error * -got_worktree_rebase_commit(struct got_object_id **new_commit_id, - struct got_pathlist_head *merged_paths, 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) +got_worktree_rebase_merge_files(struct got_pathlist_head *merged_paths, + 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; + char *commit_ref_name; + + err = get_rebase_commit_ref_name(&commit_ref_name, worktree); + if (err) + return err; + + err = store_commit_id(commit_ref_name, commit_id, repo); + if (err) + goto done; + + err = rebase_merge_files(merged_paths, commit_ref_name, worktree, + parent_commit_id, commit_id, repo, progress_cb, progress_arg, + cancel_cb, cancel_arg); +done: + free(commit_ref_name); + return err; +} + +const struct got_error * +got_worktree_histedit_merge_files(struct got_pathlist_head *merged_paths, + 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; + char *commit_ref_name; + + err = get_histedit_commit_ref_name(&commit_ref_name, worktree); + if (err) + return err; + + err = store_commit_id(commit_ref_name, commit_id, repo); + if (err) + goto done; + + err = rebase_merge_files(merged_paths, commit_ref_name, worktree, + parent_commit_id, commit_id, repo, progress_cb, progress_arg, + cancel_cb, cancel_arg); +done: + free(commit_ref_name); + return err; +} + +static const struct got_error * +rebase_commit(struct got_object_id **new_commit_id, + struct got_pathlist_head *merged_paths, struct got_reference *commit_ref, + struct got_worktree *worktree, struct got_reference *tmp_branch, + struct got_commit_object *orig_commit, const char *new_logmsg, + struct got_repository *repo) +{ const struct got_error *err, *sync_err; struct got_pathlist_head commitable_paths; struct collect_commitables_arg cc_arg; struct got_fileindex *fileindex = NULL; - char *fileindex_path = NULL, *commit_ref_name = NULL; + char *fileindex_path = NULL; struct got_reference *head_ref = NULL; struct got_object_id *head_commit_id = NULL; - struct got_reference *commit_ref = NULL; - struct got_object_id *commit_id = NULL; + char *logmsg = NULL; TAILQ_INIT(&commitable_paths); *new_commit_id = NULL; /* Work tree is locked/unlocked during rebase preparation/teardown. */ - - err = get_rebase_commit_ref_name(&commit_ref_name, worktree); - if (err) - return err; - 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 = open_fileindex(&fileindex, &fileindex_path, worktree); if (err) @@ -3963,11 +4042,17 @@ got_worktree_rebase_commit(struct got_object_id **new_ if (err) goto done; + if (new_logmsg) + logmsg = strdup(new_logmsg); + else + logmsg = strdup(got_object_commit_get_logmsg(orig_commit)); + if (logmsg == NULL) + return got_error_from_errno("strdup"); + err = commit_worktree(new_commit_id, &commitable_paths, head_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); + collect_rebase_commit_msg, logmsg, rebase_status, NULL, repo); if (err) goto done; @@ -3988,9 +4073,6 @@ done: if (fileindex) got_fileindex_free(fileindex); free(fileindex_path); - free(commit_ref_name); - if (commit_ref) - got_ref_close(commit_ref); free(head_commit_id); if (head_ref) got_ref_close(head_ref); @@ -4002,6 +4084,79 @@ done: } const struct got_error * +got_worktree_rebase_commit(struct got_object_id **new_commit_id, + struct got_pathlist_head *merged_paths, 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; + struct got_reference *commit_ref = NULL; + struct got_object_id *commit_id = NULL; + + err = get_rebase_commit_ref_name(&commit_ref_name, worktree); + if (err) + return err; + + 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 = rebase_commit(new_commit_id, merged_paths, commit_ref, + worktree, tmp_branch, orig_commit, NULL, repo); +done: + if (commit_ref) + got_ref_close(commit_ref); + free(commit_ref_name); + free(commit_id); + return err; +} + +const struct got_error * +got_worktree_histedit_commit(struct got_object_id **new_commit_id, + struct got_pathlist_head *merged_paths, struct got_worktree *worktree, + struct got_reference *tmp_branch, struct got_commit_object *orig_commit, + struct got_object_id *orig_commit_id, const char *new_logmsg, + struct got_repository *repo) +{ + const struct got_error *err; + char *commit_ref_name; + struct got_reference *commit_ref = NULL; + struct got_object_id *commit_id = NULL; + + err = get_histedit_commit_ref_name(&commit_ref_name, worktree); + if (err) + return err; + + 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_HISTEDIT_COMMITID); + goto done; + } + + err = rebase_commit(new_commit_id, merged_paths, commit_ref, + worktree, tmp_branch, orig_commit, new_logmsg, repo); +done: + if (commit_ref) + got_ref_close(commit_ref); + free(commit_ref_name); + free(commit_id); + return err; +} + +const struct got_error * got_worktree_rebase_postpone(struct got_worktree *worktree) { return lock_worktree(worktree, LOCK_SH); @@ -4212,8 +4367,339 @@ done: got_ref_close(resolved); free(tree_id); free(commit_id); + if (fileindex) + 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; +} + +const struct got_error * +got_worktree_histedit_prepare(struct got_reference **tmp_branch, + struct got_reference **branch_ref, struct got_object_id **base_commit_id, + struct got_worktree *worktree, struct got_repository *repo) +{ + const struct got_error *err = NULL; + char *tmp_branch_name = NULL; + char *branch_ref_name = NULL; + char *base_commit_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; + struct got_reference *base_commit_ref = NULL; + + *tmp_branch = NULL; + *branch_ref = NULL; + *base_commit_id = 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_histedit_tmp_ref_name(&tmp_branch_name, worktree); + if (err) + goto done; + + err = get_histedit_branch_symref_name(&branch_ref_name, worktree); + if (err) + goto done; + + err = get_histedit_base_commit_ref_name(&base_commit_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(branch_ref, branch_ref_name, wt_branch); + if (err) + goto done; + + err = got_ref_write(*branch_ref, repo); + if (err) + goto done; + + err = got_ref_alloc(&base_commit_ref, base_commit_ref_name, + worktree->base_commit_id); + if (err) + goto done; + err = got_ref_write(base_commit_ref, repo); + if (err) + goto done; + *base_commit_id = got_object_id_dup(worktree->base_commit_id); + if (*base_commit_id == NULL) { + err = got_error_from_errno("got_object_id_dup"); + 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(branch_ref_name); + free(base_commit_ref_name); + if (wt_branch) + got_ref_close(wt_branch); + if (err) { + if (*branch_ref) { + got_ref_close(*branch_ref); + *branch_ref = NULL; + } + if (*tmp_branch) { + got_ref_close(*tmp_branch); + *tmp_branch = NULL; + } + free(*base_commit_id); + lock_worktree(worktree, LOCK_SH); + } + return err; +} + +const struct got_error * +got_worktree_histedit_postpone(struct got_worktree *worktree) +{ + return lock_worktree(worktree, LOCK_SH); +} + +const struct got_error * +got_worktree_histedit_in_progress(int *in_progress, + struct got_worktree *worktree) +{ + const struct got_error *err; + char *tmp_branch_name = NULL; + + err = get_histedit_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; +} + +const struct got_error * +got_worktree_histedit_continue(struct got_object_id **commit_id, + struct got_reference **tmp_branch, struct got_reference **branch_ref, + struct got_object_id **base_commit_id, + struct got_worktree *worktree, struct got_repository *repo) +{ + const struct got_error *err; + char *commit_ref_name = NULL, *base_commit_ref_name = NULL; + char *tmp_branch_name = NULL, *branch_ref_name = NULL; + struct got_reference *commit_ref = NULL; + struct got_reference *base_commit_ref = NULL; + + *commit_id = NULL; + *tmp_branch = NULL; + *base_commit_id = NULL; + + err = get_histedit_tmp_ref_name(&tmp_branch_name, worktree); + if (err) + return err; + + err = get_histedit_branch_symref_name(&branch_ref_name, worktree); + if (err) + goto done; + + err = get_histedit_commit_ref_name(&commit_ref_name, worktree); + if (err) + goto done; + + err = get_histedit_base_commit_ref_name(&base_commit_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(&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(&base_commit_ref, repo, base_commit_ref_name, 0); + if (err) + goto done; + err = got_ref_resolve(base_commit_id, repo, base_commit_ref); + 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 (base_commit_ref) + got_ref_close(base_commit_ref); + if (err) { + free(*commit_id); + *commit_id = NULL; + free(*base_commit_id); + *base_commit_id = NULL; + if (*tmp_branch) { + got_ref_close(*tmp_branch); + *tmp_branch = NULL; + } + } + return err; +} + +static const struct got_error * +delete_histedit_refs(struct got_worktree *worktree, struct got_repository *repo) +{ + const struct got_error *err; + char *tmp_branch_name = NULL, *base_commit_ref_name = NULL; + char *branch_ref_name = NULL, *commit_ref_name = NULL; + + err = get_histedit_tmp_ref_name(&tmp_branch_name, worktree); + if (err) + goto done; + err = delete_ref(tmp_branch_name, repo); + if (err) + goto done; + + err = get_histedit_base_commit_ref_name(&base_commit_ref_name, + worktree); + if (err) + goto done; + err = delete_ref(base_commit_ref_name, repo); + if (err) + goto done; + + err = get_histedit_branch_symref_name(&branch_ref_name, worktree); + if (err) + goto done; + err = delete_ref(branch_ref_name, repo); + if (err) + goto done; + + err = get_histedit_commit_ref_name(&commit_ref_name, worktree); + if (err) + goto done; + err = delete_ref(commit_ref_name, repo); + if (err) + goto done; +done: + free(tmp_branch_name); + free(base_commit_ref_name); + free(branch_ref_name); + free(commit_ref_name); + return err; +} + +const struct got_error * +got_worktree_histedit_abort(struct got_worktree *worktree, + struct got_repository *repo, struct got_reference *branch, + struct got_object_id *base_commit_id, + got_worktree_checkout_cb progress_cb, void *progress_arg) +{ + const struct got_error *err, *unlockerr, *sync_err; + struct got_reference *resolved = 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; + struct got_object_id *tree_id = NULL; + + TAILQ_INIT(&revertible_paths); + + err = lock_worktree(worktree, LOCK_EX); + if (err) + return err; + + err = got_ref_open(&resolved, repo, + got_ref_get_symref_target(branch), 0); + if (err) + goto done; + + err = got_worktree_set_head_ref(worktree, resolved); + if (err) + goto done; + + err = got_worktree_set_base_commit_id(worktree, repo, base_commit_id); + if (err) + goto done; + + err = got_object_id_by_path(&tree_id, repo, base_commit_id, + worktree->path_prefix); + if (err) + goto done; + + err = delete_histedit_refs(worktree, repo); + if (err) + goto done; + + err = open_fileindex(&fileindex, &fileindex_path, worktree); + if (err) + goto done; + + crp_arg.revertible_paths = &revertible_paths; + crp_arg.worktree = worktree; + err = worktree_status(worktree, "", fileindex, 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) + goto sync; + } + + err = checkout_files(worktree, fileindex, "", tree_id, NULL, + repo, progress_cb, progress_arg, NULL, NULL); +sync: + sync_err = sync_fileindex(fileindex, fileindex_path); + if (sync_err && err == NULL) + err = sync_err; +done: + got_ref_close(resolved); + free(tree_id); + if (fileindex) + got_fileindex_free(fileindex); free(fileindex_path); TAILQ_FOREACH(pe, &revertible_paths, entry) free((char *)pe->path); @@ -4224,3 +4710,63 @@ done: err = unlockerr; return err; } + +const struct got_error * +got_worktree_histedit_complete(struct got_worktree *worktree, + struct got_reference *tmp_branch, struct got_reference *edited_branch, + struct got_repository *repo) +{ + const struct got_error *err, *unlockerr; + struct got_object_id *new_head_commit_id = NULL; + struct got_reference *resolved = NULL; + + err = got_ref_resolve(&new_head_commit_id, repo, tmp_branch); + if (err) + return err; + + err = got_ref_open(&resolved, repo, + got_ref_get_symref_target(edited_branch), 0); + if (err) + goto done; + + err = got_ref_change_ref(resolved, new_head_commit_id); + if (err) + goto done; + + err = got_ref_write(resolved, repo); + if (err) + goto done; + + err = got_worktree_set_head_ref(worktree, resolved); + if (err) + goto done; + + err = delete_histedit_refs(worktree, repo); +done: + free(new_head_commit_id); + unlockerr = lock_worktree(worktree, LOCK_SH); + if (unlockerr && err == NULL) + err = unlockerr; + return err; +} + +const struct got_error * +got_worktree_histedit_skip_commit(struct got_worktree *worktree, + struct got_object_id *commit_id, struct got_repository *repo) +{ + const struct got_error *err; + char *commit_ref_name; + + err = get_histedit_commit_ref_name(&commit_ref_name, worktree); + if (err) + return err; + + err = store_commit_id(commit_ref_name, commit_id, repo); + if (err) + goto done; + + err = delete_ref(commit_ref_name, repo); +done: + free(commit_ref_name); + return err; +} blob - 682c9e1542649798fd47bc71d7532329397d40f9 blob + 191b50efa64f9e598f4f8865387623806d8453c5 --- regress/cmdline/Makefile +++ regress/cmdline/Makefile @@ -1,5 +1,5 @@ REGRESS_TARGETS=checkout update status log add rm diff commit \ - cherrypick backout rebase import + cherrypick backout rebase import histedit NOOBJ=Yes checkout: @@ -38,4 +38,7 @@ rebase: import: ./import.sh +histedit: + ./histedit.sh + .include blob - /dev/null blob + 98604e6814628faa295d9b0a155046626d878a18 (mode 755) --- /dev/null +++ regress/cmdline/histedit.sh @@ -0,0 +1,772 @@ +#!/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_histedit_no_op { + local testroot=`test_init histedit_no_op` + + local orig_commit=`git_show_head $testroot/repo` + + echo "modified alpha on master" > $testroot/repo/alpha + (cd $testroot/repo && git rm -q beta) + echo "new file on master" > $testroot/repo/epsilon/new + (cd $testroot/repo && git add epsilon/new) + git_commit $testroot/repo -m "committing changes" + local old_commit1=`git_show_head $testroot/repo` + + 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` + + got checkout -c $orig_commit $testroot/repo $testroot/wt > /dev/null + ret="$?" + if [ "$ret" != "0" ]; then + test_done "$testroot" "$ret" + return 1 + fi + + echo "pick $old_commit1" > $testroot/histedit-script + echo "pick $old_commit2" >> $testroot/histedit-script + + (cd $testroot/wt && got histedit -F $testroot/histedit-script \ + > $testroot/stdout) + + local new_commit1=`git_show_parent_commit $testroot/repo` + local new_commit2=`git_show_head $testroot/repo` + + local short_old_commit1=`trim_obj_id 28 $old_commit1` + local short_old_commit2=`trim_obj_id 28 $old_commit2` + local short_new_commit1=`trim_obj_id 28 $new_commit1` + local short_new_commit2=`trim_obj_id 28 $new_commit2` + + echo "G alpha" > $testroot/stdout.expected + echo "D beta" >> $testroot/stdout.expected + echo "A epsilon/new" >> $testroot/stdout.expected + echo "$short_old_commit1 -> $short_new_commit1: committing changes" \ + >> $testroot/stdout.expected + echo "G epsilon/zeta" >> $testroot/stdout.expected + echo -n "$short_old_commit2 -> $short_new_commit2: " \ + >> $testroot/stdout.expected + echo "committing to zeta on master" >> $testroot/stdout.expected + echo "Switching work tree to refs/heads/master" \ + >> $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 + + 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 master" > $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 (master)" > $testroot/stdout.expected + echo "commit $new_commit1" >> $testroot/stdout.expected + echo "commit $orig_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" +} + +function test_histedit_swap { + local testroot=`test_init histedit_swap` + + local orig_commit=`git_show_head $testroot/repo` + + echo "modified alpha on master" > $testroot/repo/alpha + (cd $testroot/repo && git rm -q beta) + echo "new file on master" > $testroot/repo/epsilon/new + (cd $testroot/repo && git add epsilon/new) + git_commit $testroot/repo -m "committing changes" + local old_commit1=`git_show_head $testroot/repo` + + 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` + + got checkout -c $orig_commit $testroot/repo $testroot/wt > /dev/null + ret="$?" + if [ "$ret" != "0" ]; then + test_done "$testroot" "$ret" + return 1 + fi + + echo "pick $old_commit2" > $testroot/histedit-script + echo "pick $old_commit1" >> $testroot/histedit-script + + (cd $testroot/wt && got histedit -F $testroot/histedit-script \ + > $testroot/stdout) + + local new_commit2=`git_show_parent_commit $testroot/repo` + local new_commit1=`git_show_head $testroot/repo` + + local short_old_commit1=`trim_obj_id 28 $old_commit1` + local short_old_commit2=`trim_obj_id 28 $old_commit2` + local short_new_commit1=`trim_obj_id 28 $new_commit1` + local short_new_commit2=`trim_obj_id 28 $new_commit2` + + echo "G epsilon/zeta" > $testroot/stdout.expected + echo -n "$short_old_commit2 -> $short_new_commit2: " \ + >> $testroot/stdout.expected + echo "committing to zeta on master" >> $testroot/stdout.expected + echo "G alpha" >> $testroot/stdout.expected + echo "D beta" >> $testroot/stdout.expected + echo "A epsilon/new" >> $testroot/stdout.expected + echo "$short_old_commit1 -> $short_new_commit1: committing changes" \ + >> $testroot/stdout.expected + echo "Switching work tree to refs/heads/master" \ + >> $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 + + 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 master" > $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_commit1 (master)" > $testroot/stdout.expected + echo "commit $new_commit2" >> $testroot/stdout.expected + echo "commit $orig_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" +} + +function test_histedit_drop { + local testroot=`test_init histedit_drop` + + local orig_commit=`git_show_head $testroot/repo` + + echo "modified alpha on master" > $testroot/repo/alpha + (cd $testroot/repo && git rm -q beta) + echo "new file on master" > $testroot/repo/epsilon/new + (cd $testroot/repo && git add epsilon/new) + git_commit $testroot/repo -m "committing changes" + local old_commit1=`git_show_head $testroot/repo` + + 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` + + got checkout -c $orig_commit $testroot/repo $testroot/wt > /dev/null + ret="$?" + if [ "$ret" != "0" ]; then + test_done "$testroot" "$ret" + return 1 + fi + + echo "drop $old_commit1" > $testroot/histedit-script + echo "pick $old_commit2" >> $testroot/histedit-script + + (cd $testroot/wt && got histedit -F $testroot/histedit-script \ + > $testroot/stdout) + + local new_commit1=`git_show_parent_commit $testroot/repo` + local new_commit2=`git_show_head $testroot/repo` + + local short_old_commit1=`trim_obj_id 28 $old_commit1` + local short_old_commit2=`trim_obj_id 28 $old_commit2` + local short_new_commit1=`trim_obj_id 28 $new_commit1` + local short_new_commit2=`trim_obj_id 28 $new_commit2` + + echo "$short_old_commit1 -> drop commit: committing changes" \ + > $testroot/stdout.expected + echo "G epsilon/zeta" >> $testroot/stdout.expected + echo -n "$short_old_commit2 -> $short_new_commit2: " \ + >> $testroot/stdout.expected + echo "committing to zeta on master" >> $testroot/stdout.expected + echo "Switching work tree to refs/heads/master" \ + >> $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 + + for f in alpha beta; do + echo "$f" > $testroot/content.expected + cat $testroot/wt/$f > $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 + done + + if [ -e $testroot/wt/new ]; then + echo "file new exists on disk but should not" >&2 + test_done "$testroot" "1" + 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 (master)" > $testroot/stdout.expected + echo "commit $orig_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" +} + +function test_histedit_fold { + local testroot=`test_init histedit_fold` + + local orig_commit=`git_show_head $testroot/repo` + + echo "modified alpha on master" > $testroot/repo/alpha + (cd $testroot/repo && git rm -q beta) + echo "new file on master" > $testroot/repo/epsilon/new + (cd $testroot/repo && git add epsilon/new) + git_commit $testroot/repo -m "committing changes" + local old_commit1=`git_show_head $testroot/repo` + + 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` + + got checkout -c $orig_commit $testroot/repo $testroot/wt > /dev/null + ret="$?" + if [ "$ret" != "0" ]; then + test_done "$testroot" "$ret" + return 1 + fi + + echo "fold $old_commit1" > $testroot/histedit-script + echo "pick $old_commit2" >> $testroot/histedit-script + echo "mesg committing folded changes" >> $testroot/histedit-script + + (cd $testroot/wt && got histedit -F $testroot/histedit-script \ + > $testroot/stdout) + + local new_commit1=`git_show_parent_commit $testroot/repo` + local new_commit2=`git_show_head $testroot/repo` + + local short_old_commit1=`trim_obj_id 28 $old_commit1` + local short_old_commit2=`trim_obj_id 28 $old_commit2` + local short_new_commit1=`trim_obj_id 28 $new_commit1` + local short_new_commit2=`trim_obj_id 28 $new_commit2` + + echo "G alpha" > $testroot/stdout.expected + echo "D beta" >> $testroot/stdout.expected + echo "A epsilon/new" >> $testroot/stdout.expected + echo "$short_old_commit1 -> fold commit: committing changes" \ + >> $testroot/stdout.expected + echo "G epsilon/zeta" >> $testroot/stdout.expected + echo -n "$short_old_commit2 -> $short_new_commit2: " \ + >> $testroot/stdout.expected + echo "committing folded changes" >> $testroot/stdout.expected + echo "Switching work tree to refs/heads/master" \ + >> $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 + + 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 master" > $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 (master)" > $testroot/stdout.expected + echo "commit $orig_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" +} + +function test_histedit_edit { + local testroot=`test_init histedit_edit` + + local orig_commit=`git_show_head $testroot/repo` + + echo "modified alpha on master" > $testroot/repo/alpha + (cd $testroot/repo && git rm -q beta) + echo "new file on master" > $testroot/repo/epsilon/new + (cd $testroot/repo && git add epsilon/new) + git_commit $testroot/repo -m "committing changes" + local old_commit1=`git_show_head $testroot/repo` + + 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` + + got checkout -c $orig_commit $testroot/repo $testroot/wt > /dev/null + ret="$?" + if [ "$ret" != "0" ]; then + test_done "$testroot" "$ret" + return 1 + fi + + echo "edit $old_commit1" > $testroot/histedit-script + echo "mesg committing changes" >> $testroot/histedit-script + echo "pick $old_commit2" >> $testroot/histedit-script + + (cd $testroot/wt && got histedit -F $testroot/histedit-script \ + > $testroot/stdout) + + local short_old_commit1=`trim_obj_id 28 $old_commit1` + local short_old_commit2=`trim_obj_id 28 $old_commit2` + + echo "G alpha" > $testroot/stdout.expected + echo "D beta" >> $testroot/stdout.expected + echo "A epsilon/new" >> $testroot/stdout.expected + echo "Stopping histedit for amending commit $old_commit1" \ + >> $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 "edited modified alpha on master" > $testroot/wt/alpha + + (cd $testroot/wt && got histedit -c > $testroot/stdout) + + local new_commit1=`git_show_parent_commit $testroot/repo` + local new_commit2=`git_show_head $testroot/repo` + + local short_new_commit1=`trim_obj_id 28 $new_commit1` + local short_new_commit2=`trim_obj_id 28 $new_commit2` + + echo "$short_old_commit1 -> $short_new_commit1: committing changes" \ + > $testroot/stdout.expected + echo "G epsilon/zeta" >> $testroot/stdout.expected + echo -n "$short_old_commit2 -> $short_new_commit2: " \ + >> $testroot/stdout.expected + echo "committing to zeta on master" >> $testroot/stdout.expected + echo "Switching work tree to refs/heads/master" \ + >> $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 "edited 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 + + 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 master" > $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 (master)" > $testroot/stdout.expected + echo "commit $new_commit1" >> $testroot/stdout.expected + echo "commit $orig_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" +} + +function test_histedit_fold_last_commit { + local testroot=`test_init histedit_fold_last_commit` + + local orig_commit=`git_show_head $testroot/repo` + + echo "modified alpha on master" > $testroot/repo/alpha + (cd $testroot/repo && git rm -q beta) + echo "new file on master" > $testroot/repo/epsilon/new + (cd $testroot/repo && git add epsilon/new) + git_commit $testroot/repo -m "committing changes" + local old_commit1=`git_show_head $testroot/repo` + + 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` + + got checkout -c $orig_commit $testroot/repo $testroot/wt > /dev/null + ret="$?" + if [ "$ret" != "0" ]; then + test_done "$testroot" "$ret" + return 1 + fi + + echo "pick $old_commit1" > $testroot/histedit-script + echo "fold $old_commit2" >> $testroot/histedit-script + echo "mesg committing folded changes" >> $testroot/histedit-script + + (cd $testroot/wt && got histedit -F $testroot/histedit-script \ + > $testroot/stdout 2> $testroot/stderr) + + ret="$?" + if [ "$ret" == "0" ]; then + echo "histedit succeeded unexpectedly" >&2 + test_done "$testroot" "1" + return 1 + fi + + echo "got: last commit in histedit script cannot be folded" \ + > $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_histedit_missing_commit { + local testroot=`test_init histedit_missing_commit` + + local orig_commit=`git_show_head $testroot/repo` + + echo "modified alpha on master" > $testroot/repo/alpha + (cd $testroot/repo && git rm -q beta) + echo "new file on master" > $testroot/repo/epsilon/new + (cd $testroot/repo && git add epsilon/new) + git_commit $testroot/repo -m "committing changes" + local old_commit1=`git_show_head $testroot/repo` + + 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` + + got checkout -c $orig_commit $testroot/repo $testroot/wt > /dev/null + ret="$?" + if [ "$ret" != "0" ]; then + test_done "$testroot" "$ret" + return 1 + fi + + echo "pick $old_commit1" > $testroot/histedit-script + echo "mesg committing changes" >> $testroot/histedit-script + + (cd $testroot/wt && got histedit -F $testroot/histedit-script \ + > $testroot/stdout 2> $testroot/stderr) + + ret="$?" + if [ "$ret" == "0" ]; then + echo "histedit succeeded unexpectedly" >&2 + test_done "$testroot" "1" + return 1 + fi + + echo "got: commit $old_commit2 missing from histedit script" \ + > $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_histedit_abort { + local testroot=`test_init histedit_abort` + + local orig_commit=`git_show_head $testroot/repo` + + echo "modified alpha on master" > $testroot/repo/alpha + (cd $testroot/repo && git rm -q beta) + echo "new file on master" > $testroot/repo/epsilon/new + (cd $testroot/repo && git add epsilon/new) + git_commit $testroot/repo -m "committing changes" + local old_commit1=`git_show_head $testroot/repo` + + 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` + + got checkout -c $orig_commit $testroot/repo $testroot/wt > /dev/null + ret="$?" + if [ "$ret" != "0" ]; then + test_done "$testroot" "$ret" + return 1 + fi + + echo "edit $old_commit1" > $testroot/histedit-script + echo "mesg committing changes" >> $testroot/histedit-script + echo "pick $old_commit2" >> $testroot/histedit-script + + (cd $testroot/wt && got histedit -F $testroot/histedit-script \ + > $testroot/stdout) + + local short_old_commit1=`trim_obj_id 28 $old_commit1` + local short_old_commit2=`trim_obj_id 28 $old_commit2` + + echo "G alpha" > $testroot/stdout.expected + echo "D beta" >> $testroot/stdout.expected + echo "A epsilon/new" >> $testroot/stdout.expected + echo "Stopping histedit for amending commit $old_commit1" \ + >> $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 "edited modified alpha on master" > $testroot/wt/alpha + + (cd $testroot/wt && got histedit -a > $testroot/stdout) + + local new_commit1=`git_show_parent_commit $testroot/repo` + local new_commit2=`git_show_head $testroot/repo` + + local short_new_commit1=`trim_obj_id 28 $new_commit1` + local short_new_commit2=`trim_obj_id 28 $new_commit2` + + echo "Switching work tree to refs/heads/master" \ + > $testroot/stdout.expected + echo "R alpha" >> $testroot/stdout.expected + echo "R beta" >> $testroot/stdout.expected + echo "R epsilon/new" >> $testroot/stdout.expected + echo "Histedit of refs/heads/master 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 + + for f in alpha beta; do + echo "$f" > $testroot/content.expected + cat $testroot/wt/$f > $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 + done + + echo "new file on master" > $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 "? epsilon/new" > $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 (master)" > $testroot/stdout.expected + echo "commit $new_commit1" >> $testroot/stdout.expected + echo "commit $orig_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_histedit_no_op +run_test test_histedit_swap +run_test test_histedit_drop +run_test test_histedit_fold +run_test test_histedit_edit +run_test test_histedit_fold_last_commit +run_test test_histedit_missing_commit +run_test test_histedit_abort