Skip to content

Commit fd0618d

Browse files
committed
WIP range-diff: support reading mbox files
TODO: - document - split out the `mbox:-` code - add a test (probably by using `git format-patch --stdout`) Internally, the `git range-diff` command spawns a `git log` process and parses its output for the given commit ranges. This works well when the patches that need to be compared are present in the local repository in the form of commits. In scenarios where that is not the case, the `range-diff` command is unhelpful. For example, if one desired to compare commits in a local repository to patches that were sent to the Git mailing list, it would be necessary to apply the patches locally first, which requires finding an appropriate base commit or adjusting the patches manually so that they apply. This is a lot of effort given that `range-diff` would then go and internally re-generate the patches, anyway. Let's offer a way to read those patches from pre-prepared MBox files instead when an argument "mbox:<filename>" is passed instead of a commit range. This addresses gitgitgadget#207 Signed-off-by: Johannes Schindelin <[email protected]>
1 parent 741c469 commit fd0618d

File tree

2 files changed

+173
-1
lines changed

2 files changed

+173
-1
lines changed

builtin/range-diff.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ static int is_range(const char *range)
1616
size_t i;
1717
char c;
1818

19-
if (strstr(range, ".."))
19+
if (starts_with(range, "mbox:") || strstr(range, ".."))
2020
return 1;
2121

2222
i = strlen(range);

range-diff.c

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,171 @@ static size_t find_end_of_line(char *buffer, unsigned long size)
3636
return eol + 1 - buffer;
3737
}
3838

39+
static int read_mbox(const char *path, struct string_list *list)
40+
{
41+
struct strbuf buf = STRBUF_INIT, contents = STRBUF_INIT;
42+
struct patch_util *util = NULL;
43+
int in_header = 1, in_diff = 0;
44+
char *line, *current_filename = NULL;
45+
int offset, len;
46+
size_t size;
47+
const char *author = NULL, *subject = NULL;
48+
49+
if (!strcmp(path, "-")) {
50+
if (strbuf_read(&contents, STDIN_FILENO, 0) < 0)
51+
return error_errno(_("could not read stdin"));
52+
} else if (strbuf_read_file(&contents, path, 0) < 0)
53+
return error_errno(_("could not read '%s'"), path);
54+
55+
line = contents.buf;
56+
size = contents.len;
57+
for (offset = 0; size > 0; offset += len, size -= len, line += len) {
58+
const char *p;
59+
60+
len = find_end_of_line(line, size);
61+
line[len - 1] = '\0';
62+
63+
if (starts_with(line, "diff --git")) {
64+
struct patch patch = { 0 };
65+
struct strbuf root = STRBUF_INIT;
66+
int linenr = 0;
67+
int orig_len;
68+
69+
in_diff = 1;
70+
strbuf_addch(&buf, '\n');
71+
if (!util) {
72+
util = xcalloc(sizeof(*util), 1);
73+
oidcpy(&util->oid, &null_oid);
74+
util->matching = -1;
75+
author = subject = NULL;
76+
}
77+
if (!util->diff_offset)
78+
util->diff_offset = buf.len;
79+
line[len - 1] = '\n';
80+
orig_len = len;
81+
len = parse_git_diff_header(&root, &linenr, 0, line,
82+
len, size, &patch);
83+
if (len < 0) {
84+
error(_("could not parse git header '%.*s'"),
85+
orig_len, line);
86+
free(util);
87+
free(current_filename);
88+
string_list_clear(list, 1);
89+
strbuf_release(&buf);
90+
strbuf_release(&contents);
91+
return -1;
92+
}
93+
94+
if (patch.old_name)
95+
skip_prefix(patch.old_name, "a/",
96+
(const char **)&patch.old_name);
97+
if (patch.new_name)
98+
skip_prefix(patch.new_name, "b/",
99+
(const char **)&patch.new_name);
100+
101+
strbuf_addstr(&buf, " ## ");
102+
if (patch.is_new > 0)
103+
strbuf_addf(&buf, "%s (new)", patch.new_name);
104+
else if (patch.is_delete > 0)
105+
strbuf_addf(&buf, "%s (deleted)", patch.old_name);
106+
else if (patch.is_rename)
107+
strbuf_addf(&buf, "%s => %s", patch.old_name, patch.new_name);
108+
else
109+
strbuf_addstr(&buf, patch.new_name);
110+
111+
free(current_filename);
112+
if (patch.is_delete > 0)
113+
current_filename = xstrdup(patch.old_name);
114+
else
115+
current_filename = xstrdup(patch.new_name);
116+
117+
if (patch.new_mode && patch.old_mode &&
118+
patch.old_mode != patch.new_mode)
119+
strbuf_addf(&buf, " (mode change %06o => %06o)",
120+
patch.old_mode, patch.new_mode);
121+
122+
strbuf_addstr(&buf, " ##\n");
123+
util->diffsize++;
124+
continue;
125+
} else if (in_diff) {
126+
switch (line[0]) {
127+
case '\0':
128+
continue; /* ignore empty lines after diff */
129+
case '+':
130+
case '-':
131+
case ' ':
132+
case '\\':
133+
strbuf_addstr(&buf, line);
134+
strbuf_addch(&buf, '\n');
135+
util->diffsize++;
136+
continue;
137+
case '@':
138+
if (skip_prefix(line, "@@ ", &p)) {
139+
p = strstr(p, "@@");
140+
strbuf_addstr(&buf, "@@");
141+
if (current_filename && p[2])
142+
strbuf_addf(&buf, " %s:",
143+
current_filename);
144+
if (p)
145+
strbuf_addstr(&buf, p + 2);
146+
147+
strbuf_addch(&buf, '\n');
148+
util->diffsize++;
149+
continue;
150+
}
151+
break;
152+
}
153+
154+
if (util) {
155+
string_list_append(list, buf.buf)->util = util;
156+
strbuf_reset(&buf);
157+
}
158+
util = xcalloc(sizeof(*util), 1);
159+
oidcpy(&util->oid, &null_oid);
160+
util->matching = -1;
161+
author = subject = NULL;
162+
in_header = 1;
163+
in_diff = 0;
164+
}
165+
166+
if (in_header) {
167+
if (!line[0]) {
168+
in_header = 0;
169+
strbuf_addstr(&buf, " ## Metadata ##\n");
170+
if (author)
171+
strbuf_addf(&buf, "Author: %s\n", author);
172+
strbuf_addstr(&buf, "\n ## Commit message ##\n");
173+
if (subject)
174+
strbuf_addf(&buf, "%s\n", subject);
175+
} else if (skip_prefix(line, "From: ", &p)) {
176+
author = p;
177+
} else if (skip_prefix(line, "Subject: ", &p)) {
178+
const char *q;
179+
180+
subject = p;
181+
182+
if (starts_with(p, "[PATCH") &&
183+
(q = strchr(p, ']'))) {
184+
while (isspace(*q))
185+
q++;
186+
subject = q;
187+
}
188+
}
189+
} else {
190+
strbuf_addch(&buf, ' ');
191+
strbuf_addstr(&buf, line);
192+
}
193+
}
194+
strbuf_release(&contents);
195+
196+
if (util)
197+
string_list_append(list, buf.buf)->util = util;
198+
strbuf_release(&buf);
199+
free(current_filename);
200+
201+
return 0;
202+
}
203+
39204
/*
40205
* Reads the patches into a string list, with the `util` field being populated
41206
* as struct object_id (will need to be free()d).
@@ -50,6 +215,10 @@ static int read_patches(const char *range, struct string_list *list,
50215
char *line, *current_filename = NULL;
51216
int offset, len;
52217
size_t size;
218+
const char *path;
219+
220+
if (skip_prefix(range, "mbox:", &path))
221+
return read_mbox(path, list);
53222

54223
strvec_pushl(&cp.args, "log", "--no-color", "-p", "--no-merges",
55224
"--reverse", "--date-order", "--decorate=no",
@@ -534,6 +703,9 @@ int show_range_diff(const char *range1, const char *range2,
534703
struct string_list branch1 = STRING_LIST_INIT_DUP;
535704
struct string_list branch2 = STRING_LIST_INIT_DUP;
536705

706+
if (!strcmp(range1, "mbox:-") && !strcmp(range2, "mbox:-"))
707+
res = error(_("only one mbox can be read from stdin"));
708+
537709
if (read_patches(range1, &branch1, other_arg))
538710
res = error(_("could not parse log for '%s'"), range1);
539711
if (!res && read_patches(range2, &branch2, other_arg))

0 commit comments

Comments
 (0)