diff --git a/Makefile b/Makefile index cf504963c2..8f4432ae57 100644 --- a/Makefile +++ b/Makefile @@ -1334,10 +1334,11 @@ THIRD_PARTY_SOURCES += compat/regex/% THIRD_PARTY_SOURCES += sha1collisiondetection/% THIRD_PARTY_SOURCES += sha1dc/% -UNIT_TEST_PROGRAMS += t-mem-pool -UNIT_TEST_PROGRAMS += t-strbuf UNIT_TEST_PROGRAMS += t-ctype +UNIT_TEST_PROGRAMS += t-mem-pool UNIT_TEST_PROGRAMS += t-prio-queue +UNIT_TEST_PROGRAMS += t-strbuf +UNIT_TEST_PROGRAMS += t-trailer UNIT_TEST_PROGS = $(patsubst %,$(UNIT_TEST_BIN)/%$X,$(UNIT_TEST_PROGRAMS)) UNIT_TEST_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TEST_PROGRAMS)) UNIT_TEST_OBJS += $(UNIT_TEST_DIR)/test-lib.o diff --git a/builtin/interpret-trailers.c b/builtin/interpret-trailers.c index 8768bfea3c..1d969494cf 100644 --- a/builtin/interpret-trailers.c +++ b/builtin/interpret-trailers.c @@ -141,7 +141,7 @@ static void interpret_trailers(const struct process_trailer_options *opts, LIST_HEAD(head); struct strbuf sb = STRBUF_INIT; struct strbuf trailer_block = STRBUF_INIT; - struct trailer_info info; + struct trailer_info *info; FILE *outfile = stdout; trailer_config_init(); @@ -151,13 +151,13 @@ static void interpret_trailers(const struct process_trailer_options *opts, if (opts->in_place) outfile = create_in_place_tempfile(file); - parse_trailers(opts, &info, sb.buf, &head); + info = parse_trailers(opts, sb.buf, &head); /* Print the lines before the trailers */ if (!opts->only_trailers) - fwrite(sb.buf, 1, info.trailer_block_start, outfile); + fwrite(sb.buf, 1, trailer_block_start(info), outfile); - if (!opts->only_trailers && !info.blank_line_before_trailer) + if (!opts->only_trailers && !blank_line_before_trailer_block(info)) fprintf(outfile, "\n"); @@ -178,8 +178,8 @@ static void interpret_trailers(const struct process_trailer_options *opts, /* Print the lines after the trailers as is */ if (!opts->only_trailers) - fwrite(sb.buf + info.trailer_block_end, 1, sb.len - info.trailer_block_end, outfile); - trailer_info_release(&info); + fwrite(sb.buf + trailer_block_end(info), 1, sb.len - trailer_block_end(info), outfile); + trailer_info_release(info); if (opts->in_place) if (rename_tempfile(&trailers_tempfile, file)) diff --git a/sequencer.c b/sequencer.c index dbd60d79b9..aa2a239835 100644 --- a/sequencer.c +++ b/sequencer.c @@ -359,35 +359,32 @@ static const char *get_todo_path(const struct replay_opts *opts) static int has_conforming_footer(struct strbuf *sb, struct strbuf *sob, size_t ignore_footer) { - struct process_trailer_options opts = PROCESS_TRAILER_OPTIONS_INIT; - struct trailer_info info; - size_t i; + struct trailer_iterator iter; + size_t i = 0; int found_sob = 0, found_sob_last = 0; char saved_char; - opts.no_divider = 1; - if (ignore_footer) { saved_char = sb->buf[sb->len - ignore_footer]; sb->buf[sb->len - ignore_footer] = '\0'; } - trailer_info_get(&opts, sb->buf, &info); + trailer_iterator_init(&iter, sb->buf); if (ignore_footer) sb->buf[sb->len - ignore_footer] = saved_char; - if (info.trailer_block_start == info.trailer_block_end) + while (trailer_iterator_advance(&iter)) { + i++; + if (sob && !strncmp(iter.raw, sob->buf, sob->len)) + found_sob = i; + } + trailer_iterator_release(&iter); + + if (!i) return 0; - for (i = 0; i < info.trailer_nr; i++) - if (sob && !strncmp(info.trailers[i], sob->buf, sob->len)) { - found_sob = 1; - if (i == info.trailer_nr - 1) - found_sob_last = 1; - } - - trailer_info_release(&info); + found_sob_last = (int)i == found_sob; if (found_sob_last) return 3; diff --git a/t/unit-tests/t-trailer.c b/t/unit-tests/t-trailer.c new file mode 100644 index 0000000000..2ecca359d9 --- /dev/null +++ b/t/unit-tests/t-trailer.c @@ -0,0 +1,315 @@ +#include "test-lib.h" +#include "trailer.h" + +struct contents { + const char *raw; + const char *key; + const char *val; +}; + +static void t_trailer_iterator(const char *msg, size_t num_expected, + struct contents *contents) +{ + struct trailer_iterator iter; + size_t i = 0; + + trailer_iterator_init(&iter, msg); + while (trailer_iterator_advance(&iter)) { + if (num_expected) { + check_str(iter.raw, contents[i].raw); + check_str(iter.key.buf, contents[i].key); + check_str(iter.val.buf, contents[i].val); + } + i++; + } + trailer_iterator_release(&iter); + + check_uint(i, ==, num_expected); +} + +static void run_t_trailer_iterator(void) +{ + + static struct test_cases { + const char *name; + const char *msg; + size_t num_expected; + struct contents contents[10]; + } tc[] = { + { + "empty input", + "", + 0, + {{0}}, + }, + { + "no newline at beginning", + "Fixes: x\n" + "Acked-by: x\n" + "Reviewed-by: x\n", + 0, + {{0}}, + }, + { + "newline at beginning", + "\n" + "Fixes: x\n" + "Acked-by: x\n" + "Reviewed-by: x\n", + 3, + { + { + .raw = "Fixes: x\n", + .key = "Fixes", + .val = "x", + }, + { + .raw = "Acked-by: x\n", + .key = "Acked-by", + .val = "x", + }, + { + .raw = "Reviewed-by: x\n", + .key = "Reviewed-by", + .val = "x", + }, + { + 0 + }, + }, + }, + { + "without body text", + "subject: foo bar\n" + "\n" + "Fixes: x\n" + "Acked-by: x\n" + "Reviewed-by: x\n", + 3, + { + { + .raw = "Fixes: x\n", + .key = "Fixes", + .val = "x", + }, + { + .raw = "Acked-by: x\n", + .key = "Acked-by", + .val = "x", + }, + { + .raw = "Reviewed-by: x\n", + .key = "Reviewed-by", + .val = "x", + }, + { + 0 + }, + }, + }, + { + "with body text, without divider", + "my subject\n" + "\n" + "my body which is long\n" + "and contains some special\n" + "chars like : = ? !\n" + "hello\n" + "\n" + "Fixes: x\n" + "Acked-by: x\n" + "Reviewed-by: x\n" + "Signed-off-by: x\n", + 4, + { + { + .raw = "Fixes: x\n", + .key = "Fixes", + .val = "x", + }, + { + .raw = "Acked-by: x\n", + .key = "Acked-by", + .val = "x", + }, + { + .raw = "Reviewed-by: x\n", + .key = "Reviewed-by", + .val = "x", + }, + { + .raw = "Signed-off-by: x\n", + .key = "Signed-off-by", + .val = "x", + }, + { + 0 + }, + }, + }, + { + "with body text, without divider (second trailer block)", + "my subject\n" + "\n" + "my body which is long\n" + "and contains some special\n" + "chars like : = ? !\n" + "hello\n" + "\n" + "Fixes: x\n" + "Acked-by: x\n" + "Reviewed-by: x\n" + "Signed-off-by: x\n" + "\n" + /* + * Because this is the last trailer block, it takes + * precedence over the first one encountered above. + */ + "Helped-by: x\n" + "Signed-off-by: x\n", + 2, + { + { + .raw = "Helped-by: x\n", + .key = "Helped-by", + .val = "x", + }, + { + .raw = "Signed-off-by: x\n", + .key = "Signed-off-by", + .val = "x", + }, + { + 0 + }, + }, + }, + { + "with body text, with divider", + "my subject\n" + "\n" + "my body which is long\n" + "and contains some special\n" + "chars like : = ? !\n" + "hello\n" + "\n" + "---\n" + "\n" + /* + * This trailer still counts because the iterator + * always ignores the divider. + */ + "Signed-off-by: x\n", + 1, + { + { + .raw = "Signed-off-by: x\n", + .key = "Signed-off-by", + .val = "x", + }, + { + 0 + }, + }, + }, + { + "with non-trailer lines in trailer block", + "subject: foo bar\n" + "\n" + /* + * Even though this trailer block has a non-trailer line + * in it, it's still a valid trailer block because it's + * at least 25% trailers and is Git-generated (see + * git_generated_prefixes[] in trailer.c). + */ + "not a trailer line\n" + "not a trailer line\n" + "not a trailer line\n" + "Signed-off-by: x\n", + /* + * Even though there is only really 1 real "trailer" + * (Signed-off-by), we still have 4 trailer objects + * because we still want to iterate through the entire + * block. + */ + 4, + { + { + .raw = "not a trailer line\n", + .key = "not a trailer line", + .val = "", + }, + { + .raw = "not a trailer line\n", + .key = "not a trailer line", + .val = "", + }, + { + .raw = "not a trailer line\n", + .key = "not a trailer line", + .val = "", + }, + { + .raw = "Signed-off-by: x\n", + .key = "Signed-off-by", + .val = "x", + }, + { + 0 + }, + }, + }, + { + "with non-trailer lines (one too many) in trailer block", + "subject: foo bar\n" + "\n" + /* + * This block has only 20% trailers, so it's below the + * 25% threshold. + */ + "not a trailer line\n" + "not a trailer line\n" + "not a trailer line\n" + "not a trailer line\n" + "Signed-off-by: x\n", + 0, + {{0}}, + }, + { + "with non-trailer lines (only 1) in trailer block, but no Git-generated trailers", + "subject: foo bar\n" + "\n" + /* + * This block has only 1 non-trailer out of 10 (IOW, 90% + * trailers) but is not considered a trailer block + * because the 25% threshold only applies to cases where + * there was a Git-generated trailer. + */ + "Reviewed-by: x\n" + "Reviewed-by: x\n" + "Reviewed-by: x\n" + "Helped-by: x\n" + "Helped-by: x\n" + "Helped-by: x\n" + "Acked-by: x\n" + "Acked-by: x\n" + "Acked-by: x\n" + "not a trailer line\n", + 0, + {{0}}, + }, + }; + + for (int i = 0; i < sizeof(tc) / sizeof(tc[0]); i++) { + TEST(t_trailer_iterator(tc[i].msg, + tc[i].num_expected, + tc[i].contents), + "%s", tc[i].name); + } +} + +int cmd_main(int argc, const char **argv) +{ + run_t_trailer_iterator(); + return test_done(); +} diff --git a/trailer.c b/trailer.c index 724cb78be5..2bcb9ba8f7 100644 --- a/trailer.c +++ b/trailer.c @@ -11,6 +11,27 @@ * Copyright (c) 2013, 2014 Christian Couder */ +struct trailer_info { + /* + * True if there is a blank line before the location pointed to by + * trailer_block_start. + */ + int blank_line_before_trailer; + + /* + * Offsets to the trailer block start and end positions in the input + * string. If no trailer block is found, these are both set to the + * "true" end of the input (find_end_of_log_message()). + */ + size_t trailer_block_start, trailer_block_end; + + /* + * Array of trailers found. + */ + char **trailers; + size_t trailer_nr; +}; + struct conf_info { char *name; char *key; @@ -952,58 +973,16 @@ static void unfold_value(struct strbuf *val) strbuf_release(&out); } -/* - * Parse trailers in "str", populating the trailer info and "head" - * linked list structure. - */ -void parse_trailers(const struct process_trailer_options *opts, - struct trailer_info *info, - const char *str, - struct list_head *head) +static struct trailer_info *trailer_info_new(void) { - struct strbuf tok = STRBUF_INIT; - struct strbuf val = STRBUF_INIT; - size_t i; - - trailer_info_get(opts, str, info); - - for (i = 0; i < info->trailer_nr; i++) { - int separator_pos; - char *trailer = info->trailers[i]; - if (starts_with(trailer, comment_line_str)) - continue; - separator_pos = find_separator(trailer, separators); - if (separator_pos >= 1) { - parse_trailer(&tok, &val, NULL, trailer, - separator_pos); - if (opts->unfold) - unfold_value(&val); - add_trailer_item(head, - strbuf_detach(&tok, NULL), - strbuf_detach(&val, NULL)); - } else if (!opts->only_trailers) { - strbuf_addstr(&val, trailer); - strbuf_strip_suffix(&val, "\n"); - add_trailer_item(head, - NULL, - strbuf_detach(&val, NULL)); - } - } + struct trailer_info *info = xcalloc(1, sizeof(*info)); + return info; } -void free_trailers(struct list_head *trailers) -{ - struct list_head *pos, *p; - list_for_each_safe(pos, p, trailers) { - list_del(pos); - free_trailer_item(list_entry(pos, struct trailer_item, list)); - } -} - -void trailer_info_get(const struct process_trailer_options *opts, - const char *str, - struct trailer_info *info) +static struct trailer_info *trailer_info_get(const struct process_trailer_options *opts, + const char *str) { + struct trailer_info *info = trailer_info_new(); size_t end_of_log_message = 0, trailer_block_start = 0; struct strbuf **trailer_lines, **ptr; char **trailer_strings = NULL; @@ -1042,6 +1021,73 @@ void trailer_info_get(const struct process_trailer_options *opts, info->trailer_block_end = end_of_log_message; info->trailers = trailer_strings; info->trailer_nr = nr; + + return info; +} + +/* + * Parse trailers in "str", populating the trailer info and "trailer_objects" + * linked list structure. + */ +struct trailer_info *parse_trailers(const struct process_trailer_options *opts, + const char *str, + struct list_head *trailer_objects) +{ + struct trailer_info *info; + struct strbuf tok = STRBUF_INIT; + struct strbuf val = STRBUF_INIT; + size_t i; + + info = trailer_info_get(opts, str); + + for (i = 0; i < info->trailer_nr; i++) { + int separator_pos; + char *trailer = info->trailers[i]; + if (starts_with(trailer, comment_line_str)) + continue; + separator_pos = find_separator(trailer, separators); + if (separator_pos >= 1) { + parse_trailer(&tok, &val, NULL, trailer, + separator_pos); + if (opts->unfold) + unfold_value(&val); + add_trailer_item(trailer_objects, + strbuf_detach(&tok, NULL), + strbuf_detach(&val, NULL)); + } else if (!opts->only_trailers) { + strbuf_addstr(&val, trailer); + strbuf_strip_suffix(&val, "\n"); + add_trailer_item(trailer_objects, + NULL, + strbuf_detach(&val, NULL)); + } + } + + return info; +} + +void free_trailers(struct list_head *trailers) +{ + struct list_head *pos, *p; + list_for_each_safe(pos, p, trailers) { + list_del(pos); + free_trailer_item(list_entry(pos, struct trailer_item, list)); + } +} + +size_t trailer_block_start(struct trailer_info *info) +{ + return info->trailer_block_start; +} + +size_t trailer_block_end(struct trailer_info *info) +{ + return info->trailer_block_end; +} + +int blank_line_before_trailer_block(struct trailer_info *info) +{ + return info->blank_line_before_trailer; } void trailer_info_release(struct trailer_info *info) @@ -1050,6 +1096,7 @@ void trailer_info_release(struct trailer_info *info) for (i = 0; i < info->trailer_nr; i++) free(info->trailers[i]); free(info->trailers); + free(info); } void format_trailers(const struct process_trailer_options *opts, @@ -1117,21 +1164,19 @@ void format_trailers_from_commit(const struct process_trailer_options *opts, struct strbuf *out) { LIST_HEAD(trailer_objects); - struct trailer_info info; - - parse_trailers(opts, &info, msg, &trailer_objects); + struct trailer_info *info = parse_trailers(opts, msg, &trailer_objects); /* If we want the whole block untouched, we can take the fast path. */ if (!opts->only_trailers && !opts->unfold && !opts->filter && !opts->separator && !opts->key_only && !opts->value_only && !opts->key_value_separator) { - strbuf_add(out, msg + info.trailer_block_start, - info.trailer_block_end - info.trailer_block_start); + strbuf_add(out, msg + info->trailer_block_start, + info->trailer_block_end - info->trailer_block_start); } else format_trailers(opts, &trailer_objects, out); free_trailers(&trailer_objects); - trailer_info_release(&info); + trailer_info_release(info); } void trailer_iterator_init(struct trailer_iterator *iter, const char *msg) @@ -1140,23 +1185,21 @@ void trailer_iterator_init(struct trailer_iterator *iter, const char *msg) strbuf_init(&iter->key, 0); strbuf_init(&iter->val, 0); opts.no_divider = 1; - trailer_info_get(&opts, msg, &iter->internal.info); + iter->internal.info = trailer_info_get(&opts, msg); iter->internal.cur = 0; } int trailer_iterator_advance(struct trailer_iterator *iter) { - while (iter->internal.cur < iter->internal.info.trailer_nr) { - char *trailer = iter->internal.info.trailers[iter->internal.cur++]; - int separator_pos = find_separator(trailer, separators); - - if (separator_pos < 1) - continue; /* not a real trailer */ + if (iter->internal.cur < iter->internal.info->trailer_nr) { + char *line = iter->internal.info->trailers[iter->internal.cur++]; + int separator_pos = find_separator(line, separators); + iter->raw = line; strbuf_reset(&iter->key); strbuf_reset(&iter->val); parse_trailer(&iter->key, &iter->val, NULL, - trailer, separator_pos); + line, separator_pos); /* Always unfold values during iteration. */ unfold_value(&iter->val); return 1; @@ -1166,7 +1209,7 @@ int trailer_iterator_advance(struct trailer_iterator *iter) void trailer_iterator_release(struct trailer_iterator *iter) { - trailer_info_release(&iter->internal.info); + trailer_info_release(iter->internal.info); strbuf_release(&iter->val); strbuf_release(&iter->key); } diff --git a/trailer.h b/trailer.h index c364405267..6eb53df155 100644 --- a/trailer.h +++ b/trailer.h @@ -4,6 +4,7 @@ #include "list.h" #include "strbuf.h" +struct trailer_info; struct strvec; enum trailer_where { @@ -31,27 +32,6 @@ int trailer_set_where(enum trailer_where *item, const char *value); int trailer_set_if_exists(enum trailer_if_exists *item, const char *value); int trailer_set_if_missing(enum trailer_if_missing *item, const char *value); -struct trailer_info { - /* - * True if there is a blank line before the location pointed to by - * trailer_block_start. - */ - int blank_line_before_trailer; - - /* - * Offsets to the trailer block start and end positions in the input - * string. If no trailer block is found, these are both set to the - * "true" end of the input (find_end_of_log_message()). - */ - size_t trailer_block_start, trailer_block_end; - - /* - * Array of trailers found. - */ - char **trailers; - size_t trailer_nr; -}; - /* * A list that represents newly-added trailers, such as those provided * with the --trailer command line option of git-interpret-trailers. @@ -91,15 +71,63 @@ void parse_trailers_from_command_line_args(struct list_head *arg_head, void process_trailers_lists(struct list_head *head, struct list_head *arg_head); -void parse_trailers(const struct process_trailer_options *, - struct trailer_info *, - const char *str, - struct list_head *head); +/* + * Given some input string "str", return a pointer to an opaque trailer_info + * structure. Also populate the trailer_objects list with parsed trailer + * objects. Internally this calls trailer_info_get() to get the opaque pointer, + * but does some extra work to populate the trailer_objects linked list. + * + * The opaque trailer_info pointer can be used to check the position of the + * trailer block as offsets relative to the beginning of "str" in + * trailer_block_start() and trailer_block_end(). + * blank_line_before_trailer_block() returns 1 if there is a blank line just + * before the trailer block. All of these functions are useful for preserving + * the input before and after the trailer block, if we were to write out the + * original input (but with the trailer block itself modified); see + * builtin/interpret-trailers.c for an example. + * + * For iterating through the parsed trailer block (if you don't care about the + * position of the trailer block itself in the context of the larger string text + * from which it was parsed), please see trailer_iterator_init() which uses the + * trailer_info struct internally. + * + * Lastly, callers should call trailer_info_release() when they are done using + * the opaque pointer. + * + * NOTE: Callers should treat both trailer_info and trailer_objects as + * read-only items, because there is some overlap between the two (trailer_info + * has "char **trailers" string array, and trailer_objects will have the same + * data but as a linked list of trailer_item objects). This API does not perform + * any synchronization between the two. In the future we should be able to + * reduce the duplication and use just the linked list. + */ +struct trailer_info *parse_trailers(const struct process_trailer_options *, + const char *str, + struct list_head *trailer_objects); -void trailer_info_get(const struct process_trailer_options *, - const char *str, - struct trailer_info *); +/* + * Return the offset of the start of the trailer block. That is, 0 is the start + * of the input ("str" in parse_trailers()) and some other positive number + * indicates how many bytes we have to skip over before we get to the beginning + * of the trailer block. + */ +size_t trailer_block_start(struct trailer_info *); +/* + * Return the end of the trailer block, again relative to the start of the + * input. + */ +size_t trailer_block_end(struct trailer_info *); + +/* + * Return 1 if the trailer block had an extra newline (blank line) just before + * it. + */ +int blank_line_before_trailer_block(struct trailer_info *); + +/* + * Free trailer_info struct. + */ void trailer_info_release(struct trailer_info *info); void trailer_config_init(void); @@ -127,12 +155,19 @@ void format_trailers_from_commit(const struct process_trailer_options *, * trailer_iterator_release(&iter); */ struct trailer_iterator { + /* + * Raw line (e.g., "foo: bar baz") before being parsed as a trailer + * key/val pair as part of a trailer block (as the "key" and "val" + * fields below). If a line fails to parse as a trailer, then the "key" + * will be the entire line and "val" will be the empty string. + */ + const char *raw; struct strbuf key; struct strbuf val; /* private */ struct { - struct trailer_info info; + struct trailer_info *info; size_t cur; } internal; };