@@ -14,6 +14,9 @@ export const Parameters = Schema.Struct({
1414 refresh : Schema . optional ( Schema . Boolean ) . annotate ( {
1515 description : "When true, fetches the latest remote state into the managed cache" ,
1616 } ) ,
17+ branch : Schema . optional ( Schema . String ) . annotate ( {
18+ description : "Branch or ref to clone and inspect" ,
19+ } ) ,
1720} )
1821
1922type Metadata = {
@@ -26,16 +29,19 @@ type Metadata = {
2629 branch ?: string
2730}
2831
29- function statusForRepository ( input : { reuse : boolean ; refresh ?: boolean } ) {
32+ function statusForRepository ( input : { reuse : boolean ; refresh ?: boolean ; branchMatches ?: boolean } ) {
3033 if ( ! input . reuse ) return "cloned" as const
34+ if ( input . branchMatches === false ) return "refreshed" as const
3135 if ( input . refresh ) return "refreshed" as const
3236 return "cached" as const
3337}
3438
3539function resetTarget ( input : {
40+ requestedBranch ?: string
3641 remoteHead : { code : number ; stdout : string }
3742 branch : { code : number ; stdout : string }
3843} ) {
44+ if ( input . requestedBranch ) return `origin/${ input . requestedBranch } `
3945 if ( input . remoteHead . code === 0 && input . remoteHead . stdout ) {
4046 return input . remoteHead . stdout . replace ( / ^ r e f s \/ r e m o t e s \/ / , "" )
4147 }
@@ -45,6 +51,14 @@ function resetTarget(input: {
4551 return "HEAD"
4652}
4753
54+ function validateBranch ( branch : string ) {
55+ if ( ! / ^ [ A - Z a - z 0 - 9 / _ . - ] + $ / . test ( branch ) || branch . startsWith ( "-" ) || branch . includes ( ".." ) ) {
56+ throw new Error (
57+ "Branch must contain only alphanumeric characters, /, _, ., and -, and cannot start with - or contain .." ,
58+ )
59+ }
60+ }
61+
4862export const RepoCloneTool = Tool . define < typeof Parameters , Metadata , AppFileSystem . Service | Git . Service > (
4963 "repo_clone" ,
5064 Effect . gen ( function * ( ) {
@@ -57,7 +71,9 @@ export const RepoCloneTool = Tool.define<typeof Parameters, Metadata, AppFileSys
5771 execute : ( params : Schema . Schema . Type < typeof Parameters > , ctx : Tool . Context < Metadata > ) =>
5872 Effect . gen ( function * ( ) {
5973 const reference = parseRepositoryReference ( params . repository )
60- if ( ! reference ) throw new Error ( "Repository must be a git URL, host/path reference, or GitHub owner/repo shorthand" )
74+ if ( ! reference )
75+ throw new Error ( "Repository must be a git URL, host/path reference, or GitHub owner/repo shorthand" )
76+ if ( params . branch ) validateBranch ( params . branch )
6177
6278 const repository = reference . label
6379 const remote = reference . remote
@@ -73,6 +89,7 @@ export const RepoCloneTool = Tool.define<typeof Parameters, Metadata, AppFileSys
7389 remote,
7490 path : localPath ,
7591 refresh : Boolean ( params . refresh ) ,
92+ branch : params . branch ,
7693 } ,
7794 } )
7895
@@ -87,37 +104,74 @@ export const RepoCloneTool = Tool.define<typeof Parameters, Metadata, AppFileSys
87104 const origin = hasGitDir
88105 ? yield * git . run ( [ "config" , "--get" , "remote.origin.url" ] , { cwd : localPath } )
89106 : undefined
90- const originReference = origin ?. exitCode === 0 ? parseRepositoryReference ( origin . text ( ) . trim ( ) ) : undefined
91- const reuse = hasGitDir && Boolean ( originReference && sameRepositoryReference ( originReference , cloneTarget ) )
107+ const originReference =
108+ origin ?. exitCode === 0 ? parseRepositoryReference ( origin . text ( ) . trim ( ) ) : undefined
109+ const reuse =
110+ hasGitDir && Boolean ( originReference && sameRepositoryReference ( originReference , cloneTarget ) )
92111 if ( exists && ! reuse ) {
93112 yield * fs . remove ( localPath , { recursive : true } ) . pipe ( Effect . orDie )
94113 }
95114
96- const status = statusForRepository ( { reuse, refresh : params . refresh } )
115+ const currentBranch = hasGitDir ? yield * git . branch ( localPath ) : undefined
116+ const status = statusForRepository ( {
117+ reuse,
118+ refresh : params . refresh ,
119+ branchMatches : params . branch ? currentBranch === params . branch : undefined ,
120+ } )
97121
98122 if ( status === "cloned" ) {
99- const clone = yield * git . run ( [ "clone" , "--depth" , "100" , remote , localPath ] , { cwd : path . dirname ( localPath ) } )
123+ const clone = yield * git . run (
124+ [
125+ "clone" ,
126+ "--depth" ,
127+ "100" ,
128+ ...( params . branch ? [ "--branch" , params . branch ] : [ ] ) ,
129+ remote ,
130+ localPath ,
131+ ] ,
132+ { cwd : path . dirname ( localPath ) } ,
133+ )
100134 if ( clone . exitCode !== 0 ) {
101- throw new Error ( clone . stderr . toString ( ) . trim ( ) || clone . text ( ) . trim ( ) || `Failed to clone ${ repository } ` )
135+ throw new Error (
136+ clone . stderr . toString ( ) . trim ( ) || clone . text ( ) . trim ( ) || `Failed to clone ${ repository } ` ,
137+ )
102138 }
103139 }
104140
105141 if ( status === "refreshed" ) {
106142 const fetch = yield * git . run ( [ "fetch" , "--all" , "--prune" ] , { cwd : localPath } )
107143 if ( fetch . exitCode !== 0 ) {
108- throw new Error ( fetch . stderr . toString ( ) . trim ( ) || fetch . text ( ) . trim ( ) || `Failed to refresh ${ repository } ` )
144+ throw new Error (
145+ fetch . stderr . toString ( ) . trim ( ) || fetch . text ( ) . trim ( ) || `Failed to refresh ${ repository } ` ,
146+ )
147+ }
148+
149+ if ( params . branch ) {
150+ const checkout = yield * git . run ( [ "checkout" , "-B" , params . branch , `origin/${ params . branch } ` ] , {
151+ cwd : localPath ,
152+ } )
153+ if ( checkout . exitCode !== 0 ) {
154+ throw new Error (
155+ checkout . stderr . toString ( ) . trim ( ) ||
156+ checkout . text ( ) . trim ( ) ||
157+ `Failed to checkout ${ params . branch } ` ,
158+ )
159+ }
109160 }
110161
111162 const remoteHead = yield * git . run ( [ "symbolic-ref" , "refs/remotes/origin/HEAD" ] , { cwd : localPath } )
112163 const branch = yield * git . run ( [ "symbolic-ref" , "--quiet" , "--short" , "HEAD" ] , { cwd : localPath } )
113164 const target = resetTarget ( {
165+ requestedBranch : params . branch ,
114166 remoteHead : { code : remoteHead . exitCode , stdout : remoteHead . text ( ) . trim ( ) } ,
115167 branch : { code : branch . exitCode , stdout : branch . text ( ) . trim ( ) } ,
116168 } )
117169
118170 const reset = yield * git . run ( [ "reset" , "--hard" , target ] , { cwd : localPath } )
119171 if ( reset . exitCode !== 0 ) {
120- throw new Error ( reset . stderr . toString ( ) . trim ( ) || reset . text ( ) . trim ( ) || `Failed to reset ${ repository } ` )
172+ throw new Error (
173+ reset . stderr . toString ( ) . trim ( ) || reset . text ( ) . trim ( ) || `Failed to reset ${ repository } ` ,
174+ )
121175 }
122176 }
123177
0 commit comments