2 * Copyright (C) the libgit2 contributors. All rights reserved.
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.
9 #include "git2/message.h"
15 #define COMMENT_LINE_CHAR '#'
16 #define TRAILER_SEPARATORS ":"
18 static const char *const git_generated_prefixes
[] = {
20 "(cherry picked from commit ",
24 static int is_blank_line(const char *str
)
27 while (*s
&& *s
!= '\n' && isspace(*s
))
29 return !*s
|| *s
== '\n';
32 static const char *next_line(const char *str
)
34 const char *nl
= strchr(str
, '\n');
39 /* return pointer to the NUL terminator: */
40 return str
+ strlen(str
);
45 * Return the position of the start of the last line. If len is 0, return 0.
47 static bool last_line(size_t *out
, const char *buf
, size_t len
)
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.
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 "#".
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.
86 static bool find_separator(size_t *out
, const char *line
, const char *separators
)
88 int whitespace_found
= 0;
90 for (c
= line
; *c
; c
++) {
91 if (strchr(separators
, *c
)) {
96 if (!whitespace_found
&& (isalnum(*c
) || *c
== '-'))
98 if (c
!= line
&& (*c
== ' ' || *c
== '\t')) {
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.
114 * Returns the number of bytes from the tail to ignore, to be fed as
115 * the second parameter to append_signoff().
117 static size_t ignore_non_trailer(const char *buf
, size_t len
)
119 size_t boc
= 0, bol
= 0;
120 int in_old_conflicts_block
= 0;
123 while (bol
< cutoff
) {
124 const char *next_line
= memchr(buf
+ bol
, '\n', len
- bol
);
127 next_line
= buf
+ len
;
131 if (buf
[bol
] == COMMENT_LINE_CHAR
|| buf
[bol
] == '\n') {
132 /* is this the first of the run of comments? */
135 /* otherwise, it is just continuing */
136 } else if (git__prefixcmp(buf
+ bol
, "Conflicts:\n") == 0) {
137 in_old_conflicts_block
= 1;
140 } else if (in_old_conflicts_block
&& buf
[bol
] == '\t') {
141 ; /* a pathname in the conflicts block */
143 /* the previous was not trailing comment */
145 in_old_conflicts_block
= 0;
147 bol
= next_line
- buf
;
149 return boc
? len
- boc
: len
- cutoff
;
153 * Return the position of the start of the patch or the length of str if there
154 * is no patch in the message.
156 static size_t find_patch_start(const char *str
)
160 for (s
= str
; *s
; s
= next_line(s
)) {
161 if (git__prefixcmp(s
, "---") == 0)
169 * Return the position of the first trailer line or len if there are no
172 static size_t find_trailer_start(const char *buf
, size_t len
)
175 size_t end_of_title
, l
;
177 int recognized_prefix
= 0, trailer_lines
= 0, non_trailer_lines
= 0;
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).
185 int possible_continuation_lines
= 0;
187 /* The first paragraph is the title and cannot be trailers */
188 for (s
= buf
; s
< buf
+ len
; s
= next_line(s
)) {
189 if (s
[0] == COMMENT_LINE_CHAR
)
191 if (is_blank_line(s
))
194 end_of_title
= s
- buf
;
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.
203 while (last_line(&l
, buf
, l
) && l
>= end_of_title
) {
204 const char *bol
= buf
+ l
;
205 const char *const *p
;
206 size_t separator_pos
= 0;
208 if (bol
[0] == COMMENT_LINE_CHAR
) {
209 non_trailer_lines
+= possible_continuation_lines
;
210 possible_continuation_lines
= 0;
213 if (is_blank_line(bol
)) {
216 non_trailer_lines
+= possible_continuation_lines
;
217 if (recognized_prefix
&&
218 trailer_lines
* 3 >= non_trailer_lines
)
219 return next_line(bol
) - buf
;
220 else if (trailer_lines
&& !non_trailer_lines
)
221 return next_line(bol
) - buf
;
226 for (p
= git_generated_prefixes
; *p
; p
++) {
227 if (git__prefixcmp(bol
, *p
) == 0) {
229 possible_continuation_lines
= 0;
230 recognized_prefix
= 1;
231 goto continue_outer_loop
;
235 find_separator(&separator_pos
, bol
, TRAILER_SEPARATORS
);
236 if (separator_pos
>= 1 && !isspace(bol
[0])) {
238 possible_continuation_lines
= 0;
239 if (recognized_prefix
)
241 } else if (isspace(bol
[0]))
242 possible_continuation_lines
++;
245 non_trailer_lines
+= possible_continuation_lines
;
246 possible_continuation_lines
= 0;
255 /* Return the position of the end of the trailers. */
256 static size_t find_trailer_end(const char *buf
, size_t len
)
258 return len
- ignore_non_trailer(buf
, len
);
261 static char *extract_trailer_block(const char *message
, size_t *len
)
263 size_t patch_start
= find_patch_start(message
);
264 size_t trailer_end
= find_trailer_end(message
, patch_start
);
265 size_t trailer_start
= find_trailer_start(message
, trailer_end
);
267 size_t trailer_len
= trailer_end
- trailer_start
;
269 char *buffer
= git__malloc(trailer_len
+ 1);
273 memcpy(buffer
, message
+ trailer_start
, trailer_len
);
274 buffer
[trailer_len
] = 0;
292 #define NEXT(st) { state = (st); ptr++; continue; }
293 #define GOTO(st) { state = (st); continue; }
295 typedef git_array_t(git_message_trailer
) git_array_trailer_t
;
297 int git_message_trailers(git_message_trailer_array
*trailer_arr
, const char *message
)
299 enum trailer_state state
= S_START
;
304 git_array_trailer_t arr
= GIT_ARRAY_INIT
;
307 char *trailer
= extract_trailer_block(message
, &trailer_len
);
311 for (ptr
= trailer
;;) {
326 if (isalnum(*ptr
) || *ptr
== '-') {
327 /* legal key character */
331 if (*ptr
== ' ' || *ptr
== '\t') {
332 /* optional whitespace before separator */
337 if (strchr(TRAILER_SEPARATORS
, *ptr
)) {
342 /* illegal character */
350 if (*ptr
== ' ' || *ptr
== '\t') {
354 if (strchr(TRAILER_SEPARATORS
, *ptr
)) {
358 /* illegal character */
366 if (*ptr
== ' ' || *ptr
== '\t') {
394 git_message_trailer
*t
= git_array_alloc(arr
);
419 trailer_arr
->_trailer_block
= trailer
;
420 trailer_arr
->trailers
= arr
.ptr
;
421 trailer_arr
->count
= arr
.size
;
426 void git_message_trailer_array_free(git_message_trailer_array
*arr
)
428 git__free(arr
->_trailer_block
);
429 git__free(arr
->trailers
);