diff --git a/Documentation/RelNotes/2.56.0.adoc b/Documentation/RelNotes/2.56.0.adoc index d33dd57996a486..65f0d965633959 100644 --- a/Documentation/RelNotes/2.56.0.adoc +++ b/Documentation/RelNotes/2.56.0.adoc @@ -121,6 +121,11 @@ UI, Workflows & Features code did not check the presence of a value and instead segfaulted without one, which has been corrected. + * 'git repack' has been taught '--drop-filtered' to delete local + promisor blobs exceeding a limit (currently 'blob:limit=') in partial + clones, reclaiming space. Guards prevent running during other + operations or if referenced by the index. + Performance, Internal Implementation, Development Support etc. -------------------------------------------------------------- @@ -419,6 +424,16 @@ Performance, Internal Implementation, Development Support etc. * The setting of a now-unused member '.pretty_given' in the sequencer machinery has been removed. + * The performance of adding numerous new packfiles has been improved + by introducing a fast path for known-new packfiles to skip an + unnecessary traversal in packfile_list_append(), avoiding a + quadratic complexity regression on load. + + * The unused name parameter in 'struct chdir_notify_entry' has been + removed from chdir_notify_register(), chdir_notify_unregister(), and + related callback signatures across several subsystems, simplifying the + API now that trace output no longer uses it. + Fixes since v2.55 ----------------- @@ -669,3 +684,14 @@ Fixes since v2.55 * The help text for the '-l' option of 'git diff' has been updated. (merge 764243bdf4 en/diff-l-opt-help later to maint). + + * 'git -C diff fi' did not complete 'file', which has + been corrected. + (merge 354d1bf3a0 jc/complete-diff-tracked-paths later to maint). + + * 'git -C checkout fi' did not complete 'file', which has + been corrected. + (merge 05e2ab1f31 jc/complete-checkout later to maint). + + * Other code cleanup, docfix, build fix, etc. + (merge 026636128f ss/submittingpatches-typofix later to maint). diff --git a/Documentation/SubmittingPatches b/Documentation/SubmittingPatches index c269e474e3f2bf..c60855f7069492 100644 --- a/Documentation/SubmittingPatches +++ b/Documentation/SubmittingPatches @@ -117,7 +117,7 @@ of review. can still continue to further improve them by adding more patches on top, but by the time a topic gets merged to 'next', it is expected that everybody agrees that the scope and the basic direction of the - topic are appropriate, so such an incremental updates are limited to + topic are appropriate, so such incremental updates are limited to small corrections and polishing. After a topic cooks for some time (like 7 calendar days) in 'next' without needing further tweaks on top, it gets merged to the 'master' branch and waits to become part diff --git a/Documentation/git-repack.adoc b/Documentation/git-repack.adoc index 72c42015e23f94..a1f9e64f668750 100644 --- a/Documentation/git-repack.adoc +++ b/Documentation/git-repack.adoc @@ -12,6 +12,7 @@ SYNOPSIS 'git repack' [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m] [--window=] [--depth=] [--threads=] [--keep-pack=] [--write-midx[=]] [--name-hash-version=] [--path-walk] + [--filter=] [--drop-filtered [--dry-run]] DESCRIPTION ----------- @@ -182,6 +183,42 @@ depth is 4095. `objects` and `objects/info/alternates` sections of linkgit:gitrepository-layout[5]. +--drop-filtered:: + Delete the local objects that match the `--filter` specification + instead of keeping them in a separate packfile, reclaiming the + disk space they occupy. This is intended for partial clones, + where the filtered objects are promisor objects that remain + recoverable from the promisor remote and are lazily re-fetched + on demand when they are next needed. ++ +Only large blobs are supported for now, so `--filter=blob:limit=` +is currently the only accepted filter. Because dropped objects must be +recoverable, this option requires a promisor remote to be configured +and refuses to run otherwise. ++ +This option requires `-a`, and implies `-d`: the objects are dropped by +rebuilding the promisor pack without them and then removing the now +redundant old packs, so the redundant packs must be deleted for the +space to actually be reclaimed. It is incompatible with `--filter-to` +and with bitmap writing (`-b`/`--write-bitmap-index`), since filtering +breaks the single-pack closure that bitmaps require. A bitmap setting +coming from configuration is silently disabled for the duration of the +command. ++ +As a convenience, since dropped objects remain recoverable by lazy fetch, +`--drop-filtered` refuses to run while another operation +(merge, rebase, am, cherry-pick, revert, or bisect) is in progress, to +avoid a surprising network fetch mid-operation, and refuses to drop any +blob that the current index references, since such a blob would only be +lazily re-fetched by the next command that inspects the working tree. +These checks are skipped in bare repositories, which have neither a +working tree nor an index. + +--dry-run:: + Only meaningful with `--drop-filtered`. List the objects that + would be dropped, one object ID per line, without rebuilding any + pack or deleting anything. + -b:: --write-bitmap-index:: Write a reachability bitmap index as part of the repack. This diff --git a/builtin/repack.c b/builtin/repack.c index db504d673fcf52..c4360382c1fce2 100644 --- a/builtin/repack.c +++ b/builtin/repack.c @@ -14,6 +14,11 @@ #include "promisor-remote.h" #include "repack.h" #include "shallow.h" +#include "list-objects-filter-options.h" +#include "oidset.h" +#include "hex.h" +#include "wt-status.h" +#include "read-cache-ll.h" #define ALL_INTO_ONE 1 #define LOOSEN_UNREACHABLE 2 @@ -28,11 +33,15 @@ static int use_delta_islands; static int run_update_server_info = 1; static char *packdir, *packtmp_name, *packtmp; static int midx_must_contain_cruft = 1; +static int drop_filtered; +static int dry_run; +static int write_bitmaps_given; static const char *const git_repack_usage[] = { N_("git repack [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\n" "[--window=] [--depth=] [--threads=] [--keep-pack=]\n" - "[--write-midx[=]] [--name-hash-version=] [--path-walk]"), + "[--write-midx[=]] [--name-hash-version=] [--path-walk]\n" + "[--filter=] [--drop-filtered [--dry-run]]"), NULL }; @@ -111,6 +120,21 @@ static int repack_config(const char *var, const char *value, return git_default_config(var, value, ctx, cb); } +static int option_parse_write_bitmaps(const struct option *opt, const char *arg, + int unset) +{ + int *value = opt->value; + + BUG_ON_OPT_ARG(arg); + if (unset) + *value = 0; + else + *value = 1; + + write_bitmaps_given = 1; + return 0; +} + static int option_parse_write_midx(const struct option *opt, const char *arg, int unset) { @@ -140,6 +164,7 @@ int cmd_repack(int argc, struct string_list_item *item; struct string_list names = STRING_LIST_INIT_DUP; struct existing_packs existing = EXISTING_PACKS_INIT; + struct oidset drop_oids = OIDSET_INIT; struct pack_geometry geometry = { 0 }; struct tempfile *refs_snapshot = NULL; int i, ret; @@ -194,8 +219,9 @@ int cmd_repack(int argc, OPT__QUIET(&po_args.quiet, N_("be quiet")), OPT_BOOL('l', "local", &po_args.local, N_("pass --local to git-pack-objects")), - OPT_BOOL('b', "write-bitmap-index", &write_bitmaps, - N_("write bitmap index")), + OPT_CALLBACK_F('b', "write-bitmap-index", &write_bitmaps, NULL, + N_("write bitmap index"), + PARSE_OPT_NOARG, option_parse_write_bitmaps), OPT_BOOL('i', "delta-islands", &use_delta_islands, N_("pass --delta-islands to git-pack-objects")), OPT_STRING(0, "unpack-unreachable", &unpack_unreachable, N_("approxidate"), @@ -231,6 +257,10 @@ int cmd_repack(int argc, N_("pack prefix to store a pack containing pruned objects")), OPT_STRING(0, "filter-to", &filter_to, N_("dir"), N_("pack prefix to store a pack containing filtered out objects")), + OPT_BOOL(0, "drop-filtered", &drop_filtered, + N_("delete filtered out objects (requires --filter)")), + OPT_BOOL(0, "dry-run", &dry_run, + N_("only show which objects would be dropped")), OPT_END() }; @@ -252,6 +282,118 @@ int cmd_repack(int argc, po_args.depth = xstrdup_or_null(opt_depth); po_args.threads = xstrdup_or_null(opt_threads); + die_for_incompatible_opt2(drop_filtered, "--drop-filtered", + !!filter_to, "--filter-to"); + + if (dry_run && !drop_filtered) + die(_("--dry-run only takes effect with --drop-filtered")); + + if (drop_filtered) { + if (!po_args.filter_options.choice) + die(_("--drop-filtered requires --filter")); + + if (!(pack_everything & ALL_INTO_ONE)) + die(_("--drop-filtered requires -a")); + + /* + * Only blob:limit= is supported for now. Reject other + * filter choices early, before walking the object database. + */ + if (po_args.filter_options.choice != LOFC_BLOB_LIMIT) + die(_("--drop-filtered only supports --filter=blob:limit= for now")); + + /* + * An explicit -b on the command line is a conflict we have to + * report; a bitmap setting from config is silently overridden + * for the duration of the command. + */ + if (write_bitmaps_given && write_bitmaps > 0) + die(_("options '%s' and '%s' cannot be used together"), + "--drop-filtered", "--write-bitmap-index"); + + /* + * Without a promisor remote there is nowhere to re-fetch the + * dropped objects from, so dropping them would be permanent + * data loss. + */ + if (!repo_has_promisor_remote(repo)) + die(_("--drop-filtered requires a promisor remote")); + + /* + * Refuse to run while another operation is in progress. A + * dropped object would just be lazily re-fetched when the + * operation resumes, but triggering a network fetch in the + * middle of a half-finished + * merge/rebase/cherry-pick/revert/bisect is a poor + * experience, so this is a UX convenience rather than a + * safety measure. Bare repositories have no such state, so + * the check is skipped there. + */ + if (!is_bare_repository(repo)) { + struct wt_status_state state = { 0 }; + + wt_status_get_state(repo, &state, 0); + if (state.merge_in_progress || state.revert_in_progress || + state.rebase_in_progress || state.bisect_in_progress || + state.cherry_pick_in_progress || state.am_in_progress || + state.rebase_interactive_in_progress) { + wt_status_state_free_buffers(&state); + die(_("--drop-filtered cannot be used while " + "another operation (merge, rebase, am, " + "cherry-pick, revert, or bisect) is in " + "progress")); + } + wt_status_state_free_buffers(&state); + } + + write_bitmaps = 0; + + /* + * Dropping objects means rebuilding the promisor packs + * without them and then removing the old packs, so the + * redundant packs must be deleted. Imply -d on a real run. + */ + if (!dry_run) + delete_redundant = 1; + + ret = enumerate_promisor_blobs(repo, &po_args.filter_options, &drop_oids); + + if (ret) + goto cleanup; + + /* + * Refuse to drop blobs that the current index references. + * Such a blob would only be lazily re-fetched by the next + * command that touches the worktree, so dropping it reclaims + * nothing. This guard just avoids that churn. Bare + * repositories have no index, so the check is skipped there. + */ + if (!is_bare_repository(repo) && oidset_size(&drop_oids)) { + struct index_state *istate = repo->index; + unsigned int i; + + if (repo_read_index(repo) < 0) + die(_("could not read the index")); + + for (i = 0; i < istate->cache_nr; i++) { + const struct cache_entry *ce = istate->cache[i]; + + if (oidset_contains(&drop_oids, &ce->oid)) + die(_("cannot drop '%s' (%s): it is referenced by the current index"), + ce->name, oid_to_hex(&ce->oid)); + } + } + + if (dry_run) { + struct oidset_iter iter; + const struct object_id *oid; + + oidset_iter_init(&drop_oids, &iter); + while ((oid = oidset_iter_next(&iter))) + printf("%s\n", oid_to_hex(oid)); + } + } + if (delete_redundant && repo->repository_format_precious_objects) die(_("cannot delete packs in a precious-objects repo")); @@ -362,7 +504,8 @@ int cmd_repack(int argc, strvec_push(&cmd.args, "--delta-islands"); if (pack_everything & ALL_INTO_ONE) { - repack_promisor_objects(repo, &po_args, &names, packtmp); + repack_promisor_objects(repo, &po_args, &names, packtmp, + (drop_filtered && !dry_run) ? &drop_oids : NULL); if (existing_packs_has_non_kept(&existing) && delete_redundant && @@ -545,7 +688,7 @@ int cmd_repack(int argc, } } - if (po_args.filter_options.choice) { + if (po_args.filter_options.choice && !drop_filtered) { struct write_pack_opts opts = { .po_args = &po_args, .destination = filter_to, @@ -638,6 +781,7 @@ int cmd_repack(int argc, cleanup: string_list_clear(&keep_pack_list, 0); string_list_clear(&names, 1); + oidset_clear(&drop_oids); existing_packs_release(&existing); pack_geometry_release(&geometry); pack_objects_args_release(&po_args); diff --git a/chdir-notify.c b/chdir-notify.c index 1237a45e2e6492..55773c24c96eda 100644 --- a/chdir-notify.c +++ b/chdir-notify.c @@ -7,25 +7,22 @@ #include "trace.h" struct chdir_notify_entry { - const char *name; chdir_notify_callback cb; void *data; struct list_head list; }; static LIST_HEAD(chdir_notify_entries); -void chdir_notify_register(const char *name, - chdir_notify_callback cb, +void chdir_notify_register(chdir_notify_callback cb, void *data) { struct chdir_notify_entry *e = xmalloc(sizeof(*e)); - e->name = name; e->cb = cb; e->data = data; list_add_tail(&e->list, &chdir_notify_entries); } -void chdir_notify_unregister(const char *name, chdir_notify_callback cb, +void chdir_notify_unregister(chdir_notify_callback cb, void *data) { struct list_head *pos, *p; @@ -34,8 +31,7 @@ void chdir_notify_unregister(const char *name, chdir_notify_callback cb, struct chdir_notify_entry *e = list_entry(pos, struct chdir_notify_entry, list); - if (e->cb != cb || e->data != data || !e->name != !name || - (e->name && strcmp(e->name, name))) + if (e->cb != cb || e->data != data) continue; list_del(pos); @@ -64,7 +60,7 @@ int chdir_notify(const char *new_cwd) list_for_each(pos, &chdir_notify_entries) { struct chdir_notify_entry *e = list_entry(pos, struct chdir_notify_entry, list); - e->cb(e->name, old_cwd.buf, new_cwd, e->data); + e->cb(old_cwd.buf, new_cwd, e->data); } strbuf_release(&old_cwd); diff --git a/chdir-notify.h b/chdir-notify.h index 36b4114472e31d..e4ae38e12dd8b9 100644 --- a/chdir-notify.h +++ b/chdir-notify.h @@ -33,13 +33,11 @@ * $GIT_TRACE_SETUP. It may be NULL, but if non-NULL should point to * storage which lasts as long as the registration is active. */ -typedef void (*chdir_notify_callback)(const char *name, - const char *old_cwd, +typedef void (*chdir_notify_callback)(const char *old_cwd, const char *new_cwd, void *data); -void chdir_notify_register(const char *name, chdir_notify_callback cb, void *data); -void chdir_notify_unregister(const char *name, chdir_notify_callback cb, - void *data); +void chdir_notify_register(chdir_notify_callback cb, void *data); +void chdir_notify_unregister(chdir_notify_callback cb, void *data); /* * diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index e8757877104eb9..e6dce62d3c3fb4 100644 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -1735,49 +1735,59 @@ __git_checkout_default_dwim_mode () _git_checkout () { - __git_has_doubledash && return + if ! __git_has_doubledash; then + local dwim_opt="$(__git_checkout_default_dwim_mode)" - local dwim_opt="$(__git_checkout_default_dwim_mode)" + case "$prev" in + -b|-B|--orphan) + # Complete local branches (and DWIM branch + # remote branch names) for an option argument + # specifying a new branch name. This is for + # convenience, assuming new branches are + # possibly based on pre-existing branch names. + __git_complete_refs $dwim_opt --mode="heads" + return + ;; + *) + ;; + esac - case "$prev" in - -b|-B|--orphan) - # Complete local branches (and DWIM branch - # remote branch names) for an option argument - # specifying a new branch name. This is for - # convenience, assuming new branches are - # possibly based on pre-existing branch names. - __git_complete_refs $dwim_opt --mode="heads" - return - ;; - *) - ;; - esac + case "$cur" in + --conflict=*) + __gitcomp "diff3 merge zdiff3" "" "${cur##--conflict=}" + return + ;; + --*) + __gitcomp_builtin checkout + return + ;; + *) + # At this point, we've already handled special completion for + # the arguments to -b/-B, and --orphan. There are 3 main + # things left we can possibly complete: + # 1) a start-point for -b/-B, -d/--detach, or --orphan + # 2) a remote head, for --track + # 3) an arbitrary reference, possibly including DWIM names + # + + if [ -n "$(__git_find_on_cmdline "-b -B -d --detach --orphan")" ]; then + __git_complete_refs --mode="refs" + elif [ -n "$(__git_find_on_cmdline "-t --track")" ]; then + __git_complete_refs --mode="remote-heads" + else + __git_complete_refs $dwim_opt --mode="refs" + fi + ;; + esac + fi - case "$cur" in - --conflict=*) - __gitcomp "diff3 merge zdiff3" "" "${cur##--conflict=}" - ;; - --*) - __gitcomp_builtin checkout - ;; - *) - # At this point, we've already handled special completion for - # the arguments to -b/-B, and --orphan. There are 3 main - # things left we can possibly complete: - # 1) a start-point for -b/-B, -d/--detach, or --orphan - # 2) a remote head, for --track - # 3) an arbitrary reference, possibly including DWIM names - # + if [ ${#COMPREPLY[@]} -eq 0 ]; then + __git_complete_index_file "" + fi - if [ -n "$(__git_find_on_cmdline "-b -B -d --detach --orphan")" ]; then - __git_complete_refs --mode="refs" - elif [ -n "$(__git_find_on_cmdline "-t --track")" ]; then - __git_complete_refs --mode="remote-heads" - else - __git_complete_refs $dwim_opt --mode="refs" - fi - ;; - esac + if [ ${#COMPREPLY[@]} -eq 0 ]; then + __git_complete_index_file "--others --directory" + fi } __git_sequencer_inprogress_options="--continue --quit --abort --skip" @@ -1947,35 +1957,48 @@ __git_diff_difftool_options="--cached --staged _git_diff () { - __git_has_doubledash && return + if ! __git_has_doubledash; then + case "$cur" in + --diff-algorithm=*) + __gitcomp "$__git_diff_algorithms" \ + "" "${cur##--diff-algorithm=}" + return + ;; + --submodule=*) + __gitcomp "$__git_diff_submodule_formats" \ + "" "${cur##--submodule=}" + return + ;; + --color-moved=*) + __gitcomp "$__git_color_moved_opts" \ + "" "${cur##--color-moved=}" + return + ;; + --color-moved-ws=*) + __gitcomp "$__git_color_moved_ws_opts" \ + "" "${cur##--color-moved-ws=}" + return + ;; + --ws-error-highlight=*) + __gitcomp "$__git_ws_error_highlight_opts" \ + "" "${cur##--ws-error-highlight=}" + return + ;; + --*) + __gitcomp "$__git_diff_difftool_options" + return + ;; + esac + __git_complete_revlist_file + fi - case "$cur" in - --diff-algorithm=*) - __gitcomp "$__git_diff_algorithms" "" "${cur##--diff-algorithm=}" - return - ;; - --submodule=*) - __gitcomp "$__git_diff_submodule_formats" "" "${cur##--submodule=}" - return - ;; - --color-moved=*) - __gitcomp "$__git_color_moved_opts" "" "${cur##--color-moved=}" - return - ;; - --color-moved-ws=*) - __gitcomp "$__git_color_moved_ws_opts" "" "${cur##--color-moved-ws=}" - return - ;; - --ws-error-highlight=*) - __gitcomp "$__git_ws_error_highlight_opts" "" "${cur##--ws-error-highlight=}" - return - ;; - --*) - __gitcomp "$__git_diff_difftool_options" - return - ;; - esac - __git_complete_revlist_file + if [ ${#COMPREPLY[@]} -eq 0 ]; then + __git_complete_index_file "" + fi + + if [ ${#COMPREPLY[@]} -eq 0 ]; then + __git_complete_index_file "--others --directory" + fi } __git_mergetools_common="diffuse diffmerge ecmerge emerge kdiff3 meld opendiff diff --git a/list-objects-filter.c b/list-objects-filter.c index c912ff3079a7d7..10e44f135780ba 100644 --- a/list-objects-filter.c +++ b/list-objects-filter.c @@ -828,3 +828,48 @@ void list_objects_filter__free(struct filter *filter) filter->free_fn(filter->filter_data); free(filter); } + +/* + * NEEDSWORK: this reimplements the blob:limit size check rather than + * reusing the existing filter machinery in + * list_objects_filter__filter_object(). That machinery is currently + * tied to the object-walk path and cannot easily be driven from a + * plain oidset. It would be nice to refactor the filter code so this + * helper can reuse it instead of duplicating the size check. + */ +int list_objects_filter__filter_oidset(struct repository *r, + const struct list_objects_filter_options *opts, + const struct oidset *in, + struct oidset *omitted) +{ + struct oidset_iter iter; + const struct object_id *oid; + + if (opts->choice != LOFC_BLOB_LIMIT) + return error(_("filter_oidset: only blob:limit filters are supported")); + + oidset_iter_init(in, &iter); + while ((oid = oidset_iter_next(&iter))) { + struct object_info info = OBJECT_INFO_INIT; + enum object_type type; + size_t size; + + info.typep = &type; + info.sizep = &size; + + /* + * Use OBJECT_INFO_SKIP_FETCH_OBJECT to avoid triggering + * a lazy fetch while inspecting candidates for removal. + */ + if (odb_read_object_info_extended(r->objects, oid, &info, + OBJECT_INFO_SKIP_FETCH_OBJECT) < 0) + continue; + + if (type != OBJ_BLOB) + continue; + + if (size >= opts->blob_limit_value) + oidset_insert(omitted, oid); + } + return 0; +} diff --git a/list-objects-filter.h b/list-objects-filter.h index 9e98814111cee4..5207ab70a9188e 100644 --- a/list-objects-filter.h +++ b/list-objects-filter.h @@ -94,4 +94,20 @@ enum list_objects_filter_result list_objects_filter__filter_object( */ void list_objects_filter__free(struct filter *filter); +/* + * Given a set of OIDs in 'in', populate 'omitted' with those that + * would be filtered by 'opts'. Currently only blob:limit=N is + * supported. Objects that cannot be read are silently skipped. + * + * NEEDSWORK: this reimplements the blob:limit size check rather than + * reusing the existing filter machinery. See the matching comment in + * list-objects-filter.c. + * + * Return 0 on success, -1 if the filter is not supported. + */ +int list_objects_filter__filter_oidset(struct repository *r, + const struct list_objects_filter_options *opts, + const struct oidset *in, + struct oidset *omitted); + #endif /* LIST_OBJECTS_FILTER_H */ diff --git a/odb/source-files.c b/odb/source-files.c index b7b3a297bb79d7..3945d651b92cd7 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -22,8 +22,7 @@ #include "tree.h" #include "write-or-die.h" -static void odb_source_files_reparent(const char *name UNUSED, - const char *old_cwd, +static void odb_source_files_reparent(const char *old_cwd, const char *new_cwd, void *cb_data) { @@ -37,7 +36,7 @@ static void odb_source_files_reparent(const char *name UNUSED, static void odb_source_files_free(struct odb_source *source) { struct odb_source_files *files = odb_source_files_downcast(source); - chdir_notify_unregister(NULL, odb_source_files_reparent, files); + chdir_notify_unregister(odb_source_files_reparent, files); odb_source_free(&files->loose->base); odb_source_free(&files->packed->base); odb_source_release(&files->base); @@ -780,7 +779,7 @@ struct odb_source_files *odb_source_files_new(struct object_database *odb, * paths in the primary ODB source in some user-facing functionality. */ if (!is_absolute_path(path)) - chdir_notify_register(NULL, odb_source_files_reparent, files); + chdir_notify_register(odb_source_files_reparent, files); return files; } diff --git a/odb/source-loose.c b/odb/source-loose.c index bb3455dfbd3334..0921a8748082ba 100644 --- a/odb/source-loose.c +++ b/odb/source-loose.c @@ -1009,8 +1009,7 @@ static void odb_source_loose_close(struct odb_source *source UNUSED) /* Nothing to do. */ } -static void odb_source_loose_reparent(const char *name UNUSED, - const char *old_cwd, +static void odb_source_loose_reparent(const char *old_cwd, const char *new_cwd, void *cb_data) { @@ -1026,7 +1025,7 @@ static void odb_source_loose_free(struct odb_source *source) struct odb_source_loose *loose = odb_source_loose_downcast(source); odb_source_loose_clear_cache(loose); loose_object_map_clear(&loose->map); - chdir_notify_unregister(NULL, odb_source_loose_reparent, loose); + chdir_notify_unregister(odb_source_loose_reparent, loose); odb_source_release(&loose->base); free(loose); } @@ -1056,7 +1055,7 @@ struct odb_source_loose *odb_source_loose_new(struct object_database *odb, loose->base.write_alternate = odb_source_loose_write_alternate; if (!is_absolute_path(loose->base.path)) - chdir_notify_register(NULL, odb_source_loose_reparent, loose); + chdir_notify_register(odb_source_loose_reparent, loose); loose_object_map_load(loose); diff --git a/odb/source-packed.c b/odb/source-packed.c index 630d9555856d7c..9b23ea15dc1900 100644 --- a/odb/source-packed.c +++ b/odb/source-packed.c @@ -785,8 +785,7 @@ static void odb_source_packed_prepare(struct odb_source *source, packed->initialized = true; } -static void odb_source_packed_reparent(const char *name UNUSED, - const char *old_cwd, +static void odb_source_packed_reparent(const char *old_cwd, const char *new_cwd, void *cb_data) { @@ -815,7 +814,7 @@ static void odb_source_packed_free(struct odb_source *source) { struct odb_source_packed *packed = odb_source_packed_downcast(source); - chdir_notify_unregister(NULL, odb_source_packed_reparent, packed); + chdir_notify_unregister(odb_source_packed_reparent, packed); for (struct packfile_list_entry *e = packed->packs.head; e; e = e->next) free(e->pack); @@ -852,7 +851,7 @@ struct odb_source_packed *odb_source_packed_new(struct object_database *odb, packed->base.write_alternate = odb_source_packed_write_alternate; if (!is_absolute_path(path)) - chdir_notify_register(NULL, odb_source_packed_reparent, packed); + chdir_notify_register(odb_source_packed_reparent, packed); return packed; } diff --git a/packfile-list.c b/packfile-list.c index 01fb913abf78fc..d6d411823c34a9 100644 --- a/packfile-list.c +++ b/packfile-list.c @@ -57,11 +57,12 @@ void packfile_list_prepend(struct packfile_list *list, struct packed_git *pack) list->tail = entry; } -void packfile_list_append(struct packfile_list *list, struct packed_git *pack) +void packfile_list_append(struct packfile_list *list, struct packed_git *pack, + int skip_dup_check) { struct packfile_list_entry *entry; - entry = packfile_list_remove_internal(list, pack); + entry = skip_dup_check ? NULL : packfile_list_remove_internal(list, pack); if (!entry) { entry = xmalloc(sizeof(*entry)); entry->pack = pack; diff --git a/packfile-list.h b/packfile-list.h index 1b05e2aa36de08..2b4b98b22673a4 100644 --- a/packfile-list.h +++ b/packfile-list.h @@ -15,7 +15,8 @@ struct packfile_list_entry { void packfile_list_clear(struct packfile_list *list); void packfile_list_remove(struct packfile_list *list, struct packed_git *pack); void packfile_list_prepend(struct packfile_list *list, struct packed_git *pack); -void packfile_list_append(struct packfile_list *list, struct packed_git *pack); +void packfile_list_append(struct packfile_list *list, struct packed_git *pack, + int skip_dup_check); /* * Find the pack within the "packs" list whose index contains the object diff --git a/packfile.c b/packfile.c index 7dc451bade983a..2ec5d221b4c088 100644 --- a/packfile.c +++ b/packfile.c @@ -781,7 +781,7 @@ void packfile_store_add_pack(struct odb_source_packed *store, if (pack->pack_fd != -1) pack_open_fds++; - packfile_list_append(&store->packs, pack); + packfile_list_append(&store->packs, pack, 1); strmap_put(&store->packs_by_path, pack->pack_name, pack); } diff --git a/refs/files-backend.c b/refs/files-backend.c index 1cc20aa486c263..71628550f20a57 100644 --- a/refs/files-backend.c +++ b/refs/files-backend.c @@ -111,8 +111,7 @@ static void clear_loose_ref_cache(struct files_ref_store *refs) } } -static void files_ref_store_reparent(const char *name UNUSED, - const char *old_cwd, +static void files_ref_store_reparent(const char *old_cwd, const char *new_cwd, void *payload) { @@ -182,7 +181,7 @@ static struct ref_store *files_ref_store_init(struct repository *repo, packed_ref_store_init(repo, NULL, refs->gitcommondir, opts); refs->store_flags = opts->access_flags; - chdir_notify_register(NULL, files_ref_store_reparent, refs); + chdir_notify_register(files_ref_store_reparent, refs); strbuf_release(&refdir); @@ -234,7 +233,7 @@ static void files_ref_store_release(struct ref_store *ref_store) free(refs->gitcommondir); ref_store_release(refs->packed_ref_store); free(refs->packed_ref_store); - chdir_notify_unregister(NULL, files_ref_store_reparent, refs); + chdir_notify_unregister(files_ref_store_reparent, refs); } static void files_reflog_path(struct files_ref_store *refs, diff --git a/refs/packed-backend.c b/refs/packed-backend.c index b9b04b70105f9a..a73fc6aca78023 100644 --- a/refs/packed-backend.c +++ b/refs/packed-backend.c @@ -217,8 +217,7 @@ static size_t snapshot_hexsz(const struct snapshot *snapshot) return snapshot->refs->base.repo->hash_algo->hexsz; } -static void packed_ref_store_reparent(const char *name UNUSED, - const char *old_cwd, +static void packed_ref_store_reparent(const char *old_cwd, const char *new_cwd, void *payload) { @@ -248,7 +247,7 @@ struct ref_store *packed_ref_store_init(struct repository *repo, strbuf_addf(&sb, "%s/packed-refs", gitdir); refs->path = strbuf_detach(&sb, NULL); - chdir_notify_register(NULL, packed_ref_store_reparent, refs); + chdir_notify_register(packed_ref_store_reparent, refs); return ref_store; } @@ -293,7 +292,7 @@ static void packed_ref_store_release(struct ref_store *ref_store) clear_snapshot(refs); rollback_lock_file(&refs->lock); delete_tempfile(&refs->tempfile); - chdir_notify_unregister(NULL, packed_ref_store_reparent, refs); + chdir_notify_unregister(packed_ref_store_reparent, refs); free(refs->path); } diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c index 028f0211af3d0f..08a75fb3287502 100644 --- a/refs/reftable-backend.c +++ b/refs/reftable-backend.c @@ -391,8 +391,7 @@ static const struct reftable_be_write_options *reftable_be_write_options(struct return opts; } -static void reftable_be_reparent(const char *name UNUSED, - const char *old_cwd, +static void reftable_be_reparent(const char *old_cwd, const char *new_cwd, void *payload) { @@ -465,7 +464,7 @@ static struct ref_store *reftable_be_init(struct repository *repo, goto done; } - chdir_notify_register(NULL, reftable_be_reparent, refs); + chdir_notify_register(reftable_be_reparent, refs); done: assert(refs->err != REFTABLE_API_ERROR); @@ -492,7 +491,7 @@ static void reftable_be_release(struct ref_store *ref_store) free(be); } strmap_clear(&refs->worktree_backends, 0); - chdir_notify_unregister(NULL, reftable_be_reparent, refs); + chdir_notify_unregister(reftable_be_reparent, refs); } static int reftable_be_create_on_disk(struct ref_store *ref_store, diff --git a/repack-filtered.c b/repack-filtered.c index edcf7667c5c378..869b9fc6e3b94c 100644 --- a/repack-filtered.c +++ b/repack-filtered.c @@ -3,6 +3,12 @@ #include "repository.h" #include "run-command.h" #include "string-list.h" +#include "hex.h" +#include "packfile.h" +#include "list-objects-filter-options.h" +#include "list-objects-filter.h" +#include "odb.h" +#include "promisor-remote.h" int write_filtered_pack(const struct write_pack_opts *opts, struct existing_packs *existing, @@ -49,3 +55,79 @@ int write_filtered_pack(const struct write_pack_opts *opts, return finish_pack_objects_cmd(existing->repo->hash_algo, opts, &cmd, names); } + +struct collect_cb_data { + struct repository *repo; + struct oidset *set; +}; + +static int collect_promisor_blob(const struct object_id *oid, + struct object_info *oi UNUSED, + void *cb_data) +{ + struct collect_cb_data *data = cb_data; + struct object_info info = OBJECT_INFO_INIT; + enum object_type type; + + info.typep = &type; + + /* + * Use OBJECT_INFO_SKIP_FETCH_OBJECT to avoid triggering a + * lazy fetch while collecting promisor blobs. + */ + if (odb_read_object_info_extended(data->repo->objects, oid, &info, + OBJECT_INFO_SKIP_FETCH_OBJECT) < 0) + return 0; + + if (type == OBJ_BLOB) + oidset_insert(data->set, oid); + + return 0; +} + +int enumerate_promisor_blobs(struct repository *repo, + const struct list_objects_filter_options *filter, + struct oidset *to_drop) +{ + struct oidset all_promisor_blobs = OIDSET_INIT; + struct collect_cb_data cb = { + .repo = repo, + .set = &all_promisor_blobs + }; + int ret = 0; + + /* + * The caller (cmd_repack) is responsible for validating that a + * blob:limit filter and a promisor remote are present before + * calling this function. + * + * Walk only promisor objects. Every object visited here is a + * promisor object, so it is recoverable from the promisor remote + * as long as the remote still has it, the same assumption the rest + * of partial clone relies on. + * + * We do not use write_filtered_pack() here because git repack + * routes promisor objects through repack_promisor_objects() + * before the filter machinery runs, so the filtered pack never + * contains promisor blobs. Direct enumeration via + * ODB_FOR_EACH_OBJECT_PROMISOR_ONLY is the correct approach. + */ + ret = odb_for_each_object(repo->objects, NULL, + collect_promisor_blob, &cb, + ODB_FOR_EACH_OBJECT_PROMISOR_ONLY); + if (ret) + goto cleanup; + + /* + * Apply the filter to find which blobs exceed the threshold. + * The caller has to_drop and is responsible for clearing it. + */ + ret = list_objects_filter__filter_oidset(repo, + filter, + &all_promisor_blobs, + to_drop); + +cleanup: + oidset_clear(&all_promisor_blobs); + return ret; +} diff --git a/repack-promisor.c b/repack-promisor.c index 90318ce15093f5..fabfdc168a9b86 100644 --- a/repack-promisor.c +++ b/repack-promisor.c @@ -6,10 +6,12 @@ #include "path.h" #include "repository.h" #include "run-command.h" +#include "oidset.h" struct write_oid_context { struct child_process *cmd; const struct git_hash_algo *algop; + const struct oidset *to_drop; }; /* @@ -23,6 +25,15 @@ static int write_oid(const struct object_id *oid, struct write_oid_context *ctx = data; struct child_process *cmd = ctx->cmd; + /* + * Objects in to_drop are being removed from the repository, so + * omit them from the rebuilt promisor pack. Each such object is a + * promisor object and therefore remains recoverable from the + * promisor remote. + */ + if (ctx->to_drop && oidset_contains(ctx->to_drop, oid)) + return 0; + if (cmd->in == -1) { if (start_command(cmd)) die(_("could not start pack-objects to repack promisor objects")); @@ -81,7 +92,8 @@ static void finish_repacking_promisor_objects(struct repository *repo, void repack_promisor_objects(struct repository *repo, const struct pack_objects_args *args, - struct string_list *names, const char *packtmp) + struct string_list *names, const char *packtmp, + const struct oidset *to_drop) { struct write_oid_context ctx; struct child_process cmd = CHILD_PROCESS_INIT; @@ -98,6 +110,7 @@ void repack_promisor_objects(struct repository *repo, */ ctx.cmd = &cmd; ctx.algop = repo->hash_algo; + ctx.to_drop = to_drop; odb_for_each_object(repo->objects, NULL, write_oid, &ctx, ODB_FOR_EACH_OBJECT_PROMISOR_ONLY); diff --git a/repack.h b/repack.h index f9fbc895f02940..61e554e4ed383d 100644 --- a/repack.h +++ b/repack.h @@ -3,6 +3,7 @@ #include "list-objects-filter-options.h" #include "string-list.h" +#include "oidset.h" struct pack_objects_args { char *window; @@ -100,7 +101,8 @@ void generated_pack_install(struct generated_pack *pack, const char *name, void repack_promisor_objects(struct repository *repo, const struct pack_objects_args *args, - struct string_list *names, const char *packtmp); + struct string_list *names, const char *packtmp, + const struct oidset *to_drop); struct pack_geometry { struct packed_git **pack; @@ -165,6 +167,10 @@ int write_filtered_pack(const struct write_pack_opts *opts, struct existing_packs *existing, struct string_list *names); +int enumerate_promisor_blobs(struct repository *repo, + const struct list_objects_filter_options *filter, + struct oidset *to_drop); + int write_cruft_pack(const struct write_pack_opts *opts, const char *cruft_expiration, unsigned long combine_cruft_below_size, diff --git a/setup.c b/setup.c index 20d29f31f428b1..22beb86f7acb1e 100644 --- a/setup.c +++ b/setup.c @@ -1057,8 +1057,7 @@ static void apply_gitdir_and_environment(struct repository *repo, const char *pa strvec_clear(&to_free); } -static void update_relative_gitdir(const char *name UNUSED, - const char *old_cwd, +static void update_relative_gitdir(const char *old_cwd, const char *new_cwd, void *data) { @@ -1086,7 +1085,7 @@ static void apply_and_export_relative_gitdir(struct repository *repo, const char xsetenv(GIT_DIR_ENVIRONMENT, path, 1); if (!is_absolute_path(path)) - chdir_notify_register(NULL, update_relative_gitdir, repo); + chdir_notify_register(update_relative_gitdir, repo); strbuf_release(&realpath); } diff --git a/t/meson.build b/t/meson.build index 181d61a8a0bd18..7f53cca7d1f891 100644 --- a/t/meson.build +++ b/t/meson.build @@ -966,6 +966,7 @@ integration_tests = [ 't7703-repack-geometric.sh', 't7704-repack-cruft.sh', 't7705-repack-incremental-midx.sh', + 't7706-repack-drop-filtered.sh', 't7800-difftool.sh', 't7810-grep.sh', 't7811-grep-open.sh', diff --git a/t/perf/p5303-many-packs.sh b/t/perf/p5303-many-packs.sh index af173a7b73e398..4221f9dd706243 100755 --- a/t/perf/p5303-many-packs.sh +++ b/t/perf/p5303-many-packs.sh @@ -141,4 +141,8 @@ test_perf "load 10,000 packs" ' git rev-parse --verify "HEAD^{commit}" ' +test_perf "abbreviate with 10,000 packs" ' + git rev-parse --short HEAD +' + test_done diff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh new file mode 100755 index 00000000000000..cb361158349c13 --- /dev/null +++ b/t/t7706-repack-drop-filtered.sh @@ -0,0 +1,185 @@ +#!/bin/sh + +test_description='git repack --drop-filtered enumerates filtered promisor blobs' + +. ./test-lib.sh + +# Delete a loose or packed object from "repo". +delete_object () { + local repo="$1" && + local obj="$2" && + local path="$repo/.git/objects/$(test_oid_to_path "$obj")" && + rm "$path" +} + +# Pack the objects into a promisor pack inside "repo". It is a pack +# accompanied by an empty ".promisor" marker file. Objects +# in such a pack are treated as recoverable from the promisor remote. +pack_as_from_promisor () { + HASH=$(git -C repo pack-objects .git/objects/pack/pack) && + >repo/.git/objects/pack/pack-$HASH.promisor && + echo $HASH +} + +# Write a blob of $1 bytes into "repo", record it as coming from the +# promisor remote, and remove the loose copy so the object is only +# present in the promisor pack. +promisor_blob () { + test-tool genrandom "$1" "$2" >blob_content && + OID=$(git -C repo hash-object -w --stdin /dev/null && + delete_object repo "$OID" && + echo "$OID" +} + +# Check option validation before any promisor walk +test_expect_success 'setup plain repo for validation' ' + git init plain && + test_commit -C plain initial && + git clone --bare plain plain.git && + git -C plain.git repack -a -d +' + +test_expect_success '--drop-filtered requires --filter' ' + test_must_fail git -C plain.git repack --drop-filtered --dry-run -a 2>err && + test_grep "drop-filtered requires --filter" err +' + +test_expect_success '--drop-filtered cannot be used with --filter-to' ' + test_must_fail git -C plain.git repack --drop-filtered \ + --filter=blob:limit=1k --filter-to=./filter-out 2>err && + test_grep "options .--drop-filtered. and .--filter-to. cannot be used together" err +' + +test_expect_success '--dry-run only takes effect with --drop-filtered' ' + test_must_fail git -C plain.git repack --dry-run 2>err && + test_grep "dry-run only takes effect with --drop-filtered" err +' + +test_expect_success '--drop-filtered requires -a' ' + test_must_fail git -C plain.git repack --drop-filtered \ + --filter=blob:limit=1k --dry-run 2>err && + test_grep "drop-filtered requires -a" err +' + +test_expect_success '--drop-filtered fails with --write-bitmap-index' ' + test_must_fail git -C plain.git repack --drop-filtered \ + --filter=blob:limit=1k --dry-run -a -b 2>err && + test_grep "options .--drop-filtered. and .--write-bitmap-index. cannot be used together" err +' + +test_expect_success '--drop-filtered rejects explicit -b even when repack.writeBitmaps=true' ' + test_must_fail git -C plain.git -c repack.writeBitmaps=true \ + repack --drop-filtered --filter=blob:limit=1k --dry-run -a -b 2>err && + test_grep "options .--drop-filtered. and .--write-bitmap-index. cannot be used together" err +' + +test_expect_success '--drop-filtered fails without a promisor remote' ' + test_must_fail git -C plain.git repack --drop-filtered \ + --filter=blob:limit=1k --dry-run -a 2>err && + test_grep "drop-filtered requires a promisor remote" err +' + +# Enumeration tests using promisor pack +test_expect_success 'setup repo with a promisor remote' ' + rm -rf repo && + test_create_repo repo && + test_commit -C repo base && + + # Mark the repo as a partial clone with a promisor remote so the + # promisor walk and the safety guard are satisfied. + git -C repo config core.repositoryformatversion 1 && + git -C repo config extensions.partialclone origin && + git -C repo config remote.origin.promisor true && + git -C repo config remote.origin.url "." && + + BIG=$(promisor_blob big 3072) && + SMALL=$(promisor_blob small 512) && + echo "$BIG" >big_oid && + echo "$SMALL" >small_oid +' + +test_expect_success 'promisor blob over the threshold is listed' ' + BIG=$(cat big_oid) && + SMALL=$(cat small_oid) && + + git -C repo -c repack.writeBitmaps=false \ + repack --drop-filtered --filter=blob:limit=1k --dry-run -a >out && + + test_grep "$BIG" out && + test_grep ! "$SMALL" out +' + +test_expect_success 'locally created blob is never listed' ' + BIG=$(cat big_oid) && + + # Large blob that exists only locally must never be a drop candidate. + # Dropping it would be unrecoverable. + test-tool genrandom local 4096 >local_content && + LOCAL=$(git -C repo hash-object -w --stdin out && + + test_grep "$BIG" out && + test_grep ! "$LOCAL" out +' + +test_expect_success '--dry-run does not remove the filtered objects' ' + BIG=$(cat big_oid) && + + git -C repo -c repack.writeBitmaps=false \ + repack --drop-filtered --filter=blob:limit=1k --dry-run -a >out && + + # Candidate blob must still be present after a dry run. + git -C repo cat-file -e "$BIG" +' + +test_expect_success '--drop-filtered removes the promisor blob locally' ' + BIG=$(cat big_oid) && + SMALL=$(cat small_oid) && + + git -C repo -c repack.writeBitmaps=false \ + repack --drop-filtered --filter=blob:limit=1k -a && + + git -C repo cat-file --batch-all-objects --batch-check="%(objectname)" >present && + test_grep ! "$BIG" present && + test_grep "$SMALL" present +' + +test_expect_success '--drop-filtered refuses when a merge is in progress' ' + test_when_finished "git -C repo merge --abort || :" && + + # Create a conflicting merge so wt_status reports it. + git -C repo checkout -B mergebase base && + echo one >repo/conflict.txt && + git -C repo add conflict.txt && + git -C repo commit -m one && + + git -C repo checkout -B mergeother base && + echo two >repo/conflict.txt && + git -C repo add conflict.txt && + git -C repo commit -m two && + + test_must_fail git -C repo merge mergebase && + + test_must_fail git -C repo -c repack.writeBitmaps=false \ + repack --drop-filtered --filter=blob:limit=1k --dry-run -a 2>err && + test_grep "in progress" err +' + +test_expect_success '--drop-filtered refuses to drop an index-referenced blob' ' + # Create a large blob, add it to the index and make it a promisor object + # so the index references it and enumeration picks it up. + test-tool genrandom idx 4096 >repo/tracked-big.bin && + git -C repo add tracked-big.bin && + OID=$(git -C repo rev-parse :tracked-big.bin) && + printf "%s\n" "$OID" | pack_as_from_promisor >/dev/null && + delete_object repo "$OID" && + + test_must_fail git -C repo -c repack.writeBitmaps=false \ + repack --drop-filtered --filter=blob:limit=1k --dry-run -a 2>err && + test_grep "referenced by the current index" err +' + +test_done diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh index 9ae3c48ebdd5e9..628f632ebf13e4 100755 --- a/t/t9902-completion.sh +++ b/t/t9902-completion.sh @@ -2663,6 +2663,8 @@ test_expect_success 'setup for integration tests' ' echo content >file1 && echo more >file2 && git add file1 file2 && + echo untracked >file3 && + echo untracked >ufile && git commit -m one && git branch mybranch && git tag mytag @@ -2712,6 +2714,118 @@ test_expect_success 'git -C checkout uses the right repo' ' EOF ' +test_expect_success 'git checkout completes tracked paths when no refs match' ' + # file1 and file2 are tracked but file3 is not + # there is no ref that begins with f + test_completion "git checkout f" <<-\EOF && + file1 + file2 + EOF + test_completion "git checkout -- f" <<-\EOF + file1 + file2 + EOF +' + +test_expect_success 'git checkout completes untracked paths, too' ' + # ufile is not tracked and there is no ref that begins with u + test_completion "git checkout u" <<-\EOF && + ufile + EOF + test_completion "git checkout -- u" <<-\EOF + ufile + EOF +' + +test_expect_success 'git -C checkout completes paths in specified repo' ' + # otherfile is tracked, oops is not + # lostfile is tracked but lost, ufile is untracked. + test_when_finished "rm -rf repo-for-checkout" && + git init repo-for-checkout && + echo content >repo-for-checkout/otherfile && + echo content >repo-for-checkout/lostfile && + git -C repo-for-checkout add otherfile && + git -C repo-for-checkout add lostfile && + git -C repo-for-checkout commit -m otherfile && + echo untracked >repo-for-checkout/oops && + echo untracked >repo-for-checkout/ufile && + rm -f repo-for-checkout/lostfile && + test_completion "git -C repo-for-checkout checkout o" <<-\EOF && + otherfile + EOF + test_completion "git -C repo-for-checkout checkout -- o" <<-\EOF && + otherfile + EOF + test_completion "git -C repo-for-checkout checkout l" <<-\EOF && + lostfile + EOF + test_completion "git -C repo-for-checkout checkout -- l" <<-\EOF && + lostfile + EOF + test_completion "git -C repo-for-checkout checkout u" <<-\EOF && + ufile + EOF + test_completion "git -C repo-for-checkout checkout -- u" <<-\EOF + ufile + EOF +' + +test_expect_success 'git diff completes tracked paths when no refs match' ' + # file1 and file2 are tracked but file3 is not + # there is no ref that begins with f + test_completion "git diff f" <<-\EOF && + file1 + file2 + EOF + test_completion "git diff -- f" <<-\EOF + file1 + file2 + EOF +' + +test_expect_success 'git diff [--] completes untracked paths, too' ' + # ufile is not tracked and there is no ref that begins with u + test_completion "git diff u" <<-\EOF && + ufile + EOF + test_completion "git diff -- u" <<-\EOF + ufile + EOF +' + +test_expect_success 'git -C diff completes paths in specified repo' ' + test_when_finished "rm -rf repo-for-diff" && + git init repo-for-diff && + echo content >repo-for-diff/otherfile && + echo content >repo-for-diff/lostfile && + git -C repo-for-diff add otherfile && + git -C repo-for-diff add lostfile && + git -C repo-for-diff commit -m otherfile && + echo untracked >repo-for-diff/oops && + echo untracked >repo-for-diff/ufile && + rm -f repo-for-diff/lostfile && + + test_completion "git -C repo-for-diff diff o" <<-\EOF && + otherfile + EOF + test_completion "git -C repo-for-diff diff l" <<-\EOF && + lostfile + EOF + test_completion "git -C repo-for-diff diff u" <<-\EOF && + ufile + EOF + + test_completion "git -C repo-for-diff diff -- o" <<-\EOF && + otherfile + EOF + test_completion "git -C repo-for-diff diff -- l" <<-\EOF && + lostfile + EOF + test_completion "git -C repo-for-diff diff -- u" <<-\EOF + ufile + EOF +' + test_expect_success 'show completes all refs' ' test_completion "git show m" <<-\EOF main Z diff --git a/tmp-objdir.c b/tmp-objdir.c index d199d39e7c9d51..520df2df8c5a6b 100644 --- a/tmp-objdir.c +++ b/tmp-objdir.c @@ -37,8 +37,7 @@ static void tmp_objdir_free(struct tmp_objdir *t) free(t); } -static void tmp_objdir_reparent(const char *name UNUSED, - const char *old_cwd, +static void tmp_objdir_reparent(const char *old_cwd, const char *new_cwd, void *cb_data) { @@ -67,7 +66,7 @@ int tmp_objdir_destroy(struct tmp_objdir *t) err = remove_dir_recursively(&t->path, 0); - chdir_notify_unregister(NULL, tmp_objdir_reparent, t); + chdir_notify_unregister(tmp_objdir_reparent, t); tmp_objdir_free(t); return err; @@ -155,7 +154,7 @@ struct tmp_objdir *tmp_objdir_create(struct repository *r, repo_get_object_directory(r), prefix); if (!is_absolute_path(t->path.buf)) - chdir_notify_register(NULL, tmp_objdir_reparent, t); + chdir_notify_register(tmp_objdir_reparent, t); if (!mkdtemp(t->path.buf)) { /* free, not destroy, as we never touched the filesystem */