From a6df2569113eb461902e01c5f39c6732b4748e95 Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Sat, 15 Dec 2007 12:34:04 +0530 Subject: [PATCH] importing bzrfruit into git --- HEADER.html | 10 ++ README | 7 + browser.py | 251 +++++++++++++++++++++++++++++++ bzrfruit.conf.sample | 8 + do.cgi | 7 + static/css/nearfar.css | 170 +++++++++++++++++++++ static/css/pygments.css | 57 +++++++ static/css/trac.css | 193 ++++++++++++++++++++++++ static/img/file.png | Bin 0 -> 285 bytes static/img/folder.png | Bin 0 -> 357 bytes static/img/parent.png | Bin 0 -> 228 bytes templates/browser.html | 96 ++++++++++++ templates/index.html | 22 +++ templates/skel-override-nearfar.org.html | 27 ++++ templates/skel.html | 26 ++++ 15 files changed, 874 insertions(+) create mode 100644 HEADER.html create mode 100644 README create mode 100644 browser.py create mode 100644 bzrfruit.conf.sample create mode 100755 do.cgi create mode 100644 static/css/nearfar.css create mode 100644 static/css/pygments.css create mode 100644 static/css/trac.css create mode 100644 static/img/file.png create mode 100644 static/img/folder.png create mode 100644 static/img/parent.png create mode 100644 templates/browser.html create mode 100644 templates/index.html create mode 100644 templates/skel-override-nearfar.org.html create mode 100644 templates/skel.html diff --git a/HEADER.html b/HEADER.html new file mode 100644 index 0000000..dbd7957 --- /dev/null +++ b/HEADER.html @@ -0,0 +1,10 @@ + + +

bzrfruit

+

BzrFruit is a simple (Zen-like) branch viewer for + bazaar written in Python. It runs in CGI and + does not require long-running server processes. +

+ + + diff --git a/README b/README new file mode 100644 index 0000000..949978b --- /dev/null +++ b/README @@ -0,0 +1,7 @@ +bzrfruit +======== + +BzrFruit is a simple (Zen-like) branch viewer for +[bazaar](http://bazaar-vcs.org/) written in Python. It runs in CGI and +does not require long-running server processes. + diff --git a/browser.py b/browser.py new file mode 100644 index 0000000..e836294 --- /dev/null +++ b/browser.py @@ -0,0 +1,251 @@ +# Branch Browser for Bazaar +# Run this with web.py + +from os import listdir +from os.path import join, isdir, abspath, expanduser +from datetime import datetime +from configobj import ConfigObj + +from bzrlib.branch import BzrBranch +from bzrlib.errors import NoSuchRevision +from bzrlib.osutils import format_date +import bzrlib + +from pygments import highlight +from pygments.formatters import HtmlFormatter +from pygments.lexers import TextLexer, guess_lexer_for_filename + +import web +web.webapi.internalerror = web.debugerror + +from genshi.template import TemplateLoader +from genshi.core import Markup + + +# Read config file from $HOME +usr_config = ConfigObj(expanduser("~/.bzrfruit.conf")) +config = ConfigObj(dict(web=dict(extra_css="", + skel_template="skel.html"), + bzr=dict())) +config.merge(usr_config) # merge with the default values + + +loader = TemplateLoader(["templates"], auto_reload=True) +def render(name, **namespace): + "Render the genshi template `name' using `namespace'" + tmpl = loader.load(name) + namespace.update(globals()) # we need not pass common methods explicitly + stream = tmpl.generate(**namespace) + web.header("Content-Type","%s; charset=utf-8" % "text/html") + print stream.render('html') + + +# This directory contains the bzr branches +BRANCHES_PATH = expanduser(config["bzr"]["branches_path"]) +# $ bzr get BRANCH_URL+nick +BRANCH_URL = config["web"]["branch_url_base"] + +WWW_URL = config["web"]["www_url_base"] + +def link_to(*segs): + "Sorry.. this function is difficult to explain. May be some other time." + # root = web.prefixurl() + root = WWW_URL + segs = map(lambda s: str(s).lstrip("/"), segs) + return root + "/".join(segs) + +# http://trac.edgewall.org/browser/trunk/trac/util/datefmt.py +def pretty_timedelta(time1, time2, resolution=None): + """Calculate time delta (inaccurately, only for decorative purposes ;-) for + prettyprinting. If time1 is None, the current time is used.""" + if time1 > time2: + time2, time1 = time1, time2 + units = ((3600 * 24 * 365, 'year', 'years'), + (3600 * 24 * 30, 'month', 'months'), + (3600 * 24 * 7, 'week', 'weeks'), + (3600 * 24, 'day', 'days'), + (3600, 'hour', 'hours'), + (60, 'minute', 'minutes')) + diff = time2 - time1 + age_s = int(diff.days * 86400 + diff.seconds) + if resolution and age_s < resolution: + return '' + if age_s < 60: + return '%i second%s' % (age_s, age_s != 1 and 's' or '') + for u, unit, unit_plural in units: + r = float(age_s) / float(u) + if r >= 0.9: + r = int(round(r)) + return '%d %s' % (r, r == 1 and unit or unit_plural) + return '' + +def pretty_size(size): + jump = 512 + if size < jump: + return "%d bytes" % size + + units = ['kB', 'MB', 'GB', 'TB'] + i = 0 + while size >= jump and i < len(units): + i += 1 + size /= 1024.0 + + return "%.1f %s" % (size, units[i - 1]) + + +class Branch: + + def __init__(self, url): + self.branch = BzrBranch.open(url) + + def name(self): + return self.branch.nick + + def inventory(self): + "Return the Inventory for last revision" + b = self.branch + return b.repository.get_revision_inventory(b.last_revision()) + + def revision_tree(self): + b = self.branch + return b.repository.revision_tree(b.last_revision()) + + +def path2inventory(branch, path): + # XXX: investigate if this can be done using bzrlib itself + i = branch.inventory().root + path = path.strip("/") + if path == "": return i + for component in path.split("/"): + i = i.children[component] + return i + + +class BranchPath: + + def __init__(self, branch, path, inventory): + self.path = path + self.branch = branch + self.inventory = inventory + self.name = inventory.name + + def has_text(self): + "Usually calls self.inventory.has_text" + return self.inventory.has_text() + + def revision(self): + return self.branch.branch.repository.get_revision( + self.inventory.revision) + + def rev(self): + try: + revno = self.branch.branch.revision_id_to_revno( + self.inventory.revision) + except NoSuchRevision: + # FIXME: eek! + revno = "??" # self.inventory.revision + return revno, self.inventory.revision + + def age(self): + r = self.revision() + dt = datetime.fromtimestamp(r.timestamp) + return format_date(r.timestamp, r.timezone), \ + pretty_timedelta(dt, datetime.now()) + + def demo(self): + h = self.branch.branch.revision_history() + last_revid = h[-1] + r = bzrlib.tsort.merge_sort( + self.branch.branch.repository.get_revision_graph( ),last_revid) + l=list([ revid for (seq, revid, merge_depth, end_of_merge) in r ]) + return '\n'.join(l) + + +class BranchDir(BranchPath): + "Represents a directory in branch" + + def get_children(self): + children = self.inventory.sorted_children() + + # directories first + for name, inv in children: + if not inv.has_text(): + yield BranchDir(self.branch, self.path+inv.name+"/", inv) + + # then files + for name, inv in children: + if inv.has_text(): + yield BranchFile(self.branch, self.path+inv.name, inv) + + +class BranchFile(BranchPath): + "Represents a file is branch" + + def content(self): + tree = self.branch.revision_tree() + return tree.get_file_text(self.inventory.file_id) + + def highlighted_html(self): + "Return the pygments highlighted HTML" + code = self.content() + try: + lexer = guess_lexer_for_filename(self.path, code) + except: + lexer = TextLexer() + html = highlight(code, lexer, HtmlFormatter()) + # TODO: this is genshi's bug. It removes double \n in Markup() + # since we use
, \n are important.
+        #       let's use 
as a workaround. + # In near future, this must be fixed in genshi. + html = html.replace('\n', '
') + return html + + +urls = ( + "/", "branch_list", + "/([^/]*)/(.*)", "branch_browser" + ) + + +class branch_list: + def GET(self): + def available_branches(): + for dir in listdir(BRANCHES_PATH): + branch_path = join(BRANCHES_PATH, dir) + # Bazaar branch directory should contain + # a .bzr sub-directory + if isdir(join(branch_path, ".bzr")): + yield Branch(branch_path) + + render('index.html', + title="Branches", + branches=list(available_branches())) + + +class branch_browser: + def GET(self, name, p): + p = "/" + p + b = Branch(join(BRANCHES_PATH, name)) + if p.endswith("/"): + path = BranchDir(b, p, path2inventory(b, p)) + else: + path = BranchFile(b, p, path2inventory(b, p)) + + # breadcrumb + breadcrumb = [] + components = path.path.strip('/').split('/') + cumulative = [b.name()] + for c in components: + cumulative.append(c) + url = link_to(*cumulative) + '/' + breadcrumb.append([c, url]) + breadcrumb[-1][1] = None # "current" page, so need not link + + render('browser.html', + title="%s%s" % (b.name(), path.path), + branch=b, + branch_url=BRANCH_URL+b.name(), + path=path, + breadcrumb=breadcrumb) + + diff --git a/bzrfruit.conf.sample b/bzrfruit.conf.sample new file mode 100644 index 0000000..cfd1c5e --- /dev/null +++ b/bzrfruit.conf.sample @@ -0,0 +1,8 @@ +[web] +www_url_base = http://example.com:8080/ +# www_url_base = http://example.com/bzrfruit/do.cgi/ +branch_url_base = http://example.com/bzr/ +skel_template = skel-override-yoursite.html + +[bzr] +branches_path = /var/www/bzr/ diff --git a/do.cgi b/do.cgi new file mode 100755 index 0000000..1f4778b --- /dev/null +++ b/do.cgi @@ -0,0 +1,7 @@ +#!/home/protected/bin/python -OO +# -*- mode: python -*- + +import web +import browser + +web.run(browser.urls, browser.__dict__, web.reloader) diff --git a/static/css/nearfar.css b/static/css/nearfar.css new file mode 100644 index 0000000..93eef17 --- /dev/null +++ b/static/css/nearfar.css @@ -0,0 +1,170 @@ +/* css based of nearfar.org + */ + +/* The Standard */ + +body { + font-family: verdana; + background-color: white; + margin-left: 3%; + font-size: 14px; +} + +img { border: none; } + +a:link { text-decoration: none; } +a:visited { text-decoration: none; } +a:hover { text-decoration: none; background-color: #b9badb; } +a:active { text-decoration: none; background-color: #b9badb; } + + +/* Browser table */ + +table.listing { +border-bottom:1px solid #D7D7D7; +border-collapse:collapse; +border-spacing:0; +clear:both; +margin-top:1em; +width:100%; +font-size: 0.95em; +} + +table.listing th { +font-size:12px; +text-align:left; +padding:0 1em 0.1em 0; +} + +table.listing thead { +background:#F7F7F0 none repeat scroll 0; +} + +table.listing thead th { +font-size:11px; +font-weight:700; +vertical-align:bottom; +border-color:#D7D7D7 #d7d7d7 #999; +border-style:solid; +border-width:1px; +padding:2px 0.5em; +} + +table.listing tbody td,table.listing tbody th { +border:1px dotted #DDD; +vertical-align:top; +padding:0.33em 0.5em; +} + +table.listing tbody tr { +border-top:1px solid #DDD; +} + +table.listing tbody tr.even { +background-color:#FCFCFC; +} + +table.listing tbody tr.odd { +background-color:#F7F7F7; +} + +table.listing tbody tr:hover { +background:#EED none repeat scroll 0 !important; +} + +#dirlist td.rev,#dirlist td.age,#dirlist td.change { +color:#888; +vertical-align:baseline; +white-space:nowrap; +} + +#dirlist td.rev { +font-family:monospace; +font-size:90%; +letter-spacing:-0.08em; +text-align:right; +} + +#dirlist td.size { +color:#888; +font-size:70%; +text-align:right; +vertical-align:middle; +white-space:nowrap; +} + +#dirlist td.name { +width:100%; +} + +#dirlist td.name a,#dirlist td.name span { +background-position:0 50%; +background-repeat:no-repeat; +padding-left:20px; +} + +#dirlist td.name a.parent { +background-image:url(../img/parent.png); +} + +#dirlist td.name a.dir { +background-image:url(../img/folder.png); +} + +#dirlist td.name a.file { +background-image:url(../img/file.png); +} + +#dirlist td.name a,#dirlist td.rev a { +border-bottom:medium none; +display:block; +} + +#dirlist td.change { +font-size:65%; +vertical-align:middle; +white-space:nowrap; +} + +/* Links */ +*:link,*:visited { +color:#B00; +text-decoration:none; +} + +*:link:hover,*:visited:hover { +background-color:#EEE; +color:#555; +} + +/* pygments */ +div.highlight { +padding-left: 1em; +border: 1px solid gray; +} + +/* breadcrumb */ +h2.breadcrumb { +color:#555555; +} + +h2.breadcrumb strong { +color:#BB0000; +} + +h2.breadcrumb strong#branch{ +background-color: lavender; +} + +/* file:rev */ +div.fileinfo { +float: right; +background-color: lavender; +margin: 1em; +padding: 2px; +opacity: .3; /* TODO: opacity: does not work in IE */ +} + +div.fileinfo:hover { +opacity: 1; +} \ No newline at end of file diff --git a/static/css/pygments.css b/static/css/pygments.css new file mode 100644 index 0000000..647f781 --- /dev/null +++ b/static/css/pygments.css @@ -0,0 +1,57 @@ +.c { color: #008800; font-style: italic } /* Comment */ +.err { border: 1px solid #FF0000 } /* Error */ +.k { color: #AA22FF; font-weight: bold } /* Keyword */ +.o { color: #666666 } /* Operator */ +.cm { color: #008800; font-style: italic } /* Comment.Multiline */ +.cp { color: #008800 } /* Comment.Preproc */ +.c1 { color: #008800; font-style: italic } /* Comment.Single */ +.gd { color: #A00000 } /* Generic.Deleted */ +.ge { font-style: italic } /* Generic.Emph */ +.gr { color: #FF0000 } /* Generic.Error */ +.gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.gi { color: #00A000 } /* Generic.Inserted */ +.go { color: #808080 } /* Generic.Output */ +.gp { color: #000080; font-weight: bold } /* Generic.Prompt */ +.gs { font-weight: bold } /* Generic.Strong */ +.gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.gt { color: #0040D0 } /* Generic.Traceback */ +.kc { color: #AA22FF; font-weight: bold } /* Keyword.Constant */ +.kd { color: #AA22FF; font-weight: bold } /* Keyword.Declaration */ +.kp { color: #AA22FF } /* Keyword.Pseudo */ +.kr { color: #AA22FF; font-weight: bold } /* Keyword.Reserved */ +.kt { color: #AA22FF; font-weight: bold } /* Keyword.Type */ +.m { color: #666666 } /* Literal.Number */ +.s { color: #BB4444 } /* Literal.String */ +.na { color: #BB4444 } /* Name.Attribute */ +.nb { color: #AA22FF } /* Name.Builtin */ +.nc { color: #0000FF } /* Name.Class */ +.no { color: #880000 } /* Name.Constant */ +.nd { color: #AA22FF } /* Name.Decorator */ +.ni { color: #999999; font-weight: bold } /* Name.Entity */ +.ne { color: #D2413A; font-weight: bold } /* Name.Exception */ +.nf { color: #00A000 } /* Name.Function */ +.nl { color: #A0A000 } /* Name.Label */ +.nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ +.nt { color: #008000; font-weight: bold } /* Name.Tag */ +.nv { color: #B8860B } /* Name.Variable */ +.ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ +.mf { color: #666666 } /* Literal.Number.Float */ +.mh { color: #666666 } /* Literal.Number.Hex */ +.mi { color: #666666 } /* Literal.Number.Integer */ +.mo { color: #666666 } /* Literal.Number.Oct */ +.sb { color: #BB4444 } /* Literal.String.Backtick */ +.sc { color: #BB4444 } /* Literal.String.Char */ +.sd { color: #BB4444; font-style: italic } /* Literal.String.Doc */ +.s2 { color: #BB4444 } /* Literal.String.Double */ +.se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ +.sh { color: #BB4444 } /* Literal.String.Heredoc */ +.si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ +.sx { color: #008000 } /* Literal.String.Other */ +.sr { color: #BB6688 } /* Literal.String.Regex */ +.s1 { color: #BB4444 } /* Literal.String.Single */ +.ss { color: #B8860B } /* Literal.String.Symbol */ +.bp { color: #AA22FF } /* Name.Builtin.Pseudo */ +.vc { color: #B8860B } /* Name.Variable.Class */ +.vg { color: #B8860B } /* Name.Variable.Global */ +.vi { color: #B8860B } /* Name.Variable.Instance */ +.il { color: #666666 } /* Literal.Number.Integer.Long */ diff --git a/static/css/trac.css b/static/css/trac.css new file mode 100644 index 0000000..986964c --- /dev/null +++ b/static/css/trac.css @@ -0,0 +1,193 @@ +/* Based on Trac css + * http://trac.edgewall.org/ + */ + +body { +background: white; +color: black; +margin: 10px; +padding: 0; +} + +body,th,td { +font-family: verdana,arial,'Bitstream Vera Sans',helvetica,sans-serif; +font-size: 13px; +} + +h1,h2,h3,h4 { +font-family: arial,verdana,'Bitstream Vera Sans',helvetica,sans-serif; +} + + +h2.breadcrumb { +color:#555555; +} + +h2.breadcrumb strong { +color:#BB0000; +} + +h2.breadcrumb strong#branch{ +background-color: lavender; +} + +img { +border:medium none; +} + +tt { +white-space:pre; +} + +*:link,*:visited { +border-bottom:1px dotted #BBB; +color:#B00; +text-decoration:none; +} + +*:link:hover,*:visited:hover { +background-color:#EEE; +color:#555; +} + +h1 *:link,h1 *:visited,h2 *:link,h2 *:visited,h3 *:link,h3 *:visited,h4 *:link,h4 *:visited,h5 *:link,h5 *:visited,h6 *:link,h6 *:visited { +color:inherit; +} + + +table.listing { +border-bottom:1px solid #D7D7D7; +border-collapse:collapse; +border-spacing:0; +clear:both; +margin-top:1em; +width:100%; +} + +table.listing th { +font-size:12px; +text-align:left; +padding:0 1em 0.1em 0; +} + +table.listing thead { +background:#F7F7F0 none repeat scroll 0; +} + +table.listing thead th { +font-size:11px; +font-weight:700; +vertical-align:bottom; +border-color:#D7D7D7 #d7d7d7 #999; +border-style:solid; +border-width:1px; +padding:2px 0.5em; +} + +table.listing thead th a { +border:medium none; +padding-right:12px; +} + +table.listing th.asc a,table.listing th.desc a { +font-weight:700; +background-position:100% 50%; +background-repeat:no-repeat; +} + +table.listing th.asc a { +background-image:url(../img/asc.png); +} + +table.listing th.desc a { +background-image:url(../img/desc.png); +} + +table.listing tbody td,table.listing tbody th { +border:1px dotted #DDD; +vertical-align:top; +padding:0.33em 0.5em; +} + +table.listing tbody tr { +border-top:1px solid #DDD; +} + +table.listing tbody tr.even { +background-color:#FCFCFC; +} + +table.listing tbody tr.odd { +background-color:#F7F7F7; +} + +table.listing tbody tr:hover { +background:#EED none repeat scroll 0 !important; +} + +#dirlist { +margin-top:0; +} + +#dirlist td.rev,#dirlist td.age,#dirlist td.change { +color:#888; +vertical-align:baseline; +white-space:nowrap; +} + +#dirlist td.rev { +font-family:monospace; +font-size:90%; +letter-spacing:-0.08em; +text-align:right; +} + +#dirlist td.size { +color:#888; +font-size:70%; +text-align:right; +vertical-align:middle; +white-space:nowrap; +} + +#dirlist td.name { +width:100%; +} + +#dirlist td.name a,#dirlist td.name span { +background-position:0 50%; +background-repeat:no-repeat; +padding-left:20px; +} + +#dirlist td.name a.parent { +background-image:url(../img/parent.png); +} + +#dirlist td.name a.dir { +background-image:url(../img/folder.png); +} + +#dirlist td.name a.file { +background-image:url(../img/file.png); +} + +#dirlist td.name a,#dirlist td.rev a { +border-bottom:medium none; +display:block; +} + +#dirlist td.change { +font-size:65%; +vertical-align:middle; +white-space:nowrap; +} + +table.listing thead th *:link:hover,table.listing thead th *:visited:hover,table.listing tbody td a:hover,table.listing tbody th a:hover { +background-color:transparent; +} + +div.highlight { +padding-left: 1em; +border: 1px solid gray; +} \ No newline at end of file diff --git a/static/img/file.png b/static/img/file.png new file mode 100644 index 0000000000000000000000000000000000000000..f35fc99b3e6281ebab6b8fc8bea5f21a91f830b3 GIT binary patch literal 285 zcwXxa@N?(olHy`uVBq!ia0vp^0wB!73?$#)eFPHF3h)VWWnlRK{rmU#@83Ur_Uzuh zd)KdDKXc~Hkt0VA95^s@=FFCsmWqms;^N}}|NqNXN9zD}F_r}R1v5B2yO9Rua29w( z7Bet#eE?xbr!^)sfP(BLp1!W^S6GFa_?ecfYKa1cDm`5sLn>}1Cj=OonVA_UBqU7O zTmAjh(=-KV^Dpn7?iS;6U<-KiZY_g?vhLeW-abqbPu|7OWZB>mwTLIgMc|Cbd0*`- zjNLIlI!wlwxKDTIH1N1OI;ticEZ(zc&kgny0YPSkg&!Gb$?W`fW~T9S83_pq-^-^@ bJBct9m+PL3-Ctt}bOeK^tDnm{r-UW|XWVVB literal 0 HcwPel00001 diff --git a/static/img/folder.png b/static/img/folder.png new file mode 100644 index 0000000000000000000000000000000000000000..d26c06cfc3d84b08274b7971b850a60256c83998 GIT binary patch literal 357 zcwPZz0h<1aP)fgTS<;d>k%H`+e>g&|)>Duw_-tzJ2@$>NW_44=j?fm}xOh+Io(CHZf0004W zQchC7@5L(@?%u!i z^u_Zp-#`ER|7X>jmAj5HWB{cZOM?7@862M7NCR>>3p^r=85p=efH0%e8j~47LG}_) zUsv`ktinuYW?iDeQa~YpPZ!6Kid)GE2^Rtq7?_uRQ{!-69N@&Lq@ZzwMNmM5g}buE zq^!h(K~hJrS67cO;cBn0Kx~DiFJFKgyFkO4D+XnpzN;CG<|HWT=qWHT)O;7WznN%W Q2{f9))78&qol`;+02sbV7ytkO literal 0 HcwPel00001 diff --git a/templates/browser.html b/templates/browser.html new file mode 100644 index 0000000..23b6c2b --- /dev/null +++ b/templates/browser.html @@ -0,0 +1,96 @@ + + + + + + + + +
+ +
+ revision ${path.rev()[0]}: ${path.revision().get_summary()}
+ modified by ${path.revision().committer} ${path.age()[1]} ago +
+ ${Markup(path.highlighted_html())} +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameSizeRevAgeLast Change
+ ..
+ ${p.inventory.name} + ${p.name} + + + ${pretty_size(p.inventory.text_size)} + + + + ${p.rev()[0]} + + ${p.age()[1]} + + ${p.revision().committer} + ${p.revision().get_summary()} +
+
+ +
+

Download via bazaar: $ bzr get $branch_url

+
+ + + +
+ + + + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..b35ff27 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,22 @@ + + + +

List of available branches

+ +

+ +

+ ${b.name()} + (${len(b.inventory().entries())} files) +
+ +

+ +
+ + + + diff --git a/templates/skel-override-nearfar.org.html b/templates/skel-override-nearfar.org.html new file mode 100644 index 0000000..1bbcd73 --- /dev/null +++ b/templates/skel-override-nearfar.org.html @@ -0,0 +1,27 @@ + + + ${select('*')} + + + + + + + + ${select('*')} + + + + + diff --git a/templates/skel.html b/templates/skel.html new file mode 100644 index 0000000..c4e8198 --- /dev/null +++ b/templates/skel.html @@ -0,0 +1,26 @@ + + + + $title - BzrFruit + + + + + +

code

+ +
+ + ${ content() } +
+ +
+ + + -- 2.11.4.GIT