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 }