File Coverage

deps/libgit2/src/libgit2/trailer.c
Criterion Covered Total %
statement 143 191 74.8
branch 78 144 54.1
condition n/a
subroutine n/a
pod n/a
total 221 335 65.9


line stmt bran cond sub pod time code
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
12             #include
13             #include
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 8           static int is_blank_line(const char *str)
25             {
26 8           const char *s = str;
27 8 50         while (*s && *s != '\n' && isspace(*s))
    100          
    50          
28 0           s++;
29 8 50         return !*s || *s == '\n';
    100          
30             }
31              
32 10           static const char *next_line(const char *str)
33             {
34 10           const char *nl = strchr(str, '\n');
35              
36 10 50         if (nl) {
37 10           return nl + 1;
38             } else {
39             /* return pointer to the NUL terminator: */
40 0           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 6           static bool last_line(size_t *out, const char *buf, size_t len)
48             {
49             size_t i;
50              
51 6           *out = 0;
52              
53 6 50         if (len == 0)
54 0           return false;
55 6 50         if (len == 1)
56 0           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 6           i = len - 2;
64              
65 84 100         for (; i > 0; i--) {
66 83 100         if (buf[i] == '\n') {
67 5           *out = i + 1;
68 5           return true;
69             }
70             }
71 1           return true;
72             }
73              
74             /*
75             * If the given line is of the form
76             * "..." or "...", 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 is "Bug" and 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 2           static bool find_separator(size_t *out, const char *line, const char *separators)
87             {
88 2           int whitespace_found = 0;
89             const char *c;
90 4 50         for (c = line; *c; c++) {
91 4 100         if (strchr(separators, *c)) {
92 2           *out = c - line;
93 2           return true;
94             }
95              
96 2 50         if (!whitespace_found && (isalnum(*c) || *c == '-'))
    50          
    0          
97 2           continue;
98 0 0         if (c != line && (*c == ' ' || *c == '\t')) {
    0          
    0          
99 0           whitespace_found = 1;
100 0           continue;
101             }
102 0           break;
103             }
104 0           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 2           static size_t ignore_non_trailer(const char *buf, size_t len)
118             {
119 2           size_t boc = 0, bol = 0;
120 2           int in_old_conflicts_block = 0;
121 2           size_t cutoff = len;
122              
123 9 100         while (bol < cutoff) {
124 7           const char *next_line = memchr(buf + bol, '\n', len - bol);
125              
126 7 50         if (!next_line)
127 0           next_line = buf + len;
128             else
129 7           next_line++;
130              
131 7 50         if (buf[bol] == COMMENT_LINE_CHAR || buf[bol] == '\n') {
    100          
132             /* is this the first of the run of comments? */
133 2 50         if (!boc)
134 1           boc = bol;
135             /* otherwise, it is just continuing */
136 6 50         } else if (git__prefixcmp(buf + bol, "Conflicts:\n") == 0) {
137 0           in_old_conflicts_block = 1;
138 0 0         if (!boc)
139 0           boc = bol;
140 6 50         } else if (in_old_conflicts_block && buf[bol] == '\t') {
    0          
141             ; /* a pathname in the conflicts block */
142 6 100         } else if (boc) {
143             /* the previous was not trailing comment */
144 1           boc = 0;
145 1           in_old_conflicts_block = 0;
146             }
147 7           bol = next_line - buf;
148             }
149 2 50         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 2           static size_t find_patch_start(const char *str)
157             {
158             const char *s;
159              
160 9 100         for (s = str; *s; s = next_line(s)) {
161 7 50         if (git__prefixcmp(s, "---") == 0)
162 0           return s - str;
163             }
164              
165 2           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 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 2           int only_spaces = 1;
177 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 2           int possible_continuation_lines = 0;
186              
187             /* The first paragraph is the title and cannot be trailers */
188 4 100         for (s = buf; s < buf + len; s = next_line(s)) {
189 3 50         if (s[0] == COMMENT_LINE_CHAR)
190 0           continue;
191 3 100         if (is_blank_line(s))
192 1           break;
193             }
194 2           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 2           l = len;
203 6 50         while (last_line(&l, buf, l) && l >= end_of_title) {
    100          
204 5           const char *bol = buf + l;
205             const char *const *p;
206 5           size_t separator_pos = 0;
207              
208 5 50         if (bol[0] == COMMENT_LINE_CHAR) {
209 0           non_trailer_lines += possible_continuation_lines;
210 0           possible_continuation_lines = 0;
211 0           continue;
212             }
213 5 100         if (is_blank_line(bol)) {
214 1 50         if (only_spaces)
215 0           continue;
216 1           non_trailer_lines += possible_continuation_lines;
217 1 50         if (recognized_prefix &&
    50          
218 1           trailer_lines * 3 >= non_trailer_lines)
219 1           return next_line(bol) - buf;
220 0 0         else if (trailer_lines && !non_trailer_lines)
    0          
221 0           return next_line(bol) - buf;
222 0           return len;
223             }
224 4           only_spaces = 0;
225              
226 8 100         for (p = git_generated_prefixes; *p; p++) {
227 6 100         if (git__prefixcmp(bol, *p) == 0) {
228 2           trailer_lines++;
229 2           possible_continuation_lines = 0;
230 2           recognized_prefix = 1;
231 2           goto continue_outer_loop;
232             }
233             }
234              
235 2           find_separator(&separator_pos, bol, TRAILER_SEPARATORS);
236 2 50         if (separator_pos >= 1 && !isspace(bol[0])) {
    50          
237 2           trailer_lines++;
238 2           possible_continuation_lines = 0;
239 2 50         if (recognized_prefix)
240 0           continue;
241 0 0         } else if (isspace(bol[0]))
242 0           possible_continuation_lines++;
243             else {
244 0           non_trailer_lines++;
245 0           non_trailer_lines += possible_continuation_lines;
246 4           possible_continuation_lines = 0;
247             }
248             continue_outer_loop:
249             ;
250             }
251              
252 2           return len;
253             }
254              
255             /* Return the position of the end of the trailers. */
256 2           static size_t find_trailer_end(const char *buf, size_t len)
257             {
258 2           return len - ignore_non_trailer(buf, len);
259             }
260              
261 2           static char *extract_trailer_block(const char *message, size_t *len)
262             {
263 2           size_t patch_start = find_patch_start(message);
264 2           size_t trailer_end = find_trailer_end(message, patch_start);
265 2           size_t trailer_start = find_trailer_start(message, trailer_end);
266              
267 2           size_t trailer_len = trailer_end - trailer_start;
268              
269 2           char *buffer = git__malloc(trailer_len + 1);
270 2 50         if (buffer == NULL)
271 0           return NULL;
272              
273 2           memcpy(buffer, message + trailer_start, trailer_len);
274 2           buffer[trailer_len] = 0;
275              
276 2           *len = trailer_len;
277              
278 2           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 2           int git_message_trailers(git_message_trailer_array *trailer_arr, const char *message)
298             {
299 2           enum trailer_state state = S_START;
300 2           int rc = 0;
301             char *ptr;
302 2           char *key = NULL;
303 2           char *value = NULL;
304 2           git_array_trailer_t arr = GIT_ARRAY_INIT;
305              
306             size_t trailer_len;
307 2           char *trailer = extract_trailer_block(message, &trailer_len);
308 2 50         if (trailer == NULL)
309 0           return -1;
310              
311 2           for (ptr = trailer;;) {
312 83           switch (state) {
313             case S_START: {
314 6 100         if (*ptr == 0) {
315 2           goto ret;
316             }
317              
318 4           key = ptr;
319 4           GOTO(S_KEY);
320             }
321             case S_KEY: {
322 32 50         if (*ptr == 0) {
323 0           goto ret;
324             }
325              
326 32 100         if (isalnum(*ptr) || *ptr == '-') {
    100          
327             /* legal key character */
328 28           NEXT(S_KEY);
329             }
330              
331 4 50         if (*ptr == ' ' || *ptr == '\t') {
    50          
332             /* optional whitespace before separator */
333 0           *ptr = 0;
334 0           NEXT(S_KEY_WS);
335             }
336              
337 4 50         if (strchr(TRAILER_SEPARATORS, *ptr)) {
338 4           *ptr = 0;
339 4           NEXT(S_SEP_WS);
340             }
341              
342             /* illegal character */
343 0           GOTO(S_IGNORE);
344             }
345             case S_KEY_WS: {
346 0 0         if (*ptr == 0) {
347 0           goto ret;
348             }
349              
350 0 0         if (*ptr == ' ' || *ptr == '\t') {
    0          
351 0           NEXT(S_KEY_WS);
352             }
353              
354 0 0         if (strchr(TRAILER_SEPARATORS, *ptr)) {
355 0           NEXT(S_SEP_WS);
356             }
357              
358             /* illegal character */
359 0           GOTO(S_IGNORE);
360             }
361             case S_SEP_WS: {
362 8 50         if (*ptr == 0) {
363 0           goto ret;
364             }
365              
366 8 100         if (*ptr == ' ' || *ptr == '\t') {
    50          
367 4           NEXT(S_SEP_WS);
368             }
369              
370 4           value = ptr;
371 4           NEXT(S_VALUE);
372             }
373             case S_VALUE: {
374 29 50         if (*ptr == 0) {
375 0           GOTO(S_VALUE_END);
376             }
377              
378 29 100         if (*ptr == '\n') {
379 4           NEXT(S_VALUE_NL);
380             }
381              
382 25           NEXT(S_VALUE);
383             }
384             case S_VALUE_NL: {
385 4 50         if (*ptr == ' ') {
386             /* continuation; */
387 0           NEXT(S_VALUE);
388             }
389              
390 4           ptr[-1] = 0;
391 4           GOTO(S_VALUE_END);
392             }
393             case S_VALUE_END: {
394 4 100         git_message_trailer *t = git_array_alloc(arr);
    50          
395              
396 4           t->key = key;
397 4           t->value = value;
398              
399 4           key = NULL;
400 4           value = NULL;
401              
402 4           GOTO(S_START);
403             }
404             case S_IGNORE: {
405 0 0         if (*ptr == 0) {
406 0           goto ret;
407             }
408              
409 0 0         if (*ptr == '\n') {
410 0           NEXT(S_START);
411             }
412              
413 0           NEXT(S_IGNORE);
414             }
415             }
416 81           }
417              
418             ret:
419 2           trailer_arr->_trailer_block = trailer;
420 2           trailer_arr->trailers = arr.ptr;
421 2           trailer_arr->count = arr.size;
422              
423 2           return rc;
424             }
425              
426 2           void git_message_trailer_array_free(git_message_trailer_array *arr)
427             {
428 2           git__free(arr->_trailer_block);
429 2           git__free(arr->trailers);
430 2           }