#include "cache.h" #include "utf8.h" #include "strbuf.h" #include "mailinfo.h" static void cleanup_space(struct strbuf *sb) { size_t pos, cnt; for (pos = 0; pos < sb->len; pos++) { if (isspace(sb->buf[pos])) { sb->buf[pos] = ' '; for (cnt = 0; isspace(sb->buf[pos + cnt + 1]); cnt++); strbuf_remove(sb, pos + 1, cnt); } } } static void get_sane_name(struct strbuf *out, struct strbuf *name, struct strbuf *email) { struct strbuf *src = name; if (name->len < 3 || 60 < name->len || strchr(name->buf, '@') || strchr(name->buf, '<') || strchr(name->buf, '>')) src = email; else if (name == out) return; strbuf_reset(out); strbuf_addbuf(out, src); } static void parse_bogus_from(struct mailinfo *mi, const struct strbuf *line) { /* John Doe */ char *bra, *ket; /* This is fallback, so do not bother if we already have an * e-mail address. */ if (mi->email.len) return; bra = strchr(line->buf, '<'); if (!bra) return; ket = strchr(bra, '>'); if (!ket) return; strbuf_reset(&mi->email); strbuf_add(&mi->email, bra + 1, ket - bra - 1); strbuf_reset(&mi->name); strbuf_add(&mi->name, line->buf, bra - line->buf); strbuf_trim(&mi->name); get_sane_name(&mi->name, &mi->name, &mi->email); } static void handle_from(struct mailinfo *mi, const struct strbuf *from) { char *at; size_t el; struct strbuf f; strbuf_init(&f, from->len); strbuf_addbuf(&f, from); at = strchr(f.buf, '@'); if (!at) { parse_bogus_from(mi, from); return; } /* * If we already have one email, don't take any confusing lines */ if (mi->email.len && strchr(at + 1, '@')) { strbuf_release(&f); return; } /* Pick up the string around '@', possibly delimited with <> * pair; that is the email part. */ while (at > f.buf) { char c = at[-1]; if (isspace(c)) break; if (c == '<') { at[-1] = ' '; break; } at--; } el = strcspn(at, " \n\t\r\v\f>"); strbuf_reset(&mi->email); strbuf_add(&mi->email, at, el); strbuf_remove(&f, at - f.buf, el + (at[el] ? 1 : 0)); /* The remainder is name. It could be * * - "John Doe " (a), or * - "john.doe@xz (John Doe)" (b), or * - "John (zzz) Doe (Comment)" (c) * * but we have removed the email part, so * * - remove extra spaces which could stay after email (case 'c'), and * - trim from both ends, possibly removing the () pair at the end * (cases 'a' and 'b'). */ cleanup_space(&f); strbuf_trim(&f); if (f.buf[0] == '(' && f.len && f.buf[f.len - 1] == ')') { strbuf_remove(&f, 0, 1); strbuf_setlen(&f, f.len - 1); } get_sane_name(&mi->name, &f, &mi->email); strbuf_release(&f); } static void handle_header(struct strbuf **out, const struct strbuf *line) { if (!*out) { *out = xmalloc(sizeof(struct strbuf)); strbuf_init(*out, line->len); } else strbuf_reset(*out); strbuf_addbuf(*out, line); } /* NOTE NOTE NOTE. We do not claim we do full MIME. We just attempt * to have enough heuristics to grok MIME encoded patches often found * on our mailing lists. For example, we do not even treat header lines * case insensitively. */ static int slurp_attr(const char *line, const char *name, struct strbuf *attr) { const char *ends, *ap = strcasestr(line, name); size_t sz; strbuf_setlen(attr, 0); if (!ap) return 0; ap += strlen(name); if (*ap == '"') { ap++; ends = "\""; } else ends = "; \t"; sz = strcspn(ap, ends); strbuf_add(attr, ap, sz); return 1; } static void handle_content_type(struct mailinfo *mi, struct strbuf *line) { struct strbuf *boundary = xmalloc(sizeof(struct strbuf)); strbuf_init(boundary, line->len); if (slurp_attr(line->buf, "boundary=", boundary)) { strbuf_insert(boundary, 0, "--", 2); if (++mi->content_top >= &mi->content[MAX_BOUNDARIES]) { fprintf(stderr, "Too many boundaries to handle\n"); exit(1); } *(mi->content_top) = boundary; boundary = NULL; } slurp_attr(line->buf, "charset=", &mi->charset); if (boundary) { strbuf_release(boundary); free(boundary); } } static void handle_message_id(struct mailinfo *mi, const struct strbuf *line) { if (mi->add_message_id) mi->message_id = strdup(line->buf); } static void handle_content_transfer_encoding(struct mailinfo *mi, const struct strbuf *line) { if (strcasestr(line->buf, "base64")) mi->transfer_encoding = TE_BASE64; else if (strcasestr(line->buf, "quoted-printable")) mi->transfer_encoding = TE_QP; else mi->transfer_encoding = TE_DONTCARE; } static int is_multipart_boundary(struct mailinfo *mi, const struct strbuf *line) { struct strbuf *content_top = *(mi->content_top); return ((content_top->len <= line->len) && !memcmp(line->buf, content_top->buf, content_top->len)); } static void cleanup_subject(struct mailinfo *mi, struct strbuf *subject) { size_t at = 0; while (at < subject->len) { char *pos; size_t remove; switch (subject->buf[at]) { case 'r': case 'R': if (subject->len <= at + 3) break; if ((subject->buf[at + 1] == 'e' || subject->buf[at + 1] == 'E') && subject->buf[at + 2] == ':') { strbuf_remove(subject, at, 3); continue; } at++; break; case ' ': case '\t': case ':': strbuf_remove(subject, at, 1); continue; case '[': pos = strchr(subject->buf + at, ']'); if (!pos) break; remove = pos - subject->buf + at + 1; if (!mi->keep_non_patch_brackets_in_subject || (7 <= remove && memmem(subject->buf + at, remove, "PATCH", 5))) strbuf_remove(subject, at, remove); else { at += remove; /* * If the input had a space after the ], keep * it. We don't bother with finding the end of * the space, since we later normalize it * anyway. */ if (isspace(subject->buf[at])) at += 1; } continue; } break; } strbuf_trim(subject); } #define MAX_HDR_PARSED 10 static const char *header[MAX_HDR_PARSED] = { "From","Subject","Date", }; static inline int cmp_header(const struct strbuf *line, const char *hdr) { int len = strlen(hdr); return !strncasecmp(line->buf, hdr, len) && line->len > len && line->buf[len] == ':' && isspace(line->buf[len + 1]); } static int is_format_patch_separator(const char *line, int len) { static const char SAMPLE[] = "From e6807f3efca28b30decfecb1732a56c7db1137ee Mon Sep 17 00:00:00 2001\n"; const char *cp; if (len != strlen(SAMPLE)) return 0; if (!skip_prefix(line, "From ", &cp)) return 0; if (strspn(cp, "0123456789abcdef") != 40) return 0; cp += 40; return !memcmp(SAMPLE + (cp - line), cp, strlen(SAMPLE) - (cp - line)); } static struct strbuf *decode_q_segment(const struct strbuf *q_seg, int rfc2047) { const char *in = q_seg->buf; int c; struct strbuf *out = xmalloc(sizeof(struct strbuf)); strbuf_init(out, q_seg->len); while ((c = *in++) != 0) { if (c == '=') { int d = *in++; if (d == '\n' || !d) break; /* drop trailing newline */ strbuf_addch(out, (hexval(d) << 4) | hexval(*in++)); continue; } if (rfc2047 && c == '_') /* rfc2047 4.2 (2) */ c = 0x20; strbuf_addch(out, c); } return out; } static struct strbuf *decode_b_segment(const struct strbuf *b_seg) { /* Decode in..ep, possibly in-place to ot */ int c, pos = 0, acc = 0; const char *in = b_seg->buf; struct strbuf *out = xmalloc(sizeof(struct strbuf)); strbuf_init(out, b_seg->len); while ((c = *in++) != 0) { if (c == '+') c = 62; else if (c == '/') c = 63; else if ('A' <= c && c <= 'Z') c -= 'A'; else if ('a' <= c && c <= 'z') c -= 'a' - 26; else if ('0' <= c && c <= '9') c -= '0' - 52; else continue; /* garbage */ switch (pos++) { case 0: acc = (c << 2); break; case 1: strbuf_addch(out, (acc | (c >> 4))); acc = (c & 15) << 4; break; case 2: strbuf_addch(out, (acc | (c >> 2))); acc = (c & 3) << 6; break; case 3: strbuf_addch(out, (acc | c)); acc = pos = 0; break; } } return out; } static int convert_to_utf8(struct mailinfo *mi, struct strbuf *line, const char *charset) { char *out; if (!mi->metainfo_charset || !charset || !*charset) return 0; if (same_encoding(mi->metainfo_charset, charset)) return 0; out = reencode_string(line->buf, mi->metainfo_charset, charset); if (!out) return error("cannot convert from %s to %s", charset, mi->metainfo_charset); strbuf_attach(line, out, strlen(out), strlen(out)); return 0; } static void decode_header(struct mailinfo *mi, struct strbuf *it) { char *in, *ep, *cp; struct strbuf outbuf = STRBUF_INIT, *dec; struct strbuf charset_q = STRBUF_INIT, piecebuf = STRBUF_INIT; in = it->buf; while (in - it->buf <= it->len && (ep = strstr(in, "=?")) != NULL) { int encoding; strbuf_reset(&charset_q); strbuf_reset(&piecebuf); if (in != ep) { /* * We are about to process an encoded-word * that begins at ep, but there is something * before the encoded word. */ char *scan; for (scan = in; scan < ep; scan++) if (!isspace(*scan)) break; if (scan != ep || in == it->buf) { /* * We should not lose that "something", * unless we have just processed an * encoded-word, and there is only LWS * before the one we are about to process. */ strbuf_add(&outbuf, in, ep - in); } } /* E.g. * ep : "=?iso-2022-jp?B?GyR...?= foo" * ep : "=?ISO-8859-1?Q?Foo=FCbar?= baz" */ ep += 2; if (ep - it->buf >= it->len || !(cp = strchr(ep, '?'))) goto release_return; if (cp + 3 - it->buf > it->len) goto release_return; strbuf_add(&charset_q, ep, cp - ep); encoding = cp[1]; if (!encoding || cp[2] != '?') goto release_return; ep = strstr(cp + 3, "?="); if (!ep) goto release_return; strbuf_add(&piecebuf, cp + 3, ep - cp - 3); switch (tolower(encoding)) { default: goto release_return; case 'b': dec = decode_b_segment(&piecebuf); break; case 'q': dec = decode_q_segment(&piecebuf, 1); break; } if (convert_to_utf8(mi, dec, charset_q.buf)) goto release_return; strbuf_addbuf(&outbuf, dec); strbuf_release(dec); free(dec); in = ep + 2; } strbuf_addstr(&outbuf, in); strbuf_reset(it); strbuf_addbuf(it, &outbuf); release_return: strbuf_release(&outbuf); strbuf_release(&charset_q); strbuf_release(&piecebuf); } static int check_header(struct mailinfo *mi, const struct strbuf *line, struct strbuf *hdr_data[], int overwrite) { int i, ret = 0, len; struct strbuf sb = STRBUF_INIT; /* search for the interesting parts */ for (i = 0; header[i]; i++) { int len = strlen(header[i]); if ((!hdr_data[i] || overwrite) && cmp_header(line, header[i])) { /* Unwrap inline B and Q encoding, and optionally * normalize the meta information to utf8. */ strbuf_add(&sb, line->buf + len + 2, line->len - len - 2); decode_header(mi, &sb); handle_header(&hdr_data[i], &sb); ret = 1; goto check_header_out; } } /* Content stuff */ if (cmp_header(line, "Content-Type")) { len = strlen("Content-Type: "); strbuf_add(&sb, line->buf + len, line->len - len); decode_header(mi, &sb); strbuf_insert(&sb, 0, "Content-Type: ", len); handle_content_type(mi, &sb); ret = 1; goto check_header_out; } if (cmp_header(line, "Content-Transfer-Encoding")) { len = strlen("Content-Transfer-Encoding: "); strbuf_add(&sb, line->buf + len, line->len - len); decode_header(mi, &sb); handle_content_transfer_encoding(mi, &sb); ret = 1; goto check_header_out; } if (cmp_header(line, "Message-Id")) { len = strlen("Message-Id: "); strbuf_add(&sb, line->buf + len, line->len - len); decode_header(mi, &sb); handle_message_id(mi, &sb); ret = 1; goto check_header_out; } /* for inbody stuff */ if (starts_with(line->buf, ">From") && isspace(line->buf[5])) { ret = is_format_patch_separator(line->buf + 1, line->len - 1); goto check_header_out; } if (starts_with(line->buf, "[PATCH]") && isspace(line->buf[7])) { for (i = 0; header[i]; i++) { if (!strcmp("Subject", header[i])) { handle_header(&hdr_data[i], line); ret = 1; goto check_header_out; } } } check_header_out: strbuf_release(&sb); return ret; } static void decode_transfer_encoding(struct mailinfo *mi, struct strbuf *line) { struct strbuf *ret; switch (mi->transfer_encoding) { case TE_QP: ret = decode_q_segment(line, 0); break; case TE_BASE64: ret = decode_b_segment(line); break; case TE_DONTCARE: default: return; } strbuf_reset(line); strbuf_addbuf(line, ret); strbuf_release(ret); free(ret); } static inline int patchbreak(const struct strbuf *line) { size_t i; /* Beginning of a "diff -" header? */ if (starts_with(line->buf, "diff -")) return 1; /* CVS "Index: " line? */ if (starts_with(line->buf, "Index: ")) return 1; /* * "--- " starts patches without headers * "---*" is a manual separator */ if (line->len < 4) return 0; if (starts_with(line->buf, "---")) { /* space followed by a filename? */ if (line->buf[3] == ' ' && !isspace(line->buf[4])) return 1; /* Just whitespace? */ for (i = 3; i < line->len; i++) { unsigned char c = line->buf[i]; if (c == '\n') return 1; if (!isspace(c)) break; } return 0; } return 0; } static int is_scissors_line(const struct strbuf *line) { size_t i, len = line->len; int scissors = 0, gap = 0; int first_nonblank = -1; int last_nonblank = 0, visible, perforation = 0, in_perforation = 0; const char *buf = line->buf; for (i = 0; i < len; i++) { if (isspace(buf[i])) { if (in_perforation) { perforation++; gap++; } continue; } last_nonblank = i; if (first_nonblank < 0) first_nonblank = i; if (buf[i] == '-') { in_perforation = 1; perforation++; continue; } if (i + 1 < len && (!memcmp(buf + i, ">8", 2) || !memcmp(buf + i, "8<", 2) || !memcmp(buf + i, ">%", 2) || !memcmp(buf + i, "%<", 2))) { in_perforation = 1; perforation += 2; scissors += 2; i++; continue; } in_perforation = 0; } /* * The mark must be at least 8 bytes long (e.g. "-- >8 --"). * Even though there can be arbitrary cruft on the same line * (e.g. "cut here"), in order to avoid misidentification, the * perforation must occupy more than a third of the visible * width of the line, and dashes and scissors must occupy more * than half of the perforation. */ visible = last_nonblank - first_nonblank + 1; return (scissors && 8 <= visible && visible < perforation * 3 && gap * 2 < perforation); } static int handle_commit_msg(struct mailinfo *mi, struct strbuf *line) { assert(!mi->filter_stage); if (mi->header_stage) { if (!line->len || (line->len == 1 && line->buf[0] == '\n')) return 0; } if (mi->use_inbody_headers && mi->header_stage) { mi->header_stage = check_header(mi, line, mi->s_hdr_data, 0); if (mi->header_stage) return 0; } else /* Only trim the first (blank) line of the commit message * when ignoring in-body headers. */ mi->header_stage = 0; /* normalize the log message to UTF-8. */ if (convert_to_utf8(mi, line, mi->charset.buf)) exit(128); if (mi->use_scissors && is_scissors_line(line)) { int i; strbuf_setlen(&mi->log_message, 0); mi->header_stage = 1; /* * We may have already read "secondary headers"; purge * them to give ourselves a clean restart. */ for (i = 0; header[i]; i++) { if (mi->s_hdr_data[i]) strbuf_release(mi->s_hdr_data[i]); mi->s_hdr_data[i] = NULL; } return 0; } if (patchbreak(line)) { if (mi->message_id) strbuf_addf(&mi->log_message, "Message-Id: %s\n", mi->message_id); return 1; } strbuf_addbuf(&mi->log_message, line); return 0; } static void handle_patch(struct mailinfo *mi, const struct strbuf *line) { fwrite(line->buf, 1, line->len, mi->patchfile); mi->patch_lines++; } static void handle_filter(struct mailinfo *mi, struct strbuf *line) { switch (mi->filter_stage) { case 0: if (!handle_commit_msg(mi, line)) break; mi->filter_stage++; case 1: handle_patch(mi, line); break; } } static int is_rfc2822_header(const struct strbuf *line) { /* * The section that defines the loosest possible * field name is "3.6.8 Optional fields". * * optional-field = field-name ":" unstructured CRLF * field-name = 1*ftext * ftext = %d33-57 / %59-126 */ int ch; char *cp = line->buf; /* Count mbox From headers as headers */ if (starts_with(cp, "From ") || starts_with(cp, ">From ")) return 1; while ((ch = *cp++)) { if (ch == ':') return 1; if ((33 <= ch && ch <= 57) || (59 <= ch && ch <= 126)) continue; break; } return 0; } static int read_one_header_line(struct strbuf *line, FILE *in) { struct strbuf continuation = STRBUF_INIT; /* Get the first part of the line. */ if (strbuf_getline(line, in, '\n')) return 0; /* * Is it an empty line or not a valid rfc2822 header? * If so, stop here, and return false ("not a header") */ strbuf_rtrim(line); if (!line->len || !is_rfc2822_header(line)) { /* Re-add the newline */ strbuf_addch(line, '\n'); return 0; } /* * Now we need to eat all the continuation lines.. * Yuck, 2822 header "folding" */ for (;;) { int peek; peek = fgetc(in); ungetc(peek, in); if (peek != ' ' && peek != '\t') break; if (strbuf_getline(&continuation, in, '\n')) break; continuation.buf[0] = ' '; strbuf_rtrim(&continuation); strbuf_addbuf(line, &continuation); } strbuf_release(&continuation); return 1; } static int find_boundary(struct mailinfo *mi, struct strbuf *line) { while (!strbuf_getline(line, mi->input, '\n')) { if (*(mi->content_top) && is_multipart_boundary(mi, line)) return 1; } return 0; } static int handle_boundary(struct mailinfo *mi, struct strbuf *line) { struct strbuf newline = STRBUF_INIT; strbuf_addch(&newline, '\n'); again: if (line->len >= (*(mi->content_top))->len + 2 && !memcmp(line->buf + (*(mi->content_top))->len, "--", 2)) { /* we hit an end boundary */ /* pop the current boundary off the stack */ strbuf_release(*(mi->content_top)); free(*(mi->content_top)); *(mi->content_top) = NULL; /* technically won't happen as is_multipart_boundary() will fail first. But just in case.. */ if (--mi->content_top < mi->content) { fprintf(stderr, "Detected mismatched boundaries, " "can't recover\n"); exit(1); } handle_filter(mi, &newline); strbuf_release(&newline); /* skip to the next boundary */ if (!find_boundary(mi, line)) return 0; goto again; } /* set some defaults */ mi->transfer_encoding = TE_DONTCARE; strbuf_reset(&mi->charset); /* slurp in this section's info */ while (read_one_header_line(line, mi->input)) check_header(mi, line, mi->p_hdr_data, 0); strbuf_release(&newline); /* replenish line */ if (strbuf_getline(line, mi->input, '\n')) return 0; strbuf_addch(line, '\n'); return 1; } static void handle_body(struct mailinfo *mi, struct strbuf *line) { struct strbuf prev = STRBUF_INIT; /* Skip up to the first boundary */ if (*(mi->content_top)) { if (!find_boundary(mi, line)) goto handle_body_out; } do { /* process any boundary lines */ if (*(mi->content_top) && is_multipart_boundary(mi, line)) { /* flush any leftover */ if (prev.len) { handle_filter(mi, &prev); strbuf_reset(&prev); } if (!handle_boundary(mi, line)) goto handle_body_out; } /* Unwrap transfer encoding */ decode_transfer_encoding(mi, line); switch (mi->transfer_encoding) { case TE_BASE64: case TE_QP: { struct strbuf **lines, **it, *sb; /* Prepend any previous partial lines */ strbuf_insert(line, 0, prev.buf, prev.len); strbuf_reset(&prev); /* * This is a decoded line that may contain * multiple new lines. Pass only one chunk * at a time to handle_filter() */ lines = strbuf_split(line, '\n'); for (it = lines; (sb = *it); it++) { if (*(it + 1) == NULL) /* The last line */ if (sb->buf[sb->len - 1] != '\n') { /* Partial line, save it for later. */ strbuf_addbuf(&prev, sb); break; } handle_filter(mi, sb); } /* * The partial chunk is saved in "prev" and will be * appended by the next iteration of read_line_with_nul(). */ strbuf_list_free(lines); break; } default: handle_filter(mi, line); } } while (!strbuf_getwholeline(line, mi->input, '\n')); handle_body_out: strbuf_release(&prev); } static void output_header_lines(FILE *fout, const char *hdr, const struct strbuf *data) { const char *sp = data->buf; while (1) { char *ep = strchr(sp, '\n'); int len; if (!ep) len = strlen(sp); else len = ep - sp; fprintf(fout, "%s: %.*s\n", hdr, len, sp); if (!ep) break; sp = ep + 1; } } static void handle_info(struct mailinfo *mi) { struct strbuf *hdr; int i; for (i = 0; header[i]; i++) { /* only print inbody headers if we output a patch file */ if (mi->patch_lines && mi->s_hdr_data[i]) hdr = mi->s_hdr_data[i]; else if (mi->p_hdr_data[i]) hdr = mi->p_hdr_data[i]; else continue; if (!strcmp(header[i], "Subject")) { if (!mi->keep_subject) { cleanup_subject(mi, hdr); cleanup_space(hdr); } output_header_lines(mi->output, "Subject", hdr); } else if (!strcmp(header[i], "From")) { cleanup_space(hdr); handle_from(mi, hdr); fprintf(mi->output, "Author: %s\n", mi->name.buf); fprintf(mi->output, "Email: %s\n", mi->email.buf); } else { cleanup_space(hdr); fprintf(mi->output, "%s: %s\n", header[i], hdr->buf); } } fprintf(mi->output, "\n"); } int mailinfo(struct mailinfo *mi, const char *msg, const char *patch) { FILE *cmitmsg; int peek; struct strbuf line = STRBUF_INIT; cmitmsg = fopen(msg, "w"); if (!cmitmsg) { perror(msg); return -1; } mi->patchfile = fopen(patch, "w"); if (!mi->patchfile) { perror(patch); fclose(cmitmsg); return -1; } mi->p_hdr_data = xcalloc(MAX_HDR_PARSED, sizeof(*(mi->p_hdr_data))); mi->s_hdr_data = xcalloc(MAX_HDR_PARSED, sizeof(*(mi->s_hdr_data))); do { peek = fgetc(mi->input); } while (isspace(peek)); ungetc(peek, mi->input); /* process the email header */ while (read_one_header_line(&line, mi->input)) check_header(mi, &line, mi->p_hdr_data, 1); handle_body(mi, &line); fwrite(mi->log_message.buf, 1, mi->log_message.len, cmitmsg); fclose(cmitmsg); fclose(mi->patchfile); handle_info(mi); strbuf_release(&line); return 0; } static int git_mailinfo_config(const char *var, const char *value, void *mi_) { struct mailinfo *mi = mi_; if (!starts_with(var, "mailinfo.")) return git_default_config(var, value, NULL); if (!strcmp(var, "mailinfo.scissors")) { mi->use_scissors = git_config_bool(var, value); return 0; } /* perhaps others here */ return 0; } void setup_mailinfo(struct mailinfo *mi) { memset(mi, 0, sizeof(*mi)); strbuf_init(&mi->name, 0); strbuf_init(&mi->email, 0); strbuf_init(&mi->charset, 0); strbuf_init(&mi->log_message, 0); mi->header_stage = 1; mi->use_inbody_headers = 1; mi->content_top = mi->content; git_config(git_mailinfo_config, &mi); } void clear_mailinfo(struct mailinfo *mi) { int i; strbuf_release(&mi->name); strbuf_release(&mi->email); strbuf_release(&mi->charset); free(mi->message_id); for (i = 0; mi->p_hdr_data[i]; i++) strbuf_release(mi->p_hdr_data[i]); free(mi->p_hdr_data); for (i = 0; mi->s_hdr_data[i]; i++) strbuf_release(mi->s_hdr_data[i]); free(mi->s_hdr_data); while (mi->content < mi->content_top) { free(*(mi->content_top)); mi->content_top--; } strbuf_release(&mi->log_message); }