Package trac :: Package versioncontrol :: Package web_ui :: Module util

Source Code for Module trac.versioncontrol.web_ui.util

  1  # -*- coding: utf-8 -*- 
  2  # 
  3  # Copyright (C) 2003-2020 Edgewall Software 
  4  # Copyright (C) 2003-2005 Jonas Borgström <[email protected]> 
  5  # Copyright (C) 2005-2007 Christian Boos <[email protected]> 
  6  # All rights reserved. 
  7  # 
  8  # This software is licensed as described in the file COPYING, which 
  9  # you should have received as part of this distribution. The terms 
 10  # are also available at https://trac.edgewall.org/wiki/TracLicense. 
 11  # 
 12  # This software consists of voluntary contributions made by many 
 13  # individuals. For the exact contribution history, see the revision 
 14  # history and logs, available at https://trac.edgewall.org/log/. 
 15  # 
 16  # Author: Jonas Borgström <[email protected]> 
 17  #         Christian Boos <[email protected]> 
 18   
 19  from itertools import izip 
 20  from tempfile import TemporaryFile 
 21  from zipfile import ZipFile, ZIP_DEFLATED 
 22   
 23  from genshi.builder import tag 
 24   
 25  from trac.resource import ResourceNotFound 
 26  from trac.util import content_disposition, create_zipinfo 
 27  from trac.util.datefmt import http_date 
 28  from trac.util.translation import tag_, _ 
 29  from trac.versioncontrol.api import EmptyChangeset, NoSuchChangeset, \ 
 30                                      NoSuchNode 
 31  from trac.web.api import RequestDone 
 32   
 33  __all__ = ['content_closing', 'get_changes', 'get_path_links', 
 34             'get_existing_node', 'get_allowed_node', 'make_log_graph', 
 35             'render_zip'] 
 36   
 37   
38 -class content_closing(object):
39
40 - def __init__(self, content):
41 self.content = content
42
43 - def __enter__(self):
44 return self.content
45
46 - def __exit__(self, *exc_info):
47 if hasattr(self.content, 'close'): 48 self.content.close()
49 50
51 -def get_changes(repos, revs, log=None):
52 changes = {} 53 for rev in revs: 54 if rev in changes: 55 continue 56 try: 57 changeset = repos.get_changeset(rev) 58 except NoSuchChangeset: 59 changeset = EmptyChangeset(repos, rev) 60 if log is not None: 61 log.warning("Unable to get changeset [%s] in %s", rev, 62 repos.reponame or '(default)') 63 changes[rev] = changeset 64 return changes
65 66 85 86
87 -def get_existing_node(req, repos, path, rev):
88 try: 89 return repos.get_node(path, rev) 90 except NoSuchNode as e: 91 # TRANSLATOR: You can 'search' in the repository history... (link) 92 search_a = tag.a(_("search"), 93 href=req.href.log(repos.reponame or None, path, 94 rev=rev, mode='path_history')) 95 raise ResourceNotFound(tag( 96 tag.p(e, class_="message"), 97 tag.p(tag_("You can %(search)s in the repository history to see " 98 "if that path existed but was later removed", 99 search=search_a))))
100 101
102 -def get_allowed_node(repos, path, rev, perm):
103 if repos is not None: 104 try: 105 node = repos.get_node(path, rev) 106 except (NoSuchNode, NoSuchChangeset): 107 return None 108 if node.is_viewable(perm): 109 return node
110 111
112 -def make_log_graph(repos, revs):
113 """Generate graph information for the given revisions. 114 115 Returns a tuple `(threads, vertices, columns)`, where: 116 117 * `threads`: List of paint command lists `[(type, column, line)]`, where 118 `type` is either 0 for "move to" or 1 for "line to", and `column` and 119 `line` are coordinates. 120 * `vertices`: List of `(column, thread_index)` tuples, where the `i`th 121 item specifies the column in which to draw the dot in line `i` and the 122 corresponding thread. 123 * `columns`: Maximum width of the graph. 124 """ 125 threads = [] 126 vertices = [] 127 columns = 0 128 revs = iter(revs) 129 130 def add_edge(thread, column, line): 131 if thread and thread[-1][:2] == [1, column] \ 132 and thread[-2][1] == column: 133 thread[-1][2] = line 134 else: 135 thread.append([1, column, line])
136 137 try: 138 next_rev = revs.next() 139 line = 0 140 active = [] 141 active_thread = [] 142 while True: 143 rev = next_rev 144 if rev not in active: 145 # Insert new head 146 threads.append([[0, len(active), line]]) 147 active_thread.append(threads[-1]) 148 active.append(rev) 149 150 columns = max(columns, len(active)) 151 column = active.index(rev) 152 vertices.append((column, threads.index(active_thread[column]))) 153 154 next_rev = revs.next() # Raises StopIteration when no more revs 155 next = active[:] 156 parents = list(repos.parent_revs(rev)) 157 158 # Replace current item with parents not already present 159 new_parents = [p for p in parents if p not in active] 160 next[column:column + 1] = new_parents 161 162 # Add edges to parents 163 for col, (r, thread) in enumerate(izip(active, active_thread)): 164 if r in next: 165 add_edge(thread, next.index(r), line + 1) 166 elif r == rev: 167 if new_parents: 168 parents.remove(new_parents[0]) 169 parents.append(new_parents[0]) 170 for parent in parents: 171 if parent != parents[0]: 172 thread.append([0, col, line]) 173 add_edge(thread, next.index(parent), line + 1) 174 175 if not new_parents: 176 del active_thread[column] 177 else: 178 base = len(threads) 179 threads.extend([[0, column + 1 + i, line + 1]] 180 for i in xrange(len(new_parents) - 1)) 181 active_thread[column + 1:column + 1] = threads[base:] 182 183 active = next 184 line += 1 185 except StopIteration: 186 pass 187 return threads, vertices, columns 188 189
190 -def render_zip(req, filename, repos, root_node, iter_nodes):
191 """Send a ZIP file containing the data corresponding to the `nodes` 192 iterable. 193 194 :type root_node: `~trac.versioncontrol.api.Node` 195 :param root_node: optional ancestor for all the *nodes* 196 197 :param iter_nodes: callable taking the optional *root_node* as input 198 and generating the `~trac.versioncontrol.api.Node` 199 for which the content should be added into the zip. 200 """ 201 req.send_response(200) 202 req.send_header('Content-Type', 'application/zip') 203 req.send_header('Content-Disposition', 204 content_disposition('inline', filename)) 205 if root_node: 206 req.send_header('Last-Modified', http_date(root_node.last_modified)) 207 root_path = root_node.path.rstrip('/') 208 else: 209 root_path = '' 210 if root_path: 211 root_path += '/' 212 root_name = root_node.name + '/' 213 else: 214 root_name = '' 215 root_len = len(root_path) 216 req.end_headers() 217 218 def write_partial(fileobj, start): 219 end = fileobj.tell() 220 fileobj.seek(start, 0) 221 remaining = end - start 222 while remaining > 0: 223 chunk = fileobj.read(min(remaining, 4096)) 224 req.write(chunk) 225 remaining -= len(chunk) 226 fileobj.seek(end, 0) 227 return end
228 229 pos = 0 230 fileobj = TemporaryFile(prefix='trac-', suffix='.zip') 231 try: 232 zipfile = ZipFile(fileobj, 'w', ZIP_DEFLATED) 233 for node in iter_nodes(root_node): 234 if node is root_node: 235 continue 236 path = node.path.strip('/') 237 assert path.startswith(root_path) 238 path = root_name + path[root_len:] 239 kwargs = {'mtime': node.last_modified} 240 data = None 241 if node.isfile: 242 with content_closing( 243 node.get_processed_content(eol_hint='CRLF')) \ 244 as content: 245 data = content.read() 246 properties = node.get_properties() 247 # Subversion specific 248 if 'svn:special' in properties and data.startswith('link '): 249 data = data[5:] 250 kwargs['symlink'] = True 251 if 'svn:executable' in properties: 252 kwargs['executable'] = True 253 elif node.isdir and path: 254 kwargs['dir'] = True 255 data = '' 256 if data is not None: 257 zipfile.writestr(create_zipinfo(path, **kwargs), data) 258 pos = write_partial(fileobj, pos) 259 finally: 260 try: 261 zipfile.close() 262 write_partial(fileobj, pos) 263 finally: 264 fileobj.close() 265 raise RequestDone 266