Merge branch 'dev' into t50_hide_project_icon_placeholder
diff --git a/Allura/allura/command/__init__.py b/Allura/allura/command/__init__.py
index 88ddebc..1179086 100644
--- a/Allura/allura/command/__init__.py
+++ b/Allura/allura/command/__init__.py
@@ -5,3 +5,4 @@
 from create_neighborhood import CreateNeighborhoodCommand
 from create_trove_categories import CreateTroveCategoriesCommand
 from set_neighborhood_features import SetNeighborhoodFeaturesCommand
+from rssfeeds import RssFeedsCommand
diff --git a/Allura/allura/command/rssfeeds.py b/Allura/allura/command/rssfeeds.py
new file mode 100644
index 0000000..335ede9
--- /dev/null
+++ b/Allura/allura/command/rssfeeds.py
@@ -0,0 +1,84 @@
+import feedparser
+import html2text
+from bson import ObjectId
+
+import base
+
+from pylons import c
+
+from allura import model as M
+from forgeblog import model as BM
+from forgeblog import version
+from forgeblog.main import ForgeBlogApp 
+from allura.lib import exceptions
+
+
+class RssFeedsCommand(base.Command):
+    summary = 'Rss feed client'
+    parser = base.Command.standard_parser(verbose=True)
+    parser.add_option('-a', '--appid', dest='appid', default='',
+                      help='application id')
+    parser.add_option('-u', '--username', dest='username', default='root',
+                      help='poster username')
+
+    def command(self):
+        self.basic_setup()
+
+        user = M.User.query.get(username=self.options.username)
+        c.user = user
+
+        self.prepare_feeds()
+        for appid in self.feed_dict:
+            for feed_url in self.feed_dict[appid]:
+                self.process_feed(appid, feed_url)
+
+    def prepare_feeds(self):
+        feed_dict = {}
+        if self.options.appid != '':
+            gl_app = BM.Globals.query.get(app_config_id=ObjectId(self.options.appid))
+            if not gl_app:
+                raise exceptions.NoSuchGlobalsError("The globals %s " \
+                     "could not be found in the database" % self.options.appid)
+            if len(gl_app.external_feeds) > 0:
+                feed_dict[gl_app.app_config_id] = gl_app.external_feeds
+        else:
+            for gl_app in BM.Globals.query.find().all():
+                if len(gl_app.external_feeds) > 0:
+                    feed_dict[gl_app.app_config_id] = gl_app.external_feeds
+        self.feed_dict = feed_dict
+
+    def process_feed(self, appid, feed_url):
+        appconf = M.AppConfig.query.get(_id=appid)
+        if not appconf:
+            return
+
+        c.project = appconf.project
+        app = ForgeBlogApp(c.project, appconf)
+        c.app = app
+
+        base.log.info("Get feed: %s" % feed_url)
+        f = feedparser.parse(feed_url)
+        if f.bozo:
+            base.log.exception("%s: %s" % (feed_url, f.bozo_exception))
+            return
+        for e in f.entries:
+            title = e.title
+            if 'content' in e:
+                content = u''
+                for ct in e.content:
+                    if ct.type != 'text/html':
+                        content = u"%s<p>%s</p>" % (content, ct.value)
+                    else:
+                        content = content + ct.value
+            else:
+                content = e.summary
+
+            content = u'%s <a href="%s">link</a>' % (content, e.link)
+            content = html2text.html2text(content, e.link)
+
+            post = BM.BlogPost(title=title, text=content, app_config_id=appid,
+                               tool_version={'blog': version.__version__},
+                               state='draft')
+            post.neighborhood_id=c.project.neighborhood_id
+            post.make_slug()
+            post.commit()
diff --git a/Allura/allura/config/middleware.py b/Allura/allura/config/middleware.py
index dd575d5..9a9fb49 100644
--- a/Allura/allura/config/middleware.py
+++ b/Allura/allura/config/middleware.py
@@ -23,7 +23,7 @@
 from allura.config.app_cfg import base_config
 from allura.config.environment import load_environment
 from allura.config.app_cfg import ForgeConfig
-from allura.lib.custom_middleware import StatsMiddleware
+from allura.lib.custom_middleware import AlluraTimerMiddleware
 from allura.lib.custom_middleware import SSLMiddleware
 from allura.lib.custom_middleware import StaticFilesMiddleware
 from allura.lib.custom_middleware import CSRFMiddleware
@@ -33,8 +33,8 @@
 
 __all__ = ['make_app']
 
-# Use base_config to setup the necessary PasteDeploy application factory. 
-# make_base_app will wrap the TG2 app with all the middleware it needs. 
+# Use base_config to setup the necessary PasteDeploy application factory.
+# make_base_app will wrap the TG2 app with all the middleware it needs.
 make_base_app = base_config.setup_tg_wsgi_app(load_environment)
 
 
@@ -55,13 +55,13 @@
     :type full_stack: str or bool
     :return: The allura application with all the relevant middleware
         loaded.
-    
+
     This is the PasteDeploy factory for the allura application.
-    
+
     ``app_conf`` contains all the application-specific settings (those defined
     under ``[app:main]``.
-    
-   
+
+
     """
     # Run all the initialization code here
     mimetypes.init(
@@ -73,7 +73,7 @@
 
     # Configure EW variable provider
     ew.render.TemplateEngine.register_variable_provider(get_tg_vars)
-    
+
     # Create base app
     base_config = ForgeConfig(root)
     load_environment = base_config.make_load_environment()
@@ -86,7 +86,7 @@
     load_environment(global_conf, app_conf)
 
     if config.get('zarkov.host'):
-        try:    
+        try:
             import zmq
         except ImportError:
             raise ImportError, "Unable to import the zmq library. Please"\
@@ -113,9 +113,7 @@
     # Redirect 401 to the login page
     app = LoginRedirectMiddleware(app)
     # Add instrumentation
-    if app_conf.get('stats.sample_rate', '0.25') != '0':
-        stats_config = dict(global_conf, **app_conf)
-        app = StatsMiddleware(app, stats_config)
+    app = AlluraTimerMiddleware(app, app_conf)
     # Clear cookies when the CSRF field isn't posted
     if not app_conf.get('disable_csrf_protection'):
         app = CSRFMiddleware(app, '_session_id')
@@ -145,7 +143,7 @@
     #    the WSGI application's iterator is exhausted
     app = RegistryManager(app, streaming=True)
     return app
-    
+
 def set_scheme_middleware(app):
     def SchemeMiddleware(environ, start_response):
         if asbool(environ.get('HTTP_X_SFINC_SSL', 'false')):
diff --git a/Allura/allura/controllers/repository.py b/Allura/allura/controllers/repository.py
index 8fcf797..19403e4 100644
--- a/Allura/allura/controllers/repository.py
+++ b/Allura/allura/controllers/repository.py
@@ -1,6 +1,8 @@
 import os
 import json
 import logging
+import re
+import difflib
 from urllib import quote, unquote
 from collections import defaultdict
 
@@ -15,7 +17,6 @@
 from ming.orm import ThreadLocalORMSession, session
 
 import allura.tasks
-from allura.lib import patience
 from allura.lib import security
 from allura.lib import helpers as h
 from allura.lib import widgets as w
@@ -61,7 +62,7 @@
 
     @with_trailing_slash
     @expose('jinja:allura:templates/repo/fork.html')
-    def fork(self, to_name=None, project_id=None):
+    def fork(self, project_id=None, mount_point=None, mount_label=None):
         # this shows the form and handles the submission
         security.require_authenticated()
         if not c.app.forkable: raise exc.HTTPNotFound
@@ -70,10 +71,13 @@
         ThreadLocalORMSession.close_all()
         from_project = c.project
         to_project = M.Project.query.get(_id=ObjectId(project_id))
-        if request.method != 'POST' or not to_name:
+        mount_label = mount_label or '%s - %s' % (c.project.name, from_repo.tool_name)
+        mount_point = (mount_point or from_project.shortname)
+        if request.method != 'POST' or not mount_point:
             return dict(from_repo=from_repo,
                         user_project=c.user.private_project(),
-                        to_name=to_name or '')
+                        mount_point=mount_point,
+                        mount_label=mount_label)
         else:
             with h.push_config(c, project=to_project):
                 if not to_project.database_configured:
@@ -81,10 +85,12 @@
                 security.require(security.has_access(to_project, 'admin'))
                 try:
                     to_project.install_app(
-                        from_repo.tool_name, to_name,
+                        ep_name=from_repo.tool_name,
+                        mount_point=mount_point,
+                        mount_label=mount_label,
                         cloned_from_project_id=from_project._id,
                         cloned_from_repo_id=from_repo._id)
-                    redirect(to_project.url()+to_name+'/')
+                    redirect(to_project.url()+mount_point+'/')
                 except exc.HTTPRedirection:
                     raise
                 except Exception, ex:
@@ -514,7 +520,7 @@
         b = self._blob
         la = list(a)
         lb = list(b)
-        diff = ''.join(patience.unified_diff(
+        diff = ''.join(difflib.unified_diff(
                 la, lb,
                 ('a' + apath).encode('utf-8'),
                 ('b' + b.path()).encode('utf-8')))
diff --git a/Allura/allura/ext/user_profile/templates/user_index.html b/Allura/allura/ext/user_profile/templates/user_index.html
index 2ccaacd..25ce980 100644
--- a/Allura/allura/ext/user_profile/templates/user_index.html
+++ b/Allura/allura/ext/user_profile/templates/user_index.html
@@ -2,7 +2,7 @@
 
 {% block title %}{{user.display_name}} / Profile{% endblock %}
 
-{% block header %}{{c.project.homepage_title}}{% endblock %}
+{% block header %}{{ user.display_name|default(user.username) }}{% endblock %}
 
 {% block extra_css %}
   <link rel="stylesheet" type="text/css"
diff --git a/Allura/allura/lib/app_globals.py b/Allura/allura/lib/app_globals.py
index d43fe29..3e76d12 100644
--- a/Allura/allura/lib/app_globals.py
+++ b/Allura/allura/lib/app_globals.py
@@ -60,7 +60,7 @@
         if asbool(config.get('solr.mock')):
             self.solr = MockSOLR()
         elif self.solr_server:
-            self.solr =  pysolr.Solr(self.solr_server)
+            self.solr = pysolr.Solr(self.solr_server)
         else: # pragma no cover
             self.solr = None
         self.use_queue = asbool(config.get('use_queue', False))
@@ -77,7 +77,7 @@
         # Setup pygments
         self.pygments_formatter = utils.LineAnchorCodeHtmlFormatter(
             cssclass='codehilite',
-            linenos='inline')
+            linenos='table')
 
         # Setup Pypeline
         self.pypeline_markup = pypeline_markup
@@ -86,42 +86,42 @@
         self.analytics = analytics.GoogleAnalytics(account=config.get('ga.account', 'UA-XXXXX-X'))
 
         self.icons = dict(
-            admin = Icon('x', 'ico-admin'),
-            pencil = Icon('p', 'ico-pencil'),
-            help = Icon('h', 'ico-help'),
-            search = Icon('s', 'ico-search'),
-            history = Icon('N', 'ico-history'),
-            feed = Icon('f', 'ico-feed'),
-            mail = Icon('M', 'ico-mail'),
-            reply = Icon('w', 'ico-reply'),
-            tag = Icon('z', 'ico-tag'),
-            flag = Icon('^', 'ico-flag'),
-            undelete = Icon('+', 'ico-undelete'),
-            delete = Icon('#', 'ico-delete'),
-            close = Icon('D', 'ico-close'),
-            table = Icon('n', 'ico-table'),
-            stats = Icon('Y', 'ico-stats'),
-            pin = Icon('@', 'ico-pin'),
-            folder = Icon('o', 'ico-folder'),
-            fork = Icon('R', 'ico-fork'),
-            merge = Icon('J', 'ico-merge'),
-            plus = Icon('+', 'ico-plus'),
-            conversation = Icon('q', 'ico-conversation'),
-            group = Icon('g', 'ico-group'),
-            user = Icon('U', 'ico-user'),
-            secure = Icon('(', 'ico-lock'),
-            unsecure = Icon(')', 'ico-unlock'),
+            admin=Icon('x', 'ico-admin'),
+            pencil=Icon('p', 'ico-pencil'),
+            help=Icon('h', 'ico-help'),
+            search=Icon('s', 'ico-search'),
+            history=Icon('N', 'ico-history'),
+            feed=Icon('f', 'ico-feed'),
+            mail=Icon('M', 'ico-mail'),
+            reply=Icon('w', 'ico-reply'),
+            tag=Icon('z', 'ico-tag'),
+            flag=Icon('^', 'ico-flag'),
+            undelete=Icon('+', 'ico-undelete'),
+            delete=Icon('#', 'ico-delete'),
+            close=Icon('D', 'ico-close'),
+            table=Icon('n', 'ico-table'),
+            stats=Icon('Y', 'ico-stats'),
+            pin=Icon('@', 'ico-pin'),
+            folder=Icon('o', 'ico-folder'),
+            fork=Icon('R', 'ico-fork'),
+            merge=Icon('J', 'ico-merge'),
+            plus=Icon('+', 'ico-plus'),
+            conversation=Icon('q', 'ico-conversation'),
+            group=Icon('g', 'ico-group'),
+            user=Icon('U', 'ico-user'),
+            secure=Icon('(', 'ico-lock'),
+            unsecure=Icon(')', 'ico-unlock'),
             # Permissions
-            perm_read = Icon('E', 'ico-focus'),
-            perm_update = Icon('0', 'ico-sync'),
-            perm_create = Icon('e', 'ico-config'),
-            perm_register = Icon('e', 'ico-config'),
-            perm_delete = Icon('-', 'ico-minuscirc'),
-            perm_tool = Icon('x', 'ico-config'),
-            perm_admin = Icon('(', 'ico-lock'),
-            perm_has_yes = Icon('3', 'ico-check'),
-            perm_has_no = Icon('d', 'ico-noentry'),
-            perm_has_inherit = Icon('2', 'ico-checkcircle'),
+            perm_read=Icon('E', 'ico-focus'),
+            perm_update=Icon('0', 'ico-sync'),
+            perm_create=Icon('e', 'ico-config'),
+            perm_register=Icon('e', 'ico-config'),
+            perm_delete=Icon('-', 'ico-minuscirc'),
+            perm_tool=Icon('x', 'ico-config'),
+            perm_admin=Icon('(', 'ico-lock'),
+            perm_has_yes=Icon('3', 'ico-check'),
+            perm_has_no=Icon('d', 'ico-noentry'),
+            perm_has_inherit=Icon('2', 'ico-checkcircle'),
         )
 
         # Cache some loaded entry points
diff --git a/Allura/allura/lib/custom_middleware.py b/Allura/allura/lib/custom_middleware.py
index a323298..15d49c8 100644
--- a/Allura/allura/lib/custom_middleware.py
+++ b/Allura/allura/lib/custom_middleware.py
@@ -1,21 +1,14 @@
 import os
 import re
 import logging
-from contextlib import contextmanager
-from threading import local
-from random import random
 
 import tg
-import pylons
 import pkg_resources
-import markdown
 from paste import fileapp
-from paste.deploy.converters import asbool
 from pylons.util import call_wsgi_application
-from tg.controllers import DecoratedController
+from timermiddleware import Timer, TimerMiddleware
 from webob import exc, Request
 
-from allura.lib.stats import timing, StatsRecord
 from allura.lib import helpers as h
 
 log = logging.getLogger(__name__)
@@ -151,57 +144,39 @@
             resp = req.get_response(self.app)
         return resp(environ, start_response)
 
-class StatsMiddleware(object):
-
-    def __init__(self, app, config):
-        self.app = app
-        self.config = config
-        self.log = logging.getLogger('stats')
-        self.active = False
-        try:
-            self.sample_rate = config.get('stats.sample_rate', 0.25)
-            self.debug = asbool(config.get('debug', 'false'))
-            self.instrument_pymongo()
-            self.instrument_template()
-            self.active = True
-        except KeyError:
-            self.sample_rate = 0
-
-    def instrument_pymongo(self):
-        import pymongo.collection
-        import ming.odm
-        timing('mongo').decorate(pymongo.collection.Collection,
-                                 'count find find_one')
-        timing('mongo').decorate(pymongo.cursor.Cursor,
-                                 'count distinct explain hint limit next rewind'
-                                 ' skip sort where')
-        timing('ming').decorate(ming.odm.odmsession.ODMSession,
-                                'flush find get')
-        timing('ming').decorate(ming.odm.odmsession.ODMCursor,
-                                'next')
-
-    def instrument_template(self):
+class AlluraTimerMiddleware(TimerMiddleware):
+    def timers(self):
+        import genshi
         import jinja2
-        import genshi.template
-        timing('template').decorate(genshi.template.Template,
-                                    '_prepare _parse generate')
-        timing('render').decorate(genshi.Stream,
-                                  'render')
-        timing('render').decorate(jinja2.Template,
-                                  'render')
-        timing('markdown').decorate(markdown.Markdown,
-                                    'convert')
+        import markdown
+        import ming
+        import pymongo
+        import socket
+        import urllib2
 
-
-    def __call__(self, environ, start_response):
-        req = Request(environ)
-        req.environ['sf.stats'] = s = StatsRecord(req, random() < self.sample_rate)
-        with s.timing('total'):
-            resp = req.get_response(self.app, catch_exc_info=self.debug)
-            result = resp(environ, start_response)
-        if s.active:
-            self.log.info('Stats: %r', s)
-            from allura import model as M
-            M.Stats.make(s.asdict()).m.insert()
-        return result
-
+        return [
+            Timer('markdown', markdown.Markdown, 'convert'),
+            Timer('ming', ming.odm.odmsession.ODMCursor, 'next'),
+            Timer('ming', ming.odm.odmsession.ODMSession, 'flush', 'find',
+                'get'),
+            Timer('ming', ming.schema.Document, 'validate',
+                debug_each_call=False),
+            Timer('ming', ming.schema.FancySchemaItem, '_validate_required',
+                '_validate_fast_missing', '_validate_optional',
+                debug_each_call=False),
+            Timer('mongo', pymongo.collection.Collection, 'count', 'find',
+                'find_one'),
+            Timer('mongo', pymongo.cursor.Cursor, 'count', 'distinct',
+                'explain', 'hint', 'limit', 'next', 'rewind', 'skip',
+                'sort', 'where'),
+            Timer('jinja', jinja2.Template, 'render', 'stream', 'generate'),
+            # urlopen and socket io may or may not overlap partially
+            Timer('urlopen', urllib2, 'urlopen'),
+            Timer('render', genshi.Stream, 'render'),
+            Timer('socket_read', socket._fileobject, 'read', 'readline',
+                'readlines', debug_each_call=False),
+            Timer('socket_write', socket._fileobject, 'write', 'writelines',
+                'flush', debug_each_call=False),
+            Timer('template', genshi.template.Template, '_prepare', '_parse',
+                'generate'),
+        ]
diff --git a/Allura/allura/lib/exceptions.py b/Allura/allura/lib/exceptions.py
index fa9f4a0..30250f7 100644
--- a/Allura/allura/lib/exceptions.py
+++ b/Allura/allura/lib/exceptions.py
@@ -4,6 +4,7 @@
 class ToolError(ForgeError): pass
 class NoSuchProjectError(ForgeError): pass
 class NoSuchNeighborhoodError(ForgeError): pass
+class NoSuchGlobalsError(ForgeError): pass
 class MailError(ForgeError): pass
 class AddressException(MailError): pass
 class NoSuchNBFeatureError(ForgeError): pass
diff --git a/Allura/allura/lib/helpers.py b/Allura/allura/lib/helpers.py
index 57b42ee..b144cc8 100644
--- a/Allura/allura/lib/helpers.py
+++ b/Allura/allura/lib/helpers.py
@@ -142,8 +142,8 @@
     results = dict(
         (r._id, r)
         for r in X.query.find(dict(_id={'$in':ids})))
-    result = ( results.get(i) for i in ids )
-    return ( r for r in result if r is not None )
+    result = (results.get(i) for i in ids)
+    return (r for r in result if r is not None)
 
 @contextmanager
 def push_config(obj, **kw):
@@ -158,14 +158,14 @@
     try:
         yield obj
     finally:
-        for k,v in saved_attrs.iteritems():
+        for k, v in saved_attrs.iteritems():
             setattr(obj, k, v)
         for k in new_attrs:
             delattr(obj, k)
 
 def sharded_path(name, num_parts=2):
     parts = [
-        name[:i+1]
+        name[:i + 1]
         for i in range(num_parts) ]
     return '/'.join(parts)
 
@@ -227,12 +227,12 @@
     a valid kwargs argument'''
     return dict(
         (k.encode('utf-8'), v)
-        for k,v in d.iteritems())
+        for k, v in d.iteritems())
 
 def vardec(fun):
     def vardec_hook(remainder, params):
         new_params = variable_decode(dict(
-                (k,v) for k,v in params.items()
+                (k, v) for k, v in params.items()
                 if re_clean_vardec_key.match(k)))
         params.update(new_params)
     before_validate(vardec_hook)(fun)
@@ -276,7 +276,7 @@
         try:
             return parse(value)
         except ValueError:
-            if self.if_invalid!=formencode.api.NoDefault:
+            if self.if_invalid != formencode.api.NoDefault:
                 return self.if_invalid
             else:
                 raise
@@ -336,7 +336,7 @@
                 v.cls = cls
 
 class attrproxy(object):
-    cls=None
+    cls = None
     def __init__(self, *attrs):
         self.attrs = attrs
 
@@ -413,7 +413,7 @@
                 errors=c.validation_exception.unpack_errors(),
                 value=c.validation_exception.value,
                 params=kwargs)
-    response.status=400
+    response.status = 400
     return json.dumps(result, indent=2)
 
 def pop_user_notifications(user=None):
@@ -429,8 +429,8 @@
     '''Return a subdictionary keys with a given prefix,
     with the prefix stripped
     '''
-    plen=len(prefix)
-    return dict((k[plen:], v) for k,v in d.iteritems()
+    plen = len(prefix)
+    return dict((k[plen:], v) for k, v in d.iteritems()
                 if k.startswith(prefix))
 
 @contextmanager
@@ -478,7 +478,7 @@
         extra = kwargs.setdefault('extra', {})
         meta = kwargs.pop('meta', {})
         kwpairs = extra.setdefault('kwpairs', {})
-        for k,v in meta.iteritems():
+        for k, v in meta.iteritems():
             kwpairs['meta_%s' % k] = v
         extra.update(self._make_extra())
         self._logger.log(level, self._action + ': ' + message, *args, **kwargs)
@@ -500,7 +500,7 @@
 
     def warning(self, message, *args, **kwargs):
         self.log(logging.EXCEPTION, message, *args, **kwargs)
-    warn=warning
+    warn = warning
 
     def _make_extra(self):
         result = dict(self.extra_proto, action=self._action)
@@ -542,7 +542,37 @@
     page = min(max(int(page), (0 if zero_based_pages else 1)), max_page)
     return limit, page
 
-def render_any_markup(name, text, code_mode=False):
+
+def _add_inline_line_numbers_to_text(text):
+    markup_text = '<div class="codehilite"><pre>'
+    for line_num, line in enumerate(text.splitlines(), 1):
+        markup_text = markup_text + '<span id="l%s" class="code_block"><span class="lineno">%s</span> %s</span>' % (line_num, line_num, line)
+    markup_text = markup_text + '</pre></div>'
+    return markup_text
+
+
+def _add_table_line_numbers_to_text(text):
+    def _prepend_whitespaces(num, max_num):
+        num, max_num = str(num), str(max_num)
+        diff = len(max_num) - len(num)
+        return ' ' * diff + num
+
+    def _len_to_str_column(l, start=1):
+        max_num = l + start
+        return '\n'.join(map(_prepend_whitespaces, range(start, max_num), [max_num] * l))
+
+    lines = text.splitlines(True)
+    linenumbers = '<td class="linenos"><div class="linenodiv"><pre>' + _len_to_str_column(len(lines)) + '</pre></div></td>'
+    markup_text = '<table class="codehilitetable"><tbody><tr>' + linenumbers + '<td class="code"><div class="codehilite"><pre>'
+    for line_num, line in enumerate(lines, 1):
+        markup_text = markup_text + '<span id="l%s" class="code_block">%s</span>' % (line_num, line)
+    markup_text = markup_text + '</pre></div></td></tr></tbody></table>'
+    return markup_text
+
+
+INLINE = 'inline'
+TABLE = 'table'
+def render_any_markup(name, text, code_mode=False, linenumbers_style=TABLE):
     """
     renders any markup format using the pypeline
     Returns jinja-safe text
@@ -552,14 +582,42 @@
     else:
         text = pylons.g.pypeline_markup.render(name, text)
         if not pylons.g.pypeline_markup.can_render(name):
-            if code_mode:
-                markup_text = '<div class="codehilite"><pre>'
-                line_num = 1
-                for line in text.splitlines():
-                    markup_text = markup_text + '<span id="l%s" class="code_block"><span class="lineno">%s</span> %s</span>' % (line_num, line_num, line)
-                    line_num += 1
-                markup_text = markup_text + '</pre></div>'
-                text = markup_text
+            if code_mode and linenumbers_style == INLINE:
+                text = _add_inline_line_numbers_to_text(text)
+            elif code_mode and linenumbers_style == TABLE:
+                text = _add_table_line_numbers_to_text(text)
             else:
                 text = '<pre>%s</pre>' % text
     return Markup(text)
+
+# copied from jinja2 dev
+# latest release, 2.6, implements this incorrectly
+# can remove and use jinja2 implementation after upgrading to 2.7
+def do_filesizeformat(value, binary=False):
+    """Format the value like a 'human-readable' file size (i.e. 13 kB,
+4.1 MB, 102 Bytes, etc). Per default decimal prefixes are used (Mega,
+Giga, etc.), if the second parameter is set to `True` the binary
+prefixes are used (Mebi, Gibi).
+"""
+    bytes = float(value)
+    base = binary and 1024 or 1000
+    prefixes = [
+        (binary and 'KiB' or 'kB'),
+        (binary and 'MiB' or 'MB'),
+        (binary and 'GiB' or 'GB'),
+        (binary and 'TiB' or 'TB'),
+        (binary and 'PiB' or 'PB'),
+        (binary and 'EiB' or 'EB'),
+        (binary and 'ZiB' or 'ZB'),
+        (binary and 'YiB' or 'YB')
+    ]
+    if bytes == 1:
+        return '1 Byte'
+    elif bytes < base:
+        return '%d Bytes' % bytes
+    else:
+        for i, prefix in enumerate(prefixes):
+            unit = base ** (i + 2)
+            if bytes < unit:
+                return '%.1f %s' % ((base * bytes / unit), prefix)
+        return '%.1f %s' % ((base * bytes / unit), prefix)
diff --git a/Allura/allura/lib/patience.py b/Allura/allura/lib/patience.py
deleted file mode 100644
index 2433cab..0000000
--- a/Allura/allura/lib/patience.py
+++ /dev/null
@@ -1,205 +0,0 @@
-import sys
-import difflib
-from itertools import chain
-
-class Region(object):
-    '''Simple utility class that keeps track of subsequences'''
-    __slots__=('seq', # sequence
-               'l',   # lower bound
-               'h',   # upper bound
-               )
-    def __init__(self, seq, l=0, h=None):
-        if h is None: h = len(seq)
-        self.seq, self.l, self.h = seq, l, h
-
-    def __iter__(self):
-        '''Iterates over the subsequence only'''
-        for i in xrange(self.l, self.h):
-            yield self.seq[i]
-
-    def __getitem__(self, i):
-        '''works like getitem on the subsequence.  Slices return new
-        regions.'''
-        if isinstance(i, slice):
-            start, stop, step = i.indices(len(self))
-            assert step == 1
-            return self.clone(l=self.l+start,h=self.l+stop)
-        elif i >= 0:
-            return self.seq[i+self.l]
-        else:
-            return self.seq[i+self.h]
-
-    def __len__(self):
-        return self.h - self.l
-
-    def __repr__(self):
-        if len(self) > 8:
-            srepr = '[%s,...]' % (','.join(repr(x) for x in self[:5]))
-        else:
-            srepr = repr(list(self))
-        return '<Region (%s,%s): %s>' % (self.l, self.h, srepr)
-
-    def clone(self, **kw):
-        '''Return a new Region based on this one with the
-        provided kw arguments replaced in the constructor.
-        '''
-        kwargs = dict(seq=self.seq, l=self.l, h=self.h)
-        kwargs.update(kw)
-        return Region(**kwargs)
-
-def region_diff(ra, rb):
-    '''generator yielding up to two matching blocks, one at the
-    beginning of the region and one at the end.  This function
-    mutates the a and b regions, removing any matched blocks.
-    '''
-    # Yield match at the beginning
-    i = 0
-    while i < len(ra) and i < len(rb) and ra[i] == rb[i]:
-        i += 1
-    if i:
-        yield ra.l, rb.l, i
-        ra.l+=i
-        rb.l+=i
-    # Yield match at the end
-    j = 0
-    while j < len(ra) and j < len(rb) and ra[-j-1]==rb[-j-1]:
-        j+=1
-    if j:
-        yield ra.h-j, rb.h-j, j
-        ra.h-=j
-        rb.h-=j
-
-def unique(a):
-    '''generator yielding unique lines of a sequence and their positions'''
-    count = {}
-    for aa in a:
-        count[aa] = count.get(aa, 0) + 1
-    for i, aa in enumerate(a):
-        if count[aa] == 1: yield aa, i
-
-def common_unique(a, b):
-    '''generator yielding pairs i,j where
-    a[i] == b[j] and a[i] and b[j] are unique within a and b,
-    in increasing j order.'''
-    uq_a = dict(unique(a))
-    for bb, j in unique(b):
-        try:
-            yield uq_a[bb], j
-        except KeyError, ke:
-            continue
-
-def patience(seq):
-    stacks = []
-    for i, j in seq:
-        last_top = None
-        for stack in stacks:
-            top_i, top_j, top_back = stack[-1]
-            if top_i > i:
-                stack.append((i, j, last_top))
-                break
-            last_top = len(stack)-1
-        else:
-            stacks.append([(i, j, last_top)])
-    if not stacks: return []
-    prev = len(stacks[-1])-1
-    seq = []
-    for stack in reversed(stacks):
-        top_i, top_j, top_back = stack[prev]
-        seq.append((top_i, top_j))
-        prev = top_back
-    return reversed(seq)
-
-def match_core(a, b):
-    '''Returns blocks that match between sequences a and b as
-    (index_a, index_b, size)
-    '''
-    ra = Region(a)
-    rb = Region(b)
-    # beginning/end match
-    for block in region_diff(ra,rb): yield block
-    # patience core
-    last_i = last_j = None
-    for i, j in chain(
-        patience(common_unique(ra, rb)),
-        [(ra.h, rb.h)]):
-        if last_i is not None:
-            for block in region_diff(ra[last_i:i], rb[last_j:j]):
-                yield block
-        last_i = i
-        last_j = j
-
-def diff_gen(a, b, opcode_gen):
-    '''Convert a sequence of SequenceMatcher opcodes to
-    unified diff-like output
-    '''
-    def _iter():
-        for op, i1, i2, j1, j2 in opcode_gen:
-            if op == 'equal':
-                yield '  ', Region(a, i1, i2)
-            if op in ('delete', 'replace'):
-                yield '- ', Region(a, i1, i2)
-            if op in ('replace', 'insert'):
-                yield '+ ', Region(b, j1, j2)
-    for prefix, rgn in _iter():
-        for line in rgn:
-            yield prefix, line
-
-def unified_diff(
-    a, b, fromfile='', tofile='', fromfiledate='',
-    tofiledate='', n=3, lineterm='\n'):
-    started = False
-    for group in SequenceMatcher(None,a,b).get_grouped_opcodes(n):
-        if not started:
-            yield '--- %s %s%s' % (fromfile, fromfiledate, lineterm)
-            yield '+++ %s %s%s' % (tofile, tofiledate, lineterm)
-            started = True
-        i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4]
-        yield "@@ -%d,%d +%d,%d @@%s" % (i1+1, i2-i1, j1+1, j2-j1, lineterm)
-        for tag, i1, i2, j1, j2 in group:
-            if tag == 'equal':
-                for line in a[i1:i2]:
-                    yield ' ' + line
-                continue
-            if tag == 'replace' or tag == 'delete':
-                for line in a[i1:i2]:
-                    yield '-' + line
-            if tag == 'replace' or tag == 'insert':
-                for line in b[j1:j2]:
-                    yield '+' + line
-
-class SequenceMatcher(difflib.SequenceMatcher):
-
-    def get_matching_blocks(self):
-        '''Uses patience diff algorithm to find matching blocks.'''
-        if self.matching_blocks is not None:
-            return self.matching_blocks
-        matching_blocks = list(match_core(self.a, self.b))
-        matching_blocks.append((len(self.a), len(self.b), 0))
-        self.matching_blocks = sorted(matching_blocks)
-        return self.matching_blocks
-
-def test(): # pragma no cover
-    if len(sys.argv) == 3:
-        fn_a = sys.argv[1]
-        fn_b = sys.argv[2]
-    else:
-        fn_a = 'a.c'
-        fn_b = 'b.c'
-    a = open(fn_a).readlines()
-    b = open(fn_b).readlines()
-    # print '====', fn_a
-    # sys.stdout.write(''.join(a))
-    # print '====', fn_b
-    # sys.stdout.write(''.join(b))
-    sm = SequenceMatcher(None, a, b)
-    # print 'Patience opcodes:', sm.get_opcodes()
-    print ''.join(unified_diff(a, b)) #pragma:printok
-    # for prefix, line in diff_gen(a, b, sm.get_opcodes()):
-    #     sys.stdout.write(''.join((prefix, line)))
-    # sm = difflib.SequenceMatcher(None, a, b)
-    # print 'Difflib opcodes:', sm.get_opcodes()
-    # for prefix, line in diff_gen(a, b, sm.get_opcodes()):
-    #     sys.stdout.write(''.join((prefix, line)))
-
-if __name__ == '__main__': # pragma no cover
-    test()
diff --git a/Allura/allura/model/notification.py b/Allura/allura/model/notification.py
index 2562c41..60ded08 100644
--- a/Allura/allura/model/notification.py
+++ b/Allura/allura/model/notification.py
@@ -121,7 +121,7 @@
                 file_info.file.seek(0, 2)
                 bytecount = file_info.file.tell()
                 file_info.file.seek(0)
-                text = "%s\n%s (%s bytes in %s)" % (text, file_info.filename, bytecount, file_info.type)
+                text = "%s\n\n\nAttachment: %s (%s; %s)" % (text, file_info.filename, h.do_filesizeformat(bytecount), file_info.type)
 
             subject = post.subject or ''
             if post.parent_id and not subject.lower().startswith('re:'):
diff --git a/Allura/allura/model/project.py b/Allura/allura/model/project.py
index 7efab5b..5ca3f4c 100644
--- a/Allura/allura/model/project.py
+++ b/Allura/allura/model/project.py
@@ -123,6 +123,7 @@
     shortname = FieldProperty(str)
     name=FieldProperty(str)
     notifications_disabled = FieldProperty(bool)
+    suppress_emails = FieldProperty(bool)
     show_download_button=FieldProperty(S.Deprecated)
     short_description=FieldProperty(str, if_missing='')
     summary=FieldProperty(str, if_missing='')
@@ -388,11 +389,17 @@
         delta_ordinal = 0
         max_ordinal = 0
         neighborhood_admin_mode = False
-        if self == self.neighborhood.neighborhood_project:
-            delta_ordinal = 1
-            neighborhood_admin_mode = True
-            entries.append({'ordinal':0,'entry':SitemapEntry('Home', self.neighborhood.url(), ui_icon="tool-home")})
 
+        if self.is_user_project:
+            entries.append({'ordinal': delta_ordinal, 'entry':SitemapEntry('Profile', "%sprofile/" % self.url(), ui_icon="tool-home")})
+            max_ordinal = delta_ordinal
+            delta_ordinal = delta_ordinal + 1
+
+        if self == self.neighborhood.neighborhood_project:
+            entries.append({'ordinal':delta_ordinal, 'entry':SitemapEntry('Home', self.neighborhood.url(), ui_icon="tool-home")})
+            max_ordinal = delta_ordinal
+            delta_ordinal = delta_ordinal + 1
+            neighborhood_admin_mode = True
 
         for sub in self.direct_subprojects:
             ordinal = sub.ordinal + delta_ordinal
@@ -417,9 +424,6 @@
             entries.append({'ordinal': max_ordinal + 1,'entry':SitemapEntry('Moderate', "%s_moderate/" % self.neighborhood.url(), ui_icon="tool-admin")})
             max_ordinal += 1
 
-        if self.is_user_project:
-            entries.append({'ordinal': max_ordinal + 1,'entry':SitemapEntry('Profile', "%sprofile/" % self.url(), ui_icon="tool-home")})
-
         entries = sorted(entries, key=lambda e: e['ordinal'])
         for e in entries:
             sitemap.children.append(e['entry'])
diff --git a/Allura/allura/model/repository.py b/Allura/allura/model/repository.py
index ba8ffef..5ed3a87 100644
--- a/Allura/allura/model/repository.py
+++ b/Allura/allura/model/repository.py
@@ -5,6 +5,7 @@
 import logging
 import string
 import re
+from difflib import SequenceMatcher
 from hashlib import sha1
 from datetime import datetime
 from collections import defaultdict
@@ -20,7 +21,6 @@
 from ming.orm import FieldProperty, session, Mapper
 from ming.orm.declarative import MappedClass
 
-from allura.lib.patience import SequenceMatcher
 from allura.lib import helpers as h
 from allura.lib import utils
 
diff --git a/Allura/allura/nf/allura/css/site_style.css b/Allura/allura/nf/allura/css/site_style.css
index 3e08a4f..a07fd51 100644
--- a/Allura/allura/nf/allura/css/site_style.css
+++ b/Allura/allura/nf/allura/css/site_style.css
@@ -20,7 +20,7 @@
  * Color variables for the theme.
  *
  */
-/* 
+/*
  * Your run-of-the-mill reset CSS, inspired by:
  *
  *   yui.yahooapis.com/2.8.1/build/base/base.css
@@ -99,7 +99,7 @@
   background: #fff;
 }
 
-/* 
+/*
  * Setup a minimal, baseline CSS, layered on top of a reset
  * to define the default styles we've come to expect. Inspired by:
  *
@@ -245,7 +245,7 @@
   font-weight: normal;
 }
 
-/* 
+/*
  * General CSS rules governing high-level elements.
  *
  */
@@ -380,7 +380,7 @@
   padding-left: 1em;
 }
 
-/* 
+/*
  * Style elements in the main header and footer areas.
  *
  */
@@ -2707,3 +2707,43 @@
 .neighborhood_feed_entry h3 {
   font-size: 1.1em;
 }
+
+/*linenumbers in codeblock viewer style*/
+
+table.codehilitetable {
+    background: #F8F8F8;
+    margin-left:0px;
+}
+
+td.linenos {
+    width:auto;
+    padding: 0;
+}
+div.linenodiv {
+
+}
+td.linenos pre {
+    font-size: 100%;
+    padding: 1px;
+    padding-left: 7px;
+    padding-right: 5px;
+    margin-left: 15px;
+    background-color: #EBEBEB;
+    color: #555;
+    border-right: solid 1px #DDD;
+}
+td.code {
+    padding-left: 0px;
+    width:100%;
+}
+
+div.codehilite pre {
+    padding-left: 0px;
+    padding-top:10px;
+    padding-bottom:10px;
+}
+div.codehilite pre div.code_block {
+    padding-left:10px;
+    width: 97%;
+}
+
diff --git a/Allura/allura/public/nf/css/forge/hilite.css b/Allura/allura/public/nf/css/forge/hilite.css
index cf5a6a7..44d2eb4 100644
--- a/Allura/allura/public/nf/css/forge/hilite.css
+++ b/Allura/allura/public/nf/css/forge/hilite.css
@@ -66,4 +66,9 @@
 .codehilite div { margin:0; padding: 0; }
 .codehilite .code_block { width:100%; }
 .codehilite .code_block:hover { background-color: #ffff99; }
-.codehilite .lineno { background-color: #ebebeb; display:inline-block; padding:0 .5em; border-width: 0 1px 0 0; border-style: solid; border-color: #ddd; }
+.codehilite .lineno { background-color: #ebebeb;
+                      display:inline-block;
+                      padding:0 .5em;
+                      border-width: 0 1px 0 0;
+                      border-style: solid;
+                      border-color: #ddd; }
diff --git a/Allura/allura/tasks/repo_tasks.py b/Allura/allura/tasks/repo_tasks.py
index 8a9c2ee..f0c24d5 100644
--- a/Allura/allura/tasks/repo_tasks.py
+++ b/Allura/allura/tasks/repo_tasks.py
@@ -1,10 +1,13 @@
 import shutil
 import logging
+import traceback
 
 from pylons import c
 
 from allura.lib.decorators import task
 from allura.lib.repository import RepositoryApp
+from allura.lib import helpers as h
+from allura.tasks.mail_tasks import sendmail
 
 @task
 def init(**kwargs):
@@ -20,15 +23,49 @@
     cloned_from_path,
     cloned_from_name,
     cloned_from_url):
-    from allura import model as M
-    c.app.repo.init_as_clone(
-        cloned_from_path,
-        cloned_from_name,
-        cloned_from_url)
-    M.Notification.post_user(
-        c.user, c.app.repo, 'created',
-        text='Repository %s/%s created' % (
-            c.project.shortname, c.app.config.options.mount_point))
+    try:
+        from allura import model as M
+        c.app.repo.init_as_clone(
+            cloned_from_path,
+            cloned_from_name,
+            cloned_from_url)
+        M.Notification.post_user(
+            c.user, c.app.repo, 'created',
+            text='Repository %s/%s created' % (
+                c.project.shortname, c.app.config.options.mount_point))
+        if not c.project.suppress_emails:
+            sendmail(
+                destinations=[str(c.user._id)],
+                fromaddr=u'SourceForge.net <noreply+project-upgrade@in.sf.net>',
+                reply_to=u'noreply@in.sf.net',
+                subject=u'SourceForge Repo Clone Complete',
+                message_id=h.gen_message_id(),
+                text=u''.join([
+                    u'Clone of repo %s in project %s from %s is complete. Your repo is now ready to use.\n'
+                ]) % (c.app.config.options.mount_point, c.project.shortname, cloned_from_url))
+    except:
+        sendmail(
+            destinations=['sfengineers@geek.net'],
+            fromaddr=u'SourceForge.net <noreply+project-upgrade@in.sf.net>',
+            reply_to=u'noreply@in.sf.net',
+            subject=u'SourceForge Repo Clone Failure',
+            message_id=h.gen_message_id(),
+            text=u''.join([
+                u'Clone of repo %s in project %s from %s failed.\n',
+                u'\n',
+                u'%s',
+            ]) % (c.app.config.options.mount_point, c.project.shortname, cloned_from_url, traceback.format_exc()))
+        if not c.project.suppress_emails:
+            sendmail(
+                destinations=[str(c.user._id)],
+                fromaddr=u'SourceForge.net <noreply+project-upgrade@in.sf.net>',
+                reply_to=u'noreply@in.sf.net',
+                subject=u'SourceForge Repo Clone Failed',
+                message_id=h.gen_message_id(),
+                text=u''.join([
+                    u'Clone of repo %s in project %s from %s failed. ',
+                    u'The SourceForge engineering team has been notified.\n',
+                ]) % (c.app.config.options.mount_point, c.project.shortname, cloned_from_url))
 
 @task
 def reclone(*args, **kwargs):
diff --git a/Allura/allura/templates/repo/file.html b/Allura/allura/templates/repo/file.html
index 1e75f1e..9928258 100644
--- a/Allura/allura/templates/repo/file.html
+++ b/Allura/allura/templates/repo/file.html
@@ -70,7 +70,7 @@
     <div class="clip grid-19">
       <h3><span class="ico-l"><b data-icon="{{g.icons['table'].char}}" class="ico {{g.icons['table'].css}}"></b> {{h.really_unicode(blob.name)}}</span></h3>
       {% if blob.has_pypeline_view %}
-        {{h.render_any_markup(blob.name, blob.text, code_mode=True)}}
+        {{h.render_any_markup(blob.name, blob.text, code_mode=True, linenumbers_style=h.TABLE)}}
       {% else %}
         {{g.highlight(blob.text, filename=blob.name)}}
       {% endif %}
diff --git a/Allura/allura/templates/repo/fork.html b/Allura/allura/templates/repo/fork.html
index d760f24..77af8d4 100644
--- a/Allura/allura/templates/repo/fork.html
+++ b/Allura/allura/templates/repo/fork.html
@@ -16,9 +16,13 @@
         {% endfor %}
         </select>
       </div>
-      <label class="grid-4">Repository Name:</label>
+      <label class="grid-4">Label:</label>
       <div class="grid-15">
-        <input type="text" name="to_name" value="{{to_name}}"/>
+        <input type="text" name="mount_label" value="{{mount_label}}"/>
+      </div>
+      <label class="grid-4">Mount point:</label>
+      <div class="grid-15">
+        <input type="text" name="mount_point" value="{{mount_point}}"/>
       </div>
       <label class="grid-4">&nbsp;</label>
       <div class="grid-15">
diff --git a/Allura/allura/tests/functional/test_user_profile.py b/Allura/allura/tests/functional/test_user_profile.py
index 9ea68e6..1450d41 100644
--- a/Allura/allura/tests/functional/test_user_profile.py
+++ b/Allura/allura/tests/functional/test_user_profile.py
@@ -8,6 +8,7 @@
     @td.with_user_project('test-admin')
     def test_profile(self):
         response = self.app.get('/u/test-admin/profile/')
+        assert '<h2 class="dark title">Test Admin' in response
         assert 'OpenIDs' in response
         response = self.app.get('/u/test-admin/profile/configuration')
         assert 'Configure Dashboard' in response
diff --git a/Allura/allura/tests/model/test_discussion.py b/Allura/allura/tests/model/test_discussion.py
index c43efe3..f80805e 100644
--- a/Allura/allura/tests/model/test_discussion.py
+++ b/Allura/allura/tests/model/test_discussion.py
@@ -167,7 +167,7 @@
     p = t.post(text=u'test message', forum= None, subject= '', file_info=fs)
     ThreadLocalORMSession.flush_all()
     n = M.Notification.query.get(subject=u'[test:wiki] Test comment notification')
-    assert u'test message\nfake.txt (37 bytes in text/plain)'==n.text
+    assert_equals(u'test message\n\n\nAttachment: fake.txt (37 Bytes; text/plain)', n.text)
 
 @with_setup(setUp, tearDown)
 def test_discussion_delete():
diff --git a/Allura/allura/tests/test_commands.py b/Allura/allura/tests/test_commands.py
index 6bcdb8a..94de9dd 100644
--- a/Allura/allura/tests/test_commands.py
+++ b/Allura/allura/tests/test_commands.py
@@ -1,10 +1,12 @@
 from nose.tools import assert_raises
 
-from alluratest.controller import setup_basic_test, setup_global_objects
-from allura.command import script, set_neighborhood_features
-from allura import model as M
-from allura.lib.exceptions import InvalidNBFeatureValueError
+from ming.orm.ormsession import ThreadLocalORMSession
 
+from alluratest.controller import setup_basic_test, setup_global_objects
+from allura.command import script, set_neighborhood_features, rssfeeds
+from allura import model as M
+from forgeblog import model as BM
+from allura.lib.exceptions import InvalidNBFeatureValueError
 
 test_config = 'test.ini#main'
 
@@ -116,3 +118,21 @@
     assert_raises(InvalidNBFeatureValueError, cmd.run, [test_config, str(n_id), 'css', '2.8'])
     assert_raises(InvalidNBFeatureValueError, cmd.run, [test_config, str(n_id), 'css', 'None'])
     assert_raises(InvalidNBFeatureValueError, cmd.run, [test_config, str(n_id), 'css', 'True'])
+
+def test_pull_rss_feeds():
+    base_app =  M.AppConfig.query.find().all()[0]
+    tmp_app = M.AppConfig(tool_name=u'Blog', discussion_id=base_app.discussion_id,
+                          project_id=base_app.project_id,
+                          options={u'ordinal': 0, u'show_right_bar': True,
+                                    u'project_name': base_app.project.name,
+                                    u'mount_point': u'blog',
+                                    u'mount_label': u'Blog'})
+    new_external_feeds = ['http://wordpress.org/news/feed/']
+    BM.Globals(app_config_id=tmp_app._id, external_feeds=new_external_feeds)
+    ThreadLocalORMSession.flush_all()
+
+    cmd = rssfeeds.RssFeedsCommand('pull-rss-feeds')
+    cmd.run([test_config, '-a', tmp_app._id])
+    cmd.command()
+
+    assert len(BM.BlogPost.query.find({'app_config_id': tmp_app._id}).all()) > 0
diff --git a/Allura/allura/tests/test_patience.py b/Allura/allura/tests/test_patience.py
deleted file mode 100644
index 0537544..0000000
--- a/Allura/allura/tests/test_patience.py
+++ /dev/null
@@ -1,76 +0,0 @@
-from os import path, environ
-from collections import defaultdict
-
-from allura.lib import patience
-
-def text2lines(text):
-    return [l + '\n' for l in text.split('\n')]
-
-def test_region():
-    r = patience.Region('foobar')
-    r2 = r.clone()
-    assert id(r) != id(r2)
-    assert '-'.join(r) == '-'.join(r2)
-    subr = r[1:5]
-    assert type(subr) is type(r)
-    assert ''.join(subr) == ''.join(r)[1:5]
-    repr(r)
-    repr(patience.Region('fffffffffffffffffffffffffffffffffffffffff'))
-
-def test_unified_diff():
-    text1 = '''\
-from paste.deploy import loadapp
-from paste.deploy import loadapp
-from paste.deploy import loadapp
-from paste.deploy import loadapp
-from paste.script.appinstall import SetupCommand
-from paste.script.appinstall import SetupCommand
-from paste.script.appinstall import SetupCommand
-from paste.script.appinstall import SetupCommand
-from paste.deploy import appconfig
-'''
-    text2 = '''\
-from paste.deploy import loadapp
-from paste.deploy import loadapp
-from paste.deploy import loadapp
-from paste.deploy import loadapp
-from paste.script.appinstall import SetupCommand2
-from paste.script.appinstall import SetupCommand3
-from paste.script.appinstall import SetupCommand4
-from paste.deploy import appconfig
-'''
-    line_uni_diff = '''\
- from paste.deploy import loadapp
- from paste.deploy import loadapp
- from paste.deploy import loadapp
--from paste.script.appinstall import SetupCommand
--from paste.script.appinstall import SetupCommand
--from paste.script.appinstall import SetupCommand
--from paste.script.appinstall import SetupCommand
-+from paste.script.appinstall import SetupCommand2
-+from paste.script.appinstall import SetupCommand3
-+from paste.script.appinstall import SetupCommand4
- from paste.deploy import appconfig'''
-
-    line_diff = '''\
- from paste.deploy import loadapp
-''' + line_uni_diff
-
-    lines1 = text2lines(text1)
-    lines2 = text2lines(text2)
-    diff = patience.unified_diff(lines1, lines2)
-    diff = ''.join(diff)
-    assert diff == '''\
----  
-+++  
-@@ -2,9 +2,8 @@
-%s
- 
-''' % line_uni_diff, '=' + diff + '='
-
-    sm = patience.SequenceMatcher(None, lines1, lines2)
-    buf = ''
-    for prefix, line in patience.diff_gen(lines1, lines2, sm.get_opcodes()):
-        assert prefix[1] == ' '
-        buf += prefix[0] + line
-    assert buf == line_diff + '\n \n', '=' + buf + '='
diff --git a/Allura/setup.py b/Allura/setup.py
index 6420acd..9b4f5a0 100644
--- a/Allura/setup.py
+++ b/Allura/setup.py
@@ -113,6 +113,7 @@
     create-neighborhood = allura.command:CreateNeighborhoodCommand
     create-trove-categories = allura.command:CreateTroveCategoriesCommand
     set-neighborhood-features = allura.command:SetNeighborhoodFeaturesCommand
+    pull-rss-feeds = allura.command.rssfeeds:RssFeedsCommand
 
     [easy_widgets.resources]
     ew_resources=allura.config.resources:register_ew_resources
diff --git a/Allura/test.ini b/Allura/test.ini
index 55be3ea..a8b1c81 100644
--- a/Allura/test.ini
+++ b/Allura/test.ini
@@ -75,7 +75,7 @@
 
 scm.repos.root = /tmp
 
-stats.sample_rate=0
+#stats.sample_rate = 0
 
 disable_csrf_protection=1
 
diff --git a/ForgeBlog/forgeblog/main.py b/ForgeBlog/forgeblog/main.py
index bacc532..5d039b3 100644
--- a/ForgeBlog/forgeblog/main.py
+++ b/ForgeBlog/forgeblog/main.py
@@ -12,12 +12,14 @@
 from formencode import validators
 from webob import exc
 
+from ming.orm import session
+
 # Pyforge-specific imports
 from allura.app import Application, ConfigOption, SitemapEntry
 from allura.app import DefaultAdminController
 from allura.lib import helpers as h
 from allura.lib.search import search
-from allura.lib.decorators import require_post
+from allura.lib.decorators import require_post, Property
 from allura.lib.security import has_access, require_access
 from allura.lib import widgets as w
 from allura.lib.widgets.subscriptions import SubscribeForm
@@ -56,6 +58,7 @@
     ordinal=14
     installable=True
     config_options = Application.config_options
+    default_external_feeds = []
     icons={
         24:'images/blog_24.png',
         32:'images/blog_32.png',
@@ -67,6 +70,24 @@
         self.root = RootController()
         self.admin = BlogAdminController(self)
 
+    @Property
+    def external_feeds_list():
+        def fget(self):
+            globals = BM.Globals.query.get(app_config_id=self.config._id)
+            if globals is not None:
+                external_feeds = globals.external_feeds
+            else:
+                external_feeds = self.default_external_feeds
+            return external_feeds
+        def fset(self, new_external_feeds):
+            globals = BM.Globals.query.get(app_config_id=self.config._id)
+            if globals is not None:
+                globals.external_feeds = new_external_feeds
+            elif len(new_external_feeds) > 0:
+                globals = BM.Globals(app_config_id=self.config._id, external_feeds=new_external_feeds)
+            if globals is not None:
+                session(globals).flush()
+
     @property
     @h.exceptionless([], log)
     def sitemap(self):
@@ -94,7 +115,12 @@
         return links
 
     def admin_menu(self):
-        return super(ForgeBlogApp, self).admin_menu(force_options=True)
+        admin_url = c.project.url() + 'admin/' + self.config.options.mount_point + '/'
+        # temporarily disabled until some bugs are fixed
+        links = []#[SitemapEntry('External feeds', admin_url + 'exfeed', className='admin_modal')]
+        links += super(ForgeBlogApp, self).admin_menu(force_options=True)
+        return links
+        #return super(ForgeBlogApp, self).admin_menu(force_options=True)
 
     def install(self, project):
         'Set up any default permissions and roles here'
@@ -170,7 +196,7 @@
         require_access(c.app, 'write')
         now = datetime.utcnow()
         post = dict(
-            state='draft')
+            state='published')
         c.form = W.new_post_form
         return dict(post=post)
 
@@ -359,3 +385,34 @@
         self.app.config.options['show_discussion'] = show_discussion and True or False
         flash('Blog options updated')
         redirect(h.really_unicode(c.project.url()+'admin/tools').encode('utf-8'))
+
+    @without_trailing_slash
+    @expose('jinja:forgeblog:templates/blog/admin_exfeed.html')
+    def exfeed(self):
+        #self.app.external_feeds_list = ['feed1', 'feed2']
+        #log.info("EXFEED: %s" % self.app.external_feeds_list)
+        feeds_list = []
+        for feed in self.app.external_feeds_list:
+            feeds_list.append(feed)
+        return dict(app=self.app,
+                    feeds_list=feeds_list,
+                    allow_config=has_access(self.app, 'configure')())
+
+    @without_trailing_slash
+    @expose()
+    @require_post()
+    def set_exfeed(self, **kw):
+        new_exfeed = kw.get('new_exfeed', None)
+        exfeed_val = kw.get('exfeed', [])
+        if type(exfeed_val) == unicode:
+            exfeed_list = []
+            exfeed_list.append(exfeed_val)
+        else:
+            exfeed_list = exfeed_val
+
+        if new_exfeed is not None and new_exfeed != '':
+            exfeed_list.append(new_exfeed)
+
+        self.app.external_feeds_list = exfeed_list
+        flash('External feeds updated')
+        redirect(c.project.url()+'admin/tools')
diff --git a/ForgeBlog/forgeblog/model/__init__.py b/ForgeBlog/forgeblog/model/__init__.py
index 3f6c73e..3a7399f 100644
--- a/ForgeBlog/forgeblog/model/__init__.py
+++ b/ForgeBlog/forgeblog/model/__init__.py
@@ -1 +1 @@
-from blog import BlogPost, Attachment, BlogPostSnapshot
+from blog import Globals, BlogPost, Attachment, BlogPostSnapshot
diff --git a/ForgeBlog/forgeblog/model/blog.py b/ForgeBlog/forgeblog/model/blog.py
index 938b408..06177d7 100644
--- a/ForgeBlog/forgeblog/model/blog.py
+++ b/ForgeBlog/forgeblog/model/blog.py
@@ -1,3 +1,4 @@
+import difflib
 from datetime import datetime
 from random import randint
 
@@ -9,13 +10,28 @@
 
 from ming import schema
 from ming.orm import FieldProperty, ForeignIdProperty, Mapper, session, state
+from ming.orm.declarative import MappedClass
+
 from allura import model as M
 from allura.lib import helpers as h
-from allura.lib import utils, patience
+from allura.lib import utils
 
 config = utils.ConfigProxy(
     common_suffix='forgemail.domain')
 
+class Globals(MappedClass):
+
+    class __mongometa__:
+        name = 'blog-globals'
+        session = M.project_orm_session
+        indexes = [ 'app_config_id' ]
+
+    type_s = 'BlogGlobals'
+    _id = FieldProperty(schema.ObjectId)
+    app_config_id = ForeignIdProperty('AppConfig', if_missing=lambda:c.app.config._id)
+    external_feeds=FieldProperty([str])
+
+
 class BlogPostSnapshot(M.Snapshot):
     class __mongometa__:
         name='blog_post_snapshot'
@@ -166,7 +182,7 @@
             v2 = self
             la = [ line + '\n'  for line in v1.text.splitlines() ]
             lb = [ line + '\n'  for line in v2.text.splitlines() ]
-            diff = ''.join(patience.unified_diff(
+            diff = ''.join(difflib.unified_diff(
                     la, lb,
                     'v%d' % v1.version,
                     'v%d' % v2.version))
diff --git a/ForgeBlog/forgeblog/templates/blog/admin_exfeed.html b/ForgeBlog/forgeblog/templates/blog/admin_exfeed.html
new file mode 100644
index 0000000..2c337bc
--- /dev/null
+++ b/ForgeBlog/forgeblog/templates/blog/admin_exfeed.html
@@ -0,0 +1,27 @@
+<form method="POST" action="{{c.project.url()}}admin/{{app.config.options.mount_point}}/set_exfeed">
+  <label class="grid-13">Existing external feeds:</label>
+  <div class="grid-13">
+    <ul>
+    {% if allow_config %}
+     {% for feed in feeds_list %}
+     <li><input type="checkbox" name="exfeed" value="{{ feed }}" checked="checked"><span>{{ feed }}</span></li>
+     {% endfor %} 
+    {% else %}
+     {% for feed in feeds_list %}
+     <li><span>{{ feed.value }}</span></li>
+     {% endfor %} 
+    {% endif %}
+  </div>
+  {% if allow_config %}
+    <div class="grid-13">&nbsp;</div>
+    <div class="grid-13">
+       <input type="text" name="new_exfeed" id="new_exfeed" value=""/>
+    </div>
+    <div class="grid-13">&nbsp;</div>
+    <hr>
+    <div class="grid-13">&nbsp;</div>
+    <div class="grid-13">
+      <input type="submit" value="Save"/>
+    </div>
+  {% endif %}
+</form>
diff --git a/ForgeBlog/forgeblog/tests/functional/test_root.py b/ForgeBlog/forgeblog/tests/functional/test_root.py
index 4f6a4b1..1f50dc0 100644
--- a/ForgeBlog/forgeblog/tests/functional/test_root.py
+++ b/ForgeBlog/forgeblog/tests/functional/test_root.py
@@ -56,6 +56,7 @@
 
     def test_root_new_post(self):
         response = self.app.get('/blog/new')
+        assert '<option selected value="published">Published</option>' in response
         assert 'Enter your title here' in response
 
     def test_validation(self):
diff --git a/ForgeGit/forgegit/tests/functional/test_controllers.py b/ForgeGit/forgegit/tests/functional/test_controllers.py
index 2660955..b9816d7 100644
--- a/ForgeGit/forgegit/tests/functional/test_controllers.py
+++ b/ForgeGit/forgegit/tests/functional/test_controllers.py
@@ -37,18 +37,26 @@
         ThreadLocalORMSession.close_all()
 
     def test_fork(self):
+        r = self.app.get('%sfork/' % c.app.repo.url())
+        assert '<input type="text" name="mount_point" value="test"/>' in r
+        assert '<input type="text" name="mount_label" value="test - Git"/>' in r
+
         to_project = M.Project.query.get(shortname='test2', neighborhood_id=c.project.neighborhood_id)
+        mount_point = 'reponame'
         r = self.app.post('/src-git/fork', params=dict(
             project_id=str(to_project._id),
-            to_name='code'))
+            mount_point=mount_point,
+            mount_label='Test forked repository'))
+        assert "{status: 'error'}" not in str(r.follow())
         cloned_from = c.app.repo
-        with h.push_context('test2', 'code', neighborhood='Projects'):
+        with h.push_context('test2', mount_point, neighborhood='Projects'):
             c.app.repo.init_as_clone(
                     cloned_from.full_fs_path,
                     cloned_from.app.config.script_name(),
                     cloned_from.full_fs_path)
-        r = self.app.get('/p/test2/code').follow().follow().follow()
+        r = self.app.get('/p/test2/%s' % mount_point).follow().follow().follow()
         assert 'Clone of' in r
+        assert 'Test forked repository' in r
         r = self.app.get('/src-git/').follow().follow()
         assert 'Forks' in r
 
@@ -56,7 +64,7 @@
         to_project = M.Project.query.get(shortname='test2', neighborhood_id=c.project.neighborhood_id)
         r = self.app.post('/src-git/fork', params=dict(
             project_id=str(to_project._id),
-            to_name='code'))
+            mount_point='code'))
         cloned_from = c.app.repo
         with h.push_context('test2', 'code', neighborhood='Projects'):
             c.app.repo.init_as_clone(
@@ -147,8 +155,8 @@
     def test_file(self):
         ci = self._get_ci()
         resp = self.app.get(ci + 'tree/README')
-        assert 'README' in resp.html.find('h2',{'class':'dark title'}).contents[2]
-        content = str(resp.html.find('div',{'class':'clip grid-19'}))
+        assert 'README' in resp.html.find('h2', {'class':'dark title'}).contents[2]
+        content = str(resp.html.find('div', {'class':'clip grid-19'}))
         assert 'This is readme' in content, content
         assert '<span id="l1" class="code_block">' in resp
         assert 'var hash = window.location.hash.substring(1);' in resp
@@ -174,6 +182,6 @@
     def test_file_force_display(self):
         ci = self._get_ci()
         resp = self.app.get(ci + 'tree/README?force=True')
-        content = str(resp.html.find('div',{'class':'clip grid-19'}))
+        content = str(resp.html.find('div', {'class':'clip grid-19'}))
         assert re.search(r'<pre>.*This is readme', content), content
         assert '</pre>' in content, content
diff --git a/ForgeHg/forgehg/tests/functional/test_controllers.py b/ForgeHg/forgehg/tests/functional/test_controllers.py
index 896d550..c01601e 100644
--- a/ForgeHg/forgehg/tests/functional/test_controllers.py
+++ b/ForgeHg/forgehg/tests/functional/test_controllers.py
@@ -1,6 +1,9 @@
 import json
 
 import pkg_resources
+import pylons
+pylons.c = pylons.tmpl_context
+pylons.g = pylons.app_globals
 from pylons import c
 from ming.orm import ThreadLocalORMSession
 from datadiff.tools import assert_equal
@@ -35,7 +38,8 @@
         to_project = M.Project.query.get(shortname='test2', neighborhood_id=c.project.neighborhood_id)
         r = self.app.post('/src-hg/fork', params=dict(
             project_id=str(to_project._id),
-            to_name='code'))
+            mount_point='code'))
+        assert "{status: 'error'}" not in str(r.follow())
         cloned_from = c.app.repo
         with h.push_context('test2', 'code', neighborhood='Projects'):
             c.app.repo.init_as_clone(
@@ -51,7 +55,8 @@
         to_project = M.Project.query.get(shortname='test2', neighborhood_id=c.project.neighborhood_id)
         r = self.app.post('/src-hg/fork', params=dict(
             project_id=str(to_project._id),
-            to_name='code'))
+            mount_point='code'))
+        assert "{status: 'error'}" not in str(r.follow())
         cloned_from = c.app.repo
         with h.push_context('test2', 'code', neighborhood='Projects'):
             c.app.repo.init_as_clone(
@@ -117,14 +122,14 @@
     def test_tree(self):
         ci = self._get_ci()
         resp = self.app.get(ci + 'tree/')
-        assert len(resp.html.findAll('tr')) ==2, resp.showbrowser()
+        assert len(resp.html.findAll('tr')) == 2, resp.showbrowser()
         assert 'README' in resp, resp.showbrowser()
 
     def test_file(self):
         ci = self._get_ci()
         resp = self.app.get(ci + 'tree/README')
-        assert 'README' in resp.html.find('h2',{'class':'dark title'}).contents[2]
-        content = str(resp.html.find('div',{'class':'clip grid-19'}))
+        assert 'README' in resp.html.find('h2', {'class':'dark title'}).contents[2]
+        content = str(resp.html.find('div', {'class':'clip grid-19'}))
         assert 'This is readme' in content, content
         assert '<span id="l1" class="code_block">' in resp
         assert 'var hash = window.location.hash.substring(1);' in resp
diff --git a/ForgeSVN/forgesvn/model/svn.py b/ForgeSVN/forgesvn/model/svn.py
index 399c4f2..128dc2e 100644
--- a/ForgeSVN/forgesvn/model/svn.py
+++ b/ForgeSVN/forgesvn/model/svn.py
@@ -259,12 +259,15 @@
             log.info('ClientError processing %r %r, treating as empty', ci, self._repo, exc_info=True)
             log_entry = Object(date=0, message='', changed_paths=[])
         # Save commit metadata
+        log_date = None
+        if hasattr(log_entry, 'date'):
+            log_date = datetime.utcfromtimestamp(log_entry.date)
         ci.committed = Object(
             name=log_entry.get('author', '--none--'),
             email='',
-            date=datetime.utcfromtimestamp(log_entry.date))
+            date=log_date)
         ci.authored=Object(ci.committed)
-        ci.message=log_entry.message
+        ci.message=log_entry.get("message", "--none--")
         if revno > 1:
             parent_oid = self._oid(revno - 1)
             ci.parent_ids = [ parent_oid ]
@@ -278,13 +281,14 @@
             D=ci.diffs.removed,
             M=ci.diffs.changed,
             R=ci.diffs.changed)
-        for path in log_entry.changed_paths:
-            if path.copyfrom_path:
-                ci.diffs.copied.append(dict(
-                        old=h.really_unicode(path.copyfrom_path),
-                        new=h.really_unicode(path.path)))
-                continue
-            lst[path.action].append(h.really_unicode(path.path))
+        if hasattr(log_entry, 'changed_paths'):
+            for path in log_entry.changed_paths:
+                if path.copyfrom_path:
+                    ci.diffs.copied.append(dict(
+                            old=h.really_unicode(path.copyfrom_path),
+                            new=h.really_unicode(path.path)))
+                    continue
+                lst[path.action].append(h.really_unicode(path.path))
 
     def refresh_commit_info(self, oid, seen_object_ids, lazy=True):
         from allura.model.repo import CommitDoc
@@ -301,15 +305,18 @@
         except pysvn.ClientError:
             log.info('ClientError processing %r %r, treating as empty', oid, self._repo, exc_info=True)
             log_entry = Object(date='', message='', changed_paths=[])
+        log_date = None
+        if hasattr(log_entry, 'date'):
+            log_date = datetime.utcfromtimestamp(log_entry.date)
         user = Object(
             name=log_entry.get('author', '--none--'),
             email='',
-           date=datetime.utcfromtimestamp(log_entry.date))
+           date=log_date)
         args = dict(
             tree_id=None,
             committed=user,
             authored=user,
-            message=log_entry.message,
+            message=log_entry.get("message", "--none--"),
             parent_ids=[],
             child_ids=[])
         if revno > 1:
diff --git a/ForgeSVN/forgesvn/tests/functional/test_controllers.py b/ForgeSVN/forgesvn/tests/functional/test_controllers.py
index 7274804..a021b58 100644
--- a/ForgeSVN/forgesvn/tests/functional/test_controllers.py
+++ b/ForgeSVN/forgesvn/tests/functional/test_controllers.py
@@ -73,8 +73,8 @@
 
     def test_file(self):
         resp = self.app.get('/src/1/tree/README')
-        assert 'README' in resp.html.find('h2',{'class':'dark title'}).contents[2]
-        content = str(resp.html.find('div',{'class':'clip grid-19'}))
+        assert 'README' in resp.html.find('h2', {'class':'dark title'}).contents[2]
+        content = str(resp.html.find('div', {'class':'clip grid-19'}))
         assert 'This is readme' in content, content
         assert '<span id="l1" class="code_block">' in resp
         assert 'var hash = window.location.hash.substring(1);' in resp
diff --git a/ForgeTracker/forgetracker/data/ticket_changed_tmpl b/ForgeTracker/forgetracker/data/ticket_changed_tmpl
index 6f735ad..3dbf2f7 100644
--- a/ForgeTracker/forgetracker/data/ticket_changed_tmpl
+++ b/ForgeTracker/forgetracker/data/ticket_changed_tmpl
@@ -1,4 +1,4 @@
-{% python from allura.lib import patience %}\
+{% python import difflib %}\
 {% python from allura.model import User %}\
 {% for key, values in changelist %}\
 {% choose %}\
@@ -9,7 +9,7 @@
 
 ~~~~
 
-${'\n'.join(patience.unified_diff(
+${'\n'.join(difflib.unified_diff(
                             a=values[0].splitlines(),
                             b=values[1].splitlines(),
                             fromfile='old',
diff --git a/ForgeTracker/forgetracker/model/ticket.py b/ForgeTracker/forgetracker/model/ticket.py
index 7e0d19c..0e81a91 100644
--- a/ForgeTracker/forgetracker/model/ticket.py
+++ b/ForgeTracker/forgetracker/model/ticket.py
@@ -1,6 +1,7 @@
 import logging
 import urllib
 import json
+import difflib
 from datetime import datetime, timedelta
 
 import bson
@@ -20,7 +21,6 @@
 from allura.model import Artifact, VersionedArtifact, Snapshot, project_orm_session, BaseAttachment
 from allura.model import User, Feed, Thread, Notification, ProjectRole
 from allura.model import ACE, ALL_PERMISSIONS, DENY_ALL
-from allura.lib import patience
 from allura.lib import security
 from allura.lib.search import search_artifact
 from allura.lib import utils
@@ -392,7 +392,7 @@
             if old.description != self.description:
                 changes.append('Description updated:')
                 changes.append('\n'.join(
-                        patience.unified_diff(
+                        difflib.unified_diff(
                             a=old.description.split('\n'),
                             b=self.description.split('\n'),
                             fromfile='description-old',
diff --git a/ForgeTracker/forgetracker/tracker_main.py b/ForgeTracker/forgetracker/tracker_main.py
index c3635e9..0cd3b7f 100644
--- a/ForgeTracker/forgetracker/tracker_main.py
+++ b/ForgeTracker/forgetracker/tracker_main.py
@@ -1241,8 +1241,9 @@
         self.app.globals.closed_status_names=post_data['closed_status_names']
         custom_fields = post_data.get('custom_fields', [])
         for field in custom_fields:
-            field['name'] = '_' + '_'.join([
-                w for w in NONALNUM_RE.split(field['label'].lower()) if w])
+            if 'name' not in field or not field['name']:
+                field['name'] = '_' + '_'.join([
+                    w for w in NONALNUM_RE.split(field['label'].lower()) if w])
             if field['type'] == 'milestone':
                 field.setdefault('milestones', [])
 
diff --git a/ForgeTracker/forgetracker/widgets/admin_custom_fields.py b/ForgeTracker/forgetracker/widgets/admin_custom_fields.py
index c222202..4b2dacc 100644
--- a/ForgeTracker/forgetracker/widgets/admin_custom_fields.py
+++ b/ForgeTracker/forgetracker/widgets/admin_custom_fields.py
@@ -81,6 +81,7 @@
         yield ew.JSLink('tracker_js/custom-fields.js')
 
     fields = [
+        ew.HiddenField(name='name'),
         ew.TextField(name='label'),
         ew.Checkbox(
             name='show_in_search',
diff --git a/ForgeWiki/forgewiki/model/wiki.py b/ForgeWiki/forgewiki/model/wiki.py
index 2317aa8..0b41a13 100644
--- a/ForgeWiki/forgewiki/model/wiki.py
+++ b/ForgeWiki/forgewiki/model/wiki.py
@@ -1,4 +1,5 @@
 import pylons
+import difflib
 pylons.c = pylons.tmpl_context
 pylons.g = pylons.app_globals
 from pylons import g #g is a namespace for globally accessable app helpers
@@ -11,7 +12,6 @@
 from allura.model import VersionedArtifact, Snapshot, Feed, Thread, Post, User, BaseAttachment
 from allura.model import Notification, project_orm_session
 from allura.lib import helpers as h
-from allura.lib import patience
 from allura.lib import utils
 
 config = utils.ConfigProxy(
@@ -86,7 +86,7 @@
             v2 = self
             la = [ line + '\n'  for line in v1.text.splitlines() ]
             lb = [ line + '\n'  for line in v2.text.splitlines() ]
-            diff = ''.join(patience.unified_diff(
+            diff = ''.join(difflib.unified_diff(
                     la, lb,
                     'v%d' % v1.version,
                     'v%d' % v2.version))
diff --git a/requirements-common.txt b/requirements-common.txt
index c6b1a0f..19dfc9f 100644
--- a/requirements-common.txt
+++ b/requirements-common.txt
@@ -15,6 +15,7 @@
 # dep of Creoleparser
 Genshi==0.6
 # dep of oauth2
+html2text==3.200.3
 httplib2==0.7.4
 iso8601==0.1.4
 Jinja2==2.6
@@ -39,6 +40,7 @@
 textile==2.1.5
 # dep of colander
 translationstring==0.4
+TimerMiddleware==0.2.1
 TurboGears2==2.1.3
 # part of the stdlib, but with a version number.  see http://guide.python-distribute.org/pip.html#listing-installed-packages
 wsgiref==0.1.2
diff --git a/requirements-sf.txt b/requirements-sf.txt
index a5d07c1..c1135d4 100644
--- a/requirements-sf.txt
+++ b/requirements-sf.txt
@@ -14,7 +14,6 @@
 pyzmq==2.1.7
 
 # for the migration scripts only
-html2text==3.101
 postmarkup==1.1.4
 # suds needed for teamforge import script
 suds==0.4
diff --git a/scripts/migrations/010-fix-home-permissions.py b/scripts/migrations/010-fix-home-permissions.py
index 05212da..f66e76a 100644
--- a/scripts/migrations/010-fix-home-permissions.py
+++ b/scripts/migrations/010-fix-home-permissions.py
@@ -7,6 +7,7 @@
 from bson import ObjectId
 
 from allura import model as M
+from allura.lib import utils
 from forgewiki.wiki_main import ForgeWikiApp
 
 log = logging.getLogger('fix-home-permissions')
@@ -22,8 +23,9 @@
     else:
         log.info('Fixing permissions for all Home Wikis')
 
-    for some_projects in chunked_project_iterator({'neighborhood_id': {'$nin': [ObjectId('4be2faf8898e33156f00003e'),       # /u
-                                                                                ObjectId('4dbf2563bfc09e6362000005')]}}):   # /motorola
+    for some_projects in utils.chunked_find(M.Project, {'neighborhood_id': {
+                '$nin': [ObjectId('4be2faf8898e33156f00003e'),      # /u
+                         ObjectId('4dbf2563bfc09e6362000005')]}}):  # /motorola
         for project in some_projects:
             c.project = project
             home_app = project.app_instance('home')
@@ -56,21 +58,6 @@
                     home_app.config.acl = map(dict, new_acl.values())
                     session(home_app.config).flush()
 
-PAGESIZE=1024
-
-def chunked_project_iterator(q_project):
-    '''shamelessly copied from refresh-all-repos.py'''
-    page = 0
-    while True:
-        results = (M.Project.query
-                   .find(q_project)
-                   .skip(PAGESIZE*page)
-                   .limit(PAGESIZE)
-                   .all())
-        if not results: break
-        yield results
-        page += 1
-
 def project_role(project, name):
     role = M.ProjectRole.query.get(project_id=project._id, name=name)
     if role is None:
diff --git a/scripts/migrations/011-fix-subroles.py b/scripts/migrations/011-fix-subroles.py
index c982a87..2d1e007 100644
--- a/scripts/migrations/011-fix-subroles.py
+++ b/scripts/migrations/011-fix-subroles.py
@@ -12,11 +12,11 @@
 import sys
 import logging
 
-from pylons import c
 from ming.orm import session
 from ming.orm.ormsession import ThreadLocalORMSession
 
 from allura import model as M
+from allura.lib import utils
 
 log = logging.getLogger('fix-subroles')
 log.addHandler(logging.StreamHandler(sys.stdout))
@@ -27,7 +27,7 @@
     log.info('Examining subroles in all non-user projects.')
     n_users = M.Neighborhood.query.get(name='Users')
     project_filter = dict(neighborhood_id={'$ne':n_users._id})
-    for some_projects in chunked_project_iterator(project_filter):
+    for some_projects in utils.chunked_find(M.Project, project_filter):
         for project in some_projects:
             project_name = '%s.%s' % (project.neighborhood.name, project.shortname)
             project_roles = {}
@@ -53,7 +53,7 @@
             for user in project.users():
                 pr = user.project_role(project=project)
                 if not pr.roles: continue
-                for parent, children in [('Admin', ('Developer', 'Member')), 
+                for parent, children in [('Admin', ('Developer', 'Member')),
                                          ('Developer', ('Member',))]:
                     if project_roles[parent]._id not in pr.roles: continue
                     for role_name in children:
@@ -73,21 +73,5 @@
 
         log.info('%s projects examined.' % num_projects_examined)
 
-
-PAGESIZE=1024
-
-def chunked_project_iterator(q_project):
-    '''shamelessly copied from refresh-all-repos.py'''
-    page = 0
-    while True:
-        results = (M.Project.query
-                   .find(q_project)
-                   .skip(PAGESIZE*page)
-                   .limit(PAGESIZE)
-                   .all())
-        if not results: break
-        yield results
-        page += 1
-
 if __name__ == '__main__':
     main()
diff --git a/scripts/migrations/012-uninstall-home.py b/scripts/migrations/012-uninstall-home.py
index 7f1f9f5..a8b66f5 100644
--- a/scripts/migrations/012-uninstall-home.py
+++ b/scripts/migrations/012-uninstall-home.py
@@ -7,6 +7,7 @@
 from mock import Mock, patch
 
 from allura.lib import helpers as h
+from allura.lib import utils
 from allura import model as M
 from forgewiki import model as WM
 from allura.ext.project_home import ProjectHomeApp
@@ -21,7 +22,8 @@
     possibly_orphaned_projects = 0
     solr_delete = Mock()
     notification_post = Mock()
-    for some_projects in chunked_project_iterator({'neighborhood_id': {'$ne': ObjectId("4be2faf8898e33156f00003e")}}):
+    for some_projects in utils.chunked_find(M.Project, {'neighborhood_id': {
+            '$ne': ObjectId("4be2faf8898e33156f00003e")}}):
         for project in some_projects:
             c.project = project
             old_home_app = project.app_instance('home')
@@ -102,20 +104,5 @@
         assert solr_delete.call_count == affected_projects, solr_delete.call_count
         assert notification_post.call_count == 2 * affected_projects, notification_post.call_count
 
-PAGESIZE=1024
-
-def chunked_project_iterator(q_project):
-    '''shamelessly copied from refresh-all-repos.py'''
-    page = 0
-    while True:
-        results = (M.Project.query
-                   .find(q_project)
-                   .skip(PAGESIZE*page)
-                   .limit(PAGESIZE)
-                   .all())
-        if not results: break
-        yield results
-        page += 1
-
 if __name__ == '__main__':
     main()
diff --git a/scripts/migrations/013-update-ordinals.py b/scripts/migrations/013-update-ordinals.py
index 9d8b13d..ef81d32 100644
--- a/scripts/migrations/013-update-ordinals.py
+++ b/scripts/migrations/013-update-ordinals.py
@@ -6,6 +6,7 @@
 from ming.orm.ormsession import ThreadLocalORMSession
 
 from allura import model as M
+from allura.lib import utils
 
 log = logging.getLogger('update-ordinals')
 log.addHandler(logging.StreamHandler(sys.stdout))
@@ -14,7 +15,7 @@
     test = sys.argv[-1] == 'test'
     num_projects_examined = 0
     log.info('Examining all projects for mount order.')
-    for some_projects in chunked_project_iterator({}):
+    for some_projects in utils.chunked_find(M.Project):
         for project in some_projects:
             c.project = project
             mounts = project.ordered_mounts(include_search=True)
@@ -47,21 +48,5 @@
         ThreadLocalORMSession.flush_all()
         ThreadLocalORMSession.close_all()
 
-
-PAGESIZE=1024
-
-def chunked_project_iterator(q_project):
-    '''shamelessly copied from refresh-all-repos.py'''
-    page = 0
-    while True:
-        results = (M.Project.query
-                   .find(q_project)
-                   .skip(PAGESIZE*page)
-                   .limit(PAGESIZE)
-                   .all())
-        if not results: break
-        yield results
-        page += 1
-
 if __name__ == '__main__':
     main()
diff --git a/scripts/migrations/023-migrate-custom-profile-text.py b/scripts/migrations/023-migrate-custom-profile-text.py
deleted file mode 100644
index e2eda7d..0000000
--- a/scripts/migrations/023-migrate-custom-profile-text.py
+++ /dev/null
@@ -1,56 +0,0 @@
-import logging
-import re
-
-from pylons import c
-
-from ming.orm import ThreadLocalORMSession
-
-from allura import model as M
-from forgewiki import model as WM
-from forgewiki.wiki_main import ForgeWikiApp
-
-log = logging.getLogger(__name__)
-
-default_description = r'^\s*(?:You can edit this description in the admin page)?\s*$'
-
-default_personal_project_tmpl = ("This is the personal project of %s."
-            " This project is created automatically during user registration"
-            " as an easy place to store personal data that doesn't need its own"
-            " project such as cloned repositories.\n\n%s")
-
-def main():
-    for p in M.Project.query.find().all():
-        user = p.user_project_of
-        if not user:
-            continue
-
-        description = p.description
-        if description is None or re.match(default_description, description):
-            continue
-
-        app = p.app_instance('wiki')
-        if app is None:
-            p.install_app('wiki')
-
-        page = WM.Page.query.get(app_config_id=app.config._id, title='Home')
-        if page is None:
-            continue
-
-        c.app = app
-        c.project = p
-        c.user = user
-
-        if "This is the personal project of" in page.text:
-            if description not in page.text:
-                page.text = "%s\n\n%s" % (page.text, description)
-                log.info("Update wiki home page text for %s" % user.username)
-        elif "This is the default page" in page.text:
-            page.text = default_personal_project_tmpl % (user.display_name, description)
-            log.info("Update wiki home page text for %s" % user.username)
-        else:
-            pass
-
-    ThreadLocalORMSession.flush_all()
-
-if __name__ == '__main__':
-    main()
diff --git a/scripts/migrations/024-migrate-custom-profile-text.py b/scripts/migrations/024-migrate-custom-profile-text.py
new file mode 100644
index 0000000..db199eb
--- /dev/null
+++ b/scripts/migrations/024-migrate-custom-profile-text.py
@@ -0,0 +1,59 @@
+import logging
+import re
+
+from pylons import c
+
+from ming.orm import ThreadLocalORMSession
+
+from allura import model as M
+from allura.lib import utils
+from forgewiki import model as WM
+from forgewiki.wiki_main import ForgeWikiApp
+
+log = logging.getLogger(__name__)
+
+default_description = r'^\s*(?:You can edit this description in the admin page)?\s*$'
+
+default_personal_project_tmpl = ("This is the personal project of %s."
+            " This project is created automatically during user registration"
+            " as an easy place to store personal data that doesn't need its own"
+            " project such as cloned repositories.\n\n%s")
+
+def main():
+    users = M.Neighborhood.query.get(name='Users')
+    for chunk in utils.chunked_find(M.Project, {'neighborhood_id': users._id}):
+        for p in chunk:
+            user = p.user_project_of
+            if not user:
+                continue
+
+            description = p.description
+            if description is None or re.match(default_description, description):
+                continue
+
+            app = p.app_instance('wiki')
+            if app is None:
+                p.install_app('wiki')
+
+            page = WM.Page.query.get(app_config_id=app.config._id, title='Home')
+            if page is None:
+                continue
+
+            c.app = app
+            c.project = p
+            c.user = user
+
+            if "This is the personal project of" in page.text:
+                if description not in page.text:
+                    page.text = "%s\n\n%s" % (page.text, description)
+                    log.info("Update wiki home page text for %s" % user.username)
+            elif "This is the default page" in page.text:
+                page.text = default_personal_project_tmpl % (user.display_name, description)
+                log.info("Update wiki home page text for %s" % user.username)
+            else:
+                pass
+
+        ThreadLocalORMSession.flush_all()
+
+if __name__ == '__main__':
+    main()
diff --git a/scripts/refresh-all-repos.py b/scripts/refresh-all-repos.py
index c62d0f3..99cb4d1 100644
--- a/scripts/refresh-all-repos.py
+++ b/scripts/refresh-all-repos.py
@@ -1,11 +1,11 @@
 import logging
 import optparse
-from collections import defaultdict
 
 from pylons import c
 from ming.orm import ThreadLocalORMSession
 
 from allura import model as M
+from allura.lib import utils
 
 log = logging.getLogger(__name__)
 
@@ -45,7 +45,7 @@
         M.repo.DiffInfoDoc.m.remove({})
         M.repo.LastCommitDoc.m.remove({})
         M.repo.CommitRunDoc.m.remove({})
-    for chunk in chunked_project_iterator(q_project):
+    for chunk in utils.chunked_find(M.Project, q_project):
         for p in chunk:
             c.project = p
             if projects:
@@ -73,18 +73,5 @@
         ThreadLocalORMSession.flush_all()
         ThreadLocalORMSession.close_all()
 
-def chunked_project_iterator(q_project):
-    page = 0
-    while True:
-        results = (M.Project.query
-                   .find(q_project)
-                   .skip(PAGESIZE*page)
-                   .limit(PAGESIZE)
-                   .all())
-        if not results: break
-        yield results
-        page += 1
-
-
 if __name__ == '__main__':
     main()