Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/cache.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/cache.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/cache.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/cache.py Sat Nov 15 01:14:46 2014 @@ -101,7 +101,7 @@ class CachedRepository(Repository): """, (to_utimestamp(cset.date), cset.author, cset.message, self.id, srev)) else: - self._insert_changeset(db, rev, cset) + self._insert_changeset(db, cset.rev, cset) return old_cset @cached('_metadata_id') @@ -115,62 +115,10 @@ class CachedRepository(Repository): def sync(self, feedback=None, clean=False): if clean: - self.log.info("Cleaning cache") - with self.env.db_transaction as db: - db("DELETE FROM revision WHERE repos=%s", - (self.id,)) - db("DELETE FROM node_change WHERE repos=%s", - (self.id,)) - db.executemany("DELETE FROM repository WHERE id=%s AND name=%s", - [(self.id, k) for k in CACHE_METADATA_KEYS]) - db.executemany(""" - INSERT INTO repository (id, name, value) - VALUES (%s, %s, %s) - """, [(self.id, k, '') for k in CACHE_METADATA_KEYS]) - del self.metadata + self.remove_cache() metadata = self.metadata - - with self.env.db_transaction as db: - invalidate = False - - # -- check that we're populating the cache for the correct - # repository - repository_dir = metadata.get(CACHE_REPOSITORY_DIR) - if repository_dir: - # directory part of the repo name can vary on case insensitive - # fs - if os.path.normcase(repository_dir) \ - != os.path.normcase(self.name): - self.log.info("'repository_dir' has changed from %r to %r", - repository_dir, self.name) - raise TracError(_("The repository directory has changed, " - "you should resynchronize the " - "repository with: trac-admin $ENV " - "repository resync '%(reponame)s'", - reponame=self.reponame or '(default)')) - elif repository_dir is None: # - self.log.info('Storing initial "repository_dir": %s', - self.name) - db("""INSERT INTO repository (id, name, value) - VALUES (%s, %s, %s) - """, (self.id, CACHE_REPOSITORY_DIR, self.name)) - invalidate = True - else: # 'repository_dir' cleared by a resync - self.log.info('Resetting "repository_dir": %s', self.name) - db("UPDATE repository SET value=%s WHERE id=%s AND name=%s", - (self.name, self.id, CACHE_REPOSITORY_DIR)) - invalidate = True - - # -- insert a 'youngeset_rev' for the repository if necessary - if metadata.get(CACHE_YOUNGEST_REV) is None: - db("""INSERT INTO repository (id, name, value) - VALUES (%s, %s, %s) - """, (self.id, CACHE_YOUNGEST_REV, '')) - invalidate = True - - if invalidate: - del self.metadata + self.save_metadata(metadata) # -- retrieve the youngest revision in the repository and the youngest # revision cached so far @@ -263,6 +211,65 @@ class CachedRepository(Repository): if feedback: feedback(youngest) + def remove_cache(self): + """Remove the repository cache.""" + self.log.info("Cleaning cache") + with self.env.db_transaction as db: + db("DELETE FROM revision WHERE repos=%s", + (self.id,)) + db("DELETE FROM node_change WHERE repos=%s", + (self.id,)) + db.executemany("DELETE FROM repository WHERE id=%s AND name=%s", + [(self.id, k) for k in CACHE_METADATA_KEYS]) + db.executemany(""" + INSERT INTO repository (id, name, value) + VALUES (%s, %s, %s) + """, [(self.id, k, '') for k in CACHE_METADATA_KEYS]) + del self.metadata + + def save_metadata(self, metadata): + """Save the repository metadata.""" + with self.env.db_transaction as db: + invalidate = False + + # -- check that we're populating the cache for the correct + # repository + repository_dir = metadata.get(CACHE_REPOSITORY_DIR) + if repository_dir: + # directory part of the repo name can vary on case insensitive + # fs + if os.path.normcase(repository_dir) \ + != os.path.normcase(self.name): + self.log.info("'repository_dir' has changed from %r to %r", + repository_dir, self.name) + raise TracError(_("The repository directory has changed, " + "you should resynchronize the " + "repository with: trac-admin $ENV " + "repository resync '%(reponame)s'", + reponame=self.reponame or '(default)')) + elif repository_dir is None: # + self.log.info('Storing initial "repository_dir": %s', + self.name) + db("""INSERT INTO repository (id, name, value) + VALUES (%s, %s, %s) + """, (self.id, CACHE_REPOSITORY_DIR, self.name)) + invalidate = True + else: # 'repository_dir' cleared by a resync + self.log.info('Resetting "repository_dir": %s', self.name) + db("UPDATE repository SET value=%s WHERE id=%s AND name=%s", + (self.name, self.id, CACHE_REPOSITORY_DIR)) + invalidate = True + + # -- insert a 'youngeset_rev' for the repository if necessary + if metadata.get(CACHE_YOUNGEST_REV) is None: + db("""INSERT INTO repository (id, name, value) + VALUES (%s, %s, %s) + """, (self.id, CACHE_YOUNGEST_REV, '')) + invalidate = True + + if invalidate: + del self.metadata + def _insert_changeset(self, db, rev, cset): srev = self.db_rev(rev) # 1. Attempt to resync the 'revision' table. In case of @@ -310,9 +317,64 @@ class CachedRepository(Repository): return [int(rev) for rev, in db(""" SELECT DISTINCT rev FROM node_change WHERE repos=%%s AND rev>=%%s AND rev<=%%s - AND (path=%%s OR path %s)""" % db.like(), + AND (path=%%s OR path %s)""" % db.prefix_match(), (self.id, sfirst, slast, path, - db.like_escape(path + '/') + '%'))] + db.prefix_match_value(path + '/')))] + + def _get_changed_revs(self, node_infos): + if not node_infos: + return {} + + node_infos = [(node, self.normalize_rev(first)) for node, first + in node_infos] + sfirst = self.db_rev(min(first for node, first in node_infos)) + slast = self.db_rev(max(node.rev for node, first in node_infos)) + path_infos = dict((node.path, (node, first)) for node, first + in node_infos) + path_revs = dict((node.path, []) for node, first in node_infos) + + db = self.env.get_read_db() + cursor = db.cursor() + prefix_match = db.prefix_match() + + # Prevent "too many SQL variables" since max number of parameters is + # 999 on SQLite. No limitation on PostgreSQL and MySQL. + idx = 0 + delta = (999 - 3) // 5 + while idx < len(node_infos): + subset = node_infos[idx:idx + delta] + idx += delta + count = len(subset) + + holders = ','.join(('%s',) * count) + query = """\ + SELECT DISTINCT + rev, (CASE WHEN path IN (%s) THEN path %s END) AS path + FROM node_change + WHERE repos=%%s AND rev>=%%s AND rev<=%%s AND (path IN (%s) %s) + """ % \ + (holders, + ' '.join(('WHEN path ' + prefix_match + ' THEN %s',) * count), + holders, + ' '.join(('OR path ' + prefix_match,) * count)) + args = [] + args.extend(node.path for node, first in subset) + for node, first in subset: + args.append(db.prefix_match_value(node.path + '/')) + args.append(node.path) + args.extend((self.id, sfirst, slast)) + args.extend(node.path for node, first in subset) + args.extend(db.prefix_match_value(node.path + '/') + for node, first in subset) + cursor.execute(query, args) + + for srev, path in cursor: + rev = self.rev_db(srev) + node, first = path_infos[path] + if first <= rev <= node.rev: + path_revs[path].append(rev) + + return path_revs def has_node(self, path, rev=None): return self.repos.has_node(path, self.normalize_rev(rev)) @@ -324,10 +386,16 @@ class CachedRepository(Repository): return self.rev_db(self.metadata.get(CACHE_YOUNGEST_REV)) def previous_rev(self, rev, path=''): - if self.has_linear_changesets: - return self._next_prev_rev('<', rev, path) - else: - return self.repos.previous_rev(self.normalize_rev(rev), path) + # Hitting the repository directly is faster than searching the + # database. When there is a long stretch of inactivity on a file (in + # particular, when a file is added late in the history) the database + # query can take a very long time to determine that there is no + # previous revision in the node_changes table. However, the repository + # will have a datastructure that will allow it to find the previous + # version of a node fairly directly. + #if self.has_linear_changesets: + # return self._next_prev_rev('<', rev, path) + return self.repos.previous_rev(self.normalize_rev(rev), path) def next_rev(self, rev, path=''): if self.has_linear_changesets: @@ -346,8 +414,8 @@ class CachedRepository(Repository): if path: path = path.lstrip('/') # changes on path itself or its children - sql += " AND (path=%s OR path " + db.like() - args.extend((path, db.like_escape(path + '/') + '%')) + sql += " AND (path=%s OR path " + db.prefix_match() + args.extend((path, db.prefix_match_value(path + '/'))) # deletion of path ancestors components = path.lstrip('/').split('/') parents = ','.join(('%s',) * len(components)) @@ -361,6 +429,12 @@ class CachedRepository(Repository): for rev, in db(sql, args): return int(rev) + def parent_revs(self, rev): + if self.has_linear_changesets: + return Repository.parent_revs(self, rev) + else: + return self.repos.parent_revs(rev) + def rev_older_than(self, rev1, rev2): return self.repos.rev_older_than(self.normalize_rev(rev1), self.normalize_rev(rev2))
Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/admin_repositories.html URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/admin_repositories.html?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/admin_repositories.html (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/admin_repositories.html Sat Nov 15 01:14:46 2014 @@ -1,3 +1,13 @@ +<!--! Copyright (C) 2009-2014 Edgewall Software + + This software is licensed as described in the file COPYING, which + you should have received as part of this distribution. The terms + are also available at http://trac.edgewall.com/license.html. + + This software consists of voluntary contributions made by many + individuals. For the exact contribution history, see the revision + history and logs, available at http://trac.edgewall.org/. +--> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> @@ -11,7 +21,7 @@ </head> <body py:with="sorted_repos = sorted(repositories.iteritems(), key=lambda item: item[0].lower())"> - <h2>Manage Repositories</h2> + <h2>Manage Repositories <span py:if="view == 'list'" class="trac-count">(${len(repositories)})</span></h2> <py:def function="type_field(editable, multiline=False, selected=None)"> <div class="field"> @@ -39,7 +49,8 @@ <py:choose test="view"> <form py:when="'detail'" py:with="info = repositories[reponame]" class="mod" id="trac-modrepos" method="post" action=""> - <fieldset py:choose="" py:with="readonly = not info.editable or None"> + <fieldset py:choose="" py:with="disabled = 'disabled' if not info.editable else None; + readonly = 'readonly' if not info.editable else None"> <legend py:when="info.editable">Modify Repository:</legend> <legend py:otherwise="">View Repository:</legend> <p py:if="not info.editable" class="hint" i18n:msg=""><strong>Note:</strong> @@ -47,7 +58,8 @@ and cannot be edited on this page. </p> <div class="field"> - <label>Name:<br/><input type="text" name="name" value="$info.name" readonly="$readonly"/></label> + <label>Name:<br/><input type="text" name="name" class="trac-autofocus" + value="$info.name" readonly="$readonly" /></label> </div> <py:choose> <py:when test="'alias' in info"> @@ -64,25 +76,26 @@ </py:otherwise> </py:choose> <div class="field"> - <label><input type="checkbox" name="hidden" value="1" checked="${info.hidden or None}" disabled="$readonly"/> + <label><input type="checkbox" name="hidden" value="1" checked="${info.hidden or None}" + disabled="${not info.editable or None}"/> Hide from repository index </label> </div> <div class="field"> <fieldset> <label for="description" i18n:msg=""> - Description (you may use <a tabindex="42" href="${href.wiki('WikiFormatting')}">WikiFormatting</a> here): + Description: (you may use <a tabindex="42" href="${href.wiki('WikiFormatting')}">WikiFormatting</a> here) </label> <p> - <textarea id="description" name="description" class="wikitext trac-resizable" + <textarea id="description" name="description" class="wikitext trac-fullwidth trac-resizable" rows="6" cols="60" readonly="$readonly"> $info.description</textarea> </p> </fieldset> </div> <div class="buttons"> + <input py:if="info.editable" type="submit" name="save" class="trac-disable-on-submit" value="${_('Save')}"/> <input type="submit" name="cancel" value="${_('Cancel')}"/> - <input py:if="info.editable" type="submit" name="save" value="${_('Save')}"/> </div> </fieldset> </form> @@ -99,7 +112,7 @@ $info.description</textarea> <label>Directory: <input type="text" name="dir"/></label> </div> <div class="buttons"> - <input type="submit" name="add_repos" value="${_('Add')}"/> + <input type="submit" name="add_repos" class="trac-disable-on-submit" value="${_('Add')}"/> </div> </fieldset> </form> @@ -113,12 +126,12 @@ $info.description</textarea> </div> ${alias_field(True)} <div class="buttons"> - <input type="submit" name="add_alias" value="${_('Add')}"/> + <input type="submit" name="add_alias" class="trac-disable-on-submit" value="${_('Add')}"/> </div> </fieldset> </form> - <form id="trac-repository_table" method="post" action=""> + <form py:if="sorted_repos" id="trac-repository_table" method="post" action=""> <table class="listing" id="trac-reposlist"> <thead> <tr><th class="sel"> </th> @@ -142,7 +155,7 @@ $info.description</textarea> </table> <div class="buttons"> <input type="submit" name="refresh" value="${_('Refresh')}"/> - <input type="submit" name="remove" value="${_('Remove selected items')}"/> + <input type="submit" name="remove" class="trac-disable-on-submit" value="${_('Remove selected items')}"/> </div> </form> </py:otherwise> Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/browser.html URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/browser.html?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/browser.html (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/browser.html Sat Nov 15 01:14:46 2014 @@ -1,3 +1,13 @@ +<!--! Copyright (C) 2006-2014 Edgewall Software + + This software is licensed as described in the file COPYING, which + you should have received as part of this distribution. The terms + are also available at http://trac.edgewall.com/license.html. + + This software consists of voluntary contributions made by many + individuals. For the exact contribution history, see the revision + history and logs, available at http://trac.edgewall.org/. +--> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> @@ -96,7 +106,7 @@ </form> </div> - <div class="trac-tags" py:with="changeset = repos.get_changeset(repos.normalize_rev(stickyrev))"> + <div class="trac-tags" py:if="changeset"> <span py:for="branch, head in changeset.get_branches()" py:if="branch not in ('default', 'master')" class="branch${' head' if head else ''}" title="${_('Branch head') if head else _('Branch')}">${branch}</span> Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/changeset.html URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/changeset.html?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/changeset.html (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/changeset.html Sat Nov 15 01:14:46 2014 @@ -1,3 +1,13 @@ +<!--! Copyright (C) 2006-2014 Edgewall Software + + This software is licensed as described in the file COPYING, which + you should have received as part of this distribution. The terms + are also available at http://trac.edgewall.com/license.html. + + This software consists of voluntary contributions made by many + individuals. For the exact contribution history, see the revision + history and logs, available at http://trac.edgewall.org/. +--> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/diff_form.html URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/diff_form.html?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/diff_form.html (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/diff_form.html Sat Nov 15 01:14:46 2014 @@ -1,3 +1,13 @@ +<!--! Copyright (C) 2006-2014 Edgewall Software + + This software is licensed as described in the file COPYING, which + you should have received as part of this distribution. The terms + are also available at http://trac.edgewall.com/license.html. + + This software consists of voluntary contributions made by many + individuals. For the exact contribution history, see the revision + history and logs, available at http://trac.edgewall.org/. +--> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/dir_entries.html URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/dir_entries.html?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/dir_entries.html (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/dir_entries.html Sat Nov 15 01:14:46 2014 @@ -1,4 +1,15 @@ -<!--! Template for generating rows corresponding to directory entries --> +<!--! Copyright (C) 2007-2014 Edgewall Software + + This software is licensed as described in the file COPYING, which + you should have received as part of this distribution. The terms + are also available at http://trac.edgewall.com/license.html. + + This software consists of voluntary contributions made by many + individuals. For the exact contribution history, see the revision + history and logs, available at http://trac.edgewall.org/. + +Template for generating rows corresponding to directory entries +--> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://genshi.edgewall.org/" xmlns:xi="http://www.w3.org/2001/XInclude" py:strip=""> Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/dirlist_thead.html URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/dirlist_thead.html?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/dirlist_thead.html (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/dirlist_thead.html Sat Nov 15 01:14:46 2014 @@ -1,4 +1,15 @@ -<!--! Template snippet for a standard table header for a dirlist --> +<!--! Copyright (C) 2008-2014 Edgewall Software + + This software is licensed as described in the file COPYING, which + you should have received as part of this distribution. The terms + are also available at http://trac.edgewall.com/license.html. + + This software consists of voluntary contributions made by many + individuals. For the exact contribution history, see the revision + history and logs, available at http://trac.edgewall.org/. + +Template snippet for a standard table header for a dirlist +--> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://genshi.edgewall.org/" xmlns:xi="http://www.w3.org/2001/XInclude" py:strip=""> Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/path_links.html URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/path_links.html?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/path_links.html (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/path_links.html Sat Nov 15 01:14:46 2014 @@ -1,3 +1,13 @@ +<!--! Copyright (C) 2008-2014 Edgewall Software + + This software is licensed as described in the file COPYING, which + you should have received as part of this distribution. The terms + are also available at http://trac.edgewall.com/license.html. + + This software consists of voluntary contributions made by many + individuals. For the exact contribution history, see the revision + history and logs, available at http://trac.edgewall.org/. +--> <div xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://genshi.edgewall.org/" py:strip=""> <!--! Display a sequence of path components. Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/repository_index.html URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/repository_index.html?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/repository_index.html (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/repository_index.html Sat Nov 15 01:14:46 2014 @@ -1,4 +1,15 @@ -<!--! Template snippet for a table of repositories --> +<!--! Copyright (C) 2008-2014 Edgewall Software + + This software is licensed as described in the file COPYING, which + you should have received as part of this distribution. The terms + are also available at http://trac.edgewall.com/license.html. + + This software consists of voluntary contributions made by many + individuals. For the exact contribution history, see the revision + history and logs, available at http://trac.edgewall.org/. + +Template snippet for a table of repositories +--> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://genshi.edgewall.org/" xmlns:xi="http://www.w3.org/2001/XInclude" py:strip=""> @@ -11,11 +22,11 @@ <tr class="${'odd' if idx % 2 else 'even'}"> <td class="name"> <em py:strip="not err"> - <b py:strip="repoinfo.alias != ''"> + <strong py:strip="repoinfo.alias != ''"> <a class="dir" title="View Root Directory" href="${href.browser(repos.reponame if repos else reponame, order=order if order != 'name' else None, desc=desc)}">$reponame</a> - </b> + </strong> </em> </td> <td class="size"> Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/revisionlog.html URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/revisionlog.html?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/revisionlog.html (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/revisionlog.html Sat Nov 15 01:14:46 2014 @@ -1,3 +1,13 @@ +<!--! Copyright (C) 2006-2014 Edgewall Software + + This software is licensed as described in the file COPYING, which + you should have received as part of this distribution. The terms + are also available at http://trac.edgewall.com/license.html. + + This software consists of voluntary contributions made by many + individuals. For the exact contribution history, see the revision + history and logs, available at http://trac.edgewall.org/. +--> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/sortable_th.html URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/sortable_th.html?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/sortable_th.html (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/templates/sortable_th.html Sat Nov 15 01:14:46 2014 @@ -1,4 +1,14 @@ -<!--! Snippet for a <th> corresponding to a sortable column. +<!--! Copyright (C) 2008-2014 Edgewall Software + + This software is licensed as described in the file COPYING, which + you should have received as part of this distribution. The terms + are also available at http://trac.edgewall.com/license.html. + + This software consists of voluntary contributions made by many + individuals. For the exact contribution history, see the revision + history and logs, available at http://trac.edgewall.org/. + +Snippet for a <th> corresponding to a sortable column. Expects the following variables to be set specifically: Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/tests/__init__.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/tests/__init__.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/tests/__init__.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/tests/__init__.py Sat Nov 15 01:14:46 2014 @@ -1,3 +1,16 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2013 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://trac.edgewall.org/wiki/TracLicense. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://trac.edgewall.org/log/. + import unittest from trac.versioncontrol.tests import cache, diff, svn_authz, api Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/tests/api.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/tests/api.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/tests/api.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/tests/api.py Sat Nov 15 01:14:46 2014 @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # +# Copyright (C) 2007-2013 Edgewall Software # Copyright (C) 2007 CommProve, Inc. <eli.car...@commprove.com> # All rights reserved. # @@ -27,40 +28,40 @@ class ApiTestCase(unittest.TestCase): None) def test_raise_NotImplementedError_close(self): - self.failUnlessRaises(NotImplementedError, self.repo_base.close) + self.assertRaises(NotImplementedError, self.repo_base.close) def test_raise_NotImplementedError_get_changeset(self): - self.failUnlessRaises(NotImplementedError, self.repo_base.get_changeset, 1) + self.assertRaises(NotImplementedError, self.repo_base.get_changeset, 1) def test_raise_NotImplementedError_get_node(self): - self.failUnlessRaises(NotImplementedError, self.repo_base.get_node, 'path') + self.assertRaises(NotImplementedError, self.repo_base.get_node, 'path') def test_raise_NotImplementedError_get_oldest_rev(self): - self.failUnlessRaises(NotImplementedError, self.repo_base.get_oldest_rev) + self.assertRaises(NotImplementedError, self.repo_base.get_oldest_rev) def test_raise_NotImplementedError_get_youngest_rev(self): - self.failUnlessRaises(NotImplementedError, self.repo_base.get_youngest_rev) + self.assertRaises(NotImplementedError, self.repo_base.get_youngest_rev) def test_raise_NotImplementedError_previous_rev(self): - self.failUnlessRaises(NotImplementedError, self.repo_base.previous_rev, 1) + self.assertRaises(NotImplementedError, self.repo_base.previous_rev, 1) def test_raise_NotImplementedError_next_rev(self): - self.failUnlessRaises(NotImplementedError, self.repo_base.next_rev, 1) + self.assertRaises(NotImplementedError, self.repo_base.next_rev, 1) def test_raise_NotImplementedError_rev_older_than(self): - self.failUnlessRaises(NotImplementedError, self.repo_base.rev_older_than, 1, 2) + self.assertRaises(NotImplementedError, self.repo_base.rev_older_than, 1, 2) def test_raise_NotImplementedError_get_path_history(self): - self.failUnlessRaises(NotImplementedError, self.repo_base.get_path_history, 'path') + self.assertRaises(NotImplementedError, self.repo_base.get_path_history, 'path') def test_raise_NotImplementedError_normalize_path(self): - self.failUnlessRaises(NotImplementedError, self.repo_base.normalize_path, 'path') + self.assertRaises(NotImplementedError, self.repo_base.normalize_path, 'path') def test_raise_NotImplementedError_normalize_rev(self): - self.failUnlessRaises(NotImplementedError, self.repo_base.normalize_rev, 1) + self.assertRaises(NotImplementedError, self.repo_base.normalize_rev, 1) def test_raise_NotImplementedError_get_changes(self): - self.failUnlessRaises(NotImplementedError, self.repo_base.get_changes, 'path', 1, 'path', 2) + self.assertRaises(NotImplementedError, self.repo_base.get_changes, 'path', 1, 'path', 2) class ResourceManagerTestCase(unittest.TestCase): @@ -110,13 +111,19 @@ class ResourceManagerTestCase(unittest.T self.assertEqual('/trac.cgi/browser/testrepo', get_resource_url(self.env, res, self.env.href)) + res = Resource('repository', '') # default repository + self.assertEqual('Default repository', + get_resource_description(self.env, res)) + self.assertEqual('/trac.cgi/browser', + get_resource_url(self.env, res, self.env.href)) + def suite(): suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(ApiTestCase, 'test')) - suite.addTest(unittest.makeSuite(ResourceManagerTestCase, 'test')) + suite.addTest(unittest.makeSuite(ApiTestCase)) + suite.addTest(unittest.makeSuite(ResourceManagerTestCase)) return suite if __name__ == '__main__': - unittest.main() + unittest.main(defaultTest='suite') Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/tests/cache.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/tests/cache.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/tests/cache.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/tests/cache.py Sat Nov 15 01:14:46 2014 @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C)2005-2009 Edgewall Software +# Copyright (C) 2005-2013 Edgewall Software # Copyright (C) 2005 Christopher Lenz <cml...@gmx.de> # All rights reserved. # @@ -18,6 +18,7 @@ from __future__ import with_statement from datetime import datetime +import trac.tests.compat from trac.test import EnvironmentStub, Mock from trac.util.datefmt import to_utimestamp, utc from trac.versioncontrol import Repository, Changeset, Node, NoSuchChangeset @@ -81,9 +82,9 @@ class CacheTestCase(unittest.TestCase): cache.sync() with self.env.db_query as db: - self.assertEquals([], db( + self.assertEqual([], db( "SELECT rev, time, author, message FROM revision")) - self.assertEquals(0, db("SELECT COUNT(*) FROM node_change")[0][0]) + self.assertEqual(0, db("SELECT COUNT(*) FROM node_change")[0][0]) def test_initial_sync(self): t1 = datetime(2001, 1, 1, 1, 1, 1, 0, utc) @@ -101,17 +102,17 @@ class CacheTestCase(unittest.TestCase): with self.env.db_query as db: rows = db("SELECT rev, time, author, message FROM revision") - self.assertEquals(len(rows), 2) - self.assertEquals(('0', to_utimestamp(t1), '', ''), rows[0]) - self.assertEquals(('1', to_utimestamp(t2), 'joe', 'Import'), - rows[1]) + self.assertEqual(len(rows), 2) + self.assertEqual(('0', to_utimestamp(t1), '', ''), rows[0]) + self.assertEqual(('1', to_utimestamp(t2), 'joe', 'Import'), + rows[1]) rows = db(""" SELECT rev, path, node_type, change_type, base_path, base_rev FROM node_change""") - self.assertEquals(len(rows), 2) - self.assertEquals(('1', 'trunk', 'D', 'A', None, None), rows[0]) - self.assertEquals(('1', 'trunk/README', 'F', 'A', None, None), - rows[1]) + self.assertEqual(len(rows), 2) + self.assertEqual(('1', 'trunk', 'D', 'A', None, None), rows[0]) + self.assertEqual(('1', 'trunk/README', 'F', 'A', None, None), + rows[1]) def test_update_sync(self): t1 = datetime(2001, 1, 1, 1, 1, 1, 0, utc) @@ -137,10 +138,10 @@ class CacheTestCase(unittest.TestCase): cache.sync() with self.env.db_query as db: - self.assertEquals([(to_utimestamp(t3), 'joe', 'Update')], + self.assertEqual([(to_utimestamp(t3), 'joe', 'Update')], db("SELECT time, author, message FROM revision WHERE rev='2'")) - self.assertEquals([('trunk/README', 'F', 'E', 'trunk/README', - '1')], + self.assertEqual([('trunk/README', 'F', 'E', 'trunk/README', + '1')], db("""SELECT path, node_type, change_type, base_path, base_rev FROM node_change WHERE rev='2'""")) @@ -175,20 +176,20 @@ class CacheTestCase(unittest.TestCase): rows = self.env.db_query(""" SELECT time, author, message FROM revision ORDER BY rev """) - self.assertEquals(3, len(rows)) - self.assertEquals((to_utimestamp(t1), 'joe', '**empty**'), rows[0]) - self.assertEquals((to_utimestamp(t2), 'joe', 'Initial Import'), - rows[1]) - self.assertEquals((to_utimestamp(t3), 'joe', 'Update'), rows[2]) + self.assertEqual(3, len(rows)) + self.assertEqual((to_utimestamp(t1), 'joe', '**empty**'), rows[0]) + self.assertEqual((to_utimestamp(t2), 'joe', 'Initial Import'), + rows[1]) + self.assertEqual((to_utimestamp(t3), 'joe', 'Update'), rows[2]) rows = self.env.db_query(""" SELECT rev, path, node_type, change_type, base_path, base_rev FROM node_change ORDER BY rev, path""") - self.assertEquals(3, len(rows)) - self.assertEquals(('1', 'trunk', 'D', 'A', None, None), rows[0]) - self.assertEquals(('1', 'trunk/README', 'F', 'A', None, None), rows[1]) - self.assertEquals(('2', 'trunk/README', 'F', 'E', 'trunk/README', '1'), - rows[2]) + self.assertEqual(3, len(rows)) + self.assertEqual(('1', 'trunk', 'D', 'A', None, None), rows[0]) + self.assertEqual(('1', 'trunk/README', 'F', 'A', None, None), rows[1]) + self.assertEqual(('2', 'trunk/README', 'F', 'E', 'trunk/README', '1'), + rows[2]) def test_sync_changeset(self): t1 = datetime(2001, 1, 1, 1, 1, 1, 0, utc) @@ -215,9 +216,9 @@ class CacheTestCase(unittest.TestCase): rows = self.env.db_query( "SELECT time, author, message FROM revision ORDER BY rev") - self.assertEquals(2, len(rows)) - self.assertEquals((to_utimestamp(t1), 'joe', '**empty**'), rows[0]) - self.assertEquals((to_utimestamp(t2), 'joe', 'Import'), rows[1]) + self.assertEqual(2, len(rows)) + self.assertEqual((to_utimestamp(t1), 'joe', '**empty**'), rows[0]) + self.assertEqual((to_utimestamp(t2), 'joe', 'Import'), rows[1]) def test_sync_changeset_if_not_exists(self): t = [ @@ -260,7 +261,7 @@ class CacheTestCase(unittest.TestCase): cache.sync() self.assertRaises(NoSuchChangeset, cache.get_changeset, 2) - self.assertEqual(None, cache.sync_changeset(2)) + self.assertIsNone(cache.sync_changeset(2)) cset = cache.get_changeset(2) self.assertEqual('john', cset.author) self.assertEqual('Created directories', cset.message) @@ -275,12 +276,46 @@ class CacheTestCase(unittest.TestCase): rows = self.env.db_query( "SELECT time,author,message FROM revision ORDER BY rev") - self.assertEquals(4, len(rows)) - self.assertEquals((to_utimestamp(t[0]), 'joe', '**empty**'), rows[0]) - self.assertEquals((to_utimestamp(t[1]), 'joe', 'Import'), rows[1]) - self.assertEquals((to_utimestamp(t[2]), 'john', 'Created directories'), - rows[2]) - self.assertEquals((to_utimestamp(t[3]), 'joe', 'Add COPYING'), rows[3]) + self.assertEqual(4, len(rows)) + self.assertEqual((to_utimestamp(t[0]), 'joe', '**empty**'), rows[0]) + self.assertEqual((to_utimestamp(t[1]), 'joe', 'Import'), rows[1]) + self.assertEqual((to_utimestamp(t[2]), 'john', 'Created directories'), + rows[2]) + self.assertEqual((to_utimestamp(t[3]), 'joe', 'Add COPYING'), rows[3]) + + def test_sync_changeset_with_string_rev(self): # ticket:11660 + + class MockCachedRepository(CachedRepository): + def db_rev(self, rev): + return '%010d' % rev + + t1 = datetime(2001, 1, 1, 1, 1, 1, 0, utc) + t2 = datetime(2002, 1, 1, 1, 1, 1, 0, utc) + repos = self.get_repos(get_changeset=lambda x: changesets[int(x)], + youngest_rev=1) + changesets = [ + Mock(Changeset, repos, 0, 'empty', 'joe', t1, + get_changes=lambda: []), + Mock(Changeset, repos, 1, 'first', 'joe', t2, + get_changes=lambda: []), + ] + cache = MockCachedRepository(self.env, repos, self.log) + + cache.sync_changeset('0') # not cached yet + cache.sync_changeset(u'1') # not cached yet + rows = self.env.db_query( + "SELECT rev,author FROM revision ORDER BY rev") + self.assertEqual(2, len(rows)) + self.assertEquals(('0000000000', 'joe'), rows[0]) + self.assertEquals(('0000000001', 'joe'), rows[1]) + + cache.sync_changeset(u'0') # cached + cache.sync_changeset('1') # cached + rows = self.env.db_query( + "SELECT rev,author FROM revision ORDER BY rev") + self.assertEqual(2, len(rows)) + self.assertEquals(('0000000000', 'joe'), rows[0]) + self.assertEquals(('0000000001', 'joe'), rows[1]) def test_get_changes(self): t1 = datetime(2001, 1, 1, 1, 1, 1, 0, utc) @@ -307,7 +342,8 @@ class CacheTestCase(unittest.TestCase): def suite(): - return unittest.makeSuite(CacheTestCase, 'test') + return unittest.makeSuite(CacheTestCase) + if __name__ == '__main__': - unittest.main() + unittest.main(defaultTest='suite') Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/tests/diff.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/tests/diff.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/tests/diff.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/tests/diff.py Sat Nov 15 01:14:46 2014 @@ -1,3 +1,16 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2004-2013 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://trac.edgewall.org/wiki/TracLicense. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://trac.edgewall.org/log/. + from trac.versioncontrol import diff import unittest @@ -159,55 +172,55 @@ class DiffTestCase(unittest.TestCase): """Make sure that the escape calls leave quotes along, we don't need to escape them.""" changes = diff.diff_blocks(['ab'], ['a"b']) - self.assertEquals(len(changes), 1) + self.assertEqual(len(changes), 1) blocks = changes[0] - self.assertEquals(len(blocks), 1) + self.assertEqual(len(blocks), 1) block = blocks[0] - self.assertEquals(block['type'], 'mod') - self.assertEquals(str(block['base']['lines'][0]), 'a<del></del>b') - self.assertEquals(str(block['changed']['lines'][0]), 'a<ins>"</ins>b') + self.assertEqual(block['type'], 'mod') + self.assertEqual(str(block['base']['lines'][0]), 'a<del></del>b') + self.assertEqual(str(block['changed']['lines'][0]), 'a<ins>"</ins>b') def test_whitespace_marked_up1(self): """Regression test for #5795""" changes = diff.diff_blocks(['*a'], [' *a']) block = changes[0][0] - self.assertEquals(block['type'], 'mod') - self.assertEquals(str(block['base']['lines'][0]), '<del></del>*a') - self.assertEquals(str(block['changed']['lines'][0]), - '<ins> </ins>*a') + self.assertEqual(block['type'], 'mod') + self.assertEqual(str(block['base']['lines'][0]), '<del></del>*a') + self.assertEqual(str(block['changed']['lines'][0]), + '<ins> </ins>*a') def test_whitespace_marked_up2(self): """Related to #5795""" changes = diff.diff_blocks([' a'], [' b']) block = changes[0][0] - self.assertEquals(block['type'], 'mod') - self.assertEquals(str(block['base']['lines'][0]), - ' <del>a</del>') - self.assertEquals(str(block['changed']['lines'][0]), - ' <ins>b</ins>') + self.assertEqual(block['type'], 'mod') + self.assertEqual(str(block['base']['lines'][0]), + ' <del>a</del>') + self.assertEqual(str(block['changed']['lines'][0]), + ' <ins>b</ins>') def test_whitespace_marked_up3(self): """Related to #5795""" changes = diff.diff_blocks(['a '], ['b ']) block = changes[0][0] - self.assertEquals(block['type'], 'mod') - self.assertEquals(str(block['base']['lines'][0]), - '<del>a</del> ') - self.assertEquals(str(block['changed']['lines'][0]), - '<ins>b</ins> ') + self.assertEqual(block['type'], 'mod') + self.assertEqual(str(block['base']['lines'][0]), + '<del>a</del> ') + self.assertEqual(str(block['changed']['lines'][0]), + '<ins>b</ins> ') def test_expandtabs_works_right(self): """Regression test for #4557""" changes = diff.diff_blocks(['aa\tb'], ['aaxb']) block = changes[0][0] - self.assertEquals(block['type'], 'mod') - self.assertEquals(str(block['base']['lines'][0]), - 'aa<del> </del>b') - self.assertEquals(str(block['changed']['lines'][0]), - 'aa<ins>x</ins>b') + self.assertEqual(block['type'], 'mod') + self.assertEqual(str(block['base']['lines'][0]), + 'aa<del> </del>b') + self.assertEqual(str(block['changed']['lines'][0]), + 'aa<ins>x</ins>b') def suite(): - return unittest.makeSuite(DiffTestCase, 'test') + return unittest.makeSuite(DiffTestCase) if __name__ == '__main__': unittest.main() Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/tests/functional.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/tests/functional.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/tests/functional.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/tests/functional.py Sat Nov 15 01:14:46 2014 @@ -1,7 +1,31 @@ -#!/usr/bin/python +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2008-2013 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://trac.edgewall.org/wiki/TracLicense. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://trac.edgewall.org/log/. + +import tempfile + +from trac.admin.tests.functional import AuthorizationTestCaseSetup from trac.tests.functional import * +class TestAdminRepositoryAuthorization(AuthorizationTestCaseSetup): + def runTest(self): + """Check permissions required to access the Version Control + Repositories panel.""" + self.test_authorization('/admin/versioncontrol/repository', + 'VERSIONCONTROL_ADMIN', "Manage Repositories") + + class TestEmptySvnRepo(FunctionalTwillTestCaseSetup): def runTest(self): """Check empty repository""" @@ -114,6 +138,53 @@ class RegressionTestTicket5819(Functiona tc.find(components, 's') +class RegressionTestTicket11186(FunctionalTwillTestCaseSetup): + def runTest(self): + """Test for regression of http://trac.edgewall.org/ticket/11186 + TracError should be raised when repository with name already exists + """ + self._tester.go_to_admin() + tc.follow("\\bRepositories\\b") + tc.url(self._tester.url + '/admin/versioncontrol/repository') + name = random_word() + tc.formvalue('trac-addrepos', 'name', name) + tc.formvalue('trac-addrepos', 'dir', '/var/svn/%s' % name) + tc.submit() + tc.find('The repository "%s" has been added.' % name) + tc.formvalue('trac-addrepos', 'name', name) + tc.formvalue('trac-addrepos', 'dir', '/var/svn/%s' % name) + tc.submit() + tc.find('The repository "%s" already exists.' % name) + tc.notfind(internal_error) + + +class RegressionTestTicket11186Alias(FunctionalTwillTestCaseSetup): + def runTest(self): + """Test for regression of http://trac.edgewall.org/ticket/11186 alias + TracError should be raised when repository alias with name already + exists + """ + self._tester.go_to_admin() + tc.follow("\\bRepositories\\b") + tc.url(self._tester.url + '/admin/versioncontrol/repository') + word = random_word() + target = '%s_repos' % word + name = '%s_alias' % word + tc.formvalue('trac-addrepos', 'name', target) + tc.formvalue('trac-addrepos', 'dir', '/var/svn/%s' % target) + tc.submit() + tc.find('The repository "%s" has been added.' % target) + tc.formvalue('trac-addalias', 'name', name) + tc.formvalue('trac-addalias', 'alias', target) + tc.submit() + tc.find('The alias "%s" has been added.' % name) + tc.formvalue('trac-addalias', 'name', name) + tc.formvalue('trac-addalias', 'alias', target) + tc.submit() + tc.find('The alias "%s" already exists.' % name) + tc.notfind(internal_error) + + class RegressionTestRev5877(FunctionalTwillTestCaseSetup): def runTest(self): """Test for regression of the source browser fix in r5877""" @@ -121,16 +192,169 @@ class RegressionTestRev5877(FunctionalTw tc.notfind(internal_error) +class RegressionTestTicket11194(FunctionalTwillTestCaseSetup): + def runTest(self): + """Test for regression of http://trac.edgewall.org/ticket/11194 + TracError should be raised when repository with name already exists + """ + self._tester.go_to_admin() + tc.follow("\\bRepositories\\b") + tc.url(self._tester.url + '/admin/versioncontrol/repository') + + word = random_word() + names = ['%s_%d' % (word, n) for n in xrange(3)] + tc.formvalue('trac-addrepos', 'name', names[0]) + tc.formvalue('trac-addrepos', 'dir', '/var/svn/%s' % names[0]) + tc.submit() + tc.notfind(internal_error) + + tc.formvalue('trac-addrepos', 'name', names[1]) + tc.formvalue('trac-addrepos', 'dir', '/var/svn/%s' % names[1]) + tc.submit() + tc.notfind(internal_error) + + tc.follow('\\b' + names[1] + '\\b') + tc.url(self._tester.url + '/admin/versioncontrol/repository/' + names[1]) + tc.formvalue('trac-modrepos', 'name', names[2]) + tc.submit('save') + tc.notfind(internal_error) + tc.url(self._tester.url + '/admin/versioncontrol/repository') + + tc.follow('\\b' + names[2] + '\\b') + tc.url(self._tester.url + '/admin/versioncontrol/repository/' + names[2]) + tc.formvalue('trac-modrepos', 'name', names[0]) + tc.submit('save') + tc.find('The repository "%s" already exists.' % names[0]) + tc.notfind(internal_error) + + +class RegressionTestTicket11346(FunctionalTwillTestCaseSetup): + def runTest(self): + """Test for regression of http://trac.edgewall.org/ticket/11346 + fix for log: link with revision ranges included oldest wrongly + showing HEAD revision + """ + # create new 3 revisions + self._testenv.svn_mkdir(['ticket11346'], '') + for i in (1, 2): + rev = self._testenv.svn_add('ticket11346/file%d.txt' % i, '') + tc.go(self._tester.url + '/log?revs=1-2') + tc.find('@1') + tc.find('@2') + tc.notfind('@3') + tc.notfind('@%d' % rev) + + +class RegressionTestTicket11355(FunctionalTwillTestCaseSetup): + def runTest(self): + """Test for regression of http://trac.edgewall.org/ticket/11355 + Save with no changes should redirect back to the repository listing. + """ + # Add a repository + self._tester.go_to_admin("Repositories") + name = random_unique_camel() + dir = os.path.join(tempfile.gettempdir(), name.lower()) + tc.formvalue('trac-addrepos', 'name', name) + tc.formvalue('trac-addrepos', 'dir', dir) + tc.submit('add_repos') + tc.find('The repository "%s" has been added.' % name) + + # Save unmodified form and redirect back to listing page + tc.follow(r"\b%s\b" % name) + tc.url(self._tester.url + '/admin/versioncontrol/repository/' + name) + tc.submit('save', formname='trac-modrepos') + tc.url(self._tester.url + '/admin/versioncontrol/repository') + tc.find("Your changes have been saved.") + + # Warning is added when repository dir is not an absolute path + tc.follow(r"\b%s\b" % name) + tc.url(self._tester.url + '/admin/versioncontrol/repository/' + name) + tc.formvalue('trac-modrepos', 'dir', os.path.basename(dir)) + tc.submit('save') + tc.url(self._tester.url + '/admin/versioncontrol/repository/' + name) + tc.find('The repository directory must be an absolute path.') + + +class RegressionTestTicket11438(FunctionalTwillTestCaseSetup): + def runTest(self): + """Test for regression of http://trac.edgewall.org/ticket/11438 + fix for log: link with revision ranges included "head" keyword + """ + rev = self._testenv.svn_mkdir(['ticket11438'], '') + rev = self._testenv.svn_add('ticket11438/file1.txt', '') + rev = self._testenv.svn_add('ticket11438/file2.txt', '') + tc.go(self._tester.url + '/intertrac/log:@%d:head' % (rev - 1)) + tc.url(self._tester.url + r'/log/\?revs=' + str(rev - 1) + '%3Ahead') + tc.notfind('@%d' % (rev + 1)) + tc.find('@%d' % rev) + tc.find('@%d' % (rev - 1)) + tc.notfind('@%d' % (rev - 2)) + + +class RegressionTestTicket11584(FunctionalTwillTestCaseSetup): + def runTest(self): + """Test for regression of http://trac.edgewall.org/ticket/11584 + don't raise NoSuchChangeset for empty repository if no "rev" parameter + """ + repo_path = self._testenv.svnadmin_create('repo-t11584') + + self._tester.go_to_admin() + tc.follow("\\bRepositories\\b") + tc.url(self._tester.url + '/admin/versioncontrol/repository') + + tc.formvalue('trac-addrepos', 'name', 't11584') + tc.formvalue('trac-addrepos', 'dir', repo_path) + tc.submit() + tc.notfind(internal_error) + self._testenv._tracadmin('repository', 'sync', 't11584') + + browser_url = self._tester.url + '/browser/t11584' + tc.go(browser_url) + tc.url(browser_url) + tc.notfind('Error: No such changeset') + + +class RegressionTestTicket11618(FunctionalTwillTestCaseSetup): + def runTest(self): + """Test for regression of http://trac.edgewall.org/ticket/11618 + fix for malformed `readonly="True"` attribute in repository admin. + """ + env = self._testenv.get_trac_environment() + env.config.set('repositories', 't11618.dir', + self._testenv.repo_path_for_initenv()) + env.config.save() + try: + self._tester.go_to_admin() + tc.follow(r'\bRepositories\b') + tc.url(self._tester.url + '/admin/versioncontrol/repository') + tc.follow(r'\bt11618\b') + tc.url(self._tester.url + '/admin/versioncontrol/repository/t11618') + tc.notfind(' readonly="True"') + tc.find(' readonly="readonly"') + finally: + env.config.remove('repositories', 't11618.dir') + env.config.save() + + def functionalSuite(suite=None): if not suite: - import trac.tests.functional.testcases - suite = trac.tests.functional.testcases.functionalSuite() + import trac.tests.functional + suite = trac.tests.functional.functionalSuite() + suite.addTest(TestAdminRepositoryAuthorization()) + suite.addTest(RegressionTestTicket11355()) if has_svn: suite.addTest(TestEmptySvnRepo()) suite.addTest(TestRepoCreation()) suite.addTest(TestRepoBrowse()) suite.addTest(TestNewFileLog()) suite.addTest(RegressionTestTicket5819()) + suite.addTest(RegressionTestTicket11186()) + suite.addTest(RegressionTestTicket11186Alias()) + suite.addTest(RegressionTestTicket11194()) + suite.addTest(RegressionTestTicket11346()) + suite.addTest(RegressionTestTicket11438()) + suite.addTest(RegressionTestTicket11584()) + suite.addTest(RegressionTestTicket11618()) suite.addTest(RegressionTestRev5877()) else: print "SKIP: versioncontrol/tests/functional.py (no svn bindings)" Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/tests/svn_authz.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/tests/svn_authz.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/tests/svn_authz.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/tests/svn_authz.py Sat Nov 15 01:14:46 2014 @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2010 Edgewall Software +# Copyright (C) 2005-2013 Edgewall Software # All rights reserved. # # This software is licensed as described in the file COPYING, which @@ -390,12 +390,10 @@ unknown = r def suite(): suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(AuthzParserTestCase, 'test')) - suite.addTest(unittest.makeSuite(AuthzSourcePolicyTestCase, 'test')) + suite.addTest(unittest.makeSuite(AuthzParserTestCase)) + suite.addTest(unittest.makeSuite(AuthzSourcePolicyTestCase)) return suite if __name__ == '__main__': - runner = unittest.TextTestRunner() - runner.run(suite()) - + unittest.main(defaultTest='suite') Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/web_ui/__init__.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/web_ui/__init__.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/web_ui/__init__.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/web_ui/__init__.py Sat Nov 15 01:14:46 2014 @@ -1,3 +1,16 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2013 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://trac.edgewall.com/license.html. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://trac.edgewall.org/. + from trac.versioncontrol.web_ui.browser import * from trac.versioncontrol.web_ui.changeset import * from trac.versioncontrol.web_ui.log import * Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/web_ui/browser.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/web_ui/browser.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/web_ui/browser.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/web_ui/browser.py Sat Nov 15 01:14:46 2014 @@ -24,15 +24,14 @@ from genshi.builder import tag from trac.config import ListOption, BoolOption, Option from trac.core import * from trac.mimeview.api import IHTMLPreviewAnnotator, Mimeview, is_binary -from trac.perm import IPermissionRequestor +from trac.perm import IPermissionRequestor, PermissionError from trac.resource import Resource, ResourceNotFound from trac.util import as_bool, embedded_numbers -from trac.util.compat import cleandoc from trac.util.datefmt import http_date, to_datetime, utc from trac.util.html import escape, Markup from trac.util.text import exception_to_unicode, shorten_line from trac.util.translation import _, cleandoc_ -from trac.web import IRequestHandler, RequestDone +from trac.web.api import IRequestHandler, RequestDone from trac.web.chrome import (INavigationContributor, add_ctxtnav, add_link, add_script, add_stylesheet, prevnext_nav, web_context) @@ -296,7 +295,8 @@ class BrowserModule(Component): def get_navigation_items(self, req): rm = RepositoryManager(self.env) - if 'BROWSER_VIEW' in req.perm and rm.get_real_repositories(): + if any(repos.is_viewable(req.perm) for repos + in rm.get_real_repositories()): yield ('mainnav', 'browser', tag.a(_('Browse Source'), href=req.href.browser())) @@ -327,8 +327,6 @@ class BrowserModule(Component): return True def process_request(self, req): - req.perm.require('BROWSER_VIEW') - presel = req.args.get('preselected') if presel and (presel + '/').startswith(req.href.browser() + '/'): req.redirect(presel) @@ -337,8 +335,9 @@ class BrowserModule(Component): rev = req.args.get('rev', '') if rev.lower() in ('', 'head'): rev = None + format = req.args.get('format') order = req.args.get('order', 'name').lower() - desc = req.args.has_key('desc') + desc = 'desc' in req.args xhr = req.get_header('X-Requested-With') == 'XMLHttpRequest' rm = RepositoryManager(self.env) @@ -365,6 +364,7 @@ class BrowserModule(Component): # Find node for the requested path/rev context = web_context(req) node = None + changeset = None display_rev = lambda rev: rev if repos: try: @@ -377,6 +377,12 @@ class BrowserModule(Component): except NoSuchChangeset, e: raise ResourceNotFound(e.message, _('Invalid changeset number')) + if node: + try: + # use changeset instance to retrieve branches and tags + changeset = repos.get_changeset(node.rev) + except NoSuchChangeset: + pass context = context.child(repos.resource.child('source', path, version=rev_or_latest)) @@ -391,13 +397,25 @@ class BrowserModule(Component): repo_data = self._render_repository_index( context, all_repositories, order, desc) if node: + if not node.is_viewable(req.perm): + raise PermissionError('BROWSER_VIEW' if node.isdir else + 'FILE_VIEW', node.resource, self.env) if node.isdir: + if format in ('zip',): # extension point here... + self._render_zip(req, context, repos, node, rev) + # not reached dir_data = self._render_dir(req, repos, node, rev, order, desc) elif node.isfile: file_data = self._render_file(req, context, repos, node, rev) if not repos and not (repo_data and repo_data['repositories']): - raise ResourceNotFound(_("No node %(path)s", path=path)) + # If no viewable repositories, check permission instead of + # repos.is_viewable() + req.perm.require('BROWSER_VIEW') + if show_index: + raise ResourceNotFound(_("No viewable repositories")) + else: + raise ResourceNotFound(_("No node %(path)s", path=path)) quickjump_data = properties_data = None if node and not xhr: @@ -409,7 +427,7 @@ class BrowserModule(Component): 'context': context, 'reponame': reponame, 'repos': repos, 'repoinfo': all_repositories.get(reponame or ''), 'path': path, 'rev': node and node.rev, 'stickyrev': rev, - 'display_rev': display_rev, + 'display_rev': display_rev, 'changeset': changeset, 'created_path': node and node.created_path, 'created_rev': node and node.created_rev, 'properties': properties_data, @@ -500,6 +518,10 @@ class BrowserModule(Component): continue try: repos = rm.get_repository(reponame) + except TracError, err: + entry = (reponame, repoinfo, None, None, + exception_to_unicode(err), None) + else: if repos: if not repos.is_viewable(context.perm): continue @@ -518,10 +540,7 @@ class BrowserModule(Component): raw_href) else: entry = (reponame, repoinfo, None, None, u"\u2013", None) - except TracError, err: - entry = (reponame, repoinfo, None, None, - exception_to_unicode(err), None) - if entry[-1] is not None: # Check permission in case of error + if entry[4] is not None: # Check permission in case of error root = Resource('repository', reponame).child('source', '/') if 'BROWSER_VIEW' not in context.perm(root): continue @@ -620,13 +639,35 @@ class BrowserModule(Component): timerange.to_seconds(timerange.oldest)), } + def _iter_nodes(self, node): + stack = [node] + while stack: + node = stack.pop() + yield node + if node.isdir: + stack.extend(sorted(node.get_entries(), + key=lambda x: x.name, + reverse=True)) + + def _render_zip(self, req, context, repos, root_node, rev=None): + if not self.is_path_downloadable(repos, root_node.path): + raise TracError(_("Path not available for download")) + req.perm(context.resource).require('FILE_VIEW') + root_path = root_node.path.rstrip('/') + if root_path: + archive_name = root_node.name + else: + archive_name = repos.reponame or 'repository' + filename = '%s-%s.zip' % (archive_name, root_node.rev) + render_zip(req, filename, repos, root_node, self._iter_nodes) + def _render_file(self, req, context, repos, node, rev=None): req.perm(node.resource).require('FILE_VIEW') mimeview = Mimeview(self.env) # MIME type detection - content = node.get_content() + content = node.get_processed_content() chunk = content.read(CHUNK_SIZE) mime_type = node.content_type if not mime_type or mime_type == 'application/octet-stream': @@ -639,7 +680,6 @@ class BrowserModule(Component): req.send_response(200) req.send_header('Content-Type', 'text/plain' if format == 'txt' else mime_type) - req.send_header('Content-Length', node.content_length) req.send_header('Last-Modified', http_date(node.last_modified)) if rev is None: req.send_header('Pragma', 'no-cache') @@ -652,11 +692,12 @@ class BrowserModule(Component): req.send_header('Content-Disposition', 'attachment') req.end_headers() - while 1: - if not chunk: - raise RequestDone - req.write(chunk) - chunk = content.read(CHUNK_SIZE) + def chunks(): + c = chunk + while c: + yield c + c = content.read(CHUNK_SIZE) + raise RequestDone(chunks()) else: # The changeset corresponding to the last change on `node` # is more interesting than the `rev` changeset. @@ -678,7 +719,7 @@ class BrowserModule(Component): self.log.debug("Rendering preview of node %s@%s with mime-type %s" % (node.name, str(rev), mime_type)) - del content # the remainder of that content is not needed + content = None # the remainder of that content is not needed add_stylesheet(req, 'common/css/code.css') @@ -686,7 +727,8 @@ class BrowserModule(Component): annotate = req.args.get('annotate') if annotate: annotations.insert(0, annotate) - preview_data = mimeview.preview_data(context, node.get_content(), + preview_data = mimeview.preview_data(context, + node.get_processed_content(), node.get_content_length(), mime_type, node.created_path, raw_href, @@ -704,18 +746,19 @@ class BrowserModule(Component): if node is not None and node.isfile: return href.export(rev or 'HEAD', repos.reponame or None, node.path) - path = npath = '' if node is None else node.path.strip('/') - if repos.reponame: - path = (repos.reponame + '/' + npath).rstrip('/') - if any(fnmatchcase(path, p.strip('/')) - for p in self.downloadable_paths): - return href.changeset(rev or repos.youngest_rev, - repos.reponame or None, npath, - old=rev, old_path=repos.reponame or '/', - format='zip') + path = '' if node is None else node.path.strip('/') + if self.is_path_downloadable(repos, path): + return href.browser(repos.reponame or None, path, + rev=rev or repos.youngest_rev, format='zip') # public methods + def is_path_downloadable(self, repos, path): + if repos.reponame: + path = repos.reponame + '/' + path + return any(fnmatchcase(path, dp.strip('/')) + for dp in self.downloadable_paths) + def render_properties(self, mode, context, props): """Prepare rendering of a collection of properties.""" return filter(None, [self.render_property(name, mode, context, props) Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/web_ui/changeset.py URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/web_ui/changeset.py?rev=1639823&r1=1639822&r2=1639823&view=diff ============================================================================== --- bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/web_ui/changeset.py (original) +++ bloodhound/branches/trac-1.0.2-integration/trac/trac/versioncontrol/web_ui/changeset.py Sat Nov 15 01:14:46 2014 @@ -20,6 +20,7 @@ from __future__ import with_statement +from functools import partial from itertools import groupby import os import posixpath @@ -39,12 +40,13 @@ from trac.util import as_bool, content_d from trac.util.datefmt import from_utimestamp, pretty_timedelta from trac.util.text import exception_to_unicode, to_unicode, \ unicode_urlencode, shorten_line, CRLF -from trac.util.translation import _, ngettext +from trac.util.translation import _, ngettext, tag_ from trac.versioncontrol.api import RepositoryManager, Changeset, Node, \ NoSuchChangeset from trac.versioncontrol.diff import get_diff_options, diff_blocks, \ unified_diff from trac.versioncontrol.web_ui.browser import BrowserModule +from trac.versioncontrol.web_ui.util import render_zip from trac.web import IRequestHandler, RequestDone from trac.web.chrome import (Chrome, INavigationContributor, add_ctxtnav, add_link, add_script, add_stylesheet, @@ -100,7 +102,7 @@ class DefaultPropertyDiffRenderer(Compon unidiff = '--- \n+++ \n' + \ '\n'.join(unified_diff(old.splitlines(), new.splitlines(), options.get('contextlines', 3))) - return tag.li('Property ', tag.strong(name), + return tag.li(tag_("Property %(name)s", name=tag.strong(name)), Mimeview(self.env).render(old_context, 'text/x-diff', unidiff)) @@ -213,9 +215,9 @@ class ChangesetModule(Component): req.perm.require('CHANGESET_VIEW') # -- retrieve arguments - full_new_path = new_path = req.args.get('new_path') + new_path = req.args.get('new_path') new = req.args.get('new') - full_old_path = old_path = req.args.get('old_path') + old_path = req.args.get('old_path') old = req.args.get('old') reponame = req.args.get('reponame') @@ -252,14 +254,14 @@ class ChangesetModule(Component): # -- normalize and check for special case try: - new_path = repos.normalize_path(new_path) new = repos.normalize_rev(new) - full_new_path = '/' + pathjoin(repos.reponame, new_path) - old_path = repos.normalize_path(old_path or new_path) old = repos.normalize_rev(old or new) - full_old_path = '/' + pathjoin(repos.reponame, old_path) except NoSuchChangeset, e: - raise ResourceNotFound(e.message, _('Invalid Changeset Number')) + raise ResourceNotFound(e.message, _("Invalid Changeset Number")) + new_path = repos.normalize_path(new_path) + old_path = repos.normalize_path(old_path or new_path) + full_new_path = '/' + pathjoin(repos.reponame, new_path) + full_old_path = '/' + pathjoin(repos.reponame, old_path) if old_path == new_path and old == new: # revert to Changeset old_path = old = None @@ -268,7 +270,7 @@ class ChangesetModule(Component): diff_opts = diff_data['options'] # -- setup the `chgset` and `restricted` flags, see docstring above. - chgset = not old and not old_path + chgset = not old and old_path is None if chgset: restricted = new_path not in ('', '/') # (subset or not) else: @@ -305,7 +307,7 @@ class ChangesetModule(Component): new = repos.youngest_rev elif not old: old = repos.youngest_rev - if not old_path: + if old_path is None: old_path = new_path data = {'old_path': old_path, 'old_rev': old, 'new_path': new_path, 'new_rev': new} @@ -339,15 +341,14 @@ class ChangesetModule(Component): if restricted: filename = 'diff-%s-from-%s-to-%s' \ % (rpath, old, new) - elif old_path == '/': # special case for download (#238) - filename = '%s-%s' % (rpath, old) else: filename = 'diff-from-%s-%s-to-%s-%s' \ % (old_path.replace('/','_'), old, rpath, new) if format == 'diff': self._render_diff(req, filename, repos, data) elif format == 'zip': - self._render_zip(req, filename, repos, data) + render_zip(req, filename + '.zip', repos, None, + partial(self._zip_iter_nodes, req, repos, data)) # -- HTML format self._render_html(req, repos, chgset, restricted, xhr, data) @@ -753,47 +754,16 @@ class ChangesetModule(Component): req.write(diff_str) raise RequestDone - def _render_zip(self, req, filename, repos, data): - """ZIP archive containing all the added and/or modified files.""" - req.send_response(200) - req.send_header('Content-Type', 'application/zip') - req.send_header('Content-Disposition', - content_disposition('attachment', filename + '.zip')) - - from zipfile import ZipFile, ZipInfo, ZIP_DEFLATED as compression - - buf = StringIO() - zipfile = ZipFile(buf, 'w', compression) + def _zip_iter_nodes(self, req, repos, data, root_node): + """Node iterator yielding all the added and/or modified files.""" for old_node, new_node, kind, change in repos.get_changes( new_path=data['new_path'], new_rev=data['new_rev'], old_path=data['old_path'], old_rev=data['old_rev']): if (kind == Node.FILE or kind == Node.DIRECTORY) and \ change != Changeset.DELETE \ and new_node.is_viewable(req.perm): - zipinfo = ZipInfo() - # Note: unicode filenames are not supported by zipfile. - # UTF-8 is not supported by all Zip tools either, - # but as some do, UTF-8 is the best option here. - zipinfo.filename = new_node.path.strip('/').encode('utf-8') - zipinfo.flag_bits |= 0x800 # filename is encoded with utf-8 - zipinfo.date_time = new_node.last_modified.utctimetuple()[:6] - zipinfo.compress_type = compression - # setting zipinfo.external_attr is needed since Python 2.5 - if new_node.isfile: - zipinfo.external_attr = 0644 << 16L - content = new_node.get_content().read() - elif new_node.isdir: - zipinfo.filename += '/' - zipinfo.external_attr = 040755 << 16L - content = '' - zipfile.writestr(zipinfo, content) - zipfile.close() + yield new_node - zip_str = buf.getvalue() - req.send_header("Content-Length", len(zip_str)) - req.end_headers() - req.write(zip_str) - raise RequestDone def title_for_diff(self, data): # TRANSLATOR: 'latest' (revision) @@ -1205,7 +1175,7 @@ class AnyDiffModule(Component): if repos.is_viewable(req.perm)) elem = tag.ul( - [tag.li(tag.b(path) if isdir else path) + [tag.li(tag.strong(path) if isdir else path) for (isdir, name, path) in sorted(entries, key=kind_order) if name.lower().startswith(prefix)])