1
0
mirror of https://github.com/sehugg/8bitworkshop.git synced 2024-06-12 03:29:31 +00:00

github: store_id and repo=

This commit is contained in:
Steven Hugg 2019-05-08 19:15:26 -04:00
parent 19d145bbd5
commit cd0d1416dc
8 changed files with 147 additions and 143 deletions

View File

@ -143,6 +143,8 @@ TODO:
- what if files already open in editor - what if files already open in editor
- import twice? - import twice?
- un-bind from repo? - un-bind from repo?
- repo/platform dichotomy
- navigate to/from repo
WEB WORKER FORMAT WEB WORKER FORMAT

View File

@ -67,8 +67,6 @@ if (window.location.host.endsWith('8bitworkshop.com')) {
<li class="dropdown dropdown-submenu"> <li class="dropdown dropdown-submenu">
<a tabindex="-1" href="#">Sync</a> <a tabindex="-1" href="#">Sync</a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<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_import">Import Project from GitHub...</a></li>
<li><a class="dropdown-item" href="#" id="item_github_publish">Publish Project on GitHub...</a></li> <li><a class="dropdown-item" href="#" id="item_github_publish">Publish Project on GitHub...</a></li>
<hr> <hr>

View File

@ -339,13 +339,13 @@ export class CodeProject {
return path; return path;
} }
migrateToNewFolder(newprefix : string) { migrateToNewFolder(newprefix : string, newstore?) {
// TODO: must end with / // TODO: must end with /
var newPath = newprefix + this.stripLocalPath(this.mainPath); var newPath = newprefix + this.stripLocalPath(this.mainPath);
console.log(this.mainPath + "->" + newPath); console.log(this.mainPath + "->" + newPath);
var data = this.filedata[this.mainPath]; var data = this.filedata[this.mainPath];
console.log(data.length + " bytes"); console.log(data.length + " bytes");
return this.store.setItem(newPath, data).then(() => { return (newstore || this.store).setItem(newPath, data).then(() => {
//return this.store.removeItem(this.mainPath); //return this.store.removeItem(this.mainPath);
console.log("moved " + this.mainPath + " to " + newPath); console.log("moved " + this.mainPath + " to " + newPath);
this.filedata[newPath] = this.filedata[this.mainPath]; //TODO? this.filedata[newPath] = this.filedata[this.mainPath]; //TODO?

View File

@ -8,13 +8,14 @@ declare var exports;
declare var firebase; declare var firebase;
export interface GHSession { export interface GHSession {
url : string; url : string; // github url
user : string; user : string; // user name
reponame : string; reponame : string; // repo name
repo : any; repopath : string; // "user/repo"
prefix : string; prefix : string; // file prefix, "local/" or ""
repo : any; // [repo object]
mainPath?: string; // main file path
paths? : 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"; 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";
@ -75,13 +76,9 @@ export class GithubService {
if (toks.length < 5) return null; if (toks.length < 5) return null;
if (toks[0] != 'https:') return null; if (toks[0] != 'https:') return null;
if (toks[2] != 'github.com') return null; if (toks[2] != 'github.com') return null;
return {user:toks[3], repo:toks[4]}; return {user:toks[3], repo:toks[4], repopath:toks[3]+'/'+toks[4]};
} }
getPrefix(user, reponame) : string {
return 'shared/' + user + '/' + reponame + '/';
}
getGithubSession(ghurl:string) : Promise<GHSession> { getGithubSession(ghurl:string) : Promise<GHSession> {
return new Promise( (yes,no) => { return new Promise( (yes,no) => {
var urlparse = this.parseGithubURL(ghurl); var urlparse = this.parseGithubURL(ghurl);
@ -92,28 +89,26 @@ export class GithubService {
url: ghurl, url: ghurl,
user: urlparse.user, user: urlparse.user,
reponame: urlparse.repo, reponame: urlparse.repo,
prefix: this.getPrefix(urlparse.user, 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)
}; };
yes(sess); yes(sess);
}); });
} }
// bind a folder path to the Github URL in local storage getRepos() {
bind(sess : GHSession, dobind : boolean) { return JSON.parse(localStorage.getItem('__repos') || '{}');
var key = '__github_url_' + sess.prefix;
console.log('bind', key, dobind);
if (dobind)
localStorage.setItem(key, sess.url);
else
localStorage.removeItem(key);
} }
getBoundURL(path : string) : string { bind(sess:GHSession, dobind:boolean) {
var p = getFolderForPath(path); var repos = this.getRepos();
var key = '__github_url_' + p + '/'; if (dobind) {
console.log("getBoundURL", key); repos[sess.repopath] = sess.url;
return localStorage.getItem(key) as string; // TODO } else {
delete repos[sess.repopath];
}
localStorage.setItem('__repos', JSON.stringify(repos));
} }
import(ghurl:string) : Promise<GHSession> { import(ghurl:string) : Promise<GHSession> {
@ -147,7 +142,7 @@ export class GithubService {
}); });
} }
pull(ghurl:string) : Promise<GHSession> { pull(ghurl:string, deststore?) : Promise<GHSession> {
var sess : GHSession; var sess : GHSession;
return this.getGithubSession(ghurl).then( (session) => { return this.getGithubSession(ghurl).then( (session) => {
sess = session; sess = session;
@ -168,7 +163,7 @@ export class GithubService {
var size = item.size; var size = item.size;
var isBinary = isProbablyBinary(blob); var isBinary = isProbablyBinary(blob);
var data = isBinary ? stringToByteArray(blob) : blob; //byteArrayToUTF8(blob); var data = isBinary ? stringToByteArray(blob) : blob; //byteArrayToUTF8(blob);
return this.store.setItem(path, data); return (deststore || this.store).setItem(path, data);
}); });
blobreads.push(read); blobreads.push(read);
} else { } else {

View File

@ -31,14 +31,14 @@ var Ver2xFileStore = function(storage, prefix:string) {
} }
// copy localStorage to new driver // copy localStorage to new driver
function copyFromVer2xStorageFormat(platformid:string, newstore, callback:(store)=>void) { function copyFromVer2xStorageFormat(storeid:string, newstore, callback:(store)=>void) {
var alreadyMigratedKey = "__migrated_" + platformid; var alreadyMigratedKey = "__migrated_" + storeid;
//localStorage.removeItem(alreadyMigratedKey); //localStorage.removeItem(alreadyMigratedKey);
if (localStorage.getItem(alreadyMigratedKey)) { if (localStorage.getItem(alreadyMigratedKey)) {
callback(newstore); callback(newstore);
return; return;
} }
var oldstore = new Ver2xFileStore(localStorage, platformid + '/'); var oldstore = new Ver2xFileStore(localStorage, storeid + '/');
var keys = oldstore.getFiles(''); var keys = oldstore.getFiles('');
// no files to convert? // no files to convert?
if (keys.length == 0) { if (keys.length == 0) {
@ -73,11 +73,11 @@ function copyFromVer2xStorageFormat(platformid:string, newstore, callback:(store
migrateNext(); // start the conversion migrateNext(); // start the conversion
} }
export function createNewPersistentStore(platformid:string, callback:(store)=>void) { export function createNewPersistentStore(storeid:string, callback:(store)=>void) {
var store = localforage.createInstance({ var store = localforage.createInstance({
name: "__" + platformid, name: "__" + storeid,
version: 2.0 version: 2.0
}); });
copyFromVer2xStorageFormat(platformid, store, callback); copyFromVer2xStorageFormat(storeid, store, callback);
return store; return store;
} }

196
src/ui.ts
View File

@ -12,7 +12,7 @@ import { PLATFORMS, EmuHalt, Toolbar } from "./emu";
import * as Views from "./views"; import * as Views from "./views";
import { createNewPersistentStore } from "./store"; import { createNewPersistentStore } from "./store";
import { getFilenameForPath, getFilenamePrefix, highlightDifferences, invertMap, byteArrayToString, compressLZG, import { getFilenameForPath, getFilenamePrefix, highlightDifferences, invertMap, byteArrayToString, compressLZG,
byteArrayToUTF8, isProbablyBinary, getWithBinary } from "./util"; byteArrayToUTF8, isProbablyBinary, getWithBinary, getBasePlatform } from "./util";
import { StateRecorderImpl } from "./recorder"; import { StateRecorderImpl } from "./recorder";
import { GHSession, GithubService } from "./services"; import { GHSession, GithubService } from "./services";
@ -24,10 +24,12 @@ declare var exports;
// make sure VCS doesn't start // make sure VCS doesn't start
if (window['Javatari']) window['Javatari'].AUTO_START = false; if (window['Javatari']) window['Javatari'].AUTO_START = false;
var PRESETS : Preset[]; // presets array var PRESETS : Preset[]; // presets array
export var platform_id : string; // platform ID string export var platform_id : string; // platform ID string (platform)
export var platform : Platform; // platform object export var store_id : string; // store ID string (repo || platform)
export var repo_id : string; // repository ID (repo)
export var platform : Platform; // emulator object
var toolbar = $("#controls_top"); var toolbar = $("#controls_top");
@ -258,16 +260,17 @@ function loadProject(preset_id:string) {
}); });
} }
function reloadPresetNamed(id:string) { function reloadProject(id:string, repo?:string) {
qs['platform'] = platform_id; qs['platform'] = platform_id;
qs['file'] = id; qs['file'] = id;
if (repo) qs['repo'] = repo;
gotoNewLocation(); gotoNewLocation();
} }
function getSkeletonFile(fileid:string, callback) { function getSkeletonFile(fileid:string, callback) {
var ext = platform.getToolForFilename(fileid); var ext = platform.getToolForFilename(fileid);
// TODO: .mame // TODO: .mame
$.get( "presets/"+platform_id+"/skeleton."+ext, function( text ) { $.get( "presets/"+getBasePlatform(platform_id)+"/skeleton."+ext, function( text ) {
callback(null, text); callback(null, text);
}, 'text') }, 'text')
.fail(() => { .fail(() => {
@ -299,7 +302,7 @@ function _createNewFile(e) {
if (err) if (err)
alert(err+""); alert(err+"");
if (result != null) if (result != null)
reloadPresetNamed("local/" + filename); reloadProject("local/" + filename);
}); });
} }
}); });
@ -392,11 +395,43 @@ function getGithubService() {
} }
function getBoundGithubURL() : string { function getBoundGithubURL() : string {
console.log("main path: " + current_project.mainPath); var toks = (repo_id||'').split('/');
var ghurl = getGithubService().getBoundURL(current_project.mainPath); if (toks.length != 2) {
console.log("Github URL: " + ghurl); alert("This project is not bound to a GitHub project.");
if (!ghurl) alert("This project (" + current_project.mainPath + ") is not bound to a GitHub project.") return null;
return ghurl; }
return 'https://github.com/' + toks[0] + '/' + toks[1];
}
function importProjectFromGithub(githuburl:string, modal?) {
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);
});
});
} }
function _importProjectFromGithub(e) { function _importProjectFromGithub(e) {
@ -405,31 +440,12 @@ function _importProjectFromGithub(e) {
modal.modal('show'); modal.modal('show');
btn.off('click').on('click', () => { btn.off('click').on('click', () => {
var githuburl = $("#importGithubURL").val()+""; var githuburl = $("#importGithubURL").val()+"";
var sess; importProjectFromGithub(githuburl, modal);
getGithubService().import(githuburl).then( (sess1:GHSession) => {
sess = sess1;
return getGithubService().pull(githuburl);
}).then( (sess2:GHSession) => {
// TODO: only first sessino has mainPath
if (sess.mainPath) {
reloadPresetNamed(sess.prefix + sess.mainPath);
} else {
updateSelector();
alert("Files imported, but no main file was found so you'll have to select this project in the pulldown.");
}
// 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) { function _publishProjectToGithub(e) {
var ghurl = getGithubService().getBoundURL(current_project.mainPath); if (repo_id) {
if (ghurl) {
alert("This project (" + current_project.mainPath + ") is already bound to a Github repository. Choose 'Push Changes' to update."); alert("This project (" + current_project.mainPath + ") is already bound to a Github repository. Choose 'Push Changes' to update.");
return; return;
} }
@ -446,14 +462,11 @@ function _publishProjectToGithub(e) {
setWaitDialog(true); setWaitDialog(true);
getGithubService().login().then( () => { getGithubService().login().then( () => {
return getGithubService().publish(name, desc, license, priv); return getGithubService().publish(name, desc, license, priv);
}).then( (_sess) => { }).then( (sess) => {
sess = _sess; repo_id = qs['repo'] = sess.repopath;
console.log(sess);
return current_project.migrateToNewFolder(sess.prefix);
}).then( () => {
return pushChangesToGithub('initial import from 8bitworkshop.com'); return pushChangesToGithub('initial import from 8bitworkshop.com');
}).then( () => { }).then( () => {
reloadPresetNamed(current_project.mainPath); reloadProject(current_project.mainPath);
}).catch( (e) => { }).catch( (e) => {
setWaitDialog(false); setWaitDialog(false);
console.log(e); console.log(e);
@ -511,28 +524,6 @@ function pushChangesToGithub(message:string) {
}); });
} }
// TODO: remove?
function loadSharedGist(gistkey : string) {
var github = new exports['Octokat']();
var gist = this.github.gists(gistkey);
gist.fetch().done( (val) => {
var filename;
var newid;
console.log("Fetched " + gistkey, val);
store = createNewPersistentStore(platform_id, (store) => {
for (filename in val.files) {
store.setItem('shared/'+filename, val.files[filename].content);
if (!newid) newid = 'shared/'+filename;
}
// TODO: wait for set?
delete qs['gistkey'];
reloadPresetNamed(newid);
});
}).fail(function(err) {
alert("Error loading share file: " + err.message);
});
}
function _shareEmbedLink(e) { function _shareEmbedLink(e) {
if (current_output == null) { // TODO if (current_output == null) { // TODO
alert("Please fix errors before sharing."); alert("Please fix errors before sharing.");
@ -619,7 +610,7 @@ function _downloadCassetteFile(e) {
function fixFilename(fn : string) : string { function fixFilename(fn : string) : string {
if (platform_id.startsWith('vcs') && fn.indexOf('.') <= 0) if (platform_id.startsWith('vcs') && fn.indexOf('.') <= 0)
fn += ".a"; // legacy stuff fn += ".a"; // TODO: legacy stuff
return fn; return fn;
} }
@ -628,7 +619,7 @@ function _revertFile(e) {
if (wnd && wnd.setText) { if (wnd && wnd.setText) {
var fn = fixFilename(projectWindows.getActiveID()); var fn = fixFilename(projectWindows.getActiveID());
// TODO: .mame // TODO: .mame
$.get( "presets/"+getFilenamePrefix(platform_id)+"/"+fn, function(text) { $.get( "presets/"+getBasePlatform(platform_id)+"/"+fn, function(text) {
if (confirm("Reset '" + fn + "' to default?")) { if (confirm("Reset '" + fn + "' to default?")) {
wnd.setText(text); wnd.setText(text);
} }
@ -680,7 +671,7 @@ function _renameFile(e) {
alert("Renamed " + fn + " to " + newfn); alert("Renamed " + fn + " to " + newfn);
updateSelector(); updateSelector();
if (fn == current_project.mainPath) { if (fn == current_project.mainPath) {
reloadPresetNamed(newfn); reloadProject(newfn);
} }
}); });
}); });
@ -788,15 +779,23 @@ function populateFiles(sel:JQuery, category:string, prefix:string, callback:() =
function updateSelector() { function updateSelector() {
var sel = $("#preset_select").empty(); var sel = $("#preset_select").empty();
populateFiles(sel, "Local Files", "local/", () => { if (!repo_id) {
populateFiles(sel, "Shared", "shared/", () => { // normal: populate local and shared files
populateExamples(sel); populateFiles(sel, "Local Files", "local/", () => {
populateFiles(sel, "Shared", "shared/", () => {
populateExamples(sel);
sel.css('visibility','visible');
});
});
} else {
// repo: populate all files
populateFiles(sel, "Repository Files", "", () => {
sel.css('visibility','visible'); sel.css('visibility','visible');
}); });
}); }
// set click handlers // set click handlers
sel.off('change').change(function(e) { sel.off('change').change(function(e) {
reloadPresetNamed($(this).val().toString()); reloadProject($(this).val().toString());
}); });
} }
@ -847,7 +846,7 @@ function setCompileOutput(data: WorkerResult) {
function loadBIOSFromProject() { function loadBIOSFromProject() {
if (platform.loadBIOS) { if (platform.loadBIOS) {
var biospath = 'local/'+platform_id+'.rom'; var biospath = 'local/' + platform_id + '.rom';
store.getItem(biospath).then( (biosdata) => { store.getItem(biospath).then( (biosdata) => {
console.log('loading BIOS') console.log('loading BIOS')
platform.loadBIOS('BIOS', biosdata); platform.loadBIOS('BIOS', biosdata);
@ -1302,7 +1301,7 @@ function setupDebugControls() {
$("#item_download_zip").click(_downloadProjectZipFile); $("#item_download_zip").click(_downloadProjectZipFile);
$("#item_download_allzip").click(_downloadAllFilesZipFile); $("#item_download_allzip").click(_downloadAllFilesZipFile);
$("#item_record_video").click(_recordVideo); $("#item_record_video").click(_recordVideo);
if (platform_id == 'apple2') if (platform_id.startsWith('apple2'))
$("#item_export_cassette").click(_downloadCassetteFile); $("#item_export_cassette").click(_downloadCassetteFile);
else else
$("#item_export_cassette").hide(); $("#item_export_cassette").hide();
@ -1386,7 +1385,7 @@ function isLandscape() {
function showWelcomeMessage() { function showWelcomeMessage() {
if (hasLocalStorage && !localStorage.getItem("8bitworkshop.hello")) { if (hasLocalStorage && !localStorage.getItem("8bitworkshop.hello")) {
// Instance the tour // Instance the tour
var is_vcs = platform_id == 'vcs'; var is_vcs = platform_id.startsWith('vcs');
var steps = [ var steps = [
{ {
element: "#workspace", element: "#workspace",
@ -1557,7 +1556,7 @@ export function loadScript(scriptfn, onload, onerror?) {
export function setupSplits() { export function setupSplits() {
const splitName = 'workspace-split3-' + platform_id; const splitName = 'workspace-split3-' + platform_id;
var sizes = [0, 50, 50]; var sizes = [0, 50, 50];
if (platform_id != 'vcs') // TODO if (platform_id.startsWith('vcs')) // TODO
sizes = [12, 44, 44]; sizes = [12, 44, 44];
var sizesStr = hasLocalStorage && localStorage.getItem(splitName); var sizesStr = hasLocalStorage && localStorage.getItem(splitName);
if (sizesStr) { if (sizesStr) {
@ -1619,33 +1618,28 @@ export function startUI(loadplatform : boolean) {
} }
$("#item_platform_"+platform_id).addClass("dropdown-item-checked"); $("#item_platform_"+platform_id).addClass("dropdown-item-checked");
setupSplits(); setupSplits();
// parse query string // create store
// is this a share URL? can't create store until we know platform_id... repo_id = qs['repo'];
if (qs['gistkey']) { store_id = repo_id || getBasePlatform(platform_id);
loadSharedGist(qs['gistkey']); store = createNewPersistentStore(store_id, (store) => {
} // is this an importURL?
// otherwise, open IDE // TODO: use repo
else { if (qs['githubURL']) {
// create store for platform getGithubService().import(qs['githubURL']);
store = createNewPersistentStore(platform_id, (store) => { return;
// is this an importURL? }
if (qs['githubURL']) { // is this an importURL?
getGithubService().import(qs['githubURL']); if (qs['importURL']) {
return; loadImportedURL(qs['importURL']);
} return;
// is this an importURL? }
if (qs['importURL']) { // load and start platform object
loadImportedURL(qs['importURL']); if (loadplatform) {
return; loadAndStartPlatform();
} } else {
// load and start platform object startPlatform();
if (loadplatform) { }
loadAndStartPlatform(); });
} else {
startPlatform();
}
});
}
} }
function loadAndStartPlatform() { function loadAndStartPlatform() {
@ -1654,7 +1648,7 @@ function loadAndStartPlatform() {
console.log("loaded platform", platform_id); console.log("loaded platform", platform_id);
startPlatform(); startPlatform();
showWelcomeMessage(); showWelcomeMessage();
document.title = document.title + " [" + platform_id + "] - " + current_project.mainPath; document.title = document.title + " [" + platform_id + "] - " + (('['+repo_id+'] - ')||'') + current_project.mainPath;
}, () => { }, () => {
alert('Platform "' + platform_id + '" not supported.'); alert('Platform "' + platform_id + '" not supported.');
}); });

View File

@ -468,3 +468,18 @@ export function getWithBinary(url:string, success:(text:string|Uint8Array)=>void
oReq.send(null); oReq.send(null);
} }
// get platform ID without . emulator
export function getBasePlatform(platform : string) : string {
return platform.split('.')[0];
}
// get platform ID without - specialization
export function getRootPlatform(platform : string) : string {
return platform.split('-')[0];
}
// get platform ID without emulator or specialization
export function getRootBasePlatform(platform : string) : string {
return getRootPlatform(getBasePlatform(platform));
}

View File

@ -19,7 +19,7 @@ function newGH(store, platform_id) {
var project = new prj.CodeProject({}, platform_id||test_platform_id, null, store); var project = new prj.CodeProject({}, platform_id||test_platform_id, null, store);
project.mainPath = 'local/main.asm'; project.mainPath = 'local/main.asm';
project.updateFileInStore(project.mainPath, '\torg $0 ; test\n'); project.updateFileInStore(project.mainPath, '\torg $0 ; test\n');
return new serv.GithubService(Octokat, 'ec64fdd81dedab8b7547388eabef09288e9243a9', store, project); return new serv.GithubService(Octokat, process.env.TEST8BIT_GITHUB_TOKEN, store, project);
} }
const t0 = new Date().getTime(); const t0 = new Date().getTime();
@ -95,11 +95,11 @@ describe('Store', function() {
it('Should bind paths to Github', function(done) { it('Should bind paths to Github', function(done) {
var store = mstore.createNewPersistentStore(test_platform_id, function(store) { var store = mstore.createNewPersistentStore(test_platform_id, function(store) {
var gh = newGH(store); var gh = newGH(store);
var sess = {prefix:'shared/foo/bar/', url:'_'}; var sess = {repopath:'foo/bar', url:'_'};
gh.bind(sess, true); gh.bind(sess, true);
assert.equal(gh.getBoundURL('shared/foo/bar/'), '_'); assert.deepEqual(gh.getRepos(), {'foo/bar':'_'});
gh.bind(sess, false); gh.bind(sess, false);
assert.equal(gh.getBoundURL('shared/foo/bar/'), null); assert.deepEqual(gh.getRepos(), {});
done(); done();
}); });
}); });