Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@xxxxxxxxx> --- Regression: "-M" is gone. Don't really want to mess up struct option for "-M" Makefile | 2 +- builtin-rebase.c | 992 ++++++++++++++++++++++++++++++++++++++++ builtin.h | 1 + contrib/examples/git-rebase.sh | 530 +++++++++++++++++++++ git-rebase.sh | 530 --------------------- git.c | 1 + 6 files changed, 1525 insertions(+), 531 deletions(-) create mode 100644 builtin-rebase.c create mode 100755 contrib/examples/git-rebase.sh delete mode 100755 git-rebase.sh diff --git a/Makefile b/Makefile index fdb39fa..fec6c40 100644 --- a/Makefile +++ b/Makefile @@ -302,7 +302,6 @@ SCRIPT_SH += git-parse-remote.sh SCRIPT_SH += git-pull.sh SCRIPT_SH += git-quiltimport.sh SCRIPT_SH += git-rebase--interactive.sh -SCRIPT_SH += git-rebase.sh SCRIPT_SH += git-repack.sh SCRIPT_SH += git-request-pull.sh SCRIPT_SH += git-sh-setup.sh @@ -597,6 +596,7 @@ BUILTIN_OBJS += builtin-prune-packed.o BUILTIN_OBJS += builtin-prune.o BUILTIN_OBJS += builtin-push.o BUILTIN_OBJS += builtin-read-tree.o +BUILTIN_OBJS += builtin-rebase.o BUILTIN_OBJS += builtin-receive-pack.o BUILTIN_OBJS += builtin-reflog.o BUILTIN_OBJS += builtin-remote.o diff --git a/builtin-rebase.c b/builtin-rebase.c new file mode 100644 index 0000000..51c3121 --- /dev/null +++ b/builtin-rebase.c @@ -0,0 +1,992 @@ +#include "builtin.h" +#include "cache.h" +#include "parse-options.h" +#include "diff.h" +#include "diffcore.h" +#include "revision.h" +#include "commit.h" +#include "run-command.h" +#include "dir.h" +#include "refs.h" +#include "quote.h" +#include "log-tree.h" + +static char const * const rebase_usage[] = { + "git rebase [-i | --interactive] [options] [--onto <newbase>]\n" + " <upstream> [<branch>]", + "git rebase [-i | --interactive] [options] --onto <newbase>\n" + " --root [<branch>]", + "git rebase --continue | --skip | --abort", + NULL +}; + +static const char *resolve_msg = + "When you have resolved this problem, run \"git rebase --continue\".\n" + "If you would prefer to skip this patch, instead run \"git rebase --skip\".\n" + "To restore the original branch and stop rebasing run \"git rebase --abort\".\n"; + +#define REBASE_ABORT 0x0001 +#define REBASE_CONTINUE 0x0002 +#define REBASE_FORCE 0x0004 +#define REBASE_IGNORE_DATE 0x0008 +#define REBASE_INTERACTIVE 0x0010 +#define REBASE_MERGE 0x0020 +#define REBASE_STAT 0x0040 +#define REBASE_NO_VERIFY 0x0080 +#define REBASE_PRESERVE_MERGES 0x0100 +#define REBASE_ROOT 0x0200 +#define REBASE_SKIP 0x0400 +#define REBASE_VERBOSE 0x0800 + +struct rebase_opt { + int flags; + int context; + const char *onto; + const char *upstream; + const char *branch; + const char *strategy; + const char *whitespace; +}; + +static int rebase_config(const char *var, const char *value, void *data) +{ + if (!strcmp(var, "rebase.stat")) { + if (git_config_bool(var, value)) + ((struct rebase_opt*)data)->flags |= REBASE_STAT; + return 0; + } + return 0; +} + +/* utility functions */ + +static char *load_string(char *git_pathname) +{ + struct strbuf sb = STRBUF_INIT; + const char *path = git_path(git_pathname); + + if (strbuf_read_file(&sb, path, 41) == -1) + die("Could not read %s", path); + + while (sb.len && sb.buf[sb.len-1] == '\n') { + sb.len--; + sb.buf[sb.len] = '\0'; + } + + return sb.buf; +} + +static void save_string(char *git_pathname, const char *str) +{ + const char *path = git_path(git_pathname); + int fd = open(path, O_CREAT | O_WRONLY, 0600); + if (fd < 0) + die("Could not open %s for writing", path); + write_in_full(fd, str, strlen(str)); + write_in_full(fd, "\n", 1); + close(fd); +} + +static void load_sha1(char *git_pathname, unsigned char *sha1) +{ + char *buf = load_string(git_pathname); + if (get_sha1_hex(buf, sha1) == -1) + die("Invalid SHA-1 in %s", git_path(git_pathname)); + free(buf); +} + +static void save_sha1(char *git_pathname, const unsigned char *sha1) +{ + save_string(git_pathname, sha1_to_hex(sha1)); +} + +static int load_int(char *git_pathname) +{ + char *buf = load_string(git_pathname); + int ret; + + if (sscanf(buf, "%d", &ret) != 1) + die("Failed to read number from %s", git_path(git_pathname)); + free(buf); + return ret; +} + +static void save_int(char *git_pathname, int number) +{ + const char *path = git_path(git_pathname); + FILE *fp = fopen(path, "w"); + if (!fp) + die("Could not open %s for writing", path); + fprintf(fp, "%d\n", number); + fclose(fp); +} + +static void get_commit(const char *name, unsigned char *sha1, struct commit **cmt) +{ + if (get_sha1(name, sha1)) + die("Failed to resolve SHA-1 from %s", name); + *cmt = lookup_commit(sha1); + if (!*cmt) + die("%s is not a commitish", name); +} + +static int continue_merge(int msgnum, unsigned char *prev_head) +{ + const char *argv_diff[] = { "--quiet", "--ignore-submodules", "HEAD", "--", NULL }; + struct strbuf result_line = STRBUF_INIT; + unsigned char cmt[20]; + struct rev_info rev; + struct stat st; + int i, result; + + if (stat(git_path("rebase-merge"), &st) || !S_ISDIR(st.st_mode)) + die("%s directory does not exist", git_path("rebase-merge")); + + /* git-am or git-merge-* may have been run, can no longer trust the old cache */ + discard_cache(); + if (read_cache() < 0) + die("Could not read the index"); + + for (i = 0;i < active_nr;i++) + if (ce_stage(active_cache[i])) + die("You still have unmerged paths in your index\n" + "did you forget to use git add?\n" + "%s", + resolve_msg); + + init_revisions(&rev, NULL); + rev.abbrev = 0; + setup_revisions(4, argv_diff, &rev, NULL); + result = run_diff_index(&rev, 0); + load_sha1("rebase-merge/current", cmt); + + if (DIFF_OPT_TST(&rev.diffopt, HAS_CHANGES)) { + struct child_process cp; + const char *argv[5] = {"commit", "--no-verify", "-C", sha1_to_hex(cmt), NULL }; + + memset(&cp, 0, sizeof(cp)); + cp.git_cmd = 1; + cp.argv = argv; + if (run_command(&cp)) + die("Commit failed, please do not call \"git commit\"\n" + "directly, but instead do one of the following:\n" + "%s", resolve_msg); + strbuf_addf(&result_line, "Committed: %04d ", msgnum); + } + else + strbuf_addf(&result_line, "Already applied: %04d ", msgnum); + + parse_commit(lookup_commit(cmt)); + format_commit_message(lookup_commit(cmt), "%f", &result_line, 0); + puts(result_line.buf); + strbuf_release(&result_line); + + if (get_sha1("HEAD^0", prev_head)) + die("Could not resolve HEAD commit"); + save_sha1("rebase-merge/prev_head", prev_head); + msgnum++; + save_int("rebase-merge/msgnum", msgnum); + return msgnum; +} + +static void call_merge(const char *strategy, int msgnum) +{ + struct strbuf sb = STRBUF_INIT; + struct strbuf sb2 = STRBUF_INIT; + struct strbuf sb_env1 = STRBUF_INIT; + struct strbuf sb_env2 = STRBUF_INIT; + unsigned char cmt[20]; + unsigned char head[20]; + struct child_process cp; + const char *full_ref; + int end, result, type; + const char *argv[5]; + char *onto_name, *head_hex, *cmt_hex; + + strbuf_addf(&sb, "rebase-merge/cmt.%d", msgnum); + load_sha1(sb.buf, cmt); + save_sha1("rebase-merge/current", cmt); + + full_ref = resolve_ref("HEAD", head, 1, &type); + if (!lookup_commit(head)) + die("HEAD does not point to a commit"); + msgnum = load_int("rebase-merge/msgnum"); + end = load_int("rebase-merge/end"); + + strbuf_setlen(&sb, 0); + strbuf_addf(&sb, "%s~%d", + !strncmp(full_ref, "refs/heads/", 11) ? full_ref + 11 : full_ref, + end - msgnum); + strbuf_addf(&sb_env1, "GITHEAD_%s", sha1_to_hex(cmt)); + setenv(sb_env1.buf, sb.buf, 1); + + onto_name = load_string("rebase-merge/onto_name"); + strbuf_setlen(&sb, 0); + strbuf_addf(&sb_env2, "GITHEAD_%s", sha1_to_hex(head)); + setenv(sb_env2.buf, onto_name, 1); + free(onto_name); + + strbuf_setlen(&sb, 0); + strbuf_addf(&sb, "merge-%s", strategy); + strbuf_addf(&sb2, "%s^", sha1_to_hex(cmt)); + head_hex = xstrdup(sha1_to_hex(head)); + cmt_hex = xstrdup(sha1_to_hex(cmt)); + argv[0] = sb.buf; + argv[1] = sb2.buf; + argv[2] = "--"; + argv[3] = head_hex; + argv[4] = cmt_hex; + argv[5] = NULL; + memset(&cp, 0, sizeof(cp)); + cp.git_cmd = 1; + cp.argv = argv; + result = run_command(&cp); + free(head_hex); + free(cmt_hex); + strbuf_release(&sb); + strbuf_release(&sb2); + switch (result) { + case 0: + unsetenv(sb_env1.buf); + strbuf_release(&sb_env1); + unsetenv(sb_env2.buf); + strbuf_release(&sb_env2); + break; + case -1: + argv[0] = "rerere"; + argv[1] = NULL; + run_command(&cp); + die(resolve_msg); + + case -2: + fprintf(stderr, "Strategy: %d %s failed, try another", result, strategy); + die(resolve_msg); + + default: + cmt_hex = xstrdup(sha1_to_hex(cmt)); + die("Unknown exit code (%d) from command: git-merge-%s %s^ -- HEAD %s", + result, strategy, cmt_hex, cmt_hex); + } +} + +static void move_to_original_branch(const unsigned char *onto, + const unsigned char *orig_head, + const char *head_name) +{ + struct strbuf sb = STRBUF_INIT; + unsigned char head[20]; + + if (strncmp(head_name, "refs/", 5)) + return; + + strbuf_addf(&sb, "rebase finished: %s onto %s", head_name, sha1_to_hex(onto)); + if (get_sha1("HEAD", head)) + die("Could not resolve HEAD"); + update_ref(sb.buf, head_name, head, orig_head, 0, DIE_ON_ERR); + create_symref("HEAD", head_name, NULL); + strbuf_release(&sb); +} + +static int finish_rebase_merge(const unsigned char *onto, + const unsigned char *orig_head, + const char *head_name) +{ + struct strbuf sb = STRBUF_INIT; + move_to_original_branch(onto, orig_head, head_name); + strbuf_addstr(&sb, git_path("rebase-merge")); + remove_dir_recursively(&sb, 0); + strbuf_release(&sb); + puts("All done."); + return 0; +} + +static void check_rebase_in_progress() +{ + struct stat st; + if ((!stat(git_path("rebase-merge"), &st) && S_ISDIR(st.st_mode)) || + (!stat(git_path("rebase-apply"), &st) && S_ISDIR(st.st_mode))) + return; + die("No rebase in progress?"); +} + +static int cmd_continue(const struct rebase_opt *opt) +{ + struct stat st; + struct rev_info rev; + struct child_process cp; + const char *argv_am[] = { "am", "--resolved", "--3way", "--resolvemsg", resolve_msg, NULL }; + char *head_name; + unsigned char onto[20], orig_head[20]; + int ret; + + check_rebase_in_progress(); + + if (read_cache() < 0) + die("Could not read the index"); + + init_revisions(&rev, NULL); + rev.abbrev = 0; + DIFF_OPT_SET(&rev.diffopt, QUIET); + DIFF_OPT_SET(&rev.diffopt, IGNORE_SUBMODULES); + run_diff_files(&rev, DIFF_SILENT_ON_REMOVED); + if (DIFF_OPT_TST(&rev.diffopt, HAS_CHANGES)) { + printf("You must edit all merge conflicts and then\n" + "mark them as resolved using git add\n"); + return 1; + } + + if (!stat(git_path("rebase-merge"), &st) && S_ISDIR(st.st_mode)) { + unsigned char prev_head[20], onto[20], orig_head[20]; + char *head_name; + int msgnum, end; + + load_sha1("rebase-merge/prev_head", prev_head); + load_sha1("rebase-merge/onto", onto); + end = load_int("rebase-merge/end"); + msgnum = load_int("rebase-merge/msgnum"); + + msgnum = continue_merge(msgnum, prev_head); + while (msgnum <= end) { + call_merge(opt->strategy, msgnum); + msgnum = continue_merge(msgnum, prev_head); + } + + load_sha1("rebase-merge/orig-head", orig_head); + head_name = load_string("rebase-merge/head-name"); + + ret = finish_rebase_merge(onto, orig_head, head_name); + free(head_name); + return ret; + } + + head_name = load_string("rebase-apply/head-name"); + load_sha1("rebase-apply/onto", onto); + load_sha1("rebase-apply/orig-head", orig_head); + + memset(&cp, 0, sizeof(cp)); + cp.git_cmd = 1; + cp.argv = argv_am; + ret = run_command(&cp); + if (IS_RUN_COMMAND_ERR(ret)) + die("Could not run git am"); + if (!ret) + move_to_original_branch(onto, orig_head, head_name); + free(head_name); + return ret < 0 ? -ret : 0; +} + +static int cmd_abort() +{ + const char *argv_rerere[] = { "rerere", "clear", NULL }; + const char *argv_reset[] = { "reset", "--hard", NULL /* placeholder */, NULL }; + struct child_process cp; + struct stat st; + unsigned char onto[20], orig_head[20]; + char *head_name; + struct strbuf sb = STRBUF_INIT; + + check_rebase_in_progress(); + memset(&cp, 0, sizeof(cp)); + cp.git_cmd = 1; + cp.argv = argv_rerere; + run_command(&cp); + + if (!stat(git_path("rebase-merge"), &st) && S_ISDIR(st.st_mode)) { + strbuf_addstr(&sb, git_path("rebase-merge")); + head_name = load_string("rebase-merge/head-name"); + load_sha1("rebase-merge/onto", onto); + load_sha1("rebase-merge/orig-head", orig_head); + move_to_original_branch(onto, orig_head, head_name); + } + else { + strbuf_addstr(&sb, git_path("rebase-apply")); + head_name = load_string("rebase-apply/head-name"); + load_sha1("rebase-apply/onto", onto); + load_sha1("rebase-apply/orig-head", orig_head); + move_to_original_branch(onto, orig_head, head_name); + } + free(head_name); + + cp.argv = argv_reset; + argv_reset[2] = sha1_to_hex(orig_head); + if (run_command(&cp)) + die("Failed running git reset"); + remove_dir_recursively(&sb, 0); + strbuf_release(&sb); + return 0; +} + +static int cmd_skip(const struct rebase_opt *opt) +{ + const char *argv_reset[] = { "reset", "--hard", "HEAD", NULL }; + const char *argv_am[] = { "am", "-3", "--skip", "--resolvemsg", resolve_msg, NULL }; + struct child_process cp; + char *head_name; + unsigned char orig_head[20], onto[20]; + struct stat st; + int ret; + + check_rebase_in_progress(); + memset(&cp, 0, sizeof(cp)); + cp.git_cmd = 1; + cp.argv = argv_reset; + ret = run_command(&cp); + if (IS_RUN_COMMAND_ERR(ret)) + die("Failed to run git reset"); + if (ret) + return -ret; + if (!stat(git_path("rebase-merge"), &st) && S_ISDIR(st.st_mode)) { + const char *argv_rerere[] = { "rerere", "clear", NULL }; + unsigned char prev_head[20], onto[20], orig_head[20]; + int msgnum, end; + char *head_name; + + cp.argv = argv_rerere; + run_command(&cp); + load_sha1("rebase-merge/prev_head", prev_head); + end = load_int("rebase-merge/end"); + msgnum = load_int("rebase-merge/msgnum") + 1; + load_sha1("rebase-merge/onto", onto); + while (msgnum <= end) { + call_merge(opt->strategy, msgnum); + msgnum = continue_merge(msgnum, prev_head); + } + + load_sha1("rebase-merge/orig-head", orig_head); + head_name = load_string("rebase-merge/head-name"); + + ret = finish_rebase_merge(onto, orig_head, head_name); + free(head_name); + return ret; + } + + head_name = load_string("rebase-apply/head-name"); + load_sha1("rebase-apply/onto", onto); + load_sha1("rebase-apply/orig-head", orig_head); + + cp.argv = argv_am; + ret = run_command(&cp); + if (IS_RUN_COMMAND_ERR(ret)) + die("Failed to run git am"); + if (ret) + return -ret; + + move_to_original_branch(onto, orig_head, head_name); + free(head_name); + return ret < 0 ? -ret : 0; +} + +static void run_interactive(int argc, const char **argv) +{ + int ret, i; + struct child_process cp; + for (i = 1;i < argc;i++) { + if (!strcmp(argv[i], "-i") || !strcmp(argv[i], "--interactive")) + break; + if (!strcmp(argv[i], "-p") || !strcmp(argv[i], "--preserve-merges")) { + setenv("GIT_EDITOR", ":", 1); + break; + } + } + + if (i == argc) { + struct stat st; + if (stat(git_path("rebase-merge/interactive"), &st) || + !S_ISREG(st.st_mode)) + return; + } + + memset(&cp, 0, sizeof(cp)); + cp.git_cmd = 1; + cp.argv = argv; + argv[0] = "rebase--interactive"; + ret = run_command(&cp); + if (IS_RUN_COMMAND_ERR(ret)) + die("Failed to run git rebase--interactive"); + exit(ret); +} + +/* from diff.c */ +static void strip_prefix(int prefix_length, const char **namep, const char **otherp) +{ + /* Strip the prefix but do not molest /dev/null and absolute paths */ + if (*namep && **namep != '/') + *namep += prefix_length; + if (*otherp && **otherp != '/') + *otherp += prefix_length; +} + +static void die_dirty_cache(struct diff_queue_struct *q, + struct diff_options *opt, + void *data) +{ + int i; + + if (!q->nr) + return; + + fprintf(stderr,"cannot rebase: your index contains uncommitted changes\n"); + + for (i = 0; i < q->nr; i++) { + struct diff_filepair *p = q->queue[i]; + + fprintf(stderr, "%c ",p->status); + + if (p->status == DIFF_STATUS_COPIED || + p->status == DIFF_STATUS_RENAMED) { + const char *name_a, *name_b; + name_a = p->one->path; + name_b = p->two->path; + strip_prefix(opt->prefix_length, &name_a, &name_b); + write_name_quoted(name_a, stderr, '\t'); + write_name_quoted(name_b, stderr, '\n'); + } else { + const char *name_a, *name_b; + name_a = p->one->mode ? p->one->path : p->two->path; + name_b = NULL; + strip_prefix(opt->prefix_length, &name_a, &name_b); + write_name_quoted(name_a, stderr, '\n'); + } + } + exit(1); +} + +static void abort_continue_skip(const struct rebase_opt *opt) +{ + int count = 0; + if (opt->flags & REBASE_CONTINUE) count++; + if (opt->flags & REBASE_SKIP) count++; + if (opt->flags & REBASE_ABORT) count++; + if (count > 1) + die("--continue, --skip and --abort are mutually exclusive"); + if (!count) + return; + + /* REBASE_STAT may be set by git config */ + if (opt->flags & ~(REBASE_CONTINUE | REBASE_SKIP | REBASE_ABORT | REBASE_STAT)) + die("--continue, --skip and --abort do not take any other argument"); + + if (opt->flags & REBASE_CONTINUE) exit(cmd_continue(opt)); + if (opt->flags & REBASE_ABORT) exit(cmd_abort()); + if (opt->flags & REBASE_SKIP) exit(cmd_skip(opt)); + + die("Unexpected operation"); +} + +static int cmd_apply_rebase(const struct rebase_opt *opt, + struct object *onto, + struct object *upstream, + struct object *orig_head, + const char *onto_name, + const char *head_name) +{ + struct strbuf sb = STRBUF_INIT; + const char *argv_fp[] = { "format-patch", "-k", "--stdout", "--full-index", + "--ignore-if-in-upstream", NULL /* placeholder */, NULL }; + const char *argv_am[] = { "am", "--rebasing", "--resolvemsg", resolve_msg, + NULL, NULL, /* --whitespace */ + NULL, /* -Cn */ + NULL, /* --ignore-date */ + NULL }; + const char **argp; + struct child_process cp_fp, cp_am; + int fd[2], ret; + char context_buf[10]; + + strbuf_addstr(&sb, sha1_to_hex(upstream->sha1)); + strbuf_addstr(&sb, ".."); + strbuf_addstr(&sb, sha1_to_hex(orig_head->sha1)); + + memset(&cp_fp, 0, sizeof(cp_fp)); + memset(&cp_am, 0, sizeof(cp_am)); + + if (pipe(fd)) + die("Failed to make pipe"); + + cp_fp.git_cmd = 1; + cp_fp.argv = argv_fp; + cp_fp.out = fd[1]; + argv_fp[5] = sb.buf; + + argp = argv_am+4; + if (opt->whitespace) { + *argp++ = "--whitespace"; + *argp++ = opt->whitespace; + } + if (opt->flags & REBASE_IGNORE_DATE) + *argp++ = "--ignore-date"; + if (opt->context >= 0) { + snprintf(context_buf, 10, "-C%d", opt->context); + *argp++ = context_buf; + } + cp_am.git_cmd = 1; + cp_am.argv = argv_am; + cp_am.in = fd[0]; + + if (start_command(&cp_fp)) + die("Could not run git format-patch"); + if (start_command(&cp_am)) + die("Could not run git am"); + + ret = finish_command(&cp_am); + strbuf_release(&sb); + close(fd[0]); + close(fd[1]); + + if (IS_RUN_COMMAND_ERR(ret)) + die("Failed to run 'git format-patch|git am'"); + if (!ret) + move_to_original_branch(onto->sha1, orig_head->sha1, head_name); + else { + struct stat st; + + if (!stat(git_path("rebase-apply"), &st) && S_ISDIR(st.st_mode)) { + save_string("rebase-apply/head-name", head_name); + save_sha1("rebase-apply/onto", onto->sha1); + save_sha1("rebase-apply/orig-head", orig_head->sha1); + } + } + return ret; +} + +static int cmd_merge_rebase(const struct rebase_opt *opt, + struct object *onto, + struct object *upstream, + struct object *orig_head, + const char *onto_name, + const char *head_name) +{ + int msgnum, end = 0; + struct rev_info revs; + struct commit *cmt; + struct strbuf sb = STRBUF_INIT; + unsigned char prev_head[20]; + + /* git rev-list --reverse --no-merges $onto..$orig_head */ + init_revisions(&revs, NULL); + revs.abbrev = 0; + revs.reverse = 1; + revs.no_merges = 1; + upstream->flags |= UNINTERESTING; + add_pending_object(&revs, upstream, NULL); + orig_head->flags &= ~UNINTERESTING; + add_pending_object(&revs, orig_head, NULL); + + if (prepare_revision_walk(&revs)) + die("revision walk setup failed"); + + memcpy(prev_head, orig_head->sha1, 20); + + mkdir(git_path("rebase-merge"), 0700); + save_sha1 ("rebase-merge/onto", onto->sha1); + save_string("rebase-merge/onto_name", onto_name); + save_sha1 ("rebase-merge/prev_head", prev_head); + save_sha1 ("rebase-merge/orig-head", orig_head->sha1); + save_string("rebase-merge/head-name", head_name); + + strbuf_addstr(&sb, "rebase-merge/cmt."); + while ((cmt = get_revision(&revs)) != NULL) { + end++; + strbuf_addf(&sb, "%d", end); + save_sha1(sb.buf, cmt->object.sha1); + strbuf_setlen(&sb, 17); + } + strbuf_release(&sb); + + msgnum = 1; + + save_int("rebase-merge/msgnum", msgnum); + save_int("rebase-merge/end", end); + + while (msgnum <= end) { + call_merge(opt->strategy, msgnum); + msgnum = continue_merge(msgnum, prev_head); + } + + return finish_rebase_merge(onto->sha1, orig_head->sha1, head_name); +} + +int cmd_rebase(int argc, const char **argv, const char *prefix) +{ + struct rebase_opt opt; + struct option options[] = { + OPT_STRING(0,"onto", &opt.onto, "newbase", "starting point at which to create new commits"), + OPT_STRING('s',"strategy", &opt.strategy, "strategy", "merge strategy to be used"), + OPT_STRING(0,"whitespace", &opt.whitespace, "action", "to be passed to \"git apply\""), + OPT_INTEGER('C', NULL, &opt.context, "surrounding context lines to be matched"), + OPT_BIT ('i', "interactive", &opt.flags, "interactive mode", REBASE_INTERACTIVE), + OPT_BIT ('v', "verbose", &opt.flags, "be verbose", REBASE_VERBOSE | REBASE_STAT), + OPT_BIT ('f', "force-rebase",&opt.flags, "force rebase", REBASE_FORCE), + OPT_BIT ('p', "preserve-merges",&opt.flags, "preserve merges during rebase", REBASE_PRESERVE_MERGES), + OPT_BIT ('m', "merge", &opt.flags, "use merge strategies to rebase", REBASE_MERGE), + OPT_BIT (0, "no-verify", &opt.flags, "bypass pre-rebase hook", REBASE_NO_VERIFY), + OPT_NEGBIT('n', "no-stat", &opt.flags, "do not show diffstat as part of rebase process", REBASE_STAT), + OPT_BIT (0, "committer-date-is-author-date", &opt.flags, "to be passed to \"git am\"", REBASE_IGNORE_DATE), + OPT_BIT (0, "ignore-date", &opt.flags, "to be passed to \"git am\"", REBASE_IGNORE_DATE), + OPT_BIT (0, "stat", &opt.flags, "show diffstat as part of rebase process", REBASE_STAT), + OPT_BIT (0, "root", &opt.flags, "rebase all reachable commits from <branch>", REBASE_ROOT), + OPT_BIT (0, "continue", &opt.flags, "restart rebase after having resolved a merge conflict", REBASE_CONTINUE), + OPT_BIT (0, "skip", &opt.flags, "restart rebase by skipping the current patch", REBASE_SKIP), + OPT_BIT (0, "abort", &opt.flags, "restore original branch and abort rebase", REBASE_ABORT), + OPT_END() + }; + + unsigned char upstream_sha1[20], branch_sha1[20], onto_sha1[20], head_sha1[20]; + struct commit *upstream_cmt, *branch_cmt, *onto_cmt, *head_cmt; + const char *argv_checkout_onto[] = { "checkout", "-q", NULL /* placeholder */, NULL }; + int (*rebase_func)(const struct rebase_opt *, struct object *, + struct object *, struct object *, + const char *, const char *); + const char *switch_to = NULL; + struct commit_list *mb_cmts; + struct child_process cp; + struct rev_info revs; + struct stat st; + char *head_name; + char *buf; + int do_merge; + + if (!stat(git_path("rebase-apply/applying"), &st) && S_ISREG(st.st_mode)) + die("It looks like git-am is in progress. Cannot rebase."); + + setenv("GIT_REFLOG_ACTION", "rebase", 1); + memset(&opt, 0, sizeof(opt)); + opt.context = -1; + git_config(rebase_config, &opt); + + run_interactive(argc, argv); + + if (argc == 1) { + int rebase_merge_exists = !stat(git_path("rebase-merge"), &st) && S_ISDIR(st.st_mode); + int rebase_apply_exists = !stat(git_path("rebase-apply"), &st) && S_ISDIR(st.st_mode); + if (rebase_merge_exists || rebase_apply_exists) { + if (rebase_merge_exists || + (!stat(git_path("rebase-apply/rebasing"), &st) && S_ISREG(st.st_mode))) + die("A rebase is in progress, try --continue, --skip or --abort."); + die("No arguments given and %s already exists.", git_path("rebase-apply")); + } + else + usage_with_options(rebase_usage, options); + } + + argc = parse_options(argc, argv, options, rebase_usage, 0); + + do_merge = (opt.flags & REBASE_MERGE) || opt.strategy; + if (!opt.strategy) + opt.strategy = "recursive"; + + abort_continue_skip(&opt); + + if (opt.whitespace && + (!strcmp(opt.whitespace, "fix") || + !strcmp(opt.whitespace, "strip"))) + opt.flags |= REBASE_FORCE; + + if (opt.flags & REBASE_ROOT) { + if (argc > 1) + die("Invalid argument"); + if (argc) + opt.branch = *argv; + if (!opt.onto) + die("--root must be used with --onto"); + } + else { + /* without --root only takes either 1 or 2 arguments */ + if (argc == 0 || argc > 2 || + (argc == 2 && (opt.flags & REBASE_ROOT))) + die("Invalid argument"); + if (argc) { + opt.upstream = *argv++; + if (argc) + opt.branch = *argv; + } + } + + /* No rebase ongoing */ + if (do_merge) { + if (!stat(git_path("rebase-merge"), &st) && S_ISREG(st.st_mode)) + die("previous rebase directory rebase-merge still exists." + "Try git rebase (--continue | --abort | --skip)"); + } + else { + if (!mkdir(git_path("rebase-apply"), 0700)) + rmdir(git_path("rebase-apply")); + else + die("It seems that I cannot create a rebase-apply directory, and\n" + "I wonder if you are in the middle of the patch application or another\n" + "rebase. If that is not the case, please\n" + "\trm -fr \"%s\"/rebase_apply\n" + "and run me again. I am stopping in case you still have something\n" + "valuable there.", + get_git_dir()); + } + + /* worktree clean */ + if (read_cache() < 0) + die("Could not read the index"); + if (refresh_cache(REFRESH_IGNORE_SUBMODULES)) + die("cannot rebase: you have unstaged changes"); + + /* cache clean */ + init_revisions(&revs, NULL); + revs.abbrev = 0; + revs.diffopt.output_format = DIFF_FORMAT_CALLBACK; + revs.diffopt.format_callback = die_dirty_cache; + DIFF_OPT_SET(&revs.diffopt, IGNORE_SUBMODULES); + DIFF_OPT_SET(&revs.diffopt, RECURSIVE); + if (get_sha1("HEAD", head_sha1)) + die("Could not resolve HEAD"); + head_cmt = lookup_commit(head_sha1); + add_pending_object(&revs, &head_cmt->object, NULL); + run_diff_index(&revs, 1); + + if (!(opt.flags & REBASE_NO_VERIFY) && + run_hook(NULL, "pre-rebase", opt.flags & REBASE_ROOT ? "--root" : opt.upstream, opt.branch, NULL)) { + die("The pre-rebase hook refused to rebase."); + } + + /* commit validation */ + if (!(opt.flags & REBASE_ROOT)) + get_commit(opt.upstream, upstream_sha1, &upstream_cmt); + get_commit("HEAD", head_sha1, &head_cmt); + if (opt.onto) + get_commit(opt.onto, onto_sha1, &onto_cmt); + else { + opt.onto = opt.upstream; + memcpy(onto_sha1, upstream_sha1, 20); + onto_cmt = upstream_cmt; + } + + if (opt.branch) { + struct strbuf sb = STRBUF_INIT; + const char *full_ref; + int type; + + switch_to = opt.branch; + strbuf_addf(&sb, "refs/heads/%s", opt.branch); + full_ref = resolve_ref(sb.buf, branch_sha1, 1, &type); + if (full_ref) + head_name = xstrdup(full_ref); + else { + if (!get_sha1(opt.branch, branch_sha1)) + head_name = xstrdup("detached HEAD"); + else + die("Failed to parse %s", opt.branch); + } + strbuf_release(&sb); + } + else { + const char *full_ref; + int type; + + opt.branch = "HEAD"; + full_ref = resolve_ref(opt.branch, branch_sha1, 1, &type); + if (full_ref) { + head_name = xstrdup(full_ref); + if (!strncmp(full_ref, "refs/heads/", 11)) + opt.branch = head_name+11; + } + else { + head_name = xstrdup("detached HEAD"); + if (get_sha1(opt.branch, branch_sha1)) + die("Could not resolve HEAD"); + } + } + + branch_cmt = lookup_commit(branch_sha1); + if (!branch_cmt) + die("Failed to lookup commit for %s", branch_sha1); + + memset(&cp, 0, sizeof(cp)); + cp.git_cmd = 1; + + /* + * Now we are rebasing commits upstream_sha1..branch_sha1 + * (or with --root, everything leading up to branch_sha1) + * on top of onto_sha1 + */ + + /* + * Check if we are already based on onto_sha1 with linear history, + * but this should be done only when upstream and onto are the same. + */ + if (!(opt.flags & REBASE_ROOT) && !memcmp(upstream_sha1, onto_sha1, 20)) { + struct commit *cmt = branch_cmt; + while (1) { + if (parse_commit(cmt)) + die("Could not parse commit"); + + if (!cmt->parents || cmt->parents->next) + break; + + if (cmt->parents->item != onto_cmt) { + cmt = cmt->parents->item; + continue; + } + + if (opt.flags & REBASE_FORCE) { + printf("Current branch %s is up to date, rebase forced.\n", opt.branch); + break; + } + if (switch_to) { + const char *argv_checkout[] = { "checkout", switch_to, NULL }; + cp.argv = argv_checkout; + if (run_command(&cp)) + die("Failed to switch to branch %s", switch_to); + } + fprintf(stderr, "Current branch %s is up to date.\n", opt.branch); + return 0; + } + } + + /* Detach HEAD and reset the tree */ + printf("First, rewinding head to replay your work on top of it...\n"); + buf = xmalloc(43); + memcpy(buf, sha1_to_hex(onto_sha1), 40); + buf[40] = '^'; + buf[41] = '0'; + buf[42] = '\0'; + argv_checkout_onto[2] = buf; + cp.argv = argv_checkout_onto; + if (run_command(&cp)) + die("could not detach HEAD"); + update_ref(NULL, "ORIG_HEAD", branch_sha1, NULL, REF_NODEREF, DIE_ON_ERR); + + /* diffstat */ + mb_cmts = get_merge_bases(onto_cmt, branch_cmt, 0); + if (mb_cmts && (opt.flags & REBASE_STAT)) { + struct rev_info diffrev; + struct commit *mb_cmt = mb_cmts->item; + + if (opt.flags & REBASE_VERBOSE) + printf("Changes from %s to %s:\n", sha1_to_hex(mb_cmt->object.sha1), opt.onto); + init_revisions(&diffrev, NULL); + diffrev.abbrev = 0; + DIFF_OPT_SET(&diffrev.diffopt, RECURSIVE); + diffrev.diffopt.output_format = DIFF_FORMAT_DIFFSTAT | DIFF_FORMAT_SUMMARY; + if (parse_commit(mb_cmt) || parse_commit(onto_cmt)) + die("Could not parse commits"); + diff_tree_sha1(mb_cmt->tree->object.sha1, onto_cmt->tree->object.sha1, "", &diffrev.diffopt); + log_tree_diff_flush(&diffrev); + } + + /* + * If the opt.onto is a proper descendant of the tip of the branch, + * then we just fast forward it. + */ + if (mb_cmts && mb_cmts->item == branch_cmt) { + fprintf(stderr, "Fast-forwarded %s to %s", opt.branch, opt.onto); + move_to_original_branch(onto_sha1, branch_sha1, head_name); + return 0; + } + + /* + * if --root, revision range will be onto..orig_head + * otherwise upstream..orig_head + */ + + rebase_func = do_merge ? cmd_merge_rebase : cmd_apply_rebase; + return rebase_func(&opt, &onto_cmt->object, + opt.flags & REBASE_ROOT ? &onto_cmt->object : &upstream_cmt->object, + &branch_cmt->object, opt.onto, head_name); +} diff --git a/builtin.h b/builtin.h index 425ff8e..53bf2b0 100644 --- a/builtin.h +++ b/builtin.h @@ -80,6 +80,7 @@ extern int cmd_prune(int argc, const char **argv, const char *prefix); extern int cmd_prune_packed(int argc, const char **argv, const char *prefix); extern int cmd_push(int argc, const char **argv, const char *prefix); extern int cmd_read_tree(int argc, const char **argv, const char *prefix); +extern int cmd_rebase(int argc, const char **argv, const char *prefix); extern int cmd_receive_pack(int argc, const char **argv, const char *prefix); extern int cmd_reflog(int argc, const char **argv, const char *prefix); extern int cmd_remote(int argc, const char **argv, const char *prefix); diff --git a/contrib/examples/git-rebase.sh b/contrib/examples/git-rebase.sh new file mode 100755 index 0000000..b83fd3f --- /dev/null +++ b/contrib/examples/git-rebase.sh @@ -0,0 +1,530 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano. +# + +USAGE='[--interactive | -i] [-v] [--force-rebase | -f] [--onto <newbase>] [<upstream>|--root] [<branch>]' +LONG_USAGE='git-rebase replaces <branch> with a new branch of the +same name. When the --onto option is provided the new branch starts +out with a HEAD equal to <newbase>, otherwise it is equal to <upstream> +It then attempts to create a new commit for each commit from the original +<branch> that does not exist in the <upstream> branch. + +It is possible that a merge failure will prevent this process from being +completely automatic. You will have to resolve any such merge failure +and run git rebase --continue. Another option is to bypass the commit +that caused the merge failure with git rebase --skip. To restore the +original <branch> and remove the .git/rebase-apply working files, use the +command git rebase --abort instead. + +Note that if <branch> is not specified on the command line, the +currently checked out branch is used. + +Example: git-rebase master~1 topic + + A---B---C topic A'\''--B'\''--C'\'' topic + / --> / + D---E---F---G master D---E---F---G master +' + +SUBDIRECTORY_OK=Yes +OPTIONS_SPEC= +. git-sh-setup +set_reflog_action rebase +require_work_tree +cd_to_toplevel + +OK_TO_SKIP_PRE_REBASE= +RESOLVEMSG=" +When you have resolved this problem run \"git rebase --continue\". +If you would prefer to skip this patch, instead run \"git rebase --skip\". +To restore the original branch and stop rebasing run \"git rebase --abort\". +" +unset newbase +strategy=recursive +do_merge= +dotest="$GIT_DIR"/rebase-merge +prec=4 +verbose= +diffstat=$(git config --bool rebase.stat) +git_am_opt= +rebase_root= +force_rebase= + +continue_merge () { + test -n "$prev_head" || die "prev_head must be defined" + test -d "$dotest" || die "$dotest directory does not exist" + + unmerged=$(git ls-files -u) + if test -n "$unmerged" + then + echo "You still have unmerged paths in your index" + echo "did you forget to use git add?" + die "$RESOLVEMSG" + fi + + cmt=`cat "$dotest/current"` + if ! git diff-index --quiet --ignore-submodules HEAD -- + then + if ! git commit --no-verify -C "$cmt" + then + echo "Commit failed, please do not call \"git commit\"" + echo "directly, but instead do one of the following: " + die "$RESOLVEMSG" + fi + printf "Committed: %0${prec}d " $msgnum + else + printf "Already applied: %0${prec}d " $msgnum + fi + git rev-list --pretty=oneline -1 "$cmt" | sed -e 's/^[^ ]* //' + + prev_head=`git rev-parse HEAD^0` + # save the resulting commit so we can read-tree on it later + echo "$prev_head" > "$dotest/prev_head" + + # onto the next patch: + msgnum=$(($msgnum + 1)) + echo "$msgnum" >"$dotest/msgnum" +} + +call_merge () { + cmt="$(cat "$dotest/cmt.$1")" + echo "$cmt" > "$dotest/current" + hd=$(git rev-parse --verify HEAD) + cmt_name=$(git symbolic-ref HEAD 2> /dev/null || echo HEAD) + msgnum=$(cat "$dotest/msgnum") + end=$(cat "$dotest/end") + eval GITHEAD_$cmt='"${cmt_name##refs/heads/}~$(($end - $msgnum))"' + eval GITHEAD_$hd='$(cat "$dotest/onto_name")' + export GITHEAD_$cmt GITHEAD_$hd + git-merge-$strategy "$cmt^" -- "$hd" "$cmt" + rv=$? + case "$rv" in + 0) + unset GITHEAD_$cmt GITHEAD_$hd + return + ;; + 1) + git rerere + die "$RESOLVEMSG" + ;; + 2) + echo "Strategy: $rv $strategy failed, try another" 1>&2 + die "$RESOLVEMSG" + ;; + *) + die "Unknown exit code ($rv) from command:" \ + "git-merge-$strategy $cmt^ -- HEAD $cmt" + ;; + esac +} + +move_to_original_branch () { + test -z "$head_name" && + head_name="$(cat "$dotest"/head-name)" && + onto="$(cat "$dotest"/onto)" && + orig_head="$(cat "$dotest"/orig-head)" + case "$head_name" in + refs/*) + message="rebase finished: $head_name onto $onto" + git update-ref -m "$message" \ + $head_name $(git rev-parse HEAD) $orig_head && + git symbolic-ref HEAD $head_name || + die "Could not move back to $head_name" + ;; + esac +} + +finish_rb_merge () { + move_to_original_branch + rm -r "$dotest" + echo "All done." +} + +is_interactive () { + while test $# != 0 + do + case "$1" in + -i|--interactive) + interactive_rebase=explicit + break + ;; + -p|--preserve-merges) + interactive_rebase=implied + ;; + esac + shift + done + + if [ "$interactive_rebase" = implied ]; then + GIT_EDITOR=: + export GIT_EDITOR + fi + + test -n "$interactive_rebase" || test -f "$dotest"/interactive +} + +run_pre_rebase_hook () { + if test -z "$OK_TO_SKIP_PRE_REBASE" && + test -x "$GIT_DIR/hooks/pre-rebase" + then + "$GIT_DIR/hooks/pre-rebase" ${1+"$@"} || { + echo >&2 "The pre-rebase hook refused to rebase." + exit 1 + } + fi +} + +test -f "$GIT_DIR"/rebase-apply/applying && + die 'It looks like git-am is in progress. Cannot rebase.' + +is_interactive "$@" && exec git-rebase--interactive "$@" + +if test $# -eq 0 +then + test -d "$dotest" -o -d "$GIT_DIR"/rebase-apply || usage + test -d "$dotest" -o -f "$GIT_DIR"/rebase-apply/rebasing && + die 'A rebase is in progress, try --continue, --skip or --abort.' + die "No arguments given and $GIT_DIR/rebase-apply already exists." +fi + +while test $# != 0 +do + case "$1" in + --no-verify) + OK_TO_SKIP_PRE_REBASE=yes + ;; + --continue) + test -d "$dotest" -o -d "$GIT_DIR"/rebase-apply || + die "No rebase in progress?" + + git diff-files --quiet --ignore-submodules || { + echo "You must edit all merge conflicts and then" + echo "mark them as resolved using git add" + exit 1 + } + if test -d "$dotest" + then + prev_head=$(cat "$dotest/prev_head") + end=$(cat "$dotest/end") + msgnum=$(cat "$dotest/msgnum") + onto=$(cat "$dotest/onto") + continue_merge + while test "$msgnum" -le "$end" + do + call_merge "$msgnum" + continue_merge + done + finish_rb_merge + exit + fi + head_name=$(cat "$GIT_DIR"/rebase-apply/head-name) && + onto=$(cat "$GIT_DIR"/rebase-apply/onto) && + orig_head=$(cat "$GIT_DIR"/rebase-apply/orig-head) && + git am --resolved --3way --resolvemsg="$RESOLVEMSG" && + move_to_original_branch + exit + ;; + --skip) + test -d "$dotest" -o -d "$GIT_DIR"/rebase-apply || + die "No rebase in progress?" + + git reset --hard HEAD || exit $? + if test -d "$dotest" + then + git rerere clear + prev_head=$(cat "$dotest/prev_head") + end=$(cat "$dotest/end") + msgnum=$(cat "$dotest/msgnum") + msgnum=$(($msgnum + 1)) + onto=$(cat "$dotest/onto") + while test "$msgnum" -le "$end" + do + call_merge "$msgnum" + continue_merge + done + finish_rb_merge + exit + fi + head_name=$(cat "$GIT_DIR"/rebase-apply/head-name) && + onto=$(cat "$GIT_DIR"/rebase-apply/onto) && + orig_head=$(cat "$GIT_DIR"/rebase-apply/orig-head) && + git am -3 --skip --resolvemsg="$RESOLVEMSG" && + move_to_original_branch + exit + ;; + --abort) + test -d "$dotest" -o -d "$GIT_DIR"/rebase-apply || + die "No rebase in progress?" + + git rerere clear + if test -d "$dotest" + then + move_to_original_branch + else + dotest="$GIT_DIR"/rebase-apply + move_to_original_branch + fi + git reset --hard $(cat "$dotest/orig-head") + rm -r "$dotest" + exit + ;; + --onto) + test 2 -le "$#" || usage + newbase="$2" + shift + ;; + -M|-m|--m|--me|--mer|--merg|--merge) + do_merge=t + ;; + -s=*|--s=*|--st=*|--str=*|--stra=*|--strat=*|--strate=*|\ + --strateg=*|--strategy=*|\ + -s|--s|--st|--str|--stra|--strat|--strate|--strateg|--strategy) + case "$#,$1" in + *,*=*) + strategy=`expr "z$1" : 'z-[^=]*=\(.*\)'` ;; + 1,*) + usage ;; + *) + strategy="$2" + shift ;; + esac + do_merge=t + ;; + -n|--no-stat) + diffstat= + ;; + --stat) + diffstat=t + ;; + -v|--verbose) + verbose=t + diffstat=t + ;; + --whitespace=*) + git_am_opt="$git_am_opt $1" + case "$1" in + --whitespace=fix|--whitespace=strip) + force_rebase=t + ;; + esac + ;; + --committer-date-is-author-date|--ignore-date) + git_am_opt="$git_am_opt $1" + force_rebase=t + ;; + -C*) + git_am_opt="$git_am_opt $1" + ;; + --root) + rebase_root=t + ;; + -f|--f|--fo|--for|--forc|force|--force-r|--force-re|--force-reb|--force-reba|--force-rebas|--force-rebase) + force_rebase=t + ;; + -*) + usage + ;; + *) + break + ;; + esac + shift +done +test $# -gt 2 && usage + +# Make sure we do not have $GIT_DIR/rebase-apply +if test -z "$do_merge" +then + if mkdir "$GIT_DIR"/rebase-apply 2>/dev/null + then + rmdir "$GIT_DIR"/rebase-apply + else + echo >&2 ' +It seems that I cannot create a rebase-apply directory, and +I wonder if you are in the middle of patch application or another +rebase. If that is not the case, please + rm -fr '"$GIT_DIR"'/rebase-apply +and run me again. I am stopping in case you still have something +valuable there.' + exit 1 + fi +else + if test -d "$dotest" + then + die "previous rebase directory $dotest still exists." \ + 'Try git rebase (--continue | --abort | --skip)' + fi +fi + +# The tree must be really really clean. +if ! git update-index --ignore-submodules --refresh; then + echo >&2 "cannot rebase: you have unstaged changes" + exit 1 +fi +diff=$(git diff-index --cached --name-status -r --ignore-submodules HEAD --) +case "$diff" in +?*) echo >&2 "cannot rebase: your index contains uncommitted changes" + echo >&2 "$diff" + exit 1 + ;; +esac + +if test -z "$rebase_root" +then + # The upstream head must be given. Make sure it is valid. + upstream_name="$1" + shift + upstream=`git rev-parse --verify "${upstream_name}^0"` || + die "invalid upstream $upstream_name" + unset root_flag + upstream_arg="$upstream_name" +else + test -z "$newbase" && die "--root must be used with --onto" + unset upstream_name + unset upstream + root_flag="--root" + upstream_arg="$root_flag" +fi + +# Make sure the branch to rebase onto is valid. +onto_name=${newbase-"$upstream_name"} +onto=$(git rev-parse --verify "${onto_name}^0") || exit + +# If a hook exists, give it a chance to interrupt +run_pre_rebase_hook "$upstream_arg" "$@" + +# If the branch to rebase is given, that is the branch we will rebase +# $branch_name -- branch being rebased, or HEAD (already detached) +# $orig_head -- commit object name of tip of the branch before rebasing +# $head_name -- refs/heads/<that-branch> or "detached HEAD" +switch_to= +case "$#" in +1) + # Is it "rebase other $branchname" or "rebase other $commit"? + branch_name="$1" + switch_to="$1" + + if git show-ref --verify --quiet -- "refs/heads/$1" && + branch=$(git rev-parse -q --verify "refs/heads/$1") + then + head_name="refs/heads/$1" + elif branch=$(git rev-parse -q --verify "$1") + then + head_name="detached HEAD" + else + usage + fi + ;; +*) + # Do not need to switch branches, we are already on it. + if branch_name=`git symbolic-ref -q HEAD` + then + head_name=$branch_name + branch_name=`expr "z$branch_name" : 'zrefs/heads/\(.*\)'` + else + head_name="detached HEAD" + branch_name=HEAD ;# detached + fi + branch=$(git rev-parse --verify "${branch_name}^0") || exit + ;; +esac +orig_head=$branch + +# Now we are rebasing commits $upstream..$branch (or with --root, +# everything leading up to $branch) on top of $onto + +# Check if we are already based on $onto with linear history, +# but this should be done only when upstream and onto are the same. +mb=$(git merge-base "$onto" "$branch") +if test "$upstream" = "$onto" && test "$mb" = "$onto" && + # linear history? + ! (git rev-list --parents "$onto".."$branch" | grep " .* ") > /dev/null +then + if test -z "$force_rebase" + then + # Lazily switch to the target branch if needed... + test -z "$switch_to" || git checkout "$switch_to" + echo >&2 "Current branch $branch_name is up to date." + exit 0 + else + echo "Current branch $branch_name is up to date, rebase forced." + fi +fi + +# Detach HEAD and reset the tree +echo "First, rewinding head to replay your work on top of it..." +git checkout -q "$onto^0" || die "could not detach HEAD" +git update-ref ORIG_HEAD $branch + +if test -n "$diffstat" +then + if test -n "$verbose" + then + echo "Changes from $mb to $onto:" + fi + # We want color (if set), but no pager + GIT_PAGER='' git diff --stat --summary "$mb" "$onto" +fi + +# If the $onto is a proper descendant of the tip of the branch, then +# we just fast forwarded. +if test "$mb" = "$branch" +then + echo >&2 "Fast-forwarded $branch_name to $onto_name." + move_to_original_branch + exit 0 +fi + +if test -n "$rebase_root" +then + revisions="$onto..$orig_head" +else + revisions="$upstream..$orig_head" +fi + +if test -z "$do_merge" +then + git format-patch -k --stdout --full-index --ignore-if-in-upstream \ + $root_flag "$revisions" | + git am $git_am_opt --rebasing --resolvemsg="$RESOLVEMSG" && + move_to_original_branch + ret=$? + test 0 != $ret -a -d "$GIT_DIR"/rebase-apply && + echo $head_name > "$GIT_DIR"/rebase-apply/head-name && + echo $onto > "$GIT_DIR"/rebase-apply/onto && + echo $orig_head > "$GIT_DIR"/rebase-apply/orig-head + exit $ret +fi + +# start doing a rebase with git-merge +# this is rename-aware if the recursive (default) strategy is used + +mkdir -p "$dotest" +echo "$onto" > "$dotest/onto" +echo "$onto_name" > "$dotest/onto_name" +prev_head=$orig_head +echo "$prev_head" > "$dotest/prev_head" +echo "$orig_head" > "$dotest/orig-head" +echo "$head_name" > "$dotest/head-name" + +msgnum=0 +for cmt in `git rev-list --reverse --no-merges "$revisions"` +do + msgnum=$(($msgnum + 1)) + echo "$cmt" > "$dotest/cmt.$msgnum" +done + +echo 1 >"$dotest/msgnum" +echo $msgnum >"$dotest/end" + +end=$msgnum +msgnum=1 + +while test "$msgnum" -le "$end" +do + call_merge "$msgnum" + continue_merge +done + +finish_rb_merge diff --git a/git-rebase.sh b/git-rebase.sh deleted file mode 100755 index b83fd3f..0000000 --- a/git-rebase.sh +++ /dev/null @@ -1,530 +0,0 @@ -#!/bin/sh -# -# Copyright (c) 2005 Junio C Hamano. -# - -USAGE='[--interactive | -i] [-v] [--force-rebase | -f] [--onto <newbase>] [<upstream>|--root] [<branch>]' -LONG_USAGE='git-rebase replaces <branch> with a new branch of the -same name. When the --onto option is provided the new branch starts -out with a HEAD equal to <newbase>, otherwise it is equal to <upstream> -It then attempts to create a new commit for each commit from the original -<branch> that does not exist in the <upstream> branch. - -It is possible that a merge failure will prevent this process from being -completely automatic. You will have to resolve any such merge failure -and run git rebase --continue. Another option is to bypass the commit -that caused the merge failure with git rebase --skip. To restore the -original <branch> and remove the .git/rebase-apply working files, use the -command git rebase --abort instead. - -Note that if <branch> is not specified on the command line, the -currently checked out branch is used. - -Example: git-rebase master~1 topic - - A---B---C topic A'\''--B'\''--C'\'' topic - / --> / - D---E---F---G master D---E---F---G master -' - -SUBDIRECTORY_OK=Yes -OPTIONS_SPEC= -. git-sh-setup -set_reflog_action rebase -require_work_tree -cd_to_toplevel - -OK_TO_SKIP_PRE_REBASE= -RESOLVEMSG=" -When you have resolved this problem run \"git rebase --continue\". -If you would prefer to skip this patch, instead run \"git rebase --skip\". -To restore the original branch and stop rebasing run \"git rebase --abort\". -" -unset newbase -strategy=recursive -do_merge= -dotest="$GIT_DIR"/rebase-merge -prec=4 -verbose= -diffstat=$(git config --bool rebase.stat) -git_am_opt= -rebase_root= -force_rebase= - -continue_merge () { - test -n "$prev_head" || die "prev_head must be defined" - test -d "$dotest" || die "$dotest directory does not exist" - - unmerged=$(git ls-files -u) - if test -n "$unmerged" - then - echo "You still have unmerged paths in your index" - echo "did you forget to use git add?" - die "$RESOLVEMSG" - fi - - cmt=`cat "$dotest/current"` - if ! git diff-index --quiet --ignore-submodules HEAD -- - then - if ! git commit --no-verify -C "$cmt" - then - echo "Commit failed, please do not call \"git commit\"" - echo "directly, but instead do one of the following: " - die "$RESOLVEMSG" - fi - printf "Committed: %0${prec}d " $msgnum - else - printf "Already applied: %0${prec}d " $msgnum - fi - git rev-list --pretty=oneline -1 "$cmt" | sed -e 's/^[^ ]* //' - - prev_head=`git rev-parse HEAD^0` - # save the resulting commit so we can read-tree on it later - echo "$prev_head" > "$dotest/prev_head" - - # onto the next patch: - msgnum=$(($msgnum + 1)) - echo "$msgnum" >"$dotest/msgnum" -} - -call_merge () { - cmt="$(cat "$dotest/cmt.$1")" - echo "$cmt" > "$dotest/current" - hd=$(git rev-parse --verify HEAD) - cmt_name=$(git symbolic-ref HEAD 2> /dev/null || echo HEAD) - msgnum=$(cat "$dotest/msgnum") - end=$(cat "$dotest/end") - eval GITHEAD_$cmt='"${cmt_name##refs/heads/}~$(($end - $msgnum))"' - eval GITHEAD_$hd='$(cat "$dotest/onto_name")' - export GITHEAD_$cmt GITHEAD_$hd - git-merge-$strategy "$cmt^" -- "$hd" "$cmt" - rv=$? - case "$rv" in - 0) - unset GITHEAD_$cmt GITHEAD_$hd - return - ;; - 1) - git rerere - die "$RESOLVEMSG" - ;; - 2) - echo "Strategy: $rv $strategy failed, try another" 1>&2 - die "$RESOLVEMSG" - ;; - *) - die "Unknown exit code ($rv) from command:" \ - "git-merge-$strategy $cmt^ -- HEAD $cmt" - ;; - esac -} - -move_to_original_branch () { - test -z "$head_name" && - head_name="$(cat "$dotest"/head-name)" && - onto="$(cat "$dotest"/onto)" && - orig_head="$(cat "$dotest"/orig-head)" - case "$head_name" in - refs/*) - message="rebase finished: $head_name onto $onto" - git update-ref -m "$message" \ - $head_name $(git rev-parse HEAD) $orig_head && - git symbolic-ref HEAD $head_name || - die "Could not move back to $head_name" - ;; - esac -} - -finish_rb_merge () { - move_to_original_branch - rm -r "$dotest" - echo "All done." -} - -is_interactive () { - while test $# != 0 - do - case "$1" in - -i|--interactive) - interactive_rebase=explicit - break - ;; - -p|--preserve-merges) - interactive_rebase=implied - ;; - esac - shift - done - - if [ "$interactive_rebase" = implied ]; then - GIT_EDITOR=: - export GIT_EDITOR - fi - - test -n "$interactive_rebase" || test -f "$dotest"/interactive -} - -run_pre_rebase_hook () { - if test -z "$OK_TO_SKIP_PRE_REBASE" && - test -x "$GIT_DIR/hooks/pre-rebase" - then - "$GIT_DIR/hooks/pre-rebase" ${1+"$@"} || { - echo >&2 "The pre-rebase hook refused to rebase." - exit 1 - } - fi -} - -test -f "$GIT_DIR"/rebase-apply/applying && - die 'It looks like git-am is in progress. Cannot rebase.' - -is_interactive "$@" && exec git-rebase--interactive "$@" - -if test $# -eq 0 -then - test -d "$dotest" -o -d "$GIT_DIR"/rebase-apply || usage - test -d "$dotest" -o -f "$GIT_DIR"/rebase-apply/rebasing && - die 'A rebase is in progress, try --continue, --skip or --abort.' - die "No arguments given and $GIT_DIR/rebase-apply already exists." -fi - -while test $# != 0 -do - case "$1" in - --no-verify) - OK_TO_SKIP_PRE_REBASE=yes - ;; - --continue) - test -d "$dotest" -o -d "$GIT_DIR"/rebase-apply || - die "No rebase in progress?" - - git diff-files --quiet --ignore-submodules || { - echo "You must edit all merge conflicts and then" - echo "mark them as resolved using git add" - exit 1 - } - if test -d "$dotest" - then - prev_head=$(cat "$dotest/prev_head") - end=$(cat "$dotest/end") - msgnum=$(cat "$dotest/msgnum") - onto=$(cat "$dotest/onto") - continue_merge - while test "$msgnum" -le "$end" - do - call_merge "$msgnum" - continue_merge - done - finish_rb_merge - exit - fi - head_name=$(cat "$GIT_DIR"/rebase-apply/head-name) && - onto=$(cat "$GIT_DIR"/rebase-apply/onto) && - orig_head=$(cat "$GIT_DIR"/rebase-apply/orig-head) && - git am --resolved --3way --resolvemsg="$RESOLVEMSG" && - move_to_original_branch - exit - ;; - --skip) - test -d "$dotest" -o -d "$GIT_DIR"/rebase-apply || - die "No rebase in progress?" - - git reset --hard HEAD || exit $? - if test -d "$dotest" - then - git rerere clear - prev_head=$(cat "$dotest/prev_head") - end=$(cat "$dotest/end") - msgnum=$(cat "$dotest/msgnum") - msgnum=$(($msgnum + 1)) - onto=$(cat "$dotest/onto") - while test "$msgnum" -le "$end" - do - call_merge "$msgnum" - continue_merge - done - finish_rb_merge - exit - fi - head_name=$(cat "$GIT_DIR"/rebase-apply/head-name) && - onto=$(cat "$GIT_DIR"/rebase-apply/onto) && - orig_head=$(cat "$GIT_DIR"/rebase-apply/orig-head) && - git am -3 --skip --resolvemsg="$RESOLVEMSG" && - move_to_original_branch - exit - ;; - --abort) - test -d "$dotest" -o -d "$GIT_DIR"/rebase-apply || - die "No rebase in progress?" - - git rerere clear - if test -d "$dotest" - then - move_to_original_branch - else - dotest="$GIT_DIR"/rebase-apply - move_to_original_branch - fi - git reset --hard $(cat "$dotest/orig-head") - rm -r "$dotest" - exit - ;; - --onto) - test 2 -le "$#" || usage - newbase="$2" - shift - ;; - -M|-m|--m|--me|--mer|--merg|--merge) - do_merge=t - ;; - -s=*|--s=*|--st=*|--str=*|--stra=*|--strat=*|--strate=*|\ - --strateg=*|--strategy=*|\ - -s|--s|--st|--str|--stra|--strat|--strate|--strateg|--strategy) - case "$#,$1" in - *,*=*) - strategy=`expr "z$1" : 'z-[^=]*=\(.*\)'` ;; - 1,*) - usage ;; - *) - strategy="$2" - shift ;; - esac - do_merge=t - ;; - -n|--no-stat) - diffstat= - ;; - --stat) - diffstat=t - ;; - -v|--verbose) - verbose=t - diffstat=t - ;; - --whitespace=*) - git_am_opt="$git_am_opt $1" - case "$1" in - --whitespace=fix|--whitespace=strip) - force_rebase=t - ;; - esac - ;; - --committer-date-is-author-date|--ignore-date) - git_am_opt="$git_am_opt $1" - force_rebase=t - ;; - -C*) - git_am_opt="$git_am_opt $1" - ;; - --root) - rebase_root=t - ;; - -f|--f|--fo|--for|--forc|force|--force-r|--force-re|--force-reb|--force-reba|--force-rebas|--force-rebase) - force_rebase=t - ;; - -*) - usage - ;; - *) - break - ;; - esac - shift -done -test $# -gt 2 && usage - -# Make sure we do not have $GIT_DIR/rebase-apply -if test -z "$do_merge" -then - if mkdir "$GIT_DIR"/rebase-apply 2>/dev/null - then - rmdir "$GIT_DIR"/rebase-apply - else - echo >&2 ' -It seems that I cannot create a rebase-apply directory, and -I wonder if you are in the middle of patch application or another -rebase. If that is not the case, please - rm -fr '"$GIT_DIR"'/rebase-apply -and run me again. I am stopping in case you still have something -valuable there.' - exit 1 - fi -else - if test -d "$dotest" - then - die "previous rebase directory $dotest still exists." \ - 'Try git rebase (--continue | --abort | --skip)' - fi -fi - -# The tree must be really really clean. -if ! git update-index --ignore-submodules --refresh; then - echo >&2 "cannot rebase: you have unstaged changes" - exit 1 -fi -diff=$(git diff-index --cached --name-status -r --ignore-submodules HEAD --) -case "$diff" in -?*) echo >&2 "cannot rebase: your index contains uncommitted changes" - echo >&2 "$diff" - exit 1 - ;; -esac - -if test -z "$rebase_root" -then - # The upstream head must be given. Make sure it is valid. - upstream_name="$1" - shift - upstream=`git rev-parse --verify "${upstream_name}^0"` || - die "invalid upstream $upstream_name" - unset root_flag - upstream_arg="$upstream_name" -else - test -z "$newbase" && die "--root must be used with --onto" - unset upstream_name - unset upstream - root_flag="--root" - upstream_arg="$root_flag" -fi - -# Make sure the branch to rebase onto is valid. -onto_name=${newbase-"$upstream_name"} -onto=$(git rev-parse --verify "${onto_name}^0") || exit - -# If a hook exists, give it a chance to interrupt -run_pre_rebase_hook "$upstream_arg" "$@" - -# If the branch to rebase is given, that is the branch we will rebase -# $branch_name -- branch being rebased, or HEAD (already detached) -# $orig_head -- commit object name of tip of the branch before rebasing -# $head_name -- refs/heads/<that-branch> or "detached HEAD" -switch_to= -case "$#" in -1) - # Is it "rebase other $branchname" or "rebase other $commit"? - branch_name="$1" - switch_to="$1" - - if git show-ref --verify --quiet -- "refs/heads/$1" && - branch=$(git rev-parse -q --verify "refs/heads/$1") - then - head_name="refs/heads/$1" - elif branch=$(git rev-parse -q --verify "$1") - then - head_name="detached HEAD" - else - usage - fi - ;; -*) - # Do not need to switch branches, we are already on it. - if branch_name=`git symbolic-ref -q HEAD` - then - head_name=$branch_name - branch_name=`expr "z$branch_name" : 'zrefs/heads/\(.*\)'` - else - head_name="detached HEAD" - branch_name=HEAD ;# detached - fi - branch=$(git rev-parse --verify "${branch_name}^0") || exit - ;; -esac -orig_head=$branch - -# Now we are rebasing commits $upstream..$branch (or with --root, -# everything leading up to $branch) on top of $onto - -# Check if we are already based on $onto with linear history, -# but this should be done only when upstream and onto are the same. -mb=$(git merge-base "$onto" "$branch") -if test "$upstream" = "$onto" && test "$mb" = "$onto" && - # linear history? - ! (git rev-list --parents "$onto".."$branch" | grep " .* ") > /dev/null -then - if test -z "$force_rebase" - then - # Lazily switch to the target branch if needed... - test -z "$switch_to" || git checkout "$switch_to" - echo >&2 "Current branch $branch_name is up to date." - exit 0 - else - echo "Current branch $branch_name is up to date, rebase forced." - fi -fi - -# Detach HEAD and reset the tree -echo "First, rewinding head to replay your work on top of it..." -git checkout -q "$onto^0" || die "could not detach HEAD" -git update-ref ORIG_HEAD $branch - -if test -n "$diffstat" -then - if test -n "$verbose" - then - echo "Changes from $mb to $onto:" - fi - # We want color (if set), but no pager - GIT_PAGER='' git diff --stat --summary "$mb" "$onto" -fi - -# If the $onto is a proper descendant of the tip of the branch, then -# we just fast forwarded. -if test "$mb" = "$branch" -then - echo >&2 "Fast-forwarded $branch_name to $onto_name." - move_to_original_branch - exit 0 -fi - -if test -n "$rebase_root" -then - revisions="$onto..$orig_head" -else - revisions="$upstream..$orig_head" -fi - -if test -z "$do_merge" -then - git format-patch -k --stdout --full-index --ignore-if-in-upstream \ - $root_flag "$revisions" | - git am $git_am_opt --rebasing --resolvemsg="$RESOLVEMSG" && - move_to_original_branch - ret=$? - test 0 != $ret -a -d "$GIT_DIR"/rebase-apply && - echo $head_name > "$GIT_DIR"/rebase-apply/head-name && - echo $onto > "$GIT_DIR"/rebase-apply/onto && - echo $orig_head > "$GIT_DIR"/rebase-apply/orig-head - exit $ret -fi - -# start doing a rebase with git-merge -# this is rename-aware if the recursive (default) strategy is used - -mkdir -p "$dotest" -echo "$onto" > "$dotest/onto" -echo "$onto_name" > "$dotest/onto_name" -prev_head=$orig_head -echo "$prev_head" > "$dotest/prev_head" -echo "$orig_head" > "$dotest/orig-head" -echo "$head_name" > "$dotest/head-name" - -msgnum=0 -for cmt in `git rev-list --reverse --no-merges "$revisions"` -do - msgnum=$(($msgnum + 1)) - echo "$cmt" > "$dotest/cmt.$msgnum" -done - -echo 1 >"$dotest/msgnum" -echo $msgnum >"$dotest/end" - -end=$msgnum -msgnum=1 - -while test "$msgnum" -le "$end" -do - call_merge "$msgnum" - continue_merge -done - -finish_rb_merge diff --git a/git.c b/git.c index 5a00726..39456a0 100644 --- a/git.c +++ b/git.c @@ -336,6 +336,7 @@ static void handle_internal_command(int argc, const char **argv) { "prune-packed", cmd_prune_packed, RUN_SETUP }, { "push", cmd_push, RUN_SETUP }, { "read-tree", cmd_read_tree, RUN_SETUP }, + { "rebase", cmd_rebase, RUN_SETUP | NEED_WORK_TREE }, { "receive-pack", cmd_receive_pack }, { "reflog", cmd_reflog, RUN_SETUP }, { "remote", cmd_remote, RUN_SETUP }, -- test -- To unsubscribe from this list: send the line "unsubscribe git" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html