Skip to content

Commit dbe2ff6

Browse files
committed
range-diff: support reading mbox files
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 currently less helpful. The Git mailing list is such a scenario: Instead of using Git to exchange commits, the patches are sent there as plain-text and no commit range can be specified to let `range-diff` consume those patches. Instead, the expectation is to download the mails, apply them locally and then use `range-diff`. This can be quite cumbersome e.g. when a suitable base revision has to be found first where the patch applies cleanly. 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. For extra convenience, interpret the filename `-` as standard input. This makes it easy to compare contributions on the mailing list with the actual commits that were integrated into Git's main branch. Example: commit=5c4003ca3f0e9ac6d3c8aa3e387ff843bd440411 mid=bdfa3845b81531863941e6a97c28eb1afa62dd2c.1489435755.git.johannes.schindelin@gmx.de curl -s https://lore.kernel.org/git/$mid/raw | git range-diff mbox:- $commit^! This addresses gitgitgadget#207 Signed-off-by: Johannes Schindelin <[email protected]>
1 parent b757478 commit dbe2ff6

File tree

3 files changed

+327
-2
lines changed

3 files changed

+327
-2
lines changed

Documentation/git-range-diff.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ There are three ways to specify the commit ranges:
3737

3838
- `<range1> <range2>`: Either commit range can be of the form
3939
`<base>..<rev>`, `<rev>^!` or `<rev>^-<n>`. See `SPECIFYING RANGES`
40-
in linkgit:gitrevisions[7] for more details.
40+
in linkgit:gitrevisions[7] for more details. Alternatively, the
41+
patches can be provided as an mbox-formatted file via `mbox:<path>`.
4142

4243
- `<rev1>...<rev2>`. This is equivalent to
4344
`<rev2>..<rev1> <rev1>..<rev2>`.

range-diff.c

Lines changed: 316 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include "userdiff.h"
1313
#include "apply.h"
1414
#include "revision.h"
15+
#include "dir.h"
1516

1617
struct patch_util {
1718
/* For the search for an exact match */
@@ -26,6 +27,293 @@ struct patch_util {
2627
struct object_id oid;
2728
};
2829

30+
static inline int strtost(char const *s, size_t *result, const char **end)
31+
{
32+
unsigned long u;
33+
char *p;
34+
35+
errno = 0;
36+
/* negative values would be accepted by strtoul */
37+
if (*s == '-')
38+
return -1;
39+
u = strtoul(s, &p, 10);
40+
if (errno || p == s)
41+
return -1;
42+
if (result)
43+
*result = u;
44+
*end = p;
45+
46+
return 0;
47+
}
48+
49+
static int parse_hunk_header(const char *p,
50+
size_t *old_count, size_t *new_count,
51+
const char **end)
52+
{
53+
size_t o = 1, n = 1;
54+
55+
if (!skip_prefix(p, "@@ -", &p) ||
56+
strtost(p, NULL, &p) ||
57+
(*p != ' ' && (*p != ',' || strtost(p + 1, &o, &p))) ||
58+
!skip_prefix(p, " +", &p) ||
59+
strtost(p, NULL, &p) ||
60+
(*p != ' ' && (*p != ',' || strtost(p + 1, &n, &p))) ||
61+
!skip_prefix(p, " @@", &p))
62+
return -1;
63+
64+
*old_count = o;
65+
*new_count = n;
66+
*end = p;
67+
68+
return 0;
69+
}
70+
71+
static inline int find_eol(const char *line, size_t size)
72+
{
73+
char *eol;
74+
75+
eol = memchr(line, '\n', size);
76+
if (!eol)
77+
return size;
78+
79+
if (eol != line && eol[-1] == '\r')
80+
eol[-1] = '\0';
81+
else
82+
*eol = '\0';
83+
84+
return eol + 1 - line;
85+
}
86+
87+
static int read_mbox(const char *path, struct string_list *list)
88+
{
89+
struct strbuf buf = STRBUF_INIT, contents = STRBUF_INIT;
90+
struct strbuf long_subject = STRBUF_INIT;
91+
struct patch_util *util = NULL;
92+
enum {
93+
MBOX_BEFORE_HEADER,
94+
MBOX_IN_HEADER,
95+
MBOX_IN_COMMIT_MESSAGE,
96+
MBOX_AFTER_TRIPLE_DASH,
97+
MBOX_IN_DIFF
98+
} state = MBOX_BEFORE_HEADER;
99+
char *line, *current_filename = NULL;
100+
int offset, len;
101+
size_t size, old_count = 0, new_count = 0;
102+
const char *author = NULL, *subject = NULL;
103+
104+
if (!strcmp(path, "-")) {
105+
if (strbuf_read(&contents, STDIN_FILENO, 0) < 0)
106+
return error_errno(_("could not read stdin"));
107+
} else if (strbuf_read_file(&contents, path, 0) < 0)
108+
return error_errno(_("could not read '%s'"), path);
109+
110+
line = contents.buf;
111+
size = contents.len;
112+
for (offset = 0; size > 0; offset += len, size -= len, line += len) {
113+
const char *p;
114+
115+
len = find_eol(line, size);
116+
117+
if (state == MBOX_BEFORE_HEADER) {
118+
if (!skip_prefix(line, "From ", &p))
119+
continue;
120+
121+
util = xcalloc(sizeof(*util), 1);
122+
if (get_oid_hex(p, &util->oid) < 0)
123+
oidcpy(&util->oid, null_oid());
124+
util->matching = -1;
125+
author = subject = NULL;
126+
127+
state = MBOX_IN_HEADER;
128+
}
129+
130+
if (starts_with(line, "diff --git ")) {
131+
struct patch patch = { 0 };
132+
struct strbuf root = STRBUF_INIT;
133+
int linenr = 0;
134+
int orig_len;
135+
136+
state = MBOX_IN_DIFF;
137+
old_count = new_count = 0;
138+
strbuf_addch(&buf, '\n');
139+
if (!util->diff_offset)
140+
util->diff_offset = buf.len;
141+
line[len - 1] = '\n';
142+
orig_len = len;
143+
len = parse_git_diff_header(&root, &linenr, 1, line,
144+
len, size, &patch);
145+
if (len < 0) {
146+
error(_("could not parse git header '%.*s'"),
147+
orig_len, line);
148+
free(util);
149+
free(current_filename);
150+
string_list_clear(list, 1);
151+
strbuf_release(&buf);
152+
strbuf_release(&contents);
153+
strbuf_release(&long_subject);
154+
return -1;
155+
}
156+
157+
if (patch.old_name)
158+
skip_prefix(patch.old_name, "a/",
159+
(const char **)&patch.old_name);
160+
if (patch.new_name)
161+
skip_prefix(patch.new_name, "b/",
162+
(const char **)&patch.new_name);
163+
164+
strbuf_addstr(&buf, " ## ");
165+
if (patch.is_new > 0)
166+
strbuf_addf(&buf, "%s (new)", patch.new_name);
167+
else if (patch.is_delete > 0)
168+
strbuf_addf(&buf, "%s (deleted)", patch.old_name);
169+
else if (patch.is_rename)
170+
strbuf_addf(&buf, "%s => %s", patch.old_name, patch.new_name);
171+
else
172+
strbuf_addstr(&buf, patch.new_name);
173+
174+
free(current_filename);
175+
if (patch.is_delete > 0)
176+
current_filename = xstrdup(patch.old_name);
177+
else
178+
current_filename = xstrdup(patch.new_name);
179+
180+
if (patch.new_mode && patch.old_mode &&
181+
patch.old_mode != patch.new_mode)
182+
strbuf_addf(&buf, " (mode change %06o => %06o)",
183+
patch.old_mode, patch.new_mode);
184+
185+
strbuf_addstr(&buf, " ##\n");
186+
util->diffsize++;
187+
} else if (state == MBOX_IN_HEADER) {
188+
if (!line[0]) {
189+
state = MBOX_IN_COMMIT_MESSAGE;
190+
/* Look for an in-body From: */
191+
if (size > 5 && skip_prefix(line + 1, "From: ", &p)) {
192+
size -= p - line;
193+
line += p - line;
194+
len = find_eol(line, size);
195+
196+
while (isspace(*p))
197+
p++;
198+
author = p;
199+
}
200+
strbuf_addstr(&buf, " ## Metadata ##\n");
201+
if (author)
202+
strbuf_addf(&buf, "Author: %s\n", author);
203+
strbuf_addstr(&buf, "\n ## Commit message ##\n");
204+
if (subject)
205+
strbuf_addf(&buf, " %s\n\n", subject);
206+
} else if (skip_prefix(line, "From: ", &p)) {
207+
while (isspace(*p))
208+
p++;
209+
author = p;
210+
} else if (skip_prefix(line, "Subject: ", &p)) {
211+
const char *q;
212+
213+
while (isspace(*p))
214+
p++;
215+
subject = p;
216+
217+
if (starts_with(p, "[PATCH") &&
218+
(q = strchr(p, ']'))) {
219+
q++;
220+
while (isspace(*q))
221+
q++;
222+
subject = q;
223+
}
224+
225+
if (len < size && line[len] == ' ') {
226+
/* handle long subject */
227+
strbuf_reset(&long_subject);
228+
strbuf_addstr(&long_subject, subject);
229+
while (len < size && line[len] == ' ') {
230+
line += len;
231+
size -= len;
232+
len = find_eol(line, size);
233+
strbuf_addstr(&long_subject, line);
234+
}
235+
subject = long_subject.buf;
236+
}
237+
}
238+
} else if (state == MBOX_IN_COMMIT_MESSAGE) {
239+
if (!*line)
240+
strbuf_addch(&buf, '\n');
241+
else if (strcmp(line, "---")) {
242+
int tabs = 0;
243+
244+
/* simulate tab expansion */
245+
while (line[tabs] == '\t')
246+
tabs++;
247+
strbuf_addf(&buf, "%*s%s\n",
248+
4 + 8 * tabs, "", line + tabs);
249+
} else {
250+
/*
251+
* Trim the trailing newline that is added
252+
* by `format-patch`.
253+
*/
254+
strbuf_trim_trailing_newline(&buf);
255+
state = MBOX_AFTER_TRIPLE_DASH;
256+
}
257+
} else if (state == MBOX_IN_DIFF) {
258+
switch (line[0]) {
259+
case '\0':
260+
continue; /* ignore empty lines after diff */
261+
case '+':
262+
case '-':
263+
case ' ':
264+
if (!old_count && !new_count)
265+
break;
266+
if (old_count && line[0] != '+')
267+
old_count--;
268+
if (new_count && line[0] != '-')
269+
new_count--;
270+
/* fallthrough */
271+
case '\\':
272+
strbuf_addstr(&buf, line);
273+
strbuf_addch(&buf, '\n');
274+
util->diffsize++;
275+
continue;
276+
case '@':
277+
if (parse_hunk_header(line, &old_count,
278+
&new_count, &p))
279+
break;
280+
281+
strbuf_addstr(&buf, "@@");
282+
if (current_filename && *p)
283+
strbuf_addf(&buf, " %s:",
284+
current_filename);
285+
strbuf_addstr(&buf, p);
286+
strbuf_addch(&buf, '\n');
287+
util->diffsize++;
288+
continue;
289+
}
290+
291+
if (util) {
292+
string_list_append(list, buf.buf)->util = util;
293+
strbuf_reset(&buf);
294+
}
295+
util = xcalloc(sizeof(*util), 1);
296+
oidcpy(&util->oid, null_oid());
297+
util->matching = -1;
298+
author = subject = NULL;
299+
state = MBOX_BEFORE_HEADER;
300+
}
301+
}
302+
strbuf_release(&contents);
303+
304+
if (util) {
305+
if (state == MBOX_IN_DIFF)
306+
string_list_append(list, buf.buf)->util = util;
307+
else
308+
free(util);
309+
}
310+
strbuf_release(&buf);
311+
strbuf_release(&long_subject);
312+
free(current_filename);
313+
314+
return 0;
315+
}
316+
29317
/*
30318
* Reads the patches into a string list, with the `util` field being populated
31319
* as struct object_id (will need to be free()d).
@@ -41,6 +329,10 @@ static int read_patches(const char *range, struct string_list *list,
41329
ssize_t len;
42330
size_t size;
43331
int ret = -1;
332+
const char *path;
333+
334+
if (skip_prefix(range, "mbox:", &path))
335+
return read_mbox(path, list);
44336

45337
strvec_pushl(&cp.args, "log", "--no-color", "-p", "--no-merges",
46338
"--reverse", "--date-order", "--decorate=no",
@@ -424,6 +716,19 @@ static void output_pair_header(struct diff_options *diffopt,
424716

425717
strbuf_addch(buf, ' ');
426718
pp_commit_easy(CMIT_FMT_ONELINE, commit, buf);
719+
} else {
720+
struct patch_util *util = b_util ? b_util : a_util;
721+
const char *needle = "\n ## Commit message ##\n";
722+
const char *p = !util || !util->patch ?
723+
NULL : strstr(util->patch, needle);
724+
if (p) {
725+
if (status == '!')
726+
strbuf_addf(buf, "%s%s", color_reset, color);
727+
728+
strbuf_addch(buf, ' ');
729+
p += strlen(needle);
730+
strbuf_add(buf, p, strchrnul(p, '\n') - p);
731+
}
427732
}
428733
strbuf_addf(buf, "%s\n", color_reset);
429734

@@ -554,6 +859,9 @@ int show_range_diff(const char *range1, const char *range2,
554859
if (range_diff_opts->left_only && range_diff_opts->right_only)
555860
res = error(_("options '%s' and '%s' cannot be used together"), "--left-only", "--right-only");
556861

862+
if (!strcmp(range1, "mbox:-") && !strcmp(range2, "mbox:-"))
863+
res = error(_("only one mbox can be read from stdin"));
864+
557865
if (!res && read_patches(range1, &branch1, range_diff_opts->other_arg))
558866
res = error(_("could not parse log for '%s'"), range1);
559867
if (!res && read_patches(range2, &branch2, range_diff_opts->other_arg))
@@ -575,10 +883,17 @@ int show_range_diff(const char *range1, const char *range2,
575883
int is_range_diff_range(const char *arg)
576884
{
577885
char *copy = xstrdup(arg); /* setup_revisions() modifies it */
578-
const char *argv[] = { "", copy, "--", NULL };
886+
const char *argv[] = { "", copy, "--", NULL }, *path;
579887
int i, positive = 0, negative = 0;
580888
struct rev_info revs;
581889

890+
if (skip_prefix(arg, "mbox:", &path)) {
891+
if (!strcmp(path, "-") || file_exists(path))
892+
return 1;
893+
error_errno(_("not an mbox: '%s'"), path);
894+
return 0;
895+
}
896+
582897
init_revisions(&revs, NULL);
583898
if (setup_revisions(3, argv, &revs, NULL) == 1) {
584899
for (i = 0; i < revs.pending.nr; i++)

t/t3206-range-diff.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,15 @@ test_expect_success 'ranges with pathspecs' '
783783
! grep "$topic_oid" actual
784784
'
785785

786+
test_expect_success 'compare range vs mbox' '
787+
git format-patch --stdout topic..mode-only-change >mbox &&
788+
git range-diff topic...mode-only-change >expect &&
789+
git range-diff mode-only-change..topic mbox:./mbox >actual &&
790+
test_cmp expect actual &&
791+
git range-diff mode-only-change..topic mbox:- <mbox >actual &&
792+
test_cmp expect actual
793+
'
794+
786795
test_expect_success 'submodule changes are shown irrespective of diff.submodule' '
787796
git init sub-repo &&
788797
test_commit -C sub-repo sub-first &&

0 commit comments

Comments
 (0)