romele / git-trace public
62 commits · 1 branch · 1 tag · 1 contributor (top: Romele) · first commit 3d ago · last push 10m ago
src/patchset.c
#include "patchset.h" #include "auth.h" #include "collab.h" #include "gitx.h" #include "repo.h" #include "util.h" #include "webhook.h" #include <ctype.h> #include <fcntl.h> #include <limits.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> /* ============ mbox parsing (git format-patch output) ============ * A series is split at "^From <40-hex-sha> " lines. Each chunk is a * mail: headers (From:, Date:, Subject:), a blank line, the message * body up to "---", then the diff. */ struct patch { const char *chunk; size_t clen; char author_name[128]; char author_mail[128]; char date[64]; char subject[300]; const char *body; /* message body, before "---" */ size_t blen; const char *diff; /* first "diff --git" line onward */ size_t dlen; int version; /* [PATCH vN ...] tag; 1 when absent */ }; static int is_from_line(const char *l, size_t n) { if (n < 47 || memcmp(l, "From ", 5) != 0) return 0; for (int i = 0; i < 40; i++) if (!isxdigit((unsigned char)l[5 + i])) return 0; return l[45] == ' '; } /* find next line start, or end */ static const char *next_line(const char *p, const char *end) { const char *nl = memchr(p, '\n', end - p); return nl ? nl + 1 : end; } static void hdr_value(const char *chunk, size_t clen, const char *name, char *out, size_t outlen) { out[0] = '\0'; size_t nl_len = strlen(name); const char *end = chunk + clen; for (const char *l = chunk; l < end; l = next_line(l, end)) { size_t ln = (size_t)(next_line(l, end) - l); if (ln <= 1) break; /* end of headers */ if (ln >= nl_len + 1 && strncasecmp(l, name, nl_len) == 0 && l[nl_len] == ':') { const char *v = l + nl_len + 1; while (v < end && (*v == ' ' || *v == '\t')) v++; const char *ve = l + ln; while (ve > v && (ve[-1] == '\n' || ve[-1] == '\r')) ve--; size_t n = (size_t)(ve - v); if (n >= outlen) n = outlen - 1; memcpy(out, v, n); out[n] = '\0'; /* folded continuation lines: append " rest" roughly */ for (const char *c = next_line(l, end); c < end && (*c == ' ' || *c == '\t'); c = next_line(c, end)) { size_t cn = (size_t)(next_line(c, end) - c); const char *cv = c; while (cv < c + cn && (*cv == ' ' || *cv == '\t')) cv++; size_t cur = strlen(out); if (cur + 1 < outlen) { out[cur++] = ' '; size_t add = cn - (size_t)(cv - c); while (add > 0 && (cv[add-1] == '\n' || cv[add-1] == '\r')) add--; if (cur + add >= outlen) add = outlen - 1 - cur; memcpy(out + cur, cv, add); out[cur + add] = '\0'; } } return; } } } static void split_author(const char *v, char *name, size_t nl, char *mail, size_t ml) { name[0] = mail[0] = '\0'; const char *lt = strchr(v, '<'); const char *gt = lt ? strchr(lt, '>') : NULL; if (lt && gt) { size_t n = (size_t)(lt - v); while (n && v[n - 1] == ' ') n--; if (n >= nl) n = nl - 1; memcpy(name, v, n); name[n] = '\0'; size_t m = (size_t)(gt - lt - 1); if (m >= ml) m = ml - 1; memcpy(mail, lt + 1, m); mail[m] = '\0'; } else { size_t x = strlen(v); if (x >= nl) x = nl - 1; memcpy(name, v, x); name[x] = '\0'; } } /* "[PATCH vN n/m] foo" / "[PATCH n/m] foo" -> "foo"; returns vN (1 * when the tag carries no version). */ static int strip_patch_tag(char *s) { int ver = 1; char *b = strchr(s, '['); char *e = b ? strchr(b, ']') : NULL; if (b && e && e - b >= 6 && memcmp(b, "[PATCH", 6) == 0) { const char *t = b + 6; while (t < e && *t == ' ') t++; if (t < e && *t == 'v') { long v = strtol(t + 1, NULL, 10); if (v > 0 && v < 1000) ver = (int)v; } char *p = e + 1; while (*p == ' ') p++; memmove(s, p, strlen(p) + 1); } return ver; } /* parse one mail chunk; returns 0 ok */ static int parse_patch(const char *chunk, size_t clen, struct patch *p) { const char *end = chunk + clen; p->chunk = chunk; p->clen = clen; p->body = p->diff = NULL; p->blen = p->dlen = 0; char a[256]; hdr_value(chunk, clen, "From", a, sizeof a); split_author(a, p->author_name, sizeof p->author_name, p->author_mail, sizeof p->author_mail); hdr_value(chunk, clen, "Subject", p->subject, sizeof p->subject); hdr_value(chunk, clen, "Date", p->date, sizeof p->date); p->version = strip_patch_tag(p->subject); /* body: after first blank line, until "---" or first "diff --git" */ const char *l = chunk; while (l < end) { size_t ln = (size_t)(next_line(l, end) - l); if (ln <= 2 && (l[0] == '\n' || l[0] == '\r')) { l = next_line(l, end); break; } l = next_line(l, end); } p->body = l; const char *b = l; int seen_sep = 0; while (b < end) { const char *nl = next_line(b, end); size_t ln = (size_t)(nl - b); if (!seen_sep && ln >= 4 && ln <= 5 && b[0] == '-' && b[1] == '-' && b[2] == '-' && (b[3] == '\n' || b[3] == '\r')) { seen_sep = 1; /* "---" mail separator */ p->blen = (size_t)(b - p->body); } else if (ln >= 11 && memcmp(b, "diff --git ", 11) == 0) { p->diff = b; p->dlen = (size_t)(end - b); if (!seen_sep) p->blen = (size_t)(b - p->body); break; } b = nl; } if (!seen_sep && !p->diff) p->blen = (size_t)(end - p->body); return 0; } static int is_cover(const struct patch *p) { return p->diff == NULL; } static struct patch *mbox_parse(const char *mbox, size_t len, int *outn) { /* count patches: number of "From <sha>" lines */ int np = 0; const char *end = mbox + len; for (const char *l = mbox; l < end; l = next_line(l, end)) { size_t ln = (size_t)(next_line(l, end) - l); if (is_from_line(l, ln)) np++; } if (np == 0) return NULL; struct patch *ps = xmalloc(sizeof *ps * (size_t)np); int i = 0; const char *start = NULL; for (const char *l = mbox; l < end; l = next_line(l, end)) { size_t ln = (size_t)(next_line(l, end) - l); if (is_from_line(l, ln)) { if (start) parse_patch(start, (size_t)(l - start), &ps[i++]); start = l; } } if (start) parse_patch(start, (size_t)(end - start), &ps[i++]); *outn = i; return ps; } /* ============ server-side apply (no work tree) ============ * A scratch index is seeded from the base commit; each patch diff is * `git apply --cached` into it, then committed with the original author * identity. The destination ref moves via compare-and-swap. */ static int mk_temp(char *path, size_t n, const char *tag) { snprintf(path, n, "/tmp/gt-%s-%ld-XXXXXX", tag, (long)getpid()); int fd = mkstemp(path); return fd; } /* run git with GIT_INDEX_FILE=idx (+ author env for commit-tree) */ static int git_idx(const char *repo, const char *idx, const char *const extra_env[], const char *const argv[], struct buf *out) { char idxenv[600]; snprintf(idxenv, sizeof idxenv, "GIT_INDEX_FILE=%s", idx); const char *envs[16]; int i = 0; envs[i++] = idxenv; if (extra_env) for (int k = 0; extra_env[k] && i < 15; k++) envs[i++] = extra_env[k]; envs[i] = NULL; return git_capture_env(repo, envs, argv, out); } /* returns 0 applied, 1 patch failed, -1 error. * target == NULL updates the base ref itself; otherwise the series * lands on refs/heads/<target> created fresh from the base tip * (CAS on the null old value — refuses to overwrite). * tip receives the final commit sha (if applied). */ static int apply_series(const char *repo, const char *base_ref, const char *target, struct patch *ps, int np, int check_only, const char *committer, char tip[41], struct buf *err) { char idx[256], pfile[256]; int ifd = mk_temp(idx, sizeof idx, "idx"); if (ifd < 0) { buf_puts(err, "cannot create temp index"); return -1; } close(ifd); unlink(idx); /* git creates it on first write */ struct buf out; buf_init(&out); const char *rt[] = { "git", "read-tree", base_ref, NULL }; if (git_idx(repo, idx, NULL, rt, &out) != 0) { buf_puts(err, "read-tree failed on base"); goto fail; } char base[41]; { char ref[300]; snprintf(ref, sizeof ref, "refs/heads/%s", base_ref); struct buf rb; buf_init(&rb); const char *rv[] = { "git", "rev-parse", "--verify", "-q", ref, NULL }; if (git_capture(repo, rv, &rb) != 0 || rb.len < 40) { buf_free(&rb); buf_puts(err, "unknown base branch"); goto fail; } memcpy(base, rb.data, 40); base[40] = '\0'; buf_free(&rb); } int pfd = mk_temp(pfile, sizeof pfile, "patch"); if (pfd < 0) { buf_puts(err, "cannot create temp patch file"); goto fail; } char cur[41]; memcpy(cur, base, 41); int applied = 0; for (int i = 0; i < np; i++) { if (is_cover(&ps[i])) continue; /* rewrite the temp patch file with this diff */ if (ftruncate(pfd, 0) != 0 || lseek(pfd, 0, SEEK_SET) < 0) goto io_fail; size_t left = ps[i].dlen; const char *dp = ps[i].diff; while (left) { ssize_t w = write(pfd, dp, left); if (w <= 0) goto io_fail; dp += w; left -= (size_t)w; } fsync(pfd); out.len = 0; if (out.data) out.data[0] = '\0'; const char *ap[] = { "git", "apply", "--cached", "--whitespace=nowarn", "-v", pfile, NULL }; if (git_idx(repo, idx, NULL, ap, &out) != 0) { buf_printf(err, "patch %d (%s) does not apply cleanly:\n", i + 1, ps[i].subject); if (out.data) buf_put(err, out.data, out.len); goto patch_fail; } applied++; if (check_only) continue; out.len = 0; const char *wt[] = { "git", "write-tree", NULL }; if (git_idx(repo, idx, NULL, wt, &out) != 0 || out.len < 40) goto patch_fail; char tree[41]; memcpy(tree, out.data, 40); tree[40] = '\0'; char an[160], am[160], ad[80], cn[160], ce[160]; snprintf(an, sizeof an, "GIT_AUTHOR_NAME=%s", ps[i].author_name[0] ? ps[i].author_name : "unknown"); snprintf(am, sizeof am, "GIT_AUTHOR_EMAIL=%s", ps[i].author_mail[0] ? ps[i].author_mail : "unknown"); snprintf(ad, sizeof ad, "GIT_AUTHOR_DATE=%s", ps[i].date); snprintf(cn, sizeof cn, "GIT_COMMITTER_NAME=%s", committer); snprintf(ce, sizeof ce, "GIT_COMMITTER_EMAIL=%s@git-trace", committer); const char *cenv[] = { an, am, ad, cn, ce, NULL }; /* message: subject + body up to "---" */ char msg[8192]; int ml = snprintf(msg, sizeof msg, "%s", ps[i].subject); if (ps[i].blen > 0) ml += snprintf(msg + ml, sizeof msg - (size_t)ml, "\n\n%.*s", (int)ps[i].blen, ps[i].body); out.len = 0; const char *ct[] = { "git", "commit-tree", tree, "-p", cur, "-m", msg, NULL }; if (git_idx(repo, idx, cenv, ct, &out) != 0 || out.len < 40) goto patch_fail; memcpy(cur, out.data, 40); cur[40] = '\0'; } close(pfd); unlink(pfile); unlink(idx); if (!applied) { buf_puts(err, "series contains no patches"); buf_free(&out); return -1; } if (check_only) { buf_free(&out); return 0; } /* compare-and-swap the destination ref */ char ref[300]; const char *zero = "0000000000000000000000000000000000000000"; if (target) snprintf(ref, sizeof ref, "refs/heads/%s", target); else snprintf(ref, sizeof ref, "refs/heads/%s", base_ref); out.len = 0; const char *ur[] = { "git", "update-ref", ref, cur, target ? zero : base, NULL }; if (git_capture(repo, ur, &out) != 0) { buf_printf(err, "update-ref failed — %s", target ? "target branch exists" : "base moved meanwhile"); buf_free(&out); return -1; } memcpy(tip, cur, 41); buf_free(&out); return 0; io_fail: close(pfd); unlink(pfile); unlink(idx); buf_free(&out); return -1; patch_fail: close(pfd); unlink(pfile); fail: unlink(idx); buf_free(&out); return 1; } /* ============ pages ============ */ static void ps_list(struct app_ctx *app, struct request *r, const char *o, const char *n, int rk, int repo_id) { struct buf b; buf_init(&b); gt_page_top(&b, n, r); gt_repo_tabs(&b, app, o, n, rk, "changes"); if (r->user_id > 0) buf_printf(&b, "<p><a class=btn2 href='/%s/%s/patches/new'>" I_PLUS "submit patch set" "</a></p><p class=muted>or from your tree: " "<code>git format-patch --stdout <base>.. | " "curl -u user:token --data-binary @- " "/%s/%s/patches/new</code></p>", o, n, o, n); sqlite3_stmt *st; if (sqlite3_prepare_v2(app->db, "SELECT p.id,p.title,p.state,p.dst_branch," " COALESCE(u.name,'(deleted)')," " strftime('%Y-%m-%d', p.created_at, 'unixepoch')," " (SELECT MAX(version) FROM patch_revisions v" " WHERE v.patch_set_id=p.id)" " FROM patch_sets p LEFT JOIN users u ON u.id=p.author_id" " WHERE p.repo_id=?" " ORDER BY (p.state='open') DESC, p.id DESC LIMIT 200", -1, &st, NULL) != SQLITE_OK) { buf_free(&b); http_error(r, 500, "db error"); return; } sqlite3_bind_int(st, 1, repo_id); buf_puts(&b, "<table><tr><th>#</th><th>title</th><th>base</th>" "<th>state</th><th>author</th><th>date</th></tr>"); int any = 0; while (sqlite3_step(st) == SQLITE_ROW) { any = 1; buf_printf(&b, "<tr><td>%lld</td><td><a href='/%s/%s/patches/%lld'>", sqlite3_column_int64(st, 0), o, n, sqlite3_column_int64(st, 0)); gt_esc(&b, (const char *)sqlite3_column_text(st, 1)); if (sqlite3_column_int(st, 6) > 1) buf_printf(&b, " <span class=muted>v%d</span>", sqlite3_column_int(st, 6)); buf_puts(&b, "</a></td><td class=muted>"); gt_esc(&b, (const char *)sqlite3_column_text(st, 3)); buf_puts(&b, "</td><td class=muted>"); gt_esc(&b, (const char *)sqlite3_column_text(st, 2)); buf_puts(&b, "</td><td class=muted>"); gt_esc(&b, (const char *)sqlite3_column_text(st, 4)); buf_puts(&b, "</td><td class=muted>"); gt_esc(&b, (const char *)sqlite3_column_text(st, 5)); buf_puts(&b, "</td></tr>"); } sqlite3_finalize(st); if (!any) buf_puts(&b, "<tr><td class=muted colspan=6>no patch sets</td></tr>"); buf_puts(&b, "</table>"); gt_repo_body_end(&b); gt_page_bottom(&b); gt_send_page(r, &b); buf_free(&b); } static void ps_new_form(struct app_ctx *app, struct request *r, const char *o, const char *n, int rk, const char *defbase) { char setv[16] = ""; req_query(r, "set", setv, sizeof setv); long setid = setv[0] ? strtol(setv, NULL, 10) : 0; struct buf b; buf_init(&b); gt_page_top(&b, n, r); gt_repo_tabs(&b, app, o, n, rk, "changes"); if (setid > 0) { buf_printf(&b, "<h3>new version of patch set #%ld</h3>" "<form method=post action='/%s/%s/patches/new'>", setid, o, n); gt_csrf_input(&b, r); buf_printf(&b, "<input type=hidden name=set value=%ld>" "<p class=muted>title, cover letter and base follow the" " series being revised.</p>" "<p><textarea name=mbox rows=20 cols=90 required " "placeholder='git format-patch --stdout -v2 output'>" "</textarea></p>" "<p><button type=submit>submit new version</button></p>" "</form>", setid); } else { buf_printf(&b, "<h3>submit patch set</h3>" "<form method=post action='/%s/%s/patches/new'>", o, n); gt_csrf_input(&b, r); buf_printf(&b, "<p><input name=title maxlength=200 size=60 " "placeholder='title (optional: taken from cover " "letter)'></p>" "<p>base branch <input name=base value='%s'></p>" "<p><textarea name=mbox rows=20 cols=90 required " "placeholder='git format-patch --stdout output'>" "</textarea></p>" "<p><button type=submit>submit patch set" "</button></p>" "</form>", defbase); } gt_repo_body_end(&b); gt_page_bottom(&b); gt_send_page(r, &b); buf_free(&b); } static int ps_insert(struct app_ctx *app, int repo_id, int uid, const char *title, const char *cover, const char *base, const char *mbox, size_t mlen) { sqlite3_stmt *st; if (sqlite3_prepare_v2(app->db, "INSERT INTO patch_sets(repo_id,author_id,title,cover_letter," " dst_branch) VALUES(?,?,?,?,?)", -1, &st, NULL) != SQLITE_OK) return -1; sqlite3_bind_int(st, 1, repo_id); sqlite3_bind_int(st, 2, uid); sqlite3_bind_text(st, 3, title, -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 4, cover ? cover : "", -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 5, base, -1, SQLITE_TRANSIENT); if (sqlite3_step(st) != SQLITE_DONE) { sqlite3_finalize(st); return -1; } long id = sqlite3_last_insert_rowid(app->db); sqlite3_finalize(st); if (sqlite3_prepare_v2(app->db, "INSERT INTO patch_revisions(patch_set_id,version,mbox)" " VALUES(?,1,?)", -1, &st, NULL) != SQLITE_OK) return -1; sqlite3_bind_int64(st, 1, id); sqlite3_bind_blob(st, 2, mbox, (int)mlen, SQLITE_TRANSIENT); sqlite3_step(st); sqlite3_finalize(st); return (int)id; } /* attach a new revision to an existing patch set; the set's title, * cover letter and updated_at follow the latest series. * returns the new version number, or -1. */ static int ps_add_revision(struct app_ctx *app, long set_id, const char *title, const char *cover, const char *mbox, size_t mlen) { sqlite3_stmt *st; if (sqlite3_prepare_v2(app->db, "SELECT COALESCE(MAX(version),0) FROM patch_revisions" " WHERE patch_set_id=?", -1, &st, NULL) != SQLITE_OK) return -1; sqlite3_bind_int64(st, 1, set_id); int ver = 1; if (sqlite3_step(st) == SQLITE_ROW) ver = sqlite3_column_int(st, 0) + 1; sqlite3_finalize(st); if (sqlite3_prepare_v2(app->db, "INSERT INTO patch_revisions(patch_set_id,version,mbox)" " VALUES(?,?,?)", -1, &st, NULL) != SQLITE_OK) return -1; sqlite3_bind_int64(st, 1, set_id); sqlite3_bind_int(st, 2, ver); sqlite3_bind_blob(st, 3, mbox, (int)mlen, SQLITE_TRANSIENT); int ok = sqlite3_step(st) == SQLITE_DONE; sqlite3_finalize(st); if (!ok) return -1; sqlite3_prepare_v2(app->db, "UPDATE patch_sets SET title=?, cover_letter=?," " updated_at=strftime('%s','now') WHERE id=?", -1, &st, NULL); sqlite3_bind_text(st, 1, title, -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 2, cover ? cover : "", -1, SQLITE_TRANSIENT); sqlite3_bind_int64(st, 3, set_id); sqlite3_step(st); sqlite3_finalize(st); return ver; } /* result of a patch-set submission (new set or new revision) */ struct ps_out { long set_id; int ver; int is_new; int status; /* 0 ok, else an HTTP-style status */ char err[160]; }; static int ps_fail(struct ps_out *out, int status, const char *msg) { out->status = status; snprintf(out->err, sizeof out->err, "%s", msg); return -1; } /* * Shared submission core: web form, raw-mbox API and mailrecv all * funnel here. The caller resolves uid/rk; actor/ip feed the audit * journal (ip may be NULL). mbox must stay alive for the call. * Returns 0 on success (out->set_id/ver/is_new), -1 with out->status. */ /* fire a 'patch' webhook event; owner/name are resolved from the DB * so the same helper serves web submit and mailrecv alike */ static void ps_notify(struct app_ctx *app, long repo_id, long set_id, int ver, const char *title, const char *actor) { char on[200] = "?/?"; sqlite3_stmt *st; if (sqlite3_prepare_v2(app->db, "SELECT COALESCE(u.name,'_'), r.name FROM repos r" " LEFT JOIN users u ON u.id=r.owner_id WHERE r.id=?", -1, &st, NULL) == SQLITE_OK) { sqlite3_bind_int64(st, 1, repo_id); if (sqlite3_step(st) == SQLITE_ROW) { const char *o = (const char *)sqlite3_column_text(st, 0); const char *nm = (const char *)sqlite3_column_text(st, 1); snprintf(on, sizeof on, "%s/%s", o ? o : "?", nm ? nm : "?"); } sqlite3_finalize(st); } struct buf p; buf_init(&p); buf_puts(&p, "{\"event\":\"patch\",\"repo\":\""); json_put_esc(&p, on, strlen(on)); buf_printf(&p, "\",\"ps\":%ld,\"version\":%d,\"title\":\"", set_id, ver); json_put_esc(&p, title, strlen(title)); buf_puts(&p, "\",\"actor\":\""); if (actor) json_put_esc(&p, actor, strlen(actor)); buf_puts(&p, "\"}"); wh_notify(app, repo_id, "patch", p.data ? p.data : "{}"); buf_free(&p); } static int gt_ps_submit(struct app_ctx *app, const char *repo_dir, int repo_id, int uid, int rk, const char *actor, const char *ip, const char *title_in, const char *base_in, long set_req, const char *mbox, size_t mlen, struct ps_out *out) { memset(out, 0, sizeof *out); int np = 0; struct patch *ps = mbox_parse(mbox, mlen, &np); if (!ps || np == 0) { free(ps); return ps_fail(out, 400, "not a format-patch mbox"); } int real = 0, first_ver = 1; const char *first_subj = NULL, *cover_subj = NULL; char cover[8192] = ""; for (int i = 0; i < np; i++) { if (is_cover(&ps[i])) { if (!cover_subj) cover_subj = ps[i].subject; snprintf(cover, sizeof cover, "%.*s", (int)ps[i].blen > 8000 ? 8000 : (int)ps[i].blen, ps[i].body ? ps[i].body : ""); } else { real++; if (!first_subj) { first_subj = ps[i].subject; first_ver = ps[i].version; } } } if (!real) { free(ps); return ps_fail(out, 400, "series contains no patches"); } char title[210], base[256]; /* the cover subject is the series title when one is present */ snprintf(title, sizeof title, "%s", title_in && title_in[0] ? title_in : cover_subj ? cover_subj : first_subj ? first_subj : "patch set"); snprintf(base, sizeof base, "%s", base_in ? base_in : ""); char defbase[64] = ""; if (!base[0]) { char *d = git_default_branch(repo_dir); if (d) { snprintf(defbase, sizeof defbase, "%s", d); free(d); } snprintf(base, sizeof base, "%s", defbase[0] ? defbase : "master"); } if (!git_ref_ok(base)) { free(ps); return ps_fail(out, 400, "bad base branch"); } { char ref[300]; snprintf(ref, sizeof ref, "refs/heads/%s", base); struct buf rb; buf_init(&rb); const char *rv[] = { "git", "rev-parse", "--verify", "-q", ref, NULL }; int rc = git_capture(repo_dir, rv, &rb); int ok = (rc == 0 && rb.len >= 40); buf_free(&rb); if (!ok) { free(ps); return ps_fail(out, 400, "unknown base branch"); } } /* revision attach: explicit set wins; a [PATCH vN>=2] tag * auto-attaches when exactly one open set by the same author * carries the same title — `format-patch -v2` just works. */ long set_id = set_req; if (set_id <= 0 && first_ver >= 2) { sqlite3_stmt *st; if (sqlite3_prepare_v2(app->db, "SELECT id FROM patch_sets WHERE repo_id=?" " AND author_id=? AND title=? AND state='open'", -1, &st, NULL) == SQLITE_OK) { sqlite3_bind_int(st, 1, repo_id); sqlite3_bind_int(st, 2, uid); sqlite3_bind_text(st, 3, title, -1, SQLITE_TRANSIENT); long cand = 0; int cnt = 0; while (sqlite3_step(st) == SQLITE_ROW) { cand = sqlite3_column_int64(st, 0); cnt++; } if (cnt == 1) set_id = cand; sqlite3_finalize(st); } } if (set_id > 0) { sqlite3_stmt *st; sqlite3_prepare_v2(app->db, "SELECT author_id,state FROM patch_sets" " WHERE id=? AND repo_id=?", -1, &st, NULL); sqlite3_bind_int64(st, 1, set_id); sqlite3_bind_int(st, 2, repo_id); int author = -1; char sstate[16] = ""; if (sqlite3_step(st) == SQLITE_ROW) { author = sqlite3_column_int(st, 0); snprintf(sstate, sizeof sstate, "%s", (const char *)sqlite3_column_text(st, 1)); } sqlite3_finalize(st); if (author < 0) { free(ps); return ps_fail(out, 404, "no such patch set"); } if (strcmp(sstate, "open") != 0) { free(ps); return ps_fail(out, 409, "patch set not open"); } if (uid != author && rk < 3) { free(ps); return ps_fail(out, 403, "only the author can add revisions"); } int ver = ps_add_revision(app, set_id, title, cover, mbox, mlen); free(ps); if (ver < 0) return ps_fail(out, 500, "db error"); db_audit(app->db, uid, actor, ip, repo_id, "pset_rev", "#%ld v%d", set_id, ver); ps_notify(app, repo_id, set_id, ver, title, actor); out->set_id = set_id; out->ver = ver; return 0; } int id = ps_insert(app, repo_id, uid, title, cover, base, mbox, mlen); free(ps); if (id < 0) return ps_fail(out, 500, "db error"); db_audit(app->db, uid, actor, ip, repo_id, "pset_create", "#%d %.80s", id, title); ps_notify(app, repo_id, id, 1, title, actor); out->set_id = id; out->ver = 1; out->is_new = 1; return 0; } static void ps_create(struct app_ctx *app, struct request *r, const char *repo_dir, const char *o, const char *n, int rk, int repo_id) { if (!gt_require_post(r)) return; size_t blen; char *body = gt_read_form(r, 4 * 1024 * 1024, &blen); if (!body) { http_error(r, 400, "bad body (max 4 MiB)"); return; } char title[210] = "", base[256] = "", setv[24] = ""; const char *mbox = NULL; size_t mlen = 0; int is_form = 0; const char *ct = req_header(r, "Content-Type"); if (ct && strstr(ct, "form-urlencoded")) { is_form = 1; char *mb = xmalloc(blen + 1); if (form_get(body, blen, "mbox", mb, blen + 1) != 0 || !mb[0]) { free(mb); free(body); http_error(r, 400, "mbox required"); return; } if (!gt_csrf_ok(r, body, blen)) { free(mb); free(body); http_error(r, 403, "bad csrf token"); return; } form_get(body, blen, "title", title, sizeof title); form_get(body, blen, "base", base, sizeof base); form_get(body, blen, "set", setv, sizeof setv); mbox = mb; mlen = strlen(mb); /* keep mb alive through body lifetime use below */ free(body); body = mb; } else { /* raw mbox: API path (curl --data-binary @-). gt_csrf_ok * still enforces the token when a session cookie is present, * and requires an authenticated caller otherwise. */ if (!gt_csrf_ok(r, body, blen)) { free(body); http_error(r, 403, "bad csrf token"); return; } req_query(r, "base", base, sizeof base); req_query(r, "set", setv, sizeof setv); mbox = body; mlen = blen; } struct ps_out out; int rc = gt_ps_submit(app, repo_dir, repo_id, r->user_id, rk, r->user_name, r->remote_ip, title, base, setv[0] ? strtol(setv, NULL, 10) : 0, mbox, mlen, &out); free(body); if (rc != 0) { http_error(r, out.status, out.err); return; } char loc[256]; snprintf(loc, sizeof loc, "/%s/%s/patches/%ld", o, n, out.set_id); if (!is_form) { /* API clients get a plain-text answer */ char txt[300]; int tl = out.is_new ? snprintf(txt, sizeof txt, "patch set #%ld: %s\n", out.set_id, loc) : snprintf(txt, sizeof txt, "patch set #%ld v%d: %s\n", out.set_id, out.ver, loc); http_respond(r, 200, "text/plain", txt, (size_t)tl); return; } http_redirect(r, loc); } static void ps_view(struct app_ctx *app, struct request *r, const char *repo_dir, const char *o, const char *n, int rk, int repo_id, long id) { (void)repo_dir; sqlite3_stmt *st; if (sqlite3_prepare_v2(app->db, "SELECT p.title,p.state,p.dst_branch,p.cover_letter," " COALESCE(u.name,'(deleted)'),p.author_id," " strftime('%Y-%m-%d %H:%M', p.created_at, 'unixepoch')" " FROM patch_sets p LEFT JOIN users u ON u.id=p.author_id" " WHERE p.id=? AND p.repo_id=?", -1, &st, NULL) != SQLITE_OK) { http_error(r, 500, "db error"); return; } sqlite3_bind_int64(st, 1, id); sqlite3_bind_int(st, 2, repo_id); if (sqlite3_step(st) != SQLITE_ROW) { sqlite3_finalize(st); http_error(r, 404, "no such patch set"); return; } char title[210], state[16], base[256]; snprintf(title, sizeof title, "%s", (const char *)sqlite3_column_text(st, 0)); snprintf(state, sizeof state, "%s", (const char *)sqlite3_column_text(st, 1)); snprintf(base, sizeof base, "%s", (const char *)sqlite3_column_text(st, 2)); int author = sqlite3_column_int(st, 5); /* revision timeline: ?v=N picks one, default is the latest */ int vcount = 0, latest = 0; sqlite3_stmt *vs; if (sqlite3_prepare_v2(app->db, "SELECT version FROM patch_revisions WHERE patch_set_id=?" " ORDER BY version", -1, &vs, NULL) == SQLITE_OK) { sqlite3_bind_int64(vs, 1, id); while (sqlite3_step(vs) == SQLITE_ROW) { latest = sqlite3_column_int(vs, 0); vcount++; } sqlite3_finalize(vs); } char vq[16] = ""; req_query(r, "v", vq, sizeof vq); int cur = vq[0] ? atoi(vq) : 0; if (cur <= 0 || cur > latest) cur = latest; /* copy the mbox while the statement is alive: column_blob points * into the row buffer, which finalize() frees */ char *mcopy = NULL; int mlen = 0; if (cur > 0 && sqlite3_prepare_v2(app->db, "SELECT mbox FROM patch_revisions" " WHERE patch_set_id=? AND version=?", -1, &vs, NULL) == SQLITE_OK) { sqlite3_bind_int64(vs, 1, id); sqlite3_bind_int(vs, 2, cur); if (sqlite3_step(vs) == SQLITE_ROW) { const void *mb = sqlite3_column_blob(vs, 0); mlen = sqlite3_column_bytes(vs, 0); mcopy = xmalloc((size_t)mlen + 1); memcpy(mcopy, mb ? mb : "", (size_t)mlen); mcopy[mlen] = '\0'; } sqlite3_finalize(vs); } if (!mcopy) { mcopy = xmalloc(1); mcopy[0] = '\0'; } struct buf b; buf_init(&b); gt_page_top(&b, n, r); gt_repo_tabs(&b, app, o, n, rk, "changes"); buf_printf(&b, "<h3>patch set #%ld <span class=muted>(%s)</span>" " <span class=bpub>v%d</span></h3>", id, state, cur); if (vcount > 1) { buf_puts(&b, "<p class=muted>revisions:"); for (int v = 1; v <= vcount; v++) { if (v == cur) buf_printf(&b, " <b>v%d</b>", v); else buf_printf(&b, " <a href='/%s/%s/patches/%ld?v=%d'>v%d</a>", o, n, id, v, v); } buf_puts(&b, "</p>"); if (cur != latest) buf_printf(&b, "<p class=muted>viewing v%d — " "<a href='/%s/%s/patches/%ld'>latest is v%d</a>" "</p>", cur, o, n, id, latest); } buf_puts(&b, "<p><b>"); gt_esc(&b, title); buf_puts(&b, "</b></p><p class=muted>against "); gt_esc(&b, base); buf_puts(&b, " · by "); gt_esc(&b, (const char *)sqlite3_column_text(st, 4)); buf_puts(&b, " on "); gt_esc(&b, (const char *)sqlite3_column_text(st, 6)); buf_puts(&b, "</p>"); const char *cov = (const char *)sqlite3_column_text(st, 3); if (cov && cov[0]) { buf_puts(&b, "<pre style='border:0;background:none'>"); gt_esc(&b, cov); buf_puts(&b, "</pre>"); } sqlite3_finalize(st); /* patch list + combined diff */ int np = 0; struct patch *ps = mbox_parse(mcopy, (size_t)mlen, &np); if (ps) { buf_puts(&b, "<h4>series</h4><ol>"); int shown = 0; for (int i = 0; i < np; i++) { if (is_cover(&ps[i])) continue; shown++; buf_puts(&b, "<li>"); gt_esc(&b, ps[i].subject); buf_puts(&b, " <span class=muted>— "); gt_esc(&b, ps[i].author_name); buf_puts(&b, "</span></li>"); } buf_puts(&b, "</ol>"); buf_printf(&b, "<h4>diff (%d patch%s)</h4><pre>", shown, shown == 1 ? "" : "es"); for (int i = 0; i < np; i++) if (!is_cover(&ps[i]) && ps[i].diff) gt_render_diff(&b, ps[i].diff, ps[i].dlen); buf_puts(&b, "</pre>"); free(ps); } free(mcopy); buf_puts(&b, "<h4>comments</h4><table>"); gt_comment_list(&b, app->db, "patch_set", id); buf_puts(&b, "</table>"); /* cross-link: has this series been materialized into an MR? */ { char pbr[64]; snprintf(pbr, sizeof pbr, "psets/%ld", id); sqlite3_stmt *mst; if (sqlite3_prepare_v2(app->db, "SELECT id,state FROM merge_requests" " WHERE repo_id=? AND src_branch=?", -1, &mst, NULL) == SQLITE_OK) { sqlite3_bind_int(mst, 1, repo_id); sqlite3_bind_text(mst, 2, pbr, -1, SQLITE_TRANSIENT); if (sqlite3_step(mst) == SQLITE_ROW) buf_printf(&b, "<p class=muted>↔ merge request " "<a href='/%s/%s/mr/%lld'>#%lld</a> (%s)</p>", o, n, sqlite3_column_int64(mst, 0), sqlite3_column_int64(mst, 0), (const char *)sqlite3_column_text(mst, 1)); sqlite3_finalize(mst); } } if (r->user_id > 0 && strcmp(state, "open") == 0) { gt_comment_form(&b, r, ""); if (cur == latest) { buf_puts(&b, "<form method=post style='display:inline'>"); gt_csrf_input(&b, r); buf_puts(&b, "<input type=hidden name=action value=check>" "<button>check applies</button></form> "); if (rk >= 2) { buf_puts(&b, "<form method=post style='display:inline'>"); gt_csrf_input(&b, r); buf_puts(&b, "<input type=hidden name=action value=apply>" "<button>apply to base</button></form> "); buf_puts(&b, "<form method=post style='display:inline'>"); gt_csrf_input(&b, r); buf_puts(&b, "<input type=hidden name=action value=materialize>" "<button>open as merge request</button></form> "); } } if (r->user_id == author) buf_printf(&b, "<a class=btn2 href='/%s/%s/patches/new" "?set=%ld'>submit new version</a> ", o, n, id); if (cur == latest && (r->user_id == author || rk >= 2)) { buf_puts(&b, "<form method=post style='display:inline'>"); gt_csrf_input(&b, r); buf_puts(&b, "<input type=hidden name=action value=reject>" "<button>reject</button></form>"); } } else if (r->user_id > 0 && strcmp(state, "rejected") == 0 && (r->user_id == author || rk >= 2)) { buf_puts(&b, "<form method=post style='display:inline'>"); gt_csrf_input(&b, r); buf_puts(&b, "<input type=hidden name=action value=reopen>" "<button>reopen</button></form>"); } gt_repo_body_end(&b); gt_page_bottom(&b); gt_send_page(r, &b); buf_free(&b); } static void ps_result(struct app_ctx *app, struct request *r, const char *o, const char *n, int rk, const char *head, struct buf *det) { struct buf b; buf_init(&b); gt_page_top(&b, n, r); gt_repo_tabs(&b, app, o, n, rk, "changes"); buf_puts(&b, "<h3>"); gt_esc(&b, head); buf_puts(&b, "</h3><pre>"); if (det->data) gt_esc(&b, det->data); buf_printf(&b, "</pre><p><a href='/%s/%s/patches'>back to patch " "sets</a></p>", o, n); gt_repo_body_end(&b); gt_page_bottom(&b); gt_send_page(r, &b); buf_free(&b); } static void ps_post(struct app_ctx *app, struct request *r, const char *repo_dir, const char *o, const char *n, int rk, int repo_id, long id) { size_t blen; char *body = gt_read_post(r, 65536, &blen); if (!body) return; char act[16] = "", cbody[8192] = ""; form_get(body, blen, "action", act, sizeof act); form_get(body, blen, "body", cbody, sizeof cbody); free(body); char loc[256]; snprintf(loc, sizeof loc, "/%s/%s/patches/%ld", o, n, id); if (strcmp(act, "comment") == 0) { if (cbody[0]) gt_add_comment(app->db, "patch_set", id, r->user_id, cbody); http_redirect(r, loc); return; } if (strcmp(act, "check") == 0 || strcmp(act, "apply") == 0) { if (strcmp(act, "apply") == 0 && rk < 2) { http_error(r, 403, "write permission required"); return; } /* load state + mbox */ sqlite3_stmt *st; sqlite3_prepare_v2(app->db, "SELECT p.state,p.dst_branch," " (SELECT mbox FROM patch_revisions v" " WHERE v.patch_set_id=p.id ORDER BY v.version DESC LIMIT 1)" " FROM patch_sets p WHERE p.id=? AND p.repo_id=?", -1, &st, NULL); sqlite3_bind_int64(st, 1, id); sqlite3_bind_int(st, 2, repo_id); if (sqlite3_step(st) != SQLITE_ROW) { sqlite3_finalize(st); http_error(r, 404, "no such patch set"); return; } char state[16], base[256]; snprintf(state, sizeof state, "%s", (const char *)sqlite3_column_text(st, 0)); snprintf(base, sizeof base, "%s", (const char *)sqlite3_column_text(st, 1)); const void *mbox = sqlite3_column_blob(st, 2); int mlen = sqlite3_column_bytes(st, 2); char *mcopy = xmalloc((size_t)mlen + 1); memcpy(mcopy, mbox ? mbox : "", (size_t)mlen); mcopy[mlen] = '\0'; sqlite3_finalize(st); if (strcmp(state, "open") != 0) { free(mcopy); http_error(r, 409, "patch set not open"); return; } int np = 0; struct patch *ps = mbox_parse(mcopy, (size_t)mlen, &np); if (!ps) { free(mcopy); http_error(r, 400, "corrupt mbox"); return; } int check_only = strcmp(act, "check") == 0; char tip[41] = ""; struct buf err; buf_init(&err); int rc = apply_series(repo_dir, base, NULL, ps, np, check_only, r->user_name, tip, &err); free(ps); free(mcopy); if (check_only) { const char *h = rc == 0 ? "series applies cleanly" : "series does not apply"; if (rc == 0) buf_puts(&err, "all patches apply on top of base"); ps_result(app, r, o, n, rk, h, &err); buf_free(&err); return; } if (rc == 0) { sqlite3_prepare_v2(app->db, "UPDATE patch_sets SET state='applied'," " updated_at=strftime('%s','now') WHERE id=?", -1, &st, NULL); sqlite3_bind_int64(st, 1, id); sqlite3_step(st); sqlite3_finalize(st); sqlite3_prepare_v2(app->db, "UPDATE patch_revisions SET tip_commit=?" " WHERE patch_set_id=? AND version=" " (SELECT MAX(version) FROM patch_revisions" " WHERE patch_set_id=?)", -1, &st, NULL); sqlite3_bind_text(st, 1, tip, -1, SQLITE_TRANSIENT); sqlite3_bind_int64(st, 2, id); sqlite3_bind_int64(st, 3, id); sqlite3_step(st); sqlite3_finalize(st); buf_free(&err); http_redirect(r, loc); return; } ps_result(app, r, o, n, rk, "apply failed", &err); buf_free(&err); return; } if (strcmp(act, "materialize") == 0) { /* GNU -> web flow: apply the series onto a synthetic branch * refs/heads/psets/<id> and open a merge request for it. */ if (rk < 2) { http_error(r, 403, "write permission required"); return; } sqlite3_stmt *st; sqlite3_prepare_v2(app->db, "SELECT p.state,p.dst_branch,p.title,p.author_id," " (SELECT mbox FROM patch_revisions v" " WHERE v.patch_set_id=p.id ORDER BY v.version DESC LIMIT 1)" " FROM patch_sets p WHERE p.id=? AND p.repo_id=?", -1, &st, NULL); sqlite3_bind_int64(st, 1, id); sqlite3_bind_int(st, 2, repo_id); if (sqlite3_step(st) != SQLITE_ROW) { sqlite3_finalize(st); http_error(r, 404, "no such patch set"); return; } char state[16], base[256], title[210]; int author; snprintf(state, sizeof state, "%s", (const char *)sqlite3_column_text(st, 0)); snprintf(base, sizeof base, "%s", (const char *)sqlite3_column_text(st, 1)); snprintf(title, sizeof title, "%s", (const char *)sqlite3_column_text(st, 2)); author = sqlite3_column_int(st, 3); const void *mbox = sqlite3_column_blob(st, 4); int mlen = sqlite3_column_bytes(st, 4); char *mcopy = xmalloc((size_t)mlen + 1); memcpy(mcopy, mbox ? mbox : "", (size_t)mlen); mcopy[mlen] = '\0'; sqlite3_finalize(st); if (strcmp(state, "open") != 0) { free(mcopy); http_error(r, 409, "patch set not open"); return; } int np = 0; struct patch *ps = mbox_parse(mcopy, (size_t)mlen, &np); if (!ps) { free(mcopy); http_error(r, 400, "corrupt mbox"); return; } char branch[64], tip[41] = ""; snprintf(branch, sizeof branch, "psets/%ld", id); struct buf err; buf_init(&err); int rc = apply_series(repo_dir, base, branch, ps, np, 0, r->user_name, tip, &err); free(ps); free(mcopy); if (rc != 0) { ps_result(app, r, o, n, rk, "materialize failed", &err); buf_free(&err); return; } buf_free(&err); /* record the applied tip, then create the merge request */ sqlite3_prepare_v2(app->db, "UPDATE patch_revisions SET tip_commit=?" " WHERE patch_set_id=? AND version=" " (SELECT MAX(version) FROM patch_revisions" " WHERE patch_set_id=?)", -1, &st, NULL); sqlite3_bind_text(st, 1, tip, -1, SQLITE_TRANSIENT); sqlite3_bind_int64(st, 2, id); sqlite3_bind_int64(st, 3, id); sqlite3_step(st); sqlite3_finalize(st); sqlite3_prepare_v2(app->db, "INSERT INTO merge_requests(repo_id,author_id,title,body," " src_branch,dst_branch,src_commit) VALUES(?,?,?,?,?,?,?)", -1, &st, NULL); sqlite3_bind_int(st, 1, repo_id); sqlite3_bind_int(st, 2, author > 0 ? author : r->user_id); sqlite3_bind_text(st, 3, title, -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 4, "(materialized from patch set)", -1, SQLITE_STATIC); sqlite3_bind_text(st, 5, branch, -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 6, base, -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 7, tip, -1, SQLITE_TRANSIENT); sqlite3_step(st); long mrid = sqlite3_last_insert_rowid(app->db); sqlite3_finalize(st); db_audit(app->db, r->user_id, r->user_name, r->remote_ip, repo_id, "mr_create", "#%ld (materialized pset #%ld)", mrid, id); char mloc[256]; snprintf(mloc, sizeof mloc, "/%s/%s/mr/%ld", o, n, mrid); http_redirect(r, mloc); return; } if (strcmp(act, "reject") == 0 || strcmp(act, "reopen") == 0) { sqlite3_stmt *st; sqlite3_prepare_v2(app->db, "SELECT author_id,state FROM patch_sets" " WHERE id=? AND repo_id=?", -1, &st, NULL); sqlite3_bind_int64(st, 1, id); sqlite3_bind_int(st, 2, repo_id); int author = -1; char state[16] = ""; if (sqlite3_step(st) == SQLITE_ROW) { author = sqlite3_column_int(st, 0); snprintf(state, sizeof state, "%s", (const char *)sqlite3_column_text(st, 1)); } sqlite3_finalize(st); int rej = strcmp(act, "reject") == 0; if ((author == r->user_id || rk >= 2) && (rej ? strcmp(state, "open") == 0 : strcmp(state, "rejected") == 0)) { sqlite3_prepare_v2(app->db, "UPDATE patch_sets SET state=?," " updated_at=strftime('%s','now')" " WHERE id=? AND repo_id=?", -1, &st, NULL); sqlite3_bind_text(st, 1, rej ? "rejected" : "open", -1, SQLITE_STATIC); sqlite3_bind_int64(st, 2, id); sqlite3_bind_int(st, 3, repo_id); sqlite3_step(st); sqlite3_finalize(st); } } http_redirect(r, loc); } void collab_patches(struct app_ctx *app, struct request *r, const char *repo_dir, const char *o, const char *n, int rk, const char *tail) { int repo_id = gt_repo_rowid(app->db, o, n); if (repo_id < 0) { http_error(r, 404, "repository not registered"); return; } if (strcmp(tail, "") == 0) { ps_list(app, r, o, n, rk, repo_id); return; } if (strcmp(tail, "/new") == 0) { if (strcmp(r->method, "GET") == 0) { if (r->user_id == 0) { http_redirect(r, "/login"); return; } char *d = git_default_branch(repo_dir); ps_new_form(app, r, o, n, rk, d ? d : "master"); free(d); } else { if (r->user_id == 0) { http_unauthorized(r, "authentication required"); return; } ps_create(app, r, repo_dir, o, n, rk, repo_id); } return; } long id = strtol(tail + 1, NULL, 10); if (id <= 0) { http_error(r, 404, "no such patch set"); return; } if (strcmp(r->method, "POST") == 0) { ps_post(app, r, repo_dir, o, n, rk, repo_id, id); return; } ps_view(app, r, repo_dir, o, n, rk, repo_id, id); } /* ============ mail-in endpoint ============ * `git-trace mailrecv <owner> <repo>` — invoked by a real MTA through * a pipe alias (Postfix: `patches-demo: |"/usr/bin/git-trace mailrecv * alice demo"`). Reads one RFC822 message on stdin, authenticates it * and feeds the patch-set workflow. git send-email series arrive as * one mail per part; parts collect in mail_items and the assembled * series is submitted once the thread is complete. * * Auth: X-Git-Trace-Token header ("<token>" or "<user>:<token>"), * else From: matching a registered users.email — the MTA is expected * to enforce SPF/DKIM upstream. The token header is stripped before * storage so credentials never land in patch_revisions.mbox. */ /* does the buffer contain an mbox "From <40hex> " separator? */ static int has_mbox_sep(const char *p, size_t len) { const char *end = p + len; for (const char *l = p; l < end; l = next_line(l, end)) if (is_from_line(l, (size_t)(next_line(l, end) - l))) return 1; return 0; } /* copy the message to dst dropping one named header + its folded * continuation lines */ static void strip_header(struct buf *dst, const char *src, size_t n, const char *name) { size_t nl = strlen(name); const char *end = src + n; int in_headers = 1; for (const char *l = src; l < end; ) { const char *nx = next_line(l, end); size_t ln = (size_t)(nx - l); if (ln <= 1) in_headers = 0; if (in_headers && ln > nl + 1 && strncasecmp(l, name, nl) == 0 && l[nl] == ':') { l = nx; while (l < end && (*l == ' ' || *l == '\t')) l = next_line(l, end); continue; } buf_put(dst, l, ln); l = nx; } } /* "[PATCH vN n/m] foo": fills ver/n/m, returns 1 if a tag was found */ static int parse_mail_tag(const char *subj, int *ver, int *n, int *m) { const char *b = strchr(subj, '['); const char *e = b ? strchr(b, ']') : NULL; *ver = 1; *n = *m = 0; if (!b || !e || e - b < 7 || memcmp(b, "[PATCH", 6) != 0) return 0; char tok[96]; size_t tl = (size_t)(e - (b + 6)); if (tl >= sizeof tok) tl = sizeof tok - 1; memcpy(tok, b + 6, tl); tok[tl] = '\0'; char *sv = NULL; for (char *t = strtok_r(tok, " \t", &sv); t; t = strtok_r(NULL, " \t", &sv)) { if (t[0] == 'v' && isdigit((unsigned char)t[1])) *ver = atoi(t + 1); else if (*n == 0) sscanf(t, "%d/%d", n, m); } return 1; } static int mail_submit(struct app_ctx *app, const char *repo_dir, int repo_id, int uid, int rk, const char *uname, const char *mbox, size_t mlen) { struct ps_out out; int rc = gt_ps_submit(app, repo_dir, repo_id, uid, rk, uname, NULL, NULL, NULL, 0, mbox, mlen, &out); if (rc != 0) { fprintf(stderr, "mailrecv: rejected (%d: %s)\n", out.status, out.err); return 1; } printf("mailrecv: patch set #%ld v%d\n", out.set_id, out.ver); return 0; } int gt_mailrecv(struct app_ctx *app, const char *owner, const char *name) { char rdir[PATH_MAX]; if (repo_path(rdir, sizeof rdir, app->repos_root, owner, name) != 0) { fprintf(stderr, "mailrecv: no such repo %s/%s\n", owner, name); return 1; } int repo_id = gt_repo_rowid(app->db, owner, name); if (repo_id < 0) { fprintf(stderr, "mailrecv: unregistered repo\n"); return 1; } /* whole message, capped like the web form */ struct buf raw; buf_init(&raw); char rd[16384]; ssize_t rn; int cap = 0; while ((rn = read(0, rd, sizeof rd)) > 0) { if (raw.len + (size_t)rn > 4 * 1024 * 1024) { cap = 1; break; } buf_put(&raw, rd, (size_t)rn); } if (cap) { fprintf(stderr, "mailrecv: message over 4 MiB\n"); buf_free(&raw); return 1; } if (!raw.data || raw.len == 0) { fprintf(stderr, "mailrecv: empty message\n"); buf_free(&raw); return 1; } char from[256], subj[512], msgid[256], irt[256], tok[200], date[128]; hdr_value(raw.data ? raw.data : "", raw.len, "From", from, sizeof from); hdr_value(raw.data ? raw.data : "", raw.len, "Subject", subj, sizeof subj); hdr_value(raw.data ? raw.data : "", raw.len, "Message-ID", msgid, sizeof msgid); hdr_value(raw.data ? raw.data : "", raw.len, "In-Reply-To", irt, sizeof irt); hdr_value(raw.data ? raw.data : "", raw.len, "X-Git-Trace-Token", tok, sizeof tok); hdr_value(raw.data ? raw.data : "", raw.len, "Date", date, sizeof date); int uid = -1; if (tok[0]) { char *colon = strchr(tok, ':'); if (colon) { *colon = '\0'; uid = auth_token(app->db, tok, colon + 1); } else { uid = auth_token_lookup(app->db, tok); } } if (uid < 0 && from[0]) { char nm[128], em[128]; split_author(from, nm, sizeof nm, em, sizeof em); if (em[0]) uid = auth_email_uid(app->db, em); } if (uid < 0) { fprintf(stderr, "mailrecv: unauthenticated message rejected\n"); buf_free(&raw); return 1; } int rk = perm_rank(repo_perm(app->db, owner, name, uid)); if (rk < 1) { fprintf(stderr, "mailrecv: sender has no repo access\n"); buf_free(&raw); return 1; } char uname[64] = "(mail)"; { sqlite3_stmt *st; if (sqlite3_prepare_v2(app->db, "SELECT name FROM users WHERE id=?", -1, &st, NULL) == SQLITE_OK) { sqlite3_bind_int(st, 1, uid); if (sqlite3_step(st) == SQLITE_ROW) snprintf(uname, sizeof uname, "%s", (const char *)sqlite3_column_text(st, 0)); sqlite3_finalize(st); } } sqlite3_stmt *st; sqlite3_prepare_v2(app->db, "DELETE FROM mail_items WHERE" " received < strftime('%s','now') - 604800", -1, &st, NULL); sqlite3_step(st); sqlite3_finalize(st); /* fast path: the body already is an mbox (pasted format-patch) */ if (has_mbox_sep(raw.data, raw.len)) { int rc = mail_submit(app, rdir, repo_id, uid, rk, uname, raw.data, raw.len); buf_free(&raw); return rc; } /* synthesize an mbox chunk, token header stripped */ struct buf chunk; buf_init(&chunk); buf_printf(&chunk, "From %040d %s\n", 0, date[0] ? date : "Mon Sep 17 00:00:00 2001"); strip_header(&chunk, raw.data, raw.len, "X-Git-Trace-Token"); buf_free(&raw); if (chunk.len && chunk.data[chunk.len - 1] != '\n') buf_put(&chunk, "\n", 1); int ver, part, total; parse_mail_tag(subj, &ver, &part, &total); if (!(total >= 1 && part <= total && msgid[0])) { /* not a numbered series part: single-patch submission */ int rc = mail_submit(app, rdir, repo_id, uid, rk, uname, chunk.data, chunk.len); buf_free(&chunk); return rc; } /* send-email series part: drop tombstoned threads, then collect */ char grp[300]; snprintf(grp, sizeof grp, "%s", part == 0 ? msgid : (irt[0] ? irt : msgid)); sqlite3_prepare_v2(app->db, "SELECT 1 FROM mail_items WHERE repo_id=? AND grp=?" " AND part=-1 LIMIT 1", -1, &st, NULL); sqlite3_bind_int(st, 1, repo_id); sqlite3_bind_text(st, 2, grp, -1, SQLITE_TRANSIENT); int dead = sqlite3_step(st) == SQLITE_ROW; sqlite3_finalize(st); if (dead) { buf_free(&chunk); return 0; /* already assembled once */ } sqlite3_prepare_v2(app->db, "INSERT OR IGNORE INTO mail_items" " (repo_id,grp,msgid,part,total,raw) VALUES(?,?,?,?,?,?)", -1, &st, NULL); sqlite3_bind_int(st, 1, repo_id); sqlite3_bind_text(st, 2, grp, -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 3, msgid, -1, SQLITE_TRANSIENT); sqlite3_bind_int(st, 4, part); sqlite3_bind_int(st, 5, total); sqlite3_bind_blob(st, 6, chunk.data, (int)chunk.len, SQLITE_TRANSIENT); sqlite3_step(st); sqlite3_finalize(st); buf_free(&chunk); /* complete when every part 1..total of the thread has arrived */ int present[256]; memset(present, 0, sizeof present); int maxt = 0; sqlite3_prepare_v2(app->db, "SELECT DISTINCT part,total FROM mail_items" " WHERE repo_id=? AND grp=? AND part>=0", -1, &st, NULL); sqlite3_bind_int(st, 1, repo_id); sqlite3_bind_text(st, 2, grp, -1, SQLITE_TRANSIENT); while (sqlite3_step(st) == SQLITE_ROW) { int p = sqlite3_column_int(st, 0); int t = sqlite3_column_int(st, 1); if (p >= 0 && p < 256) present[p] = 1; if (t > maxt) maxt = t; } sqlite3_finalize(st); if (maxt < 1 || maxt >= 256) { printf("mailrecv: queued part %d\n", part); return 0; } for (int i = 1; i <= maxt; i++) if (!present[i]) { printf("mailrecv: queued part %d/%d\n", part, total); return 0; } /* thread complete: assemble cover(0) + parts in order */ struct buf mbox; buf_init(&mbox); sqlite3_prepare_v2(app->db, "SELECT raw FROM mail_items WHERE repo_id=? AND grp=?" " AND part>=0 ORDER BY part", -1, &st, NULL); sqlite3_bind_int(st, 1, repo_id); sqlite3_bind_text(st, 2, grp, -1, SQLITE_TRANSIENT); while (sqlite3_step(st) == SQLITE_ROW) buf_put(&mbox, sqlite3_column_blob(st, 0), (size_t)sqlite3_column_bytes(st, 0)); sqlite3_finalize(st); int rc = mail_submit(app, rdir, repo_id, uid, rk, uname, mbox.data, mbox.len); buf_free(&mbox); if (rc == 0) { /* tombstone the thread: an MTA redelivery must not open a * duplicate set */ sqlite3_prepare_v2(app->db, "DELETE FROM mail_items WHERE repo_id=? AND grp=?", -1, &st, NULL); sqlite3_bind_int(st, 1, repo_id); sqlite3_bind_text(st, 2, grp, -1, SQLITE_TRANSIENT); sqlite3_step(st); sqlite3_finalize(st); sqlite3_prepare_v2(app->db, "INSERT OR IGNORE INTO mail_items" " (repo_id,grp,msgid,part,total,raw)" " VALUES(?,?,?,-1,0,'')", -1, &st, NULL); sqlite3_bind_int(st, 1, repo_id); sqlite3_bind_text(st, 2, grp, -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 3, grp, -1, SQLITE_TRANSIENT); sqlite3_step(st); sqlite3_finalize(st); } return rc; }