| … | … | @@ -37,11 +37,23 @@ function scanRepos (dir) { |
| 37 | 37 | |
| 38 | 38 | /** |
| 39 | 39 | * Get info for a single bare repo. |
| 40 | * |
| 41 | * U40 fix (audit doc 2026-04-14-ux-reaudit.md): if HEAD points to a |
| 42 | * branch that doesn't exist (`git init --bare` leaves HEAD → |
| 43 | * refs/heads/master, but the operator's first push was `main` so |
| 44 | * master never gets created), `git show HEAD:path` and friends throw |
| 45 | * "fatal: invalid object name 'HEAD'". Detect that here and repoint |
| 46 | * HEAD to the first available branch — the SAME branch repoInfo |
| 47 | * picks for the .head field, so downstream readers stay consistent. |
| 40 | 48 | */ |
| 41 | 49 | function repoInfo (repoPath, name) { |
| 42 | 50 | const refs = readRefs(repoPath); |
| 43 | 51 | const branches = Object.keys(refs); |
| 44 | | const head = refs.master || refs.main || refs[branches[0]] || null; |
| 52 | const headBranch = refs.master ? 'master' |
| 53 | : refs.main ? 'main' |
| 54 | : (branches[0] || null); |
| 55 | const head = headBranch ? refs[headBranch] : null; |
| 56 | if (headBranch) repointHeadIfDangling(repoPath, refs, headBranch); |
| 45 | 57 | |
| 46 | 58 | let size = 0; |
| 47 | 59 | try { |
| … | … | @@ -78,6 +90,37 @@ function repoInfo (repoPath, name) { |
| 78 | 90 | }; |
| 79 | 91 | } |
| 80 | 92 | |
| 93 | /** |
| 94 | * If the bare repo's HEAD is a symbolic ref pointing at a branch that |
| 95 | * doesn't exist (the dangling-HEAD case described above), rewrite |
| 96 | * HEAD to point at `targetBranch` instead. No-op when HEAD already |
| 97 | * resolves cleanly. |
| 98 | * |
| 99 | * Direct file write — no `git symbolic-ref` subprocess. The HEAD |
| 100 | * file format is: `ref: refs/heads/<branch>\n` for symbolic refs, |
| 101 | * or a plain SHA for detached HEAD. |
| 102 | */ |
| 103 | function repointHeadIfDangling (repoPath, refs, targetBranch) { |
| 104 | if (!targetBranch || refs[targetBranch] == null) return false; |
| 105 | const headPath = path.join(repoPath, 'HEAD'); |
| 106 | let raw; |
| 107 | try { raw = fs.readFileSync(headPath, 'utf-8').trim(); } catch { return false; } |
| 108 | // Detached HEAD (just a SHA) — leave alone. |
| 109 | if (!raw.startsWith('ref:')) return false; |
| 110 | const currentTarget = raw.slice('ref:'.length).trim(); |
| 111 | if (!currentTarget.startsWith('refs/heads/')) return false; |
| 112 | const currentBranch = currentTarget.slice('refs/heads/'.length); |
| 113 | // HEAD already points at a branch that exists — nothing to do. |
| 114 | if (refs[currentBranch] != null) return false; |
| 115 | if (currentBranch === targetBranch) return false; |
| 116 | try { |
| 117 | fs.writeFileSync(headPath, `ref: refs/heads/${targetBranch}\n`); |
| 118 | return true; |
| 119 | } catch { |
| 120 | return false; |
| 121 | } |
| 122 | } |
| 123 | |
| 81 | 124 | /** |
| 82 | 125 | * Read refs from a bare repo — direct file read, no subprocess. |
| 83 | 126 | * Returns { refShortName: sha } |