changeset 588:182ee00df287 demo

merged with default
author Marcin Kuzminski <marcin@python-works.com>
date Sun, 03 Oct 2010 02:27:44 +0200
parents 0f7c14c51c5f (current diff) 168273591e44 (diff)
children 7622a41efb90
files pylons_app/controllers/admin/repos.py pylons_app/controllers/admin/settings.py pylons_app/controllers/admin/users.py pylons_app/model/user_model.py
diffstat 39 files changed, 700 insertions(+), 213 deletions(-) [+]
line wrap: on
line diff
--- a/.hgtags	Tue Sep 28 20:38:35 2010 +0000
+++ b/.hgtags	Sun Oct 03 02:27:44 2010 +0200
@@ -13,5 +13,4 @@
 ccbe729908844884aea89d00fb14a6cb92e10c06 v0.8.2
 ca41d544dbdfd2f81bd0304168492a26276aadb6 v0.8.3
 2fa16ec5822da0c6fade3dd1ed9b6c0655e5dbbf v0.8.4
-
- 							 
\ No newline at end of file
+16ba57d8fe2317c49dbd422afd07ab497687aa02 v0.8.5
--- a/README.rst	Tue Sep 28 20:38:35 2010 +0000
+++ b/README.rst	Sun Oct 03 02:27:44 2010 +0200
@@ -11,7 +11,7 @@
 - full permissions per project read/write/admin access even on mercurial request
 - mako templates let's you cusmotize look and feel of application.
 - diffs annotations and source code all colored by pygments.
-- mercurial branch graph and yui-flot powered graphs with zooming
+- mercurial branch graph and yui-flot powered graphs with zooming and statistics
 - admin interface for performing user/permission managments as well as repository
   managment. 
 - full text search of source codes with indexing daemons using whoosh
@@ -44,26 +44,38 @@
 -------------
 Installation
 -------------
-.. note::
-   I recomend to install tip version of vcs while the app is in beta mode.
-   
-   
-- create new virtualenv and activate it - highly recommend that you use separate
-  virtual-env for whole application
-- download hg app from default branch from bitbucket and run 
-  'python setup.py install' this will install all required dependencies needed
-- run paster setup-app production.ini it should create all needed tables 
+
+- I highly recommend to install new virtualenv for hg-app see 
+  http://pypi.python.org/pypi/virtualenv
+- Create new virtualenv using `virtualenv --no-site-packages /var/www/hgapp-venv`
+  this will install new virtual env into /var/www/hgapp-venv. 
+  Activate the virtualenv by running 
+  `source activate /var/www/hgapp-venv/bin/activate`   
+- Make a folder for hg-app somewhere on the filesystem for example /var/www/hgapp  
+- Download and extract http://bitbucket.org/marcinkuzminski/hg-app/get/tip.zip
+  into created directory.
+- Run `python setup.py install` in order to install the application and all
+  needed dependencies. Make sure that You're using activated virutalenv  
+- Run `paster setup-app production.ini` it should create all needed tables 
   and an admin account make sure You specify correct path to repositories. 
-- remember that the given path for mercurial repositories must be write 
+- Remember that the given path for mercurial repositories must be write 
   accessible for the application
-- run paster serve development.ini - or you can use manage-hg_app script.
+- Run paster serve development.ini - or you can use sample init.d scripts.
   the app should be available at the 127.0.0.1:5000
-- use admin account you created to login.
-- default permissions on each repository is read, and owner is admin. So remember
+- Use admin account you created to login.
+- Default permissions on each repository is read, and owner is admin. So remember
   to update these.
-- in order to use full power of async tasks, You must install message broker
-  preferrably rabbitmq and start celeryd daemon. The app should gain some speed 
-  than. For installation instructions 
-  You can visit: http://ask.github.com/celery/getting-started/index.html. All
-  needed configs are inside hg-app ie. celeryconfig.py
-     
\ No newline at end of file
+- In order to use full power of async tasks, You must install message broker
+  preferrably rabbitmq and start celeryd daemon together with hg-app. 
+  The app should gain a lot of speed and become much more responsible. 
+  For installation instructions You can visit: 
+  http://ask.github.com/celery/getting-started/index.html. 
+- All needed configs are inside hg-app ie. celeryconfig.py , production.ini
+  You can configure the email, ports, loggers, workers from there.
+- For full text search You can either put crontab entry for 
+  `python /var/www/hgapp/pylons_app/lib/indexers/daemon.py incremental <path_to_repos>`
+  or run indexer from admin panel. This will scann the repos given in the 
+  application setup or given path for daemon.py and each scann in incremental 
+  mode will scann only changed files, 
+  Hg Update hook must be activated to index the content it's enabled by default
+  after setup
\ No newline at end of file
--- a/celeryconfig.py	Tue Sep 28 20:38:35 2010 +0000
+++ b/celeryconfig.py	Sun Oct 03 02:27:44 2010 +0200
@@ -40,6 +40,9 @@
 
 #Tasks will never be sent to the queue, but executed locally instead.
 CELERY_ALWAYS_EAGER = False
+if PYLONS_CONFIG_NAME == 'test.ini':
+    #auto eager for tests
+    CELERY_ALWAYS_EAGER = True
 
 #===============================================================================
 # EMAIL SETTINGS
--- a/pylons_app/__init__.py	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/__init__.py	Sun Oct 03 02:27:44 2010 +0200
@@ -24,7 +24,7 @@
 @author: marcink
 """
 
-VERSION = (0, 8, 4, 'beta')
+VERSION = (0, 8, 5, 'beta')
 
 __version__ = '.'.join((str(each) for each in VERSION[:4]))
 
--- a/pylons_app/config/routing.py	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/config/routing.py	Sun Oct 03 02:27:44 2010 +0200
@@ -108,7 +108,8 @@
         m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
                   action='add_repo')
     #SEARCH
-    map.connect('search', '/_admin/search', controller='search')
+    map.connect('search', '/_admin/search', controller='search',)
+    map.connect('search_repo', '/_admin/search/{search_repo:.*}', controller='search')
     
     #LOGIN/LOGOUT/REGISTER/SIGN IN
     map.connect('login_home', '/_admin/login', controller='login')
@@ -159,7 +160,10 @@
                 conditions=dict(function=check_repo))    
     map.connect('files_archive_home', '/{repo_name:.*}/archive/{revision}/{fileformat}',
                 controller='files', action='archivefile', revision='tip',
-                conditions=dict(function=check_repo))
+                conditions=dict(function=check_repo))   
+    map.connect('repo_settings_delete', '/{repo_name:.*}/settings',
+                controller='settings', action="delete",
+                conditions=dict(method=["DELETE"], function=check_repo))
     map.connect('repo_settings_update', '/{repo_name:.*}/settings',
                 controller='settings', action="update",
                 conditions=dict(method=["PUT"], function=check_repo))
@@ -167,5 +171,11 @@
                 controller='settings', action='index',
                 conditions=dict(function=check_repo))
 
-    
+    map.connect('repo_fork_create_home', '/{repo_name:.*}/fork',
+                controller='settings', action='fork_create',
+                conditions=dict(function=check_repo, method=["POST"]))
+    map.connect('repo_fork_home', '/{repo_name:.*}/fork',
+                controller='settings', action='fork',
+                conditions=dict(function=check_repo))
+        
     return map
--- a/pylons_app/controllers/admin/repos.py	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/controllers/admin/repos.py	Sun Oct 03 02:27:44 2010 +0200
@@ -32,7 +32,7 @@
 from pylons_app.lib.auth import LoginRequired, HasPermissionAllDecorator, \
     HasPermissionAnyDecorator
 from pylons_app.lib.base import BaseController, render
-from pylons_app.lib.utils import invalidate_cache
+from pylons_app.lib.utils import invalidate_cache, action_logger
 from pylons_app.model.db import User
 from pylons_app.model.forms import RepoForm
 from pylons_app.model.hg_model import HgModel
@@ -77,13 +77,20 @@
             invalidate_cache('cached_repo_list')
             h.flash(_('created repository %s') % form_result['repo_name'],
                     category='success')
-                                                             
+
+            if request.POST.get('user_created'):
+                action_logger(self.hg_app_user, 'user_created_repo', 
+                              form_result['repo_name'], '', self.sa)
+            else:
+                action_logger(self.hg_app_user, 'admin_created_repo', 
+                              form_result['repo_name'], '', self.sa)                
+                                                                             
         except formencode.Invalid as errors:
             c.new_repo = errors.value['repo_name']
             
             if request.POST.get('user_created'):
                 r = render('admin/repos/repo_add_create_repository.html')
-            else:
+            else:              
                 r = render('admin/repos/repo_add.html')
             
             return htmlfill.render(
@@ -173,10 +180,14 @@
         
             return redirect(url('repos'))
         try:
+            action_logger(self.hg_app_user, 'admin_deleted_repo', 
+                              repo_name, '', self.sa)
             repo_model.delete(repo)            
             invalidate_cache('cached_repo_list')
             h.flash(_('deleted repository %s') % repo_name, category='success')
-        except Exception:
+           
+        except Exception, e:
+            log.error(traceback.format_exc())
             h.flash(_('An error occured during deletion of %s') % repo_name,
                     category='error')
         
--- a/pylons_app/controllers/admin/settings.py	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/controllers/admin/settings.py	Sun Oct 03 02:27:44 2010 +0200
@@ -101,7 +101,7 @@
             initial = HgModel.repo_scan(g.paths[0][0], g.paths[0][1], g.baseui)
             repo2db_mapper(initial, rm_obsolete)
             invalidate_cache('cached_repo_list')
-            h.flash(_('Repositories sucessfully rescanned'), category='success')            
+            h.flash(_('Repositories successfully rescanned'), category='success')            
         
         if setting_id == 'whoosh':
             repo_location = get_hg_ui_settings()['paths_root_path']
@@ -134,7 +134,7 @@
                                     
                 except:
                     log.error(traceback.format_exc())
-                    h.flash(_('error occured during updating application settings'),
+                    h.flash(_('error occurred during updating application settings'),
                             category='error')
                                 
                     self.sa.rollback()
@@ -187,7 +187,7 @@
                                     
                 except:
                     log.error(traceback.format_exc())
-                    h.flash(_('error occured during updating application settings'),
+                    h.flash(_('error occurred during updating application settings'),
                             category='error')
                                 
                     self.sa.rollback()
--- a/pylons_app/controllers/admin/users.py	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/controllers/admin/users.py	Sun Oct 03 02:27:44 2010 +0200
@@ -17,6 +17,7 @@
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 # MA  02110-1301, USA.
+from pylons_app.lib.utils import action_logger
 """
 Created on April 4, 2010
 users controller for pylons
@@ -71,6 +72,7 @@
             user_model.create(form_result)
             h.flash(_('created user %s') % form_result['username'],
                     category='success')
+            #action_logger(self.hg_app_user, 'new_user', '', '', self.sa)
         except formencode.Invalid as errors:
             return htmlfill.render(
                 render('admin/users/user_add.html'),
--- a/pylons_app/controllers/search.py	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/controllers/search.py	Sun Oct 03 02:27:44 2010 +0200
@@ -44,7 +44,8 @@
     def __before__(self):
         super(SearchController, self).__before__()    
 
-    def index(self):
+    def index(self, search_repo=None):
+        c.repo_name = search_repo
         c.formated_results = []
         c.runtime = ''
         c.cur_query = request.GET.get('q', None)
@@ -59,6 +60,8 @@
                 searcher = idx.searcher()
 
                 qp = QueryParser("content", schema=SCHEMA)
+                if c.repo_name:
+                    cur_query = u'repository:%s %s' % (c.repo_name, cur_query)
                 try:
                     query = qp.parse(unicode(cur_query))
                     
--- a/pylons_app/controllers/settings.py	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/controllers/settings.py	Sun Oct 03 02:27:44 2010 +0200
@@ -28,8 +28,8 @@
 from pylons.i18n.translation import _
 from pylons_app.lib.auth import LoginRequired, HasRepoPermissionAllDecorator
 from pylons_app.lib.base import BaseController, render
-from pylons_app.lib.utils import invalidate_cache
-from pylons_app.model.forms import RepoSettingsForm
+from pylons_app.lib.utils import invalidate_cache, action_logger
+from pylons_app.model.forms import RepoSettingsForm, RepoForkForm
 from pylons_app.model.repo_model import RepoModel
 import formencode
 import logging
@@ -55,7 +55,7 @@
                       ' in order to rescan repositories') % repo_name,
                       category='error')
         
-            return redirect(url('repos'))        
+            return redirect(url('hg_home'))        
         defaults = c.repo_info.__dict__
         defaults.update({'user':c.repo_info.user.username})
         c.users_array = repo_model.get_users_js()
@@ -79,7 +79,7 @@
             form_result = _form.to_python(dict(request.POST))
             repo_model.update(repo_name, form_result)
             invalidate_cache('cached_repo_list')
-            h.flash(_('Repository %s updated succesfully' % repo_name),
+            h.flash(_('Repository %s updated successfully' % repo_name),
                     category='success')
             changed_name = form_result['repo_name']               
         except formencode.Invalid as errors:
@@ -98,3 +98,78 @@
                     % repo_name, category='error')
                     
         return redirect(url('repo_settings_home', repo_name=changed_name))
+
+
+
+    def delete(self, repo_name):    
+        """DELETE /repos/repo_name: Delete an existing item"""
+        # Forms posted to this method should contain a hidden field:
+        #    <input type="hidden" name="_method" value="DELETE" />
+        # Or using helpers:
+        #    h.form(url('repo_settings_delete', repo_name=ID),
+        #           method='delete')
+        # url('repo_settings_delete', repo_name=ID)
+        
+        repo_model = RepoModel()
+        repo = repo_model.get(repo_name)
+        if not repo:
+            h.flash(_('%s repository is not mapped to db perhaps' 
+                      ' it was moved or renamed  from the filesystem'
+                      ' please run the application again'
+                      ' in order to rescan repositories') % repo_name,
+                      category='error')
+        
+            return redirect(url('hg_home'))
+        try:
+            action_logger(self.hg_app_user, 'user_deleted_repo', 
+                              repo_name, '', self.sa)            
+            repo_model.delete(repo)            
+            invalidate_cache('cached_repo_list')
+            h.flash(_('deleted repository %s') % repo_name, category='success')
+        except Exception:
+            h.flash(_('An error occurred during deletion of %s') % repo_name,
+                    category='error')
+        
+        return redirect(url('hg_home'))
+    
+    def fork(self, repo_name):
+        repo_model = RepoModel()
+        c.repo_info = repo = repo_model.get(repo_name)
+        if not repo:
+            h.flash(_('%s repository is not mapped to db perhaps' 
+                      ' it was created or renamed from the filesystem'
+                      ' please run the application again'
+                      ' in order to rescan repositories') % repo_name,
+                      category='error')
+        
+            return redirect(url('hg_home'))
+        
+        return render('settings/repo_fork.html')
+    
+    
+    
+    def fork_create(self, repo_name):
+        repo_model = RepoModel()
+        c.repo_info = repo_model.get(repo_name)
+        _form = RepoForkForm()()
+        form_result = {}
+        try:
+            form_result = _form.to_python(dict(request.POST))
+            form_result.update({'repo_name':repo_name})
+            repo_model.create_fork(form_result, c.hg_app_user)
+            h.flash(_('fork %s repository as %s task added') \
+                      % (repo_name, form_result['fork_name']),
+                    category='success')
+            action_logger(self.hg_app_user, 'user_forked_repo',
+                            repo_name, '', self.sa)                                                 
+        except formencode.Invalid as errors:
+            c.new_repo = errors.value['fork_name']
+            r = render('settings/repo_fork.html')
+            
+            return htmlfill.render(
+                r,
+                defaults=errors.value,
+                errors=errors.error_dict or {},
+                prefix_error=False,
+                encoding="UTF-8")   
+        return redirect(url('hg_home'))
--- a/pylons_app/lib/base.py	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/lib/base.py	Sun Oct 03 02:27:44 2010 +0200
@@ -22,8 +22,14 @@
         c.repo_switcher_list = _get_repos_switcher_cached(c.cached_repo_list)
         
         if c.repo_name:
-            c.repository_tags = c.cached_repo_list[c.repo_name].tags
-            c.repository_branches = c.cached_repo_list[c.repo_name].branches
+            cached_repo = c.cached_repo_list.get(c.repo_name)
+            
+            if cached_repo:
+                c.repository_tags = cached_repo.tags
+                c.repository_branches = cached_repo.branches
+            else:
+                c.repository_tags = {}
+                c.repository_branches = {}
                     
         self.sa = meta.Session
     
@@ -34,7 +40,7 @@
         # available in environ['pylons.routes_dict']
         try:
             #putting this here makes sure that we update permissions every time
-            c.hg_app_user = auth.get_user(session)
+            self.hg_app_user = c.hg_app_user = auth.get_user(session)
             return WSGIController.__call__(self, environ, start_response)
         finally:
             meta.Session.remove()
--- a/pylons_app/lib/celerylib/tasks.py	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/lib/celerylib/tasks.py	Sun Oct 03 02:27:44 2010 +0200
@@ -271,6 +271,22 @@
         return False
     return True
 
+@task
+def create_repo_fork(form_data, cur_user):
+    import os
+    from pylons_app.model.repo_model import RepoModel
+    sa = get_session()
+    rm = RepoModel(sa)
+    
+    rm.create(form_data, cur_user, just_db=True, fork=True)
+    
+    repos_path = get_hg_ui_settings()['paths_root_path'].replace('*', '')
+    repo_path = os.path.join(repos_path, form_data['repo_name'])
+    repo_fork_path = os.path.join(repos_path, form_data['fork_name'])
+    
+    MercurialRepository(str(repo_fork_path), True, clone_url=str(repo_path))
+
+    
 def __get_codes_stats(repo_name):
     LANGUAGES_EXTENSIONS = ['action', 'adp', 'ashx', 'asmx', 'aspx', 'asx', 'axd', 'c',
                     'cfg', 'cfm', 'cpp', 'cs', 'diff', 'do', 'el', 'erl',
--- a/pylons_app/lib/db_manage.py	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/lib/db_manage.py	Sun Oct 03 02:27:44 2010 +0200
@@ -88,9 +88,9 @@
             self.create_user(username, password, email, True)
         else:
             log.info('creating admin and regular test users')
-            self.create_user('test_admin', 'test', 'test_admin@mail.com', True)
-            self.create_user('test_regular', 'test', 'test_regular@mail.com', False)
-            self.create_user('test_regular2', 'test', 'test_regular2@mail.com', False)
+            self.create_user('test_admin', 'test12', 'test_admin@mail.com', True)
+            self.create_user('test_regular', 'test12', 'test_regular@mail.com', False)
+            self.create_user('test_regular2', 'test12', 'test_regular2@mail.com', False)
             
         
     
--- a/pylons_app/lib/hooks.py	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/lib/hooks.py	Sun Oct 03 02:27:44 2010 +0200
@@ -63,10 +63,10 @@
         username = kwargs['url'].split(':')[-1]
         user_log = sa.query(UserLog)\
             .filter(UserLog.user == sa.query(User)\
-                                        .filter(User.username == username).one())\
+                                    .filter(User.username == username).one())\
             .order_by(UserLog.user_log_id.desc()).first()
-            
-        if not user_log.revision:
+        
+        if user_log and not user_log.revision:
             user_log.revision = str(repo['tip'])
             sa.add(user_log)
             sa.commit()
--- a/pylons_app/lib/middleware/simplehg.py	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/lib/middleware/simplehg.py	Sun Oct 03 02:27:44 2010 +0200
@@ -24,7 +24,6 @@
 SimpleHG middleware for handling mercurial protocol request (push/clone etc.)
 It's implemented with basic auth function
 """
-from datetime import datetime
 from itertools import chain
 from mercurial.error import RepoError
 from mercurial.hgweb import hgweb
@@ -35,12 +34,10 @@
     get_user_cached
 from pylons_app.lib.utils import is_mercurial, make_ui, invalidate_cache, \
     check_repo_fast, ui_sections
-from pylons_app.model import meta
-from pylons_app.model.db import UserLog, User
 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
+from pylons_app.lib.utils import action_logger
 import logging
 import os
-import pylons_app.lib.helpers as h
 import traceback
  
 log = logging.getLogger(__name__)
@@ -183,23 +180,7 @@
                     return mapping[cmd]
     
     def __log_user_action(self, user, action, repo, ipaddr):
-        sa = meta.Session
-        try:
-            user_log = UserLog()
-            user_log.user_id = user.user_id
-            user_log.action = action
-            user_log.repository = repo.replace('/', '')
-            user_log.action_date = datetime.now()
-            user_log.user_ip = ipaddr
-            sa.add(user_log)
-            sa.commit()
-            log.info('Adding user %s, action %s on %s',
-                                            user.username, action, repo)
-        except Exception, e:
-            sa.rollback()
-            log.error('could not log user action:%s', str(e))
-        finally:
-            meta.Session.remove()
+        action_logger(user, action, repo, ipaddr)
         
     def __invalidate_cache(self, repo_name):
         """we know that some change was made to repositories and we should
--- a/pylons_app/lib/utils.py	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/lib/utils.py	Sun Oct 03 02:27:44 2010 +0200
@@ -26,16 +26,17 @@
 from mercurial import ui, config, hg
 from mercurial.error import RepoError
 from pylons_app.model import meta
-from pylons_app.model.db import Repository, User, HgAppUi, HgAppSettings
+from pylons_app.model.db import Repository, User, HgAppUi, HgAppSettings, UserLog
 from vcs.backends.base import BaseChangeset
 from vcs.utils.lazy import LazyProperty
 import logging
+import datetime
 import os
 
 log = logging.getLogger(__name__)
 
 
-def get_repo_slug(request):
+def get_repo_slug(request):    
     return request.environ['pylons.routes_dict'].get('repo_name')
 
 def is_mercurial(environ):
@@ -48,6 +49,40 @@
         return True
     return False
 
+def action_logger(user, action, repo, ipaddr, sa=None):
+    """
+    Action logger for various action made by users
+    """
+    
+    if not sa:
+        sa = meta.Session 
+        
+    try:
+        if hasattr(user, 'user_id'):
+            user_id = user.user_id
+        elif isinstance(user, basestring):
+            user_id = sa.query(User).filter(User.username == user).one()
+        else:
+            raise Exception('You have to provide user object or username')
+       
+        repo_name = repo.lstrip('/')
+        user_log = UserLog()
+        user_log.user_id = user_id
+        user_log.action = action
+        user_log.repository_name = repo_name
+        user_log.repository = sa.query(Repository)\
+            .filter(Repository.repo_name == repo_name).one()
+        user_log.action_date = datetime.datetime.now()
+        user_log.user_ip = ipaddr
+        sa.add(user_log)
+        sa.commit()
+        log.info('Adding user %s, action %s on %s',
+                                        user.username, action, repo)
+    except Exception, e:
+        raise
+        sa.rollback()
+        log.error('could not log user action:%s', str(e))
+                
 def check_repo_dir(paths):
     repos_path = paths[0][1].split('/')
     if repos_path[-1] in ['*', '**']:
@@ -227,8 +262,11 @@
         Returns raw string identifing this changeset, useful for web
         representation.
         """
-        return '0' * 12
-
+        return '0' * 40
+    
+    @LazyProperty
+    def short_id(self):
+        return self.raw_id[:12]
 
 def repo2db_mapper(initial_repo_list, remove_obsolete=False):
     """
--- a/pylons_app/model/db.py	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/model/db.py	Sun Oct 03 02:27:44 2010 +0200
@@ -66,12 +66,15 @@
     __table_args__ = {'useexisting':True}
     user_log_id = Column("user_log_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True)
     user_id = Column("user_id", INTEGER(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None)
+    repository_id = Column("repository_id", INTEGER(length=None, convert_unicode=False, assert_unicode=None), ForeignKey(u'repositories.repo_id'), nullable=False, unique=None, default=None)
+    repository_name = Column("repository_name", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
     user_ip = Column("user_ip", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) 
-    repository = Column("repository", TEXT(length=None, convert_unicode=False, assert_unicode=None), ForeignKey(u'repositories.repo_name'), nullable=False, unique=None, default=None)
     action = Column("action", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
     action_date = Column("action_date", DATETIME(timezone=False), nullable=True, unique=None, default=None)
     revision = Column('revision', TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    
     user = relation('User')
+    repository = relation('Repository')
     
 class Repository(Base):
     __tablename__ = 'repositories'
@@ -81,8 +84,10 @@
     user_id = Column("user_id", INTEGER(), ForeignKey(u'users.user_id'), nullable=False, unique=False, default=None)
     private = Column("private", BOOLEAN(), nullable=True, unique=None, default=None)
     description = Column("description", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
+    fork_id = Column("fork_id", INTEGER(), ForeignKey(u'repositories.repo_id'), nullable=True, unique=False, default=None)
     
     user = relation('User')
+    fork = relation('Repository', remote_side=repo_id)
     repo_to_perm = relation('RepoToPerm', cascade='all')
     
     def __repr__(self):
--- a/pylons_app/model/forms.py	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/model/forms.py	Sun Oct 03 02:27:44 2010 +0200
@@ -247,7 +247,7 @@
     filter_extra_fields = True
     username = UnicodeString(
                              strip=True,
-                             min=3,
+                             min=1,
                              not_empty=True,
                              messages={
                                        'empty':_('Please enter a login'),
@@ -256,7 +256,7 @@
 
     password = UnicodeString(
                             strip=True,
-                            min=3,
+                            min=6,
                             not_empty=True,
                             messages={
                                       'empty':_('Please enter a password'),
@@ -271,15 +271,15 @@
     class _UserForm(formencode.Schema):
         allow_extra_fields = True
         filter_extra_fields = True
-        username = All(UnicodeString(strip=True, min=3, not_empty=True), ValidUsername(edit, old_data))
+        username = All(UnicodeString(strip=True, min=1, not_empty=True), ValidUsername(edit, old_data))
         if edit:
-            new_password = All(UnicodeString(strip=True, min=3, not_empty=False), ValidPassword)
+            new_password = All(UnicodeString(strip=True, min=6, not_empty=False), ValidPassword)
             admin = StringBoolean(if_missing=False)
         else:
-            password = All(UnicodeString(strip=True, min=8, not_empty=True), ValidPassword)
+            password = All(UnicodeString(strip=True, min=6, not_empty=True), ValidPassword)
         active = StringBoolean(if_missing=False)
-        name = UnicodeString(strip=True, min=3, not_empty=True)
-        lastname = UnicodeString(strip=True, min=3, not_empty=True)
+        name = UnicodeString(strip=True, min=1, not_empty=True)
+        lastname = UnicodeString(strip=True, min=1, not_empty=True)
         email = All(Email(not_empty=True), UniqSystemEmail(old_data))
         
     return _UserForm
@@ -298,7 +298,7 @@
         allow_extra_fields = True
         filter_extra_fields = False
         repo_name = All(UnicodeString(strip=True, min=1, not_empty=True), ValidRepoName(edit, old_data))
-        description = UnicodeString(strip=True, min=3, not_empty=True)
+        description = UnicodeString(strip=True, min=1, not_empty=True)
         private = StringBoolean(if_missing=False)
         
         if edit:
@@ -307,12 +307,22 @@
         chained_validators = [ValidPerms]
     return _RepoForm
 
+def RepoForkForm(edit=False, old_data={}):
+    class _RepoForkForm(formencode.Schema):
+        allow_extra_fields = True
+        filter_extra_fields = False
+        fork_name = All(UnicodeString(strip=True, min=1, not_empty=True), ValidRepoName(edit, old_data))
+        description = UnicodeString(strip=True, min=1, not_empty=True)
+        private = StringBoolean(if_missing=False)
+        
+    return _RepoForkForm
+
 def RepoSettingsForm(edit=False, old_data={}):
     class _RepoForm(formencode.Schema):
         allow_extra_fields = True
         filter_extra_fields = False
         repo_name = All(UnicodeString(strip=True, min=1, not_empty=True), ValidRepoName(edit, old_data))
-        description = UnicodeString(strip=True, min=3, not_empty=True)
+        description = UnicodeString(strip=True, min=1, not_empty=True)
         private = StringBoolean(if_missing=False)
         
         chained_validators = [ValidPerms, ValidSettings]
@@ -323,8 +333,8 @@
     class _ApplicationSettingsForm(formencode.Schema):
         allow_extra_fields = True
         filter_extra_fields = False
-        hg_app_title = UnicodeString(strip=True, min=3, not_empty=True)
-        hg_app_realm = UnicodeString(strip=True, min=3, not_empty=True)
+        hg_app_title = UnicodeString(strip=True, min=1, not_empty=True)
+        hg_app_realm = UnicodeString(strip=True, min=1, not_empty=True)
         
     return _ApplicationSettingsForm
  
@@ -333,7 +343,7 @@
         allow_extra_fields = True
         filter_extra_fields = False
         web_push_ssl = OneOf(['true', 'false'], if_missing='false')
-        paths_root_path = All(ValidPath(), UnicodeString(strip=True, min=3, not_empty=True))
+        paths_root_path = All(ValidPath(), UnicodeString(strip=True, min=1, not_empty=True))
         hooks_changegroup_update = OneOf(['True', 'False'], if_missing=False)
         hooks_changegroup_repo_size = OneOf(['True', 'False'], if_missing=False)
         
--- a/pylons_app/model/hg_model.py	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/model/hg_model.py	Sun Oct 03 02:27:44 2010 +0200
@@ -26,10 +26,12 @@
 from mercurial import ui
 from mercurial.hgweb.hgwebdir_mod import findrepos
 from pylons.i18n.translation import _
+from pylons_app.lib import helpers as h
+from pylons_app.lib.utils import invalidate_cache
 from pylons_app.lib.auth import HasRepoPermissionAny
 from pylons_app.model import meta
 from pylons_app.model.db import Repository, User
-from pylons_app.lib import helpers as h
+from sqlalchemy.orm import joinedload
 from vcs.exceptions import RepositoryError, VCSError
 import logging
 import os
@@ -122,8 +124,12 @@
                     
                     dbrepo = None
                     if not initial:
+                        #for initial scann on application first run we don't
+                        #have db repos yet.
                         dbrepo = sa.query(Repository)\
-                            .filter(Repository.repo_name == name).scalar()
+                            .options(joinedload(Repository.fork))\
+                            .filter(Repository.repo_name == name)\
+                            .scalar()
                             
                     if dbrepo:
                         log.info('Adding db instance to cached list')
@@ -166,4 +172,15 @@
             yield tmp_d
 
     def get_repo(self, repo_name):
-        return _get_repos_cached()[repo_name]
+        try:
+            repo = _get_repos_cached()[repo_name]
+            return repo
+        except KeyError:
+            #i we're here and we got key errors let's try to invalidate the
+            #cahce and try again
+            invalidate_cache('cached_repo_list')
+            repo = _get_repos_cached()[repo_name]
+            return repo
+            
+        
+        
--- a/pylons_app/model/repo_model.py	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/model/repo_model.py	Sun Oct 03 02:27:44 2010 +0200
@@ -27,6 +27,7 @@
 from pylons_app.model.db import Repository, RepoToPerm, User, Permission
 from pylons_app.model.meta import Session
 from pylons_app.model.user_model import UserModel
+from pylons_app.lib.celerylib.tasks import create_repo_fork, run_task
 import logging
 import os
 import shutil
@@ -35,11 +36,15 @@
 
 class RepoModel(object):
     
-    def __init__(self):
-        self.sa = Session()
+    def __init__(self, sa=None):
+        if not sa:
+            self.sa = Session()
+        else:
+            self.sa = sa
     
     def get(self, id):
-        return self.sa.query(Repository).filter(Repository.repo_name == id).scalar()
+        return self.sa.query(Repository)\
+            .filter(Repository.repo_name == id).scalar()
         
     def get_users_js(self):
         
@@ -100,20 +105,32 @@
             self.sa.rollback()
             raise    
     
-    def create(self, form_data, cur_user, just_db=False):
+    def create(self, form_data, cur_user, just_db=False, fork=False):
         try:
-            repo_name = form_data['repo_name']
+            if fork:
+                repo_name = str(form_data['fork_name'])
+                org_name = str(form_data['repo_name'])
+                
+            else:
+                org_name = repo_name = str(form_data['repo_name'])
             new_repo = Repository()
             for k, v in form_data.items():
+                if k == 'repo_name':
+                    v = repo_name
                 setattr(new_repo, k, v)
                 
+            if fork:
+                parent_repo = self.sa.query(Repository)\
+                        .filter(Repository.repo_name == org_name).scalar()
+                new_repo.fork = parent_repo
+                            
             new_repo.user_id = cur_user.user_id
             self.sa.add(new_repo)
             
             #create default permission
             repo_to_perm = RepoToPerm()
             default = 'repository.read'
-            for p in UserModel().get_default().user_perms:
+            for p in UserModel(self.sa).get_default().user_perms:
                 if p.permission.permission_name.startswith('repository.'):
                     default = p.permission.permission_name
                     break
@@ -136,7 +153,10 @@
             log.error(traceback.format_exc())
             self.sa.rollback()
             raise    
-                     
+    
+    def create_fork(self, form_data, cur_user):
+        run_task(create_repo_fork, form_data, cur_user)
+                         
     def delete(self, repo):
         try:
             self.sa.delete(repo)
--- a/pylons_app/model/user_model.py	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/model/user_model.py	Sun Oct 03 02:27:44 2010 +0200
@@ -36,8 +36,11 @@
 
 class UserModel(object):
 
-    def __init__(self):
-        self.sa = Session() 
+    def __init__(self, sa=None):
+        if not sa:
+            self.sa = Session()
+        else:
+            self.sa = sa
     
     def get_default(self):
         return self.sa.query(User).filter(User.username == 'default').scalar()
--- a/pylons_app/public/css/style.css	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/public/css/style.css	Sun Oct 03 02:27:44 2010 +0200
@@ -14,7 +14,7 @@
     height: 100%;
 	background: #d1d1d1 url("../images/background.png") repeat;
 	font-family: Lucida Grande, Verdana, Lucida Sans Regular, Lucida Sans Unicode, Arial, sans-serif;
-	font-size: 11px;
+	font-size: 12px;
 }
 
 /* -----------------------------------------------------------
@@ -569,18 +569,47 @@
     width:167px;        
 }
 
-#header #header-inner #quick li ul li a.branches,#header #header-inner #quick li ul li a.branches:hover
-{
-
+
+#header #header-inner #quick li ul li a.fork,
+#header #header-inner #quick li ul li a.fork:hover
+{
+
+    background: #FFFFFF url("../images/icons/arrow_divide.png") no-repeat 4px 9px;
+    margin:0;
+    padding:12px 9px 7px 24px;
+    width:167px;        
+}
+
+#header #header-inner #quick li ul li a.search,
+#header #header-inner #quick li ul li a.search:hover
+{
+    background: #FFFFFF url("../images/icons/search_16.png") no-repeat 4px 9px;
+    margin:0;
+    padding:12px 9px 7px 24px;
+    width:167px;        
+}
+
+#header #header-inner #quick li ul li a.delete,
+#header #header-inner #quick li ul li a.delete:hover
+{
+    background: #FFFFFF url("../images/icons/delete.png") no-repeat 4px 9px;
+    margin:0;
+    padding:12px 9px 7px 24px;
+    width:167px;        
+}
+
+#header #header-inner #quick li ul li a.branches,
+#header #header-inner #quick li ul li a.branches:hover
+{
     background: #FFFFFF url("../images/icons/arrow_branch.png") no-repeat 4px 9px;
     margin:0;
     padding:12px 9px 7px 24px;
     width:167px;        
 }
 
-#header #header-inner #quick li ul li a.tags,#header #header-inner #quick li ul li a.tags:hover
-{
-
+#header #header-inner #quick li ul li a.tags,
+#header #header-inner #quick li ul li a.tags:hover
+{
     background: #FFFFFF url("../images/icons/tag_blue.png") no-repeat 4px 9px;
     margin:0;
     padding:12px 9px 7px 24px;
@@ -1132,7 +1161,7 @@
 #content div.box div.message
 {
 	margin: 0 0 0px 0;
-    padding: 0 0 10px 0;
+    padding: 10px 0 10px 0;
 	clear: both;
 	overflow: hidden;
 }
@@ -3029,7 +3058,9 @@
 }
 
 #changeset_content .container {
-	height: 120px;
+	min-height: 120px;
+	font-size: 1.2em;
+	
 }
 
 #changeset_content .container .left {
@@ -3123,7 +3154,7 @@
 	border-bottom: 1px solid #CCCCCC;
 	border-left: 1px solid #CCCCCC;
 	border-right: 1px solid #CCCCCC;
-	min-height: 90px;
+	min-height: 80px;
 	overflow: hidden;
 	font-size:1.2em;	
 }
@@ -3161,7 +3192,6 @@
 	border: 1px solid #DDDDDD;
 	display: block;
 	float: right;
-	font-size: 0.75em;
 	text-align: center;
 	min-width: 15px;
 }
@@ -3193,7 +3223,16 @@
 	font-family: monospace;
 }
 
-
+.right .logtags .branchtag{
+	background: #FFFFFF url("../images/icons/arrow_branch.png") no-repeat right 6px;
+    display:block;
+    padding:8px 16px 0px 0px
+}
+.right .logtags .tagtag{
+    background: #FFFFFF url("../images/icons/tag_blue.png") no-repeat right 6px;
+    display:block;
+    padding:6px 18px 0px 0px
+}
 
 /* -----------------------------------------------------------
 	FILE BROWSER
@@ -3464,12 +3503,14 @@
 .action_button {
     border: 0px;
     display: block;
+    color:#0066CC;
 }
 
 .action_button:hover {
     border: 0px;
-    font-style: italic;
+    text-decoration:underline;
     cursor: pointer;
+    color:#0066CC;
 }
 
 /* -----------------------------------------------------------
--- a/pylons_app/templates/admin/admin_log.html	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/templates/admin/admin_log.html	Sun Oct 03 02:27:44 2010 +0200
@@ -12,11 +12,17 @@
 	%for cnt,l in enumerate(c.users_log):
 	<tr class="parity${cnt%2}">
 		<td>${h.link_to(l.user.username,h.url('edit_user', id=l.user.user_id))}</td>
-		<td>${h.link_to(l.repository,h.url('summary_home',repo_name=l.repository))}</td>
+		<td>
+		%if l.repository:
+		  ${h.link_to(l.repository.repo_name,h.url('summary_home',repo_name=l.repository.repo_name))}
+		%else:
+		  ${l.repository_name}
+		%endif
+		</td>
 		<td>
 		% if l.action == 'push' and l.revision:
 		  ${h.link_to('%s - %s' % (l.action,l.revision),
-		  h.url('changeset_home',repo_name=l.repository,revision=l.revision))}
+		  h.url('changeset_home',repo_name=l.repository.repo_name,revision=l.revision))}
 		%else:
 		  ${l.action}
 		%endif
--- a/pylons_app/templates/admin/repos/repo_add.html	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/templates/admin/repos/repo_add.html	Sun Oct 03 02:27:44 2010 +0200
@@ -2,7 +2,7 @@
 <%inherit file="/base/base.html"/>
 
 <%def name="title()">
-    ${_('Repositories administration')}
+    ${_('Add new repository')}
 </%def>
 
 <%def name="breadcrumbs_links()">
--- a/pylons_app/templates/admin/repos/repos.html	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/templates/admin/repos/repos.html	Sun Oct 03 02:27:44 2010 +0200
@@ -27,9 +27,11 @@
     <div class="table">
         <table class="table_disp">
         <tr class="header">
-            <th class="left">${_('name')}</th>
-            <th class="left">${_('owner')}</th>
-            <th class="left">${_('last revision')}</th>
+	        <th class="left">${_('Name')}</th>
+	        <th class="left">${_('Description')}</th>
+	        <th class="left">${_('Last change')}</th>
+	        <th class="left">${_('Tip')}</th>
+	        <th class="left">${_('Contact')}</th>
             <th class="left">${_('action')}</th>
         </tr>
             %for cnt,repo in enumerate(c.repos_list):
@@ -40,9 +42,28 @@
                  %else:
                     <img alt="${_('public')}" src="/images/icons/lock_open.png"/>
                  %endif         
-                ${h.link_to(repo['name'],h.url('edit_repo',repo_name=repo['name']))}</td>
-                <td>${repo['contact']}</td>
-                <td>r${repo['rev']}:${repo['tip']}</td>
+                ${h.link_to(repo['name'],h.url('edit_repo',repo_name=repo['name']))}
+                
+	            %if repo['repo'].dbrepo.fork:
+	            	<a href="${h.url('summary_home',repo_name=repo['repo'].dbrepo.fork.repo_name)}">
+	            	<img class="icon" alt="${_('public')}"
+	            	title="${_('Fork of')} ${repo['repo'].dbrepo.fork.repo_name}" 
+	            	src="/images/icons/arrow_divide.png"/></a>
+	            %endif                
+                </td>
+				<td title="${repo['description']}">${h.truncate(repo['description'],60)}</td>
+	            <td>${h.age(repo['last_change'])}</td>
+	            <td>
+	            	%if repo['rev']>=0:
+	            	${h.link_to('r%s:%s' % (repo['rev'],repo['tip']),
+	                h.url('changeset_home',repo_name=repo['name'],revision=repo['tip']),
+	                class_="tooltip",
+	                tooltip_title=h.tooltip(repo['last_msg']))}
+	            	%else:
+	            		${_('No changesets yet')}
+	            	%endif    
+	            </td>
+	            <td title="${repo['contact']}">${h.person(repo['contact'])}</td>
                 <td>
                   ${h.form(url('repo', repo_name=repo['name']),method='delete')}
                     ${h.submit('remove_%s' % repo['name'],'delete',class_="delete_icon action_button",onclick="return confirm('Confirm to delete this repository');")}
--- a/pylons_app/templates/admin/users/user_edit_my_account.html	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/templates/admin/users/user_edit_my_account.html	Sun Oct 03 02:27:44 2010 +0200
@@ -88,21 +88,40 @@
     <div class="table">
 	    <table>
 	     <tbody>
-	     %for repo in c.user_repos:
-	        <tr>
-	            <td>
-	             %if repo.dbrepo.private:
-	                <img class="icon" alt="${_('private')}" src="/images/icons/lock.png"/>
-	             %else:
-	                <img class="icon" alt="${_('public')}" src="/images/icons/lock_open.png"/>
-	             %endif
-	                                             
-	            ${h.link_to(repo.name, h.url('summary_home',repo_name=repo.name))}</td> 
-	            <td>${_('revision')}: ${h.get_changeset_safe(repo,'tip').revision}</td>
-	            <td>${_('last changed')}: ${h.age(repo.last_change)}</td>
-	            <td><img class="icon" alt="${_('private')}" src="/images/icons/application_form_edit.png"/> ${h.link_to(_('edit'),h.url('edit_repo',repo_name=repo.name))}</td>
-	        </tr>
-	     %endfor
+	     %if c.user_repos:
+		     %for repo in c.user_repos:
+		        <tr>
+		            <td>
+		             %if repo.dbrepo.private:
+		                <img class="icon" alt="${_('private')}" src="/images/icons/lock.png"/>
+		             %else:
+		                <img class="icon" alt="${_('public')}" src="/images/icons/lock_open.png"/>
+		             %endif
+		                                             
+		            ${h.link_to(repo.name, h.url('summary_home',repo_name=repo.name))}
+		            %if repo.dbrepo.fork:
+		            	<a href="${h.url('summary_home',repo_name=repo.dbrepo.fork.repo_name)}">
+		            	<img class="icon" alt="${_('public')}"
+		            	title="${_('Fork of')} ${repo.dbrepo.fork.repo_name}" 
+		            	src="/images/icons/arrow_divide.png"/></a>
+		            %endif		            
+		            </td> 
+		            <td>${_('revision')}: ${h.get_changeset_safe(repo,'tip').revision}</td>
+		            <td>${_('last changed')}: ${h.age(repo.last_change)}</td>
+		            <td><img class="icon" alt="${_('private')}" src="/images/icons/application_form_edit.png"/> ${h.link_to(_('edit'),h.url('repo_settings_home',repo_name=repo.name))}</td>
+		            <td>
+	                  ${h.form(url('repo_settings_delete', repo_name=repo.name),method='delete')}
+	                    ${h.submit('remove_%s' % repo.name,'delete',class_="delete_icon action_button",onclick="return confirm('Confirm to delete this repository');")}
+	                  ${h.end_form()}	            
+		            </td>
+		        </tr>
+		     %endfor
+	     %else:
+	     	${_('No repositories yet')} 
+	     	%if h.HasPermissionAny('hg.admin','hg.create.repository')():
+	     		${h.link_to(_('create one now'),h.url('admin_settings_create_repository'))}
+	     	%endif
+	     %endif
 	     </tbody>
 	     </table>
     </div>
--- a/pylons_app/templates/base/base.html	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/templates/base/base.html	Sun Oct 03 02:27:44 2010 +0200
@@ -174,16 +174,30 @@
                    <span>${_('Files')}</span>                 
                    </a>             
                 </li>                            
-				%if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
-                <li ${is_current('settings')}>
-                   <a title="${_('Settings')}" href="${h.url('repo_settings_home',repo_name=c.repo_name)}">
+				
+                <li ${is_current('options')}>
+                   <a title="${_('Options')}" href="#">
                    <span class="icon">
-                       <img src="/images/icons/cog_edit.png" alt="${_('Settings')}" />
+                       <img src="/images/icons/table_gear.png" alt="${_('Admin')}" />
                    </span>
-                   <span>${_('Settings')}</span>                 
-                   </a>             
-                </li>				
-				%endif					        
+                   <span>${_('Options')}</span>                 
+                   </a>
+                   <ul>
+                   %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
+                   	<li>${h.link_to(_('settings'),h.url('repo_settings_home',repo_name=c.repo_name),class_='settings')}</li>
+                   %endif	
+                   	<li>${h.link_to(_('fork'),h.url('repo_fork_home',repo_name=c.repo_name),class_='fork')}</li>
+                   	<li>${h.link_to(_('search'),h.url('search_repo',search_repo=c.repo_name),class_='search')}</li>
+##                   %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
+##                   	<li class="last">
+##                   	${h.link_to(_('delete'),'#',class_='delete')}
+##	                  ${h.form(url('repo_settings_delete', repo_name=c.repo_name),method='delete')}
+##	                    ${h.submit('remove_%s' % c.repo_name,'delete',class_="delete_icon action_button",onclick="return confirm('Confirm to delete this repository');")}
+##	                  ${h.end_form()}
+##                   	</li>
+##                   	%endif
+                   </ul>             
+                </li>
 	        </ul>
 		%else:
 		    ##ROOT MENU
--- a/pylons_app/templates/changelog/changelog.html	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/templates/changelog/changelog.html	Sun Oct 03 02:27:44 2010 +0200
@@ -3,7 +3,7 @@
 <%inherit file="/base/base.html"/>
 
 <%def name="title()">
-    ${_('Changelog - %s') % c.repo_name}
+    ${_('Changelog')} - ${c.repo_name}
 </%def>
 
 <%def name="breadcrumbs_links()">
@@ -47,12 +47,6 @@
 					<div id="chg_${cnt+1}" class="container">
 						<div class="left">
 							<div class="date">${_('commit')} ${cs.revision}: ${cs.short_id}@${cs.date}</div>
-								<span class="logtags">
-									<span class="branchtag">${cs.branch}</span>
-									%for tag in cs.tags:
-										<span class="tagtag">${tag}</span>
-									%endfor
-								</span>					
 							<div class="author">
 								<div class="gravatar">
 									<img alt="gravatar" src="${h.gravatar_url(h.email(cs.author),20)}"/>
@@ -80,7 +74,15 @@
 										<div class="parent">${_('Parent')} ${p_cs.revision}: ${h.link_to(p_cs.short_id,
 											h.url('changeset_home',repo_name=c.repo_name,revision=p_cs.short_id),title=p_cs.message)}
 										</div>
-									%endfor								
+									%endfor
+								<span class="logtags">
+									<span class="branchtag" title="${'%s %s' % (_('branch'),cs.branch)}">
+									${h.link_to(cs.branch,h.url('files_home',repo_name=c.repo_name,revision=cs.short_id))}</span>
+									%for tag in cs.tags:
+										<span class="tagtag"  title="${'%s %s' % (_('tag'),tag)}">
+										${h.link_to(tag,h.url('files_home',repo_name=c.repo_name,revision=cs.short_id))}</span>
+									%endfor
+								</span>																	
 						</div>				
 					</div>
 					
--- a/pylons_app/templates/changeset/changeset.html	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/templates/changeset/changeset.html	Sun Oct 03 02:27:44 2010 +0200
@@ -1,7 +1,7 @@
 <%inherit file="/base/base.html"/>
 
 <%def name="title()">
-    ${_('Changeset')}
+    ${_('Changeset')} - r${c.changeset.revision}:${c.changeset.short_id} - ${c.repo_name}
 </%def>
 
 <%def name="breadcrumbs_links()">
@@ -34,40 +34,56 @@
 				</div>
 			</div>
 		</div>
-		    
-    <div id="changeset_content">
-		<div class="container">
-			<div class="left">
-				<div class="date">${_('Date')}: ${c.changeset.date}</div>
-				<div class="author">${_('Author')}: ${c.changeset.author}</div>
-				<div class="message">${h.wrap_paragraphs(c.changeset.message)}</div>
-			</div>	
-			<div class="right">
-				<span class="logtags">
-					<span class="branchtag">${c.changeset.branch}</span>
-					%for tag in c.changeset.tags:
-						<span class="tagtag">${tag}</span>
-					%endfor
-				</span>					
-				%if len(c.changeset.parents)>1:
-				<div class="merge">		
-				${_('merge')}
-				<img alt="merge" src="/images/icons/arrow_join.png">
-				</div>
-				%endif						
-				%for p_cs in reversed(c.changeset.parents):
-					<div class="parent">${_('Parent')} ${p_cs.revision}: ${h.link_to(p_cs.short_id,
-						h.url('changeset_home',repo_name=c.repo_name,revision=p_cs.short_id),title=p_cs.message)}
-					</div>
-				%endfor								
-			</div>		
-		</div>
-		<span style="font-size:1.1em;font-weight: bold">${_('Files affected')}</span>
-	    <div class="cs_files">
-				%for change,filenode,diff,cs1,cs2 in c.changes:
-					<div class="cs_${change}">${h.link_to(filenode.path,h.url.current(anchor='CHANGE-%s'%filenode.path))}</div>
-				%endfor
-		</div>		    
+	    <div id="changeset_content">
+			<div class="container">
+	             <div class="left">
+	                 <div class="date">${_('commit')} ${c.changeset.revision}: ${c.changeset.short_id}@${c.changeset.date}</div>
+	                 <div class="author">
+	                     <div class="gravatar">
+	                         <img alt="gravatar" src="${h.gravatar_url(h.email(c.changeset.author),20)}"/>
+	                     </div>
+	                     <span>${h.person(c.changeset.author)}</span><br/>
+	                     <span><a href="mailto:${h.email_or_none(c.changeset.author)}">${h.email_or_none(c.changeset.author)}</a></span><br/>
+	                 </div>
+	                 <div class="message">
+	                     ${h.link_to(h.wrap_paragraphs(c.changeset.message),
+	                     h.url('changeset_home',repo_name=c.repo_name,revision=c.changeset.short_id))}
+	                 </div>
+	             </div>
+	             <div class="right">
+		             <div class="changes">
+		                 <span class="removed" title="${_('removed')}">${len(c.changeset.removed)}</span>
+		                 <span class="changed" title="${_('changed')}">${len(c.changeset.changed)}</span>
+		                 <span class="added" title="${_('added')}">${len(c.changeset.added)}</span>
+		             </div>                  
+		                 %if len(c.changeset.parents)>1:
+		                 <div class="merge">
+		                     ${_('merge')}<img alt="merge" src="/images/icons/arrow_join.png"/>
+		                 </div>
+		                 %endif                      
+		             %for p_cs in reversed(c.changeset.parents):
+		                 <div class="parent">${_('Parent')} ${p_cs.revision}: ${h.link_to(p_cs.short_id,
+		                     h.url('changeset_home',repo_name=c.repo_name,revision=p_cs.short_id),title=p_cs.message)}
+		                 </div>
+		             %endfor
+		         <span class="logtags">
+		             <span class="branchtag" title="${'%s %s' % (_('branch'),c.changeset.branch)}">
+		             ${h.link_to(c.changeset.branch,h.url('files_home',repo_name=c.repo_name,revision=c.changeset.short_id))}</span>
+		             %for tag in c.changeset.tags:
+		                 <span class="tagtag"  title="${'%s %s' % (_('tag'),tag)}">
+		                 ${h.link_to(tag,h.url('files_home',repo_name=c.repo_name,revision=c.changeset.short_id))}</span>
+		             %endfor
+		         </span>                                                                 
+	                </div>              
+	        </div>
+	        <span style="font-size:1.1em;font-weight: bold">${_('Files affected')}</span>
+	        <div class="cs_files">
+	                %for change,filenode,diff,cs1,cs2 in c.changes:
+	                    <div class="cs_${change}">${h.link_to(filenode.path,h.url.current(anchor='CHANGE-%s'%filenode.path))}</div>
+	                %endfor
+	        </div>         
+	    </div>
+	    
     </div>
     	
 	%for change,filenode,diff,cs1,cs2 in c.changes:
--- a/pylons_app/templates/index.html	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/templates/index.html	Sun Oct 03 02:27:44 2010 +0200
@@ -31,7 +31,7 @@
 	        %if h.HasPermissionAny('hg.admin','hg.create.repository')():
 	        <ul class="links">
 	          <li>
-	            <span>${h.link_to(u'ADD NEW REPOSITORY',h.url('admin_settings_create_repository'),class_="add_icon")}</span>
+	            <span>${h.link_to(_('ADD NEW REPOSITORY'),h.url('admin_settings_create_repository'),class_="add_icon")}</span>
 	          </li>          
 	        </ul>  	        
 	        %endif
@@ -61,7 +61,14 @@
 					                <img class="icon" alt="${_('public')}" src="/images/icons/lock_open.png"/>
 					             %endif  
 					            ${h.link_to(repo['name'],
-					                h.url('summary_home',repo_name=repo['name']))}</td>
+					                h.url('summary_home',repo_name=repo['name']))}
+					            %if repo['repo'].dbrepo.fork:
+					            	<a href="${h.url('summary_home',repo_name=repo['repo'].dbrepo.fork.repo_name)}">
+					            	<img class="icon" alt="${_('public')}"
+					            	title="${_('Fork of')} ${repo['repo'].dbrepo.fork.repo_name}" 
+					            	src="/images/icons/arrow_divide.png"/></a>
+					            %endif
+					            </td>
 					            <td title="${repo['description']}">${h.truncate(repo['description'],60)}</td>
 					            <td>${h.age(repo['last_change'])}</td>
 					            <td>
--- a/pylons_app/templates/search/search.html	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/templates/search/search.html	Sun Oct 03 02:27:44 2010 +0200
@@ -1,7 +1,13 @@
 ## -*- coding: utf-8 -*-
 <%inherit file="/base/base.html"/>
 <%def name="title()">
-   ${_('Search')}: ${c.cur_query}
+   ${_('Search')} 
+	%if c.repo_name:
+		${_('in repository: ') + c.repo_name}
+	%else:
+		${_('in all repositories')}		
+	%endif
+	:${c.cur_query}
 </%def>
 <%def name="breadcrumbs()">
 	${c.hg_app_name}
@@ -14,16 +20,26 @@
 <div class="box">
 	<!-- box / title -->
 	<div class="title">
-		<h5>${_('Search')}</h5>
+		<h5>${_('Search')}
+		%if c.repo_name:
+			${_('in repository: ') + c.repo_name}
+		%else:
+			${_('in all repositories')}
+		%endif		
+		</h5>
 	</div>
 	<!-- end box / title -->
-	${h.form('search',method='get')}
+	%if c.repo_name:
+		${h.form(h.url('search_repo',search_repo=c.repo_name),method='get')}	
+	%else:
+		${h.form(h.url('search'),method='get')}
+	%endif
 	<div class="form">
 		<div class="fields">
 		
 			<div class="field ">
 				<div class="label">
-					<label for="q">${_('Search:')}</label>
+					<label for="q">${_('Search')}:</label>
 				</div>
 				<div class="input">
 					${h.text('q',c.cur_query,class_="small")}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pylons_app/templates/settings/repo_fork.html	Sun Oct 03 02:27:44 2010 +0200
@@ -0,0 +1,58 @@
+## -*- coding: utf-8 -*-
+<%inherit file="/base/base.html"/>
+
+<%def name="title()">
+    ${_('Fork repository')} ${c.repo_info.repo_name}
+</%def>
+
+<%def name="breadcrumbs_links()">
+    ${h.link_to(c.repo_info.repo_name,h.url('summary_home',repo_name=c.repo_info.repo_name))} 
+    &raquo;
+    ${_('fork')}
+</%def>
+
+<%def name="page_nav()">
+	${self.menu('')}
+</%def>
+<%def name="main()">
+<div class="box">
+    <!-- box / title -->
+    <div class="title">
+        ${self.breadcrumbs()}      
+    </div>
+    ${h.form(url('repo_fork_create_home',repo_name=c.repo_info.repo_name))}
+    <div class="form">
+        <!-- fields -->
+        <div class="fields">
+            <div class="field">
+	            <div class="label">
+	                <label for="repo_name">${_('Fork name')}:</label>
+	            </div>
+	            <div class="input">
+	                ${h.text('fork_name')}
+	            </div>
+             </div>
+            <div class="field">
+                <div class="label label-textarea">
+                    <label for="description">${_('Description')}:</label>
+                </div>
+                <div class="textarea text-area editor">
+                    ${h.textarea('description',cols=23,rows=5)}
+                </div>
+             </div>
+            <div class="field">
+                <div class="label label-checkbox">
+                    <label for="private">${_('Private')}:</label>
+                </div>
+                <div class="checkboxes">
+                    ${h.checkbox('private',value="True")}
+                </div>
+             </div>
+	        <div class="buttons">
+	          ${h.submit('','fork this repository',class_="ui-button ui-widget ui-state-default ui-corner-all")}
+	        </div>                                                          
+        </div>
+    </div>    
+    ${h.end_form()}    
+</div>
+</%def>   
--- a/pylons_app/templates/settings/repo_settings.html	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/templates/settings/repo_settings.html	Sun Oct 03 02:27:44 2010 +0200
@@ -2,7 +2,7 @@
 <%inherit file="/base/base.html"/>
 
 <%def name="title()">
-    ${_('Repositories administration')}
+    ${_('Repository settings')}
 </%def>
 
 <%def name="breadcrumbs_links()">
--- a/pylons_app/templates/summary/summary.html	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/templates/summary/summary.html	Sun Oct 03 02:27:44 2010 +0200
@@ -42,7 +42,23 @@
 			      <label>${_('Name')}:</label>
 			  </div>
 			  <div class="input-short">
-			      <span style="font-size: 1.6em;font-weight: bold">${c.repo_info.name}</span>
+	             %if c.repo_info.dbrepo.private:
+	                <img style="margin-bottom:2px" class="icon" title="${_('private repository')}" alt="${_('private')}" src="/images/icons/lock.png"/>
+	             %else:
+	                <img style="margin-bottom:2px" class="icon" title="${_('public repository')}" alt="${_('public')}" src="/images/icons/lock_open.png"/>
+	             %endif
+			      <span style="font-size: 1.6em;font-weight: bold;vertical-align: baseline;">${c.repo_info.name}</span>
+			      <br/>
+		            %if c.repo_info.dbrepo.fork:
+		            	<span style="margin-top:5px">
+		            	<a href="${h.url('summary_home',repo_name=c.repo_info.dbrepo.fork.repo_name)}">
+		            	<img class="icon" alt="${_('public')}"
+		            	title="${_('Fork of')} ${c.repo_info.dbrepo.fork.repo_name}" 
+		            	src="/images/icons/arrow_divide.png"/>
+		            	${_('Fork of')} ${c.repo_info.dbrepo.fork.repo_name}
+		            	</a>
+		            	</span>
+		            %endif			      
 			  </div>
 			 </div>
 			
@@ -76,7 +92,7 @@
 			      <label>${_('Last change')}:</label>
 			  </div>
 			  <div class="input-short">
-			      ${h.age(c.repo_info.last_change)} - ${h.rfc822date(c.repo_info.last_change)} 
+			      ${h.age(c.repo_info.last_change)} - ${h.rfc822date_notz(c.repo_info.last_change)} 
 			      ${_('by')} ${h.get_changeset_safe(c.repo_info,'tip').author} 
 			      
 			  </div>
@@ -122,7 +138,7 @@
 				  		var td2 = document.createElement('td');
 			  		    var trending_language = document.createElement('div');
 			  		    trending_language.title = k;
-			  		    trending_language.innerHTML = "<b>"+value+" ${_('files')} - "+percentage+" %</b>";
+			  		    trending_language.innerHTML = "<b>"+percentage+"% "+value+" ${_('files')}</b>";
 			  		    trending_language.setAttribute("class", 'trending_language');
 			  		    trending_language.style.width=percentage+"%";
 						td2.appendChild(trending_language);
@@ -130,7 +146,6 @@
 						tr.appendChild(td1);
 						tr.appendChild(td2);
 			  		    tbl.appendChild(tr);
- 				  		//YAHOO.util.Dom.get('lang_stats').appendChild(trending_language_label);
 			  		    
 			  		}
 			  		if(no_data){
--- a/pylons_app/tests/__init__.py	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/tests/__init__.py	Sun Oct 03 02:27:44 2010 +0200
@@ -28,6 +28,8 @@
 # Invoke websetup with the current config file
 #SetupCommand('setup-app').run([config_file])
 
+##RUNNING DESIRED TESTS
+#nosetests pylons_app.tests.functional.test_admin_settings:TestSettingsController.test_my_account
 
 environ = {}
 
@@ -42,10 +44,15 @@
 
         TestCase.__init__(self, *args, **kwargs)
     
-    def log_user(self, username='test_admin', password='test'):
+    def log_user(self, username='test_admin', password='test12'):
         response = self.app.post(url(controller='login', action='index'),
                                  {'username':username,
                                   'password':password})
+        print response
+        
+        if 'invalid user name' in response.body:
+            assert False, 'could not login using %s %s' % (username, password)
+        
         assert response.status == '302 Found', 'Wrong response code from login got %s' % response.status
-        assert response.session['hg_app_user'].username == 'test_admin', 'wrong logged in user'
+        assert response.session['hg_app_user'].username == username, 'wrong logged in user got %s expected %s' % (response.session['hg_app_user'].username, username)
         return response.follow()
--- a/pylons_app/tests/functional/test_admin_settings.py	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/tests/functional/test_admin_settings.py	Sun Oct 03 02:27:44 2010 +0200
@@ -57,7 +57,7 @@
         response = self.app.post(url('admin_settings_my_account_update'), params=dict(
                                                             _method='put',
                                                             username='test_admin',
-                                                            new_password='test',
+                                                            new_password='test12',
                                                             password='',
                                                             name='NewName',
                                                             lastname='NewLastname',
@@ -78,7 +78,7 @@
         response = self.app.post(url('admin_settings_my_account_update'), params=dict(
                                                             _method='put',
                                                             username='test_admin',
-                                                            new_password='test',
+                                                            new_password='test12',
                                                             name='NewName',
                                                             lastname='NewLastname',
                                                             email=new_email,))
@@ -91,7 +91,7 @@
         response = self.app.post(url('admin_settings_my_account_update'), params=dict(
                                                             _method='put',
                                                             username='test_admin',
-                                                            new_password='test',
+                                                            new_password='test12',
                                                             name='NewName',
                                                             lastname='NewLastname',
                                                             email=new_email,))
@@ -101,13 +101,13 @@
         
         
     def test_my_account_update_err(self):
-        self.log_user()
+        self.log_user('test_regular2', 'test12')
                 
         new_email = 'newmail.pl'
         response = self.app.post(url('admin_settings_my_account_update'), params=dict(
                                                             _method='put',
-                                                            username='test_regular2',
-                                                            new_password='test',
+                                                            username='test_admin',
+                                                            new_password='test12',
                                                             name='NewName',
                                                             lastname='NewLastname',
                                                             email=new_email,))
--- a/pylons_app/tests/functional/test_login.py	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/tests/functional/test_login.py	Sun Oct 03 02:27:44 2010 +0200
@@ -13,7 +13,7 @@
     def test_login_admin_ok(self):
         response = self.app.post(url(controller='login', action='index'),
                                  {'username':'test_admin',
-                                  'password':'test'})
+                                  'password':'test12'})
         assert response.status == '302 Found', 'Wrong response code from login got %s' % response.status
         assert response.session['hg_app_user'].username == 'test_admin', 'wrong logged in user'
         response = response.follow()
@@ -22,7 +22,8 @@
     def test_login_regular_ok(self):
         response = self.app.post(url(controller='login', action='index'),
                                  {'username':'test_regular',
-                                  'password':'test'})
+                                  'password':'test12'})
+        print response
         assert response.status == '302 Found', 'Wrong response code from login got %s' % response.status
         assert response.session['hg_app_user'].username == 'test_regular', 'wrong logged in user'
         response = response.follow()
@@ -33,7 +34,7 @@
         test_came_from = '/_admin/users'
         response = self.app.post(url(controller='login', action='index', came_from=test_came_from),
                                  {'username':'test_admin',
-                                  'password':'test'})
+                                  'password':'test12'})
         assert response.status == '302 Found', 'Wrong response code from came from redirection'
         response = response.follow()
         
@@ -41,15 +42,23 @@
         assert 'Users administration' in response.body, 'No proper title in response'
         
                 
-    def test_login_wrong(self):
+    def test_login_short_password(self):
         response = self.app.post(url(controller='login', action='index'),
                                  {'username':'error',
                                   'password':'test'})
         assert response.status == '200 OK', 'Wrong response from login page'
         
+        assert 'Enter a value 6 characters long or more' in response.body, 'No error password message in response'
+
+    def test_login_wrong_username_password(self):
+        response = self.app.post(url(controller='login', action='index'),
+                                 {'username':'error',
+                                  'password':'test12'})
+        assert response.status == '200 OK', 'Wrong response from login page'
+        
         assert 'invalid user name' in response.body, 'No error username message in response'
         assert 'invalid password' in response.body, 'No error password message in response'
-        
+                
         
     def test_register(self):
         response = self.app.get(url(controller='login', action='register'))
@@ -76,8 +85,7 @@
         
         assert response.status == '200 OK', 'Wrong response from register page got %s' % response.status
         assert 'An email address must contain a single @' in response.body
-        assert 'Enter a value 3 characters long or more' in response.body
-        assert 'Please enter a value<' in response.body
+        assert 'Please enter a value' in response.body
         
         
         
--- a/pylons_app/tests/functional/test_settings.py	Tue Sep 28 20:38:35 2010 +0000
+++ b/pylons_app/tests/functional/test_settings.py	Sun Oct 03 02:27:44 2010 +0200
@@ -1,3 +1,4 @@
+from pylons_app.model.db import Repository
 from pylons_app.tests import *
 
 class TestSettingsController(TestController):
@@ -7,3 +8,48 @@
         response = self.app.get(url(controller='settings', action='index',
                                     repo_name='vcs_test'))
         # Test response...
+    
+    def test_fork(self):
+        self.log_user()
+        response = self.app.get(url(controller='settings', action='fork',
+                                    repo_name='vcs_test'))
+        
+
+    def test_fork_create(self):
+        self.log_user()
+        fork_name = 'vcs_test_fork'
+        description = 'fork of vcs test'
+        repo_name = 'vcs_test'
+        response = self.app.post(url(controller='settings', action='fork_create',
+                                    repo_name=repo_name),
+                                    {'fork_name':fork_name,
+                                     'description':description,
+                                     'private':'False'})
+        
+        
+        print response
+        
+        #test if we have a message that fork is ok
+        assert 'fork %s repository as %s task added' \
+                      % (repo_name, fork_name) in response.session['flash'][0], 'No flash message about fork'
+                      
+        #test if the fork was created in the database
+        fork_repo = self.sa.query(Repository).filter(Repository.repo_name == fork_name).one()
+        
+        assert fork_repo.repo_name == fork_name, 'wrong name of repo name in new db fork repo'
+        assert fork_repo.fork.repo_name == repo_name, 'wron fork parrent'
+        
+        
+        #test if fork is visible in the list ?
+        response = response.follow()
+
+
+        #check if fork is marked as fork
+        response = self.app.get(url(controller='summary', action='index',
+                                    repo_name=fork_name))
+        
+        
+        print response
+        
+        assert 'Fork of %s' % repo_name in response.body, 'no message about that this repo is a fork'
+        
--- a/setup.py	Tue Sep 28 20:38:35 2010 +0000
+++ b/setup.py	Sun Oct 03 02:27:44 2010 +0200
@@ -20,11 +20,11 @@
         "SQLAlchemy>=0.6",
         "babel",
         "Mako>=0.3.2",
-        "vcs>=0.1.6",
+        "vcs>=0.1.7",
         "pygments>=1.3.0",
         "mercurial>=1.6",
         "pysqlite",
-        "whoosh==1.0.0b17",
+        "whoosh==1.0.0b20",
         "py-bcrypt",
         "celery",
     ],