@@ -2,6 +2,7 @@ | |||
"core.min.js": [ | |||
"wn/provide.js", | |||
"wn/xmlhttp.js", | |||
"wn/versions.js", | |||
"wn/assets.js", | |||
"wn/require.js", | |||
"wn/dom.js", | |||
@@ -1,3 +1,5 @@ | |||
// find files changed since last version | |||
wn.versions.check(); | |||
// load all critical libraries | |||
wn.require("lib/js/lib/jquery.min.js"); | |||
@@ -5,9 +5,14 @@ parent=parent[n];}} | |||
wn.provide('wn.settings');wn.provide('wn.ui');wn.xmlhttp={request:function(){if(window.XMLHttpRequest) | |||
return new XMLHttpRequest();else if(window.ActiveXObject) | |||
return new ActiveXObject("MsXml2.XmlHttp");},complete:function(req,callback,url){if(req.status==200||req.status==304){callback(req.responseText);}else{alert(url+' request error: '+req.statusText+' ('+req.status+')');}},get:function(url,callback,args,async){if(async===null)async=true;var req=wn.xmlhttp.request();req.onreadystatechange=function(){if(req.readyState==4){wn.xmlhttp.complete(req,callback,url)}} | |||
req.open('GET',url,async);req.send(args?url+'?'+args:null);if(!async){wn.xmlhttp.complete(req,callback,url)}}} | |||
wn.assets={executed_:{},exists:function(src){if('localStorage'in window&&localStorage.getItem(src)&&localStorage.getItem('[ts] '+src)==asset_timestamps_[src]) | |||
return true},add:function(src,txt){if('localStorage'in window){localStorage.setItem(src,txt);localStorage.setItem('[ts] '+src,asset_timestamps_[src]);}},get:function(src){return localStorage.getItem(src);},extn:function(src){return src.split('.').slice(-1)[0];},html_src:function(src){if(src.indexOf('/')!=-1){var t=src.split('/').slice(0,-1);t.push('src');t=t.join('/')+'/'+a.split('/').slice(-1)[0];}else{var t='src/'+src;} | |||
var u=args?(url+'?'+args):url;req.open('GET',u,async);req.send(null);if(!async){wn.xmlhttp.complete(req,callback,url)}}} | |||
wn.versions={is_latest:function(){if(window._version_number==(localStorage?localStorage['_version_number']:null)){return true;} | |||
return false;},get_diff:function(){if(!localStorage)return;wn.xmlhttp.get('index.cgi',function(txt){r=JSON.parse(txt);if(r.exc){alert(r.exc);} | |||
wn.versions.set(r.message);},'cmd=get_diff&version_number='+localStorage['_version_number'],false);},set:function(diff){for(var i=0;i<diff.length;i++){localStorage.removeItem(diff[i]);} | |||
localStorage['_version_number']=_version_number;},check:function(){if(localStorage&&!localStorage['_version_number']){localStorage['_version_number']=_version_number;return;} | |||
if(!wn.versions.is_latest())wn.versions.get_diff();}} | |||
wn.assets={executed_:{},exists:function(src){if('localStorage'in window&&localStorage.getItem(src)) | |||
return true},add:function(src,txt){if('localStorage'in window){localStorage.setItem(src,txt);}},get:function(src){return localStorage.getItem(src);},extn:function(src){return src.split('.').slice(-1)[0];},html_src:function(src){if(src.indexOf('/')!=-1){var t=src.split('/').slice(0,-1);t.push('src');t=t.join('/')+'/'+a.split('/').slice(-1)[0];}else{var t='src/'+src;} | |||
return t;},load:function(src){var t=wn.assets.extn(src)=='html'?wn.assets.html_src(src):src;wn.xmlhttp.get(t,function(txt){wn.assets.add(src,txt);},'q='&Math.floor(Math.random()*1000),false)},execute:function(src){if(!wn.assets.exists(src)){wn.assets.load(src);} | |||
var type=wn.assets.extn(src);if(wn.assets.handler[type]){wn.assets.handler[type](wn.assets.get(src),src);wn.assets.executed_[src]=1;}},handler:{js:function(txt,src){wn.dom.eval(txt);},css:function(txt,src){var se=document.createElement('style');se.type="text/css";if(se.styleSheet){se.styleSheet.cssText=txt;}else{se.appendChild(document.createTextNode(txt));} | |||
document.getElementsByTagName('head')[0].appendChild(se);},html:function(txt,src){var page=wn.dom.add($('.outer .inner').get(0),'div','content',null,txt);page.setAttribute("_src",src);}}} | |||
@@ -52,4 +57,4 @@ return reviver.call(holder,key,value);} | |||
text=String(text);cx.lastIndex=0;if(cx.test(text)){text=text.replace(cx,function(a){return'\\u'+ | |||
('0000'+a.charCodeAt(0).toString(16)).slice(-4);});} | |||
if(/^[\],:{}\s]*$/.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,'@').replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,']').replace(/(?:^|:|,)(?:\s*\[)+/g,''))){j=eval('('+text+')');return typeof reviver==='function'?walk({'':j},''):j;} | |||
throw new SyntaxError('JSON.parse');};}}());wn.require("lib/js/lib/jquery.min.js");wn.require("lib/js/lib/history/history.min.js");$(document).bind('ready',function(){var base=window.location.href.split('#')[0];$.each($('a[softlink!="false"]'),function(i,v){if(v.href.substr(0,base.length)==base){var path=(v.href.substr(base.length));if(path.substr(0,1)!='#'){v.href=base+'#'+path;}}});if(!wn.settings.no_history&&window.location.hash){wn.page.set(window.location.hash.substr(1));}}); | |||
throw new SyntaxError('JSON.parse');};}}());wn.versions.check();wn.require("lib/js/lib/jquery.min.js");wn.require("lib/js/lib/history/history.min.js");$(document).bind('ready',function(){var base=window.location.href.split('#')[0];$.each($('a[softlink!="false"]'),function(i,v){if(v.href.substr(0,base.length)==base){var path=(v.href.substr(base.length));if(path.substr(0,1)!='#'){v.href=base+'#'+path;}}});if(!wn.settings.no_history&&window.location.hash){wn.page.set(window.location.hash.substr(1));}}); |
@@ -1,19 +1,17 @@ | |||
// library to mange assets (js, css, models, html) etc in the app. | |||
// will try and get from localStorge if latest are available | |||
// or will load them via xmlhttp | |||
// depends on asset_timestamps_ loaded via boot | |||
// depends on wn.versions to manage versioning | |||
wn.assets = { | |||
// keep track of executed assets | |||
executed_ : {}, | |||
// check if the asset exists in | |||
// localstorage and if the timestamp | |||
// matches with the loaded timestamp | |||
// localstorage | |||
exists: function(src) { | |||
if('localStorage' in window | |||
&& localStorage.getItem(src) | |||
&& localStorage.getItem('[ts] '+src) == asset_timestamps_[src]) | |||
&& localStorage.getItem(src)) | |||
return true | |||
}, | |||
@@ -22,7 +20,6 @@ wn.assets = { | |||
add: function(src, txt) { | |||
if('localStorage' in window) { | |||
localStorage.setItem(src, txt); | |||
localStorage.setItem('[ts] ' + src, asset_timestamps_[src]); | |||
} | |||
}, | |||
@@ -0,0 +1,42 @@ | |||
// manage app versioning | |||
// get the last_version_number from the server (loaded) | |||
// and update based on it | |||
wn.versions = { | |||
is_latest: function() { | |||
if(window._version_number == (localStorage ? localStorage['_version_number'] : null)) { | |||
return true; | |||
} | |||
return false; | |||
}, | |||
// get the change list of all files | |||
// from current version and local version | |||
get_diff: function() { | |||
if(!localStorage) return; | |||
wn.xmlhttp.get('index.cgi', function(txt) { | |||
// add it to localstorage | |||
r = JSON.parse(txt); | |||
if(r.exc) { alert(r.exc); } | |||
wn.versions.set(r.message); | |||
}, 'cmd=get_diff&version_number=' + localStorage['_version_number'], false); | |||
}, | |||
// set will clear all changes since the last update | |||
set: function(diff) { | |||
for(var i=0; i<diff.length; i++) { | |||
localStorage.removeItem(diff[i]); | |||
} | |||
localStorage['_version_number'] = _version_number; | |||
}, | |||
check: function() { | |||
if(localStorage && !localStorage['_version_number']) { | |||
// first load | |||
localStorage['_version_number'] = _version_number; | |||
return; | |||
} | |||
if(!wn.versions.is_latest()) wn.versions.get_diff(); | |||
} | |||
} |
@@ -25,8 +25,9 @@ wn.xmlhttp = { | |||
wn.xmlhttp.complete(req, callback, url) | |||
} | |||
} | |||
req.open('GET', url, async); | |||
req.send(args ? url + '?' + args : null); | |||
var u = args ? (url + '?' + args) : url; | |||
req.open('GET', u, async); | |||
req.send(null); | |||
// for sync | |||
if(!async) { | |||
@@ -2,20 +2,17 @@ verbose = True | |||
force_rebuild = False | |||
no_minify = False | |||
def run(): | |||
def run(path): | |||
""" | |||
Run the builder | |||
""" | |||
global verbose | |||
import sys, os | |||
sys.path.append('py') | |||
sys.path.append('lib/py') | |||
from build.project import Project | |||
verbose = True | |||
Project().build() | |||
Project().build(path) | |||
if __name__=='__main__': | |||
run() |
@@ -40,7 +40,9 @@ class Bundle: | |||
f = open(outfile, 'w') | |||
f.write(temp.getvalue()) | |||
f.close() | |||
self.timestamps.update(outfile) | |||
self.vc.repo.add(outfile) | |||
if verbose: print 'Wrote %s' % outfile | |||
return temp | |||
@@ -50,13 +52,15 @@ class Bundle: | |||
Returns true if the files are changed since last build | |||
""" | |||
import os | |||
from build import force_rebuild | |||
from build import force_rebuild, verbose | |||
if force_rebuild: | |||
return True | |||
for f in files: | |||
if f in self.timestamps.dirty: | |||
if f in self.dirty: | |||
if verbose: | |||
print '*** %s changed' % f | |||
return True | |||
return False | |||
@@ -82,7 +86,7 @@ class Bundle: | |||
jsm.minify(temp, out) | |||
out.close() | |||
self.timestamps.update(outfile) | |||
self.vc.repo.add(outfile) | |||
new_size = os.path.getsize(outfile) | |||
@@ -92,7 +96,7 @@ class Bundle: | |||
print 'Compressed: %.2f kB' % (new_size / 1024.0) | |||
print 'Reduction: %.1f%%' % (float(org_size - new_size) / org_size * 100) | |||
def make(self, path): | |||
def make(self, bpath): | |||
""" | |||
Build (stitch + compress) the file defined in build.json | |||
""" | |||
@@ -101,10 +105,12 @@ class Bundle: | |||
# open the build.json file and read | |||
# the dict | |||
bfile = open(os.path.join(path, 'build.json'), 'r') | |||
bfile = open(bpath, 'r') | |||
bdata = json.loads(bfile.read()) | |||
bfile.close() | |||
path = os.path.dirname(bpath) | |||
for outfile in bdata: | |||
prefix, fname = False, outfile | |||
@@ -115,8 +121,6 @@ class Bundle: | |||
# build the file list relative to the main folder | |||
fl = [os.path.relpath(os.path.join(path, f), os.curdir) for f in bdata[outfile]] | |||
self.timestamps.bundled += fl | |||
if self.changed(fl): | |||
# js files are minified by default unless explicitly | |||
# mentioned in the prefix. | |||
@@ -127,16 +131,15 @@ class Bundle: | |||
else: | |||
self.concat(fl, os.path.relpath(os.path.join(path, fname), os.curdir)) | |||
def bundle(self, timestamps): | |||
def bundle(self, vc): | |||
""" | |||
Build js files from "build.json" | |||
Build js files from "build.json" found in version control | |||
""" | |||
import os | |||
self.timestamps = timestamps | |||
self.dirty = vc.repo.uncommitted() | |||
self.vc = vc | |||
# walk the parent folder and build all files as defined in the build.json files | |||
for wt in os.walk('.', followlinks=True): | |||
if 'build.json' in wt[2]: | |||
# found build file | |||
self.make(os.path.abspath(wt[0])) | |||
for b in vc.repo.sql("select fname from bundles"): | |||
self.make(os.path.abspath(os.path.join(vc.root_path, b[0]))) | |||
@@ -14,11 +14,9 @@ class Project: | |||
""" | |||
load libraries | |||
""" | |||
from build.timestamps import Timestamps | |||
from build.bundle import Bundle | |||
from nav import Nav | |||
self.timestamps = Timestamps() | |||
self.bundle = Bundle() | |||
self.nav = Nav() | |||
@@ -29,11 +27,13 @@ class Project: | |||
import json | |||
corejs = open('lib/js/core.min.js', 'r') | |||
v = int(self.vc.repo.get_value('last_version_number') or 0) + 1 | |||
boot = 'var asset_timestamps_=' + self.timestamps.get('json', ('js', 'html', 'css')) \ | |||
+ '\n' + corejs.read() | |||
boot = ('window._version_number="%s"' % str(v)) + \ | |||
'\n' + corejs.read() | |||
corejs.close() | |||
return boot | |||
def render_templates(self): | |||
@@ -69,15 +69,22 @@ class Project: | |||
print "Rendered %s | %.2fkb" % (fpath, os.path.getsize(fpath) / 1024.0) | |||
def build(self): | |||
def build(self, root_path): | |||
""" | |||
Build all js files, timestamps.js, index.html and template.html | |||
Build all js files, index.html and template.html | |||
""" | |||
from build.version import VersionControl | |||
# make bundles | |||
self.bundle.bundle(self.timestamps) | |||
self.vc = VersionControl(root_path) | |||
self.vc.add_all() | |||
# index, template if framework is dirty | |||
if self.timestamps.dirty: | |||
if self.vc.repo.uncommitted(): | |||
self.bundle.bundle(self.vc) | |||
self.render_templates() | |||
self.timestamps.write() | |||
# again add all bundles | |||
self.vc.add_all() | |||
self.vc.repo.commit() | |||
self.vc.close() |
@@ -7,6 +7,7 @@ | |||
uncommitted (fname, ftype, content, timestamp) | |||
files (fname, ftype, content, timestamp, version) | |||
log (fname, ftype, version) | |||
bundle_files (fname primary key) | |||
Discussion: | |||
@@ -27,23 +28,29 @@ TODO | |||
import unittest | |||
import os | |||
root_path = os.path.abspath(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', '..')) | |||
test_file = {'fname':'test.js', 'ftype':'js', 'content':'test_code', 'timestamp':'1100'} | |||
root_path = os.path.abspath(os.curdir) | |||
def edit_file(): | |||
# edit a file | |||
p = os.path.join(root_path, 'js/core.min.js') | |||
content = open(p, 'r').read() | |||
p = os.path.join(root_path, 'lib/js/core.js') | |||
# read | |||
f1 = open(p, 'r') | |||
content = f1.read() | |||
f1.close() | |||
# write | |||
f = open(p, 'w') | |||
f.write(content) | |||
f.close() | |||
return p | |||
return os.path.relpath(p, root_path) | |||
verbose = False | |||
class TestVC(unittest.TestCase): | |||
def setUp(self): | |||
self.vc = VersionControl(root_path) | |||
self.vc = VersionControl() | |||
self.vc.repo.setup() | |||
def test_add(self): | |||
@@ -86,13 +93,12 @@ class TestVC(unittest.TestCase): | |||
self.vc.repo.commit() | |||
p = edit_file() | |||
# add | |||
self.vc.add_all() | |||
# check if added | |||
ret = self.vc.repo.sql("select * from uncommitted", as_dict=1) | |||
self.assertTrue(ret[0]['fname']==p) | |||
self.assertTrue(p in [r['fname'] for r in ret]) | |||
def test_merge(self): | |||
self.vc.add_all() | |||
@@ -114,7 +120,7 @@ class TestVC(unittest.TestCase): | |||
self.vc.merge(self.vc.repo, self.vc.master) | |||
log = self.vc.master.diff(int(self.vc.master.get_value('last_version_number'))-1) | |||
self.assertTrue(log, [p]) | |||
self.assertTrue(p in log) | |||
def tearDown(self): | |||
self.vc.close() | |||
@@ -124,25 +130,26 @@ class TestVC(unittest.TestCase): | |||
class VersionControl: | |||
def __init__(self, root): | |||
def __init__(self, root=None): | |||
#self.master = Repository(self, 'versions-master.db') | |||
self.root(root) | |||
self.set_root(root) | |||
self.repo = Repository(self, 'versions-local.db') | |||
self.ignore_folders = ['.git', '.', '..'] | |||
self.ignore_files = ['pyc', 'DS_Store', 'txt', 'db-journal', 'db'] | |||
self.ignore_files = ['py', 'pyc', 'DS_Store', 'txt', 'db-journal', 'db'] | |||
def setup_master(self): | |||
self.master = Repository(self, 'versions-master.db') | |||
def root(self, path=None): | |||
def set_root(self, path=None): | |||
""" | |||
set / reset root and connect | |||
(the root path is the path of the folder) | |||
""" | |||
if path: | |||
self.root_path = path | |||
else: | |||
return self.root_path | |||
if not path: | |||
raise Exception, 'path must be given' | |||
self.root_path = path | |||
def timestamp(self, path): | |||
""" | |||
@@ -164,12 +171,16 @@ class VersionControl: | |||
wt[1].remove(folder) | |||
for fname in wt[2]: | |||
fpath = os.path.join(wt[0], fname) | |||
if fname.endswith('build.json'): | |||
self.repo.add_bundle(fpath) | |||
continue | |||
if fname.split('.')[-1] in self.ignore_files: | |||
# nothing to do | |||
continue | |||
fpath = os.path.join(wt[0], fname) | |||
# file does not exist | |||
if not self.repo.exists(fpath): | |||
if verbose: | |||
@@ -208,6 +219,7 @@ class VersionControl: | |||
target.commit(d[0]) | |||
def close(self): | |||
self.repo.conn.commit() | |||
self.repo.conn.close() | |||
class Repository: | |||
@@ -223,12 +235,14 @@ class Repository: | |||
""" | |||
setup the schema | |||
""" | |||
print "setting up %s..." % self.db_path | |||
self.cur.executescript(""" | |||
create table properties(pkey primary key, value); | |||
create table uncommitted(fname primary key, ftype, content, timestamp); | |||
create table files (fname primary key, ftype, content, timestamp, version); | |||
create table log (fname, ftype, version); | |||
create table versions (number integer primary key, version); | |||
create table bundles(fname primary key); | |||
""") | |||
def sql(self, query, values=(), as_dict=None): | |||
@@ -267,13 +281,17 @@ class Repository: | |||
""" | |||
add to uncommitted | |||
""" | |||
import os | |||
# commit relative path | |||
fname = os.path.relpath(fname, self.vc.root_path) | |||
if not ftype: | |||
ftype = fname.split('.')[-1] | |||
if not timestamp: | |||
timestamp = self.vc.timestamp(fname) | |||
self.sql("insert into uncommitted(fname, ftype, timestamp, content) values (?, ?, ?, ?)" \ | |||
self.sql("insert or replace into uncommitted(fname, ftype, timestamp, content) values (?, ?, ?, ?)" \ | |||
, (fname, ftype, timestamp, content)) | |||
def new_version(self): | |||
@@ -296,15 +314,30 @@ class Repository: | |||
def commit(self, version=None): | |||
""" | |||
rebuild bundles if necessary | |||
copy uncommitted files to repository, update the log and add the change | |||
""" | |||
# make bundles | |||
from bundle import Bundle | |||
Bundle().bundle(self.vc) | |||
# get a new version number | |||
if not version: | |||
version = self.new_version() | |||
if not version: version = self.new_version() | |||
self.update_number(version) | |||
# find added files to commit | |||
self.add_from_uncommitted(version) | |||
# clear uncommitted | |||
self.sql("delete from uncommitted") | |||
def add_from_uncommitted(self, version): | |||
""" | |||
move files from uncommitted table to files table | |||
""" | |||
added = self.sql("select * from uncommitted", as_dict=1) | |||
for f in added: | |||
@@ -316,21 +349,20 @@ class Repository: | |||
""", (f['fname'], f['ftype'], f['timestamp'], f['content'], version)) | |||
# update log | |||
self.add_log(f['fname'], f['ftype'], version) | |||
# clear uncommitted | |||
self.sql("delete from uncommitted") | |||
self.add_log(f['fname'], f['ftype'], version) | |||
def exists(self, fname): | |||
""" | |||
true if exists | |||
""" | |||
fname = os.path.relpath(fname, self.vc.root_path) | |||
return self.sql("select fname from files where fname=?", (fname,)) | |||
def timestamp(self, fname): | |||
""" | |||
get timestamp | |||
""" | |||
fname = os.path.relpath(fname, self.vc.root_path) | |||
return int(self.sql("select timestamp from files where fname=?", (fname,))[0][0] or 0) | |||
def diff(self, number): | |||
@@ -345,6 +377,12 @@ class Repository: | |||
return list(set([f[0] for f in ret])) | |||
def uncommitted(self): | |||
""" | |||
return list of uncommitted files | |||
""" | |||
return [f[0] for f in self.sql("select fname from uncommitted")] | |||
def get_file(self, fname): | |||
""" | |||
return file info as dict | |||
@@ -356,7 +394,15 @@ class Repository: | |||
add file to log | |||
""" | |||
self.sql("insert into log(fname, ftype, version) values (?,?,?)", (fname, ftype, version)) | |||
def add_bundle(self, fname): | |||
""" | |||
add to bundles | |||
""" | |||
self.sql("insert or replace into bundles(fname) values (?)", (fname,)) | |||
if __name__=='__main__': | |||
import os, sys | |||
sys.path.append('py') | |||
sys.path.append('lib/py') | |||
unittest.main() |
@@ -40,6 +40,15 @@ def runserverobj(arg=None): | |||
def logout(): | |||
webnotes.login_manager.logout() | |||
# versions | |||
# -------- | |||
def get_diff(): | |||
import os | |||
v = webnotes.form_dict.get('version_number') | |||
from build.version import VersionControl | |||
webnotes.response['message'] = VersionControl(os.path.abspath(os.path.curdir)).repo.diff(v) | |||
# DocType Mapper | |||
# ------------------------------------------------------------------------------------ | |||
@@ -1,4 +1,57 @@ | |||
#!/usr/bin/env python | |||
print 'hello' | |||
import os, sys | |||
from py.build import version | |||
version.verbose = True | |||
def run(): | |||
sys.path.append('lib') | |||
sys.path.append('lib/py') | |||
vc = version.VersionControl(os.path.abspath(os.curdir)) | |||
if len(sys.argv)<2: | |||
print "wnframework version control utility" | |||
print "Usage: wnf build|add|commit|diff|merge|setup" | |||
cmd = sys.argv[1] | |||
if cmd=='build': | |||
from py import build | |||
build.run(os.path.abspath(os.curdir)) | |||
if cmd=='add': | |||
if not len(sys.argv)>1: | |||
print 'usage: wnf add path/to/file' | |||
return | |||
vc.repo.add(sys.argv[2]) | |||
if cmd=='commit': | |||
if len(sys.argv>2) and sys.argv[2]=='-a': | |||
vc.add_all() | |||
vc.repo.commit() | |||
if cmd=='diff': | |||
vc.repo.uncommitted() | |||
if cmd=='merge': | |||
vc.setup_master() | |||
if sys.argv[2]=='local': | |||
vc.merge(vc.repo, vc.master) | |||
elif sys.argv[2]=='master': | |||
vc.merge(vc.master, vc.repo) | |||
else: | |||
print "usage: wnf merge local|master" | |||
print "help: parameter (local or master) is the source" | |||
if cmd=='setup': | |||
vc.repo.setup() | |||
vc.close() | |||
if __name__=='__main__': | |||
run() |