namespace pxt . github { export interface GHRef { ref : string ; url : string ; object : { sha : string ; type : string ; url : string ; } ; } /**
     * Commit user info
     */ export interface UserInfo { date : string ; name : string ; email : string ; } export interface SHAObject { url : string ; sha : string ; } export interface TreeEntry extends SHAObject { path : string ; mode : string ; type : "blob" | "tree" ; size ? : number ; blobContent ? : string ; } export interface Tree extends SHAObject { tree : TreeEntry [ ] ; truncated : boolean ; } export interface CommitInfo extends SHAObject { author : UserInfo ; committer : UserInfo ; message : string ; tree : SHAObject ; } export interface Commit extends SHAObject { author : UserInfo ; committer : UserInfo ; message : string ; tag ? : string ; parents : SHAObject [ ] ; tree : Tree ; } export let token = null ; export interface RefsResult { refs : pxt . Map < string > ; head ? : string ; } export interface FileContent { encoding : string ; content : string ; size : number ; sha : string ; download_url : string ; } interface CommitComment { id : number ; body : string ; path ? : string ; position ? : number ; user : User ; } export let forceProxy = false ; function hasProxy ( ) { if ( forceProxy ) return true ; if ( U . isNodeJS ) return false ; if ( pxt ?. appTarget ?. cloud ?. noGithubProxy ) return false ; return true ; } function shouldUseProxy ( force ? ) { if ( forceProxy ) return true ; if ( token && ! force ) return false ; return hasProxy ( ) ; } export let handleGithubNetworkError ; const isPrivateRepoCache = { } ; export interface CachedPackage { files : Map < string > ; } export interface IGithubDb { loadConfigAsync ( repopath , tag ) : Promise < pxt . PackageConfig > ; loadPackageAsync ( repopath , tag ) : Promise < CachedPackage > ; } function ghRequestAsync ( options ) { options . method = options . method ?? "GET" ; return workAsync ( ! ! token ) ; function workAsync ( canRetry ) { const opts = U . clone ( options ) as U . HttpRequestOptions ; if ( token ) { if ( ! opts . headers ) opts . headers = { } ; if ( opts . url == GRAPHQL_URL ) opts . headers [ 'Authorization' ] = `bearer ${ token }` ; else { opts . url = pxt . BrowserUtils . cacheBustingUrl ( opts . url ) ; opts . headers [ 'Authorization' ] = `token ${ token }` ; } } opts . allowHttpErrors = opts . allowHttpErrors ?? false ; return U . requestAsync ( opts ) . catch ( e => { pxt . tickEvent ( "github.error" , { statusCode : e . statusCode } ) ; if ( handleGithubNetworkError ) { const retry = handleGithubNetworkError ( opts , e ) ; if ( retry ) return workAsync ( false ) ; } throw e ; } ) ; } } function ghGetJsonAsync ( url ) { return ghRequestAsync ( { url , method : "GET" } ) . then ( resp => resp . json ) ; } function ghProxyWithCdnJsonAsync ( path ) { return Cloud . apiRequestWithCdnAsync ( { url : "gh/" + path , forceLiveEndpoint : true } ) . then ( r => r . json ) ; } function ghProxyHandleException ( e ) { pxt . log ( `github proxy error: ${ e . message }` ) ; pxt . debug ( e ) ; } export function isOrgAsync ( owner ) { return ghRequestAsync ( { url : `https://api.github.com/orgs/${ owner }` , method : "GET" , allowHttpErrors : true } ) . then ( resp => resp . statusCode == 200 ) ; } export class MemoryGithubDb implements IGithubDb { private configs : pxt . Map < pxt . PackageConfig > = { } ; private packages : pxt . Map < CachedPackage > = { } ; private proxyWithCdnLoadPackageAsync ( repopath , tag ) { const key = `${ repopath }/${ tag }` ; let res = this . packages [ key ] ; if ( res ) { pxt . debug ( `github cache ${ repopath }/${ tag }/text` ) ; return Promise . resolve ( res ) ; } const parsed = parseRepoId ( repopath ) ; return ghProxyWithCdnJsonAsync ( join ( parsed . slug , tag , parsed . fileName , "text" ) ) . then ( v => this . packages [ key ] = { files : v } ) ; } private cacheConfig ( key , v ) { const cfg = pxt . Package . parseAndValidConfig ( v ) ; this . configs [ key ] = cfg ; return U . clone ( cfg ) ; } async loadConfigAsync ( repopath , tag ) { if ( ! tag ) tag = "master" ; const key = `${ repopath }/${ tag }` ; let res = this . configs [ key ] ; if ( res ) { pxt . debug ( `github cache ${ repopath }/${ tag }/config` ) ; return U . clone ( res ) ; } if ( hasProxy ( ) ) { try { const gpkg = await this . proxyWithCdnLoadPackageAsync ( repopath , tag ) ; return this . cacheConfig ( key , gpkg . files [ pxt . CONFIG_NAME ] ) ; } catch ( e ) { ghProxyHandleException ( e ) ; } } const cfg = await downloadTextAsync ( repopath , tag , pxt . CONFIG_NAME ) ; return this . cacheConfig ( key , cfg ) ; } async loadPackageAsync ( repopath , tag ) { if ( ! tag ) tag = "master" ; if ( hasProxy ( ) ) { try { return await this . proxyWithCdnLoadPackageAsync ( repopath , tag ) . then ( v => U . clone ( v ) ) ; } catch ( e ) { ghProxyHandleException ( e ) ; } } return await this . githubLoadPackageAsync ( repopath , tag ) ; } private githubLoadPackageAsync ( repopath , tag ) { return tagToShaAsync ( repopath , tag ) . then ( sha => { const key = `${ repopath }/${ sha }` ; let res = this . packages [ key ] ; if ( res ) { pxt . debug ( `github cache ${ repopath }/${ tag }/text` ) ; return Promise . resolve ( U . clone ( res ) ) ; } pxt . log ( `Downloading ${ repopath }/${ tag } -> ${ sha }` ) ; return downloadTextAsync ( repopath , sha , pxt . CONFIG_NAME ) . then ( pkg => { const current = { files : { } } ; current . files [ pxt . CONFIG_NAME ] = pkg ; const cfg = JSON . parse ( pkg ) ; return U . promiseMapAll ( pxt . allPkgFiles ( cfg ) . slice ( 1 ) , fn => downloadTextAsync ( repopath , sha , fn ) . then ( text => { current . files [ fn ] = text ; } ) ) . then ( ( ) => { this . packages [ key ] = current ; return U . clone ( current ) ; } ) ; } ) ; } ) ; } } function fallbackDownloadTextAsync ( parsed , commitid , filepath ) { return ghRequestAsync ( { url : "https://api.github.com/repos/" + join ( parsed . slug , "contents" , parsed . fileName , filepath + "?ref=" + commitid ) , method : "GET" } ) . then ( resp => { const f = resp . json as FileContent ; isPrivateRepoCache [ parsed . slug ] = true ; if ( f && f . encoding == "base64" && f . content != null ) return atob ( f . content ) ; return U . httpGetTextAsync ( f . download_url ) ; } ) ; } export function downloadTextAsync ( repopath , commitid , filepath ) { const parsed = parseRepoId ( repopath ) ; if ( isPrivateRepoCache [ parsed . slug ] ) return fallbackDownloadTextAsync ( parsed , commitid , filepath ) ; return U . requestAsync ( { url : "https://raw.githubusercontent.com/" + join ( parsed . slug , commitid , parsed . fileName , filepath ) , allowHttpErrors : true } ) . then ( resp => { if ( resp . statusCode == 200 ) return resp . text ; return fallbackDownloadTextAsync ( parsed , commitid , filepath ) ; } ) ; } export let db = new MemoryGithubDb ( ) ; export function authenticatedUserAsync ( ) { if ( ! token ) return Promise . resolve ( undefined ) ; return ghGetJsonAsync ( "https://api.github.com/user" ) ; } export function getCommitsAsync ( repopath , sha ) { const parsed = parseRepoId ( repopath ) ; return ghGetJsonAsync ( "https://api.github.com/repos/" + parsed . slug + "/commits?sha=" + sha ) . then ( objs => objs . map ( ( obj ) => { const c = obj . commit ; c . url = obj . url ; c . sha = obj . sha ; return c ; } ) ) ; } export function getCommitAsync ( repopath , sha ) { const parsed = parseRepoId ( repopath ) ; return ghGetJsonAsync ( "https://api.github.com/repos/" + parsed . slug + "/git/commits/" + sha ) . then ( ( commit ) => ghGetJsonAsync ( commit . tree . url + "?recursive=1" ) . then ( ( tree ) => { commit . tree = tree ; return commit ; } ) ) ; } export interface CreateBlobReq { content : string ; encoding : "utf-8" | "base64" ; } export interface CreateTreeReq { base_tree : string ; tree : TreeEntry [ ] ; } export interface CreateCommitReq { message : string ; parents : string [ ] ; tree : string ; } function ghPostAsync ( path , data , headers ? , method ? ) { return ghRequestAsync ( { url : /^https:/ . test ( path ) ? path : "https://api.github.com/repos/" + path , headers , method : method || "POST" , data : data , successCodes : [ 200 , 201 , 202 , 204 ] } ) . then ( resp => resp . json ) ; } export function createObjectAsync ( repopath , type , data ) { const parsed = parseRepoId ( repopath ) ; return ghPostAsync ( parsed . slug + "/git/" + type + "s" , data ) . then ( ( resp ) => resp . sha ) ; } export function postCommitComment ( repopath , commitSha , body , path ? , position ? ) { const parsed = parseRepoId ( repopath ) ; return ghPostAsync ( `${ parsed . slug }/commits/${ commitSha }/comments` , { body , path , position } ) . then ( ( resp ) => resp . id ) ; } export async function fastForwardAsync ( repopath , branch , commitid ) { const parsed = parseRepoId ( repopath ) ; const resp = await ghRequestAsync ( { url : `https://api.github.com/repos/${ parsed . slug }/git/refs/heads/${ branch }` , method : "PATCH" , allowHttpErrors : true , data : { sha : commitid , force : false } } ) ; return ( resp . statusCode == 200 ) ; } export async function putFileAsync ( repopath , path , content ) { const parsed = parseRepoId ( repopath ) ; await ghRequestAsync ( { url : `https://api.github.com/repos/${ pxt . github . join ( parsed . slug , "contents" , parsed . fileName , path ) }` , method : "PUT" , allowHttpErrors : true , data : { message : lf ( "Initialize empty repo" ) , content : btoa ( U . toUTF8 ( content ) ) , branch : "master" } , successCodes : [ 201 ] } ) ; } export async function createTagAsync ( repopath , tag , commitid ) { await ghPostAsync ( repopath + "/git/refs" , { ref : "refs/tags/" + tag , sha : commitid } ) ; } export async function createReleaseAsync ( repopath , tag , commitid ) { await ghPostAsync ( repopath + "/releases" , { tag_name : tag , target_commitish : commitid , name : tag , draft : false , prerelease : false } ) ; } export async function createPRFromBranchAsync ( repopath , baseBranch , headBranch , title , msg ? ) { const res = await ghPostAsync ( repopath + "/pulls" , { title : title , body : msg || lf ( "Automatically created from MakeCode." ) , head : headBranch , base : baseBranch , maintainer_can_modify : true } ) ; return res ?. html_url as string ; } export function mergeAsync ( repopath , base , head , message ? ) { const parsed = parseRepoId ( repopath ) ; return ghRequestAsync ( { url : `https://api.github.com/repos/${ parsed . slug }/merges` , method : "POST" , successCodes : [ 201 , 204 , 409 ] , data : { base , head , commit_message : message } } ) . then ( resp => { if ( resp . statusCode == 201 || resp . statusCode == 204 ) return ( resp . json as SHAObject ) . sha ; if ( resp . statusCode == 409 ) { return null ; } throw U . userError ( lf ( "Cannot merge in github.com/{1}; code: {2}" , repopath , resp . statusCode ) ) ; } ) ; } export function getRefAsync ( repopath , branch ) { branch = branch || "master" ; return ghGetJsonAsync ( "https://api.github.com/repos/" + repopath + "/git/refs/heads/" + branch ) . then ( resolveRefAsync ) . catch ( err => { if ( err . statusCode == 404 ) return undefined ; else Promise . reject ( err ) ; } ) ; } function generateNextRefName ( res , pref ) { let n = 1 ; while ( res . refs [ pref + n ] ) n ++ ; return pref + n ; } export async function getNewBranchNameAsync ( repopath , pref = "patch-" ) { const res = await listRefsExtAsync ( repopath , "heads" ) ; return generateNextRefName ( res , pref ) ; } export async function createNewBranchAsync ( repopath , branchName , commitid ) { await ghPostAsync ( repopath + "/git/refs" , { ref : "refs/heads/" + branchName , sha : commitid } ) ; return branchName ; } export async function forkRepoAsync ( repopath , commitid , pref = "pr-" ) { const parsed = parseRepoId ( repopath ) ; const res = await ghPostAsync ( `${ parsed . slug }/forks` , { } ) ; const repoInfo = mkRepo ( res , { fullName : parsed . fullName , fileName : parsed . fileName } ) ; const endTm = Date . now ( ) + 5 * 60 * 1000 ; let refs = null ; while ( ! refs && Date . now ( ) < endTm ) { await U . delay ( 1000 ) ; try { refs = await listRefsExtAsync ( repoInfo . slug , "heads" ) ; } catch ( err ) { } } if ( ! refs ) throw new Error ( lf ( "Timeout waiting for fork" ) ) ; const branchName = generateNextRefName ( refs , pref ) ; await createNewBranchAsync ( repoInfo . slug , branchName , commitid ) ; return repoInfo . fullName + "#" + branchName ; } export function listRefsAsync ( repopath , namespace = "tags" , useProxy ? , noCache ? ) { return listRefsExtAsync ( repopath , namespace , useProxy , noCache ) . then ( res => Object . keys ( res . refs ) ) ; } export function listRefsExtAsync ( repopath , namespace = "tags" , useProxy ? , noCache ? ) { const parsed = parseRepoId ( repopath ) ; const proxy = shouldUseProxy ( useProxy ) ; let head = null ; const fetch = ! proxy ? ghGetJsonAsync ( `https://api.github.com/repos/${ parsed . slug }/git/refs/${ namespace }/?per_page=100` ) : U . httpGetJsonAsync ( pxt . BrowserUtils . cacheBustingUrl ( `${ pxt . Cloud . apiRoot }gh/${ parsed . slug }/refs${ noCache ? "?nocache=1" : "" }` ) ) . then ( r => { let res = Object . keys ( r . refs ) . filter ( k => U . startsWith ( k , "refs/" + namespace + "/" ) ) . map ( k => ( { ref : k , object : { sha : r . refs [ k ] } } ) ) ; head = r . refs [ "HEAD" ] ; return res ; } ) ; let clean = ( x ) => x . replace ( /^refs\/[^\/]+\// , "" ) ; return fetch . then < RefsResult > ( ( resp ) => { resp . sort ( ( a , b ) => semver . strcmp ( clean ( a . ref ) , clean ( b . ref ) ) ) ; let r = { } ; for ( let obj of resp ) { r [ clean ( obj . ref ) ] = obj . object . sha ; } return { refs : r , head } ; } , err => { if ( err . statusCode == 404 ) return { refs : { } } as any ; else return Promise . reject ( err ) ; } ) ; } function resolveRefAsync ( r ) { if ( r . object . type == "commit" ) return Promise . resolve ( r . object . sha ) ; else if ( r . object . type == "tag" ) return ghGetJsonAsync ( r . object . url ) . then ( ( r ) => r . object . type == "commit" ? r . object . sha : Promise . reject ( new Error ( "Bad type (2nd order) " + r . object . type ) ) ) ; else return Promise . reject ( new Error ( "Bad type " + r . object . type ) ) ; } function tagToShaAsync ( repopath , tag ) { if ( /^[a-f0-9]{40}$/ . test ( tag ) ) return Promise . resolve ( tag ) ; const parsed = parseRepoId ( repopath ) ; return ghGetJsonAsync ( `https://api.github.com/repos/${ parsed . slug }/git/refs/tags/${ tag }` ) . then ( resolveRefAsync , e => ghGetJsonAsync ( `https://api.github.com/repos/${ parsed . slug }/git/refs/heads/${ tag }` ) . then ( resolveRefAsync ) ) ; } export function pkgConfigAsync ( repopath , tag = "master" ) { return db . loadConfigAsync ( repopath , tag ) ; } export function downloadPackageAsync ( repoWithTag , config ) { const p = parseRepoId ( repoWithTag ) ; if ( ! p ) { pxt . log ( 'Unknown GitHub syntax' ) ; return Promise . resolve < CachedPackage > ( undefined ) ; } if ( isRepoBanned ( p , config ) ) { pxt . tickEvent ( "github.download.banned" ) ; pxt . log ( 'Github repo is banned' ) ; return Promise . resolve < CachedPackage > ( undefined ) ; } return db . loadPackageAsync ( p . fullName , p . tag ) . then ( cached => { const dv = upgradedDisablesVariants ( config , repoWithTag ) ; if ( dv ) { const cfg = Package . parseAndValidConfig ( cached . files [ pxt . CONFIG_NAME ] ) ; if ( cfg ) { pxt . log ( `auto-disable ${ dv . join ( "," ) } due to targetconfig entry for ${ repoWithTag }` ) ; cfg . disablesVariants = dv ; cached . files [ pxt . CONFIG_NAME ] = Package . stringifyConfig ( cfg ) ; } } return cached ; } ) ; } export async function downloadLatestPackageAsync ( repo ) { const packageConfig = await pxt . packagesConfigAsync ( ) ; const tag = await pxt . github . latestVersionAsync ( repo . slug , packageConfig ) ; const repoWithTag = `${ repo . fullName }#${ tag }` ; await pxt . github . downloadPackageAsync ( repoWithTag , packageConfig ) ; const config = await pkgConfigAsync ( repo . fullName , tag ) ; const version = `github:${ repoWithTag }` ; return { version , config } ; } export async function cacheProjectDependenciesAsync ( cfg ) { const ghExtensions = Object . keys ( cfg . dependencies ) ?. filter ( dep => isGithubId ( cfg . dependencies [ dep ] ) ) ; if ( ghExtensions . length ) { const pkgConfig = await pxt . packagesConfigAsync ( ) ; await Promise . all ( ghExtensions . map ( async ( ext ) => { const extSrc = cfg . dependencies [ ext ] ; const ghPkg = await downloadPackageAsync ( extSrc , pkgConfig ) ; if ( ! ghPkg ) { throw new Error ( lf ( "Cannot load extension {0} from {1}" , ext , extSrc ) ) ; } } ) ) ; } } export interface User { login : string ; id : number ; avatar_url : string ; gravatar_id : string ; html_url : string ; type : string ; name : string ; company : string ; } interface Repo { id : number ; name : string ; full_name : string ; owner : User ; private : boolean ; html_url : string ; description : string ; fork : boolean ; created_at : string ; updated_at : string ; pushed_at : string ; homepage : string ; size : number ; stargazers_count : number ; watchers_count : number ; forks_count : number ; open_issues_count : number ; forks : number ; open_issues : number ; watchers : number ; default_branch : string ; score : number ; tag ? : string ; } interface SearchResults { total_count : number ; incomplete_results : boolean ; items : Repo [ ] ; } export interface ParsedRepo { owner ? : string ; project ? : string ; slug : string ; fullName : string ; tag ? : string ; fileName ? : string ; } export enum GitRepoStatus { Unknown , Approved , Banned } export interface GitRepo extends ParsedRepo { name : string ; description : string ; defaultBranch : string ; status ? : GitRepoStatus ; updatedAt ? : number ; private ? : boolean ; fork ? : boolean ; } export function listUserReposAsync ( ) { const q = `{
  viewer {
    repositories(first: 100, affiliations: [OWNER, COLLABORATOR], orderBy: {field: PUSHED_AT, direction: DESC}) {
      nodes {
        name
        description
        full_name: nameWithOwner
        private: isPrivate
        fork: isFork
        updated_at: updatedAt
        owner {
          login
        }
        defaultBranchRef {
          name
        }
        pxtjson: object(expression: "master:pxt.json") {
          ... on Blob {
            text
          }
        }
        readme: object(expression: "master:README.md") {
          ... on Blob {
            text
          }
        }
      }
    }
  }
}` ; return ghGraphQLQueryAsync ( q ) . then ( res => ( < any [ ] > res . data . viewer . repositories . nodes ) . filter ( ( node ) => node . pxtjson ) . filter ( ( node ) => { node . default_branch = node . defaultBranchRef . name ; const pxtJson = pxt . Package . parseAndValidConfig ( node . pxtjson && node . pxtjson . text ) ; const readme = node . readme && node . readme . text ; if ( ! pxtJson ) return false ; if ( pxtJson . supportedTargets ) return pxtJson . supportedTargets . indexOf ( pxt . appTarget . id ) > - 1 ; return readme && readme . indexOf ( "PXT/" + pxt . appTarget . id ) > - 1 ; } ) . map ( ( node ) => mkRepo ( node , { fullName : node . full_name } ) ) ) ; } export function createRepoAsync ( name , description , priv ? ) { return ghPostAsync ( "https://api.github.com/user/repos" , { name , description , private : ! ! priv , has_issues : true , has_projects : false , has_wiki : false , allow_rebase_merge : false , allow_merge_commit : true , delete_branch_on_merge : false } ) . then ( v => mkRepo ( v ) ) ; } export async function enablePagesAsync ( repo ) { const parsed = parseRepoId ( repo ) ; let url = undefined ; try { const status = await ghGetJsonAsync ( `https://api.github.com/repos/${ parsed . slug }/pages` ) ; if ( status ) url = status . html_url ; } catch ( e ) { } if ( ! url ) { try { const r = await ghPostAsync ( `https://api.github.com/repos/${ parsed . slug }/pages` , { source : { branch : "master" , path : "/" } } , { "Accept" : "application/vnd.github.switcheroo-preview+json" } ) ; url = r . html_url ; } catch ( e ) { pxt . tickEvent ( "github.pages.error" ) ; pxt . reportException ( e ) ; } } if ( url ) { const rep = await ghGetJsonAsync ( `https://api.github.com/repos/${ repo }` ) ; if ( rep && ! rep . homepage ) { try { await ghPostAsync ( `https://api.github.com/repos/${ repo }` , { "homepage" : url } , undefined , "PATCH" ) ; } catch ( e ) { pxt . tickEvent ( "github.homepage.error" ) ; } } } } export function repoIconUrl ( repo ) { if ( repo . status != GitRepoStatus . Approved ) return undefined ; return mkRepoIconUrl ( repo ) ; } export function mkRepoIconUrl ( repo ) { return Cloud . cdnApiUrl ( `gh/${ repo . fullName }/icon` ) ; } function mkRepo ( r , options ? ) { if ( ! r ) return undefined ; const rr = { owner : r . owner . login . toLowerCase ( ) , slug : r . full_name . toLowerCase ( ) , fullName : ( options ?. fullName || r . full_name ) . toLowerCase ( ) , fileName : options ?. fileName ?. toLocaleLowerCase ( ) , name : r . name , description : r . description , defaultBranch : r . default_branch , tag : options ?. tag , updatedAt : Math . round ( new Date ( r . updated_at ) . getTime ( ) / 1000 ) , fork : r . fork , private : r . private , } ; rr . status = repoStatus ( rr , options ?. config ) ; return rr ; } export function repoStatus ( rr , config ) { if ( ! rr ) return GitRepoStatus . Unknown ; return isRepoBanned ( rr , config ) ? GitRepoStatus . Banned : isRepoApproved ( rr , config ) ? GitRepoStatus . Approved : GitRepoStatus . Unknown ; } function isOrgBanned ( repo , config ) { if ( ! config ) return false ; if ( ! repo || ! repo . owner ) return true ; if ( config . bannedOrgs && config . bannedOrgs . some ( org => org . toLowerCase ( ) == repo . owner . toLowerCase ( ) ) ) return true ; return false ; } function isRepoBanned ( repo , config ) { if ( isOrgBanned ( repo , config ) ) return true ; if ( ! config ) return false ; if ( ! repo || ! repo . fullName ) return true ; if ( config . bannedRepos && config . bannedRepos . some ( fn => fn . toLowerCase ( ) == repo . fullName . toLowerCase ( ) ) ) return true ; return false ; } function isOrgApproved ( repo , config ) { if ( ! repo || ! config ) return false ; if ( repo . owner && config . approvedOrgs && config . approvedOrgs . some ( org => org . toLowerCase ( ) == repo . owner . toLowerCase ( ) ) ) return true ; return false ; } function isRepoApproved ( repo , config ) { if ( isOrgApproved ( repo , config ) ) return true ; if ( ! repo || ! config ) return false ; if ( repo . fullName && config . approvedRepos && config . approvedRepos . some ( fn => fn . toLowerCase ( ) == repo . fullName . toLowerCase ( ) ) ) return true ; return false ; } export async function repoAsync ( repopath , config ) { const rid = parseRepoId ( repopath ) ; if ( ! rid ) return undefined ; const status = repoStatus ( rid , config ) ; if ( status == GitRepoStatus . Banned ) return undefined ; if ( hasProxy ( ) ) { try { return await proxyRepoAsync ( rid , status ) ; } catch ( e ) { ghProxyHandleException ( e ) ; } } const r = await ghGetJsonAsync ( "https://api.github.com/repos/" + rid . slug ) ; return mkRepo ( r , { config , fullName : rid . fullName , fileName : rid . fileName , tag : rid . tag } ) ; } function proxyRepoAsync ( rid , status ) { return ghProxyWithCdnJsonAsync ( rid . slug ) . then ( meta => { if ( ! meta ) return undefined ; return { github : true , owner : rid . owner , fullName : rid . fullName , fileName : rid . fileName , slug : rid . slug , name : rid . fileName ? `${ meta . name }-${ rid . fileName }` : meta . name , description : meta . description , defaultBranch : meta . defaultBranch || "master" , tag : rid . tag , status } ; } ) . catch ( err => { pxt . reportException ( err ) ; return undefined ; } ) ; } export function searchAsync ( query , config ) { if ( ! config ) return Promise . resolve ( [ ] ) ; let repos = query . split ( '|' ) . map ( parseRepoId ) . filter ( repo => ! ! repo ) ; if ( repos . length > 0 ) return Promise . all ( repos . map ( id => repoAsync ( id . fullName , config ) ) ) . then ( rs => rs . filter ( r => r && r . status != GitRepoStatus . Banned ) ) ; const fetch = ( ) => U . httpGetJsonAsync ( `${ pxt . Cloud . apiRoot }ghsearch/${ appTarget . id }/${ appTarget . platformid || appTarget . id }?q=${ encodeURIComponent ( query ) }` ) ; return fetch ( ) . then ( ( rs ) => rs . items . map ( item => mkRepo ( item , { config , fullName : item . full_name } ) ) . filter ( r => r . status == GitRepoStatus . Approved || ( config . allowUnapproved && r . status == GitRepoStatus . Unknown ) ) . filter ( r => ! pxt . appTarget . appTheme . githubUrl || `https://github.com/${ r . fullName }` != pxt . appTarget . appTheme . githubUrl . toLowerCase ( ) ) ) . catch ( err => [ ] ) ; } export function parseRepoId ( repo ) { if ( ! repo ) return undefined ; repo = repo . trim ( ) ; repo = repo . replace ( /\/$/ , '' ) ; const mgh = /^https:\/\/([^./#]+)\.github\.io\/([^/#]+)\/?$/i . exec ( repo ) ; if ( mgh ) repo = `github:${ mgh [ 1 ] }/${ mgh [ 2 ] }` ; repo = repo . replace ( /^github:/i , "" ) ; repo = repo . replace ( /^https:\/\/github\.com\//i , "" ) ; repo = repo . replace ( /\.git\b/i , "" ) ; const m = /^([^#\/:]+)\/([^#\/:]+)(\/([^#]+))?(#([^\/:]*))?$/ . exec ( repo ) ; if ( ! m ) return undefined ; const owner = m [ 1 ] ; const project = m [ 2 ] ; let fileName = m [ 4 ] ; const tag = m [ 6 ] ; const treeM = fileName && /^tree\/([^\/]+\/)/ . exec ( fileName ) ; if ( treeM ) { fileName = fileName . slice ( treeM [ 0 ] . length ) ; } return { owner , project , slug : join ( owner , project ) , fullName : join ( owner , project , fileName ) , tag , fileName } ; } export function toGithubDependencyPath ( path , tag ? ) { let r = "github:" + path ; if ( tag ) r += "#" + tag ; return r ; } export function isGithubId ( id ) { if ( ! id ) return false ; return id . slice ( 0 , 7 ) == "github:" ; } export function stringifyRepo ( p ) { return p ? "github:" + p . fullName . toLowerCase ( ) + "#" + ( p . tag || "master" ) : undefined ; } export function normalizeRepoId ( id ) { const gid = parseRepoId ( id ) ; if ( ! gid ) return undefined ; gid . tag = gid . tag || "master" ; return stringifyRepo ( gid ) ; } export function join ( ... parts ) { return parts . filter ( p => ! ! p ) . join ( '/' ) ; } function upgradeRule ( cfg , id ) { if ( ! cfg || ! cfg . upgrades ) return null ; const parsed = parseRepoId ( id ) ; if ( ! parsed ) return null ; return U . lookup ( cfg . upgrades , parsed . fullName . toLowerCase ( ) ) ; } function upgradedDisablesVariants ( cfg , id ) { const upgr = upgradeRule ( cfg , id ) ; const m = /^dv:(.*)/ . exec ( upgr ) ; if ( m ) { const disabled = m [ 1 ] . split ( /,/ ) ; if ( disabled . some ( d => ! /^\w+$/ . test ( d ) ) ) return null ; return disabled ; } return null ; } export function upgradedPackageReference ( cfg , id ) { const upgr = upgradeRule ( cfg , id ) ; if ( ! upgr ) return null ; const m = /^min:(.*)/ . exec ( upgr ) ; const minV = m && pxt . semver . tryParse ( m [ 1 ] ) ; if ( minV ) { const parsed = parseRepoId ( id ) ; const currV = pxt . semver . tryParse ( parsed . tag ) ; if ( currV && pxt . semver . cmp ( currV , minV ) < 0 ) { parsed . tag = m [ 1 ] ; pxt . debug ( `upgrading ${ id } to ${ m [ 1 ] }` ) ; return stringifyRepo ( parsed ) ; } else { if ( ! currV ) pxt . log ( `not upgrading ${ id } - cannot parse version` ) ; return null ; } } else { if ( ! upgradedDisablesVariants ( cfg , id ) ) pxt . log ( `invalid upgrade rule: ${ id } -> ${ upgr }` ) ; } return id ; } export function upgradedPackageId ( cfg , id ) { const dv = upgradedDisablesVariants ( cfg , id ) ; if ( dv ) return id + "?dv=" + dv . join ( "," ) ; return id ; } export function latestVersionAsync ( repopath , config , useProxy ? , noCache ? ) { const parsed = parseRepoId ( repopath ) ; if ( ! parsed ) return Promise . resolve < string > ( null ) ; return repoAsync ( parsed . slug , config ) . then ( scr => { if ( ! scr ) return undefined ; return listRefsExtAsync ( scr . slug , "tags" , useProxy , noCache ) . then ( refsRes => { let tags = Object . keys ( refsRes . refs ) ; tags = pxt . semver . sortLatestTags ( tags ) ; const targetVersion = pxt . appTarget . versions && pxt . semver . tryParse ( pxt . appTarget . versions . target ) ; if ( targetVersion && config . releases && config . releases [ "v" + targetVersion . major ] ) { const release = config . releases [ "v" + targetVersion . major ] . map ( repo => pxt . github . parseRepoId ( repo ) ) . filter ( repo => repo && repo . fullName . toLowerCase ( ) == parsed . fullName . toLowerCase ( ) ) [ 0 ] ; if ( release ) { if ( tags . some ( t => t == release . tag ) ) { pxt . debug ( `approved release ${ release . fullName }#${ release . tag } for v${ targetVersion . major }` ) ; return Promise . resolve ( release . tag ) ; } else { pxt . reportError ( `packages` , `approved release ${ release . fullName }#${ release . tag } for v${ targetVersion . major } not found anymore` , { repo : scr . fullName } ) ; } } } if ( tags [ 0 ] ) return Promise . resolve ( tags [ 0 ] ) ; else return refsRes . head || tagToShaAsync ( scr . slug , scr . defaultBranch ) ; } ) ; } ) ; } export function resolveMonoRepoVersions ( deps ) { deps = Util . clone ( deps ) ; const slugVersions = { } ; Object . keys ( deps ) . map ( id => ( { id , ghid : github . parseRepoId ( deps [ id ] ) } ) ) . filter ( v => v . ghid ?. tag ) . forEach ( v => { const { id , ghid } = v ; let version = slugVersions [ ghid . slug ] ; if ( ! version ) { version = slugVersions [ ghid . slug ] = { tag : ghid . tag , deps : [ ] } ; } version . deps . push ( v ) ; if ( pxt . semver . strcmp ( version . tag , ghid . tag ) < 0 ) { pxt . debug ( `dep: resolve later monorepo tag to ${ pxt . github . stringifyRepo ( ghid ) }` ) ; version . tag = ghid . tag ; } } ) ; U . values ( slugVersions ) . forEach ( v => v . deps . forEach ( dep => { if ( dep . ghid . tag !== v . tag ) { pxt . debug ( `dep: ${ pxt . github . stringifyRepo ( dep . ghid ) } -> ${ v . tag }` ) ; dep . ghid . tag = v . tag ; deps [ dep . id ] = pxt . github . stringifyRepo ( dep . ghid ) ; } } ) ) ; return deps ; } export interface GitJson { repo : string ; commit : pxt . github . Commit ; isFork ? : boolean ; mergeSha ? : string ; } export const GIT_JSON = ".git.json" ; const GRAPHQL_URL = "https://api.github.com/graphql" ; export function lookupFile ( parsed , commit , path ) { if ( ! commit ) return null ; const fpath = join ( parsed . fileName , path ) ; return commit . tree . tree . find ( e => e . path == fpath ) ; } param query export function ghGraphQLQueryAsync ( query ) { const payload = JSON . stringify ( { query } ) ; return ghPostAsync ( GRAPHQL_URL , payload ) ; } export interface PullRequest { number : number ; url ? : string ; title ? : string ; base ? : string ; isDraft ? : boolean ; state ? : "OPEN" | "CLOSED" | "MERGED" ; mergeable ? : "MERGEABLE" | "CONFLICTING" | "UNKNOWN" ; } param reponame param headName export function findPRNumberforBranchAsync ( reponame , headName ) { const repoId = parseRepoId ( reponame ) ; const query = `
{
    repository(owner: ${ JSON . stringify ( repoId . owner ) }, name: ${ JSON . stringify ( repoId . project ) }) {
        pullRequests(last: 1, states: [OPEN, MERGED], headRefName: ${ JSON . stringify ( headName ) }) {
            edges {
                node {
                    number
                    state
                    mergeable
                    baseRefName
                    url
                    isDraft
                }
            }
        }
    }
}
` ; return ghGraphQLQueryAsync ( query ) . then < pxt . github . PullRequest > ( resp => { const edge = resp . data . repository . pullRequests . edges [ 0 ] ; if ( edge && edge . node ) { const node = edge . node ; return { number : node . number , mergeable : node . mergeable , state : node . state , title : node . title , url : node . url , base : node . baseRefName } ; } return { number : - 1 } ; } ) ; } export interface GitHubPagesStatus { status : null | "queued" | "building" | "built" | "errored" ; html_url ? : string ; source ? : { branch : string ; directory : string ; } ; } export function getPagesStatusAsync ( repoPath ) { return ghGetJsonAsync ( `https://api.github.com/repos/${ repoPath }/pages` ) . catch ( e => ( { status : null } ) ) ; } }