diff --git a/Documentation/RelNotes/2.56.0.adoc b/Documentation/RelNotes/2.56.0.adoc index 2d01f5cdc0ab74..471b84194c4d58 100644 --- a/Documentation/RelNotes/2.56.0.adoc +++ b/Documentation/RelNotes/2.56.0.adoc @@ -31,6 +31,18 @@ UI, Workflows & Features command is now recognized as a potential typo, and advice has been added to offer a typo fix. + * The 'git refs' toolbox has been extended with new 'create', 'delete', + 'update', and 'rename' subcommands to create, delete, update, and + rename references, respectively. + + * The experimental 'git history' command has been taught a new 'drop' + subcommand to remove a commit, with its descendants replayed onto its + parent. + + * The alignment of commit object name abbreviations in 'git blame' + output has been optimized to reserve a column for marks (caret, + question mark, or asterisk) only when such marks are actually shown. + Performance, Internal Implementation, Development Support etc. -------------------------------------------------------------- @@ -88,6 +100,14 @@ Performance, Internal Implementation, Development Support etc. flag, and a new 'odb_prepare()' wrapper has been introduced to allow pre-opening object database sources. + * The 'whence' field in 'struct object_info' has been removed. The + backend-specific object information retrieval has been refactored into + an opt-in 'struct object_info_source' structure. + + * A racy build failure under Meson has been corrected by ensuring that + the generated header file 'hook-list.h' is built before compiling + files in 'builtin_sources' that depend on it. + Fixes since v2.55 ----------------- @@ -140,3 +160,12 @@ Fixes since v2.55 plugged, and the leak reporting of the test suite when running under a TAP harness has been improved. (merge 973a0373ff jk/format-patch-leakfix later to maint). + + * A write file stream resource leak has been fixed as part of a code + cleanup. + (merge ebb4d2ffa3 jc/history-message-prep-fix later to maint). + + * Various memory leaks in the Bloom-filter code paths that are exposed + when running tests with the 'GIT_TEST_COMMIT_GRAPH_CHANGED_PATHS=1' + environment variable have been plugged. + (merge 459088ec2e jk/bloom-leak-fixes later to maint). diff --git a/Documentation/git-blame.adoc b/Documentation/git-blame.adoc index 8808009e87eb1d..2b74e455997c8c 100644 --- a/Documentation/git-blame.adoc +++ b/Documentation/git-blame.adoc @@ -88,11 +88,12 @@ include::blame-options.adoc[] include::diff-algorithm-option.adoc[] `--abbrev=`:: - Instead of using the default _7+1_ hexadecimal digits as the - abbreviated object name, use _+1_ digits, where __ is at - least __ but ensures the commit object names are unique. - Note that 1 column - is used for a caret to mark the boundary commit. + Instead of using the default _7_ hexadecimal digits as the + abbreviated object name, use at least __ digits, but ensure + the commit object names are unique. + If commits marked with caret (boundary), question mark (ignored) + or asterisk (unblamable) are shown, extend unmarked object names + to align them. THE DEFAULT FORMAT diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc index 2ba812179533b8..28b477cd378150 100644 --- a/Documentation/git-history.adoc +++ b/Documentation/git-history.adoc @@ -8,6 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history SYNOPSIS -------- [synopsis] +git history drop [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)] git history fixup [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)] git history reword [--dry-run] [--update-refs=(branches|head)] git history split [--dry-run] [--update-refs=(branches|head)] [--] [...] @@ -51,13 +52,28 @@ be stateful operations. The limitation can be lifted once (if) Git learns about first-class conflicts. When using `fixup` with `--empty=drop`, dropping the root commit is not yet -supported. +supported. Likewise, `drop` cannot remove the root commit or a merge commit. COMMANDS -------- The following commands are available to rewrite history in different ways: +`drop `:: + Remove the specified commit from the history. All descendants of the + commit are replayed directly onto its parent. ++ +The root commit cannot be dropped as that may lead to edge cases where refs +end up with no commits anymore. Merge commits cannot be dropped either; see +LIMITATIONS. ++ +If `HEAD` points at a commit that is to be rewritten, the index and working +tree are updated to match the new `HEAD`. The command aborts before any +references are updated in case local modifications would be overwritten. ++ +If replaying any descendant would result in a conflict, the command aborts +with an error. + `fixup `:: Apply the currently staged changes to the specified commit. This is similar in nature to `git commit --fixup=` followed by `git @@ -170,6 +186,26 @@ The staged addition of `unrelated.txt` has been incorporated into the `first` commit. All descendant commits have been replayed on top of the rewritten history. +Drop a commit +~~~~~~~~~~~~~ + +---------- +$ git log --oneline +abc1234 (HEAD -> main) third +def5678 second +ghi9012 first + +$ git history drop 'main^{/second}' + +$ git log --oneline +jkl3456 (HEAD -> main) third +ghi9012 first +---------- + +The `second` commit has been removed from the history, and `third` has been +replayed directly on top of `first`. All branches that pointed at the dropped +commit have been moved to its parent. + Split a commit ~~~~~~~~~~~~~~ diff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc index fa33680cc781fe..ce278c59bfc1dc 100644 --- a/Documentation/git-refs.adoc +++ b/Documentation/git-refs.adoc @@ -20,6 +20,10 @@ git refs list [--count=] [--shell|--perl|--python|--tcl] [ --stdin | (...)] git refs exists git refs optimize [--all] [--no-prune] [--auto] [--include ] [--exclude ] +git refs create [--message=] [--no-deref] [--create-reflog] +git refs delete [--message=] [--no-deref] [] +git refs update [--message=] [--no-deref] [--create-reflog] [] +git refs rename [--message=] DESCRIPTION ----------- @@ -51,6 +55,28 @@ optimize:: usage. This subcommand is an alias for linkgit:git-pack-refs[1] and offers identical functionality. +create:: + Create the given reference, which must not already exist, pointing at + ``. + +delete:: + Delete the given reference. This subcommand mirrors `git update-ref -d` + (see linkgit:git-update-ref[1]). When `` is given, the + reference is only deleted after verifying that it currently contains + ``. + +update:: + Update the given reference to point at ``. If `` + is given, the reference is only updated after verifying that it + currently contains ``. As a special case, an all-zeroes + `` deletes the branch, whereas an all-zeroes `` + ensures that the branch does not yet exist. + +rename:: + Rename the reference `` to ``. The old reference must + exist and the new reference must not yet exist, and both must have a + well-formed name (see linkgit:git-check-ref-format[1]). + OPTIONS ------- @@ -90,6 +116,20 @@ The following options are specific to 'git refs optimize': include::pack-refs-options.adoc[] +The following options are specific to commands which write references: + +`--create-reflog`:: + Create a reflog for the reference even if one would not ordinarily be + created. + +`--message=`:: + Use the given string for the reflog entry associated with the + update. An empty message is rejected. + +`--no-deref`:: + Operate on itself rather than the reference it points to via a + symbolic ref. + KNOWN LIMITATIONS ----------------- diff --git a/bloom.c b/bloom.c index a805ac0c296b37..c98d1672adb71a 100644 --- a/bloom.c +++ b/bloom.c @@ -16,6 +16,7 @@ define_commit_slab(bloom_filter_slab, struct bloom_filter); static struct bloom_filter_slab bloom_filters; +static int bloom_filter_slab_initialized; struct pathmap_hash_entry { struct hashmap_entry entry; @@ -263,7 +264,10 @@ void add_key_to_filter(const struct bloom_key *key, void init_bloom_filters(void) { + if (bloom_filter_slab_initialized) + return; init_bloom_filter_slab(&bloom_filters); + bloom_filter_slab_initialized = 1; } static void free_one_bloom_filter(struct bloom_filter *filter) @@ -276,6 +280,7 @@ static void free_one_bloom_filter(struct bloom_filter *filter) void deinit_bloom_filters(void) { deep_clear_bloom_filter_slab(&bloom_filters, free_one_bloom_filter); + bloom_filter_slab_initialized = 0; } struct bloom_keyvec *bloom_keyvec_new(const char *path, size_t len, diff --git a/builtin/blame.c b/builtin/blame.c index 553f4cb78055eb..409af18f0fc1cf 100644 --- a/builtin/blame.c +++ b/builtin/blame.c @@ -453,6 +453,36 @@ static void determine_line_heat(struct commit_info *ci, const char **dest_color) *dest_color = colorfield[i].col; } +static inline int maybe_putc(int c, FILE *out) +{ + return out ? putc(c, out) : 0; +} + +static size_t print_marks(FILE *out, const struct blame_entry *ent, int opt) +{ + size_t len = 0; + + if ((ent->suspect->commit->object.flags & UNINTERESTING) && + !blank_boundary && !(opt & OUTPUT_ANNOTATE_COMPAT)) { + maybe_putc('^', out); + len++; + } + if (mark_unblamable_lines && ent->unblamable) { + maybe_putc('*', out); + len++; + } + if (mark_ignored_lines && ent->ignored) { + maybe_putc('?', out); + len++; + } + return len; +} + +static size_t count_marks(const struct blame_entry *ent, int opt) +{ + return print_marks(NULL, ent, opt); +} + static void emit_other(struct blame_scoreboard *sb, struct blame_entry *ent, int opt, struct blame_entry *prev_ent) { @@ -499,23 +529,10 @@ static void emit_other(struct blame_scoreboard *sb, struct blame_entry *ent, if (color) fputs(color, stdout); - if (suspect->commit->object.flags & UNINTERESTING) { - if (blank_boundary) { - memset(hex, ' ', strlen(hex)); - } else if (!(opt & OUTPUT_ANNOTATE_COMPAT)) { - length--; - putchar('^'); - } - } - - if (mark_unblamable_lines && ent->unblamable) { - length--; - putchar('*'); - } - if (mark_ignored_lines && ent->ignored) { - length--; - putchar('?'); - } + if ((suspect->commit->object.flags & UNINTERESTING) && + blank_boundary) + memset(hex, ' ', strlen(hex)); + length -= print_marks(stdout, ent, opt); printf("%.*s", (int)(length < GIT_MAX_HEXSZ ? length : GIT_MAX_HEXSZ), hex); if (opt & OUTPUT_ANNOTATE_COMPAT) { @@ -647,11 +664,15 @@ static void find_alignment(struct blame_scoreboard *sb, int *option) struct blame_entry *e; int compute_auto_abbrev = (abbrev < 0); int auto_abbrev = DEFAULT_ABBREV; + size_t max_marks_count = 0; for (e = sb->ent; e; e = e->next) { struct blame_origin *suspect = e->suspect; int num; + size_t marks_count = count_marks(e, *option); + if (max_marks_count < marks_count) + max_marks_count = marks_count; if (compute_auto_abbrev) auto_abbrev = update_auto_abbrev(auto_abbrev, suspect); if (strcmp(suspect->path, sb->path)) @@ -685,8 +706,12 @@ static void find_alignment(struct blame_scoreboard *sb, int *option) max_score_digits = decimal_width(largest_score); if (compute_auto_abbrev) - /* one more abbrev length is needed for the boundary commit */ - abbrev = auto_abbrev + 1; + abbrev = auto_abbrev; + if (abbrev < (int)the_hash_algo->hexsz) { + abbrev += max_marks_count; + if (abbrev > (int)the_hash_algo->hexsz) + abbrev = the_hash_algo->hexsz; + } } static void sanity_check_on_fail(struct blame_scoreboard *sb, int baa) @@ -1047,10 +1072,7 @@ int cmd_blame(int argc, } else if (show_progress < 0) show_progress = isatty(2); - if (0 < abbrev && abbrev < (int)the_hash_algo->hexsz) - /* one more abbrev length is needed for the boundary commit */ - abbrev++; - else if (!abbrev) + if (!abbrev) abbrev = the_hash_algo->hexsz; if (revs_file && read_ancestry(revs_file)) diff --git a/builtin/cat-file.c b/builtin/cat-file.c index 60869b8b37fa6b..b4b99a73da9696 100644 --- a/builtin/cat-file.c +++ b/builtin/cat-file.c @@ -500,7 +500,7 @@ static void batch_object_write(const char *obj_name, data->info.sizep = &data->size; if (pack) - ret = packed_object_info(pack, offset, &data->info); + ret = packed_object_info(NULL, pack, offset, &data->info); else ret = odb_read_object_info_extended(the_repository->objects, &data->oid, &data->info, @@ -837,8 +837,9 @@ static int batch_one_object_oi(const struct object_id *oid, void *_payload) { struct for_each_object_payload *payload = _payload; - if (oi && oi->whence == OI_PACKED) - return payload->callback(oid, oi->u.packed.pack, oi->u.packed.offset, + if (oi && oi->source_infop->source->type == ODB_SOURCE_PACKED) + return payload->callback(oid, oi->source_infop->u.packed.pack, + oi->source_infop->u.packed.offset, payload->payload); return payload->callback(oid, NULL, 0, payload->payload); } @@ -909,7 +910,10 @@ static void batch_each_object(struct batch_options *opt, &payload, flags); } } else { - struct object_info oi = { 0 }; + struct odb_source_info source_info; + struct object_info oi = { + .source_infop = &source_info, + }; for (source = the_repository->objects->sources; source; source = source->next) { struct odb_source_files *files = odb_source_files_downcast(source); diff --git a/builtin/history.c b/builtin/history.c index fd83de8265e817..d28c1f08bb66ea 100644 --- a/builtin/history.c +++ b/builtin/history.c @@ -17,13 +17,17 @@ #include "read-cache.h" #include "refs.h" #include "replay.h" +#include "reset.h" #include "revision.h" #include "sequencer.h" #include "strvec.h" #include "tree.h" +#include "tree-walk.h" #include "unpack-trees.h" #include "wt-status.h" +#define GIT_HISTORY_DROP_USAGE \ + N_("git history drop [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)]") #define GIT_HISTORY_FIXUP_USAGE \ N_("git history fixup [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]") #define GIT_HISTORY_REWORD_USAGE \ @@ -52,11 +56,6 @@ static int fill_commit_message(struct repository *repo, " empty message aborts the commit.\n"); struct wt_status s; - strbuf_addstr(out, default_message); - strbuf_addch(out, '\n'); - strbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str); - write_file_buf(path, out->buf, out->len); - wt_status_prepare(repo, &s); FREE_AND_NULL(s.branch); s.ahead_behind_flags = AHEAD_BEHIND_QUICK; @@ -68,14 +67,22 @@ static int fill_commit_message(struct repository *repo, s.whence = FROM_COMMIT; s.committable = 1; - s.fp = fopen(git_path_commit_editmsg(), "a"); + s.fp = fopen(path, "w"); if (!s.fp) - return error_errno(_("could not open '%s'"), git_path_commit_editmsg()); + return error_errno(_("could not open '%s'"), path); + + strbuf_addstr(out, default_message); + strbuf_addch(out, '\n'); + strbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str); + if (fwrite(out->buf, 1, out->len, s.fp) != out->len) + die_errno(_("could not write to '%s'"), path); wt_status_collect_changes_trees(&s, old_tree, new_tree); wt_status_print(&s); wt_status_collect_free_buffers(&s); string_list_clear_func(&s.change, change_data_free); + if (fclose(s.fp)) + die_errno(_("could not write to '%s'"), path); strbuf_reset(out); if (launch_editor(path, out, NULL)) { @@ -333,21 +340,17 @@ static int handle_ref_update(struct ref_transaction *transaction, NULL, NULL, 0, reflog_msg, err); } -static int handle_reference_updates(struct rev_info *revs, - enum ref_action action, - struct commit *original, - struct commit *rewritten, - const char *reflog_msg, - int dry_run, - enum replay_empty_commit_action empty) +static int compute_pending_ref_updates(struct rev_info *revs, + enum ref_action action, + struct commit *original, + struct commit *rewritten, + enum replay_empty_commit_action empty, + struct replay_result *result) { const struct name_decoration *decoration; struct replay_revisions_options opts = { .empty = empty, }; - struct replay_result result = { 0 }; - struct ref_transaction *transaction = NULL; - struct strbuf err = STRBUF_INIT; char hex[GIT_MAX_HEXSZ + 1]; bool detached_head; int head_flags = 0; @@ -359,34 +362,13 @@ static int handle_reference_updates(struct rev_info *revs, opts.onto = oid_to_hex_r(hex, &rewritten->object.oid); - ret = replay_revisions(revs, &opts, &result); + ret = replay_revisions(revs, &opts, result); if (ret) - goto out; + return ret; if (action != REF_ACTION_BRANCHES && action != REF_ACTION_HEAD) BUG("unsupported ref action %d", action); - if (!dry_run) { - transaction = ref_store_transaction_begin(get_main_ref_store(revs->repo), 0, &err); - if (!transaction) { - ret = error(_("failed to begin ref transaction: %s"), err.buf); - goto out; - } - } - - for (size_t i = 0; i < result.updates_nr; i++) { - ret = handle_ref_update(transaction, - result.updates[i].refname, - &result.updates[i].new_oid, - &result.updates[i].old_oid, - reflog_msg, &err); - if (ret) { - ret = error(_("failed to update ref '%s': %s"), - result.updates[i].refname, err.buf); - goto out; - } - } - /* * `replay_revisions()` only updates references that are * ancestors of `rewritten`, so we need to manually @@ -414,14 +396,41 @@ static int handle_reference_updates(struct rev_info *revs, !detached_head) continue; + replay_result_queue_update(result, decoration->name, + &original->object.oid, + &rewritten->object.oid); + } + + return 0; +} + +static int apply_pending_ref_updates(struct repository *repo, + const struct replay_result *result, + const char *reflog_msg, + int dry_run) +{ + struct ref_transaction *transaction = NULL; + struct strbuf err = STRBUF_INIT; + int ret; + + if (!dry_run) { + transaction = ref_store_transaction_begin(get_main_ref_store(repo), + 0, &err); + if (!transaction) { + ret = error(_("failed to begin ref transaction: %s"), err.buf); + goto out; + } + } + + for (size_t i = 0; i < result->updates_nr; i++) { ret = handle_ref_update(transaction, - decoration->name, - &rewritten->object.oid, - &original->object.oid, + result->updates[i].refname, + &result->updates[i].new_oid, + &result->updates[i].old_oid, reflog_msg, &err); if (ret) { ret = error(_("failed to update ref '%s': %s"), - decoration->name, err.buf); + result->updates[i].refname, err.buf); goto out; } } @@ -435,11 +444,33 @@ static int handle_reference_updates(struct rev_info *revs, out: ref_transaction_free(transaction); - replay_result_release(&result); strbuf_release(&err); return ret; } +static int handle_reference_updates(struct rev_info *revs, + enum ref_action action, + struct commit *original, + struct commit *rewritten, + const char *reflog_msg, + int dry_run, + enum replay_empty_commit_action empty) +{ + struct replay_result result = { 0 }; + int ret; + + ret = compute_pending_ref_updates(revs, action, original, rewritten, + empty, &result); + if (ret) + goto out; + + ret = apply_pending_ref_updates(revs->repo, &result, reflog_msg, dry_run); + +out: + replay_result_release(&result); + return ret; +} + static int commit_became_empty(struct repository *repo, struct commit *original, struct tree *result) @@ -975,12 +1006,191 @@ static int cmd_history_split(int argc, return ret; } +static int update_worktree(struct repository *repo, + const struct commit *old_head, + const struct commit *new_head, + bool dry_run) +{ + struct reset_working_tree_options opts = { + .oid_from = &old_head->object.oid, + .oid = &new_head->object.oid, + }; + if (dry_run) + opts.flags |= RESET_WORKING_TREE_DRY_RUN; + return reset_working_tree(repo, &opts); +} + +static int find_head_tree_change(struct repository *repo, + const struct replay_result *result, + struct commit **old_head, + struct commit **new_head, + bool *changed) +{ + const struct replay_ref_update *head_update = NULL; + struct commit *old_head_commit, *new_head_commit; + struct tree *old_head_tree, *new_head_tree; + const char *head_target; + int head_flags; + + *changed = false; + + head_target = refs_resolve_ref_unsafe(get_main_ref_store(repo), "HEAD", + RESOLVE_REF_NO_RECURSE | RESOLVE_REF_READING, + NULL, &head_flags); + if (!head_target) + return error(_("cannot look up HEAD")); + + for (size_t i = 0; i < result->updates_nr; i++) { + if (!strcmp(result->updates[i].refname, head_target)) { + head_update = &result->updates[i]; + break; + } + } + + if (!head_update) + return 0; + + old_head_commit = lookup_commit_reference(repo, &head_update->old_oid); + new_head_commit = lookup_commit_reference(repo, &head_update->new_oid); + if (!old_head_commit || !new_head_commit) + return error(_("cannot resolve HEAD commit")); + + old_head_tree = repo_get_commit_tree(repo, old_head_commit); + new_head_tree = repo_get_commit_tree(repo, new_head_commit); + if (!old_head_tree || !new_head_tree) + return error(_("cannot resolve tree for HEAD")); + + if (oideq(&old_head_tree->object.oid, &new_head_tree->object.oid)) + return 0; + + *old_head = old_head_commit; + *new_head = new_head_commit; + *changed = true; + + return 0; +} + +static int cmd_history_drop(int argc, + const char **argv, + const char *prefix, + struct repository *repo) +{ + const char * const usage[] = { + GIT_HISTORY_DROP_USAGE, + NULL, + }; + enum replay_empty_commit_action empty = REPLAY_EMPTY_COMMIT_DROP; + enum ref_action action = REF_ACTION_DEFAULT; + int dry_run = 0; + struct option options[] = { + OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)", + N_("control which refs should be updated"), + PARSE_OPT_NONEG, parse_ref_action), + OPT_BOOL('n', "dry-run", &dry_run, + N_("perform a dry-run without updating any refs")), + OPT_CALLBACK_F(0, "empty", &empty, "(drop|keep|abort)", + N_("how to handle descendants that become empty"), + PARSE_OPT_NONEG, parse_opt_empty), + OPT_END(), + }; + struct strbuf reflog_msg = STRBUF_INIT; + struct commit *original, *rewritten; + struct rev_info revs = { 0 }; + struct replay_result result = { 0 }; + struct commit *old_head, *new_head; + bool head_moves = false; + int ret; + + argc = parse_options(argc, argv, prefix, options, usage, 0); + if (argc != 1) { + ret = error(_("command expects a single revision")); + goto out; + } + repo_config(repo, git_default_config, NULL); + + if (action == REF_ACTION_DEFAULT) + action = REF_ACTION_BRANCHES; + + original = lookup_commit_reference_by_name(argv[0]); + if (!original) { + ret = error(_("commit cannot be found: %s"), argv[0]); + goto out; + } + + if (!original->parents) { + ret = error(_("cannot drop root commit %s: " + "it has no parent to replay onto"), + argv[0]); + goto out; + } else if (original->parents->next) { + ret = error(_("cannot drop merge commit: %s"), argv[0]); + goto out; + } + + ret = setup_revwalk(repo, action, original, &revs); + if (ret) + goto out; + + rewritten = original->parents->item; + + ret = compute_pending_ref_updates(&revs, action, original, rewritten, + empty, &result); + if (ret) { + ret = error(_("failed replaying descendants")); + goto out; + } + + /* + * If HEAD will move as a result of the rewrite then we'll have to + * merge in the changes into the worktree and index. This merge can of + * course conflict, which will cause the whole operation to abort. + * + * If we had already updated the refs at that point then we'd have an + * inconsistent repository state. So we first perform a dry-run merge + * here before updating refs. + */ + if (!is_bare_repository(repo)) { + ret = find_head_tree_change(repo, &result, &old_head, + &new_head, &head_moves); + if (ret < 0) + goto out; + + if (head_moves && update_worktree(repo, old_head, new_head, true) < 0) { + ret = error(_("dropping this commit would " + "overwrite local changes; aborting")); + goto out; + } + } + + strbuf_addf(&reflog_msg, "drop: dropping %s", argv[0]); + ret = apply_pending_ref_updates(repo, &result, reflog_msg.buf, dry_run); + if (ret < 0) { + ret = error(_("failed to update references")); + goto out; + } + + if (!dry_run && head_moves && update_worktree(repo, old_head, new_head, false) < 0) { + ret = error(_("could not update working tree to new commit %s"), + oid_to_hex(&new_head->object.oid)); + goto out; + } + + ret = 0; + +out: + replay_result_release(&result); + strbuf_release(&reflog_msg); + release_revisions(&revs); + return ret; +} + int cmd_history(int argc, const char **argv, const char *prefix, struct repository *repo) { const char * const usage[] = { + GIT_HISTORY_DROP_USAGE, GIT_HISTORY_FIXUP_USAGE, GIT_HISTORY_REWORD_USAGE, GIT_HISTORY_SPLIT_USAGE, @@ -988,6 +1198,7 @@ int cmd_history(int argc, }; parse_opt_subcommand_fn *fn = NULL; struct option options[] = { + OPT_SUBCOMMAND("drop", &fn, cmd_history_drop), OPT_SUBCOMMAND("fixup", &fn, cmd_history_fixup), OPT_SUBCOMMAND("reword", &fn, cmd_history_reword), OPT_SUBCOMMAND("split", &fn, cmd_history_split), diff --git a/builtin/index-pack.c b/builtin/index-pack.c index f3966584683990..7af1aea6f9f13f 100644 --- a/builtin/index-pack.c +++ b/builtin/index-pack.c @@ -1825,11 +1825,16 @@ static void repack_local_links(void) oidset_iter_init(&outgoing_links, &iter); while ((oid = oidset_iter_next(&iter))) { - struct object_info info = OBJECT_INFO_INIT; + struct odb_source_info source_info; + struct object_info info = { + .source_infop = &source_info, + }; + if (odb_read_object_info_extended(the_repository->objects, oid, &info, 0)) /* Missing; assume it is a promisor object */ continue; - if (info.whence == OI_PACKED && info.u.packed.pack->pack_promisor) + if (source_info.source->type == ODB_SOURCE_PACKED && + source_info.u.packed.pack->pack_promisor) continue; if (!cmd.args.nr) { diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c index e3760b34925bbf..ea5eab4cf841bf 100644 --- a/builtin/pack-objects.c +++ b/builtin/pack-objects.c @@ -2463,7 +2463,7 @@ static void drop_reused_delta(struct object_entry *entry) oi.sizep = &size; oi.typep = &type; - if (packed_object_info(IN_PACK(entry), entry->in_pack_offset, &oi) < 0) { + if (packed_object_info(NULL, IN_PACK(entry), entry->in_pack_offset, &oi) < 0) { /* * We failed to get the info from this pack for some reason; * fall back to odb_read_object_info, which may find another copy. @@ -3820,7 +3820,7 @@ static int add_object_entry_from_pack(const struct object_id *oid, ofs = nth_packed_object_offset(p, pos); oi.typep = &type; - if (packed_object_info(p, ofs, &oi) < 0) { + if (packed_object_info(NULL, p, ofs, &oi) < 0) { die(_("could not get type of object %s in pack %s"), oid_to_hex(oid), p->pack_name); } else if (type == OBJ_COMMIT) { @@ -4495,8 +4495,9 @@ static int add_object_in_unpacked_pack(const struct object_id *oid, void *data UNUSED) { if (cruft) { - add_cruft_object_entry(oid, OBJ_NONE, oi->u.packed.pack, - oi->u.packed.offset, NULL, *oi->mtimep); + add_cruft_object_entry(oid, OBJ_NONE, oi->source_infop->u.packed.pack, + oi->source_infop->u.packed.offset, NULL, + *oi->mtimep); } else { add_object_entry(oid, OBJ_NONE, "", 0); } @@ -4513,8 +4514,10 @@ static void add_objects_in_unpacked_packs(void) ODB_FOR_EACH_OBJECT_SKIP_IN_CORE_KEPT_PACKS | ODB_FOR_EACH_OBJECT_SKIP_ON_DISK_KEPT_PACKS, }; + struct odb_source_info source_info; struct object_info oi = { .mtimep = &mtime, + .source_infop = &source_info, }; odb_prepare_alternates(to_pack.repo->objects); @@ -5036,10 +5039,14 @@ static int option_parse_cruft_expiration(const struct option *opt UNUSED, static int is_not_in_promisor_pack_obj(struct object *obj, void *data UNUSED) { - struct object_info info = OBJECT_INFO_INIT; + struct odb_source_info source_info; + struct object_info info = { + .source_infop = &source_info, + }; + if (odb_read_object_info_extended(the_repository->objects, &obj->oid, &info, 0)) BUG("should_include_obj should only be called on existing objects"); - return info.whence != OI_PACKED || !info.u.packed.pack->pack_promisor; + return source_info.source->type != ODB_SOURCE_PACKED || !source_info.u.packed.pack->pack_promisor; } static int is_not_in_promisor_pack(struct commit *commit, void *data) { diff --git a/builtin/rebase.c b/builtin/rebase.c index fa4f5d9306b856..10a306310cd439 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -592,7 +592,7 @@ static int finish_rebase(struct rebase_options *opts) static int move_to_original_branch(struct rebase_options *opts) { struct strbuf branch_reflog = STRBUF_INIT, head_reflog = STRBUF_INIT; - struct reset_head_opts ropts = { 0 }; + struct reset_working_tree_options ropts = { 0 }; int ret; if (!opts->head_name) @@ -607,10 +607,11 @@ static int move_to_original_branch(struct rebase_options *opts) strbuf_addf(&head_reflog, "%s (finish): returning to %s", opts->reflog_action, opts->head_name); ropts.branch = opts->head_name; - ropts.flags = RESET_HEAD_REFS_ONLY; + ropts.flags = RESET_WORKING_TREE_REFS_ONLY | + RESET_WORKING_TREE_UPDATE_HEAD; ropts.branch_msg = branch_reflog.buf; ropts.head_msg = head_reflog.buf; - ret = reset_head(the_repository, &ropts); + ret = reset_working_tree(the_repository, &ropts); strbuf_release(&branch_reflog); strbuf_release(&head_reflog); @@ -685,7 +686,7 @@ static int run_am(struct rebase_options *opts) status = run_command(&format_patch); if (status) { - struct reset_head_opts ropts = { 0 }; + struct reset_working_tree_options ropts = { 0 }; unlink(rebased_patches); free(rebased_patches); child_process_clear(&am); @@ -693,7 +694,8 @@ static int run_am(struct rebase_options *opts) ropts.oid = &opts->orig_head->object.oid; ropts.branch = opts->head_name; ropts.default_reflog_action = opts->reflog_action; - reset_head(the_repository, &ropts); + ropts.flags = RESET_WORKING_TREE_UPDATE_HEAD; + reset_working_tree(the_repository, &ropts); error(_("\ngit encountered an error while preparing the " "patches to replay\n" "these revisions:\n" @@ -855,18 +857,19 @@ static int rebase_config(const char *var, const char *value, static int checkout_up_to_date(struct rebase_options *options) { struct strbuf buf = STRBUF_INIT; - struct reset_head_opts ropts = { 0 }; + struct reset_working_tree_options ropts = { 0 }; int ret = 0; strbuf_addf(&buf, "%s: checkout %s", options->reflog_action, options->switch_to); ropts.oid = &options->orig_head->object.oid; ropts.branch = options->head_name; - ropts.flags = RESET_HEAD_RUN_POST_CHECKOUT_HOOK; + ropts.flags = RESET_WORKING_TREE_RUN_POST_CHECKOUT_HOOK | + RESET_WORKING_TREE_UPDATE_HEAD; if (!ropts.branch) - ropts.flags |= RESET_HEAD_DETACH; + ropts.flags |= RESET_WORKING_TREE_DETACH; ropts.head_msg = buf.buf; - if (reset_head(the_repository, &ropts) < 0) + if (reset_working_tree(the_repository, &ropts) < 0) ret = error(_("could not switch to %s"), options->switch_to); strbuf_release(&buf); @@ -1116,7 +1119,7 @@ int cmd_rebase(int argc, int reschedule_failed_exec = -1; int allow_preemptive_ff = 1; int preserve_merges_selected = 0; - struct reset_head_opts ropts = { 0 }; + struct reset_working_tree_options ropts = { 0 }; struct option builtin_rebase_options[] = { OPT_STRING(0, "onto", &options.onto_name, N_("revision"), @@ -1384,8 +1387,9 @@ int cmd_rebase(int argc, rerere_clear(the_repository, &merge_rr); string_list_clear(&merge_rr, 1); - ropts.flags = RESET_HEAD_HARD; - if (reset_head(the_repository, &ropts) < 0) + ropts.flags = RESET_WORKING_TREE_HARD | + RESET_WORKING_TREE_UPDATE_HEAD; + if (reset_working_tree(the_repository, &ropts) < 0) die(_("could not discard worktree changes")); remove_branch_state(the_repository, 0); if (read_basic_state(&options)) @@ -1409,8 +1413,9 @@ int cmd_rebase(int argc, ropts.oid = &options.orig_head->object.oid; ropts.head_msg = head_msg.buf; ropts.branch = options.head_name; - ropts.flags = RESET_HEAD_HARD; - if (reset_head(the_repository, &ropts) < 0) + ropts.flags = RESET_WORKING_TREE_HARD | + RESET_WORKING_TREE_UPDATE_HEAD; + if (reset_working_tree(the_repository, &ropts) < 0) die(_("could not move back to %s"), oid_to_hex(&options.orig_head->object.oid)); strbuf_release(&head_msg); @@ -1876,11 +1881,13 @@ int cmd_rebase(int argc, options.reflog_action, options.onto_name); ropts.oid = &options.onto->object.oid; ropts.orig_head = &options.orig_head->object.oid; - ropts.flags = RESET_HEAD_DETACH | RESET_ORIG_HEAD | - RESET_HEAD_RUN_POST_CHECKOUT_HOOK; + ropts.flags = RESET_WORKING_TREE_DETACH | + RESET_WORKING_TREE_UPDATE_HEAD | + RESET_WORKING_TREE_UPDATE_ORIG_HEAD | + RESET_WORKING_TREE_RUN_POST_CHECKOUT_HOOK; ropts.head_msg = msg.buf; ropts.default_reflog_action = options.reflog_action; - if (reset_head(the_repository, &ropts)) { + if (reset_working_tree(the_repository, &ropts)) { ret = error(_("Could not detach HEAD")); goto cleanup_autostash; } diff --git a/builtin/refs.c b/builtin/refs.c index e3125bc61b20e0..a9ca2058eeb55c 100644 --- a/builtin/refs.c +++ b/builtin/refs.c @@ -1,4 +1,3 @@ -#define USE_THE_REPOSITORY_VARIABLE #include "builtin.h" #include "config.h" #include "fsck.h" @@ -22,8 +21,20 @@ #define REFS_OPTIMIZE_USAGE \ N_("git refs optimize " PACK_REFS_OPTS) +#define REFS_CREATE_USAGE \ + N_("git refs create [--message=] [--no-deref] [--create-reflog] ") + +#define REFS_DELETE_USAGE \ + N_("git refs delete [--message=] [--no-deref] []") + +#define REFS_UPDATE_USAGE \ + N_("git refs update [--message=] [--no-deref] [--create-reflog] []") + +#define REFS_RENAME_USAGE \ + N_("git refs rename [--message=] ") + static int cmd_refs_migrate(int argc, const char **argv, const char *prefix, - struct repository *repo UNUSED) + struct repository *repo) { const char * const migrate_usage[] = { REFS_MIGRATE_USAGE, @@ -59,13 +70,13 @@ static int cmd_refs_migrate(int argc, const char **argv, const char *prefix, goto out; } - if (the_repository->ref_storage_format == format) { + if (repo->ref_storage_format == format) { err = error(_("repository already uses '%s' format"), ref_storage_format_to_name(format)); goto out; } - if (repo_migrate_ref_storage_format(the_repository, format, flags, &errbuf) < 0) { + if (repo_migrate_ref_storage_format(repo, format, flags, &errbuf) < 0) { err = error("%s", errbuf.buf); goto out; } @@ -99,8 +110,8 @@ static int cmd_refs_verify(int argc, const char **argv, const char *prefix, if (argc) usage(_("'git refs verify' takes no arguments")); - repo_config(the_repository, git_fsck_config, &fsck_refs_options); - prepare_repo_settings(the_repository); + repo_config(repo, git_fsck_config, &fsck_refs_options); + prepare_repo_settings(repo); worktrees = get_worktrees_without_reading_head(); for (size_t i = 0; worktrees[i]; i++) @@ -124,7 +135,7 @@ static int cmd_refs_list(int argc, const char **argv, const char *prefix, } static int cmd_refs_exists(int argc, const char **argv, const char *prefix, - struct repository *repo UNUSED) + struct repository *repo) { struct strbuf unused_referent = STRBUF_INIT; struct object_id unused_oid; @@ -145,7 +156,7 @@ static int cmd_refs_exists(int argc, const char **argv, const char *prefix, die(_("'git refs exists' requires a reference")); ref = *argv++; - if (refs_read_raw_ref(get_main_ref_store(the_repository), ref, + if (refs_read_raw_ref(get_main_ref_store(repo), ref, &unused_oid, &unused_referent, &unused_type, &failure_errno)) { if (failure_errno == ENOENT || failure_errno == EISDIR) { @@ -176,6 +187,193 @@ static int cmd_refs_optimize(int argc, const char **argv, const char *prefix, return pack_refs_core(argc, argv, prefix, repo, refs_optimize_usage); } +static int cmd_refs_create(int argc, const char **argv, const char *prefix, + struct repository *repo) +{ + static char const * const refs_create_usage[] = { + REFS_CREATE_USAGE, + NULL + }; + const char *message = NULL; + unsigned flags = 0; + struct option opts[] = { + OPT_STRING(0, "message", &message, N_("reason"), + N_("reason of the update")), + OPT_BIT(0 ,"no-deref", &flags, + N_("update not the one it points to"), + REF_NO_DEREF), + OPT_BIT(0, "create-reflog", &flags, N_("create a reflog"), + REF_FORCE_CREATE_REFLOG), + OPT_END(), + }; + struct object_id newoid; + const char *refname; + int ret; + + argc = parse_options(argc, argv, prefix, opts, refs_create_usage, 0); + if (argc != 2) + usage(_("create requires reference name and an object ID")); + + if (message && !*message) + die(_("refusing to perform update with empty message")); + + repo_config(repo, git_default_config, NULL); + + refname = argv[0]; + if (repo_get_oid_with_flags(repo, argv[1], &newoid, GET_OID_SKIP_AMBIGUITY_CHECK)) + die(_("invalid object ID: '%s'"), argv[1]); + if (is_null_oid(&newoid)) + die(_("cannot create reference with null new object ID")); + + ret = refs_update_ref(get_main_ref_store(repo), message, refname, + &newoid, null_oid(repo->hash_algo), flags, + UPDATE_REFS_MSG_ON_ERR); + + if (ret < 0) + ret = 1; + return ret; +} + +static int cmd_refs_delete(int argc, const char **argv, const char *prefix, + struct repository *repo) +{ + static char const * const refs_delete_usage[] = { + REFS_DELETE_USAGE, + NULL + }; + const char *message = NULL; + unsigned flags = 0; + struct option opts[] = { + OPT_STRING(0, "message", &message, N_("reason"), + N_("reason of the update")), + OPT_BIT(0 ,"no-deref", &flags, + N_("update not the one it points to"), + REF_NO_DEREF), + OPT_END(), + }; + struct object_id oldoid; + const char *refname; + int ret; + + argc = parse_options(argc, argv, prefix, opts, refs_delete_usage, 0); + if (argc < 1 || argc > 2) + usage(_("delete requires reference name and an optional old object ID")); + + if (message && !*message) + die(_("refusing to perform update with empty message")); + + repo_config(repo, git_default_config, NULL); + + refname = argv[0]; + if (argc == 2) { + if (repo_get_oid_with_flags(repo, argv[1], &oldoid, GET_OID_SKIP_AMBIGUITY_CHECK)) + die(_("invalid old object ID: '%s'"), argv[1]); + if (is_null_oid(&oldoid)) + die(_("cannot delete reference with null old object ID")); + } + + ret = refs_delete_ref(get_main_ref_store(repo), message, refname, + argc == 2 ? &oldoid : NULL, flags); + + if (ret < 0) + ret = 1; + return ret; +} + +static int cmd_refs_update(int argc, const char **argv, const char *prefix, + struct repository *repo) +{ + static char const * const refs_update_usage[] = { + REFS_UPDATE_USAGE, + NULL + }; + const char *message = NULL; + unsigned flags = 0; + struct option opts[] = { + OPT_STRING(0, "message", &message, N_("reason"), + N_("reason of the update")), + OPT_BIT(0 ,"no-deref", &flags, + N_("update not the one it points to"), + REF_NO_DEREF), + OPT_BIT(0, "create-reflog", &flags, N_("create a reflog"), + REF_FORCE_CREATE_REFLOG), + OPT_END(), + }; + struct object_id newoid, oldoid; + const char *refname; + int ret; + + argc = parse_options(argc, argv, prefix, opts, refs_update_usage, 0); + if (argc < 2 || argc > 3) + usage(_("update requires reference name, new value and an optional old value")); + + if (message && !*message) + die(_("refusing to perform update with empty message")); + + repo_config(repo, git_default_config, NULL); + + refname = argv[0]; + if (repo_get_oid_with_flags(repo, argv[1], &newoid, + GET_OID_SKIP_AMBIGUITY_CHECK)) + die(_("invalid new object ID: '%s'"), argv[1]); + if (argc == 3 && + repo_get_oid_with_flags(repo, argv[2], &oldoid, + GET_OID_SKIP_AMBIGUITY_CHECK)) + die(_("invalid old object ID: '%s'"), argv[2]); + + ret = refs_update_ref(get_main_ref_store(repo), message, refname, + &newoid, argc == 3 ? &oldoid : NULL, flags, + UPDATE_REFS_MSG_ON_ERR); + + if (ret < 0) + ret = 1; + return ret; +} + +static int cmd_refs_rename(int argc, const char **argv, const char *prefix, + struct repository *repo) +{ + static char const * const refs_rename_usage[] = { + REFS_RENAME_USAGE, + NULL + }; + const char *message = NULL; + struct option opts[] = { + OPT_STRING(0, "message", &message, N_("reason"), + N_("reason of the update")), + OPT_END(), + }; + const char *oldref, *newref; + int ret; + + argc = parse_options(argc, argv, prefix, opts, refs_rename_usage, 0); + if (argc != 2) + usage(_("rename requires old and new reference name")); + if (message && !*message) + die(_("refusing to perform update with empty message")); + + repo_config(repo, git_default_config, NULL); + + oldref = argv[0]; + newref = argv[1]; + + if (check_refname_format(oldref, 0)) + die(_("invalid ref format: '%s'"), oldref); + if (check_refname_format(newref, 0)) + die(_("invalid ref format: '%s'"), newref); + + if (!refs_ref_exists(get_main_ref_store(repo), oldref)) + die(_("reference does not exist: '%s'"), oldref); + if (refs_ref_exists(get_main_ref_store(repo), newref)) + die(_("reference already exists: '%s'"), newref); + + ret = refs_rename_ref(get_main_ref_store(repo), oldref, newref, message); + + if (ret < 0) + ret = 1; + return ret; +} + int cmd_refs(int argc, const char **argv, const char *prefix, @@ -187,6 +385,10 @@ int cmd_refs(int argc, "git refs list " COMMON_USAGE_FOR_EACH_REF, REFS_EXISTS_USAGE, REFS_OPTIMIZE_USAGE, + REFS_CREATE_USAGE, + REFS_DELETE_USAGE, + REFS_UPDATE_USAGE, + REFS_RENAME_USAGE, NULL, }; parse_opt_subcommand_fn *fn = NULL; @@ -196,6 +398,10 @@ int cmd_refs(int argc, OPT_SUBCOMMAND("list", &fn, cmd_refs_list), OPT_SUBCOMMAND("exists", &fn, cmd_refs_exists), OPT_SUBCOMMAND("optimize", &fn, cmd_refs_optimize), + OPT_SUBCOMMAND("create", &fn, cmd_refs_create), + OPT_SUBCOMMAND("delete", &fn, cmd_refs_delete), + OPT_SUBCOMMAND("update", &fn, cmd_refs_update), + OPT_SUBCOMMAND("rename", &fn, cmd_refs_rename), OPT_END(), }; diff --git a/commit-graph.c b/commit-graph.c index c6d9c5c740e94d..9dc8bd5eee785f 100644 --- a/commit-graph.c +++ b/commit-graph.c @@ -1538,7 +1538,7 @@ static int add_packed_commits(const struct object_id *oid, struct object_info oi = OBJECT_INFO_INIT; oi.typep = &type; - if (packed_object_info(pack, offset, &oi) < 0) + if (packed_object_info(NULL, pack, offset, &oi) < 0) die(_("unable to get type of object %s"), oid_to_hex(oid)); return add_packed_commits_oi(oid, &oi, data); diff --git a/line-log.c b/line-log.c index 5fc75ae275e03a..0179f138f70288 100644 --- a/line-log.c +++ b/line-log.c @@ -1141,8 +1141,7 @@ int line_log_process_ranges_arbitrary_commit(struct rev_info *rev, struct commit if (range) { if (commit->parents && !bloom_filter_check(rev, commit, range)) { - struct line_log_data *prange = line_log_data_copy(range); - add_line_range(rev, commit->parents->item, prange); + add_line_range(rev, commit->parents->item, range); clear_commit_line_range(rev, commit); } else if (commit->parents && commit->parents->next) changed = process_ranges_merge_commit(rev, commit, range); diff --git a/meson.build b/meson.build index ca235801cf4848..9434b56960ba80 100644 --- a/meson.build +++ b/meson.build @@ -278,7 +278,20 @@ compat_sources = [ 'compat/terminal.c', ] +hook_list = custom_target( + input: 'Documentation/githooks.adoc', + output: 'hook-list.h', + command: [ + shell, + meson.current_source_dir() + '/tools/generate-hooklist.sh', + meson.current_source_dir(), + '@OUTPUT@', + ], + env: script_environment, +) + libgit_sources = [ + hook_list, 'abspath.c', 'add-interactive.c', 'add-patch.c', @@ -568,19 +581,8 @@ libgit_sources += custom_target( env: script_environment, ) -libgit_sources += custom_target( - input: 'Documentation/githooks.adoc', - output: 'hook-list.h', - command: [ - shell, - meson.current_source_dir() + '/tools/generate-hooklist.sh', - meson.current_source_dir(), - '@OUTPUT@', - ], - env: script_environment, -) - builtin_sources = [ + hook_list, 'builtin/add.c', 'builtin/am.c', 'builtin/annotate.c', diff --git a/odb.c b/odb.c index 68dca6357f7548..cf6e7938c01e56 100644 --- a/odb.c +++ b/odb.c @@ -691,8 +691,8 @@ static int oid_object_info_convert(struct repository *r, return -1; } } - input_oi->whence = new_oi.whence; - input_oi->u = new_oi.u; + if (input_oi->source_infop) + *input_oi->source_infop = *new_oi.source_infop; return ret; } diff --git a/odb.h b/odb.h index a1948650526cca..94754643d24997 100644 --- a/odb.h +++ b/odb.h @@ -260,33 +260,19 @@ int odb_pretend_object(struct object_database *odb, void *buf, size_t len, enum object_type type, struct object_id *oid); -struct object_info { - /* Request */ - enum object_type *typep; - size_t *sizep; - off_t *disk_sizep; - struct object_id *delta_base_oid; - void **contentp; +/* + * Object database source information that can be used to uniquely identify an + * object and learn more about how exactly it is stored. + */ +struct odb_source_info { + /* The source that this object has been looked up from. */ + struct odb_source *source; /* - * The time the given looked-up object has been last modified. - * - * Note: the mtime may be ambiguous in case the object exists multiple - * times in the object database. It is thus _not_ recommended to use - * this field outside of contexts where you would read every instance - * of the object, like for example with `odb_for_each_object()`. As it - * is impossible to say at the ODB level what the intent of the caller - * is (e.g. whether to find the oldest or newest object), it is the - * responsibility of the caller to disambiguate the mtimes. + * Backend-specific information about the specific object. This can be + * used for example to uniquely identify a given object in case it + * exists multiple times. */ - time_t *mtimep; - - /* Response */ - enum { - OI_CACHED, - OI_LOOSE, - OI_PACKED, - } whence; union { /* * struct { @@ -309,6 +295,52 @@ struct object_info { } u; }; +/* + * The object info contains the query and response that is to be used for + * functions that end up reading object information. Callers are expected to + * populate pointers whose information they want to request. + */ +struct object_info { + /* The object type. */ + enum object_type *typep; + + /* The inflated object size in bytes. */ + size_t *sizep; + + /* The object size as stored on disk. */ + off_t *disk_sizep; + + /* + * The base the object is deltified against, in case it is stored as a + * delta. + */ + struct object_id *delta_base_oid; + + /* The object contents. Ownership of memory goes over to the caller. */ + void **contentp; + + /* + * The time the given looked-up object has been last modified. + * + * Note: the mtime may be ambiguous in case the object exists multiple + * times in the object database. It is thus _not_ recommended to use + * this field outside of contexts where you would read every instance + * of the object, like for example with `odb_for_each_object()`. As it + * is impossible to say at the ODB level what the intent of the caller + * is (e.g. whether to find the oldest or newest object), it is the + * responsibility of the caller to disambiguate the mtimes. + */ + time_t *mtimep; + + /* + * Backend-specific information that tells the caller where exactly an + * object was looked up from. This information should help disambiguate + * object lookups in case the same object exists in multiple sources, + * or multiple times in the same source. + */ + struct odb_source_info *source_infop; +}; + /* * Initializer for a "struct object_info" that wants no items. You may * also memset() the memory to all-zeroes. diff --git a/odb/source-inmemory.c b/odb/source-inmemory.c index 0b51c582384ee9..f41a602e641897 100644 --- a/odb/source-inmemory.c +++ b/odb/source-inmemory.c @@ -52,7 +52,8 @@ static void populate_object_info(struct odb_source_inmemory *source, *oi->contentp = xmemdupz(object->buf, object->size); if (oi->mtimep) *oi->mtimep = 0; - oi->whence = OI_CACHED; + if (oi->source_infop) + oi->source_infop->source = &source->base; } static int odb_source_inmemory_read_object_info(struct odb_source *source, diff --git a/odb/source-loose.c b/odb/source-loose.c index a404046d503a4b..091e8e00a77bf4 100644 --- a/odb/source-loose.c +++ b/odb/source-loose.c @@ -196,8 +196,8 @@ static int read_object_info_from_path(struct odb_source_loose *loose, oi->typep = NULL; if (oi->delta_base_oid) oidclr(oi->delta_base_oid, loose->base.odb->repo->hash_algo); - if (!ret) - oi->whence = OI_LOOSE; + if (oi->source_infop && !ret) + oi->source_infop->source = &loose->base; } return ret; diff --git a/odb/source-packed.c b/odb/source-packed.c index a0259b95bfb5f2..647f23cdb9edc9 100644 --- a/odb/source-packed.c +++ b/odb/source-packed.c @@ -59,7 +59,7 @@ static int odb_source_packed_read_object_info(struct odb_source *source, if (!oi) return 0; - ret = packed_object_info(e.p, e.offset, oi); + ret = packed_object_info(packed, e.p, e.offset, oi); if (ret < 0) { mark_bad_packed_object(e.p, oid); return -1; @@ -99,7 +99,7 @@ static int odb_source_packed_for_each_object_wrapper(const struct object_id *oid off_t offset = nth_packed_object_offset(pack, index_pos); struct object_info oi = *data->request; - if (packed_object_info_with_index_pos(pack, offset, + if (packed_object_info_with_index_pos(data->store, pack, offset, &index_pos, &oi) < 0) { mark_bad_packed_object(pack, oid); return -1; diff --git a/pack-bitmap.c b/pack-bitmap.c index 83eb47a28ba9de..35774b6f0c0da7 100644 --- a/pack-bitmap.c +++ b/pack-bitmap.c @@ -1877,7 +1877,7 @@ static unsigned long get_size_by_pos(struct bitmap_index *bitmap_git, ofs = pack_pos_to_offset(pack, pos); } - if (packed_object_info(pack, ofs, &oi) < 0) { + if (packed_object_info(NULL, pack, ofs, &oi) < 0) { struct object_id oid; nth_bitmap_object_oid(bitmap_git, &oid, pack_pos_to_index(pack, pos)); diff --git a/packfile.c b/packfile.c index 60dd0f286abb07..0eee45055f833e 100644 --- a/packfile.c +++ b/packfile.c @@ -1324,7 +1324,8 @@ static void add_delta_base_cache(struct packed_git *p, off_t base_offset, hashmap_add(&delta_base_cache, &ent->ent); } -int packed_object_info_with_index_pos(struct packed_git *p, off_t obj_offset, +int packed_object_info_with_index_pos(struct odb_source_packed *source, + struct packed_git *p, off_t obj_offset, uint32_t *maybe_index_pos, struct object_info *oi) { struct pack_window *w_curs = NULL; @@ -1420,23 +1421,28 @@ int packed_object_info_with_index_pos(struct packed_git *p, off_t obj_offset, oidclr(oi->delta_base_oid, p->repo->hash_algo); } - oi->whence = OI_PACKED; - oi->u.packed.offset = obj_offset; - oi->u.packed.pack = p; + if (oi->source_infop) { + if (!source) + BUG("cannot request source without an owning source"); + oi->source_infop->source = &source->base; - switch (type) { - case OBJ_NONE: - oi->u.packed.type = PACKED_OBJECT_TYPE_UNKNOWN; - break; - case OBJ_REF_DELTA: - oi->u.packed.type = PACKED_OBJECT_TYPE_REF_DELTA; - break; - case OBJ_OFS_DELTA: - oi->u.packed.type = PACKED_OBJECT_TYPE_OFS_DELTA; - break; - default: - oi->u.packed.type = PACKED_OBJECT_TYPE_FULL; - break; + oi->source_infop->u.packed.offset = obj_offset; + oi->source_infop->u.packed.pack = p; + + switch (type) { + case OBJ_NONE: + oi->source_infop->u.packed.type = PACKED_OBJECT_TYPE_UNKNOWN; + break; + case OBJ_REF_DELTA: + oi->source_infop->u.packed.type = PACKED_OBJECT_TYPE_REF_DELTA; + break; + case OBJ_OFS_DELTA: + oi->source_infop->u.packed.type = PACKED_OBJECT_TYPE_OFS_DELTA; + break; + default: + oi->source_infop->u.packed.type = PACKED_OBJECT_TYPE_FULL; + break; + } } ret = 0; @@ -1446,10 +1452,11 @@ int packed_object_info_with_index_pos(struct packed_git *p, off_t obj_offset, return ret; } -int packed_object_info(struct packed_git *p, off_t obj_offset, +int packed_object_info(struct odb_source_packed *source, + struct packed_git *p, off_t obj_offset, struct object_info *oi) { - return packed_object_info_with_index_pos(p, obj_offset, NULL, oi); + return packed_object_info_with_index_pos(source, p, obj_offset, NULL, oi); } static void *unpack_compressed_entry(struct packed_git *p, diff --git a/packfile.h b/packfile.h index 2329a697014a66..e1f77152b5c4bf 100644 --- a/packfile.h +++ b/packfile.h @@ -320,9 +320,11 @@ extern int do_check_packed_object_crc; * Look up the object info for a specific offset in the packfile. * Returns zero on success, a negative error code otherwise. */ -int packed_object_info(struct packed_git *pack, +int packed_object_info(struct odb_source_packed *source, + struct packed_git *pack, off_t offset, struct object_info *); -int packed_object_info_with_index_pos(struct packed_git *p, off_t obj_offset, +int packed_object_info_with_index_pos(struct odb_source_packed *source, + struct packed_git *p, off_t obj_offset, uint32_t *maybe_index_pos, struct object_info *oi); void mark_bad_packed_object(struct packed_git *, const struct object_id *); diff --git a/reachable.c b/reachable.c index 101cfc272715fb..caadacc02ad2ad 100644 --- a/reachable.c +++ b/reachable.c @@ -234,8 +234,9 @@ static int add_recent_object(const struct object_id *oid, add_pending_object(data->revs, obj, ""); if (data->cb) { - if (oi->whence == OI_PACKED) - data->cb(obj, oi->u.packed.pack, oi->u.packed.offset, *oi->mtimep); + if (oi->source_infop->source->type == ODB_SOURCE_PACKED) + data->cb(obj, oi->source_infop->u.packed.pack, + oi->source_infop->u.packed.offset, *oi->mtimep); else data->cb(obj, NULL, 0, *oi->mtimep); } @@ -252,9 +253,11 @@ int add_unseen_recent_objects_to_traversal(struct rev_info *revs, unsigned flags; enum object_type type; time_t mtime; + struct odb_source_info source_info; struct object_info oi = { .mtimep = &mtime, .typep = &type, + .source_infop = &source_info, }; int r; diff --git a/read-cache-ll.h b/read-cache-ll.h index 2c8b4b21b1c7e9..71b87615ebc6d3 100644 --- a/read-cache-ll.h +++ b/read-cache-ll.h @@ -309,6 +309,7 @@ int write_locked_index(struct index_state *, struct lock_file *lock, unsigned fl void discard_index(struct index_state *); void move_index_extensions(struct index_state *dst, struct index_state *src); int unmerged_index(const struct index_state *); +int index_state_unmerged_to_stage0(struct index_state *istate); /** * Returns 1 if istate differs from tree, 0 otherwise. If tree is NULL, diff --git a/read-cache.c b/read-cache.c index 7c1cdcf696094f..651bb915efbfce 100644 --- a/read-cache.c +++ b/read-cache.c @@ -3404,13 +3404,15 @@ int write_locked_index(struct index_state *istate, struct lock_file *lock, */ int repo_read_index_unmerged(struct repository *repo) { - struct index_state *istate; - int i; + repo_read_index(repo); + return index_state_unmerged_to_stage0(repo->index); +} + +int index_state_unmerged_to_stage0(struct index_state *istate) +{ int unmerged = 0; - repo_read_index(repo); - istate = repo->index; - for (i = 0; i < istate->cache_nr; i++) { + for (unsigned int i = 0; i < istate->cache_nr; i++) { struct cache_entry *ce = istate->cache[i]; struct cache_entry *new_ce; int len; diff --git a/replay.c b/replay.c index da531d5bc68add..aac9178875e902 100644 --- a/replay.c +++ b/replay.c @@ -351,10 +351,10 @@ void replay_result_release(struct replay_result *result) free(result->updates); } -static void replay_result_queue_update(struct replay_result *result, - const char *refname, - const struct object_id *old_oid, - const struct object_id *new_oid) +void replay_result_queue_update(struct replay_result *result, + const char *refname, + const struct object_id *old_oid, + const struct object_id *new_oid) { ALLOC_GROW(result->updates, result->updates_nr + 1, result->updates_alloc); result->updates[result->updates_nr].refname = xstrdup(refname); diff --git a/replay.h b/replay.h index faf95c7459e594..491db145e30151 100644 --- a/replay.h +++ b/replay.h @@ -80,6 +80,11 @@ struct replay_result { void replay_result_release(struct replay_result *result); +void replay_result_queue_update(struct replay_result *result, + const char *refname, + const struct object_id *old_oid, + const struct object_id *new_oid); + /* * Replay a set of commits onto a new location. Leaves both the working tree, * index and references untouched. Reference updates caused by the replay will diff --git a/reset.c b/reset.c index 46e30e639458c5..71254bde93fc51 100644 --- a/reset.c +++ b/reset.c @@ -1,5 +1,3 @@ -#define USE_THE_REPOSITORY_VARIABLE - #include "git-compat-util.h" #include "cache-tree.h" #include "gettext.h" @@ -13,13 +11,14 @@ #include "unpack-trees.h" #include "hook.h" -static int update_refs(const struct reset_head_opts *opts, +static int update_refs(struct repository *repo, + const struct reset_working_tree_options *opts, const struct object_id *oid, const struct object_id *head) { - unsigned detach_head = opts->flags & RESET_HEAD_DETACH; - unsigned run_hook = opts->flags & RESET_HEAD_RUN_POST_CHECKOUT_HOOK; - unsigned update_orig_head = opts->flags & RESET_ORIG_HEAD; + unsigned detach_head = opts->flags & RESET_WORKING_TREE_DETACH; + unsigned run_hook = opts->flags & RESET_WORKING_TREE_RUN_POST_CHECKOUT_HOOK; + unsigned update_orig_head = opts->flags & RESET_WORKING_TREE_UPDATE_ORIG_HEAD; const struct object_id *orig_head = opts->orig_head; const char *switch_to_branch = opts->branch; const char *reflog_branch = opts->branch_msg; @@ -42,19 +41,19 @@ static int update_refs(const struct reset_head_opts *opts, prefix_len = msg.len; if (update_orig_head) { - if (!repo_get_oid(the_repository, "ORIG_HEAD", &oid_old_orig)) + if (!repo_get_oid(repo, "ORIG_HEAD", &oid_old_orig)) old_orig = &oid_old_orig; if (head) { if (!reflog_orig_head) { strbuf_addstr(&msg, "updating ORIG_HEAD"); reflog_orig_head = msg.buf; } - refs_update_ref(get_main_ref_store(the_repository), + refs_update_ref(get_main_ref_store(repo), reflog_orig_head, "ORIG_HEAD", orig_head ? orig_head : head, old_orig, 0, UPDATE_REFS_MSG_ON_ERR); } else if (old_orig) - refs_delete_ref(get_main_ref_store(the_repository), + refs_delete_ref(get_main_ref_store(repo), NULL, "ORIG_HEAD", old_orig, 0); } @@ -64,40 +63,45 @@ static int update_refs(const struct reset_head_opts *opts, reflog_head = msg.buf; } if (!switch_to_branch) - ret = refs_update_ref(get_main_ref_store(the_repository), + ret = refs_update_ref(get_main_ref_store(repo), reflog_head, "HEAD", oid, head, detach_head ? REF_NO_DEREF : 0, UPDATE_REFS_MSG_ON_ERR); else { - ret = refs_update_ref(get_main_ref_store(the_repository), + ret = refs_update_ref(get_main_ref_store(repo), reflog_branch ? reflog_branch : reflog_head, switch_to_branch, oid, NULL, 0, UPDATE_REFS_MSG_ON_ERR); if (!ret) - ret = refs_update_symref(get_main_ref_store(the_repository), + ret = refs_update_symref(get_main_ref_store(repo), "HEAD", switch_to_branch, reflog_head); } if (!ret && run_hook) - run_hooks_l(the_repository, "post-checkout", - oid_to_hex(head ? head : null_oid(the_hash_algo)), + run_hooks_l(repo, "post-checkout", + oid_to_hex(head ? head : null_oid(repo->hash_algo)), oid_to_hex(oid), "1", NULL); strbuf_release(&msg); return ret; } -int reset_head(struct repository *r, const struct reset_head_opts *opts) +int reset_working_tree(struct repository *r, + const struct reset_working_tree_options *opts) { const struct object_id *oid = opts->oid; const char *switch_to_branch = opts->branch; - unsigned reset_hard = opts->flags & RESET_HEAD_HARD; - unsigned refs_only = opts->flags & RESET_HEAD_REFS_ONLY; - unsigned update_orig_head = opts->flags & RESET_ORIG_HEAD; + unsigned reset_hard = opts->flags & RESET_WORKING_TREE_HARD; + unsigned refs_only = opts->flags & RESET_WORKING_TREE_REFS_ONLY; + unsigned update_head = opts->flags & RESET_WORKING_TREE_UPDATE_HEAD; + unsigned update_orig_head = opts->flags & RESET_WORKING_TREE_UPDATE_ORIG_HEAD; + unsigned dry_run = opts->flags & RESET_WORKING_TREE_DRY_RUN; struct object_id *head = NULL, head_oid; struct tree_desc desc[2] = { { NULL }, { NULL } }; struct lock_file lock = LOCK_INIT; struct unpack_trees_options unpack_tree_opts = { 0 }; struct tree *tree; + struct index_state scratch_index = INDEX_STATE_INIT(r); + struct index_state *istate; const char *action; int ret = 0, nr = 0; @@ -110,12 +114,18 @@ int reset_head(struct repository *r, const struct reset_head_opts *opts) if (opts->branch_msg && !opts->branch) BUG("branch reflog message given without a branch"); - if (!refs_only && repo_hold_locked_index(r, &lock, LOCK_REPORT_ON_ERROR) < 0) { + if (update_orig_head && !update_head) + BUG("cannot update ORIG_HEAD without updating HEAD"); + + if (!refs_only && !dry_run && repo_hold_locked_index(r, &lock, LOCK_REPORT_ON_ERROR) < 0) { ret = -1; goto leave_reset_head; } - if (!repo_get_oid(r, "HEAD", &head_oid)) { + if (opts->oid_from) { + oidcpy(&head_oid, opts->oid_from); + head = &head_oid; + } else if (!repo_get_oid(r, "HEAD", &head_oid)) { head = &head_oid; } else if (!oid || !reset_hard) { ret = error(_("could not determine HEAD revision")); @@ -125,26 +135,42 @@ int reset_head(struct repository *r, const struct reset_head_opts *opts) if (!oid) oid = &head_oid; - if (refs_only) - return update_refs(opts, oid, head); + if (refs_only) { + if (!dry_run && update_head) + return update_refs(r, opts, oid, head); + return 0; + } + + if (dry_run) { + if (read_index_from(&scratch_index, r->index_file, r->gitdir) < 0 || + index_state_unmerged_to_stage0(&scratch_index) < 0) { + ret = error(_("could not read index")); + goto leave_reset_head; + } + + istate = &scratch_index; + } else { + if (repo_read_index_unmerged(r) < 0) { + ret = error(_("could not read index")); + goto leave_reset_head; + } + istate = r->index; + } action = reset_hard ? "reset" : "checkout"; setup_unpack_trees_porcelain(&unpack_tree_opts, action); unpack_tree_opts.head_idx = 1; - unpack_tree_opts.src_index = r->index; - unpack_tree_opts.dst_index = r->index; + unpack_tree_opts.src_index = istate; + unpack_tree_opts.dst_index = istate; unpack_tree_opts.fn = reset_hard ? oneway_merge : twoway_merge; - unpack_tree_opts.update = 1; + unpack_tree_opts.update = !dry_run; + unpack_tree_opts.dry_run = dry_run; unpack_tree_opts.merge = 1; unpack_tree_opts.preserve_ignored = 0; /* FIXME: !overwrite_ignore */ - unpack_tree_opts.skip_cache_tree_update = 1; init_checkout_metadata(&unpack_tree_opts.meta, switch_to_branch, oid, NULL); - if (reset_hard) + if (reset_hard) { + unpack_tree_opts.skip_cache_tree_update = 1; unpack_tree_opts.reset = UNPACK_RESET_PROTECT_UNTRACKED; - - if (repo_read_index_unmerged(r) < 0) { - ret = error(_("could not read index")); - goto leave_reset_head; } if (!reset_hard && !fill_tree_descriptor(r, &desc[nr++], &head_oid)) { @@ -163,25 +189,31 @@ int reset_head(struct repository *r, const struct reset_head_opts *opts) goto leave_reset_head; } - tree = repo_parse_tree_indirect(the_repository, oid); + if (dry_run) + goto leave_reset_head; + + tree = repo_parse_tree_indirect(r, oid); if (!tree) { ret = error(_("unable to read tree (%s)"), oid_to_hex(oid)); goto leave_reset_head; } - prime_cache_tree(r, r->index, tree); + if (reset_hard) + prime_cache_tree(r, r->index, tree); if (write_locked_index(r->index, &lock, COMMIT_LOCK) < 0) { ret = error(_("could not write index")); goto leave_reset_head; } - if (oid != &head_oid || update_orig_head || switch_to_branch) - ret = update_refs(opts, oid, head); + if (update_head && + (oid != &head_oid || update_orig_head || switch_to_branch)) + ret = update_refs(r, opts, oid, head); leave_reset_head: rollback_lock_file(&lock); clear_unpack_trees_porcelain(&unpack_tree_opts); + release_index(&scratch_index); while (nr) free((void *)desc[--nr].buffer); return ret; diff --git a/reset.h b/reset.h index a28f81829d859d..4c992ba671c7f1 100644 --- a/reset.h +++ b/reset.h @@ -6,22 +6,42 @@ #define GIT_REFLOG_ACTION_ENVIRONMENT "GIT_REFLOG_ACTION" -/* Request a detached checkout */ -#define RESET_HEAD_DETACH (1<<0) -/* Request a reset rather than a checkout */ -#define RESET_HEAD_HARD (1<<1) -/* Run the post-checkout hook */ -#define RESET_HEAD_RUN_POST_CHECKOUT_HOOK (1<<2) -/* Only update refs, do not touch the worktree */ -#define RESET_HEAD_REFS_ONLY (1<<3) -/* Update ORIG_HEAD as well as HEAD */ -#define RESET_ORIG_HEAD (1<<4) - -struct reset_head_opts { +enum reset_working_tree_flags { + /* Request a detached checkout */ + RESET_WORKING_TREE_DETACH = (1 << 0), + + /* Request a reset rather than a checkout */ + RESET_WORKING_TREE_HARD = (1 << 1), + + /* Run the post-checkout hook */ + RESET_WORKING_TREE_RUN_POST_CHECKOUT_HOOK = (1 << 2), + + /* Only update refs, do not touch the worktree */ + RESET_WORKING_TREE_REFS_ONLY = (1 << 3), + + /* Update HEAD */ + RESET_WORKING_TREE_UPDATE_HEAD = (1 << 4), + + /* Update ORIG_HEAD */ + RESET_WORKING_TREE_UPDATE_ORIG_HEAD = (1 << 5), + + /* + * Perform a dry-run by performing the operation without updating + * any user-visible state. + */ + RESET_WORKING_TREE_DRY_RUN = (1 << 6), +}; + +struct reset_working_tree_options { /* * The commit to checkout/reset to. Defaults to HEAD. */ const struct object_id *oid; + /* + * The commit to checkout/reset from when doing a two-way merge. This + * is used as one of the sides to merge. + */ + const struct object_id *oid_from; /* * Optional value to set ORIG_HEAD. Defaults to HEAD. */ @@ -33,7 +53,7 @@ struct reset_head_opts { /* * Flags defined above. */ - unsigned flags; + enum reset_working_tree_flags flags; /* * Optional reflog message for branch, defaults to head_msg. */ @@ -45,7 +65,8 @@ struct reset_head_opts { const char *head_msg; /* * Optional reflog message for ORIG_HEAD, if this omitted and flags - * contains RESET_ORIG_HEAD then default_reflog_action must be given. + * contains RESET_WORKING_TREE_UPDATE_ORIG_HEAD then + * default_reflog_action must be given. */ const char *orig_head_msg; /* @@ -55,6 +76,6 @@ struct reset_head_opts { const char *default_reflog_action; }; -int reset_head(struct repository *r, const struct reset_head_opts *opts); +int reset_working_tree(struct repository *r, const struct reset_working_tree_options *opts); #endif diff --git a/revision.c b/revision.c index 137a86d33bbbe1..ccbe2e03d1406a 100644 --- a/revision.c +++ b/revision.c @@ -707,6 +707,8 @@ static int convert_pathspec_to_bloom_keyvec(struct bloom_keyvec **out, static void prepare_to_use_bloom_filter(struct rev_info *revs) { + release_revisions_bloom_keyvecs(revs); + if (!revs->commits) return; diff --git a/sequencer.c b/sequencer.c index 0fe8fed6c3e51d..1355a99a092268 100644 --- a/sequencer.c +++ b/sequencer.c @@ -4692,7 +4692,10 @@ static void create_autostash_internal(struct repository *r, if (has_unstaged_changes(r, 1) || has_uncommitted_changes(r, 1)) { struct child_process stash = CHILD_PROCESS_INIT; - struct reset_head_opts ropts = { .flags = RESET_HEAD_HARD }; + struct reset_working_tree_options ropts = { + .flags = RESET_WORKING_TREE_HARD | + RESET_WORKING_TREE_UPDATE_HEAD, + }; struct object_id oid; strvec_pushl(&stash.args, @@ -4722,7 +4725,7 @@ static void create_autostash_internal(struct repository *r, if (!silent) printf(_("Created autostash: %s\n"), buf.buf); - if (reset_head(r, &ropts) < 0) + if (reset_working_tree(r, &ropts) < 0) die(_("could not reset --hard")); discard_index(r->index); if (repo_read_index(r) < 0) @@ -4882,16 +4885,18 @@ static int checkout_onto(struct repository *r, struct replay_opts *opts, const char *onto_name, const struct object_id *onto, const struct object_id *orig_head) { - struct reset_head_opts ropts = { + struct reset_working_tree_options ropts = { .oid = onto, .orig_head = orig_head, - .flags = RESET_HEAD_DETACH | RESET_ORIG_HEAD | - RESET_HEAD_RUN_POST_CHECKOUT_HOOK, + .flags = RESET_WORKING_TREE_DETACH | + RESET_WORKING_TREE_UPDATE_HEAD | + RESET_WORKING_TREE_UPDATE_ORIG_HEAD | + RESET_WORKING_TREE_RUN_POST_CHECKOUT_HOOK, .head_msg = reflog_message(opts, "start", "checkout %s", onto_name), .default_reflog_action = sequencer_reflog_action(opts) }; - if (reset_head(r, &ropts)) { + if (reset_working_tree(r, &ropts)) { apply_autostash(rebase_path_autostash()); sequencer_remove_state(opts); return error(_("could not detach HEAD")); diff --git a/t/helper/test-bitmap.c b/t/helper/test-bitmap.c index b130832b81ecce..8547ef67e243eb 100644 --- a/t/helper/test-bitmap.c +++ b/t/helper/test-bitmap.c @@ -52,7 +52,7 @@ static int add_packed_object(const struct object_id *oid, entry = packlist_alloc(packed, oid); entry->idx.offset = nth_packed_object_offset(pack, pos); - if (packed_object_info(pack, entry->idx.offset, &oi) < 0) + if (packed_object_info(NULL, pack, entry->idx.offset, &oi) < 0) die("could not get type of object %s", oid_to_hex(oid)); oe_set_type(entry, type); diff --git a/t/meson.build b/t/meson.build index 7c3c070426dc9e..8ae6ab6c5fe1e2 100644 --- a/t/meson.build +++ b/t/meson.build @@ -224,6 +224,10 @@ integration_tests = [ 't1461-refs-list.sh', 't1462-refs-exists.sh', 't1463-refs-optimize.sh', + 't1464-refs-delete.sh', + 't1465-refs-update.sh', + 't1466-refs-create.sh', + 't1467-refs-rename.sh', 't1500-rev-parse.sh', 't1501-work-tree.sh', 't1502-rev-parse-parseopt.sh', @@ -400,6 +404,7 @@ integration_tests = [ 't3451-history-reword.sh', 't3452-history-split.sh', 't3453-history-fixup.sh', + 't3454-history-drop.sh', 't3500-cherry.sh', 't3501-revert-cherry-pick.sh', 't3502-cherry-pick-merge.sh', diff --git a/t/t1464-refs-delete.sh b/t/t1464-refs-delete.sh new file mode 100755 index 00000000000000..c88063e4942e89 --- /dev/null +++ b/t/t1464-refs-delete.sh @@ -0,0 +1,152 @@ +#!/bin/sh + +test_description='git refs delete' + +. ./test-lib.sh + +setup_repo () { + git init "$1" && + test_commit -C "$1" A && + test_commit -C "$1" B +} + +test_expect_success 'delete without oldvalue verification' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + git update-ref refs/heads/foo $A && + git refs delete refs/heads/foo && + test_must_fail git refs exists refs/heads/foo + ) +' + +test_expect_success 'delete with matching oldvalue' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + git update-ref refs/heads/foo $A && + git refs delete refs/heads/foo $A && + test_must_fail git refs exists refs/heads/foo + ) +' + +test_expect_success 'delete with stale oldvalue fails' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + B=$(git rev-parse B) && + git update-ref refs/heads/foo $A && + test_must_fail git refs delete refs/heads/foo $B 2>err && + test_grep " but expected " err && + git refs exists refs/heads/foo + ) +' + +test_expect_success 'delete with null oldvalue fails' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + git update-ref refs/heads/foo $A && + test_must_fail git refs delete refs/heads/foo $ZERO_OID 2>err && + test_grep "null old object ID" err && + git refs exists refs/heads/foo + ) +' + +test_expect_success 'delete with invalid oldvalue fails' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + git update-ref refs/heads/foo $A && + test_must_fail git refs delete refs/heads/foo invalid-oid 2>err && + test_grep "invalid old object ID" err && + git refs exists refs/heads/foo + ) +' + +test_expect_success 'delete symref with --no-deref leaves target intact' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + git update-ref refs/heads/foo $A && + git symbolic-ref refs/heads/symref refs/heads/foo && + git refs delete --no-deref refs/heads/symref && + test_must_fail git refs exists refs/heads/symref && + git refs exists refs/heads/foo + ) +' + +test_expect_success 'delete symref with --no-deref verifies target OID' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + B=$(git rev-parse B) && + git update-ref refs/heads/foo $A && + git symbolic-ref refs/heads/symref refs/heads/foo && + + test_must_fail git refs delete --no-deref refs/heads/symref $B && + git refs exists refs/heads/symref && + + git refs delete --no-deref refs/heads/symref $A && + test_must_fail git refs exists refs/heads/symref && + git refs exists refs/heads/foo + ) +' + +test_expect_success 'delete with message records reason in reflog' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + git update-ref refs/heads/foo $A && + git symbolic-ref HEAD refs/heads/foo && + git refs delete --message=delete-reason refs/heads/foo && + test_must_fail git refs exists refs/heads/foo && + test-tool ref-store main for-each-reflog-ent HEAD >actual && + test_grep "delete-reason$" actual + ) +' + +test_expect_success 'delete with empty message fails' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + git update-ref refs/heads/foo $A && + test_must_fail git refs delete --message= refs/heads/foo 2>err && + test_grep "empty message" err && + git refs exists refs/heads/foo + ) +' + +test_expect_success 'delete without arguments fails' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + test_must_fail git -C repo refs delete 2>err && + test_grep "requires reference name" err +' + +test_expect_success 'delete with too many arguments fails' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + test_must_fail git refs delete one two three 2>err && + test_grep "requires reference name" err +' + +test_done diff --git a/t/t1465-refs-update.sh b/t/t1465-refs-update.sh new file mode 100755 index 00000000000000..a9becdda99c09b --- /dev/null +++ b/t/t1465-refs-update.sh @@ -0,0 +1,268 @@ +#!/bin/sh + +test_description='git refs update' + +. ./test-lib.sh + +setup_repo () { + git init "$1" && + test_commit -C "$1" A && + test_commit -C "$1" B +} + +test_ref_matches () { + git rev-parse "$1" >expect && + echo "$2" >actual && + test_cmp expect actual +} + +test_expect_success 'update creates a new reference' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + git refs update refs/heads/foo $A && + test_ref_matches refs/heads/foo "$A" + ) +' + +test_expect_success 'update an existing reference without oldvalue' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + B=$(git rev-parse B) && + git refs update refs/heads/foo $A && + git refs update refs/heads/foo $B && + test_ref_matches refs/heads/foo $B + ) +' + +test_expect_success 'update with matching oldvalue' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + B=$(git rev-parse B) && + git refs update refs/heads/foo $A && + git refs update refs/heads/foo $B $A && + test_ref_matches refs/heads/foo $B + ) +' + +test_expect_success 'update with stale oldvalue fails' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + B=$(git rev-parse B) && + git refs update refs/heads/foo $A && + test_must_fail git refs update refs/heads/foo $B $B 2>err && + test_grep " but expected " err && + test_ref_matches refs/heads/foo $A + ) +' + +test_expect_success 'update can create a new branch with oldvalue' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + git refs update refs/heads/foo $A $ZERO_OID 2>err && + test_ref_matches refs/heads/foo $A + ) +' + +test_expect_success 'update can create a new branch without oldvalue' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + git refs update refs/heads/foo $A 2>err && + test_ref_matches refs/heads/foo $A + ) +' + +test_expect_success 'update refuses to create preexisting branch' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + B=$(git rev-parse B) && + git refs update refs/heads/foo $A && + test_must_fail git refs update refs/heads/foo $B $ZERO_OID 2>err && + test_grep "reference already exists" err && + test_ref_matches refs/heads/foo $A + ) +' + +test_expect_success 'update can delete a branch with oldvalue' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + git refs update refs/heads/foo $A 2>err && + git refs update refs/heads/foo $ZERO_OID $A 2>err && + test_must_fail git refs exists refs/heads/foo + ) +' + +test_expect_success 'update can delete a branch without oldvalue' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + git refs update refs/heads/foo $A 2>err && + git refs update refs/heads/foo $ZERO_OID 2>err && + test_must_fail git refs exists refs/heads/foo + ) +' + +test_expect_success 'update refuses to delete a branch with mismatching value' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + B=$(git rev-parse B) && + git refs update refs/heads/foo $A 2>err && + test_must_fail git refs update refs/heads/foo $ZERO_OID $B 2>err && + test_grep " but expected " err && + git refs exists refs/heads/foo + ) +' + +test_expect_success 'update refuses to create preexisting branch' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + B=$(git rev-parse B) && + git refs update refs/heads/foo $A && + test_must_fail git refs update refs/heads/foo $B $ZERO_OID 2>err && + test_grep "reference already exists" err && + test_ref_matches refs/heads/foo $A + ) +' + + +test_expect_success 'update with invalid new value fails' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + test_must_fail git refs update refs/heads/foo invalid-oid 2>err && + test_grep "invalid new object ID" err && + test_must_fail git refs exists refs/heads/foo + ) +' + +test_expect_success 'update with invalid old value fails' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + B=$(git rev-parse B) && + git refs update refs/heads/foo $A && + test_must_fail git refs update refs/heads/foo $B invalid-oid 2>err && + test_grep "invalid old object ID" err && + test_ref_matches refs/heads/foo $A + ) +' + +test_expect_success 'update --no-deref rewrites the symref itself' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + B=$(git rev-parse B) && + git refs update refs/heads/foo $A && + git symbolic-ref refs/heads/symref refs/heads/foo && + git refs update --no-deref refs/heads/symref $B && + test_must_fail git symbolic-ref refs/heads/symref && + test_ref_matches refs/heads/symref $B && + test_ref_matches refs/heads/foo $A + ) +' + +test_expect_success 'update does not create a reflog by default' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + git refs update refs/foo $A && + test_must_fail git reflog exists refs/foo + ) +' + +test_expect_success 'update creates a reflog with --create-reflog' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + git refs update --create-reflog refs/foo $A && + git reflog exists refs/foo + ) +' + +test_expect_success 'update with message records reason in reflog' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + B=$(git rev-parse B) && + git refs update refs/heads/foo $A && + git refs update --message=update-reason refs/heads/foo $B && + git reflog show refs/heads/foo >actual && + test_grep "update-reason$" actual + ) +' + +test_expect_success 'update with empty message fails' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + B=$(git rev-parse B) && + git refs update refs/heads/foo $A && + test_must_fail git refs update --message= refs/heads/foo $B 2>err && + test_grep "empty message" err + ) +' + +test_expect_success 'update with too few arguments fails' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + test_must_fail git -C repo refs update refs/heads/foo 2>err && + test_grep "requires reference name, new value" err +' + +test_expect_success 'update with too many arguments fails' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + B=$(git rev-parse B) && + test_must_fail git refs update refs/heads/foo $A $B extra 2>err && + test_grep "requires reference name, new value" err + ) +' + +test_done diff --git a/t/t1466-refs-create.sh b/t/t1466-refs-create.sh new file mode 100755 index 00000000000000..cfb21bf8632235 --- /dev/null +++ b/t/t1466-refs-create.sh @@ -0,0 +1,151 @@ +#!/bin/sh + +test_description='git refs create' + +. ./test-lib.sh + +setup_repo () { + git init "$1" && + test_commit -C "$1" A && + test_commit -C "$1" B +} + +test_ref_matches () { + git rev-parse "$1" >expect && + echo "$2" >actual && + test_cmp expect actual +} + +test_expect_success 'create a new reference' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + git refs create refs/heads/foo $A && + test_ref_matches refs/heads/foo "$A" + ) +' + +test_expect_success 'create fails when the reference already exists' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + B=$(git rev-parse B) && + git refs create refs/heads/foo $A && + test_must_fail git refs create refs/heads/foo $B 2>err && + test_grep "reference already exists" err && + test_ref_matches refs/heads/foo "$A" + ) +' + +test_expect_success 'create with null new value fails' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + test_must_fail git refs create refs/heads/foo $ZERO_OID 2>err && + test_grep "null new object ID" err && + test_must_fail git refs exists refs/heads/foo + ) +' + +test_expect_success 'create with invalid new value fails' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + test_must_fail git refs create refs/heads/foo invalid-oid 2>err && + test_grep "invalid object ID" err && + test_must_fail git refs exists refs/heads/foo + ) +' + +test_expect_success 'create does not create a reflog by default' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + git refs create refs/foo $A && + test_must_fail git reflog exists refs/foo + ) +' + +test_expect_success 'create creates a reflog with --create-reflog' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + git refs create --create-reflog refs/foo $A && + git reflog exists refs/foo + ) +' + +test_expect_success 'create with message records reason in reflog' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + git refs create --message="create reason" refs/heads/foo $A && + git reflog show refs/heads/foo >actual && + test_grep "create reason$" actual + ) +' + +test_expect_success 'create with symref target creates target reference' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + git symbolic-ref refs/heads/symref refs/heads/target && + git refs create refs/heads/symref $A && + git reflog exists refs/heads/target + ) +' + +test_expect_success 'create with symref target and --no-deref refuses to create reference' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + git symbolic-ref refs/heads/symref refs/heads/target && + test_must_fail git refs create --no-deref refs/heads/symref $A 2>err && + test_grep "dangling symref already exists" err && + test_must_fail git reflog exists refs/heads/target + ) +' + +test_expect_success 'create with empty message fails' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + test_must_fail git refs create --message= refs/heads/foo $A 2>err && + test_grep "empty message" err && + test_must_fail git refs exists refs/heads/foo + ) +' + +test_expect_success 'create without arguments fails' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + test_must_fail git -C repo refs create 2>err && + test_grep "requires reference name" err +' + +test_expect_success 'create with too many arguments fails' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + test_must_fail git -C repo refs create refs/heads/foo a b 2>err && + test_grep "requires reference name" err +' + +test_done diff --git a/t/t1467-refs-rename.sh b/t/t1467-refs-rename.sh new file mode 100755 index 00000000000000..2b28be75c82cb2 --- /dev/null +++ b/t/t1467-refs-rename.sh @@ -0,0 +1,144 @@ +#!/bin/sh + +test_description='git refs rename' + +. ./test-lib.sh + +setup_repo () { + git init "$1" && + test_commit -C "$1" A && + test_commit -C "$1" B +} + +test_ref_matches () { + git rev-parse "$1" >expect && + echo "$2" >actual && + test_cmp expect actual +} + +test_expect_success 'rename an existing reference' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + git refs update refs/heads/foo $A && + git refs rename refs/heads/foo refs/heads/bar && + test_must_fail git refs exists refs/heads/foo && + test_ref_matches refs/heads/bar $A + ) +' + +test_expect_success 'rename moves the reflog along with the reference' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + git refs update --message="rename me" refs/heads/foo $A && + git refs rename refs/heads/foo refs/heads/bar && + git reflog show refs/heads/bar >reflog && + test_grep "rename me" reflog && + test_must_fail git reflog exists refs/heads/foo + ) +' + +test_expect_success 'rename with message records reason in reflog' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + git refs update refs/heads/foo $A && + git refs rename --message="rename reason" refs/heads/foo refs/heads/bar && + git reflog show refs/heads/bar >actual && + test_grep "rename reason" actual + ) +' + +test_expect_success 'rename a nonexistent reference fails' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + test_must_fail git refs rename refs/heads/foo refs/heads/bar 2>err && + test_grep "reference does not exist" err + ) +' + +test_expect_success 'rename to an existing reference fails' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + B=$(git rev-parse B) && + git refs update refs/heads/foo $A && + git refs update refs/heads/bar $B && + test_must_fail git refs rename refs/heads/foo refs/heads/bar 2>err && + test_grep "reference already exists" err + ) +' + +test_expect_success 'rename with symbolic ref fails' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + git refs create refs/heads/target $A && + git symbolic-ref refs/heads/symref refs/heads/target && + ! git refs rename refs/heads/symref refs/heads/renamed 2>err && + test_grep "is a symbolic ref, .* not supported" err + ) +' + +test_expect_success 'rename with empty message fails' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + git refs update refs/heads/foo $A && + test_must_fail git refs rename --message= refs/heads/foo refs/heads/bar 2>err && + test_grep "empty message" err + ) +' + +test_expect_success 'rename with invalid old reference name fails' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + test_must_fail git refs rename "refs/heads/foo..bar" refs/heads/bar 2>err && + test_grep "invalid ref format" err + ) +' + +test_expect_success 'rename with invalid new reference name fails' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + ( + cd repo && + A=$(git rev-parse A) && + git refs update refs/heads/foo $A && + test_must_fail git refs rename refs/heads/foo "refs/heads/bar..baz" 2>err && + test_grep "invalid ref format" err + ) +' + +test_expect_success 'rename with too few arguments fails' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + test_must_fail git -C repo refs rename refs/heads/foo 2>err && + test_grep "requires old and new reference name" err +' + +test_expect_success 'rename with too many arguments fails' ' + test_when_finished "rm -rf repo" && + setup_repo repo && + test_must_fail git -C repo refs rename refs/heads/foo refs/heads/bar refs/heads/baz 2>err && + test_grep "requires old and new reference name" err +' + +test_done diff --git a/t/t3454-history-drop.sh b/t/t3454-history-drop.sh new file mode 100755 index 00000000000000..68a86d1e370a94 --- /dev/null +++ b/t/t3454-history-drop.sh @@ -0,0 +1,561 @@ +#!/bin/sh + +test_description='tests for git-history drop subcommand' + +. ./test-lib.sh +. "$TEST_DIRECTORY/lib-log-graph.sh" + +expect_graph () { + cat >expect && + lib_test_cmp_graph --format=%s "$@" +} + +expect_log () { + git log --format="%s" "$@" >actual && + cat >expect && + test_cmp expect actual +} + +test_expect_success 'errors on missing commit argument' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test_commit initial && + test_must_fail git history drop 2>err && + test_grep "command expects a single revision" err + ) +' + +test_expect_success 'errors on too many arguments' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test_commit initial && + test_must_fail git history drop HEAD HEAD 2>err && + test_grep "command expects a single revision" err + ) +' + +test_expect_success 'errors on unknown revision' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test_commit initial && + test_must_fail git history drop does-not-exist 2>err && + test_grep "commit cannot be found: does-not-exist" err + ) +' + +test_expect_success 'errors with invalid --empty= value' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test_commit initial && + test_commit second && + test_must_fail git history drop --empty=bogus HEAD 2>err && + test_grep "unrecognized.*--empty.*bogus" err + ) +' + +test_expect_success 'drops a commit in the middle and replays descendants' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test_commit first && + test_commit second && + test_commit third && + + git symbolic-ref HEAD >expect && + git history drop HEAD~ && + git symbolic-ref HEAD >actual && + test_cmp expect actual && + + expect_log <<-\EOF && + third + first + EOF + + test_must_fail git show HEAD:second.t && + test_path_is_missing second.t && + + git reflog >reflog && + test_grep "drop: dropping HEAD~" reflog + ) +' + +test_expect_success 'drops the HEAD commit' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test_commit first && + test_commit second && + + git history drop HEAD && + + expect_log <<-\EOF + first + EOF + ) +' + +test_expect_success 'drops a commit on detached HEAD' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test_commit first && + test_commit second && + test_commit third && + git checkout --detach HEAD && + + git history drop HEAD~ && + + expect_log <<-\EOF + third + first + EOF + ) +' + +# Note: in this case it would actually be fine to drop the root commit, as we +# do have a descendant commit, and no reference points to the root commit +# directly. So this is something that we may relax eventually. +test_expect_success 'refuses to drop the root commit' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test_commit first && + test_commit second && + + test_must_fail git history drop HEAD~ 2>err && + test_grep "cannot drop root commit" err + ) +' + +# In contrast to the above case, we actually don't want to drop the root commit +# here as that would cause us to end up with an empty commit graph. +test_expect_success 'refuses to drop the root commit when branch becomes empty' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test_commit first && + + test_must_fail git history drop HEAD 2>err && + test_grep "cannot drop root commit" err + ) +' + +test_expect_success 'refuses to drop a merge commit' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test_commit base && + git branch branch && + test_commit ours && + git switch branch && + test_commit theirs && + git switch - && + git merge theirs && + + test_must_fail git history drop HEAD 2>err && + test_grep "cannot drop merge commit" err + ) +' + +test_expect_success 'refuses when descendants contain a merge commit' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test_commit base && + test_commit middle && + git branch branch && + test_commit ours && + git switch branch && + test_commit theirs && + git switch - && + git merge theirs && + + test_must_fail git history drop middle 2>err && + test_grep "replaying merge commits is not supported yet" err + ) +' + +test_expect_success 'works in a bare repository' ' + test_when_finished "rm -rf repo repo.git" && + + git init repo && + test_commit -C repo first && + test_commit -C repo second && + test_commit -C repo third && + + git clone --bare repo repo.git && + ( + cd repo.git && + + git history drop HEAD~ && + expect_log <<-\EOF + third + first + EOF + ) +' + +test_expect_success 'updates branches on other lines of descent' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test_commit base && + test_commit target && + git branch theirs && + test_commit ours && + git switch theirs && + test_commit theirs && + + expect_graph --branches <<-\EOF && + * theirs + | * ours + |/ + * target + * base + EOF + + git history drop target && + + expect_graph --branches <<-\EOF + * ours + | * theirs + |/ + * base + EOF + ) +' + +test_expect_success 'moves branch pointing at dropped commit to its parent' ' + test_when_finished "rm -rf repo" && + git init repo --initial-branch=main && + ( + cd repo && + test_commit first && + test_commit second && + git branch points-at-second && + test_commit third && + + git rev-parse first >expect && + git history drop second && + git rev-parse points-at-second >actual && + test_cmp expect actual && + + expect_log --format="%s %D" --branches <<-\EOF + third HEAD -> main + first tag: first, points-at-second + EOF + ) +' + +test_expect_success '--dry-run prints ref updates without modifying repo' ' + test_when_finished "rm -rf repo" && + git init repo --initial-branch=main && + ( + cd repo && + test_commit base && + git branch branch && + test_commit middle && + test_commit ours && + git switch branch && + test_commit theirs && + + git refs list >refs-expect && + git history drop --dry-run main~ >updates && + git refs list >refs-actual && + test_cmp refs-expect refs-actual && + test_grep "update refs/heads/main" updates && + + git update-ref --stdin modify-me && + + git refs list >refs-expect && + git diff >diff-expect && + test_must_fail git history drop --dry-run HEAD 2>err && + test_grep "dropping this commit would overwrite local changes" err && + git diff >diff-actual && + git refs list >refs-actual && + + test_cmp diff-expect diff-actual && + test_cmp refs-expect refs-actual + ) +' + +test_expect_success '--update-refs=head updates only HEAD' ' + test_when_finished "rm -rf repo" && + git init repo --initial-branch=main && + ( + cd repo && + test_commit base && + test_commit target && + git branch theirs && + test_commit ours && + git switch theirs && + test_commit theirs && + + # When told to update HEAD only, the command refuses to + # rewrite commits that are not an ancestor of HEAD. + test_must_fail git history drop --update-refs=head main 2>err && + test_grep "rewritten commit must be an ancestor of HEAD" err && + + expect_graph --branches <<-\EOF && + * theirs + | * ours + |/ + * target + * base + EOF + + git switch main && + git history drop --update-refs=head target && + + expect_graph --branches <<-\EOF + * ours + | * theirs + | * target + |/ + * base + EOF + ) +' + +test_expect_success '--update-refs=head can rewrite detached HEAD' ' + test_when_finished "rm -rf repo" && + git init repo --initial-branch=main && + ( + cd repo && + test_commit first && + test_commit second && + test_commit third && + git switch --detach HEAD && + + git history drop --update-refs=head second && + + expect_log HEAD <<-\EOF && + third + first + EOF + expect_log main <<-\EOF + third + second + first + EOF + ) +' + +test_expect_success 'conflict with replayed commit aborts cleanly' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test_commit base && + test_commit conflict-a file && + test_commit conflict-b file && + + git refs list >refs-expect && + test_must_fail git history drop HEAD~ 2>err && + test_grep "failed replaying descendants" err && + git refs list >refs-actual && + test_cmp refs-expect refs-actual + ) +' + +# Build a history where a descendant of the drop target reverts the change +# introduced by the drop target. After dropping, the descendant's diff applies +# against a tree that already lacks the change, so it becomes empty. +setup_empty_descendant_repo () { + git init "$1" && + ( + cd "$1" && + echo C1 >file && + git add file && + git commit -m "base" && + git tag base && + echo C2 >file && + git add file && + git commit -m "drop-me" && + git tag drop-me && + test_commit middle && + echo C1 >file && + git add file && + git commit -m "revert-drop-me" && + git tag revert-drop-me + ) +} + +test_expect_success '--empty=drop drops descendants that become empty' ' + test_when_finished "rm -rf repo" && + setup_empty_descendant_repo repo && + ( + cd repo && + + git history drop --empty=drop drop-me && + + expect_log <<-\EOF + middle + base + EOF + ) +' + +test_expect_success '--empty=keep keeps descendants that become empty' ' + test_when_finished "rm -rf repo" && + setup_empty_descendant_repo repo && + ( + cd repo && + + git history drop --empty=keep drop-me && + + expect_log <<-\EOF && + revert-drop-me + middle + base + EOF + git diff HEAD~ HEAD >diff && + test_must_be_empty diff + ) +' + +test_expect_success '--empty=abort errors out when a descendant becomes empty' ' + test_when_finished "rm -rf repo" && + setup_empty_descendant_repo repo && + ( + cd repo && + + test_must_fail git history drop --empty=abort drop-me 2>err && + test_grep "became empty after replay" err + ) +' + +test_expect_success 'updates index and worktree when HEAD moves' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test_commit first && + test_commit second && + test_commit third && + + git history drop second && + + # Worktree should no longer contain second.t. + test_path_is_missing second.t && + test_path_is_file first.t && + test_path_is_file third.t && + + # Index and worktree should both match the new HEAD. + git status --porcelain --untracked-files=no >status && + test_must_be_empty status + ) +' + +test_expect_success 'updates worktree when dropping HEAD itself' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test_commit first && + test_commit second && + + git history drop HEAD && + + test_path_is_missing second.t && + test_path_is_file first.t && + + git status --porcelain --untracked-files=no >status && + test_must_be_empty status + ) +' + +test_expect_success 'preserves unrelated unstaged modifications' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test_commit first && + echo first-content >unrelated.txt && + git add unrelated.txt && + git commit -m "add unrelated" && + test_commit second && + test_commit third && + + echo locally-modified >unrelated.txt && + + git diff >diff-expect && + git history drop second && + git diff >diff-actual && + test_cmp diff-expect diff-actual && + test_path_is_missing second.t + ) +' + +test_expect_success 'preserves unrelated staged changes' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test_commit first && + echo first-content >unrelated.txt && + git add unrelated.txt && + git commit -m "add unrelated" && + test_commit second && + test_commit third && + + echo staged-change >unrelated.txt && + git add unrelated.txt && + + git diff --cached >diff-expect && + git history drop second && + git diff --cached >diff-actual && + test_cmp diff-expect diff-actual && + test_path_is_missing second.t + ) +' + +test_expect_success 'aborts when local modifications would be overwritten' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test_commit base && + test_commit conflict && + + echo local-edit >conflict.t && + git diff >diff-expect && + test_must_fail git history drop HEAD 2>err && + test_grep "would overwrite local changes" err && + git diff >diff-actual && + test_cmp diff-expect diff-actual + ) +' + +test_done diff --git a/t/t8002-blame.sh b/t/t8002-blame.sh index 7822947f028ee6..bf04b8273efd56 100755 --- a/t/t8002-blame.sh +++ b/t/t8002-blame.sh @@ -113,8 +113,7 @@ test_expect_success 'set up abbrev tests' ' ' test_expect_success 'blame --abbrev= works' ' - # non-boundary commits get +1 for alignment - check_abbrev 31 --abbrev=30 HEAD && + check_abbrev 30 --abbrev=30 HEAD && check_abbrev 30 --abbrev=30 ^HEAD ' @@ -141,10 +140,8 @@ test_expect_success 'blame --abbrev gets truncated with boundary commit' ' ' test_expect_success 'blame --abbrev -b truncates the blank boundary' ' - # Note that `--abbrev=` always gets incremented by 1, which is why we - # expect 11 leading spaces and not 10. cat >expect <<-EOF && - $(printf "%11s" "") ( 2005-04-07 15:45:13 -0700 1) abbrev + $(printf "%10s" "") ( 2005-04-07 15:45:13 -0700 1) abbrev EOF git blame -b --abbrev=10 ^HEAD -- abbrev.t >actual && test_cmp expect actual