# 示例：PR 长期未活动催办工作流

> 本示例演示对长期未活动的 PR 执行催办流程。
> ⚠️ 与 Issue 不同，PR 端点暂不支持 label 操作，因此**只评论催办**，不打 stale 标签。

## 场景

- **仓库**：`Gitlink/forgeplus`
- **目标**：识别 60+ 天未活动的 open PR，评论催办；74+ 天的关闭
- **执行者**：Claude Code + 用户（人在环路）

---

## Step 0 — 准备环境

```powershell
cd D:\code\SE\Evolution_and_Maintenance_of_SE\Mission2\gitlink-cli

.\gitlink-cli.exe version
.\gitlink-cli.exe auth status
```

---

## Step 1 — 拉取 PR 列表

### 1.1 获取所有 open PR

```powershell
.\gitlink-cli.exe pr +list `
    --owner Gitlink `
    --repo forgeplus `
    --state open `
    --format json | Out-File -Encoding utf8 "$env:TEMP\prs-open.json"
```

### 1.2 客户端二次过滤

> ⚠️ **关键**：GitLink 的 `pr +list --state open` 的 `--state` 参数仅影响统计计数，返回列表可能包含所有状态。**必须**按 `pull_request_status == 0` 二次过滤。

```powershell
$raw = Get-Content "$env:TEMP\prs-open.json" -Raw | ConvertFrom-Json

# 二次过滤：仅保留真正 open 的 PR
$openPRs = $raw.data.pull_requests | Where-Object { $_.pull_request_status -eq 0 }

# 时间过滤：60+ 天未活动
$threshold = (Get-Date).AddDays(-60)
$stalePRs = $openPRs | Where-Object {
    $updated = if ($_.updated_at) { [DateTime]::Parse($_.updated_at) } else { [DateTime]::Parse($_.created_at) }
    $updated -lt $threshold
}

Write-Host "Total open PRs: $($openPRs.Count)"
Write-Host "Stale candidates (60+ days): $($stalePRs.Count)"
```

**示例输出**：
```
Total open PRs: 12
Stale candidates (60+ days): 4
```

---

## Step 2 — 逐个详情分析

对每个候选 PR：

```powershell
$results = @()

foreach ($pr in $stalePRs) {
    # 拉取 PR 详情
    $detail = (& .\gitlink-cli.exe pr +view `
        --owner Gitlink --repo forgeplus `
        --id $pr.pull_request_number `
        --format json) | ConvertFrom-Json
    
    # 计算天数
    $lastActivity = if ($detail.data.updated_at) {
        [DateTime]::Parse($detail.data.updated_at)
    } else {
        [DateTime]::Parse($detail.data.created_at)
    }
    $days = [int]((Get-Date) - $lastActivity).TotalDays
    
    # AI 判断（PR 特化规则）
    $analysis = ai_judge_pr_stale $detail
    
    $results += [PSCustomObject]@{
        Id = $pr.pull_request_number
        Title = $pr.title
        Author = $pr.user.login
        Days = $days
        Confidence = $analysis.confidence
        Action = if ($days -ge 74) { "auto_close" } else { "mark_stale" }
        Reason = $analysis.reason
    }
}

$results | Format-Table
```

### AI 判断 PR 的特殊规则

PR 与 Issue 的差异：

| 维度 | Issue | PR |
|------|-------|-----|
| 标签 | 支持豁免标签 | ❌ 暂不支持 |
| 评论催办 | mark_stale + 评论 | **仅评论** |
| 自动关闭 | issue +close | pr +close |
| 合并状态 | N/A | 已 merged 的不算 stale |

```python
def ai_judge_pr_stale(pr_detail):
    """
    PR 特化判断
    """
    # 已 merged 或已 closed 的不算（理论上已被过滤）
    if pr_detail.pull_request_status != 0:
        return {"truly_stale": False, "exempt": True, "reason": "已 merged/closed"}
    
    # 是否有冲突？
    if pr_detail.conflict:
        return {
            "truly_stale": True,
            "confidence": 0.9,
            "reason": "存在冲突，可能需要 rebase"
        }
    
    # 是否等待 review？
    if pr_detail.reviewers and not pr_detail.approved:
        return {
            "truly_stale": True,
            "confidence": 0.75,
            "reason": "等待 reviewer 回应"
        }
    
    # 作者活跃度
    if pr_detail.user.login in repo_contributors:
        return {
            "truly_stale": True,
            "confidence": 0.65,
            "reason": "贡献者提交后未跟进"
        }
    
    return {
        "truly_stale": True,
        "confidence": 0.8,
        "reason": "默认判断"
    }
```

---

## Step 3 — 展示报告

Claude Code 输出：

```
发现 4 个 60+ 天未活动的 PR：

┌──────┬────────────────────────────┬──────────┬────────┬─────────────┬────────────┐
│ #    │ 标题                        │ 作者      │ 天数   │ 置信度      │ 动作       │
├──────┼────────────────────────────┼──────────┼────────┼─────────────┼────────────┤
│ 8    │ feat: 新增搜索功能          │ contrib-a│ 68     │ 0.85        │ mark_stale │
│ 12   │ fix: 修复登录 bug           │ newbie   │ 92     │ 0.92        │ auto_close │
│ 15   │ docs: 更新 README          │ user-1   │ 65     │ 0.70        │ mark_stale │
│ 21   │ refactor: 重构 API         │ contrib-b│ 78     │ 0.88        │ auto_close │
└──────┴────────────────────────────┴──────────┴────────┴─────────────┴────────────┘

⚠️ 注意：PR 暂不支持 label 操作，将仅评论催办。

是否应用？[yes / 选择性 / 取消]
```

---

## Step 4 — 应用动作（用户确认后）

### 4.1 备份

```powershell
$ts = Get-Date -Format "yyyyMMddHHmmss"
.\gitlink-cli.exe pr +list `
    --owner Gitlink --repo forgeplus `
    --state open --format json |
    Out-File -Encoding utf8 "$env:TEMP\before-pr-stale-$ts.json"
```

### 4.2 批量应用

```powershell
$OWNER = "Gitlink"
$REPO = "forgeplus"
$report = Get-Content "$env:TEMP\pr-stale-report.json" -Raw | ConvertFrom-Json

$toApply = $report.items | Where-Object {
    $_.recommended_action -in @("mark_stale", "auto_close") -and
    $_.ai_analysis.confidence -ge 0.6
}

foreach ($item in $toApply) {
    $id = $item.number
    $action = $item.recommended_action
    
    Write-Host "→ PR #$id : $action"
    
    # 选择评论模板
    if ($action -eq "mark_stale") {
        $body = @"
⏰ **PR 长期未活动**

本 PR 已 60 天未更新，可能存在以下情况：

- 合并遇到冲突？请 rebase 后重新推送
- 等待 review？可 @mention 相关维护者
- 不再需要？欢迎手动关闭

如果 **14 天内**没有新活动，将默认关闭。

> 🤖 由 gitlink-stale skill 自动生成。
"@
    } else {
        $body = @"
🔒 **PR 自动关闭（长期未活动）**

本 PR 已 74 天无活动，自动关闭。

- 如仍需合并，请 rebase 后重新打开
- 如有冲突，可重新发起 PR

> 🤖 由 gitlink-stale skill 自动关闭。
"@
    }
    
    # 1. 评论催办
    & .\gitlink-cli.exe pr +comment `
        --owner $OWNER --repo $REPO `
        --id $id --body $body 2>&1 | Out-Null
    
    # 2. 若 auto_close，关闭 PR
    if ($action -eq "auto_close") {
        & .\gitlink-cli.exe pr +close `
            --owner $OWNER --repo $REPO `
            --id $id 2>&1 | Out-Null
    }
    
    Start-Sleep -Milliseconds 500
}

Write-Host "✓ Batch applied"
```

---

## Step 5 — 验证

```powershell
# 检查 PR #8 是否已评论
.\gitlink-cli.exe pr +view `
    --owner Gitlink --repo forgeplus `
    --id 8 --format json |
    ConvertFrom-Json |
    Select-Object -ExpandProperty data |
    Select-Object title, @{N="status";E={$_.pull_request_status}}, @{N="journals_count";E={$_.journals.Count}}
```

---

## 故障恢复

### 误关闭的 PR 恢复

```powershell
# 重新打开 PR（Raw API）
# 注意：GitLink PR 端点重新打开的 API 可能不完善
# 推荐做法：让作者重新发起 PR
```

### 评论失败

```powershell
# 现象：pr +comment 返回 404
# 原因：--id 用了内部 id 而非 pull_request_number
# 处理：确认 id 是网页 URL 中的 pull_request_number
```

---

## 与 Issue 处理的差异

| 维度 | Issue | PR |
|------|-------|-----|
| 标签 | 支持 stale/pinned 等 | ❌ 不支持 |
| 评论催办 | ✅ | ✅ |
| 自动关闭 | `issue +close --number N` | `pr +close --id N` |
| 客户端过滤 | 直接看 status_id | 必须看 pull_request_status（state 参数不可靠） |
| 重开 | PATCH status_id=1 | API 可能不完善 |

> 💡 **核心差异**：PR 没有 label 维度，所有"stale 状态"必须通过评论标题或正文中的 ⏰/🔒 emoji 表达。

---

## 关键检查点

- ✅ Step 1 完成后，按 `pull_request_status == 0` 二次过滤
- ✅ Step 2 PR 详情中检查是否有冲突
- ✅ Step 3 展示时明确告知用户"PR 不打标签，仅评论"
- ✅ Step 4 应用前备份 PR 列表

---

## 性能数据

| 阶段 | API 调用次数 | 耗时 |
|------|-------------|------|
| Step 1 列表 | 1 | 3s |
| Step 2 详情 | N × 1 | 8s |
| Step 4 评论+关闭 | N × 2 | 6s |
| **总计（4 个 PR）** | **13** | **~20s** |

---

## 总结

PR stale 处理的核心要点：

1. ✅ **必过滤 `pull_request_status`** — `--state` 参数不可靠
2. ✅ **仅评论催办** — 不打标签
3. ✅ **AI 判断考虑 PR 特性** — 冲突、reviewer、合并状态
4. ✅ **关闭操作可逆性差** — 建议作者重新发起而非自动重开
