git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH v3 10/10] builtin/merge-tree.c: implement support for `--write-pack`

From
Taylor Blau <me@ttaylorr.com>
Date
Oct 18, 2023, 17:08 UTC
Message-ID
<ae70508037265ed220d1d33543d61c5d9f0721e0.1697648864.git.me@ttaylorr.com>
In-Reply-To
<cover.1697648864.git.me@ttaylorr.com>

When using merge-tree often within a repository[^1], it is possible to generate a relatively large number of loose objects, which can result in degraded performance, and inode exhaustion in extreme cases.

Building on the functionality introduced in previous commits, the bulk-checkin machinery now has support to write arbitrary blob and tree objects which are small enough to be held in-core. We can use this to write any blob/tree objects generated by ORT into a separate pack instead of writing them out individually as loose.

This functionality is gated behind a new `--write-pack` option to `merge-tree` that works with the (non-deprecated) `--write-tree` mode.

The implementation is relatively straightforward. There are two spots within the ORT mechanism where we call `write_object_file()`, one for content differences within blobs, and another to assemble any new trees necessary to construct the merge. In each of those locations, conditionally replace calls to `write_object_file()` with `index_blob_bulk_checkin_incore()` or `index_tree_bulk_checkin_incore()` depending on which kind of object we are writing.

The only remaining task is to begin and end the transaction necessary to initialize the bulk-checkin machinery, and move any new pack(s) it created into the main object store.

[^1]: Such is the case at GitHub, where we run presumptive "test merges"
  on open pull requests to see whether or not we can light up the merge
  button green depending on whether or not the presumptive merge was
  conflicted.
  This is done in response to a number of user-initiated events,
  including viewing an open pull request whose last test merge is stale
  with respect to the current base and tip of the pull request. As a
  result, merge-tree can be run very frequently on large, active
  repositories.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
 Documentation/git-merge-tree.txt |  4 ++
 builtin/merge-tree.c             |  5 ++
 merge-ort.c                      | 42 +++++++++++----
 merge-recursive.h                |  1 +
 t/t4301-merge-tree-write-tree.sh | 93 ++++++++++++++++++++++++++++++++
 5 files changed, 136 insertions(+), 9 deletions(-)
diff --git a/Documentation/git-merge-tree.txt b/Documentation/git-merge-tree.txt
index ffc4fbf7e8..9d37609ef1 100644
--- a/Documentation/git-merge-tree.txt
+++ b/Documentation/git-merge-tree.txt
@@ -69,6 +69,10 @@ OPTIONS
 	specify a merge-base for the merge, and specifying multiple bases is
 	currently not supported. This option is incompatible with `--stdin`.
 
+--write-pack::
+	Write any new objects into a separate packfile instead of as
+	individual loose objects.
+
 [[OUTPUT]]
 OUTPUT
 ------
diff --git a/builtin/merge-tree.c b/builtin/merge-tree.c
index 0de42aecf4..672ebd4c54 100644
--- a/builtin/merge-tree.c
+++ b/builtin/merge-tree.c
@@ -18,6 +18,7 @@
 #include "quote.h"
 #include "tree.h"
 #include "config.h"
+#include "bulk-checkin.h"
 
 static int line_termination = '\n';
 
@@ -414,6 +415,7 @@ struct merge_tree_options {
 	int show_messages;
 	int name_only;
 	int use_stdin;
+	int write_pack;
 };
 
 static int real_merge(struct merge_tree_options *o,
@@ -440,6 +442,7 @@ static int real_merge(struct merge_tree_options *o,
 	init_merge_options(&opt, the_repository);
 
 	opt.show_rename_progress = 0;
+	opt.write_pack = o->write_pack;
 
 	opt.branch1 = branch1;
 	opt.branch2 = branch2;
@@ -548,6 +551,8 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 			   &merge_base,
 			   N_("commit"),
 			   N_("specify a merge-base for the merge")),
+		OPT_BOOL(0, "write-pack", &o.write_pack,
+			 N_("write new objects to a pack instead of as loose")),
 		OPT_END()
 	};
 
diff --git a/merge-ort.c b/merge-ort.c
index 7857ce9fbd..e198d2bc2b 100644
--- a/merge-ort.c
+++ b/merge-ort.c
@@ -48,6 +48,7 @@
 #include "tree.h"
 #include "unpack-trees.h"
 #include "xdiff-interface.h"
+#include "bulk-checkin.h"
 
 /*
  * We have many arrays of size 3.  Whenever we have such an array, the
@@ -2107,10 +2108,19 @@ static int handle_content_merge(struct merge_options *opt,
 		if ((merge_status < 0) || !result_buf.ptr)
 			ret = error(_("failed to execute internal merge"));
 
-		if (!ret &&
-		    write_object_file(result_buf.ptr, result_buf.size,
-				      OBJ_BLOB, &result->oid))
-			ret = error(_("unable to add %s to database"), path);
+		if (!ret) {
+			ret = opt->write_pack
+				? index_blob_bulk_checkin_incore(&result->oid,
+								 result_buf.ptr,
+								 result_buf.size,
+								 path, 1)
+				: write_object_file(result_buf.ptr,
+						    result_buf.size,
+						    OBJ_BLOB, &result->oid);
+			if (ret)
+				ret = error(_("unable to add %s to database"),
+					    path);
+		}
 
 		free(result_buf.ptr);
 		if (ret)
@@ -3596,7 +3606,8 @@ static int tree_entry_order(const void *a_, const void *b_)
 				 b->string, strlen(b->string), bmi->result.mode);
 }
 
-static int write_tree(struct object_id *result_oid,
+static int write_tree(struct merge_options *opt,
+		      struct object_id *result_oid,
 		      struct string_list *versions,
 		      unsigned int offset,
 		      size_t hash_size)
@@ -3630,8 +3641,14 @@ static int write_tree(struct object_id *result_oid,
 	}
 
 	/* Write this object file out, and record in result_oid */
-	if (write_object_file(buf.buf, buf.len, OBJ_TREE, result_oid))
+	ret = opt->write_pack
+		? index_tree_bulk_checkin_incore(result_oid,
+						 buf.buf, buf.len, "", 1)
+		: write_object_file(buf.buf, buf.len, OBJ_TREE, result_oid);
+
+	if (ret)
 		ret = -1;
+
 	strbuf_release(&buf);
 	return ret;
 }
@@ -3796,8 +3813,8 @@ static int write_completed_directory(struct merge_options *opt,
 		 */
 		dir_info->is_null = 0;
 		dir_info->result.mode = S_IFDIR;
-		if (write_tree(&dir_info->result.oid, &info->versions, offset,
-			       opt->repo->hash_algo->rawsz) < 0)
+		if (write_tree(opt, &dir_info->result.oid, &info->versions,
+			       offset, opt->repo->hash_algo->rawsz) < 0)
 			ret = -1;
 	}
 
@@ -4331,9 +4348,13 @@ static int process_entries(struct merge_options *opt,
 		fflush(stdout);
 		BUG("dir_metadata accounting completely off; shouldn't happen");
 	}
-	if (write_tree(result_oid, &dir_metadata.versions, 0,
+	if (write_tree(opt, result_oid, &dir_metadata.versions, 0,
 		       opt->repo->hash_algo->rawsz) < 0)
 		ret = -1;
+
+	if (opt->write_pack)
+		end_odb_transaction();
+
 cleanup:
 	string_list_clear(&plist, 0);
 	string_list_clear(&dir_metadata.versions, 0);
@@ -4877,6 +4898,9 @@ static void merge_start(struct merge_options *opt, struct merge_result *result)
 	 */
 	strmap_init(&opt->priv->conflicts);
 
+	if (opt->write_pack)
+		begin_odb_transaction();
+
 	trace2_region_leave("merge", "allocate/init", opt->repo);
 }
 
diff --git a/merge-recursive.h b/merge-recursive.h
index b88000e3c2..156e160876 100644
--- a/merge-recursive.h
+++ b/merge-recursive.h
@@ -48,6 +48,7 @@ struct merge_options {
 	unsigned renormalize : 1;
 	unsigned record_conflict_msgs_as_headers : 1;
 	const char *msg_header_prefix;
+	unsigned write_pack : 1;
 
 	/* internal fields used by the implementation */
 	struct merge_options_internal *priv;
diff --git a/t/t4301-merge-tree-write-tree.sh b/t/t4301-merge-tree-write-tree.sh
index 250f721795..2d81ff4de5 100755
--- a/t/t4301-merge-tree-write-tree.sh
+++ b/t/t4301-merge-tree-write-tree.sh
@@ -922,4 +922,97 @@ test_expect_success 'check the input format when --stdin is passed' '
 	test_cmp expect actual
 '
 
+packdir=".git/objects/pack"
+
+test_expect_success 'merge-tree can pack its result with --write-pack' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+
+	# base has lines [3, 4, 5]
+	#   - side adds to the beginning, resulting in [1, 2, 3, 4, 5]
+	#   - other adds to the end, resulting in [3, 4, 5, 6, 7]
+	#
+	# merging the two should result in a new blob object containing
+	# [1, 2, 3, 4, 5, 6, 7], along with a new tree.
+	test_commit -C repo base file "$(test_seq 3 5)" &&
+	git -C repo branch -M main &&
+	git -C repo checkout -b side main &&
+	test_commit -C repo side file "$(test_seq 1 5)" &&
+	git -C repo checkout -b other main &&
+	test_commit -C repo other file "$(test_seq 3 7)" &&
+
+	find repo/$packdir -type f -name "pack-*.idx" >packs.before &&
+	tree="$(git -C repo merge-tree --write-pack \
+		refs/tags/side refs/tags/other)" &&
+	blob="$(git -C repo rev-parse $tree:file)" &&
+	find repo/$packdir -type f -name "pack-*.idx" >packs.after &&
+
+	test_must_be_empty packs.before &&
+	test_line_count = 1 packs.after &&
+
+	git show-index <$(cat packs.after) >objects &&
+	test_line_count = 2 objects &&
+	grep "^[1-9][0-9]* $tree" objects &&
+	grep "^[1-9][0-9]* $blob" objects
+'
+
+test_expect_success 'merge-tree can write multiple packs with --write-pack' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+
+		git config pack.packSizeLimit 512 &&
+
+		test_seq 512 >f &&
+
+		# "f" contains roughly ~2,000 bytes.
+		#
+		# Each side ("foo" and "bar") adds a small amount of data at the
+		# beginning and end of "base", respectively.
+		git add f &&
+		test_tick &&
+		git commit -m base &&
+		git branch -M main &&
+
+		git checkout -b foo main &&
+		{
+			echo foo && cat f
+		} >f.tmp &&
+		mv f.tmp f &&
+		git add f &&
+		test_tick &&
+		git commit -m foo &&
+
+		git checkout -b bar main &&
+		echo bar >>f &&
+		git add f &&
+		test_tick &&
+		git commit -m bar &&
+
+		find $packdir -type f -name "pack-*.idx" >packs.before &&
+		# Merging either side should result in a new object which is
+		# larger than 1M, thus the result should be split into two
+		# separate packs.
+		tree="$(git merge-tree --write-pack \
+			refs/heads/foo refs/heads/bar)" &&
+		blob="$(git rev-parse $tree:f)" &&
+		find $packdir -type f -name "pack-*.idx" >packs.after &&
+
+		test_must_be_empty packs.before &&
+		test_line_count = 2 packs.after &&
+		for idx in $(cat packs.after)
+		do
+			git show-index <$idx || return 1
+		done >objects &&
+
+		# The resulting set of packs should contain one copy of both
+		# objects, each in a separate pack.
+		test_line_count = 2 objects &&
+		grep "^[1-9][0-9]* $tree" objects &&
+		grep "^[1-9][0-9]* $blob" objects
+
+	)
+'
+
 test_done
-- 
2.42.0.408.g97fac66ae4
Previous: Taylor BlauNext: Taylor Blau
Message 34 of 89 in “merge-ort: implement support for packing objects together”
  1. 0/7 merge-ort: implement support for packing objects togetherTaylor Blau, Oct 6, 2023
  2. 1/7 bulk-checkin: factor out `format_object_header_hash()`Taylor Blau, Oct 6, 2023
  3. 2/7 bulk-checkin: factor out `prepare_checkpoint()`Taylor Blau, Oct 6, 2023
  4. 3/7 bulk-checkin: factor out `truncate_checkpoint()`Taylor Blau, Oct 6, 2023
  5. 4/7 bulk-checkin: factor our `finalize_checkpoint()`Taylor Blau, Oct 6, 2023
  6. 5/7 bulk-checkin: introduce `index_blob_bulk_checkin_incore()`Taylor Blau, Oct 6, 2023
  7. 6/7 bulk-checkin: introduce `index_tree_bulk_checkin_incore()`Taylor Blau, Oct 6, 2023
  8. Eric BiedermanOct 7, 2023
  9. Taylor BlauOct 9, 2023
  10. 7/7 builtin/merge-tree.c: implement support for `--write-pack`Taylor Blau, Oct 6, 2023
  11. Junio C HamanoOct 6, 2023
  12. Taylor BlauOct 6, 2023
  13. Elijah NewrenOct 8, 2023
  14. Taylor BlauOct 8, 2023
  15. Jeff KingOct 8, 2023
  16. Taylor BlauOct 9, 2023
  17. Jeff KingOct 9, 2023
  18. Junio C HamanoOct 9, 2023
  19. Patrick SteinhardtOct 9, 2023
  20. Taylor BlauOct 9, 2023
  21. Patrick SteinhardtOct 10, 2023
  22. 0/7 merge-ort: implement support for packing objects togetherTaylor Blau, Oct 17, 2023
  23. 1/7 bulk-checkin: factor out `format_object_header_hash()`Taylor Blau, Oct 17, 2023
  24. 2/7 bulk-checkin: factor out `prepare_checkpoint()`Taylor Blau, Oct 17, 2023
  25. 3/7 bulk-checkin: factor out `truncate_checkpoint()`Taylor Blau, Oct 17, 2023
  26. 4/7 bulk-checkin: factor our `finalize_checkpoint()`Taylor Blau, Oct 17, 2023
  27. 5/7 bulk-checkin: introduce `index_blob_bulk_checkin_incore()`Taylor Blau, Oct 17, 2023
  28. Junio C HamanoOct 18, 2023
  29. Taylor BlauOct 18, 2023
  30. 6/7 bulk-checkin: introduce `index_tree_bulk_checkin_incore()`Taylor Blau, Oct 17, 2023
  31. 7/7 builtin/merge-tree.c: implement support for `--write-pack`Taylor Blau, Oct 17, 2023
  32. 00/10 merge-ort: implement support for packing objects togetherTaylor Blau, Oct 18, 2023
  33. 03/10 bulk-checkin: factor out `truncate_checkpoint()`Taylor Blau, Oct 18, 2023
  34. 10/10 builtin/merge-tree.c: implement support for `--write-pack`Taylor Blau, Oct 18, 2023
  35. 07/10 bulk-checkin: generify `stream_blob_to_pack()` for arbitrary typesTaylor Blau, Oct 18, 2023
  36. 08/10 bulk-checkin: introduce `index_blob_bulk_checkin_incore()`Taylor Blau, Oct 18, 2023
  37. Junio C HamanoOct 18, 2023
  38. Taylor BlauOct 19, 2023
  39. 09/10 bulk-checkin: introduce `index_tree_bulk_checkin_incore()`Taylor Blau, Oct 18, 2023
  40. 01/10 bulk-checkin: factor out `format_object_header_hash()`Taylor Blau, Oct 18, 2023
  41. 02/10 bulk-checkin: factor out `prepare_checkpoint()`Taylor Blau, Oct 18, 2023
  42. 04/10 bulk-checkin: factor out `finalize_checkpoint()`Taylor Blau, Oct 18, 2023
  43. 05/10 bulk-checkin: extract abstract `bulk_checkin_source`Taylor Blau, Oct 18, 2023
  44. Junio C HamanoOct 18, 2023
  45. Taylor BlauOct 19, 2023
  46. Junio C HamanoOct 19, 2023
  47. 06/10 bulk-checkin: implement `SOURCE_INCORE` mode for `bulk_checkin_source`Taylor Blau, Oct 18, 2023
  48. 00/17 bloom: changed-path Bloom filters v2 (& sundries)Taylor Blau, Oct 18, 2023
  49. 01/17 t/t4216-log-bloom.sh: harden `test_bloom_filters_not_used()`Taylor Blau, Oct 18, 2023
  50. 02/17 revision.c: consult Bloom filters for root commitsTaylor Blau, Oct 18, 2023
  51. 03/17 commit-graph: ensure Bloom filters are read with consistent settingsTaylor Blau, Oct 18, 2023
  52. 04/17 gitformat-commit-graph: describe version 2 of BDATTaylor Blau, Oct 18, 2023
  53. 05/17 t/helper/test-read-graph.c: extract `dump_graph_info()`Taylor Blau, Oct 18, 2023
  54. 06/17 bloom.h: make `load_bloom_filter_from_graph()` publicTaylor Blau, Oct 18, 2023
  55. 07/17 t/helper/test-read-graph: implement `bloom-filters` modeTaylor Blau, Oct 18, 2023
  56. 08/17 t4216: test changed path filters with high bit pathsTaylor Blau, Oct 18, 2023
  57. 09/17 repo-settings: introduce commitgraph.changedPathsVersionTaylor Blau, Oct 18, 2023
  58. 10/17 commit-graph: new filter ver. that fixes murmur3Taylor Blau, Oct 18, 2023
  59. 11/17 bloom: annotate filters with hash versionTaylor Blau, Oct 18, 2023
  60. 12/17 bloom: prepare to discard incompatible Bloom filtersTaylor Blau, Oct 18, 2023
  61. 13/17 commit-graph.c: unconditionally load Bloom filtersTaylor Blau, Oct 18, 2023
  62. 14/17 commit-graph: drop unnecessary `graph_read_bloom_data_context`Taylor Blau, Oct 18, 2023
  63. 15/17 object.h: fix mis-aligned flag bits tableTaylor Blau, Oct 18, 2023
  64. 16/17 commit-graph: reuse existing Bloom filters where possibleTaylor Blau, Oct 18, 2023
  65. 17/17 bloom: introduce `deinit_bloom_filters()`Taylor Blau, Oct 18, 2023
  66. Junio C HamanoOct 18, 2023
  67. Taylor BlauOct 20, 2023
  68. SZEDER GáborOct 23, 2023
  69. Taylor BlauOct 30, 2023
  70. 00/17 bloom: changed-path Bloom filters v2 (& sundries)Taylor Blau, Jan 16, 2024
  71. 01/17 t/t4216-log-bloom.sh: harden `test_bloom_filters_not_used()`Taylor Blau, Jan 16, 2024
  72. 02/17 revision.c: consult Bloom filters for root commitsTaylor Blau, Jan 16, 2024
  73. 03/17 commit-graph: ensure Bloom filters are read with consistent settingsTaylor Blau, Jan 16, 2024
  74. 04/17 gitformat-commit-graph: describe version 2 of BDATTaylor Blau, Jan 16, 2024
  75. 05/17 t/helper/test-read-graph.c: extract `dump_graph_info()`Taylor Blau, Jan 16, 2024
  76. 06/17 bloom.h: make `load_bloom_filter_from_graph()` publicTaylor Blau, Jan 16, 2024
  77. 07/17 t/helper/test-read-graph: implement `bloom-filters` modeTaylor Blau, Jan 16, 2024
  78. 08/17 t4216: test changed path filters with high bit pathsTaylor Blau, Jan 16, 2024
  79. 09/17 repo-settings: introduce commitgraph.changedPathsVersionTaylor Blau, Jan 16, 2024
  80. SZEDER GáborJan 29, 2024
  81. Taylor BlauJan 29, 2024
  82. 10/17 commit-graph: new Bloom filter version that fixes murmur3Taylor Blau, Jan 16, 2024
  83. 11/17 bloom: annotate filters with hash versionTaylor Blau, Jan 16, 2024
  84. 12/17 bloom: prepare to discard incompatible Bloom filtersTaylor Blau, Jan 16, 2024
  85. 13/17 commit-graph.c: unconditionally load Bloom filtersTaylor Blau, Jan 16, 2024
  86. 14/17 commit-graph: drop unnecessary `graph_read_bloom_data_context`Taylor Blau, Jan 16, 2024
  87. 15/17 object.h: fix mis-aligned flag bits tableTaylor Blau, Jan 16, 2024
  88. 16/17 commit-graph: reuse existing Bloom filters where possibleTaylor Blau, Jan 16, 2024
  89. 17/17 bloom: introduce `deinit_bloom_filters()`Taylor Blau, Jan 16, 2024

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.