github repository metadata, binary files, fixed bugs

This commit is contained in:
Steven Hugg 2019-05-09 08:44:47 -04:00
parent e73388b24e
commit 85f0650bfe
4 changed files with 152 additions and 97 deletions

View File

@ -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

View File

@ -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
View File

@ -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']);

View File

@ -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();
});
});