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"
2019-05-10 19:55:32 +00:00
subtreepath : string ; // tree/master/[...]
2019-05-08 23:15:26 +00:00
prefix : string ; // file prefix, "local/" or ""
2019-05-10 19:26:12 +00:00
branch : string ; // "master" is default
2019-05-08 23:15:26 +00:00
repo : any ; // [repo object]
2019-05-10 19:26:12 +00:00
tree? : any ; // [tree object]
head? : any ; // [head ref]
2019-05-12 17:33:21 +00:00
commit? : any ; // after commit()
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 ) {
2019-05-10 19:26:12 +00:00
var toks = ghurl . split ( '/' , 8 ) ;
2019-05-09 17:22:24 +00:00
if ( toks . length < 5 ) return null ;
if ( toks [ 0 ] != 'https:' ) return null ;
if ( toks [ 2 ] != 'github.com' ) return null ;
2019-05-10 19:26:12 +00:00
if ( toks [ 5 ] && toks [ 5 ] != 'tree' ) return null ;
2019-05-10 19:55:32 +00:00
return { user :toks [ 3 ] , repo :toks [ 4 ] , repopath :toks [ 3 ] + '/' + toks [ 4 ] , branch :toks [ 6 ] , subtreepath :toks [ 7 ] } ;
2019-05-09 17:22:24 +00:00
}
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 ;
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 ) ;
2019-05-12 13:52:09 +00:00
bootbox . alert ( "Could not login to GitHub: " + error ) ;
2019-05-08 17:42:24 +00:00
} ) ;
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-10 19:55:32 +00:00
url : ghurl ,
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 ,
2019-05-10 19:26:12 +00:00
branch : urlparse.branch || "master" ,
2019-05-10 19:55:32 +00:00
subtreepath : urlparse.subtreepath ,
2019-05-08 23:15:26 +00:00
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-10 19:26:12 +00:00
getGithubHEADTree ( ghurl :string ) : Promise < GHSession > {
var sess ;
return this . getGithubSession ( ghurl ) . then ( ( session ) = > {
sess = session ;
return sess . repo . git . refs . heads ( sess . branch ) . fetch ( ) ;
} )
. then ( ( head ) = > {
sess . head = head ;
return sess . repo . git . trees ( head . object . sha ) . fetch ( ) ;
} )
2019-05-10 19:55:32 +00:00
. then ( ( tree ) = > {
if ( sess . subtreepath ) {
for ( let subtree of tree . tree ) {
if ( subtree . type == 'tree' && subtree . path == sess . subtreepath && subtree . sha ) {
return sess . repo . git . trees ( subtree . sha ) . fetch ( ) ;
}
}
throw "Cannot find subtree '" + sess . subtreepath + "' in tree " + tree . sha ;
}
return tree ;
} )
2019-05-10 19:26:12 +00:00
. then ( ( tree ) = > {
sess . tree = tree ;
return 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 ;
2019-05-10 19:26:12 +00:00
return this . getGithubHEADTree ( ghurl ) . then ( ( session ) = > {
2019-05-07 19:37:37 +00:00
sess = session ;
let blobreads = [ ] ;
sess . paths = [ ] ;
2019-05-10 19:26:12 +00:00
sess . tree . tree . forEach ( ( item ) = > {
2019-05-07 19:37:37 +00:00
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-13 22:36:25 +00:00
var platform_id = this . project . platform_id ;
var mainPath = this . project . stripLocalPath ( this . project . mainPath ) ;
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 ) ) ;
2019-05-13 22:36:25 +00:00
s = s . replace ( /\$PLATFORM/g , encodeURIComponent ( platform_id ) ) ;
2019-05-16 17:04:27 +00:00
s = s . replace ( /\$GITHUBURL/g , encodeURIComponent ( repo . htmlUrl ) ) ;
2019-05-13 22:36:25 +00:00
s = s . replace ( /\$MAINFILE/g , encodeURIComponent ( 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 ;
} ) ;
}
2019-05-12 17:33:21 +00:00
commit ( ghurl :string , message :string , files : { path :string , data :FileData } [ ] ) : Promise < GHSession > {
2019-05-07 19:37:37 +00:00
var sess : GHSession ;
2019-05-10 19:26:12 +00:00
return this . getGithubHEADTree ( ghurl ) . then ( ( session ) = > {
2019-05-07 19:37:37 +00:00
sess = session ;
2019-05-10 19:55:32 +00:00
if ( sess . subtreepath ) {
throw "Sorry, right now you can only commit files to the root directory of a repository." ;
}
2019-05-07 19:37:37 +00:00
return Promise . all ( files . map ( ( file ) = > {
2019-05-09 12:44:47 +00:00
if ( typeof file . data === 'string' ) {
2019-05-10 19:26:12 +00:00
return sess . repo . git . blobs . create ( {
2019-05-09 12:44:47 +00:00
content : file.data ,
encoding : 'utf-8'
} ) ;
} else {
2019-05-10 19:26:12 +00:00
return sess . repo . git . blobs . create ( {
2019-05-09 12:44:47 +00:00
content : btoa ( byteArrayToString ( file . data ) ) ,
encoding : 'base64'
} ) ;
}
2019-05-07 19:37:37 +00:00
} ) ) ;
} ) . then ( ( blobs ) = > {
2019-05-10 19:26:12 +00:00
return sess . repo . git . trees . create ( {
2019-05-07 19:37:37 +00:00
tree : files.map ( ( file , index ) = > {
return {
path : file.path ,
mode : '100644' ,
type : 'blob' ,
sha : blobs [ index ] [ 'sha' ]
} ;
} ) ,
2019-05-10 19:26:12 +00:00
base_tree : sess.tree.sha
2019-05-07 19:37:37 +00:00
} ) ;
2019-05-10 19:26:12 +00:00
} ) . then ( ( newtree ) = > {
return sess . repo . git . commits . create ( {
2019-05-07 19:37:37 +00:00
message : message ,
2019-05-10 19:26:12 +00:00
tree : newtree.sha ,
2019-05-07 19:37:37 +00:00
parents : [
2019-05-10 19:26:12 +00:00
sess . head . object . sha
2019-05-07 19:37:37 +00:00
]
} ) ;
2019-05-12 17:33:21 +00:00
} ) . then ( ( commit1 ) = > {
return sess . repo . commits ( commit1 . sha ) . fetch ( ) ;
2019-05-07 19:37:37 +00:00
} ) . then ( ( commit ) = > {
2019-05-12 17:33:21 +00:00
sess . commit = commit ;
return sess ;
} ) ;
}
push ( sess :GHSession ) : Promise < GHSession > {
return sess . head . update ( {
sha : sess.commit.sha
2019-05-07 19:37:37 +00:00
} ) . 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
}