1
1
const DEFAULT_MSG =
2
- "Enter a GitHub repo and branch to review. Runs Python entirely in your browser using WebAssembly. Built with React, MaterialUI, and Pyodide." ;
2
+ "Enter a GitHub repo and branch/tag to review. Runs Python entirely in your browser using WebAssembly. Built with React, MaterialUI, and Pyodide." ;
3
3
4
4
const urlParams = new URLSearchParams ( window . location . search ) ;
5
5
const baseurl = window . location . pathname ;
@@ -154,6 +154,39 @@ function Results(props) {
154
154
) ;
155
155
}
156
156
157
+ async function fetchRepoRefs ( repo ) {
158
+ if ( ! repo ) return { branches : [ ] , tags : [ ] } ;
159
+ try {
160
+ // Fetch both branches and tags from GitHub API
161
+ const [ branchesResponse , tagsResponse ] = await Promise . all ( [
162
+ fetch ( `https://api.github.com/repos/${ repo } /branches` ) ,
163
+ fetch ( `https://api.github.com/repos/${ repo } /tags` ) ,
164
+ ] ) ;
165
+
166
+ if ( ! branchesResponse . ok || ! tagsResponse . ok ) {
167
+ console . error ( "Error fetching repo data" ) ;
168
+ return { branches : [ ] , tags : [ ] } ;
169
+ }
170
+
171
+ const branches = await branchesResponse . json ( ) ;
172
+ const tags = await tagsResponse . json ( ) ;
173
+
174
+ return {
175
+ branches : branches . map ( ( branch ) => ( {
176
+ name : branch . name ,
177
+ type : "branch" ,
178
+ } ) ) ,
179
+ tags : tags . map ( ( tag ) => ( {
180
+ name : tag . name ,
181
+ type : "tag" ,
182
+ } ) ) ,
183
+ } ;
184
+ } catch ( error ) {
185
+ console . error ( "Error fetching repo references:" , error ) ;
186
+ return { branches : [ ] , tags : [ ] } ;
187
+ }
188
+ }
189
+
157
190
async function prepare_pyodide ( deps ) {
158
191
const deps_str = deps . map ( ( i ) => `"${ i } "` ) . join ( ", " ) ;
159
192
const pyodide = await loadPyodide ( ) ;
@@ -196,28 +229,58 @@ class App extends React.Component {
196
229
this . state = {
197
230
results : [ ] ,
198
231
repo : urlParams . get ( "repo" ) || "" ,
199
- branch : urlParams . get ( "branch" ) || "" ,
232
+ ref : urlParams . get ( "ref" ) || "" ,
233
+ refType : urlParams . get ( "refType" ) || "branch" ,
234
+ refs : { branches : [ ] , tags : [ ] } ,
200
235
msg : `<p>${ DEFAULT_MSG } </p><h4>Packages:</h4> ${ deps_str } ` ,
201
236
progress : false ,
237
+ loadingRefs : false ,
202
238
err_msg : "" ,
203
239
skip_reason : "" ,
204
240
url : "" ,
205
241
} ;
206
242
this . pyodide_promise = prepare_pyodide ( props . deps ) ;
243
+ this . refInputDebounce = null ;
244
+ }
245
+
246
+ async fetchRepoReferences ( repo ) {
247
+ if ( ! repo ) return ;
248
+
249
+ this . setState ( { loadingRefs : true } ) ;
250
+ const refs = await fetchRepoRefs ( repo ) ;
251
+ this . setState ( {
252
+ refs : refs ,
253
+ loadingRefs : false ,
254
+ } ) ;
255
+ }
256
+
257
+ handleRepoChange ( repo ) {
258
+ this . setState ( { repo } ) ;
259
+
260
+ // debounce the API call to avoid too many requests
261
+ clearTimeout ( this . refInputDebounce ) ;
262
+ this . refInputDebounce = setTimeout ( ( ) => {
263
+ this . fetchRepoReferences ( repo ) ;
264
+ } , 500 ) ;
265
+ }
266
+
267
+ handleRefChange ( ref , refType ) {
268
+ this . setState ( { ref, refType } ) ;
207
269
}
208
270
209
271
handleCompute ( ) {
210
- if ( ! this . state . repo || ! this . state . branch ) {
272
+ if ( ! this . state . repo || ! this . state . ref ) {
211
273
this . setState ( { results : [ ] , msg : DEFAULT_MSG } ) ;
212
274
window . history . replaceState ( null , "" , baseurl ) ;
213
275
alert (
214
- `Please enter a repo (${ this . state . repo } ) and branch (${ this . state . branch } )` ,
276
+ `Please enter a repo (${ this . state . repo } ) and branch/tag (${ this . state . ref } )` ,
215
277
) ;
216
278
return ;
217
279
}
218
280
const local_params = new URLSearchParams ( {
219
281
repo : this . state . repo ,
220
- branch : this . state . branch ,
282
+ ref : this . state . ref ,
283
+ refType : this . state . refType ,
221
284
} ) ;
222
285
window . history . replaceState ( null , "" , `${ baseurl } ?${ local_params } ` ) ;
223
286
this . setState ( {
@@ -234,7 +297,7 @@ class App extends React.Component {
234
297
from repo_review.ghpath import GHPath
235
298
from dataclasses import replace
236
299
237
- package = GHPath(repo="${ state . repo } ", branch="${ state . branch } ")
300
+ package = GHPath(repo="${ state . repo } ", branch="${ state . ref } ")
238
301
families, checks = process(package)
239
302
240
303
for v in families.values():
@@ -249,7 +312,7 @@ class App extends React.Component {
249
312
this . setState ( {
250
313
msg : DEFAULT_MSG ,
251
314
progress : false ,
252
- err_msg : "Invalid repository or branch. Please try again." ,
315
+ err_msg : "Invalid repository or branch/tag . Please try again." ,
253
316
} ) ;
254
317
return ;
255
318
}
@@ -288,7 +351,7 @@ class App extends React.Component {
288
351
this . setState ( {
289
352
results : results ,
290
353
families : families ,
291
- msg : `Results for ${ state . repo } @${ state . branch } ` ,
354
+ msg : `Results for ${ state . repo } @${ state . ref } ( ${ state . refType } ) ` ,
292
355
progress : false ,
293
356
err_msg : "" ,
294
357
url : "" ,
@@ -300,13 +363,78 @@ class App extends React.Component {
300
363
}
301
364
302
365
componentDidMount ( ) {
303
- if ( urlParams . get ( "repo" ) && urlParams . get ( "branch" ) ) {
304
- this . handleCompute ( ) ;
366
+ if ( urlParams . get ( "repo" ) ) {
367
+ this . fetchRepoReferences ( urlParams . get ( "repo" ) ) ;
368
+
369
+ if ( urlParams . get ( "ref" ) ) {
370
+ this . handleCompute ( ) ;
371
+ }
305
372
}
306
373
}
307
374
308
375
render ( ) {
309
- const common_branches = [ "main" , "master" , "develop" , "stable" ] ;
376
+ const priorityBranches = [ "HEAD" , "main" , "master" , "develop" , "stable" ] ;
377
+ const branchMap = new Map (
378
+ this . state . refs . branches . map ( ( branch ) => [ branch . name , branch ] ) ,
379
+ ) ;
380
+
381
+ let availableOptions = [ ] ;
382
+
383
+ // If no repo is entered or API hasn't returned any branches/tags yet,
384
+ // show all five priority branches.
385
+ if (
386
+ this . state . repo === "" ||
387
+ ( this . state . refs . branches . length === 0 &&
388
+ this . state . refs . tags . length === 0 )
389
+ ) {
390
+ availableOptions = [
391
+ { label : "HEAD (default branch)" , value : "HEAD" , type : "branch" } ,
392
+ { label : "main (branch)" , value : "main" , type : "branch" } ,
393
+ { label : "master (branch)" , value : "master" , type : "branch" } ,
394
+ { label : "develop (branch)" , value : "develop" , type : "branch" } ,
395
+ { label : "stable (branch)" , value : "stable" , type : "branch" } ,
396
+ ] ;
397
+ } else {
398
+ const prioritizedBranches = [
399
+ { label : "HEAD (default branch)" , value : "HEAD" , type : "branch" } ,
400
+ ] ;
401
+
402
+ priorityBranches . slice ( 1 ) . forEach ( ( branchName ) => {
403
+ if ( branchMap . has ( branchName ) ) {
404
+ prioritizedBranches . push ( {
405
+ label : `${ branchName } (branch)` ,
406
+ value : branchName ,
407
+ type : "branch" ,
408
+ } ) ;
409
+ // Remove from map so it doesn't get added twice.
410
+ branchMap . delete ( branchName ) ;
411
+ }
412
+ } ) ;
413
+
414
+ const otherBranches = [ ] ;
415
+ branchMap . forEach ( ( branch ) => {
416
+ otherBranches . push ( {
417
+ label : `${ branch . name } (branch)` ,
418
+ value : branch . name ,
419
+ type : "branch" ,
420
+ } ) ;
421
+ } ) ;
422
+ otherBranches . sort ( ( a , b ) => a . value . localeCompare ( b . value ) ) ;
423
+
424
+ const tagOptions = this . state . refs . tags . map ( ( tag ) => ( {
425
+ label : `${ tag . name } (tag)` ,
426
+ value : tag . name ,
427
+ type : "tag" ,
428
+ } ) ) ;
429
+ tagOptions . sort ( ( a , b ) => a . value . localeCompare ( b . value ) ) ;
430
+
431
+ availableOptions = [
432
+ ...prioritizedBranches ,
433
+ ...otherBranches ,
434
+ ...tagOptions ,
435
+ ] ;
436
+ }
437
+
310
438
return (
311
439
< MyThemeProvider >
312
440
< MaterialUI . CssBaseline />
@@ -326,29 +454,64 @@ class App extends React.Component {
326
454
autoFocus = { true }
327
455
onKeyDown = { ( e ) => {
328
456
if ( e . keyCode === 13 )
329
- document . getElementById ( "branch -select" ) . focus ( ) ;
457
+ document . getElementById ( "ref -select" ) . focus ( ) ;
330
458
} }
331
- onInput = { ( e ) => this . setState ( { repo : e . target . value } ) }
459
+ onInput = { ( e ) => this . handleRepoChange ( e . target . value ) }
332
460
defaultValue = { urlParams . get ( "repo" ) }
333
461
sx = { { flexGrow : 3 } }
334
462
/>
335
463
< MaterialUI . Autocomplete
336
464
disablePortal
337
- id = "branch-select"
338
- options = { common_branches }
465
+ id = "ref-select"
466
+ options = { availableOptions }
467
+ loading = { this . state . loadingRefs }
339
468
freeSolo = { true }
340
469
onKeyDown = { ( e ) => {
341
470
if ( e . keyCode === 13 ) this . handleCompute ( ) ;
342
471
} }
343
- onInputChange = { ( e , value ) => this . setState ( { branch : value } ) }
344
- defaultValue = { urlParams . get ( "branch" ) }
472
+ getOptionLabel = { ( option ) =>
473
+ typeof option === "string" ? option : option . label
474
+ }
475
+ renderOption = { ( props , option ) => (
476
+ < li { ...props } > { option . label } </ li >
477
+ ) }
478
+ onInputChange = { ( e , value ) => {
479
+ // If the user enters free text, treat it as a branch
480
+ if ( typeof value === "string" ) {
481
+ this . handleRefChange ( value , "branch" ) ;
482
+ }
483
+ } }
484
+ onChange = { ( e , option ) => {
485
+ if ( option ) {
486
+ if ( typeof option === "object" ) {
487
+ this . handleRefChange ( option . value , option . type ) ;
488
+ } else {
489
+ this . handleRefChange ( option , "branch" ) ;
490
+ }
491
+ }
492
+ } }
493
+ defaultValue = { urlParams . get ( "ref" ) }
345
494
renderInput = { ( params ) => (
346
495
< MaterialUI . TextField
347
496
{ ...params }
348
- label = "Branch"
497
+ label = "Branch/Tag "
349
498
variant = "outlined"
350
- helperText = "e.g. main"
351
- sx = { { flexGrow : 2 , minWidth : 130 } }
499
+ helperText = "e.g. HEAD, main, or v1.0.0"
500
+ sx = { { flexGrow : 2 , minWidth : 200 } }
501
+ InputProps = { {
502
+ ...params . InputProps ,
503
+ endAdornment : (
504
+ < React . Fragment >
505
+ { this . state . loadingRefs ? (
506
+ < MaterialUI . CircularProgress
507
+ color = "inherit"
508
+ size = { 20 }
509
+ />
510
+ ) : null }
511
+ { params . InputProps . endAdornment }
512
+ </ React . Fragment >
513
+ ) ,
514
+ } }
352
515
/>
353
516
) }
354
517
/>
@@ -358,7 +521,7 @@ class App extends React.Component {
358
521
variant = "contained"
359
522
size = "large"
360
523
disabled = {
361
- this . state . progress || ! this . state . repo || ! this . state . branch
524
+ this . state . progress || ! this . state . repo || ! this . state . ref
362
525
}
363
526
>
364
527
< MaterialUI . Icon > start</ MaterialUI . Icon >
0 commit comments