···6161 ) {
6262 return 'A repo with this name already exists on your tangled account. Rename or remove the existing one, then resync.'
6363 }
6464- if (err.includes('invalid path sequence')) {
6565- return 'The repository name can\'t be used as a tangled repo name. This one can\'t be mirrored as-is.'
6464+ if (err.includes('invalid path sequence') || err.includes('can\'t be used as a tangled repo name')) {
6565+ return 'This repository name can\'t be used as a tangled repo name, so it can\'t be mirrored.'
6666 }
6767 if (err.includes('repository access blocked') || err.includes('repo no longer exists')) {
6868 return 'The mirror on tangled is no longer reachable. Try a resync; if it persists, let us know.'
+22-1
server/utils/tangled-repo.ts
···90909191export interface EnrolResult {
9292 status: 'enrolled' | 'already' | 'skipped'
9393- reason?: 'private' | 'fork' | 'no-identity' | 'name-conflict'
9393+ reason?: 'private' | 'fork' | 'no-identity' | 'name-conflict' | 'invalid-name'
9494}
95959696/**
···111111 if (status !== 400) return false
112112 const lc = body.toLowerCase()
113113 return lc.includes('accesscontrol') || lc.includes('sufficient access permissions')
114114+}
115115+116116+/**
117117+ * Stored `repo_mapping.lastError` when the GitHub repo name can't be a tangled
118118+ * repo name (e.g. `.github`, whose leading dot the knot rejects as an invalid
119119+ * path sequence). Terminal: the name won't become valid on retry.
120120+ */
121121+export const INVALID_NAME_ERROR
122122+ = 'this repository name can\'t be used as a tangled repo name, so it can\'t be mirrored'
123123+124124+/**
125125+ * True when the knot rejects `repo.create` because the name isn't a valid repo
126126+ * path (surfaced as a `Generic` 400 mentioning an invalid path sequence).
127127+ */
128128+function isInvalidNameResponse(status: number, body: string): boolean {
129129+ if (status !== 400) return false
130130+ return body.toLowerCase().includes('invalid path sequence')
114131}
115132116133/**
···244261 if (isNameConflictResponse(knotResponse.status, body)) {
245262 await recordEnrolError(db, opts, repo.full_name, NAME_CONFLICT_ERROR)
246263 return { status: 'skipped', reason: 'name-conflict' }
264264+ }
265265+ if (isInvalidNameResponse(knotResponse.status, body)) {
266266+ await recordEnrolError(db, opts, repo.full_name, INVALID_NAME_ERROR)
267267+ return { status: 'skipped', reason: 'invalid-name' }
247268 }
248269 throw new Error(`knot ${knot} returned ${knotResponse.status}: ${body}`)
249270 }