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('hg |
|
|
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 = |
|
|
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))), |
|
|
23 | cfg.read(os.path.join(dn(dn(dn(cur_dir))), repo_conf)) | |
|
24 | 24 | except IOError: |
|
25 |
logging.error('Could not read |
|
|
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= |
|
|
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