diff --git a/src/app/features/issue/providers/gitlab/gitlab-cfg-form.const.spec.ts b/src/app/features/issue/providers/gitlab/gitlab-cfg-form.const.spec.ts index 69b22d7d55..2d9a44c55d 100644 --- a/src/app/features/issue/providers/gitlab/gitlab-cfg-form.const.spec.ts +++ b/src/app/features/issue/providers/gitlab/gitlab-cfg-form.const.spec.ts @@ -29,9 +29,9 @@ describe('GITLAB_PROJECT_REGEX', () => { 'a.b-c/d_e', 'group%2Fproject', 'group%2Fsub%2Fproject', - // GitLab allows consecutive hyphens in a path segment; must not be rejected. - 'foo--bar', - 'single', + // GitLab allows consecutive hyphens in a path segment; must not be rejected + // (as long as the reference is still namespace-qualified). + 'group/foo--bar', '12345', ]; validCases.forEach((value) => { @@ -42,8 +42,9 @@ describe('GITLAB_PROJECT_REGEX', () => { }); describe('invalid project identifiers', () => { - // Regression for #8665: a display name with a space must be rejected inline - // instead of producing a confusing 404 at poll time. + // Regression for #8665: both a display name with a space AND a bare + // single-segment slug (which the REST API can never resolve, so it 404s at + // poll time) must be rejected inline instead. const invalidCases = [ 'My Group/My Gitlab', 'group/Test Gitlab', @@ -52,6 +53,10 @@ describe('GITLAB_PROJECT_REGEX', () => { 'trailingSpace ', 'https://gitlab.com/foo/bar', 'foo/bar?scope=all', + // Missing namespace — the exact value from the #8665 logs. + 'test_config', + 'single', + 'foo--bar', ]; invalidCases.forEach((value) => { it(`rejects "${value}"`, () => { diff --git a/src/app/features/issue/providers/gitlab/gitlab-cfg-form.const.ts b/src/app/features/issue/providers/gitlab/gitlab-cfg-form.const.ts index 247047acff..1be44f9758 100644 --- a/src/app/features/issue/providers/gitlab/gitlab-cfg-form.const.ts +++ b/src/app/features/issue/providers/gitlab/gitlab-cfg-form.const.ts @@ -8,14 +8,15 @@ import { CROSS_ORIGIN_WARNING, ISSUE_PROVIDER_COMMON_FORM_FIELDS, } from '../../common-issue-form-stuff.const'; -// Matches either a numeric project ID or a path (namespace/project-slug, or its -// %2F-encoded form). Anchored at both ends: without a leading `^` the original -// pattern only matched a valid *tail*, so display names with spaces (e.g. -// "My Group/My Gitlab") passed validation and produced a 404 at poll time (#8665). -// This is a "did you paste a display name?" sanity guard, not a strict GitLab -// path validator, so the char class stays permissive (e.g. accepts consecutive -// hyphens, which GitLab paths allow) rather than risk false-rejecting valid paths. -export const GITLAB_PROJECT_REGEX = /^(?:[1-9][0-9]*|[\w%./-]+)$/i; +// A GitLab project reference is EITHER a numeric project ID OR a namespace-qualified +// path (`group/project`, subgroups, or the `%2F`-encoded form) — the REST API has no +// way to resolve a project by a bare slug, so a single-segment name like `test_config` +// always 404s at poll time (#8665). Require a path separator (`/` or `%2F`) for the +// non-numeric branch so that mistake gets inline feedback instead. Still permissive +// about the segment chars (e.g. consecutive hyphens, which GitLab paths allow) to +// avoid false-rejecting valid paths; the separator lookahead keeps the char class a +// single unnested quantifier (no catastrophic backtracking). +export const GITLAB_PROJECT_REGEX = /^(?:[1-9][0-9]*|(?=.*(?:\/|%2F))[\w.%/-]+)$/i; export const GITLAB_CONFIG_FORM: LimitedFormlyFieldConfig[] = [ ...CROSS_ORIGIN_WARNING,