From 704b59099e4d3f47d547b452d10979b328533cca Mon Sep 17 00:00:00 2001 From: Linus Arver Date: Thu, 2 May 2024 04:54:18 +0000 Subject: [PATCH 01/10] Makefile: sort UNIT_TEST_PROGRAMS Signed-off-by: Linus Arver Signed-off-by: Junio C Hamano --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 4e255c81f2..d3a3f16f07 100644 --- a/Makefile +++ b/Makefile @@ -1343,10 +1343,10 @@ THIRD_PARTY_SOURCES += sha1collisiondetection/% THIRD_PARTY_SOURCES += sha1dc/% UNIT_TEST_PROGRAMS += t-basic -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_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 From 56b04883f04944240368f28a056d5aae59f3f52f Mon Sep 17 00:00:00 2001 From: Linus Arver Date: Thu, 2 May 2024 04:54:19 +0000 Subject: [PATCH 02/10] trailer: add unit tests for trailer iterator Test the number of trailers found by the iterator (to be more precise, the parsing mechanism which the iterator just walks over) when given some arbitrary log message. We test the iterator because it is a public interface function exposed by the trailer API (we generally don't want to test internal implementation details which are, unlike the API, subject to drastic changes). Signed-off-by: Linus Arver Signed-off-by: Junio C Hamano --- Makefile | 1 + t/unit-tests/t-trailer.c | 174 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 t/unit-tests/t-trailer.c diff --git a/Makefile b/Makefile index d3a3f16f07..5418ddd03b 100644 --- a/Makefile +++ b/Makefile @@ -1347,6 +1347,7 @@ 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/t/unit-tests/t-trailer.c b/t/unit-tests/t-trailer.c new file mode 100644 index 0000000000..c1f897235c --- /dev/null +++ b/t/unit-tests/t-trailer.c @@ -0,0 +1,174 @@ +#include "test-lib.h" +#include "trailer.h" + +static void t_trailer_iterator(const char *msg, size_t num_expected_trailers) +{ + struct trailer_iterator iter; + size_t i = 0; + + trailer_iterator_init(&iter, msg); + while (trailer_iterator_advance(&iter)) + i++; + trailer_iterator_release(&iter); + + check_uint(i, ==, num_expected_trailers); +} + +static void run_t_trailer_iterator(void) +{ + static struct test_cases { + const char *name; + const char *msg; + size_t num_expected_trailers; + } tc[] = { + { + "empty input", + "", + 0 + }, + { + "no newline at beginning", + "Fixes: x\n" + "Acked-by: x\n" + "Reviewed-by: x\n", + 0 + }, + { + "newline at beginning", + "\n" + "Fixes: x\n" + "Acked-by: x\n" + "Reviewed-by: x\n", + 3 + }, + { + "without body text", + "subject: foo bar\n" + "\n" + "Fixes: x\n" + "Acked-by: x\n" + "Reviewed-by: x\n", + 3 + }, + { + "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 + }, + { + "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 + }, + { + "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 + }, + { + "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", + 1 + }, + { + "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 + }, + { + "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 + }, + }; + + for (int i = 0; i < sizeof(tc) / sizeof(tc[0]); i++) { + TEST(t_trailer_iterator(tc[i].msg, + tc[i].num_expected_trailers), + "%s", tc[i].name); + } +} + +int cmd_main(int argc, const char **argv) +{ + run_t_trailer_iterator(); + return test_done(); +} From 3be65e6ee2f585a0aad0363c8ce7d966a6f8c2b3 Mon Sep 17 00:00:00 2001 From: Linus Arver Date: Thu, 2 May 2024 04:54:20 +0000 Subject: [PATCH 03/10] trailer: teach iterator about non-trailer lines Previously the iterator did not iterate over non-trailer lines. This was somewhat unfortunate, because trailer blocks could have non-trailer lines in them since 146245063e (trailer: allow non-trailers in trailer block, 2016-10-21), which was before the iterator was created in f0939a0eb1 (trailer: add interface for iterating over commit trailers, 2020-09-27). So if trailer API users wanted to iterate over all lines in a trailer block (including non-trailer lines), they could not use the iterator and were forced to use the lower-level trailer_info struct directly (which provides a raw string array that includes all lines in the trailer block). Change the iterator's behavior so that we also iterate over non-trailer lines, instead of skipping over them. The new "raw" member of the iterator allows API users to access previously inaccessible non-trailer lines. Reword the variable "trailer" to just "line" because this variable can now hold both trailer lines _and_ non-trailer lines. The new "raw" member is important because anyone currently not using the iterator is using trailer_info's raw string array directly to access lines to check what the combined key + value looks like. If we didn't provide a "raw" member here, iterator users would have to re-construct the unparsed line by concatenating the key and value back together again Signed-off-by: Junio C Hamano --- t/unit-tests/t-trailer.c | 16 +++++++++++----- trailer.c | 12 +++++------- trailer.h | 7 +++++++ 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/t/unit-tests/t-trailer.c b/t/unit-tests/t-trailer.c index c1f897235c..4f640d2a4b 100644 --- a/t/unit-tests/t-trailer.c +++ b/t/unit-tests/t-trailer.c @@ -1,7 +1,7 @@ #include "test-lib.h" #include "trailer.h" -static void t_trailer_iterator(const char *msg, size_t num_expected_trailers) +static void t_trailer_iterator(const char *msg, size_t num_expected) { struct trailer_iterator iter; size_t i = 0; @@ -11,7 +11,7 @@ static void t_trailer_iterator(const char *msg, size_t num_expected_trailers) i++; trailer_iterator_release(&iter); - check_uint(i, ==, num_expected_trailers); + check_uint(i, ==, num_expected); } static void run_t_trailer_iterator(void) @@ -19,7 +19,7 @@ static void run_t_trailer_iterator(void) static struct test_cases { const char *name; const char *msg; - size_t num_expected_trailers; + size_t num_expected; } tc[] = { { "empty input", @@ -119,7 +119,13 @@ static void run_t_trailer_iterator(void) "not a trailer line\n" "not a trailer line\n" "Signed-off-by: x\n", - 1 + /* + * 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 }, { "with non-trailer lines (one too many) in trailer block", @@ -162,7 +168,7 @@ static void run_t_trailer_iterator(void) for (int i = 0; i < sizeof(tc) / sizeof(tc[0]); i++) { TEST(t_trailer_iterator(tc[i].msg, - tc[i].num_expected_trailers), + tc[i].num_expected), "%s", tc[i].name); } } diff --git a/trailer.c b/trailer.c index 3e4dab9c06..4700c44144 100644 --- a/trailer.c +++ b/trailer.c @@ -1146,17 +1146,15 @@ void trailer_iterator_init(struct trailer_iterator *iter, const char *msg) 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; diff --git a/trailer.h b/trailer.h index 9f42aa7599..7e36da7d13 100644 --- a/trailer.h +++ b/trailer.h @@ -125,6 +125,13 @@ 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; From 2ade05431ebfc8c159a00202c44580754c7d42e2 Mon Sep 17 00:00:00 2001 From: Linus Arver Date: Thu, 2 May 2024 04:54:21 +0000 Subject: [PATCH 04/10] sequencer: use the trailer iterator Instead of calling "trailer_info_get()", which is a low-level function in the trailers implementation (trailer.c), call trailer_iterator_advance(), which was specifically designed for public consumption in f0939a0eb1 (trailer: add interface for iterating over commit trailers, 2020-09-27). Avoiding "trailer_info_get()" means we don't have to worry about options like "no_divider" (relevant for parsing trailers). We also don't have to check for things like "info.trailer_start == info.trailer_end" to see whether there were any trailers (instead we can just check to see whether the iterator advanced at all). Note how we have to use "iter.raw" in order to get the same behavior as before when we iterated over the unparsed string array (char **trailers) in trailer_info. Signed-off-by: Linus Arver Signed-off-by: Junio C Hamano --- sequencer.c | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/sequencer.c b/sequencer.c index ea1441e617..4c1f6c675e 100644 --- a/sequencer.c +++ b/sequencer.c @@ -319,35 +319,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; From 655eb65d48bec60d24baf66bf19de394eb2e6aea Mon Sep 17 00:00:00 2001 From: Linus Arver Date: Thu, 2 May 2024 04:54:22 +0000 Subject: [PATCH 05/10] interpret-trailers: access trailer_info with new helpers Instead of directly accessing trailer_info members, access them indirectly through new helper functions exposed by the trailer API. This is the first of two preparatory commits which will allow us to use the so-called "pimpl" (pointer to implementation) idiom for the trailer API, by making the trailer_info struct private to the trailer implementation (and thus hidden from the API). Helped-by: Christian Couder Signed-off-by: Linus Arver Signed-off-by: Junio C Hamano --- builtin/interpret-trailers.c | 12 ++++++------ trailer.c | 21 +++++++++++++++++++++ trailer.h | 4 ++++ 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/builtin/interpret-trailers.c b/builtin/interpret-trailers.c index 11f4ce9e4a..f3240682e3 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 = trailer_info_new(); 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); + parse_trailers(opts, info, 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/trailer.c b/trailer.c index 4700c44144..95b4c9b8f1 100644 --- a/trailer.c +++ b/trailer.c @@ -952,6 +952,12 @@ static void unfold_value(struct strbuf *val) strbuf_release(&out); } +struct trailer_info *trailer_info_new(void) +{ + struct trailer_info *info = xcalloc(1, sizeof(*info)); + return info; +} + /* * Parse trailers in "str", populating the trailer info and "head" * linked list structure. @@ -1000,6 +1006,21 @@ void free_trailers(struct list_head *trailers) } } +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_get(const struct process_trailer_options *opts, const char *str, struct trailer_info *info) diff --git a/trailer.h b/trailer.h index 7e36da7d13..9ba9672139 100644 --- a/trailer.h +++ b/trailer.h @@ -97,6 +97,10 @@ void parse_trailers(const struct process_trailer_options *, void trailer_info_get(const struct process_trailer_options *, const char *str, struct trailer_info *); +size_t trailer_block_start(struct trailer_info *); +size_t trailer_block_end(struct trailer_info *); +int blank_line_before_trailer_block(struct trailer_info *); +struct trailer_info *trailer_info_new(void); void trailer_info_release(struct trailer_info *info); From 24a25c630cfe72d2d77fed5d2841f7c017a269b5 Mon Sep 17 00:00:00 2001 From: Linus Arver Date: Thu, 2 May 2024 04:54:23 +0000 Subject: [PATCH 06/10] trailer: make parse_trailers() return trailer_info pointer This is the second and final preparatory commit for making the trailer_info struct private to the trailer implementation. Make trailer_info_get() do the actual work of allocating a new trailer_info struct, and return a pointer to it. Because parse_trailers() wraps around trailer_info_get(), it too can return this pointer to the caller. From the trailer API user's perspective, the call to trailer_info_new() can be replaced with parse_trailers(); do so in interpret-trailers. Because trailer_info_new() is no longer called by interpret-trailers, remove this function from the trailer API. With this change, we no longer allocate trailer_info on the stack --- all uses of it are via a pointer where the actual data is always allocated at runtime through trailer_info_new(). Make trailer_info_release() free this dynamically allocated memory. Finally, due to the way the function signatures of parse_trailers() and trailer_info_get() have changed, update the callsites in format_trailers_from_commit() and trailer_iterator_init() accordingly. Helped-by: Christian Couder Signed-off-by: Linus Arver Signed-off-by: Junio C Hamano --- builtin/interpret-trailers.c | 4 ++-- trailer.c | 41 +++++++++++++++++++----------------- trailer.h | 15 ++++++------- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/builtin/interpret-trailers.c b/builtin/interpret-trailers.c index f3240682e3..6bf8cec005 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 = trailer_info_new(); + struct trailer_info *info; FILE *outfile = stdout; trailer_config_init(); @@ -151,7 +151,7 @@ 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) diff --git a/trailer.c b/trailer.c index 95b4c9b8f1..9179dd802c 100644 --- a/trailer.c +++ b/trailer.c @@ -952,7 +952,7 @@ static void unfold_value(struct strbuf *val) strbuf_release(&out); } -struct trailer_info *trailer_info_new(void) +static struct trailer_info *trailer_info_new(void) { struct trailer_info *info = xcalloc(1, sizeof(*info)); return info; @@ -962,16 +962,16 @@ struct trailer_info *trailer_info_new(void) * 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) +struct trailer_info *parse_trailers(const struct process_trailer_options *opts, + const char *str, + struct list_head *head) { + struct trailer_info *info; struct strbuf tok = STRBUF_INIT; struct strbuf val = STRBUF_INIT; size_t i; - trailer_info_get(opts, str, info); + info = trailer_info_get(opts, str); for (i = 0; i < info->trailer_nr; i++) { int separator_pos; @@ -995,6 +995,8 @@ void parse_trailers(const struct process_trailer_options *opts, strbuf_detach(&val, NULL)); } } + + return info; } void free_trailers(struct list_head *trailers) @@ -1021,10 +1023,10 @@ int blank_line_before_trailer_block(struct trailer_info *info) return info->blank_line_before_trailer; } -void trailer_info_get(const struct process_trailer_options *opts, - const char *str, - struct trailer_info *info) +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; @@ -1063,6 +1065,8 @@ 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; } void trailer_info_release(struct trailer_info *info) @@ -1071,6 +1075,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, @@ -1138,21 +1143,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) @@ -1161,14 +1164,14 @@ 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) { - if (iter->internal.cur < iter->internal.info.trailer_nr) { - char *line = iter->internal.info.trailers[iter->internal.cur++]; + 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; @@ -1185,7 +1188,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 9ba9672139..c8c0018c54 100644 --- a/trailer.h +++ b/trailer.h @@ -89,18 +89,15 @@ 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); +struct trailer_info *parse_trailers(const struct process_trailer_options *, + const char *str, + struct list_head *head); +struct trailer_info *trailer_info_get(const struct process_trailer_options *, + const char *str); -void trailer_info_get(const struct process_trailer_options *, - const char *str, - struct trailer_info *); size_t trailer_block_start(struct trailer_info *); size_t trailer_block_end(struct trailer_info *); int blank_line_before_trailer_block(struct trailer_info *); -struct trailer_info *trailer_info_new(void); void trailer_info_release(struct trailer_info *info); @@ -141,7 +138,7 @@ struct trailer_iterator { /* private */ struct { - struct trailer_info info; + struct trailer_info *info; size_t cur; } internal; }; From c1e4b2b18e9bd7e808285c88ec58eb00ea4942fc Mon Sep 17 00:00:00 2001 From: Linus Arver Date: Thu, 2 May 2024 04:54:24 +0000 Subject: [PATCH 07/10] trailer: make trailer_info struct private MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In 13211ae23f (trailer: separate public from internal portion of trailer_iterator, 2023-09-09) we moved trailer_info behind an anonymous struct to discourage use by trailer.h API users. However it still left open the possibility of external use of trailer_info itself. Now that there are no external users of trailer_info, we can make this struct private. Make this struct private by putting its definition inside trailer.c. This has two benefits: (1) it makes the surface area of the public facing interface (trailer.h) smaller, and (2) external API users are unable to peer inside this struct (because it is only ever exposed as an opaque pointer). There are a few disadvantages: (A) every time the member of the struct is accessed an extra pointer dereference must be done, and (B) for users of trailer_info outside trailer.c, this struct can no longer be allocated on the stack and may only be allocated on the heap (because its definition is hidden away in trailer.c) and appropriately deallocated by the user, and (C) without good documentation on the API, the opaque struct is hostile to programmers by going opposite to the "Show me your data structures, and I won't usually need your code; it'll be obvious." mantra [2]. (The disadvantages have already been observed in the two preparatory commits that precede this one.) This commit believes that the benefits outweigh the disadvantages for designing APIs, as explained below. Making trailer_info private exposes existing deficiencies in the API. This is because users of this struct had full access to its internals, so there wasn't much need to actually design it to be "complete" in the sense that API users only needed to use what was provided by the API. For example, the location of the trailer block (start/end offsets relative to the start of the input text) was accessible by looking at these struct members directly. Now that the struct is private, we have to expose new API functions to allow clients to access this information (see builtin/interpret-trailers.c). The idea in this commit to hide implementation details behind an "opaque pointer" is also known as the "pimpl" (pointer to implementation) idiom in C++ and is a common pattern in that language (where, for example, abstract classes only have pointers to concrete classes). However, the original inspiration to use this idiom does not come from C++, but instead the book "C Interfaces and Implementations: Techniques for Creating Reusable Software" [1]. This book recommends opaque pointers as a good design principle for designing C libraries, using the term "interface" as the functions defined in *.h (header) files and "implementation" as the corresponding *.c file which define the interfaces. The book says this about opaque pointers: ... clients can manipulate such pointers freely, but they can’t dereference them; that is, they can’t look at the innards of the structure pointed to by them. Only the implementation has that privilege. Opaque pointers hide representation details and help catch errors. In our case, "struct trailer_info" is now hidden from clients, and the ways in which this opaque pointer can be used is limited to the richness of . In other words, exclusively controls exactly how "trailer_info" pointers are to be used. [1] Hanson, David R. "C Interfaces and Implementations: Techniques for Creating Reusable Software". Addison Wesley, 1997. p. 22 [2] Raymond, Eric S. "The Cathedral and the Bazaar: Musings on Linux and Open Source by an Accidental Revolutionary". O'Reilly, 1999. Helped-by: Junio C Hamano Helped-by: Christian Couder Signed-off-by: Linus Arver Signed-off-by: Junio C Hamano --- trailer.c | 21 +++++++++++++++++++++ trailer.h | 23 ++--------------------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/trailer.c b/trailer.c index 9179dd802c..6167b707ae 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; diff --git a/trailer.h b/trailer.h index c8c0018c54..4e0a6789d7 100644 --- a/trailer.h +++ b/trailer.h @@ -4,6 +4,8 @@ #include "list.h" #include "strbuf.h" +struct trailer_info; + enum trailer_where { WHERE_DEFAULT, WHERE_END, @@ -29,27 +31,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. From cf5c9349de52d8d5fa02f7a5b6d9122260834f26 Mon Sep 17 00:00:00 2001 From: Linus Arver Date: Thu, 2 May 2024 04:54:25 +0000 Subject: [PATCH 08/10] trailer: retire trailer_info_get() from API Make trailer_info_get() "static" to be file-scoped to trailer.c, because no one outside of trailer.c uses it. Remove its declaration from . We have to also reposition it to be above parse_trailers(), which depends on it. Signed-off-by: Linus Arver Signed-off-by: Junio C Hamano --- trailer.c | 92 +++++++++++++++++++++++++++---------------------------- trailer.h | 2 -- 2 files changed, 46 insertions(+), 48 deletions(-) diff --git a/trailer.c b/trailer.c index 6167b707ae..33b6aa7e8b 100644 --- a/trailer.c +++ b/trailer.c @@ -979,6 +979,52 @@ static struct trailer_info *trailer_info_new(void) return 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; + size_t nr = 0, alloc = 0; + char **last = NULL; + + trailer_config_init(); + + end_of_log_message = find_end_of_log_message(str, opts->no_divider); + trailer_block_start = find_trailer_block_start(str, end_of_log_message); + + trailer_lines = strbuf_split_buf(str + trailer_block_start, + end_of_log_message - trailer_block_start, + '\n', + 0); + for (ptr = trailer_lines; *ptr; ptr++) { + if (last && isspace((*ptr)->buf[0])) { + struct strbuf sb = STRBUF_INIT; + strbuf_attach(&sb, *last, strlen(*last), strlen(*last)); + strbuf_addbuf(&sb, *ptr); + *last = strbuf_detach(&sb, NULL); + continue; + } + ALLOC_GROW(trailer_strings, nr + 1, alloc); + trailer_strings[nr] = strbuf_detach(*ptr, NULL); + last = find_separator(trailer_strings[nr], separators) >= 1 + ? &trailer_strings[nr] + : NULL; + nr++; + } + strbuf_list_free(trailer_lines); + + info->blank_line_before_trailer = ends_with_blank_line(str, + trailer_block_start); + info->trailer_block_start = trailer_block_start; + 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 "head" * linked list structure. @@ -1044,52 +1090,6 @@ int blank_line_before_trailer_block(struct trailer_info *info) return info->blank_line_before_trailer; } -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; - size_t nr = 0, alloc = 0; - char **last = NULL; - - trailer_config_init(); - - end_of_log_message = find_end_of_log_message(str, opts->no_divider); - trailer_block_start = find_trailer_block_start(str, end_of_log_message); - - trailer_lines = strbuf_split_buf(str + trailer_block_start, - end_of_log_message - trailer_block_start, - '\n', - 0); - for (ptr = trailer_lines; *ptr; ptr++) { - if (last && isspace((*ptr)->buf[0])) { - struct strbuf sb = STRBUF_INIT; - strbuf_attach(&sb, *last, strlen(*last), strlen(*last)); - strbuf_addbuf(&sb, *ptr); - *last = strbuf_detach(&sb, NULL); - continue; - } - ALLOC_GROW(trailer_strings, nr + 1, alloc); - trailer_strings[nr] = strbuf_detach(*ptr, NULL); - last = find_separator(trailer_strings[nr], separators) >= 1 - ? &trailer_strings[nr] - : NULL; - nr++; - } - strbuf_list_free(trailer_lines); - - info->blank_line_before_trailer = ends_with_blank_line(str, - trailer_block_start); - info->trailer_block_start = trailer_block_start; - info->trailer_block_end = end_of_log_message; - info->trailers = trailer_strings; - info->trailer_nr = nr; - - return info; -} - void trailer_info_release(struct trailer_info *info) { size_t i; diff --git a/trailer.h b/trailer.h index 4e0a6789d7..b0ec7658c6 100644 --- a/trailer.h +++ b/trailer.h @@ -73,8 +73,6 @@ void process_trailers_lists(struct list_head *head, struct trailer_info *parse_trailers(const struct process_trailer_options *, const char *str, struct list_head *head); -struct trailer_info *trailer_info_get(const struct process_trailer_options *, - const char *str); size_t trailer_block_start(struct trailer_info *); size_t trailer_block_end(struct trailer_info *); From 5f800603a9fa0cbcca83b3eb56dff893582d0ca7 Mon Sep 17 00:00:00 2001 From: Linus Arver Date: Thu, 2 May 2024 04:54:26 +0000 Subject: [PATCH 09/10] trailer: document parse_trailers() usage Explain how to use parse_trailers(), because earlier we made the trailer_info struct opaque. That is, because clients can no longer peek inside it, we should give them guidance about how the (pointer to the) opaque struct can still be useful to them. Rename "head" struct to "trailer_objects" to make the wording of the new comments a bit easier to read (because "head" itself doesn't really have any domain-specific meaning here). Signed-off-by: Linus Arver Signed-off-by: Junio C Hamano --- trailer.c | 8 ++++---- trailer.h | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/trailer.c b/trailer.c index 33b6aa7e8b..406745264a 100644 --- a/trailer.c +++ b/trailer.c @@ -1026,12 +1026,12 @@ static struct trailer_info *trailer_info_get(const struct process_trailer_option } /* - * Parse trailers in "str", populating the trailer info and "head" + * 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 *head) + struct list_head *trailer_objects) { struct trailer_info *info; struct strbuf tok = STRBUF_INIT; @@ -1051,13 +1051,13 @@ struct trailer_info *parse_trailers(const struct process_trailer_options *opts, separator_pos); if (opts->unfold) unfold_value(&val); - add_trailer_item(head, + 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(head, + add_trailer_item(trailer_objects, NULL, strbuf_detach(&val, NULL)); } diff --git a/trailer.h b/trailer.h index b0ec7658c6..82104912d7 100644 --- a/trailer.h +++ b/trailer.h @@ -70,14 +70,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); +/* + * 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 *head); + struct list_head *trailer_objects); +/* + * 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); From dc88e5279a0bc68dc7b0337b0da34d50984bb38b Mon Sep 17 00:00:00 2001 From: Linus Arver Date: Thu, 2 May 2024 04:54:27 +0000 Subject: [PATCH 10/10] trailer unit tests: inspect iterator contents Previously we only checked whether we would iterate a certain (expected) number of times. Also check the parsed "raw", "key" and "val" fields during each iteration. Helped-by: Junio C Hamano Signed-off-by: Linus Arver Signed-off-by: Junio C Hamano --- t/unit-tests/t-trailer.c | 161 +++++++++++++++++++++++++++++++++++---- 1 file changed, 148 insertions(+), 13 deletions(-) diff --git a/t/unit-tests/t-trailer.c b/t/unit-tests/t-trailer.c index 4f640d2a4b..2ecca359d9 100644 --- a/t/unit-tests/t-trailer.c +++ b/t/unit-tests/t-trailer.c @@ -1,14 +1,27 @@ #include "test-lib.h" #include "trailer.h" -static void t_trailer_iterator(const char *msg, size_t num_expected) +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)) + 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); @@ -16,22 +29,26 @@ static void t_trailer_iterator(const char *msg, size_t 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, + {{0}}, }, { "no newline at beginning", "Fixes: x\n" "Acked-by: x\n" "Reviewed-by: x\n", - 0 + 0, + {{0}}, }, { "newline at beginning", @@ -39,7 +56,27 @@ static void run_t_trailer_iterator(void) "Fixes: x\n" "Acked-by: x\n" "Reviewed-by: x\n", - 3 + 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", @@ -48,7 +85,27 @@ static void run_t_trailer_iterator(void) "Fixes: x\n" "Acked-by: x\n" "Reviewed-by: x\n", - 3 + 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", @@ -63,7 +120,32 @@ static void run_t_trailer_iterator(void) "Acked-by: x\n" "Reviewed-by: x\n" "Signed-off-by: x\n", - 4 + 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)", @@ -85,7 +167,22 @@ static void run_t_trailer_iterator(void) */ "Helped-by: x\n" "Signed-off-by: x\n", - 2 + 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", @@ -103,7 +200,17 @@ static void run_t_trailer_iterator(void) * always ignores the divider. */ "Signed-off-by: x\n", - 1 + 1, + { + { + .raw = "Signed-off-by: x\n", + .key = "Signed-off-by", + .val = "x", + }, + { + 0 + }, + }, }, { "with non-trailer lines in trailer block", @@ -125,7 +232,32 @@ static void run_t_trailer_iterator(void) * because we still want to iterate through the entire * block. */ - 4 + 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", @@ -140,7 +272,8 @@ static void run_t_trailer_iterator(void) "not a trailer line\n" "not a trailer line\n" "Signed-off-by: x\n", - 0 + 0, + {{0}}, }, { "with non-trailer lines (only 1) in trailer block, but no Git-generated trailers", @@ -162,13 +295,15 @@ static void run_t_trailer_iterator(void) "Acked-by: x\n" "Acked-by: x\n" "not a trailer line\n", - 0 + 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].num_expected, + tc[i].contents), "%s", tc[i].name); } }