@@ -27,10 +27,33 @@ export interface GitWorktreeResult {
2727export interface GitOptions {
2828 /** Working directory for git commands (default: process.cwd()) */
2929 cwd ?: string ;
30- /** Timeout in milliseconds (default: 30000) */
30+ /** Timeout in milliseconds (default: 30000 for regular operations ) */
3131 timeout ?: number ;
3232}
3333
34+ /**
35+ * Default timeout for regular git operations (30 seconds)
36+ */
37+ const DEFAULT_GIT_TIMEOUT = 30000 ;
38+
39+ /**
40+ * Timeout for git ref resolution operations (15 seconds)
41+ * git rev-parse is lightweight and should complete quickly
42+ */
43+ export const GIT_REF_RESOLVE_TIMEOUT = 15000 ;
44+
45+ /**
46+ * Timeout for git ref description operations (15 seconds)
47+ * git rev-parse --abbrev-ref and --short are lightweight and should complete quickly
48+ */
49+ export const GIT_REF_DESCRIBE_TIMEOUT = 15000 ;
50+
51+ /**
52+ * Timeout for git worktree creation (60 seconds)
53+ * Worktree creation can be slow on large repos due to file checkout operations
54+ */
55+ export const GIT_WORKTREE_TIMEOUT = 60000 ;
56+
3457/**
3558 * Execute a git command and return stdout
3659 * Uses spawn instead of exec for better security (no shell interpretation)
@@ -41,7 +64,7 @@ async function execGit(
4164 options : GitOptions = { }
4265) : Promise < string > {
4366 const cwd = options . cwd ?? process . cwd ( ) ;
44- const timeout = options . timeout ?? 30000 ;
67+ const timeout = options . timeout ?? DEFAULT_GIT_TIMEOUT ;
4568 const command = `git ${ args . join ( ' ' ) } ` ;
4669
4770 debugLog ( 'git' , `Executing: ${ command } ` , { cwd } ) ;
@@ -132,23 +155,52 @@ export async function supportsWorktrees(options: GitOptions = {}): Promise<boole
132155 }
133156}
134157
158+ /**
159+ * Maximum length for git refs (256 characters)
160+ * This is a defense-in-depth measure - Git itself will validate refs
161+ */
162+ const MAX_REF_LENGTH = 256 ;
163+
164+ /**
165+ * Validate git ref input (lightweight validation)
166+ * Trims whitespace and checks length, but lets Git be the source of truth for semantic validity
167+ *
168+ * @param ref - Git ref to validate
169+ * @throws Error if ref is empty or exceeds length limit
170+ */
171+ function validateGitRef ( ref : string ) : void {
172+ const trimmed = ref . trim ( ) ;
173+ if ( trimmed . length === 0 ) {
174+ throw new Error ( 'Invalid baseline ref: ref is empty or exceeds 256 characters' ) ;
175+ }
176+ if ( trimmed . length > MAX_REF_LENGTH ) {
177+ throw new Error ( 'Invalid baseline ref: ref is empty or exceeds 256 characters' ) ;
178+ }
179+ }
180+
135181/**
136182 * Resolve a git ref to its commit hash
137183 * Validates that the ref exists
138184 *
139185 * @param ref - Git ref (branch, tag, commit, HEAD, HEAD~1, etc.)
140186 * @returns The resolved commit hash
141- * @throws Error if ref doesn't exist
187+ * @throws Error if ref doesn't exist or is invalid
142188 */
143189export async function resolveGitRef (
144190 ref : string ,
145191 options : GitOptions = { }
146192) : Promise < string > {
193+ // Lightweight validation: trim whitespace and check length
194+ // Let Git be the source of truth for semantic validity
195+ validateGitRef ( ref ) ;
196+
197+ const trimmedRef = ref . trim ( ) ;
198+
147199 try {
148- const hash = await execGit ( [ 'rev-parse' , '--verify' , ref ] , options ) ;
200+ const hash = await execGit ( [ 'rev-parse' , '--verify' , trimmedRef ] , options ) ;
149201 return hash ;
150202 } catch ( error ) {
151- throw new Error ( `Invalid git ref "${ ref } ": ref does not exist` ) ;
203+ throw new Error ( `Invalid git ref "${ trimmedRef } ": ref does not exist` ) ;
152204 }
153205}
154206
@@ -160,10 +212,12 @@ export async function describeGitRef(
160212 ref : string ,
161213 options : GitOptions = { }
162214) : Promise < string > {
215+ // Use trimmed ref for consistency with resolveGitRef
216+ const trimmedRef = ref . trim ( ) ;
163217 try {
164218 // Try to get a symbolic name first
165219 const symbolic = await execGit (
166- [ 'rev-parse' , '--abbrev-ref' , ref ] ,
220+ [ 'rev-parse' , '--abbrev-ref' , trimmedRef ] ,
167221 options
168222 ) ;
169223 if ( symbolic && symbolic !== 'HEAD' ) {
@@ -175,10 +229,10 @@ export async function describeGitRef(
175229
176230 try {
177231 // Fall back to short commit hash
178- const shortHash = await execGit ( [ 'rev-parse' , '--short' , ref ] , options ) ;
232+ const shortHash = await execGit ( [ 'rev-parse' , '--short' , trimmedRef ] , options ) ;
179233 return shortHash ;
180234 } catch {
181- return ref ; // Return original if all else fails
235+ return trimmedRef ; // Return trimmed ref if all else fails
182236 }
183237}
184238
@@ -195,6 +249,9 @@ export async function createWorktree(
195249 targetDir ?: string ,
196250 options : GitOptions = { }
197251) : Promise < GitWorktreeResult > {
252+ // Use trimmed ref for consistency
253+ const trimmedRef = ref . trim ( ) ;
254+
198255 // Validate we're in a git repo
199256 if ( ! ( await isGitRepo ( options ) ) ) {
200257 throw new Error ( 'Not a git repository' ) ;
@@ -205,16 +262,16 @@ export async function createWorktree(
205262 throw new Error ( 'Git worktrees not supported (requires git >= 2.5)' ) ;
206263 }
207264
208- // Resolve the ref to a commit hash
209- const commitHash = await resolveGitRef ( ref , options ) ;
265+ // Resolve the ref to a commit hash (this will also validate the ref)
266+ const commitHash = await resolveGitRef ( trimmedRef , options ) ;
210267
211268 // Generate worktree path if not provided
212269 const worktreePath = targetDir ?? join (
213270 tmpdir ( ) ,
214271 `logicstamp-worktree-${ Date . now ( ) } -${ commitHash . substring ( 0 , 8 ) } `
215272 ) ;
216273
217- debugLog ( 'git' , `Creating worktree at ${ worktreePath } for ref ${ ref } ` , {
274+ debugLog ( 'git' , `Creating worktree at ${ worktreePath } for ref ${ trimmedRef } ` , {
218275 commitHash,
219276 } ) ;
220277
@@ -236,7 +293,7 @@ export async function createWorktree(
236293 return {
237294 worktreePath,
238295 commitHash,
239- ref,
296+ ref : trimmedRef ,
240297 } ;
241298 } catch ( error ) {
242299 // Clean up on failure
@@ -248,12 +305,12 @@ export async function createWorktree(
248305
249306 const err = error as Error ;
250307 debugError ( 'git' , 'createWorktree' , {
251- ref,
308+ ref : trimmedRef ,
252309 targetDir : worktreePath ,
253310 message : err . message ,
254311 } ) ;
255312
256- throw new Error ( `Failed to create worktree for "${ ref } ": ${ err . message } ` ) ;
313+ throw new Error ( `Failed to create worktree for "${ trimmedRef } ": ${ err . message } ` ) ;
257314 }
258315}
259316
0 commit comments