Skip to content

Commit bbb2b93

Browse files
jltoblergitster
authored andcommitted
builtin/repo: introduce structure subcommand
The structure of a repository's history can have huge impacts on the performance and health of the repository itself. Currently, Git lacks a means to surface repository metrics regarding its structure/shape via a single command. Acquiring this information requires users to be familiar with the relevant data points and the various Git commands required to surface them. To fill this gap, supplemental tools such as git-sizer(1) have been developed. To allow users to more readily identify repository structure related information, introduce the "structure" subcommand in git-repo(1). The goal of this subcommand is to eventually provide similar functionality to git-sizer(1), but natively in Git. The initial version of this command only iterates through all references in the repository and tracks the count of branches, tags, remote refs, and other reference types. The corresponding information is displayed in a human-friendly table formatted in a very similar manner to git-sizer(1). The width of each table column is adjusted automatically to satisfy the requirements of the widest row contained. Subsequent commits will surface additional relevant data points to output and also provide other more machine-friendly output formats. Based-on-patch-by: Derrick Stolee <stolee@gmail.com> Signed-off-by: Justin Tobler <jltobler@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
1 parent 6d1997f commit bbb2b93

File tree

4 files changed

+272
-0
lines changed

4 files changed

+272
-0
lines changed

Documentation/git-repo.adoc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ SYNOPSIS
99
--------
1010
[synopsis]
1111
git repo info [--format=(keyvalue|nul)] [-z] [<key>...]
12+
git repo structure
1213

1314
DESCRIPTION
1415
-----------
@@ -43,6 +44,15 @@ supported:
4344
+
4445
`-z` is an alias for `--format=nul`.
4546

47+
`structure`::
48+
Retrieve statistics about the current repository structure. The
49+
following kinds of information are reported:
50+
+
51+
* Reference counts categorized by type
52+
53+
+
54+
The table output format may change and is not intended for machine parsing.
55+
4656
INFO KEYS
4757
---------
4858
In order to obtain a set of values from `git repo info`, you should provide

builtin/repo.c

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@
44
#include "environment.h"
55
#include "parse-options.h"
66
#include "quote.h"
7+
#include "ref-filter.h"
78
#include "refs.h"
89
#include "strbuf.h"
10+
#include "string-list.h"
911
#include "shallow.h"
12+
#include "utf8.h"
1013

1114
static const char *const repo_usage[] = {
1215
"git repo info [--format=(keyvalue|nul)] [-z] [<key>...]",
16+
"git repo structure",
1317
NULL
1418
};
1519

@@ -156,12 +160,208 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix,
156160
return print_fields(argc, argv, repo, format);
157161
}
158162

163+
struct ref_stats {
164+
size_t branches;
165+
size_t remotes;
166+
size_t tags;
167+
size_t others;
168+
};
169+
170+
struct stats_table {
171+
struct string_list rows;
172+
173+
int name_col_width;
174+
int value_col_width;
175+
};
176+
177+
/*
178+
* Holds column data that gets stored for each row.
179+
*/
180+
struct stats_table_entry {
181+
char *value;
182+
};
183+
184+
static void stats_table_vaddf(struct stats_table *table,
185+
struct stats_table_entry *entry,
186+
const char *format, va_list ap)
187+
{
188+
struct strbuf buf = STRBUF_INIT;
189+
struct string_list_item *item;
190+
char *formatted_name;
191+
int name_width;
192+
193+
strbuf_vaddf(&buf, format, ap);
194+
formatted_name = strbuf_detach(&buf, NULL);
195+
name_width = utf8_strwidth(formatted_name);
196+
197+
item = string_list_append_nodup(&table->rows, formatted_name);
198+
item->util = entry;
199+
200+
if (name_width > table->name_col_width)
201+
table->name_col_width = name_width;
202+
if (entry) {
203+
int value_width = utf8_strwidth(entry->value);
204+
if (value_width > table->value_col_width)
205+
table->value_col_width = value_width;
206+
}
207+
}
208+
209+
static void stats_table_addf(struct stats_table *table, const char *format, ...)
210+
{
211+
va_list ap;
212+
213+
va_start(ap, format);
214+
stats_table_vaddf(table, NULL, format, ap);
215+
va_end(ap);
216+
}
217+
218+
static void stats_table_count_addf(struct stats_table *table, size_t value,
219+
const char *format, ...)
220+
{
221+
struct stats_table_entry *entry;
222+
va_list ap;
223+
224+
CALLOC_ARRAY(entry, 1);
225+
entry->value = xstrfmt("%" PRIuMAX, (uintmax_t)value);
226+
227+
va_start(ap, format);
228+
stats_table_vaddf(table, entry, format, ap);
229+
va_end(ap);
230+
}
231+
232+
static inline size_t get_total_reference_count(struct ref_stats *stats)
233+
{
234+
return stats->branches + stats->remotes + stats->tags + stats->others;
235+
}
236+
237+
static void stats_table_setup_structure(struct stats_table *table,
238+
struct ref_stats *refs)
239+
{
240+
size_t ref_total;
241+
242+
ref_total = get_total_reference_count(refs);
243+
stats_table_addf(table, "* %s", _("References"));
244+
stats_table_count_addf(table, ref_total, " * %s", _("Count"));
245+
stats_table_count_addf(table, refs->branches, " * %s", _("Branches"));
246+
stats_table_count_addf(table, refs->tags, " * %s", _("Tags"));
247+
stats_table_count_addf(table, refs->remotes, " * %s", _("Remotes"));
248+
stats_table_count_addf(table, refs->others, " * %s", _("Others"));
249+
}
250+
251+
static void stats_table_print_structure(const struct stats_table *table)
252+
{
253+
const char *name_col_title = _("Repository structure");
254+
const char *value_col_title = _("Value");
255+
int name_col_width = utf8_strwidth(name_col_title);
256+
int value_col_width = utf8_strwidth(value_col_title);
257+
struct string_list_item *item;
258+
259+
if (table->name_col_width > name_col_width)
260+
name_col_width = table->name_col_width;
261+
if (table->value_col_width > value_col_width)
262+
value_col_width = table->value_col_width;
263+
264+
printf("| %-*s | %-*s |\n", name_col_width, name_col_title,
265+
value_col_width, value_col_title);
266+
printf("| ");
267+
for (int i = 0; i < name_col_width; i++)
268+
putchar('-');
269+
printf(" | ");
270+
for (int i = 0; i < value_col_width; i++)
271+
putchar('-');
272+
printf(" |\n");
273+
274+
for_each_string_list_item(item, &table->rows) {
275+
struct stats_table_entry *entry = item->util;
276+
const char *value = "";
277+
278+
if (entry) {
279+
struct stats_table_entry *entry = item->util;
280+
value = entry->value;
281+
}
282+
283+
printf("| %-*s | %*s |\n", name_col_width, item->string,
284+
value_col_width, value);
285+
}
286+
}
287+
288+
static void stats_table_clear(struct stats_table *table)
289+
{
290+
struct stats_table_entry *entry;
291+
struct string_list_item *item;
292+
293+
for_each_string_list_item(item, &table->rows) {
294+
entry = item->util;
295+
if (entry)
296+
free(entry->value);
297+
}
298+
299+
string_list_clear(&table->rows, 1);
300+
}
301+
302+
static int count_references(const char *refname,
303+
const char *referent UNUSED,
304+
const struct object_id *oid UNUSED,
305+
int flags UNUSED, void *cb_data)
306+
{
307+
struct ref_stats *stats = cb_data;
308+
309+
switch (ref_kind_from_refname(refname)) {
310+
case FILTER_REFS_BRANCHES:
311+
stats->branches++;
312+
break;
313+
case FILTER_REFS_REMOTES:
314+
stats->remotes++;
315+
break;
316+
case FILTER_REFS_TAGS:
317+
stats->tags++;
318+
break;
319+
case FILTER_REFS_OTHERS:
320+
stats->others++;
321+
break;
322+
default:
323+
BUG("unexpected reference type");
324+
}
325+
326+
return 0;
327+
}
328+
329+
static void structure_count_references(struct ref_stats *stats,
330+
struct repository *repo)
331+
{
332+
refs_for_each_ref(get_main_ref_store(repo), count_references, &stats);
333+
}
334+
335+
static int cmd_repo_structure(int argc, const char **argv, const char *prefix,
336+
struct repository *repo)
337+
{
338+
struct stats_table table = {
339+
.rows = STRING_LIST_INIT_DUP,
340+
};
341+
struct ref_stats stats = { 0 };
342+
struct option options[] = { 0 };
343+
344+
argc = parse_options(argc, argv, prefix, options, repo_usage, 0);
345+
if (argc)
346+
usage(_("too many arguments"));
347+
348+
structure_count_references(&stats, repo);
349+
350+
stats_table_setup_structure(&table, &stats);
351+
stats_table_print_structure(&table);
352+
353+
stats_table_clear(&table);
354+
355+
return 0;
356+
}
357+
159358
int cmd_repo(int argc, const char **argv, const char *prefix,
160359
struct repository *repo)
161360
{
162361
parse_opt_subcommand_fn *fn = NULL;
163362
struct option options[] = {
164363
OPT_SUBCOMMAND("info", &fn, cmd_repo_info),
364+
OPT_SUBCOMMAND("structure", &fn, cmd_repo_structure),
165365
OPT_END()
166366
};
167367

t/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ integration_tests = [
236236
't1701-racy-split-index.sh',
237237
't1800-hook.sh',
238238
't1900-repo.sh',
239+
't1901-repo-structure.sh',
239240
't2000-conflict-when-checking-files-out.sh',
240241
't2002-checkout-cache-u.sh',
241242
't2003-checkout-cache-mkdir.sh',

t/t1901-repo-structure.sh

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#!/bin/sh
2+
3+
test_description='test git repo structure'
4+
5+
. ./test-lib.sh
6+
7+
test_expect_success 'empty repository' '
8+
test_when_finished "rm -rf repo" &&
9+
git init repo &&
10+
(
11+
cd repo &&
12+
cat >expect <<-\EOF &&
13+
| Repository structure | Value |
14+
| -------------------- | ----- |
15+
| * References | |
16+
| * Count | 0 |
17+
| * Branches | 0 |
18+
| * Tags | 0 |
19+
| * Remotes | 0 |
20+
| * Others | 0 |
21+
EOF
22+
23+
git repo structure >out 2>err &&
24+
25+
test_cmp expect out &&
26+
test_line_count = 0 err
27+
)
28+
'
29+
30+
test_expect_success 'repository with references' '
31+
test_when_finished "rm -rf repo" &&
32+
git init repo &&
33+
(
34+
cd repo &&
35+
git commit --allow-empty -m init &&
36+
git tag -a foo -m bar &&
37+
38+
oid="$(git rev-parse HEAD)" &&
39+
git update-ref refs/remotes/origin/foo "$oid" &&
40+
41+
git notes add -m foo &&
42+
43+
cat >expect <<-\EOF &&
44+
| Repository structure | Value |
45+
| -------------------- | ----- |
46+
| * References | |
47+
| * Count | 4 |
48+
| * Branches | 1 |
49+
| * Tags | 1 |
50+
| * Remotes | 1 |
51+
| * Others | 1 |
52+
EOF
53+
54+
git repo structure >out 2>err &&
55+
56+
test_cmp expect out &&
57+
test_line_count = 0 err
58+
)
59+
'
60+
61+
test_done

0 commit comments

Comments
 (0)