1 # Branch Browser for Bazaar
5 from os
.path
import join
, isdir
, abspath
, expanduser
6 from datetime
import datetime
7 from configobj
import ConfigObj
9 from bzrlib
.branch
import BzrBranch
10 from bzrlib
.errors
import NoSuchRevision
11 from bzrlib
.osutils
import format_date
14 from pygments
import highlight
15 from pygments
.formatters
import HtmlFormatter
16 from pygments
.lexers
import TextLexer
, guess_lexer_for_filename
19 web
.webapi
.internalerror
= web
.debugerror
21 from genshi
.template
import TemplateLoader
22 from genshi
.core
import Markup
25 # Read config file from $HOME
26 usr_config
= ConfigObj(expanduser("~/.bzrfruit.conf"))
27 config
= ConfigObj(dict(web
=dict(extra_css
="",
28 skel_template
="skel.html"),
30 config
.merge(usr_config
) # merge with the default values
33 loader
= TemplateLoader(["templates"], auto_reload
=True)
34 def render(name
, **namespace
):
35 "Render the genshi template `name' using `namespace'"
36 tmpl
= loader
.load(name
)
37 namespace
.update(globals()) # we need not pass common methods explicitly
38 stream
= tmpl
.generate(**namespace
)
39 web
.header("Content-Type","%s; charset=utf-8" % "text/html")
40 print stream
.render('html')
43 # This directory contains the bzr branches
44 BRANCHES_PATH
= expanduser(config
["bzr"]["branches_path"])
45 # $ bzr get BRANCH_URL+nick
46 BRANCH_URL
= config
["web"]["branch_url_base"]
48 WWW_URL
= config
["web"]["www_url_base"]
51 "Sorry.. this function is difficult to explain. May be some other time."
52 # root = web.prefixurl()
54 segs
= map(lambda s
: str(s
).lstrip("/"), segs
)
55 return root
+ "/".join(segs
)
57 # http://trac.edgewall.org/browser/trunk/trac/util/datefmt.py
58 def pretty_timedelta(time1
, time2
, resolution
=None):
59 """Calculate time delta (inaccurately, only for decorative purposes ;-) for
60 prettyprinting. If time1 is None, the current time is used."""
62 time2
, time1
= time1
, time2
63 units
= ((3600 * 24 * 365, 'year', 'years'),
64 (3600 * 24 * 30, 'month', 'months'),
65 (3600 * 24 * 7, 'week', 'weeks'),
66 (3600 * 24, 'day', 'days'),
67 (3600, 'hour', 'hours'),
68 (60, 'minute', 'minutes'))
70 age_s
= int(diff
.days
* 86400 + diff
.seconds
)
71 if resolution
and age_s
< resolution
:
74 return '%i second%s' % (age_s
, age_s
!= 1 and 's' or '')
75 for u
, unit
, unit_plural
in units
:
76 r
= float(age_s
) / float(u
)
79 return '%d %s' % (r
, r
== 1 and unit
or unit_plural
)
82 def pretty_size(size
):
85 return "%d bytes" % size
87 units
= ['kB', 'MB', 'GB', 'TB']
89 while size
>= jump
and i
< len(units
):
93 return "%.1f %s" % (size
, units
[i
- 1])
98 def __init__(self
, url
):
99 self
.branch
= BzrBranch
.open(url
)
102 return self
.branch
.nick
105 "Return the Inventory for last revision"
107 return b
.repository
.get_revision_inventory(b
.last_revision())
109 def revision_tree(self
):
111 return b
.repository
.revision_tree(b
.last_revision())
114 def path2inventory(branch
, path
):
115 # XXX: investigate if this can be done using bzrlib itself
116 i
= branch
.inventory().root
117 path
= path
.strip("/")
118 if path
== "": return i
119 for component
in path
.split("/"):
120 i
= i
.children
[component
]
126 def __init__(self
, branch
, path
, inventory
):
129 self
.inventory
= inventory
130 self
.name
= inventory
.name
133 "Usually calls self.inventory.has_text"
134 return self
.inventory
.has_text()
137 return self
.branch
.branch
.repository
.get_revision(
138 self
.inventory
.revision
)
142 revno
= self
.branch
.branch
.revision_id_to_revno(
143 self
.inventory
.revision
)
144 except NoSuchRevision
:
146 revno
= "??" # self.inventory.revision
147 return revno
, self
.inventory
.revision
151 dt
= datetime
.fromtimestamp(r
.timestamp
)
152 return format_date(r
.timestamp
, r
.timezone
), \
153 pretty_timedelta(dt
, datetime
.now())
156 h
= self
.branch
.branch
.revision_history()
158 r
= bzrlib
.tsort
.merge_sort(
159 self
.branch
.branch
.repository
.get_revision_graph( ),last_revid
)
160 l
=list([ revid
for (seq
, revid
, merge_depth
, end_of_merge
) in r
])
164 class BranchDir(BranchPath
):
165 "Represents a directory in branch"
167 def get_children(self
):
168 children
= self
.inventory
.sorted_children()
171 for name
, inv
in children
:
172 if not inv
.has_text():
173 yield BranchDir(self
.branch
, self
.path
+inv
.name
+"/", inv
)
176 for name
, inv
in children
:
178 yield BranchFile(self
.branch
, self
.path
+inv
.name
, inv
)
181 class BranchFile(BranchPath
):
182 "Represents a file is branch"
185 tree
= self
.branch
.revision_tree()
186 return tree
.get_file_text(self
.inventory
.file_id
)
188 def highlighted_html(self
):
189 "Return the pygments highlighted HTML"
190 code
= self
.content()
192 lexer
= guess_lexer_for_filename(self
.path
, code
)
195 html
= highlight(code
, lexer
, HtmlFormatter())
196 # TODO: this is genshi's bug. It removes double \n in Markup()
197 # since we use <pre>, \n are important.
198 # let's use <br/> as a workaround.
199 # In near future, this must be fixed in genshi.
200 html
= html
.replace('\n', '<br/>')
206 "/([^/]*)/(.*)", "branch_browser"
212 def available_branches():
213 for dir in listdir(BRANCHES_PATH
):
214 branch_path
= join(BRANCHES_PATH
, dir)
215 # Bazaar branch directory should contain
216 # a .bzr sub-directory
217 if isdir(join(branch_path
, ".bzr")):
218 yield Branch(branch_path
)
222 branches
=list(available_branches()))
225 class branch_browser
:
226 def GET(self
, name
, p
):
228 b
= Branch(join(BRANCHES_PATH
, name
))
230 path
= BranchDir(b
, p
, path2inventory(b
, p
))
232 path
= BranchFile(b
, p
, path2inventory(b
, p
))
236 components
= path
.path
.strip('/').split('/')
237 cumulative
= [b
.name()]
240 url
= link_to(*cumulative
) + '/'
241 breadcrumb
.append([c
, url
])
242 breadcrumb
[-1][1] = None # "current" page, so need not link
244 render('browser.html',
245 title
="%s%s" % (b
.name(), path
.path
),
247 branch_url
=BRANCH_URL
+b
.name(),
249 breadcrumb
=breadcrumb
)