##// END OF EJS Templates
Made repos path config configurable from pylons app configs. update Readme
marcink -
r241:48727add default
parent child Browse files
Show More
@@ -1,18 +1,24 b''
1 1 Pylons based replacement for hgwebdir. Fully customizable,
2 2 with authentication, permissions. Based on vcs library.
3 3 - has it's own middleware to handle mercurial protocol request each request can
4 4 be logged and authenticated +threaded performance unlikely to hgweb
5 5 - mako templates let's you cusmotize look and feel of appplication.
6 6 - diffs annotations and source code all colored by pygments.
7 7 - admin interface for performing user/permission managments as well as repository
8 8 managment
9 9 - added cache with invalidation on push/repo managment for high performance and
10 10 always upto date data.
11 11 - rss /atom feed customizable
12 12 - future support for git
13 13 - based on pylons 1.0 / sqlalchemy 0.6
14 14
15 15 ===
16 16 This software is still in beta mode. I don't guarantee that it'll work.
17 17 I started this project since i was tired of sad looks, and zero controll over
18 18 our company regular hgwebdir.
19
20
21 == INSTALATION
22 run dbmanage.py from pylons_app/lib it should create all needed table and
23 an admin account, Edit file repositories.config and change the path for you
24 mercurial repositories, remember about permissions. No newline at end of file
@@ -1,135 +1,136 b''
1 1 ################################################################################
2 2 ################################################################################
3 3 # pylons_app - Pylons environment configuration #
4 4 # #
5 5 # The %(here)s variable will be replaced with the parent directory of this file#
6 6 ################################################################################
7 7
8 8 [DEFAULT]
9 9 debug = true
10 10 ############################################
11 11 ## Uncomment and replace with the address ##
12 12 ## which should receive any error reports ##
13 13 ############################################
14 14 #email_to = marcin.kuzminski@etelko.pl
15 15 #smtp_server = mail.etelko.pl
16 16 #error_email_from = paste_error@localhost
17 17 #smtp_username =
18 18 #smtp_password =
19 19 #error_message = 'mercurial crash !'
20 20
21 21 [server:main]
22 22 ##nr of threads to spawn
23 23 threadpool_workers = 5
24 24
25 25 ##max request before
26 26 threadpool_max_requests = 2
27 27
28 28 ##option to use threads of process
29 29 use_threadpool = true
30 30
31 31 use = egg:Paste#http
32 32 host = 127.0.0.1
33 33 port = 5000
34 34
35 35 [app:main]
36 36 use = egg:pylons_app
37 37 full_stack = true
38 38 static_files = false
39 39 lang=en
40 40 cache_dir = %(here)s/data
41 41 ##a name for our application
42 42 hg_app_name = Python-works
43 hg_app_repo_conf = repositories.config
43 44
44 45 ####################################
45 46 ### BEAKER CACHE ####
46 47 ####################################
47 48 beaker.cache.data_dir=/tmp/cache/data
48 49 beaker.cache.lock_dir=/tmp/cache/lock
49 50 beaker.cache.regions=short_term,long_term
50 51 beaker.cache.long_term.type=memory
51 52 beaker.cache.long_term.expire=36000
52 53 beaker.cache.short_term.type=memory
53 54 beaker.cache.short_term.expire=60
54 55
55 56 ################################################################################
56 57 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
57 58 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
58 59 ## execute malicious code after an exception is raised. ##
59 60 ################################################################################
60 61 #set debug = false
61 62
62 63 ##################################
63 64 ### LOGVIEW CONFIG ###
64 65 ##################################
65 66 logview.sqlalchemy = #faa
66 67 logview.pylons.templating = #bfb
67 68 logview.pylons.util = #eee
68 69
69 70 #########################################################
70 71 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
71 72 #########################################################
72 73 sqlalchemy.db1.url = sqlite:///%(here)s/hg_app.db
73 74 #sqlalchemy.db1.echo = False
74 75 #sqlalchemy.db1.pool_recycle = 3600
75 76 sqlalchemy.convert_unicode = true
76 77
77 78 ################################
78 79 ### LOGGING CONFIGURATION ####
79 80 ################################
80 81 [loggers]
81 82 keys = root, routes, pylons_app, sqlalchemy
82 83
83 84 [handlers]
84 85 keys = console
85 86
86 87 [formatters]
87 88 keys = generic,color_formatter
88 89
89 90 #############
90 91 ## LOGGERS ##
91 92 #############
92 93 [logger_root]
93 94 level = NOTSET
94 95 handlers = console
95 96
96 97 [logger_routes]
97 98 level = DEBUG
98 99 handlers = console
99 100 qualname = routes.middleware
100 101 # "level = DEBUG" logs the route matched and routing variables.
101 102
102 103 [logger_pylons_app]
103 104 level = DEBUG
104 105 handlers = console
105 106 qualname = pylons_app
106 107 propagate = 0
107 108
108 109 [logger_sqlalchemy]
109 110 level = ERROR
110 111 handlers = console
111 112 qualname = sqlalchemy.engine
112 113 propagate = 0
113 114
114 115 ##############
115 116 ## HANDLERS ##
116 117 ##############
117 118
118 119 [handler_console]
119 120 class = StreamHandler
120 121 args = (sys.stderr,)
121 122 level = NOTSET
122 123 formatter = color_formatter
123 124
124 125 ################
125 126 ## FORMATTERS ##
126 127 ################
127 128
128 129 [formatter_generic]
129 130 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
130 131 datefmt = %Y-%m-%d %H:%M:%S
131 132
132 133 [formatter_color_formatter]
133 134 class=pylons_app.lib.colored_formatter.ColorFormatter
134 135 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
135 136 datefmt = %Y-%m-%d %H:%M:%S No newline at end of file
@@ -1,135 +1,136 b''
1 1 ################################################################################
2 2 ################################################################################
3 3 # pylons_app - Pylons environment configuration #
4 4 # #
5 5 # The %(here)s variable will be replaced with the parent directory of this file#
6 6 ################################################################################
7 7
8 8 [DEFAULT]
9 9 debug = true
10 10 ############################################
11 11 ## Uncomment and replace with the address ##
12 12 ## which should receive any error reports ##
13 13 ############################################
14 14 #email_to = marcin.kuzminski@etelko.pl
15 15 #smtp_server = mail.etelko.pl
16 16 #error_email_from = paste_error@localhost
17 17 #smtp_username =
18 18 #smtp_password =
19 19 #error_message = 'mercurial crash !'
20 20
21 21 [server:main]
22 22 ##nr of threads to spawn
23 23 threadpool_workers = 5
24 24
25 25 ##max request before
26 26 threadpool_max_requests = 2
27 27
28 28 ##option to use threads of process
29 29 use_threadpool = true
30 30
31 31 use = egg:Paste#http
32 32 host = 127.0.0.1
33 33 port = 8001
34 34
35 35 [app:main]
36 36 use = egg:pylons_app
37 37 full_stack = true
38 38 static_files = false
39 39 lang=en
40 40 cache_dir = %(here)s/data
41 41 ##a name for our application
42 42 hg_app_name = Python-works
43 hg_app_repo_conf = repositories.config
43 44
44 45 ####################################
45 46 ### BEAKER CACHE ####
46 47 ####################################
47 48 beaker.cache.data_dir=/tmp/cache/data
48 49 beaker.cache.lock_dir=/tmp/cache/lock
49 50 beaker.cache.regions=short_term,long_term
50 51 beaker.cache.long_term.type=memory
51 52 beaker.cache.long_term.expire=36000
52 53 beaker.cache.short_term.type=memory
53 54 beaker.cache.short_term.expire=60
54 55
55 56 ################################################################################
56 57 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
57 58 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
58 59 ## execute malicious code after an exception is raised. ##
59 60 ################################################################################
60 61 set debug = false
61 62
62 63 ##################################
63 64 ### LOGVIEW CONFIG ###
64 65 ##################################
65 66 logview.sqlalchemy = #faa
66 67 logview.pylons.templating = #bfb
67 68 logview.pylons.util = #eee
68 69
69 70 #########################################################
70 71 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
71 72 #########################################################
72 73 sqlalchemy.db1.url = sqlite:///%(here)s/hg_app.db
73 74 #sqlalchemy.db1.echo = False
74 75 #sqlalchemy.db1.pool_recycle = 3600
75 76 sqlalchemy.convert_unicode = true
76 77
77 78 ################################
78 79 ### LOGGING CONFIGURATION ####
79 80 ################################
80 81 [loggers]
81 82 keys = root, routes, pylons_app, sqlalchemy
82 83
83 84 [handlers]
84 85 keys = console
85 86
86 87 [formatters]
87 88 keys = generic,color_formatter
88 89
89 90 #############
90 91 ## LOGGERS ##
91 92 #############
92 93 [logger_root]
93 94 level = INFO
94 95 handlers = console
95 96
96 97 [logger_routes]
97 98 level = INFO
98 99 handlers = console
99 100 qualname = routes.middleware
100 101 # "level = DEBUG" logs the route matched and routing variables.
101 102
102 103 [logger_pylons_app]
103 104 level = DEBUG
104 105 handlers = console
105 106 qualname = pylons_app
106 107 propagate = 0
107 108
108 109 [logger_sqlalchemy]
109 110 level = ERROR
110 111 handlers = console
111 112 qualname = sqlalchemy.engine
112 113 propagate = 0
113 114
114 115 ##############
115 116 ## HANDLERS ##
116 117 ##############
117 118
118 119 [handler_console]
119 120 class = StreamHandler
120 121 args = (sys.stderr,)
121 122 level = NOTSET
122 123 formatter = color_formatter
123 124
124 125 ################
125 126 ## FORMATTERS ##
126 127 ################
127 128
128 129 [formatter_generic]
129 130 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
130 131 datefmt = %Y-%m-%d %H:%M:%S
131 132
132 133 [formatter_color_formatter]
133 134 class=pylons_app.lib.colored_formatter.ColorFormatter
134 135 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
135 136 datefmt = %Y-%m-%d %H:%M:%S No newline at end of file
@@ -1,25 +1,25 b''
1 1 """The application's Globals object"""
2 2
3 3 from beaker.cache import CacheManager
4 4 from beaker.util import parse_cache_config_options
5 5 from pylons_app.lib.utils import make_ui
6 6
7 7 class Globals(object):
8 8
9 9 """Globals acts as a container for objects available throughout the
10 10 life of the application
11 11
12 12 """
13 13
14 14 def __init__(self, config):
15 15 """One instance of Globals is created during application
16 16 initialization and is available during requests via the
17 17 'app_globals' variable
18 18
19 19 """
20 20 self.cache = CacheManager(**parse_cache_config_options(config))
21 self.baseui = make_ui('hgwebdir.config')
21 self.baseui = make_ui(config['hg_app_repo_conf'])
22 22 self.paths = self.baseui.configitems('paths')
23 23 self.base_path = self.paths[0][1].replace('*', '')
24 24 self.changeset_annotation_colors = {}
25 25 self.available_permissions = None # propagated after init_model
@@ -1,88 +1,88 b''
1 1 '''BACKUP MANAGER'''
2 2 import logging
3 3 from mercurial import config
4 4 import tarfile
5 5 import os
6 6 import datetime
7 7 import sys
8 8 import subprocess
9 9 logging.basicConfig(level=logging.DEBUG,
10 10 format="%(asctime)s %(levelname)-5.5s %(message)s")
11 11
12 12 class BackupManager(object):
13 def __init__(self):
13 def __init__(self, id_rsa_path, repo_conf):
14 14 self.repos_path = None
15 15 self.backup_file_name = None
16 self.id_rsa_path = '/home/pylons/id_rsa'
16 self.id_rsa_path = id_rsa_path
17 17 self.check_id_rsa()
18 18 cur_dir = os.path.realpath(__file__)
19 19 dn = os.path.dirname
20 20 self.backup_file_path = os.path.join(dn(dn(dn(cur_dir))), 'data')
21 21 cfg = config.config()
22 22 try:
23 cfg.read(os.path.join(dn(dn(dn(cur_dir))), 'hgwebdir.config'))
23 cfg.read(os.path.join(dn(dn(dn(cur_dir))), repo_conf))
24 24 except IOError:
25 logging.error('Could not read hgwebdir.config')
25 logging.error('Could not read %s', repo_conf)
26 26 sys.exit()
27 27 self.set_repos_path(cfg.items('paths'))
28 28 logging.info('starting backup for %s', self.repos_path)
29 29 logging.info('backup target %s', self.backup_file_path)
30 30
31 31 if not os.path.isdir(self.repos_path):
32 32 raise Exception('Not a valid directory in %s' % self.repos_path)
33 33
34 34 def check_id_rsa(self):
35 35 if not os.path.isfile(self.id_rsa_path):
36 36 logging.error('Could not load id_rsa key file in %s',
37 37 self.id_rsa_path)
38 38 sys.exit()
39 39
40 40 def set_repos_path(self, paths):
41 41 repos_path = paths[0][1].split('/')
42 42 if repos_path[-1] in ['*', '**']:
43 43 repos_path = repos_path[:-1]
44 44 if repos_path[0] != '/':
45 45 repos_path[0] = '/'
46 46 self.repos_path = os.path.join(*repos_path)
47 47
48 48 def backup_repos(self):
49 49 today = datetime.datetime.now().weekday() + 1
50 50 self.backup_file_name = "mercurial_repos.%s.tar.gz" % today
51 51 bckp_file = os.path.join(self.backup_file_path, self.backup_file_name)
52 52 tar = tarfile.open(bckp_file, "w:gz")
53 53
54 54 for dir_name in os.listdir(self.repos_path):
55 55 logging.info('backing up %s', dir_name)
56 56 tar.add(os.path.join(self.repos_path, dir_name), dir_name)
57 57 tar.close()
58 58 logging.info('finished backup of mercurial repositories')
59 59
60 60
61 61
62 62 def transfer_files(self):
63 63 params = {
64 64 'id_rsa_key': self.id_rsa_path,
65 65 'backup_file_path':self.backup_file_path,
66 66 'backup_file_name':self.backup_file_name,
67 67 }
68 68 cmd = ['scp', '-l', '40000', '-i', '%(id_rsa_key)s' % params,
69 69 '%(backup_file_path)s/%(backup_file_name)s' % params,
70 70 'root@192.168.2.102:/backups/mercurial' % params]
71 71
72 72 subprocess.call(cmd)
73 73 logging.info('Transfered file %s to %s', self.backup_file_name, cmd[4])
74 74
75 75
76 76 def rm_file(self):
77 77 logging.info('Removing file %s', self.backup_file_name)
78 78 os.remove(os.path.join(self.backup_file_path, self.backup_file_name))
79 79
80 80
81 81
82 82 if __name__ == "__main__":
83 B_MANAGER = BackupManager()
83 B_MANAGER = BackupManager('/home/pylons/id_rsa', 'repositories.config')
84 84 B_MANAGER.backup_repos()
85 85 B_MANAGER.transfer_files()
86 86 B_MANAGER.rm_file()
87 87
88 88
@@ -1,136 +1,136 b''
1 1 #!/usr/bin/env python
2 2 # encoding: utf-8
3 3 #
4 4 # Copyright (c) 2010 marcink. All rights reserved.
5 5 #
6 6 """
7 7 Created on 2010-04-28
8 8
9 9 @author: marcink
10 10 SimpleHG middleware for handling mercurial protocol request (push/clone etc.)
11 11 It's implemented with basic auth function
12 12 """
13 13 from datetime import datetime
14 14 from mercurial.hgweb import hgweb
15 15 from mercurial.hgweb.request import wsgiapplication
16 16 from paste.auth.basic import AuthBasicAuthenticator
17 17 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
18 18 from pylons_app.lib.auth import authfunc
19 19 from pylons_app.lib.utils import is_mercurial, make_ui, invalidate_cache
20 20 from pylons_app.model import meta
21 21 from pylons_app.model.db import UserLog, User
22 22 from webob.exc import HTTPNotFound
23 23 import logging
24 24 import os
25 25 log = logging.getLogger(__name__)
26 26
27 27 class SimpleHg(object):
28 28
29 29 def __init__(self, application, config):
30 30 self.application = application
31 31 self.config = config
32 32 #authenticate this mercurial request using
33 33 realm = '%s %s' % (config['hg_app_name'], 'mercurial repository')
34 34 self.authenticate = AuthBasicAuthenticator(realm, authfunc)
35 35
36 36 def __call__(self, environ, start_response):
37 37 if not is_mercurial(environ):
38 38 return self.application(environ, start_response)
39 39 else:
40 40 #===================================================================
41 41 # AUTHENTICATE THIS MERCURIAL REQUEST
42 42 #===================================================================
43 43 username = REMOTE_USER(environ)
44 44 if not username:
45 45 result = self.authenticate(environ)
46 46 if isinstance(result, str):
47 47 AUTH_TYPE.update(environ, 'basic')
48 48 REMOTE_USER.update(environ, result)
49 49 else:
50 50 return result.wsgi_application(environ, start_response)
51 51
52 52 try:
53 53 repo_name = environ['PATH_INFO'].split('/')[1]
54 54 except:
55 55 return HTTPNotFound()(environ, start_response)
56 56
57 57 #since we wrap into hgweb, just reset the path
58 58 environ['PATH_INFO'] = '/'
59 self.baseui = make_ui()
59 self.baseui = make_ui(self.config['hg_app_repo_conf'])
60 60 self.basepath = self.baseui.configitems('paths')[0][1]\
61 61 .replace('*', '')
62 62 self.repo_path = os.path.join(self.basepath, repo_name)
63 63 try:
64 64 app = wsgiapplication(self.__make_app)
65 65 except Exception as e:
66 66 return HTTPNotFound()(environ, start_response)
67 67 action = self.__get_action(environ)
68 68 #invalidate cache on push
69 69 if action == 'push':
70 70 self.__invalidate_cache(repo_name)
71 71
72 72 if action:
73 73 username = self.__get_environ_user(environ)
74 74 self.__log_user_action(username, action, repo_name)
75 75
76 76 return app(environ, start_response)
77 77
78 78 def __make_app(self):
79 79 hgserve = hgweb(self.repo_path)
80 80 return self.__load_web_settings(hgserve)
81 81
82 82 def __get_environ_user(self, environ):
83 83 return environ.get('REMOTE_USER')
84 84
85 85 def __get_action(self, environ):
86 86 """
87 87 Maps mercurial request commands into a pull or push command.
88 88 @param environ:
89 89 """
90 90 mapping = {
91 91 'changegroup': 'pull',
92 92 'changegroupsubset': 'pull',
93 93 'unbundle': 'push',
94 94 'stream_out': 'pull',
95 95 }
96 96 for qry in environ['QUERY_STRING'].split('&'):
97 97 if qry.startswith('cmd'):
98 98 cmd = qry.split('=')[-1]
99 99 if mapping.has_key(cmd):
100 100 return mapping[cmd]
101 101
102 102 def __log_user_action(self, username, action, repo):
103 103 sa = meta.Session
104 104 try:
105 105 user = sa.query(User).filter(User.username == username).one()
106 106 user_log = UserLog()
107 107 user_log.user_id = user.user_id
108 108 user_log.action = action
109 109 user_log.repository = repo.replace('/', '')
110 110 user_log.action_date = datetime.now()
111 111 sa.add(user_log)
112 112 sa.commit()
113 113 log.info('Adding user %s, action %s on %s',
114 114 username, action, repo)
115 115 except Exception as e:
116 116 sa.rollback()
117 117 log.error('could not log user action:%s', str(e))
118 118
119 119 def __invalidate_cache(self, repo_name):
120 120 """we know that some change was made to repositories and we should
121 121 invalidate the cache to see the changes right away but only for
122 122 push requests"""
123 123 invalidate_cache('cached_repo_list')
124 124 invalidate_cache('full_changelog', repo_name)
125 125
126 126
127 127 def __load_web_settings(self, hgserve):
128 128 repoui = make_ui(os.path.join(self.repo_path, '.hg', 'hgrc'), False)
129 129 #set the global ui for hgserve
130 130 hgserve.repo.ui = self.baseui
131 131
132 132 if repoui:
133 133 #set the repository based config
134 134 hgserve.repo.ui = repoui
135 135
136 136 return hgserve
@@ -1,127 +1,130 b''
1 1 import os
2 2 import logging
3 3 from mercurial import ui, config, hg
4 4 from mercurial.error import RepoError
5 5 log = logging.getLogger(__name__)
6 6
7 7
8 8 def get_repo_slug(request):
9 9 path_info = request.environ.get('PATH_INFO')
10 10 uri_lst = path_info.split('/')
11 11 repo_name = uri_lst[1]
12 12 return repo_name
13 13
14 14 def is_mercurial(environ):
15 15 """
16 16 Returns True if request's target is mercurial server - header
17 17 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
18 18 """
19 19 http_accept = environ.get('HTTP_ACCEPT')
20 20 if http_accept and http_accept.startswith('application/mercurial'):
21 21 return True
22 22 return False
23 23
24 24 def check_repo_dir(paths):
25 25 repos_path = paths[0][1].split('/')
26 26 if repos_path[-1] in ['*', '**']:
27 27 repos_path = repos_path[:-1]
28 28 if repos_path[0] != '/':
29 29 repos_path[0] = '/'
30 30 if not os.path.isdir(os.path.join(*repos_path)):
31 31 raise Exception('Not a valid repository in %s' % paths[0][1])
32 32
33 33 def check_repo(repo_name, base_path):
34 34
35 35 repo_path = os.path.join(base_path, repo_name)
36 36
37 37 try:
38 38 r = hg.repository(ui.ui(), repo_path)
39 39 hg.verify(r)
40 40 #here we hnow that repo exists it was verified
41 41 log.info('%s repo is already created', repo_name)
42 42 return False
43 43 #raise Exception('Repo exists')
44 44 except RepoError:
45 45 log.info('%s repo is free for creation', repo_name)
46 46 #it means that there is no valid repo there...
47 47 return True
48 48
49 def make_ui(path='hgwebdir.config', checkpaths=True):
49 def make_ui(path=None, checkpaths=True):
50 50 """
51 51 A funcion that will read python rc files and make an ui from read options
52 52
53 53 @param path: path to mercurial config file
54 54 """
55 if not path:
56 log.error('repos config path is empty !')
57
55 58 if not os.path.isfile(path):
56 59 log.warning('Unable to read config file %s' % path)
57 60 return False
58 61 #propagated from mercurial documentation
59 62 sections = [
60 63 'alias',
61 64 'auth',
62 65 'decode/encode',
63 66 'defaults',
64 67 'diff',
65 68 'email',
66 69 'extensions',
67 70 'format',
68 71 'merge-patterns',
69 72 'merge-tools',
70 73 'hooks',
71 74 'http_proxy',
72 75 'smtp',
73 76 'patch',
74 77 'paths',
75 78 'profiling',
76 79 'server',
77 80 'trusted',
78 81 'ui',
79 82 'web',
80 83 ]
81 84
82 85 baseui = ui.ui()
83 86 cfg = config.config()
84 87 cfg.read(path)
85 88 if checkpaths:check_repo_dir(cfg.items('paths'))
86 89
87 90 for section in sections:
88 91 for k, v in cfg.items(section):
89 92 baseui.setconfig(section, k, v)
90 93
91 94 return baseui
92 95
93 96 def invalidate_cache(name, *args):
94 97 """Invalidates given name cache"""
95 98
96 99 from beaker.cache import region_invalidate
97 100 log.info('INVALIDATING CACHE FOR %s', name)
98 101
99 102 """propagate our arguments to make sure invalidation works. First
100 103 argument has to be the name of cached func name give to cache decorator
101 104 without that the invalidation would not work"""
102 105 tmp = [name]
103 106 tmp.extend(args)
104 107 args = tuple(tmp)
105 108
106 109 if name == 'cached_repo_list':
107 110 from pylons_app.lib.base import _get_repos_cached
108 111 region_invalidate(_get_repos_cached, None, *args)
109 112
110 113 if name == 'full_changelog':
111 114 from pylons_app.lib.base import _full_changelog_cached
112 115 region_invalidate(_full_changelog_cached, None, *args)
113 116
114 117 from vcs.backends.base import BaseChangeset
115 118 from vcs.utils.lazy import LazyProperty
116 119 class EmptyChangeset(BaseChangeset):
117 120
118 121 revision = -1
119 122
120 123 @LazyProperty
121 124 def raw_id(self):
122 125 """
123 126 Returns raw string identifing this changeset, useful for web
124 127 representation.
125 128 """
126 129 return '0' * 12
127 130
1 NO CONTENT: file renamed from hgwebdir.config to repositories.config
General Comments 0
You need to be logged in to leave comments. Login now