diff --git a/doc/notes.txt b/doc/notes.txt index e91dc124..05fd5e72 100644 --- a/doc/notes.txt +++ b/doc/notes.txt @@ -137,15 +137,16 @@ TODO: - Github - gh-pages branch with embedded - handle overwrite logic - - put extensions on vcs example files - test edge/failure cases - what to do about included files? - what if files already open in editor - - import twice? - un-bind from repo? - - repo/platform dichotomy - - navigate to/from repo + - navigate to repo - import 'examples/' retains path? + - what if import interrupted and partial files? + - CORS for some blobs? + - confusing when examples load if file not found + - don't import useless files WEB WORKER FORMAT diff --git a/src/services.ts b/src/services.ts index 26f2b033..4c2fb8a3 100644 --- a/src/services.ts +++ b/src/services.ts @@ -1,5 +1,5 @@ -import { getFolderForPath, isProbablyBinary, stringToByteArray } from "./util"; +import { getFolderForPath, isProbablyBinary, stringToByteArray, byteArrayToString, byteArrayToUTF8 } from "./util"; import { FileData } from "./workertypes"; import { CodeProject } from "./project"; @@ -7,19 +7,28 @@ import { CodeProject } from "./project"; declare var exports; declare var firebase; -export interface GHSession { +export interface GHRepoMetadata { + url : string; // github url + platform_id : string; // e.g. "vcs" + mainPath?: string; // main file path +} + +export interface GHSession extends GHRepoMetadata { 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] - mainPath?: string; // main file path paths? : string[]; } 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"; +export function getRepos() : {[key:string]:GHRepoMetadata} { + return JSON.parse(localStorage.getItem('__repos') || '{}'); +} + export class GithubService { githubCons; @@ -91,20 +100,17 @@ export class GithubService { reponame: urlparse.repo, repopath: urlparse.repopath, prefix: '', //this.getPrefix(urlparse.user, urlparse.repo), - repo: this.github.repos(urlparse.user, urlparse.repo) + repo: this.github.repos(urlparse.user, urlparse.repo), + platform_id: this.project.platform_id }; yes(sess); }); } - getRepos() { - return JSON.parse(localStorage.getItem('__repos') || '{}'); - } - bind(sess:GHSession, dobind:boolean) { - var repos = this.getRepos(); + var repos = getRepos(); if (dobind) { - repos[sess.repopath] = sess.url; + repos[sess.repopath] = {url:sess.url, platform_id:sess.platform_id, mainPath:sess.mainPath}; } else { delete repos[sess.repopath]; } @@ -134,9 +140,14 @@ export class GithubService { // check README for proper platform const re8plat = /8bitworkshop.com[^)]+platform=(\w+)/; m = re8plat.exec(readme); - if (m && !this.project.platform_id.startsWith(m[1])) { - throw "Platform mismatch: Repository is " + m[1] + ", you have " + this.project.platform_id + " selected."; + if (m) { + console.log("platform id: '" + m[1] + "'"); + sess.platform_id = m[1]; + if (!this.project.platform_id.startsWith(m[1])) + throw "Platform mismatch: Repository is " + m[1] + ", you have " + this.project.platform_id + " selected."; } + // bind to repository + this.bind(sess, true); // get head commit return sess; }); @@ -158,11 +169,19 @@ export class GithubService { console.log(item.path, item.type, item.size); sess.paths.push(item.path); if (item.type == 'blob' && !this.isFileIgnored(item.path)) { - var read = sess.repo.git.blobs(item.sha).readBinary().then( (blob) => { + var read = sess.repo.git.blobs(item.sha).fetch().then( (blob) => { var path = sess.prefix + item.path; var size = item.size; - var isBinary = isProbablyBinary(blob); - var data = isBinary ? stringToByteArray(blob) : blob; //byteArrayToUTF8(blob); + 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); + } return (deststore || this.store).setItem(path, data); }); blobreads.push(read); @@ -173,7 +192,6 @@ export class GithubService { return Promise.all(blobreads); }) .then( (blobs) => { - this.bind(sess, true); return sess; }); } @@ -197,10 +215,10 @@ export class GithubService { repo = _repo; // create README.md var s = README_md_template; - s = s.replace(/\$NAME/g, reponame); - s = s.replace(/\$PLATFORM/g, this.project.platform_id); - s = s.replace(/\$IMPORTURL/g, repo.html_url); - s = s.replace(/\$MAINFILE/g, this.project.stripLocalPath(this.project.mainPath)); + 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))); var config = { message: '8bitworkshop: updated metadata in README.md', content: btoa(s) @@ -231,10 +249,17 @@ export class GithubService { }).then( (_tree) => { tree = _tree; return Promise.all(files.map( (file) => { - return repo.git.blobs.create({ - content: file.data, - encoding: 'utf-8' - }); + 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' + }); + } })); }).then( (blobs) => { return repo.git.trees.create({ @@ -264,5 +289,11 @@ export class GithubService { return sess; }); } + + deleteRepository(ghurl:string) { + return this.getGithubSession(ghurl).then( (session) => { + return session.repo.remove(); + }); + } } diff --git a/src/ui.ts b/src/ui.ts index 49ca658d..a18c1d6d 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -14,7 +14,7 @@ import { createNewPersistentStore } from "./store"; import { getFilenameForPath, getFilenamePrefix, highlightDifferences, invertMap, byteArrayToString, compressLZG, byteArrayToUTF8, isProbablyBinary, getWithBinary, getBasePlatform } from "./util"; import { StateRecorderImpl } from "./recorder"; -import { GHSession, GithubService } from "./services"; +import { GHSession, GithubService, getRepos } from "./services"; // external libs (TODO) declare var Tour, GIF, saveAs, JSZip, Mousetrap, Split, firebase; @@ -99,14 +99,14 @@ function getCurrentPresetTitle() : string { function setLastPreset(id:string) { if (hasLocalStorage) { localStorage.setItem("__lastplatform", platform_id); - localStorage.setItem("__lastid_"+platform_id, id); + localStorage.setItem("__lastid_"+store_id, id); } } function unsetLastPreset() { if (hasLocalStorage) { delete qs['file']; - localStorage.removeItem("__lastid_"+platform_id); + localStorage.removeItem("__lastid_"+store_id); } } @@ -230,18 +230,7 @@ function refreshWindowList() { }); } -// can pass integer or string id function loadProject(preset_id:string) { - var index = parseInt(preset_id+""); // might fail -1 - for (var i=0; i= 0) { - // load the preset - current_preset_entry = PRESETS[index]; - preset_id = current_preset_entry.id; - } // set current file ID current_project.mainPath = preset_id; setLastPreset(preset_id); @@ -260,10 +249,14 @@ function loadProject(preset_id:string) { }); } -function reloadProject(id:string, repo?:string) { - qs['platform'] = platform_id; - qs['file'] = id; - if (repo) qs['repo'] = repo; +function reloadProject(id:string) { + // leave repository == '..' + if (id == '..') { + qs = {}; + } else { + qs['platform'] = platform_id; + qs['file'] = id; + } gotoNewLocation(); } @@ -403,34 +396,39 @@ function getBoundGithubURL() : string { return 'https://github.com/' + toks[0] + '/' + toks[1]; } -function importProjectFromGithub(githuburl:string, modal?) { +function importProjectFromGithub(githuburl:string) { var sess : GHSession; - // create new repository store var urlparse = getGithubService().parseGithubURL(githuburl); if (!urlparse) { alert('Could not parse Github URL.'); - if (modal) modal.modal('hide'); return; } - var newstore = createNewPersistentStore(urlparse.repopath, () => { - // import into new store - return getGithubService().import(githuburl).then( (sess1:GHSession) => { - sess = sess1; - return getGithubService().pull(githuburl, newstore); - }).then( (sess2:GHSession) => { - if (modal) modal.modal('hide'); - // TODO: only first sessino has mainPath - if (sess.mainPath) { - reloadProject(sess.mainPath, sess.repopath); - } else { - updateSelector(); - alert("Files imported, but no main file was found so you'll have to select this project in the pulldown."); - } - }).catch( (e) => { - if (modal) modal.modal('hide'); - console.log(e); - alert("Could not import " + githuburl + ": " + e); - }); + // redirect to repo if exists + var existing = getRepos()[urlparse.repopath]; + if (existing) { + qs = {repo:urlparse.repopath}; + gotoNewLocation(); + return; + } + // create new store for imported repository + setWaitDialog(true); + var newstore = createNewPersistentStore(urlparse.repopath, () => { }); + // import into new store + setWaitProgress(0.25); + return getGithubService().import(githuburl).then( (sess1:GHSession) => { + sess = sess1; + setWaitProgress(0.75); + return getGithubService().pull(githuburl, newstore); + }).then( (sess2:GHSession) => { + // TODO: only first session has mainPath? + // reload repo + qs = {repo:sess.repopath}; // file:sess.mainPath, platform:sess.platform_id}; + setWaitDialog(false); + gotoNewLocation(); + }).catch( (e) => { + setWaitDialog(false); + console.log(e); + alert("Could not import " + githuburl + ": " + e); }); } @@ -440,7 +438,8 @@ function _importProjectFromGithub(e) { modal.modal('show'); btn.off('click').on('click', () => { var githuburl = $("#importGithubURL").val()+""; - importProjectFromGithub(githuburl, modal); + modal.modal('hide'); + importProjectFromGithub(githuburl); }); } @@ -461,11 +460,14 @@ function _publishProjectToGithub(e) { modal.modal('hide'); setWaitDialog(true); getGithubService().login().then( () => { + setWaitProgress(0.25); return getGithubService().publish(name, desc, license, priv); }).then( (sess) => { + setWaitProgress(0.5); repo_id = qs['repo'] = sess.repopath; return pushChangesToGithub('initial import from 8bitworkshop.com'); }).then( () => { + setWaitProgress(1.0); reloadProject(current_project.mainPath); }).catch( (e) => { setWaitDialog(false); @@ -512,6 +514,7 @@ function pushChangesToGithub(message:string) { // push files setWaitDialog(true); return getGithubService().login().then( () => { + setWaitProgress(0.5); return getGithubService().commitPush(ghurl, message, files); }).then( (sess) => { setWaitDialog(false); @@ -782,6 +785,7 @@ function updateSelector() { }); }); } else { + sel.append($("