blob: 083aa1f817fb5dc2ee93100c2e2a90e2951bf063 [file] [view]
<!-- SPDX-License-Identifier: Apache-2.0
https://www.apache.org/licenses/LICENSE-2.0 -->
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [`tools/mail-patch/`](#toolsmail-patch)
- [Prerequisites](#prerequisites)
- [What this maps](#what-this-maps)
- [Operations](#operations)
- [Attribution](#attribution)
- [Diff identity across backends](#diff-identity-across-backends)
- [Configuration](#configuration)
- [Cross-references](#cross-references)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
<!-- SPDX-License-Identifier: Apache-2.0
https://www.apache.org/licenses/LICENSE-2.0 -->
# `tools/mail-patch/`
**Capability:** contract:change-request
**Kind:** implementation
**Vendor:** email
**Organization:** ASF
`[PATCH]`-mail change-request adapter — the backend that lets a
project driving code review over its **developer mailing list** use
the `pr-management-*` skills. It implements the
[`tools/change-request/`](../change-request/) contract for the oldest
review mechanism in open source: a patch posted to `dev@` as a
`[PATCH]` thread, discussed in replies, and committed to trunk by a
committer.
There is no forge behind this backend at all. The "pull request" is a
mail thread; the "diff" is the `[PATCH]` body or attachment; the
"review" is the reply chain. This adapter composes two contracts the
framework already ships:
- **Reads** flow through [`contract:mail-archive`](../mail-archive/)
(`tools/ponymail/` at the ASF) — thread search, thread fetch,
participant extraction.
- **Review replies** are drafted through
[`contract:mail-create`](../gmail/) — the adapter never sends mail
autonomously; `post_review` and `reject` produce a drafted reply the
maintainer sends by hand.
Its `land` verb, like `jira-patch`, delegates the actual commit to the
project's [`contract:source-control`](../asf-svn/) adapter
(`svn patch` + `svn commit`) and then drafts an "applied in rNNNNN"
reply on the thread.
## Prerequisites
- **Runtime:** Bash — doc-only adapter; composes the `mail-archive`,
`mail-create`, and `source-control` adapters, no local package.
- **CLIs:** the mail-archive backend (PonyMail MCP at the ASF), the
mail-create backend (Gmail draft API), and `svn` for the delegated
`land`.
- **Credentials / auth:** anonymous read for public `dev@` archives
(PonyMail); an authenticated mail-create session to compose replies
(drafts only — never sends); and ASF committer credentials for the
SVN commit that `land` delegates to `tools/asf-svn/`. As with
`jira-patch`, the commit runs under the landing committer's identity
and credits the patch author in the commit message — see
[Attribution](#attribution).
- **Network:** the mail archive host (`lists.apache.org`) and
`svn.apache.org` for the delegated land.
## What this maps
The mail-patch backend resolves the change-request as: **a `[PATCH]`
thread on the developer list is one change proposal.** The thread's
opaque archive id is the proposal `id`; the `[PATCH]` body/attachment
is the diff; the reply chain is the review discussion. A follow-up
`[PATCH v2]` thread is a `superseded`-then-new proposal.
## Operations
Each change-request verb resolves onto the mail surfaces as follows.
| Verb | mail-patch resolution |
|---|---|
| `list_open(filter)` | `mail-archive.list_recent_threads(dev-list, since)` filtered to `[PATCH]`-subject threads that have no terminal "applied"/"rejected" reply. One `proposal_summary` per open patch thread. |
| `get(id)` | `mail-archive.fetch_thread_by_url(id)`; the `diff` is extracted from the first message's `[PATCH]` body or `.patch` attachment. `base` is the trunk from config; `commits` is `[]`; `mergeable` is `unknown` until an `svn patch --dry-run`. |
| `get_discussion(id)` | The thread's reply chain, normalised to `{author, date, body, kind}`. A reply containing the configured approval token (`LGTM` / `+1`) maps to `kind: approval`. |
| `post_review(id, verdict, body)` | **Drafts** a threaded reply via `contract:mail-create` (`in-reply-to` the thread), never sends. The `verdict` shapes the reply's opening line (`+1`, `needs work`); the maintainer reviews and sends. |
| `land(id, strategy)` | **Delegates to `contract:source-control`.** Extracts the patch via `get`, calls the source-control adapter's apply + commit (`svn patch <file>` then `svn commit` — see [`tools/asf-svn/source-control.md`](../asf-svn/source-control.md)), then drafts an "applied in rNNNNN, thanks!" reply on the thread. `strategy` is advisory — SVN lands a patch as one commit (`squash`). |
| `reject(id, reason)` | Drafts a threaded reply carrying `reason`. **No commit** — the absence of a `land` is the rejection. Nothing is transitioned; the thread simply closes socially. |
| `status(id)` | `checks: none` (a mail thread has no CI), `mergeable` from an `svn patch --dry-run`. Skills degrade the `checks` gate to advisory and fall back to a human-judgement prompt (the contract's `status` graceful-degradation path — this is the maximally-degraded backend). |
### Attribution
The delegated `land` credits the patch author in the SVN commit
message (`Patch by <From: header>.`) while committing under the
landing committer's identity — the same convention ASF committers
follow when applying a mailed patch by hand. The adapter reads the
author from the `[PATCH]` message's `From:` header; it never
impersonates the author's identity. This answers #669's
*"patch-author vs. committer attribution"* open question for the
mail-patch backend.
### Diff identity across backends
A `[PATCH]` body has no forge-assigned id, so the adapter derives a
stable proposal id from the thread's archive permalink (the
`mail-archive` thread hash). Re-posted `[PATCH v2]` threads get a new
id and mark the prior thread `superseded` — this is the mail-patch
answer to #669's *"diff identity normalization across backends"* open
question: identity is the *thread*, not the diff bytes.
## Configuration
Declared under the change-request block in
`projects/<project>/project.md`:
```yaml
change_request:
backend: mail-patch
land_via: source-control # land delegates to the VCS adapter
review_channel: mailing-list
default_strategy: squash # SVN applies a patch as one commit
mail_patch:
dev_list: dev@<project>.apache.org
patch_subject_prefix: "[PATCH]"
approval_token: "LGTM" # reply token that counts as an approval
trunk_url: https://svn.apache.org/repos/asf/<project>/trunk
```
- **`dev_list`** — the list `list_open` scans and replies draft against.
- **`patch_subject_prefix`** — the subject marker that identifies a
patch thread (some projects use `[PATCH]`, some `[PROPOSAL]`).
- **`approval_token`** — the reply string `get_discussion` reads as a
`kind: approval`.
- **`trunk_url`** — the SVN trunk the delegated `land` applies to.
Backend-specific keys live under `change_request.mail_patch.*`; the
generic keys are the contract's.
## Cross-references
- Contract: [`tools/change-request/`](../change-request/)
- Archive reads: [`tools/mail-archive/`](../mail-archive/) (ASF: [`tools/ponymail/`](../ponymail/))
- Reply drafting: [`contract:mail-create`](../gmail/)
- Delegated land: [`tools/asf-svn/source-control.md`](../asf-svn/source-control.md)
- Issue: [#669](https://github.com/apache/magpie/issues/669)