Skip to content

Commit c9c16ca

Browse files
committed
test-tool: add a "historian" subcommand for building merge fixtures
The merge-replay tests added in a follow-up commit need a way to set up specific topologies with full control over blob contents, parent order, and per-side trees. Sequencing plumbing commands or driving plain `git fast-import` from shell quickly becomes unreadable for the kinds of scenarios that exercise non-trivial merge resolution (textual conflicts, semantic edits outside the conflict region, intentional limitations such as new content on one side). Add a small `test-tool historian` subcommand that reads a tight, shell-quoted, one-line-per-object DSL and feeds an equivalent stream to a `git fast-import` child process. Each blob and commit is given a logical name; the helper allocates fast-import marks on first use and emits a lightweight tag for every commit so tests can refer to the resulting object via `refs/tags/<name>`. The DSL has just two directives: blob NAME LINE... commit NAME BRANCH SUBJECT [from=NAME] [merge=NAME]... [PATH=BLOB]... A blob's content is the listed lines joined with `\n` (and a final `\n`); a commit's tree is exactly the listed PATH=BLOB pairs (the helper emits a `deleteall` so nothing leaks in from the implicit parent). Token splitting is delegated to `split_cmdline()` so quoted arguments work as in shell. Marks for parent references and file contents go through the same `strintmap`-backed name resolver, which keeps the helper itself trivially small: blob writing, tree construction, commit creation and merge-base computation are all handled by `git fast-import`. Note that the DSL reserves the names `from` and `merge` (with a trailing `=`) for parent specification; a tree path called `from` or `merge` cannot be expressed via this helper. That is acceptable here because every input is a tightly controlled test fixture and the filenames are chosen by the test author. The helper trusts its caller: malformed input results in a fast-import error rather than a friendly diagnostic. Wire the new subcommand into the Makefile and meson build, register it in `t/helper/test-tool.{c,h}`. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
1 parent 2f3d696 commit c9c16ca

5 files changed

Lines changed: 193 additions & 0 deletions

File tree

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -832,6 +832,7 @@ TEST_BUILTINS_OBJS += test-hash-speed.o
832832
TEST_BUILTINS_OBJS += test-hash.o
833833
TEST_BUILTINS_OBJS += test-hashmap.o
834834
TEST_BUILTINS_OBJS += test-hexdump.o
835+
TEST_BUILTINS_OBJS += test-historian.o
835836
TEST_BUILTINS_OBJS += test-json-writer.o
836837
TEST_BUILTINS_OBJS += test-lazy-init-name-hash.o
837838
TEST_BUILTINS_OBJS += test-match-trees.o

t/helper/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ test_tool_sources = [
2929
'test-hash.c',
3030
'test-hashmap.c',
3131
'test-hexdump.c',
32+
'test-historian.c',
3233
'test-json-writer.c',
3334
'test-lazy-init-name-hash.c',
3435
'test-match-trees.c',

t/helper/test-historian.c

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/*
2+
* Build a small history out of a tiny declarative input. Used by tests
3+
* that need specific merge topologies without long sequences of
4+
* plumbing commands or fragile shell helpers.
5+
*
6+
* The historian reads stdin line by line and emits an equivalent
7+
* stream to a `git fast-import` child process. It also allocates marks
8+
* for named objects so tests can refer to commits and blobs by name.
9+
*
10+
* Input directives (one per line, shell-style quoting):
11+
*
12+
* blob NAME LINE1 LINE2 ...
13+
* Each LINE becomes a content line in the blob; lines are
14+
* joined with '\n' and the blob ends with a final '\n'. With
15+
* no LINEs, the blob is empty.
16+
*
17+
* commit NAME BRANCH SUBJECT [from=PARENT] [merge=PARENT]... [PATH=BLOB]...
18+
* Creates a commit on refs/heads/BRANCH using the listed
19+
* file=blob mappings as the entire tree (no inheritance from
20+
* parents). Up to one `from=` and any number of `merge=`
21+
* parents may be given. `from=` defaults to the current branch
22+
* tip; if BRANCH has no tip yet, the commit becomes a root.
23+
*
24+
* Each `commit NAME` directive also creates a lightweight tag
25+
* `refs/tags/NAME` so tests can `git rev-parse NAME`.
26+
*
27+
* This helper trusts its caller; malformed input results in fast-import
28+
* errors. That is fine because test scripts feed it tightly controlled
29+
* input.
30+
*/
31+
32+
#define USE_THE_REPOSITORY_VARIABLE
33+
34+
#include "test-tool.h"
35+
#include "git-compat-util.h"
36+
#include "alias.h"
37+
#include "run-command.h"
38+
#include "setup.h"
39+
#include "strbuf.h"
40+
#include "strmap.h"
41+
#include "strvec.h"
42+
43+
static int next_mark = 1;
44+
45+
static int resolve_mark(struct strintmap *names, const char *name)
46+
{
47+
int n = strintmap_get(names, name);
48+
if (!n) {
49+
n = next_mark++;
50+
strintmap_set(names, name, n);
51+
}
52+
return n;
53+
}
54+
55+
static void emit_data(FILE *out, const char *data, size_t len)
56+
{
57+
fprintf(out, "data %"PRIuMAX"\n", (uintmax_t)len);
58+
fwrite(data, 1, len, out);
59+
fputc('\n', out);
60+
}
61+
62+
static void emit_blob(FILE *out, struct strintmap *names,
63+
int argc, const char **argv)
64+
{
65+
struct strbuf content = STRBUF_INIT;
66+
int n = resolve_mark(names, argv[1]);
67+
int i;
68+
69+
for (i = 2; i < argc; i++) {
70+
strbuf_addstr(&content, argv[i]);
71+
strbuf_addch(&content, '\n');
72+
}
73+
74+
fprintf(out, "blob\nmark :%d\n", n);
75+
emit_data(out, content.buf, content.len);
76+
strbuf_release(&content);
77+
}
78+
79+
static void emit_tag(FILE *out, const char *name, int mark)
80+
{
81+
fprintf(out, "reset refs/tags/%s\nfrom :%d\n\n", name, mark);
82+
}
83+
84+
static void emit_commit(FILE *out, struct strintmap *names,
85+
int argc, const char **argv, int seq)
86+
{
87+
int n = resolve_mark(names, argv[1]);
88+
const char *branch = argv[2];
89+
const char *subject = argv[3];
90+
const char *rest;
91+
int i;
92+
93+
fprintf(out, "commit refs/heads/%s\nmark :%d\n", branch, n);
94+
fprintf(out, "author A <a@e> %d +0000\n", 1700000000 + seq);
95+
fprintf(out, "committer A <a@e> %d +0000\n", 1700000000 + seq);
96+
emit_data(out, subject, strlen(subject));
97+
98+
/*
99+
* fast-import requires `from` and `merge` to precede all file
100+
* operations; emit them first regardless of argv ordering.
101+
*/
102+
for (i = 4; i < argc; i++) {
103+
if (skip_prefix(argv[i], "from=", &rest))
104+
fprintf(out, "from :%d\n", resolve_mark(names, rest));
105+
else if (skip_prefix(argv[i], "merge=", &rest))
106+
fprintf(out, "merge :%d\n", resolve_mark(names, rest));
107+
}
108+
109+
/*
110+
* The PATH=BLOB list is the entire tree; wipe whatever the
111+
* implicit parent contributed before re-applying it.
112+
*/
113+
fprintf(out, "deleteall\n");
114+
for (i = 4; i < argc; i++) {
115+
const char *eq;
116+
size_t key_len;
117+
char *path;
118+
119+
if (skip_prefix(argv[i], "from=", &rest) ||
120+
skip_prefix(argv[i], "merge=", &rest))
121+
continue;
122+
eq = strchr(argv[i], '=');
123+
if (!eq)
124+
die("bad commit spec '%s'", argv[i]);
125+
key_len = eq - argv[i];
126+
path = xmemdupz(argv[i], key_len);
127+
fprintf(out, "M 100644 :%d %s\n",
128+
resolve_mark(names, eq + 1), path);
129+
free(path);
130+
}
131+
132+
fputc('\n', out);
133+
emit_tag(out, argv[1], n);
134+
}
135+
136+
int cmd__historian(int argc, const char **argv UNUSED)
137+
{
138+
struct child_process fi = CHILD_PROCESS_INIT;
139+
struct strintmap names = STRINTMAP_INIT;
140+
struct strbuf line = STRBUF_INIT;
141+
int seq = 0;
142+
int ret = 0;
143+
FILE *fi_in;
144+
145+
if (argc != 1)
146+
die("usage: test-tool historian <input");
147+
148+
setup_git_directory();
149+
150+
strvec_pushl(&fi.args, "fast-import", "--quiet", "--force", NULL);
151+
fi.git_cmd = 1;
152+
fi.in = -1;
153+
fi.no_stdout = 1;
154+
if (start_command(&fi))
155+
die("failed to start git fast-import");
156+
fi_in = xfdopen(fi.in, "w");
157+
158+
while (strbuf_getline_lf(&line, stdin) != EOF) {
159+
const char **a = NULL;
160+
int n;
161+
162+
strbuf_trim(&line);
163+
if (!line.len || line.buf[0] == '#')
164+
continue;
165+
166+
n = split_cmdline(line.buf, &a);
167+
if (n < 0)
168+
die("split_cmdline failed: %s",
169+
split_cmdline_strerror(n));
170+
171+
if (n >= 2 && !strcmp(a[0], "blob"))
172+
emit_blob(fi_in, &names, n, a);
173+
else if (n >= 4 && !strcmp(a[0], "commit"))
174+
emit_commit(fi_in, &names, n, a, seq++);
175+
else
176+
die("unknown directive: %s", a[0]);
177+
178+
free(a);
179+
}
180+
181+
if (fclose(fi_in))
182+
die_errno("close fast-import stdin");
183+
if (finish_command(&fi))
184+
ret = 1;
185+
186+
strbuf_release(&line);
187+
strintmap_clear(&names);
188+
return ret;
189+
}

t/helper/test-tool.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ static struct test_cmd cmds[] = {
3939
{ "hashmap", cmd__hashmap },
4040
{ "hash-speed", cmd__hash_speed },
4141
{ "hexdump", cmd__hexdump },
42+
{ "historian", cmd__historian },
4243
{ "json-writer", cmd__json_writer },
4344
{ "lazy-init-name-hash", cmd__lazy_init_name_hash },
4445
{ "match-trees", cmd__match_trees },

t/helper/test-tool.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ int cmd__getcwd(int argc, const char **argv);
3232
int cmd__hashmap(int argc, const char **argv);
3333
int cmd__hash_speed(int argc, const char **argv);
3434
int cmd__hexdump(int argc, const char **argv);
35+
int cmd__historian(int argc, const char **argv);
3536
int cmd__json_writer(int argc, const char **argv);
3637
int cmd__lazy_init_name_hash(int argc, const char **argv);
3738
int cmd__match_trees(int argc, const char **argv);

0 commit comments

Comments
 (0)