started on GithubService

This commit is contained in:
Steven Hugg 2019-05-07 15:37:37 -04:00
parent 5ded34e668
commit 93c0e8f50b
10 changed files with 476 additions and 120 deletions

View File

@ -123,6 +123,7 @@ TODO:
- quantify verilog "graph iterations"
- toolbar overlaps scope
- CPU debugging
- use $readmem for inline asm programs?
- single-stepping vector games makes screen fade
- break on stack overflow, bad op, bad access, etc
- PPU/TIA register write visualization
@ -132,6 +133,11 @@ TODO:
- allow "include graphics.asm" instead of "include project/graphics.asm"
- chrome looks blurry on vcs
- 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
WEB WORKER FORMAT

View File

@ -20,8 +20,24 @@ body {
}
</style>
<link rel="stylesheet" href="css/ui.css">
<!-- google analytics -->
<script>
window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
if (window.location.host.endsWith('8bitworkshop.com')) {
ga('create', 'UA-54497476-9', 'auto');
ga('set', 'anonymizeIp', true);
ga('send', 'pageview');
}
</script>
<script async src='https://www.google-analytics.com/analytics.js'></script>
</head>
<!-- firebase libs -->
<script defer src="https://www.gstatic.com/firebasejs/5.11.1/firebase-app.js"></script>
<script defer src="https://www.gstatic.com/firebasejs/5.11.1/firebase-auth.js"></script>
<script defer src="config.js"></script>
<body>
<!-- for file upload -->
@ -54,7 +70,7 @@ body {
<li><a class="dropdown-item" href="/login.html">Login to GitHub...</a></li>
<hr>
<li><a class="dropdown-item" href="#" id="item_github_import">Import Project from GitHub...</a></li>
<li><a class="dropdown-item" href="#" id="item_github_connect">Publish Project on GitHub...</a></li>
<li><a class="dropdown-item" href="#" id="item_github_publish">Publish Project on GitHub...</a></li>
<hr>
<li><a class="dropdown-item" href="#" id="item_github_push">Push Changes to Repository...</a></li>
<hr>
@ -234,6 +250,10 @@ body {
<div class="modal-content">
<div class="modal-body">
Please wait...
<div class="progress">
<div class="progress-bar progress-bar-striped active" role="progressbar" id="pleaseWaitProgressBar">
</div>
</div>
</div>
</div>
</div>
@ -293,17 +313,29 @@ body {
</div>
</div>
</div>
<div id="connectGithubModal" class="modal fade">
<div id="publishGithubModal" class="modal fade">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Publish Project on GitHub</h3>
</div>
<div class="modal-body">
<p>This will migrate your existing files to a GitHub repository.</p>
<p><input id="connectGithubURL" size="40" placeholder="Enter a project name"></input></p>
<p><button type="button" class="btn btn-primary" id="connectGithubButton">Upload Project</button></p>
<p>If the destination repository does not exist, it will be created.</p>
<p>This will migrate your existing files to a new GitHub repository.</p>
<p><input id="githubRepoName" size="50" placeholder="Enter a project name"></input></p>
<p><input id="githubRepoDesc" size="50" placeholder="Enter a project description"></input></p>
<p>Your repository will be <select id="githubRepoPrivate">
<option value="public">Public</option>
<option value="private">Private</option>
</select></p>
<p>License: <select id="githubRepoLicense">
<option value="">Will decide later</option>
<option value="cc0-1.0">CC Zero (public domain)</option>
<option value="mit">MIT (public domain, must preserve notices)</option>
<option value="cc-by-4.0">CC BY (must attribute)</option>
<option value="cc-by-sa-4.0">CC BY-SA (must attribute, use same license)</option>
<option value="gpl-3.0">GPL v3 (must publish source)</option>
</select></p>
<p><button type="button" class="btn btn-primary" id="publishGithubButton">Upload Project</button></p>
<p>Your existing file will be moved to a new folder in the IDE.</p>
</div>
<div class="modal-footer">
@ -312,6 +344,22 @@ body {
</div>
</div>
</div>
<div id="pushGithubModal" class="modal fade">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Push Project Changes to GitHub</h3>
</div>
<div class="modal-body">
<p><input id="githubCommitMsg" size="50" placeholder="Enter a commit message"></input></p>
<p><button type="button" class="btn btn-primary" id="pushGithubButton">Push Changes</button></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<script src="jquery/jquery-3.4.1.min.js"></script>
@ -381,6 +429,7 @@ function require(modname) {
<script src="gen/recorder.js"></script>
<script src="gen/waveform.js"></script>
<script src="gen/pixed/pixeleditor.js"></script>
<script src="gen/services.js"></script>
<script src="gen/ui.js"></script>
<!-- <script src="src/audio/votrax.js"></script> -->
<!-- <script src="local/lzg.js"></script> -->
@ -405,10 +454,5 @@ $( ".dropdown-submenu" ).click(function(event) {
*/
</script>
<!-- firebase libs -->
<script defer src="https://www.gstatic.com/firebasejs/5.11.1/firebase-app.js"></script>
<script defer src="https://www.gstatic.com/firebasejs/5.11.1/firebase-auth.js"></script>
<script defer src="config.js"></script>
</body>
</html>

View File

@ -14,6 +14,7 @@
"jsdom": "^12.2.0",
"lzg": "^1.0.x",
"mocha": "^5.2.x",
"octokat": "^0.10.0",
"pngjs": "^3.3.3",
"typescript": "^3.3.3",
"wavedrom-cli": "^0.5.x"

View File

@ -330,9 +330,11 @@ export class CodeProject {
}
stripLocalPath(path : string) : string {
var folder = getFolderForPath(this.mainPath);
if (folder != '' && path.startsWith(folder)) {
path = path.substring(folder.length+1);
if (this.mainPath) {
var folder = getFolderForPath(this.mainPath);
if (folder != '' && path.startsWith(folder)) {
path = path.substring(folder.length+1);
}
}
return path;
}

196
src/services.ts Normal file
View File

@ -0,0 +1,196 @@
import { getFolderForPath, isProbablyBinary, stringToByteArray } from "./util";
import { FileData } from "./workertypes";
import { CodeProject } from "./project";
// in index.html
declare var exports;
declare var firebase;
interface GHSession {
url : string;
user : string;
reponame : string;
repo : any;
prefix : string;
paths? : string[];
}
export class GithubService {
github;
store;
project : CodeProject;
branch : string = "master";
constructor(github, store, project : CodeProject) {
this.github = github;
this.store = store;
this.project = project;
}
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;
}
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;
return {user:toks[3], repo:toks[4]};
}
getPrefix(user, reponame) : string {
return 'shared/' + user + '-' + reponame + '/';
}
getGithubRepo(ghurl:string) : Promise<GHSession> {
return new Promise( (yes,no) => {
var urlparse = this.parseGithubURL(ghurl);
if (!urlparse) {
no("Please enter a valid GitHub URL.");
}
var sess = {
url: ghurl,
user: urlparse.user,
reponame: urlparse.repo,
prefix: this.getPrefix(urlparse.user, urlparse.repo),
repo: this.github.repos(urlparse.user, urlparse.repo)
};
yes(sess);
});
}
// 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);
else
localStorage.removeItem(key);
}
getBoundURL(path : string) : string {
var p = getFolderForPath(path);
var key = '__github_url_' + p + '/';
console.log(key);
return localStorage.getItem(key) as string; // TODO
}
import(ghurl:string) : Promise<GHSession> {
var sess : GHSession;
return this.getGithubRepo(ghurl).then( (session) => {
sess = session;
return sess.repo.commits(this.branch).fetch();
})
.then( (sha) => {
return sess.repo.git.trees(sha.sha).fetch();
})
.then( (tree) => {
let blobreads = [];
sess.paths = [];
tree.tree.forEach( (item) => {
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 path = sess.prefix + item.path;
var size = item.size;
var isBinary = isProbablyBinary(blob);
var data = isBinary ? stringToByteArray(blob) : blob; //byteArrayToUTF8(blob);
return this.store.setItem(path, data);
});
blobreads.push(read);
} else {
console.log("ignoring " + item.path);
}
});
return Promise.all(blobreads);
})
.then( (blobs) => {
this.bind(sess, true);
return sess;
});
}
publish(reponame:string, desc:string, license:string, isprivate:boolean) : Promise<GHSession> {
return this.github.user.repos.create({
name: reponame,
description: desc,
private: isprivate,
auto_init: true,
license_template: license
})
.then( (repo) => {
let sess = {
url: repo.htmlUrl,
user: repo.owner.login,
reponame: reponame,
repo: repo,
prefix : ''
};
this.bind(sess, true);
return sess;
});
}
commitPush( ghurl:string, message:string, files:{path:string,data:FileData}[] ) : Promise<GHSession> {
var sess : GHSession;
var repo;
var head;
var tree;
return this.getGithubRepo(ghurl).then( (session) => {
sess = session;
repo = sess.repo;
return repo.git.refs.heads(this.branch).fetch();
}).then( (_head) => {
head = _head;
return repo.git.trees(head.object.sha).fetch();
}).then( (_tree) => {
tree = _tree;
return Promise.all(files.map( (file) => {
return repo.git.blobs.create({
content: file.data,
encoding: 'utf-8'
});
}));
}).then( (blobs) => {
return repo.git.trees.create({
tree: files.map( (file, index) => {
return {
path: file.path,
mode: '100644',
type: 'blob',
sha: blobs[index]['sha']
};
}),
base_tree: tree.sha
});
}).then( (tree) => {
return repo.git.commits.create({
message: message,
tree: tree.sha,
parents: [
head.object.sha
]
});
}).then( (commit) => {
return repo.git.refs.heads(this.branch).update({
sha: commit.sha
});
}).then( (update) => {
return sess;
});
}
}

View File

@ -43,6 +43,7 @@ function copyFromVer2xStorageFormat(platformid:string, newstore, callback:(store
// no files to convert?
if (keys.length == 0) {
localStorage.setItem(alreadyMigratedKey, 'true');
callback(newstore);
return;
}
// convert function

173
src/ui.ts
View File

@ -12,11 +12,12 @@ import { PLATFORMS, EmuHalt, Toolbar } from "./emu";
import * as Views from "./views";
import { createNewPersistentStore } from "./store";
import { getFilenameForPath, getFilenamePrefix, highlightDifferences, invertMap, byteArrayToString, compressLZG,
byteArrayToUTF8, isProbablyBinary, getWithBinary, stringToByteArray } from "./util";
byteArrayToUTF8, isProbablyBinary, getWithBinary } from "./util";
import { StateRecorderImpl } from "./recorder";
import { GithubService } from "./services";
// external libs (TODO)
declare var Tour, GIF, saveAs, JSZip, Mousetrap, Split, GitHub;
declare var Tour, GIF, saveAs, JSZip, Mousetrap, Split, firebase;
// in index.html
declare var exports;
@ -366,84 +367,109 @@ function getCurrentEditorFilename() : string {
// GITHUB stuff (TODO: move)
var githubService : GithubService;
function getCookie(name) {
var nameEQ = name + "=";
var ca = document.cookie.split(';');
for(var i=0;i < ca.length;i++) {
var c = ca[i];
while (c.charAt(0)==' ') c = c.substring(1,c.length);
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
}
return null;
}
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);
});
}
return githubService;
}
function _importProjectFromGithub(e) {
var githuburl_ta = $("#importGithubURL");
getGithubService(); // load it
var modal = $("#importGithubModal");
var btn = $("#importGithubButton");
modal.modal('show');
btn.off('click').on('click', () => {
importFromGithub(githuburl_ta.val());
});
}
function _connectProjectToGithub(e) {
var githuburl_ta = $("#connectGithubURL");
var modal = $("#connectGithubModal");
var btn = $("#connectGithubButton");
modal.modal('show');
btn.off('click').on('click', () => {
connectToGithub(githuburl_ta.val());
});
}
function parseGithubURL(ghurl) {
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;
return {user:toks[3], repo:toks[4]};
}
function getGithubRepo(ghurl, callback) {
var urlparse = parseGithubURL(ghurl);
if (!urlparse) {
alert("Please enter a valid GitHub URL.");
return;
}
loadScript("lib/octokat.js", () => {
var github = new exports['Octokat']();
var prefix = 'shared/' + urlparse.user + '-' + urlparse.repo + '/';
var repo = github.repos(urlparse.user, urlparse.repo);
callback(github, repo, prefix);
});
}
function importFromGithub(ghurl) {
getGithubRepo(ghurl, (github, repo, prefix) => {
repo.commits('master').fetch().then( (sha) => {
repo.git.trees(sha.sha).fetch().then( (tree) => {
tree.tree.forEach( (item) => {
console.log(item);
if (item.type == 'blob') {
repo.git.blobs(item.sha).readBinary().then( (blob) => {
var path = prefix + item.path;
var size = item.size;
var isBinary = isProbablyBinary(blob);
var data = isBinary ? stringToByteArray(blob) : blob; //byteArrayToUTF8(blob);
// TODO projectWindows.updateFile(path, data);
store.setItem(path, data);
// TODO; wait for set?
console.log(path, size, isBinary, typeof blob);
// TODO: redirect to main file?
});
}
});
});
var githuburl = $("#importGithubURL").val()+"";
getGithubService().import(githuburl).then( (sess) => {
// TODO : redirect to main file
modal.modal('hide');
}).catch( (e) => {
modal.modal('hide');
alert("Could not import " + githuburl + ": " + e);
});
});
}
function connectToGithub(ghurl) {
getGithubRepo(ghurl, (github, repo, prefix) => {
// TODO
function _publishProjectToGithub(e) {
getGithubService(); // load it
var modal = $("#publishGithubModal");
var btn = $("#publishGithubButton");
modal.modal('show');
btn.off('click').on('click', () => {
var name = $("#githubRepoName").val()+"";
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
console.log(sess);
alert("Created repository at " + sess.url);
pushChangesToGithub('initial import from 8bitworkshop.com');
}).catch( (e) => {
modal.modal('hide');
alert("Could not create GitHub repository: " + e);
});
});
}
function _pushProjectToGithub(e) {
getGithubService(); // load it
var modal = $("#pushGithubModal");
var btn = $("#pushGithubButton");
modal.modal('show');
btn.off('click').on('click', () => {
var commitMsg = $("#githubCommitMsg").val()+"";
modal.modal('hide');
pushChangesToGithub(commitMsg);
});
}
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;
}
// 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]});
}
// push files
setWaitDialog(true);
getGithubService().commitPush(ghurl, message, files).then( (sess) => {
setWaitDialog(false);
});
}
// TODO: remove?
function loadSharedGist(gistkey : string) {
loadScript("lib/octokat.js", () => {
var github = new exports['Octokat']();
var gist = github.gists(gistkey);
var gist = this.github.gists(gistkey);
gist.fetch().done( (val) => {
var filename;
var newid;
@ -983,12 +1009,17 @@ function updateDebugWindows() {
function setWaitDialog(b : boolean) {
if (b) {
$("#pleaseWaitProgressBar").hide();
$("#pleaseWaitModal").modal('show');
} else {
$("#pleaseWaitModal").modal('hide');
}
}
function setWaitProgress(prog : number) {
$("#pleaseWaitProgressBar").css('width', (prog*100)+'%').show();
}
var recordingVideo = false;
function _recordVideo() {
if (recordingVideo) return;
@ -1013,7 +1044,10 @@ function _recordVideo() {
rotate: rotate
});
var img = $('#videoPreviewImage');
gif.on('finished', function(blob) {
gif.on('progress', (prog) => {
setWaitProgress(prog);
});
gif.on('finished', (blob) => {
img.attr('src', URL.createObjectURL(blob));
setWaitDialog(false);
_resume();
@ -1198,7 +1232,8 @@ function setupDebugControls() {
$("#item_new_file").click(_createNewFile);
$("#item_upload_file").click(_uploadNewFile);
$("#item_github_import").click(_importProjectFromGithub);
$("#item_github_connect").click(_connectProjectToGithub);
$("#item_github_publish").click(_publishProjectToGithub);
$("#item_github_push").click(_pushProjectToGithub);
$("#item_share_file").click(_shareEmbedLink);
$("#item_reset_file").click(_revertFile);
$("#item_rename_file").click(_renameFile);
@ -1455,7 +1490,7 @@ function startPlatform() {
return true;
}
function loadScript(scriptfn, onload, onerror?) {
export function loadScript(scriptfn, onload, onerror?) {
var script = document.createElement('script');
script.onload = onload;
script.onerror = onerror;
@ -1539,7 +1574,7 @@ export function startUI(loadplatform : boolean) {
store = createNewPersistentStore(platform_id, (store) => {
// is this an importURL?
if (qs['githubURL']) {
importFromGithub(qs['githubURL']);
getGithubService().import(qs['githubURL']);
return;
}
// is this an importURL?

69
test/cli/testgithub.js Normal file
View File

@ -0,0 +1,69 @@
"use strict";
var vm = require('vm');
var fs = require('fs');
var assert = require('assert');
var wtu = require('./workertestutils.js'); // loads localStorage
global.localforage = require("localForage/dist/localforage.js");
var util = require("gen/util.js");
var mstore = require("gen/store.js");
var prj = require("gen/project.js");
var serv = require("gen/services.js");
var Octokat = require('octokat');
var test_platform_id = "_TEST";
function newGH(store) {
// pzpinfo user
return new serv.GithubService(new Octokat({token:'ec64fdd81dedab8b7547388eabef09288e9243a9'}), store);
}
describe('Store', function() {
it('Should import from Github', 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);
// TODO: test for presence in local storage, make sure returns keys
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) => {
done();
});
});
});
it('Should commit/push to Github', function(done) {
var store = mstore.createNewPersistentStore(test_platform_id, function(store) {
var gh = newGH(store);
var files = [
{path:'text.txt', data:'hello world'}
];
gh.commitPush('https://github.com/pzpinfo/testrepo3', 'test commit', files).then( (sess) => {
done();
});
});
});
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:'_'};
gh.bind(sess, true);
assert.equal(gh.getBoundURL('prefix', '_'));
gh.bind(sess, false);
assert.equal(gh.getBoundURL('prefix', null));
done();
});
});
});

View File

@ -4,42 +4,7 @@ var vm = require('vm');
var fs = require('fs');
var assert = require('assert');
var localItems = {};
var localMods = 0;
global.localStorage = {
clear: function() {
localItems = {};
localMods = 0;
this.length = 0;
},
getItem: function(k) {
console.log('get',k);
return localItems[k];
},
setItem: function(k,v) {
console.log('set',k,v);
if (!localItems[k]) this.length++;
localItems[k] = v;
localMods++;
},
removeItem: function(k) {
if (localItems[k]) {
this.length--;
delete localItems[k];
localMods++;
}
},
length: 0,
key: function(i) {
var keys = [];
for (var k in localItems)
keys.push(k);
console.log(i,keys[i]);
return keys[i];
}
};
var wtu = require('./workertestutils.js'); // loads localStorage
global.localforage = require("localForage/dist/localforage.js");
var util = require("gen/util.js");
var mstore = require("gen/store.js");
@ -83,7 +48,7 @@ describe('Store', function() {
project.loadFiles(['local/test','test'], function(err, result) {
assert.equal(null, err);
assert.deepEqual(["presets/_TEST/test"], remote);
assert.deepEqual([ { path: 'local/test', filename: 'test', data: 'a', link:true } ], result);
assert.deepEqual([ { path: 'local/test', filename: 'local/test', data: 'a', link:true } ], result);
done();
});
});

View File

@ -69,3 +69,40 @@ includeInThisContext("gen/worker/workermain.js");
global.ab2str = function(buf) {
return String.fromCharCode.apply(null, new Uint16Array(buf));
}
global.localItems = {};
global.localMods = 0;
global.localStorage = {
clear: function() {
localItems = {};
localMods = 0;
this.length = 0;
},
getItem: function(k) {
console.log('get',k);
return localItems[k];
},
setItem: function(k,v) {
console.log('set',k,v.length<100?v:v.length);
if (!localItems[k]) this.length++;
localItems[k] = v;
localMods++;
},
removeItem: function(k) {
if (localItems[k]) {
this.length--;
delete localItems[k];
localMods++;
}
},
length: 0,
key: function(i) {
var keys = [];
for (var k in localItems)
keys.push(k);
console.log(i,keys[i]);
return keys[i];
}
};