11const 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." ;
33
44const urlParams = new URLSearchParams ( window . location . search ) ;
55const baseurl = window . location . pathname ;
@@ -154,6 +154,39 @@ function Results(props) {
154154 ) ;
155155}
156156
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+
157190async function prepare_pyodide ( deps ) {
158191 const deps_str = deps . map ( ( i ) => `"${ i } "` ) . join ( ", " ) ;
159192 const pyodide = await loadPyodide ( ) ;
@@ -196,28 +229,58 @@ class App extends React.Component {
196229 this . state = {
197230 results : [ ] ,
198231 repo : urlParams . get ( "repo" ) || "" ,
199- branch : urlParams . get ( "branch" ) || "" ,
232+ ref : urlParams . get ( "ref" ) || "" ,
233+ refType : urlParams . get ( "refType" ) || "branch" ,
234+ refs : { branches : [ ] , tags : [ ] } ,
200235 msg : `<p>${ DEFAULT_MSG } </p><h4>Packages:</h4> ${ deps_str } ` ,
201236 progress : false ,
237+ loadingRefs : false ,
202238 err_msg : "" ,
203239 skip_reason : "" ,
204240 url : "" ,
205241 } ;
206242 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 } ) ;
207269 }
208270
209271 handleCompute ( ) {
210- if ( ! this . state . repo || ! this . state . branch ) {
272+ if ( ! this . state . repo || ! this . state . ref ) {
211273 this . setState ( { results : [ ] , msg : DEFAULT_MSG } ) ;
212274 window . history . replaceState ( null , "" , baseurl ) ;
213275 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 } )` ,
215277 ) ;
216278 return ;
217279 }
218280 const local_params = new URLSearchParams ( {
219281 repo : this . state . repo ,
220- branch : this . state . branch ,
282+ ref : this . state . ref ,
283+ refType : this . state . refType ,
221284 } ) ;
222285 window . history . replaceState ( null , "" , `${ baseurl } ?${ local_params } ` ) ;
223286 this . setState ( {
@@ -234,7 +297,7 @@ class App extends React.Component {
234297 from repo_review.ghpath import GHPath
235298 from dataclasses import replace
236299
237- package = GHPath(repo="${ state . repo } ", branch="${ state . branch } ")
300+ package = GHPath(repo="${ state . repo } ", branch="${ state . ref } ")
238301 families, checks = process(package)
239302
240303 for v in families.values():
@@ -249,7 +312,7 @@ class App extends React.Component {
249312 this . setState ( {
250313 msg : DEFAULT_MSG ,
251314 progress : false ,
252- err_msg : "Invalid repository or branch. Please try again." ,
315+ err_msg : "Invalid repository or branch/tag . Please try again." ,
253316 } ) ;
254317 return ;
255318 }
@@ -288,7 +351,7 @@ class App extends React.Component {
288351 this . setState ( {
289352 results : results ,
290353 families : families ,
291- msg : `Results for ${ state . repo } @${ state . branch } ` ,
354+ msg : `Results for ${ state . repo } @${ state . ref } ( ${ state . refType } ) ` ,
292355 progress : false ,
293356 err_msg : "" ,
294357 url : "" ,
@@ -300,13 +363,78 @@ class App extends React.Component {
300363 }
301364
302365 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+ }
305372 }
306373 }
307374
308375 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+
310438 return (
311439 < MyThemeProvider >
312440 < MaterialUI . CssBaseline />
@@ -326,29 +454,64 @@ class App extends React.Component {
326454 autoFocus = { true }
327455 onKeyDown = { ( e ) => {
328456 if ( e . keyCode === 13 )
329- document . getElementById ( "branch -select" ) . focus ( ) ;
457+ document . getElementById ( "ref -select" ) . focus ( ) ;
330458 } }
331- onInput = { ( e ) => this . setState ( { repo : e . target . value } ) }
459+ onInput = { ( e ) => this . handleRepoChange ( e . target . value ) }
332460 defaultValue = { urlParams . get ( "repo" ) }
333461 sx = { { flexGrow : 3 } }
334462 />
335463 < MaterialUI . Autocomplete
336464 disablePortal
337- id = "branch-select"
338- options = { common_branches }
465+ id = "ref-select"
466+ options = { availableOptions }
467+ loading = { this . state . loadingRefs }
339468 freeSolo = { true }
340469 onKeyDown = { ( e ) => {
341470 if ( e . keyCode === 13 ) this . handleCompute ( ) ;
342471 } }
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" ) }
345494 renderInput = { ( params ) => (
346495 < MaterialUI . TextField
347496 { ...params }
348- label = "Branch"
497+ label = "Branch/Tag "
349498 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+ } }
352515 />
353516 ) }
354517 />
@@ -358,7 +521,7 @@ class App extends React.Component {
358521 variant = "contained"
359522 size = "large"
360523 disabled = {
361- this . state . progress || ! this . state . repo || ! this . state . branch
524+ this . state . progress || ! this . state . repo || ! this . state . ref
362525 }
363526 >
364527 < MaterialUI . Icon > start</ MaterialUI . Icon >
0 commit comments