2019-05-07 19:37:37 +00:00
2019-05-09 12:44:47 +00:00
import { getFolderForPath , isProbablyBinary , stringToByteArray , byteArrayToString , byteArrayToUTF8 } from "./util" ;
2019-05-07 19:37:37 +00:00
import { FileData } from "./workertypes" ;
import { CodeProject } from "./project" ;
// in index.html
declare var exports ;
declare var firebase ;
2019-05-09 12:44:47 +00:00
export interface GHRepoMetadata {
url : string ; // github url
platform_id : string ; // e.g. "vcs"
mainPath? : string ; // main file path
}
export interface GHSession extends GHRepoMetadata {
2019-05-08 23:15:26 +00:00
url : string ; // github url
user : string ; // user name
reponame : string ; // repo name
repopath : string ; // "user/repo"
prefix : string ; // file prefix, "local/" or ""
repo : any ; // [repo object]
2019-05-07 19:37:37 +00:00
paths? : string [ ] ;
}
2019-05-08 13:39:57 +00:00
const README_md_template = "$NAME\n=====\n\nCompatible with the [$PLATFORM](http://8bitworkshop.com/redir.html?platform=$PLATFORM&importURL=$GITHUBURL) platform in [8bitworkshop](http://8bitworkshop.com/). Main file is [$MAINFILE]($MAINFILE#mainfile).\n" ;
2019-05-09 12:44:47 +00:00
export function getRepos ( ) : { [ key :string ] : GHRepoMetadata } {
return JSON . parse ( localStorage . getItem ( '__repos' ) || '{}' ) ;
}
2019-05-09 17:22:24 +00:00
export function parseGithubURL ( ghurl :string ) {
var toks = ghurl . split ( '/' ) ;
if ( toks . length < 5 ) return null ;
if ( toks [ 0 ] != 'https:' ) return null ;
if ( toks [ 2 ] != 'github.com' ) return null ;
return { user :toks [ 3 ] , repo :toks [ 4 ] , repopath :toks [ 3 ] + '/' + toks [ 4 ] } ;
}
2019-05-07 19:37:37 +00:00
export class GithubService {
2019-05-08 17:42:24 +00:00
githubCons ;
githubToken ;
2019-05-07 19:37:37 +00:00
github ;
store ;
project : CodeProject ;
branch : string = "master" ;
2019-05-08 17:42:24 +00:00
constructor ( githubCons : ( ) = > any , githubToken :string , store , project : CodeProject ) {
this . githubCons = githubCons ;
this . githubToken = githubToken ;
2019-05-07 19:37:37 +00:00
this . store = store ;
this . project = project ;
2019-05-08 17:42:24 +00:00
this . recreateGithub ( ) ;
}
recreateGithub() {
this . github = new this . githubCons ( { token :this.githubToken } ) ;
}
login ( ) : Promise < void > {
// already logged in? return immediately
if ( this . githubToken && this . githubToken . length ) {
return new Promise < void > ( ( yes , no ) = > {
yes ( ) ;
} ) ;
}
// login via popup
var provider = new firebase . auth . GithubAuthProvider ( ) ;
provider . addScope ( 'repo' ) ;
return firebase . auth ( ) . signInWithPopup ( provider ) . then ( ( result ) = > {
this . githubToken = result . credential . accessToken ;
var user = result . user ;
this . recreateGithub ( ) ;
document . cookie = "__github_key=" + this . githubToken + ";path=/;max-age=31536000" ;
console . log ( "Stored GitHub OAUTH key" ) ;
} ) . catch ( ( error ) = > {
console . log ( error ) ;
alert ( "Could not login to GitHub: " + error ) ;
} ) ;
2019-05-07 19:37:37 +00:00
}
isFileIgnored ( s : string ) : boolean {
s = s . toUpperCase ( ) ;
if ( s . startsWith ( "LICENSE" ) ) return true ;
if ( s . startsWith ( "README" ) ) return true ;
if ( s . startsWith ( "." ) ) return true ;
return false ;
}
2019-05-08 13:39:57 +00:00
getGithubSession ( ghurl :string ) : Promise < GHSession > {
2019-05-07 19:37:37 +00:00
return new Promise ( ( yes , no ) = > {
2019-05-09 17:22:24 +00:00
var urlparse = parseGithubURL ( ghurl ) ;
2019-05-07 19:37:37 +00:00
if ( ! urlparse ) {
no ( "Please enter a valid GitHub URL." ) ;
}
var sess = {
2019-05-09 17:22:24 +00:00
url : 'https://github.com/' + urlparse . repopath ,
2019-05-07 19:37:37 +00:00
user : urlparse.user ,
reponame : urlparse.repo ,
2019-05-08 23:15:26 +00:00
repopath : urlparse.repopath ,
prefix : '' , //this.getPrefix(urlparse.user, urlparse.repo),
2019-05-09 12:44:47 +00:00
repo : this.github.repos ( urlparse . user , urlparse . repo ) ,
2019-05-09 17:22:24 +00:00
platform_id : this.project ? this . project.platform_id : null
2019-05-07 19:37:37 +00:00
} ;
yes ( sess ) ;
} ) ;
}
2019-05-08 23:15:26 +00:00
bind ( sess :GHSession , dobind :boolean ) {
2019-05-09 12:44:47 +00:00
var repos = getRepos ( ) ;
2019-05-08 23:15:26 +00:00
if ( dobind ) {
2019-05-09 12:44:47 +00:00
repos [ sess . repopath ] = { url :sess.url , platform_id :sess.platform_id , mainPath :sess.mainPath } ;
2019-05-08 23:15:26 +00:00
} else {
delete repos [ sess . repopath ] ;
}
localStorage . setItem ( '__repos' , JSON . stringify ( repos ) ) ;
2019-05-07 19:37:37 +00:00
}
2019-05-08 13:39:57 +00:00
2019-05-07 19:37:37 +00:00
import ( ghurl :string ) : Promise < GHSession > {
var sess : GHSession ;
2019-05-08 13:39:57 +00:00
return this . getGithubSession ( ghurl ) . then ( ( session ) = > {
sess = session ;
// load README
return sess . repo . contents ( 'README.md' ) . read ( ) ;
} )
2019-05-09 17:22:24 +00:00
. catch ( ( e ) = > {
console . log ( e ) ;
2019-05-08 17:42:24 +00:00
console . log ( 'no README.md found' )
2019-05-08 13:39:57 +00:00
return '' ; // empty README
} )
. then ( ( readme ) = > {
var m ;
// check README for main file
const re8main = /\(([^)]+)#mainfile\)/ ;
m = re8main . exec ( readme ) ;
2019-05-08 17:42:24 +00:00
if ( m && m [ 1 ] ) {
2019-05-08 13:39:57 +00:00
console . log ( "main path: '" + m [ 1 ] + "'" ) ;
sess . mainPath = m [ 1 ] ;
}
// check README for proper platform
2019-05-09 17:22:24 +00:00
// unless we use githubURL=
2019-05-08 13:39:57 +00:00
const re8plat = /8bitworkshop.com[^)]+platform=(\w+)/ ;
m = re8plat . exec ( readme ) ;
2019-05-09 12:44:47 +00:00
if ( m ) {
console . log ( "platform id: '" + m [ 1 ] + "'" ) ;
2019-05-09 17:22:24 +00:00
if ( sess . platform_id && ! sess . platform_id . startsWith ( m [ 1 ] ) )
2019-05-09 12:44:47 +00:00
throw "Platform mismatch: Repository is " + m [ 1 ] + ", you have " + this . project . platform_id + " selected." ;
2019-05-09 17:22:24 +00:00
sess . platform_id = m [ 1 ] ;
2019-05-08 13:39:57 +00:00
}
2019-05-09 12:44:47 +00:00
// bind to repository
this . bind ( sess , true ) ;
2019-05-08 13:39:57 +00:00
// get head commit
2019-05-08 17:42:24 +00:00
return sess ;
2019-05-08 13:39:57 +00:00
} ) ;
}
2019-05-08 23:15:26 +00:00
pull ( ghurl :string , deststore ? ) : Promise < GHSession > {
2019-05-08 13:39:57 +00:00
var sess : GHSession ;
return this . getGithubSession ( ghurl ) . then ( ( session ) = > {
2019-05-07 19:37:37 +00:00
sess = session ;
return sess . repo . commits ( this . branch ) . fetch ( ) ;
} )
. then ( ( sha ) = > {
return sess . repo . git . trees ( sha . sha ) . fetch ( ) ;
} )
. then ( ( tree ) = > {
let blobreads = [ ] ;
sess . paths = [ ] ;
tree . tree . forEach ( ( item ) = > {
console . log ( item . path , item . type , item . size ) ;
sess . paths . push ( item . path ) ;
if ( item . type == 'blob' && ! this . isFileIgnored ( item . path ) ) {
2019-05-09 12:44:47 +00:00
var read = sess . repo . git . blobs ( item . sha ) . fetch ( ) . then ( ( blob ) = > {
2019-05-07 19:37:37 +00:00
var path = sess . prefix + item . path ;
var size = item . size ;
2019-05-09 12:44:47 +00:00
var encoding = blob . encoding ;
var isBinary = isProbablyBinary ( item . path , blob ) ;
var data = blob . content ;
if ( blob . encoding == 'base64' ) {
var bindata = stringToByteArray ( atob ( data ) ) ;
data = isBinary ? bindata : byteArrayToUTF8 ( bindata ) ;
}
if ( blob . size != data . length ) {
data = data . slice ( 0 , blob . size ) ;
}
2019-05-08 23:15:26 +00:00
return ( deststore || this . store ) . setItem ( path , data ) ;
2019-05-07 19:37:37 +00:00
} ) ;
blobreads . push ( read ) ;
} else {
console . log ( "ignoring " + item . path ) ;
}
} ) ;
return Promise . all ( blobreads ) ;
} )
. then ( ( blobs ) = > {
return sess ;
} ) ;
}
2019-05-08 17:42:24 +00:00
importAndPull ( ghurl :string ) {
return this . import ( ghurl ) . then ( ( sess ) = > {
return this . pull ( ghurl ) ;
} ) ;
}
2019-05-07 19:37:37 +00:00
publish ( reponame :string , desc :string , license :string , isprivate :boolean ) : Promise < GHSession > {
2019-05-08 13:39:57 +00:00
var repo ;
2019-05-07 19:37:37 +00:00
return this . github . user . repos . create ( {
name : reponame ,
description : desc ,
private : isprivate ,
2019-05-08 13:39:57 +00:00
auto_init : false ,
2019-05-07 19:37:37 +00:00
license_template : license
} )
2019-05-08 13:39:57 +00:00
. then ( ( _repo ) = > {
repo = _repo ;
// create README.md
var s = README_md_template ;
2019-05-09 12:44:47 +00:00
s = s . replace ( /\$NAME/g , encodeURIComponent ( reponame ) ) ;
s = s . replace ( /\$PLATFORM/g , encodeURIComponent ( this . project . platform_id ) ) ;
s = s . replace ( /\$GITHUBURL/g , encodeURIComponent ( repo . html_url ) ) ;
s = s . replace ( /\$MAINFILE/g , encodeURIComponent ( this . project . stripLocalPath ( this . project . mainPath ) ) ) ;
2019-05-08 13:39:57 +00:00
var config = {
message : '8bitworkshop: updated metadata in README.md' ,
content : btoa ( s )
}
return repo . contents ( 'README.md' ) . add ( config ) ;
} )
. then ( ( ) = > {
return this . getGithubSession ( repo . htmlUrl ) ;
} )
. then ( ( sess ) = > {
2019-05-07 19:37:37 +00:00
this . bind ( sess , true ) ;
return sess ;
} ) ;
}
commitPush ( ghurl :string , message :string , files : { path :string , data :FileData } [ ] ) : Promise < GHSession > {
var sess : GHSession ;
var repo ;
var head ;
var tree ;
2019-05-08 13:39:57 +00:00
return this . getGithubSession ( ghurl ) . then ( ( session ) = > {
2019-05-07 19:37:37 +00:00
sess = session ;
repo = sess . repo ;
return repo . git . refs . heads ( this . branch ) . fetch ( ) ;
} ) . then ( ( _head ) = > {
head = _head ;
return repo . git . trees ( head . object . sha ) . fetch ( ) ;
} ) . then ( ( _tree ) = > {
tree = _tree ;
return Promise . all ( files . map ( ( file ) = > {
2019-05-09 12:44:47 +00:00
if ( typeof file . data === 'string' ) {
return repo . git . blobs . create ( {
content : file.data ,
encoding : 'utf-8'
} ) ;
} else {
return repo . git . blobs . create ( {
content : btoa ( byteArrayToString ( file . data ) ) ,
encoding : 'base64'
} ) ;
}
2019-05-07 19:37:37 +00:00
} ) ) ;
} ) . then ( ( blobs ) = > {
return repo . git . trees . create ( {
tree : files.map ( ( file , index ) = > {
return {
path : file.path ,
mode : '100644' ,
type : 'blob' ,
sha : blobs [ index ] [ 'sha' ]
} ;
} ) ,
base_tree : tree.sha
} ) ;
} ) . then ( ( tree ) = > {
return repo . git . commits . create ( {
message : message ,
tree : tree.sha ,
parents : [
head . object . sha
]
} ) ;
} ) . then ( ( commit ) = > {
return repo . git . refs . heads ( this . branch ) . update ( {
sha : commit.sha
} ) ;
} ) . then ( ( update ) = > {
return sess ;
} ) ;
}
2019-05-09 12:44:47 +00:00
deleteRepository ( ghurl :string ) {
return this . getGithubSession ( ghurl ) . then ( ( session ) = > {
return session . repo . remove ( ) ;
} ) ;
}
2019-05-07 19:37:37 +00:00
}