Skip to content

Commit ff6ff20

Browse files
WesleySmitsclaude
andcommitted
fix(server): onboarding reads a remote url on its section header line
Git accepts the first setting on a section header's own line, so `[remote "origin"] url = ...` is valid config. parseOriginUrlFromGitConfig only allowed a comment there, reset the section, and returned null or the next remote's url. Parse the header tail like isBareGitConfig does. Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent c79ef8c commit ff6ff20

2 files changed

Lines changed: 21 additions & 6 deletions

File tree

‎packages/shared/src/git.test.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,21 @@ describe("parseOriginUrlFromGitConfig", () => {
113113
).toBe("https://github.com/acme/repo.git");
114114
});
115115

116+
it("reads settings on the remote header line", () => {
117+
const upstream = '\n[remote "upstream"]\n\turl = https://github.com/other/source.git\n';
118+
expect(
119+
parseOriginUrlFromGitConfig(`[remote "origin"] url = [email protected]:a/b.git${upstream}`),
120+
).toBe("[email protected]:a/b.git");
121+
expect(parseOriginUrlFromGitConfig("[remote.Origin][email protected]:a/b.git\n")).toBe(
122+
"[email protected]:a/b.git",
123+
);
124+
expect(
125+
parseOriginUrlFromGitConfig(
126+
`[remote "origin"] fetch = +refs/heads/*:refs/remotes/origin/*\n\turl = [email protected]:a/b.git${upstream}`,
127+
),
128+
).toBe("[email protected]:a/b.git");
129+
});
130+
116131
it("falls back to the first remote when there is no origin", () => {
117132
const config = [
118133
'[remote "upstream"]',

‎packages/shared/src/git.ts‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -214,15 +214,15 @@ export function parseOriginUrlFromGitConfig(configText: string): string | null {
214214
let firstRemoteUrl: string | null = null;
215215
const joined = configText.replace(/\\\r?\n[ \t]*/g, "");
216216
for (const rawLine of joined.split(/\r?\n/)) {
217-
const line = rawLine.trim();
217+
let line = rawLine.trim();
218218
if (line.length === 0 || line.startsWith("#") || line.startsWith(";")) continue;
219-
// Git preserves case in quoted remote subsections but lowercases legacy dotted ones.
220-
const header = /^\[\s*remote(?:\s+"([^"]+)"|\.([^\]\s]+))\s*\](?:\s*[#;].*)?$/i.exec(line);
219+
// Git preserves case in quoted remote subsections but lowercases legacy dotted ones,
220+
// and accepts the first setting on the section header's own line.
221+
const header = /^\[\s*remote(?:\s+"([^"]+)"|\.([^\]\s]+))\s*\](.*)$/i.exec(line);
221222
if (header) {
222223
section = header[1] ?? header[2]?.toLowerCase() ?? null;
223-
continue;
224-
}
225-
if (line.startsWith("[")) {
224+
line = header[3]!.trim();
225+
} else if (line.startsWith("[")) {
226226
section = null;
227227
continue;
228228
}

0 commit comments

Comments
 (0)