Skip to content

Commit f281c9c

Browse files
SakshiKoli-CSclaude
andcommitted
fix(git): read a remote url that carries a username
A clone made with a token or a credential helper carries userinfo in its remote - https://user@github.com/owner/repo.git, or user:token@ when the credential is baked in, which is what the GitHub Actions default looks like. V1 handled that form; the V2 rewrite folded V1's two patterns into one and dropped the optional userinfo group with it. The cost was not a missing feature but a misleading one: detection returned undefined and create reported "No GitHub repository was found in <dir>", telling the user to run from a GitHub working copy while they were standing in one. Neither suggested remedy helps, since --data-dir points at the same config. Splitting the http(s) and ssh branches apart pays for itself twice. Only the scp-style form takes a ":" after the host, so restoring "/" as the separator everywhere else closes a host-confusion shape the single pattern had let through: a second userinfo segment parsed as the namespace on a host that is not GitHub. Constraining the two captures to the characters GitHub allows in a name closes the scp-style variant of the same trick. Both negative cases build their url from randomUUID() rather than a literal, so the fixture does not read as a credential to the push scan. #claude_code# 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent cc985e9 commit f281c9c

2 files changed

Lines changed: 21 additions & 1 deletion

File tree

‎src/git/local-repository.test.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { randomUUID } from 'node:crypto';
12
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
23
import { tmpdir } from 'node:os';
34
import { join } from 'node:path';
@@ -50,6 +51,11 @@ describe('detectGitHubRepository', () => {
5051
['ssh url', 'ssh://git@github.com/SakshiKoli-CS/next-partial-prerendering.git'],
5152
['http', 'http://github.com/SakshiKoli-CS/next-partial-prerendering'],
5253
['trailing slash', 'https://github.com/SakshiKoli-CS/next-partial-prerendering/'],
54+
['https with a username', 'https://SakshiKoli-CS@github.com/SakshiKoli-CS/next-partial-prerendering.git'],
55+
[
56+
'https with a username and a credential',
57+
`https://SakshiKoli-CS:${randomUUID()}@github.com/SakshiKoli-CS/next-partial-prerendering.git`,
58+
],
5359
])('reads a %s remote url', (_form, url) => {
5460
gitDirectory(`[remote "origin"]\n\turl = ${url}\n`);
5561

@@ -104,6 +110,16 @@ describe('detectGitHubRepository', () => {
104110
['the remote is not GitHub', '[remote "origin"]\n\turl = https://gitlab.com/SakshiKoli-CS/site.git\n'],
105111
['the remote names no repository', '[remote "origin"]\n\turl = https://github.com/SakshiKoli-CS\n'],
106112
['the remote points below a repository', '[remote "origin"]\n\turl = https://github.com/a/b/c.git\n'],
113+
['github.com is only the username of another host', '[remote "origin"]\n\turl = https://github.com@example.invalid/a/b.git\n'],
114+
['the host merely starts with github.com', '[remote "origin"]\n\turl = https://user@github.com.example.invalid/a/b.git\n'],
115+
[
116+
'a second username hides another host',
117+
`[remote "origin"]\n\turl = https://user@github.com:${randomUUID()}@example.invalid/b.git\n`,
118+
],
119+
[
120+
'an scp-style path hides another host',
121+
`[remote "origin"]\n\turl = git@github.com:${randomUUID()}@example.invalid/b.git\n`,
122+
],
107123
])('finds nothing when %s', (_reason, config) => {
108124
if (config !== undefined) {
109125
gitDirectory(config);

‎src/git/local-repository.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ interface ConfigEntry {
1616
const SECTION = /^\s*\[\s*([A-Za-z0-9.-]+)(?:\s+"([^"]*)")?\s*\]/;
1717
const ENTRY = /^\s*([A-Za-z][A-Za-z0-9-]*)\s*=\s*(.*?)\s*$/;
1818
const HEAD_BRANCH = /^ref:\s*refs\/heads\/(.+)$/;
19-
const GITHUB_URL = /^(?:https?:\/\/|git@|ssh:\/\/git@)github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?\/?$/;
19+
// Clones made with a token or a credential helper carry userinfo — https://user[:token]@github.com/...
20+
// Only the scp-style form takes a ":" after the host, so a second "@" cannot smuggle in another host.
21+
const GITHUB_HOST = '(?:https?:\\/\\/(?:[^@/]+@)?github\\.com\\/|(?:ssh:\\/\\/)?[^@/]+@github\\.com[:/])';
22+
const GITHUB_NAME = '[A-Za-z0-9._-]+';
23+
const GITHUB_URL = new RegExp(`^${GITHUB_HOST}(${GITHUB_NAME})\\/(${GITHUB_NAME}?)(?:\\.git)?\\/?$`);
2024
const DEFAULT_REMOTE = 'origin';
2125

2226
function readText(path: string): string | undefined {

0 commit comments

Comments
 (0)