mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2025-02-16 17:30:27 +00:00
github repository metadata, binary files, fixed bugs
This commit is contained in:
parent
e73388b24e
commit
85f0650bfe
@ -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
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
119
src/ui.ts
119
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<PRESETS.length; i++)
|
||||
if (PRESETS[i].id == preset_id)
|
||||
index = i;
|
||||
index = (index + PRESETS.length) % PRESETS.length;
|
||||
if (index >= 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($("<option />").val('..').text('Leave Repository'));
|
||||
// repo: populate all files
|
||||
populateFiles(sel, "Repository Files", "", () => {
|
||||
sel.css('visibility','visible');
|
||||
@ -1056,9 +1060,10 @@ function updateDebugWindows() {
|
||||
|
||||
function setWaitDialog(b : boolean) {
|
||||
if (b) {
|
||||
$("#pleaseWaitProgressBar").hide();
|
||||
setWaitProgress(0);
|
||||
$("#pleaseWaitModal").modal('show');
|
||||
} else {
|
||||
setWaitProgress(1);
|
||||
$("#pleaseWaitModal").modal('hide');
|
||||
}
|
||||
}
|
||||
@ -1522,18 +1527,16 @@ function startPlatform() {
|
||||
// try to load last file (redirect)
|
||||
var lastid;
|
||||
if (hasLocalStorage) {
|
||||
lastid = localStorage.getItem("__lastid_"+platform_id) || localStorage.getItem("__lastid");
|
||||
localStorage.removeItem("__lastid");
|
||||
lastid = localStorage.getItem("__lastid_"+store_id);
|
||||
}
|
||||
qs['file'] = lastid || PRESETS[0].id;
|
||||
replaceURLState();
|
||||
}
|
||||
// legacy vcs stuff
|
||||
if (platform_id == 'vcs' && qs['file'].startsWith('examples/') && !qs['file'].endsWith('.a')) {
|
||||
qs['file'] += '.a';
|
||||
replaceURLState();
|
||||
}
|
||||
// start platform and load file
|
||||
replaceURLState();
|
||||
platform.start();
|
||||
loadBIOSFromProject();
|
||||
initProject();
|
||||
@ -1555,7 +1558,7 @@ export function loadScript(scriptfn, onload, onerror?) {
|
||||
export function setupSplits() {
|
||||
const splitName = 'workspace-split3-' + platform_id;
|
||||
var sizes = [0, 50, 50];
|
||||
if (platform_id.startsWith('vcs')) // TODO
|
||||
if (!platform_id.startsWith('vcs'))
|
||||
sizes = [12, 44, 44];
|
||||
var sizesStr = hasLocalStorage && localStorage.getItem(splitName);
|
||||
if (sizesStr) {
|
||||
@ -1610,6 +1613,21 @@ function loadImportedURL(url : string) {
|
||||
// start
|
||||
export function startUI(loadplatform : boolean) {
|
||||
installErrorHandler();
|
||||
// import from github?
|
||||
if (qs['githubURL']) {
|
||||
importProjectFromGithub(qs['githubURL']);
|
||||
return;
|
||||
}
|
||||
// lookup repository
|
||||
repo_id = qs['repo'];
|
||||
if (hasLocalStorage && repo_id) {
|
||||
var repo = getRepos()[repo_id];
|
||||
console.log(repo_id, repo);
|
||||
if (repo && !qs['file'])
|
||||
qs['file'] = repo.mainPath;
|
||||
if (repo && !qs['platform'])
|
||||
qs['platform'] = repo.platform_id;
|
||||
}
|
||||
// add default platform?
|
||||
platform_id = qs['platform'] || (hasLocalStorage && localStorage.getItem("__lastplatform"));
|
||||
if (!platform_id) {
|
||||
@ -1618,15 +1636,8 @@ export function startUI(loadplatform : boolean) {
|
||||
$("#item_platform_"+platform_id).addClass("dropdown-item-checked");
|
||||
setupSplits();
|
||||
// create store
|
||||
repo_id = qs['repo'];
|
||||
store_id = repo_id || getBasePlatform(platform_id);
|
||||
store = createNewPersistentStore(store_id, (store) => {
|
||||
// is this an importURL?
|
||||
// TODO: use repo
|
||||
if (qs['githubURL']) {
|
||||
getGithubService().import(qs['githubURL']);
|
||||
return;
|
||||
}
|
||||
// is this an importURL?
|
||||
if (qs['importURL']) {
|
||||
loadImportedURL(qs['importURL']);
|
||||
|
@ -15,6 +15,7 @@ var Octokat = require('octokat');
|
||||
var test_platform_id = "_TEST";
|
||||
|
||||
function newGH(store, platform_id) {
|
||||
localStorage.removeItem('__repos');
|
||||
// pzpinfo user
|
||||
var project = new prj.CodeProject({}, platform_id||test_platform_id, null, store);
|
||||
project.mainPath = 'local/main.asm';
|
||||
@ -27,24 +28,28 @@ const t0 = new Date().getTime();
|
||||
describe('Store', function() {
|
||||
|
||||
it('Should import from Github (check README)', function(done) {
|
||||
var store = mstore.createNewPersistentStore(test_platform_id, function(store) {
|
||||
var gh = newGH(store);
|
||||
gh.importAndPull('https://github.com/pzpinfo/testrepo1557322631070').then( (sess) => {
|
||||
var store = mstore.createNewPersistentStore('vcs', function(store) {
|
||||
var gh = newGH(store, 'vcs');
|
||||
gh.importAndPull('https://github.com/pzpinfo/test123123').then( (sess) => {
|
||||
console.log(sess.paths);
|
||||
assert.equal(2, sess.paths.length);
|
||||
// TODO: test for presence in local storage, make sure returns keys
|
||||
assert.deepEqual(serv.getRepos(), {"pzpinfo/test123123":{url: 'https://github.com/pzpinfo/test123123', platform_id: 'vcs', mainPath:'helloworld.bas'}});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Should import from Github (no README)', function(done) {
|
||||
var store = mstore.createNewPersistentStore(test_platform_id, function(store) {
|
||||
var gh = newGH(store);
|
||||
it('Should import from Github (binary files)', function(done) {
|
||||
var store = mstore.createNewPersistentStore('vcs', function(store) {
|
||||
var gh = newGH(store, 'vcs');
|
||||
gh.importAndPull('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
|
||||
assert.equal(4, sess.paths.length);
|
||||
var txt = localStorage.getItem('__vcs/text.txt');
|
||||
assert.equal(txt, '"hello world"');
|
||||
var bin = localStorage.getItem('__vcs/data.bin');
|
||||
console.log(bin);
|
||||
assert.equal(bin.length, 348+9); // encoded
|
||||
done();
|
||||
});
|
||||
});
|
||||
@ -73,8 +78,12 @@ describe('Store', function() {
|
||||
it('Should publish new repository on Github', function(done) {
|
||||
var store = mstore.createNewPersistentStore(test_platform_id, function(store) {
|
||||
var gh = newGH(store);
|
||||
var reponame = 'testrepo'+t0;
|
||||
// should fail
|
||||
gh.publish('testrepo'+t0, "new description", "mit", false).then( (sess) => {
|
||||
gh.publish(reponame, "new description", "mit", false).then( (sess) => {
|
||||
assert.ok(serv.getRepos()[sess.repopath]);
|
||||
return gh.deleteRepository(sess.url);
|
||||
}).then( () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
@ -83,8 +92,12 @@ describe('Store', function() {
|
||||
it('Should commit/push to Github', function(done) {
|
||||
var store = mstore.createNewPersistentStore(test_platform_id, function(store) {
|
||||
var gh = newGH(store);
|
||||
var binfile = new Uint8Array(256);
|
||||
for (var i=0; i<256; i++)
|
||||
binfile[i] = i;
|
||||
var files = [
|
||||
{path:'text.txt', data:'hello world'}
|
||||
{path:'text.txt', data:'hello world'},
|
||||
{path:'data.bin', data:binfile}
|
||||
];
|
||||
gh.commitPush('https://github.com/pzpinfo/testrepo3', 'test commit', files).then( (sess) => {
|
||||
done();
|
||||
@ -95,12 +108,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);
|
||||
localStorage.removeItem('__repos');
|
||||
var sess = {repopath:'foo/bar', url:'_'};
|
||||
var sess = {repopath:'foo/bar', url:'_',platform_id:'vcs',mainPath:'test.c'};
|
||||
gh.bind(sess, true);
|
||||
assert.deepEqual(gh.getRepos(), {'foo/bar':'_'});
|
||||
assert.deepEqual(serv.getRepos(), {'foo/bar':{url:'_',platform_id:'vcs',mainPath:'test.c'}});
|
||||
gh.bind(sess, false);
|
||||
assert.deepEqual(gh.getRepos(), {});
|
||||
assert.deepEqual(serv.getRepos(), {});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user