Skip to content

Commit

Permalink
WIP range-diff: support reading mbox files
Browse files Browse the repository at this point in the history
TODO:
	- document
	- 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 <johannes.schindelin@gmx.de>
  • Loading branch information
dscho committed Dec 3, 2021
1 parent abe6bb3 commit 28457ee
Show file tree
Hide file tree
Showing 2 changed files with 212 additions and 2 deletions.
4 changes: 2 additions & 2 deletions builtin/range-diff.c
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@ int cmd_range_diff(int argc, const char **argv, const char *prefix)
diffopt.use_color = 1;

if (argc == 2) {
if (!is_range_diff_range(argv[0]))
if (!starts_with(argv[0], "mbox:") && !is_range_diff_range(argv[0]))
die(_("not a commit range: '%s'"), argv[0]);
strbuf_addstr(&range1, argv[0]);

if (!is_range_diff_range(argv[1]))
if (!starts_with(argv[1], "mbox:") && !is_range_diff_range(argv[1]))
die(_("not a commit range: '%s'"), argv[1]);
strbuf_addstr(&range2, argv[1]);
} else if (argc == 3) {
Expand Down
210 changes: 210 additions & 0 deletions range-diff.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,196 @@ struct patch_util {
struct object_id oid;
};

static int read_mbox(const char *path, struct string_list *list)
{
struct strbuf buf = STRBUF_INIT, contents = STRBUF_INIT;
struct patch_util *util = NULL;
enum {
MBOX_BEFORE_HEADER,
MBOX_IN_HEADER,
MBOX_IN_COMMIT_MESSAGE,
MBOX_AFTER_TRIPLE_DASH,
MBOX_IN_DIFF
} state = MBOX_BEFORE_HEADER;
char *line, *current_filename = NULL;
int offset, len;
size_t size;
const char *author = NULL, *subject = NULL;

if (!strcmp(path, "-")) {
if (strbuf_read(&contents, STDIN_FILENO, 0) < 0)
return error_errno(_("could not read stdin"));
} else if (strbuf_read_file(&contents, path, 0) < 0)
return error_errno(_("could not read '%s'"), path);

line = contents.buf;
size = contents.len;
for (offset = 0; size > 0; offset += len, size -= len, line += len) {
char *eol;
const char *p;

eol = memchr(line, '\n', size);
if (!eol)
len = eol - line;
else {
len = eol + 1 - line;
if (eol != line && eol[-1] == '\r')
eol[-1] = '\0';
else
*eol = '\0';
}

if (state == MBOX_BEFORE_HEADER) {
if (starts_with(line, "From "))
state = MBOX_IN_HEADER;
else
continue;
}

if (starts_with(line, "diff --git ")) {
struct patch patch = { 0 };
struct strbuf root = STRBUF_INIT;
int linenr = 0;
int orig_len;

state = MBOX_IN_DIFF;
strbuf_addch(&buf, '\n');
if (!util) {
util = xcalloc(sizeof(*util), 1);
oidcpy(&util->oid, null_oid());
util->matching = -1;
author = subject = NULL;
}
if (!util->diff_offset)
util->diff_offset = buf.len;
line[len - 1] = '\n';
orig_len = len;
len = parse_git_diff_header(&root, &linenr, 0, line,
len, size, &patch);
if (len < 0) {
error(_("could not parse git header '%.*s'"),
orig_len, line);
free(util);
free(current_filename);
string_list_clear(list, 1);
strbuf_release(&buf);
strbuf_release(&contents);
return -1;
}

if (patch.old_name)
skip_prefix(patch.old_name, "a/",
(const char **)&patch.old_name);
if (patch.new_name)
skip_prefix(patch.new_name, "b/",
(const char **)&patch.new_name);

strbuf_addstr(&buf, " ## ");
if (patch.is_new > 0)
strbuf_addf(&buf, "%s (new)", patch.new_name);
else if (patch.is_delete > 0)
strbuf_addf(&buf, "%s (deleted)", patch.old_name);
else if (patch.is_rename)
strbuf_addf(&buf, "%s => %s", patch.old_name, patch.new_name);
else
strbuf_addstr(&buf, patch.new_name);

free(current_filename);
if (patch.is_delete > 0)
current_filename = xstrdup(patch.old_name);
else
current_filename = xstrdup(patch.new_name);

if (patch.new_mode && patch.old_mode &&
patch.old_mode != patch.new_mode)
strbuf_addf(&buf, " (mode change %06o => %06o)",
patch.old_mode, patch.new_mode);

strbuf_addstr(&buf, " ##\n");
util->diffsize++;
} else if (state == MBOX_IN_HEADER) {
if (!line[0]) {
state = MBOX_IN_COMMIT_MESSAGE;
strbuf_addstr(&buf, " ## Metadata ##\n");
if (author)
strbuf_addf(&buf, "Author: %s\n", author);
strbuf_addstr(&buf, "\n ## Commit message ##\n");
if (subject)
strbuf_addf(&buf, " %s\n\n", subject);
} else if (skip_prefix(line, "From: ", &p)) {
author = p;
} else if (skip_prefix(line, "Subject: ", &p)) {
const char *q;

subject = p;

if (starts_with(p, "[PATCH") &&
(q = strchr(p, ']'))) {
q++;
while (isspace(*q))
q++;
subject = q;
}
}
} else if (state == MBOX_IN_COMMIT_MESSAGE) {
if (!strcmp(line, "---"))
state = MBOX_AFTER_TRIPLE_DASH;
else
strbuf_addf(&buf, " %s\n", line);
} else if (state == MBOX_IN_DIFF) {
switch (line[0]) {
case '\0':
continue; /* ignore empty lines after diff */
case '+':
case '-':
case ' ':
case '\\':
strbuf_addstr(&buf, line);
strbuf_addch(&buf, '\n');
util->diffsize++;
continue;
case '@':
if (skip_prefix(line, "@@ ", &p)) {
p = strstr(p, "@@");
strbuf_addstr(&buf, "@@");
if (current_filename && p[2])
strbuf_addf(&buf, " %s:",
current_filename);
if (p)
strbuf_addstr(&buf, p + 2);

strbuf_addch(&buf, '\n');
util->diffsize++;
continue;
}
break;
}

if (util) {
string_list_append(list, buf.buf)->util = util;
strbuf_reset(&buf);
}
util = xcalloc(sizeof(*util), 1);
oidcpy(&util->oid, null_oid());
util->matching = -1;
author = subject = NULL;
state = MBOX_BEFORE_HEADER;
}
}
strbuf_release(&contents);

if (util) {
if (state == MBOX_IN_DIFF)
string_list_append(list, buf.buf)->util = util;
else
free(util);
}
strbuf_release(&buf);
free(current_filename);

return 0;
}

/*
* Reads the patches into a string list, with the `util` field being populated
* as struct object_id (will need to be free()d).
Expand All @@ -40,6 +230,10 @@ static int read_patches(const char *range, struct string_list *list,
char *line, *current_filename = NULL;
ssize_t len;
size_t size;
const char *path;

if (skip_prefix(range, "mbox:", &path))
return read_mbox(path, list);

strvec_pushl(&cp.args, "log", "--no-color", "-p", "--no-merges",
"--reverse", "--date-order", "--decorate=no",
Expand Down Expand Up @@ -428,6 +622,19 @@ static void output_pair_header(struct diff_options *diffopt,

strbuf_addch(buf, ' ');
pp_commit_easy(CMIT_FMT_ONELINE, commit, buf);
} else {
struct patch_util *util = b_util ? b_util : a_util;
const char *needle = "\n ## Commit message ##\n";
const char *p = !util || !util->patch ?
NULL : strstr(util->patch, needle);
if (p) {
if (status == '!')
strbuf_addf(buf, "%s%s", color_reset, color);

strbuf_addch(buf, ' ');
p += strlen(needle);
strbuf_add(buf, p, strchrnul(p, '\n') - p);
}
}
strbuf_addf(buf, "%s\n", color_reset);

Expand Down Expand Up @@ -558,6 +765,9 @@ int show_range_diff(const char *range1, const char *range2,
if (range_diff_opts->left_only && range_diff_opts->right_only)
res = error(_("--left-only and --right-only are mutually exclusive"));

if (!strcmp(range1, "mbox:-") && !strcmp(range2, "mbox:-"))
res = error(_("only one mbox can be read from stdin"));

if (!res && read_patches(range1, &branch1, range_diff_opts->other_arg))
res = error(_("could not parse log for '%s'"), range1);
if (!res && read_patches(range2, &branch2, range_diff_opts->other_arg))
Expand Down

0 comments on commit 28457ee

Please sign in to comment.