* [PATCH 0/2] repack: write filtered-out objects as loose objects
@ 2026-11-13 22:11 Adithya Chakilam via GitGitGadget
2026-11-13 22:11 ` [PATCH 1/2] repack: add --filter-to-loose to " Adithya Chakilam via GitGitGadget
2026-11-13 22:11 ` [PATCH 2/2] repack: also copy loose objects excluded by --filter-to-loose Adithya Chakilam via GitGitGadget
0 siblings, 2 replies; 3+ messages in thread
From: Adithya Chakilam via GitGitGadget @ 2026-11-13 22:11 UTC (permalink / raw)
To: git; +Cc: Martin Fick, Christian Couder, Adithya Chakilam
Some key-value object stores manage and retrieve Git objects individually.
The existing --filter-to option writes filtered-out objects into packs, so
using it with such stores requires unpacking the resulting packs before
importing the objects.
This series adds --filter-to-loose=dir to git repack, allowing filtered-out
objects to be written directly as loose objects into another object
directory.
With --filter-to-loose, repack streams the filtered pack into
unpack-objects, avoiding that extra step and the intermediate pack file. For
example:
git repack -a --filter=blob=1m --filter-to-loose=/path/to/objects
The destination is treated as a Git object directory.
The first patch adds the option for objects already stored in packs. The
second extends it to include loose objects excluded by the filter, so the
destination also receives those objects. Objects already present in the
destination are not rewritten. With -d, the source copies of those loose
objects are removed after all objects have been written successfully.
Adithya Chakilam (2):
repack: add --filter-to-loose to write filtered-out objects as loose
objects
repack: also copy loose objects excluded by --filter-to-loose
Documentation/git-repack.adoc | 14 ++-
builtin/repack.c | 32 +++++-
repack-filtered.c | 152 ++++++++++++++++++++++++++-
repack.h | 14 +++
t/t7700-repack.sh | 189 ++++++++++++++++++++++++++++++++++
5 files changed, 394 insertions(+), 7 deletions(-)
base-commit: 1891de46f79dc444a47db1c87bd0c8c76b5463f2
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2314%2Fachakilam-nv%2Ffilter-to-loose-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2314/achakilam-nv/filter-to-loose-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/2314
--
gitgitgadget
^ permalink raw reply [flat|nested] 3+ messages in thread
* [PATCH 1/2] repack: add --filter-to-loose to write filtered-out objects as loose objects
2026-11-13 22:11 [PATCH 0/2] repack: write filtered-out objects as loose objects Adithya Chakilam via GitGitGadget
@ 2026-11-13 22:11 ` Adithya Chakilam via GitGitGadget
2026-11-13 22:11 ` [PATCH 2/2] repack: also copy loose objects excluded by --filter-to-loose Adithya Chakilam via GitGitGadget
1 sibling, 0 replies; 3+ messages in thread
From: Adithya Chakilam via GitGitGadget @ 2026-11-13 22:11 UTC (permalink / raw)
To: git; +Cc: Martin Fick, Christian Couder, Adithya Chakilam, Adithya Chakilam
From: Adithya Chakilam <achakilam@nvidia.com>
Some external object stores (like key-value) manage and retrieve git
objects individually. Although repack's --filter-to option can separate
objects from the main object store, it writes them into packs. Using
those objects individually then requires an additional unpacking step.
Add --filter-to-loose=<dir> to write filtered-out objects directly as
loose objects into <dir>, treating it as an object directory. Stream
the filtered pack into unpack-objects instead of writing an intermediate
pack file to disk.
The filtered pack is built with --stdin-packs, so this initially handles
only objects already stored in packs. Loose objects in the source
repository are left in place.
Signed-off-by: Adithya Chakilam <achakilam@nvidia.com>
---
Documentation/git-repack.adoc | 10 ++-
builtin/repack.c | 21 ++++--
repack-filtered.c | 44 +++++++++++-
repack.h | 1 +
t/t7700-repack.sh | 130 ++++++++++++++++++++++++++++++++++
5 files changed, 199 insertions(+), 7 deletions(-)
diff --git a/Documentation/git-repack.adoc b/Documentation/git-repack.adoc
index a1f9e64f66..df1390154f 100644
--- a/Documentation/git-repack.adoc
+++ b/Documentation/git-repack.adoc
@@ -12,7 +12,7 @@ SYNOPSIS
'git repack' [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]
[--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>]
[--write-midx[=<mode>]] [--name-hash-version=<n>] [--path-walk]
- [--filter=<filter-spec>] [--drop-filtered [--dry-run]]
+ [--filter=<filter-spec> [--filter-to-loose=<dir>]] [--drop-filtered [--dry-run]]
DESCRIPTION
-----------
@@ -183,6 +183,14 @@ depth is 4095.
`objects` and `objects/info/alternates` sections of
linkgit:gitrepository-layout[5].
+--filter-to-loose=<dir>::
+ Like `--filter-to`, but treat `<dir>` as an object directory
+ and write the filtered out objects there as loose objects
+ instead of as a pack. Cannot be combined with `--filter-to`
+ or `--max-pack-size`. Objects already present in `<dir>` are
+ not rewritten. All objects are written before any existing
+ pack is removed.
+
--drop-filtered::
Delete the local objects that match the `--filter` specification
instead of keeping them in a separate packfile, reclaiming the
diff --git a/builtin/repack.c b/builtin/repack.c
index 21835453c6..140a992f1b 100644
--- a/builtin/repack.c
+++ b/builtin/repack.c
@@ -41,7 +41,7 @@ static const char *const git_repack_usage[] = {
N_("git repack [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\n"
"[--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>]\n"
"[--write-midx[=<mode>]] [--name-hash-version=<n>] [--path-walk]\n"
- "[--filter=<filter-spec>] [--drop-filtered [--dry-run]]"),
+ "[--filter=<filter-spec> [--filter-to-loose=<dir>]] [--drop-filtered [--dry-run]]"),
NULL
};
@@ -182,6 +182,7 @@ int cmd_repack(int argc,
const char *cruft_expiration = NULL;
const char *expire_to = NULL;
const char *filter_to = NULL;
+ const char *filter_to_loose = NULL;
const char *opt_window = NULL;
const char *opt_window_memory = NULL;
const char *opt_depth = NULL;
@@ -257,6 +258,8 @@ 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_STRING(0, "filter-to-loose", &filter_to_loose, N_("dir"),
+ N_("object directory to write filtered out objects into as loose objects")),
OPT_BOOL(0, "drop-filtered", &drop_filtered,
N_("delete filtered out objects (requires --filter)")),
OPT_BOOL(0, "dry-run", &dry_run,
@@ -282,8 +285,16 @@ 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");
+ die_for_incompatible_opt3(drop_filtered, "--drop-filtered",
+ !!filter_to, "--filter-to",
+ !!filter_to_loose, "--filter-to-loose");
+ die_for_incompatible_opt2(!!filter_to, "--filter-to",
+ !!filter_to_loose, "--filter-to-loose");
+ die_for_incompatible_opt2(!!filter_to_loose, "--filter-to-loose",
+ !!po_args.max_pack_size, "--max-pack-size");
+
+ if (filter_to_loose)
+ filter_to = filter_to_loose;
if (dry_run && !drop_filtered)
die(_("--dry-run only takes effect with --drop-filtered"));
@@ -553,7 +564,8 @@ int cmd_repack(int argc,
strvec_pushf(&cmd.args, "--filter=%s",
expand_list_objects_filter_spec(&po_args.filter_options));
else if (filter_to)
- die(_("option '%s' can only be used along with '%s'"), "--filter-to", "--filter");
+ die(_("option '%s' can only be used along with '%s'"),
+ filter_to_loose ? "--filter-to-loose" : "--filter-to", "--filter");
if (geometry.split_factor)
cmd.in = -1;
@@ -698,6 +710,7 @@ int cmd_repack(int argc,
struct write_pack_opts opts = {
.po_args = &po_args,
.destination = filter_to,
+ .loose = !!filter_to_loose,
.packdir = packdir,
.packtmp = packtmp,
};
diff --git a/repack-filtered.c b/repack-filtered.c
index 869b9fc6e3..51c9dfeb13 100644
--- a/repack-filtered.c
+++ b/repack-filtered.c
@@ -9,19 +9,40 @@
#include "list-objects-filter.h"
#include "odb.h"
#include "promisor-remote.h"
+#include "sigchain.h"
+
+static void start_unpack_objects(struct child_process *unpack,
+ const char *objdir)
+{
+ strvec_push(&unpack->args, "unpack-objects");
+ strvec_push(&unpack->args, "-q");
+ strvec_pushf(&unpack->env, "GIT_OBJECT_DIRECTORY=%s", objdir);
+ strvec_push(&unpack->env, "GIT_ALTERNATE_OBJECT_DIRECTORIES");
+ unpack->git_cmd = 1;
+ unpack->in = -1;
+ if (start_command(unpack))
+ die(_("could not start unpack-objects for '%s'"), objdir);
+}
int write_filtered_pack(const struct write_pack_opts *opts,
struct existing_packs *existing,
struct string_list *names)
{
struct child_process cmd = CHILD_PROCESS_INIT;
+ struct child_process unpack = CHILD_PROCESS_INIT;
struct string_list_item *item;
FILE *in;
int ret;
const char *caret;
const char *pack_prefix = write_pack_opts_pack_prefix(opts);
- prepare_pack_objects(&cmd, opts->po_args, opts->destination);
+ if (opts->loose) {
+ sigchain_push(SIGPIPE, SIG_IGN);
+ start_unpack_objects(&unpack, opts->destination);
+ prepare_pack_objects(&cmd, opts->po_args, "--stdout");
+ } else {
+ prepare_pack_objects(&cmd, opts->po_args, opts->destination);
+ }
strvec_push(&cmd.args, "--stdin-packs");
@@ -29,10 +50,18 @@ int write_filtered_pack(const struct write_pack_opts *opts,
strvec_pushf(&cmd.args, "--keep-pack=%s", item->string);
cmd.in = -1;
+ if (opts->loose)
+ cmd.out = unpack.in;
ret = start_command(&cmd);
- if (ret)
+ if (ret) {
+ if (opts->loose) {
+ close(unpack.in);
+ finish_command(&unpack);
+ sigchain_pop(SIGPIPE);
+ }
return ret;
+ }
/*
* Here 'names' contains only the pack(s) that were just
@@ -52,6 +81,17 @@ int write_filtered_pack(const struct write_pack_opts *opts,
fprintf(in, "%s%s.pack\n", caret, item->string);
fclose(in);
+ if (opts->loose) {
+ ret = finish_command(&cmd);
+ if (finish_command(&unpack))
+ ret = -1;
+ sigchain_pop(SIGPIPE);
+ if (ret)
+ ret = error(_("failed to write filtered out objects to '%s'"),
+ opts->destination);
+ return ret;
+ }
+
return finish_pack_objects_cmd(existing->repo->hash_algo, opts, &cmd,
names);
}
diff --git a/repack.h b/repack.h
index 61e554e4ed..d5528745ec 100644
--- a/repack.h
+++ b/repack.h
@@ -41,6 +41,7 @@ void repack_remove_redundant_pack(struct repository *repo, const char *dir_name,
struct write_pack_opts {
struct pack_objects_args *po_args;
const char *destination;
+ bool loose;
const char *packdir;
const char *packtmp;
};
diff --git a/t/t7700-repack.sh b/t/t7700-repack.sh
index f0a390e3c6..5aa4b698f1 100755
--- a/t/t7700-repack.sh
+++ b/t/t7700-repack.sh
@@ -489,6 +489,136 @@ test_expect_success '--filter-to stores filtered out objects' '
test "$blob_content" = "content1"
'
+test_expect_success '--filter-to-loose writes filtered out objects loose' '
+ rm -rf loose-work loose-src.git loose-dst.git &&
+ git init --bare loose-dst.git &&
+ git init loose-work &&
+ test_commit -C loose-work base file1 content1 &&
+ git clone --bare loose-work loose-src.git &&
+ git -C loose-src.git repack -a -d &&
+
+ blob=$(git -C loose-src.git rev-parse HEAD:file1) &&
+ git -C loose-src.git -c repack.writebitmaps=false repack -a -d \
+ --filter=blob:none --filter-to-loose=../loose-dst.git/objects &&
+
+ test_path_is_file loose-dst.git/objects/$(test_oid_to_path $blob) &&
+ find loose-dst.git/objects/pack -name "*.pack" >actual-packs &&
+ test_must_be_empty actual-packs &&
+ test_must_fail git -C loose-src.git cat-file -e $blob &&
+
+ echo $(pwd)/loose-dst.git/objects >loose-src.git/objects/info/alternates &&
+ echo content1 >expect &&
+ git -C loose-src.git cat-file blob $blob >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success '--filter-to-loose writes objects that also exist in the source' '
+ rm -rf loose-work loose-src.git loose-dst.git &&
+ git init --bare loose-dst.git &&
+ git init loose-work &&
+ test_commit -C loose-work base file1 content1 &&
+ git clone --bare loose-work loose-src.git &&
+ git -C loose-src.git repack -a -d &&
+
+ blob=$(git -C loose-src.git rev-parse HEAD:file1) &&
+ git -C loose-src.git -c repack.writebitmaps=false repack -a -d \
+ --filter=blob:none --filter-to-loose=../loose-dst.git/objects &&
+
+ test_path_is_file loose-dst.git/objects/$(test_oid_to_path $blob)
+'
+
+# Create loose-work/loose-src.git/loose-dst.git; $big is a 2k blob on
+# refs/heads/big in loose-src.git, packed unless $1 is "loose".
+setup_loose_src () {
+ rm -rf loose-work loose-src.git loose-dst.git &&
+ git init --bare loose-dst.git &&
+ git init loose-work &&
+ test_commit -C loose-work base small small &&
+ git clone --bare loose-work loose-src.git &&
+
+ big=$(test-tool genrandom big 2k | git -C loose-src.git hash-object -w --stdin) &&
+ tree=$(printf "100644 blob %s\tbig\n" $big | git -C loose-src.git mktree) &&
+ commit=$(git -C loose-src.git commit-tree -m big $tree) &&
+ git -C loose-src.git update-ref refs/heads/big $commit &&
+
+ if test "$1" != loose
+ then
+ git -C loose-src.git repack -a -d
+ fi
+}
+
+test_expect_success '--filter-to-loose failure to write leaves the source intact' '
+ setup_loose_src &&
+ >not-a-dir &&
+
+ ls loose-src.git/objects/pack/*.pack >packs-before &&
+ test_must_fail git -C loose-src.git -c repack.writebitmaps=false \
+ repack -a -d --filter=blob:limit=1k \
+ --filter-to-loose=../not-a-dir &&
+
+ ls loose-src.git/objects/pack/*.pack >packs-after &&
+ test_cmp packs-before packs-after &&
+
+ find loose-src.git/objects/pack -name ".tmp-*" >tmp-leftovers &&
+ test_must_be_empty tmp-leftovers &&
+ git -C loose-src.git cat-file -e $big
+'
+
+test_expect_success '--filter-to-loose does not move objects from kept packs' '
+ setup_loose_src &&
+
+ for p in loose-src.git/objects/pack/pack-*.pack
+ do
+ >${p%.pack}.keep || return 1
+ done &&
+
+ git -C loose-src.git -c repack.writebitmaps=false repack -a -d \
+ --filter=blob:limit=1k --filter-to-loose=../loose-dst.git/objects &&
+
+ test_path_is_missing loose-dst.git/objects/$(test_oid_to_path $big) &&
+ git -C loose-src.git cat-file -e $big
+'
+
+test_expect_success '--filter-to-loose leaves unreachable objects in cruft packs alone' '
+ setup_loose_src loose &&
+ git -C loose-src.git update-ref -d refs/heads/big &&
+ git -C loose-src.git repack --cruft -d &&
+
+ git -C loose-src.git -c repack.writebitmaps=false repack -a -d --cruft \
+ --filter=blob:limit=1k --filter-to-loose=../loose-dst.git/objects &&
+
+ test_path_is_missing loose-dst.git/objects/$(test_oid_to_path $big) &&
+ git -C loose-src.git cat-file -e $big
+'
+
+test_expect_success '--filter-to-loose output survives gc and prune of the source' '
+ setup_loose_src &&
+
+ git -C loose-src.git -c repack.writebitmaps=false repack -a -d \
+ --filter=blob:limit=1k --filter-to-loose=../loose-dst.git/objects &&
+
+ echo $(pwd)/loose-dst.git/objects >loose-src.git/objects/info/alternates &&
+ git -C loose-src.git gc --prune=now &&
+ git -C loose-src.git prune --expire=now &&
+
+ git -C loose-src.git fsck &&
+ git -C loose-src.git cat-file -e $big &&
+ test_path_is_file loose-dst.git/objects/$(test_oid_to_path $big)
+'
+
+test_expect_success '--filter-to-loose requires --filter, rejects --filter-to and --max-pack-size' '
+ test_must_fail git -C bare.git repack -a --filter-to-loose=../x 2>err &&
+ test_grep "can only be used along with" err &&
+
+ test_must_fail git -C bare.git repack -a --filter=blob:none \
+ --filter-to=../y --filter-to-loose=../x 2>err &&
+ test_grep "cannot be used together" err &&
+
+ test_must_fail git -C bare.git repack -a --filter=blob:none \
+ --filter-to-loose=../x --max-pack-size=1M 2>err &&
+ test_grep "cannot be used together" err
+'
+
test_expect_success '--filter works with --max-pack-size' '
rm -rf filtered.git &&
git init --bare filtered.git &&
--
gitgitgadget
^ permalink raw reply related [flat|nested] 3+ messages in thread
* [PATCH 2/2] repack: also copy loose objects excluded by --filter-to-loose
2026-11-13 22:11 [PATCH 0/2] repack: write filtered-out objects as loose objects Adithya Chakilam via GitGitGadget
2026-11-13 22:11 ` [PATCH 1/2] repack: add --filter-to-loose to " Adithya Chakilam via GitGitGadget
@ 2026-11-13 22:11 ` Adithya Chakilam via GitGitGadget
1 sibling, 0 replies; 3+ messages in thread
From: Adithya Chakilam via GitGitGadget @ 2026-11-13 22:11 UTC (permalink / raw)
To: git; +Cc: Martin Fick, Christian Couder, Adithya Chakilam, Adithya Chakilam
From: Adithya Chakilam <achakilam@nvidia.com>
--filter-to-loose only writes objects that were in an existing pack,
because the filtered pack is built with --stdin-packs. Loose objects
that the filter excludes stay behind in the source repository, so the
destination does not get the complete set of filtered-out objects.
Copy such loose objects to the destination as well. An object already
present in the destination is not rewritten. With -d, the source copies
are removed once everything is written.
Signed-off-by: Adithya Chakilam <achakilam@nvidia.com>
---
Documentation/git-repack.adoc | 4 ++
builtin/repack.c | 11 ++++
repack-filtered.c | 108 ++++++++++++++++++++++++++++++++++
repack.h | 13 ++++
t/t7700-repack.sh | 59 +++++++++++++++++++
5 files changed, 195 insertions(+)
diff --git a/Documentation/git-repack.adoc b/Documentation/git-repack.adoc
index df1390154f..e2025c29ee 100644
--- a/Documentation/git-repack.adoc
+++ b/Documentation/git-repack.adoc
@@ -191,6 +191,10 @@ depth is 4095.
not rewritten. All objects are written before any existing
pack is removed.
+ Loose objects in this repository that the filter excludes are
+ copied to `<dir>` as well, and with `-d` the source copies are
+ removed once everything has been written.
+
--drop-filtered::
Delete the local objects that match the `--filter` specification
instead of keeping them in a separate packfile, reclaiming the
diff --git a/builtin/repack.c b/builtin/repack.c
index 140a992f1b..a946ad28a3 100644
--- a/builtin/repack.c
+++ b/builtin/repack.c
@@ -183,6 +183,7 @@ int cmd_repack(int argc,
const char *expire_to = NULL;
const char *filter_to = NULL;
const char *filter_to_loose = NULL;
+ struct oidset moved_loose = OIDSET_INIT;
const char *opt_window = NULL;
const char *opt_window_memory = NULL;
const char *opt_depth = NULL;
@@ -721,6 +722,14 @@ int cmd_repack(int argc,
ret = write_filtered_pack(&opts, &existing, &names);
if (ret)
goto cleanup;
+
+ if (filter_to_loose) {
+ ret = move_loose_filtered_objects(repo,
+ &po_args.filter_options, filter_to,
+ &moved_loose);
+ if (ret)
+ goto cleanup;
+ }
}
string_list_sort(&names);
@@ -777,6 +786,7 @@ int cmd_repack(int argc,
if (show_progress)
opts |= PRUNE_PACKED_VERBOSE;
prune_packed_objects(repo, opts);
+ remove_moved_loose_objects(repo, &moved_loose);
if (!keep_unreachable &&
(!(pack_everything & LOOSEN_UNREACHABLE) ||
@@ -801,6 +811,7 @@ cleanup:
string_list_clear(&keep_pack_list, 0);
string_list_clear(&names, 1);
oidset_clear(&drop_oids);
+ oidset_clear(&moved_loose);
existing_packs_release(&existing);
pack_geometry_release(&geometry);
pack_objects_args_release(&po_args);
diff --git a/repack-filtered.c b/repack-filtered.c
index 51c9dfeb13..cb1f034f09 100644
--- a/repack-filtered.c
+++ b/repack-filtered.c
@@ -9,6 +9,11 @@
#include "list-objects-filter.h"
#include "odb.h"
#include "promisor-remote.h"
+#include "copy.h"
+#include "object-file.h"
+#include "odb/source-files.h"
+#include "oidset.h"
+#include "path.h"
#include "sigchain.h"
static void start_unpack_objects(struct child_process *unpack,
@@ -171,3 +176,106 @@ cleanup:
oidset_clear(&all_promisor_blobs);
return ret;
}
+
+static int collect_loose_oid(const struct object_id *oid,
+ const char *path UNUSED, void *cb_data)
+{
+ oidset_insert(cb_data, oid);
+ return 0;
+}
+
+static void dest_loose_path(struct strbuf *buf, const char *objdir,
+ const struct object_id *oid)
+{
+ const char *hex = oid_to_hex(oid);
+
+ strbuf_reset(buf);
+ strbuf_addf(buf, "%s/%.2s/%s", objdir, hex, hex + 2);
+}
+
+int move_loose_filtered_objects(struct repository *repo,
+ const struct list_objects_filter_options *filter,
+ const char *dest_objdir,
+ struct oidset *moved)
+{
+ struct odb_source_files *files =
+ odb_source_files_downcast(repo->objects->source);
+ struct oidset loose = OIDSET_INIT;
+ struct oidset omitted = OIDSET_INIT;
+ struct oidset_iter iter;
+ const struct object_id *oid;
+ struct strbuf src = STRBUF_INIT;
+ struct strbuf dst = STRBUF_INIT;
+ struct strbuf tmp = STRBUF_INIT;
+ int ret = 0;
+
+ if (filter->choice != LOFC_BLOB_LIMIT)
+ return 0;
+
+ ret = for_each_loose_file_in_source(&files->dirs->loose->base,
+ collect_loose_oid, NULL, NULL,
+ &loose);
+ if (ret)
+ goto out;
+ ret = list_objects_filter__filter_oidset(repo, filter, &loose,
+ &omitted);
+ if (ret)
+ goto out;
+
+ oidset_iter_init(&omitted, &iter);
+ while ((oid = oidset_iter_next(&iter))) {
+ odb_loose_path(files->dirs->loose, &src, oid);
+ dest_loose_path(&dst, dest_objdir, oid);
+
+ /*
+ * Only the destination decides whether the copy is needed;
+ * the object being present in the source says nothing.
+ */
+ if (!access(dst.buf, F_OK)) {
+ oidset_insert(moved, oid);
+ continue;
+ }
+
+ if (safe_create_leading_directories(repo, dst.buf) < 0) {
+ ret = error_errno(_("unable to create leading directories of '%s'"),
+ dst.buf);
+ goto out;
+ }
+ strbuf_reset(&tmp);
+ strbuf_addf(&tmp, "%s.tmp-%"PRIuMAX, dst.buf,
+ (uintmax_t)getpid());
+ if (copy_file(repo, tmp.buf, src.buf, 0444) ||
+ finalize_object_file(repo, tmp.buf, dst.buf)) {
+ unlink(tmp.buf);
+ ret = error(_("unable to copy loose object '%s' to '%s'"),
+ src.buf, dst.buf);
+ goto out;
+ }
+ oidset_insert(moved, oid);
+ }
+
+out:
+ oidset_clear(&loose);
+ oidset_clear(&omitted);
+ strbuf_release(&src);
+ strbuf_release(&dst);
+ strbuf_release(&tmp);
+ return ret;
+}
+
+void remove_moved_loose_objects(struct repository *repo,
+ const struct oidset *moved)
+{
+ struct odb_source_files *files =
+ odb_source_files_downcast(repo->objects->source);
+ struct strbuf path = STRBUF_INIT;
+ struct oidset_iter iter;
+ const struct object_id *oid;
+
+ oidset_iter_init(moved, &iter);
+ while ((oid = oidset_iter_next(&iter))) {
+ odb_loose_path(files->dirs->loose, &path, oid);
+ unlink(path.buf);
+ }
+ strbuf_release(&path);
+}
diff --git a/repack.h b/repack.h
index d5528745ec..18a0dc6314 100644
--- a/repack.h
+++ b/repack.h
@@ -168,6 +168,19 @@ int write_filtered_pack(const struct write_pack_opts *opts,
struct existing_packs *existing,
struct string_list *names);
+/*
+ * Copy loose objects that the filter excludes into the loose-object store
+ * at `dest_objdir`, recording them in `moved`.
+ */
+int move_loose_filtered_objects(struct repository *repo,
+ const struct list_objects_filter_options *filter,
+ const char *dest_objdir,
+ struct oidset *moved);
+
+/* Delete the source copies of objects copied by move_loose_filtered_objects. */
+void remove_moved_loose_objects(struct repository *repo,
+ const struct oidset *moved);
+
int enumerate_promisor_blobs(struct repository *repo,
const struct list_objects_filter_options *filter,
struct oidset *to_drop);
diff --git a/t/t7700-repack.sh b/t/t7700-repack.sh
index 5aa4b698f1..37dc5fdb13 100755
--- a/t/t7700-repack.sh
+++ b/t/t7700-repack.sh
@@ -547,6 +547,44 @@ setup_loose_src () {
fi
}
+test_expect_success '--filter-to-loose copies loose filtered out blobs, and -d removes the source copy' '
+ setup_loose_src loose &&
+ test_path_is_file loose-src.git/objects/$(test_oid_to_path $big) &&
+
+ # without -d the source copy is left alone
+ git -C loose-src.git -c repack.writebitmaps=false repack -a \
+ --filter=blob:limit=1k --filter-to-loose=../loose-dst.git/objects &&
+ test_path_is_file loose-dst.git/objects/$(test_oid_to_path $big) &&
+ test_path_is_file loose-src.git/objects/$(test_oid_to_path $big) &&
+
+ # with -d it is removed from the source after being copied
+ rm -rf loose-dst.git/objects/$(dirname $(test_oid_to_path $big)) &&
+ git -C loose-src.git -c repack.writebitmaps=false repack -a -d \
+ --filter=blob:limit=1k --filter-to-loose=../loose-dst.git/objects &&
+ test_path_is_file loose-dst.git/objects/$(test_oid_to_path $big) &&
+ test_path_is_missing loose-src.git/objects/$(test_oid_to_path $big) &&
+ echo $(pwd)/loose-dst.git/objects >loose-src.git/objects/info/alternates &&
+ git -C loose-src.git cat-file -e $big
+'
+
+test_expect_success '--filter-to-loose leaves an identical existing destination object alone' '
+ setup_loose_src &&
+
+ test-tool genrandom big 2k |
+ GIT_OBJECT_DIRECTORY=loose-dst.git/objects git hash-object -w --stdin &&
+
+ path=loose-dst.git/objects/$(test_oid_to_path $big) &&
+ test_path_is_file $path &&
+
+ ls -i $path >before &&
+ git -C loose-src.git -c repack.writebitmaps=false repack -a -d \
+ --filter=blob:limit=1k --filter-to-loose=../loose-dst.git/objects &&
+
+ ls -i $path >after &&
+ test_cmp before after &&
+ git -C loose-dst.git fsck
+'
+
test_expect_success '--filter-to-loose failure to write leaves the source intact' '
setup_loose_src &&
>not-a-dir &&
@@ -564,6 +602,27 @@ test_expect_success '--filter-to-loose failure to write leaves the source intact
git -C loose-src.git cat-file -e $big
'
+test_expect_success '--filter-to-loose failure to copy a loose object leaves the source intact' '
+ setup_loose_src loose &&
+ path=$(test_oid_to_path $big) &&
+ >loose-dst.git/objects/$(dirname $path) &&
+
+ find loose-src.git/objects/pack -name "*.pack" | sort >packs-before &&
+ test_must_fail git -C loose-src.git -c repack.writebitmaps=false \
+ repack -a -d --filter=blob:limit=1k \
+ --filter-to-loose=../loose-dst.git/objects 2>err &&
+
+ test_grep "unable to create leading directories" err &&
+ find loose-src.git/objects/pack -name "*.pack" | sort >packs-after &&
+ test_cmp packs-before packs-after &&
+
+ find loose-src.git/objects/pack -name ".tmp-*" >tmp-leftovers &&
+ test_must_be_empty tmp-leftovers &&
+
+ test_path_is_file loose-src.git/objects/$path &&
+ git -C loose-src.git cat-file -e $big
+'
+
test_expect_success '--filter-to-loose does not move objects from kept packs' '
setup_loose_src &&
--
gitgitgadget
^ permalink raw reply related [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-11-13 22:11 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-13 22:11 [PATCH 0/2] repack: write filtered-out objects as loose objects Adithya Chakilam via GitGitGadget
2026-11-13 22:11 ` [PATCH 1/2] repack: add --filter-to-loose to " Adithya Chakilam via GitGitGadget
2026-11-13 22:11 ` [PATCH 2/2] repack: also copy loose objects excluded by --filter-to-loose Adithya Chakilam via GitGitGadget
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.