| 193 | * - Each component cannot begin with a dot |
| 194 | */ |
| 195 | export function isValidBranchName(name: string): { valid: boolean; reason?: string } { |
| 196 | if (!name || name.trim().length === 0) { |
| 197 | return { valid: false, reason: 'branch name cannot be empty' }; |
| 198 | } |
| 199 | |
| 200 | if (name === '@') { |
| 201 | return { valid: false, reason: 'branch name cannot be a single @' }; |
| 202 | } |
| 203 | |
| 204 | if (name.includes('..')) { |
| 205 | return { valid: false, reason: 'branch name cannot contain ".."' }; |
| 206 | } |
| 207 | |
| 208 | if (name.includes('@{')) { |
| 209 | return { valid: false, reason: 'branch name cannot contain "@{"' }; |
| 210 | } |
| 211 | |
| 212 | if (name.endsWith('.lock')) { |
| 213 | return { valid: false, reason: 'branch name cannot end with ".lock"' }; |
| 214 | } |
| 215 | |
| 216 | if (name.startsWith('.') || name.endsWith('.')) { |
| 217 | return { valid: false, reason: 'branch name cannot start or end with "."' }; |
| 218 | } |
| 219 | |
| 220 | if (name.startsWith('-')) { |
| 221 | return { valid: false, reason: 'branch name cannot start with "-"' }; |
| 222 | } |
| 223 | |
| 224 | // No ASCII control chars, space, ~, ^, :, ?, *, [, backslash |
| 225 | // eslint-disable-next-line no-control-regex |
| 226 | const invalidChars = /[\x00-\x1f\x7f ~^:?*[\]\\]/; |
| 227 | if (invalidChars.test(name)) { |
| 228 | return { valid: false, reason: 'branch name contains invalid characters' }; |
| 229 | } |
| 230 | |
| 231 | // Each slash-separated component cannot begin with a dot |
| 232 | const components = name.split('/'); |
| 233 | for (const comp of components) { |
| 234 | if (comp.startsWith('.')) { |
| 235 | return { valid: false, reason: `path component "${comp}" cannot start with "."` }; |
| 236 | } |
| 237 | if (comp.length === 0) { |
| 238 | return { valid: false, reason: 'branch name cannot contain empty path components (consecutive slashes)' }; |
| 239 | } |
| 240 | } |
| 241 | |
| 242 | return { valid: true }; |
| 243 | } |