source src/trailer.c
| Line | Flow | Count | Block(s) | Source |
|---|---|---|---|---|
| 1 | - | /* | ||
| 2 | - | * Copyright (C) the libgit2 contributors. All rights reserved. | ||
| 3 | - | * | ||
| 4 | - | * This file is part of libgit2, distributed under the GNU GPL v2 with | ||
| 5 | - | * a Linking Exception. For full terms see the included COPYING file. | ||
| 6 | - | */ | ||
| 7 | - | #include "array.h" | ||
| 8 | - | #include "common.h" | ||
| 9 | - | #include "git2/message.h" | ||
| 10 | - | |||
| 11 | - | #include <stddef.h> | ||
| 12 | - | #include <string.h> | ||
| 13 | - | #include <ctype.h> | ||
| 14 | - | |||
| 15 | - | #define COMMENT_LINE_CHAR '#' | ||
| 16 | - | #define TRAILER_SEPARATORS ":" | ||
| 17 | - | |||
| 18 | - | static const char *const git_generated_prefixes[] = { | ||
| 19 | - | "Signed-off-by: ", | ||
| 20 | - | "(cherry picked from commit ", | ||
| 21 | - | NULL | ||
| 22 | - | }; | ||
| 23 | - | |||
| 24 | ![]() |
43 | 2 | static int is_blank_line(const char *str) |
| 25 | - | { | ||
| 26 | 43 | 2 | const char *s = str; | |
| 27 | 45 | 2,4-7 | while (*s && *s != '\n' && isspace(*s)) | |
| 28 | 2 | 3 | s++; | |
| 29 | 43 | 8 | return !*s || *s == '\n'; | |
| 30 | - | } | ||
| 31 | - | |||
| 32 | 57 | 2 | static const char *next_line(const char *str) | |
| 33 | - | { | ||
| 34 | 57 | 2 | const char *nl = strchr(str, '\n'); | |
| 35 | - | |||
| 36 | 57 | 2 | if (nl) { | |
| 37 | 56 | 3 | return nl + 1; | |
| 38 | - | } else { | ||
| 39 | - | /* return pointer to the NUL terminator: */ | ||
| 40 | 1 | 4 | return str + strlen(str); | |
| 41 | - | } | ||
| 42 | - | } | ||
| 43 | - | |||
| 44 | - | /* | ||
| 45 | - | * Return the position of the start of the last line. If len is 0, return 0. | ||
| 46 | - | */ | ||
| 47 | ![]() |
25 | 2 | static bool last_line(size_t *out, const char *buf, size_t len) |
| 48 | - | { | ||
| 49 | - | size_t i; | ||
| 50 | - | |||
| 51 | 25 | 2 | *out = 0; | |
| 52 | - | |||
| 53 | 25 | 2 | if (len == 0) | |
| 54 | ##### | 3 | return false; | |
| 55 | 25 | 4 | if (len == 1) | |
| 56 | ##### | 5 | return true; | |
| 57 | - | |||
| 58 | - | /* | ||
| 59 | - | * Skip the last character (in addition to the null terminator), | ||
| 60 | - | * because if the last character is a newline, it is considered as part | ||
| 61 | - | * of the last line anyway. | ||
| 62 | - | */ | ||
| 63 | 25 | 6 | i = len - 2; | |
| 64 | - | |||
| 65 | 222 | 6,9,10 | for (; i > 0; i--) { | |
| 66 | 222 | 7 | if (buf[i] == '\n') { | |
| 67 | 25 | 8 | *out = i + 1; | |
| 68 | 25 | 8 | return true; | |
| 69 | - | } | ||
| 70 | - | } | ||
| 71 | ##### | 11 | return true; | |
| 72 | - | } | ||
| 73 | - | |||
| 74 | - | /* | ||
| 75 | - | * If the given line is of the form | ||
| 76 | - | * "<token><optional whitespace><separator>..." or "<separator>...", sets out | ||
| 77 | - | * to the location of the separator and returns true. Otherwise, returns | ||
| 78 | - | * false. The optional whitespace is allowed there primarily to allow things | ||
| 79 | - | * like "Bug #43" where <token> is "Bug" and <separator> is "#". | ||
| 80 | - | * | ||
| 81 | - | * The separator-starts-line case (in which this function returns true and | ||
| 82 | - | * sets out to 0) is distinguished from the non-well-formed-line case (in | ||
| 83 | - | * which this function returns false) because some callers of this function | ||
| 84 | - | * need such a distinction. | ||
| 85 | - | */ | ||
| 86 | ![]() |
13 | 2 | static bool find_separator(size_t *out, const char *line, const char *separators) |
| 87 | - | { | ||
| 88 | 13 | 2 | int whitespace_found = 0; | |
| 89 | - | const char *c; | ||
| 90 | 50 | 2,15,16 | for (c = line; *c; c++) { | |
| 91 | 50 | 3 | if (strchr(separators, *c)) { | |
| 92 | 9 | 4 | *out = c - line; | |
| 93 | 9 | 4 | return true; | |
| 94 | - | } | ||
| 95 | - | |||
| 96 | 41 | 5-8 | if (!whitespace_found && (isalnum(*c) || *c == '-')) | |
| 97 | 32 | 9 | continue; | |
| 98 | 9 | 10-12 | if (c != line && (*c == ' ' || *c == '\t')) { | |
| 99 | 5 | 13 | whitespace_found = 1; | |
| 100 | 5 | 13 | continue; | |
| 101 | - | } | ||
| 102 | 4 | 14 | break; | |
| 103 | - | } | ||
| 104 | 4 | 17 | return false; | |
| 105 | - | } | ||
| 106 | - | |||
| 107 | - | /* | ||
| 108 | - | * Inspect the given string and determine the true "end" of the log message, in | ||
| 109 | - | * order to find where to put a new Signed-off-by: line. Ignored are | ||
| 110 | - | * trailing comment lines and blank lines. To support "git commit -s | ||
| 111 | - | * --amend" on an existing commit, we also ignore "Conflicts:". To | ||
| 112 | - | * support "git commit -v", we truncate at cut lines. | ||
| 113 | - | * | ||
| 114 | - | * Returns the number of bytes from the tail to ignore, to be fed as | ||
| 115 | - | * the second parameter to append_signoff(). | ||
| 116 | - | */ | ||
| 117 | ![]() |
9 | 2 | static size_t ignore_non_trailer(const char *buf, size_t len) |
| 118 | - | { | ||
| 119 | 9 | 2 | size_t boc = 0, bol = 0; | |
| 120 | 9 | 2 | int in_old_conflicts_block = 0; | |
| 121 | 9 | 2 | size_t cutoff = len; | |
| 122 | - | |||
| 123 | 49 | 2,22 | while (bol < cutoff) { | |
| 124 | 40 | 3 | const char *next_line = memchr(buf + bol, '\n', len - bol); | |
| 125 | - | |||
| 126 | 40 | 3 | if (!next_line) | |
| 127 | 1 | 4 | next_line = buf + len; | |
| 128 | - | else | ||
| 129 | 39 | 5 | next_line++; | |
| 130 | - | |||
| 131 | 40 | 6,7 | if (buf[bol] == COMMENT_LINE_CHAR || buf[bol] == '\n') { | |
| 132 | - | /* is this the first of the run of comments? */ | ||
| 133 | 12 | 8,10 | if (!boc) | |
| 134 | 12 | 9 | boc = bol; | |
| 135 | - | /* otherwise, it is just continuing */ | ||
| 136 | 28 | 11,12 | } else if (git__prefixcmp(buf + bol, "Conflicts:\n") == 0) { | |
| 137 | 1 | 13 | in_old_conflicts_block = 1; | |
| 138 | 1 | 13 | if (!boc) | |
| 139 | 1 | 14,15 | boc = bol; | |
| 140 | 27 | 16-18 | } else if (in_old_conflicts_block && buf[bol] == '\t') { | |
| 141 | - | ; /* a pathname in the conflicts block */ | ||
| 142 | 26 | 19 | } else if (boc) { | |
| 143 | - | /* the previous was not trailing comment */ | ||
| 144 | 10 | 20 | boc = 0; | |
| 145 | 10 | 20 | in_old_conflicts_block = 0; | |
| 146 | - | } | ||
| 147 | 40 | 21 | bol = next_line - buf; | |
| 148 | - | } | ||
| 149 | 9 | 23 | return boc ? len - boc : len - cutoff; | |
| 150 | - | } | ||
| 151 | - | |||
| 152 | - | /* | ||
| 153 | - | * Return the position of the start of the patch or the length of str if there | ||
| 154 | - | * is no patch in the message. | ||
| 155 | - | */ | ||
| 156 | ![]() |
9 | 2 | static size_t find_patch_start(const char *str) |
| 157 | - | { | ||
| 158 | - | const char *s; | ||
| 159 | - | |||
| 160 | 49 | 2,6,7 | for (s = str; *s; s = next_line(s)) { | |
| 161 | 41 | 3,4 | if (git__prefixcmp(s, "---") == 0) | |
| 162 | 1 | 5 | return s - str; | |
| 163 | - | } | ||
| 164 | - | |||
| 165 | 8 | 8 | return s - str; | |
| 166 | - | } | ||
| 167 | - | |||
| 168 | - | /* | ||
| 169 | - | * Return the position of the first trailer line or len if there are no | ||
| 170 | - | * trailers. | ||
| 171 | - | */ | ||
| 172 | ![]() |
9 | 2 | static size_t find_trailer_start(const char *buf, size_t len) |
| 173 | - | { | ||
| 174 | - | const char *s; | ||
| 175 | - | size_t end_of_title, l; | ||
| 176 | 9 | 2 | int only_spaces = 1; | |
| 177 | 9 | 2 | int recognized_prefix = 0, trailer_lines = 0, non_trailer_lines = 0; | |
| 178 | - | /* | ||
| 179 | - | * Number of possible continuation lines encountered. This will be | ||
| 180 | - | * reset to 0 if we encounter a trailer (since those lines are to be | ||
| 181 | - | * considered continuations of that trailer), and added to | ||
| 182 | - | * non_trailer_lines if we encounter a non-trailer (since those lines | ||
| 183 | - | * are to be considered non-trailers). | ||
| 184 | - | */ | ||
| 185 | 9 | 2 | int possible_continuation_lines = 0; | |
| 186 | - | |||
| 187 | - | /* The first paragraph is the title and cannot be trailers */ | ||
| 188 | 18 | 2,8,9 | for (s = buf; s < buf + len; s = next_line(s)) { | |
| 189 | 18 | 3 | if (s[0] == COMMENT_LINE_CHAR) | |
| 190 | ##### | 4 | continue; | |
| 191 | 18 | 5,6 | if (is_blank_line(s)) | |
| 192 | 9 | 7 | break; | |
| 193 | - | } | ||
| 194 | 9 | 10 | end_of_title = s - buf; | |
| 195 | - | |||
| 196 | - | /* | ||
| 197 | - | * Get the start of the trailers by looking starting from the end for a | ||
| 198 | - | * blank line before a set of non-blank lines that (i) are all | ||
| 199 | - | * trailers, or (ii) contains at least one Git-generated trailer and | ||
| 200 | - | * consists of at least 25% trailers. | ||
| 201 | - | */ | ||
| 202 | 9 | 10 | l = len; | |
| 203 | 25 | 10,46-48 | while (last_line(&l, buf, l) && l >= end_of_title) { | |
| 204 | 25 | 11 | const char *bol = buf + l; | |
| 205 | - | const char *const *p; | ||
| 206 | 25 | 11 | size_t separator_pos = 0; | |
| 207 | - | |||
| 208 | 25 | 11 | if (bol[0] == COMMENT_LINE_CHAR) { | |
| 209 | ##### | 12 | non_trailer_lines += possible_continuation_lines; | |
| 210 | ##### | 12 | possible_continuation_lines = 0; | |
| 211 | ##### | 12,44 | continue; | |
| 212 | - | } | ||
| 213 | 25 | 13,14 | if (is_blank_line(bol)) { | |
| 214 | 9 | 15 | if (only_spaces) | |
| 215 | ##### | 16 | continue; | |
| 216 | 9 | 17 | non_trailer_lines += possible_continuation_lines; | |
| 217 | 9 | 17,18 | if (recognized_prefix && | |
| 218 | 2 | 18 | trailer_lines * 3 >= non_trailer_lines) | |
| 219 | 9 | 19,20,45 | return next_line(bol) - buf; | |
| 220 | 7 | 21,22 | else if (trailer_lines && !non_trailer_lines) | |
| 221 | 6 | 23,24 | return next_line(bol) - buf; | |
| 222 | 1 | 25 | return len; | |
| 223 | - | } | ||
| 224 | 16 | 26 | only_spaces = 0; | |
| 225 | - | |||
| 226 | 42 | 26,30,31 | for (p = git_generated_prefixes; *p; p++) { | |
| 227 | 29 | 27,28 | if (git__prefixcmp(bol, *p) == 0) { | |
| 228 | 3 | 29 | trailer_lines++; | |
| 229 | 3 | 29 | possible_continuation_lines = 0; | |
| 230 | 3 | 29 | recognized_prefix = 1; | |
| 231 | 3 | 29 | goto continue_outer_loop; | |
| 232 | - | } | ||
| 233 | - | } | ||
| 234 | - | |||
| 235 | 13 | 32 | find_separator(&separator_pos, bol, TRAILER_SEPARATORS); | |
| 236 | 13 | 33-35 | if (separator_pos >= 1 && !isspace(bol[0])) { | |
| 237 | 9 | 36 | trailer_lines++; | |
| 238 | 9 | 36 | possible_continuation_lines = 0; | |
| 239 | 9 | 36,38 | if (recognized_prefix) | |
| 240 | ##### | 37 | continue; | |
| 241 | 4 | 39,40 | } else if (isspace(bol[0])) | |
| 242 | 2 | 41 | possible_continuation_lines++; | |
| 243 | - | else { | ||
| 244 | 2 | 42 | non_trailer_lines++; | |
| 245 | 2 | 42 | non_trailer_lines += possible_continuation_lines; | |
| 246 | 16 | 42,43 | possible_continuation_lines = 0; | |
| 247 | - | } | ||
| 248 | - | continue_outer_loop: | ||
| 249 | - | ; | ||
| 250 | - | } | ||
| 251 | - | |||
| 252 | ##### | 49 | return len; | |
| 253 | - | } | ||
| 254 | - | |||
| 255 | - | /* Return the position of the end of the trailers. */ | ||
| 256 | 9 | 2 | static size_t find_trailer_end(const char *buf, size_t len) | |
| 257 | - | { | ||
| 258 | 9 | 2 | return len - ignore_non_trailer(buf, len); | |
| 259 | - | } | ||
| 260 | - | |||
| 261 | 9 | 2 | static char *extract_trailer_block(const char *message, size_t* len) | |
| 262 | - | { | ||
| 263 | 9 | 2 | size_t patch_start = find_patch_start(message); | |
| 264 | 9 | 3 | size_t trailer_end = find_trailer_end(message, patch_start); | |
| 265 | 9 | 4 | size_t trailer_start = find_trailer_start(message, trailer_end); | |
| 266 | - | |||
| 267 | 9 | 5 | size_t trailer_len = trailer_end - trailer_start; | |
| 268 | - | |||
| 269 | 9 | 5 | char *buffer = git__malloc(trailer_len + 1); | |
| 270 | 9 | 6 | if (buffer == NULL) | |
| 271 | ##### | 7 | return NULL; | |
| 272 | - | |||
| 273 | 9 | 8 | memcpy(buffer, message + trailer_start, trailer_len); | |
| 274 | 9 | 8 | buffer[trailer_len] = 0; | |
| 275 | - | |||
| 276 | 9 | 8 | *len = trailer_len; | |
| 277 | - | |||
| 278 | 9 | 8 | return buffer; | |
| 279 | - | } | ||
| 280 | - | |||
| 281 | - | enum trailer_state { | ||
| 282 | - | S_START = 0, | ||
| 283 | - | S_KEY = 1, | ||
| 284 | - | S_KEY_WS = 2, | ||
| 285 | - | S_SEP_WS = 3, | ||
| 286 | - | S_VALUE = 4, | ||
| 287 | - | S_VALUE_NL = 5, | ||
| 288 | - | S_VALUE_END = 6, | ||
| 289 | - | S_IGNORE = 7, | ||
| 290 | - | }; | ||
| 291 | - | |||
| 292 | - | #define NEXT(st) { state = (st); ptr++; continue; } | ||
| 293 | - | #define GOTO(st) { state = (st); continue; } | ||
| 294 | - | |||
| 295 | - | typedef git_array_t(git_message_trailer) git_array_trailer_t; | ||
| 296 | - | |||
| 297 | ![]() |
9 | 2 | int git_message_trailers(git_message_trailer_array *trailer_arr, const char *message) |
| 298 | - | { | ||
| 299 | 9 | 2 | enum trailer_state state = S_START; | |
| 300 | 9 | 2 | int rc = 0; | |
| 301 | - | char *ptr; | ||
| 302 | 9 | 2 | char *key = NULL; | |
| 303 | 9 | 2 | char *value = NULL; | |
| 304 | 9 | 2 | git_array_trailer_t arr = GIT_ARRAY_INIT; | |
| 305 | - | |||
| 306 | - | size_t trailer_len; | ||
| 307 | 9 | 2 | char *trailer = extract_trailer_block(message, &trailer_len); | |
| 308 | 9 | 3 | if (trailer == NULL) | |
| 309 | ##### | 4 | return -1; | |
| 310 | - | |||
| 311 | 9 | 5 | for (ptr = trailer;;) { | |
| 312 | 249 | 6 | switch (state) { | |
| 313 | - | case S_START: { | ||
| 314 | 22 | 7 | if (*ptr == 0) { | |
| 315 | 9 | 8 | goto ret; | |
| 316 | - | } | ||
| 317 | - | |||
| 318 | 13 | 9 | key = ptr; | |
| 319 | 13 | 9 | GOTO(S_KEY); | |
| 320 | - | } | ||
| 321 | - | case S_KEY: { | ||
| 322 | 80 | 10 | if (*ptr == 0) { | |
| 323 | ##### | 11 | goto ret; | |
| 324 | - | } | ||
| 325 | - | |||
| 326 | 80 | 12-14 | if (isalnum(*ptr) || *ptr == '-') { | |
| 327 | - | /* legal key character */ | ||
| 328 | 67 | 15 | NEXT(S_KEY); | |
| 329 | - | } | ||
| 330 | - | |||
| 331 | 13 | 16,17 | if (*ptr == ' ' || *ptr == '\t') { | |
| 332 | - | /* optional whitespace before separator */ | ||
| 333 | 2 | 18 | *ptr = 0; | |
| 334 | 2 | 18 | NEXT(S_KEY_WS); | |
| 335 | - | } | ||
| 336 | - | |||
| 337 | 11 | 19 | if (strchr(TRAILER_SEPARATORS, *ptr)) { | |
| 338 | 11 | 20 | *ptr = 0; | |
| 339 | 11 | 20 | NEXT(S_SEP_WS); | |
| 340 | - | } | ||
| 341 | - | |||
| 342 | - | /* illegal character */ | ||
| 343 | ##### | 21 | GOTO(S_IGNORE); | |
| 344 | - | } | ||
| 345 | - | case S_KEY_WS: { | ||
| 346 | 4 | 22 | if (*ptr == 0) { | |
| 347 | ##### | 23 | goto ret; | |
| 348 | - | } | ||
| 349 | - | |||
| 350 | 4 | 24,25 | if (*ptr == ' ' || *ptr == '\t') { | |
| 351 | 2 | 26 | NEXT(S_KEY_WS); | |
| 352 | - | } | ||
| 353 | - | |||
| 354 | 2 | 27 | if (strchr(TRAILER_SEPARATORS, *ptr)) { | |
| 355 | 1 | 28 | NEXT(S_SEP_WS); | |
| 356 | - | } | ||
| 357 | - | |||
| 358 | - | /* illegal character */ | ||
| 359 | 1 | 29 | GOTO(S_IGNORE); | |
| 360 | - | } | ||
| 361 | - | case S_SEP_WS: { | ||
| 362 | 25 | 30 | if (*ptr == 0) { | |
| 363 | ##### | 31 | goto ret; | |
| 364 | - | } | ||
| 365 | - | |||
| 366 | 25 | 32,33 | if (*ptr == ' ' || *ptr == '\t') { | |
| 367 | 13 | 34 | NEXT(S_SEP_WS); | |
| 368 | - | } | ||
| 369 | - | |||
| 370 | 12 | 35 | value = ptr; | |
| 371 | 12 | 35 | NEXT(S_VALUE); | |
| 372 | - | } | ||
| 373 | - | case S_VALUE: { | ||
| 374 | 83 | 36 | if (*ptr == 0) { | |
| 375 | 1 | 37 | GOTO(S_VALUE_END); | |
| 376 | - | } | ||
| 377 | - | |||
| 378 | 82 | 38 | if (*ptr == '\n') { | |
| 379 | 13 | 39 | NEXT(S_VALUE_NL); | |
| 380 | - | } | ||
| 381 | - | |||
| 382 | 69 | 40 | NEXT(S_VALUE); | |
| 383 | - | } | ||
| 384 | - | case S_VALUE_NL: { | ||
| 385 | 13 | 41 | if (*ptr == ' ') { | |
| 386 | - | /* continuation; */ | ||
| 387 | 2 | 42 | NEXT(S_VALUE); | |
| 388 | - | } | ||
| 389 | - | |||
| 390 | 11 | 43 | ptr[-1] = 0; | |
| 391 | 11 | 43 | GOTO(S_VALUE_END); | |
| 392 | - | } | ||
| 393 | - | case S_VALUE_END: { | ||
| 394 | 12 | 44-49 | git_message_trailer *t = git_array_alloc(arr); | |
| 395 | - | |||
| 396 | 12 | 50 | t->key = key; | |
| 397 | 12 | 50 | t->value = value; | |
| 398 | - | |||
| 399 | 12 | 50 | key = NULL; | |
| 400 | 12 | 50 | value = NULL; | |
| 401 | - | |||
| 402 | 12 | 50 | GOTO(S_START); | |
| 403 | - | } | ||
| 404 | - | case S_IGNORE: { | ||
| 405 | 10 | 51 | if (*ptr == 0) { | |
| 406 | ##### | 52 | goto ret; | |
| 407 | - | } | ||
| 408 | - | |||
| 409 | 10 | 53 | if (*ptr == '\n') { | |
| 410 | 1 | 54 | NEXT(S_START); | |
| 411 | - | } | ||
| 412 | - | |||
| 413 | 9 | 55 | NEXT(S_IGNORE); | |
| 414 | - | } | ||
| 415 | - | } | ||
| 416 | 240 | 56 | } | |
| 417 | - | |||
| 418 | - | ret: | ||
| 419 | 9 | 57 | trailer_arr->_trailer_block = trailer; | |
| 420 | 9 | 57 | trailer_arr->trailers = arr.ptr; | |
| 421 | 9 | 57 | trailer_arr->count = arr.size; | |
| 422 | - | |||
| 423 | 9 | 57 | return rc; | |
| 424 | - | } | ||
| 425 | - | |||
| 426 | 9 | 2 | void git_message_trailer_array_free(git_message_trailer_array *arr) | |
| 427 | - | { | ||
| 428 | 9 | 2 | git__free(arr->_trailer_block); | |
| 429 | 9 | 3 | git__free(arr->trailers); | |
| 430 | 9 | 4 | } |