- 本机状态(会话id/worktree/profile/prDiffFile)迁 workspaceState, 共享 state JSON 只留团队字段;读侧本地记录叠加,旧数据兜底 - mergeStateJsonComment 写后读回校验,并发覆盖时在最新状态上重放一次 - webhook 支持 Gitea secret,校验 X-Gitea-Signature(HMAC-SHA256), 不匹配 401;设置面板新增 Webhook Secret 字段 - 看板新增 我的/全部 范围切换(团队视图),按工作区持久化
121 lines
5.0 KiB
TypeScript
121 lines
5.0 KiB
TypeScript
/**
|
|
* Source-aware workflow-state persistence.
|
|
*
|
|
* Gitea and YouTrack cards share the same board and the same session/worktree
|
|
* machinery, but their workflow state (column, sessionId, worktreePath, …)
|
|
* lives in different places: a gitea issue comment vs a YouTrack issue comment.
|
|
* Handlers call these helpers with an `IssueRef` instead of hard-coding the
|
|
* gitea backend, so the same handler body serves both sources.
|
|
*
|
|
* The gitea branch reproduces exactly what the handlers did inline before, so
|
|
* gitea behavior is unchanged.
|
|
*/
|
|
|
|
import type { ExtensionContext } from 'vscode'
|
|
import { workspace } from 'vscode'
|
|
import { getToken, getYouTrackToken } from '../auth/secrets'
|
|
import { detectRepo } from '../git/remote'
|
|
import { closeIssue as giteaCloseIssue } from '../gitea/api'
|
|
import { mergeStateJsonComment, readStateJsonComment } from '../gitea/stateJson'
|
|
import { getSettings } from '../settings/store'
|
|
import { applyCommand, resolvedStateCommand } from '../youtrack/api'
|
|
import { youtrackHost } from '../youtrack/issueLoader'
|
|
import { mergeStateComment, readStateComment } from '../youtrack/stateComment'
|
|
import { getLocalIssueState, mergeLocalIssueState, splitLocalStateFields } from './localState'
|
|
|
|
export interface IssueRef {
|
|
/** Absent = gitea (back-compat). */
|
|
source?: 'gitea' | 'youtrack'
|
|
/** The board's (possibly synthetic) issue number — the routing key. */
|
|
number: number
|
|
/** YouTrack readable id (`LXF-12`); required when source is youtrack. */
|
|
externalId?: string
|
|
}
|
|
|
|
function isYouTrack(ref: IssueRef): ref is IssueRef & { externalId: string } {
|
|
return ref.source === 'youtrack' && typeof ref.externalId === 'string' && ref.externalId.length > 0
|
|
}
|
|
|
|
async function giteaDeps(ctx: ExtensionContext): Promise<{ host: string, owner: string, repo: string, token: string }> {
|
|
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
|
if (!workspaceRoot)
|
|
throw new Error('未打开工作区文件夹')
|
|
const remote = await detectRepo(workspaceRoot)
|
|
if (!remote)
|
|
throw new Error('当前工作区没有 Gitea 远程仓库')
|
|
const token = await getToken(ctx, remote.host)
|
|
if (!token)
|
|
throw new Error('请先完成 Gitea 配置')
|
|
return { host: remote.host, owner: remote.owner, repo: remote.repo, token }
|
|
}
|
|
|
|
async function youtrackAuth(ctx: ExtensionContext): Promise<{ baseUrl: string, token: string }> {
|
|
const baseUrl = getSettings(ctx).youtrackBaseUrl.trim()
|
|
if (!baseUrl)
|
|
throw new Error('未配置 YouTrack Base URL')
|
|
const token = await getYouTrackToken(ctx, youtrackHost(baseUrl))
|
|
if (!token)
|
|
throw new Error('未配置 YouTrack Token')
|
|
return { baseUrl, token }
|
|
}
|
|
|
|
/**
|
|
* Read the workflow-state blob for an issue from its tracker, with this
|
|
* machine's local record overlaid on top(本地有键即覆盖,'' 墓碑与
|
|
* "空串=unset" 的既有约定一致)。
|
|
*/
|
|
export async function readIssueState(ctx: ExtensionContext, ref: IssueRef): Promise<Record<string, unknown>> {
|
|
let base: Record<string, unknown>
|
|
if (isYouTrack(ref)) {
|
|
const auth = await youtrackAuth(ctx)
|
|
base = await readStateComment(auth, ref.externalId)
|
|
}
|
|
else {
|
|
const d = await giteaDeps(ctx)
|
|
base = await readStateJsonComment({ ...d, issueNumber: ref.number })
|
|
}
|
|
return { ...base, ...getLocalIssueState(ctx, ref.source, ref.number) }
|
|
}
|
|
|
|
/**
|
|
* Merge `extra` into the issue's workflow state。机器本地字段
|
|
* (会话 id / worktree / profile 路径)落 workspaceState,只有共享字段
|
|
* (column / pr / branch / spec 等)才写进 tracker 评论——跨机没有语义的
|
|
* 值从此不进共享存储,也不参与多机写竞争。
|
|
*/
|
|
export async function mergeIssueState(ctx: ExtensionContext, ref: IssueRef, extra: Record<string, unknown>): Promise<void> {
|
|
const { local, shared } = splitLocalStateFields(extra)
|
|
if (Object.keys(local).length > 0)
|
|
await mergeLocalIssueState(ctx, ref.source, ref.number, local)
|
|
if (Object.keys(shared).length === 0)
|
|
return
|
|
if (isYouTrack(ref)) {
|
|
const auth = await youtrackAuth(ctx)
|
|
await mergeStateComment(auth, ref.externalId, shared)
|
|
return
|
|
}
|
|
const d = await giteaDeps(ctx)
|
|
await mergeStateJsonComment({ ...d, issueNumber: ref.number, extra: shared })
|
|
}
|
|
|
|
/**
|
|
* Resolve/close an issue in its tracker. Gitea: PATCH state=closed. YouTrack:
|
|
* apply the configured close command, else auto-detect the project's first
|
|
* `isResolved` state value. Returns false (with no throw) when a YouTrack close
|
|
* command can't be determined, so callers can surface a hint.
|
|
*/
|
|
export async function closeIssueByRef(ctx: ExtensionContext, ref: IssueRef): Promise<boolean> {
|
|
if (isYouTrack(ref)) {
|
|
const auth = await youtrackAuth(ctx)
|
|
const configured = getSettings(ctx).youtrackCloseCommand.trim()
|
|
const command = configured || (await resolvedStateCommand(auth, ref.externalId))
|
|
if (!command)
|
|
return false
|
|
await applyCommand(auth, ref.externalId, command)
|
|
return true
|
|
}
|
|
const d = await giteaDeps(ctx)
|
|
await giteaCloseIssue({ ...d, issueNumber: ref.number })
|
|
return true
|
|
}
|