From 4cc9aaeaca9847536da60d94085974e5df534868 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Wed, 8 May 2019 09:39:57 -0400 Subject: [PATCH] write README when creating github repository, check platform, migrate files --- doc/notes.txt | 14 ++++-- index.html | 1 + src/project.ts | 15 +++++++ src/services.ts | 80 ++++++++++++++++++++++++--------- src/ui.ts | 90 +++++++++++++++++++++++++------------ src/windows.ts | 9 ++-- test/cli/testgithub.js | 56 +++++++++++++++++++---- test/cli/testplatforms.js | 2 - test/cli/workertestutils.js | 3 ++ 9 files changed, 200 insertions(+), 70 deletions(-) diff --git a/doc/notes.txt b/doc/notes.txt index 751e2d9b..bc6db787 100644 --- a/doc/notes.txt +++ b/doc/notes.txt @@ -41,7 +41,6 @@ TODO: - VCS asm library - better VCS single stepping, maybe also listings - VCS skips step on lsr/lsr after run to line -- links to external tools in ide - error msg when #link doesn't work - figure out folders for projects for real - click to break on raster position @@ -132,12 +131,19 @@ TODO: - upload multiple files/zip file to subdirectory - allow "include graphics.asm" instead of "include project/graphics.asm" - chrome looks blurry on vcs +- convert more stuff to Promises - don't have to include bootstrap-tourist each time? - don't have to include firebase always? - Github - - write/read metadata w/ main file - - push/pull changes - - gh-pages branch with embedded + - 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? + - login/logout? WEB WORKER FORMAT diff --git a/index.html b/index.html index 8b9ec49e..13db4f22 100644 --- a/index.html +++ b/index.html @@ -407,6 +407,7 @@ function require(modname) { } + diff --git a/src/project.ts b/src/project.ts index 9c652087..6a269bb6 100644 --- a/src/project.ts +++ b/src/project.ts @@ -338,4 +338,19 @@ export class CodeProject { } return path; } + + migrateToNewFolder(newprefix : string) { + // TODO: must end with / + var newPath = newprefix + this.stripLocalPath(this.mainPath); + console.log(this.mainPath + "->" + newPath); + var data = this.filedata[this.mainPath]; + console.log(data.length + " bytes"); + return this.store.setItem(newPath, data).then(() => { + //return this.store.removeItem(this.mainPath); + console.log("moved " + this.mainPath + " to " + newPath); + this.filedata[newPath] = this.filedata[this.mainPath]; //TODO? + this.mainPath = newPath; + }); + } + } diff --git a/src/services.ts b/src/services.ts index 4239ea6f..49042b8e 100644 --- a/src/services.ts +++ b/src/services.ts @@ -7,15 +7,18 @@ import { CodeProject } from "./project"; declare var exports; declare var firebase; -interface GHSession { +export interface GHSession { url : string; user : string; reponame : string; repo : any; prefix : string; paths? : string[]; + mainPath?: 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 class GithubService { github; @@ -39,7 +42,6 @@ export class GithubService { parseGithubURL(ghurl:string) { var toks = ghurl.split('/'); - console.log(toks); if (toks.length < 5) return null; if (toks[0] != 'https:') return null; if (toks[2] != 'github.com') return null; @@ -47,10 +49,10 @@ export class GithubService { } getPrefix(user, reponame) : string { - return 'shared/' + user + '-' + reponame + '/'; + return 'shared/' + user + '/' + reponame + '/'; } - getGithubRepo(ghurl:string) : Promise { + getGithubSession(ghurl:string) : Promise { return new Promise( (yes,no) => { var urlparse = this.parseGithubURL(ghurl); if (!urlparse) { @@ -70,9 +72,6 @@ export class GithubService { // bind a folder path to the Github URL in local storage bind(sess : GHSession, dobind : boolean) { var key = '__github_url_' + sess.prefix; - // TODO: this doesn't work b/c it binds the entire root to a url - if (!key.endsWith('/')) - key = key + '/'; console.log('bind', key, dobind); if (dobind) localStorage.setItem(key, sess.url); @@ -83,13 +82,43 @@ export class GithubService { getBoundURL(path : string) : string { var p = getFolderForPath(path); var key = '__github_url_' + p + '/'; - console.log(key); + console.log("getBoundURL", key); return localStorage.getItem(key) as string; // TODO } - + import(ghurl:string) : Promise { var sess : GHSession; - return this.getGithubRepo(ghurl).then( (session) => { + return this.getGithubSession(ghurl).then( (session) => { + sess = session; + // load README + return sess.repo.contents('README.md').read(); + }) + .catch( () => { + return ''; // empty README + }) + .then( (readme) => { + var m; + // check README for main file + const re8main = /\(([^)]+)#mainfile\)/; + m = re8main.exec(readme); + if (m) { + console.log("main path: '" + m[1] + "'"); + sess.mainPath = m[1]; + } + // 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."; + } + // get head commit + return this.pull(ghurl); + }); + } + + pull(ghurl:string) : Promise { + var sess : GHSession; + return this.getGithubSession(ghurl).then( (session) => { sess = session; return sess.repo.commits(this.branch).fetch(); }) @@ -124,21 +153,32 @@ export class GithubService { } publish(reponame:string, desc:string, license:string, isprivate:boolean) : Promise { + var repo; return this.github.user.repos.create({ name: reponame, description: desc, private: isprivate, - auto_init: true, + auto_init: false, license_template: license }) - .then( (repo) => { - let sess = { - url: repo.htmlUrl, - user: repo.owner.login, - reponame: reponame, - repo: repo, - prefix : '' - }; + .then( (_repo) => { + 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)); + 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) => { this.bind(sess, true); return sess; }); @@ -149,7 +189,7 @@ export class GithubService { var repo; var head; var tree; - return this.getGithubRepo(ghurl).then( (session) => { + return this.getGithubSession(ghurl).then( (session) => { sess = session; repo = sess.repo; return repo.git.refs.heads(this.branch).fetch(); diff --git a/src/ui.ts b/src/ui.ts index 69c0e8b6..251104f5 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 } from "./util"; import { StateRecorderImpl } from "./recorder"; -import { GithubService } from "./services"; +import { GHSession, GithubService } from "./services"; // external libs (TODO) declare var Tour, GIF, saveAs, JSZip, Mousetrap, Split, firebase; @@ -382,35 +382,48 @@ function getCookie(name) { function getGithubService() { if (!githubService) { - loadScript("lib/octokat.js", () => { - // get github API key from cookie - var ghkey = getCookie('__github_key'); - var ghopts = {token:ghkey}; - githubService = new GithubService(new exports['Octokat'](ghopts), store, current_project); - }); + // get github API key from cookie + var ghkey = getCookie('__github_key'); + var ghopts = {token:ghkey}; + githubService = new GithubService(new exports['Octokat'](ghopts), store, current_project); + console.log("loaded github service"); } return githubService; } +function getBoundGithubURL() { + console.log("main path: " + current_project.mainPath); + var ghurl = getGithubService().getBoundURL(current_project.mainPath); + console.log("Github URL: " + ghurl); + return ghurl || alert("This project (" + current_project.mainPath + ") is not bound to a GitHub project."); +} + function _importProjectFromGithub(e) { - getGithubService(); // load it var modal = $("#importGithubModal"); var btn = $("#importGithubButton"); modal.modal('show'); btn.off('click').on('click', () => { var githuburl = $("#importGithubURL").val()+""; - getGithubService().import(githuburl).then( (sess) => { + getGithubService().import(githuburl).then( (sess:GHSession) => { + if (sess.mainPath) { + reloadPresetNamed(sess.prefix + sess.mainPath); + } // TODO : redirect to main file modal.modal('hide'); }).catch( (e) => { modal.modal('hide'); + console.log(e); alert("Could not import " + githuburl + ": " + e); }); }); } function _publishProjectToGithub(e) { - getGithubService(); // load it + var ghurl = getGithubService().getBoundURL(current_project.mainPath); + if (ghurl) { + alert("This project (" + current_project.mainPath + ") is already bound to a Github repository. Choose 'Push Changes' to update."); + return; + } var modal = $("#publishGithubModal"); var btn = $("#publishGithubButton"); modal.modal('show'); @@ -419,22 +432,28 @@ function _publishProjectToGithub(e) { var desc = $("#githubRepoDesc").val()+""; var priv = $("#githubRepoPrivate").val() == 'private'; var license = $("#githubRepoLicense").val()+""; - getGithubService().publish(name, desc, license, priv).then( (sess) => { - modal.modal('hide'); - // TODO: commit files - // TODO: migrate project files + var sess; + modal.modal('hide'); + setWaitDialog(true); + getGithubService().publish(name, desc, license, priv).then( (_sess) => { + sess = _sess; console.log(sess); - alert("Created repository at " + sess.url); - pushChangesToGithub('initial import from 8bitworkshop.com'); + return current_project.migrateToNewFolder(sess.prefix); + }).then( () => { + return pushChangesToGithub('initial import from 8bitworkshop.com'); + }).then( () => { + reloadPresetNamed(current_project.mainPath); }).catch( (e) => { - modal.modal('hide'); - alert("Could not create GitHub repository: " + e); + setWaitDialog(false); + console.log(e); + alert("Could not publish GitHub repository: " + e); }); }); } function _pushProjectToGithub(e) { - getGithubService(); // load it + var ghurl = getBoundGithubURL(); + if (!ghurl) return; var modal = $("#pushGithubModal"); var btn = $("#pushGithubButton"); modal.modal('show'); @@ -445,29 +464,42 @@ function _pushProjectToGithub(e) { }); } +function _pullProjectFromGithub(e) { + var ghurl = getBoundGithubURL(); + if (!ghurl) return; + setWaitDialog(true); + getGithubService().pull(ghurl).then( (sess:GHSession) => { + setWaitDialog(false); + }); +} + function pushChangesToGithub(message:string) { - var ghurl = getGithubService().getBoundURL(current_project.mainPath); - console.log("Github URL: " + ghurl); - if (!ghurl) { - alert("This project is not bound to a GitHub project."); - return; - } + var ghurl = getBoundGithubURL(); + if (!ghurl) return; // build file list for push var files = []; for (var path in current_project.filedata) { var newpath = current_project.stripLocalPath(path); - files.push({path:newpath, data:current_project.filedata[path]}); + var data = current_project.filedata[path]; + if (newpath && data) { + files.push({path:newpath, data:data}); + } } // push files setWaitDialog(true); - getGithubService().commitPush(ghurl, message, files).then( (sess) => { + return getGithubService().commitPush(ghurl, message, files).then( (sess) => { setWaitDialog(false); + alert("Pushed files to " + ghurl); + return sess; + }).catch( (e) => { + setWaitDialog(false); + console.log(e); + alert("Could not push GitHub repository: " + e); }); } // TODO: remove? function loadSharedGist(gistkey : string) { - loadScript("lib/octokat.js", () => { var github = new exports['Octokat'](); var gist = this.github.gists(gistkey); gist.fetch().done( (val) => { @@ -486,7 +518,6 @@ function loadSharedGist(gistkey : string) { }).fail(function(err) { alert("Error loading share file: " + err.message); }); - }); } function _shareEmbedLink(e) { @@ -1244,6 +1275,7 @@ function setupDebugControls() { $("#item_github_import").click(_importProjectFromGithub); $("#item_github_publish").click(_publishProjectToGithub); $("#item_github_push").click(_pushProjectToGithub); + $("#item_github_pull").click(_pullProjectFromGithub); $("#item_share_file").click(_shareEmbedLink); $("#item_reset_file").click(_revertFile); $("#item_rename_file").click(_renameFile); diff --git a/src/windows.ts b/src/windows.ts index 01497b5f..328ffc66 100644 --- a/src/windows.ts +++ b/src/windows.ts @@ -115,17 +115,14 @@ export class ProjectWindows { } updateFile(fileid:string, data:FileData) { - // is there an editor? we should create one... - var wnd = this.create(fileid); + // is there an editor? if so, use it + var wnd = this.id2window[fileid]; if (wnd && wnd.setText && typeof data === 'string') { wnd.setText(data); + this.undofiles.push(fileid); } else { this.project.updateFile(fileid, data); - if (wnd) { - wnd.refresh(false); - } } - this.undofiles.push(fileid); } undoStep() { diff --git a/test/cli/testgithub.js b/test/cli/testgithub.js index 2f0ce328..0a67c66e 100644 --- a/test/cli/testgithub.js +++ b/test/cli/testgithub.js @@ -14,29 +14,67 @@ var Octokat = require('octokat'); var test_platform_id = "_TEST"; -function newGH(store) { +function newGH(store, platform_id) { // pzpinfo user - return new serv.GithubService(new Octokat({token:'ec64fdd81dedab8b7547388eabef09288e9243a9'}), store); + var project = new prj.CodeProject({}, platform_id||test_platform_id, null, store); + project.mainPath = 'local/main.asm'; + project.updateFileInStore(project.mainPath, '\torg $0 ; test\n'); + return new serv.GithubService(new Octokat({token:'ec64fdd81dedab8b7547388eabef09288e9243a9'}), store, project); } +const t0 = new Date().getTime(); + describe('Store', function() { - it('Should import from Github', function(done) { + it('Should import from Github (check README)', function(done) { var store = mstore.createNewPersistentStore(test_platform_id, function(store) { var gh = newGH(store); - gh.import('https://github.com/sehugg/genemedic/extra/garbage').then( (sess) => { - assert.equal(4, sess.paths.length); + gh.import('https://github.com/pzpinfo/testrepo1557322631070').then( (sess) => { + console.log(sess.paths); + assert.equal(2, sess.paths.length); // TODO: test for presence in local storage, make sure returns keys done(); }); }); }); + it('Should import from Github (no README)', function(done) { + var store = mstore.createNewPersistentStore(test_platform_id, function(store) { + var gh = newGH(store); + gh.import('https://github.com/pzpinfo/testrepo3').then( (sess) => { + console.log(sess.paths); + assert.equal(3, sess.paths.length); + // TODO: test for presence in local storage, make sure returns keys + done(); + }); + }); + }); + + it('Should import from Github (wrong platform)', function(done) { + var store = mstore.createNewPersistentStore('_FOO', function(store) { + var gh = newGH(store, '_FOO'); + gh.import('https://github.com/pzpinfo/testrepo1557326056720').catch( (e) => { + assert.ok(e.startsWith('Platform mismatch')); + done(); + }); + }); + }); + it('Should publish (fail) on Github', function(done) { var store = mstore.createNewPersistentStore(test_platform_id, function(store) { var gh = newGH(store); // should fail - gh.publish('testrepo4').catch( (e) => { + gh.publish('testrepo1').catch( (e) => { + done(); + }); + }); + }); + + it('Should publish new repository on Github', function(done) { + var store = mstore.createNewPersistentStore(test_platform_id, function(store) { + var gh = newGH(store); + // should fail + gh.publish('testrepo'+t0, "new description", "mit", false).then( (sess) => { done(); }); }); @@ -57,11 +95,11 @@ describe('Store', function() { it('Should bind paths to Github', function(done) { var store = mstore.createNewPersistentStore(test_platform_id, function(store) { var gh = newGH(store); - var sess = {prefix:'prefix', url:'_'}; + var sess = {prefix:'shared/foo/bar/', url:'_'}; gh.bind(sess, true); - assert.equal(gh.getBoundURL('prefix', '_')); + assert.equal(gh.getBoundURL('shared/foo/bar/'), '_'); gh.bind(sess, false); - assert.equal(gh.getBoundURL('prefix', null)); + assert.equal(gh.getBoundURL('shared/foo/bar/'), null); done(); }); }); diff --git a/test/cli/testplatforms.js b/test/cli/testplatforms.js index 989d731f..6f91bf2a 100644 --- a/test/cli/testplatforms.js +++ b/test/cli/testplatforms.js @@ -13,8 +13,6 @@ global.window = dom.window; global.document = dom.window.document; dom.window.Audio = null; global.Image = function() { } -global.btoa = require('btoa'); -global.atob = require('atob'); global['$'] = require("jquery/jquery-2.2.3.min.js"); global.includeInThisContext('src/cpu/z80fast.js'); includeInThisContext("javatari.js/release/javatari/javatari.js"); diff --git a/test/cli/workertestutils.js b/test/cli/workertestutils.js index ab678237..ea4a4259 100644 --- a/test/cli/workertestutils.js +++ b/test/cli/workertestutils.js @@ -8,6 +8,9 @@ var worker = {}; global.window = global; global.exports = {}; +global.btoa = require('btoa'); +global.atob = require('atob'); + global.includeInThisContext = function(path) { var code = fs.readFileSync(path); vm.runInThisContext(code, path);