提交 5c4e6138 编写于 作者: T Tomas Vik

fix: some self-managed GitLab deployments not handling project URLs

上级 d19fb6a6
...@@ -38,7 +38,8 @@ ...@@ -38,7 +38,8 @@
"import/no-extraneous-dependencies": [ "import/no-extraneous-dependencies": [
"error", "error",
{ "devDependencies": ["**/*.test.ts", "test/**/*"] } { "devDependencies": ["**/*.test.ts", "test/**/*"] }
] ],
"no-useless-constructor": "off"
}, },
"settings": { "settings": {
"import/resolver": { "import/resolver": {
......
...@@ -54,7 +54,7 @@ async function uploadSnippet(project, editor, visibility, context) { ...@@ -54,7 +54,7 @@ async function uploadSnippet(project, editor, visibility, context) {
data.content = content; data.content = content;
if (project) { if (project) {
data.id = project.id; data.id = project.restId;
} }
const snippet = await gitLabService.createSnippet(data); const snippet = await gitLabService.createSnippet(data);
......
...@@ -10,6 +10,8 @@ import { FetchError } from '../errors/fetch_error'; ...@@ -10,6 +10,8 @@ import { FetchError } from '../errors/fetch_error';
import { getUserAgentHeader } from '../utils/get_user_agent_header'; import { getUserAgentHeader } from '../utils/get_user_agent_header';
import { getAvatarUrl } from '../utils/get_avatar_url'; import { getAvatarUrl } from '../utils/get_avatar_url';
import { getHttpAgentOptions } from '../utils/get_http_agent_options'; import { getHttpAgentOptions } from '../utils/get_http_agent_options';
import { GitLabProject, GqlProject } from './gitlab_project';
import { getRestIdFromGraphQLId } from '../utils/get_rest_id_from_graphql_id';
interface Node<T> { interface Node<T> {
pageInfo?: { pageInfo?: {
...@@ -124,6 +126,19 @@ const queryGetSnippets = gql` ...@@ -124,6 +126,19 @@ const queryGetSnippets = gql`
} }
`; `;
const queryGetProject = gql`
query GetProject($projectPath: ID!) {
project(fullPath: $projectPath) {
id
name
fullPath
group {
id
}
}
}
`;
const positionFragment = gql` const positionFragment = gql`
fragment position on Note { fragment position on Note {
position { position {
...@@ -238,6 +253,13 @@ export class GitLabNewService { ...@@ -238,6 +253,13 @@ export class GitLabNewService {
}; };
} }
async getProject(projectPath: string): Promise<GitLabProject | undefined> {
const result = await this.client.request<GqlProjectResult<GqlProject>>(queryGetProject, {
projectPath,
});
return result.project && new GitLabProject(result.project);
}
async getSnippets(projectPath: string): Promise<GqlSnippet[]> { async getSnippets(projectPath: string): Promise<GqlSnippet[]> {
const result = await this.client.request<GqlProjectResult<GqlSnippetProject>>( const result = await this.client.request<GqlProjectResult<GqlSnippetProject>>(
queryGetSnippets, queryGetSnippets,
...@@ -264,8 +286,8 @@ export class GitLabNewService { ...@@ -264,8 +286,8 @@ export class GitLabNewService {
// TODO change this method to use GraphQL when https://gitlab.com/gitlab-org/gitlab/-/issues/260316 is done // TODO change this method to use GraphQL when https://gitlab.com/gitlab-org/gitlab/-/issues/260316 is done
async getSnippetContent(snippet: GqlSnippet, blob: GqlBlob): Promise<string> { async getSnippetContent(snippet: GqlSnippet, blob: GqlBlob): Promise<string> {
const projectId = snippet.projectId.replace('gid://gitlab/Project/', ''); const projectId = getRestIdFromGraphQLId(snippet.projectId);
const snippetId = snippet.id.replace('gid://gitlab/ProjectSnippet/', ''); const snippetId = getRestIdFromGraphQLId(snippet.id);
const url = `${this.instanceUrl}/api/v4/projects/${projectId}/snippets/${snippetId}/files/master/${blob.path}/raw`; const url = `${this.instanceUrl}/api/v4/projects/${projectId}/snippets/${snippetId}/files/master/${blob.path}/raw`;
const result = await crossFetch(url, this.fetchOptions); const result = await crossFetch(url, this.fetchOptions);
if (!result.ok) { if (!result.ok) {
......
import { getRestIdFromGraphQLId } from '../utils/get_rest_id_from_graphql_id';
interface GqlGroup {
id: string;
}
export interface GqlProject {
id: string;
name: string;
fullPath: string;
group?: GqlGroup;
}
export class GitLabProject {
constructor(private readonly gqlProject: GqlProject) {}
get gqlId(): string {
return this.gqlProject.id;
}
get restId(): number {
return getRestIdFromGraphQLId(this.gqlProject.id);
}
get name(): string {
return this.gqlProject.name;
}
get fullPath(): string {
return this.gqlProject.fullPath;
}
get groupRestId(): number | undefined {
return this.gqlProject.group && getRestIdFromGraphQLId(this.gqlProject.group.id);
}
}
...@@ -5,7 +5,7 @@ import { tokenService } from './services/token_service'; ...@@ -5,7 +5,7 @@ import { tokenService } from './services/token_service';
import { UserFriendlyError } from './errors/user_friendly_error'; import { UserFriendlyError } from './errors/user_friendly_error';
import { ApiError } from './errors/api_error'; import { ApiError } from './errors/api_error';
import { getCurrentWorkspaceFolder } from './services/workspace_service'; import { getCurrentWorkspaceFolder } from './services/workspace_service';
import { createGitService } from './service_factory'; import { createGitLabNewService, createGitService } from './service_factory';
import { GitRemote } from './git/git_remote_parser'; import { GitRemote } from './git/git_remote_parser';
import { handleError, logError } from './log'; import { handleError, logError } from './log';
import { getUserAgentHeader } from './utils/get_user_agent_header'; import { getUserAgentHeader } from './utils/get_user_agent_header';
...@@ -14,17 +14,7 @@ import { CustomQuery } from './gitlab/custom_query'; ...@@ -14,17 +14,7 @@ import { CustomQuery } from './gitlab/custom_query';
import { getAvatarUrl } from './utils/get_avatar_url'; import { getAvatarUrl } from './utils/get_avatar_url';
import { getHttpAgentOptions } from './utils/get_http_agent_options'; import { getHttpAgentOptions } from './utils/get_http_agent_options';
import { getInstanceUrl as getInstanceUrlUtil } from './utils/get_instance_url'; import { getInstanceUrl as getInstanceUrlUtil } from './utils/get_instance_url';
import { GitLabProject } from './gitlab/gitlab_project';
interface GitLabProject {
id: number;
name: string;
namespace: {
id: number;
kind: string;
};
// eslint-disable-next-line camelcase
path_with_namespace: string;
}
interface GitLabPipeline { interface GitLabPipeline {
id: number; id: number;
...@@ -100,14 +90,16 @@ async function fetch(path: string, method = 'GET', data?: Record<string, unknown ...@@ -100,14 +90,16 @@ async function fetch(path: string, method = 'GET', data?: Record<string, unknown
return await request(`${apiRoot}${path}`, config); return await request(`${apiRoot}${path}`, config);
} }
async function fetchProjectData(remote: GitRemote | null) { async function fetchProjectData(remote: GitRemote | null, workspaceFolder: string) {
if (remote) { if (remote) {
if (!(`${remote.namespace}_${remote.project}` in projectCache)) { if (!(`${remote.namespace}_${remote.project}` in projectCache)) {
const { namespace, project } = remote; const { namespace, project } = remote;
const { response } = await fetch(`/projects/${namespace.replace(/\//g, '%2F')}%2F${project}`); const gitlabNewService = await createGitLabNewService(workspaceFolder);
const projectData = response; const projectData = await gitlabNewService.getProject(`${namespace}/${project}`);
if (projectData) {
projectCache[`${remote.namespace}_${remote.project}`] = projectData; projectCache[`${remote.namespace}_${remote.project}`] = projectData;
} }
}
return projectCache[`${remote.namespace}_${remote.project}`] || null; return projectCache[`${remote.namespace}_${remote.project}`] || null;
} }
...@@ -118,7 +110,7 @@ export async function fetchCurrentProject(workspaceFolder: string): Promise<GitL ...@@ -118,7 +110,7 @@ export async function fetchCurrentProject(workspaceFolder: string): Promise<GitL
try { try {
const remote = await createGitService(workspaceFolder).fetchGitRemote(); const remote = await createGitService(workspaceFolder).fetchGitRemote();
return await fetchProjectData(remote); return await fetchProjectData(remote, workspaceFolder);
} catch (e) { } catch (e) {
throw new ApiError(e, 'get current project'); throw new ApiError(e, 'get current project');
} }
...@@ -137,7 +129,7 @@ export async function fetchCurrentPipelineProject(workspaceFolder: string) { ...@@ -137,7 +129,7 @@ export async function fetchCurrentPipelineProject(workspaceFolder: string) {
try { try {
const remote = await createGitService(workspaceFolder).fetchGitRemotePipeline(); const remote = await createGitService(workspaceFolder).fetchGitRemotePipeline();
return await fetchProjectData(remote); return await fetchProjectData(remote, workspaceFolder);
} catch (e) { } catch (e) {
logError(e); logError(e);
return null; return null;
...@@ -195,7 +187,7 @@ export async function fetchLastPipelineForCurrentBranch(workspaceFolder: string) ...@@ -195,7 +187,7 @@ export async function fetchLastPipelineForCurrentBranch(workspaceFolder: string)
if (project) { if (project) {
const branchName = await createGitService(workspaceFolder).fetchTrackingBranchName(); const branchName = await createGitService(workspaceFolder).fetchTrackingBranchName();
const pipelinesRootPath = `/projects/${project.id}/pipelines`; const pipelinesRootPath = `/projects/${project.restId}/pipelines`;
const { response } = await fetch(`${pipelinesRootPath}?ref=${branchName}`); const { response } = await fetch(`${pipelinesRootPath}?ref=${branchName}`);
const pipelines = response; const pipelines = response;
...@@ -246,15 +238,15 @@ export async function fetchIssuables(params: CustomQuery, workspaceFolder: strin ...@@ -246,15 +238,15 @@ export async function fetchIssuables(params: CustomQuery, workspaceFolder: strin
let path = ''; let path = '';
if (config.type === 'epics') { if (config.type === 'epics') {
if (project.namespace.kind === 'group') { if (project.groupRestId) {
path = `/groups/${project.namespace.id}/${config.type}?include_ancestor_groups=true&state=${config.state}`; path = `/groups/${project.groupRestId}/${config.type}?include_ancestor_groups=true&state=${config.state}`;
} else { } else {
return []; return [];
} }
} else { } else {
const searchKind = const searchKind =
config.type === CustomQueryType.VULNERABILITY ? 'vulnerability_findings' : config.type; config.type === CustomQueryType.VULNERABILITY ? 'vulnerability_findings' : config.type;
path = `/projects/${project.id}/${searchKind}?scope=${config.scope}&state=${config.state}`; path = `/projects/${project.restId}/${searchKind}?scope=${config.scope}&state=${config.state}`;
} }
if (config.type === 'issues') { if (config.type === 'issues') {
if (author) { if (author) {
...@@ -344,7 +336,7 @@ export async function fetchLastJobsForCurrentBranch( ...@@ -344,7 +336,7 @@ export async function fetchLastJobsForCurrentBranch(
) { ) {
const project = await fetchCurrentPipelineProject(workspaceFolder); const project = await fetchCurrentPipelineProject(workspaceFolder);
if (project) { if (project) {
const { response } = await fetch(`/projects/${project.id}/pipelines/${pipeline.id}/jobs`); const { response } = await fetch(`/projects/${project.restId}/pipelines/${pipeline.id}/jobs`);
let jobs: GitLabJob[] = response; let jobs: GitLabJob[] = response;
// Gitlab return multiple jobs if you retry the pipeline we filter to keep only the last // Gitlab return multiple jobs if you retry the pipeline we filter to keep only the last
...@@ -368,7 +360,7 @@ export async function fetchOpenMergeRequestForCurrentBranch(workspaceFolder: str ...@@ -368,7 +360,7 @@ export async function fetchOpenMergeRequestForCurrentBranch(workspaceFolder: str
const project = await fetchCurrentProjectSwallowError(workspaceFolder); const project = await fetchCurrentProjectSwallowError(workspaceFolder);
const branchName = await createGitService(workspaceFolder).fetchTrackingBranchName(); const branchName = await createGitService(workspaceFolder).fetchTrackingBranchName();
const path = `/projects/${project?.id}/merge_requests?state=opened&source_branch=${branchName}`; const path = `/projects/${project?.restId}/merge_requests?state=opened&source_branch=${branchName}`;
const { response } = await fetch(path); const { response } = await fetch(path);
const mrs = response; const mrs = response;
...@@ -389,11 +381,11 @@ export async function handlePipelineAction(action: string, workspaceFolder: stri ...@@ -389,11 +381,11 @@ export async function handlePipelineAction(action: string, workspaceFolder: stri
const project = await fetchCurrentProjectSwallowError(workspaceFolder); const project = await fetchCurrentProjectSwallowError(workspaceFolder);
if (pipeline && project) { if (pipeline && project) {
let endpoint = `/projects/${project.id}/pipelines/${pipeline.id}/${action}`; let endpoint = `/projects/${project.restId}/pipelines/${pipeline.id}/${action}`;
if (action === 'create') { if (action === 'create') {
const branchName = await createGitService(workspaceFolder).fetchTrackingBranchName(); const branchName = await createGitService(workspaceFolder).fetchTrackingBranchName();
endpoint = `/projects/${project.id}/pipeline?ref=${branchName}`; endpoint = `/projects/${project.restId}/pipeline?ref=${branchName}`;
} }
try { try {
...@@ -415,7 +407,7 @@ export async function fetchMRIssues(mrId: number, workspaceFolder: string) { ...@@ -415,7 +407,7 @@ export async function fetchMRIssues(mrId: number, workspaceFolder: string) {
if (project) { if (project) {
try { try {
const { response } = await fetch( const { response } = await fetch(
`/projects/${project.id}/merge_requests/${mrId}/closes_issues`, `/projects/${project.restId}/merge_requests/${mrId}/closes_issues`,
); );
issues = response; issues = response;
} catch (e) { } catch (e) {
...@@ -480,8 +472,7 @@ export async function renderMarkdown(markdown: string, workspaceFolder: string) ...@@ -480,8 +472,7 @@ export async function renderMarkdown(markdown: string, workspaceFolder: string)
const project = await fetchCurrentProject(workspaceFolder); const project = await fetchCurrentProject(workspaceFolder);
const { response } = await fetch('/markdown', 'POST', { const { response } = await fetch('/markdown', 'POST', {
text: markdown, text: markdown,
// eslint-disable-next-line camelcase project: project?.fullPath,
project: project?.path_with_namespace,
gfm: 'true', // Needs to be a string for the API gfm: 'true', // Needs to be a string for the API
}); });
rendered = response; rendered = response;
......
import * as assert from 'assert';
// copied from the gitlab-org/gitlab project
// https://gitlab.com/gitlab-org/gitlab/-/blob/a4b939809c68c066e358a280491bf4ec2ff439a2/app/assets/javascripts/graphql_shared/utils.js#L9-10
export const getRestIdFromGraphQLId = (gid: string): number => {
const result = parseInt(gid.replace(/gid:\/\/gitlab\/.*\//g, ''), 10);
assert(result, `the gid ${gid} can't be parsed into REST id`);
return result;
};
{
"project": {
"id": "gid://gitlab/Project/278964",
"name": "GitLab",
"fullPath": "gitlab-org/gitlab",
"group": {
"id": "gid://gitlab/Group/9970"
}
}
}
{
"avatar_url": "https://assets.gitlab-static.net/uploads/-/system/project/avatar/278964/logo-extra-whitespace.png",
"created_at": "2015-05-20T10:47:11.949Z",
"default_branch": "master",
"description": "GitLab is an open source end-to-end software development platform with built-in version control, issue tracking, code review, CI/CD, and more. Self-host GitLab on your own servers, in a container, or on a cloud provider.",
"forks_count": 2193,
"http_url_to_repo": "https://gitlab.com/gitlab-org/gitlab.git",
"id": 278964,
"last_activity_at": "2020-07-23T12:59:24.905Z",
"name": "GitLab",
"name_with_namespace": "GitLab.org / GitLab",
"namespace": {
"avatar_url": "/uploads/-/system/group/avatar/9970/logo-extra-whitespace.png",
"full_path": "gitlab-org",
"id": 9970,
"kind": "group",
"name": "GitLab.org",
"parent_id": null,
"path": "gitlab-org",
"web_url": "https://gitlab.com/groups/gitlab-org"
},
"path": "gitlab",
"path_with_namespace": "gitlab-org/gitlab",
"readme_url": "https://gitlab.com/gitlab-org/gitlab/-/blob/master/README.md",
"ssh_url_to_repo": "git@gitlab.com:gitlab-org/gitlab.git",
"star_count": 1974,
"tag_list": [],
"web_url": "https://gitlab.com/gitlab-org/gitlab"
}
const { setupServer } = require('msw/node'); const { setupServer } = require('msw/node');
const { rest } = require('msw'); const { rest, graphql } = require('msw');
const { API_URL_PREFIX } = require('./constants'); const { API_URL_PREFIX } = require('./constants');
const projectResponse = require('../fixtures/rest/project.json'); const projectResponse = require('../fixtures/graphql/project.json');
const versionResponse = require('../fixtures/rest/version.json'); const versionResponse = require('../fixtures/rest/version.json');
const createJsonEndpoint = (path, response) => const createJsonEndpoint = (path, response) =>
...@@ -46,7 +46,10 @@ const notFoundByDefault = rest.get(/.*/, (req, res, ctx) => { ...@@ -46,7 +46,10 @@ const notFoundByDefault = rest.get(/.*/, (req, res, ctx) => {
const getServer = (handlers = []) => { const getServer = (handlers = []) => {
const server = setupServer( const server = setupServer(
createJsonEndpoint('/projects/gitlab-org%2Fgitlab', projectResponse), graphql.query('GetProject', (req, res, ctx) => {
if (req.variables.projectPath === 'gitlab-org/gitlab') return res(ctx.data(projectResponse));
return res(ctx.data({ project: null }));
}),
createJsonEndpoint('/version', versionResponse), createJsonEndpoint('/version', versionResponse),
...handlers, ...handlers,
notFoundByDefault, notFoundByDefault,
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册