Git development
 help / color / mirror / Atom feed
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

      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