- 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)
139 lines
4.9 KiB
JavaScript
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);
|
|
}
|