// // Inline review comments. Nova has no editor decoration or comment-thread API, // so pull request review comments for the current branch are published as // issues: they appear in the gutter, on hover, and in the Issues sidebar. // const config = require("../util/config.js"); const log = require("../util/log.js"); const { lineFromDiffHunk } = require("./review.js"); const DEBOUNCE_MS = 750; function uriFor(path) { return `file://${encodeURI(path).replace(/#/g, "%23")}`; } function severityFor(comment) { if (comment.resolver && comment.resolver.login) return IssueSeverity.Info; const state = String(comment.review ? comment.review.state : "").toUpperCase(); return state === "REQUEST_CHANGES" ? IssueSeverity.Error : IssueSeverity.Warning; } class ReviewCommentsController { constructor(store) { this.store = store; this.collection = new IssueCollection("Gitea Review"); this._paths = new Set(); this._signature = null; this._timer = null; this._running = false; this._subscription = store.onDidChange(() => this.scheduleReload()); } /** Coalesces the frequent store updates into one rebuild. */ scheduleReload() { if (this._timer) clearTimeout(this._timer); this._timer = setTimeout(() => { this._timer = null; this.reload(); }, DEBOUNCE_MS); } /** * Cheap fingerprint of the pull requests in scope. When it is unchanged the * comment fetch is skipped, which keeps idle polling free of API calls. */ signature(targets) { return targets .map(({ repo, pull }) => `${repo.key}:${pull.number}:${pull.updated_at}`) .sort() .join("|"); } /** Pull requests whose head branch matches the branch filter, per repo. */ targets() { const found = []; for (const repo of this.store.repos) { if (!repo.dir) continue; // Without a checkout there is no file to annotate. const branch = this.store.branchFor(repo); for (const pull of this.store.pullRequestsFor(repo.key).items) { if (branch && (!pull.head || pull.head.ref !== branch)) continue; found.push({ repo, pull }); } } return found; } async reload({ force = false } = {}) { if (this._running) return; if (!config.reviewCommentsEnabled()) { this.clear(); this._signature = null; return; } const targets = this.targets(); const signature = this.signature(targets); if (!force && signature === this._signature) return; this._running = true; try { const byPath = new Map(); for (const { repo, pull } of targets) { const detail = this.store.detailFor(repo.key, pull.number) || (await this.store.loadPullRequestDetail(repo, pull)); if (!detail) continue; for (const comment of detail.comments) { if (!comment.path) continue; const path = nova.path.join(repo.dir, comment.path); if (!nova.fs.access(path, nova.fs.F_OK)) continue; const issue = new Issue(); issue.source = `Gitea #${pull.number}`; issue.code = String(comment.id); issue.severity = severityFor(comment); issue.message = this.describe(comment); const line = lineFromDiffHunk(comment.diff_hunk, comment.position) || 1; issue.line = line; issue.column = 1; issue.endLine = line; issue.endColumn = 1; if (!byPath.has(path)) byPath.set(path, []); byPath.get(path).push(issue); } } this.apply(byPath); this._signature = signature; } catch (error) { log.error("review comments failed", String(error && error.stack ? error.stack : error)); } finally { this._running = false; } } describe(comment) { const author = (comment.user && comment.user.login) || "someone"; const resolved = comment.resolver && comment.resolver.login ? " (resolved)" : ""; const body = String(comment.body || "").trim() || "(empty comment)"; return `${author}${resolved}: ${body}`; } /** Replaces the published set, clearing files that no longer have comments. */ apply(byPath) { for (const path of this._paths) { if (!byPath.has(path)) this.collection.remove(uriFor(path)); } for (const [path, issues] of byPath) { this.collection.set(uriFor(path), issues); } this._paths = new Set(byPath.keys()); } clear() { this.collection.clear(); this._paths = new Set(); } dispose() { if (this._timer) clearTimeout(this._timer); if (this._subscription) this._subscription.dispose(); this.collection.dispose(); } } exports.ReviewCommentsController = ReviewCommentsController; exports.uriFor = uriFor;