builtin/repo: find commit with most parents

Complex merge events may produce an octopus merge where the resulting merge commit has more than two parents. While iterating through objects in the repository for git-repo-structure, identify the commit with the most parents and display it in the output. Signed-off-by: Justin Tobler <jltobler@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Justin Tobler committed Mar 2, 2026 at 15:45 UTC 18952a1ef1a14d2fca19638118dc2eea1e24d671
2 files changed +123 -73
builtin/repo.c
+45
@@ -1,6 +1,7 @@
1 #define USE_THE_REPOSITORY_VARIABLE
2
3 #include "builtin.h"
4 +#include "commit.h"
5 #include "environment.h"
6 #include "hash.h"
7 #include "hex.h"
@@ -208,6 +209,8 @@ struct largest_objects {
209 struct object_data commit_size;
210 struct object_data tree_size;
211 struct object_data blob_size;
212 +
213 + struct object_data parent_count;
214 };
215
216 struct ref_stats {
@@ -318,6 +321,27 @@ static void stats_table_count_addf(struct stats_table *table, size_t value,
321 va_end(ap);
322 }
323
324 +static void stats_table_object_count_addf(struct stats_table *table,
325 + struct object_id *oid, size_t value,
326 + const char *format, ...)
327 +{
328 + struct stats_table_entry *entry;
329 + va_list ap;
330 +
331 + CALLOC_ARRAY(entry, 1);
332 + humanise_count(value, &entry->value, &entry->unit);
333 +
334 + /*
335 + * A NULL OID should not have a table annotation.
336 + */
337 + if (!is_null_oid(oid))
338 + entry->oid = oid;
339 +
340 + va_start(ap, format);
341 + stats_table_vaddf(table, entry, format, ap);
342 + va_end(ap);
343 +}
344 +
345 static void stats_table_size_addf(struct stats_table *table, size_t value,
346 const char *format, ...)
347 {
@@ -425,6 +449,10 @@ static void stats_table_setup_structure(struct stats_table *table,
449 &objects->largest.commit_size.oid,
450 objects->largest.commit_size.value,
451 " * %s", _("Maximum size"));
452 + stats_table_object_count_addf(table,
453 + &objects->largest.parent_count.oid,
454 + objects->largest.parent_count.value,
455 + " * %s", _("Maximum parents"));
456 stats_table_addf(table, " * %s", _("Trees"));
457 stats_table_object_size_addf(table,
458 &objects->largest.tree_size.oid,
@@ -595,6 +623,9 @@ static void structure_keyvalue_print(struct repo_structure *stats,
623 print_object_data("objects.tags.max_size", key_delim,
624 &stats->objects.largest.tag_size, value_delim);
625
626 + print_object_data("objects.commits.max_parents", key_delim,
627 + &stats->objects.largest.parent_count, value_delim);
628 +
629 fflush(stdout);
630 }
631
@@ -682,16 +713,24 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,
713 for (size_t i = 0; i < oids->nr; i++) {
714 struct object_info oi = OBJECT_INFO_INIT;
715 unsigned long inflated;
716 + struct commit *commit;
717 + struct object *obj;
718 + void *content;
719 off_t disk;
720 + int eaten;
721
722 oi.sizep = &inflated;
723 oi.disk_sizep = &disk;
724 + oi.contentp = &content;
725
726 if (odb_read_object_info_extended(data->odb, &oids->oid[i], &oi,
727 OBJECT_INFO_SKIP_FETCH_OBJECT |
728 OBJECT_INFO_QUICK) < 0)
729 continue;
730
731 + obj = parse_object_buffer(the_repository, &oids->oid[i], type,
732 + inflated, content, &eaten);
733 +
734 switch (type) {
735 case OBJ_TAG:
736 stats->type_counts.tags++;
@@ -701,11 +740,14 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,
740 inflated);
741 break;
742 case OBJ_COMMIT:
743 + commit = object_as_type(obj, OBJ_COMMIT, 0);
744 stats->type_counts.commits++;
745 stats->inflated_sizes.commits += inflated;
746 stats->disk_sizes.commits += disk;
747 check_largest(&stats->largest.commit_size, &oids->oid[i],
748 inflated);
749 + check_largest(&stats->largest.parent_count, &oids->oid[i],
750 + commit_list_count(commit->parents));
751 break;
752 case OBJ_TREE:
753 stats->type_counts.trees++;
@@ -724,6 +766,9 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids,
766 default:
767 BUG("invalid object type");
768 }
769 +
770 + if (!eaten)
771 + free(content);
772 }
773
774 object_count = get_total_object_values(&stats->type_counts);
t/t1901-repo-structure.sh
+78 -73
@@ -27,41 +27,42 @@ test_expect_success 'empty repository' '
27 (
28 cd repo &&
29 cat >expect <<-\EOF &&
30 - | Repository structure | Value |
31 - | ------------------------ | ------ |
32 - | * References | |
33 - | * Count | 0 |
34 - | * Branches | 0 |
35 - | * Tags | 0 |
36 - | * Remotes | 0 |
37 - | * Others | 0 |
38 - | | |
39 - | * Reachable objects | |
40 - | * Count | 0 |
41 - | * Commits | 0 |
42 - | * Trees | 0 |
43 - | * Blobs | 0 |
44 - | * Tags | 0 |
45 - | * Inflated size | 0 B |
46 - | * Commits | 0 B |
47 - | * Trees | 0 B |
48 - | * Blobs | 0 B |
49 - | * Tags | 0 B |
50 - | * Disk size | 0 B |
51 - | * Commits | 0 B |
52 - | * Trees | 0 B |
53 - | * Blobs | 0 B |
54 - | * Tags | 0 B |
55 - | | |
56 - | * Largest objects | |
57 - | * Commits | |
58 - | * Maximum size | 0 B |
59 - | * Trees | |
60 - | * Maximum size | 0 B |
61 - | * Blobs | |
62 - | * Maximum size | 0 B |
63 - | * Tags | |
64 - | * Maximum size | 0 B |
30 + | Repository structure | Value |
31 + | ------------------------- | ------ |
32 + | * References | |
33 + | * Count | 0 |
34 + | * Branches | 0 |
35 + | * Tags | 0 |
36 + | * Remotes | 0 |
37 + | * Others | 0 |
38 + | | |
39 + | * Reachable objects | |
40 + | * Count | 0 |
41 + | * Commits | 0 |
42 + | * Trees | 0 |
43 + | * Blobs | 0 |
44 + | * Tags | 0 |
45 + | * Inflated size | 0 B |
46 + | * Commits | 0 B |
47 + | * Trees | 0 B |
48 + | * Blobs | 0 B |
49 + | * Tags | 0 B |
50 + | * Disk size | 0 B |
51 + | * Commits | 0 B |
52 + | * Trees | 0 B |
53 + | * Blobs | 0 B |
54 + | * Tags | 0 B |
55 + | | |
56 + | * Largest objects | |
57 + | * Commits | |
58 + | * Maximum size | 0 B |
59 + | * Maximum parents | 0 |
60 + | * Trees | |
61 + | * Maximum size | 0 B |
62 + | * Blobs | |
63 + | * Maximum size | 0 B |
64 + | * Tags | |
65 + | * Maximum size | 0 B |
66 EOF
67
68 git repo structure >out 2>err &&
@@ -89,46 +90,48 @@ test_expect_success SHA1 'repository with references and objects' '
90 # git-rev-list(1) --disk-usage=human option printing the full
91 # "byte/bytes" unit string instead of just "B".
92 cat >expect <<-EOF &&
92 - | Repository structure | Value |
93 - | ------------------------ | ---------- |
94 - | * References | |
95 - | * Count | 4 |
96 - | * Branches | 1 |
97 - | * Tags | 1 |
98 - | * Remotes | 1 |
99 - | * Others | 1 |
100 - | | |
101 - | * Reachable objects | |
102 - | * Count | 3.02 k |
103 - | * Commits | 1.01 k |
104 - | * Trees | 1.01 k |
105 - | * Blobs | 1.01 k |
106 - | * Tags | 1 |
107 - | * Inflated size | 16.03 MiB |
108 - | * Commits | 217.92 KiB |
109 - | * Trees | 15.81 MiB |
110 - | * Blobs | 11.68 KiB |
111 - | * Tags | 132 B |
112 - | * Disk size | $(object_type_disk_usage all true) |
113 - | * Commits | $(object_type_disk_usage commit true) |
114 - | * Trees | $(object_type_disk_usage tree true) |
115 - | * Blobs | $(object_type_disk_usage blob true) |
116 - | * Tags | $(object_type_disk_usage tag) B |
117 - | | |
118 - | * Largest objects | |
119 - | * Commits | |
120 - | * Maximum size [1] | 223 B |
121 - | * Trees | |
122 - | * Maximum size [2] | 32.29 KiB |
123 - | * Blobs | |
124 - | * Maximum size [3] | 13 B |
125 - | * Tags | |
126 - | * Maximum size [4] | 132 B |
93 + | Repository structure | Value |
94 + | ------------------------- | ---------- |
95 + | * References | |
96 + | * Count | 4 |
97 + | * Branches | 1 |
98 + | * Tags | 1 |
99 + | * Remotes | 1 |
100 + | * Others | 1 |
101 + | | |
102 + | * Reachable objects | |
103 + | * Count | 3.02 k |
104 + | * Commits | 1.01 k |
105 + | * Trees | 1.01 k |
106 + | * Blobs | 1.01 k |
107 + | * Tags | 1 |
108 + | * Inflated size | 16.03 MiB |
109 + | * Commits | 217.92 KiB |
110 + | * Trees | 15.81 MiB |
111 + | * Blobs | 11.68 KiB |
112 + | * Tags | 132 B |
113 + | * Disk size | $(object_type_disk_usage all true) |
114 + | * Commits | $(object_type_disk_usage commit true) |
115 + | * Trees | $(object_type_disk_usage tree true) |
116 + | * Blobs | $(object_type_disk_usage blob true) |
117 + | * Tags | $(object_type_disk_usage tag) B |
118 + | | |
119 + | * Largest objects | |
120 + | * Commits | |
121 + | * Maximum size [1] | 223 B |
122 + | * Maximum parents [2] | 1 |
123 + | * Trees | |
124 + | * Maximum size [3] | 32.29 KiB |
125 + | * Blobs | |
126 + | * Maximum size [4] | 13 B |
127 + | * Tags | |
128 + | * Maximum size [5] | 132 B |
129
130 [1] 0dc91eb18580102a3a216c8bfecedeba2b9f9b9a
129 - [2] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c
130 - [3] 97d808e45116bf02103490294d3d46dad7a2ac62
131 - [4] 4dae4f5954f5e6feb3577cfb1b181daa3fd3afd2
131 + [2] 0dc91eb18580102a3a216c8bfecedeba2b9f9b9a
132 + [3] 60665251ab71dbd8c18d9bf2174f4ee0d58aa06c
133 + [4] 97d808e45116bf02103490294d3d46dad7a2ac62
134 + [5] 4dae4f5954f5e6feb3577cfb1b181daa3fd3afd2
135 EOF
136
137 git repo structure >out 2>err &&
@@ -171,6 +174,8 @@ test_expect_success SHA1 'keyvalue and nul format' '
174 objects.blobs.max_size_oid=eaeeedced46482bd4281fda5a5f05ce24854151f
175 objects.tags.max_size=132
176 objects.tags.max_size_oid=1ee0f2b16ea37d895dbe9dbd76cd2ac70446176c
177 + objects.commits.max_parents=1
178 + objects.commits.max_parents_oid=de3508174b5c2ace6993da67cae9be9069e2df39
179 EOF
180
181 git repo structure --format=keyvalue >out 2>err &&