diff options
author | Junio C Hamano <gitster@pobox.com> | 2023-12-19 01:10:11 +0300 |
---|---|---|
committer | Junio C Hamano <gitster@pobox.com> | 2023-12-19 01:10:11 +0300 |
commit | 02230b74e8a7c83a7dd57da2e7a299dbb4174f33 (patch) | |
tree | d99f9864d0d08b43ad0e82f507756579f577cef6 /t | |
parent | 333536527011ebcc0a4d1e7f072dc435f605e224 (diff) | |
parent | e928c11e29966b0a8f4340465b8238501c994eb1 (diff) |
Merge branch 'cc/git-replay'
Introduce "git replay", a tool meant on the server side without
working tree to recreate a history.
* cc/git-replay:
replay: stop assuming replayed branches do not diverge
replay: add --contained to rebase contained branches
replay: add --advance or 'cherry-pick' mode
replay: use standard revision ranges
replay: make it a minimal server side command
replay: remove HEAD related sanity check
replay: remove progress and info output
replay: add an important FIXME comment about gpg signing
replay: change rev walking options
replay: introduce pick_regular_commit()
replay: die() instead of failing assert()
replay: start using parse_options API
replay: introduce new builtin
t6429: remove switching aspects of fast-rebase
Diffstat (limited to 't')
-rw-r--r-- | t/helper/test-fast-rebase.c | 241 | ||||
-rw-r--r-- | t/helper/test-tool.c | 1 | ||||
-rw-r--r-- | t/helper/test-tool.h | 1 | ||||
-rwxr-xr-x | t/t3650-replay-basics.sh | 198 | ||||
-rwxr-xr-x | t/t6429-merge-sequence-rename-caching.sh | 45 |
5 files changed, 223 insertions, 263 deletions
diff --git a/t/helper/test-fast-rebase.c b/t/helper/test-fast-rebase.c deleted file mode 100644 index cac20a72b3..0000000000 --- a/t/helper/test-fast-rebase.c +++ /dev/null @@ -1,241 +0,0 @@ -/* - * "git fast-rebase" builtin command - * - * FAST: Forking Any Subprocesses (is) Taboo - * - * This is meant SOLELY as a demo of what is possible. sequencer.c and - * rebase.c should be refactored to use the ideas here, rather than attempting - * to extend this file to replace those (unless Phillip or Dscho say that - * refactoring is too hard and we need a clean slate, but I'm guessing that - * refactoring is the better route). - */ - -#define USE_THE_INDEX_VARIABLE -#include "test-tool.h" -#include "cache-tree.h" -#include "commit.h" -#include "environment.h" -#include "gettext.h" -#include "hash.h" -#include "hex.h" -#include "lockfile.h" -#include "merge-ort.h" -#include "object-name.h" -#include "read-cache-ll.h" -#include "refs.h" -#include "revision.h" -#include "sequencer.h" -#include "setup.h" -#include "strvec.h" -#include "tree.h" - -static const char *short_commit_name(struct commit *commit) -{ - return repo_find_unique_abbrev(the_repository, &commit->object.oid, - DEFAULT_ABBREV); -} - -static struct commit *peel_committish(const char *name) -{ - struct object *obj; - struct object_id oid; - - if (repo_get_oid(the_repository, name, &oid)) - return NULL; - obj = parse_object(the_repository, &oid); - return (struct commit *)repo_peel_to_type(the_repository, name, 0, obj, - OBJ_COMMIT); -} - -static char *get_author(const char *message) -{ - size_t len; - const char *a; - - a = find_commit_header(message, "author", &len); - if (a) - return xmemdupz(a, len); - - return NULL; -} - -static struct commit *create_commit(struct tree *tree, - struct commit *based_on, - struct commit *parent) -{ - struct object_id ret; - struct object *obj; - struct commit_list *parents = NULL; - char *author; - char *sign_commit = NULL; - struct commit_extra_header *extra; - struct strbuf msg = STRBUF_INIT; - const char *out_enc = get_commit_output_encoding(); - const char *message = repo_logmsg_reencode(the_repository, based_on, - NULL, out_enc); - const char *orig_message = NULL; - const char *exclude_gpgsig[] = { "gpgsig", NULL }; - - commit_list_insert(parent, &parents); - extra = read_commit_extra_headers(based_on, exclude_gpgsig); - find_commit_subject(message, &orig_message); - strbuf_addstr(&msg, orig_message); - author = get_author(message); - reset_ident_date(); - if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents, - &ret, author, NULL, sign_commit, extra)) { - error(_("failed to write commit object")); - return NULL; - } - free(author); - strbuf_release(&msg); - - obj = parse_object(the_repository, &ret); - return (struct commit *)obj; -} - -int cmd__fast_rebase(int argc, const char **argv) -{ - struct commit *onto; - struct commit *last_commit = NULL, *last_picked_commit = NULL; - struct object_id head; - struct lock_file lock = LOCK_INIT; - struct strvec rev_walk_args = STRVEC_INIT; - struct rev_info revs; - struct commit *commit; - struct merge_options merge_opt; - struct tree *next_tree, *base_tree, *head_tree; - struct merge_result result; - struct strbuf reflog_msg = STRBUF_INIT; - struct strbuf branch_name = STRBUF_INIT; - int ret = 0; - - /* - * test-tool stuff doesn't set up the git directory by default; need to - * do that manually. - */ - setup_git_directory(); - - if (argc == 2 && !strcmp(argv[1], "-h")) { - printf("Sorry, I am not a psychiatrist; I can not give you the help you need. Oh, you meant usage...\n"); - exit(129); - } - - if (argc != 5 || strcmp(argv[1], "--onto")) - die("usage: read the code, figure out how to use it, then do so"); - - onto = peel_committish(argv[2]); - strbuf_addf(&branch_name, "refs/heads/%s", argv[4]); - - /* Sanity check */ - if (repo_get_oid(the_repository, "HEAD", &head)) - die(_("Cannot read HEAD")); - assert(oideq(&onto->object.oid, &head)); - - repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR); - if (repo_read_index(the_repository) < 0) - BUG("Could not read index"); - - repo_init_revisions(the_repository, &revs, NULL); - revs.verbose_header = 1; - revs.max_parents = 1; - revs.cherry_mark = 1; - revs.limited = 1; - revs.reverse = 1; - revs.right_only = 1; - revs.sort_order = REV_SORT_IN_GRAPH_ORDER; - revs.topo_order = 1; - strvec_pushl(&rev_walk_args, "", argv[4], "--not", argv[3], NULL); - - if (setup_revisions(rev_walk_args.nr, rev_walk_args.v, &revs, NULL) > 1) { - ret = error(_("unhandled options")); - goto cleanup; - } - - strvec_clear(&rev_walk_args); - - if (prepare_revision_walk(&revs) < 0) { - ret = error(_("error preparing revisions")); - goto cleanup; - } - - init_merge_options(&merge_opt, the_repository); - memset(&result, 0, sizeof(result)); - merge_opt.show_rename_progress = 1; - merge_opt.branch1 = "HEAD"; - head_tree = repo_get_commit_tree(the_repository, onto); - result.tree = head_tree; - last_commit = onto; - while ((commit = get_revision(&revs))) { - struct commit *base; - - fprintf(stderr, "Rebasing %s...\r", - oid_to_hex(&commit->object.oid)); - assert(commit->parents && !commit->parents->next); - base = commit->parents->item; - - next_tree = repo_get_commit_tree(the_repository, commit); - base_tree = repo_get_commit_tree(the_repository, base); - - merge_opt.branch2 = short_commit_name(commit); - merge_opt.ancestor = xstrfmt("parent of %s", merge_opt.branch2); - - merge_incore_nonrecursive(&merge_opt, - base_tree, - result.tree, - next_tree, - &result); - - free((char*)merge_opt.ancestor); - merge_opt.ancestor = NULL; - if (!result.clean) - break; - last_picked_commit = commit; - last_commit = create_commit(result.tree, commit, last_commit); - } - - merge_switch_to_result(&merge_opt, head_tree, &result, 1, !result.clean); - - if (result.clean < 0) - exit(128); - - if (result.clean) { - fprintf(stderr, "\nDone.\n"); - strbuf_addf(&reflog_msg, "finish rebase %s onto %s", - oid_to_hex(&last_picked_commit->object.oid), - oid_to_hex(&last_commit->object.oid)); - if (update_ref(reflog_msg.buf, branch_name.buf, - &last_commit->object.oid, - &last_picked_commit->object.oid, - REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) { - error(_("could not update %s"), argv[4]); - die("Failed to update %s", argv[4]); - } - if (create_symref("HEAD", branch_name.buf, reflog_msg.buf) < 0) - die(_("unable to update HEAD")); - - prime_cache_tree(the_repository, the_repository->index, - result.tree); - } else { - fprintf(stderr, "\nAborting: Hit a conflict.\n"); - strbuf_addf(&reflog_msg, "rebase progress up to %s", - oid_to_hex(&last_picked_commit->object.oid)); - if (update_ref(reflog_msg.buf, "HEAD", - &last_commit->object.oid, - &head, - REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR)) { - error(_("could not update %s"), argv[4]); - die("Failed to update %s", argv[4]); - } - } - if (write_locked_index(&the_index, &lock, - COMMIT_LOCK | SKIP_IF_UNCHANGED)) - die(_("unable to write %s"), get_index_file()); - - ret = (result.clean == 0); -cleanup: - strbuf_release(&reflog_msg); - strbuf_release(&branch_name); - release_revisions(&revs); - return ret; -} diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c index 876cd2dc31..37ba996539 100644 --- a/t/helper/test-tool.c +++ b/t/helper/test-tool.c @@ -30,7 +30,6 @@ static struct test_cmd cmds[] = { { "dump-untracked-cache", cmd__dump_untracked_cache }, { "env-helper", cmd__env_helper }, { "example-decorate", cmd__example_decorate }, - { "fast-rebase", cmd__fast_rebase }, { "find-pack", cmd__find_pack }, { "fsmonitor-client", cmd__fsmonitor_client }, { "genrandom", cmd__genrandom }, diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h index 70dd4eba11..8a1a7c63da 100644 --- a/t/helper/test-tool.h +++ b/t/helper/test-tool.h @@ -24,7 +24,6 @@ int cmd__dump_untracked_cache(int argc, const char **argv); int cmd__dump_reftable(int argc, const char **argv); int cmd__env_helper(int argc, const char **argv); int cmd__example_decorate(int argc, const char **argv); -int cmd__fast_rebase(int argc, const char **argv); int cmd__find_pack(int argc, const char **argv); int cmd__fsmonitor_client(int argc, const char **argv); int cmd__genrandom(int argc, const char **argv); diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh new file mode 100755 index 0000000000..389670262e --- /dev/null +++ b/t/t3650-replay-basics.sh @@ -0,0 +1,198 @@ +#!/bin/sh + +test_description='basic git replay tests' + +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main +export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME + +. ./test-lib.sh + +GIT_AUTHOR_NAME=author@name +GIT_AUTHOR_EMAIL=bogus@email@address +export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL + +test_expect_success 'setup' ' + test_commit A && + test_commit B && + + git switch -c topic1 && + test_commit C && + git switch -c topic2 && + test_commit D && + test_commit E && + git switch topic1 && + test_commit F && + git switch -c topic3 && + test_commit G && + test_commit H && + git switch -c topic4 main && + test_commit I && + test_commit J && + + git switch -c next main && + test_commit K && + git merge -m "Merge topic1" topic1 && + git merge -m "Merge topic2" topic2 && + git merge -m "Merge topic3" topic3 && + >evil && + git add evil && + git commit --amend && + git merge -m "Merge topic4" topic4 && + + git switch main && + test_commit L && + test_commit M && + + git switch -c conflict B && + test_commit C.conflict C.t conflict +' + +test_expect_success 'setup bare' ' + git clone --bare . bare +' + +test_expect_success 'using replay to rebase two branches, one on top of other' ' + git replay --onto main topic1..topic2 >result && + + test_line_count = 1 result && + + git log --format=%s $(cut -f 3 -d " " result) >actual && + test_write_lines E D M L B A >expect && + test_cmp expect actual && + + printf "update refs/heads/topic2 " >expect && + printf "%s " $(cut -f 3 -d " " result) >>expect && + git rev-parse topic2 >>expect && + + test_cmp expect result +' + +test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' ' + git -C bare replay --onto main topic1..topic2 >result-bare && + test_cmp expect result-bare +' + +test_expect_success 'using replay to rebase with a conflict' ' + test_expect_code 1 git replay --onto topic1 B..conflict +' + +test_expect_success 'using replay on bare repo to rebase with a conflict' ' + test_expect_code 1 git -C bare replay --onto topic1 B..conflict +' + +test_expect_success 'using replay to perform basic cherry-pick' ' + # The differences between this test and previous ones are: + # --advance vs --onto + # 2nd field of result is refs/heads/main vs. refs/heads/topic2 + # 4th field of result is hash for main instead of hash for topic2 + + git replay --advance main topic1..topic2 >result && + + test_line_count = 1 result && + + git log --format=%s $(cut -f 3 -d " " result) >actual && + test_write_lines E D M L B A >expect && + test_cmp expect actual && + + printf "update refs/heads/main " >expect && + printf "%s " $(cut -f 3 -d " " result) >>expect && + git rev-parse main >>expect && + + test_cmp expect result +' + +test_expect_success 'using replay on bare repo to perform basic cherry-pick' ' + git -C bare replay --advance main topic1..topic2 >result-bare && + test_cmp expect result-bare +' + +test_expect_success 'replay on bare repo fails with both --advance and --onto' ' + test_must_fail git -C bare replay --advance main --onto main topic1..topic2 >result-bare +' + +test_expect_success 'replay fails when both --advance and --onto are omitted' ' + test_must_fail git replay topic1..topic2 >result +' + +test_expect_success 'using replay to also rebase a contained branch' ' + git replay --contained --onto main main..topic3 >result && + + test_line_count = 2 result && + cut -f 3 -d " " result >new-branch-tips && + + git log --format=%s $(head -n 1 new-branch-tips) >actual && + test_write_lines F C M L B A >expect && + test_cmp expect actual && + + git log --format=%s $(tail -n 1 new-branch-tips) >actual && + test_write_lines H G F C M L B A >expect && + test_cmp expect actual && + + printf "update refs/heads/topic1 " >expect && + printf "%s " $(head -n 1 new-branch-tips) >>expect && + git rev-parse topic1 >>expect && + printf "update refs/heads/topic3 " >>expect && + printf "%s " $(tail -n 1 new-branch-tips) >>expect && + git rev-parse topic3 >>expect && + + test_cmp expect result +' + +test_expect_success 'using replay on bare repo to also rebase a contained branch' ' + git -C bare replay --contained --onto main main..topic3 >result-bare && + test_cmp expect result-bare +' + +test_expect_success 'using replay to rebase multiple divergent branches' ' + git replay --onto main ^topic1 topic2 topic4 >result && + + test_line_count = 2 result && + cut -f 3 -d " " result >new-branch-tips && + + git log --format=%s $(head -n 1 new-branch-tips) >actual && + test_write_lines E D M L B A >expect && + test_cmp expect actual && + + git log --format=%s $(tail -n 1 new-branch-tips) >actual && + test_write_lines J I M L B A >expect && + test_cmp expect actual && + + printf "update refs/heads/topic2 " >expect && + printf "%s " $(head -n 1 new-branch-tips) >>expect && + git rev-parse topic2 >>expect && + printf "update refs/heads/topic4 " >>expect && + printf "%s " $(tail -n 1 new-branch-tips) >>expect && + git rev-parse topic4 >>expect && + + test_cmp expect result +' + +test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' ' + git -C bare replay --contained --onto main ^main topic2 topic3 topic4 >result && + + test_line_count = 4 result && + cut -f 3 -d " " result >new-branch-tips && + + >expect && + for i in 2 1 3 4 + do + printf "update refs/heads/topic$i " >>expect && + printf "%s " $(grep topic$i result | cut -f 3 -d " ") >>expect && + git -C bare rev-parse topic$i >>expect || return 1 + done && + + test_cmp expect result && + + test_write_lines F C M L B A >expect1 && + test_write_lines E D C M L B A >expect2 && + test_write_lines H G F C M L B A >expect3 && + test_write_lines J I M L B A >expect4 && + + for i in 1 2 3 4 + do + git -C bare log --format=%s $(grep topic$i result | cut -f 3 -d " ") >actual && + test_cmp expect$i actual || return 1 + done +' + +test_done diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh index d02fa16614..0f39ed0d08 100755 --- a/t/t6429-merge-sequence-rename-caching.sh +++ b/t/t6429-merge-sequence-rename-caching.sh @@ -71,8 +71,9 @@ test_expect_success 'caching renames does not preclude finding new ones' ' git switch upstream && - test-tool fast-rebase --onto HEAD upstream~1 topic && - #git cherry-pick upstream~1..topic + git replay --onto HEAD upstream~1..topic >out && + git update-ref --stdin <out && + git checkout topic && git ls-files >tracked-files && test_line_count = 2 tracked-files && @@ -140,8 +141,9 @@ test_expect_success 'cherry-pick both a commit and its immediate revert' ' GIT_TRACE2_PERF="$(pwd)/trace.output" && export GIT_TRACE2_PERF && - test-tool fast-rebase --onto HEAD upstream~1 topic && - #git cherry-pick upstream~1..topic && + git replay --onto HEAD upstream~1..topic >out && + git update-ref --stdin <out && + git checkout topic && grep region_enter.*diffcore_rename trace.output >calls && test_line_count = 1 calls @@ -199,8 +201,9 @@ test_expect_success 'rename same file identically, then reintroduce it' ' GIT_TRACE2_PERF="$(pwd)/trace.output" && export GIT_TRACE2_PERF && - test-tool fast-rebase --onto HEAD upstream~1 topic && - #git cherry-pick upstream~1..topic && + git replay --onto HEAD upstream~1..topic >out && + git update-ref --stdin <out && + git checkout topic && git ls-files >tracked && test_line_count = 2 tracked && @@ -276,8 +279,9 @@ test_expect_success 'rename same file identically, then add file to old dir' ' GIT_TRACE2_PERF="$(pwd)/trace.output" && export GIT_TRACE2_PERF && - test-tool fast-rebase --onto HEAD upstream~1 topic && - #git cherry-pick upstream~1..topic && + git replay --onto HEAD upstream~1..topic >out && + git update-ref --stdin <out && + git checkout topic && git ls-files >tracked && test_line_count = 4 tracked && @@ -353,10 +357,7 @@ test_expect_success 'cached dir rename does not prevent noticing later conflict' GIT_TRACE2_PERF="$(pwd)/trace.output" && export GIT_TRACE2_PERF && - test_must_fail test-tool fast-rebase --onto HEAD upstream~1 topic >output && - #git cherry-pick upstream..topic && - - grep CONFLICT..rename/rename output && + test_must_fail git replay --onto HEAD upstream~1..topic >output && grep region_enter.*diffcore_rename trace.output >calls && test_line_count = 2 calls @@ -455,8 +456,9 @@ test_expect_success 'dir rename unneeded, then add new file to old dir' ' GIT_TRACE2_PERF="$(pwd)/trace.output" && export GIT_TRACE2_PERF && - test-tool fast-rebase --onto HEAD upstream~1 topic && - #git cherry-pick upstream..topic && + git replay --onto HEAD upstream~1..topic >out && + git update-ref --stdin <out && + git checkout topic && grep region_enter.*diffcore_rename trace.output >calls && test_line_count = 2 calls && @@ -521,8 +523,9 @@ test_expect_success 'dir rename unneeded, then rename existing file into old dir GIT_TRACE2_PERF="$(pwd)/trace.output" && export GIT_TRACE2_PERF && - test-tool fast-rebase --onto HEAD upstream~1 topic && - #git cherry-pick upstream..topic && + git replay --onto HEAD upstream~1..topic >out && + git update-ref --stdin <out && + git checkout topic && grep region_enter.*diffcore_rename trace.output >calls && test_line_count = 3 calls && @@ -623,8 +626,9 @@ test_expect_success 'caching renames only on upstream side, part 1' ' GIT_TRACE2_PERF="$(pwd)/trace.output" && export GIT_TRACE2_PERF && - test-tool fast-rebase --onto HEAD upstream~1 topic && - #git cherry-pick upstream..topic && + git replay --onto HEAD upstream~1..topic >out && + git update-ref --stdin <out && + git checkout topic && grep region_enter.*diffcore_rename trace.output >calls && test_line_count = 1 calls && @@ -681,8 +685,9 @@ test_expect_success 'caching renames only on upstream side, part 2' ' GIT_TRACE2_PERF="$(pwd)/trace.output" && export GIT_TRACE2_PERF && - test-tool fast-rebase --onto HEAD upstream~1 topic && - #git cherry-pick upstream..topic && + git replay --onto HEAD upstream~1..topic >out && + git update-ref --stdin <out && + git checkout topic && grep region_enter.*diffcore_rename trace.output >calls && test_line_count = 2 calls && |