🐛 fix(vscode): 修复 PR 上的审查意见没自动转给实施会话
识别审查评论标记和 PR 里 Closes #N 的规则被写坏(多了反斜杠、标记名少了个 s), 审查评论全被当成普通评论丢掉。现在这两条规则只在 gitea/markers.ts 里定义一份, webhook 和评论气泡都调用它,并补上单测,以后写坏会直接测出来。
This commit is contained in:
1 parent
cb540c5db5
commit
858bd1e34b
4 files changed
+105
-11
No files matched your search
@@ -1,3 +1,5 @@
|
|||||||
|
import { hasReviewMarker, stripReviewMarker } from './markers'
|
||||||
|
|
||||||
const COLUMN_LABEL: Record<string, string> = {
|
const COLUMN_LABEL: Record<string, string> = {
|
||||||
'todo': '待办',
|
'todo': '待办',
|
||||||
'in-progress': '进行中',
|
'in-progress': '进行中',
|
||||||
@@ -26,8 +28,8 @@ const STATE_KEYS = new Set([
|
|||||||
/** 插件自己写的评论。返回系统气泡文案;普通讨论返回 undefined。 */
|
/** 插件自己写的评论。返回系统气泡文案;普通讨论返回 undefined。 */
|
||||||
export function systemCommentText(body: string): string | undefined {
|
export function systemCommentText(body: string): string | undefined {
|
||||||
const trimmed = body.trim()
|
const trimmed = body.trim()
|
||||||
if (/<!--\s*spx:review=1\s*-->/i.test(trimmed)) {
|
if (hasReviewMarker(trimmed)) {
|
||||||
const text = trimmed.replace(/<!--\s*spx:review=1\s*-->/gi, '').trim()
|
const text = stripReviewMarker(trimmed)
|
||||||
return text.length > 0 ? text : '提交了审查'
|
return text.length > 0 ? text : '提交了审查'
|
||||||
}
|
}
|
||||||
if (!trimmed.startsWith('{'))
|
if (!trimmed.startsWith('{'))
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { hasReviewMarker, parseClosingIssueNumber, stripReviewMarker } from './markers'
|
||||||
|
|
||||||
|
const REAL_REVIEW = `<!-- spx:review=1 -->
|
||||||
|
## 审查意见
|
||||||
|
|
||||||
|
- **coordinator.ts:1452** 正则多写了反斜杠
|
||||||
|
- 建议补单测
|
||||||
|
|
||||||
|
\`\`\`ts
|
||||||
|
const x = 1
|
||||||
|
\`\`\``
|
||||||
|
|
||||||
|
describe('审查标记', () => {
|
||||||
|
it('真实审查评论:识别为审查,去标记后正文完整', () => {
|
||||||
|
expect(hasReviewMarker(REAL_REVIEW)).toBe(true)
|
||||||
|
expect(stripReviewMarker(REAL_REVIEW)).toBe(REAL_REVIEW.slice('<!-- spx:review=1 -->\n'.length))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('普通评论不是审查', () => {
|
||||||
|
expect(hasReviewMarker('这个方案可以')).toBe(false)
|
||||||
|
expect(hasReviewMarker('提到了 spx:review=1 但不是注释')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('大小写和注释内空白都放宽', () => {
|
||||||
|
expect(hasReviewMarker('<!--SPX:Review=1-->\n正文')).toBe(true)
|
||||||
|
expect(hasReviewMarker('<!-- spx:review=1 -->')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('反复调用结果稳定', () => {
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
expect(hasReviewMarker(REAL_REVIEW)).toBe(true)
|
||||||
|
expect(stripReviewMarker(REAL_REVIEW)).toMatch(/^## 审查意见/)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('多个标记全部去掉,只剩标记时为空串', () => {
|
||||||
|
expect(stripReviewMarker('<!-- spx:review=1 -->\n a <!-- SPX:review=1 --> b')).toBe('a b')
|
||||||
|
expect(stripReviewMarker('<!-- spx:review=1 -->\n')).toBe('')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('parseClosingIssueNumber', () => {
|
||||||
|
it('多行 PR body 最后一行 Closes #462', () => {
|
||||||
|
const body = '## 改动\n\n- 修了正则\n- 加了测试\n\nCloses #462'
|
||||||
|
expect(parseClosingIssueNumber(body)).toBe(462)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('其它关键字与大小写', () => {
|
||||||
|
expect(parseClosingIssueNumber('fixes #12')).toBe(12)
|
||||||
|
expect(parseClosingIssueNumber('FiXeS #7')).toBe(7)
|
||||||
|
expect(parseClosingIssueNumber('RESOLVE #3')).toBe(3)
|
||||||
|
expect(parseClosingIssueNumber('close\t#5')).toBe(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('没有关键字返回 undefined', () => {
|
||||||
|
expect(parseClosingIssueNumber('关联 #462')).toBeUndefined()
|
||||||
|
expect(parseClosingIssueNumber('')).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('不误匹配:# 前无空白、关键字粘在别的词后面', () => {
|
||||||
|
expect(parseClosingIssueNumber('closes#1')).toBeUndefined()
|
||||||
|
expect(parseClosingIssueNumber('prefixcloses #1')).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* 评论 / PR 正文里约定标记的唯一解析入口。
|
||||||
|
*
|
||||||
|
* - 审查标记:审查会话发到 PR 上的评论第一行写 `<!-- spx:review=1 -->`,
|
||||||
|
* webhook 靠它识别"这是审查意见,要转给实施会话"。
|
||||||
|
* - 关闭关键字:PR body 里的 `Closes #N` 等,用来从 PR 反查工单号。
|
||||||
|
*/
|
||||||
|
|
||||||
|
const REVIEW_MARKER = /<!--\s*spx:review=1\s*-->/i
|
||||||
|
// 带 g 的正则只给 replace 用:replace 每次从头扫,不受 lastIndex 残留影响。
|
||||||
|
const REVIEW_MARKER_WITH_TRAILING_SPACE = new RegExp(`${REVIEW_MARKER.source}\\s*`, 'gi')
|
||||||
|
const CLOSING_KEYWORD = /\b(?:closes|fixes|resolves|close|fix|resolve)\s+#(\d+)/i
|
||||||
|
|
||||||
|
export function hasReviewMarker(body: string): boolean {
|
||||||
|
return REVIEW_MARKER.test(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 去掉所有审查标记及其后空白,返回 trim 后的正文。 */
|
||||||
|
export function stripReviewMarker(body: string): string {
|
||||||
|
return body.replace(REVIEW_MARKER_WITH_TRAILING_SPACE, '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从 PR body 取第一个关闭关键字指向的工单号。 */
|
||||||
|
export function parseClosingIssueNumber(body: string): number | undefined {
|
||||||
|
const m = body.match(CLOSING_KEYWORD)
|
||||||
|
return m ? Number.parseInt(m[1], 10) : undefined
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ import { detectRepo } from '../git/remote'
|
|||||||
import { deleteWebhook, getIssue, getPullRequest, listIssueComments } from '../gitea/api'
|
import { deleteWebhook, getIssue, getPullRequest, listIssueComments } from '../gitea/api'
|
||||||
import { systemCommentText } from '../gitea/issueComment'
|
import { systemCommentText } from '../gitea/issueComment'
|
||||||
import { loadIssues, loadSingleIssue } from '../gitea/issueLoader'
|
import { loadIssues, loadSingleIssue } from '../gitea/issueLoader'
|
||||||
|
import { hasReviewMarker, parseClosingIssueNumber, stripReviewMarker } from '../gitea/markers'
|
||||||
import { hasStateColumn, mergeStateJsonComment, mergeStateJsonCommentGuarded, readStateJsonComment } from '../gitea/stateJson'
|
import { hasStateColumn, mergeStateJsonComment, mergeStateJsonCommentGuarded, readStateJsonComment } from '../gitea/stateJson'
|
||||||
import { getLocalIssueState, mergeLocalIssueState, overlayLocalIssueState } from '../issues/localState'
|
import { getLocalIssueState, mergeLocalIssueState, overlayLocalIssueState } from '../issues/localState'
|
||||||
import { logger } from '../logging/logger'
|
import { logger } from '../logging/logger'
|
||||||
@@ -357,9 +358,8 @@ class WebhookCoordinator {
|
|||||||
return event.issueNumber
|
return event.issueNumber
|
||||||
|
|
||||||
const body = event.body || ''
|
const body = event.body || ''
|
||||||
const m = body.match(/\b(?:closes|fixes|resolves|close|fix|resolve)\s+#(\d+)/i)
|
const n = parseClosingIssueNumber(body)
|
||||||
if (m) {
|
if (n !== undefined) {
|
||||||
const n = Number.parseInt(m[1], 10)
|
|
||||||
logger.add({
|
logger.add({
|
||||||
level: 'info',
|
level: 'info',
|
||||||
source: 'webhook',
|
source: 'webhook',
|
||||||
@@ -1449,9 +1449,9 @@ class WebhookCoordinator {
|
|||||||
repo: ctx.repo,
|
repo: ctx.repo,
|
||||||
index: event.prNumber,
|
index: event.prNumber,
|
||||||
})
|
})
|
||||||
const m = (pr.body ?? '').match(/\\b(?:closes|fixes|resolves|close|fix|resolve)\\s+#(\\d+)/i)
|
const closing = parseClosingIssueNumber(pr.body ?? '')
|
||||||
if (m) {
|
if (closing !== undefined) {
|
||||||
realIssueNumber = Number.parseInt(m[1], 10)
|
realIssueNumber = closing
|
||||||
logger.add({
|
logger.add({
|
||||||
level: 'info',
|
level: 'info',
|
||||||
source: 'webhook',
|
source: 'webhook',
|
||||||
@@ -1532,7 +1532,7 @@ class WebhookCoordinator {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!/<!--\\s+px:review=1\\s*-->/i.test(body)) {
|
if (!hasReviewMarker(body)) {
|
||||||
logger.add({
|
logger.add({
|
||||||
level: 'info',
|
level: 'info',
|
||||||
source: 'webhook',
|
source: 'webhook',
|
||||||
@@ -1540,7 +1540,7 @@ class WebhookCoordinator {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const text = body.replace(/<!--\\s+px:review=1\\s*-->\\s*/i, '').trim()
|
const text = stripReviewMarker(body)
|
||||||
if (!text) {
|
if (!text) {
|
||||||
logger.add({
|
logger.add({
|
||||||
level: 'warn',
|
level: 'warn',
|
||||||
@@ -1570,7 +1570,7 @@ class WebhookCoordinator {
|
|||||||
repo: ctx.repo,
|
repo: ctx.repo,
|
||||||
index: commentsIndex,
|
index: commentsIndex,
|
||||||
})
|
})
|
||||||
reviewCount = comments.filter(c => /<!--\\s+px:review=1\\s*-->/i.test(c.body ?? '')).length
|
reviewCount = comments.filter(c => hasReviewMarker(c.body ?? '')).length
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
const message = err instanceof Error ? err.message : String(err)
|
const message = err instanceof Error ? err.message : String(err)
|
||||||
|
|||||||
Reference in new issue
Block a user