Files
openchamber/packages/web/server/lib/walkthrough/pull-request.js
T
bot-hermes 4ce02ee569 fix: address bot review findings
- Add codeberg.org to deriveLinkedIssueProvider built-in Gitea check
- Fix Gitea PR review prompt: !N → #N (GitLab vs Gitea syntax)
- Update gitlab.mdx: remove 'read-only' claim (write support ships)
- Gate walkthrough Gitea PR source (server can't fulfill yet)
- Thread per-project API base URL override into walkthrough GitLab diff
- Stop following cross-origin redirects in GitLab/Gitea clients (auth leak prevention)
2026-08-18 20:35:46 +00:00

139 lines
4.9 KiB
JavaScript

import { getOctokitOrNull } from '../github/octokit.js';
import { resolveGitHubRepoFromDirectory } from '../github/repo/index.js';
import { getGitLabClientOrNull } from '../gitlab/client.js';
import { createGitLabClient } from '../gitlab/client.js';
import { getGitLabAuth, getGitLabDefaultBaseUrl } from '../gitlab/auth.js';
import { getEffectiveProviderApiBaseUrl } from '../git-providers/project-config.js';
import { resolveGitLabRepoFromDirectory } from '../gitlab/repo.js';
// GitLab diff pagination cap: never loop more than 10 pages of 100 files,
// mirroring gitlab/routes.js.
const GITLAB_DIFFS_MAX_PAGES = 10;
/**
* Raw unified diff for a GitHub pull request.
*
* GitHub already returns the merge-base diff for a PR, so this matches the
* three-dot semantics used for local branch reviews: work merged in from the
* base branch is not part of it.
*/
async function getGitHubPullRequestDiff(directory, number) {
const octokit = getOctokitOrNull(directory);
if (!octokit) {
throw Object.assign(new Error('Connect a GitHub account to review pull requests'), {
statusCode: 401,
code: 'github-not-connected',
});
}
// The resolver returns `{ repo, remoteUrl }`, not the repo itself. Reading
// `.owner` off the wrapper made this check fail for every repository.
const { repo } = await resolveGitHubRepoFromDirectory(directory);
if (!repo?.owner || !repo?.repo) {
throw Object.assign(new Error('This directory has no GitHub remote'), {
statusCode: 400,
code: 'no-github-remote',
});
}
const response = await octokit.request('GET /repos/{owner}/{repo}/pulls/{pull_number}', {
owner: repo.owner,
repo: repo.repo,
pull_number: number,
headers: { accept: 'application/vnd.github.v3.diff' },
});
const patch = typeof response?.data === 'string' ? response.data : '';
if (!patch.trim()) {
throw Object.assign(new Error(`Pull request #${number} has no diff`), {
statusCode: 404,
code: 'empty-diff',
});
}
return { patch, meta: { owner: repo.owner, repo: repo.repo, number } };
}
/**
* Raw unified diff for a GitLab merge request.
*
* GitLab's merge request diffs endpoint returns one entry per file, so the
* pages are concatenated into a single patch. `repo` comes from the
* dispatcher's `resolveGitLabRepoFromDirectory` call and is never re-resolved
* here.
*/
async function getGitLabMergeRequestDiff(repo, number) {
// Resolve per-project API base override, mirroring getClient() in routes.js.
const auth = getGitLabAuth();
if (!auth?.accessToken) {
throw Object.assign(new Error('Connect a GitLab account to review merge requests'), {
statusCode: 401,
code: 'gitlab-not-connected',
});
}
const effectiveBaseUrl = getEffectiveProviderApiBaseUrl('gitlab', directory) || getGitLabDefaultBaseUrl();
const client = effectiveBaseUrl !== getGitLabDefaultBaseUrl()
? createGitLabClient({ token: auth.accessToken, baseUrl: effectiveBaseUrl })
: getGitLabClientOrNull();
if (!client) {
throw Object.assign(new Error('Connect a GitLab account to review merge requests'), {
statusCode: 401,
code: 'gitlab-not-connected',
});
}
// The parser always populates both fields; this guards a malformed repo so
// the failure is explicit rather than a downstream TypeError.
if (!repo?.namespace || !repo?.project) {
throw Object.assign(new Error('This directory has no GitLab remote'), {
statusCode: 400,
code: 'no-gitlab-remote',
});
}
// The client URL-encodes the path internally; never pre-encode it.
const projectPath = `${repo.namespace}/${repo.project}`;
const diffs = [];
for (let page = 1; page <= GITLAB_DIFFS_MAX_PAGES; page += 1) {
const response = await client.mergeRequestDiffs(projectPath, number, { per_page: 100, page });
if (response.status !== 200 || !Array.isArray(response.data)) {
break;
}
diffs.push(...response.data);
// The page object is the authoritative signal; the 10-page cap above is
// what stops a server that lies about hasMore from looping forever.
if (!response.page?.hasMore) {
break;
}
}
const patch = diffs
.map((item) => (typeof item?.diff === 'string' ? item.diff : ''))
.filter(Boolean)
.join('\n');
if (!patch.trim()) {
throw Object.assign(new Error(`Merge request #${number} has no diff`), {
statusCode: 404,
code: 'empty-diff',
});
}
return { patch, meta: { namespace: repo.namespace, project: repo.project, number } };
}
/**
* Raw unified diff for a pull request or merge request.
*
* The provider is chosen by the repository's git remote: a GitLab remote uses
* the GitLab merge request API, anything else falls back to the GitHub pull
* request API.
*/
export async function getPullRequestDiff(directory, number) {
const { repo } = await resolveGitLabRepoFromDirectory(directory);
if (repo) {
return getGitLabMergeRequestDiff(repo, number);
}
return getGitHubPullRequestDiff(directory, number);
}