From: "Adithya Chakilam via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: Martin Fick <mfick@nvidia.com>,
Christian Couder <christian.couder@gmail.com>,
Adithya Chakilam <achakilam@nvidia.com>,
Adithya Chakilam <achakilam@nvidia.com>
Subject: [PATCH 2/2] repack: also copy loose objects excluded by --filter-to-loose
Date: Fri, 13 Nov 2026 22:11:51 +0000 [thread overview]
Message-ID: <ba2534e3bc411352271f085725287c468860ae85.1794607911.git.gitgitgadget@gmail.com> (raw)
In-Reply-To: <pull.2314.git.1794607911.gitgitgadget@gmail.com>
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
prev parent reply other threads:[~2026-11-13 22:11 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
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 [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=ba2534e3bc411352271f085725287c468860ae85.1794607911.git.gitgitgadget@gmail.com \
--to=gitgitgadget@gmail.com \
--cc=achakilam@nvidia.com \
--cc=christian.couder@gmail.com \
--cc=git@vger.kernel.org \
--cc=mfick@nvidia.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox