# # # patch "mtn.py" # from [8fbed63f4646db8406ddcebf20e85697ce1f7e01] # to [219dcc3089d7a5cdaf9b4be6d27992208f3100c9] # # patch "viewmtn.py" # from [b025f7cc219e5597bb29cb146a74bb91c59e691b] # to [f175d4989b3a34435d6f5df3ce800b020a29a13d] # ============================================================ --- mtn.py 8fbed63f4646db8406ddcebf20e85697ce1f7e01 +++ mtn.py 219dcc3089d7a5cdaf9b4be6d27992208f3100c9 @@ -101,7 +101,7 @@ class Automate(Runner): self.process.childerr ]) def run(self, *args, **kwargs): -# debug(("automate is running:", args, kwargs)) + #debug(("automate is running:", args, kwargs)) lock = self.lock stop = self.stop @@ -407,6 +407,10 @@ class Operations: continue yield apply(Revision, (line,)) + def get_corresponding_path(self, revision1, path, revision2): + for stanza in basic_io_from_stream(self.automate.run('get_corresponding_path', [revision1, path, revision2])): + yield stanza + def get_content_changed(self, revision, path): for stanza in basic_io_from_stream(self.automate.run('get_content_changed', [revision, path])): yield stanza @@ -432,7 +436,6 @@ class Operations: for line in self.standalone.run('diff', args): yield line - ### ### vi:expandtab:sw=4:ts=4 ### ============================================================ --- viewmtn.py b025f7cc219e5597bb29cb146a74bb91c59e691b +++ viewmtn.py f175d4989b3a34435d6f5df3ce800b020a29a13d @@ -16,7 +16,6 @@ import json import sys import web import json -import heapq import struct import string import rfc822 @@ -33,6 +32,7 @@ hq = cgi.escape from fdo import sharedmimeinfo, icontheme import release hq = cgi.escape +import heapq import web debug = web.debug @@ -296,6 +296,22 @@ def link(obj, link_type=None, **kwargs): kwargs['link_type'] = link_type return link_class(obj, **kwargs) +class ComparisonRev: + def __init__(self, ops, revision): + self.revision = revision + self.certs = map (None, ops.certs(self.revision)) + self.date = None + for cert in self.certs: + if cert[4] == 'name' and cert[5] == 'date': + self.date = common.parse_timecert(cert[7]) + def __repr__(self): + return "ComparisonRev <%s>" % (repr(self.revision)) + def __cmp__(self, other): + # irritating edge-case, heapq compares us to empty string if + # there's only one thing in the list + if not other: return 1 + return cmp(other.date, self.date) + class Renderer: def __init__(self): # any templates that can be inherited from, should be added to the list here @@ -374,54 +390,35 @@ class Help: def GET(self): renderer.render('help.html', page_title="Help") -class BranchChanges: - def get_last_changes(self, branch, heads, from_change, to_change): +class Changes: + def __get_last_changes(self, start_from, parent_func, selection_func, n): + """returns at least n revisions that are parents of the revisions in start_from, + ordered by time (descending). selection_func is called for each revision, and + that revision is only included in the result if the function returns True.""" # revised algorithm in colaboration with Matthias Radestock # # use a heapq to keep a list of interesting revisions # pop from the heapq; get the most recent interesting revision # insert the parents of this revision into the heap # .. repeat until len(heap) >= to_count - class ComparisonRev: - def __init__(self, revision): - self.revision = revision - self.certs = map (None, ops.certs(self.revision)) - self.date = None - for cert in self.certs: - if cert[4] == 'name' and cert[5] == 'date': - self.date = common.parse_timecert(cert[7]) - def __cmp__(self, other): - # irritating edge-case, heapq compares us to empty string if - # there's only one thing in the list - if not other: return 1 - return cmp(other.date, self.date) + if not start_from: + raise Exception("get_last_changes() unable to find somewhere to start.") - def on_our_branch(r): - rv = False - for cert in ops.certs(r): - if cert[4] == 'name' and cert[5] == 'branch': - if cert[7] == branch.name: - rv = True - return rv - - if not heads: - raise Exception("get_last_changes() unable to find somewhere to start - probably a non-existent branch?") - last_result = None in_result = set() result = [] revq = [] - for rev in heads: - heapq.heappush(revq, ComparisonRev(rev)) - while len(result) < to_change: -# print >>sys.stderr, "start_revq state:", map(lambda x: (x.revision, x.date), revq) + for rev in start_from: + heapq.heappush(revq, ComparisonRev(ops, rev)) + while len(result) < n: + # print >>sys.stderr, "start_revq state:", map(lambda x: (x.revision, x.date), revq) # update based on the last result we output if last_result != None: - parents = filter(None, ops.parents(last_result.revision)) + parents = filter(None, parent_func(last_result.revision)) for parent_rev in parents: - if parent_rev == None or not on_our_branch(parent_rev): + if parent_rev == None or not selection_func(parent_rev): continue - heapq.heappush(revq, ComparisonRev(parent_rev)) + heapq.heappush(revq, ComparisonRev(ops, parent_rev)) # try and find something we haven't already output in the heap last_result = None @@ -435,70 +432,82 @@ class BranchChanges: # follow the newest edge in_result.add(last_result.revision) result.append(last_result) - - rv = map (lambda x: (x.revision, x.certs), result[from_change:to_change]), revq + + rv = map (lambda x: (x.revision, x.certs), result), revq return rv - def GET(self, branch, from_change, to_change, template_name): - def for_template(revs): - rv = [] - for rev, certs in revs: - rev_branch = "" - revision, diffs, ago, author, changelog, shortlog, when = mtn.Revision(rev), [], "", mtn.Author(""), "", "", "" - for cert in certs: - if cert[4] != 'name': - continue - if cert[5] == "branch": - rev_branch = cert[7] - elif cert[5] == 'date': - revdate = common.parse_timecert(cert[7]) - ago = common.ago(revdate) - when = revdate.strftime('%a, %d %b %Y %H:%M:%S GMT') - elif cert[5] == 'author': - author = mtn.Author(cert[7]) - elif cert[5] == 'changelog': - changelog = normalise_changelog(cert[7]) # NB: this HTML escapes! - shortlog = quicklog(changelog) # so this is also HTML escaped. - if rev_branch != branch.name: - # yikes, fallen down a well + def on_our_branch(self, branch, revision): + rv = False + for cert in ops.certs(revision): + if cert[4] == 'name' and cert[5] == 'branch': + if cert[7] == branch.name: + rv = True + return rv + + def for_template(self, revs): + rv = [] + for rev, certs in revs: + rev_branch = "" + revision, diffs, ago, author, changelog, shortlog, when = mtn.Revision(rev), [], "", mtn.Author(""), "", "", "" + for cert in certs: + if cert[4] != 'name': continue - for stanza in ops.get_revision(rev): - if stanza and stanza[0] == "old_revision": - old_revision = stanza[1] - diffs.append(Diff(mtn.Revision(old_revision), revision)) - if diffs: - diffs = '| ' + ', '.join([link(d).html('diff') for d in diffs]) - else: - diffs = '' - rv.append((revision, diffs, ago, mtn.Author(author), '
\n'.join(changelog), shortlog, when)) - return rv - - branch = mtn.Branch(branch) + if cert[5] == "branch": + rev_branch = cert[7] + elif cert[5] == 'date': + revdate = common.parse_timecert(cert[7]) + ago = common.ago(revdate) + when = revdate.strftime('%a, %d %b %Y %H:%M:%S GMT') + elif cert[5] == 'author': + author = mtn.Author(cert[7]) + elif cert[5] == 'changelog': + changelog = normalise_changelog(cert[7]) # NB: this HTML escapes! + shortlog = quicklog(changelog) # so this is also HTML escaped. + for stanza in ops.get_revision(rev): + if stanza and stanza[0] == "old_revision": + old_revision = stanza[1] + diffs.append(Diff(mtn.Revision(old_revision), revision)) + if diffs: + diffs = '| ' + ', '.join([link(d).html('diff') for d in diffs]) + else: + diffs = '' + rv.append((revision, diffs, ago, mtn.Author(author), '
\n'.join(changelog), shortlog, when)) + return rv + + def determine_bounds(self, from_change, to_change): + per_page = 10 + max_per_page = 100 + if from_change: from_change = int(from_change) + else: from_change = 0 + if to_change: to_change = int(to_change) + else: to_change = per_page + if to_change - from_change > max_per_page: + to_change = from_change + max_per_page + next_from = to_change + next_to = to_change + per_page + previous_from = from_change - per_page + previous_to = previous_from + per_page + return (from_change, to_change, next_from, next_to, previous_from, previous_to) + + def branch_get_last_changes(self, branch, from_change, to_change): heads = [t for t in ops.heads(branch.name)] if not heads: return web.notfound() - per_page = 10 - if from_change: - from_change = int(from_change) - else: - from_change = 0 - if to_change: - to_change = int(to_change) - else: - to_change = per_page - changed, new_starting_point = self.get_last_changes(branch, heads, from_change, to_change) - # next and previous 'from' and 'to' indexes - if len(changed) == to_change - from_change: - next_from, next_to = to_change, to_change + per_page - else: + changed, new_starting_point = self.__get_last_changes(heads, + lambda r: ops.parents(r), + lambda r: self.on_our_branch(branch, r), + to_change) + return changed, new_starting_point + + def Branch_GET(self, branch, from_change, to_change, template_name): + branch = mtn.Branch(branch) + from_change, to_change, next_from, next_to, previous_from, previous_to = self.determine_bounds(from_change, to_change) + changed, new_starting_point = self.branch_get_last_changes(branch, from_change, to_change) + changed = changed[from_change:to_change] + if len(changed) != to_change - from_change: next_from, next_to = None, None - if from_change > 0: - previous_from = from_change - per_page - if previous_from < 0: previous_from = 0 - previous_to = previous_from + per_page - else: + if from_change <= 0: previous_from, previous_to = None, None - renderer.render(template_name, page_title="Branch %s" % branch.name, branch=branch, @@ -508,15 +517,54 @@ class BranchChanges: previous_to=previous_to, next_from=next_from, next_to=next_to, - display_revs=for_template(changed)) + display_revs=self.for_template(changed)) + + def file_get_last_changes(self, from_change, to_change, revision, path): + def content_changed_fn(in_revision): + uniq = set() + parents = map(None, ops.parents(in_revision)) + for parent in parents: + stanza = map(None, ops.get_corresponding_path(revision, path, parent)) + # file does not exist in this revision; skip! + if not stanza: + continue + stanza = map(None, ops.get_content_changed(parent, path)) + uniq.add(stanza[0][1]) + return list(uniq) + start_at = content_changed_fn(revision) # not just the starting revision! we might not have changed 'path' in the starting rev.. + changed, new_starting_point = self.__get_last_changes(start_at, + content_changed_fn, + lambda r: True, + to_change) + return changed, new_starting_point + + def File_GET(self, from_change, to_change, revision, path, template_name): + from_change, to_change, next_from, next_to, previous_from, previous_to = self.determine_bounds(from_change, to_change) + changed, new_starting_point = self.file_get_last_changes(from_change, to_change, revision, path) + changed = changed[from_change:to_change] + if len(changed) != to_change - from_change: + next_from, next_to = None, None + if from_change <= 0: + previous_from, previous_to = None, None + renderer.render(template_name, + page_title="Changes to '%s' (from %s)" % (cgi.escape(path), revision.abbrev()), + path=path, + revision=revision, + from_change=from_change, + to_change=to_change, + previous_from=previous_from, + previous_to=previous_to, + next_from=next_from, + next_to=next_to, + display_revs=self.for_template(changed)) -class HTMLBranchChanges(BranchChanges): +class HTMLBranchChanges(Changes): def GET(self, branch, from_change, to_change): - BranchChanges.GET(self, branch, from_change, to_change, "branchchanges.html") + Changes.Branch_GET(self, branch, from_change, to_change, "branchchanges.html") -class RSSBranchChanges(BranchChanges): +class RSSBranchChanges(Changes): def GET(self, branch, from_change, to_change): - BranchChanges.GET(self, branch, from_change, to_change, "branchchangesrss.html") + Changes.Branch_GET(self, branch, from_change, to_change, "branchchangesrss.html") class RevisionPage(object): def get_fileid(self, revision, filename): @@ -540,6 +588,20 @@ class RevisionPage(object): rv.append(stanza[7]) return rv +class RevisionFileChanges(Changes, RevisionPage): + def GET(self, from_change, to_change, revision, path): + revision = mtn.Revision(revision) + if not self.exists(revision): + return web.notfound() + Changes.File_GET(self, from_change, to_change, revision, path, "revisionfilechanges.html") + +class RevisionFileChangesRSS(Changes, RevisionPage): + def GET(self, from_change, to_change, revision, path): + revision = mtn.Revision(revision) + if not self.exists(revision): + return web.notfound() + Changes.File_GET(self, from_change, to_change, revision, path, "revisionfilechangesrss.html") + class RevisionInfo(RevisionPage): def GET(self, revision): revision = mtn.Revision(revision) @@ -1007,10 +1069,9 @@ class Json(object): 'branch' : for_branch, } branch = mtn.Branch(for_branch) - changes = BranchChanges().get_last_changes(branch, [t for t in ops.heads(branch.name)], 0, 1) + changes, new_starting_point = Changes().branch_get_last_changes(branch, 0, 1) if len(changes) < 1: return web.notfound() - changes, new_starting_point = changes if not changes: rv['error_string'] = 'no revisions in branch' else: @@ -1092,6 +1153,7 @@ class RobotsTxt: # as this is an enormous amount of information. for access_method in ['/revision/', '/branch/head/', '/branch/anyhead/', '/branch/changes/from/', '/json/', '/mimeicon/']: print "Disallow:", access_method + revision_page + urls = ( r'/', 'Index', #done r'/about', 'About', #done @@ -1104,6 +1166,9 @@ urls = ( r'/revision/diff/('+mtn.revision_re+')/with/('+mtn.revision_re+')', 'RevisionDiff', r'/revision/diff/('+mtn.revision_re+')/with/('+mtn.revision_re+')'+'/(.*)', 'RevisionDiff', r'/revision/file/('+mtn.revision_re+')/(.*)', 'RevisionFile', + r'/revision/filechanges/()()('+mtn.revision_re+')/(.*)', 'RevisionFileChanges', + r'/revision/filechanges/from/(\d+)/to/(\d+)/('+mtn.revision_re+')/(.*)', 'RevisionFileChanges', + r'/revision/filechanges/rss/('+mtn.revision_re+')/(.*)', 'RevisionFileChangesRSS', r'/revision/downloadfile/('+mtn.revision_re+')/(.*)', 'RevisionDownloadFile', r'/revision/info/('+mtn.revision_re+')', 'RevisionInfo', r'/revision/tar/('+mtn.revision_re+')', 'RevisionTar',