# 更改请求

会话工作如何通过评审进入默认分支。

Canonical page: https://beeuni.ai/docs/zh/work/change-requests

更改请求（CR）会将一个 git 分支合并到另一个分支。Beeuni 会将会话分支（`head_ref`）上的更改创建为 CR，目标是项目的默认分支（`base_ref`，通常为 `main`）。CR 行是元数据；合并、差异和冲突检查会针对项目仓库作为真实的 git 操作运行。CR 是会话工作进入默认分支的唯一途径。

<a id="why-work-goes-through-a-cr"></a>

## 为什么工作要经过 CR

会话在沙箱中运行，并使用以会话 ID 命名的独立分支。沙箱不会永久存在，但分支会保留：git 是会话工作唯一持久的记录。每个新会话都从默认分支开始。在 CR 完成合并之前，工作会留在自己的分支上，未经审查，其他代理、触发器和协作者都不可见。这适用于所有更改：代码、代理、技能和清单（`beeuni.yaml`）——没有例外。

<a id="the-agent-mandate"></a>

## 代理必须遵守的要求

代理必须打开 CR，才能将任何更改落到项目的默认分支上。代理不得合并自己的 CR——合并由用户决定。请遵循以下约定：

1. 在会话分支（`$BEEUNI_BRANCH_NAME`）上提交。使用小型、可工作的提交。不要重写历史记录或强制推送。
2. 推送分支：`git push origin HEAD`。
3. 打开 CR：`beeuni cr open --title "..." --description "..."`。在会话沙箱中，`--head` 和 `--session` 默认为 `$BEEUNI_BRANCH_NAME` 和 `$BEEUNI_SESSION_ID`。`--base` 默认为项目的默认分支。
4. 告知用户 CR 编号，以便用户审查。
5. 停止操作。不要自行合并 CR。

<a id="anti-patterns"></a>

### 反模式

- **强制推送到默认分支。** 即使后端允许，这也会破坏审查约定。
- **“代码在我的分支上，你自己拉取吧。”** 沙箱停止后，会话分支就会消失，除非 CR 已先将其合并。
- **通过文件、粘贴内容或存档发送更改。** CR 系统已经解决了这个问题。

<a id="data-model"></a>

## 数据模型

CR 存储在 `change_requests` 表中。

| 列 | 类型 | 说明 |
| --- | --- | --- |
| `cr_id` | uuid | 主键。REST API 的标识符。 |
| `project_id` | uuid | CR 所属的项目。 |
| `number` | integer | 每个项目的显示编号（`#1`、`#2`……）。在项目内唯一。不会重新使用。 |
| `title` | text | 必填。 |
| `description` | text | 默认为空字符串。 |
| `base_ref` | text | 被合并到的分支。通常为 `main`。 |
| `head_ref` | text | 被合并的来源分支。在会话中，这是会话 ID（UUID）。 |
| `status` | enum | `open`、`merged` 或 `closed`。 |
| `head_commit_sha` | text | 对于打开的 CR，每次读取时都会根据实时 `head_ref` 最新提交刷新。对于已合并的 CR，在合并时记录。 |
| `base_commit_sha` | text | `base_ref` 采用相同规则。 |
| `origin_session_id` | text | 打开 CR 的会话。如果该会话被删除，则设为 null。 |
| `created_by` | uuid | 创建 CR 的用户。 |
| `merged_at` / `merged_by` | timestamp / uuid | 合并 CR 的时间和用户。 |
| `merge_commit_sha` | text | 合并提交。快速前进时等于 `head_commit_sha`。 |
| `closed_at` / `closed_by` | timestamp / uuid | 不合并而关闭 CR 的时间和用户。 |
| `metadata` | jsonb | 包含 `requested_changes`，即由 `POST /:crId/request-changes` 添加的 `{text, by, at}` 条目列表。CR 没有单独的评论表。 |
| `created_at` / `updated_at` | timestamp | 创建时设置。在每次状态更改或 SHA 刷新时更新。 |

`(project_id, number)` 上的唯一索引允许你通过短编号而不是 UUID 引用 CR。

<a id="lifecycle"></a>

## 生命周期

```
open ──(merge)──▶ merged      (terminal)
open ──(close)──▶ closed ──(reopen)──▶ open
```

- `open` 是初始状态。
- `closed` 可以撤销。`POST /:crId/reopen` 会将其设回 `open`。
- `merged` 是终止状态。无法重新打开或关闭已合并的 CR。请针对已合并状态打开新的 CR。

如果某个分支没有领先默认分支的提交，Beeuni 会拒绝创建该分支的 CR。这通常意味着代理已在本地提交，但从未推送。请推送提交，然后重新创建 CR。

<a id="sha-refresh"></a>

## SHA 刷新

对于打开的 CR，Beeuni 会在每次 `GET` 时，根据实时分支刷新 `head_commit_sha` 和 `base_commit_sha`。如果仓库无法访问，或某个分支不存在，Beeuni 会跳过刷新，并提供 CR 最后已知的元数据。已合并的 CR 会保留合并时记录的 SHA——Beeuni 不会再次刷新它们。

<a id="merge-mechanics"></a>

## 合并机制

`POST /v1/projects/:projectId/change-requests/:crId/merge` 会执行以下步骤。

1. Beeuni 从 `head_ref` 读取清单（`beeuni.yaml`），并根据清单架构进行验证。没有清单的分支可以通过验证。无效清单会返回带有 `code: "MANIFEST_INVALID"` 的 `422`，并停止合并。
2. 如果 `head_ref` 严格领先于 `base_ref`，Beeuni 会将 该分支 快速前进。
3. 否则，Beeuni 会创建合并提交。默认消息为 `Merge CR #<n>: <title>`，也可以在请求正文中通过 `message` 覆盖。提交作者为 `Beeuni <noreply@beeuni.com>`。
4. 发生冲突时，会返回带有冲突列表的 `409`。合并前可通过 `GET /:crId/merge-preview` 检查相同列表。
5. 成功后，Beeuni 会将 `status` 设置为 `merged`，记录 `merged_at`、`merged_by` 和 `merge_commit_sha`，并使项目的 git 缓存失效。

合并状态不是 `open` 的 CR 会返回 `409`。

<a id="merge-preview"></a>

### 合并预览

`GET /:crId/merge-preview` 会返回：

| 字段 | 类型 | 含义 |
| --- | --- | --- |
| `base_sha` | string | `base_ref` 的当前最新提交。 |
| `head_sha` | string | `head_ref` 的当前最新提交。 |
| `merge_base` | string \| null | 共同祖先。如果历史记录不相关，则为 Null。 |
| `is_up_to_date` | boolean | `head_ref` 已完全合并到 `base_ref`。 |
| `can_merge` | boolean | 没有冲突。 |
| `can_fast_forward` | boolean | `head_ref` 严格领先于 `base_ref`。 |
| `conflicts` | string[] | 将发生冲突的文件路径。 |

## REST API

所有路由都位于 `/v1/projects/:projectId/change-requests` 下。

| 方法 | 路径 | 说明 |
| --- | --- | --- |
| GET | `/` | `?status=open\|merged\|closed\|all`。不提供筛选条件时返回所有状态。 |
| POST | `/` | 正文：`{title, description?, head_ref, base_ref?, session_id?}`。返回 `201`。 |
| GET | `/:crId` | 返回 CR。会作为副作用刷新 SHA。 |
| PATCH | `/:crId` | 编辑 `title` 或 `description`。如果不是 `open`，则 `409`。 |
| GET | `/:crId/diff` | 统一补丁：文件列表、添加内容和删除内容。 |
| GET | `/:crId/merge-preview` | 请参阅上面的“合并预览”。 |
| POST | `/:crId/merge` | 正文：`{message?}`。清单无效时返回 `422`。发生冲突或不是 `open` 时返回 `409`。 |
| POST | `/:crId/close` | 如果已经 `merged`，则 `409`。 |
| POST | `/:crId/reopen` | 如果不是 `closed`，则 `409`。 |
| POST | `/:crId/request-changes` | 正文：`{feedback}`。追加到 `metadata.requested_changes`，并唤醒发起会话的代理。如果不是 `open`，则 `409`。 |

`POST /` 会拒绝创建没有领先 `base_ref` 的提交的 `head_ref`：`422 code: "CR_HEAD_NOT_AHEAD"`。这是代理在推送分支之前打开 CR 时看到的错误。

<a id="authorization"></a>

### 授权

读取路由需要项目的读取权限。写入路由需要写入权限。每个写入操作还需要相应的能力，下面分别列出完整令牌和会话范围令牌所需的能力。

| 操作 | 能力 |
| --- | --- |
| 打开 CR | `project.gitops.push` |
| 请求更改 | `project.review.act` |
| 合并 | `project.gitops.merge` |

每项操作对应一项能力，无论是完整令牌还是权限范围受限的会话令牌。创建和合并是两个独立的叶子权限，因此令牌可以创建变更请求，却没有合并权限——这正是上文代理授权机制的实现方式。

`project.cr.open` 和 `project.cr.merge` 是切换前 `project.gitops.push` 和 `project.gitops.merge` 的名称——同一项能力的另一种名称。仍列出其中一个名称的 `beeuni.yaml` 仍然有效（解析时，授权会被改写为当前使用的名称），但新内容请使用 `gitops` 名称。

无论获得了什么授权，会话都不能合并自己创建的变更请求。这条规则是结构性的，并非一项能力。
