2019-05-07 19:37:37 +00:00
2019-10-26 01:55:50 +00:00
import { getFolderForPath , isProbablyBinary , stringToByteArray , byteArrayToString , byteArrayToUTF8 } from "../common/util" ;
import { FileData } from "../common/workertypes" ;
2019-05-07 19:37:37 +00:00
import { CodeProject } from "./project" ;
// in index.html
declare var exports ;
declare var firebase ;
2019-08-22 16:42:48 +00:00
// https://github.com/philschatz/octokat.js/tree/master/examples
2019-05-09 12:44:47 +00:00
export interface GHRepoMetadata {
url : string ; // github url
platform_id : string ; // e.g. "vcs"
2019-05-26 14:54:36 +00:00
sha? : string ; // head commit sha
2019-05-09 12:44:47 +00:00
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-08-17 15:18:11 +00:00
const README_md_template = "$NAME\n=====\n\n[Open this project in 8bitworkshop](http://8bitworkshop.com/redir.html?platform=$PLATFORM&githubURL=$GITHUBURL&file=$MAINFILE).\n" ;
2019-05-08 13:39:57 +00:00
2019-05-09 12:44:47 +00:00
export function getRepos ( ) : { [ key :string ] : GHRepoMetadata } {
2019-05-26 14:54:36 +00:00
var repos = { } ;
for ( var i = 0 ; i < localStorage.length ; i + + ) {
var key = localStorage . key ( i ) ;
if ( key . startsWith ( '__repo__' ) ) {
var repodata : GHRepoMetadata = JSON . parse ( localStorage . getItem ( key ) ) ;
var path = key . substring ( '__repo__' . length ) ;
repos [ path ] = repodata ;
}
}
return repos ;
2019-05-09 12:44:47 +00:00
}
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" ) ;
2019-05-22 18:45:03 +00:00
} ) ;
}
logout ( ) : Promise < void > {
// already logged out? return immediately
if ( ! ( this . githubToken && this . githubToken . length ) ) {
return new Promise < void > ( ( yes , no ) = > {
yes ( ) ;
} ) ;
}
// logout
return firebase . auth ( ) . signOut ( ) . then ( ( ) = > {
document . cookie = "__github_key=;path=/;max-age=0" ;
this . githubToken = null ;
this . recreateGithub ( ) ;
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 ;
2019-05-26 14:54:36 +00:00
sess . sha = head . object . sha ;
return sess . repo . git . trees ( sess . sha ) . fetch ( ) ;
2019-05-10 19:26:12 +00:00
} )
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 ( ) ;
}
}
2019-11-13 20:45:18 +00:00
throw Error ( "Cannot find subtree '" + sess . subtreepath + "' in tree " + tree . sha ) ;
2019-05-10 19:55:32 +00:00
}
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-26 14:54:36 +00:00
var key = '__repo__' + sess . repopath ;
2019-05-08 23:15:26 +00:00
if ( dobind ) {
2019-05-26 14:54:36 +00:00
var repodata : GHRepoMetadata = { url :sess.url , platform_id :sess.platform_id , mainPath :sess.mainPath , sha :sess.sha } ;
localStorage . setItem ( key , JSON . stringify ( repodata ) ) ;
2019-05-08 23:15:26 +00:00
} else {
2019-05-26 14:54:36 +00:00
localStorage . removeItem ( key ) ;
2019-05-08 23:15:26 +00:00
}
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-08-22 16:42:48 +00:00
// make user repo exists
return sess . repo . fetch ( ) . then ( ( _repo ) = > {
return '' ; // empty README
} )
2019-05-08 13:39:57 +00:00
} )
. then ( ( readme ) = > {
var m ;
// check README for main file
2019-05-22 17:58:44 +00:00
const re8main = /8bitworkshop.com[^)]+file=([^)&]+)/ ;
2019-05-08 13:39:57 +00:00
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-08-10 01:51:05 +00:00
const re8plat = /8bitworkshop.com[^)]+platform=([A-Za-z0-9._\-]+)/ ;
2019-05-08 13:39:57 +00:00
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-11-13 20:45:18 +00:00
throw Error ( "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 data = blob . content ;
if ( blob . encoding == 'base64' ) {
var bindata = stringToByteArray ( atob ( data ) ) ;
2019-08-27 23:27:29 +00:00
var isBinary = isProbablyBinary ( item . path , bindata ) ;
2019-05-09 12:44:47 +00:00
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 ) ;
2019-08-29 02:12:45 +00:00
} ) . then ( ( ) = > {
2019-05-08 13:39:57 +00:00
return this . getGithubSession ( repo . htmlUrl ) ;
2019-05-07 19:37:37 +00:00
} ) ;
}
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-08-19 15:16:02 +00:00
if ( ! message ) { message = "updated from 8bitworkshop.com" ; }
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 ) {
2019-11-13 20:45:18 +00:00
throw Error ( "Sorry, right now you can only commit files to the root directory of a repository." ) ;
2019-05-10 19:55:32 +00:00
}
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
}