Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions packages/squad-sdk/src/platform/detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,17 @@ export interface AzureDevOpsRemoteInfo {
* git@github.com:owner/repo.git
*/
export function parseGitHubRemote(url: string): GitHubRemoteInfo | null {
// Repo capture allows dots (GitHub permits them, e.g. `foo/bar.baz`); the
// optional `\.git$` plus the non-greedy quantifier still strips a trailing
// `.git` correctly.
// HTTPS: https://github.com/owner/repo.git
const httpsMatch = url.match(/github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?$/i);
const httpsMatch = url.match(/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/i);
if (httpsMatch) {
return { owner: httpsMatch[1]!, repo: httpsMatch[2]! };
}

// SSH: git@github.com:owner/repo.git
const sshMatch = url.match(/github\.com:([^/]+)\/([^/.]+?)(?:\.git)?$/i);
const sshMatch = url.match(/github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/i);
if (sshMatch) {
return { owner: sshMatch[1]!, repo: sshMatch[2]! };
}
Expand All @@ -51,26 +54,30 @@ export function parseGitHubRemote(url: string): GitHubRemoteInfo | null {
* https://org.visualstudio.com/project/_git/repo
*/
export function parseAzureDevOpsRemote(url: string): AzureDevOpsRemoteInfo | null {
// Repo capture allows dots in repo names; the literal `/_git/` segment keeps
// the match anchored unambiguously.
// HTTPS dev.azure.com: https://dev.azure.com/org/project/_git/repo
// Also handles: https://org@dev.azure.com/org/project/_git/repo
const devAzureHttps = url.match(
/dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/([^/.]+?)(?:\.git)?$/i,
/dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/([^/]+?)(?:\.git)?$/i,
);
if (devAzureHttps) {
return { org: devAzureHttps[1]!, project: devAzureHttps[2]!, repo: devAzureHttps[3]! };
}

// SSH dev.azure.com: git@ssh.dev.azure.com:v3/org/project/repo
const devAzureSsh = url.match(
/ssh\.dev\.azure\.com:v3\/([^/]+)\/([^/]+)\/([^/.]+?)(?:\.git)?$/i,
/ssh\.dev\.azure\.com:v3\/([^/]+)\/([^/]+)\/([^/]+?)(?:\.git)?$/i,
);
if (devAzureSsh) {
return { org: devAzureSsh[1]!, project: devAzureSsh[2]!, repo: devAzureSsh[3]! };
}

// Legacy visualstudio.com: https://org.visualstudio.com/project/_git/repo
// (Org subdomain keeps the no-dot constraint — the `.visualstudio.com` anchor
// requires it. Only the repo capture is widened.)
const vsMatch = url.match(
/([^/.]+)\.visualstudio\.com\/([^/]+)\/_git\/([^/.]+?)(?:\.git)?$/i,
/([^/.]+)\.visualstudio\.com\/([^/]+)\/_git\/([^/]+?)(?:\.git)?$/i,
);
if (vsMatch) {
return { org: vsMatch[1]!, project: vsMatch[2]!, repo: vsMatch[3]! };
Expand Down
51 changes: 51 additions & 0 deletions test/platform-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,36 @@ describe('parseGitHubRemote', () => {
// trailing slash is not standard git remote but shouldn't crash
expect(parseGitHubRemote('https://github.com/owner/')).toBeNull();
});

// Regression: the repo capture used to disallow `.`, so any GitHub repo whose
// name contained a dot (which GitHub permits) failed to parse and bubbled up
// as `Could not detect platform: Could not parse GitHub remote URL: ...` in
// `squad watch`. See issue #1077.
it('parses HTTPS URL with dots in repo name', () => {
const result = parseGitHubRemote('https://github.com/myorg/JADE.xlighthousepipelines.git');
expect(result).toEqual({ owner: 'myorg', repo: 'JADE.xlighthousepipelines' });
});

it('parses HTTPS URL with dots in repo name and no .git suffix', () => {
const result = parseGitHubRemote('https://github.com/myorg/JADE.xlighthousepipelines');
expect(result).toEqual({ owner: 'myorg', repo: 'JADE.xlighthousepipelines' });
});

it('parses SSH URL with dots in repo name', () => {
const result = parseGitHubRemote('git@github.com:myorg/foo.bar.baz.git');
expect(result).toEqual({ owner: 'myorg', repo: 'foo.bar.baz' });
});

it('parses SSH URL with dots in repo name and no .git suffix', () => {
const result = parseGitHubRemote('git@github.com:myorg/foo.bar.baz');
expect(result).toEqual({ owner: 'myorg', repo: 'foo.bar.baz' });
});

it('parses repo named exactly ".github" (with leading dot before .git)', () => {
// owner/.github is a real convention for community-health files
const result = parseGitHubRemote('https://github.com/myorg/.github.git');
expect(result).toEqual({ owner: 'myorg', repo: '.github' });
});
});

// ─── Azure DevOps Remote Parsing ───────────────────────────────────────
Expand Down Expand Up @@ -152,6 +182,27 @@ describe('parseAzureDevOpsRemote', () => {
const result = parseAzureDevOpsRemote('https://dev.azure.com/org/My-Project/_git/my-repo');
expect(result).toEqual({ org: 'org', project: 'My-Project', repo: 'my-repo' });
});

// Regression: see issue #1077 — repo capture used to disallow `.`.
it('parses HTTPS dev.azure.com URL with dots in repo name', () => {
const result = parseAzureDevOpsRemote('https://dev.azure.com/org/proj/_git/repo.with.dots');
expect(result).toEqual({ org: 'org', project: 'proj', repo: 'repo.with.dots' });
});

it('parses HTTPS dev.azure.com URL with dots in repo name and .git suffix', () => {
const result = parseAzureDevOpsRemote('https://dev.azure.com/org/proj/_git/repo.with.dots.git');
expect(result).toEqual({ org: 'org', project: 'proj', repo: 'repo.with.dots' });
});

it('parses SSH dev.azure.com URL with dots in repo name', () => {
const result = parseAzureDevOpsRemote('git@ssh.dev.azure.com:v3/org/proj/repo.with.dots.git');
expect(result).toEqual({ org: 'org', project: 'proj', repo: 'repo.with.dots' });
});

it('parses legacy visualstudio.com URL with dots in repo name', () => {
const result = parseAzureDevOpsRemote('https://contoso.visualstudio.com/proj/_git/repo.with.dots.git');
expect(result).toEqual({ org: 'contoso', project: 'proj', repo: 'repo.with.dots' });
});
});

// ─── WorkItem Type Shape ───────────────────────────────────────────────
Expand Down
Loading